Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
freebsd
GitHub Repository: freebsd/phabricator
Path: blob/master/src/applications/fact/chart/PhabricatorChartStackedAreaDataset.php
12256 views
1
<?php
2
3
final class PhabricatorChartStackedAreaDataset
4
extends PhabricatorChartDataset {
5
6
const DATASETKEY = 'stacked-area';
7
8
private $stacks;
9
10
public function setStacks(array $stacks) {
11
$this->stacks = $stacks;
12
return $this;
13
}
14
15
public function getStacks() {
16
return $this->stacks;
17
}
18
19
protected function newChartDisplayData(
20
PhabricatorChartDataQuery $data_query) {
21
22
$functions = $this->getFunctions();
23
$functions = mpull($functions, null, 'getKey');
24
25
$stacks = $this->getStacks();
26
27
if (!$stacks) {
28
$stacks = array(
29
array_reverse(array_keys($functions), true),
30
);
31
}
32
33
$series = array();
34
$raw_points = array();
35
36
foreach ($stacks as $stack) {
37
$stack_functions = array_select_keys($functions, $stack);
38
39
$function_points = $this->getFunctionDatapoints(
40
$data_query,
41
$stack_functions);
42
43
$stack_points = $function_points;
44
45
$function_points = $this->getGeometry(
46
$data_query,
47
$function_points);
48
49
$baseline = array();
50
foreach ($function_points as $function_idx => $points) {
51
$bounds = array();
52
foreach ($points as $x => $point) {
53
if (!isset($baseline[$x])) {
54
$baseline[$x] = 0;
55
}
56
57
$y0 = $baseline[$x];
58
$baseline[$x] += $point['y'];
59
$y1 = $baseline[$x];
60
61
$bounds[] = array(
62
'x' => $x,
63
'y0' => $y0,
64
'y1' => $y1,
65
);
66
67
if (isset($stack_points[$function_idx][$x])) {
68
$stack_points[$function_idx][$x]['y1'] = $y1;
69
}
70
}
71
72
$series[$function_idx] = $bounds;
73
}
74
75
$raw_points += $stack_points;
76
}
77
78
$series = array_select_keys($series, array_keys($functions));
79
$series = array_values($series);
80
81
$raw_points = array_select_keys($raw_points, array_keys($functions));
82
$raw_points = array_values($raw_points);
83
84
$range_min = null;
85
$range_max = null;
86
87
foreach ($series as $geometry_list) {
88
foreach ($geometry_list as $geometry_item) {
89
$y0 = $geometry_item['y0'];
90
$y1 = $geometry_item['y1'];
91
92
if ($range_min === null) {
93
$range_min = $y0;
94
}
95
$range_min = min($range_min, $y0, $y1);
96
97
if ($range_max === null) {
98
$range_max = $y1;
99
}
100
$range_max = max($range_max, $y0, $y1);
101
}
102
}
103
104
// We're going to group multiple events into a single point if they have
105
// X values that are very close to one another.
106
//
107
// If the Y values are also close to one another (these points are near
108
// one another in a horizontal line), it can be hard to select any
109
// individual point with the mouse.
110
//
111
// Even if the Y values are not close together (the points are on a
112
// fairly steep slope up or down), it's usually better to be able to
113
// mouse over a single point at the top or bottom of the slope and get
114
// a summary of what's going on.
115
116
$domain_max = $data_query->getMaximumValue();
117
$domain_min = $data_query->getMinimumValue();
118
$resolution = ($domain_max - $domain_min) / 100;
119
120
$events = array();
121
foreach ($raw_points as $function_idx => $points) {
122
$event_list = array();
123
124
$event_group = array();
125
$head_event = null;
126
foreach ($points as $point) {
127
$x = $point['x'];
128
129
if ($head_event === null) {
130
// We don't have any points yet, so start a new group.
131
$head_event = $x;
132
$event_group[] = $point;
133
} else if (($x - $head_event) <= $resolution) {
134
// This point is close to the first point in this group, so
135
// add it to the existing group.
136
$event_group[] = $point;
137
} else {
138
// This point is not close to the first point in the group,
139
// so create a new group.
140
$event_list[] = $event_group;
141
$head_event = $x;
142
$event_group = array($point);
143
}
144
}
145
146
if ($event_group) {
147
$event_list[] = $event_group;
148
}
149
150
$event_spec = array();
151
foreach ($event_list as $key => $event_points) {
152
// NOTE: We're using the last point as the representative point so
153
// that you can learn about a section of a chart by hovering over
154
// the point to right of the section, which is more intuitive than
155
// other points.
156
$event = last($event_points);
157
158
$event = $event + array(
159
'n' => count($event_points),
160
);
161
162
$event_list[$key] = $event;
163
}
164
165
$events[] = $event_list;
166
}
167
168
$wire_labels = array();
169
foreach ($functions as $function_key => $function) {
170
$label = $function->getFunctionLabel();
171
$wire_labels[] = $label->toWireFormat();
172
}
173
174
$result = array(
175
'type' => $this->getDatasetTypeKey(),
176
'data' => $series,
177
'events' => $events,
178
'labels' => $wire_labels,
179
);
180
181
return id(new PhabricatorChartDisplayData())
182
->setWireData($result)
183
->setRange(new PhabricatorChartInterval($range_min, $range_max));
184
}
185
186
private function getAllXValuesAsMap(
187
PhabricatorChartDataQuery $data_query,
188
array $point_lists) {
189
190
// We need to define every function we're drawing at every point where
191
// any of the functions we're drawing are defined. If we don't, we'll
192
// end up with weird gaps or overlaps between adjacent areas, and won't
193
// know how much we need to lift each point above the baseline when
194
// stacking the functions on top of one another.
195
196
$must_define = array();
197
198
$min = $data_query->getMinimumValue();
199
$max = $data_query->getMaximumValue();
200
$must_define[$max] = $max;
201
$must_define[$min] = $min;
202
203
foreach ($point_lists as $point_list) {
204
foreach ($point_list as $x => $point) {
205
$must_define[$x] = $x;
206
}
207
}
208
209
ksort($must_define);
210
211
return $must_define;
212
}
213
214
private function getFunctionDatapoints(
215
PhabricatorChartDataQuery $data_query,
216
array $functions) {
217
218
assert_instances_of($functions, 'PhabricatorChartFunction');
219
220
$points = array();
221
foreach ($functions as $idx => $function) {
222
$points[$idx] = array();
223
224
$datapoints = $function->newDatapoints($data_query);
225
foreach ($datapoints as $point) {
226
$x_value = $point['x'];
227
$points[$idx][$x_value] = $point;
228
}
229
}
230
231
return $points;
232
}
233
234
private function getGeometry(
235
PhabricatorChartDataQuery $data_query,
236
array $point_lists) {
237
238
$must_define = $this->getAllXValuesAsMap($data_query, $point_lists);
239
240
foreach ($point_lists as $idx => $points) {
241
242
$missing = array();
243
foreach ($must_define as $x) {
244
if (!isset($points[$x])) {
245
$missing[$x] = true;
246
}
247
}
248
249
if (!$missing) {
250
continue;
251
}
252
253
$values = array_keys($points);
254
$cursor = -1;
255
$length = count($values);
256
257
foreach ($missing as $x => $ignored) {
258
// Move the cursor forward until we find the last point before "x"
259
// which is defined.
260
while ($cursor + 1 < $length && $values[$cursor + 1] < $x) {
261
$cursor++;
262
}
263
264
// If this new point is to the left of all defined points, we'll
265
// assume the value is 0. If the point is to the right of all defined
266
// points, we assume the value is the same as the last known value.
267
268
// If it's between two defined points, we average them.
269
270
if ($cursor < 0) {
271
$y = 0;
272
} else if ($cursor + 1 < $length) {
273
$xmin = $values[$cursor];
274
$xmax = $values[$cursor + 1];
275
276
$ymin = $points[$xmin]['y'];
277
$ymax = $points[$xmax]['y'];
278
279
// Fill in the missing point by creating a linear interpolation
280
// between the two adjacent points.
281
$distance = ($x - $xmin) / ($xmax - $xmin);
282
$y = $ymin + (($ymax - $ymin) * $distance);
283
} else {
284
$xmin = $values[$cursor];
285
$y = $points[$xmin]['y'];
286
}
287
288
$point_lists[$idx][$x] = array(
289
'x' => $x,
290
'y' => $y,
291
);
292
}
293
294
ksort($point_lists[$idx]);
295
}
296
297
return $point_lists;
298
}
299
300
}
301
302