1<?php
2
3namespace CpChart;
4
5use Exception;
6use RuntimeException;
7
8/**
9 *  Data - class to manipulate data arrays
10 *
11 *  Version     : 2.1.4
12 *  Made by     : Jean-Damien POGOLOTTI
13 *  Last Update : 19/01/2014
14 *
15 *  This file can be distributed under the license you can find at :
16 *
17 *  http://www.pchart.net/license
18 *
19 *  You can find the whole class documentation on the pChart web site.
20 */
21class Data
22{
23    /**
24     * @var array
25     */
26    public $Data = [];
27
28    /**
29     * @var array
30     */
31    public $Palette = [
32        "0" => ["R" => 188, "G" => 224, "B" => 46, "Alpha" => 100],
33        "1" => ["R" => 224, "G" => 100, "B" => 46, "Alpha" => 100],
34        "2" => ["R" => 224, "G" => 214, "B" => 46, "Alpha" => 100],
35        "3" => ["R" => 46, "G" => 151, "B" => 224, "Alpha" => 100],
36        "4" => ["R" => 176, "G" => 46, "B" => 224, "Alpha" => 100],
37        "5" => ["R" => 224, "G" => 46, "B" => 117, "Alpha" => 100],
38        "6" => ["R" => 92, "G" => 224, "B" => 46, "Alpha" => 100],
39        "7" => ["R" => 224, "G" => 176, "B" => 46, "Alpha" => 100]
40    ];
41
42    public function __construct()
43    {
44        $this->Data["XAxisDisplay"] = AXIS_FORMAT_DEFAULT;
45        $this->Data["XAxisFormat"] = null;
46        $this->Data["XAxisName"] = null;
47        $this->Data["XAxisUnit"] = null;
48        $this->Data["Abscissa"] = null;
49        $this->Data["AbsicssaPosition"] = AXIS_POSITION_BOTTOM;
50
51        $this->Data["Axis"][0]["Display"] = AXIS_FORMAT_DEFAULT;
52        $this->Data["Axis"][0]["Position"] = AXIS_POSITION_LEFT;
53        $this->Data["Axis"][0]["Identity"] = AXIS_Y;
54    }
55
56    /**
57     * Add a single point or an array to the given serie
58     * @param mixed $Values
59     * @param string $SerieName
60     * @return int
61     */
62    public function addPoints($Values, $SerieName = "Serie1")
63    {
64        if (!isset($this->Data["Series"][$SerieName])) {
65            $this->initialise($SerieName);
66        }
67        if (is_array($Values)) {
68            foreach ($Values as $Value) {
69                $this->Data["Series"][$SerieName]["Data"][] = $Value;
70            }
71        } else {
72            $this->Data["Series"][$SerieName]["Data"][] = $Values;
73        }
74
75        if ($Values != VOID) {
76            $StrippedData = $this->stripVOID($this->Data["Series"][$SerieName]["Data"]);
77            if (empty($StrippedData)) {
78                $this->Data["Series"][$SerieName]["Max"] = 0;
79                $this->Data["Series"][$SerieName]["Min"] = 0;
80                return 0;
81            }
82            $this->Data["Series"][$SerieName]["Max"] = max($StrippedData);
83            $this->Data["Series"][$SerieName]["Min"] = min($StrippedData);
84        }
85    }
86
87    /**
88     * Strip VOID values
89     * @param mixed $Values
90     * @return array
91     */
92    public function stripVOID($Values)
93    {
94        if (!is_array($Values)) {
95            return [];
96        }
97        $Result = [];
98        foreach ($Values as $Value) {
99            if ($Value != VOID) {
100                $Result[] = $Value;
101            }
102        }
103        return $Result;
104    }
105
106    /**
107     * Return the number of values contained in a given serie
108     * @param string $Serie
109     * @return int
110     */
111    public function getSerieCount($Serie)
112    {
113        if (isset($this->Data["Series"][$Serie]["Data"])) {
114            return sizeof($this->Data["Series"][$Serie]["Data"]);
115        }
116        return 0;
117    }
118
119    /**
120     * Remove a serie from the pData object
121     * @param mixed $Series
122     */
123    public function removeSerie($Series)
124    {
125        if (!is_array($Series)) {
126            $Series = $this->convertToArray($Series);
127        }
128        foreach ($Series as $Serie) {
129            if (isset($this->Data["Series"][$Serie])) {
130                unset($this->Data["Series"][$Serie]);
131            }
132        }
133    }
134
135    /**
136     * Return a value from given serie & index
137     * @param string $Serie
138     * @param int $Index
139     * @return mixed
140     */
141    public function getValueAt($Serie, $Index = 0)
142    {
143        if (isset($this->Data["Series"][$Serie]["Data"][$Index])) {
144            return $this->Data["Series"][$Serie]["Data"][$Index];
145        }
146        return null;
147    }
148
149    /**
150     * Return the values array
151     * @param string $Serie
152     * @return mixed
153     */
154    public function getValues($Serie)
155    {
156        if (isset($this->Data["Series"][$Serie]["Data"])) {
157            return $this->Data["Series"][$Serie]["Data"];
158        }
159        return null;
160    }
161
162    /**
163     * Reverse the values in the given serie
164     * @param mixed $Series
165     */
166    public function reverseSerie($Series)
167    {
168        if (!is_array($Series)) {
169            $Series = $this->convertToArray($Series);
170        }
171        foreach ($Series as $Serie) {
172            if (isset($this->Data["Series"][$Serie]["Data"])) {
173                $this->Data["Series"][$Serie]["Data"] = array_reverse(
174                    $this->Data["Series"][$Serie]["Data"]
175                );
176            }
177        }
178    }
179
180    /**
181     * Return the sum of the serie values
182     * @param string $Serie
183     * @return int|null
184     */
185    public function getSum($Serie)
186    {
187        if (isset($this->Data["Series"][$Serie])) {
188            return array_sum($this->Data["Series"][$Serie]["Data"]);
189        }
190        return null;
191    }
192
193    /**
194     * Return the max value of a given serie
195     * @param string $Serie
196     * @return mixed
197     */
198    public function getMax($Serie)
199    {
200        if (isset($this->Data["Series"][$Serie]["Max"])) {
201            return $this->Data["Series"][$Serie]["Max"];
202        }
203        return null;
204    }
205
206    /**
207     * @param string $Serie
208     * @return mixed
209     */
210    public function getMin($Serie)
211    {
212        if (isset($this->Data["Series"][$Serie]["Min"])) {
213            return $this->Data["Series"][$Serie]["Min"];
214        }
215        return null;
216    }
217
218    /**
219     * Set the description of a given serie
220     * @param mixed $Series
221     * @param string $Shape
222     */
223    public function setSerieShape($Series, $Shape = SERIE_SHAPE_FILLEDCIRCLE)
224    {
225        if (!is_array($Series)) {
226            $Series = $this->convertToArray($Series);
227        }
228        foreach ($Series as $Serie) {
229            if (isset($this->Data["Series"][$Serie])) {
230                $this->Data["Series"][$Serie]["Shape"] = $Shape;
231            }
232        }
233    }
234
235    /**
236     * Set the description of a given serie
237     * @param string|array $Series
238     * @param string $Description
239     */
240    public function setSerieDescription($Series, $Description = "My serie")
241    {
242        if (!is_array($Series)) {
243            $Series = $this->convertToArray($Series);
244        }
245        foreach ($Series as $Serie) {
246            if (isset($this->Data["Series"][$Serie])) {
247                $this->Data["Series"][$Serie]["Description"] = $Description;
248            }
249        }
250    }
251
252    /**
253     * Set a serie as "drawable" while calling a rendering public function
254     * @param string|array $Series
255     * @param boolean $Drawable
256     */
257    public function setSerieDrawable($Series, $Drawable = true)
258    {
259        if (!is_array($Series)) {
260            $Series = $this->convertToArray($Series);
261        }
262        foreach ($Series as $Serie) {
263            if (isset($this->Data["Series"][$Serie])) {
264                $this->Data["Series"][$Serie]["isDrawable"] = $Drawable;
265            }
266        }
267    }
268
269    /**
270     * Set the icon associated to a given serie
271     * @param mixed $Series
272     * @param mixed $Picture
273     */
274    public function setSeriePicture($Series, $Picture = null)
275    {
276        if (!is_array($Series)) {
277            $Series = $this->convertToArray($Series);
278        }
279        foreach ($Series as $Serie) {
280            if (isset($this->Data["Series"][$Serie])) {
281                $this->Data["Series"][$Serie]["Picture"] = $Picture;
282            }
283        }
284    }
285
286    /**
287     * Set the name of the X Axis
288     * @param string $Name
289     */
290    public function setXAxisName($Name)
291    {
292        $this->Data["XAxisName"] = $Name;
293    }
294
295    /**
296     * Set the display mode of the  X Axis
297     * @param int $Mode
298     * @param array $Format
299     */
300    public function setXAxisDisplay($Mode, $Format = null)
301    {
302        $this->Data["XAxisDisplay"] = $Mode;
303        $this->Data["XAxisFormat"] = $Format;
304    }
305
306    /**
307     * Set the unit that will be displayed on the X axis
308     * @param string $Unit
309     */
310    public function setXAxisUnit($Unit)
311    {
312        $this->Data["XAxisUnit"] = $Unit;
313    }
314
315    /**
316     * Set the serie that will be used as abscissa
317     * @param string $Serie
318     */
319    public function setAbscissa($Serie)
320    {
321        if (isset($this->Data["Series"][$Serie])) {
322            $this->Data["Abscissa"] = $Serie;
323        }
324    }
325
326    /**
327     * Set the position of the abscissa axis
328     * @param int $Position
329     */
330    public function setAbsicssaPosition($Position = AXIS_POSITION_BOTTOM)
331    {
332        $this->Data["AbsicssaPosition"] = $Position;
333    }
334
335    /**
336     * Set the name of the abscissa axis
337     * @param string $Name
338     */
339    public function setAbscissaName($Name)
340    {
341        $this->Data["AbscissaName"] = $Name;
342    }
343
344    /**
345     * Create a scatter group specified in X and Y data series
346     * @param string $SerieX
347     * @param string $SerieY
348     * @param int $ID
349     */
350    public function setScatterSerie($SerieX, $SerieY, $ID = 0)
351    {
352        if (isset($this->Data["Series"][$SerieX]) && isset($this->Data["Series"][$SerieY])) {
353            $this->initScatterSerie($ID);
354            $this->Data["ScatterSeries"][$ID]["X"] = $SerieX;
355            $this->Data["ScatterSeries"][$ID]["Y"] = $SerieY;
356        }
357    }
358
359    /**
360     *  Set the shape of a given sctatter serie
361     * @param int $ID
362     * @param int $Shape
363     */
364    public function setScatterSerieShape($ID, $Shape = SERIE_SHAPE_FILLEDCIRCLE)
365    {
366        if (isset($this->Data["ScatterSeries"][$ID])) {
367            $this->Data["ScatterSeries"][$ID]["Shape"] = $Shape;
368        }
369    }
370
371    /**
372     * Set the description of a given scatter serie
373     * @param int $ID
374     * @param string $Description
375     */
376    public function setScatterSerieDescription($ID, $Description = "My serie")
377    {
378        if (isset($this->Data["ScatterSeries"][$ID])) {
379            $this->Data["ScatterSeries"][$ID]["Description"] = $Description;
380        }
381    }
382
383    /**
384     * Set the icon associated to a given scatter serie
385     * @param int $ID
386     * @param mixed $Picture
387     */
388    public function setScatterSeriePicture($ID, $Picture = null)
389    {
390        if (isset($this->Data["ScatterSeries"][$ID])) {
391            $this->Data["ScatterSeries"][$ID]["Picture"] = $Picture;
392        }
393    }
394
395    /**
396     * Set a scatter serie as "drawable" while calling a rendering public function
397     * @param int $ID
398     * @param boolean $Drawable
399     */
400    public function setScatterSerieDrawable($ID, $Drawable = true)
401    {
402        if (isset($this->Data["ScatterSeries"][$ID])) {
403            $this->Data["ScatterSeries"][$ID]["isDrawable"] = $Drawable;
404        }
405    }
406
407    /**
408     * Define if a scatter serie should be draw with ticks
409     * @param int $ID
410     * @param int $Width
411     */
412    public function setScatterSerieTicks($ID, $Width = 0)
413    {
414        if (isset($this->Data["ScatterSeries"][$ID])) {
415            $this->Data["ScatterSeries"][$ID]["Ticks"] = $Width;
416        }
417    }
418
419    /**
420     * Define if a scatter serie should be draw with a special weight
421     * @param int $ID
422     * @param int $Weight
423     */
424    public function setScatterSerieWeight($ID, $Weight = 0)
425    {
426        if (isset($this->Data["ScatterSeries"][$ID])) {
427            $this->Data["ScatterSeries"][$ID]["Weight"] = $Weight;
428        }
429    }
430
431    /**
432     * Associate a color to a scatter serie
433     * @param int $ID
434     * @param array $Format
435     */
436    public function setScatterSerieColor($ID, array $Format)
437    {
438        $R = isset($Format["R"]) ? $Format["R"] : 0;
439        $G = isset($Format["G"]) ? $Format["G"] : 0;
440        $B = isset($Format["B"]) ? $Format["B"] : 0;
441        $Alpha = isset($Format["Alpha"]) ? $Format["Alpha"] : 100;
442
443        if (isset($this->Data["ScatterSeries"][$ID])) {
444            $this->Data["ScatterSeries"][$ID]["Color"]["R"] = $R;
445            $this->Data["ScatterSeries"][$ID]["Color"]["G"] = $G;
446            $this->Data["ScatterSeries"][$ID]["Color"]["B"] = $B;
447            $this->Data["ScatterSeries"][$ID]["Color"]["Alpha"] = $Alpha;
448        }
449    }
450
451    /**
452     * Compute the series limits for an individual and global point of view
453     * @return array
454     */
455    public function limits()
456    {
457        $GlobalMin = ABSOLUTE_MAX;
458        $GlobalMax = ABSOLUTE_MIN;
459
460        foreach (array_keys($this->Data["Series"]) as $Key) {
461            if ($this->Data["Abscissa"] != $Key
462                && $this->Data["Series"][$Key]["isDrawable"] == true
463            ) {
464                if ($GlobalMin > $this->Data["Series"][$Key]["Min"]) {
465                    $GlobalMin = $this->Data["Series"][$Key]["Min"];
466                }
467                if ($GlobalMax < $this->Data["Series"][$Key]["Max"]) {
468                    $GlobalMax = $this->Data["Series"][$Key]["Max"];
469                }
470            }
471        }
472        $this->Data["Min"] = $GlobalMin;
473        $this->Data["Max"] = $GlobalMax;
474
475        return [$GlobalMin, $GlobalMax];
476    }
477
478    /**
479     * Mark all series as drawable
480     */
481    public function drawAll()
482    {
483        foreach (array_keys($this->Data["Series"]) as $Key) {
484            if ($this->Data["Abscissa"] != $Key) {
485                $this->Data["Series"][$Key]["isDrawable"] = true;
486            }
487        }
488    }
489
490    /**
491     * Return the average value of the given serie
492     * @param string $Serie
493     * @return int|null
494     */
495    public function getSerieAverage($Serie)
496    {
497        if (isset($this->Data["Series"][$Serie])) {
498            $SerieData = $this->stripVOID($this->Data["Series"][$Serie]["Data"]);
499            return array_sum($SerieData) / sizeof($SerieData);
500        }
501
502        return null;
503    }
504
505    /**
506     * Return the geometric mean of the given serie
507     * @param string $Serie
508     * @return int|null
509     */
510    public function getGeometricMean($Serie)
511    {
512        if (isset($this->Data["Series"][$Serie])) {
513            $SerieData = $this->stripVOID($this->Data["Series"][$Serie]["Data"]);
514            $Seriesum = 1;
515            foreach ($SerieData as $Value) {
516                $Seriesum = $Seriesum * $Value;
517            }
518            return pow($Seriesum, 1 / sizeof($SerieData));
519        }
520
521        return null;
522    }
523
524    /**
525     * Return the harmonic mean of the given serie
526     * @param string $Serie
527     * @return int|null
528     */
529    public function getHarmonicMean($Serie)
530    {
531        if (isset($this->Data["Series"][$Serie])) {
532            $SerieData = $this->stripVOID($this->Data["Series"][$Serie]["Data"]);
533            $Seriesum = 0;
534            foreach ($SerieData as $Value) {
535                $Seriesum = $Seriesum + 1 / $Value;
536            }
537            return sizeof($SerieData) / $Seriesum;
538        }
539
540        return null;
541    }
542
543    /**
544     * Return the standard deviation of the given serie
545     * @param string $Serie
546     * @return double|null
547     */
548    public function getStandardDeviation($Serie)
549    {
550        if (isset($this->Data["Series"][$Serie])) {
551            $Average = $this->getSerieAverage($Serie);
552            $SerieData = $this->stripVOID($this->Data["Series"][$Serie]["Data"]);
553
554            $DeviationSum = 0;
555            foreach ($SerieData as $Key => $Value) {
556                $DeviationSum = $DeviationSum + ($Value - $Average) * ($Value - $Average);
557            }
558            return sqrt($DeviationSum / count($SerieData));
559        }
560        return null;
561    }
562
563    /**
564     * Return the Coefficient of variation of the given serie
565     * @param string $Serie
566     * @return float|null
567     */
568    public function getCoefficientOfVariation($Serie)
569    {
570        if (isset($this->Data["Series"][$Serie])) {
571            $Average = $this->getSerieAverage($Serie);
572            $StandardDeviation = $this->getStandardDeviation($Serie);
573
574            if ($StandardDeviation != 0) {
575                return $StandardDeviation / $Average;
576            }
577        }
578        return null;
579    }
580
581    /**
582     * Return the median value of the given serie
583     * @param string $Serie
584     * @return int|float
585     */
586    public function getSerieMedian($Serie)
587    {
588        if (isset($this->Data["Series"][$Serie])) {
589            $SerieData = $this->stripVOID($this->Data["Series"][$Serie]["Data"]);
590            sort($SerieData);
591            $SerieCenter = floor(sizeof($SerieData) / 2);
592
593            if (isset($SerieData[$SerieCenter])) {
594                return $SerieData[$SerieCenter];
595            }
596        }
597        return null;
598    }
599
600    /**
601     * Return the x th percentil of the given serie
602     * @param string $Serie
603     * @param int $Percentil
604     * @return int|float| null
605     */
606    public function getSeriePercentile($Serie = "Serie1", $Percentil = 95)
607    {
608        if (!isset($this->Data["Series"][$Serie]["Data"])) {
609            return null;
610        }
611
612        $Values = count($this->Data["Series"][$Serie]["Data"]) - 1;
613        if ($Values < 0) {
614            $Values = 0;
615        }
616
617        $PercentilID = floor(($Values / 100) * $Percentil + .5);
618        $SortedValues = $this->Data["Series"][$Serie]["Data"];
619        sort($SortedValues);
620
621        if (is_numeric($SortedValues[$PercentilID])) {
622            return $SortedValues[$PercentilID];
623        }
624        return null;
625    }
626
627    /**
628     * Add random values to a given serie
629     * @param string $SerieName
630     * @param array $Options
631     */
632    public function addRandomValues($SerieName = "Serie1", array $Options = [])
633    {
634        $Values = isset($Options["Values"]) ? $Options["Values"] : 20;
635        $Min = isset($Options["Min"]) ? $Options["Min"] : 0;
636        $Max = isset($Options["Max"]) ? $Options["Max"] : 100;
637        $withFloat = isset($Options["withFloat"]) ? $Options["withFloat"] : false;
638
639        for ($i = 0; $i <= $Values; $i++) {
640            $Value = $withFloat ? rand($Min * 100, $Max * 100) / 100 : rand($Min, $Max);
641            $this->addPoints($Value, $SerieName);
642        }
643    }
644
645    /**
646     * Test if we have valid data
647     * @return boolean|null
648     */
649    public function containsData()
650    {
651        if (!isset($this->Data["Series"])) {
652            return false;
653        }
654
655        foreach (array_keys($this->Data["Series"]) as $Key) {
656            if ($this->Data["Abscissa"] != $Key
657                && $this->Data["Series"][$Key]["isDrawable"] == true
658            ) {
659                return true;
660            }
661        }
662
663        return null;
664    }
665
666    /**
667     * Set the display mode of an Axis
668     * @param int $AxisID
669     * @param int $Mode
670     * @param array $Format
671     */
672    public function setAxisDisplay($AxisID, $Mode = AXIS_FORMAT_DEFAULT, $Format = null)
673    {
674        if (isset($this->Data["Axis"][$AxisID])) {
675            $this->Data["Axis"][$AxisID]["Display"] = $Mode;
676            if ($Format != null) {
677                $this->Data["Axis"][$AxisID]["Format"] = $Format;
678            }
679        }
680    }
681
682    /**
683     * Set the position of an Axis
684     * @param int $AxisID
685     * @param int $Position
686     */
687    public function setAxisPosition($AxisID, $Position = AXIS_POSITION_LEFT)
688    {
689        if (isset($this->Data["Axis"][$AxisID])) {
690            $this->Data["Axis"][$AxisID]["Position"] = $Position;
691        }
692    }
693
694    /**
695     * Associate an unit to an axis
696     * @param int $AxisID
697     * @param string $Unit
698     */
699    public function setAxisUnit($AxisID, $Unit)
700    {
701        if (isset($this->Data["Axis"][$AxisID])) {
702            $this->Data["Axis"][$AxisID]["Unit"] = $Unit;
703        }
704    }
705
706    /**
707     * Associate a name to an axis
708     * @param int $AxisID
709     * @param string $Name
710     */
711    public function setAxisName($AxisID, $Name)
712    {
713        if (isset($this->Data["Axis"][$AxisID])) {
714            $this->Data["Axis"][$AxisID]["Name"] = $Name;
715        }
716    }
717
718    /**
719     * Associate a color to an axis
720     * @param int $AxisID
721     * @param array $Format
722     */
723    public function setAxisColor($AxisID, array $Format)
724    {
725        $R = isset($Format["R"]) ? $Format["R"] : 0;
726        $G = isset($Format["G"]) ? $Format["G"] : 0;
727        $B = isset($Format["B"]) ? $Format["B"] : 0;
728        $Alpha = isset($Format["Alpha"]) ? $Format["Alpha"] : 100;
729
730        if (isset($this->Data["Axis"][$AxisID])) {
731            $this->Data["Axis"][$AxisID]["Color"]["R"] = $R;
732            $this->Data["Axis"][$AxisID]["Color"]["G"] = $G;
733            $this->Data["Axis"][$AxisID]["Color"]["B"] = $B;
734            $this->Data["Axis"][$AxisID]["Color"]["Alpha"] = $Alpha;
735        }
736    }
737
738    /**
739     * Design an axis as X or Y member
740     * @param int $AxisID
741     * @param int $Identity
742     */
743    public function setAxisXY($AxisID, $Identity = AXIS_Y)
744    {
745        if (isset($this->Data["Axis"][$AxisID])) {
746            $this->Data["Axis"][$AxisID]["Identity"] = $Identity;
747        }
748    }
749
750    /**
751     * Associate one data serie with one axis
752     * @param mixed $Series
753     * @param int $AxisID
754     */
755    public function setSerieOnAxis($Series, $AxisID)
756    {
757        if (!is_array($Series)) {
758            $Series = $this->convertToArray($Series);
759        }
760        foreach ($Series as $Serie) {
761            $PreviousAxis = $this->Data["Series"][$Serie]["Axis"];
762
763            /* Create missing axis */
764            if (!isset($this->Data["Axis"][$AxisID])) {
765                $this->Data["Axis"][$AxisID]["Position"] = AXIS_POSITION_LEFT;
766                $this->Data["Axis"][$AxisID]["Identity"] = AXIS_Y;
767            }
768
769            $this->Data["Series"][$Serie]["Axis"] = $AxisID;
770
771            /* Cleanup unused axis */
772            $Found = false;
773            foreach ($this->Data["Series"] as $Values) {
774                if ($Values["Axis"] == $PreviousAxis) {
775                    $Found = true;
776                }
777            }
778            if (!$Found) {
779                unset($this->Data["Axis"][$PreviousAxis]);
780            }
781        }
782    }
783
784    /**
785     * Define if a serie should be draw with ticks
786     * @param mixed $Series
787     * @param int $Width
788     */
789    public function setSerieTicks($Series, $Width = 0)
790    {
791        if (!is_array($Series)) {
792            $Series = $this->convertToArray($Series);
793        }
794        foreach ($Series as $Serie) {
795            if (isset($this->Data["Series"][$Serie])) {
796                $this->Data["Series"][$Serie]["Ticks"] = $Width;
797            }
798        }
799    }
800
801    /**
802     * Define if a serie should be draw with a special weight
803     * @param mixed $Series
804     * @param int $Weight
805     */
806    public function setSerieWeight($Series, $Weight = 0)
807    {
808        if (!is_array($Series)) {
809            $Series = $this->convertToArray($Series);
810        }
811        foreach ($Series as $Serie) {
812            if (isset($this->Data["Series"][$Serie])) {
813                $this->Data["Series"][$Serie]["Weight"] = $Weight;
814            }
815        }
816    }
817
818    /**
819     * Returns the palette of the given serie
820     * @param type $Serie
821     * @return null
822     */
823    public function getSeriePalette($Serie)
824    {
825        if (!isset($this->Data["Series"][$Serie])) {
826            return null;
827        }
828
829        $Result = [];
830        $Result["R"] = $this->Data["Series"][$Serie]["Color"]["R"];
831        $Result["G"] = $this->Data["Series"][$Serie]["Color"]["G"];
832        $Result["B"] = $this->Data["Series"][$Serie]["Color"]["B"];
833        $Result["Alpha"] = $this->Data["Series"][$Serie]["Color"]["Alpha"];
834
835        return $Result;
836    }
837
838    /**
839     * Set the color of one serie
840     * @param mixed $Series
841     * @param array $Format
842     */
843    public function setPalette($Series, array $Format = [])
844    {
845        if (!is_array($Series)) {
846            $Series = $this->convertToArray($Series);
847        }
848
849        foreach ($Series as $Key => $Serie) {
850            $R = isset($Format["R"]) ? $Format["R"] : 0;
851            $G = isset($Format["G"]) ? $Format["G"] : 0;
852            $B = isset($Format["B"]) ? $Format["B"] : 0;
853            $Alpha = isset($Format["Alpha"]) ? $Format["Alpha"] : 100;
854
855            if (isset($this->Data["Series"][$Serie])) {
856                $OldR = $this->Data["Series"][$Serie]["Color"]["R"];
857                $OldG = $this->Data["Series"][$Serie]["Color"]["G"];
858                $OldB = $this->Data["Series"][$Serie]["Color"]["B"];
859                $this->Data["Series"][$Serie]["Color"]["R"] = $R;
860                $this->Data["Series"][$Serie]["Color"]["G"] = $G;
861                $this->Data["Series"][$Serie]["Color"]["B"] = $B;
862                $this->Data["Series"][$Serie]["Color"]["Alpha"] = $Alpha;
863
864                /* Do reverse processing on the internal palette array */
865                foreach ($this->Palette as $Key => $Value) {
866                    if ($Value["R"] == $OldR && $Value["G"] == $OldG && $Value["B"] == $OldB) {
867                        $this->Palette[$Key]["R"] = $R;
868                        $this->Palette[$Key]["G"] = $G;
869                        $this->Palette[$Key]["B"] = $B;
870                        $this->Palette[$Key]["Alpha"] = $Alpha;
871                    }
872                }
873            }
874        }
875    }
876
877    /**
878     * Load a palette file
879     * @param string $FileName
880     * @param boolean $Overwrite
881     * @throws Exception
882     */
883    public function loadPalette($FileName, $Overwrite = false)
884    {
885        $path = file_exists($FileName)
886            ? $FileName
887            : sprintf('%s/../resources/palettes/%s', __DIR__, ltrim($FileName, '/'))
888        ;
889
890        $fileHandle = @fopen($path, "r");
891        if (!$fileHandle) {
892            throw new Exception(sprintf(
893                'The requested palette "%s" was not found at path "%s"!',
894                $FileName,
895                $path
896            ));
897        }
898
899        if ($Overwrite) {
900            $this->Palette = [];
901        }
902
903        while (!feof($fileHandle)) {
904            $line = fgets($fileHandle, 4096);
905            if (false === $line) {
906                continue;
907            }
908            $row = explode(',', $line);
909            if (empty($row)) {
910                continue;
911            }
912            if (count($row) !== 4) {
913                throw new RuntimeException(sprintf(
914                    'A palette row must supply R, G, B and Alpha components, %s given!',
915                    var_export($row, true)
916                ));
917            }
918            list($R, $G, $B, $Alpha) = $row;
919            $ID = count($this->Palette);
920            $this->Palette[$ID] = [
921                "R" => trim($R),
922                "G" => trim($G),
923                "B" => trim($B),
924                "Alpha" => trim($Alpha)
925            ];
926        }
927        fclose($fileHandle);
928
929        /* Apply changes to current series */
930        $ID = 0;
931        if (isset($this->Data["Series"])) {
932            foreach ($this->Data["Series"] as $Key => $Value) {
933                if (!isset($this->Palette[$ID])) {
934                    $this->Data["Series"][$Key]["Color"] = ["R" => 0, "G" => 0, "B" => 0, "Alpha" => 0];
935                } else {
936                    $this->Data["Series"][$Key]["Color"] = $this->Palette[$ID];
937                }
938                $ID++;
939            }
940        }
941    }
942
943    /**
944     * Initialise a given scatter serie
945     * @param int $ID
946     * @return null
947     */
948    public function initScatterSerie($ID)
949    {
950        if (isset($this->Data["ScatterSeries"][$ID])) {
951            return null;
952        }
953
954        $this->Data["ScatterSeries"][$ID]["Description"] = "Scatter " . $ID;
955        $this->Data["ScatterSeries"][$ID]["isDrawable"] = true;
956        $this->Data["ScatterSeries"][$ID]["Picture"] = null;
957        $this->Data["ScatterSeries"][$ID]["Ticks"] = 0;
958        $this->Data["ScatterSeries"][$ID]["Weight"] = 0;
959
960        if (isset($this->Palette[$ID])) {
961            $this->Data["ScatterSeries"][$ID]["Color"] = $this->Palette[$ID];
962        } else {
963            $this->Data["ScatterSeries"][$ID]["Color"]["R"] = rand(0, 255);
964            $this->Data["ScatterSeries"][$ID]["Color"]["G"] = rand(0, 255);
965            $this->Data["ScatterSeries"][$ID]["Color"]["B"] = rand(0, 255);
966            $this->Data["ScatterSeries"][$ID]["Color"]["Alpha"] = 100;
967        }
968    }
969
970    /**
971     * Initialise a given serie
972     * @param string $Serie
973     */
974    public function initialise($Serie)
975    {
976        $ID = 0;
977        if (isset($this->Data["Series"])) {
978            $ID = count($this->Data["Series"]);
979        }
980
981        $this->Data["Series"][$Serie]["Description"] = $Serie;
982        $this->Data["Series"][$Serie]["isDrawable"] = true;
983        $this->Data["Series"][$Serie]["Picture"] = null;
984        $this->Data["Series"][$Serie]["Max"] = null;
985        $this->Data["Series"][$Serie]["Min"] = null;
986        $this->Data["Series"][$Serie]["Axis"] = 0;
987        $this->Data["Series"][$Serie]["Ticks"] = 0;
988        $this->Data["Series"][$Serie]["Weight"] = 0;
989        $this->Data["Series"][$Serie]["Shape"] = SERIE_SHAPE_FILLEDCIRCLE;
990
991        if (isset($this->Palette[$ID])) {
992            $this->Data["Series"][$Serie]["Color"] = $this->Palette[$ID];
993        } else {
994            $this->Data["Series"][$Serie]["Color"]["R"] = rand(0, 255);
995            $this->Data["Series"][$Serie]["Color"]["G"] = rand(0, 255);
996            $this->Data["Series"][$Serie]["Color"]["B"] = rand(0, 255);
997            $this->Data["Series"][$Serie]["Color"]["Alpha"] = 100;
998        }
999        $this->Data["Series"][$Serie]["Data"] = [];
1000    }
1001
1002    /**
1003     * @param int $NormalizationFactor
1004     * @param mixed $UnitChange
1005     * @param int $Round
1006     */
1007    public function normalize($NormalizationFactor = 100, $UnitChange = null, $Round = 1)
1008    {
1009        $Abscissa = $this->Data["Abscissa"];
1010
1011        $SelectedSeries = [];
1012        $MaxVal = 0;
1013        foreach (array_keys($this->Data["Axis"]) as $AxisID) {
1014            if ($UnitChange != null) {
1015                $this->Data["Axis"][$AxisID]["Unit"] = $UnitChange;
1016            }
1017
1018            foreach ($this->Data["Series"] as $SerieName => $Serie) {
1019                if ($Serie["Axis"] == $AxisID
1020                    && $Serie["isDrawable"] == true
1021                    && $SerieName != $Abscissa
1022                ) {
1023                    $SelectedSeries[$SerieName] = $SerieName;
1024
1025                    if (count($Serie["Data"]) > $MaxVal) {
1026                        $MaxVal = count($Serie["Data"]);
1027                    }
1028                }
1029            }
1030        }
1031
1032        for ($i = 0; $i <= $MaxVal - 1; $i++) {
1033            $Factor = 0;
1034            foreach ($SelectedSeries as $Key => $SerieName) {
1035                $Value = $this->Data["Series"][$SerieName]["Data"][$i];
1036                if ($Value != VOID) {
1037                    $Factor = $Factor + abs($Value);
1038                }
1039            }
1040
1041            if ($Factor != 0) {
1042                $Factor = $NormalizationFactor / $Factor;
1043
1044                foreach ($SelectedSeries as $Key => $SerieName) {
1045                    $Value = $this->Data["Series"][$SerieName]["Data"][$i];
1046
1047                    if ($Value != VOID && $Factor != $NormalizationFactor) {
1048                        $this->Data["Series"][$SerieName]["Data"][$i] = round(abs($Value) * $Factor, $Round);
1049                    } elseif ($Value == VOID || $Value == 0) {
1050                        $this->Data["Series"][$SerieName]["Data"][$i] = VOID;
1051                    } elseif ($Factor == $NormalizationFactor) {
1052                        $this->Data["Series"][$SerieName]["Data"][$i] = $NormalizationFactor;
1053                    }
1054                }
1055            }
1056        }
1057
1058        foreach ($SelectedSeries as $Key => $SerieName) {
1059            $this->Data["Series"][$SerieName]["Max"] = max(
1060                $this->stripVOID($this->Data["Series"][$SerieName]["Data"])
1061            );
1062            $this->Data["Series"][$SerieName]["Min"] = min(
1063                $this->stripVOID($this->Data["Series"][$SerieName]["Data"])
1064            );
1065        }
1066    }
1067
1068    /**
1069     * Load data from a CSV (or similar) data source
1070     * @param string $FileName
1071     * @param array $Options
1072     */
1073    public function importFromCSV($FileName, array $Options = [])
1074    {
1075        $Delimiter = isset($Options["Delimiter"]) ? $Options["Delimiter"] : ",";
1076        $GotHeader = isset($Options["GotHeader"]) ? $Options["GotHeader"] : false;
1077        $SkipColumns = isset($Options["SkipColumns"]) ? $Options["SkipColumns"] : [-1];
1078        $DefaultSerieName = isset($Options["DefaultSerieName"]) ? $Options["DefaultSerieName"] : "Serie";
1079
1080        $Handle = @fopen($FileName, "r");
1081        if ($Handle) {
1082            $HeaderParsed = false;
1083            $SerieNames = [];
1084            while (!feof($Handle)) {
1085                $Buffer = fgets($Handle, 4096);
1086                $Buffer = str_replace(chr(10), "", $Buffer);
1087                $Buffer = str_replace(chr(13), "", $Buffer);
1088                $Values = preg_split("/" . $Delimiter . "/", $Buffer);
1089
1090                if ($Buffer != "") {
1091                    if ($GotHeader && !$HeaderParsed) {
1092                        foreach ($Values as $Key => $Name) {
1093                            if (!in_array($Key, $SkipColumns)) {
1094                                $SerieNames[$Key] = $Name;
1095                            }
1096                        }
1097                        $HeaderParsed = true;
1098                    } else {
1099                        if (!count($SerieNames)) {
1100                            foreach ($Values as $Key => $Name) {
1101                                if (!in_array($Key, $SkipColumns)) {
1102                                    $SerieNames[$Key] = $DefaultSerieName . $Key;
1103                                }
1104                            }
1105                        }
1106                        foreach ($Values as $Key => $Value) {
1107                            if (!in_array($Key, $SkipColumns)) {
1108                                $this->addPoints($Value, $SerieNames[$Key]);
1109                            }
1110                        }
1111                    }
1112                }
1113            }
1114            fclose($Handle);
1115        }
1116    }
1117
1118    /**
1119     * Create a dataset based on a formula
1120     *
1121     * @param string $SerieName
1122     * @param string $Formula
1123     * @param array $Options
1124     * @return null
1125     */
1126    public function createFunctionSerie($SerieName, $Formula = "", array $Options = [])
1127    {
1128        $MinX = isset($Options["MinX"]) ? $Options["MinX"] : -10;
1129        $MaxX = isset($Options["MaxX"]) ? $Options["MaxX"] : 10;
1130        $XStep = isset($Options["XStep"]) ? $Options["XStep"] : 1;
1131        $AutoDescription = isset($Options["AutoDescription"]) ? $Options["AutoDescription"] : false;
1132        $RecordAbscissa = isset($Options["RecordAbscissa"]) ? $Options["RecordAbscissa"] : false;
1133        $AbscissaSerie = isset($Options["AbscissaSerie"]) ? $Options["AbscissaSerie"] : "Abscissa";
1134
1135        if ($Formula == "") {
1136            return null;
1137        }
1138
1139        $Result = [];
1140        $Abscissa = [];
1141        for ($i = $MinX; $i <= $MaxX; $i = $i + $XStep) {
1142            $Expression = "\$return = '!'.(" . str_replace("z", $i, $Formula) . ");";
1143            if (@eval($Expression) === false) {
1144                $return = VOID;
1145            }
1146            if ($return == "!") {
1147                $return = VOID;
1148            } else {
1149                $return = $this->right($return, strlen($return) - 1);
1150            }
1151            if ($return == "NAN") {
1152                $return = VOID;
1153            }
1154            if ($return == "INF") {
1155                $return = VOID;
1156            }
1157            if ($return == "-INF") {
1158                $return = VOID;
1159            }
1160
1161            $Abscissa[] = $i;
1162            $Result[] = $return;
1163        }
1164
1165        $this->addPoints($Result, $SerieName);
1166        if ($AutoDescription) {
1167            $this->setSerieDescription($SerieName, $Formula);
1168        }
1169        if ($RecordAbscissa) {
1170            $this->addPoints($Abscissa, $AbscissaSerie);
1171        }
1172    }
1173
1174    /**
1175     * @param mixed $Series
1176     */
1177    public function negateValues($Series)
1178    {
1179        if (!is_array($Series)) {
1180            $Series = $this->convertToArray($Series);
1181        }
1182        foreach ($Series as $Key => $SerieName) {
1183            if (isset($this->Data["Series"][$SerieName])) {
1184                $Data = [];
1185                foreach ($this->Data["Series"][$SerieName]["Data"] as $Key => $Value) {
1186                    if ($Value == VOID) {
1187                        $Data[] = VOID;
1188                    } else {
1189                        $Data[] = -$Value;
1190                    }
1191                }
1192                $this->Data["Series"][$SerieName]["Data"] = $Data;
1193                $this->Data["Series"][$SerieName]["Max"] = max(
1194                    $this->stripVOID($this->Data["Series"][$SerieName]["Data"])
1195                );
1196                $this->Data["Series"][$SerieName]["Min"] = min(
1197                    $this->stripVOID($this->Data["Series"][$SerieName]["Data"])
1198                );
1199            }
1200        }
1201    }
1202
1203    /**
1204     * Return the data & configuration of the series
1205     * @return array
1206     */
1207    public function getData()
1208    {
1209        return $this->Data;
1210    }
1211
1212    /**
1213     * Save a palette element
1214     *
1215     * @param integer $ID
1216     * @param string $Color
1217     */
1218    public function savePalette($ID, $Color)
1219    {
1220        $this->Palette[$ID] = $Color;
1221    }
1222
1223    /**
1224     * Return the palette of the series
1225     * @return array
1226     */
1227    public function getPalette()
1228    {
1229        return $this->Palette;
1230    }
1231
1232    /**
1233     * Called by the scaling algorithm to save the config
1234     * @param mixed $Axis
1235     */
1236    public function saveAxisConfig($Axis)
1237    {
1238        $this->Data["Axis"] = $Axis;
1239    }
1240
1241    /**
1242     * Save the Y Margin if set
1243     * @param mixed $Value
1244     */
1245    public function saveYMargin($Value)
1246    {
1247        $this->Data["YMargin"] = $Value;
1248    }
1249
1250    /**
1251     * Save extended configuration to the pData object
1252     * @param string $Tag
1253     * @param mixed $Values
1254     */
1255    public function saveExtendedData($Tag, $Values)
1256    {
1257        $this->Data["Extended"][$Tag] = $Values;
1258    }
1259
1260    /**
1261     * Called by the scaling algorithm to save the orientation of the scale
1262     * @param mixed $Orientation
1263     */
1264    public function saveOrientation($Orientation)
1265    {
1266        $this->Data["Orientation"] = $Orientation;
1267    }
1268
1269    /**
1270     * Convert a string to a single elements array
1271     * @param mixed $Value
1272     * @return array
1273     */
1274    public function convertToArray($Value)
1275    {
1276        return [$Value];
1277    }
1278
1279    /**
1280     * Class string wrapper
1281     * @return string
1282     */
1283    public function __toString()
1284    {
1285        return "pData object.";
1286    }
1287
1288    /**
1289     * @param string $value
1290     * @param int $NbChar
1291     * @return string
1292     */
1293    public function left($value, $NbChar)
1294    {
1295        return substr($value, 0, $NbChar);
1296    }
1297
1298    /**
1299     * @param string $value
1300     * @param int $NbChar
1301     * @return string
1302     */
1303    public function right($value, $NbChar)
1304    {
1305        return substr($value, strlen($value) - $NbChar, $NbChar);
1306    }
1307
1308    /**
1309     * @param string $value
1310     * @param int $Depart
1311     * @param int $NbChar
1312     * @return string
1313     */
1314    public function mid($value, $Depart, $NbChar)
1315    {
1316        return substr($value, $Depart - 1, $NbChar);
1317    }
1318}
1319