xref: /webtrees/app/Module/StatisticsChartModule.php (revision 89f7189b61a494347591c99bdb92afb7d8b66e1b)
1168ff6f3Sric2016<?php
23976b470SGreg Roach
3168ff6f3Sric2016/**
4168ff6f3Sric2016 * webtrees: online genealogy
5*89f7189bSGreg Roach * Copyright (C) 2021 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
15*89f7189bSGreg Roach * along with this program. If not, see <https://www.gnu.org/licenses/>.
16168ff6f3Sric2016 */
17fcfa147eSGreg Roach
18e7f56f2aSGreg Roachdeclare(strict_types=1);
19e7f56f2aSGreg Roach
20168ff6f3Sric2016namespace Fisharebest\Webtrees\Module;
21168ff6f3Sric2016
22c6266a77SGreg Roachuse Fisharebest\Webtrees\Auth;
23c6266a77SGreg Roachuse Fisharebest\Webtrees\Date;
24d501c45dSGreg Roachuse Fisharebest\Webtrees\Exceptions\HttpNotFoundException;
25168ff6f3Sric2016use Fisharebest\Webtrees\I18N;
26168ff6f3Sric2016use Fisharebest\Webtrees\Individual;
279219296aSGreg Roachuse Fisharebest\Webtrees\Statistics;
285229eadeSGreg Roachuse Fisharebest\Webtrees\Tree;
296ccdf4f0SGreg Roachuse Psr\Http\Message\ResponseInterface;
306ccdf4f0SGreg Roachuse Psr\Http\Message\ServerRequestInterface;
313976b470SGreg Roach
3257ab2231SGreg Roachuse function app;
336960d4a1SGreg Roachuse function array_key_exists;
346960d4a1SGreg Roachuse function array_keys;
356960d4a1SGreg Roachuse function array_map;
366960d4a1SGreg Roachuse function array_merge;
376960d4a1SGreg Roachuse function array_sum;
386960d4a1SGreg Roachuse function array_values;
396960d4a1SGreg Roachuse function array_walk;
405229eadeSGreg Roachuse function assert;
416960d4a1SGreg Roachuse function count;
426960d4a1SGreg Roachuse function explode;
436960d4a1SGreg Roachuse function in_array;
446960d4a1SGreg Roachuse function is_numeric;
456960d4a1SGreg Roachuse function sprintf;
466960d4a1SGreg Roachuse function strip_tags;
47168ff6f3Sric2016
48168ff6f3Sric2016/**
49168ff6f3Sric2016 * Class StatisticsChartModule
50168ff6f3Sric2016 */
5137eb8894SGreg Roachclass StatisticsChartModule extends AbstractModule implements ModuleChartInterface
52c1010edaSGreg Roach{
5349a243cbSGreg Roach    use ModuleChartTrait;
5449a243cbSGreg Roach
55c6266a77SGreg Roach    public const X_AXIS_INDIVIDUAL_MAP        = 1;
56c6266a77SGreg Roach    public const X_AXIS_BIRTH_MAP             = 2;
57c6266a77SGreg Roach    public const X_AXIS_DEATH_MAP             = 3;
58c6266a77SGreg Roach    public const X_AXIS_MARRIAGE_MAP          = 4;
59c6266a77SGreg Roach    public const X_AXIS_BIRTH_MONTH           = 11;
60c6266a77SGreg Roach    public const X_AXIS_DEATH_MONTH           = 12;
61c6266a77SGreg Roach    public const X_AXIS_MARRIAGE_MONTH        = 13;
62c6266a77SGreg Roach    public const X_AXIS_FIRST_CHILD_MONTH     = 14;
63c6266a77SGreg Roach    public const X_AXIS_FIRST_MARRIAGE_MONTH  = 15;
64c6266a77SGreg Roach    public const X_AXIS_AGE_AT_DEATH          = 18;
65c6266a77SGreg Roach    public const X_AXIS_AGE_AT_MARRIAGE       = 19;
66c6266a77SGreg Roach    public const X_AXIS_AGE_AT_FIRST_MARRIAGE = 20;
67c6266a77SGreg Roach    public const X_AXIS_NUMBER_OF_CHILDREN    = 21;
68c6266a77SGreg Roach
69c6266a77SGreg Roach    public const Y_AXIS_NUMBERS = 201;
70c6266a77SGreg Roach    public const Y_AXIS_PERCENT = 202;
71c6266a77SGreg Roach
72c6266a77SGreg Roach    public const Z_AXIS_ALL  = 300;
73c6266a77SGreg Roach    public const Z_AXIS_SEX  = 301;
74c6266a77SGreg Roach    public const Z_AXIS_TIME = 302;
75c6266a77SGreg Roach
76c6266a77SGreg Roach    // First two colors are blue/pink, to work with Z_AXIS_SEX.
77c6266a77SGreg Roach    private const Z_AXIS_COLORS = ['0000FF', 'FFA0CB', '9F00FF', 'FF7000', '905030', 'FF0000', '00FF00', 'F0F000'];
78c6266a77SGreg Roach
79c6266a77SGreg Roach    private const DAYS_IN_YEAR = 365.25;
80c6266a77SGreg Roach
81168ff6f3Sric2016    /**
820cfd6963SGreg Roach     * How should this module be identified in the control panel, etc.?
83168ff6f3Sric2016     *
84168ff6f3Sric2016     * @return string
85168ff6f3Sric2016     */
8649a243cbSGreg Roach    public function title(): string
87c1010edaSGreg Roach    {
88bbb76c12SGreg Roach        /* I18N: Name of a module/chart */
89bbb76c12SGreg Roach        return I18N::translate('Statistics');
90168ff6f3Sric2016    }
91168ff6f3Sric2016
92168ff6f3Sric2016    /**
93168ff6f3Sric2016     * A sentence describing what this module does.
94168ff6f3Sric2016     *
95168ff6f3Sric2016     * @return string
96168ff6f3Sric2016     */
9749a243cbSGreg Roach    public function description(): string
98c1010edaSGreg Roach    {
99bbb76c12SGreg Roach        /* I18N: Description of the “StatisticsChart” module */
100bbb76c12SGreg Roach        return I18N::translate('Various statistics charts.');
101168ff6f3Sric2016    }
102168ff6f3Sric2016
103168ff6f3Sric2016    /**
104377a2979SGreg Roach     * CSS class for the URL.
105377a2979SGreg Roach     *
106377a2979SGreg Roach     * @return string
107377a2979SGreg Roach     */
108377a2979SGreg Roach    public function chartMenuClass(): string
109377a2979SGreg Roach    {
110377a2979SGreg Roach        return 'menu-chart-statistics';
111377a2979SGreg Roach    }
112377a2979SGreg Roach
113377a2979SGreg Roach    /**
114e6562982SGreg Roach     * The URL for this chart.
115168ff6f3Sric2016     *
11660bc3e3fSGreg Roach     * @param Individual $individual
11759597b37SGreg Roach     * @param mixed[]    $parameters
11860bc3e3fSGreg Roach     *
119e6562982SGreg Roach     * @return string
120168ff6f3Sric2016     */
121e6562982SGreg Roach    public function chartUrl(Individual $individual, array $parameters = []): string
122c1010edaSGreg Roach    {
123c6266a77SGreg Roach        return route('module', [
124c6266a77SGreg Roach                'module' => $this->name(),
125c6266a77SGreg Roach                'action' => 'Chart',
126d72b284aSGreg Roach                'tree'    => $individual->tree()->name(),
127e6562982SGreg Roach            ] + $parameters);
128168ff6f3Sric2016    }
129c6266a77SGreg Roach
130c6266a77SGreg Roach    /**
131c6266a77SGreg Roach     * A form to request the chart parameters.
132c6266a77SGreg Roach     *
13357ab2231SGreg Roach     * @param ServerRequestInterface $request
134c6266a77SGreg Roach     *
1356ccdf4f0SGreg Roach     * @return ResponseInterface
136c6266a77SGreg Roach     */
13757ab2231SGreg Roach    public function getChartAction(ServerRequestInterface $request): ResponseInterface
138c6266a77SGreg Roach    {
13957ab2231SGreg Roach        $tree = $request->getAttribute('tree');
14075964c75SGreg Roach        assert($tree instanceof Tree);
1415229eadeSGreg Roach
14257ab2231SGreg Roach        $user = $request->getAttribute('user');
14357ab2231SGreg Roach
144ef483801SGreg Roach        Auth::checkComponentAccess($this, ModuleChartInterface::class, $tree, $user);
1459867b2f0SGreg Roach
146c6266a77SGreg Roach        $tabs = [
147c6266a77SGreg Roach            I18N::translate('Individuals') => route('module', [
148c6266a77SGreg Roach                'module' => $this->name(),
149c6266a77SGreg Roach                'action' => 'Individuals',
150d72b284aSGreg Roach                'tree'    => $tree->name(),
151c6266a77SGreg Roach            ]),
152c6266a77SGreg Roach            I18N::translate('Families')    => route('module', [
153c6266a77SGreg Roach                'module' => $this->name(),
154c6266a77SGreg Roach                'action' => 'Families',
155d72b284aSGreg Roach                'tree'    => $tree->name(),
156c6266a77SGreg Roach            ]),
157c6266a77SGreg Roach            I18N::translate('Other')       => route('module', [
158c6266a77SGreg Roach                'module' => $this->name(),
159c6266a77SGreg Roach                'action' => 'Other',
160d72b284aSGreg Roach                'tree'    => $tree->name(),
161c6266a77SGreg Roach            ]),
162c6266a77SGreg Roach            I18N::translate('Custom')      => route('module', [
163c6266a77SGreg Roach                'module' => $this->name(),
164c6266a77SGreg Roach                'action' => 'Custom',
165d72b284aSGreg Roach                'tree'    => $tree->name(),
166c6266a77SGreg Roach            ]),
167c6266a77SGreg Roach        ];
168c6266a77SGreg Roach
1699b5537c3SGreg Roach        return $this->viewResponse('modules/statistics-chart/page', [
17071378461SGreg Roach            'module' => $this->name(),
171c6266a77SGreg Roach            'tabs'   => $tabs,
172c6266a77SGreg Roach            'title'  => $this->title(),
173ef5d23f1SGreg Roach            'tree'   => $tree,
174c6266a77SGreg Roach        ]);
175c6266a77SGreg Roach    }
176c6266a77SGreg Roach
177c6266a77SGreg Roach    /**
17857ab2231SGreg Roach     * @param ServerRequestInterface $request
179c6266a77SGreg Roach     *
1806ccdf4f0SGreg Roach     * @return ResponseInterface
181c6266a77SGreg Roach     */
18257ab2231SGreg Roach    public function getIndividualsAction(ServerRequestInterface $request): ResponseInterface
183c6266a77SGreg Roach    {
184b6c326d8SGreg Roach        $this->layout = 'layouts/ajax';
185b6c326d8SGreg Roach
186b6c326d8SGreg Roach        return $this->viewResponse('modules/statistics-chart/individuals', [
187c6266a77SGreg Roach            'show_oldest_living' => Auth::check(),
18857ab2231SGreg Roach            'stats'              => app(Statistics::class),
189c6266a77SGreg Roach        ]);
190c6266a77SGreg Roach    }
191c6266a77SGreg Roach
192c6266a77SGreg Roach    /**
19357ab2231SGreg Roach     * @param ServerRequestInterface $request
194c6266a77SGreg Roach     *
1956ccdf4f0SGreg Roach     * @return ResponseInterface
196c6266a77SGreg Roach     */
19757ab2231SGreg Roach    public function getFamiliesAction(ServerRequestInterface $request): ResponseInterface
198c6266a77SGreg Roach    {
199b6c326d8SGreg Roach        $this->layout = 'layouts/ajax';
200b6c326d8SGreg Roach
201b6c326d8SGreg Roach        return $this->viewResponse('modules/statistics-chart/families', [
20257ab2231SGreg Roach            'stats' => app(Statistics::class),
203c6266a77SGreg Roach        ]);
204c6266a77SGreg Roach    }
205c6266a77SGreg Roach
206c6266a77SGreg Roach    /**
20757ab2231SGreg Roach     * @param ServerRequestInterface $request
208c6266a77SGreg Roach     *
2096ccdf4f0SGreg Roach     * @return ResponseInterface
210c6266a77SGreg Roach     */
21157ab2231SGreg Roach    public function getOtherAction(ServerRequestInterface $request): ResponseInterface
212c6266a77SGreg Roach    {
213b6c326d8SGreg Roach        $this->layout = 'layouts/ajax';
214b6c326d8SGreg Roach
215b6c326d8SGreg Roach        return $this->viewResponse('modules/statistics-chart/other', [
21657ab2231SGreg Roach            'stats' => app(Statistics::class),
217c6266a77SGreg Roach        ]);
218c6266a77SGreg Roach    }
219c6266a77SGreg Roach
220c6266a77SGreg Roach    /**
22157ab2231SGreg Roach     * @param ServerRequestInterface $request
222c6266a77SGreg Roach     *
2236ccdf4f0SGreg Roach     * @return ResponseInterface
224c6266a77SGreg Roach     */
22557ab2231SGreg Roach    public function getCustomAction(ServerRequestInterface $request): ResponseInterface
226c6266a77SGreg Roach    {
227b6c326d8SGreg Roach        $this->layout = 'layouts/ajax';
228b6c326d8SGreg Roach
22957ab2231SGreg Roach        $tree = $request->getAttribute('tree');
23075964c75SGreg Roach        assert($tree instanceof Tree);
23157ab2231SGreg Roach
232b6c326d8SGreg Roach        return $this->viewResponse('modules/statistics-chart/custom', [
233c6266a77SGreg Roach            'module' => $this,
234c6266a77SGreg Roach            'tree'   => $tree,
235c6266a77SGreg Roach        ]);
236c6266a77SGreg Roach    }
237c6266a77SGreg Roach
238c6266a77SGreg Roach    /**
2396ccdf4f0SGreg Roach     * @param ServerRequestInterface $request
240c6266a77SGreg Roach     *
2416ccdf4f0SGreg Roach     * @return ResponseInterface
242c6266a77SGreg Roach     */
2433aa29b97SGreg Roach    public function postCustomChartAction(ServerRequestInterface $request): ResponseInterface
244c6266a77SGreg Roach    {
24557ab2231SGreg Roach        $statistics = app(Statistics::class);
246dca2dd78SGreg Roach        assert($statistics instanceof Statistics);
24757ab2231SGreg Roach
248b46c87bdSGreg Roach        $params = (array) $request->getParsedBody();
249b6b9dcc9SGreg Roach
250b6b9dcc9SGreg Roach        $x_axis_type = (int) $params['x-as'];
251b6b9dcc9SGreg Roach        $y_axis_type = (int) $params['y-as'];
252b6b9dcc9SGreg Roach        $z_axis_type = (int) $params['z-as'];
253c6266a77SGreg Roach        $ydata       = [];
254c6266a77SGreg Roach
255c6266a77SGreg Roach        switch ($x_axis_type) {
256c6266a77SGreg Roach            case self::X_AXIS_INDIVIDUAL_MAP:
2576ccdf4f0SGreg Roach                return response($statistics->chartDistribution(
258b6b9dcc9SGreg Roach                    $params['chart_shows'],
259b6b9dcc9SGreg Roach                    $params['chart_type'],
260b6b9dcc9SGreg Roach                    $params['SURN']
261c6266a77SGreg Roach                ));
262c6266a77SGreg Roach
263c6266a77SGreg Roach            case self::X_AXIS_BIRTH_MAP:
2646ccdf4f0SGreg Roach                return response($statistics->chartDistribution(
265b6b9dcc9SGreg Roach                    $params['chart_shows'],
266c6266a77SGreg Roach                    'birth_distribution_chart'
267c6266a77SGreg Roach                ));
268c6266a77SGreg Roach
269c6266a77SGreg Roach            case self::X_AXIS_DEATH_MAP:
2706ccdf4f0SGreg Roach                return response($statistics->chartDistribution(
271b6b9dcc9SGreg Roach                    $params['chart_shows'],
272c6266a77SGreg Roach                    'death_distribution_chart'
273c6266a77SGreg Roach                ));
274c6266a77SGreg Roach
275c6266a77SGreg Roach            case self::X_AXIS_MARRIAGE_MAP:
2766ccdf4f0SGreg Roach                return response($statistics->chartDistribution(
277b6b9dcc9SGreg Roach                    $params['chart_shows'],
278c6266a77SGreg Roach                    'marriage_distribution_chart'
279c6266a77SGreg Roach                ));
280c6266a77SGreg Roach
281c6266a77SGreg Roach            case self::X_AXIS_BIRTH_MONTH:
282c6266a77SGreg Roach                $chart_title  = I18N::translate('Month of birth');
283c6266a77SGreg Roach                $x_axis_title = I18N::translate('Month');
284c6266a77SGreg Roach                $x_axis       = $this->axisMonths();
285c6266a77SGreg Roach
286c6266a77SGreg Roach                switch ($y_axis_type) {
287c6266a77SGreg Roach                    case self::Y_AXIS_NUMBERS:
288c6266a77SGreg Roach                        $y_axis_title = I18N::translate('Individuals');
289c6266a77SGreg Roach                        break;
290c6266a77SGreg Roach                    case self::Y_AXIS_PERCENT:
291c6266a77SGreg Roach                        $y_axis_title = '%';
292c6266a77SGreg Roach                        break;
293c6266a77SGreg Roach                    default:
294d501c45dSGreg Roach                        throw new HttpNotFoundException();
295c6266a77SGreg Roach                }
296c6266a77SGreg Roach
297c6266a77SGreg Roach                switch ($z_axis_type) {
298c6266a77SGreg Roach                    case self::Z_AXIS_ALL:
299c6266a77SGreg Roach                        $z_axis = $this->axisAll();
300cde1d378SGreg Roach                        $rows   = $statistics->statsBirthQuery()->get();
301c6266a77SGreg Roach                        foreach ($rows as $row) {
302c6266a77SGreg Roach                            $this->fillYData($row->d_month, 0, $row->total, $x_axis, $z_axis, $ydata);
303c6266a77SGreg Roach                        }
304c6266a77SGreg Roach                        break;
305c6266a77SGreg Roach                    case self::Z_AXIS_SEX:
306c6266a77SGreg Roach                        $z_axis = $this->axisSexes();
307cde1d378SGreg Roach                        $rows   = $statistics->statsBirthBySexQuery()->get();
308c6266a77SGreg Roach                        foreach ($rows as $row) {
309c6266a77SGreg Roach                            $this->fillYData($row->d_month, $row->i_sex, $row->total, $x_axis, $z_axis, $ydata);
310c6266a77SGreg Roach                        }
311c6266a77SGreg Roach                        break;
312c6266a77SGreg Roach                    case self::Z_AXIS_TIME:
313b6b9dcc9SGreg Roach                        $boundaries_csv = $params['z-axis-boundaries-periods'];
314c6266a77SGreg Roach                        $z_axis         = $this->axisYears($boundaries_csv);
315c6266a77SGreg Roach                        $prev_boundary  = 0;
316c6266a77SGreg Roach                        foreach (array_keys($z_axis) as $boundary) {
317cde1d378SGreg Roach                            $rows = $statistics->statsBirthQuery($prev_boundary, $boundary)->get();
318c6266a77SGreg Roach                            foreach ($rows as $row) {
319c6266a77SGreg Roach                                $this->fillYData($row->d_month, $boundary, $row->total, $x_axis, $z_axis, $ydata);
320c6266a77SGreg Roach                            }
321c6266a77SGreg Roach                            $prev_boundary = $boundary + 1;
322c6266a77SGreg Roach                        }
323c6266a77SGreg Roach                        break;
324c6266a77SGreg Roach                    default:
325d501c45dSGreg Roach                        throw new HttpNotFoundException();
326c6266a77SGreg Roach                }
327c6266a77SGreg Roach
3286ccdf4f0SGreg Roach                return response($this->myPlot($chart_title, $x_axis, $x_axis_title, $ydata, $y_axis_title, $z_axis, $y_axis_type));
329c6266a77SGreg Roach
330c6266a77SGreg Roach            case self::X_AXIS_DEATH_MONTH:
331c6266a77SGreg Roach                $chart_title  = I18N::translate('Month of death');
332c6266a77SGreg Roach                $x_axis_title = I18N::translate('Month');
333c6266a77SGreg Roach                $x_axis       = $this->axisMonths();
334c6266a77SGreg Roach
335c6266a77SGreg Roach                switch ($y_axis_type) {
336c6266a77SGreg Roach                    case self::Y_AXIS_NUMBERS:
337c6266a77SGreg Roach                        $y_axis_title = I18N::translate('Individuals');
338c6266a77SGreg Roach                        break;
339c6266a77SGreg Roach                    case self::Y_AXIS_PERCENT:
340c6266a77SGreg Roach                        $y_axis_title = '%';
341c6266a77SGreg Roach                        break;
342c6266a77SGreg Roach                    default:
343d501c45dSGreg Roach                        throw new HttpNotFoundException();
344c6266a77SGreg Roach                }
345c6266a77SGreg Roach
346c6266a77SGreg Roach                switch ($z_axis_type) {
347c6266a77SGreg Roach                    case self::Z_AXIS_ALL:
348c6266a77SGreg Roach                        $z_axis = $this->axisAll();
349cde1d378SGreg Roach                        $rows   = $statistics->statsDeathQuery()->get();
350c6266a77SGreg Roach                        foreach ($rows as $row) {
351c6266a77SGreg Roach                            $this->fillYData($row->d_month, 0, $row->total, $x_axis, $z_axis, $ydata);
352c6266a77SGreg Roach                        }
353c6266a77SGreg Roach                        break;
354c6266a77SGreg Roach                    case self::Z_AXIS_SEX:
355c6266a77SGreg Roach                        $z_axis = $this->axisSexes();
356cde1d378SGreg Roach                        $rows   = $statistics->statsDeathBySexQuery()->get();
357c6266a77SGreg Roach                        foreach ($rows as $row) {
358c6266a77SGreg Roach                            $this->fillYData($row->d_month, $row->i_sex, $row->total, $x_axis, $z_axis, $ydata);
359c6266a77SGreg Roach                        }
360c6266a77SGreg Roach                        break;
361c6266a77SGreg Roach                    case self::Z_AXIS_TIME:
362b6b9dcc9SGreg Roach                        $boundaries_csv = $params['z-axis-boundaries-periods'];
363c6266a77SGreg Roach                        $z_axis         = $this->axisYears($boundaries_csv);
364c6266a77SGreg Roach                        $prev_boundary  = 0;
365c6266a77SGreg Roach                        foreach (array_keys($z_axis) as $boundary) {
366cde1d378SGreg Roach                            $rows = $statistics->statsDeathQuery($prev_boundary, $boundary)->get();
367c6266a77SGreg Roach                            foreach ($rows as $row) {
368c6266a77SGreg Roach                                $this->fillYData($row->d_month, $boundary, $row->total, $x_axis, $z_axis, $ydata);
369c6266a77SGreg Roach                            }
370c6266a77SGreg Roach                            $prev_boundary = $boundary + 1;
371c6266a77SGreg Roach                        }
372c6266a77SGreg Roach                        break;
373c6266a77SGreg Roach                    default:
374d501c45dSGreg Roach                        throw new HttpNotFoundException();
375c6266a77SGreg Roach                }
376c6266a77SGreg Roach
3776ccdf4f0SGreg Roach                return response($this->myPlot($chart_title, $x_axis, $x_axis_title, $ydata, $y_axis_title, $z_axis, $y_axis_type));
378c6266a77SGreg Roach
379c6266a77SGreg Roach            case self::X_AXIS_MARRIAGE_MONTH:
380c6266a77SGreg Roach                $chart_title  = I18N::translate('Month of marriage');
381c6266a77SGreg Roach                $x_axis_title = I18N::translate('Month');
382c6266a77SGreg Roach                $x_axis       = $this->axisMonths();
383c6266a77SGreg Roach
384c6266a77SGreg Roach                switch ($y_axis_type) {
385c6266a77SGreg Roach                    case self::Y_AXIS_NUMBERS:
386c6266a77SGreg Roach                        $y_axis_title = I18N::translate('Families');
387c6266a77SGreg Roach                        break;
388c6266a77SGreg Roach                    case self::Y_AXIS_PERCENT:
389c6266a77SGreg Roach                        $y_axis_title = '%';
390c6266a77SGreg Roach                        break;
391c6266a77SGreg Roach                    default:
392d501c45dSGreg Roach                        throw new HttpNotFoundException();
393c6266a77SGreg Roach                }
394c6266a77SGreg Roach
395c6266a77SGreg Roach                switch ($z_axis_type) {
396c6266a77SGreg Roach                    case self::Z_AXIS_ALL:
397c6266a77SGreg Roach                        $z_axis = $this->axisAll();
398e6f3d5e2SGreg Roach                        $rows   = $statistics->statsMarriageQuery()->get();
399c6266a77SGreg Roach                        foreach ($rows as $row) {
400c6266a77SGreg Roach                            $this->fillYData($row->d_month, 0, $row->total, $x_axis, $z_axis, $ydata);
401c6266a77SGreg Roach                        }
402c6266a77SGreg Roach                        break;
403c6266a77SGreg Roach                    case self::Z_AXIS_TIME:
404b6b9dcc9SGreg Roach                        $boundaries_csv = $params['z-axis-boundaries-periods'];
405c6266a77SGreg Roach                        $z_axis         = $this->axisYears($boundaries_csv);
406c6266a77SGreg Roach                        $prev_boundary  = 0;
407c6266a77SGreg Roach                        foreach (array_keys($z_axis) as $boundary) {
408e6f3d5e2SGreg Roach                            $rows = $statistics->statsMarriageQuery($prev_boundary, $boundary)->get();
409c6266a77SGreg Roach                            foreach ($rows as $row) {
410c6266a77SGreg Roach                                $this->fillYData($row->d_month, $boundary, $row->total, $x_axis, $z_axis, $ydata);
411c6266a77SGreg Roach                            }
412c6266a77SGreg Roach                            $prev_boundary = $boundary + 1;
413c6266a77SGreg Roach                        }
414c6266a77SGreg Roach                        break;
415c6266a77SGreg Roach                    default:
416d501c45dSGreg Roach                        throw new HttpNotFoundException();
417c6266a77SGreg Roach                }
418c6266a77SGreg Roach
4196ccdf4f0SGreg Roach                return response($this->myPlot($chart_title, $x_axis, $x_axis_title, $ydata, $y_axis_title, $z_axis, $y_axis_type));
420c6266a77SGreg Roach
421c6266a77SGreg Roach            case self::X_AXIS_FIRST_CHILD_MONTH:
422c6266a77SGreg Roach                $chart_title  = I18N::translate('Month of birth of first child in a relation');
423c6266a77SGreg Roach                $x_axis_title = I18N::translate('Month');
424c6266a77SGreg Roach                $x_axis       = $this->axisMonths();
425c6266a77SGreg Roach
426c6266a77SGreg Roach                switch ($y_axis_type) {
427c6266a77SGreg Roach                    case self::Y_AXIS_NUMBERS:
428c6266a77SGreg Roach                        $y_axis_title = I18N::translate('Children');
429c6266a77SGreg Roach                        break;
430c6266a77SGreg Roach                    case self::Y_AXIS_PERCENT:
431c6266a77SGreg Roach                        $y_axis_title = '%';
432c6266a77SGreg Roach                        break;
433c6266a77SGreg Roach                    default:
434d501c45dSGreg Roach                        throw new HttpNotFoundException();
435c6266a77SGreg Roach                }
436c6266a77SGreg Roach
437c6266a77SGreg Roach                switch ($z_axis_type) {
438c6266a77SGreg Roach                    case self::Z_AXIS_ALL:
439c6266a77SGreg Roach                        $z_axis = $this->axisAll();
440999da590SGreg Roach                        $rows   = $statistics->monthFirstChildQuery()->get();
441c6266a77SGreg Roach                        foreach ($rows as $row) {
442c6266a77SGreg Roach                            $this->fillYData($row->d_month, 0, $row->total, $x_axis, $z_axis, $ydata);
443c6266a77SGreg Roach                        }
444c6266a77SGreg Roach                        break;
445c6266a77SGreg Roach                    case self::Z_AXIS_SEX:
446c6266a77SGreg Roach                        $z_axis = $this->axisSexes();
447999da590SGreg Roach                        $rows   = $statistics->monthFirstChildBySexQuery()->get();
448c6266a77SGreg Roach                        foreach ($rows as $row) {
449c6266a77SGreg Roach                            $this->fillYData($row->d_month, $row->i_sex, $row->total, $x_axis, $z_axis, $ydata);
450c6266a77SGreg Roach                        }
451c6266a77SGreg Roach                        break;
452c6266a77SGreg Roach                    case self::Z_AXIS_TIME:
453b6b9dcc9SGreg Roach                        $boundaries_csv = $params['z-axis-boundaries-periods'];
454c6266a77SGreg Roach                        $z_axis         = $this->axisYears($boundaries_csv);
455c6266a77SGreg Roach                        $prev_boundary  = 0;
456c6266a77SGreg Roach                        foreach (array_keys($z_axis) as $boundary) {
457999da590SGreg Roach                            $rows = $statistics->monthFirstChildQuery($prev_boundary, $boundary)->get();
458c6266a77SGreg Roach                            foreach ($rows as $row) {
459c6266a77SGreg Roach                                $this->fillYData($row->d_month, $boundary, $row->total, $x_axis, $z_axis, $ydata);
460c6266a77SGreg Roach                            }
461c6266a77SGreg Roach                            $prev_boundary = $boundary + 1;
462c6266a77SGreg Roach                        }
463c6266a77SGreg Roach                        break;
464c6266a77SGreg Roach                    default:
465d501c45dSGreg Roach                        throw new HttpNotFoundException();
466c6266a77SGreg Roach                }
467c6266a77SGreg Roach
4686ccdf4f0SGreg Roach                return response($this->myPlot($chart_title, $x_axis, $x_axis_title, $ydata, $y_axis_title, $z_axis, $y_axis_type));
469c6266a77SGreg Roach
470c6266a77SGreg Roach            case self::X_AXIS_FIRST_MARRIAGE_MONTH:
471c6266a77SGreg Roach                $chart_title  = I18N::translate('Month of first marriage');
472c6266a77SGreg Roach                $x_axis_title = I18N::translate('Month');
473c6266a77SGreg Roach                $x_axis       = $this->axisMonths();
474c6266a77SGreg Roach
475c6266a77SGreg Roach                switch ($y_axis_type) {
476c6266a77SGreg Roach                    case self::Y_AXIS_NUMBERS:
477c6266a77SGreg Roach                        $y_axis_title = I18N::translate('Families');
478c6266a77SGreg Roach                        break;
479c6266a77SGreg Roach                    case self::Y_AXIS_PERCENT:
480c6266a77SGreg Roach                        $y_axis_title = '%';
481c6266a77SGreg Roach                        break;
482c6266a77SGreg Roach                    default:
483d501c45dSGreg Roach                        throw new HttpNotFoundException();
484c6266a77SGreg Roach                }
485c6266a77SGreg Roach
486c6266a77SGreg Roach                switch ($z_axis_type) {
487c6266a77SGreg Roach                    case self::Z_AXIS_ALL:
488c6266a77SGreg Roach                        $z_axis = $this->axisAll();
489e6f3d5e2SGreg Roach                        $rows   = $statistics->statsFirstMarriageQuery()->get();
490c6266a77SGreg Roach                        $indi   = [];
491c6266a77SGreg Roach                        foreach ($rows as $row) {
492dca2dd78SGreg Roach                            if (!in_array($row->f_husb, $indi, true) && !in_array($row->f_wife, $indi, true)) {
493c6266a77SGreg Roach                                $this->fillYData($row->month, 0, 1, $x_axis, $z_axis, $ydata);
494c6266a77SGreg Roach                            }
495dca2dd78SGreg Roach                            $indi[]  = $row->f_husb;
496dca2dd78SGreg Roach                            $indi[]  = $row->f_wife;
497c6266a77SGreg Roach                        }
498c6266a77SGreg Roach                        break;
499c6266a77SGreg Roach                    case self::Z_AXIS_TIME:
500b6b9dcc9SGreg Roach                        $boundaries_csv = $params['z-axis-boundaries-periods'];
501c6266a77SGreg Roach                        $z_axis         = $this->axisYears($boundaries_csv);
502c6266a77SGreg Roach                        $prev_boundary  = 0;
503c6266a77SGreg Roach                        $indi           = [];
504c6266a77SGreg Roach                        foreach (array_keys($z_axis) as $boundary) {
505e6f3d5e2SGreg Roach                            $rows = $statistics->statsFirstMarriageQuery($prev_boundary, $boundary)->get();
506c6266a77SGreg Roach                            foreach ($rows as $row) {
507dca2dd78SGreg Roach                                if (!in_array($row->f_husb, $indi, true) && !in_array($row->f_wife, $indi, true)) {
508c6266a77SGreg Roach                                    $this->fillYData($row->month, $boundary, 1, $x_axis, $z_axis, $ydata);
509c6266a77SGreg Roach                                }
510dca2dd78SGreg Roach                                $indi[]  = $row->f_husb;
511dca2dd78SGreg Roach                                $indi[]  = $row->f_wife;
512c6266a77SGreg Roach                            }
513c6266a77SGreg Roach                            $prev_boundary = $boundary + 1;
514c6266a77SGreg Roach                        }
515c6266a77SGreg Roach                        break;
516c6266a77SGreg Roach                    default:
517d501c45dSGreg Roach                        throw new HttpNotFoundException();
518c6266a77SGreg Roach                }
519c6266a77SGreg Roach
5206ccdf4f0SGreg Roach                return response($this->myPlot($chart_title, $x_axis, $x_axis_title, $ydata, $y_axis_title, $z_axis, $y_axis_type));
521c6266a77SGreg Roach
522c6266a77SGreg Roach            case self::X_AXIS_AGE_AT_DEATH:
523c6266a77SGreg Roach                $chart_title    = I18N::translate('Average age at death');
524c6266a77SGreg Roach                $x_axis_title   = I18N::translate('age');
525b6b9dcc9SGreg Roach                $boundaries_csv = $params['x-axis-boundaries-ages'];
526c6266a77SGreg Roach                $x_axis         = $this->axisNumbers($boundaries_csv);
527c6266a77SGreg Roach
528c6266a77SGreg Roach                switch ($y_axis_type) {
529c6266a77SGreg Roach                    case self::Y_AXIS_NUMBERS:
530c6266a77SGreg Roach                        $y_axis_title = I18N::translate('Individuals');
531c6266a77SGreg Roach                        break;
532c6266a77SGreg Roach                    case self::Y_AXIS_PERCENT:
533c6266a77SGreg Roach                        $y_axis_title = '%';
534c6266a77SGreg Roach                        break;
535c6266a77SGreg Roach                    default:
536d501c45dSGreg Roach                        throw new HttpNotFoundException();
537c6266a77SGreg Roach                }
538c6266a77SGreg Roach
539c6266a77SGreg Roach                switch ($z_axis_type) {
540c6266a77SGreg Roach                    case self::Z_AXIS_ALL:
541c6266a77SGreg Roach                        $z_axis = $this->axisAll();
5429219296aSGreg Roach                        $rows   = $statistics->statsAgeQuery('DEAT');
543c6266a77SGreg Roach                        foreach ($rows as $row) {
544c6266a77SGreg Roach                            foreach ($row as $age) {
545c6266a77SGreg Roach                                $years = (int) ($age / self::DAYS_IN_YEAR);
546c6266a77SGreg Roach                                $this->fillYData($years, 0, 1, $x_axis, $z_axis, $ydata);
547c6266a77SGreg Roach                            }
548c6266a77SGreg Roach                        }
549c6266a77SGreg Roach                        break;
550c6266a77SGreg Roach                    case self::Z_AXIS_SEX:
551c6266a77SGreg Roach                        $z_axis = $this->axisSexes();
552c6266a77SGreg Roach                        foreach (array_keys($z_axis) as $sex) {
5539219296aSGreg Roach                            $rows = $statistics->statsAgeQuery('DEAT', $sex);
554c6266a77SGreg Roach                            foreach ($rows as $row) {
555c6266a77SGreg Roach                                foreach ($row as $age) {
556c6266a77SGreg Roach                                    $years = (int) ($age / self::DAYS_IN_YEAR);
557c6266a77SGreg Roach                                    $this->fillYData($years, $sex, 1, $x_axis, $z_axis, $ydata);
558c6266a77SGreg Roach                                }
559c6266a77SGreg Roach                            }
560c6266a77SGreg Roach                        }
561c6266a77SGreg Roach                        break;
562c6266a77SGreg Roach                    case self::Z_AXIS_TIME:
563b6b9dcc9SGreg Roach                        $boundaries_csv = $params['z-axis-boundaries-periods'];
564c6266a77SGreg Roach                        $z_axis         = $this->axisYears($boundaries_csv);
565c6266a77SGreg Roach                        $prev_boundary  = 0;
566c6266a77SGreg Roach                        foreach (array_keys($z_axis) as $boundary) {
5679219296aSGreg Roach                            $rows = $statistics->statsAgeQuery('DEAT', 'BOTH', $prev_boundary, $boundary);
568c6266a77SGreg Roach                            foreach ($rows as $row) {
569c6266a77SGreg Roach                                foreach ($row as $age) {
570c6266a77SGreg Roach                                    $years = (int) ($age / self::DAYS_IN_YEAR);
571c6266a77SGreg Roach                                    $this->fillYData($years, $boundary, 1, $x_axis, $z_axis, $ydata);
572c6266a77SGreg Roach                                }
573c6266a77SGreg Roach                            }
574c6266a77SGreg Roach                            $prev_boundary = $boundary + 1;
575c6266a77SGreg Roach                        }
576c6266a77SGreg Roach
577c6266a77SGreg Roach                        break;
578c6266a77SGreg Roach                    default:
579d501c45dSGreg Roach                        throw new HttpNotFoundException();
580c6266a77SGreg Roach                }
581c6266a77SGreg Roach
5826ccdf4f0SGreg Roach                return response($this->myPlot($chart_title, $x_axis, $x_axis_title, $ydata, $y_axis_title, $z_axis, $y_axis_type));
583c6266a77SGreg Roach
584c6266a77SGreg Roach            case self::X_AXIS_AGE_AT_MARRIAGE:
585c6266a77SGreg Roach                $chart_title    = I18N::translate('Age in year of marriage');
586c6266a77SGreg Roach                $x_axis_title   = I18N::translate('age');
587b6b9dcc9SGreg Roach                $boundaries_csv = $params['x-axis-boundaries-ages_m'];
588c6266a77SGreg Roach                $x_axis         = $this->axisNumbers($boundaries_csv);
589c6266a77SGreg Roach
590c6266a77SGreg Roach                switch ($y_axis_type) {
591c6266a77SGreg Roach                    case self::Y_AXIS_NUMBERS:
592c6266a77SGreg Roach                        $y_axis_title = I18N::translate('Individuals');
593c6266a77SGreg Roach                        break;
594c6266a77SGreg Roach                    case self::Y_AXIS_PERCENT:
595c6266a77SGreg Roach                        $y_axis_title = '%';
596c6266a77SGreg Roach                        break;
597c6266a77SGreg Roach                    default:
598d501c45dSGreg Roach                        throw new HttpNotFoundException();
599c6266a77SGreg Roach                }
600c6266a77SGreg Roach
601c6266a77SGreg Roach                switch ($z_axis_type) {
602c6266a77SGreg Roach                    case self::Z_AXIS_ALL:
603c6266a77SGreg Roach                        $z_axis = $this->axisAll();
604afa8d404SGreg Roach                        // The stats query doesn't have an "all" function, so query M/F separately
605afa8d404SGreg Roach                        foreach (['M', 'F'] as $sex) {
6069219296aSGreg Roach                            $rows = $statistics->statsMarrAgeQuery($sex);
607c6266a77SGreg Roach                            foreach ($rows as $row) {
608c6266a77SGreg Roach                                $years = (int) ($row->age / self::DAYS_IN_YEAR);
609c6266a77SGreg Roach                                $this->fillYData($years, 0, 1, $x_axis, $z_axis, $ydata);
610c6266a77SGreg Roach                            }
611c6266a77SGreg Roach                        }
612c6266a77SGreg Roach                        break;
613c6266a77SGreg Roach                    case self::Z_AXIS_SEX:
614c6266a77SGreg Roach                        $z_axis = $this->axisSexes();
615c6266a77SGreg Roach                        foreach (array_keys($z_axis) as $sex) {
6169219296aSGreg Roach                            $rows = $statistics->statsMarrAgeQuery($sex);
617c6266a77SGreg Roach                            foreach ($rows as $row) {
618c6266a77SGreg Roach                                $years = (int) ($row->age / self::DAYS_IN_YEAR);
619c6266a77SGreg Roach                                $this->fillYData($years, $sex, 1, $x_axis, $z_axis, $ydata);
620c6266a77SGreg Roach                            }
621c6266a77SGreg Roach                        }
622c6266a77SGreg Roach                        break;
623c6266a77SGreg Roach                    case self::Z_AXIS_TIME:
624b6b9dcc9SGreg Roach                        $boundaries_csv = $params['z-axis-boundaries-periods'];
625c6266a77SGreg Roach                        $z_axis         = $this->axisYears($boundaries_csv);
626afa8d404SGreg Roach                        // The stats query doesn't have an "all" function, so query M/F separately
627afa8d404SGreg Roach                        foreach (['M', 'F'] as $sex) {
628c6266a77SGreg Roach                            $prev_boundary = 0;
629c6266a77SGreg Roach                            foreach (array_keys($z_axis) as $boundary) {
6309219296aSGreg Roach                                $rows = $statistics->statsMarrAgeQuery($sex, $prev_boundary, $boundary);
631c6266a77SGreg Roach                                foreach ($rows as $row) {
632c6266a77SGreg Roach                                    $years = (int) ($row->age / self::DAYS_IN_YEAR);
633c6266a77SGreg Roach                                    $this->fillYData($years, $boundary, 1, $x_axis, $z_axis, $ydata);
634c6266a77SGreg Roach                                }
635c6266a77SGreg Roach                                $prev_boundary = $boundary + 1;
636c6266a77SGreg Roach                            }
637c6266a77SGreg Roach                        }
638c6266a77SGreg Roach                        break;
639c6266a77SGreg Roach                    default:
640d501c45dSGreg Roach                        throw new HttpNotFoundException();
641c6266a77SGreg Roach                }
642c6266a77SGreg Roach
6436ccdf4f0SGreg Roach                return response($this->myPlot($chart_title, $x_axis, $x_axis_title, $ydata, $y_axis_title, $z_axis, $y_axis_type));
644c6266a77SGreg Roach
645c6266a77SGreg Roach            case self::X_AXIS_AGE_AT_FIRST_MARRIAGE:
646c6266a77SGreg Roach                $chart_title    = I18N::translate('Age in year of first marriage');
647c6266a77SGreg Roach                $x_axis_title   = I18N::translate('age');
648b6b9dcc9SGreg Roach                $boundaries_csv = $params['x-axis-boundaries-ages_m'];
649c6266a77SGreg Roach                $x_axis         = $this->axisNumbers($boundaries_csv);
650c6266a77SGreg Roach
651c6266a77SGreg Roach                switch ($y_axis_type) {
652c6266a77SGreg Roach                    case self::Y_AXIS_NUMBERS:
653c6266a77SGreg Roach                        $y_axis_title = I18N::translate('Individuals');
654c6266a77SGreg Roach                        break;
655c6266a77SGreg Roach                    case self::Y_AXIS_PERCENT:
656c6266a77SGreg Roach                        $y_axis_title = '%';
657c6266a77SGreg Roach                        break;
658c6266a77SGreg Roach                    default:
659d501c45dSGreg Roach                        throw new HttpNotFoundException();
660c6266a77SGreg Roach                }
661c6266a77SGreg Roach
662c6266a77SGreg Roach                switch ($z_axis_type) {
663c6266a77SGreg Roach                    case self::Z_AXIS_ALL:
664c6266a77SGreg Roach                        $z_axis = $this->axisAll();
665afa8d404SGreg Roach                        // The stats query doesn't have an "all" function, so query M/F separately
666afa8d404SGreg Roach                        foreach (['M', 'F'] as $sex) {
6679219296aSGreg Roach                            $rows = $statistics->statsMarrAgeQuery($sex);
668c6266a77SGreg Roach                            $indi = [];
669c6266a77SGreg Roach                            foreach ($rows as $row) {
6706960d4a1SGreg Roach                                if (!in_array($row->d_gid, $indi, true)) {
671c6266a77SGreg Roach                                    $years = (int) ($row->age / self::DAYS_IN_YEAR);
672c6266a77SGreg Roach                                    $this->fillYData($years, 0, 1, $x_axis, $z_axis, $ydata);
673c6266a77SGreg Roach                                    $indi[] = $row->d_gid;
674c6266a77SGreg Roach                                }
675c6266a77SGreg Roach                            }
676c6266a77SGreg Roach                        }
677c6266a77SGreg Roach                        break;
678c6266a77SGreg Roach                    case self::Z_AXIS_SEX:
679c6266a77SGreg Roach                        $z_axis = $this->axisSexes();
680c6266a77SGreg Roach                        foreach (array_keys($z_axis) as $sex) {
6819219296aSGreg Roach                            $rows = $statistics->statsMarrAgeQuery($sex);
682c6266a77SGreg Roach                            $indi = [];
683c6266a77SGreg Roach                            foreach ($rows as $row) {
6846960d4a1SGreg Roach                                if (!in_array($row->d_gid, $indi, true)) {
685c6266a77SGreg Roach                                    $years = (int) ($row->age / self::DAYS_IN_YEAR);
686c6266a77SGreg Roach                                    $this->fillYData($years, $sex, 1, $x_axis, $z_axis, $ydata);
687c6266a77SGreg Roach                                    $indi[] = $row->d_gid;
688c6266a77SGreg Roach                                }
689c6266a77SGreg Roach                            }
690c6266a77SGreg Roach                        }
691c6266a77SGreg Roach                        break;
692c6266a77SGreg Roach                    case self::Z_AXIS_TIME:
693b6b9dcc9SGreg Roach                        $boundaries_csv = $params['z-axis-boundaries-periods'];
694c6266a77SGreg Roach                        $z_axis         = $this->axisYears($boundaries_csv);
695afa8d404SGreg Roach                        // The stats query doesn't have an "all" function, so query M/F separately
696afa8d404SGreg Roach                        foreach (['M', 'F'] as $sex) {
697c6266a77SGreg Roach                            $prev_boundary = 0;
698c6266a77SGreg Roach                            $indi          = [];
699c6266a77SGreg Roach                            foreach (array_keys($z_axis) as $boundary) {
7009219296aSGreg Roach                                $rows = $statistics->statsMarrAgeQuery($sex, $prev_boundary, $boundary);
701c6266a77SGreg Roach                                foreach ($rows as $row) {
7026960d4a1SGreg Roach                                    if (!in_array($row->d_gid, $indi, true)) {
703c6266a77SGreg Roach                                        $years = (int) ($row->age / self::DAYS_IN_YEAR);
704c6266a77SGreg Roach                                        $this->fillYData($years, $boundary, 1, $x_axis, $z_axis, $ydata);
705c6266a77SGreg Roach                                        $indi[] = $row->d_gid;
706c6266a77SGreg Roach                                    }
707c6266a77SGreg Roach                                }
708c6266a77SGreg Roach                                $prev_boundary = $boundary + 1;
709c6266a77SGreg Roach                            }
710c6266a77SGreg Roach                        }
711c6266a77SGreg Roach                        break;
712c6266a77SGreg Roach                    default:
713d501c45dSGreg Roach                        throw new HttpNotFoundException();
714c6266a77SGreg Roach                }
715c6266a77SGreg Roach
7166ccdf4f0SGreg Roach                return response($this->myPlot($chart_title, $x_axis, $x_axis_title, $ydata, $y_axis_title, $z_axis, $y_axis_type));
717c6266a77SGreg Roach
718c6266a77SGreg Roach            case self::X_AXIS_NUMBER_OF_CHILDREN:
719c6266a77SGreg Roach                $chart_title  = I18N::translate('Number of children');
720c6266a77SGreg Roach                $x_axis_title = I18N::translate('Children');
7216960d4a1SGreg Roach                $x_axis       = $this->axisNumbers('0,1,2,3,4,5,6,7,8,9,10');
722c6266a77SGreg Roach
723c6266a77SGreg Roach                switch ($y_axis_type) {
724c6266a77SGreg Roach                    case self::Y_AXIS_NUMBERS:
725c6266a77SGreg Roach                        $y_axis_title = I18N::translate('Families');
726c6266a77SGreg Roach                        break;
727c6266a77SGreg Roach                    case self::Y_AXIS_PERCENT:
728c6266a77SGreg Roach                        $y_axis_title = '%';
729c6266a77SGreg Roach                        break;
730c6266a77SGreg Roach                    default:
731d501c45dSGreg Roach                        throw new HttpNotFoundException();
732c6266a77SGreg Roach                }
733c6266a77SGreg Roach
734c6266a77SGreg Roach                switch ($z_axis_type) {
735c6266a77SGreg Roach                    case self::Z_AXIS_ALL:
736c6266a77SGreg Roach                        $z_axis = $this->axisAll();
7379219296aSGreg Roach                        $rows   = $statistics->statsChildrenQuery();
738c6266a77SGreg Roach                        foreach ($rows as $row) {
739c6266a77SGreg Roach                            $this->fillYData($row->f_numchil, 0, $row->total, $x_axis, $z_axis, $ydata);
740c6266a77SGreg Roach                        }
741c6266a77SGreg Roach                        break;
742c6266a77SGreg Roach                    case self::Z_AXIS_TIME:
743b6b9dcc9SGreg Roach                        $boundaries_csv = $params['z-axis-boundaries-periods'];
744c6266a77SGreg Roach                        $z_axis         = $this->axisYears($boundaries_csv);
745c6266a77SGreg Roach                        $prev_boundary  = 0;
746c6266a77SGreg Roach                        foreach (array_keys($z_axis) as $boundary) {
747b1126ab4SGreg Roach                            $rows = $statistics->statsChildrenQuery($prev_boundary, $boundary);
748c6266a77SGreg Roach                            foreach ($rows as $row) {
749c6266a77SGreg Roach                                $this->fillYData($row->f_numchil, $boundary, $row->total, $x_axis, $z_axis, $ydata);
750c6266a77SGreg Roach                            }
751c6266a77SGreg Roach                            $prev_boundary = $boundary + 1;
752c6266a77SGreg Roach                        }
753c6266a77SGreg Roach                        break;
754c6266a77SGreg Roach                    default:
755d501c45dSGreg Roach                        throw new HttpNotFoundException();
756c6266a77SGreg Roach                }
757c6266a77SGreg Roach
7586ccdf4f0SGreg Roach                return response($this->myPlot($chart_title, $x_axis, $x_axis_title, $ydata, $y_axis_title, $z_axis, $y_axis_type));
759c6266a77SGreg Roach
760c6266a77SGreg Roach            default:
761d501c45dSGreg Roach                throw new HttpNotFoundException();
762c6266a77SGreg Roach                break;
763c6266a77SGreg Roach        }
764c6266a77SGreg Roach    }
765c6266a77SGreg Roach
766c6266a77SGreg Roach    /**
767c6266a77SGreg Roach     * @return string[]
768c6266a77SGreg Roach     */
769c6266a77SGreg Roach    private function axisAll(): array
770c6266a77SGreg Roach    {
771c6266a77SGreg Roach        return [
772c6266a77SGreg Roach            I18N::translate('Total'),
773c6266a77SGreg Roach        ];
774c6266a77SGreg Roach    }
775c6266a77SGreg Roach
776c6266a77SGreg Roach    /**
777c6266a77SGreg Roach     * @return string[]
778c6266a77SGreg Roach     */
779c6266a77SGreg Roach    private function axisSexes(): array
780c6266a77SGreg Roach    {
781c6266a77SGreg Roach        return [
782c6266a77SGreg Roach            'M' => I18N::translate('Male'),
783c6266a77SGreg Roach            'F' => I18N::translate('Female'),
784c6266a77SGreg Roach        ];
785c6266a77SGreg Roach    }
786c6266a77SGreg Roach
787c6266a77SGreg Roach    /**
788c6266a77SGreg Roach     * Labels for the X axis
789c6266a77SGreg Roach     *
790c6266a77SGreg Roach     * @return string[]
791c6266a77SGreg Roach     */
792c6266a77SGreg Roach    private function axisMonths(): array
793c6266a77SGreg Roach    {
794c6266a77SGreg Roach        return [
795c6266a77SGreg Roach            'JAN' => I18N::translateContext('NOMINATIVE', 'January'),
796c6266a77SGreg Roach            'FEB' => I18N::translateContext('NOMINATIVE', 'February'),
797c6266a77SGreg Roach            'MAR' => I18N::translateContext('NOMINATIVE', 'March'),
798c6266a77SGreg Roach            'APR' => I18N::translateContext('NOMINATIVE', 'April'),
799c6266a77SGreg Roach            'MAY' => I18N::translateContext('NOMINATIVE', 'May'),
800c6266a77SGreg Roach            'JUN' => I18N::translateContext('NOMINATIVE', 'June'),
801c6266a77SGreg Roach            'JUL' => I18N::translateContext('NOMINATIVE', 'July'),
802c6266a77SGreg Roach            'AUG' => I18N::translateContext('NOMINATIVE', 'August'),
803c6266a77SGreg Roach            'SEP' => I18N::translateContext('NOMINATIVE', 'September'),
804c6266a77SGreg Roach            'OCT' => I18N::translateContext('NOMINATIVE', 'October'),
805c6266a77SGreg Roach            'NOV' => I18N::translateContext('NOMINATIVE', 'November'),
806c6266a77SGreg Roach            'DEC' => I18N::translateContext('NOMINATIVE', 'December'),
807c6266a77SGreg Roach        ];
808c6266a77SGreg Roach    }
809c6266a77SGreg Roach
810c6266a77SGreg Roach    /**
811c6266a77SGreg Roach     * Convert a list of N year-boundaries into N+1 year-ranges for the z-axis.
812c6266a77SGreg Roach     *
813c6266a77SGreg Roach     * @param string $boundaries_csv
814c6266a77SGreg Roach     *
815c6266a77SGreg Roach     * @return string[]
816c6266a77SGreg Roach     */
817c6266a77SGreg Roach    private function axisYears(string $boundaries_csv): array
818c6266a77SGreg Roach    {
819c6266a77SGreg Roach        $boundaries = explode(',', $boundaries_csv);
820c6266a77SGreg Roach
821c6266a77SGreg Roach        $axis = [];
822c6266a77SGreg Roach        foreach ($boundaries as $n => $boundary) {
823c6266a77SGreg Roach            if ($n === 0) {
824c6266a77SGreg Roach                $date = new Date('BEF ' . $boundary);
825c6266a77SGreg Roach            } else {
826c6266a77SGreg Roach                $date = new Date('BET ' . $boundaries[$n - 1] . ' AND ' . ($boundary - 1));
827c6266a77SGreg Roach            }
828c6266a77SGreg Roach            $axis[$boundary - 1] = strip_tags($date->display());
829c6266a77SGreg Roach        }
830c6266a77SGreg Roach
831c6266a77SGreg Roach        $date              = new Date('AFT ' . $boundaries[count($boundaries) - 1]);
832c6266a77SGreg Roach        $axis[PHP_INT_MAX] = strip_tags($date->display());
833c6266a77SGreg Roach
834c6266a77SGreg Roach        return $axis;
835c6266a77SGreg Roach    }
836c6266a77SGreg Roach
837c6266a77SGreg Roach    /**
838c6266a77SGreg Roach     * Create the X axis.
839c6266a77SGreg Roach     *
840c6266a77SGreg Roach     * @param string $boundaries_csv
841c6266a77SGreg Roach     *
842fc26b4f6SGreg Roach     * @return array<string>
843c6266a77SGreg Roach     */
844c6266a77SGreg Roach    private function axisNumbers(string $boundaries_csv): array
845c6266a77SGreg Roach    {
846c6266a77SGreg Roach        $boundaries = explode(',', $boundaries_csv);
847c6266a77SGreg Roach
8480b5fd0a6SGreg Roach        $boundaries = array_map(static function (string $x): int {
849c6266a77SGreg Roach            return (int) $x;
850c6266a77SGreg Roach        }, $boundaries);
851c6266a77SGreg Roach
852c6266a77SGreg Roach        $axis = [];
853c6266a77SGreg Roach        foreach ($boundaries as $n => $boundary) {
854c6266a77SGreg Roach            if ($n === 0) {
8556960d4a1SGreg Roach                $prev_boundary = 0;
8566960d4a1SGreg Roach            } else {
8576960d4a1SGreg Roach                $prev_boundary = $boundaries[$n - 1] + 1;
8586960d4a1SGreg Roach            }
8596960d4a1SGreg Roach
8606960d4a1SGreg Roach            if ($prev_boundary === $boundary) {
861c6266a77SGreg Roach                /* I18N: A range of numbers */
8626960d4a1SGreg Roach                $axis[$boundary] = I18N::number($boundary);
863c6266a77SGreg Roach            } else {
864c6266a77SGreg Roach                /* I18N: A range of numbers */
8656960d4a1SGreg Roach                $axis[$boundary] = I18N::translate('%1$s–%2$s', I18N::number($prev_boundary), I18N::number($boundary));
866c6266a77SGreg Roach            }
867c6266a77SGreg Roach        }
868c6266a77SGreg Roach
869c6266a77SGreg Roach        /* I18N: Label on a graph; 40+ means 40 or more */
870c6266a77SGreg Roach        $axis[PHP_INT_MAX] = I18N::translate('%s+', I18N::number($boundaries[count($boundaries) - 1]));
871c6266a77SGreg Roach
872c6266a77SGreg Roach        return $axis;
873c6266a77SGreg Roach    }
874c6266a77SGreg Roach
875c6266a77SGreg Roach    /**
876c6266a77SGreg Roach     * Calculate the Y axis.
877c6266a77SGreg Roach     *
878c6266a77SGreg Roach     * @param int|string $x
879c6266a77SGreg Roach     * @param int|string $z
880c6266a77SGreg Roach     * @param int|string $value
881c6266a77SGreg Roach     * @param array      $x_axis
882c6266a77SGreg Roach     * @param array      $z_axis
883c6266a77SGreg Roach     * @param int[][]    $ydata
884c6266a77SGreg Roach     *
885c6266a77SGreg Roach     * @return void
886c6266a77SGreg Roach     */
887e364afe4SGreg Roach    private function fillYData($x, $z, $value, array $x_axis, array $z_axis, array &$ydata): void
888c6266a77SGreg Roach    {
889c6266a77SGreg Roach        $x = $this->findAxisEntry($x, $x_axis);
890c6266a77SGreg Roach        $z = $this->findAxisEntry($z, $z_axis);
891c6266a77SGreg Roach
8926960d4a1SGreg Roach        if (!array_key_exists($z, $z_axis)) {
893c6266a77SGreg Roach            foreach (array_keys($z_axis) as $key) {
894c6266a77SGreg Roach                if ($value <= $key) {
895c6266a77SGreg Roach                    $z = $key;
896c6266a77SGreg Roach                    break;
897c6266a77SGreg Roach                }
898c6266a77SGreg Roach            }
899c6266a77SGreg Roach        }
900c6266a77SGreg Roach
901c6266a77SGreg Roach        // Add the value to the appropriate data point.
902c6266a77SGreg Roach        $ydata[$z][$x] = ($ydata[$z][$x] ?? 0) + $value;
903c6266a77SGreg Roach    }
904c6266a77SGreg Roach
905c6266a77SGreg Roach    /**
906c6266a77SGreg Roach     * Find the axis entry for a given value.
907c6266a77SGreg Roach     * Some are direct lookup (e.g. M/F, JAN/FEB/MAR).
908d823340dSGreg Roach     * Others need to find the appropriate range.
909c6266a77SGreg Roach     *
910c6266a77SGreg Roach     * @param int|float|string $value
911c6266a77SGreg Roach     * @param string[]         $axis
912c6266a77SGreg Roach     *
913c6266a77SGreg Roach     * @return int|string
914c6266a77SGreg Roach     */
915c6266a77SGreg Roach    private function findAxisEntry($value, $axis)
916c6266a77SGreg Roach    {
917c6266a77SGreg Roach        if (is_numeric($value)) {
918c6266a77SGreg Roach            $value = (int) $value;
919c6266a77SGreg Roach
9206960d4a1SGreg Roach            if (!array_key_exists($value, $axis)) {
921c6266a77SGreg Roach                foreach (array_keys($axis) as $boundary) {
922c6266a77SGreg Roach                    if ($value <= $boundary) {
923c6266a77SGreg Roach                        $value = $boundary;
924c6266a77SGreg Roach                        break;
925c6266a77SGreg Roach                    }
926c6266a77SGreg Roach                }
927c6266a77SGreg Roach            }
928c6266a77SGreg Roach        }
929c6266a77SGreg Roach
930c6266a77SGreg Roach        return $value;
931c6266a77SGreg Roach    }
932c6266a77SGreg Roach
933c6266a77SGreg Roach    /**
934c6266a77SGreg Roach     * Plot the data.
935c6266a77SGreg Roach     *
936c6266a77SGreg Roach     * @param string   $chart_title
937c6266a77SGreg Roach     * @param string[] $x_axis
938c6266a77SGreg Roach     * @param string   $x_axis_title
939c6266a77SGreg Roach     * @param int[][]  $ydata
940c6266a77SGreg Roach     * @param string   $y_axis_title
941c6266a77SGreg Roach     * @param string[] $z_axis
942c6266a77SGreg Roach     * @param int      $y_axis_type
943c6266a77SGreg Roach     *
944c6266a77SGreg Roach     * @return string
945c6266a77SGreg Roach     */
946a81e5019SRico Sonntag    private function myPlot(
947a81e5019SRico Sonntag        string $chart_title,
948a81e5019SRico Sonntag        array $x_axis,
949a81e5019SRico Sonntag        string $x_axis_title,
950a81e5019SRico Sonntag        array $ydata,
951a81e5019SRico Sonntag        string $y_axis_title,
952a81e5019SRico Sonntag        array $z_axis,
953a81e5019SRico Sonntag        int $y_axis_type
954a81e5019SRico Sonntag    ): string {
9556960d4a1SGreg Roach        if (!count($ydata)) {
956a81e5019SRico Sonntag            return I18N::translate('This information is not available.');
957c6266a77SGreg Roach        }
958c6266a77SGreg Roach
959c6266a77SGreg Roach        // Colors for z-axis
960c6266a77SGreg Roach        $colors = [];
961c6266a77SGreg Roach        $index  = 0;
9626960d4a1SGreg Roach        while (count($colors) < count($ydata)) {
963c6266a77SGreg Roach            $colors[] = self::Z_AXIS_COLORS[$index];
9646960d4a1SGreg Roach            $index    = ($index + 1) % count(self::Z_AXIS_COLORS);
965c6266a77SGreg Roach        }
966c6266a77SGreg Roach
967c6266a77SGreg Roach        // Convert our sparse dataset into a fixed-size array
968c6266a77SGreg Roach        $tmp = [];
969c6266a77SGreg Roach        foreach (array_keys($z_axis) as $z) {
970c6266a77SGreg Roach            foreach (array_keys($x_axis) as $x) {
971c6266a77SGreg Roach                $tmp[$z][$x] = $ydata[$z][$x] ?? 0;
972c6266a77SGreg Roach            }
973c6266a77SGreg Roach        }
974c6266a77SGreg Roach        $ydata = $tmp;
975c6266a77SGreg Roach
976a81e5019SRico Sonntag        // Convert the chart data to percentage
977c6266a77SGreg Roach        if ($y_axis_type === self::Y_AXIS_PERCENT) {
978c6266a77SGreg Roach            // Normalise each (non-zero!) set of data to total 100%
9790b5fd0a6SGreg Roach            array_walk($ydata, static function (array &$x) {
980c6266a77SGreg Roach                $sum = array_sum($x);
981c6266a77SGreg Roach                if ($sum > 0) {
9820b5fd0a6SGreg Roach                    $x = array_map(static function ($y) use ($sum) {
983c6266a77SGreg Roach                        return $y * 100.0 / $sum;
984c6266a77SGreg Roach                    }, $x);
985c6266a77SGreg Roach                }
986c6266a77SGreg Roach            });
987c6266a77SGreg Roach        }
988c6266a77SGreg Roach
989a81e5019SRico Sonntag        $data = [
990a81e5019SRico Sonntag            array_merge(
991a81e5019SRico Sonntag                [I18N::translate('Century')],
992a81e5019SRico Sonntag                array_values($z_axis)
99371378461SGreg Roach            ),
994c6266a77SGreg Roach        ];
995c6266a77SGreg Roach
996a81e5019SRico Sonntag        $intermediate = [];
997a81e5019SRico Sonntag        foreach ($ydata as $century => $months) {
998a81e5019SRico Sonntag            foreach ($months as $month => $value) {
999a81e5019SRico Sonntag                $intermediate[$month][] = [
1000a81e5019SRico Sonntag                    'v' => $value,
1001a81e5019SRico Sonntag                    'f' => ($y_axis_type === self::Y_AXIS_PERCENT) ? sprintf('%.1f%%', $value) : $value,
1002a81e5019SRico Sonntag                ];
1003a81e5019SRico Sonntag            }
1004c6266a77SGreg Roach        }
1005c6266a77SGreg Roach
1006a81e5019SRico Sonntag        foreach ($intermediate as $key => $values) {
1007a81e5019SRico Sonntag            $data[] = array_merge(
1008a81e5019SRico Sonntag                [$x_axis[$key]],
1009a81e5019SRico Sonntag                $values
1010a81e5019SRico Sonntag            );
1011a81e5019SRico Sonntag        }
1012c6266a77SGreg Roach
1013a81e5019SRico Sonntag        $chart_options = [
1014a81e5019SRico Sonntag            'title'    => '',
1015a81e5019SRico Sonntag            'subtitle' => '',
1016a81e5019SRico Sonntag            'height'   => 400,
1017a81e5019SRico Sonntag            'width'    => '100%',
1018a81e5019SRico Sonntag            'legend'   => [
10196960d4a1SGreg Roach                'position'  => count($z_axis) > 1 ? 'right' : 'none',
1020a81e5019SRico Sonntag                'alignment' => 'center',
1021a81e5019SRico Sonntag            ],
1022a81e5019SRico Sonntag            'tooltip'  => [
1023a81e5019SRico Sonntag                'format' => '\'%\'',
1024a81e5019SRico Sonntag            ],
1025a81e5019SRico Sonntag            'vAxis'    => [
1026a81e5019SRico Sonntag                'title' => $y_axis_title ?? '',
1027a81e5019SRico Sonntag            ],
1028a81e5019SRico Sonntag            'hAxis'    => [
1029a81e5019SRico Sonntag                'title' => $x_axis_title ?? '',
1030a81e5019SRico Sonntag            ],
1031a81e5019SRico Sonntag            'colors'   => $colors,
1032a81e5019SRico Sonntag        ];
1033a81e5019SRico Sonntag
103490a2f718SGreg Roach        return view('statistics/other/charts/custom', [
1035a81e5019SRico Sonntag            'data'          => $data,
1036a81e5019SRico Sonntag            'chart_options' => $chart_options,
1037a81e5019SRico Sonntag            'chart_title'   => $chart_title,
103865cf5706SGreg Roach            'language'      => I18N::languageTag(),
103990a2f718SGreg Roach        ]);
1040c6266a77SGreg Roach    }
1041168ff6f3Sric2016}
1042