Path: blob/master/src/applications/fact/chart/PhabricatorChartStackedAreaDataset.php
12256 views
<?php12final class PhabricatorChartStackedAreaDataset3extends PhabricatorChartDataset {45const DATASETKEY = 'stacked-area';67private $stacks;89public function setStacks(array $stacks) {10$this->stacks = $stacks;11return $this;12}1314public function getStacks() {15return $this->stacks;16}1718protected function newChartDisplayData(19PhabricatorChartDataQuery $data_query) {2021$functions = $this->getFunctions();22$functions = mpull($functions, null, 'getKey');2324$stacks = $this->getStacks();2526if (!$stacks) {27$stacks = array(28array_reverse(array_keys($functions), true),29);30}3132$series = array();33$raw_points = array();3435foreach ($stacks as $stack) {36$stack_functions = array_select_keys($functions, $stack);3738$function_points = $this->getFunctionDatapoints(39$data_query,40$stack_functions);4142$stack_points = $function_points;4344$function_points = $this->getGeometry(45$data_query,46$function_points);4748$baseline = array();49foreach ($function_points as $function_idx => $points) {50$bounds = array();51foreach ($points as $x => $point) {52if (!isset($baseline[$x])) {53$baseline[$x] = 0;54}5556$y0 = $baseline[$x];57$baseline[$x] += $point['y'];58$y1 = $baseline[$x];5960$bounds[] = array(61'x' => $x,62'y0' => $y0,63'y1' => $y1,64);6566if (isset($stack_points[$function_idx][$x])) {67$stack_points[$function_idx][$x]['y1'] = $y1;68}69}7071$series[$function_idx] = $bounds;72}7374$raw_points += $stack_points;75}7677$series = array_select_keys($series, array_keys($functions));78$series = array_values($series);7980$raw_points = array_select_keys($raw_points, array_keys($functions));81$raw_points = array_values($raw_points);8283$range_min = null;84$range_max = null;8586foreach ($series as $geometry_list) {87foreach ($geometry_list as $geometry_item) {88$y0 = $geometry_item['y0'];89$y1 = $geometry_item['y1'];9091if ($range_min === null) {92$range_min = $y0;93}94$range_min = min($range_min, $y0, $y1);9596if ($range_max === null) {97$range_max = $y1;98}99$range_max = max($range_max, $y0, $y1);100}101}102103// We're going to group multiple events into a single point if they have104// X values that are very close to one another.105//106// If the Y values are also close to one another (these points are near107// one another in a horizontal line), it can be hard to select any108// individual point with the mouse.109//110// Even if the Y values are not close together (the points are on a111// fairly steep slope up or down), it's usually better to be able to112// mouse over a single point at the top or bottom of the slope and get113// a summary of what's going on.114115$domain_max = $data_query->getMaximumValue();116$domain_min = $data_query->getMinimumValue();117$resolution = ($domain_max - $domain_min) / 100;118119$events = array();120foreach ($raw_points as $function_idx => $points) {121$event_list = array();122123$event_group = array();124$head_event = null;125foreach ($points as $point) {126$x = $point['x'];127128if ($head_event === null) {129// We don't have any points yet, so start a new group.130$head_event = $x;131$event_group[] = $point;132} else if (($x - $head_event) <= $resolution) {133// This point is close to the first point in this group, so134// add it to the existing group.135$event_group[] = $point;136} else {137// This point is not close to the first point in the group,138// so create a new group.139$event_list[] = $event_group;140$head_event = $x;141$event_group = array($point);142}143}144145if ($event_group) {146$event_list[] = $event_group;147}148149$event_spec = array();150foreach ($event_list as $key => $event_points) {151// NOTE: We're using the last point as the representative point so152// that you can learn about a section of a chart by hovering over153// the point to right of the section, which is more intuitive than154// other points.155$event = last($event_points);156157$event = $event + array(158'n' => count($event_points),159);160161$event_list[$key] = $event;162}163164$events[] = $event_list;165}166167$wire_labels = array();168foreach ($functions as $function_key => $function) {169$label = $function->getFunctionLabel();170$wire_labels[] = $label->toWireFormat();171}172173$result = array(174'type' => $this->getDatasetTypeKey(),175'data' => $series,176'events' => $events,177'labels' => $wire_labels,178);179180return id(new PhabricatorChartDisplayData())181->setWireData($result)182->setRange(new PhabricatorChartInterval($range_min, $range_max));183}184185private function getAllXValuesAsMap(186PhabricatorChartDataQuery $data_query,187array $point_lists) {188189// We need to define every function we're drawing at every point where190// any of the functions we're drawing are defined. If we don't, we'll191// end up with weird gaps or overlaps between adjacent areas, and won't192// know how much we need to lift each point above the baseline when193// stacking the functions on top of one another.194195$must_define = array();196197$min = $data_query->getMinimumValue();198$max = $data_query->getMaximumValue();199$must_define[$max] = $max;200$must_define[$min] = $min;201202foreach ($point_lists as $point_list) {203foreach ($point_list as $x => $point) {204$must_define[$x] = $x;205}206}207208ksort($must_define);209210return $must_define;211}212213private function getFunctionDatapoints(214PhabricatorChartDataQuery $data_query,215array $functions) {216217assert_instances_of($functions, 'PhabricatorChartFunction');218219$points = array();220foreach ($functions as $idx => $function) {221$points[$idx] = array();222223$datapoints = $function->newDatapoints($data_query);224foreach ($datapoints as $point) {225$x_value = $point['x'];226$points[$idx][$x_value] = $point;227}228}229230return $points;231}232233private function getGeometry(234PhabricatorChartDataQuery $data_query,235array $point_lists) {236237$must_define = $this->getAllXValuesAsMap($data_query, $point_lists);238239foreach ($point_lists as $idx => $points) {240241$missing = array();242foreach ($must_define as $x) {243if (!isset($points[$x])) {244$missing[$x] = true;245}246}247248if (!$missing) {249continue;250}251252$values = array_keys($points);253$cursor = -1;254$length = count($values);255256foreach ($missing as $x => $ignored) {257// Move the cursor forward until we find the last point before "x"258// which is defined.259while ($cursor + 1 < $length && $values[$cursor + 1] < $x) {260$cursor++;261}262263// If this new point is to the left of all defined points, we'll264// assume the value is 0. If the point is to the right of all defined265// points, we assume the value is the same as the last known value.266267// If it's between two defined points, we average them.268269if ($cursor < 0) {270$y = 0;271} else if ($cursor + 1 < $length) {272$xmin = $values[$cursor];273$xmax = $values[$cursor + 1];274275$ymin = $points[$xmin]['y'];276$ymax = $points[$xmax]['y'];277278// Fill in the missing point by creating a linear interpolation279// between the two adjacent points.280$distance = ($x - $xmin) / ($xmax - $xmin);281$y = $ymin + (($ymax - $ymin) * $distance);282} else {283$xmin = $values[$cursor];284$y = $points[$xmin]['y'];285}286287$point_lists[$idx][$x] = array(288'x' => $x,289'y' => $y,290);291}292293ksort($point_lists[$idx]);294}295296return $point_lists;297}298299}300301302