summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/Http/Controllers/StatisticsChartController.php961
-rw-r--r--app/Module/StatisticsChartModule.php950
-rw-r--r--resources/views/modules/statistics-chart/chart-page.phtml50
-rw-r--r--resources/views/modules/statistics-chart/custom.phtml (renamed from resources/views/statistics-chart-custom.phtml)50
-rw-r--r--resources/views/modules/statistics-chart/families.phtml (renamed from resources/views/statistics-chart-families.phtml)0
-rw-r--r--resources/views/modules/statistics-chart/individuals.phtml (renamed from resources/views/statistics-chart-individuals.phtml)0
-rw-r--r--resources/views/modules/statistics-chart/other.phtml (renamed from resources/views/statistics-chart-other.phtml)0
-rw-r--r--resources/views/statistics-page.phtml64
-rw-r--r--routes/web.php6
9 files changed, 1022 insertions, 1059 deletions
diff --git a/app/Http/Controllers/StatisticsChartController.php b/app/Http/Controllers/StatisticsChartController.php
deleted file mode 100644
index d0db45ba33..0000000000
--- a/app/Http/Controllers/StatisticsChartController.php
+++ /dev/null
@@ -1,961 +0,0 @@
-<?php
-/**
- * webtrees: online genealogy
- * Copyright (C) 2019 webtrees development team
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- */
-declare(strict_types=1);
-
-namespace Fisharebest\Webtrees\Http\Controllers;
-
-use Fisharebest\Webtrees\Auth;
-use Fisharebest\Webtrees\Date;
-use Fisharebest\Webtrees\Html;
-use Fisharebest\Webtrees\I18N;
-use Fisharebest\Webtrees\Module\StatisticsChartModule;
-use Fisharebest\Webtrees\Stats;
-use Fisharebest\Webtrees\Tree;
-use Symfony\Component\HttpFoundation\Request;
-use Symfony\Component\HttpFoundation\Response;
-use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
-
-/**
- * A chart showing the various statistics about the family tree.
- *
- * CAUTION - The google image charts (bitmap) API is deprecated.
- * We need to migrate to the charts API (which uses SVG).
- */
-class StatisticsChartController extends AbstractBaseController
-{
- // We generate a bitmap chart with these dimensions in image pixels.
- // These set the aspect ratio. The actual image is sized using CSS
- // The maximum size (width x height) is 300,000
- private const CHART_WIDTH = 950;
- private const CHART_HEIGHT = 315;
-
- public const X_AXIS_INDIVIDUAL_MAP = 1;
- public const X_AXIS_BIRTH_MAP = 2;
- public const X_AXIS_DEATH_MAP = 3;
- public const X_AXIS_MARRIAGE_MAP = 4;
- public const X_AXIS_BIRTH_MONTH = 11;
- public const X_AXIS_DEATH_MONTH = 12;
- public const X_AXIS_MARRIAGE_MONTH = 13;
- public const X_AXIS_FIRST_CHILD_MONTH = 14;
- public const X_AXIS_FIRST_MARRIAGE_MONTH = 15;
- public const X_AXIS_AGE_AT_DEATH = 18;
- public const X_AXIS_AGE_AT_MARRIAGE = 19;
- public const X_AXIS_AGE_AT_FIRST_MARRIAGE = 20;
- public const X_AXIS_NUMBER_OF_CHILDREN = 21;
-
- public const Y_AXIS_NUMBERS = 201;
- public const Y_AXIS_PERCENT = 202;
-
- public const Z_AXIS_ALL = 300;
- public const Z_AXIS_SEX = 301;
- public const Z_AXIS_TIME = 302;
-
- // First two colors are blue/pink, to work with Z_AXIS_SEX.
- private const Z_AXIS_COLORS = ['0000FF', 'FFA0CB', '9F00FF', 'FF7000', '905030', 'FF0000', '00FF00', 'F0F000'];
-
- private const DAYS_IN_YEAR = 365.25;
-
- /**
- * A form to request the chart parameters.
- *
- * @param Tree $tree
- *
- * @return Response
- */
- public function page(Tree $tree): Response
- {
- $this->checkModuleIsActive($tree, StatisticsChartModule::class);
-
- $title = I18N::translate('Statistics');
-
- return $this->viewResponse('statistics-page', [
- 'title' => $title,
- ]);
- }
-
- /**
- * @param Tree $tree
- *
- * @return Response
- */
- public function chartIndividuals(Tree $tree): Response
- {
- $this->checkModuleIsActive($tree, StatisticsChartModule::class);
-
- $html = view('statistics-chart-individuals', [
- 'show_oldest_living' => Auth::check(),
- 'stats' => new Stats($tree),
- ]);
-
- return new Response($html);
- }
-
- /**
- * @param Tree $tree
- *
- * @return Response
- */
- public function chartFamilies(Tree $tree): Response
- {
- $this->checkModuleIsActive($tree, StatisticsChartModule::class);
-
- $html = view('statistics-chart-families', [
- 'stats' => new Stats($tree),
- ]);
-
- return new Response($html);
- }
-
- /**
- * @param Tree $tree
- *
- * @return Response
- */
- public function chartOther(Tree $tree): Response
- {
- $this->checkModuleIsActive($tree, StatisticsChartModule::class);
-
- $html = view('statistics-chart-other', [
- 'stats' => new Stats($tree),
- ]);
-
- return new Response($html);
- }
-
- /**
- * @param Tree $tree
- *
- * @return Response
- */
- public function chartCustomOptions(Tree $tree): Response
- {
- $this->checkModuleIsActive($tree, StatisticsChartModule::class);
-
- $html = view('statistics-chart-custom');
-
- return new Response($html);
- }
-
- /**
- * @param Request $request
- * @param Tree $tree
- *
- * @return Response
- */
- public function chartCustomChart(Request $request, Tree $tree): Response
- {
- $this->checkModuleIsActive($tree, StatisticsChartModule::class);
-
- $x_axis_type = (int) $request->get('x-as');
- $y_axis_type = (int) $request->get('y-as');
- $z_axis_type = (int) $request->get('z-as');
- $ydata = [];
- $stats = new Stats($tree);
-
- switch ($x_axis_type) {
- case self::X_AXIS_INDIVIDUAL_MAP:
- return new Response($stats->chartDistribution(
- $request->get('chart_shows', ''),
- $request->get('chart_type', ''),
- $request->get('SURN', '')
- ));
-
- case self::X_AXIS_BIRTH_MAP:
- return new Response($stats->chartDistribution(
- $request->get('chart_shows', ''),
- 'birth_distribution_chart'
- ));
-
- case self::X_AXIS_DEATH_MAP:
- return new Response($stats->chartDistribution(
- $request->get('chart_shows', ''),
- 'death_distribution_chart'
- ));
-
- case self::X_AXIS_MARRIAGE_MAP:
- return new Response($stats->chartDistribution(
- $request->get('chart_shows', ''),
- 'marriage_distribution_chart'
- ));
-
- case self::X_AXIS_BIRTH_MONTH:
- $chart_title = I18N::translate('Month of birth');
- $x_axis_title = I18N::translate('Month');
- $x_axis = $this->axisMonths();
-
- switch ($y_axis_type) {
- case self::Y_AXIS_NUMBERS:
- $y_axis_title = I18N::translate('Individuals');
- break;
- case self::Y_AXIS_PERCENT:
- $y_axis_title = '%';
- break;
- default:
- throw new NotFoundHttpException();
- }
-
- switch ($z_axis_type) {
- case self::Z_AXIS_ALL:
- $z_axis = $this->axisAll();
- $rows = $stats->statsBirthQuery(false, false);
- foreach ($rows as $row) {
- $this->fillYData($row->d_month, 0, $row->total, $x_axis, $z_axis, $ydata);
- }
- break;
- case self::Z_AXIS_SEX:
- $z_axis = $this->axisSexes();
- $rows = $stats->statsBirthQuery(false, true);
- foreach ($rows as $row) {
- $this->fillYData($row->d_month, $row->i_sex, $row->total, $x_axis, $z_axis, $ydata);
- }
- break;
- case self::Z_AXIS_TIME:
- $boundaries_csv = $request->get('z-axis-boundaries-periods', '');
- $z_axis = $this->axisYears($boundaries_csv);
- $prev_boundary = 0;
- foreach (array_keys($z_axis) as $boundary) {
- $rows = $stats->statsBirthQuery(false, false, $prev_boundary, $boundary);
- foreach ($rows as $row) {
- $this->fillYData($row->d_month, $boundary, $row->total, $x_axis, $z_axis, $ydata);
- }
- $prev_boundary = $boundary + 1;
- }
- break;
- default:
- throw new NotFoundHttpException();
- }
-
- return new Response($this->myPlot($chart_title, $x_axis, $x_axis_title, $ydata, $y_axis_title, $z_axis, $y_axis_type));
-
- case self::X_AXIS_DEATH_MONTH:
- $chart_title = I18N::translate('Month of death');
- $x_axis_title = I18N::translate('Month');
- $x_axis = $this->axisMonths();
-
- switch ($y_axis_type) {
- case self::Y_AXIS_NUMBERS:
- $y_axis_title = I18N::translate('Individuals');
- break;
- case self::Y_AXIS_PERCENT:
- $y_axis_title = '%';
- break;
- default:
- throw new NotFoundHttpException();
- }
-
- switch ($z_axis_type) {
- case self::Z_AXIS_ALL:
- $z_axis = $this->axisAll();
- $rows = $stats->statsDeathQuery(false, false);
- foreach ($rows as $row) {
- $this->fillYData($row->d_month, 0, $row->total, $x_axis, $z_axis, $ydata);
- }
- break;
- case self::Z_AXIS_SEX:
- $z_axis = $this->axisSexes();
- $rows = $stats->statsDeathQuery(false, true);
- foreach ($rows as $row) {
- $this->fillYData($row->d_month, $row->i_sex, $row->total, $x_axis, $z_axis, $ydata);
- }
- break;
- case self::Z_AXIS_TIME:
- $boundaries_csv = $request->get('z-axis-boundaries-periods', '');
- $z_axis = $this->axisYears($boundaries_csv);
- $prev_boundary = 0;
- foreach (array_keys($z_axis) as $boundary) {
- $rows = $stats->statsDeathQuery(false, false, $prev_boundary, $boundary);
- foreach ($rows as $row) {
- $this->fillYData($row->d_month, $boundary, $row->total, $x_axis, $z_axis, $ydata);
- }
- $prev_boundary = $boundary + 1;
- }
- break;
- default:
- throw new NotFoundHttpException();
- }
-
- return new Response($this->myPlot($chart_title, $x_axis, $x_axis_title, $ydata, $y_axis_title, $z_axis, $y_axis_type));
-
- case self::X_AXIS_MARRIAGE_MONTH:
- $chart_title = I18N::translate('Month of marriage');
- $x_axis_title = I18N::translate('Month');
- $x_axis = $this->axisMonths();
-
- switch ($y_axis_type) {
- case self::Y_AXIS_NUMBERS:
- $y_axis_title = I18N::translate('Families');
- break;
- case self::Y_AXIS_PERCENT:
- $y_axis_title = '%';
- break;
- default:
- throw new NotFoundHttpException();
- }
-
- switch ($z_axis_type) {
- case self::Z_AXIS_ALL:
- $z_axis = $this->axisAll();
- $rows = $stats->statsMarrQuery(false, false);
- foreach ($rows as $row) {
- $this->fillYData($row->d_month, 0, $row->total, $x_axis, $z_axis, $ydata);
- }
- break;
- case self::Z_AXIS_TIME:
- $boundaries_csv = $request->get('z-axis-boundaries-periods', '');
- $z_axis = $this->axisYears($boundaries_csv);
- $prev_boundary = 0;
- foreach (array_keys($z_axis) as $boundary) {
- $rows = $stats->statsMarrQuery(false, false, $prev_boundary, $boundary);
- foreach ($rows as $row) {
- $this->fillYData($row->d_month, $boundary, $row->total, $x_axis, $z_axis, $ydata);
- }
- $prev_boundary = $boundary + 1;
- }
- break;
- default:
- throw new NotFoundHttpException();
- }
-
- return new Response($this->myPlot($chart_title, $x_axis, $x_axis_title, $ydata, $y_axis_title, $z_axis, $y_axis_type));
-
- case self::X_AXIS_FIRST_CHILD_MONTH:
- $chart_title = I18N::translate('Month of birth of first child in a relation');
- $x_axis_title = I18N::translate('Month');
- $x_axis = $this->axisMonths();
-
- switch ($y_axis_type) {
- case self::Y_AXIS_NUMBERS:
- $y_axis_title = I18N::translate('Children');
- break;
- case self::Y_AXIS_PERCENT:
- $y_axis_title = '%';
- break;
- default:
- throw new NotFoundHttpException();
- }
-
- switch ($z_axis_type) {
- case self::Z_AXIS_ALL:
- $z_axis = $this->axisAll();
- $rows = $stats->monthFirstChildQuery(false);
- foreach ($rows as $row) {
- $this->fillYData($row->d_month, 0, $row->total, $x_axis, $z_axis, $ydata);
- }
- break;
- case self::Z_AXIS_SEX:
- $z_axis = $this->axisSexes();
- $rows = $stats->monthFirstChildQuery(true);
- foreach ($rows as $row) {
- $this->fillYData($row->d_month, $row->i_sex, $row->total, $x_axis, $z_axis, $ydata);
- }
- break;
- case self::Z_AXIS_TIME:
- $boundaries_csv = $request->get('z-axis-boundaries-periods', '');
- $z_axis = $this->axisYears($boundaries_csv);
- $prev_boundary = 0;
- foreach (array_keys($z_axis) as $boundary) {
- $rows = $stats->statsMarrQuery(false, false, $prev_boundary, $boundary);
- foreach ($rows as $row) {
- $this->fillYData($row->d_month, $boundary, $row->total, $x_axis, $z_axis, $ydata);
- }
- $prev_boundary = $boundary + 1;
- }
- break;
- default:
- throw new NotFoundHttpException();
- }
-
- return new Response($this->myPlot($chart_title, $x_axis, $x_axis_title, $ydata, $y_axis_title, $z_axis, $y_axis_type));
-
- case self::X_AXIS_FIRST_MARRIAGE_MONTH:
- $chart_title = I18N::translate('Month of first marriage');
- $x_axis_title = I18N::translate('Month');
- $x_axis = $this->axisMonths();
-
- switch ($y_axis_type) {
- case self::Y_AXIS_NUMBERS:
- $y_axis_title = I18N::translate('Families');
- break;
- case self::Y_AXIS_PERCENT:
- $y_axis_title = '%';
- break;
- default:
- throw new NotFoundHttpException();
- }
-
- switch ($z_axis_type) {
- case self::Z_AXIS_ALL:
- $z_axis = $this->axisAll();
- $rows = $stats->statsMarrQuery(false, true);
- $indi = [];
- $fam = [];
- foreach ($rows as $row) {
- if (!in_array($row->indi, $indi) && !in_array($row->fams, $fam)) {
- $this->fillYData($row->month, 0, 1, $x_axis, $z_axis, $ydata);
- }
- $indi[] = $row->indi;
- $fam[] = $row->fams;
- }
- break;
- case self::Z_AXIS_TIME:
- $boundaries_csv = $request->get('z-axis-boundaries-periods', '');
- $z_axis = $this->axisYears($boundaries_csv);
- $prev_boundary = 0;
- $indi = [];
- $fam = [];
- foreach (array_keys($z_axis) as $boundary) {
- $rows = $stats->statsMarrQuery(false, true, $prev_boundary, $boundary);
- foreach ($rows as $row) {
- if (!in_array($row->indi, $indi) && !in_array($row->fams, $fam)) {
- $this->fillYData($row->month, $boundary, 1, $x_axis, $z_axis, $ydata);
- }
- $indi[] = $row->indi;
- $fam[] = $row->fams;
- }
- $prev_boundary = $boundary + 1;
- }
- break;
- default:
- throw new NotFoundHttpException();
- }
-
- return new Response($this->myPlot($chart_title, $x_axis, $x_axis_title, $ydata, $y_axis_title, $z_axis, $y_axis_type));
-
- case self::X_AXIS_AGE_AT_DEATH:
- $chart_title = I18N::translate('Average age at death');
- $x_axis_title = I18N::translate('age');
- $boundaries_csv = $request->get('x-axis-boundaries-ages', '');
- $x_axis = $this->axisNumbers($boundaries_csv);
-
- switch ($y_axis_type) {
- case self::Y_AXIS_NUMBERS:
- $y_axis_title = I18N::translate('Individuals');
- break;
- case self::Y_AXIS_PERCENT:
- $y_axis_title = '%';
- break;
- default:
- throw new NotFoundHttpException();
- }
-
- switch ($z_axis_type) {
- case self::Z_AXIS_ALL:
- $z_axis = $this->axisAll();
- $rows = $stats->statsAgeQuery(false, 'DEAT');
- foreach ($rows as $row) {
- foreach ($row as $age) {
- $years = (int) ($age / self::DAYS_IN_YEAR);
- $this->fillYData($years, 0, 1, $x_axis, $z_axis, $ydata);
- }
- }
- break;
- case self::Z_AXIS_SEX:
- $z_axis = $this->axisSexes();
- foreach (array_keys($z_axis) as $sex) {
- $rows = $stats->statsAgeQuery(false, 'DEAT', $sex);
- foreach ($rows as $row) {
- foreach ($row as $age) {
- $years = (int) ($age / self::DAYS_IN_YEAR);
- $this->fillYData($years, $sex, 1, $x_axis, $z_axis, $ydata);
- }
- }
- }
- break;
- case self::Z_AXIS_TIME:
- $boundaries_csv = $request->get('z-axis-boundaries-periods', '');
- $z_axis = $this->axisYears($boundaries_csv);
- $prev_boundary = 0;
- foreach (array_keys($z_axis) as $boundary) {
- $rows = $stats->statsAgeQuery(false, 'DEAT', 'BOTH', $prev_boundary, $boundary);
- foreach ($rows as $row) {
- foreach ($row as $age) {
- $years = (int) ($age / self::DAYS_IN_YEAR);
- $this->fillYData($years, $boundary, 1, $x_axis, $z_axis, $ydata);
- }
- }
- $prev_boundary = $boundary + 1;
- }
-
- break;
- default:
- throw new NotFoundHttpException();
- }
-
- return new Response($this->myPlot($chart_title, $x_axis, $x_axis_title, $ydata, $y_axis_title, $z_axis, $y_axis_type));
-
- case self::X_AXIS_AGE_AT_MARRIAGE:
- $chart_title = I18N::translate('Age in year of marriage');
- $x_axis_title = I18N::translate('age');
- $boundaries_csv = $request->get('x-axis-boundaries-ages_m', '');
- $x_axis = $this->axisNumbers($boundaries_csv);
-
- switch ($y_axis_type) {
- case self::Y_AXIS_NUMBERS:
- $y_axis_title = I18N::translate('Individuals');
- break;
- case self::Y_AXIS_PERCENT:
- $y_axis_title = '%';
- break;
- default:
- throw new NotFoundHttpException();
- }
-
- switch ($z_axis_type) {
- case self::Z_AXIS_ALL:
- $z_axis = $this->axisAll();
- // The stats query doesn't have an "all" function, so query M/F/U separately
- foreach (['M', 'F', 'U'] as $sex) {
- $rows = $stats->statsMarrAgeQuery(false, $sex);
- foreach ($rows as $row) {
- $years = (int) ($row->age / self::DAYS_IN_YEAR);
- $this->fillYData($years, 0, 1, $x_axis, $z_axis, $ydata);
- }
- }
- break;
- case self::Z_AXIS_SEX:
- $z_axis = $this->axisSexes();
- foreach (array_keys($z_axis) as $sex) {
- $rows = $stats->statsMarrAgeQuery(false, $sex);
- foreach ($rows as $row) {
- $years = (int) ($row->age / self::DAYS_IN_YEAR);
- $this->fillYData($years, $sex, 1, $x_axis, $z_axis, $ydata);
- }
- }
- break;
- case self::Z_AXIS_TIME:
- $boundaries_csv = $request->get('z-axis-boundaries-periods', '');
- $z_axis = $this->axisYears($boundaries_csv);
- // The stats query doesn't have an "all" function, so query M/F/U separately
- foreach (['M', 'F', 'U'] as $sex) {
- $prev_boundary = 0;
- foreach (array_keys($z_axis) as $boundary) {
- $rows = $stats->statsMarrAgeQuery(false, $sex, $prev_boundary, $boundary);
- foreach ($rows as $row) {
- $years = (int) ($row->age / self::DAYS_IN_YEAR);
- $this->fillYData($years, $boundary, 1, $x_axis, $z_axis, $ydata);
- }
- $prev_boundary = $boundary + 1;
- }
- }
- break;
- default:
- throw new NotFoundHttpException();
- }
-
- return new Response($this->myPlot($chart_title, $x_axis, $x_axis_title, $ydata, $y_axis_title, $z_axis, $y_axis_type));
-
- case self::X_AXIS_AGE_AT_FIRST_MARRIAGE:
- $chart_title = I18N::translate('Age in year of first marriage');
- $x_axis_title = I18N::translate('age');
- $boundaries_csv = $request->get('x-axis-boundaries-ages_m', '');
- $x_axis = $this->axisNumbers($boundaries_csv);
-
- switch ($y_axis_type) {
- case self::Y_AXIS_NUMBERS:
- $y_axis_title = I18N::translate('Individuals');
- break;
- case self::Y_AXIS_PERCENT:
- $y_axis_title = '%';
- break;
- default:
- throw new NotFoundHttpException();
- }
-
- switch ($z_axis_type) {
- case self::Z_AXIS_ALL:
- $z_axis = $this->axisAll();
- // The stats query doesn't have an "all" function, so query M/F/U separately
- foreach (['M', 'F', 'U'] as $sex) {
- $rows = $stats->statsMarrAgeQuery(false, $sex);
- $indi = [];
- foreach ($rows as $row) {
- if (!in_array($row->d_gid, $indi)) {
- $years = (int) ($row->age / self::DAYS_IN_YEAR);
- $this->fillYData($years, 0, 1, $x_axis, $z_axis, $ydata);
- $indi[] = $row->d_gid;
- }
- }
- }
- break;
- case self::Z_AXIS_SEX:
- $z_axis = $this->axisSexes();
- foreach (array_keys($z_axis) as $sex) {
- $rows = $stats->statsMarrAgeQuery(false, $sex);
- $indi = [];
- foreach ($rows as $row) {
- if (!in_array($row->d_gid, $indi)) {
- $years = (int) ($row->age / self::DAYS_IN_YEAR);
- $this->fillYData($years, $sex, 1, $x_axis, $z_axis, $ydata);
- $indi[] = $row->d_gid;
- }
- }
- }
- break;
- case self::Z_AXIS_TIME:
- $boundaries_csv = $request->get('z-axis-boundaries-periods', '');
- $z_axis = $this->axisYears($boundaries_csv);
- // The stats query doesn't have an "all" function, so query M/F/U separately
- foreach (['M', 'F', 'U'] as $sex) {
- $prev_boundary = 0;
- $indi = [];
- foreach (array_keys($z_axis) as $boundary) {
- $rows = $stats->statsMarrAgeQuery(false, $sex, $prev_boundary, $boundary);
- foreach ($rows as $row) {
- if (!in_array($row->d_gid, $indi)) {
- $years = (int) ($row->age / self::DAYS_IN_YEAR);
- $this->fillYData($years, $boundary, 1, $x_axis, $z_axis, $ydata);
- $indi[] = $row->d_gid;
- }
- }
- $prev_boundary = $boundary + 1;
- }
- }
- break;
- default:
- throw new NotFoundHttpException();
- }
-
- return new Response($this->myPlot($chart_title, $x_axis, $x_axis_title, $ydata, $y_axis_title, $z_axis, $y_axis_type));
-
- case self::X_AXIS_NUMBER_OF_CHILDREN:
- $chart_title = I18N::translate('Number of children');
- $x_axis_title = I18N::translate('Children');
- $x_axis = $this->axisNumbers('1,2,3,4,5,6,7,8,9,10');
-
- switch ($y_axis_type) {
- case self::Y_AXIS_NUMBERS:
- $y_axis_title = I18N::translate('Families');
- break;
- case self::Y_AXIS_PERCENT:
- $y_axis_title = '%';
- break;
- default:
- throw new NotFoundHttpException();
- }
-
- switch ($z_axis_type) {
- case self::Z_AXIS_ALL:
- $z_axis = $this->axisAll();
- $rows = $stats->statsChildrenQuery(false);
- foreach ($rows as $row) {
- $this->fillYData($row->f_numchil, 0, $row->total, $x_axis, $z_axis, $ydata);
- }
- break;
- case self::Z_AXIS_TIME:
- $boundaries_csv = $request->get('z-axis-boundaries-periods', '');
- $z_axis = $this->axisYears($boundaries_csv);
- $prev_boundary = 0;
- foreach (array_keys($z_axis) as $boundary) {
- $rows = $stats->statsChildrenQuery(false, 'BOTH', $prev_boundary, $boundary);
- foreach ($rows as $row) {
- $this->fillYData($row->f_numchil, $boundary, $row->total, $x_axis, $z_axis, $ydata);
- }
- $prev_boundary = $boundary + 1;
- }
- break;
- default:
- throw new NotFoundHttpException();
- }
-
- return new Response($this->myPlot($chart_title, $x_axis, $x_axis_title, $ydata, $y_axis_title, $z_axis, $y_axis_type));
-
- default:
- throw new NotFoundHttpException();
- break;
- }
- }
-
- /**
- * @return string[]
- */
- private function axisAll(): array
- {
- return [
- I18N::translate('Total'),
- ];
- }
-
- /**
- * @return string[]
- */
- private function axisSexes(): array
- {
- return [
- 'M' => I18N::translate('Male'),
- 'F' => I18N::translate('Female'),
- ];
- }
-
- /**
- * Labels for the X axis
- *
- * @return string[]
- */
- private function axisMonths(): array
- {
- return [
- 'JAN' => I18N::translateContext('NOMINATIVE', 'January'),
- 'FEB' => I18N::translateContext('NOMINATIVE', 'February'),
- 'MAR' => I18N::translateContext('NOMINATIVE', 'March'),
- 'APR' => I18N::translateContext('NOMINATIVE', 'April'),
- 'MAY' => I18N::translateContext('NOMINATIVE', 'May'),
- 'JUN' => I18N::translateContext('NOMINATIVE', 'June'),
- 'JUL' => I18N::translateContext('NOMINATIVE', 'July'),
- 'AUG' => I18N::translateContext('NOMINATIVE', 'August'),
- 'SEP' => I18N::translateContext('NOMINATIVE', 'September'),
- 'OCT' => I18N::translateContext('NOMINATIVE', 'October'),
- 'NOV' => I18N::translateContext('NOMINATIVE', 'November'),
- 'DEC' => I18N::translateContext('NOMINATIVE', 'December'),
- ];
- }
-
- /**
- * Convert a list of N year-boundaries into N+1 year-ranges for the z-axis.
- *
- * @param string $boundaries_csv
- *
- * @return string[]
- */
- private function axisYears(string $boundaries_csv): array
- {
- $boundaries = explode(',', $boundaries_csv);
-
- $axis = [];
- foreach ($boundaries as $n => $boundary) {
- if ($n === 0) {
- $date = new Date('BEF ' . $boundary);
- } else {
- $date = new Date('BET ' . $boundaries[$n - 1] . ' AND ' . ($boundary - 1));
- }
- $axis[$boundary - 1] = strip_tags($date->display());
- }
-
- $date = new Date('AFT ' . $boundaries[count($boundaries) - 1]);
- $axis[PHP_INT_MAX] = strip_tags($date->display());
-
- return $axis;
- }
-
- /**
- * Create the X axis.
- *
- * @param string $boundaries_csv
- *
- * @return array
- */
- private function axisNumbers(string $boundaries_csv): array
- {
- $boundaries = explode(',', $boundaries_csv);
-
- $boundaries = array_map(function (string $x): int {
- return (int) $x;
- }, $boundaries);
-
- $axis = [];
- foreach ($boundaries as $n => $boundary) {
- if ($n === 0) {
- /* I18N: A range of numbers */
- $axis[$boundary - 1] = I18N::translate('%1$s–%2$s', I18N::number(0), I18N::number($boundary));
- } else {
- /* I18N: A range of numbers */
- $axis[$boundary - 1] = I18N::translate('%1$s–%2$s', I18N::number($boundaries[$n - 1]), I18N::number($boundary));
- }
- }
-
- /* I18N: Label on a graph; 40+ means 40 or more */
- $axis[PHP_INT_MAX] = I18N::translate('%s+', I18N::number($boundaries[count($boundaries) - 1]));
-
- return $axis;
- }
-
- /**
- * Calculate the Y axis.
- *
- * @param int|string $x
- * @param int|string $z
- * @param int|string $value
- * @param array $x_axis
- * @param array $z_axis
- * @param int[][] $ydata
- *
- * @return void
- */
- private function fillYData($x, $z, $value, array $x_axis, array $z_axis, array &$ydata)
- {
- $x = $this->findAxisEntry($x, $x_axis);
- $z = $this->findAxisEntry($z, $z_axis);
-
- if (!array_key_exists($z, $z_axis)) {
- foreach (array_keys($z_axis) as $key) {
- if ($value <= $key) {
- $z = $key;
- break;
- }
- }
- }
-
- // Add the value to the appropriate data point.
- $ydata[$z][$x] = ($ydata[$z][$x] ?? 0) + $value;
- }
-
- /**
- * Find the axis entry for a given value.
- * Some are direct lookup (e.g. M/F, JAN/FEB/MAR).
- * Others need to find the approprate range.
- *
- * @param int|float|string $value
- * @param string[] $axis
- *
- * @return int|string
- */
- private function findAxisEntry($value, $axis)
- {
- if (is_numeric($value)) {
- $value = (int) $value;
-
- if (!array_key_exists($value, $axis)) {
- foreach (array_keys($axis) as $boundary) {
- if ($value <= $boundary) {
- $value = $boundary;
- break;
- }
- }
- }
- }
-
-
- return $value;
- }
-
- /**
- * Plot the data.
- *
- * @param string $chart_title
- * @param string[] $x_axis
- * @param string $x_axis_title
- * @param int[][] $ydata
- * @param string $y_axis_title
- * @param string[] $z_axis
- * @param int $y_axis_type
- *
- * @return string
- */
- private function myPlot(string $chart_title, array $x_axis, string $x_axis_title, array $ydata, string $y_axis_title, array $z_axis, int $y_axis_type): string
- {
- // Bar dimensions
- if (count($ydata) > 3) {
- $chbh = '5,1';
- } elseif (count($ydata) < 2) {
- $chbh = '45,1';
- } else {
- $chbh = '20,3';
- }
-
- // Colors for z-axis
- $colors = [];
- $index = 0;
- while (count($colors) < count($ydata)) {
- $colors[] = self::Z_AXIS_COLORS[$index];
- $index = ($index + 1) % count(self::Z_AXIS_COLORS);
- }
-
- // Convert our sparse dataset into a fixed-size array
- $tmp = [];
- foreach (array_keys($z_axis) as $z) {
- foreach (array_keys($x_axis) as $x) {
- $tmp[$z][$x] = $ydata[$z][$x] ?? 0;
- }
- }
- $ydata = $tmp;
-
- // The chart data
- if ($y_axis_type === self::Y_AXIS_PERCENT) {
- // Normalise each (non-zero!) set of data to total 100%
- array_walk($ydata, function (array &$x) {
- $sum = array_sum($x);
- if ($sum > 0) {
- $x = array_map(function ($y) use ($sum) {
- return $y * 100.0 / $sum;
- }, $x);
- }
- });
- }
-
- // Find the maximum value, so we can draw the scale
- $ymax = max(array_map(function (array $x) {
- return max($x);
- }, $ydata));
-
- // Google charts API requires data to be scaled 0 - 100.
- $scale = max(array_map(function (array $x) {
- return max($x);
- }, $ydata));
-
- if ($scale > 0) {
- $scalefactor = 100.0 / $scale;
- array_walk_recursive($ydata, function (&$n) use ($scalefactor) {
- $n *= $scalefactor;
- });
- }
-
- // Lables for the two axes.
- $x_axis_labels = implode('|', $x_axis);
- $y_axis_labels = '';
-
- if ($y_axis_type === self::Y_AXIS_PERCENT) {
- // Draw 10 intervals on the Y axis.
- $intervals = 10;
- for ($i = 1; $i <= $intervals; $i++) {
- if ($ymax <= 20.0) {
- $y_axis_labels .= round($ymax * $i / $intervals, 1) . '|';
- } else {
- $y_axis_labels .= round($ymax * $i / $intervals, 0) . '|';
- }
- }
- } elseif ($y_axis_type === self::Y_AXIS_NUMBERS) {
- // Draw up to 10 intervals on the Y axis.
- $intervals = min(10, $ymax);
- for ($i = 1; $i <= $intervals; $i++) {
- $y_axis_labels .= round($ymax * $i / $intervals, 0) . '|';
- }
- }
-
- $data = implode('|', array_map(function (array $x): string {
- return implode(',', $x);
- }, $ydata));
-
- $attributes = [
- 'chbh' => $chbh,
- 'chd' => 't:' . $data,
- 'chf' => 'bg,s,ffffff00|c,s,ffffff00',
- 'chco' => implode(',', $colors),
- 'chs' => self::CHART_WIDTH . 'x' . self::CHART_HEIGHT,
- 'cht' => 'bvg',
- 'chtt' => $chart_title,
- 'chxl' => '0:|' . $x_axis_labels . '|1:||||' . $x_axis_title . '|2:|0|' . $y_axis_labels . '3:||' . $y_axis_title . '|',
- 'chxt' => 'x,x,y,y',
- ];
-
- // More than one Z axis? Show a legend for them.
- if (count($z_axis) > 1) {
- $attributes['chdl'] = implode('|', $z_axis);
- }
-
- $url = Html::url('https://chart.googleapis.com/chart', $attributes);
-
- return '<img src="' . e($url) . '" class="img-fluid" alt="' . e($chart_title) . '">';
- }
-}
diff --git a/app/Module/StatisticsChartModule.php b/app/Module/StatisticsChartModule.php
index 1e530264cc..813761f17d 100644
--- a/app/Module/StatisticsChartModule.php
+++ b/app/Module/StatisticsChartModule.php
@@ -17,8 +17,16 @@ declare(strict_types=1);
namespace Fisharebest\Webtrees\Module;
+use Fisharebest\Webtrees\Auth;
+use Fisharebest\Webtrees\Date;
+use Fisharebest\Webtrees\Html;
use Fisharebest\Webtrees\I18N;
use Fisharebest\Webtrees\Individual;
+use Fisharebest\Webtrees\Stats;
+use Fisharebest\Webtrees\Tree;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpFoundation\Response;
+use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
/**
* Class StatisticsChartModule
@@ -27,6 +35,38 @@ class StatisticsChartModule extends AbstractModule implements ModuleInterface, M
{
use ModuleChartTrait;
+ // We generate a bitmap chart with these dimensions in image pixels.
+ // These set the aspect ratio. The actual image is sized using CSS
+ // The maximum size (width x height) is 300,000
+ private const CHART_WIDTH = 950;
+ private const CHART_HEIGHT = 315;
+
+ public const X_AXIS_INDIVIDUAL_MAP = 1;
+ public const X_AXIS_BIRTH_MAP = 2;
+ public const X_AXIS_DEATH_MAP = 3;
+ public const X_AXIS_MARRIAGE_MAP = 4;
+ public const X_AXIS_BIRTH_MONTH = 11;
+ public const X_AXIS_DEATH_MONTH = 12;
+ public const X_AXIS_MARRIAGE_MONTH = 13;
+ public const X_AXIS_FIRST_CHILD_MONTH = 14;
+ public const X_AXIS_FIRST_MARRIAGE_MONTH = 15;
+ public const X_AXIS_AGE_AT_DEATH = 18;
+ public const X_AXIS_AGE_AT_MARRIAGE = 19;
+ public const X_AXIS_AGE_AT_FIRST_MARRIAGE = 20;
+ public const X_AXIS_NUMBER_OF_CHILDREN = 21;
+
+ public const Y_AXIS_NUMBERS = 201;
+ public const Y_AXIS_PERCENT = 202;
+
+ public const Z_AXIS_ALL = 300;
+ public const Z_AXIS_SEX = 301;
+ public const Z_AXIS_TIME = 302;
+
+ // First two colors are blue/pink, to work with Z_AXIS_SEX.
+ private const Z_AXIS_COLORS = ['0000FF', 'FFA0CB', '9F00FF', 'FF7000', '905030', 'FF0000', '00FF00', 'F0F000'];
+
+ private const DAYS_IN_YEAR = 365.25;
+
/**
* How should this module be labelled on tabs, menus, etc.?
*
@@ -69,8 +109,914 @@ class StatisticsChartModule extends AbstractModule implements ModuleInterface, M
*/
public function chartUrl(Individual $individual, array $parameters = []): string
{
- return route('statistics', [
- 'ged' => $individual->tree()->name(),
+ return route('module', [
+ 'module' => $this->name(),
+ 'action' => 'Chart',
+ 'ged' => $individual->tree()->name(),
] + $parameters);
}
+
+ /**
+ * A form to request the chart parameters.
+ *
+ * @param Tree $tree
+ *
+ * @return Response
+ */
+ public function getChartAction(Tree $tree): Response
+ {
+ $tabs = [
+ I18N::translate('Individuals') => route('module', [
+ 'module' => $this->name(),
+ 'action' => 'Individuals',
+ 'ged' => $tree->name()
+ ]),
+ I18N::translate('Families') => route('module', [
+ 'module' => $this->name(),
+ 'action' => 'Families',
+ 'ged' => $tree->name()
+ ]),
+ I18N::translate('Other') => route('module', [
+ 'module' => $this->name(),
+ 'action' => 'Other',
+ 'ged' => $tree->name()
+ ]),
+ I18N::translate('Custom') => route('module', [
+ 'module' => $this->name(),
+ 'action' => 'Custom',
+ 'ged' => $tree->name()
+ ]),
+ ];
+
+ return $this->viewResponse('modules/statistics-chart/chart-page', [
+ 'tabs' => $tabs,
+ 'title' => $this->title(),
+ ]);
+ }
+
+ /**
+ * @param Tree $tree
+ *
+ * @return Response
+ */
+ public function getIndividualsAction(Tree $tree): Response
+ {
+ $html = view('modules/statistics-chart/individuals', [
+ 'show_oldest_living' => Auth::check(),
+ 'stats' => new Stats($tree),
+ ]);
+
+ return new Response($html);
+ }
+
+ /**
+ * @param Stats $stats
+ *
+ * @return Response
+ */
+ public function getFamiliesAction(Stats $stats): Response
+ {
+ $html = view('modules/statistics-chart/families', [
+ 'stats' => $stats,
+ ]);
+
+ return new Response($html);
+ }
+
+ /**
+ * @param Stats $stats
+ *
+ * @return Response
+ */
+ public function getOtherAction(Stats $stats): Response
+ {
+ $html = view('modules/statistics-chart/other', [
+ 'stats' => $stats,
+ ]);
+
+ return new Response($html);
+ }
+
+ /**
+ * @param Tree $tree
+ *
+ * @return Response
+ */
+ public function getCustomAction(Tree $tree): Response
+ {
+ $html = view('modules/statistics-chart/custom', [
+ 'module' => $this,
+ 'tree' => $tree,
+ ]);
+
+ return new Response($html);
+ }
+
+ /**
+ * @param Request $request
+ * @param Tree $tree
+ *
+ * @return Response
+ */
+ public function getCustomChartAction(Request $request, Tree $tree): Response
+ {
+ $x_axis_type = (int) $request->get('x-as');
+ $y_axis_type = (int) $request->get('y-as');
+ $z_axis_type = (int) $request->get('z-as');
+ $ydata = [];
+ $stats = new Stats($tree);
+
+ switch ($x_axis_type) {
+ case self::X_AXIS_INDIVIDUAL_MAP:
+ return new Response($stats->chartDistribution(
+ $request->get('chart_shows', ''),
+ $request->get('chart_type', ''),
+ $request->get('SURN', '')
+ ));
+
+ case self::X_AXIS_BIRTH_MAP:
+ return new Response($stats->chartDistribution(
+ $request->get('chart_shows', ''),
+ 'birth_distribution_chart'
+ ));
+
+ case self::X_AXIS_DEATH_MAP:
+ return new Response($stats->chartDistribution(
+ $request->get('chart_shows', ''),
+ 'death_distribution_chart'
+ ));
+
+ case self::X_AXIS_MARRIAGE_MAP:
+ return new Response($stats->chartDistribution(
+ $request->get('chart_shows', ''),
+ 'marriage_distribution_chart'
+ ));
+
+ case self::X_AXIS_BIRTH_MONTH:
+ $chart_title = I18N::translate('Month of birth');
+ $x_axis_title = I18N::translate('Month');
+ $x_axis = $this->axisMonths();
+
+ switch ($y_axis_type) {
+ case self::Y_AXIS_NUMBERS:
+ $y_axis_title = I18N::translate('Individuals');
+ break;
+ case self::Y_AXIS_PERCENT:
+ $y_axis_title = '%';
+ break;
+ default:
+ throw new NotFoundHttpException();
+ }
+
+ switch ($z_axis_type) {
+ case self::Z_AXIS_ALL:
+ $z_axis = $this->axisAll();
+ $rows = $stats->statsBirthQuery(false, false);
+ foreach ($rows as $row) {
+ $this->fillYData($row->d_month, 0, $row->total, $x_axis, $z_axis, $ydata);
+ }
+ break;
+ case self::Z_AXIS_SEX:
+ $z_axis = $this->axisSexes();
+ $rows = $stats->statsBirthQuery(false, true);
+ foreach ($rows as $row) {
+ $this->fillYData($row->d_month, $row->i_sex, $row->total, $x_axis, $z_axis, $ydata);
+ }
+ break;
+ case self::Z_AXIS_TIME:
+ $boundaries_csv = $request->get('z-axis-boundaries-periods', '');
+ $z_axis = $this->axisYears($boundaries_csv);
+ $prev_boundary = 0;
+ foreach (array_keys($z_axis) as $boundary) {
+ $rows = $stats->statsBirthQuery(false, false, $prev_boundary, $boundary);
+ foreach ($rows as $row) {
+ $this->fillYData($row->d_month, $boundary, $row->total, $x_axis, $z_axis, $ydata);
+ }
+ $prev_boundary = $boundary + 1;
+ }
+ break;
+ default:
+ throw new NotFoundHttpException();
+ }
+
+ return new Response($this->myPlot($chart_title, $x_axis, $x_axis_title, $ydata, $y_axis_title, $z_axis, $y_axis_type));
+
+ case self::X_AXIS_DEATH_MONTH:
+ $chart_title = I18N::translate('Month of death');
+ $x_axis_title = I18N::translate('Month');
+ $x_axis = $this->axisMonths();
+
+ switch ($y_axis_type) {
+ case self::Y_AXIS_NUMBERS:
+ $y_axis_title = I18N::translate('Individuals');
+ break;
+ case self::Y_AXIS_PERCENT:
+ $y_axis_title = '%';
+ break;
+ default:
+ throw new NotFoundHttpException();
+ }
+
+ switch ($z_axis_type) {
+ case self::Z_AXIS_ALL:
+ $z_axis = $this->axisAll();
+ $rows = $stats->statsDeathQuery(false, false);
+ foreach ($rows as $row) {
+ $this->fillYData($row->d_month, 0, $row->total, $x_axis, $z_axis, $ydata);
+ }
+ break;
+ case self::Z_AXIS_SEX:
+ $z_axis = $this->axisSexes();
+ $rows = $stats->statsDeathQuery(false, true);
+ foreach ($rows as $row) {
+ $this->fillYData($row->d_month, $row->i_sex, $row->total, $x_axis, $z_axis, $ydata);
+ }
+ break;
+ case self::Z_AXIS_TIME:
+ $boundaries_csv = $request->get('z-axis-boundaries-periods', '');
+ $z_axis = $this->axisYears($boundaries_csv);
+ $prev_boundary = 0;
+ foreach (array_keys($z_axis) as $boundary) {
+ $rows = $stats->statsDeathQuery(false, false, $prev_boundary, $boundary);
+ foreach ($rows as $row) {
+ $this->fillYData($row->d_month, $boundary, $row->total, $x_axis, $z_axis, $ydata);
+ }
+ $prev_boundary = $boundary + 1;
+ }
+ break;
+ default:
+ throw new NotFoundHttpException();
+ }
+
+ return new Response($this->myPlot($chart_title, $x_axis, $x_axis_title, $ydata, $y_axis_title, $z_axis, $y_axis_type));
+
+ case self::X_AXIS_MARRIAGE_MONTH:
+ $chart_title = I18N::translate('Month of marriage');
+ $x_axis_title = I18N::translate('Month');
+ $x_axis = $this->axisMonths();
+
+ switch ($y_axis_type) {
+ case self::Y_AXIS_NUMBERS:
+ $y_axis_title = I18N::translate('Families');
+ break;
+ case self::Y_AXIS_PERCENT:
+ $y_axis_title = '%';
+ break;
+ default:
+ throw new NotFoundHttpException();
+ }
+
+ switch ($z_axis_type) {
+ case self::Z_AXIS_ALL:
+ $z_axis = $this->axisAll();
+ $rows = $stats->statsMarrQuery(false, false);
+ foreach ($rows as $row) {
+ $this->fillYData($row->d_month, 0, $row->total, $x_axis, $z_axis, $ydata);
+ }
+ break;
+ case self::Z_AXIS_TIME:
+ $boundaries_csv = $request->get('z-axis-boundaries-periods', '');
+ $z_axis = $this->axisYears($boundaries_csv);
+ $prev_boundary = 0;
+ foreach (array_keys($z_axis) as $boundary) {
+ $rows = $stats->statsMarrQuery(false, false, $prev_boundary, $boundary);
+ foreach ($rows as $row) {
+ $this->fillYData($row->d_month, $boundary, $row->total, $x_axis, $z_axis, $ydata);
+ }
+ $prev_boundary = $boundary + 1;
+ }
+ break;
+ default:
+ throw new NotFoundHttpException();
+ }
+
+ return new Response($this->myPlot($chart_title, $x_axis, $x_axis_title, $ydata, $y_axis_title, $z_axis, $y_axis_type));
+
+ case self::X_AXIS_FIRST_CHILD_MONTH:
+ $chart_title = I18N::translate('Month of birth of first child in a relation');
+ $x_axis_title = I18N::translate('Month');
+ $x_axis = $this->axisMonths();
+
+ switch ($y_axis_type) {
+ case self::Y_AXIS_NUMBERS:
+ $y_axis_title = I18N::translate('Children');
+ break;
+ case self::Y_AXIS_PERCENT:
+ $y_axis_title = '%';
+ break;
+ default:
+ throw new NotFoundHttpException();
+ }
+
+ switch ($z_axis_type) {
+ case self::Z_AXIS_ALL:
+ $z_axis = $this->axisAll();
+ $rows = $stats->monthFirstChildQuery(false);
+ foreach ($rows as $row) {
+ $this->fillYData($row->d_month, 0, $row->total, $x_axis, $z_axis, $ydata);
+ }
+ break;
+ case self::Z_AXIS_SEX:
+ $z_axis = $this->axisSexes();
+ $rows = $stats->monthFirstChildQuery(true);
+ foreach ($rows as $row) {
+ $this->fillYData($row->d_month, $row->i_sex, $row->total, $x_axis, $z_axis, $ydata);
+ }
+ break;
+ case self::Z_AXIS_TIME:
+ $boundaries_csv = $request->get('z-axis-boundaries-periods', '');
+ $z_axis = $this->axisYears($boundaries_csv);
+ $prev_boundary = 0;
+ foreach (array_keys($z_axis) as $boundary) {
+ $rows = $stats->statsMarrQuery(false, false, $prev_boundary, $boundary);
+ foreach ($rows as $row) {
+ $this->fillYData($row->d_month, $boundary, $row->total, $x_axis, $z_axis, $ydata);
+ }
+ $prev_boundary = $boundary + 1;
+ }
+ break;
+ default:
+ throw new NotFoundHttpException();
+ }
+
+ return new Response($this->myPlot($chart_title, $x_axis, $x_axis_title, $ydata, $y_axis_title, $z_axis, $y_axis_type));
+
+ case self::X_AXIS_FIRST_MARRIAGE_MONTH:
+ $chart_title = I18N::translate('Month of first marriage');
+ $x_axis_title = I18N::translate('Month');
+ $x_axis = $this->axisMonths();
+
+ switch ($y_axis_type) {
+ case self::Y_AXIS_NUMBERS:
+ $y_axis_title = I18N::translate('Families');
+ break;
+ case self::Y_AXIS_PERCENT:
+ $y_axis_title = '%';
+ break;
+ default:
+ throw new NotFoundHttpException();
+ }
+
+ switch ($z_axis_type) {
+ case self::Z_AXIS_ALL:
+ $z_axis = $this->axisAll();
+ $rows = $stats->statsMarrQuery(false, true);
+ $indi = [];
+ $fam = [];
+ foreach ($rows as $row) {
+ if (!in_array($row->indi, $indi) && !in_array($row->fams, $fam)) {
+ $this->fillYData($row->month, 0, 1, $x_axis, $z_axis, $ydata);
+ }
+ $indi[] = $row->indi;
+ $fam[] = $row->fams;
+ }
+ break;
+ case self::Z_AXIS_TIME:
+ $boundaries_csv = $request->get('z-axis-boundaries-periods', '');
+ $z_axis = $this->axisYears($boundaries_csv);
+ $prev_boundary = 0;
+ $indi = [];
+ $fam = [];
+ foreach (array_keys($z_axis) as $boundary) {
+ $rows = $stats->statsMarrQuery(false, true, $prev_boundary, $boundary);
+ foreach ($rows as $row) {
+ if (!in_array($row->indi, $indi) && !in_array($row->fams, $fam)) {
+ $this->fillYData($row->month, $boundary, 1, $x_axis, $z_axis, $ydata);
+ }
+ $indi[] = $row->indi;
+ $fam[] = $row->fams;
+ }
+ $prev_boundary = $boundary + 1;
+ }
+ break;
+ default:
+ throw new NotFoundHttpException();
+ }
+
+ return new Response($this->myPlot($chart_title, $x_axis, $x_axis_title, $ydata, $y_axis_title, $z_axis, $y_axis_type));
+
+ case self::X_AXIS_AGE_AT_DEATH:
+ $chart_title = I18N::translate('Average age at death');
+ $x_axis_title = I18N::translate('age');
+ $boundaries_csv = $request->get('x-axis-boundaries-ages', '');
+ $x_axis = $this->axisNumbers($boundaries_csv);
+
+ switch ($y_axis_type) {
+ case self::Y_AXIS_NUMBERS:
+ $y_axis_title = I18N::translate('Individuals');
+ break;
+ case self::Y_AXIS_PERCENT:
+ $y_axis_title = '%';
+ break;
+ default:
+ throw new NotFoundHttpException();
+ }
+
+ switch ($z_axis_type) {
+ case self::Z_AXIS_ALL:
+ $z_axis = $this->axisAll();
+ $rows = $stats->statsAgeQuery(false, 'DEAT');
+ foreach ($rows as $row) {
+ foreach ($row as $age) {
+ $years = (int) ($age / self::DAYS_IN_YEAR);
+ $this->fillYData($years, 0, 1, $x_axis, $z_axis, $ydata);
+ }
+ }
+ break;
+ case self::Z_AXIS_SEX:
+ $z_axis = $this->axisSexes();
+ foreach (array_keys($z_axis) as $sex) {
+ $rows = $stats->statsAgeQuery(false, 'DEAT', $sex);
+ foreach ($rows as $row) {
+ foreach ($row as $age) {
+ $years = (int) ($age / self::DAYS_IN_YEAR);
+ $this->fillYData($years, $sex, 1, $x_axis, $z_axis, $ydata);
+ }
+ }
+ }
+ break;
+ case self::Z_AXIS_TIME:
+ $boundaries_csv = $request->get('z-axis-boundaries-periods', '');
+ $z_axis = $this->axisYears($boundaries_csv);
+ $prev_boundary = 0;
+ foreach (array_keys($z_axis) as $boundary) {
+ $rows = $stats->statsAgeQuery(false, 'DEAT', 'BOTH', $prev_boundary, $boundary);
+ foreach ($rows as $row) {
+ foreach ($row as $age) {
+ $years = (int) ($age / self::DAYS_IN_YEAR);
+ $this->fillYData($years, $boundary, 1, $x_axis, $z_axis, $ydata);
+ }
+ }
+ $prev_boundary = $boundary + 1;
+ }
+
+ break;
+ default:
+ throw new NotFoundHttpException();
+ }
+
+ return new Response($this->myPlot($chart_title, $x_axis, $x_axis_title, $ydata, $y_axis_title, $z_axis, $y_axis_type));
+
+ case self::X_AXIS_AGE_AT_MARRIAGE:
+ $chart_title = I18N::translate('Age in year of marriage');
+ $x_axis_title = I18N::translate('age');
+ $boundaries_csv = $request->get('x-axis-boundaries-ages_m', '');
+ $x_axis = $this->axisNumbers($boundaries_csv);
+
+ switch ($y_axis_type) {
+ case self::Y_AXIS_NUMBERS:
+ $y_axis_title = I18N::translate('Individuals');
+ break;
+ case self::Y_AXIS_PERCENT:
+ $y_axis_title = '%';
+ break;
+ default:
+ throw new NotFoundHttpException();
+ }
+
+ switch ($z_axis_type) {
+ case self::Z_AXIS_ALL:
+ $z_axis = $this->axisAll();
+ // The stats query doesn't have an "all" function, so query M/F/U separately
+ foreach (['M', 'F', 'U'] as $sex) {
+ $rows = $stats->statsMarrAgeQuery(false, $sex);
+ foreach ($rows as $row) {
+ $years = (int) ($row->age / self::DAYS_IN_YEAR);
+ $this->fillYData($years, 0, 1, $x_axis, $z_axis, $ydata);
+ }
+ }
+ break;
+ case self::Z_AXIS_SEX:
+ $z_axis = $this->axisSexes();
+ foreach (array_keys($z_axis) as $sex) {
+ $rows = $stats->statsMarrAgeQuery(false, $sex);
+ foreach ($rows as $row) {
+ $years = (int) ($row->age / self::DAYS_IN_YEAR);
+ $this->fillYData($years, $sex, 1, $x_axis, $z_axis, $ydata);
+ }
+ }
+ break;
+ case self::Z_AXIS_TIME:
+ $boundaries_csv = $request->get('z-axis-boundaries-periods', '');
+ $z_axis = $this->axisYears($boundaries_csv);
+ // The stats query doesn't have an "all" function, so query M/F/U separately
+ foreach (['M', 'F', 'U'] as $sex) {
+ $prev_boundary = 0;
+ foreach (array_keys($z_axis) as $boundary) {
+ $rows = $stats->statsMarrAgeQuery(false, $sex, $prev_boundary, $boundary);
+ foreach ($rows as $row) {
+ $years = (int) ($row->age / self::DAYS_IN_YEAR);
+ $this->fillYData($years, $boundary, 1, $x_axis, $z_axis, $ydata);
+ }
+ $prev_boundary = $boundary + 1;
+ }
+ }
+ break;
+ default:
+ throw new NotFoundHttpException();
+ }
+
+ return new Response($this->myPlot($chart_title, $x_axis, $x_axis_title, $ydata, $y_axis_title, $z_axis, $y_axis_type));
+
+ case self::X_AXIS_AGE_AT_FIRST_MARRIAGE:
+ $chart_title = I18N::translate('Age in year of first marriage');
+ $x_axis_title = I18N::translate('age');
+ $boundaries_csv = $request->get('x-axis-boundaries-ages_m', '');
+ $x_axis = $this->axisNumbers($boundaries_csv);
+
+ switch ($y_axis_type) {
+ case self::Y_AXIS_NUMBERS:
+ $y_axis_title = I18N::translate('Individuals');
+ break;
+ case self::Y_AXIS_PERCENT:
+ $y_axis_title = '%';
+ break;
+ default:
+ throw new NotFoundHttpException();
+ }
+
+ switch ($z_axis_type) {
+ case self::Z_AXIS_ALL:
+ $z_axis = $this->axisAll();
+ // The stats query doesn't have an "all" function, so query M/F/U separately
+ foreach (['M', 'F', 'U'] as $sex) {
+ $rows = $stats->statsMarrAgeQuery(false, $sex);
+ $indi = [];
+ foreach ($rows as $row) {
+ if (!in_array($row->d_gid, $indi)) {
+ $years = (int) ($row->age / self::DAYS_IN_YEAR);
+ $this->fillYData($years, 0, 1, $x_axis, $z_axis, $ydata);
+ $indi[] = $row->d_gid;
+ }
+ }
+ }
+ break;
+ case self::Z_AXIS_SEX:
+ $z_axis = $this->axisSexes();
+ foreach (array_keys($z_axis) as $sex) {
+ $rows = $stats->statsMarrAgeQuery(false, $sex);
+ $indi = [];
+ foreach ($rows as $row) {
+ if (!in_array($row->d_gid, $indi)) {
+ $years = (int) ($row->age / self::DAYS_IN_YEAR);
+ $this->fillYData($years, $sex, 1, $x_axis, $z_axis, $ydata);
+ $indi[] = $row->d_gid;
+ }
+ }
+ }
+ break;
+ case self::Z_AXIS_TIME:
+ $boundaries_csv = $request->get('z-axis-boundaries-periods', '');
+ $z_axis = $this->axisYears($boundaries_csv);
+ // The stats query doesn't have an "all" function, so query M/F/U separately
+ foreach (['M', 'F', 'U'] as $sex) {
+ $prev_boundary = 0;
+ $indi = [];
+ foreach (array_keys($z_axis) as $boundary) {
+ $rows = $stats->statsMarrAgeQuery(false, $sex, $prev_boundary, $boundary);
+ foreach ($rows as $row) {
+ if (!in_array($row->d_gid, $indi)) {
+ $years = (int) ($row->age / self::DAYS_IN_YEAR);
+ $this->fillYData($years, $boundary, 1, $x_axis, $z_axis, $ydata);
+ $indi[] = $row->d_gid;
+ }
+ }
+ $prev_boundary = $boundary + 1;
+ }
+ }
+ break;
+ default:
+ throw new NotFoundHttpException();
+ }
+
+ return new Response($this->myPlot($chart_title, $x_axis, $x_axis_title, $ydata, $y_axis_title, $z_axis, $y_axis_type));
+
+ case self::X_AXIS_NUMBER_OF_CHILDREN:
+ $chart_title = I18N::translate('Number of children');
+ $x_axis_title = I18N::translate('Children');
+ $x_axis = $this->axisNumbers('1,2,3,4,5,6,7,8,9,10');
+
+ switch ($y_axis_type) {
+ case self::Y_AXIS_NUMBERS:
+ $y_axis_title = I18N::translate('Families');
+ break;
+ case self::Y_AXIS_PERCENT:
+ $y_axis_title = '%';
+ break;
+ default:
+ throw new NotFoundHttpException();
+ }
+
+ switch ($z_axis_type) {
+ case self::Z_AXIS_ALL:
+ $z_axis = $this->axisAll();
+ $rows = $stats->statsChildrenQuery(false);
+ foreach ($rows as $row) {
+ $this->fillYData($row->f_numchil, 0, $row->total, $x_axis, $z_axis, $ydata);
+ }
+ break;
+ case self::Z_AXIS_TIME:
+ $boundaries_csv = $request->get('z-axis-boundaries-periods', '');
+ $z_axis = $this->axisYears($boundaries_csv);
+ $prev_boundary = 0;
+ foreach (array_keys($z_axis) as $boundary) {
+ $rows = $stats->statsChildrenQuery(false, 'BOTH', $prev_boundary, $boundary);
+ foreach ($rows as $row) {
+ $this->fillYData($row->f_numchil, $boundary, $row->total, $x_axis, $z_axis, $ydata);
+ }
+ $prev_boundary = $boundary + 1;
+ }
+ break;
+ default:
+ throw new NotFoundHttpException();
+ }
+
+ return new Response($this->myPlot($chart_title, $x_axis, $x_axis_title, $ydata, $y_axis_title, $z_axis, $y_axis_type));
+
+ default:
+ throw new NotFoundHttpException();
+ break;
+ }
+ }
+
+ /**
+ * @return string[]
+ */
+ private function axisAll(): array
+ {
+ return [
+ I18N::translate('Total'),
+ ];
+ }
+
+ /**
+ * @return string[]
+ */
+ private function axisSexes(): array
+ {
+ return [
+ 'M' => I18N::translate('Male'),
+ 'F' => I18N::translate('Female'),
+ ];
+ }
+
+ /**
+ * Labels for the X axis
+ *
+ * @return string[]
+ */
+ private function axisMonths(): array
+ {
+ return [
+ 'JAN' => I18N::translateContext('NOMINATIVE', 'January'),
+ 'FEB' => I18N::translateContext('NOMINATIVE', 'February'),
+ 'MAR' => I18N::translateContext('NOMINATIVE', 'March'),
+ 'APR' => I18N::translateContext('NOMINATIVE', 'April'),
+ 'MAY' => I18N::translateContext('NOMINATIVE', 'May'),
+ 'JUN' => I18N::translateContext('NOMINATIVE', 'June'),
+ 'JUL' => I18N::translateContext('NOMINATIVE', 'July'),
+ 'AUG' => I18N::translateContext('NOMINATIVE', 'August'),
+ 'SEP' => I18N::translateContext('NOMINATIVE', 'September'),
+ 'OCT' => I18N::translateContext('NOMINATIVE', 'October'),
+ 'NOV' => I18N::translateContext('NOMINATIVE', 'November'),
+ 'DEC' => I18N::translateContext('NOMINATIVE', 'December'),
+ ];
+ }
+
+ /**
+ * Convert a list of N year-boundaries into N+1 year-ranges for the z-axis.
+ *
+ * @param string $boundaries_csv
+ *
+ * @return string[]
+ */
+ private function axisYears(string $boundaries_csv): array
+ {
+ $boundaries = explode(',', $boundaries_csv);
+
+ $axis = [];
+ foreach ($boundaries as $n => $boundary) {
+ if ($n === 0) {
+ $date = new Date('BEF ' . $boundary);
+ } else {
+ $date = new Date('BET ' . $boundaries[$n - 1] . ' AND ' . ($boundary - 1));
+ }
+ $axis[$boundary - 1] = strip_tags($date->display());
+ }
+
+ $date = new Date('AFT ' . $boundaries[count($boundaries) - 1]);
+ $axis[PHP_INT_MAX] = strip_tags($date->display());
+
+ return $axis;
+ }
+
+ /**
+ * Create the X axis.
+ *
+ * @param string $boundaries_csv
+ *
+ * @return array
+ */
+ private function axisNumbers(string $boundaries_csv): array
+ {
+ $boundaries = explode(',', $boundaries_csv);
+
+ $boundaries = array_map(function (string $x): int {
+ return (int) $x;
+ }, $boundaries);
+
+ $axis = [];
+ foreach ($boundaries as $n => $boundary) {
+ if ($n === 0) {
+ /* I18N: A range of numbers */
+ $axis[$boundary - 1] = I18N::translate('%1$s–%2$s', I18N::number(0), I18N::number($boundary));
+ } else {
+ /* I18N: A range of numbers */
+ $axis[$boundary - 1] = I18N::translate('%1$s–%2$s', I18N::number($boundaries[$n - 1]), I18N::number($boundary));
+ }
+ }
+
+ /* I18N: Label on a graph; 40+ means 40 or more */
+ $axis[PHP_INT_MAX] = I18N::translate('%s+', I18N::number($boundaries[count($boundaries) - 1]));
+
+ return $axis;
+ }
+
+ /**
+ * Calculate the Y axis.
+ *
+ * @param int|string $x
+ * @param int|string $z
+ * @param int|string $value
+ * @param array $x_axis
+ * @param array $z_axis
+ * @param int[][] $ydata
+ *
+ * @return void
+ */
+ private function fillYData($x, $z, $value, array $x_axis, array $z_axis, array &$ydata)
+ {
+ $x = $this->findAxisEntry($x, $x_axis);
+ $z = $this->findAxisEntry($z, $z_axis);
+
+ if (!array_key_exists($z, $z_axis)) {
+ foreach (array_keys($z_axis) as $key) {
+ if ($value <= $key) {
+ $z = $key;
+ break;
+ }
+ }
+ }
+
+ // Add the value to the appropriate data point.
+ $ydata[$z][$x] = ($ydata[$z][$x] ?? 0) + $value;
+ }
+
+ /**
+ * Find the axis entry for a given value.
+ * Some are direct lookup (e.g. M/F, JAN/FEB/MAR).
+ * Others need to find the approprate range.
+ *
+ * @param int|float|string $value
+ * @param string[] $axis
+ *
+ * @return int|string
+ */
+ private function findAxisEntry($value, $axis)
+ {
+ if (is_numeric($value)) {
+ $value = (int) $value;
+
+ if (!array_key_exists($value, $axis)) {
+ foreach (array_keys($axis) as $boundary) {
+ if ($value <= $boundary) {
+ $value = $boundary;
+ break;
+ }
+ }
+ }
+ }
+
+
+ return $value;
+ }
+
+ /**
+ * Plot the data.
+ *
+ * @param string $chart_title
+ * @param string[] $x_axis
+ * @param string $x_axis_title
+ * @param int[][] $ydata
+ * @param string $y_axis_title
+ * @param string[] $z_axis
+ * @param int $y_axis_type
+ *
+ * @return string
+ */
+ private function myPlot(string $chart_title, array $x_axis, string $x_axis_title, array $ydata, string $y_axis_title, array $z_axis, int $y_axis_type): string
+ {
+ // Bar dimensions
+ if (count($ydata) > 3) {
+ $chbh = '5,1';
+ } elseif (count($ydata) < 2) {
+ $chbh = '45,1';
+ } else {
+ $chbh = '20,3';
+ }
+
+ // Colors for z-axis
+ $colors = [];
+ $index = 0;
+ while (count($colors) < count($ydata)) {
+ $colors[] = self::Z_AXIS_COLORS[$index];
+ $index = ($index + 1) % count(self::Z_AXIS_COLORS);
+ }
+
+ // Convert our sparse dataset into a fixed-size array
+ $tmp = [];
+ foreach (array_keys($z_axis) as $z) {
+ foreach (array_keys($x_axis) as $x) {
+ $tmp[$z][$x] = $ydata[$z][$x] ?? 0;
+ }
+ }
+ $ydata = $tmp;
+
+ // The chart data
+ if ($y_axis_type === self::Y_AXIS_PERCENT) {
+ // Normalise each (non-zero!) set of data to total 100%
+ array_walk($ydata, function (array &$x) {
+ $sum = array_sum($x);
+ if ($sum > 0) {
+ $x = array_map(function ($y) use ($sum) {
+ return $y * 100.0 / $sum;
+ }, $x);
+ }
+ });
+ }
+
+ // Find the maximum value, so we can draw the scale
+ $ymax = max(array_map(function (array $x) {
+ return max($x);
+ }, $ydata));
+
+ // Google charts API requires data to be scaled 0 - 100.
+ $scale = max(array_map(function (array $x) {
+ return max($x);
+ }, $ydata));
+
+ if ($scale > 0) {
+ $scalefactor = 100.0 / $scale;
+ array_walk_recursive($ydata, function (&$n) use ($scalefactor) {
+ $n *= $scalefactor;
+ });
+ }
+
+ // Lables for the two axes.
+ $x_axis_labels = implode('|', $x_axis);
+ $y_axis_labels = '';
+
+ if ($y_axis_type === self::Y_AXIS_PERCENT) {
+ // Draw 10 intervals on the Y axis.
+ $intervals = 10;
+ for ($i = 1; $i <= $intervals; $i++) {
+ if ($ymax <= 20.0) {
+ $y_axis_labels .= round($ymax * $i / $intervals, 1) . '|';
+ } else {
+ $y_axis_labels .= round($ymax * $i / $intervals, 0) . '|';
+ }
+ }
+ } elseif ($y_axis_type === self::Y_AXIS_NUMBERS) {
+ // Draw up to 10 intervals on the Y axis.
+ $intervals = min(10, $ymax);
+ for ($i = 1; $i <= $intervals; $i++) {
+ $y_axis_labels .= round($ymax * $i / $intervals, 0) . '|';
+ }
+ }
+
+ $data = implode('|', array_map(function (array $x): string {
+ return implode(',', $x);
+ }, $ydata));
+
+ $attributes = [
+ 'chbh' => $chbh,
+ 'chd' => 't:' . $data,
+ 'chf' => 'bg,s,ffffff00|c,s,ffffff00',
+ 'chco' => implode(',', $colors),
+ 'chs' => self::CHART_WIDTH . 'x' . self::CHART_HEIGHT,
+ 'cht' => 'bvg',
+ 'chtt' => $chart_title,
+ 'chxl' => '0:|' . $x_axis_labels . '|1:||||' . $x_axis_title . '|2:|0|' . $y_axis_labels . '3:||' . $y_axis_title . '|',
+ 'chxt' => 'x,x,y,y',
+ ];
+
+ // More than one Z axis? Show a legend for them.
+ if (count($z_axis) > 1) {
+ $attributes['chdl'] = implode('|', $z_axis);
+ }
+
+ $url = Html::url('https://chart.googleapis.com/chart', $attributes);
+
+ return '<img src="' . e($url) . '" class="img-fluid" alt="' . e($chart_title) . '">';
+ }
}
diff --git a/resources/views/modules/statistics-chart/chart-page.phtml b/resources/views/modules/statistics-chart/chart-page.phtml
new file mode 100644
index 0000000000..a78f389ab2
--- /dev/null
+++ b/resources/views/modules/statistics-chart/chart-page.phtml
@@ -0,0 +1,50 @@
+<?php use Fisharebest\Webtrees\I18N; ?>
+<?php use Fisharebest\Webtrees\View; ?>
+
+<h2 class="wt-page-title">
+ <?= $title ?>
+</h2>
+
+<div class="wt-page-content wt-chart wt-statistics-chart" id="statistics-tabs">
+ <ul class="nav nav-tabs" role="tablist">
+ <?php foreach ($tabs as $label => $url) : ?>
+ <li class="nav-item">
+ <a class="nav-link" href="#tab-<?= e(md5($url)) ?>" data-toggle="tab" data-href="<?= e($url) ?>" role="tab">
+ <?= $label ?>
+ </a>
+ </li>
+ <?php endforeach ?>
+ </ul>
+
+ <div class="tab-content">
+ <?php foreach ($tabs as $label => $url) : ?>
+ <div class="tab-pane fade wt-ajax-load" role="tabpanel" id="tab-<?= e(md5($url)) ?>"></div>
+ <?php endforeach ?>
+ </div>
+</div>
+
+<?php View::push('javascript') ?>
+<script>
+ "use strict";
+
+ // Bootstrap tabs - load content dynamically using AJAX
+ $('a[data-toggle="tab"][data-href]').on('show.bs.tab', function () {
+ $(this.getAttribute('href') + ':empty').load($(this).data('href'));
+ });
+
+ // If the URL contains a fragment, then activate the corresponding tab.
+ // Use a prefix on the fragment, to prevent scrolling to the element.
+ var target = window.location.hash.replace("tab-", "");
+ var tab = $("#statistics-tabs .nav-link[href='" + target + "']");
+ // If not, then activate the first tab.
+ if (tab.length === 0) {
+ tab = $("#statistics-tabs .nav-link:first");
+ }
+ tab.tab("show");
+
+ // If the user selects a tab, update the URL to reflect this
+ $('#statistics-tabs a[data-toggle="tab"]').on('shown.bs.tab', function (e) {
+ window.location.hash = "tab-" + e.target.href.substring(e.target.href.indexOf('#') + 1);
+ });
+</script>
+<?php View::endpush() ?>
diff --git a/resources/views/statistics-chart-custom.phtml b/resources/views/modules/statistics-chart/custom.phtml
index 0e53a565e3..7fb3fe9369 100644
--- a/resources/views/statistics-chart-custom.phtml
+++ b/resources/views/modules/statistics-chart/custom.phtml
@@ -1,10 +1,5 @@
-<?php use Fisharebest\Webtrees\Bootstrap4;
-use Fisharebest\Webtrees\Http\Controllers\StatisticsChartController;
-use Fisharebest\Webtrees\I18N;
-
-?>
-<?php ?>
-<?php ?>
+<?php use Fisharebest\Webtrees\Bootstrap4; ?>
+<?php use Fisharebest\Webtrees\I18N; ?>
<script>
function statusDisable(sel) {
@@ -88,7 +83,10 @@ use Fisharebest\Webtrees\I18N;
<?= I18N::translate('Create your own chart') ?>
</h3>
-<form id="own-stats-form" action="<?= e(route('statistics-chart')) ?>" onsubmit="return loadCustomChart();" class="wt-page-options wt-page-options-statistics">
+<form id="own-stats-form" action="index.php" onsubmit="return loadCustomChart();" class="wt-page-options wt-page-options-statistics">
+ <input type="hidden" name="route" value="module">
+ <input type="hidden" name="module" value="<?= e($module->name()) ?>">
+ <input type="hidden" name="action" value="CustomChart">
<input type="hidden" name="ged" value="<?= e($tree->name()) ?>">
<div class="form-group row">
<div class="col-sm-2 wt-page-options-label">
@@ -99,31 +97,31 @@ use Fisharebest\Webtrees\I18N;
<fieldset>
<legend><?= I18N::translate('Individual') ?></legend>
- <?= Bootstrap4::radioButtons('x-as', [StatisticsChartController::X_AXIS_BIRTH_MONTH => I18N::translate('Month of birth')], StatisticsChartController::X_AXIS_BIRTH_MONTH, false, ['onchange' => 'statusEnable("z_sex"); statusHide("x_years");; statusHide("map_opt");']) ?>
- <?= Bootstrap4::radioButtons('x-as', [StatisticsChartController::X_AXIS_DEATH_MONTH => I18N::translate('Month of death')], '', false, ['onchange' => 'statusEnable("z_sex"); statusHide("x_years");; statusHide("map_opt");']) ?>
- <?= Bootstrap4::radioButtons('x-as', [StatisticsChartController::X_AXIS_FIRST_CHILD_MONTH => I18N::translate('Month of birth of first child in a relation')], '', false, ['onchange' => 'statusEnable("z_sex"); statusHide("x_years");; statusHide("map_opt");']) ?>
- <?= Bootstrap4::radioButtons('x-as', [StatisticsChartController::X_AXIS_AGE_AT_DEATH => I18N::translate('Average age at death')], '', false, ['onchange' => 'statusEnable("z_sex"); statusShow("x_years");; statusHide("map_opt");']) ?>
- <?= Bootstrap4::radioButtons('x-as', [StatisticsChartController::X_AXIS_AGE_AT_MARRIAGE => I18N::translate('Age in year of marriage')], '', false, ['onchange' => 'statusEnable("z_sex"); statusHide("x_years"); statusShow("x_years_m");; statusHide("map_opt");']) ?>
- <?= Bootstrap4::radioButtons('x-as', [StatisticsChartController::X_AXIS_AGE_AT_FIRST_MARRIAGE => I18N::translate('Age in year of first marriage')], '', false, ['onchange' => 'statusEnable("z_sex"); statusHide("x_years"); statusShow("x_years_m");; statusHide("map_opt");']) ?>
+ <?= Bootstrap4::radioButtons('x-as', [$module::X_AXIS_BIRTH_MONTH => I18N::translate('Month of birth')], $module::X_AXIS_BIRTH_MONTH, false, ['onchange' => 'statusEnable("z_sex"); statusHide("x_years");; statusHide("map_opt");']) ?>
+ <?= Bootstrap4::radioButtons('x-as', [$module::X_AXIS_DEATH_MONTH => I18N::translate('Month of death')], '', false, ['onchange' => 'statusEnable("z_sex"); statusHide("x_years");; statusHide("map_opt");']) ?>
+ <?= Bootstrap4::radioButtons('x-as', [$module::X_AXIS_FIRST_CHILD_MONTH => I18N::translate('Month of birth of first child in a relation')], '', false, ['onchange' => 'statusEnable("z_sex"); statusHide("x_years");; statusHide("map_opt");']) ?>
+ <?= Bootstrap4::radioButtons('x-as', [$module::X_AXIS_AGE_AT_DEATH => I18N::translate('Average age at death')], '', false, ['onchange' => 'statusEnable("z_sex"); statusShow("x_years");; statusHide("map_opt");']) ?>
+ <?= Bootstrap4::radioButtons('x-as', [$module::X_AXIS_AGE_AT_MARRIAGE => I18N::translate('Age in year of marriage')], '', false, ['onchange' => 'statusEnable("z_sex"); statusHide("x_years"); statusShow("x_years_m");; statusHide("map_opt");']) ?>
+ <?= Bootstrap4::radioButtons('x-as', [$module::X_AXIS_AGE_AT_FIRST_MARRIAGE => I18N::translate('Age in year of first marriage')], '', false, ['onchange' => 'statusEnable("z_sex"); statusHide("x_years"); statusShow("x_years_m");; statusHide("map_opt");']) ?>
</fieldset>
<fieldset>
<legend><?= I18N::translate('Family') ?></legend>
<?= Bootstrap4::radioButtons('x-as', [
- StatisticsChartController::X_AXIS_MARRIAGE_MONTH => I18N::translate('Month of marriage'),
- StatisticsChartController::X_AXIS_FIRST_MARRIAGE_MONTH => I18N::translate('Month of first marriage'),
+ $module::X_AXIS_MARRIAGE_MONTH => I18N::translate('Month of marriage'),
+ $module::X_AXIS_FIRST_MARRIAGE_MONTH => I18N::translate('Month of first marriage'),
], '', false, ['onchange' => 'statusChecked("z_none"); statusDisable("z_sex"); statusHide("x_years");; statusHide("map_opt");']) ?>
- <?= Bootstrap4::radioButtons('x-as', [StatisticsChartController::X_AXIS_NUMBER_OF_CHILDREN => I18N::translate('Number of children')], '', false, ['onchange' => 'statusChecked("z_none"); statusDisable("z_sex"); statusHide("x_years");; statusHide("map_opt");']) ?>
+ <?= Bootstrap4::radioButtons('x-as', [$module::X_AXIS_NUMBER_OF_CHILDREN => I18N::translate('Number of children')], '', false, ['onchange' => 'statusChecked("z_none"); statusDisable("z_sex"); statusHide("x_years");; statusHide("map_opt");']) ?>
</fieldset>
<fieldset>
<legend><?= I18N::translate('Map') ?></legend>
- <?= Bootstrap4::radioButtons('x-as', [StatisticsChartController::X_AXIS_INDIVIDUAL_MAP => I18N::translate('Individuals')], '', false, ['onchange' => 'statusHide("x_years");; statusShow("map_opt"); statusShow("chart_type");']) ?>
- <?= Bootstrap4::radioButtons('x-as', [StatisticsChartController::X_AXIS_BIRTH_MAP => I18N::translate('Births')], '', false, ['onchange' => 'statusHide("x_years");; statusShow("map_opt"); statusHide("chart_type"); statusHide("surname_opt");']) ?>
- <?= Bootstrap4::radioButtons('x-as', [StatisticsChartController::X_AXIS_MARRIAGE_MAP => I18N::translate('Marriages')], '', false, ['onchange' => 'statusHide("x_years");; statusShow("map_opt"); statusHide("chart_type"); statusHide("surname_opt");']) ?>
- <?= Bootstrap4::radioButtons('x-as', [StatisticsChartController::X_AXIS_DEATH_MAP => I18N::translate('Deaths')], '', false, ['onchange' => 'statusHide("x_years");; statusShow("map_opt"); statusHide("chart_type"); statusHide("surname_opt");']) ?>
+ <?= Bootstrap4::radioButtons('x-as', [$module::X_AXIS_INDIVIDUAL_MAP => I18N::translate('Individuals')], '', false, ['onchange' => 'statusHide("x_years");; statusShow("map_opt"); statusShow("chart_type");']) ?>
+ <?= Bootstrap4::radioButtons('x-as', [$module::X_AXIS_BIRTH_MAP => I18N::translate('Births')], '', false, ['onchange' => 'statusHide("x_years");; statusShow("map_opt"); statusHide("chart_type"); statusHide("surname_opt");']) ?>
+ <?= Bootstrap4::radioButtons('x-as', [$module::X_AXIS_MARRIAGE_MAP => I18N::translate('Marriages')], '', false, ['onchange' => 'statusHide("x_years");; statusShow("map_opt"); statusHide("chart_type"); statusHide("surname_opt");']) ?>
+ <?= Bootstrap4::radioButtons('x-as', [$module::X_AXIS_DEATH_MAP => I18N::translate('Deaths')], '', false, ['onchange' => 'statusHide("x_years");; statusShow("map_opt"); statusHide("chart_type"); statusHide("surname_opt");']) ?>
</fieldset>
</div>
@@ -136,17 +134,17 @@ use Fisharebest\Webtrees\I18N;
<legend><?= I18N::translate('Categories') ?></legend>
<label>
- <input type="radio" id="z_none" name="z-as" value="<?= StatisticsChartController::Z_AXIS_ALL ?>" onclick="statusDisable('z-axis-boundaries-periods');">
+ <input type="radio" id="z_none" name="z-as" value="<?= $module::Z_AXIS_ALL ?>" onclick="statusDisable('z-axis-boundaries-periods');">
<?= I18N::translate('none') ?>
</label>
<br>
<label>
- <input type="radio" id="z_sex" name="z-as" value="<?= StatisticsChartController::Z_AXIS_SEX ?>" onclick="statusDisable('z-axis-boundaries-periods');">
+ <input type="radio" id="z_sex" name="z-as" value="<?= $module::Z_AXIS_SEX ?>" onclick="statusDisable('z-axis-boundaries-periods');">
<?= I18N::translate('gender') ?>
</label>
<br>
<label>
- <input type="radio" id="z_time" name="z-as" value="<?= StatisticsChartController::Z_AXIS_TIME ?>" checked onclick="statusEnable('z-axis-boundaries-periods');">
+ <input type="radio" id="z_time" name="z-as" value="<?= $module::Z_AXIS_TIME ?>" checked onclick="statusEnable('z-axis-boundaries-periods');">
<?= I18N::translate('date periods') ?>
</label>
<label for="z-axis-boundaries-periods" class="sr-only">
@@ -179,12 +177,12 @@ use Fisharebest\Webtrees\I18N;
<legend><?= I18N::translate('Results') ?></legend>
<label>
- <input type="radio" name="y-as" value="<?= StatisticsChartController::Y_AXIS_NUMBERS ?>" checked>
+ <input type="radio" name="y-as" value="<?= $module::Y_AXIS_NUMBERS ?>" checked>
<?= I18N::translate('numbers') ?>
</label>
<br>
<label>
- <input type="radio" name="y-as" value="<?= StatisticsChartController::Y_AXIS_PERCENT ?>">
+ <input type="radio" name="y-as" value="<?= $module::Y_AXIS_PERCENT ?>">
<?= I18N::translate('percentage') ?>
</label>
</fieldset>
diff --git a/resources/views/statistics-chart-families.phtml b/resources/views/modules/statistics-chart/families.phtml
index 9097fa3bd7..9097fa3bd7 100644
--- a/resources/views/statistics-chart-families.phtml
+++ b/resources/views/modules/statistics-chart/families.phtml
diff --git a/resources/views/statistics-chart-individuals.phtml b/resources/views/modules/statistics-chart/individuals.phtml
index 7980441272..7980441272 100644
--- a/resources/views/statistics-chart-individuals.phtml
+++ b/resources/views/modules/statistics-chart/individuals.phtml
diff --git a/resources/views/statistics-chart-other.phtml b/resources/views/modules/statistics-chart/other.phtml
index d30979de96..d30979de96 100644
--- a/resources/views/statistics-chart-other.phtml
+++ b/resources/views/modules/statistics-chart/other.phtml
diff --git a/resources/views/statistics-page.phtml b/resources/views/statistics-page.phtml
deleted file mode 100644
index 1dde58378e..0000000000
--- a/resources/views/statistics-page.phtml
+++ /dev/null
@@ -1,64 +0,0 @@
-<?php use Fisharebest\Webtrees\I18N; ?>
-<?php use Fisharebest\Webtrees\View; ?>
-
-<h2 class="wt-page-title">
- <?= $title ?>
-</h2>
-
-<div class="wt-page-content wt-chart wt-statistics-chart" id="statistics-tabs">
- <ul class="nav nav-tabs" role="tablist">
- <li class="nav-item">
- <a class="nav-link" href="#individual-statistics" data-toggle="tab" data-href="<?= e(route('statistics-individuals', ['ged' => $tree->name()])) ?>" role="tab">
- <?= I18N::translate('Individuals') ?>
- </a>
- </li>
- <li class="nav-item">
- <a class="nav-link" href="#family-statistics" data-toggle="tab" data-href="<?= e(route('statistics-families', ['ged' => $tree->name()])) ?>" role="tab">
- <?= I18N::translate('Families') ?>
- </a>
- </li>
- <li class="nav-item">
- <a class="nav-link" href="#other-statistics" data-toggle="tab" data-href="<?= e(route('statistics-other', ['ged' => $tree->name()])) ?>" role="tab">
- <?= I18N::translate('Others') ?>
- </a>
- </li>
- <li class="nav-item">
- <a class="nav-link" href="#custom-statistics" data-toggle="tab" data-href="<?= e(route('statistics-options', ['ged' => $tree->name()])) ?>" role="tab">
- <?= I18N::translate('Custom') ?>
- </a>
- </li>
- </ul>
-
- <div class="tab-content">
- <div class="tab-pane fade wt-ajax-load" role="tabpanel" id="individual-statistics"></div>
- <div class="tab-pane fade wt-ajax-load" role="tabpanel" id="family-statistics"></div>
- <div class="tab-pane fade wt-ajax-load" role="tabpanel" id="other-statistics"></div>
- <div class="tab-pane fade wt-ajax-load" role="tabpanel" id="custom-statistics"></div>
- </div>
-</div>
-
-<?php View::push('javascript') ?>
-<script>
- "use strict";
-
- // Bootstrap tabs - load content dynamically using AJAX
- $('a[data-toggle="tab"][data-href]').on('show.bs.tab', function () {
- $(this.getAttribute('href') + ':empty').load($(this).data('href'));
- });
-
- // If the URL contains a fragment, then activate the corresponding tab.
- // Use a prefix on the fragment, to prevent scrolling to the element.
- var target = window.location.hash.replace("tab-", "");
- var tab = $("#statistics-tabs .nav-link[href='" + target + "']");
- // If not, then activate the first tab.
- if (tab.length === 0) {
- tab = $("#statistics-tabs .nav-link:first");
- }
- tab.tab("show");
-
- // If the user selects a tab, update the URL to reflect this
- $('#statistics-tabs a[data-toggle="tab"]').on('shown.bs.tab', function (e) {
- window.location.hash = "tab-" + e.target.href.substring(e.target.href.indexOf('#') + 1);
- });
-</script>
-<?php View::endpush() ?>
diff --git a/routes/web.php b/routes/web.php
index b15069b911..fdc9213fe9 100644
--- a/routes/web.php
+++ b/routes/web.php
@@ -278,12 +278,6 @@ if ($tree instanceof Tree && $tree->getPreference('imported') === '1') {
'GET:interactive-chart' => 'InteractiveChartController@chart',
'GET:relationships' => 'RelationshipsChartController@page',
'GET:relationships-chart' => 'RelationshipsChartController@chart',
- 'GET:statistics' => 'StatisticsChartController@page',
- 'GET:statistics-families' => 'StatisticsChartController@chartFamilies',
- 'GET:statistics-individuals' => 'StatisticsChartController@chartIndividuals',
- 'GET:statistics-other' => 'StatisticsChartController@chartOther',
- 'GET:statistics-options' => 'StatisticsChartController@chartCustomOptions',
- 'GET:statistics-chart' => 'StatisticsChartController@chartCustomChart',
'POST:accept-changes' => 'PendingChangesController@acceptChanges',
'POST:reject-changes' => 'PendingChangesController@rejectChanges',
'POST:accept-all-changes' => 'PendingChangesController@acceptAllChanges',