diff options
| -rw-r--r-- | app/Http/Controllers/StatisticsChartController.php | 961 | ||||
| -rw-r--r-- | app/Module/StatisticsChartModule.php | 950 | ||||
| -rw-r--r-- | resources/views/modules/statistics-chart/chart-page.phtml | 50 | ||||
| -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.phtml | 64 | ||||
| -rw-r--r-- | routes/web.php | 6 |
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', |
