xref: /webtrees/app/Module/StatisticsChartModule.php (revision 3976b4703df669696105ed6b024b96d433c8fbdb)
1168ff6f3Sric2016<?php
2*3976b470SGreg Roach
3168ff6f3Sric2016/**
4168ff6f3Sric2016 * webtrees: online genealogy
58fcd0d32SGreg Roach * Copyright (C) 2019 webtrees development team
6168ff6f3Sric2016 * This program is free software: you can redistribute it and/or modify
7168ff6f3Sric2016 * it under the terms of the GNU General Public License as published by
8168ff6f3Sric2016 * the Free Software Foundation, either version 3 of the License, or
9168ff6f3Sric2016 * (at your option) any later version.
10168ff6f3Sric2016 * This program is distributed in the hope that it will be useful,
11168ff6f3Sric2016 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12168ff6f3Sric2016 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13168ff6f3Sric2016 * GNU General Public License for more details.
14168ff6f3Sric2016 * You should have received a copy of the GNU General Public License
15168ff6f3Sric2016 * along with this program. If not, see <http://www.gnu.org/licenses/>.
16168ff6f3Sric2016 */
17e7f56f2aSGreg Roachdeclare(strict_types=1);
18e7f56f2aSGreg Roach
19168ff6f3Sric2016namespace Fisharebest\Webtrees\Module;
20168ff6f3Sric2016
21c6266a77SGreg Roachuse Fisharebest\Webtrees\Auth;
22c6266a77SGreg Roachuse Fisharebest\Webtrees\Date;
23168ff6f3Sric2016use Fisharebest\Webtrees\I18N;
24168ff6f3Sric2016use Fisharebest\Webtrees\Individual;
259219296aSGreg Roachuse Fisharebest\Webtrees\Statistics;
266ccdf4f0SGreg Roachuse Psr\Http\Message\ResponseInterface;
276ccdf4f0SGreg Roachuse Psr\Http\Message\ServerRequestInterface;
28c6266a77SGreg Roachuse Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
29*3976b470SGreg Roach
3057ab2231SGreg Roachuse function app;
316960d4a1SGreg Roachuse function array_key_exists;
326960d4a1SGreg Roachuse function array_keys;
336960d4a1SGreg Roachuse function array_map;
346960d4a1SGreg Roachuse function array_merge;
356960d4a1SGreg Roachuse function array_sum;
366960d4a1SGreg Roachuse function array_values;
376960d4a1SGreg Roachuse function array_walk;
386960d4a1SGreg Roachuse function count;
396960d4a1SGreg Roachuse function explode;
406960d4a1SGreg Roachuse function in_array;
416960d4a1SGreg Roachuse function is_numeric;
426960d4a1SGreg Roachuse function sprintf;
436960d4a1SGreg Roachuse function strip_tags;
44168ff6f3Sric2016
45168ff6f3Sric2016/**
46168ff6f3Sric2016 * Class StatisticsChartModule
47168ff6f3Sric2016 */
4837eb8894SGreg Roachclass StatisticsChartModule extends AbstractModule implements ModuleChartInterface
49c1010edaSGreg Roach{
5049a243cbSGreg Roach    use ModuleChartTrait;
5149a243cbSGreg Roach
52c6266a77SGreg Roach    // We generate a bitmap chart with these dimensions in image pixels.
53c6266a77SGreg Roach    // These set the aspect ratio.  The actual image is sized using CSS
54c6266a77SGreg Roach    // The maximum size (width x height) is 300,000
55c6266a77SGreg Roach    private const CHART_WIDTH  = 950;
56c6266a77SGreg Roach    private const CHART_HEIGHT = 315;
57c6266a77SGreg Roach
58c6266a77SGreg Roach    public const X_AXIS_INDIVIDUAL_MAP        = 1;
59c6266a77SGreg Roach    public const X_AXIS_BIRTH_MAP             = 2;
60c6266a77SGreg Roach    public const X_AXIS_DEATH_MAP             = 3;
61c6266a77SGreg Roach    public const X_AXIS_MARRIAGE_MAP          = 4;
62c6266a77SGreg Roach    public const X_AXIS_BIRTH_MONTH           = 11;
63c6266a77SGreg Roach    public const X_AXIS_DEATH_MONTH           = 12;
64c6266a77SGreg Roach    public const X_AXIS_MARRIAGE_MONTH        = 13;
65c6266a77SGreg Roach    public const X_AXIS_FIRST_CHILD_MONTH     = 14;
66c6266a77SGreg Roach    public const X_AXIS_FIRST_MARRIAGE_MONTH  = 15;
67c6266a77SGreg Roach    public const X_AXIS_AGE_AT_DEATH          = 18;
68c6266a77SGreg Roach    public const X_AXIS_AGE_AT_MARRIAGE       = 19;
69c6266a77SGreg Roach    public const X_AXIS_AGE_AT_FIRST_MARRIAGE = 20;
70c6266a77SGreg Roach    public const X_AXIS_NUMBER_OF_CHILDREN    = 21;
71c6266a77SGreg Roach
72c6266a77SGreg Roach    public const Y_AXIS_NUMBERS = 201;
73c6266a77SGreg Roach    public const Y_AXIS_PERCENT = 202;
74c6266a77SGreg Roach
75c6266a77SGreg Roach    public const Z_AXIS_ALL  = 300;
76c6266a77SGreg Roach    public const Z_AXIS_SEX  = 301;
77c6266a77SGreg Roach    public const Z_AXIS_TIME = 302;
78c6266a77SGreg Roach
79c6266a77SGreg Roach    // First two colors are blue/pink, to work with Z_AXIS_SEX.
80c6266a77SGreg Roach    private const Z_AXIS_COLORS = ['0000FF', 'FFA0CB', '9F00FF', 'FF7000', '905030', 'FF0000', '00FF00', 'F0F000'];
81c6266a77SGreg Roach
82c6266a77SGreg Roach    private const DAYS_IN_YEAR = 365.25;
83c6266a77SGreg Roach
84168ff6f3Sric2016    /**
850cfd6963SGreg Roach     * How should this module be identified in the control panel, etc.?
86168ff6f3Sric2016     *
87168ff6f3Sric2016     * @return string
88168ff6f3Sric2016     */
8949a243cbSGreg Roach    public function title(): string
90c1010edaSGreg Roach    {
91bbb76c12SGreg Roach        /* I18N: Name of a module/chart */
92bbb76c12SGreg Roach        return I18N::translate('Statistics');
93168ff6f3Sric2016    }
94168ff6f3Sric2016
95168ff6f3Sric2016    /**
96168ff6f3Sric2016     * A sentence describing what this module does.
97168ff6f3Sric2016     *
98168ff6f3Sric2016     * @return string
99168ff6f3Sric2016     */
10049a243cbSGreg Roach    public function description(): string
101c1010edaSGreg Roach    {
102bbb76c12SGreg Roach        /* I18N: Description of the “StatisticsChart” module */
103bbb76c12SGreg Roach        return I18N::translate('Various statistics charts.');
104168ff6f3Sric2016    }
105168ff6f3Sric2016
106168ff6f3Sric2016    /**
107377a2979SGreg Roach     * CSS class for the URL.
108377a2979SGreg Roach     *
109377a2979SGreg Roach     * @return string
110377a2979SGreg Roach     */
111377a2979SGreg Roach    public function chartMenuClass(): string
112377a2979SGreg Roach    {
113377a2979SGreg Roach        return 'menu-chart-statistics';
114377a2979SGreg Roach    }
115377a2979SGreg Roach
116377a2979SGreg Roach    /**
117e6562982SGreg Roach     * The URL for this chart.
118168ff6f3Sric2016     *
11960bc3e3fSGreg Roach     * @param Individual $individual
120e6562982SGreg Roach     * @param string[]   $parameters
12160bc3e3fSGreg Roach     *
122e6562982SGreg Roach     * @return string
123168ff6f3Sric2016     */
124e6562982SGreg Roach    public function chartUrl(Individual $individual, array $parameters = []): string
125c1010edaSGreg Roach    {
126c6266a77SGreg Roach        return route('module', [
127c6266a77SGreg Roach                'module'  => $this->name(),
128c6266a77SGreg Roach                'action'  => 'Chart',
129e6562982SGreg Roach                'ged'     => $individual->tree()->name(),
130e6562982SGreg Roach            ] + $parameters);
131168ff6f3Sric2016    }
132c6266a77SGreg Roach
133c6266a77SGreg Roach    /**
134c6266a77SGreg Roach     * A form to request the chart parameters.
135c6266a77SGreg Roach     *
13657ab2231SGreg Roach     * @param ServerRequestInterface $request
137c6266a77SGreg Roach     *
1386ccdf4f0SGreg Roach     * @return ResponseInterface
139c6266a77SGreg Roach     */
14057ab2231SGreg Roach    public function getChartAction(ServerRequestInterface $request): ResponseInterface
141c6266a77SGreg Roach    {
14257ab2231SGreg Roach        $tree = $request->getAttribute('tree');
14357ab2231SGreg Roach        $user = $request->getAttribute('user');
14457ab2231SGreg Roach
1459867b2f0SGreg Roach        Auth::checkComponentAccess($this, 'chart', $tree, $user);
1469867b2f0SGreg Roach
147c6266a77SGreg Roach        $tabs = [
148c6266a77SGreg Roach            I18N::translate('Individuals') => route('module', [
149c6266a77SGreg Roach                'module'  => $this->name(),
150c6266a77SGreg Roach                'action'  => 'Individuals',
151c6266a77SGreg Roach                'ged'     => $tree->name()
152c6266a77SGreg Roach            ]),
153c6266a77SGreg Roach            I18N::translate('Families') => route('module', [
154c6266a77SGreg Roach                'module'  => $this->name(),
155c6266a77SGreg Roach                'action'  => 'Families',
156c6266a77SGreg Roach                'ged'     => $tree->name()
157c6266a77SGreg Roach            ]),
158c6266a77SGreg Roach            I18N::translate('Other') => route('module', [
159c6266a77SGreg Roach                'module'  => $this->name(),
160c6266a77SGreg Roach                'action'  => 'Other',
161c6266a77SGreg Roach                'ged'     => $tree->name()
162c6266a77SGreg Roach            ]),
163c6266a77SGreg Roach            I18N::translate('Custom') => route('module', [
164c6266a77SGreg Roach                'module'  => $this->name(),
165c6266a77SGreg Roach                'action'  => 'Custom',
166c6266a77SGreg Roach                'ged'     => $tree->name()
167c6266a77SGreg Roach            ]),
168c6266a77SGreg Roach        ];
169c6266a77SGreg Roach
1709b5537c3SGreg Roach        return $this->viewResponse('modules/statistics-chart/page', [
171c6266a77SGreg Roach            'tabs'  => $tabs,
172c6266a77SGreg Roach            'title' => $this->title(),
173c6266a77SGreg Roach        ]);
174c6266a77SGreg Roach    }
175c6266a77SGreg Roach
176c6266a77SGreg Roach    /**
17757ab2231SGreg Roach     * @param ServerRequestInterface $request
178c6266a77SGreg Roach     *
1796ccdf4f0SGreg Roach     * @return ResponseInterface
180c6266a77SGreg Roach     */
18157ab2231SGreg Roach    public function getIndividualsAction(ServerRequestInterface $request): ResponseInterface
182c6266a77SGreg Roach    {
183b6c326d8SGreg Roach        $this->layout = 'layouts/ajax';
184b6c326d8SGreg Roach
185b6c326d8SGreg Roach        return $this->viewResponse('modules/statistics-chart/individuals', [
186c6266a77SGreg Roach            'show_oldest_living' => Auth::check(),
18757ab2231SGreg Roach            'stats'              => app(Statistics::class),
188c6266a77SGreg Roach        ]);
189c6266a77SGreg Roach    }
190c6266a77SGreg Roach
191c6266a77SGreg Roach    /**
19257ab2231SGreg Roach     * @param ServerRequestInterface $request
193c6266a77SGreg Roach     *
1946ccdf4f0SGreg Roach     * @return ResponseInterface
195c6266a77SGreg Roach     */
19657ab2231SGreg Roach    public function getFamiliesAction(ServerRequestInterface $request): ResponseInterface
197c6266a77SGreg Roach    {
198b6c326d8SGreg Roach        $this->layout = 'layouts/ajax';
199b6c326d8SGreg Roach
200b6c326d8SGreg Roach        return $this->viewResponse('modules/statistics-chart/families', [
20157ab2231SGreg Roach            'stats' => app(Statistics::class),
202c6266a77SGreg Roach        ]);
203c6266a77SGreg Roach    }
204c6266a77SGreg Roach
205c6266a77SGreg Roach    /**
20657ab2231SGreg Roach     * @param ServerRequestInterface $request
207c6266a77SGreg Roach     *
2086ccdf4f0SGreg Roach     * @return ResponseInterface
209c6266a77SGreg Roach     */
21057ab2231SGreg Roach    public function getOtherAction(ServerRequestInterface $request): ResponseInterface
211c6266a77SGreg Roach    {
212b6c326d8SGreg Roach        $this->layout = 'layouts/ajax';
213b6c326d8SGreg Roach
214b6c326d8SGreg Roach        return $this->viewResponse('modules/statistics-chart/other', [
21557ab2231SGreg Roach            'stats' => app(Statistics::class),
216c6266a77SGreg Roach        ]);
217c6266a77SGreg Roach    }
218c6266a77SGreg Roach
219c6266a77SGreg Roach    /**
22057ab2231SGreg Roach     * @param ServerRequestInterface $request
221c6266a77SGreg Roach     *
2226ccdf4f0SGreg Roach     * @return ResponseInterface
223c6266a77SGreg Roach     */
22457ab2231SGreg Roach    public function getCustomAction(ServerRequestInterface $request): ResponseInterface
225c6266a77SGreg Roach    {
226b6c326d8SGreg Roach        $this->layout = 'layouts/ajax';
227b6c326d8SGreg Roach
22857ab2231SGreg Roach        $tree = $request->getAttribute('tree');
22957ab2231SGreg Roach
230b6c326d8SGreg Roach        return $this->viewResponse('modules/statistics-chart/custom', [
231c6266a77SGreg Roach            'module' => $this,
232c6266a77SGreg Roach            'tree'   => $tree,
233c6266a77SGreg Roach        ]);
234c6266a77SGreg Roach    }
235c6266a77SGreg Roach
236c6266a77SGreg Roach    /**
2376ccdf4f0SGreg Roach     * @param ServerRequestInterface $request
238c6266a77SGreg Roach     *
2396ccdf4f0SGreg Roach     * @return ResponseInterface
240c6266a77SGreg Roach     */
24157ab2231SGreg Roach    public function getCustomChartAction(ServerRequestInterface $request): ResponseInterface
242c6266a77SGreg Roach    {
24357ab2231SGreg Roach        $statistics = app(Statistics::class);
24457ab2231SGreg Roach
245b6b9dcc9SGreg Roach        $params = $request->getQueryParams();
246b6b9dcc9SGreg Roach
247b6b9dcc9SGreg Roach        $x_axis_type = (int) $params['x-as'];
248b6b9dcc9SGreg Roach        $y_axis_type = (int) $params['y-as'];
249b6b9dcc9SGreg Roach        $z_axis_type = (int) $params['z-as'];
250c6266a77SGreg Roach        $ydata       = [];
251c6266a77SGreg Roach
252c6266a77SGreg Roach        switch ($x_axis_type) {
253c6266a77SGreg Roach            case self::X_AXIS_INDIVIDUAL_MAP:
2546ccdf4f0SGreg Roach                return response($statistics->chartDistribution(
255b6b9dcc9SGreg Roach                    $params['chart_shows'],
256b6b9dcc9SGreg Roach                    $params['chart_type'],
257b6b9dcc9SGreg Roach                    $params['SURN']
258c6266a77SGreg Roach                ));
259c6266a77SGreg Roach
260c6266a77SGreg Roach            case self::X_AXIS_BIRTH_MAP:
2616ccdf4f0SGreg Roach                return response($statistics->chartDistribution(
262b6b9dcc9SGreg Roach                    $params['chart_shows'],
263c6266a77SGreg Roach                    'birth_distribution_chart'
264c6266a77SGreg Roach                ));
265c6266a77SGreg Roach
266c6266a77SGreg Roach            case self::X_AXIS_DEATH_MAP:
2676ccdf4f0SGreg Roach                return response($statistics->chartDistribution(
268b6b9dcc9SGreg Roach                    $params['chart_shows'],
269c6266a77SGreg Roach                    'death_distribution_chart'
270c6266a77SGreg Roach                ));
271c6266a77SGreg Roach
272c6266a77SGreg Roach            case self::X_AXIS_MARRIAGE_MAP:
2736ccdf4f0SGreg Roach                return response($statistics->chartDistribution(
274b6b9dcc9SGreg Roach                    $params['chart_shows'],
275c6266a77SGreg Roach                    'marriage_distribution_chart'
276c6266a77SGreg Roach                ));
277c6266a77SGreg Roach
278c6266a77SGreg Roach            case self::X_AXIS_BIRTH_MONTH:
279c6266a77SGreg Roach                $chart_title  = I18N::translate('Month of birth');
280c6266a77SGreg Roach                $x_axis_title = I18N::translate('Month');
281c6266a77SGreg Roach                $x_axis       = $this->axisMonths();
282c6266a77SGreg Roach
283c6266a77SGreg Roach                switch ($y_axis_type) {
284c6266a77SGreg Roach                    case self::Y_AXIS_NUMBERS:
285c6266a77SGreg Roach                        $y_axis_title = I18N::translate('Individuals');
286c6266a77SGreg Roach                        break;
287c6266a77SGreg Roach                    case self::Y_AXIS_PERCENT:
288c6266a77SGreg Roach                        $y_axis_title = '%';
289c6266a77SGreg Roach                        break;
290c6266a77SGreg Roach                    default:
291c6266a77SGreg Roach                        throw new NotFoundHttpException();
292c6266a77SGreg Roach                }
293c6266a77SGreg Roach
294c6266a77SGreg Roach                switch ($z_axis_type) {
295c6266a77SGreg Roach                    case self::Z_AXIS_ALL:
296c6266a77SGreg Roach                        $z_axis = $this->axisAll();
297cde1d378SGreg Roach                        $rows   = $statistics->statsBirthQuery()->get();
298c6266a77SGreg Roach                        foreach ($rows as $row) {
299c6266a77SGreg Roach                            $this->fillYData($row->d_month, 0, $row->total, $x_axis, $z_axis, $ydata);
300c6266a77SGreg Roach                        }
301c6266a77SGreg Roach                        break;
302c6266a77SGreg Roach                    case self::Z_AXIS_SEX:
303c6266a77SGreg Roach                        $z_axis = $this->axisSexes();
304cde1d378SGreg Roach                        $rows = $statistics->statsBirthBySexQuery()->get();
305c6266a77SGreg Roach                        foreach ($rows as $row) {
306c6266a77SGreg Roach                            $this->fillYData($row->d_month, $row->i_sex, $row->total, $x_axis, $z_axis, $ydata);
307c6266a77SGreg Roach                        }
308c6266a77SGreg Roach                        break;
309c6266a77SGreg Roach                    case self::Z_AXIS_TIME:
310b6b9dcc9SGreg Roach                        $boundaries_csv = $params['z-axis-boundaries-periods'];
311c6266a77SGreg Roach                        $z_axis         = $this->axisYears($boundaries_csv);
312c6266a77SGreg Roach                        $prev_boundary  = 0;
313c6266a77SGreg Roach                        foreach (array_keys($z_axis) as $boundary) {
314cde1d378SGreg Roach                            $rows = $statistics->statsBirthQuery($prev_boundary, $boundary)->get();
315c6266a77SGreg Roach                            foreach ($rows as $row) {
316c6266a77SGreg Roach                                $this->fillYData($row->d_month, $boundary, $row->total, $x_axis, $z_axis, $ydata);
317c6266a77SGreg Roach                            }
318c6266a77SGreg Roach                            $prev_boundary = $boundary + 1;
319c6266a77SGreg Roach                        }
320c6266a77SGreg Roach                        break;
321c6266a77SGreg Roach                    default:
322c6266a77SGreg Roach                        throw new NotFoundHttpException();
323c6266a77SGreg Roach                }
324c6266a77SGreg Roach
3256ccdf4f0SGreg Roach                return response($this->myPlot($chart_title, $x_axis, $x_axis_title, $ydata, $y_axis_title, $z_axis, $y_axis_type));
326c6266a77SGreg Roach
327c6266a77SGreg Roach            case self::X_AXIS_DEATH_MONTH:
328c6266a77SGreg Roach                $chart_title  = I18N::translate('Month of death');
329c6266a77SGreg Roach                $x_axis_title = I18N::translate('Month');
330c6266a77SGreg Roach                $x_axis       = $this->axisMonths();
331c6266a77SGreg Roach
332c6266a77SGreg Roach                switch ($y_axis_type) {
333c6266a77SGreg Roach                    case self::Y_AXIS_NUMBERS:
334c6266a77SGreg Roach                        $y_axis_title = I18N::translate('Individuals');
335c6266a77SGreg Roach                        break;
336c6266a77SGreg Roach                    case self::Y_AXIS_PERCENT:
337c6266a77SGreg Roach                        $y_axis_title = '%';
338c6266a77SGreg Roach                        break;
339c6266a77SGreg Roach                    default:
340c6266a77SGreg Roach                        throw new NotFoundHttpException();
341c6266a77SGreg Roach                }
342c6266a77SGreg Roach
343c6266a77SGreg Roach                switch ($z_axis_type) {
344c6266a77SGreg Roach                    case self::Z_AXIS_ALL:
345c6266a77SGreg Roach                        $z_axis = $this->axisAll();
346cde1d378SGreg Roach                        $rows   = $statistics->statsDeathQuery()->get();
347c6266a77SGreg Roach                        foreach ($rows as $row) {
348c6266a77SGreg Roach                            $this->fillYData($row->d_month, 0, $row->total, $x_axis, $z_axis, $ydata);
349c6266a77SGreg Roach                        }
350c6266a77SGreg Roach                        break;
351c6266a77SGreg Roach                    case self::Z_AXIS_SEX:
352c6266a77SGreg Roach                        $z_axis = $this->axisSexes();
353cde1d378SGreg Roach                        $rows = $statistics->statsDeathBySexQuery()->get();
354c6266a77SGreg Roach                        foreach ($rows as $row) {
355c6266a77SGreg Roach                            $this->fillYData($row->d_month, $row->i_sex, $row->total, $x_axis, $z_axis, $ydata);
356c6266a77SGreg Roach                        }
357c6266a77SGreg Roach                        break;
358c6266a77SGreg Roach                    case self::Z_AXIS_TIME:
359b6b9dcc9SGreg Roach                        $boundaries_csv = $params['z-axis-boundaries-periods'];
360c6266a77SGreg Roach                        $z_axis         = $this->axisYears($boundaries_csv);
361c6266a77SGreg Roach                        $prev_boundary  = 0;
362c6266a77SGreg Roach                        foreach (array_keys($z_axis) as $boundary) {
363cde1d378SGreg Roach                            $rows = $statistics->statsDeathQuery($prev_boundary, $boundary)->get();
364c6266a77SGreg Roach                            foreach ($rows as $row) {
365c6266a77SGreg Roach                                $this->fillYData($row->d_month, $boundary, $row->total, $x_axis, $z_axis, $ydata);
366c6266a77SGreg Roach                            }
367c6266a77SGreg Roach                            $prev_boundary = $boundary + 1;
368c6266a77SGreg Roach                        }
369c6266a77SGreg Roach                        break;
370c6266a77SGreg Roach                    default:
371c6266a77SGreg Roach                        throw new NotFoundHttpException();
372c6266a77SGreg Roach                }
373c6266a77SGreg Roach
3746ccdf4f0SGreg Roach                return response($this->myPlot($chart_title, $x_axis, $x_axis_title, $ydata, $y_axis_title, $z_axis, $y_axis_type));
375c6266a77SGreg Roach
376c6266a77SGreg Roach            case self::X_AXIS_MARRIAGE_MONTH:
377c6266a77SGreg Roach                $chart_title  = I18N::translate('Month of marriage');
378c6266a77SGreg Roach                $x_axis_title = I18N::translate('Month');
379c6266a77SGreg Roach                $x_axis       = $this->axisMonths();
380c6266a77SGreg Roach
381c6266a77SGreg Roach                switch ($y_axis_type) {
382c6266a77SGreg Roach                    case self::Y_AXIS_NUMBERS:
383c6266a77SGreg Roach                        $y_axis_title = I18N::translate('Families');
384c6266a77SGreg Roach                        break;
385c6266a77SGreg Roach                    case self::Y_AXIS_PERCENT:
386c6266a77SGreg Roach                        $y_axis_title = '%';
387c6266a77SGreg Roach                        break;
388c6266a77SGreg Roach                    default:
389c6266a77SGreg Roach                        throw new NotFoundHttpException();
390c6266a77SGreg Roach                }
391c6266a77SGreg Roach
392c6266a77SGreg Roach                switch ($z_axis_type) {
393c6266a77SGreg Roach                    case self::Z_AXIS_ALL:
394c6266a77SGreg Roach                        $z_axis = $this->axisAll();
395e6f3d5e2SGreg Roach                        $rows   = $statistics->statsMarriageQuery()->get();
396c6266a77SGreg Roach                        foreach ($rows as $row) {
397c6266a77SGreg Roach                            $this->fillYData($row->d_month, 0, $row->total, $x_axis, $z_axis, $ydata);
398c6266a77SGreg Roach                        }
399c6266a77SGreg Roach                        break;
400c6266a77SGreg Roach                    case self::Z_AXIS_TIME:
401b6b9dcc9SGreg Roach                        $boundaries_csv = $params['z-axis-boundaries-periods'];
402c6266a77SGreg Roach                        $z_axis         = $this->axisYears($boundaries_csv);
403c6266a77SGreg Roach                        $prev_boundary  = 0;
404c6266a77SGreg Roach                        foreach (array_keys($z_axis) as $boundary) {
405e6f3d5e2SGreg Roach                            $rows = $statistics->statsMarriageQuery($prev_boundary, $boundary)->get();
406c6266a77SGreg Roach                            foreach ($rows as $row) {
407c6266a77SGreg Roach                                $this->fillYData($row->d_month, $boundary, $row->total, $x_axis, $z_axis, $ydata);
408c6266a77SGreg Roach                            }
409c6266a77SGreg Roach                            $prev_boundary = $boundary + 1;
410c6266a77SGreg Roach                        }
411c6266a77SGreg Roach                        break;
412c6266a77SGreg Roach                    default:
413c6266a77SGreg Roach                        throw new NotFoundHttpException();
414c6266a77SGreg Roach                }
415c6266a77SGreg Roach
4166ccdf4f0SGreg Roach                return response($this->myPlot($chart_title, $x_axis, $x_axis_title, $ydata, $y_axis_title, $z_axis, $y_axis_type));
417c6266a77SGreg Roach
418c6266a77SGreg Roach            case self::X_AXIS_FIRST_CHILD_MONTH:
419c6266a77SGreg Roach                $chart_title  = I18N::translate('Month of birth of first child in a relation');
420c6266a77SGreg Roach                $x_axis_title = I18N::translate('Month');
421c6266a77SGreg Roach                $x_axis       = $this->axisMonths();
422c6266a77SGreg Roach
423c6266a77SGreg Roach                switch ($y_axis_type) {
424c6266a77SGreg Roach                    case self::Y_AXIS_NUMBERS:
425c6266a77SGreg Roach                        $y_axis_title = I18N::translate('Children');
426c6266a77SGreg Roach                        break;
427c6266a77SGreg Roach                    case self::Y_AXIS_PERCENT:
428c6266a77SGreg Roach                        $y_axis_title = '%';
429c6266a77SGreg Roach                        break;
430c6266a77SGreg Roach                    default:
431c6266a77SGreg Roach                        throw new NotFoundHttpException();
432c6266a77SGreg Roach                }
433c6266a77SGreg Roach
434c6266a77SGreg Roach                switch ($z_axis_type) {
435c6266a77SGreg Roach                    case self::Z_AXIS_ALL:
436c6266a77SGreg Roach                        $z_axis = $this->axisAll();
437999da590SGreg Roach                        $rows   = $statistics->monthFirstChildQuery()->get();
438c6266a77SGreg Roach                        foreach ($rows as $row) {
439c6266a77SGreg Roach                            $this->fillYData($row->d_month, 0, $row->total, $x_axis, $z_axis, $ydata);
440c6266a77SGreg Roach                        }
441c6266a77SGreg Roach                        break;
442c6266a77SGreg Roach                    case self::Z_AXIS_SEX:
443c6266a77SGreg Roach                        $z_axis = $this->axisSexes();
444999da590SGreg Roach                        $rows = $statistics->monthFirstChildBySexQuery()->get();
445c6266a77SGreg Roach                        foreach ($rows as $row) {
446c6266a77SGreg Roach                            $this->fillYData($row->d_month, $row->i_sex, $row->total, $x_axis, $z_axis, $ydata);
447c6266a77SGreg Roach                        }
448c6266a77SGreg Roach                        break;
449c6266a77SGreg Roach                    case self::Z_AXIS_TIME:
450b6b9dcc9SGreg Roach                        $boundaries_csv = $params['z-axis-boundaries-periods'];
451c6266a77SGreg Roach                        $z_axis         = $this->axisYears($boundaries_csv);
452c6266a77SGreg Roach                        $prev_boundary  = 0;
453c6266a77SGreg Roach                        foreach (array_keys($z_axis) as $boundary) {
454999da590SGreg Roach                            $rows = $statistics->monthFirstChildQuery($prev_boundary, $boundary)->get();
455c6266a77SGreg Roach                            foreach ($rows as $row) {
456c6266a77SGreg Roach                                $this->fillYData($row->d_month, $boundary, $row->total, $x_axis, $z_axis, $ydata);
457c6266a77SGreg Roach                            }
458c6266a77SGreg Roach                            $prev_boundary = $boundary + 1;
459c6266a77SGreg Roach                        }
460c6266a77SGreg Roach                        break;
461c6266a77SGreg Roach                    default:
462c6266a77SGreg Roach                        throw new NotFoundHttpException();
463c6266a77SGreg Roach                }
464c6266a77SGreg Roach
4656ccdf4f0SGreg Roach                return response($this->myPlot($chart_title, $x_axis, $x_axis_title, $ydata, $y_axis_title, $z_axis, $y_axis_type));
466c6266a77SGreg Roach
467c6266a77SGreg Roach            case self::X_AXIS_FIRST_MARRIAGE_MONTH:
468c6266a77SGreg Roach                $chart_title  = I18N::translate('Month of first marriage');
469c6266a77SGreg Roach                $x_axis_title = I18N::translate('Month');
470c6266a77SGreg Roach                $x_axis       = $this->axisMonths();
471c6266a77SGreg Roach
472c6266a77SGreg Roach                switch ($y_axis_type) {
473c6266a77SGreg Roach                    case self::Y_AXIS_NUMBERS:
474c6266a77SGreg Roach                        $y_axis_title = I18N::translate('Families');
475c6266a77SGreg Roach                        break;
476c6266a77SGreg Roach                    case self::Y_AXIS_PERCENT:
477c6266a77SGreg Roach                        $y_axis_title = '%';
478c6266a77SGreg Roach                        break;
479c6266a77SGreg Roach                    default:
480c6266a77SGreg Roach                        throw new NotFoundHttpException();
481c6266a77SGreg Roach                }
482c6266a77SGreg Roach
483c6266a77SGreg Roach                switch ($z_axis_type) {
484c6266a77SGreg Roach                    case self::Z_AXIS_ALL:
485c6266a77SGreg Roach                        $z_axis = $this->axisAll();
486e6f3d5e2SGreg Roach                        $rows   = $statistics->statsFirstMarriageQuery()->get();
487c6266a77SGreg Roach                        $indi   = [];
488c6266a77SGreg Roach                        $fam    = [];
489c6266a77SGreg Roach                        foreach ($rows as $row) {
4906960d4a1SGreg Roach                            if (!in_array($row->indi, $indi, true) && !in_array($row->fams, $fam, true)) {
491c6266a77SGreg Roach                                $this->fillYData($row->month, 0, 1, $x_axis, $z_axis, $ydata);
492c6266a77SGreg Roach                            }
493c6266a77SGreg Roach                            $indi[] = $row->indi;
494c6266a77SGreg Roach                            $fam[]  = $row->fams;
495c6266a77SGreg Roach                        }
496c6266a77SGreg Roach                        break;
497c6266a77SGreg Roach                    case self::Z_AXIS_TIME:
498b6b9dcc9SGreg Roach                        $boundaries_csv = $params['z-axis-boundaries-periods'];
499c6266a77SGreg Roach                        $z_axis         = $this->axisYears($boundaries_csv);
500c6266a77SGreg Roach                        $prev_boundary  = 0;
501c6266a77SGreg Roach                        $indi           = [];
502c6266a77SGreg Roach                        $fam            = [];
503c6266a77SGreg Roach                        foreach (array_keys($z_axis) as $boundary) {
504e6f3d5e2SGreg Roach                            $rows = $statistics->statsFirstMarriageQuery($prev_boundary, $boundary)->get();
505c6266a77SGreg Roach                            foreach ($rows as $row) {
5066960d4a1SGreg Roach                                if (!in_array($row->indi, $indi, true) && !in_array($row->fams, $fam, true)) {
507c6266a77SGreg Roach                                    $this->fillYData($row->month, $boundary, 1, $x_axis, $z_axis, $ydata);
508c6266a77SGreg Roach                                }
509c6266a77SGreg Roach                                $indi[] = $row->indi;
510c6266a77SGreg Roach                                $fam[]  = $row->fams;
511c6266a77SGreg Roach                            }
512c6266a77SGreg Roach                            $prev_boundary = $boundary + 1;
513c6266a77SGreg Roach                        }
514c6266a77SGreg Roach                        break;
515c6266a77SGreg Roach                    default:
516c6266a77SGreg Roach                        throw new NotFoundHttpException();
517c6266a77SGreg Roach                }
518c6266a77SGreg Roach
5196ccdf4f0SGreg Roach                return response($this->myPlot($chart_title, $x_axis, $x_axis_title, $ydata, $y_axis_title, $z_axis, $y_axis_type));
520c6266a77SGreg Roach
521c6266a77SGreg Roach            case self::X_AXIS_AGE_AT_DEATH:
522c6266a77SGreg Roach                $chart_title    = I18N::translate('Average age at death');
523c6266a77SGreg Roach                $x_axis_title   = I18N::translate('age');
524b6b9dcc9SGreg Roach                $boundaries_csv = $params['x-axis-boundaries-ages'];
525c6266a77SGreg Roach                $x_axis         = $this->axisNumbers($boundaries_csv);
526c6266a77SGreg Roach
527c6266a77SGreg Roach                switch ($y_axis_type) {
528c6266a77SGreg Roach                    case self::Y_AXIS_NUMBERS:
529c6266a77SGreg Roach                        $y_axis_title = I18N::translate('Individuals');
530c6266a77SGreg Roach                        break;
531c6266a77SGreg Roach                    case self::Y_AXIS_PERCENT:
532c6266a77SGreg Roach                        $y_axis_title = '%';
533c6266a77SGreg Roach                        break;
534c6266a77SGreg Roach                    default:
535c6266a77SGreg Roach                        throw new NotFoundHttpException();
536c6266a77SGreg Roach                }
537c6266a77SGreg Roach
538c6266a77SGreg Roach                switch ($z_axis_type) {
539c6266a77SGreg Roach                    case self::Z_AXIS_ALL:
540c6266a77SGreg Roach                        $z_axis = $this->axisAll();
5419219296aSGreg Roach                        $rows = $statistics->statsAgeQuery('DEAT');
542c6266a77SGreg Roach                        foreach ($rows as $row) {
543c6266a77SGreg Roach                            foreach ($row as $age) {
544c6266a77SGreg Roach                                $years = (int) ($age / self::DAYS_IN_YEAR);
545c6266a77SGreg Roach                                $this->fillYData($years, 0, 1, $x_axis, $z_axis, $ydata);
546c6266a77SGreg Roach                            }
547c6266a77SGreg Roach                        }
548c6266a77SGreg Roach                        break;
549c6266a77SGreg Roach                    case self::Z_AXIS_SEX:
550c6266a77SGreg Roach                        $z_axis = $this->axisSexes();
551c6266a77SGreg Roach                        foreach (array_keys($z_axis) as $sex) {
5529219296aSGreg Roach                            $rows = $statistics->statsAgeQuery('DEAT', $sex);
553c6266a77SGreg Roach                            foreach ($rows as $row) {
554c6266a77SGreg Roach                                foreach ($row as $age) {
555c6266a77SGreg Roach                                    $years = (int) ($age / self::DAYS_IN_YEAR);
556c6266a77SGreg Roach                                    $this->fillYData($years, $sex, 1, $x_axis, $z_axis, $ydata);
557c6266a77SGreg Roach                                }
558c6266a77SGreg Roach                            }
559c6266a77SGreg Roach                        }
560c6266a77SGreg Roach                        break;
561c6266a77SGreg Roach                    case self::Z_AXIS_TIME:
562b6b9dcc9SGreg Roach                        $boundaries_csv = $params['z-axis-boundaries-periods'];
563c6266a77SGreg Roach                        $z_axis         = $this->axisYears($boundaries_csv);
564c6266a77SGreg Roach                        $prev_boundary  = 0;
565c6266a77SGreg Roach                        foreach (array_keys($z_axis) as $boundary) {
5669219296aSGreg Roach                            $rows = $statistics->statsAgeQuery('DEAT', 'BOTH', $prev_boundary, $boundary);
567c6266a77SGreg Roach                            foreach ($rows as $row) {
568c6266a77SGreg Roach                                foreach ($row as $age) {
569c6266a77SGreg Roach                                    $years = (int) ($age / self::DAYS_IN_YEAR);
570c6266a77SGreg Roach                                    $this->fillYData($years, $boundary, 1, $x_axis, $z_axis, $ydata);
571c6266a77SGreg Roach                                }
572c6266a77SGreg Roach                            }
573c6266a77SGreg Roach                            $prev_boundary = $boundary + 1;
574c6266a77SGreg Roach                        }
575c6266a77SGreg Roach
576c6266a77SGreg Roach                        break;
577c6266a77SGreg Roach                    default:
578c6266a77SGreg Roach                        throw new NotFoundHttpException();
579c6266a77SGreg Roach                }
580c6266a77SGreg Roach
5816ccdf4f0SGreg Roach                return response($this->myPlot($chart_title, $x_axis, $x_axis_title, $ydata, $y_axis_title, $z_axis, $y_axis_type));
582c6266a77SGreg Roach
583c6266a77SGreg Roach            case self::X_AXIS_AGE_AT_MARRIAGE:
584c6266a77SGreg Roach                $chart_title    = I18N::translate('Age in year of marriage');
585c6266a77SGreg Roach                $x_axis_title   = I18N::translate('age');
586b6b9dcc9SGreg Roach                $boundaries_csv = $params['x-axis-boundaries-ages_m'];
587c6266a77SGreg Roach                $x_axis         = $this->axisNumbers($boundaries_csv);
588c6266a77SGreg Roach
589c6266a77SGreg Roach                switch ($y_axis_type) {
590c6266a77SGreg Roach                    case self::Y_AXIS_NUMBERS:
591c6266a77SGreg Roach                        $y_axis_title = I18N::translate('Individuals');
592c6266a77SGreg Roach                        break;
593c6266a77SGreg Roach                    case self::Y_AXIS_PERCENT:
594c6266a77SGreg Roach                        $y_axis_title = '%';
595c6266a77SGreg Roach                        break;
596c6266a77SGreg Roach                    default:
597c6266a77SGreg Roach                        throw new NotFoundHttpException();
598c6266a77SGreg Roach                }
599c6266a77SGreg Roach
600c6266a77SGreg Roach                switch ($z_axis_type) {
601c6266a77SGreg Roach                    case self::Z_AXIS_ALL:
602c6266a77SGreg Roach                        $z_axis = $this->axisAll();
603afa8d404SGreg Roach                        // The stats query doesn't have an "all" function, so query M/F separately
604afa8d404SGreg Roach                        foreach (['M', 'F'] as $sex) {
6059219296aSGreg Roach                            $rows = $statistics->statsMarrAgeQuery($sex);
606c6266a77SGreg Roach                            foreach ($rows as $row) {
607c6266a77SGreg Roach                                $years = (int) ($row->age / self::DAYS_IN_YEAR);
608c6266a77SGreg Roach                                $this->fillYData($years, 0, 1, $x_axis, $z_axis, $ydata);
609c6266a77SGreg Roach                            }
610c6266a77SGreg Roach                        }
611c6266a77SGreg Roach                        break;
612c6266a77SGreg Roach                    case self::Z_AXIS_SEX:
613c6266a77SGreg Roach                        $z_axis = $this->axisSexes();
614c6266a77SGreg Roach                        foreach (array_keys($z_axis) as $sex) {
6159219296aSGreg Roach                            $rows = $statistics->statsMarrAgeQuery($sex);
616c6266a77SGreg Roach                            foreach ($rows as $row) {
617c6266a77SGreg Roach                                $years = (int) ($row->age / self::DAYS_IN_YEAR);
618c6266a77SGreg Roach                                $this->fillYData($years, $sex, 1, $x_axis, $z_axis, $ydata);
619c6266a77SGreg Roach                            }
620c6266a77SGreg Roach                        }
621c6266a77SGreg Roach                        break;
622c6266a77SGreg Roach                    case self::Z_AXIS_TIME:
623b6b9dcc9SGreg Roach                        $boundaries_csv = $params['z-axis-boundaries-periods'];
624c6266a77SGreg Roach                        $z_axis         = $this->axisYears($boundaries_csv);
625afa8d404SGreg Roach                        // The stats query doesn't have an "all" function, so query M/F separately
626afa8d404SGreg Roach                        foreach (['M', 'F'] as $sex) {
627c6266a77SGreg Roach                            $prev_boundary = 0;
628c6266a77SGreg Roach                            foreach (array_keys($z_axis) as $boundary) {
6299219296aSGreg Roach                                $rows = $statistics->statsMarrAgeQuery($sex, $prev_boundary, $boundary);
630c6266a77SGreg Roach                                foreach ($rows as $row) {
631c6266a77SGreg Roach                                    $years = (int) ($row->age / self::DAYS_IN_YEAR);
632c6266a77SGreg Roach                                    $this->fillYData($years, $boundary, 1, $x_axis, $z_axis, $ydata);
633c6266a77SGreg Roach                                }
634c6266a77SGreg Roach                                $prev_boundary = $boundary + 1;
635c6266a77SGreg Roach                            }
636c6266a77SGreg Roach                        }
637c6266a77SGreg Roach                        break;
638c6266a77SGreg Roach                    default:
639c6266a77SGreg Roach                        throw new NotFoundHttpException();
640c6266a77SGreg Roach                }
641c6266a77SGreg Roach
6426ccdf4f0SGreg Roach                return response($this->myPlot($chart_title, $x_axis, $x_axis_title, $ydata, $y_axis_title, $z_axis, $y_axis_type));
643c6266a77SGreg Roach
644c6266a77SGreg Roach            case self::X_AXIS_AGE_AT_FIRST_MARRIAGE:
645c6266a77SGreg Roach                $chart_title    = I18N::translate('Age in year of first marriage');
646c6266a77SGreg Roach                $x_axis_title   = I18N::translate('age');
647b6b9dcc9SGreg Roach                $boundaries_csv = $params['x-axis-boundaries-ages_m'];
648c6266a77SGreg Roach                $x_axis         = $this->axisNumbers($boundaries_csv);
649c6266a77SGreg Roach
650c6266a77SGreg Roach                switch ($y_axis_type) {
651c6266a77SGreg Roach                    case self::Y_AXIS_NUMBERS:
652c6266a77SGreg Roach                        $y_axis_title = I18N::translate('Individuals');
653c6266a77SGreg Roach                        break;
654c6266a77SGreg Roach                    case self::Y_AXIS_PERCENT:
655c6266a77SGreg Roach                        $y_axis_title = '%';
656c6266a77SGreg Roach                        break;
657c6266a77SGreg Roach                    default:
658c6266a77SGreg Roach                        throw new NotFoundHttpException();
659c6266a77SGreg Roach                }
660c6266a77SGreg Roach
661c6266a77SGreg Roach                switch ($z_axis_type) {
662c6266a77SGreg Roach                    case self::Z_AXIS_ALL:
663c6266a77SGreg Roach                        $z_axis = $this->axisAll();
664afa8d404SGreg Roach                        // The stats query doesn't have an "all" function, so query M/F separately
665afa8d404SGreg Roach                        foreach (['M', 'F'] as $sex) {
6669219296aSGreg Roach                            $rows = $statistics->statsMarrAgeQuery($sex);
667c6266a77SGreg Roach                            $indi = [];
668c6266a77SGreg Roach                            foreach ($rows as $row) {
6696960d4a1SGreg Roach                                if (!in_array($row->d_gid, $indi, true)) {
670c6266a77SGreg Roach                                    $years = (int) ($row->age / self::DAYS_IN_YEAR);
671c6266a77SGreg Roach                                    $this->fillYData($years, 0, 1, $x_axis, $z_axis, $ydata);
672c6266a77SGreg Roach                                    $indi[] = $row->d_gid;
673c6266a77SGreg Roach                                }
674c6266a77SGreg Roach                            }
675c6266a77SGreg Roach                        }
676c6266a77SGreg Roach                        break;
677c6266a77SGreg Roach                    case self::Z_AXIS_SEX:
678c6266a77SGreg Roach                        $z_axis = $this->axisSexes();
679c6266a77SGreg Roach                        foreach (array_keys($z_axis) as $sex) {
6809219296aSGreg Roach                            $rows = $statistics->statsMarrAgeQuery($sex);
681c6266a77SGreg Roach                            $indi = [];
682c6266a77SGreg Roach                            foreach ($rows as $row) {
6836960d4a1SGreg Roach                                if (!in_array($row->d_gid, $indi, true)) {
684c6266a77SGreg Roach                                    $years = (int) ($row->age / self::DAYS_IN_YEAR);
685c6266a77SGreg Roach                                    $this->fillYData($years, $sex, 1, $x_axis, $z_axis, $ydata);
686c6266a77SGreg Roach                                    $indi[] = $row->d_gid;
687c6266a77SGreg Roach                                }
688c6266a77SGreg Roach                            }
689c6266a77SGreg Roach                        }
690c6266a77SGreg Roach                        break;
691c6266a77SGreg Roach                    case self::Z_AXIS_TIME:
692b6b9dcc9SGreg Roach                        $boundaries_csv = $params['z-axis-boundaries-periods'];
693c6266a77SGreg Roach                        $z_axis         = $this->axisYears($boundaries_csv);
694afa8d404SGreg Roach                        // The stats query doesn't have an "all" function, so query M/F separately
695afa8d404SGreg Roach                        foreach (['M', 'F'] as $sex) {
696c6266a77SGreg Roach                            $prev_boundary = 0;
697c6266a77SGreg Roach                            $indi = [];
698c6266a77SGreg Roach                            foreach (array_keys($z_axis) as $boundary) {
6999219296aSGreg Roach                                $rows = $statistics->statsMarrAgeQuery($sex, $prev_boundary, $boundary);
700c6266a77SGreg Roach                                foreach ($rows as $row) {
7016960d4a1SGreg Roach                                    if (!in_array($row->d_gid, $indi, true)) {
702c6266a77SGreg Roach                                        $years = (int) ($row->age / self::DAYS_IN_YEAR);
703c6266a77SGreg Roach                                        $this->fillYData($years, $boundary, 1, $x_axis, $z_axis, $ydata);
704c6266a77SGreg Roach                                        $indi[] = $row->d_gid;
705c6266a77SGreg Roach                                    }
706c6266a77SGreg Roach                                }
707c6266a77SGreg Roach                                $prev_boundary = $boundary + 1;
708c6266a77SGreg Roach                            }
709c6266a77SGreg Roach                        }
710c6266a77SGreg Roach                        break;
711c6266a77SGreg Roach                    default:
712c6266a77SGreg Roach                        throw new NotFoundHttpException();
713c6266a77SGreg Roach                }
714c6266a77SGreg Roach
7156ccdf4f0SGreg Roach                return response($this->myPlot($chart_title, $x_axis, $x_axis_title, $ydata, $y_axis_title, $z_axis, $y_axis_type));
716c6266a77SGreg Roach
717c6266a77SGreg Roach            case self::X_AXIS_NUMBER_OF_CHILDREN:
718c6266a77SGreg Roach                $chart_title  = I18N::translate('Number of children');
719c6266a77SGreg Roach                $x_axis_title = I18N::translate('Children');
7206960d4a1SGreg Roach                $x_axis       = $this->axisNumbers('0,1,2,3,4,5,6,7,8,9,10');
721c6266a77SGreg Roach
722c6266a77SGreg Roach                switch ($y_axis_type) {
723c6266a77SGreg Roach                    case self::Y_AXIS_NUMBERS:
724c6266a77SGreg Roach                        $y_axis_title = I18N::translate('Families');
725c6266a77SGreg Roach                        break;
726c6266a77SGreg Roach                    case self::Y_AXIS_PERCENT:
727c6266a77SGreg Roach                        $y_axis_title = '%';
728c6266a77SGreg Roach                        break;
729c6266a77SGreg Roach                    default:
730c6266a77SGreg Roach                        throw new NotFoundHttpException();
731c6266a77SGreg Roach                }
732c6266a77SGreg Roach
733c6266a77SGreg Roach                switch ($z_axis_type) {
734c6266a77SGreg Roach                    case self::Z_AXIS_ALL:
735c6266a77SGreg Roach                        $z_axis = $this->axisAll();
7369219296aSGreg Roach                        $rows = $statistics->statsChildrenQuery();
737c6266a77SGreg Roach                        foreach ($rows as $row) {
738c6266a77SGreg Roach                            $this->fillYData($row->f_numchil, 0, $row->total, $x_axis, $z_axis, $ydata);
739c6266a77SGreg Roach                        }
740c6266a77SGreg Roach                        break;
741c6266a77SGreg Roach                    case self::Z_AXIS_TIME:
742b6b9dcc9SGreg Roach                        $boundaries_csv = $params['z-axis-boundaries-periods'];
743c6266a77SGreg Roach                        $z_axis         = $this->axisYears($boundaries_csv);
744c6266a77SGreg Roach                        $prev_boundary = 0;
745c6266a77SGreg Roach                        foreach (array_keys($z_axis) as $boundary) {
746b1126ab4SGreg Roach                            $rows = $statistics->statsChildrenQuery($prev_boundary, $boundary);
747c6266a77SGreg Roach                            foreach ($rows as $row) {
748c6266a77SGreg Roach                                $this->fillYData($row->f_numchil, $boundary, $row->total, $x_axis, $z_axis, $ydata);
749c6266a77SGreg Roach                            }
750c6266a77SGreg Roach                            $prev_boundary = $boundary + 1;
751c6266a77SGreg Roach                        }
752c6266a77SGreg Roach                        break;
753c6266a77SGreg Roach                    default:
754c6266a77SGreg Roach                        throw new NotFoundHttpException();
755c6266a77SGreg Roach                }
756c6266a77SGreg Roach
7576ccdf4f0SGreg Roach                return response($this->myPlot($chart_title, $x_axis, $x_axis_title, $ydata, $y_axis_title, $z_axis, $y_axis_type));
758c6266a77SGreg Roach
759c6266a77SGreg Roach            default:
760c6266a77SGreg Roach                throw new NotFoundHttpException();
761c6266a77SGreg Roach                break;
762c6266a77SGreg Roach        }
763c6266a77SGreg Roach    }
764c6266a77SGreg Roach
765c6266a77SGreg Roach    /**
766c6266a77SGreg Roach     * @return string[]
767c6266a77SGreg Roach     */
768c6266a77SGreg Roach    private function axisAll(): array
769c6266a77SGreg Roach    {
770c6266a77SGreg Roach        return [
771c6266a77SGreg Roach            I18N::translate('Total'),
772c6266a77SGreg Roach        ];
773c6266a77SGreg Roach    }
774c6266a77SGreg Roach
775c6266a77SGreg Roach    /**
776c6266a77SGreg Roach     * @return string[]
777c6266a77SGreg Roach     */
778c6266a77SGreg Roach    private function axisSexes(): array
779c6266a77SGreg Roach    {
780c6266a77SGreg Roach        return [
781c6266a77SGreg Roach            'M' => I18N::translate('Male'),
782c6266a77SGreg Roach            'F' => I18N::translate('Female'),
783c6266a77SGreg Roach        ];
784c6266a77SGreg Roach    }
785c6266a77SGreg Roach
786c6266a77SGreg Roach    /**
787c6266a77SGreg Roach     * Labels for the X axis
788c6266a77SGreg Roach     *
789c6266a77SGreg Roach     * @return string[]
790c6266a77SGreg Roach     */
791c6266a77SGreg Roach    private function axisMonths(): array
792c6266a77SGreg Roach    {
793c6266a77SGreg Roach        return [
794c6266a77SGreg Roach            'JAN' => I18N::translateContext('NOMINATIVE', 'January'),
795c6266a77SGreg Roach            'FEB' => I18N::translateContext('NOMINATIVE', 'February'),
796c6266a77SGreg Roach            'MAR' => I18N::translateContext('NOMINATIVE', 'March'),
797c6266a77SGreg Roach            'APR' => I18N::translateContext('NOMINATIVE', 'April'),
798c6266a77SGreg Roach            'MAY' => I18N::translateContext('NOMINATIVE', 'May'),
799c6266a77SGreg Roach            'JUN' => I18N::translateContext('NOMINATIVE', 'June'),
800c6266a77SGreg Roach            'JUL' => I18N::translateContext('NOMINATIVE', 'July'),
801c6266a77SGreg Roach            'AUG' => I18N::translateContext('NOMINATIVE', 'August'),
802c6266a77SGreg Roach            'SEP' => I18N::translateContext('NOMINATIVE', 'September'),
803c6266a77SGreg Roach            'OCT' => I18N::translateContext('NOMINATIVE', 'October'),
804c6266a77SGreg Roach            'NOV' => I18N::translateContext('NOMINATIVE', 'November'),
805c6266a77SGreg Roach            'DEC' => I18N::translateContext('NOMINATIVE', 'December'),
806c6266a77SGreg Roach        ];
807c6266a77SGreg Roach    }
808c6266a77SGreg Roach
809c6266a77SGreg Roach    /**
810c6266a77SGreg Roach     * Convert a list of N year-boundaries into N+1 year-ranges for the z-axis.
811c6266a77SGreg Roach     *
812c6266a77SGreg Roach     * @param string $boundaries_csv
813c6266a77SGreg Roach     *
814c6266a77SGreg Roach     * @return string[]
815c6266a77SGreg Roach     */
816c6266a77SGreg Roach    private function axisYears(string $boundaries_csv): array
817c6266a77SGreg Roach    {
818c6266a77SGreg Roach        $boundaries = explode(',', $boundaries_csv);
819c6266a77SGreg Roach
820c6266a77SGreg Roach        $axis = [];
821c6266a77SGreg Roach        foreach ($boundaries as $n => $boundary) {
822c6266a77SGreg Roach            if ($n === 0) {
823c6266a77SGreg Roach                $date = new Date('BEF ' . $boundary);
824c6266a77SGreg Roach            } else {
825c6266a77SGreg Roach                $date = new Date('BET ' . $boundaries[$n - 1] . ' AND ' . ($boundary - 1));
826c6266a77SGreg Roach            }
827c6266a77SGreg Roach            $axis[$boundary - 1] = strip_tags($date->display());
828c6266a77SGreg Roach        }
829c6266a77SGreg Roach
830c6266a77SGreg Roach        $date              = new Date('AFT ' . $boundaries[count($boundaries) - 1]);
831c6266a77SGreg Roach        $axis[PHP_INT_MAX] = strip_tags($date->display());
832c6266a77SGreg Roach
833c6266a77SGreg Roach        return $axis;
834c6266a77SGreg Roach    }
835c6266a77SGreg Roach
836c6266a77SGreg Roach    /**
837c6266a77SGreg Roach     * Create the X axis.
838c6266a77SGreg Roach     *
839c6266a77SGreg Roach     * @param string $boundaries_csv
840c6266a77SGreg Roach     *
841c6266a77SGreg Roach     * @return array
842c6266a77SGreg Roach     */
843c6266a77SGreg Roach    private function axisNumbers(string $boundaries_csv): array
844c6266a77SGreg Roach    {
845c6266a77SGreg Roach        $boundaries = explode(',', $boundaries_csv);
846c6266a77SGreg Roach
8470b5fd0a6SGreg Roach        $boundaries = array_map(static function (string $x): int {
848c6266a77SGreg Roach            return (int) $x;
849c6266a77SGreg Roach        }, $boundaries);
850c6266a77SGreg Roach
851c6266a77SGreg Roach        $axis = [];
852c6266a77SGreg Roach        foreach ($boundaries as $n => $boundary) {
853c6266a77SGreg Roach            if ($n === 0) {
8546960d4a1SGreg Roach                $prev_boundary = 0;
8556960d4a1SGreg Roach            } else {
8566960d4a1SGreg Roach                $prev_boundary = $boundaries[$n - 1] + 1;
8576960d4a1SGreg Roach            }
8586960d4a1SGreg Roach
8596960d4a1SGreg Roach            if ($prev_boundary === $boundary) {
860c6266a77SGreg Roach                /* I18N: A range of numbers */
8616960d4a1SGreg Roach                $axis[$boundary] = I18N::number($boundary);
862c6266a77SGreg Roach            } else {
863c6266a77SGreg Roach                /* I18N: A range of numbers */
8646960d4a1SGreg Roach                $axis[$boundary] = I18N::translate('%1$s–%2$s', I18N::number($prev_boundary), I18N::number($boundary));
865c6266a77SGreg Roach            }
866c6266a77SGreg Roach        }
867c6266a77SGreg Roach
868c6266a77SGreg Roach        /* I18N: Label on a graph; 40+ means 40 or more */
869c6266a77SGreg Roach        $axis[PHP_INT_MAX] = I18N::translate('%s+', I18N::number($boundaries[count($boundaries) - 1]));
870c6266a77SGreg Roach
871c6266a77SGreg Roach        return $axis;
872c6266a77SGreg Roach    }
873c6266a77SGreg Roach
874c6266a77SGreg Roach    /**
875c6266a77SGreg Roach     * Calculate the Y axis.
876c6266a77SGreg Roach     *
877c6266a77SGreg Roach     * @param int|string $x
878c6266a77SGreg Roach     * @param int|string $z
879c6266a77SGreg Roach     * @param int|string $value
880c6266a77SGreg Roach     * @param array      $x_axis
881c6266a77SGreg Roach     * @param array      $z_axis
882c6266a77SGreg Roach     * @param int[][]    $ydata
883c6266a77SGreg Roach     *
884c6266a77SGreg Roach     * @return void
885c6266a77SGreg Roach     */
886e364afe4SGreg Roach    private function fillYData($x, $z, $value, array $x_axis, array $z_axis, array &$ydata): void
887c6266a77SGreg Roach    {
888c6266a77SGreg Roach        $x = $this->findAxisEntry($x, $x_axis);
889c6266a77SGreg Roach        $z = $this->findAxisEntry($z, $z_axis);
890c6266a77SGreg Roach
8916960d4a1SGreg Roach        if (!array_key_exists($z, $z_axis)) {
892c6266a77SGreg Roach            foreach (array_keys($z_axis) as $key) {
893c6266a77SGreg Roach                if ($value <= $key) {
894c6266a77SGreg Roach                    $z = $key;
895c6266a77SGreg Roach                    break;
896c6266a77SGreg Roach                }
897c6266a77SGreg Roach            }
898c6266a77SGreg Roach        }
899c6266a77SGreg Roach
900c6266a77SGreg Roach        // Add the value to the appropriate data point.
901c6266a77SGreg Roach        $ydata[$z][$x] = ($ydata[$z][$x] ?? 0) + $value;
902c6266a77SGreg Roach    }
903c6266a77SGreg Roach
904c6266a77SGreg Roach    /**
905c6266a77SGreg Roach     * Find the axis entry for a given value.
906c6266a77SGreg Roach     * Some are direct lookup (e.g. M/F, JAN/FEB/MAR).
907c6266a77SGreg Roach     * Others need to find the approprate range.
908c6266a77SGreg Roach     *
909c6266a77SGreg Roach     * @param int|float|string $value
910c6266a77SGreg Roach     * @param string[]         $axis
911c6266a77SGreg Roach     *
912c6266a77SGreg Roach     * @return int|string
913c6266a77SGreg Roach     */
914c6266a77SGreg Roach    private function findAxisEntry($value, $axis)
915c6266a77SGreg Roach    {
916c6266a77SGreg Roach        if (is_numeric($value)) {
917c6266a77SGreg Roach            $value = (int) $value;
918c6266a77SGreg Roach
9196960d4a1SGreg Roach            if (!array_key_exists($value, $axis)) {
920c6266a77SGreg Roach                foreach (array_keys($axis) as $boundary) {
921c6266a77SGreg Roach                    if ($value <= $boundary) {
922c6266a77SGreg Roach                        $value = $boundary;
923c6266a77SGreg Roach                        break;
924c6266a77SGreg Roach                    }
925c6266a77SGreg Roach                }
926c6266a77SGreg Roach            }
927c6266a77SGreg Roach        }
928c6266a77SGreg Roach
929c6266a77SGreg Roach        return $value;
930c6266a77SGreg Roach    }
931c6266a77SGreg Roach
932c6266a77SGreg Roach    /**
933c6266a77SGreg Roach     * Plot the data.
934c6266a77SGreg Roach     *
935c6266a77SGreg Roach     * @param string   $chart_title
936c6266a77SGreg Roach     * @param string[] $x_axis
937c6266a77SGreg Roach     * @param string   $x_axis_title
938c6266a77SGreg Roach     * @param int[][]  $ydata
939c6266a77SGreg Roach     * @param string   $y_axis_title
940c6266a77SGreg Roach     * @param string[] $z_axis
941c6266a77SGreg Roach     * @param int      $y_axis_type
942c6266a77SGreg Roach     *
943c6266a77SGreg Roach     * @return string
944c6266a77SGreg Roach     */
945a81e5019SRico Sonntag    private function myPlot(
946a81e5019SRico Sonntag        string $chart_title,
947a81e5019SRico Sonntag        array $x_axis,
948a81e5019SRico Sonntag        string $x_axis_title,
949a81e5019SRico Sonntag        array $ydata,
950a81e5019SRico Sonntag        string $y_axis_title,
951a81e5019SRico Sonntag        array $z_axis,
952a81e5019SRico Sonntag        int $y_axis_type
953a81e5019SRico Sonntag    ): string {
9546960d4a1SGreg Roach        if (!count($ydata)) {
955a81e5019SRico Sonntag            return I18N::translate('This information is not available.');
956c6266a77SGreg Roach        }
957c6266a77SGreg Roach
958c6266a77SGreg Roach        // Colors for z-axis
959c6266a77SGreg Roach        $colors = [];
960c6266a77SGreg Roach        $index  = 0;
9616960d4a1SGreg Roach        while (count($colors) < count($ydata)) {
962c6266a77SGreg Roach            $colors[] = self::Z_AXIS_COLORS[$index];
9636960d4a1SGreg Roach            $index    = ($index + 1) % count(self::Z_AXIS_COLORS);
964c6266a77SGreg Roach        }
965c6266a77SGreg Roach
966c6266a77SGreg Roach        // Convert our sparse dataset into a fixed-size array
967c6266a77SGreg Roach        $tmp = [];
968c6266a77SGreg Roach        foreach (array_keys($z_axis) as $z) {
969c6266a77SGreg Roach            foreach (array_keys($x_axis) as $x) {
970c6266a77SGreg Roach                $tmp[$z][$x] = $ydata[$z][$x] ?? 0;
971c6266a77SGreg Roach            }
972c6266a77SGreg Roach        }
973c6266a77SGreg Roach        $ydata = $tmp;
974c6266a77SGreg Roach
975a81e5019SRico Sonntag        // Convert the chart data to percentage
976c6266a77SGreg Roach        if ($y_axis_type === self::Y_AXIS_PERCENT) {
977c6266a77SGreg Roach            // Normalise each (non-zero!) set of data to total 100%
9780b5fd0a6SGreg Roach            array_walk($ydata, static function (array &$x) {
979c6266a77SGreg Roach                $sum = array_sum($x);
980c6266a77SGreg Roach                if ($sum > 0) {
9810b5fd0a6SGreg Roach                    $x = array_map(static function ($y) use ($sum) {
982c6266a77SGreg Roach                        return $y * 100.0 / $sum;
983c6266a77SGreg Roach                    }, $x);
984c6266a77SGreg Roach                }
985c6266a77SGreg Roach            });
986c6266a77SGreg Roach        }
987c6266a77SGreg Roach
988a81e5019SRico Sonntag        $data = [
989a81e5019SRico Sonntag            array_merge(
990a81e5019SRico Sonntag                [ I18N::translate('Century') ],
991a81e5019SRico Sonntag                array_values($z_axis)
992a81e5019SRico Sonntag            )
993c6266a77SGreg Roach        ];
994c6266a77SGreg Roach
995a81e5019SRico Sonntag        $intermediate = [];
996a81e5019SRico Sonntag        foreach ($ydata as $century => $months) {
997a81e5019SRico Sonntag            foreach ($months as $month => $value) {
998a81e5019SRico Sonntag                $intermediate[$month][] = [
999a81e5019SRico Sonntag                    'v' => $value,
1000a81e5019SRico Sonntag                    'f' => ($y_axis_type === self::Y_AXIS_PERCENT) ? sprintf('%.1f%%', $value) : $value,
1001a81e5019SRico Sonntag                ];
1002a81e5019SRico Sonntag            }
1003c6266a77SGreg Roach        }
1004c6266a77SGreg Roach
1005a81e5019SRico Sonntag        foreach ($intermediate as $key => $values) {
1006a81e5019SRico Sonntag            $data[] = array_merge(
1007a81e5019SRico Sonntag                [ $x_axis[$key] ],
1008a81e5019SRico Sonntag                $values
1009a81e5019SRico Sonntag            );
1010a81e5019SRico Sonntag        }
1011c6266a77SGreg Roach
1012a81e5019SRico Sonntag        $chart_options = [
1013a81e5019SRico Sonntag            'title' => '',
1014a81e5019SRico Sonntag            'subtitle' => '',
1015a81e5019SRico Sonntag            'height' => 400,
1016a81e5019SRico Sonntag            'width'  => '100%',
1017a81e5019SRico Sonntag            'legend' => [
10186960d4a1SGreg Roach                'position'  => count($z_axis) > 1 ? 'right' : 'none',
1019a81e5019SRico Sonntag                'alignment' => 'center',
1020a81e5019SRico Sonntag            ],
1021a81e5019SRico Sonntag            'tooltip' => [
1022a81e5019SRico Sonntag                'format' => '\'%\'',
1023a81e5019SRico Sonntag            ],
1024a81e5019SRico Sonntag            'vAxis' => [
1025a81e5019SRico Sonntag                'title' => $y_axis_title ?? '',
1026a81e5019SRico Sonntag            ],
1027a81e5019SRico Sonntag            'hAxis' => [
1028a81e5019SRico Sonntag                'title' => $x_axis_title ?? '',
1029a81e5019SRico Sonntag            ],
1030a81e5019SRico Sonntag            'colors' => $colors,
1031a81e5019SRico Sonntag        ];
1032a81e5019SRico Sonntag
1033a81e5019SRico Sonntag        return view(
1034a81e5019SRico Sonntag            'statistics/other/charts/custom',
1035a81e5019SRico Sonntag            [
1036a81e5019SRico Sonntag                'data'          => $data,
1037a81e5019SRico Sonntag                'chart_options' => $chart_options,
1038a81e5019SRico Sonntag                'chart_title'   => $chart_title,
1039a81e5019SRico Sonntag            ]
1040a81e5019SRico Sonntag        );
1041c6266a77SGreg Roach    }
1042168ff6f3Sric2016}
1043