Completed
Push — master ( 9aadcc...35c30e )
by Victor
02:16
created

IntervalGraph::setBoundToString()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 1
dl 0
loc 4
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace Vctls\IntervalGraph;
4
5
use Closure;
6
use DateTime;
7
use Exception;
8
use InvalidArgumentException;
9
use JsonSerializable;
10
11
/**
12
 * A class to manipulate and display arrays of weighted intervals.
13
 *
14
 * @package Vctls\IntervalGraph
15
 */
16
class IntervalGraph implements JsonSerializable
17
{
18
19
    /** @var array Initial intervals */
20
    protected $intervals;
21
22
    /** @var array Processed values */
23
    protected $values;
24
25
    /** @var string Path to the template used for rendering. */
26
    protected $template = 'template.php';
27
28
    /** @var Closure Return a numeric value from the inital bound value. */
29
    protected $boundToNumeric;
30
31
    /** @var Closure Return a string from the initial bound value. */
32
    protected $boundToString;
33
34
    /** @var Closure Return a numeric value from an initial interval value. */
35
    protected $valueToNumeric;
36
37
    /** @var Closure Return a string value from an initial interval value. */
38
    protected $valueToString;
39
40
    /** @var Palette */
41
    protected $palette;
42
43
    /** @var Flattener */
44
    private $flattener;
45
46
    /** @var Aggregator */
47
    private $aggregator;
48
49
    /**
50
     * Create an IntervalGraph from intervals carrying values.
51
     *
52
     * @param array[] $intervals An array of intervals,
53
     * with a low bound, high bound and a value.
54
     */
55
    public function __construct($intervals = null)
56
    {
57
        if (isset($intervals)) {
58
            $this->setIntervals($intervals);
59
        }
60
61
        $this->boundToNumeric = static function (DateTime $bound) {
62
            return $bound->getTimestamp();
63
        };
64
65
        $this->boundToString = static function (DateTime $bound) {
66
            return $bound->format('Y-m-d');
67
        };
68
69
        $this->valueToNumeric = static function ($v) {
70
            return $v === null ? null : (int)($v * 100);
71
        };
72
73
        $this->valueToString = static function ($v) {
74
            return $v === null ? null : ($v * 100 . '%');
75
        };
76
77
        $this->aggregator = new Aggregator();
78
        $this->flattener = new Flattener();
79
        $this->palette = new Palette();
80
    }
81
82
    /**
83
     * @return Flattener
84
     */
85
    public function getFlattener(): Flattener
86
    {
87
        return $this->flattener;
88
    }
89
90
    /**
91
     * @param Flattener $flattener
92
     * @return IntervalGraph
93
     */
94
    public function setFlattener(Flattener $flattener): IntervalGraph
95
    {
96
        $this->flattener = $flattener;
97
        return $this;
98
    }
99
100
    /**
101
     * Check that an array of intervals is correctly formatted.
102
     *
103
     * The first element must be the low bound.
104
     *
105
     * The second element must be the high bound.
106
     *
107
     * The third element must be the value.
108
     *
109
     * Inverted end and low bounds will be put back in chronological order.
110
     *
111
     * @return $this
112
     */
113
    public function checkIntervals(): self
114
    {
115
116
        foreach ($this->intervals as $intervalKey => $interval) {
117
118
            // Check that the interval is an array.
119
            if (!is_array($interval)) {
120
                $t = gettype($interval);
121
                throw new InvalidArgumentException(
122
                    "Each element of the '\$intervals' array should be an array, $t given."
123
                );
124
            }
125
126
            // Check that the bounds and value of the interval can be converted to both a numeric
127
            // and string value with the given closures.
128
            foreach ([['Lower bound', 'bound'], ['Higher bound', 'bound'], ['Value', 'value']] as $index => $property) {
129
130
                // Skip value property of valueless intervals.
131
                if ($property[1] === 'value' && !isset($interval[$index])) {
132
                    continue;
133
                }
134
135
                foreach (['numeric', 'string'] as $expectedType) {
136
137
                    $expectedTypeTitle = ucfirst($expectedType);
138
139
                    try {
140
                        $value = ($this->{"$property[1]To$expectedTypeTitle"})($interval[$index]);
141
                    } catch (Exception $exception) {
142
                        // FIXME Handle Type errors?
143
                        throw new PropertyConversionException(
144
                            "$property[0] of interval $intervalKey cannot be converted to a $expectedType value " .
145
                            "with the given '$property[1]To$expectedTypeTitle' function. Error : " .
146
                            $exception->getMessage()
147
                        );
148
                    }
149
150
                    $actualType = gettype($value);
151
152
                    if (!call_user_func("is_$expectedType", $value)) {
153
                        throw new PropertyConversionException(
154
                            "$property[0] of interval $intervalKey is not converted to a $expectedType value " .
155
                            "by the given '$property[1]To$expectedTypeTitle' function. Returned type : $actualType"
156
                        );
157
                    }
158
                }
159
            }
160
161
            // Ensure start and high bounds are in the right order.
162
            if ($interval[0] > $interval [1]) {
163
                $a = $interval[0];
164
                $intervals[$intervalKey][0] = $interval[1];
165
                $intervals[$intervalKey][1] = $a;
166
            }
167
        }
168
169
        // TODO Check that the values can be aggregated with the given closure.
170
171
        return $this;
172
    }
173
174
    /**
175
     * Render an HTML view of the intervalGraph.
176
     *
177
     * @return string
178
     */
179
    public function __toString()
180
    {
181
        try {
182
            $html = $this->draw();
183
        } catch (Exception $e) {
184
            $html = 'Error : ' . $e->getMessage();
185
        }
186
        return $html;
187
    }
188
189
    /**
190
     * Render an HTML view of the intervalGraph.
191
     *
192
     * @return string
193
     */
194
    public function draw(): string
195
    {
196
        if (!isset($this->values)) {
197
            $this->createView();
198
        }
199
200
        /** @noinspection PhpUnusedLocalVariableInspection */
201
        $vs = $this->values;
202
        ob_start();
203
        /** @noinspection PhpIncludeInspection */
204
        include $this->template;
205
206
        // Remove all surplus whitespace.
207
        return preg_replace(
208
            ['/(?<=>)\s+/', '/\s+(?=<)/', '/\s+/'], ['', '', ' '],
209
            ob_get_clean()
210
        );
211
    }
212
213
    /**
214
     * Process intervals and store processed values.
215
     *
216
     * @return IntervalGraph
217
     */
218
    public function createView(): IntervalGraph
219
    {
220
        $flatIntervals = $this->flattener->flatten($this->intervals);
221
        $flatIntervals = $this->aggregator->aggregate($flatIntervals, $this->intervals);
222
223
        // Extract values.
224
        $originalValues = array_column($flatIntervals, 2);
225
226
        $numVals = [];
227
228
        // Change bounds to numeric values.
229
        foreach ($flatIntervals as $interval) {
230
            $numVals[] = [
231
                ($this->boundToNumeric)($interval[0]),
232
                ($this->boundToNumeric)($interval[1]),
233
            ];
234
        }
235
236
        // Order by low bound.
237
        uasort($numVals, static function (array $i1, array $i2) {
238
            return ($i1[0] < $i2[0]) ? -1 : 1;
239
        });
240
241
        // Get the min bound value.
242
        $min = reset($numVals)[0];
243
244
        // Substract min from all bound values.
245
        foreach ($numVals as $key => $numVal) {
246
            $numVals[$key] = [
247
                $numVal[0] - $min,
248
                $numVal[1] - $min
249
            ];
250
        }
251
252
        // Order by high bound.
253
        uasort($numVals, static function (array $i1, array $i2) {
254
            return ($i1[1] < $i2[1]) ? -1 : 1;
255
        });
256
257
        // Get max timestamp.
258
        $max = end($numVals)[1];
259
260
        // Calculate percentages.
261
        foreach ($numVals as $i => $numVal) {
262
            foreach ($numVal as $j => $value) {
263
                $numVal[$j] = $max === 0 ? 0 : round($value * 100 / $max, 2);
264
            }
265
            $numVals[$i] = $numVal;
266
        }
267
268
        // Put values back in, along with the formatted bound.
269
        // Since we're using associative sorting functions, we know the keys haven't changed.
270
        $numKeys = array_keys($numVals);
271
        foreach ($numKeys as $numKey) {
272
273
            [$lowBound, $highBound] = $flatIntervals[$numKey];
274
            [$startPercent, $endPercent] = $numVals[$numKey];
275
276
            if ($lowBound === $highBound) {
277
278
                $numVals[$numKey] = [
279
                    $startPercent, // Single value position percentage
280
                    ($this->boundToString)($lowBound), // Single value string
281
                ];
282
283
            } else {
284
285
                $colval = isset($originalValues[$numKey]) ? ($this->valueToNumeric)($originalValues[$numKey]) : null;
286
                $strval = isset($originalValues[$numKey]) ? ($this->valueToString)($originalValues[$numKey]) : null;
287
288
                $numVals[$numKey] = [
289
                    $startPercent, // Interval start percentage
290
                    100 - $endPercent, // Interval end percentage from right
291
                    // Note: for some reason, using 'widht' instead of 'right'
292
                    // causes the right border to be hidden underneath the next interval.
293
                    !empty($originalValues) ? $this->palette->getColor($colval) : 50, // Interval color
294
                    ($this->boundToString)($lowBound), // Interval start string value
295
                    ($this->boundToString)($highBound), // Interval end string value
296
                    !empty($originalValues) ? $strval : null,// Interval string value
297
                ];
298
            }
299
        }
300
301
        // Put discrete values at the end and reset indices.
302
        // Reseting indices ensures the processed values are
303
        // serialized as correctly ordered JSON arrays.
304
        usort($numVals, static function ($i) {
305
            return count($i) === 2 ? 1 : -1;
306
        });
307
308
        $this->values = $numVals;
309
310
        return $this;
311
    }
312
313
    /**
314
     * Compute the numeric values of interval bounds and values.
315
     *
316
     * @return array
317
     */
318
    public function computeNumericValues(): array
319
    {
320
        $intervals = $this->flattener->flatten($this->intervals);
321
        $intervals = $this->aggregator->aggregate($intervals, $this->intervals);
322
323
        // Extract interval values.
324
        $intervalValues = array_column($intervals, 2);
325
326
        // Convert bounds to numeric values.
327
        foreach ($intervals as $interval) {
328
            $numericIntervals[] = [
329
                ($this->boundToNumeric)($interval[0]),
330
                ($this->boundToNumeric)($interval[1]),
331
            ];
332
        }
333
334
        // Order by high bound.
335
        uasort($numericIntervals, static function (array $i1, array $i2) {
336
            return ($i1[1] < $i2[1]) ? -1 : 1;
337
        });
338
339
340
        // Put values back in, along with the formatted bound.
341
        // Since we're using associative sorting functions, we know the keys haven't changed.
342
        foreach (array_keys($numericIntervals) as $index => $numKey) {
343
344
            [$lowNumericBound, $highNumericBound] = $numericIntervals[$index];
345
346
            if ($lowNumericBound === $highNumericBound) {
347
348
                $numericIntervals[$index] = $lowNumericBound;
349
350
            } else {
351
352
                $numericIntervals[$index] = [
353
                    $lowNumericBound,
354
                    $highNumericBound,
355
                    $intervalValues[$index] ?: 0
356
                ];
357
358
            }
359
        }
360
361
        // Put discrete values at the end and reset indices.
362
        // Reseting indices ensures the processed values are
363
        // serialized as correctly ordered JSON arrays.
364
        usort($numericIntervals, static function ($i) {
365
            return !is_array($i) ? 1 : -1;
366
        });
367
368
        return $numericIntervals;
369
    }
370
371
    /**
372
     * Define the function to convert the interval values to a numeric value
373
     * in order to match them to a color on the palette.
374
     *
375
     * @param Closure $valueToNumeric
376
     * @return IntervalGraph
377
     */
378
    public function setValueToNumeric(Closure $valueToNumeric): IntervalGraph
379
    {
380
        $this->valueToNumeric = $valueToNumeric;
381
        return $this;
382
    }
383
384
    /**
385
     * Define the  function to convert the interval values to strings
386
     * in order to display them in the view.
387
     *
388
     * @param Closure $valueToString
389
     * @return IntervalGraph
390
     */
391
    public function setValueToString(Closure $valueToString): IntervalGraph
392
    {
393
        $this->valueToString = $valueToString;
394
        return $this;
395
    }
396
397
    /**
398
     * Set the function to convert interval bound values to string.
399
     *
400
     * @param Closure $boundToString
401
     * @return IntervalGraph
402
     */
403
    public function setBoundToString($boundToString): IntervalGraph
404
    {
405
        $this->boundToString = $boundToString;
406
        return $this;
407
    }
408
409
    /**
410
     * @return array
411
     */
412
    public function getIntervals(): array
413
    {
414
        return $this->intervals;
415
    }
416
417
    /**
418
     * Set the intervals to be processed.
419
     *
420
     * If another set of intervals was previously processed,
421
     * the processed values will be deleted.
422
     *
423
     * @param array $intervals
424
     * @return IntervalGraph
425
     */
426
    public function setIntervals(array $intervals): IntervalGraph
427
    {
428
        $this->intervals = $intervals;
429
        $this->values = null;
430
        return $this;
431
    }
432
433
    /**
434
     * @return array
435
     */
436
    public function getValues(): array
437
    {
438
        return $this->values;
439
    }
440
441
    /**
442
     * @return string
443
     */
444
    public function getTemplate(): string
445
    {
446
        return $this->template;
447
    }
448
449
    /**
450
     * Set the PHP template to use for rendering.
451
     *
452
     * @param string $template
453
     * @return IntervalGraph
454
     */
455
    public function setTemplate($template): IntervalGraph
456
    {
457
        $this->template = $template;
458
        return $this;
459
    }
460
461
    /**
462
     * @return Palette
463
     */
464
    public function getPalette(): Palette
465
    {
466
        return $this->palette;
467
    }
468
469
    /**
470
     * Set the Palette object to be used to determine colors.
471
     *
472
     * @param Palette $palette
473
     * @return IntervalGraph
474
     */
475
    public function setPalette($palette): IntervalGraph
476
    {
477
        $this->palette = $palette;
478
        return $this;
479
    }
480
481
    /**
482
     * @param Closure $boundToNumeric
483
     * @return IntervalGraph
484
     */
485
    public function setBoundToNumeric($boundToNumeric): IntervalGraph
486
    {
487
        $this->boundToNumeric = $boundToNumeric;
488
        return $this;
489
    }
490
491
    /**
492
     * Return the array of values to be serialized by json_encode.
493
     *
494
     * @return array
495
     */
496
    public function jsonSerialize(): array
497
    {
498
        if (!isset($this->values)) {
499
            $this->createView();
500
        }
501
        return $this->values;
502
    }
503
504
    /**
505
     * @return Aggregator
506
     */
507
    public function getAggregator(): Aggregator
508
    {
509
        return $this->aggregator;
510
    }
511
512
    /**
513
     * @param Aggregator $aggregator
514
     * @return IntervalGraph
515
     */
516
    public function setAggregator(Aggregator $aggregator): IntervalGraph
517
    {
518
        $this->aggregator = $aggregator;
519
        return $this;
520
    }
521
522
}
523