StatisticsChartModule::postCustomChartAction()   F
last analyzed

Complexity

Conditions 106
Paths 124

Size

Total Lines 519
Code Lines 399

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 106
eloc 399
c 0
b 0
f 0
nc 124
nop 1
dl 0
loc 519
rs 3.1732

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
/**
4
 * webtrees: online genealogy
5
 * Copyright (C) 2022 webtrees development team
6
 * This program is free software: you can redistribute it and/or modify
7
 * it under the terms of the GNU General Public License as published by
8
 * the Free Software Foundation, either version 3 of the License, or
9
 * (at your option) any later version.
10
 * This program is distributed in the hope that it will be useful,
11
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
 * GNU General Public License for more details.
14
 * You should have received a copy of the GNU General Public License
15
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
16
 */
17
18
declare(strict_types=1);
19
20
namespace Fisharebest\Webtrees\Module;
21
22
use Fisharebest\Webtrees\Auth;
23
use Fisharebest\Webtrees\Exceptions\HttpNotFoundException;
24
use Fisharebest\Webtrees\I18N;
25
use Fisharebest\Webtrees\Individual;
26
use Fisharebest\Webtrees\Statistics;
27
use Fisharebest\Webtrees\Tree;
28
use Psr\Http\Message\ResponseInterface;
29
use Psr\Http\Message\ServerRequestInterface;
30
31
use function app;
32
use function array_key_exists;
33
use function array_keys;
34
use function array_map;
35
use function array_merge;
36
use function array_sum;
37
use function array_values;
38
use function array_walk;
39
use function assert;
40
use function count;
41
use function explode;
42
use function in_array;
43
use function is_numeric;
44
use function sprintf;
45
use function strip_tags;
46
47
/**
48
 * Class StatisticsChartModule
49
 */
50
class StatisticsChartModule extends AbstractModule implements ModuleChartInterface
51
{
52
    use ModuleChartTrait;
53
54
    public const X_AXIS_INDIVIDUAL_MAP        = 1;
55
    public const X_AXIS_BIRTH_MAP             = 2;
56
    public const X_AXIS_DEATH_MAP             = 3;
57
    public const X_AXIS_MARRIAGE_MAP          = 4;
58
    public const X_AXIS_BIRTH_MONTH           = 11;
59
    public const X_AXIS_DEATH_MONTH           = 12;
60
    public const X_AXIS_MARRIAGE_MONTH        = 13;
61
    public const X_AXIS_FIRST_CHILD_MONTH     = 14;
62
    public const X_AXIS_FIRST_MARRIAGE_MONTH  = 15;
63
    public const X_AXIS_AGE_AT_DEATH          = 18;
64
    public const X_AXIS_AGE_AT_MARRIAGE       = 19;
65
    public const X_AXIS_AGE_AT_FIRST_MARRIAGE = 20;
66
    public const X_AXIS_NUMBER_OF_CHILDREN    = 21;
67
68
    public const Y_AXIS_NUMBERS = 201;
69
    public const Y_AXIS_PERCENT = 202;
70
71
    public const Z_AXIS_ALL  = 300;
72
    public const Z_AXIS_SEX  = 301;
73
    public const Z_AXIS_TIME = 302;
74
75
    // First two colors are blue/pink, to work with Z_AXIS_SEX.
76
    private const Z_AXIS_COLORS = ['0000FF', 'FFA0CB', '9F00FF', 'FF7000', '905030', 'FF0000', '00FF00', 'F0F000'];
77
78
    private const DAYS_IN_YEAR = 365.25;
79
80
    /**
81
     * How should this module be identified in the control panel, etc.?
82
     *
83
     * @return string
84
     */
85
    public function title(): string
86
    {
87
        /* I18N: Name of a module/chart */
88
        return I18N::translate('Statistics');
89
    }
90
91
    /**
92
     * A sentence describing what this module does.
93
     *
94
     * @return string
95
     */
96
    public function description(): string
97
    {
98
        /* I18N: Description of the “StatisticsChart” module */
99
        return I18N::translate('Various statistics charts.');
100
    }
101
102
    /**
103
     * CSS class for the URL.
104
     *
105
     * @return string
106
     */
107
    public function chartMenuClass(): string
108
    {
109
        return 'menu-chart-statistics';
110
    }
111
112
    /**
113
     * The URL for this chart.
114
     *
115
     * @param Individual $individual
116
     * @param mixed[]    $parameters
117
     *
118
     * @return string
119
     */
120
    public function chartUrl(Individual $individual, array $parameters = []): string
121
    {
122
        return route('module', [
123
                'module' => $this->name(),
124
                'action' => 'Chart',
125
                'tree'    => $individual->tree()->name(),
126
            ] + $parameters);
127
    }
128
129
    /**
130
     * A form to request the chart parameters.
131
     *
132
     * @param ServerRequestInterface $request
133
     *
134
     * @return ResponseInterface
135
     */
136
    public function getChartAction(ServerRequestInterface $request): ResponseInterface
137
    {
138
        $tree = $request->getAttribute('tree');
139
        assert($tree instanceof Tree);
140
141
        $user = $request->getAttribute('user');
142
143
        Auth::checkComponentAccess($this, ModuleChartInterface::class, $tree, $user);
144
145
        $tabs = [
146
            I18N::translate('Individuals') => route('module', [
147
                'module' => $this->name(),
148
                'action' => 'Individuals',
149
                'tree'    => $tree->name(),
150
            ]),
151
            I18N::translate('Families')    => route('module', [
152
                'module' => $this->name(),
153
                'action' => 'Families',
154
                'tree'    => $tree->name(),
155
            ]),
156
            I18N::translate('Other')       => route('module', [
157
                'module' => $this->name(),
158
                'action' => 'Other',
159
                'tree'    => $tree->name(),
160
            ]),
161
            I18N::translate('Custom')      => route('module', [
162
                'module' => $this->name(),
163
                'action' => 'Custom',
164
                'tree'    => $tree->name(),
165
            ]),
166
        ];
167
168
        return $this->viewResponse('modules/statistics-chart/page', [
169
            'module' => $this->name(),
170
            'tabs'   => $tabs,
171
            'title'  => $this->title(),
172
            'tree'   => $tree,
173
        ]);
174
    }
175
176
    /**
177
     * @param ServerRequestInterface $request
178
     *
179
     * @return ResponseInterface
180
     */
181
    public function getIndividualsAction(ServerRequestInterface $request): ResponseInterface
0 ignored issues
show
Unused Code introduced by
The parameter $request is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

181
    public function getIndividualsAction(/** @scrutinizer ignore-unused */ ServerRequestInterface $request): ResponseInterface

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
182
    {
183
        $this->layout = 'layouts/ajax';
184
185
        return $this->viewResponse('modules/statistics-chart/individuals', [
186
            'show_oldest_living' => Auth::check(),
187
            'stats'              => app(Statistics::class),
188
        ]);
189
    }
190
191
    /**
192
     * @param ServerRequestInterface $request
193
     *
194
     * @return ResponseInterface
195
     */
196
    public function getFamiliesAction(ServerRequestInterface $request): ResponseInterface
0 ignored issues
show
Unused Code introduced by
The parameter $request is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

196
    public function getFamiliesAction(/** @scrutinizer ignore-unused */ ServerRequestInterface $request): ResponseInterface

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
197
    {
198
        $this->layout = 'layouts/ajax';
199
200
        return $this->viewResponse('modules/statistics-chart/families', [
201
            'stats' => app(Statistics::class),
202
        ]);
203
    }
204
205
    /**
206
     * @param ServerRequestInterface $request
207
     *
208
     * @return ResponseInterface
209
     */
210
    public function getOtherAction(ServerRequestInterface $request): ResponseInterface
0 ignored issues
show
Unused Code introduced by
The parameter $request is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

210
    public function getOtherAction(/** @scrutinizer ignore-unused */ ServerRequestInterface $request): ResponseInterface

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
211
    {
212
        $this->layout = 'layouts/ajax';
213
214
        return $this->viewResponse('modules/statistics-chart/other', [
215
            'stats' => app(Statistics::class),
216
        ]);
217
    }
218
219
    /**
220
     * @param ServerRequestInterface $request
221
     *
222
     * @return ResponseInterface
223
     */
224
    public function getCustomAction(ServerRequestInterface $request): ResponseInterface
225
    {
226
        $this->layout = 'layouts/ajax';
227
228
        $tree = $request->getAttribute('tree');
229
        assert($tree instanceof Tree);
230
231
        return $this->viewResponse('modules/statistics-chart/custom', [
232
            'module' => $this,
233
            'tree'   => $tree,
234
        ]);
235
    }
236
237
    /**
238
     * @param ServerRequestInterface $request
239
     *
240
     * @return ResponseInterface
241
     */
242
    public function postCustomChartAction(ServerRequestInterface $request): ResponseInterface
243
    {
244
        $statistics = app(Statistics::class);
245
        assert($statistics instanceof Statistics);
246
247
        $params = (array) $request->getParsedBody();
248
249
        $x_axis_type = (int) $params['x-as'];
250
        $y_axis_type = (int) $params['y-as'];
251
        $z_axis_type = (int) $params['z-as'];
252
        $ydata       = [];
253
254
        switch ($x_axis_type) {
255
            case self::X_AXIS_INDIVIDUAL_MAP:
256
                return response($statistics->chartDistribution(
257
                    $params['chart_shows'],
258
                    $params['chart_type'],
259
                    $params['SURN']
260
                ));
261
262
            case self::X_AXIS_BIRTH_MAP:
263
                return response($statistics->chartDistribution(
264
                    $params['chart_shows'],
265
                    'birth_distribution_chart'
266
                ));
267
268
            case self::X_AXIS_DEATH_MAP:
269
                return response($statistics->chartDistribution(
270
                    $params['chart_shows'],
271
                    'death_distribution_chart'
272
                ));
273
274
            case self::X_AXIS_MARRIAGE_MAP:
275
                return response($statistics->chartDistribution(
276
                    $params['chart_shows'],
277
                    'marriage_distribution_chart'
278
                ));
279
280
            case self::X_AXIS_BIRTH_MONTH:
281
                $chart_title  = I18N::translate('Month of birth');
282
                $x_axis_title = I18N::translate('Month');
283
                $x_axis       = $this->axisMonths();
284
285
                switch ($y_axis_type) {
286
                    case self::Y_AXIS_NUMBERS:
287
                        $y_axis_title = I18N::translate('Individuals');
288
                        break;
289
                    case self::Y_AXIS_PERCENT:
290
                        $y_axis_title = '%';
291
                        break;
292
                    default:
293
                        throw new HttpNotFoundException();
294
                }
295
296
                switch ($z_axis_type) {
297
                    case self::Z_AXIS_ALL:
298
                        $z_axis = $this->axisAll();
299
                        $rows   = $statistics->statsBirthQuery()->get();
300
                        foreach ($rows as $row) {
301
                            $this->fillYData($row->d_month, 0, $row->total, $x_axis, $z_axis, $ydata);
302
                        }
303
                        break;
304
                    case self::Z_AXIS_SEX:
305
                        $z_axis = $this->axisSexes();
306
                        $rows   = $statistics->statsBirthBySexQuery()->get();
307
                        foreach ($rows as $row) {
308
                            $this->fillYData($row->d_month, $row->i_sex, $row->total, $x_axis, $z_axis, $ydata);
309
                        }
310
                        break;
311
                    case self::Z_AXIS_TIME:
312
                        $boundaries_csv = $params['z-axis-boundaries-periods'];
313
                        $z_axis         = $this->axisYears($boundaries_csv);
314
                        $prev_boundary  = 0;
315
                        foreach (array_keys($z_axis) as $boundary) {
316
                            $rows = $statistics->statsBirthQuery($prev_boundary, $boundary)->get();
317
                            foreach ($rows as $row) {
318
                                $this->fillYData($row->d_month, $boundary, $row->total, $x_axis, $z_axis, $ydata);
319
                            }
320
                            $prev_boundary = $boundary + 1;
321
                        }
322
                        break;
323
                    default:
324
                        throw new HttpNotFoundException();
325
                }
326
327
                return response($this->myPlot($chart_title, $x_axis, $x_axis_title, $ydata, $y_axis_title, $z_axis, $y_axis_type));
328
329
            case self::X_AXIS_DEATH_MONTH:
330
                $chart_title  = I18N::translate('Month of death');
331
                $x_axis_title = I18N::translate('Month');
332
                $x_axis       = $this->axisMonths();
333
334
                switch ($y_axis_type) {
335
                    case self::Y_AXIS_NUMBERS:
336
                        $y_axis_title = I18N::translate('Individuals');
337
                        break;
338
                    case self::Y_AXIS_PERCENT:
339
                        $y_axis_title = '%';
340
                        break;
341
                    default:
342
                        throw new HttpNotFoundException();
343
                }
344
345
                switch ($z_axis_type) {
346
                    case self::Z_AXIS_ALL:
347
                        $z_axis = $this->axisAll();
348
                        $rows   = $statistics->statsDeathQuery()->get();
349
                        foreach ($rows as $row) {
350
                            $this->fillYData($row->d_month, 0, $row->total, $x_axis, $z_axis, $ydata);
351
                        }
352
                        break;
353
                    case self::Z_AXIS_SEX:
354
                        $z_axis = $this->axisSexes();
355
                        $rows   = $statistics->statsDeathBySexQuery()->get();
356
                        foreach ($rows as $row) {
357
                            $this->fillYData($row->d_month, $row->i_sex, $row->total, $x_axis, $z_axis, $ydata);
358
                        }
359
                        break;
360
                    case self::Z_AXIS_TIME:
361
                        $boundaries_csv = $params['z-axis-boundaries-periods'];
362
                        $z_axis         = $this->axisYears($boundaries_csv);
363
                        $prev_boundary  = 0;
364
                        foreach (array_keys($z_axis) as $boundary) {
365
                            $rows = $statistics->statsDeathQuery($prev_boundary, $boundary)->get();
366
                            foreach ($rows as $row) {
367
                                $this->fillYData($row->d_month, $boundary, $row->total, $x_axis, $z_axis, $ydata);
368
                            }
369
                            $prev_boundary = $boundary + 1;
370
                        }
371
                        break;
372
                    default:
373
                        throw new HttpNotFoundException();
374
                }
375
376
                return response($this->myPlot($chart_title, $x_axis, $x_axis_title, $ydata, $y_axis_title, $z_axis, $y_axis_type));
377
378
            case self::X_AXIS_MARRIAGE_MONTH:
379
                $chart_title  = I18N::translate('Month of marriage');
380
                $x_axis_title = I18N::translate('Month');
381
                $x_axis       = $this->axisMonths();
382
383
                switch ($y_axis_type) {
384
                    case self::Y_AXIS_NUMBERS:
385
                        $y_axis_title = I18N::translate('Families');
386
                        break;
387
                    case self::Y_AXIS_PERCENT:
388
                        $y_axis_title = '%';
389
                        break;
390
                    default:
391
                        throw new HttpNotFoundException();
392
                }
393
394
                switch ($z_axis_type) {
395
                    case self::Z_AXIS_ALL:
396
                        $z_axis = $this->axisAll();
397
                        $rows   = $statistics->statsMarriageQuery()->get();
398
                        foreach ($rows as $row) {
399
                            $this->fillYData($row->d_month, 0, $row->total, $x_axis, $z_axis, $ydata);
400
                        }
401
                        break;
402
                    case self::Z_AXIS_TIME:
403
                        $boundaries_csv = $params['z-axis-boundaries-periods'];
404
                        $z_axis         = $this->axisYears($boundaries_csv);
405
                        $prev_boundary  = 0;
406
                        foreach (array_keys($z_axis) as $boundary) {
407
                            $rows = $statistics->statsMarriageQuery($prev_boundary, $boundary)->get();
408
                            foreach ($rows as $row) {
409
                                $this->fillYData($row->d_month, $boundary, $row->total, $x_axis, $z_axis, $ydata);
410
                            }
411
                            $prev_boundary = $boundary + 1;
412
                        }
413
                        break;
414
                    default:
415
                        throw new HttpNotFoundException();
416
                }
417
418
                return response($this->myPlot($chart_title, $x_axis, $x_axis_title, $ydata, $y_axis_title, $z_axis, $y_axis_type));
419
420
            case self::X_AXIS_FIRST_CHILD_MONTH:
421
                $chart_title  = I18N::translate('Month of birth of first child in a relation');
422
                $x_axis_title = I18N::translate('Month');
423
                $x_axis       = $this->axisMonths();
424
425
                switch ($y_axis_type) {
426
                    case self::Y_AXIS_NUMBERS:
427
                        $y_axis_title = I18N::translate('Children');
428
                        break;
429
                    case self::Y_AXIS_PERCENT:
430
                        $y_axis_title = '%';
431
                        break;
432
                    default:
433
                        throw new HttpNotFoundException();
434
                }
435
436
                switch ($z_axis_type) {
437
                    case self::Z_AXIS_ALL:
438
                        $z_axis = $this->axisAll();
439
                        $rows   = $statistics->monthFirstChildQuery()->get();
440
                        foreach ($rows as $row) {
441
                            $this->fillYData($row->d_month, 0, $row->total, $x_axis, $z_axis, $ydata);
442
                        }
443
                        break;
444
                    case self::Z_AXIS_SEX:
445
                        $z_axis = $this->axisSexes();
446
                        $rows   = $statistics->monthFirstChildBySexQuery()->get();
447
                        foreach ($rows as $row) {
448
                            $this->fillYData($row->d_month, $row->i_sex, $row->total, $x_axis, $z_axis, $ydata);
449
                        }
450
                        break;
451
                    case self::Z_AXIS_TIME:
452
                        $boundaries_csv = $params['z-axis-boundaries-periods'];
453
                        $z_axis         = $this->axisYears($boundaries_csv);
454
                        $prev_boundary  = 0;
455
                        foreach (array_keys($z_axis) as $boundary) {
456
                            $rows = $statistics->monthFirstChildQuery($prev_boundary, $boundary)->get();
457
                            foreach ($rows as $row) {
458
                                $this->fillYData($row->d_month, $boundary, $row->total, $x_axis, $z_axis, $ydata);
459
                            }
460
                            $prev_boundary = $boundary + 1;
461
                        }
462
                        break;
463
                    default:
464
                        throw new HttpNotFoundException();
465
                }
466
467
                return response($this->myPlot($chart_title, $x_axis, $x_axis_title, $ydata, $y_axis_title, $z_axis, $y_axis_type));
468
469
            case self::X_AXIS_FIRST_MARRIAGE_MONTH:
470
                $chart_title  = I18N::translate('Month of first marriage');
471
                $x_axis_title = I18N::translate('Month');
472
                $x_axis       = $this->axisMonths();
473
474
                switch ($y_axis_type) {
475
                    case self::Y_AXIS_NUMBERS:
476
                        $y_axis_title = I18N::translate('Families');
477
                        break;
478
                    case self::Y_AXIS_PERCENT:
479
                        $y_axis_title = '%';
480
                        break;
481
                    default:
482
                        throw new HttpNotFoundException();
483
                }
484
485
                switch ($z_axis_type) {
486
                    case self::Z_AXIS_ALL:
487
                        $z_axis = $this->axisAll();
488
                        $rows   = $statistics->statsFirstMarriageQuery()->get();
489
                        $indi   = [];
490
                        foreach ($rows as $row) {
491
                            if (!in_array($row->f_husb, $indi, true) && !in_array($row->f_wife, $indi, true)) {
492
                                $this->fillYData($row->month, 0, 1, $x_axis, $z_axis, $ydata);
493
                            }
494
                            $indi[]  = $row->f_husb;
495
                            $indi[]  = $row->f_wife;
496
                        }
497
                        break;
498
                    case self::Z_AXIS_TIME:
499
                        $boundaries_csv = $params['z-axis-boundaries-periods'];
500
                        $z_axis         = $this->axisYears($boundaries_csv);
501
                        $prev_boundary  = 0;
502
                        $indi           = [];
503
                        foreach (array_keys($z_axis) as $boundary) {
504
                            $rows = $statistics->statsFirstMarriageQuery($prev_boundary, $boundary)->get();
505
                            foreach ($rows as $row) {
506
                                if (!in_array($row->f_husb, $indi, true) && !in_array($row->f_wife, $indi, true)) {
507
                                    $this->fillYData($row->month, $boundary, 1, $x_axis, $z_axis, $ydata);
508
                                }
509
                                $indi[]  = $row->f_husb;
510
                                $indi[]  = $row->f_wife;
511
                            }
512
                            $prev_boundary = $boundary + 1;
513
                        }
514
                        break;
515
                    default:
516
                        throw new HttpNotFoundException();
517
                }
518
519
                return response($this->myPlot($chart_title, $x_axis, $x_axis_title, $ydata, $y_axis_title, $z_axis, $y_axis_type));
520
521
            case self::X_AXIS_AGE_AT_DEATH:
522
                $chart_title    = I18N::translate('Average age at death');
523
                $x_axis_title   = I18N::translate('age');
524
                $boundaries_csv = $params['x-axis-boundaries-ages'];
525
                $x_axis         = $this->axisNumbers($boundaries_csv);
526
527
                switch ($y_axis_type) {
528
                    case self::Y_AXIS_NUMBERS:
529
                        $y_axis_title = I18N::translate('Individuals');
530
                        break;
531
                    case self::Y_AXIS_PERCENT:
532
                        $y_axis_title = '%';
533
                        break;
534
                    default:
535
                        throw new HttpNotFoundException();
536
                }
537
538
                switch ($z_axis_type) {
539
                    case self::Z_AXIS_ALL:
540
                        $z_axis = $this->axisAll();
541
                        $rows   = $statistics->statsAgeQuery('DEAT');
542
                        foreach ($rows as $row) {
543
                            foreach ($row as $age) {
544
                                $years = (int) ($age / self::DAYS_IN_YEAR);
545
                                $this->fillYData($years, 0, 1, $x_axis, $z_axis, $ydata);
546
                            }
547
                        }
548
                        break;
549
                    case self::Z_AXIS_SEX:
550
                        $z_axis = $this->axisSexes();
551
                        foreach (array_keys($z_axis) as $sex) {
552
                            $rows = $statistics->statsAgeQuery('DEAT', $sex);
553
                            foreach ($rows as $row) {
554
                                foreach ($row as $age) {
555
                                    $years = (int) ($age / self::DAYS_IN_YEAR);
556
                                    $this->fillYData($years, $sex, 1, $x_axis, $z_axis, $ydata);
557
                                }
558
                            }
559
                        }
560
                        break;
561
                    case self::Z_AXIS_TIME:
562
                        $boundaries_csv = $params['z-axis-boundaries-periods'];
563
                        $z_axis         = $this->axisYears($boundaries_csv);
564
                        $prev_boundary  = 0;
565
                        foreach (array_keys($z_axis) as $boundary) {
566
                            $rows = $statistics->statsAgeQuery('DEAT', 'BOTH', $prev_boundary, $boundary);
567
                            foreach ($rows as $row) {
568
                                foreach ($row as $age) {
569
                                    $years = (int) ($age / self::DAYS_IN_YEAR);
570
                                    $this->fillYData($years, $boundary, 1, $x_axis, $z_axis, $ydata);
571
                                }
572
                            }
573
                            $prev_boundary = $boundary + 1;
574
                        }
575
576
                        break;
577
                    default:
578
                        throw new HttpNotFoundException();
579
                }
580
581
                return response($this->myPlot($chart_title, $x_axis, $x_axis_title, $ydata, $y_axis_title, $z_axis, $y_axis_type));
582
583
            case self::X_AXIS_AGE_AT_MARRIAGE:
584
                $chart_title    = I18N::translate('Age in year of marriage');
585
                $x_axis_title   = I18N::translate('age');
586
                $boundaries_csv = $params['x-axis-boundaries-ages_m'];
587
                $x_axis         = $this->axisNumbers($boundaries_csv);
588
589
                switch ($y_axis_type) {
590
                    case self::Y_AXIS_NUMBERS:
591
                        $y_axis_title = I18N::translate('Individuals');
592
                        break;
593
                    case self::Y_AXIS_PERCENT:
594
                        $y_axis_title = '%';
595
                        break;
596
                    default:
597
                        throw new HttpNotFoundException();
598
                }
599
600
                switch ($z_axis_type) {
601
                    case self::Z_AXIS_ALL:
602
                        $z_axis = $this->axisAll();
603
                        // The stats query doesn't have an "all" function, so query M/F separately
604
                        foreach (['M', 'F'] as $sex) {
605
                            $rows = $statistics->statsMarrAgeQuery($sex);
606
                            foreach ($rows as $row) {
607
                                $years = (int) ($row->age / self::DAYS_IN_YEAR);
608
                                $this->fillYData($years, 0, 1, $x_axis, $z_axis, $ydata);
609
                            }
610
                        }
611
                        break;
612
                    case self::Z_AXIS_SEX:
613
                        $z_axis = $this->axisSexes();
614
                        foreach (array_keys($z_axis) as $sex) {
615
                            $rows = $statistics->statsMarrAgeQuery($sex);
616
                            foreach ($rows as $row) {
617
                                $years = (int) ($row->age / self::DAYS_IN_YEAR);
618
                                $this->fillYData($years, $sex, 1, $x_axis, $z_axis, $ydata);
619
                            }
620
                        }
621
                        break;
622
                    case self::Z_AXIS_TIME:
623
                        $boundaries_csv = $params['z-axis-boundaries-periods'];
624
                        $z_axis         = $this->axisYears($boundaries_csv);
625
                        // The stats query doesn't have an "all" function, so query M/F separately
626
                        foreach (['M', 'F'] as $sex) {
627
                            $prev_boundary = 0;
628
                            foreach (array_keys($z_axis) as $boundary) {
629
                                $rows = $statistics->statsMarrAgeQuery($sex, $prev_boundary, $boundary);
630
                                foreach ($rows as $row) {
631
                                    $years = (int) ($row->age / self::DAYS_IN_YEAR);
632
                                    $this->fillYData($years, $boundary, 1, $x_axis, $z_axis, $ydata);
633
                                }
634
                                $prev_boundary = $boundary + 1;
635
                            }
636
                        }
637
                        break;
638
                    default:
639
                        throw new HttpNotFoundException();
640
                }
641
642
                return response($this->myPlot($chart_title, $x_axis, $x_axis_title, $ydata, $y_axis_title, $z_axis, $y_axis_type));
643
644
            case self::X_AXIS_AGE_AT_FIRST_MARRIAGE:
645
                $chart_title    = I18N::translate('Age in year of first marriage');
646
                $x_axis_title   = I18N::translate('age');
647
                $boundaries_csv = $params['x-axis-boundaries-ages_m'];
648
                $x_axis         = $this->axisNumbers($boundaries_csv);
649
650
                switch ($y_axis_type) {
651
                    case self::Y_AXIS_NUMBERS:
652
                        $y_axis_title = I18N::translate('Individuals');
653
                        break;
654
                    case self::Y_AXIS_PERCENT:
655
                        $y_axis_title = '%';
656
                        break;
657
                    default:
658
                        throw new HttpNotFoundException();
659
                }
660
661
                switch ($z_axis_type) {
662
                    case self::Z_AXIS_ALL:
663
                        $z_axis = $this->axisAll();
664
                        // The stats query doesn't have an "all" function, so query M/F separately
665
                        foreach (['M', 'F'] as $sex) {
666
                            $rows = $statistics->statsMarrAgeQuery($sex);
667
                            $indi = [];
668
                            foreach ($rows as $row) {
669
                                if (!in_array($row->d_gid, $indi, true)) {
670
                                    $years = (int) ($row->age / self::DAYS_IN_YEAR);
671
                                    $this->fillYData($years, 0, 1, $x_axis, $z_axis, $ydata);
672
                                    $indi[] = $row->d_gid;
673
                                }
674
                            }
675
                        }
676
                        break;
677
                    case self::Z_AXIS_SEX:
678
                        $z_axis = $this->axisSexes();
679
                        foreach (array_keys($z_axis) as $sex) {
680
                            $rows = $statistics->statsMarrAgeQuery($sex);
681
                            $indi = [];
682
                            foreach ($rows as $row) {
683
                                if (!in_array($row->d_gid, $indi, true)) {
684
                                    $years = (int) ($row->age / self::DAYS_IN_YEAR);
685
                                    $this->fillYData($years, $sex, 1, $x_axis, $z_axis, $ydata);
686
                                    $indi[] = $row->d_gid;
687
                                }
688
                            }
689
                        }
690
                        break;
691
                    case self::Z_AXIS_TIME:
692
                        $boundaries_csv = $params['z-axis-boundaries-periods'];
693
                        $z_axis         = $this->axisYears($boundaries_csv);
694
                        // The stats query doesn't have an "all" function, so query M/F separately
695
                        foreach (['M', 'F'] as $sex) {
696
                            $prev_boundary = 0;
697
                            $indi          = [];
698
                            foreach (array_keys($z_axis) as $boundary) {
699
                                $rows = $statistics->statsMarrAgeQuery($sex, $prev_boundary, $boundary);
700
                                foreach ($rows as $row) {
701
                                    if (!in_array($row->d_gid, $indi, true)) {
702
                                        $years = (int) ($row->age / self::DAYS_IN_YEAR);
703
                                        $this->fillYData($years, $boundary, 1, $x_axis, $z_axis, $ydata);
704
                                        $indi[] = $row->d_gid;
705
                                    }
706
                                }
707
                                $prev_boundary = $boundary + 1;
708
                            }
709
                        }
710
                        break;
711
                    default:
712
                        throw new HttpNotFoundException();
713
                }
714
715
                return response($this->myPlot($chart_title, $x_axis, $x_axis_title, $ydata, $y_axis_title, $z_axis, $y_axis_type));
716
717
            case self::X_AXIS_NUMBER_OF_CHILDREN:
718
                $chart_title  = I18N::translate('Number of children');
719
                $x_axis_title = I18N::translate('Children');
720
                $x_axis       = $this->axisNumbers('0,1,2,3,4,5,6,7,8,9,10');
721
722
                switch ($y_axis_type) {
723
                    case self::Y_AXIS_NUMBERS:
724
                        $y_axis_title = I18N::translate('Families');
725
                        break;
726
                    case self::Y_AXIS_PERCENT:
727
                        $y_axis_title = '%';
728
                        break;
729
                    default:
730
                        throw new HttpNotFoundException();
731
                }
732
733
                switch ($z_axis_type) {
734
                    case self::Z_AXIS_ALL:
735
                        $z_axis = $this->axisAll();
736
                        $rows   = $statistics->statsChildrenQuery();
737
                        foreach ($rows as $row) {
738
                            $this->fillYData($row->f_numchil, 0, $row->total, $x_axis, $z_axis, $ydata);
739
                        }
740
                        break;
741
                    case self::Z_AXIS_TIME:
742
                        $boundaries_csv = $params['z-axis-boundaries-periods'];
743
                        $z_axis         = $this->axisYears($boundaries_csv);
744
                        $prev_boundary  = 0;
745
                        foreach (array_keys($z_axis) as $boundary) {
746
                            $rows = $statistics->statsChildrenQuery($prev_boundary, $boundary);
747
                            foreach ($rows as $row) {
748
                                $this->fillYData($row->f_numchil, $boundary, $row->total, $x_axis, $z_axis, $ydata);
749
                            }
750
                            $prev_boundary = $boundary + 1;
751
                        }
752
                        break;
753
                    default:
754
                        throw new HttpNotFoundException();
755
                }
756
757
                return response($this->myPlot($chart_title, $x_axis, $x_axis_title, $ydata, $y_axis_title, $z_axis, $y_axis_type));
758
759
            default:
760
                throw new HttpNotFoundException();
761
        }
762
    }
763
764
    /**
765
     * @return array<string>
766
     */
767
    private function axisAll(): array
768
    {
769
        return [
770
            I18N::translate('Total'),
771
        ];
772
    }
773
774
    /**
775
     * @return array<string>
776
     */
777
    private function axisSexes(): array
778
    {
779
        return [
780
            'M' => I18N::translate('Male'),
781
            'F' => I18N::translate('Female'),
782
        ];
783
    }
784
785
    /**
786
     * Labels for the X axis
787
     *
788
     * @return array<string>
789
     */
790
    private function axisMonths(): array
791
    {
792
        return [
793
            'JAN' => I18N::translateContext('NOMINATIVE', 'January'),
794
            'FEB' => I18N::translateContext('NOMINATIVE', 'February'),
795
            'MAR' => I18N::translateContext('NOMINATIVE', 'March'),
796
            'APR' => I18N::translateContext('NOMINATIVE', 'April'),
797
            'MAY' => I18N::translateContext('NOMINATIVE', 'May'),
798
            'JUN' => I18N::translateContext('NOMINATIVE', 'June'),
799
            'JUL' => I18N::translateContext('NOMINATIVE', 'July'),
800
            'AUG' => I18N::translateContext('NOMINATIVE', 'August'),
801
            'SEP' => I18N::translateContext('NOMINATIVE', 'September'),
802
            'OCT' => I18N::translateContext('NOMINATIVE', 'October'),
803
            'NOV' => I18N::translateContext('NOMINATIVE', 'November'),
804
            'DEC' => I18N::translateContext('NOMINATIVE', 'December'),
805
        ];
806
    }
807
808
    /**
809
     * Convert a list of N year-boundaries into N+1 year-ranges for the z-axis.
810
     *
811
     * @param string $boundaries_csv
812
     *
813
     * @return array<string>
814
     */
815
    private function axisYears(string $boundaries_csv): array
816
    {
817
        $boundaries = explode(',', $boundaries_csv);
818
819
        $axis = [];
820
        foreach ($boundaries as $n => $boundary) {
821
            if ($n === 0) {
822
                $axis[$boundary - 1] = '–' . I18N::digits($boundary);
823
            } else {
824
                $axis[$boundary - 1] = I18N::digits($boundaries[$n - 1]) . '–' . I18N::digits($boundary);
825
            }
826
        }
827
828
        $axis[PHP_INT_MAX] = I18N::digits($boundaries[count($boundaries) - 1]) . '–';
829
830
        return $axis;
831
    }
832
833
    /**
834
     * Create the X axis.
835
     *
836
     * @param string $boundaries_csv
837
     *
838
     * @return array<string>
839
     */
840
    private function axisNumbers(string $boundaries_csv): array
841
    {
842
        $boundaries = explode(',', $boundaries_csv);
843
844
        $boundaries = array_map(static function (string $x): int {
845
            return (int) $x;
846
        }, $boundaries);
847
848
        $axis = [];
849
        foreach ($boundaries as $n => $boundary) {
850
            if ($n === 0) {
851
                $prev_boundary = 0;
852
            } else {
853
                $prev_boundary = $boundaries[$n - 1] + 1;
854
            }
855
856
            if ($prev_boundary === $boundary) {
857
                /* I18N: A range of numbers */
858
                $axis[$boundary] = I18N::number($boundary);
859
            } else {
860
                /* I18N: A range of numbers */
861
                $axis[$boundary] = I18N::translate('%1$s–%2$s', I18N::number($prev_boundary), I18N::number($boundary));
862
            }
863
        }
864
865
        /* I18N: Label on a graph; 40+ means 40 or more */
866
        $axis[PHP_INT_MAX] = I18N::translate('%s+', I18N::number($boundaries[count($boundaries) - 1]));
867
868
        return $axis;
869
    }
870
871
    /**
872
     * Calculate the Y axis.
873
     *
874
     * @param int|string $x
875
     * @param int|string $z
876
     * @param int|string $value
877
     * @param array      $x_axis
878
     * @param array      $z_axis
879
     * @param int[][]    $ydata
880
     *
881
     * @return void
882
     */
883
    private function fillYData($x, $z, $value, array $x_axis, array $z_axis, array &$ydata): void
884
    {
885
        $x = $this->findAxisEntry($x, $x_axis);
886
        $z = $this->findAxisEntry($z, $z_axis);
887
888
        if (!array_key_exists($z, $z_axis)) {
889
            foreach (array_keys($z_axis) as $key) {
890
                if ($value <= $key) {
891
                    $z = $key;
892
                    break;
893
                }
894
            }
895
        }
896
897
        // Add the value to the appropriate data point.
898
        $ydata[$z][$x] = ($ydata[$z][$x] ?? 0) + $value;
899
    }
900
901
    /**
902
     * Find the axis entry for a given value.
903
     * Some are direct lookup (e.g. M/F, JAN/FEB/MAR).
904
     * Others need to find the appropriate range.
905
     *
906
     * @param int|float|string $value
907
     * @param string[]         $axis
908
     *
909
     * @return int|string
910
     */
911
    private function findAxisEntry($value, array $axis)
912
    {
913
        if (is_numeric($value)) {
914
            $value = (int) $value;
915
916
            if (!array_key_exists($value, $axis)) {
917
                foreach (array_keys($axis) as $boundary) {
918
                    if ($value <= $boundary) {
919
                        $value = $boundary;
920
                        break;
921
                    }
922
                }
923
            }
924
        }
925
926
        return $value;
927
    }
928
929
    /**
930
     * Plot the data.
931
     *
932
     * @param string   $chart_title
933
     * @param string[] $x_axis
934
     * @param string   $x_axis_title
935
     * @param int[][]  $ydata
936
     * @param string   $y_axis_title
937
     * @param string[] $z_axis
938
     * @param int      $y_axis_type
939
     *
940
     * @return string
941
     */
942
    private function myPlot(
943
        string $chart_title,
944
        array $x_axis,
945
        string $x_axis_title,
946
        array $ydata,
947
        string $y_axis_title,
948
        array $z_axis,
949
        int $y_axis_type
950
    ): string {
951
        if (!count($ydata)) {
952
            return I18N::translate('This information is not available.');
953
        }
954
955
        // Colors for z-axis
956
        $colors = [];
957
        $index  = 0;
958
        while (count($colors) < count($ydata)) {
959
            $colors[] = self::Z_AXIS_COLORS[$index];
960
            $index    = ($index + 1) % count(self::Z_AXIS_COLORS);
961
        }
962
963
        // Convert our sparse dataset into a fixed-size array
964
        $tmp = [];
965
        foreach (array_keys($z_axis) as $z) {
966
            foreach (array_keys($x_axis) as $x) {
967
                $tmp[$z][$x] = $ydata[$z][$x] ?? 0;
968
            }
969
        }
970
        $ydata = $tmp;
971
972
        // Convert the chart data to percentage
973
        if ($y_axis_type === self::Y_AXIS_PERCENT) {
974
            // Normalise each (non-zero!) set of data to total 100%
975
            array_walk($ydata, static function (array &$x) {
976
                $sum = array_sum($x);
977
                if ($sum > 0) {
978
                    $x = array_map(static function ($y) use ($sum) {
979
                        return $y * 100.0 / $sum;
980
                    }, $x);
981
                }
982
            });
983
        }
984
985
        $data = [
986
            array_merge(
987
                [I18N::translate('Century')],
988
                array_values($z_axis)
989
            ),
990
        ];
991
992
        $intermediate = [];
993
        foreach ($ydata as $century => $months) {
994
            foreach ($months as $month => $value) {
995
                $intermediate[$month][] = [
996
                    'v' => $value,
997
                    'f' => ($y_axis_type === self::Y_AXIS_PERCENT) ? sprintf('%.1f%%', $value) : $value,
998
                ];
999
            }
1000
        }
1001
1002
        foreach ($intermediate as $key => $values) {
1003
            $data[] = array_merge(
1004
                [$x_axis[$key]],
1005
                $values
1006
            );
1007
        }
1008
1009
        $chart_options = [
1010
            'title'    => '',
1011
            'subtitle' => '',
1012
            'height'   => 400,
1013
            'width'    => '100%',
1014
            'legend'   => [
1015
                'position'  => count($z_axis) > 1 ? 'right' : 'none',
1016
                'alignment' => 'center',
1017
            ],
1018
            'tooltip'  => [
1019
                'format' => '\'%\'',
1020
            ],
1021
            'vAxis'    => [
1022
                'title' => $y_axis_title ?? '',
1023
            ],
1024
            'hAxis'    => [
1025
                'title' => $x_axis_title ?? '',
1026
            ],
1027
            'colors'   => $colors,
1028
        ];
1029
1030
        return view('statistics/other/charts/custom', [
1031
            'data'          => $data,
1032
            'chart_options' => $chart_options,
1033
            'chart_title'   => $chart_title,
1034
            'language'      => I18N::languageTag(),
1035
        ]);
1036
    }
1037
}
1038