3 final class PhabricatorChartStackedAreaDataset
4 extends PhabricatorChartDataset
{
6 const DATASETKEY
= 'stacked-area';
10 public function setStacks(array $stacks) {
11 $this->stacks
= $stacks;
15 public function getStacks() {
19 protected function newChartDisplayData(
20 PhabricatorChartDataQuery
$data_query) {
22 $functions = $this->getFunctions();
23 $functions = mpull($functions, null, 'getKey');
25 $stacks = $this->getStacks();
29 array_reverse(array_keys($functions), true),
34 $raw_points = array();
36 foreach ($stacks as $stack) {
37 $stack_functions = array_select_keys($functions, $stack);
39 $function_points = $this->getFunctionDatapoints(
43 $stack_points = $function_points;
45 $function_points = $this->getGeometry(
50 foreach ($function_points as $function_idx => $points) {
52 foreach ($points as $x => $point) {
53 if (!isset($baseline[$x])) {
58 $baseline[$x] +
= $point['y'];
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);
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) {
95 $range_min = min($range_min, $y0, $y1);
97 if ($range_max === null) {
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;
121 foreach ($raw_points as $function_idx => $points) {
122 $event_list = array();
124 $event_group = array();
126 foreach ($points as $point) {
129 if ($head_event === null) {
130 // We don't have any points yet, so start a new group.
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;
138 // This point is not close to the first point in the group,
139 // so create a new group.
140 $event_list[] = $event_group;
142 $event_group = array($point);
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
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();
175 'type' => $this->getDatasetTypeKey(),
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;
214 private function getFunctionDatapoints(
215 PhabricatorChartDataQuery
$data_query,
218 assert_instances_of($functions, 'PhabricatorChartFunction');
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;
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) {
243 foreach ($must_define as $x) {
244 if (!isset($points[$x])) {
253 $values = array_keys($points);
255 $length = count($values);
257 foreach ($missing as $x => $ignored) {
258 // Move the cursor forward until we find the last point before "x"
260 while ($cursor +
1 < $length && $values[$cursor +
1] < $x) {
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.
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);
284 $xmin = $values[$cursor];
285 $y = $points[$xmin]['y'];
288 $point_lists[$idx][$x] = array(
294 ksort($point_lists[$idx]);