xref: /webtrees/app/Module/StatisticsChartModule.php (revision 7413816e6dd2d50e569034fb804f3dce7471bb94)
1168ff6f3Sric2016<?php
23976b470SGreg Roach
3168ff6f3Sric2016/**
4168ff6f3Sric2016 * webtrees: online genealogy
5d11be702SGreg Roach * Copyright (C) 2023 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
1589f7189bSGreg 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;
2381b729d3SGreg Roachuse Fisharebest\Webtrees\Http\Exceptions\HttpNotFoundException;
24168ff6f3Sric2016use Fisharebest\Webtrees\I18N;
25168ff6f3Sric2016use Fisharebest\Webtrees\Individual;
26d35568b4SGreg Roachuse Fisharebest\Webtrees\Registry;
279219296aSGreg Roachuse Fisharebest\Webtrees\Statistics;
28b55cbc6bSGreg Roachuse Fisharebest\Webtrees\Validator;
296ccdf4f0SGreg Roachuse Psr\Http\Message\ResponseInterface;
306ccdf4f0SGreg Roachuse Psr\Http\Message\ServerRequestInterface;
313976b470SGreg Roach
326960d4a1SGreg Roachuse function array_key_exists;
336960d4a1SGreg Roachuse function array_keys;
346960d4a1SGreg Roachuse function array_map;
356960d4a1SGreg Roachuse function array_merge;
366960d4a1SGreg Roachuse function array_sum;
376960d4a1SGreg Roachuse function array_values;
386960d4a1SGreg Roachuse function array_walk;
395229eadeSGreg Roachuse function assert;
406960d4a1SGreg Roachuse function count;
416960d4a1SGreg Roachuse function explode;
426960d4a1SGreg Roachuse function in_array;
436bd19c8cSGreg Roachuse function intdiv;
446960d4a1SGreg Roachuse function is_numeric;
456960d4a1SGreg Roachuse function sprintf;
46168ff6f3Sric2016
47168ff6f3Sric2016/**
48168ff6f3Sric2016 * Class StatisticsChartModule
49168ff6f3Sric2016 */
5037eb8894SGreg Roachclass StatisticsChartModule extends AbstractModule implements ModuleChartInterface
51c1010edaSGreg Roach{
5249a243cbSGreg Roach    use ModuleChartTrait;
5349a243cbSGreg Roach
54c6266a77SGreg Roach    public const X_AXIS_INDIVIDUAL_MAP        = 1;
55c6266a77SGreg Roach    public const X_AXIS_BIRTH_MAP             = 2;
56c6266a77SGreg Roach    public const X_AXIS_DEATH_MAP             = 3;
57c6266a77SGreg Roach    public const X_AXIS_MARRIAGE_MAP          = 4;
58c6266a77SGreg Roach    public const X_AXIS_BIRTH_MONTH           = 11;
59c6266a77SGreg Roach    public const X_AXIS_DEATH_MONTH           = 12;
60c6266a77SGreg Roach    public const X_AXIS_MARRIAGE_MONTH        = 13;
61c6266a77SGreg Roach    public const X_AXIS_FIRST_CHILD_MONTH     = 14;
62c6266a77SGreg Roach    public const X_AXIS_FIRST_MARRIAGE_MONTH  = 15;
63c6266a77SGreg Roach    public const X_AXIS_AGE_AT_DEATH          = 18;
64c6266a77SGreg Roach    public const X_AXIS_AGE_AT_MARRIAGE       = 19;
65c6266a77SGreg Roach    public const X_AXIS_AGE_AT_FIRST_MARRIAGE = 20;
66c6266a77SGreg Roach    public const X_AXIS_NUMBER_OF_CHILDREN    = 21;
67c6266a77SGreg Roach
68c6266a77SGreg Roach    public const Y_AXIS_NUMBERS = 201;
69c6266a77SGreg Roach    public const Y_AXIS_PERCENT = 202;
70c6266a77SGreg Roach
71c6266a77SGreg Roach    public const Z_AXIS_ALL  = 300;
72c6266a77SGreg Roach    public const Z_AXIS_SEX  = 301;
73c6266a77SGreg Roach    public const Z_AXIS_TIME = 302;
74c6266a77SGreg Roach
75c6266a77SGreg Roach    // First two colors are blue/pink, to work with Z_AXIS_SEX.
76c6266a77SGreg Roach    private const Z_AXIS_COLORS = ['0000FF', 'FFA0CB', '9F00FF', 'FF7000', '905030', 'FF0000', '00FF00', 'F0F000'];
77c6266a77SGreg Roach
78c6266a77SGreg Roach    private const DAYS_IN_YEAR = 365.25;
79c6266a77SGreg Roach
80168ff6f3Sric2016    /**
810cfd6963SGreg Roach     * How should this module be identified in the control panel, etc.?
82168ff6f3Sric2016     *
83168ff6f3Sric2016     * @return string
84168ff6f3Sric2016     */
8549a243cbSGreg Roach    public function title(): string
86c1010edaSGreg Roach    {
87bbb76c12SGreg Roach        /* I18N: Name of a module/chart */
88bbb76c12SGreg Roach        return I18N::translate('Statistics');
89168ff6f3Sric2016    }
90168ff6f3Sric2016
9149a243cbSGreg Roach    public function description(): string
92c1010edaSGreg Roach    {
93bbb76c12SGreg Roach        /* I18N: Description of the “StatisticsChart” module */
94bbb76c12SGreg Roach        return I18N::translate('Various statistics charts.');
95168ff6f3Sric2016    }
96168ff6f3Sric2016
97168ff6f3Sric2016    /**
98377a2979SGreg Roach     * CSS class for the URL.
99377a2979SGreg Roach     *
100377a2979SGreg Roach     * @return string
101377a2979SGreg Roach     */
102377a2979SGreg Roach    public function chartMenuClass(): string
103377a2979SGreg Roach    {
104377a2979SGreg Roach        return 'menu-chart-statistics';
105377a2979SGreg Roach    }
106377a2979SGreg Roach
107377a2979SGreg Roach    /**
108e6562982SGreg Roach     * The URL for this chart.
109168ff6f3Sric2016     *
11060bc3e3fSGreg Roach     * @param Individual                                $individual
11176d39c55SGreg Roach     * @param array<bool|int|string|array<string>|null> $parameters
11260bc3e3fSGreg Roach     *
113e6562982SGreg Roach     * @return string
114168ff6f3Sric2016     */
115e6562982SGreg Roach    public function chartUrl(Individual $individual, array $parameters = []): string
116c1010edaSGreg Roach    {
117c6266a77SGreg Roach        return route('module', [
118c6266a77SGreg Roach                'module' => $this->name(),
119c6266a77SGreg Roach                'action' => 'Chart',
120d72b284aSGreg Roach                'tree'    => $individual->tree()->name(),
121e6562982SGreg Roach            ] + $parameters);
122168ff6f3Sric2016    }
123c6266a77SGreg Roach
124c6266a77SGreg Roach    /**
125c6266a77SGreg Roach     * A form to request the chart parameters.
126c6266a77SGreg Roach     *
12757ab2231SGreg Roach     * @param ServerRequestInterface $request
128c6266a77SGreg Roach     *
1296ccdf4f0SGreg Roach     * @return ResponseInterface
130c6266a77SGreg Roach     */
13157ab2231SGreg Roach    public function getChartAction(ServerRequestInterface $request): ResponseInterface
132c6266a77SGreg Roach    {
133b55cbc6bSGreg Roach        $tree = Validator::attributes($request)->tree();
134b55cbc6bSGreg Roach        $user = Validator::attributes($request)->user();
13557ab2231SGreg Roach
136ef483801SGreg Roach        Auth::checkComponentAccess($this, ModuleChartInterface::class, $tree, $user);
1379867b2f0SGreg Roach
138c6266a77SGreg Roach        $tabs = [
139c6266a77SGreg Roach            I18N::translate('Individuals') => route('module', [
140c6266a77SGreg Roach                'module' => $this->name(),
141c6266a77SGreg Roach                'action' => 'Individuals',
142d72b284aSGreg Roach                'tree'    => $tree->name(),
143c6266a77SGreg Roach            ]),
144c6266a77SGreg Roach            I18N::translate('Families')    => route('module', [
145c6266a77SGreg Roach                'module' => $this->name(),
146c6266a77SGreg Roach                'action' => 'Families',
147d72b284aSGreg Roach                'tree'    => $tree->name(),
148c6266a77SGreg Roach            ]),
149c6266a77SGreg Roach            I18N::translate('Other')       => route('module', [
150c6266a77SGreg Roach                'module' => $this->name(),
151c6266a77SGreg Roach                'action' => 'Other',
152d72b284aSGreg Roach                'tree'    => $tree->name(),
153c6266a77SGreg Roach            ]),
154c6266a77SGreg Roach            I18N::translate('Custom')      => route('module', [
155c6266a77SGreg Roach                'module' => $this->name(),
156c6266a77SGreg Roach                'action' => 'Custom',
157d72b284aSGreg Roach                'tree'    => $tree->name(),
158c6266a77SGreg Roach            ]),
159c6266a77SGreg Roach        ];
160c6266a77SGreg Roach
1619b5537c3SGreg Roach        return $this->viewResponse('modules/statistics-chart/page', [
16271378461SGreg Roach            'module' => $this->name(),
163c6266a77SGreg Roach            'tabs'   => $tabs,
164c6266a77SGreg Roach            'title'  => $this->title(),
165ef5d23f1SGreg Roach            'tree'   => $tree,
166c6266a77SGreg Roach        ]);
167c6266a77SGreg Roach    }
168c6266a77SGreg Roach
169c6266a77SGreg Roach    /**
17057ab2231SGreg Roach     * @param ServerRequestInterface $request
171c6266a77SGreg Roach     *
1726ccdf4f0SGreg Roach     * @return ResponseInterface
173c6266a77SGreg Roach     */
17449528f2bSGreg Roach    public function getIndividualsAction(ServerRequestInterface $request): ResponseInterface
175c6266a77SGreg Roach    {
176b6c326d8SGreg Roach        $this->layout = 'layouts/ajax';
177b6c326d8SGreg Roach
178b6c326d8SGreg Roach        return $this->viewResponse('modules/statistics-chart/individuals', [
179c6266a77SGreg Roach            'show_oldest_living' => Auth::check(),
180d35568b4SGreg Roach            'statistics'         => Registry::container()->get(Statistics::class),
181c6266a77SGreg Roach        ]);
182c6266a77SGreg Roach    }
183c6266a77SGreg Roach
184c6266a77SGreg Roach    /**
18557ab2231SGreg Roach     * @param ServerRequestInterface $request
186c6266a77SGreg Roach     *
1876ccdf4f0SGreg Roach     * @return ResponseInterface
188c6266a77SGreg Roach     */
18949528f2bSGreg Roach    public function getFamiliesAction(ServerRequestInterface $request): ResponseInterface
190c6266a77SGreg Roach    {
191b6c326d8SGreg Roach        $this->layout = 'layouts/ajax';
192b6c326d8SGreg Roach
193b6c326d8SGreg Roach        return $this->viewResponse('modules/statistics-chart/families', [
194d35568b4SGreg Roach            'statistics' => Registry::container()->get(Statistics::class),
195c6266a77SGreg Roach        ]);
196c6266a77SGreg Roach    }
197c6266a77SGreg Roach
198c6266a77SGreg Roach    /**
19957ab2231SGreg Roach     * @param ServerRequestInterface $request
200c6266a77SGreg Roach     *
2016ccdf4f0SGreg Roach     * @return ResponseInterface
202c6266a77SGreg Roach     */
20349528f2bSGreg Roach    public function getOtherAction(ServerRequestInterface $request): ResponseInterface
204c6266a77SGreg Roach    {
205b6c326d8SGreg Roach        $this->layout = 'layouts/ajax';
206b6c326d8SGreg Roach
207b6c326d8SGreg Roach        return $this->viewResponse('modules/statistics-chart/other', [
208d35568b4SGreg Roach            'statistics' => Registry::container()->get(Statistics::class),
209c6266a77SGreg Roach        ]);
210c6266a77SGreg Roach    }
211c6266a77SGreg Roach
212c6266a77SGreg Roach    /**
21357ab2231SGreg Roach     * @param ServerRequestInterface $request
214c6266a77SGreg Roach     *
2156ccdf4f0SGreg Roach     * @return ResponseInterface
216c6266a77SGreg Roach     */
21757ab2231SGreg Roach    public function getCustomAction(ServerRequestInterface $request): ResponseInterface
218c6266a77SGreg Roach    {
219b6c326d8SGreg Roach        $this->layout = 'layouts/ajax';
220b6c326d8SGreg Roach
221b55cbc6bSGreg Roach        $tree = Validator::attributes($request)->tree();
22257ab2231SGreg Roach
223b6c326d8SGreg Roach        return $this->viewResponse('modules/statistics-chart/custom', [
224c6266a77SGreg Roach            'module' => $this,
225c6266a77SGreg Roach            'tree'   => $tree,
226c6266a77SGreg Roach        ]);
227c6266a77SGreg Roach    }
228c6266a77SGreg Roach
229c6266a77SGreg Roach    /**
2306ccdf4f0SGreg Roach     * @param ServerRequestInterface $request
231c6266a77SGreg Roach     *
2326ccdf4f0SGreg Roach     * @return ResponseInterface
233c6266a77SGreg Roach     */
2343aa29b97SGreg Roach    public function postCustomChartAction(ServerRequestInterface $request): ResponseInterface
235c6266a77SGreg Roach    {
236d35568b4SGreg Roach        $statistics = Registry::container()->get(Statistics::class);
237dca2dd78SGreg Roach        assert($statistics instanceof Statistics);
23857ab2231SGreg Roach
239748dbe15SGreg Roach        $x_axis_type = Validator::parsedBody($request)->integer('x-as');
240748dbe15SGreg Roach        $y_axis_type = Validator::parsedBody($request)->integer('y-as');
241748dbe15SGreg Roach        $z_axis_type = Validator::parsedBody($request)->integer('z-as');
242c6266a77SGreg Roach        $ydata       = [];
243c6266a77SGreg Roach
244c6266a77SGreg Roach        switch ($x_axis_type) {
245c6266a77SGreg Roach            case self::X_AXIS_INDIVIDUAL_MAP:
2466ccdf4f0SGreg Roach                return response($statistics->chartDistribution(
247748dbe15SGreg Roach                    Validator::parsedBody($request)->string('chart_shows'),
248748dbe15SGreg Roach                    Validator::parsedBody($request)->string('chart_type'),
249748dbe15SGreg Roach                    Validator::parsedBody($request)->string('SURN')
250c6266a77SGreg Roach                ));
251c6266a77SGreg Roach
252c6266a77SGreg Roach            case self::X_AXIS_BIRTH_MAP:
2536ccdf4f0SGreg Roach                return response($statistics->chartDistribution(
254748dbe15SGreg Roach                    Validator::parsedBody($request)->string('chart_shows'),
255c6266a77SGreg Roach                    'birth_distribution_chart'
256c6266a77SGreg Roach                ));
257c6266a77SGreg Roach
258c6266a77SGreg Roach            case self::X_AXIS_DEATH_MAP:
2596ccdf4f0SGreg Roach                return response($statistics->chartDistribution(
260748dbe15SGreg Roach                    Validator::parsedBody($request)->string('chart_shows'),
261c6266a77SGreg Roach                    'death_distribution_chart'
262c6266a77SGreg Roach                ));
263c6266a77SGreg Roach
264c6266a77SGreg Roach            case self::X_AXIS_MARRIAGE_MAP:
2656ccdf4f0SGreg Roach                return response($statistics->chartDistribution(
266748dbe15SGreg Roach                    Validator::parsedBody($request)->string('chart_shows'),
267c6266a77SGreg Roach                    'marriage_distribution_chart'
268c6266a77SGreg Roach                ));
269c6266a77SGreg Roach
270c6266a77SGreg Roach            case self::X_AXIS_BIRTH_MONTH:
271c6266a77SGreg Roach                $chart_title  = I18N::translate('Month of birth');
272c6266a77SGreg Roach                $x_axis_title = I18N::translate('Month');
273c6266a77SGreg Roach                $x_axis       = $this->axisMonths();
274c6266a77SGreg Roach
275c6266a77SGreg Roach                switch ($y_axis_type) {
276c6266a77SGreg Roach                    case self::Y_AXIS_NUMBERS:
277c6266a77SGreg Roach                        $y_axis_title = I18N::translate('Individuals');
278c6266a77SGreg Roach                        break;
279c6266a77SGreg Roach                    case self::Y_AXIS_PERCENT:
280c6266a77SGreg Roach                        $y_axis_title = '%';
281c6266a77SGreg Roach                        break;
282c6266a77SGreg Roach                    default:
283d501c45dSGreg Roach                        throw new HttpNotFoundException();
284c6266a77SGreg Roach                }
285c6266a77SGreg Roach
286c6266a77SGreg Roach                switch ($z_axis_type) {
287c6266a77SGreg Roach                    case self::Z_AXIS_ALL:
288c6266a77SGreg Roach                        $z_axis = $this->axisAll();
289cde1d378SGreg Roach                        $rows   = $statistics->statsBirthQuery()->get();
290c6266a77SGreg Roach                        foreach ($rows as $row) {
291c6266a77SGreg Roach                            $this->fillYData($row->d_month, 0, $row->total, $x_axis, $z_axis, $ydata);
292c6266a77SGreg Roach                        }
293c6266a77SGreg Roach                        break;
294c6266a77SGreg Roach                    case self::Z_AXIS_SEX:
295c6266a77SGreg Roach                        $z_axis = $this->axisSexes();
296cde1d378SGreg Roach                        $rows   = $statistics->statsBirthBySexQuery()->get();
297c6266a77SGreg Roach                        foreach ($rows as $row) {
298c6266a77SGreg Roach                            $this->fillYData($row->d_month, $row->i_sex, $row->total, $x_axis, $z_axis, $ydata);
299c6266a77SGreg Roach                        }
300c6266a77SGreg Roach                        break;
301c6266a77SGreg Roach                    case self::Z_AXIS_TIME:
302748dbe15SGreg Roach                        $boundaries_csv = Validator::parsedBody($request)->string('z-axis-boundaries-periods');
303c6266a77SGreg Roach                        $z_axis         = $this->axisYears($boundaries_csv);
304c6266a77SGreg Roach                        $prev_boundary  = 0;
305c6266a77SGreg Roach                        foreach (array_keys($z_axis) as $boundary) {
306cde1d378SGreg Roach                            $rows = $statistics->statsBirthQuery($prev_boundary, $boundary)->get();
307c6266a77SGreg Roach                            foreach ($rows as $row) {
308c6266a77SGreg Roach                                $this->fillYData($row->d_month, $boundary, $row->total, $x_axis, $z_axis, $ydata);
309c6266a77SGreg Roach                            }
310c6266a77SGreg Roach                            $prev_boundary = $boundary + 1;
311c6266a77SGreg Roach                        }
312c6266a77SGreg Roach                        break;
313c6266a77SGreg Roach                    default:
314d501c45dSGreg Roach                        throw new HttpNotFoundException();
315c6266a77SGreg Roach                }
316c6266a77SGreg Roach
3176ccdf4f0SGreg Roach                return response($this->myPlot($chart_title, $x_axis, $x_axis_title, $ydata, $y_axis_title, $z_axis, $y_axis_type));
318c6266a77SGreg Roach
319c6266a77SGreg Roach            case self::X_AXIS_DEATH_MONTH:
320c6266a77SGreg Roach                $chart_title  = I18N::translate('Month of death');
321c6266a77SGreg Roach                $x_axis_title = I18N::translate('Month');
322c6266a77SGreg Roach                $x_axis       = $this->axisMonths();
323c6266a77SGreg Roach
324c6266a77SGreg Roach                switch ($y_axis_type) {
325c6266a77SGreg Roach                    case self::Y_AXIS_NUMBERS:
326c6266a77SGreg Roach                        $y_axis_title = I18N::translate('Individuals');
327c6266a77SGreg Roach                        break;
328c6266a77SGreg Roach                    case self::Y_AXIS_PERCENT:
329c6266a77SGreg Roach                        $y_axis_title = '%';
330c6266a77SGreg Roach                        break;
331c6266a77SGreg Roach                    default:
332d501c45dSGreg Roach                        throw new HttpNotFoundException();
333c6266a77SGreg Roach                }
334c6266a77SGreg Roach
335c6266a77SGreg Roach                switch ($z_axis_type) {
336c6266a77SGreg Roach                    case self::Z_AXIS_ALL:
337c6266a77SGreg Roach                        $z_axis = $this->axisAll();
338cde1d378SGreg Roach                        $rows   = $statistics->statsDeathQuery()->get();
339c6266a77SGreg Roach                        foreach ($rows as $row) {
340c6266a77SGreg Roach                            $this->fillYData($row->d_month, 0, $row->total, $x_axis, $z_axis, $ydata);
341c6266a77SGreg Roach                        }
342c6266a77SGreg Roach                        break;
343c6266a77SGreg Roach                    case self::Z_AXIS_SEX:
344c6266a77SGreg Roach                        $z_axis = $this->axisSexes();
345cde1d378SGreg Roach                        $rows   = $statistics->statsDeathBySexQuery()->get();
346c6266a77SGreg Roach                        foreach ($rows as $row) {
347c6266a77SGreg Roach                            $this->fillYData($row->d_month, $row->i_sex, $row->total, $x_axis, $z_axis, $ydata);
348c6266a77SGreg Roach                        }
349c6266a77SGreg Roach                        break;
350c6266a77SGreg Roach                    case self::Z_AXIS_TIME:
351748dbe15SGreg Roach                        $boundaries_csv = Validator::parsedBody($request)->string('z-axis-boundaries-periods');
352c6266a77SGreg Roach                        $z_axis         = $this->axisYears($boundaries_csv);
353c6266a77SGreg Roach                        $prev_boundary  = 0;
354c6266a77SGreg Roach                        foreach (array_keys($z_axis) as $boundary) {
355cde1d378SGreg Roach                            $rows = $statistics->statsDeathQuery($prev_boundary, $boundary)->get();
356c6266a77SGreg Roach                            foreach ($rows as $row) {
357c6266a77SGreg Roach                                $this->fillYData($row->d_month, $boundary, $row->total, $x_axis, $z_axis, $ydata);
358c6266a77SGreg Roach                            }
359c6266a77SGreg Roach                            $prev_boundary = $boundary + 1;
360c6266a77SGreg Roach                        }
361c6266a77SGreg Roach                        break;
362c6266a77SGreg Roach                    default:
363d501c45dSGreg Roach                        throw new HttpNotFoundException();
364c6266a77SGreg Roach                }
365c6266a77SGreg Roach
3666ccdf4f0SGreg Roach                return response($this->myPlot($chart_title, $x_axis, $x_axis_title, $ydata, $y_axis_title, $z_axis, $y_axis_type));
367c6266a77SGreg Roach
368c6266a77SGreg Roach            case self::X_AXIS_MARRIAGE_MONTH:
369c6266a77SGreg Roach                $chart_title  = I18N::translate('Month of marriage');
370c6266a77SGreg Roach                $x_axis_title = I18N::translate('Month');
371c6266a77SGreg Roach                $x_axis       = $this->axisMonths();
372c6266a77SGreg Roach
373c6266a77SGreg Roach                switch ($y_axis_type) {
374c6266a77SGreg Roach                    case self::Y_AXIS_NUMBERS:
375c6266a77SGreg Roach                        $y_axis_title = I18N::translate('Families');
376c6266a77SGreg Roach                        break;
377c6266a77SGreg Roach                    case self::Y_AXIS_PERCENT:
378c6266a77SGreg Roach                        $y_axis_title = '%';
379c6266a77SGreg Roach                        break;
380c6266a77SGreg Roach                    default:
381d501c45dSGreg Roach                        throw new HttpNotFoundException();
382c6266a77SGreg Roach                }
383c6266a77SGreg Roach
384c6266a77SGreg Roach                switch ($z_axis_type) {
385c6266a77SGreg Roach                    case self::Z_AXIS_ALL:
386c6266a77SGreg Roach                        $z_axis = $this->axisAll();
387e6f3d5e2SGreg Roach                        $rows   = $statistics->statsMarriageQuery()->get();
388c6266a77SGreg Roach                        foreach ($rows as $row) {
389c6266a77SGreg Roach                            $this->fillYData($row->d_month, 0, $row->total, $x_axis, $z_axis, $ydata);
390c6266a77SGreg Roach                        }
391c6266a77SGreg Roach                        break;
392c6266a77SGreg Roach                    case self::Z_AXIS_TIME:
393748dbe15SGreg Roach                        $boundaries_csv = Validator::parsedBody($request)->string('z-axis-boundaries-periods');
394c6266a77SGreg Roach                        $z_axis         = $this->axisYears($boundaries_csv);
395c6266a77SGreg Roach                        $prev_boundary  = 0;
396c6266a77SGreg Roach                        foreach (array_keys($z_axis) as $boundary) {
397e6f3d5e2SGreg Roach                            $rows = $statistics->statsMarriageQuery($prev_boundary, $boundary)->get();
398c6266a77SGreg Roach                            foreach ($rows as $row) {
399c6266a77SGreg Roach                                $this->fillYData($row->d_month, $boundary, $row->total, $x_axis, $z_axis, $ydata);
400c6266a77SGreg Roach                            }
401c6266a77SGreg Roach                            $prev_boundary = $boundary + 1;
402c6266a77SGreg Roach                        }
403c6266a77SGreg Roach                        break;
404c6266a77SGreg Roach                    default:
405d501c45dSGreg Roach                        throw new HttpNotFoundException();
406c6266a77SGreg Roach                }
407c6266a77SGreg Roach
4086ccdf4f0SGreg Roach                return response($this->myPlot($chart_title, $x_axis, $x_axis_title, $ydata, $y_axis_title, $z_axis, $y_axis_type));
409c6266a77SGreg Roach
410c6266a77SGreg Roach            case self::X_AXIS_FIRST_CHILD_MONTH:
411c6266a77SGreg Roach                $chart_title  = I18N::translate('Month of birth of first child in a relation');
412c6266a77SGreg Roach                $x_axis_title = I18N::translate('Month');
413c6266a77SGreg Roach                $x_axis       = $this->axisMonths();
414c6266a77SGreg Roach
415c6266a77SGreg Roach                switch ($y_axis_type) {
416c6266a77SGreg Roach                    case self::Y_AXIS_NUMBERS:
417c6266a77SGreg Roach                        $y_axis_title = I18N::translate('Children');
418c6266a77SGreg Roach                        break;
419c6266a77SGreg Roach                    case self::Y_AXIS_PERCENT:
420c6266a77SGreg Roach                        $y_axis_title = '%';
421c6266a77SGreg Roach                        break;
422c6266a77SGreg Roach                    default:
423d501c45dSGreg Roach                        throw new HttpNotFoundException();
424c6266a77SGreg Roach                }
425c6266a77SGreg Roach
426c6266a77SGreg Roach                switch ($z_axis_type) {
427c6266a77SGreg Roach                    case self::Z_AXIS_ALL:
428c6266a77SGreg Roach                        $z_axis = $this->axisAll();
429999da590SGreg Roach                        $rows   = $statistics->monthFirstChildQuery()->get();
430c6266a77SGreg Roach                        foreach ($rows as $row) {
431c6266a77SGreg Roach                            $this->fillYData($row->d_month, 0, $row->total, $x_axis, $z_axis, $ydata);
432c6266a77SGreg Roach                        }
433c6266a77SGreg Roach                        break;
434c6266a77SGreg Roach                    case self::Z_AXIS_SEX:
435c6266a77SGreg Roach                        $z_axis = $this->axisSexes();
436999da590SGreg Roach                        $rows   = $statistics->monthFirstChildBySexQuery()->get();
437c6266a77SGreg Roach                        foreach ($rows as $row) {
438c6266a77SGreg Roach                            $this->fillYData($row->d_month, $row->i_sex, $row->total, $x_axis, $z_axis, $ydata);
439c6266a77SGreg Roach                        }
440c6266a77SGreg Roach                        break;
441c6266a77SGreg Roach                    case self::Z_AXIS_TIME:
442748dbe15SGreg Roach                        $boundaries_csv = Validator::parsedBody($request)->string('z-axis-boundaries-periods');
443c6266a77SGreg Roach                        $z_axis         = $this->axisYears($boundaries_csv);
444c6266a77SGreg Roach                        $prev_boundary  = 0;
445c6266a77SGreg Roach                        foreach (array_keys($z_axis) as $boundary) {
446999da590SGreg Roach                            $rows = $statistics->monthFirstChildQuery($prev_boundary, $boundary)->get();
447c6266a77SGreg Roach                            foreach ($rows as $row) {
448c6266a77SGreg Roach                                $this->fillYData($row->d_month, $boundary, $row->total, $x_axis, $z_axis, $ydata);
449c6266a77SGreg Roach                            }
450c6266a77SGreg Roach                            $prev_boundary = $boundary + 1;
451c6266a77SGreg Roach                        }
452c6266a77SGreg Roach                        break;
453c6266a77SGreg Roach                    default:
454d501c45dSGreg Roach                        throw new HttpNotFoundException();
455c6266a77SGreg Roach                }
456c6266a77SGreg Roach
4576ccdf4f0SGreg Roach                return response($this->myPlot($chart_title, $x_axis, $x_axis_title, $ydata, $y_axis_title, $z_axis, $y_axis_type));
458c6266a77SGreg Roach
459c6266a77SGreg Roach            case self::X_AXIS_FIRST_MARRIAGE_MONTH:
460c6266a77SGreg Roach                $chart_title  = I18N::translate('Month of first marriage');
461c6266a77SGreg Roach                $x_axis_title = I18N::translate('Month');
462c6266a77SGreg Roach                $x_axis       = $this->axisMonths();
463c6266a77SGreg Roach
464c6266a77SGreg Roach                switch ($y_axis_type) {
465c6266a77SGreg Roach                    case self::Y_AXIS_NUMBERS:
466c6266a77SGreg Roach                        $y_axis_title = I18N::translate('Families');
467c6266a77SGreg Roach                        break;
468c6266a77SGreg Roach                    case self::Y_AXIS_PERCENT:
469c6266a77SGreg Roach                        $y_axis_title = '%';
470c6266a77SGreg Roach                        break;
471c6266a77SGreg Roach                    default:
472d501c45dSGreg Roach                        throw new HttpNotFoundException();
473c6266a77SGreg Roach                }
474c6266a77SGreg Roach
475c6266a77SGreg Roach                switch ($z_axis_type) {
476c6266a77SGreg Roach                    case self::Z_AXIS_ALL:
477c6266a77SGreg Roach                        $z_axis = $this->axisAll();
478e6f3d5e2SGreg Roach                        $rows   = $statistics->statsFirstMarriageQuery()->get();
479c6266a77SGreg Roach                        $indi   = [];
480c6266a77SGreg Roach                        foreach ($rows as $row) {
481dca2dd78SGreg Roach                            if (!in_array($row->f_husb, $indi, true) && !in_array($row->f_wife, $indi, true)) {
482c6266a77SGreg Roach                                $this->fillYData($row->month, 0, 1, $x_axis, $z_axis, $ydata);
483c6266a77SGreg Roach                            }
484dca2dd78SGreg Roach                            $indi[]  = $row->f_husb;
485dca2dd78SGreg Roach                            $indi[]  = $row->f_wife;
486c6266a77SGreg Roach                        }
487c6266a77SGreg Roach                        break;
488c6266a77SGreg Roach                    case self::Z_AXIS_TIME:
489748dbe15SGreg Roach                        $boundaries_csv = Validator::parsedBody($request)->string('z-axis-boundaries-periods');
490c6266a77SGreg Roach                        $z_axis         = $this->axisYears($boundaries_csv);
491c6266a77SGreg Roach                        $prev_boundary  = 0;
492c6266a77SGreg Roach                        $indi           = [];
493c6266a77SGreg Roach                        foreach (array_keys($z_axis) as $boundary) {
494e6f3d5e2SGreg Roach                            $rows = $statistics->statsFirstMarriageQuery($prev_boundary, $boundary)->get();
495c6266a77SGreg Roach                            foreach ($rows as $row) {
496dca2dd78SGreg Roach                                if (!in_array($row->f_husb, $indi, true) && !in_array($row->f_wife, $indi, true)) {
497c6266a77SGreg Roach                                    $this->fillYData($row->month, $boundary, 1, $x_axis, $z_axis, $ydata);
498c6266a77SGreg Roach                                }
499dca2dd78SGreg Roach                                $indi[]  = $row->f_husb;
500dca2dd78SGreg Roach                                $indi[]  = $row->f_wife;
501c6266a77SGreg Roach                            }
502c6266a77SGreg Roach                            $prev_boundary = $boundary + 1;
503c6266a77SGreg Roach                        }
504c6266a77SGreg Roach                        break;
505c6266a77SGreg Roach                    default:
506d501c45dSGreg Roach                        throw new HttpNotFoundException();
507c6266a77SGreg Roach                }
508c6266a77SGreg Roach
5096ccdf4f0SGreg Roach                return response($this->myPlot($chart_title, $x_axis, $x_axis_title, $ydata, $y_axis_title, $z_axis, $y_axis_type));
510c6266a77SGreg Roach
511c6266a77SGreg Roach            case self::X_AXIS_AGE_AT_DEATH:
512c6266a77SGreg Roach                $chart_title    = I18N::translate('Average age at death');
513c6266a77SGreg Roach                $x_axis_title   = I18N::translate('age');
514748dbe15SGreg Roach                $boundaries_csv = Validator::parsedBody($request)->string('x-axis-boundaries-ages');
515c6266a77SGreg Roach                $x_axis         = $this->axisNumbers($boundaries_csv);
516c6266a77SGreg Roach
517c6266a77SGreg Roach                switch ($y_axis_type) {
518c6266a77SGreg Roach                    case self::Y_AXIS_NUMBERS:
519c6266a77SGreg Roach                        $y_axis_title = I18N::translate('Individuals');
520c6266a77SGreg Roach                        break;
521c6266a77SGreg Roach                    case self::Y_AXIS_PERCENT:
522c6266a77SGreg Roach                        $y_axis_title = '%';
523c6266a77SGreg Roach                        break;
524c6266a77SGreg Roach                    default:
525d501c45dSGreg Roach                        throw new HttpNotFoundException();
526c6266a77SGreg Roach                }
527c6266a77SGreg Roach
528c6266a77SGreg Roach                switch ($z_axis_type) {
529c6266a77SGreg Roach                    case self::Z_AXIS_ALL:
530c6266a77SGreg Roach                        $z_axis = $this->axisAll();
5319219296aSGreg Roach                        $rows   = $statistics->statsAgeQuery('DEAT');
532c6266a77SGreg Roach                        foreach ($rows as $row) {
533c6266a77SGreg Roach                            foreach ($row as $age) {
534c6266a77SGreg Roach                                $years = (int) ($age / self::DAYS_IN_YEAR);
535c6266a77SGreg Roach                                $this->fillYData($years, 0, 1, $x_axis, $z_axis, $ydata);
536c6266a77SGreg Roach                            }
537c6266a77SGreg Roach                        }
538c6266a77SGreg Roach                        break;
539c6266a77SGreg Roach                    case self::Z_AXIS_SEX:
540c6266a77SGreg Roach                        $z_axis = $this->axisSexes();
541c6266a77SGreg Roach                        foreach (array_keys($z_axis) as $sex) {
5429219296aSGreg Roach                            $rows = $statistics->statsAgeQuery('DEAT', $sex);
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, $sex, 1, $x_axis, $z_axis, $ydata);
547c6266a77SGreg Roach                                }
548c6266a77SGreg Roach                            }
549c6266a77SGreg Roach                        }
550c6266a77SGreg Roach                        break;
551c6266a77SGreg Roach                    case self::Z_AXIS_TIME:
552748dbe15SGreg Roach                        $boundaries_csv = Validator::parsedBody($request)->string('z-axis-boundaries-periods');
553c6266a77SGreg Roach                        $z_axis         = $this->axisYears($boundaries_csv);
554c6266a77SGreg Roach                        $prev_boundary  = 0;
555c6266a77SGreg Roach                        foreach (array_keys($z_axis) as $boundary) {
5569219296aSGreg Roach                            $rows = $statistics->statsAgeQuery('DEAT', 'BOTH', $prev_boundary, $boundary);
557c6266a77SGreg Roach                            foreach ($rows as $row) {
558c6266a77SGreg Roach                                foreach ($row as $age) {
559c6266a77SGreg Roach                                    $years = (int) ($age / self::DAYS_IN_YEAR);
560c6266a77SGreg Roach                                    $this->fillYData($years, $boundary, 1, $x_axis, $z_axis, $ydata);
561c6266a77SGreg Roach                                }
562c6266a77SGreg Roach                            }
563c6266a77SGreg Roach                            $prev_boundary = $boundary + 1;
564c6266a77SGreg Roach                        }
565c6266a77SGreg Roach
566c6266a77SGreg Roach                        break;
567c6266a77SGreg Roach                    default:
568d501c45dSGreg Roach                        throw new HttpNotFoundException();
569c6266a77SGreg Roach                }
570c6266a77SGreg Roach
5716ccdf4f0SGreg Roach                return response($this->myPlot($chart_title, $x_axis, $x_axis_title, $ydata, $y_axis_title, $z_axis, $y_axis_type));
572c6266a77SGreg Roach
573c6266a77SGreg Roach            case self::X_AXIS_AGE_AT_MARRIAGE:
574c6266a77SGreg Roach                $chart_title    = I18N::translate('Age in year of marriage');
575c6266a77SGreg Roach                $x_axis_title   = I18N::translate('age');
576748dbe15SGreg Roach                $boundaries_csv = Validator::parsedBody($request)->string('x-axis-boundaries-ages_m');
577c6266a77SGreg Roach                $x_axis         = $this->axisNumbers($boundaries_csv);
578c6266a77SGreg Roach
579c6266a77SGreg Roach                switch ($y_axis_type) {
580c6266a77SGreg Roach                    case self::Y_AXIS_NUMBERS:
581c6266a77SGreg Roach                        $y_axis_title = I18N::translate('Individuals');
582c6266a77SGreg Roach                        break;
583c6266a77SGreg Roach                    case self::Y_AXIS_PERCENT:
584c6266a77SGreg Roach                        $y_axis_title = '%';
585c6266a77SGreg Roach                        break;
586c6266a77SGreg Roach                    default:
587d501c45dSGreg Roach                        throw new HttpNotFoundException();
588c6266a77SGreg Roach                }
589c6266a77SGreg Roach
590c6266a77SGreg Roach                switch ($z_axis_type) {
591c6266a77SGreg Roach                    case self::Z_AXIS_ALL:
592c6266a77SGreg Roach                        $z_axis = $this->axisAll();
593afa8d404SGreg Roach                        // The stats query doesn't have an "all" function, so query M/F separately
594afa8d404SGreg Roach                        foreach (['M', 'F'] as $sex) {
5959219296aSGreg Roach                            $rows = $statistics->statsMarrAgeQuery($sex);
596c6266a77SGreg Roach                            foreach ($rows as $row) {
597c6266a77SGreg Roach                                $years = (int) ($row->age / self::DAYS_IN_YEAR);
598c6266a77SGreg Roach                                $this->fillYData($years, 0, 1, $x_axis, $z_axis, $ydata);
599c6266a77SGreg Roach                            }
600c6266a77SGreg Roach                        }
601c6266a77SGreg Roach                        break;
602c6266a77SGreg Roach                    case self::Z_AXIS_SEX:
603c6266a77SGreg Roach                        $z_axis = $this->axisSexes();
604c6266a77SGreg Roach                        foreach (array_keys($z_axis) 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, $sex, 1, $x_axis, $z_axis, $ydata);
609c6266a77SGreg Roach                            }
610c6266a77SGreg Roach                        }
611c6266a77SGreg Roach                        break;
612c6266a77SGreg Roach                    case self::Z_AXIS_TIME:
613748dbe15SGreg Roach                        $boundaries_csv = Validator::parsedBody($request)->string('z-axis-boundaries-periods');
614c6266a77SGreg Roach                        $z_axis         = $this->axisYears($boundaries_csv);
615afa8d404SGreg Roach                        // The stats query doesn't have an "all" function, so query M/F separately
616afa8d404SGreg Roach                        foreach (['M', 'F'] as $sex) {
617c6266a77SGreg Roach                            $prev_boundary = 0;
618c6266a77SGreg Roach                            foreach (array_keys($z_axis) as $boundary) {
6199219296aSGreg Roach                                $rows = $statistics->statsMarrAgeQuery($sex, $prev_boundary, $boundary);
620c6266a77SGreg Roach                                foreach ($rows as $row) {
621c6266a77SGreg Roach                                    $years = (int) ($row->age / self::DAYS_IN_YEAR);
622c6266a77SGreg Roach                                    $this->fillYData($years, $boundary, 1, $x_axis, $z_axis, $ydata);
623c6266a77SGreg Roach                                }
624c6266a77SGreg Roach                                $prev_boundary = $boundary + 1;
625c6266a77SGreg Roach                            }
626c6266a77SGreg Roach                        }
627c6266a77SGreg Roach                        break;
628c6266a77SGreg Roach                    default:
629d501c45dSGreg Roach                        throw new HttpNotFoundException();
630c6266a77SGreg Roach                }
631c6266a77SGreg Roach
6326ccdf4f0SGreg Roach                return response($this->myPlot($chart_title, $x_axis, $x_axis_title, $ydata, $y_axis_title, $z_axis, $y_axis_type));
633c6266a77SGreg Roach
634c6266a77SGreg Roach            case self::X_AXIS_AGE_AT_FIRST_MARRIAGE:
635c6266a77SGreg Roach                $chart_title    = I18N::translate('Age in year of first marriage');
636c6266a77SGreg Roach                $x_axis_title   = I18N::translate('age');
637748dbe15SGreg Roach                $boundaries_csv = Validator::parsedBody($request)->string('x-axis-boundaries-ages_m');
638c6266a77SGreg Roach                $x_axis         = $this->axisNumbers($boundaries_csv);
639c6266a77SGreg Roach
640c6266a77SGreg Roach                switch ($y_axis_type) {
641c6266a77SGreg Roach                    case self::Y_AXIS_NUMBERS:
642c6266a77SGreg Roach                        $y_axis_title = I18N::translate('Individuals');
643c6266a77SGreg Roach                        break;
644c6266a77SGreg Roach                    case self::Y_AXIS_PERCENT:
645c6266a77SGreg Roach                        $y_axis_title = '%';
646c6266a77SGreg Roach                        break;
647c6266a77SGreg Roach                    default:
648d501c45dSGreg Roach                        throw new HttpNotFoundException();
649c6266a77SGreg Roach                }
650c6266a77SGreg Roach
651c6266a77SGreg Roach                switch ($z_axis_type) {
652c6266a77SGreg Roach                    case self::Z_AXIS_ALL:
653c6266a77SGreg Roach                        $z_axis = $this->axisAll();
654afa8d404SGreg Roach                        // The stats query doesn't have an "all" function, so query M/F separately
655afa8d404SGreg Roach                        foreach (['M', 'F'] as $sex) {
6569219296aSGreg Roach                            $rows = $statistics->statsMarrAgeQuery($sex);
657c6266a77SGreg Roach                            $indi = [];
658c6266a77SGreg Roach                            foreach ($rows as $row) {
6596960d4a1SGreg Roach                                if (!in_array($row->d_gid, $indi, true)) {
660c6266a77SGreg Roach                                    $years = (int) ($row->age / self::DAYS_IN_YEAR);
661c6266a77SGreg Roach                                    $this->fillYData($years, 0, 1, $x_axis, $z_axis, $ydata);
662c6266a77SGreg Roach                                    $indi[] = $row->d_gid;
663c6266a77SGreg Roach                                }
664c6266a77SGreg Roach                            }
665c6266a77SGreg Roach                        }
666c6266a77SGreg Roach                        break;
667c6266a77SGreg Roach                    case self::Z_AXIS_SEX:
668c6266a77SGreg Roach                        $z_axis = $this->axisSexes();
669c6266a77SGreg Roach                        foreach (array_keys($z_axis) as $sex) {
6709219296aSGreg Roach                            $rows = $statistics->statsMarrAgeQuery($sex);
671c6266a77SGreg Roach                            $indi = [];
672c6266a77SGreg Roach                            foreach ($rows as $row) {
6736960d4a1SGreg Roach                                if (!in_array($row->d_gid, $indi, true)) {
674c6266a77SGreg Roach                                    $years = (int) ($row->age / self::DAYS_IN_YEAR);
675c6266a77SGreg Roach                                    $this->fillYData($years, $sex, 1, $x_axis, $z_axis, $ydata);
676c6266a77SGreg Roach                                    $indi[] = $row->d_gid;
677c6266a77SGreg Roach                                }
678c6266a77SGreg Roach                            }
679c6266a77SGreg Roach                        }
680c6266a77SGreg Roach                        break;
681c6266a77SGreg Roach                    case self::Z_AXIS_TIME:
682748dbe15SGreg Roach                        $boundaries_csv = Validator::parsedBody($request)->string('z-axis-boundaries-periods');
683c6266a77SGreg Roach                        $z_axis         = $this->axisYears($boundaries_csv);
684afa8d404SGreg Roach                        // The stats query doesn't have an "all" function, so query M/F separately
685afa8d404SGreg Roach                        foreach (['M', 'F'] as $sex) {
686c6266a77SGreg Roach                            $prev_boundary = 0;
687c6266a77SGreg Roach                            $indi          = [];
688c6266a77SGreg Roach                            foreach (array_keys($z_axis) as $boundary) {
6899219296aSGreg Roach                                $rows = $statistics->statsMarrAgeQuery($sex, $prev_boundary, $boundary);
690c6266a77SGreg Roach                                foreach ($rows as $row) {
6916960d4a1SGreg Roach                                    if (!in_array($row->d_gid, $indi, true)) {
692c6266a77SGreg Roach                                        $years = (int) ($row->age / self::DAYS_IN_YEAR);
693c6266a77SGreg Roach                                        $this->fillYData($years, $boundary, 1, $x_axis, $z_axis, $ydata);
694c6266a77SGreg Roach                                        $indi[] = $row->d_gid;
695c6266a77SGreg Roach                                    }
696c6266a77SGreg Roach                                }
697c6266a77SGreg Roach                                $prev_boundary = $boundary + 1;
698c6266a77SGreg Roach                            }
699c6266a77SGreg Roach                        }
700c6266a77SGreg Roach                        break;
701c6266a77SGreg Roach                    default:
702d501c45dSGreg Roach                        throw new HttpNotFoundException();
703c6266a77SGreg Roach                }
704c6266a77SGreg Roach
7056ccdf4f0SGreg Roach                return response($this->myPlot($chart_title, $x_axis, $x_axis_title, $ydata, $y_axis_title, $z_axis, $y_axis_type));
706c6266a77SGreg Roach
707c6266a77SGreg Roach            case self::X_AXIS_NUMBER_OF_CHILDREN:
708c6266a77SGreg Roach                $chart_title  = I18N::translate('Number of children');
709c6266a77SGreg Roach                $x_axis_title = I18N::translate('Children');
7106960d4a1SGreg Roach                $x_axis       = $this->axisNumbers('0,1,2,3,4,5,6,7,8,9,10');
711c6266a77SGreg Roach
712c6266a77SGreg Roach                switch ($y_axis_type) {
713c6266a77SGreg Roach                    case self::Y_AXIS_NUMBERS:
714c6266a77SGreg Roach                        $y_axis_title = I18N::translate('Families');
715c6266a77SGreg Roach                        break;
716c6266a77SGreg Roach                    case self::Y_AXIS_PERCENT:
717c6266a77SGreg Roach                        $y_axis_title = '%';
718c6266a77SGreg Roach                        break;
719c6266a77SGreg Roach                    default:
720d501c45dSGreg Roach                        throw new HttpNotFoundException();
721c6266a77SGreg Roach                }
722c6266a77SGreg Roach
723c6266a77SGreg Roach                switch ($z_axis_type) {
724c6266a77SGreg Roach                    case self::Z_AXIS_ALL:
725c6266a77SGreg Roach                        $z_axis = $this->axisAll();
7269219296aSGreg Roach                        $rows   = $statistics->statsChildrenQuery();
727c6266a77SGreg Roach                        foreach ($rows as $row) {
728c6266a77SGreg Roach                            $this->fillYData($row->f_numchil, 0, $row->total, $x_axis, $z_axis, $ydata);
729c6266a77SGreg Roach                        }
730c6266a77SGreg Roach                        break;
731c6266a77SGreg Roach                    case self::Z_AXIS_TIME:
732748dbe15SGreg Roach                        $boundaries_csv = Validator::parsedBody($request)->string('z-axis-boundaries-periods');
733c6266a77SGreg Roach                        $z_axis         = $this->axisYears($boundaries_csv);
734c6266a77SGreg Roach                        $prev_boundary  = 0;
735c6266a77SGreg Roach                        foreach (array_keys($z_axis) as $boundary) {
736b1126ab4SGreg Roach                            $rows = $statistics->statsChildrenQuery($prev_boundary, $boundary);
737c6266a77SGreg Roach                            foreach ($rows as $row) {
738c6266a77SGreg Roach                                $this->fillYData($row->f_numchil, $boundary, $row->total, $x_axis, $z_axis, $ydata);
739c6266a77SGreg Roach                            }
740c6266a77SGreg Roach                            $prev_boundary = $boundary + 1;
741c6266a77SGreg Roach                        }
742c6266a77SGreg Roach                        break;
743c6266a77SGreg Roach                    default:
744d501c45dSGreg Roach                        throw new HttpNotFoundException();
745c6266a77SGreg Roach                }
746c6266a77SGreg Roach
7476ccdf4f0SGreg Roach                return response($this->myPlot($chart_title, $x_axis, $x_axis_title, $ydata, $y_axis_title, $z_axis, $y_axis_type));
748c6266a77SGreg Roach
749c6266a77SGreg Roach            default:
750d501c45dSGreg Roach                throw new HttpNotFoundException();
751c6266a77SGreg Roach        }
752c6266a77SGreg Roach    }
753c6266a77SGreg Roach
754c6266a77SGreg Roach    /**
75524f2a3afSGreg Roach     * @return array<string>
756c6266a77SGreg Roach     */
757c6266a77SGreg Roach    private function axisAll(): array
758c6266a77SGreg Roach    {
759c6266a77SGreg Roach        return [
760c6266a77SGreg Roach            I18N::translate('Total'),
761c6266a77SGreg Roach        ];
762c6266a77SGreg Roach    }
763c6266a77SGreg Roach
764c6266a77SGreg Roach    /**
76524f2a3afSGreg Roach     * @return array<string>
766c6266a77SGreg Roach     */
767c6266a77SGreg Roach    private function axisSexes(): array
768c6266a77SGreg Roach    {
769c6266a77SGreg Roach        return [
770c6266a77SGreg Roach            'M' => I18N::translate('Male'),
771c6266a77SGreg Roach            'F' => I18N::translate('Female'),
772c6266a77SGreg Roach        ];
773c6266a77SGreg Roach    }
774c6266a77SGreg Roach
775c6266a77SGreg Roach    /**
776c6266a77SGreg Roach     * Labels for the X axis
777c6266a77SGreg Roach     *
77824f2a3afSGreg Roach     * @return array<string>
779c6266a77SGreg Roach     */
780c6266a77SGreg Roach    private function axisMonths(): array
781c6266a77SGreg Roach    {
782c6266a77SGreg Roach        return [
783c6266a77SGreg Roach            'JAN' => I18N::translateContext('NOMINATIVE', 'January'),
784c6266a77SGreg Roach            'FEB' => I18N::translateContext('NOMINATIVE', 'February'),
785c6266a77SGreg Roach            'MAR' => I18N::translateContext('NOMINATIVE', 'March'),
786c6266a77SGreg Roach            'APR' => I18N::translateContext('NOMINATIVE', 'April'),
787c6266a77SGreg Roach            'MAY' => I18N::translateContext('NOMINATIVE', 'May'),
788c6266a77SGreg Roach            'JUN' => I18N::translateContext('NOMINATIVE', 'June'),
789c6266a77SGreg Roach            'JUL' => I18N::translateContext('NOMINATIVE', 'July'),
790c6266a77SGreg Roach            'AUG' => I18N::translateContext('NOMINATIVE', 'August'),
791c6266a77SGreg Roach            'SEP' => I18N::translateContext('NOMINATIVE', 'September'),
792c6266a77SGreg Roach            'OCT' => I18N::translateContext('NOMINATIVE', 'October'),
793c6266a77SGreg Roach            'NOV' => I18N::translateContext('NOMINATIVE', 'November'),
794c6266a77SGreg Roach            'DEC' => I18N::translateContext('NOMINATIVE', 'December'),
795c6266a77SGreg Roach        ];
796c6266a77SGreg Roach    }
797c6266a77SGreg Roach
798c6266a77SGreg Roach    /**
799c6266a77SGreg Roach     * Convert a list of N year-boundaries into N+1 year-ranges for the z-axis.
800c6266a77SGreg Roach     *
801c6266a77SGreg Roach     * @param string $boundaries_csv
802c6266a77SGreg Roach     *
80324f2a3afSGreg Roach     * @return array<string>
804c6266a77SGreg Roach     */
805c6266a77SGreg Roach    private function axisYears(string $boundaries_csv): array
806c6266a77SGreg Roach    {
807c6266a77SGreg Roach        $boundaries = explode(',', $boundaries_csv);
808c6266a77SGreg Roach
809c6266a77SGreg Roach        $axis = [];
810c6266a77SGreg Roach        foreach ($boundaries as $n => $boundary) {
811c6266a77SGreg Roach            if ($n === 0) {
81267ff9eb7SGreg Roach                $axis[$boundary - 1] = '–' . I18N::digits($boundary);
813c6266a77SGreg Roach            } else {
81467ff9eb7SGreg Roach                $axis[$boundary - 1] = I18N::digits($boundaries[$n - 1]) . '–' . I18N::digits($boundary);
815c6266a77SGreg Roach            }
816c6266a77SGreg Roach        }
817c6266a77SGreg Roach
81867ff9eb7SGreg Roach        $axis[PHP_INT_MAX] = I18N::digits($boundaries[count($boundaries) - 1]) . '–';
819c6266a77SGreg Roach
820c6266a77SGreg Roach        return $axis;
821c6266a77SGreg Roach    }
822c6266a77SGreg Roach
823c6266a77SGreg Roach    /**
824c6266a77SGreg Roach     * Create the X axis.
825c6266a77SGreg Roach     *
826c6266a77SGreg Roach     * @param string $boundaries_csv
827c6266a77SGreg Roach     *
828fc26b4f6SGreg Roach     * @return array<string>
829c6266a77SGreg Roach     */
830c6266a77SGreg Roach    private function axisNumbers(string $boundaries_csv): array
831c6266a77SGreg Roach    {
832c6266a77SGreg Roach        $boundaries = explode(',', $boundaries_csv);
833c6266a77SGreg Roach
834*f25fc0f9SGreg Roach        $boundaries = array_map(static fn (string $x): int => (int) $x, $boundaries);
835c6266a77SGreg Roach
836c6266a77SGreg Roach        $axis = [];
837c6266a77SGreg Roach        foreach ($boundaries as $n => $boundary) {
838c6266a77SGreg Roach            if ($n === 0) {
8396960d4a1SGreg Roach                $prev_boundary = 0;
8406960d4a1SGreg Roach            } else {
8416960d4a1SGreg Roach                $prev_boundary = $boundaries[$n - 1] + 1;
8426960d4a1SGreg Roach            }
8436960d4a1SGreg Roach
8446960d4a1SGreg Roach            if ($prev_boundary === $boundary) {
845c6266a77SGreg Roach                /* I18N: A range of numbers */
8466960d4a1SGreg Roach                $axis[$boundary] = I18N::number($boundary);
847c6266a77SGreg Roach            } else {
848c6266a77SGreg Roach                /* I18N: A range of numbers */
8496960d4a1SGreg Roach                $axis[$boundary] = I18N::translate('%1$s–%2$s', I18N::number($prev_boundary), I18N::number($boundary));
850c6266a77SGreg Roach            }
851c6266a77SGreg Roach        }
852c6266a77SGreg Roach
853c6266a77SGreg Roach        /* I18N: Label on a graph; 40+ means 40 or more */
854c6266a77SGreg Roach        $axis[PHP_INT_MAX] = I18N::translate('%s+', I18N::number($boundaries[count($boundaries) - 1]));
855c6266a77SGreg Roach
856c6266a77SGreg Roach        return $axis;
857c6266a77SGreg Roach    }
858c6266a77SGreg Roach
859c6266a77SGreg Roach    /**
860c6266a77SGreg Roach     * Calculate the Y axis.
861c6266a77SGreg Roach     *
862c6266a77SGreg Roach     * @param int|string        $x
863c6266a77SGreg Roach     * @param int|string        $z
864c6266a77SGreg Roach     * @param int|string        $value
86576d39c55SGreg Roach     * @param array<string>     $x_axis
86676d39c55SGreg Roach     * @param array<string>     $z_axis
86709482a55SGreg Roach     * @param array<array<int>> $ydata
868c6266a77SGreg Roach     *
869c6266a77SGreg Roach     * @return void
870c6266a77SGreg Roach     */
871e364afe4SGreg Roach    private function fillYData($x, $z, $value, array $x_axis, array $z_axis, array &$ydata): void
872c6266a77SGreg Roach    {
873c6266a77SGreg Roach        $x = $this->findAxisEntry($x, $x_axis);
874c6266a77SGreg Roach        $z = $this->findAxisEntry($z, $z_axis);
875c6266a77SGreg Roach
8766960d4a1SGreg Roach        if (!array_key_exists($z, $z_axis)) {
877c6266a77SGreg Roach            foreach (array_keys($z_axis) as $key) {
878c6266a77SGreg Roach                if ($value <= $key) {
879c6266a77SGreg Roach                    $z = $key;
880c6266a77SGreg Roach                    break;
881c6266a77SGreg Roach                }
882c6266a77SGreg Roach            }
883c6266a77SGreg Roach        }
884c6266a77SGreg Roach
885c6266a77SGreg Roach        // Add the value to the appropriate data point.
886c6266a77SGreg Roach        $ydata[$z][$x] = ($ydata[$z][$x] ?? 0) + $value;
887c6266a77SGreg Roach    }
888c6266a77SGreg Roach
889c6266a77SGreg Roach    /**
890c6266a77SGreg Roach     * Find the axis entry for a given value.
891c6266a77SGreg Roach     * Some are direct lookup (e.g. M/F, JAN/FEB/MAR).
892d823340dSGreg Roach     * Others need to find the appropriate range.
893c6266a77SGreg Roach     *
89476d39c55SGreg Roach     * @param int|string    $value
89509482a55SGreg Roach     * @param array<string> $axis
896c6266a77SGreg Roach     *
897c6266a77SGreg Roach     * @return int|string
898c6266a77SGreg Roach     */
89924f2a3afSGreg Roach    private function findAxisEntry($value, array $axis)
900c6266a77SGreg Roach    {
901c6266a77SGreg Roach        if (is_numeric($value)) {
902c6266a77SGreg Roach            $value = (int) $value;
903c6266a77SGreg Roach
9046960d4a1SGreg Roach            if (!array_key_exists($value, $axis)) {
905c6266a77SGreg Roach                foreach (array_keys($axis) as $boundary) {
906c6266a77SGreg Roach                    if ($value <= $boundary) {
907c6266a77SGreg Roach                        $value = $boundary;
908c6266a77SGreg Roach                        break;
909c6266a77SGreg Roach                    }
910c6266a77SGreg Roach                }
911c6266a77SGreg Roach            }
912c6266a77SGreg Roach        }
913c6266a77SGreg Roach
914c6266a77SGreg Roach        return $value;
915c6266a77SGreg Roach    }
916c6266a77SGreg Roach
917c6266a77SGreg Roach    /**
918c6266a77SGreg Roach     * Plot the data.
919c6266a77SGreg Roach     *
920c6266a77SGreg Roach     * @param string            $chart_title
92109482a55SGreg Roach     * @param array<string>     $x_axis
922c6266a77SGreg Roach     * @param string            $x_axis_title
92309482a55SGreg Roach     * @param array<array<int>> $ydata
924c6266a77SGreg Roach     * @param string            $y_axis_title
92509482a55SGreg Roach     * @param array<string>     $z_axis
926c6266a77SGreg Roach     * @param int               $y_axis_type
927c6266a77SGreg Roach     *
928c6266a77SGreg Roach     * @return string
929c6266a77SGreg Roach     */
930a81e5019SRico Sonntag    private function myPlot(
931a81e5019SRico Sonntag        string $chart_title,
932a81e5019SRico Sonntag        array $x_axis,
933a81e5019SRico Sonntag        string $x_axis_title,
934a81e5019SRico Sonntag        array $ydata,
935a81e5019SRico Sonntag        string $y_axis_title,
936a81e5019SRico Sonntag        array $z_axis,
937a81e5019SRico Sonntag        int $y_axis_type
938a81e5019SRico Sonntag    ): string {
9396960d4a1SGreg Roach        if (!count($ydata)) {
940a81e5019SRico Sonntag            return I18N::translate('This information is not available.');
941c6266a77SGreg Roach        }
942c6266a77SGreg Roach
943c6266a77SGreg Roach        // Colors for z-axis
944c6266a77SGreg Roach        $colors = [];
945c6266a77SGreg Roach        $index  = 0;
9466960d4a1SGreg Roach        while (count($colors) < count($ydata)) {
947c6266a77SGreg Roach            $colors[] = self::Z_AXIS_COLORS[$index];
9486960d4a1SGreg Roach            $index    = ($index + 1) % count(self::Z_AXIS_COLORS);
949c6266a77SGreg Roach        }
950c6266a77SGreg Roach
951c6266a77SGreg Roach        // Convert our sparse dataset into a fixed-size array
952c6266a77SGreg Roach        $tmp = [];
953c6266a77SGreg Roach        foreach (array_keys($z_axis) as $z) {
954c6266a77SGreg Roach            foreach (array_keys($x_axis) as $x) {
955c6266a77SGreg Roach                $tmp[$z][$x] = $ydata[$z][$x] ?? 0;
956c6266a77SGreg Roach            }
957c6266a77SGreg Roach        }
958c6266a77SGreg Roach        $ydata = $tmp;
959c6266a77SGreg Roach
960a81e5019SRico Sonntag        // Convert the chart data to percentage
961c6266a77SGreg Roach        if ($y_axis_type === self::Y_AXIS_PERCENT) {
962c6266a77SGreg Roach            // Normalise each (non-zero!) set of data to total 100%
9630b5fd0a6SGreg Roach            array_walk($ydata, static function (array &$x) {
964c6266a77SGreg Roach                $sum = array_sum($x);
965c6266a77SGreg Roach                if ($sum > 0) {
9664c78e066SGreg Roach                    $x = array_map(static fn (float $y): float => $y * 100.0 / $sum, $x);
967c6266a77SGreg Roach                }
968c6266a77SGreg Roach            });
969c6266a77SGreg Roach        }
970c6266a77SGreg Roach
971a81e5019SRico Sonntag        $data = [
972a81e5019SRico Sonntag            array_merge(
973a81e5019SRico Sonntag                [I18N::translate('Century')],
974a81e5019SRico Sonntag                array_values($z_axis)
97571378461SGreg Roach            ),
976c6266a77SGreg Roach        ];
977c6266a77SGreg Roach
978a81e5019SRico Sonntag        $intermediate = [];
979d85de00fSGreg Roach        foreach ($ydata as $months) {
980a81e5019SRico Sonntag            foreach ($months as $month => $value) {
981a81e5019SRico Sonntag                $intermediate[$month][] = [
982a81e5019SRico Sonntag                    'v' => $value,
983d85de00fSGreg Roach                    'f' => $y_axis_type === self::Y_AXIS_PERCENT ? sprintf('%.1f%%', $value) : $value,
984a81e5019SRico Sonntag                ];
985a81e5019SRico Sonntag            }
986c6266a77SGreg Roach        }
987c6266a77SGreg Roach
988a81e5019SRico Sonntag        foreach ($intermediate as $key => $values) {
989a81e5019SRico Sonntag            $data[] = array_merge(
990a81e5019SRico Sonntag                [$x_axis[$key]],
991a81e5019SRico Sonntag                $values
992a81e5019SRico Sonntag            );
993a81e5019SRico Sonntag        }
994c6266a77SGreg Roach
995a81e5019SRico Sonntag        $chart_options = [
996a81e5019SRico Sonntag            'title'    => '',
997a81e5019SRico Sonntag            'subtitle' => '',
998a81e5019SRico Sonntag            'height'   => 400,
999a81e5019SRico Sonntag            'width'    => '100%',
1000a81e5019SRico Sonntag            'legend'   => [
10016960d4a1SGreg Roach                'position'  => count($z_axis) > 1 ? 'right' : 'none',
1002a81e5019SRico Sonntag                'alignment' => 'center',
1003a81e5019SRico Sonntag            ],
1004a81e5019SRico Sonntag            'tooltip'  => [
1005a81e5019SRico Sonntag                'format' => '\'%\'',
1006a81e5019SRico Sonntag            ],
1007a81e5019SRico Sonntag            'vAxis'    => [
100876d39c55SGreg Roach                'title' => $y_axis_title,
1009a81e5019SRico Sonntag            ],
1010a81e5019SRico Sonntag            'hAxis'    => [
101176d39c55SGreg Roach                'title' => $x_axis_title,
1012a81e5019SRico Sonntag            ],
1013a81e5019SRico Sonntag            'colors'   => $colors,
1014a81e5019SRico Sonntag        ];
1015a81e5019SRico Sonntag
101690a2f718SGreg Roach        return view('statistics/other/charts/custom', [
1017a81e5019SRico Sonntag            'data'          => $data,
1018a81e5019SRico Sonntag            'chart_options' => $chart_options,
1019a81e5019SRico Sonntag            'chart_title'   => $chart_title,
102065cf5706SGreg Roach            'language'      => I18N::languageTag(),
102190a2f718SGreg Roach        ]);
1022c6266a77SGreg Roach    }
1023168ff6f3Sric2016}
1024