Remove all "FileHasObject" edge reads and writes
[phabricator.git] / src / applications / fact / chart / PhabricatorChartStackedAreaDataset.php
blob2ba08ea1c909815c8ba66a245549efad2ba92a68
1 <?php
3 final class PhabricatorChartStackedAreaDataset
4 extends PhabricatorChartDataset {
6 const DATASETKEY = 'stacked-area';
8 private $stacks;
10 public function setStacks(array $stacks) {
11 $this->stacks = $stacks;
12 return $this;
15 public function getStacks() {
16 return $this->stacks;
19 protected function newChartDisplayData(
20 PhabricatorChartDataQuery $data_query) {
22 $functions = $this->getFunctions();
23 $functions = mpull($functions, null, 'getKey');
25 $stacks = $this->getStacks();
27 if (!$stacks) {
28 $stacks = array(
29 array_reverse(array_keys($functions), true),
33 $series = array();
34 $raw_points = array();
36 foreach ($stacks as $stack) {
37 $stack_functions = array_select_keys($functions, $stack);
39 $function_points = $this->getFunctionDatapoints(
40 $data_query,
41 $stack_functions);
43 $stack_points = $function_points;
45 $function_points = $this->getGeometry(
46 $data_query,
47 $function_points);
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;
57 $y0 = $baseline[$x];
58 $baseline[$x] += $point['y'];
59 $y1 = $baseline[$x];
61 $bounds[] = array(
62 'x' => $x,
63 'y0' => $y0,
64 'y1' => $y1,
67 if (isset($stack_points[$function_idx][$x])) {
68 $stack_points[$function_idx][$x]['y1'] = $y1;
72 $series[$function_idx] = $bounds;
75 $raw_points += $stack_points;
78 $series = array_select_keys($series, array_keys($functions));
79 $series = array_values($series);
81 $raw_points = array_select_keys($raw_points, array_keys($functions));
82 $raw_points = array_values($raw_points);
84 $range_min = null;
85 $range_max = null;
87 foreach ($series as $geometry_list) {
88 foreach ($geometry_list as $geometry_item) {
89 $y0 = $geometry_item['y0'];
90 $y1 = $geometry_item['y1'];
92 if ($range_min === null) {
93 $range_min = $y0;
95 $range_min = min($range_min, $y0, $y1);
97 if ($range_max === null) {
98 $range_max = $y1;
100 $range_max = max($range_max, $y0, $y1);
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.
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.
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.
116 $domain_max = $data_query->getMaximumValue();
117 $domain_min = $data_query->getMinimumValue();
118 $resolution = ($domain_max - $domain_min) / 100;
120 $events = array();
121 foreach ($raw_points as $function_idx => $points) {
122 $event_list = array();
124 $event_group = array();
125 $head_event = null;
126 foreach ($points as $point) {
127 $x = $point['x'];
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);
146 if ($event_group) {
147 $event_list[] = $event_group;
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);
158 $event = $event + array(
159 'n' => count($event_points),
162 $event_list[$key] = $event;
165 $events[] = $event_list;
168 $wire_labels = array();
169 foreach ($functions as $function_key => $function) {
170 $label = $function->getFunctionLabel();
171 $wire_labels[] = $label->toWireFormat();
174 $result = array(
175 'type' => $this->getDatasetTypeKey(),
176 'data' => $series,
177 'events' => $events,
178 'labels' => $wire_labels,
181 return id(new PhabricatorChartDisplayData())
182 ->setWireData($result)
183 ->setRange(new PhabricatorChartInterval($range_min, $range_max));
186 private function getAllXValuesAsMap(
187 PhabricatorChartDataQuery $data_query,
188 array $point_lists) {
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.
196 $must_define = array();
198 $min = $data_query->getMinimumValue();
199 $max = $data_query->getMaximumValue();
200 $must_define[$max] = $max;
201 $must_define[$min] = $min;
203 foreach ($point_lists as $point_list) {
204 foreach ($point_list as $x => $point) {
205 $must_define[$x] = $x;
209 ksort($must_define);
211 return $must_define;
214 private function getFunctionDatapoints(
215 PhabricatorChartDataQuery $data_query,
216 array $functions) {
218 assert_instances_of($functions, 'PhabricatorChartFunction');
220 $points = array();
221 foreach ($functions as $idx => $function) {
222 $points[$idx] = array();
224 $datapoints = $function->newDatapoints($data_query);
225 foreach ($datapoints as $point) {
226 $x_value = $point['x'];
227 $points[$idx][$x_value] = $point;
231 return $points;
234 private function getGeometry(
235 PhabricatorChartDataQuery $data_query,
236 array $point_lists) {
238 $must_define = $this->getAllXValuesAsMap($data_query, $point_lists);
240 foreach ($point_lists as $idx => $points) {
242 $missing = array();
243 foreach ($must_define as $x) {
244 if (!isset($points[$x])) {
245 $missing[$x] = true;
249 if (!$missing) {
250 continue;
253 $values = array_keys($points);
254 $cursor = -1;
255 $length = count($values);
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++;
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.
268 // If it's between two defined points, we average them.
270 if ($cursor < 0) {
271 $y = 0;
272 } else if ($cursor + 1 < $length) {
273 $xmin = $values[$cursor];
274 $xmax = $values[$cursor + 1];
276 $ymin = $points[$xmin]['y'];
277 $ymax = $points[$xmax]['y'];
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'];
288 $point_lists[$idx][$x] = array(
289 'x' => $x,
290 'y' => $y,
294 ksort($point_lists[$idx]);
297 return $point_lists;