Completed
Push — master ( 56eca4...2839b8 )
by Greg
06:20
created

StatisticsChartModule::postCustomChartAction()   F

Complexity

Conditions 106
Paths 124

Size

Total Lines 521
Code Lines 401

Duplication

Lines 0
Ratio 0 %

Importance

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

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) 2019 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 <http://www.gnu.org/licenses/>.
16
 */
17
18
declare(strict_types=1);
19
20
namespace Fisharebest\Webtrees\Module;
21
22
use Fisharebest\Localization\Locale\LocaleInterface;
23
use Fisharebest\Webtrees\Auth;
24
use Fisharebest\Webtrees\Date;
25
use Fisharebest\Webtrees\I18N;
26
use Fisharebest\Webtrees\Individual;
27
use Fisharebest\Webtrees\Statistics;
28
use Fisharebest\Webtrees\Tree;
29
use Psr\Http\Message\ResponseInterface;
30
use Psr\Http\Message\ServerRequestInterface;
31
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
32
33
use function app;
34
use function array_key_exists;
35
use function array_keys;
36
use function array_map;
37
use function array_merge;
38
use function array_sum;
39
use function array_values;
40
use function array_walk;
41
use function assert;
42
use function count;
43
use function explode;
44
use function in_array;
45
use function is_numeric;
46
use function sprintf;
47
use function strip_tags;
48
49
/**
50
 * Class StatisticsChartModule
51
 */
52
class StatisticsChartModule extends AbstractModule implements ModuleChartInterface
53
{
54
    use ModuleChartTrait;
55
56
    // We generate a bitmap chart with these dimensions in image pixels.
57
    // These set the aspect ratio.  The actual image is sized using CSS
58
    // The maximum size (width x height) is 300,000
59
    private const CHART_WIDTH  = 950;
60
    private const CHART_HEIGHT = 315;
61
62
    public const X_AXIS_INDIVIDUAL_MAP        = 1;
63
    public const X_AXIS_BIRTH_MAP             = 2;
64
    public const X_AXIS_DEATH_MAP             = 3;
65
    public const X_AXIS_MARRIAGE_MAP          = 4;
66
    public const X_AXIS_BIRTH_MONTH           = 11;
67
    public const X_AXIS_DEATH_MONTH           = 12;
68
    public const X_AXIS_MARRIAGE_MONTH        = 13;
69
    public const X_AXIS_FIRST_CHILD_MONTH     = 14;
70
    public const X_AXIS_FIRST_MARRIAGE_MONTH  = 15;
71
    public const X_AXIS_AGE_AT_DEATH          = 18;
72
    public const X_AXIS_AGE_AT_MARRIAGE       = 19;
73
    public const X_AXIS_AGE_AT_FIRST_MARRIAGE = 20;
74
    public const X_AXIS_NUMBER_OF_CHILDREN    = 21;
75
76
    public const Y_AXIS_NUMBERS = 201;
77
    public const Y_AXIS_PERCENT = 202;
78
79
    public const Z_AXIS_ALL  = 300;
80
    public const Z_AXIS_SEX  = 301;
81
    public const Z_AXIS_TIME = 302;
82
83
    // First two colors are blue/pink, to work with Z_AXIS_SEX.
84
    private const Z_AXIS_COLORS = ['0000FF', 'FFA0CB', '9F00FF', 'FF7000', '905030', 'FF0000', '00FF00', 'F0F000'];
85
86
    private const DAYS_IN_YEAR = 365.25;
87
88
    /**
89
     * How should this module be identified in the control panel, etc.?
90
     *
91
     * @return string
92
     */
93
    public function title(): string
94
    {
95
        /* I18N: Name of a module/chart */
96
        return I18N::translate('Statistics');
97
    }
98
99
    /**
100
     * A sentence describing what this module does.
101
     *
102
     * @return string
103
     */
104
    public function description(): string
105
    {
106
        /* I18N: Description of the “StatisticsChart” module */
107
        return I18N::translate('Various statistics charts.');
108
    }
109
110
    /**
111
     * CSS class for the URL.
112
     *
113
     * @return string
114
     */
115
    public function chartMenuClass(): string
116
    {
117
        return 'menu-chart-statistics';
118
    }
119
120
    /**
121
     * The URL for this chart.
122
     *
123
     * @param Individual $individual
124
     * @param mixed[]    $parameters
125
     *
126
     * @return string
127
     */
128
    public function chartUrl(Individual $individual, array $parameters = []): string
129
    {
130
        return route('module', [
131
                'module' => $this->name(),
132
                'action' => 'Chart',
133
                'tree'    => $individual->tree()->name(),
134
            ] + $parameters);
135
    }
136
137
    /**
138
     * A form to request the chart parameters.
139
     *
140
     * @param ServerRequestInterface $request
141
     *
142
     * @return ResponseInterface
143
     */
144
    public function getChartAction(ServerRequestInterface $request): ResponseInterface
145
    {
146
        $tree = $request->getAttribute('tree');
147
        assert($tree instanceof Tree);
148
149
        $user = $request->getAttribute('user');
150
151
        Auth::checkComponentAccess($this, 'chart', $tree, $user);
152
153
        $tabs = [
154
            I18N::translate('Individuals') => route('module', [
155
                'module' => $this->name(),
156
                'action' => 'Individuals',
157
                'tree'    => $tree->name(),
158
            ]),
159
            I18N::translate('Families')    => route('module', [
160
                'module' => $this->name(),
161
                'action' => 'Families',
162
                'tree'    => $tree->name(),
163
            ]),
164
            I18N::translate('Other')       => route('module', [
165
                'module' => $this->name(),
166
                'action' => 'Other',
167
                'tree'    => $tree->name(),
168
            ]),
169
            I18N::translate('Custom')      => route('module', [
170
                'module' => $this->name(),
171
                'action' => 'Custom',
172
                'tree'    => $tree->name(),
173
            ]),
174
        ];
175
176
        return $this->viewResponse('modules/statistics-chart/page', [
177
            'module' => $this->name(),
178
            'tabs'   => $tabs,
179
            'title'  => $this->title(),
180
            'tree'   => $tree,
181
        ]);
182
    }
183
184
    /**
185
     * @param ServerRequestInterface $request
186
     *
187
     * @return ResponseInterface
188
     */
189
    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

189
    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...
190
    {
191
        $this->layout = 'layouts/ajax';
192
193
        return $this->viewResponse('modules/statistics-chart/individuals', [
194
            'show_oldest_living' => Auth::check(),
195
            'stats'              => app(Statistics::class),
196
        ]);
197
    }
198
199
    /**
200
     * @param ServerRequestInterface $request
201
     *
202
     * @return ResponseInterface
203
     */
204
    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

204
    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...
205
    {
206
        $this->layout = 'layouts/ajax';
207
208
        return $this->viewResponse('modules/statistics-chart/families', [
209
            'stats' => app(Statistics::class),
210
        ]);
211
    }
212
213
    /**
214
     * @param ServerRequestInterface $request
215
     *
216
     * @return ResponseInterface
217
     */
218
    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

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