Changeset View
Changeset View
Standalone View
Standalone View
src/applications/fact/chart/PhabricatorChartStackedAreaDataset.php
<?php | <?php | ||||
final class PhabricatorChartStackedAreaDataset | final class PhabricatorChartStackedAreaDataset | ||||
extends PhabricatorChartDataset { | extends PhabricatorChartDataset { | ||||
const DATASETKEY = 'stacked-area'; | const DATASETKEY = 'stacked-area'; | ||||
protected function newChartDisplayData( | private $stacks; | ||||
PhabricatorChartDataQuery $data_query) { | |||||
$functions = $this->getFunctions(); | |||||
$reversed_functions = array_reverse($functions, true); | public function setStacks(array $stacks) { | ||||
$this->stacks = $stacks; | |||||
$function_points = array(); | return $this; | ||||
foreach ($reversed_functions as $function_idx => $function) { | |||||
$function_points[$function_idx] = array(); | |||||
$datapoints = $function->newDatapoints($data_query); | |||||
foreach ($datapoints as $point) { | |||||
$x_value = $point['x']; | |||||
$function_points[$function_idx][$x_value] = $point; | |||||
} | |||||
} | } | ||||
$raw_points = $function_points; | public function getStacks() { | ||||
return $this->stacks; | |||||
// We need to define every function we're drawing at every point where | |||||
// any of the functions we're drawing are defined. If we don't, we'll | |||||
// end up with weird gaps or overlaps between adjacent areas, and won't | |||||
// know how much we need to lift each point above the baseline when | |||||
// stacking the functions on top of one another. | |||||
$must_define = array(); | |||||
foreach ($function_points as $function_idx => $points) { | |||||
foreach ($points as $x => $point) { | |||||
$must_define[$x] = $x; | |||||
} | } | ||||
} | |||||
ksort($must_define); | |||||
foreach ($reversed_functions as $function_idx => $function) { | protected function newChartDisplayData( | ||||
$missing = array(); | PhabricatorChartDataQuery $data_query) { | ||||
foreach ($must_define as $x) { | |||||
if (!isset($function_points[$function_idx][$x])) { | |||||
$missing[$x] = true; | |||||
} | |||||
} | |||||
if (!$missing) { | |||||
continue; | |||||
} | |||||
$points = $function_points[$function_idx]; | $functions = $this->getFunctions(); | ||||
$functions = mpull($functions, null, 'getKey'); | |||||
$values = array_keys($points); | $stacks = $this->getStacks(); | ||||
$cursor = -1; | |||||
$length = count($values); | |||||
foreach ($missing as $x => $ignored) { | if (!$stacks) { | ||||
// Move the cursor forward until we find the last point before "x" | $stacks = array( | ||||
// which is defined. | array_reverse(array_keys($functions), true), | ||||
while ($cursor + 1 < $length && $values[$cursor + 1] < $x) { | ); | ||||
$cursor++; | |||||
} | } | ||||
// If this new point is to the left of all defined points, we'll | $series = array(); | ||||
// assume the value is 0. If the point is to the right of all defined | $raw_points = array(); | ||||
// points, we assume the value is the same as the last known value. | |||||
// If it's between two defined points, we average them. | foreach ($stacks as $stack) { | ||||
$stack_functions = array_select_keys($functions, $stack); | |||||
if ($cursor < 0) { | $function_points = $this->getFunctionDatapoints( | ||||
$y = 0; | $data_query, | ||||
} else if ($cursor + 1 < $length) { | $stack_functions); | ||||
$xmin = $values[$cursor]; | |||||
$xmax = $values[$cursor + 1]; | |||||
$ymin = $points[$xmin]['y']; | $stack_points = $function_points; | ||||
$ymax = $points[$xmax]['y']; | |||||
// Fill in the missing point by creating a linear interpolation | $function_points = $this->getGeometry( | ||||
// between the two adjacent points. | $data_query, | ||||
$distance = ($x - $xmin) / ($xmax - $xmin); | $function_points); | ||||
$y = $ymin + (($ymax - $ymin) * $distance); | |||||
} else { | |||||
$xmin = $values[$cursor]; | |||||
$y = $function_points[$function_idx][$xmin]['y']; | |||||
} | |||||
$function_points[$function_idx][$x] = array( | |||||
'x' => $x, | |||||
'y' => $y, | |||||
); | |||||
} | |||||
ksort($function_points[$function_idx]); | |||||
} | |||||
$range_min = null; | |||||
$range_max = null; | |||||
$series = array(); | |||||
$baseline = array(); | $baseline = array(); | ||||
foreach ($function_points as $function_idx => $points) { | foreach ($function_points as $function_idx => $points) { | ||||
$below = idx($function_points, $function_idx - 1); | |||||
$bounds = array(); | $bounds = array(); | ||||
foreach ($points as $x => $point) { | foreach ($points as $x => $point) { | ||||
if (!isset($baseline[$x])) { | if (!isset($baseline[$x])) { | ||||
$baseline[$x] = 0; | $baseline[$x] = 0; | ||||
} | } | ||||
$y0 = $baseline[$x]; | $y0 = $baseline[$x]; | ||||
$baseline[$x] += $point['y']; | $baseline[$x] += $point['y']; | ||||
$y1 = $baseline[$x]; | $y1 = $baseline[$x]; | ||||
$bounds[] = array( | $bounds[] = array( | ||||
'x' => $x, | 'x' => $x, | ||||
'y0' => $y0, | 'y0' => $y0, | ||||
'y1' => $y1, | 'y1' => $y1, | ||||
); | ); | ||||
if (isset($raw_points[$function_idx][$x])) { | if (isset($stack_points[$function_idx][$x])) { | ||||
$raw_points[$function_idx][$x]['y1'] = $y1; | $stack_points[$function_idx][$x]['y1'] = $y1; | ||||
} | |||||
} | } | ||||
$series[$function_idx] = $bounds; | |||||
} | |||||
$raw_points += $stack_points; | |||||
} | |||||
$series = array_select_keys($series, array_keys($functions)); | |||||
$series = array_values($series); | |||||
$raw_points = array_select_keys($raw_points, array_keys($functions)); | |||||
$raw_points = array_values($raw_points); | |||||
$range_min = null; | |||||
$range_max = null; | |||||
foreach ($series as $geometry_list) { | |||||
foreach ($geometry_list as $geometry_item) { | |||||
$y0 = $geometry_item['y0']; | |||||
$y1 = $geometry_item['y1']; | |||||
if ($range_min === null) { | if ($range_min === null) { | ||||
$range_min = $y0; | $range_min = $y0; | ||||
} | } | ||||
$range_min = min($range_min, $y0, $y1); | $range_min = min($range_min, $y0, $y1); | ||||
if ($range_max === null) { | if ($range_max === null) { | ||||
$range_max = $y1; | $range_max = $y1; | ||||
} | } | ||||
$range_max = max($range_max, $y0, $y1); | $range_max = max($range_max, $y0, $y1); | ||||
} | } | ||||
$series[] = $bounds; | |||||
} | } | ||||
$series = array_reverse($series); | |||||
// We're going to group multiple events into a single point if they have | // We're going to group multiple events into a single point if they have | ||||
// X values that are very close to one another. | // X values that are very close to one another. | ||||
// | // | ||||
// If the Y values are also close to one another (these points are near | // If the Y values are also close to one another (these points are near | ||||
// one another in a horizontal line), it can be hard to select any | // one another in a horizontal line), it can be hard to select any | ||||
// individual point with the mouse. | // individual point with the mouse. | ||||
// | // | ||||
// Even if the Y values are not close together (the points are on a | // Even if the Y values are not close together (the points are on a | ||||
▲ Show 20 Lines • Show All 66 Lines • ▼ Show 20 Lines | $result = array( | ||||
'labels' => $wire_labels, | 'labels' => $wire_labels, | ||||
); | ); | ||||
return id(new PhabricatorChartDisplayData()) | return id(new PhabricatorChartDisplayData()) | ||||
->setWireData($result) | ->setWireData($result) | ||||
->setRange(new PhabricatorChartInterval($range_min, $range_max)); | ->setRange(new PhabricatorChartInterval($range_min, $range_max)); | ||||
} | } | ||||
private function getAllXValuesAsMap( | |||||
PhabricatorChartDataQuery $data_query, | |||||
array $point_lists) { | |||||
// We need to define every function we're drawing at every point where | |||||
// any of the functions we're drawing are defined. If we don't, we'll | |||||
// end up with weird gaps or overlaps between adjacent areas, and won't | |||||
// know how much we need to lift each point above the baseline when | |||||
// stacking the functions on top of one another. | |||||
$must_define = array(); | |||||
$min = $data_query->getMinimumValue(); | |||||
$max = $data_query->getMaximumValue(); | |||||
$must_define[$max] = $max; | |||||
$must_define[$min] = $min; | |||||
foreach ($point_lists as $point_list) { | |||||
foreach ($point_list as $x => $point) { | |||||
$must_define[$x] = $x; | |||||
} | |||||
} | |||||
ksort($must_define); | |||||
return $must_define; | |||||
} | |||||
private function getFunctionDatapoints( | |||||
PhabricatorChartDataQuery $data_query, | |||||
array $functions) { | |||||
assert_instances_of($functions, 'PhabricatorChartFunction'); | |||||
$points = array(); | |||||
foreach ($functions as $idx => $function) { | |||||
$points[$idx] = array(); | |||||
$datapoints = $function->newDatapoints($data_query); | |||||
foreach ($datapoints as $point) { | |||||
$x_value = $point['x']; | |||||
$points[$idx][$x_value] = $point; | |||||
} | |||||
} | |||||
return $points; | |||||
} | |||||
private function getGeometry( | |||||
PhabricatorChartDataQuery $data_query, | |||||
array $point_lists) { | |||||
$must_define = $this->getAllXValuesAsMap($data_query, $point_lists); | |||||
foreach ($point_lists as $idx => $points) { | |||||
$missing = array(); | |||||
foreach ($must_define as $x) { | |||||
if (!isset($points[$x])) { | |||||
$missing[$x] = true; | |||||
} | |||||
} | |||||
if (!$missing) { | |||||
continue; | |||||
} | |||||
$values = array_keys($points); | |||||
$cursor = -1; | |||||
$length = count($values); | |||||
foreach ($missing as $x => $ignored) { | |||||
// Move the cursor forward until we find the last point before "x" | |||||
// which is defined. | |||||
while ($cursor + 1 < $length && $values[$cursor + 1] < $x) { | |||||
$cursor++; | |||||
} | |||||
// If this new point is to the left of all defined points, we'll | |||||
// assume the value is 0. If the point is to the right of all defined | |||||
// points, we assume the value is the same as the last known value. | |||||
// If it's between two defined points, we average them. | |||||
if ($cursor < 0) { | |||||
$y = 0; | |||||
} else if ($cursor + 1 < $length) { | |||||
$xmin = $values[$cursor]; | |||||
$xmax = $values[$cursor + 1]; | |||||
$ymin = $points[$xmin]['y']; | |||||
$ymax = $points[$xmax]['y']; | |||||
// Fill in the missing point by creating a linear interpolation | |||||
// between the two adjacent points. | |||||
$distance = ($x - $xmin) / ($xmax - $xmin); | |||||
$y = $ymin + (($ymax - $ymin) * $distance); | |||||
} else { | |||||
$xmin = $values[$cursor]; | |||||
$y = $points[$xmin]['y']; | |||||
} | |||||
$point_lists[$idx][$x] = array( | |||||
'x' => $x, | |||||
'y' => $y, | |||||
); | |||||
} | |||||
ksort($point_lists[$idx]); | |||||
} | |||||
return $point_lists; | |||||
} | |||||
} | } |