CWIS Developer Documentation
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 
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 }
__construct($Data)
Class constructor.
Definition: BarChart.php:26
Stacked($NewValue=NULL)
Get/Set bar stacking setting.
Definition: BarChart.php:141
Horizontal($NewValue=NULL)
Get/Set horizontal display setting.
Definition: BarChart.php:162
Gridlines($NewValue=NULL)
Enable/disable display of grid lines.
Definition: BarChart.php:182
const AXIS_TIME_WEEKLY
Definition: BarChart.php:221
Base class for generating and displaying a chart.
Definition: Chart_Base.php:13
ShowCategoryLabels($NewValue=NULL)
Enable/disable display of category labels along the X axis on categorical charts (by default...
Definition: BarChart.php:203
Labels($NewValue=NULL)
Get/set chart element labels (pie slices, bars, etc).
Definition: Chart_Base.php:57
LegendLabels($LegendLabels)
Set shortened labels to be used in the legend of the chart.
Definition: Chart_Base.php:43
const AXIS_TIME_YEARLY
Definition: BarChart.php:223
BarWidth($NewValue=NULL)
Get/set bar width as a percentage of the distance between ticks.
Definition: BarChart.php:102
const AXIS_TIME_MONTHLY
Definition: BarChart.php:222
AxisType($NewValue=NULL)
Get/set the axis type of a bar chart (default is AXIS_CATEGORY).
Definition: BarChart.php:57
const AXIS_TIME_DAILY
Definition: BarChart.php:220
Zoom($NewValue=NULL)
Enable/Disable zooming for this chart.
Definition: BarChart.php:122
NextBin($BinTS)
Get the next bin.
Definition: BarChart.php:501
BinTimestamp($TS)
Determine which bin a specified timestamp belongs in.
Definition: BarChart.php:462
AddToChart($Data)
Merge an array of settings into $this->Chart.
Definition: Chart_Base.php:298
$ShowCategoryLabels
Definition: BarChart.php:539
const AXIS_CATEGORY
Definition: BarChart.php:219
ShortCategoryNames($LongNames)
Get abbreviated category names (e.g., for the legend).
Definition: BarChart.php:519
YLabel($NewValue=NULL)
Get/set the Y axis label for a bar chart.
Definition: BarChart.php:86
PrepareData()
Prepare data for plotting.
Definition: BarChart.php:231
$SingleCategory
Definition: BarChart.php:537
LegendPosition($Position)
Set legend position.
Definition: Chart_Base.php:26
Class for generating and displaying a bar chart.
Definition: BarChart.php:13
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