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 
56  public function AxisType($NewValue)
57  {
58  $ValidTypes = [
59  self::AXIS_CATEGORY,
60  self::AXIS_TIME_DAILY,
61  self::AXIS_TIME_WEEKLY,
62  self::AXIS_TIME_MONTHLY,
63  self::AXIS_TIME_YEARLY,
64  ];
65 
66  # toss exception if the given type is not valid
67  if (!in_array($NewValue, $ValidTypes))
68  {
69  throw new Exception("Invalid axis type for bar charts: ".$NewValue);
70  }
71 
72  # otherwise, save the new type and clear any cached chart data
73  $this->AxisType = $NewValue;
74  }
75 
80  public function YLabel($NewValue)
81  {
82  $this->YLabel = $NewValue;
83  }
84 
89  public function Zoom($NewValue)
90  {
91  $this->Zoom = $NewValue;
92  }
93 
98  public function Stacked($NewValue)
99  {
100  $this->Stacked = $NewValue;
101  }
102 
107  public function Horizontal($NewValue)
108  {
109  $this->Horizontal = TRUE;
110  }
111 
116  public function Gridlines($NewValue)
117  {
118  $this->Gridlines = $NewValue;
119  }
120 
126  public function ShowCategoryLabels($NewValue)
127  {
128  $this->ShowCategoryLabels = $NewValue;
129  }
130 
131  # axis types
132  const AXIS_CATEGORY = "category";
133  const AXIS_TIME_DAILY = "daily";
134  const AXIS_TIME_WEEKLY = "weekly";
135  const AXIS_TIME_MONTHLY = "monthly";
136  const AXIS_TIME_YEARLY = "yearly";
137 
138  # ---- PRIVATE INTERFACE --------------------------------------------------
139 
144  protected function PrepareData()
145  {
146  # input data is
147  # [ CatNameOrTimestamp => [Data1 => Val, Data2 => Val, ...], ... ]
148  #
149  # and the format that C3 expects is
150  # [ "Data1", Val, Val, ... ]
151  # [ "Data2", Val, Val, ... ]
152 
153  # extract the names of all the bars
154  $BarNames = [];
155  foreach ($this->Data as $Entries)
156  {
157  foreach ($Entries as $BarName => $YVal)
158  {
159  $BarNames[$BarName] = 1;
160  }
161  }
162  $BarNames = array_keys($BarNames);
163 
164  # start the chart off with no data
165  $this->Chart["data"]["columns"] = [];
166 
167  if ($this->AxisType == self::AXIS_CATEGORY)
168  {
169  # for categorical plots, data stays in place
170  $Data = $this->Data;
171 
172  # set up X labels
173  if ($this->ShowCategoryLabels)
174  {
175  $this->Chart["axis"]["x"]["categories"] =
176  $this->ShortCategoryNames($BarNames);
177  }
178  else
179  {
180  $this->Chart["axis"]["x"]["categories"] =
181  array_fill(0, count($BarNames), "");
182  }
183 
184  # and fix up the display for single-category charts
185  if ($this->SingleCategory)
186  {
187  $this->Chart["tooltip"]["grouped"] = FALSE;
188  }
189  }
190  else
191  {
192  # for time series data, we need to sort our data into bins
193  $Data = $this->SortDataIntoBins($BarNames);
194 
195  # convert our timestamps to JS-friendly date strings
196  $Timestamps = array_keys($Data);
197  array_walk($Timestamps, function(&$Val, $Key)
198  {
199  $Val = strftime("%Y-%m-%d", $Val);
200  });
201  array_unshift($Timestamps, "x-timestamp-x");
202 
203  # add this in to our data columns
204  $this->Chart["data"]["columns"][]= $Timestamps;
205  }
206 
207  # generate one row of data per bar to use for plotting
208  foreach ($BarNames as $BarName)
209  {
210  if (isset($this->LegendLabels[$BarName]))
211  {
212  $MyLabel = $this->LegendLabels[$BarName];
213  $this->LabelLUT[$MyLabel] = $BarName;
214  }
215  else
216  {
217  $MyLabel = $BarName;
218  }
219 
220  $DataRow = [$MyLabel];
221  foreach ($Data as $Entries)
222  {
223  $DataRow[]= isset($Entries[$BarName]) ? $Entries[$BarName] : 0;
224  }
225  $this->Chart["data"]["columns"][] = $DataRow;
226  }
227 
228  $this->Chart["data"]["type"] = "bar";
229 
230  if ($this->AxisType == self::AXIS_CATEGORY)
231  {
232  $this->Chart["axis"]["x"]["type"] = "category";
233  }
234  else
235  {
236  $this->AddToChart([
237  "data" => [
238  "x" => "x-timestamp-x",
239  "xFormat" => "%Y-%m-%d",
240  ],
241  "axis" => [
242  "x" => [
243  "type" => "timeseries",
244  ],
245  ],
246  ]);
247  }
248 
249  if (!is_null($this->YLabel))
250  {
251  $this->Chart["axis"]["y"]["label"] = $this->YLabel;
252  }
253 
254  if ($this->Zoom)
255  {
256  $this->Chart["zoom"]["enabled"] = TRUE;
257  }
258 
259  if ($this->Stacked)
260  {
261  $this->Chart["data"]["groups"] = [
262  $this->ShortCategoryNames($BarNames),
263  ];
264  }
265 
266  if ($this->Horizontal)
267  {
268  $this->Chart["axis"]["rotated"] = TRUE;
269  }
270 
271  if ($this->Gridlines)
272  {
273  $this->Chart["grid"]["y"]["show"] = TRUE;
274  }
275  }
276 
284  protected function SortDataIntoBins($BarNames)
285  {
286  # create an array to store the binned data
287  $BinnedData = [];
288 
289  # iterate over all our input data.
290  foreach ($this->Data as $TS => $Entries)
291  {
292  # place this timestamp in the appropriate bin
293  $TS = $this->BinTimestamp($TS);
294 
295  # if we have no results in this bin, then these are
296  # the first
297  if (!isset($BinnedData[$TS]))
298  {
299  $BinnedData[$TS] = $Entries;
300  }
301  else
302  {
303  # otherwise, iterate over the keys we were given
304  foreach ($Entries as $Key => $Val)
305  {
306  # if we have a value for this key
307  if (isset($BinnedData[$TS][$Key]))
308  {
309  # then add this new value to it
310  $BinnedData[$TS][$Key] += $Val;
311  }
312  else
313  {
314  # otherwise, insert the new value
315  $BinnedData[$TS][$Key] = $Val;
316  }
317  }
318  }
319  }
320 
321  ksort($BinnedData);
322  reset($BinnedData);
323 
324  # build up a revised data set with no gaps
325  $GaplessData = [];
326  # prime the revised set with the first element
327  $GaplessData[key($BinnedData)] = current($BinnedData);
328 
329  # iterate over the remaining elements
330  while (($Row = next($BinnedData)) !== FALSE)
331  {
332  $BinsAdded = 0;
333 
334  # if the next element is not the next bin, add an empty element
335  while (key($BinnedData) != $this->NextBin(key($GaplessData)))
336  {
337  $GaplessData[$this->NextBin(key($GaplessData))] =
338  array_fill_keys($BarNames, 0);
339  end($GaplessData);
340 
341  if ($BinsAdded > 1000)
342  {
343  throw new Exception(
344  "Over 1000 empty bins added. "
345  ."Terminating possible infinite loop.");
346  }
347  }
348 
349  # and add the current element
350  $GaplessData[key($BinnedData)] = $Row;
351  end($GaplessData);
352  }
353 
354  return $GaplessData;
355  }
356 
362  protected function BinTimestamp($TS)
363  {
364  if (!preg_match("/^[0-9]+$/", $TS))
365  {
366  $TS = strtotime($TS);
367  }
368 
369  switch ($this->AxisType)
370  {
371  case self::AXIS_TIME_DAILY:
372  return strtotime(strftime("%Y-%m-%d 00:00:00", $TS));
373  break;
374 
375  case self::AXIS_TIME_WEEKLY:
376  $DateInfo = strptime(strftime(
377  "%Y-%m-%d 00:00:00", $TS), "%Y-%m-%d %H:%M:%S");
378 
379  $Year = $DateInfo["tm_year"] + 1900;
380  $Month = $DateInfo["tm_mon"] + 1;
381  $Day = $DateInfo["tm_mday"] - $DateInfo["tm_wday"];
382 
383  return mktime(0, 0, 0, $Month, $Day, $Year);
384  break;
385 
386  case self::AXIS_TIME_MONTHLY:
387  return strtotime(strftime("%Y-%m-01 00:00:00", $TS));
388  break;
389 
390  case self::AXIS_TIME_YEARLY:
391  return strtotime(strftime("%Y-01-01 00:00:00", $TS));
392  break;
393  }
394  }
395 
401  protected function NextBin($BinTS)
402  {
403  $ThisBin = strftime("%Y-%m-%d %H:%M:%S", $BinTS);
404  $Units = [
405  self::AXIS_TIME_DAILY => "day",
406  self::AXIS_TIME_WEEKLY => "week",
407  self::AXIS_TIME_MONTHLY => "month",
408  self::AXIS_TIME_YEARLY => "year",
409  ];
410 
411  return strtotime($ThisBin." + 1 ".$Units[$this->AxisType]);
412  }
413 
419  protected function ShortCategoryNames($LongNames)
420  {
421  $ShortNames = [];
422 
423  foreach ($LongNames as $Name)
424  {
425  $ShortNames[]= isset($this->LegendLabels[$Name]) ?
426  $this->LegendLabels[$Name] : $Name ;
427  }
428 
429  return $ShortNames;
430  }
431 
432  protected $AxisType = self::AXIS_CATEGORY;
433  protected $YLabel = NULL;
434  protected $Zoom = FALSE;
435  protected $Stacked = FALSE;
436  protected $Horizontal = FALSE;
437  protected $SingleCategory = FALSE;
438  protected $Gridlines = TRUE;
439  protected $ShowCategoryLabels = TRUE;
440 }
__construct($Data)
Class constructor.
Definition: BarChart.php:26
const AXIS_TIME_WEEKLY
Definition: BarChart.php:134
YLabel($NewValue)
Set the Y axis label for a bar chart.
Definition: BarChart.php:80
Base class for generating and displaying a chart.
Definition: Chart_Base.php:13
Zoom($NewValue)
Enable/Disable zooming for this chart.
Definition: BarChart.php:89
ShowCategoryLabels($NewValue)
Enable/disable display of category labels along the X axis on categorical charts (by default...
Definition: BarChart.php:126
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:136
const AXIS_TIME_MONTHLY
Definition: BarChart.php:135
const AXIS_TIME_DAILY
Definition: BarChart.php:133
NextBin($BinTS)
Get the next bin.
Definition: BarChart.php:401
Gridlines($NewValue)
Enable/disable display of grid lines.
Definition: BarChart.php:116
BinTimestamp($TS)
Determine which bin a specified timestamp belongs in.
Definition: BarChart.php:362
AddToChart($Data)
Merge an array of settings into $this->Chart.
Definition: Chart_Base.php:261
$ShowCategoryLabels
Definition: BarChart.php:439
const AXIS_CATEGORY
Definition: BarChart.php:132
AxisType($NewValue)
Set the axis type of a bar chart (default is AXIS_CATEGORY).
Definition: BarChart.php:56
Horizontal($NewValue)
Enable/disable horizontal display.
Definition: BarChart.php:107
ShortCategoryNames($LongNames)
Get abbreviated category names (e.g., for the legend).
Definition: BarChart.php:419
Stacked($NewValue)
Enable/Disable bar stacking.
Definition: BarChart.php:98
PrepareData()
Prepare data for plotting.
Definition: BarChart.php:144
$SingleCategory
Definition: BarChart.php:437
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:284