mirror of
https://we.phorge.it/source/phorge.git
synced 2024-11-29 10:12:41 +01:00
Support explicit stacking configuration in stacked area charts
Summary: Ref T13279. Allow engines to choose how areas in a stacked area chart stack on top of one another. This could also be accomplished by using multiple stacked area datasets, but datasets would still need to know if they're stacking "up" or "down" so it's probably about the same at the end of the day. Test Plan: {F6865165} Subscribers: yelirekim Maniphest Tasks: T13279 Differential Revision: https://secure.phabricator.com/D20818
This commit is contained in:
parent
d4ed5d0428
commit
c06dd4818b
5 changed files with 247 additions and 134 deletions
|
@ -59,13 +59,6 @@ abstract class PhabricatorChartDataset
|
||||||
return $dataset;
|
return $dataset;
|
||||||
}
|
}
|
||||||
|
|
||||||
final public function toDictionary() {
|
|
||||||
return array(
|
|
||||||
'type' => $this->getDatasetTypeKey(),
|
|
||||||
'functions' => mpull($this->getFunctions(), 'toDictionary'),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
final public function getChartDisplayData(
|
final public function getChartDisplayData(
|
||||||
PhabricatorChartDataQuery $data_query) {
|
PhabricatorChartDataQuery $data_query) {
|
||||||
return $this->newChartDisplayData($data_query);
|
return $this->newChartDisplayData($data_query);
|
||||||
|
|
|
@ -60,6 +60,10 @@ abstract class PhabricatorChartFunction
|
||||||
return $this->functionLabel;
|
return $this->functionLabel;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final public function getKey() {
|
||||||
|
return $this->getFunctionLabel()->getKey();
|
||||||
|
}
|
||||||
|
|
||||||
final public static function newFromDictionary(array $map) {
|
final public static function newFromDictionary(array $map) {
|
||||||
PhutilTypeSpec::checkMap(
|
PhutilTypeSpec::checkMap(
|
||||||
$map,
|
$map,
|
||||||
|
@ -86,13 +90,6 @@ abstract class PhabricatorChartFunction
|
||||||
return $function;
|
return $function;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function toDictionary() {
|
|
||||||
return array(
|
|
||||||
'function' => $this->getFunctionKey(),
|
|
||||||
'arguments' => $this->getArgumentParser()->getRawArguments(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getSubfunctions() {
|
public function getSubfunctions() {
|
||||||
$result = array();
|
$result = array();
|
||||||
$result[] = $this;
|
$result[] = $this;
|
||||||
|
|
|
@ -3,11 +3,21 @@
|
||||||
final class PhabricatorChartFunctionLabel
|
final class PhabricatorChartFunctionLabel
|
||||||
extends Phobject {
|
extends Phobject {
|
||||||
|
|
||||||
|
private $key;
|
||||||
private $name;
|
private $name;
|
||||||
private $color;
|
private $color;
|
||||||
private $icon;
|
private $icon;
|
||||||
private $fillColor;
|
private $fillColor;
|
||||||
|
|
||||||
|
public function setKey($key) {
|
||||||
|
$this->key = $key;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getKey() {
|
||||||
|
return $this->key;
|
||||||
|
}
|
||||||
|
|
||||||
public function setName($name) {
|
public function setName($name) {
|
||||||
$this->name = $name;
|
$this->name = $name;
|
||||||
return $this;
|
return $this;
|
||||||
|
@ -46,6 +56,7 @@ final class PhabricatorChartFunctionLabel
|
||||||
|
|
||||||
public function toWireFormat() {
|
public function toWireFormat() {
|
||||||
return array(
|
return array(
|
||||||
|
'key' => $this->getKey(),
|
||||||
'name' => $this->getName(),
|
'name' => $this->getName(),
|
||||||
'color' => $this->getColor(),
|
'color' => $this->getColor(),
|
||||||
'icon' => $this->getIcon(),
|
'icon' => $this->getIcon(),
|
||||||
|
|
|
@ -5,105 +5,49 @@ final class PhabricatorChartStackedAreaDataset
|
||||||
|
|
||||||
const DATASETKEY = 'stacked-area';
|
const DATASETKEY = 'stacked-area';
|
||||||
|
|
||||||
|
private $stacks;
|
||||||
|
|
||||||
|
public function setStacks(array $stacks) {
|
||||||
|
$this->stacks = $stacks;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getStacks() {
|
||||||
|
return $this->stacks;
|
||||||
|
}
|
||||||
|
|
||||||
protected function newChartDisplayData(
|
protected function newChartDisplayData(
|
||||||
PhabricatorChartDataQuery $data_query) {
|
PhabricatorChartDataQuery $data_query) {
|
||||||
|
|
||||||
$functions = $this->getFunctions();
|
$functions = $this->getFunctions();
|
||||||
|
$functions = mpull($functions, null, 'getKey');
|
||||||
|
|
||||||
$reversed_functions = array_reverse($functions, true);
|
$stacks = $this->getStacks();
|
||||||
|
|
||||||
$function_points = array();
|
if (!$stacks) {
|
||||||
foreach ($reversed_functions as $function_idx => $function) {
|
$stacks = array(
|
||||||
$function_points[$function_idx] = array();
|
array_reverse(array_keys($functions), true),
|
||||||
|
|
||||||
$datapoints = $function->newDatapoints($data_query);
|
|
||||||
foreach ($datapoints as $point) {
|
|
||||||
$x_value = $point['x'];
|
|
||||||
$function_points[$function_idx][$x_value] = $point;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$raw_points = $function_points;
|
|
||||||
|
|
||||||
// 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) {
|
|
||||||
$missing = array();
|
|
||||||
foreach ($must_define as $x) {
|
|
||||||
if (!isset($function_points[$function_idx][$x])) {
|
|
||||||
$missing[$x] = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!$missing) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
$points = $function_points[$function_idx];
|
|
||||||
|
|
||||||
$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 = $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();
|
$series = array();
|
||||||
|
$raw_points = array();
|
||||||
|
|
||||||
|
foreach ($stacks as $stack) {
|
||||||
|
$stack_functions = array_select_keys($functions, $stack);
|
||||||
|
|
||||||
|
$function_points = $this->getFunctionDatapoints(
|
||||||
|
$data_query,
|
||||||
|
$stack_functions);
|
||||||
|
|
||||||
|
$stack_points = $function_points;
|
||||||
|
|
||||||
|
$function_points = $this->getGeometry(
|
||||||
|
$data_query,
|
||||||
|
$function_points);
|
||||||
|
|
||||||
$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])) {
|
||||||
|
@ -120,9 +64,30 @@ final class PhabricatorChartStackedAreaDataset
|
||||||
'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;
|
||||||
|
@ -134,12 +99,8 @@ final class PhabricatorChartStackedAreaDataset
|
||||||
}
|
}
|
||||||
$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.
|
||||||
//
|
//
|
||||||
|
@ -222,5 +183,118 @@ final class PhabricatorChartStackedAreaDataset
|
||||||
->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;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,6 +29,8 @@ final class PhabricatorProjectBurndownChartEngine
|
||||||
}
|
}
|
||||||
|
|
||||||
$functions = array();
|
$functions = array();
|
||||||
|
$stacks = array();
|
||||||
|
|
||||||
if ($project_phids) {
|
if ($project_phids) {
|
||||||
foreach ($project_phids as $project_phid) {
|
foreach ($project_phids as $project_phid) {
|
||||||
$function = $this->newFunction(
|
$function = $this->newFunction(
|
||||||
|
@ -42,12 +44,31 @@ final class PhabricatorProjectBurndownChartEngine
|
||||||
));
|
));
|
||||||
|
|
||||||
$function->getFunctionLabel()
|
$function->getFunctionLabel()
|
||||||
|
->setKey('moved-in')
|
||||||
->setName(pht('Tasks Moved Into Project'))
|
->setName(pht('Tasks Moved Into Project'))
|
||||||
->setColor('rgba(128, 128, 200, 1)')
|
->setColor('rgba(128, 128, 200, 1)')
|
||||||
->setFillColor('rgba(128, 128, 200, 0.15)');
|
->setFillColor('rgba(128, 128, 200, 0.15)');
|
||||||
|
|
||||||
$functions[] = $function;
|
$functions[] = $function;
|
||||||
|
|
||||||
|
$function = $this->newFunction(
|
||||||
|
array(
|
||||||
|
'accumulate',
|
||||||
|
array(
|
||||||
|
'compose',
|
||||||
|
array('fact', 'tasks.open-count.status.project', $project_phid),
|
||||||
|
array('min', 0),
|
||||||
|
),
|
||||||
|
));
|
||||||
|
|
||||||
|
$function->getFunctionLabel()
|
||||||
|
->setKey('reopened')
|
||||||
|
->setName(pht('Tasks Reopened'))
|
||||||
|
->setColor('rgba(128, 128, 200, 1)')
|
||||||
|
->setFillColor('rgba(128, 128, 200, 0.15)');
|
||||||
|
|
||||||
|
$functions[] = $function;
|
||||||
|
|
||||||
$function = $this->newFunction(
|
$function = $this->newFunction(
|
||||||
array(
|
array(
|
||||||
'accumulate',
|
'accumulate',
|
||||||
|
@ -55,12 +76,31 @@ final class PhabricatorProjectBurndownChartEngine
|
||||||
));
|
));
|
||||||
|
|
||||||
$function->getFunctionLabel()
|
$function->getFunctionLabel()
|
||||||
|
->setKey('created')
|
||||||
->setName(pht('Tasks Created'))
|
->setName(pht('Tasks Created'))
|
||||||
->setColor('rgba(0, 0, 200, 1)')
|
->setColor('rgba(0, 0, 200, 1)')
|
||||||
->setFillColor('rgba(0, 0, 200, 0.15)');
|
->setFillColor('rgba(0, 0, 200, 0.15)');
|
||||||
|
|
||||||
$functions[] = $function;
|
$functions[] = $function;
|
||||||
|
|
||||||
|
$function = $this->newFunction(
|
||||||
|
array(
|
||||||
|
'accumulate',
|
||||||
|
array(
|
||||||
|
'compose',
|
||||||
|
array('fact', 'tasks.open-count.status.project', $project_phid),
|
||||||
|
array('max', 0),
|
||||||
|
),
|
||||||
|
));
|
||||||
|
|
||||||
|
$function->getFunctionLabel()
|
||||||
|
->setKey('closed')
|
||||||
|
->setName(pht('Tasks Closed'))
|
||||||
|
->setColor('rgba(0, 200, 0, 1)')
|
||||||
|
->setFillColor('rgba(0, 200, 0, 0.15)');
|
||||||
|
|
||||||
|
$functions[] = $function;
|
||||||
|
|
||||||
$function = $this->newFunction(
|
$function = $this->newFunction(
|
||||||
array(
|
array(
|
||||||
'accumulate',
|
'accumulate',
|
||||||
|
@ -72,24 +112,15 @@ final class PhabricatorProjectBurndownChartEngine
|
||||||
));
|
));
|
||||||
|
|
||||||
$function->getFunctionLabel()
|
$function->getFunctionLabel()
|
||||||
|
->setKey('moved-out')
|
||||||
->setName(pht('Tasks Moved Out of Project'))
|
->setName(pht('Tasks Moved Out of Project'))
|
||||||
->setColor('rgba(128, 200, 128, 1)')
|
->setColor('rgba(128, 200, 128, 1)')
|
||||||
->setFillColor('rgba(128, 200, 128, 0.15)');
|
->setFillColor('rgba(128, 200, 128, 0.15)');
|
||||||
|
|
||||||
$functions[] = $function;
|
$functions[] = $function;
|
||||||
|
|
||||||
$function = $this->newFunction(
|
$stacks[] = array('created', 'reopened', 'moved-in');
|
||||||
array(
|
$stacks[] = array('closed', 'moved-out');
|
||||||
'accumulate',
|
|
||||||
array('fact', 'tasks.open-count.status.project', $project_phid),
|
|
||||||
));
|
|
||||||
|
|
||||||
$function->getFunctionLabel()
|
|
||||||
->setName(pht('Tasks Closed'))
|
|
||||||
->setColor('rgba(0, 200, 0, 1)')
|
|
||||||
->setFillColor('rgba(0, 200, 0, 0.15)');
|
|
||||||
|
|
||||||
$functions[] = $function;
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
$function = $this->newFunction(
|
$function = $this->newFunction(
|
||||||
|
@ -99,7 +130,8 @@ final class PhabricatorProjectBurndownChartEngine
|
||||||
));
|
));
|
||||||
|
|
||||||
$function->getFunctionLabel()
|
$function->getFunctionLabel()
|
||||||
->setName(pht('Tasks Created'))
|
->setKey('open')
|
||||||
|
->setName(pht('Open Tasks'))
|
||||||
->setColor('rgba(0, 0, 200, 1)')
|
->setColor('rgba(0, 0, 200, 1)')
|
||||||
->setFillColor('rgba(0, 0, 200, 0.15)');
|
->setFillColor('rgba(0, 0, 200, 0.15)');
|
||||||
|
|
||||||
|
@ -112,7 +144,8 @@ final class PhabricatorProjectBurndownChartEngine
|
||||||
));
|
));
|
||||||
|
|
||||||
$function->getFunctionLabel()
|
$function->getFunctionLabel()
|
||||||
->setName(pht('Tasks Closed'))
|
->setKey('closed')
|
||||||
|
->setName(pht('Closed Tasks'))
|
||||||
->setColor('rgba(0, 200, 0, 1)')
|
->setColor('rgba(0, 200, 0, 1)')
|
||||||
->setFillColor('rgba(0, 200, 0, 0.15)');
|
->setFillColor('rgba(0, 200, 0, 0.15)');
|
||||||
|
|
||||||
|
@ -121,9 +154,14 @@ final class PhabricatorProjectBurndownChartEngine
|
||||||
|
|
||||||
$datasets = array();
|
$datasets = array();
|
||||||
|
|
||||||
$datasets[] = id(new PhabricatorChartStackedAreaDataset())
|
$dataset = id(new PhabricatorChartStackedAreaDataset())
|
||||||
->setFunctions($functions);
|
->setFunctions($functions);
|
||||||
|
|
||||||
|
if ($stacks) {
|
||||||
|
$dataset->setStacks($stacks);
|
||||||
|
}
|
||||||
|
|
||||||
|
$datasets[] = $dataset;
|
||||||
$chart->attachDatasets($datasets);
|
$chart->attachDatasets($datasets);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue