CWIS Developer Documentation

- Home
- Main Page
- Namespaces
- Classes
- Files

- File List
- File Members

BarChart.php
Go to the documentation of this file.
1 <?PHP
2 #
3 # FILE: BarChart.php
4 #
5 # Part of the Collection Workflow Integration System (CWIS)
6 # Copyright 2017 Edward Almasy and Internet Scout Research Group
7 # http://scout.wisc.edu/cwis/
8 #
9
13 class BarChart extends Chart_Base
14 {
15 # ---- PUBLIC INTERFACE --------------------------------------------------
16
26 public function __construct($Data)
27 {
28 # if data provided is an array of arrays, then just copy it in
29 if (is_array(reset($Data)))
30 {
31 $this->Data = $Data;
32 }
33 # otherwise, normalize to array of arrays format
34 else
35 {
36 $this->SingleCategory = TRUE;
37 $this->Stacked = TRUE;
38 $this->LegendPosition = static::LEGEND_NONE;
39
40 $this->Data = [];
41 foreach ($Data as $Name => $Val)
42 {
43 $this->Data[$Name] = [$Name => $Val];
44 }
45 }
46 }
47
57 public function AxisType($NewValue=NULL)
58 {
59 if (func_num_args()>0)
60 {
61 $ValidTypes = [
62 self::AXIS_CATEGORY,
63 self::AXIS_TIME_DAILY,
64 self::AXIS_TIME_WEEKLY,
65 self::AXIS_TIME_MONTHLY,
66 self::AXIS_TIME_YEARLY,
67 ];
68
69 # toss exception if the given type is not valid
70 if (!in_array($NewValue, $ValidTypes))
71 {
72 throw new Exception("Invalid axis type for bar charts: ".$NewValue);
73 }
74
75 $this->AxisType = $NewValue;
76 }
77
78 return $this->AxisType;
79 }
80
86 public function YLabel($NewValue=NULL)
87 {
88 if (func_num_args()>0)
89 {
90 $this->YLabel = $NewValue;
91 }
92
93 return $this->YLabel;
94 }
95
102 public function BarWidth($NewValue=NULL)
103 {
104 if (func_num_args()>0)
105 {
106 if (!is_null($NewValue) &&
107 $NewValue <= 0 || $NewValue > 100)
108 {
109 throw new Exception("Invalid bar width: ".$NewValue);
110 }
111 $this->BarWidth = $NewValue;
112 }
113 return $this->BarWidth;
114 }
115
122 public function Zoom($NewValue=NULL)
123 {
124 if (func_num_args()>0)
125 {
126 if (!is_bool($NewValue))
127 {
128 throw new Exception("Invalid Zoom setting -- must be boolean");
129 }
130 $this->Zoom = $NewValue;
131 }
132 return $this->Zoom;
133 }
134
141 public function Stacked($NewValue=NULL)
142 {
143 if (func_num_args()>0)
144 {
145 if (!is_bool($NewValue))
146 {
147 throw new Exception("Invalid Stacked setting -- must be boolean");
148 }
149
150 $this->Stacked = $NewValue;
151 }
152
153 return $this->Stacked;
154 }
155
162 public function Horizontal($NewValue=NULL)
163 {
164 if (func_num_args()>0)
165 {
166 if (!is_bool($NewValue))
167 {
168 throw new Exception("Invalid Horizontal setting -- must be boolean");
169 }
170 $this->Horizontal = TRUE;
171 }
172
173 return $this->Horizontal;
174 }
175
182 public function Gridlines($NewValue=NULL)
183 {
184 if (func_num_args()>0)
185 {
186 if (!is_bool($NewValue))
187 {
188 throw new Exception("Invalid Gridlines setting -- must be boolean");
189 }
190 $this->Gridlines = $NewValue;
191 }
192
193 return $this->Gridlines;
194 }
195
203 public function ShowCategoryLabels($NewValue=NULL)
204 {
205 if (func_num_args()>0)
206 {
207 if (!is_bool($NewValue))
208 {
209 throw new Exception(
210 "Invalid ShowCategoryLabels setting -- must be boolean");
211 }
212 $this->ShowCategoryLabels = $NewValue;
213 }
214
215 return $this->ShowCategoryLabels;
216 }
217
218 # axis types
219 const AXIS_CATEGORY = "category";
220 const AXIS_TIME_DAILY = "daily";
221 const AXIS_TIME_WEEKLY = "weekly";
222 const AXIS_TIME_MONTHLY = "monthly";
223 const AXIS_TIME_YEARLY = "yearly";
224
225 # ---- PRIVATE INTERFACE --------------------------------------------------
226
231 protected function PrepareData()
232 {
233 # input data is
234 # [ CatNameOrTimestamp => [Data1 => Val, Data2 => Val, ...], ... ]
235 #
236 # and the format that C3 expects is
237 # [ "Data1", Val, Val, ... ]
238 # [ "Data2", Val, Val, ... ]
239
240 # extract the names of all the bars
241 $BarNames = [];
242 foreach ($this->Data as $Entries)
243 {
244 foreach ($Entries as $BarName => $YVal)
245 {
246 $BarNames[$BarName] = 1;
247 }
248 }
249 $BarNames = array_keys($BarNames);
250
251 # start the chart off with no data
252 $this->Chart["data"]["columns"] = [];
253
254 if ($this->AxisType == self::AXIS_CATEGORY)
255 {
256 # for categorical plots, data stays in place
257 $Data = $this->Data;
258
259 # set up X labels
260 if ($this->ShowCategoryLabels)
261 {
262 $this->Chart["axis"]["x"]["categories"] =
263 $this->ShortCategoryNames($BarNames);
264 }
265 else
266 {
267 $this->Chart["axis"]["x"]["categories"] =
268 array_fill(0, count($BarNames), "");
269 }
270
271 # and fix up the display for single-category charts
272 if ($this->SingleCategory)
273 {
274 $this->Chart["tooltip"]["grouped"] = FALSE;
275 }
276 }
277 else
278 {
279 # for time series data, we need to sort our data into bins
280 $Data = $this->SortDataIntoBins($BarNames);
281
282 # convert our timestamps to JS-friendly date strings
283 $Timestamps = array_keys($Data);
284 array_walk($Timestamps, function(&$Val, $Key)
285 {
286 $Val = strftime("%Y-%m-%d", $Val);
287 });
288 array_unshift($Timestamps, "x-timestamp-x");
289
290 # add this in to our data columns
291 $this->Chart["data"]["columns"][]= $Timestamps;
292 }
293
294 # generate one row of data per bar to use for plotting
295
296 # see http://c3js.org/reference.html#data-columns for format of 'columns' element.
297 # since C3 always uses the label in 'columns' for the legend,
298 # we'll need to populate the TooltipLabels array that is keyed
299 # by legend label where values give the tooltip label
300 foreach ($BarNames as $BarName)
301 {
302 $Label = isset($this->Labels[$BarName]) ?
303 $this->Labels[$BarName] : $BarName ;
304
305 if (isset($this->LegendLabels[$BarName]))
306 {
307 $MyLabel = $this->LegendLabels[$BarName];
308 $this->TooltipLabels[$MyLabel] = $Label;
309 }
310 else
311 {
312 $MyLabel = $Label;
313 }
314
315 $DataRow = [$MyLabel];
316 foreach ($Data as $Entries)
317 {
318 $DataRow[]= isset($Entries[$BarName]) ? $Entries[$BarName] : 0;
319 }
320 $this->Chart["data"]["columns"][] = $DataRow;
321 }
322
323 $this->Chart["data"]["type"] = "bar";
324
325 if ($this->AxisType == self::AXIS_CATEGORY)
326 {
327 $this->Chart["axis"]["x"]["type"] = "category";
328 }
329 else
330 {
331 $this->AddToChart([
332 "data" => [
333 "x" => "x-timestamp-x",
334 "xFormat" => "%Y-%m-%d",
335 ],
336 "axis" => [
337 "x" => [
338 "type" => "timeseries",
339 ],
340 ],
341 ]);
342 }
343
344 if (!is_null($this->BarWidth))
345 {
346 $this->Chart["bar"]["width"]["ratio"] = $this->BarWidth / 100;
347 }
348
349 if (!is_null($this->YLabel))
350 {
351 $this->Chart["axis"]["y"]["label"] = $this->YLabel;
352 }
353
354 if ($this->Zoom)
355 {
356 $this->Chart["zoom"]["enabled"] = TRUE;
357 }
358
359 if ($this->Stacked)
360 {
361 $this->Chart["data"]["groups"] = [
362 $this->ShortCategoryNames($BarNames),
363 ];
364 }
365
366 if ($this->Horizontal)
367 {
368 $this->Chart["axis"]["rotated"] = TRUE;
369 }
370
371 if ($this->Gridlines)
372 {
373 $this->Chart["grid"]["y"]["show"] = TRUE;
374 }
375 }
376
384 protected function SortDataIntoBins($BarNames)
385 {
386 # create an array to store the binned data
387 $BinnedData = [];
388
389 # iterate over all our input data.
390 foreach ($this->Data as $TS => $Entries)
391 {
392 # place this timestamp in the appropriate bin
393 $TS = $this->BinTimestamp($TS);
394
395 # if we have no results in this bin, then these are
396 # the first
397 if (!isset($BinnedData[$TS]))
398 {
399 $BinnedData[$TS] = $Entries;
400 }
401 else
402 {
403 # otherwise, iterate over the keys we were given
404 foreach ($Entries as $Key => $Val)
405 {
406 # if we have a value for this key
407 if (isset($BinnedData[$TS][$Key]))
408 {
409 # then add this new value to it
410 $BinnedData[$TS][$Key] += $Val;
411 }
412 else
413 {
414 # otherwise, insert the new value
415 $BinnedData[$TS][$Key] = $Val;
416 }
417 }
418 }
419 }
420
421 ksort($BinnedData);
422 reset($BinnedData);
423
424 # build up a revised data set with no gaps
425 $GaplessData = [];
426 # prime the revised set with the first element
427 $GaplessData[key($BinnedData)] = current($BinnedData);
428
429 # iterate over the remaining elements
430 while (($Row = next($BinnedData)) !== FALSE)
431 {
432 $BinsAdded = 0;
433
434 # if the next element is not the next bin, add an empty element
435 while (key($BinnedData) != $this->NextBin(key($GaplessData)))
436 {
437 $GaplessData[$this->NextBin(key($GaplessData))] =
438 array_fill_keys($BarNames, 0);
439 end($GaplessData);
440
441 if ($BinsAdded > 1000)
442 {
443 throw new Exception(
444 "Over 1000 empty bins added. "
445 ."Terminating possible infinite loop.");
446 }
447 }
448
449 # and add the current element
450 $GaplessData[key($BinnedData)] = $Row;
451 end($GaplessData);
452 }
453
454 return $GaplessData;
455 }
456
462 protected function BinTimestamp($TS)
463 {
464 if (!preg_match("/^[0-9]+$/", $TS))
465 {
466 $TS = strtotime($TS);
467 }
468
469 switch ($this->AxisType)
470 {
471 case self::AXIS_TIME_DAILY:
472 return strtotime(strftime("%Y-%m-%d 00:00:00", $TS));
473 break;
474
475 case self::AXIS_TIME_WEEKLY:
476 $DateInfo = strptime(strftime(
477 "%Y-%m-%d 00:00:00", $TS), "%Y-%m-%d %H:%M:%S");
478
479 $Year = $DateInfo["tm_year"] + 1900;
480 $Month = $DateInfo["tm_mon"] + 1;
481 $Day = $DateInfo["tm_mday"] - $DateInfo["tm_wday"];
482
483 return mktime(0, 0, 0, $Month, $Day, $Year);
484 break;
485
486 case self::AXIS_TIME_MONTHLY:
487 return strtotime(strftime("%Y-%m-01 00:00:00", $TS));
488 break;
489
490 case self::AXIS_TIME_YEARLY:
491 return strtotime(strftime("%Y-01-01 00:00:00", $TS));
492 break;
493 }
494 }
495
501 protected function NextBin($BinTS)
502 {
503 $ThisBin = strftime("%Y-%m-%d %H:%M:%S", $BinTS);
504 $Units = [
505 self::AXIS_TIME_DAILY => "day",
506 self::AXIS_TIME_WEEKLY => "week",
507 self::AXIS_TIME_MONTHLY => "month",
508 self::AXIS_TIME_YEARLY => "year",
509 ];
510
511 return strtotime($ThisBin." + 1 ".$Units[$this->AxisType]);
512 }
513
519 protected function ShortCategoryNames($LongNames)
520 {
521 $ShortNames = [];
522
523 foreach ($LongNames as $Name)
524 {
525 $ShortNames[]= isset($this->LegendLabels[$Name]) ?
526 $this->LegendLabels[$Name] : $Name ;
527 }
528
529 return $ShortNames;
530 }
531
532 protected $AxisType = self::AXIS_CATEGORY;
533 protected $YLabel = NULL;
534 protected $Zoom = FALSE;
535 protected $Stacked = FALSE;
536 protected $Horizontal = FALSE;
537 protected $SingleCategory = FALSE;
538 protected $Gridlines = TRUE;
539 protected $ShowCategoryLabels = TRUE;
540 protected $BarWidth = NULL;
541 }
BarChart\__construct
__construct($Data)
Class constructor.
Definition: BarChart.php:26
BarChart\Stacked
Stacked($NewValue=NULL)
Get/Set bar stacking setting.
Definition: BarChart.php:141
BarChart\Horizontal
Horizontal($NewValue=NULL)
Get/Set horizontal display setting.
Definition: BarChart.php:162
BarChart\Gridlines
Gridlines($NewValue=NULL)
Enable/disable display of grid lines.
Definition: BarChart.php:182
BarChart\AXIS_TIME_WEEKLY
const AXIS_TIME_WEEKLY
Definition: BarChart.php:221
Chart_Base
Base class for generating and displaying a chart.
Definition: Chart_Base.php:13
BarChart\ShowCategoryLabels
ShowCategoryLabels($NewValue=NULL)
Enable/disable display of category labels along the X axis on categorical charts (by default...
Definition: BarChart.php:203
Chart_Base\Labels
Labels($NewValue=NULL)
Get/set chart element labels (pie slices, bars, etc).
Definition: Chart_Base.php:57
BarChart\$YLabel
$YLabel
Definition: BarChart.php:533
Chart_Base\LegendLabels
LegendLabels($LegendLabels)
Set shortened labels to be used in the legend of the chart.
Definition: Chart_Base.php:43
BarChart\AXIS_TIME_YEARLY
const AXIS_TIME_YEARLY
Definition: BarChart.php:223
BarChart\$BarWidth
$BarWidth
Definition: BarChart.php:540
BarChart\BarWidth
BarWidth($NewValue=NULL)
Get/set bar width as a percentage of the distance between ticks.
Definition: BarChart.php:102
BarChart\AXIS_TIME_MONTHLY
const AXIS_TIME_MONTHLY
Definition: BarChart.php:222
BarChart\AxisType
AxisType($NewValue=NULL)
Get/set the axis type of a bar chart (default is AXIS_CATEGORY).
Definition: BarChart.php:57
BarChart\AXIS_TIME_DAILY
const AXIS_TIME_DAILY
Definition: BarChart.php:220
BarChart\Zoom
Zoom($NewValue=NULL)
Enable/Disable zooming for this chart.
Definition: BarChart.php:122
BarChart\NextBin
NextBin($BinTS)
Get the next bin.
Definition: BarChart.php:501
BarChart\$Gridlines
$Gridlines
Definition: BarChart.php:538
PHP
PHP
Definition: RSSClient--Test.php:12
BarChart\BinTimestamp
BinTimestamp($TS)
Determine which bin a specified timestamp belongs in.
Definition: BarChart.php:462
Chart_Base\AddToChart
AddToChart($Data)
Merge an array of settings into $this->Chart.
Definition: Chart_Base.php:298
BarChart\$ShowCategoryLabels
$ShowCategoryLabels
Definition: BarChart.php:539
Chart_Base\$Data
$Data
Definition: Chart_Base.php:325
BarChart\AXIS_CATEGORY
const AXIS_CATEGORY
Definition: BarChart.php:219
BarChart\ShortCategoryNames
ShortCategoryNames($LongNames)
Get abbreviated category names (e.g., for the legend).
Definition: BarChart.php:519
BarChart\$Horizontal
$Horizontal
Definition: BarChart.php:536
BarChart\$AxisType
$AxisType
Definition: BarChart.php:532
BarChart\YLabel
YLabel($NewValue=NULL)
Get/set the Y axis label for a bar chart.
Definition: BarChart.php:86
BarChart\$Zoom
$Zoom
Definition: BarChart.php:534
BarChart\PrepareData
PrepareData()
Prepare data for plotting.
Definition: BarChart.php:231
BarChart\$SingleCategory
$SingleCategory
Definition: BarChart.php:537
Chart_Base\LegendPosition
LegendPosition($Position)
Set legend position.
Definition: Chart_Base.php:26
BarChart\$Stacked
$Stacked
Definition: BarChart.php:535
BarChart
Class for generating and displaying a bar chart.
Definition: BarChart.php:13
BarChart\SortDataIntoBins
SortDataIntoBins($BarNames)
Sort the user-provided data into bins with sizes given by $this->AxisType, filling in any gaps in the...
Definition: BarChart.php:384