@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.) hq.recaptime.dev/wiki/Phorge
phorge phabricator
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

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

+235 -122
-7
src/applications/fact/chart/PhabricatorChartDataset.php
··· 59 59 return $dataset; 60 60 } 61 61 62 - final public function toDictionary() { 63 - return array( 64 - 'type' => $this->getDatasetTypeKey(), 65 - 'functions' => mpull($this->getFunctions(), 'toDictionary'), 66 - ); 67 - } 68 - 69 62 final public function getChartDisplayData( 70 63 PhabricatorChartDataQuery $data_query) { 71 64 return $this->newChartDisplayData($data_query);
+4 -7
src/applications/fact/chart/PhabricatorChartFunction.php
··· 60 60 return $this->functionLabel; 61 61 } 62 62 63 + final public function getKey() { 64 + return $this->getFunctionLabel()->getKey(); 65 + } 66 + 63 67 final public static function newFromDictionary(array $map) { 64 68 PhutilTypeSpec::checkMap( 65 69 $map, ··· 84 88 ->setArguments($map['arguments']); 85 89 86 90 return $function; 87 - } 88 - 89 - public function toDictionary() { 90 - return array( 91 - 'function' => $this->getFunctionKey(), 92 - 'arguments' => $this->getArgumentParser()->getRawArguments(), 93 - ); 94 91 } 95 92 96 93 public function getSubfunctions() {
+11
src/applications/fact/chart/PhabricatorChartFunctionLabel.php
··· 3 3 final class PhabricatorChartFunctionLabel 4 4 extends Phobject { 5 5 6 + private $key; 6 7 private $name; 7 8 private $color; 8 9 private $icon; 9 10 private $fillColor; 11 + 12 + public function setKey($key) { 13 + $this->key = $key; 14 + return $this; 15 + } 16 + 17 + public function getKey() { 18 + return $this->key; 19 + } 10 20 11 21 public function setName($name) { 12 22 $this->name = $name; ··· 46 56 47 57 public function toWireFormat() { 48 58 return array( 59 + 'key' => $this->getKey(), 49 60 'name' => $this->getName(), 50 61 'color' => $this->getColor(), 51 62 'icon' => $this->getIcon(),
+171 -97
src/applications/fact/chart/PhabricatorChartStackedAreaDataset.php
··· 5 5 6 6 const DATASETKEY = 'stacked-area'; 7 7 8 - protected function newChartDisplayData( 9 - PhabricatorChartDataQuery $data_query) { 10 - $functions = $this->getFunctions(); 8 + private $stacks; 11 9 12 - $reversed_functions = array_reverse($functions, true); 10 + public function setStacks(array $stacks) { 11 + $this->stacks = $stacks; 12 + return $this; 13 + } 13 14 14 - $function_points = array(); 15 - foreach ($reversed_functions as $function_idx => $function) { 16 - $function_points[$function_idx] = array(); 15 + public function getStacks() { 16 + return $this->stacks; 17 + } 17 18 18 - $datapoints = $function->newDatapoints($data_query); 19 - foreach ($datapoints as $point) { 20 - $x_value = $point['x']; 21 - $function_points[$function_idx][$x_value] = $point; 22 - } 23 - } 19 + protected function newChartDisplayData( 20 + PhabricatorChartDataQuery $data_query) { 24 21 25 - $raw_points = $function_points; 22 + $functions = $this->getFunctions(); 23 + $functions = mpull($functions, null, 'getKey'); 26 24 27 - // We need to define every function we're drawing at every point where 28 - // any of the functions we're drawing are defined. If we don't, we'll 29 - // end up with weird gaps or overlaps between adjacent areas, and won't 30 - // know how much we need to lift each point above the baseline when 31 - // stacking the functions on top of one another. 25 + $stacks = $this->getStacks(); 32 26 33 - $must_define = array(); 34 - foreach ($function_points as $function_idx => $points) { 35 - foreach ($points as $x => $point) { 36 - $must_define[$x] = $x; 37 - } 27 + if (!$stacks) { 28 + $stacks = array( 29 + array_reverse(array_keys($functions), true), 30 + ); 38 31 } 39 - ksort($must_define); 40 32 41 - foreach ($reversed_functions as $function_idx => $function) { 42 - $missing = array(); 43 - foreach ($must_define as $x) { 44 - if (!isset($function_points[$function_idx][$x])) { 45 - $missing[$x] = true; 46 - } 47 - } 33 + $series = array(); 34 + $raw_points = array(); 48 35 49 - if (!$missing) { 50 - continue; 51 - } 52 - 53 - $points = $function_points[$function_idx]; 36 + foreach ($stacks as $stack) { 37 + $stack_functions = array_select_keys($functions, $stack); 54 38 55 - $values = array_keys($points); 56 - $cursor = -1; 57 - $length = count($values); 39 + $function_points = $this->getFunctionDatapoints( 40 + $data_query, 41 + $stack_functions); 58 42 59 - foreach ($missing as $x => $ignored) { 60 - // Move the cursor forward until we find the last point before "x" 61 - // which is defined. 62 - while ($cursor + 1 < $length && $values[$cursor + 1] < $x) { 63 - $cursor++; 64 - } 43 + $stack_points = $function_points; 65 44 66 - // If this new point is to the left of all defined points, we'll 67 - // assume the value is 0. If the point is to the right of all defined 68 - // points, we assume the value is the same as the last known value. 45 + $function_points = $this->getGeometry( 46 + $data_query, 47 + $function_points); 69 48 70 - // If it's between two defined points, we average them. 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; 55 + } 71 56 72 - if ($cursor < 0) { 73 - $y = 0; 74 - } else if ($cursor + 1 < $length) { 75 - $xmin = $values[$cursor]; 76 - $xmax = $values[$cursor + 1]; 57 + $y0 = $baseline[$x]; 58 + $baseline[$x] += $point['y']; 59 + $y1 = $baseline[$x]; 77 60 78 - $ymin = $points[$xmin]['y']; 79 - $ymax = $points[$xmax]['y']; 61 + $bounds[] = array( 62 + 'x' => $x, 63 + 'y0' => $y0, 64 + 'y1' => $y1, 65 + ); 80 66 81 - // Fill in the missing point by creating a linear interpolation 82 - // between the two adjacent points. 83 - $distance = ($x - $xmin) / ($xmax - $xmin); 84 - $y = $ymin + (($ymax - $ymin) * $distance); 85 - } else { 86 - $xmin = $values[$cursor]; 87 - $y = $function_points[$function_idx][$xmin]['y']; 67 + if (isset($stack_points[$function_idx][$x])) { 68 + $stack_points[$function_idx][$x]['y1'] = $y1; 69 + } 88 70 } 89 71 90 - $function_points[$function_idx][$x] = array( 91 - 'x' => $x, 92 - 'y' => $y, 93 - ); 72 + $series[$function_idx] = $bounds; 94 73 } 95 74 96 - ksort($function_points[$function_idx]); 75 + $raw_points += $stack_points; 97 76 } 98 77 99 - $range_min = null; 100 - $range_max = null; 101 - 102 - $series = array(); 103 - $baseline = array(); 104 - foreach ($function_points as $function_idx => $points) { 105 - $below = idx($function_points, $function_idx - 1); 106 - 107 - $bounds = array(); 108 - foreach ($points as $x => $point) { 109 - if (!isset($baseline[$x])) { 110 - $baseline[$x] = 0; 111 - } 78 + $series = array_select_keys($series, array_keys($functions)); 79 + $series = array_values($series); 112 80 113 - $y0 = $baseline[$x]; 114 - $baseline[$x] += $point['y']; 115 - $y1 = $baseline[$x]; 81 + $raw_points = array_select_keys($raw_points, array_keys($functions)); 82 + $raw_points = array_values($raw_points); 116 83 117 - $bounds[] = array( 118 - 'x' => $x, 119 - 'y0' => $y0, 120 - 'y1' => $y1, 121 - ); 84 + $range_min = null; 85 + $range_max = null; 122 86 123 - if (isset($raw_points[$function_idx][$x])) { 124 - $raw_points[$function_idx][$x]['y1'] = $y1; 125 - } 87 + foreach ($series as $geometry_list) { 88 + foreach ($geometry_list as $geometry_item) { 89 + $y0 = $geometry_item['y0']; 90 + $y1 = $geometry_item['y1']; 126 91 127 92 if ($range_min === null) { 128 93 $range_min = $y0; ··· 134 99 } 135 100 $range_max = max($range_max, $y0, $y1); 136 101 } 137 - 138 - $series[] = $bounds; 139 102 } 140 - 141 - $series = array_reverse($series); 142 103 143 104 // We're going to group multiple events into a single point if they have 144 105 // X values that are very close to one another. ··· 222 183 ->setRange(new PhabricatorChartInterval($range_min, $range_max)); 223 184 } 224 185 186 + private function getAllXValuesAsMap( 187 + PhabricatorChartDataQuery $data_query, 188 + array $point_lists) { 189 + 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. 195 + 196 + $must_define = array(); 197 + 198 + $min = $data_query->getMinimumValue(); 199 + $max = $data_query->getMaximumValue(); 200 + $must_define[$max] = $max; 201 + $must_define[$min] = $min; 202 + 203 + foreach ($point_lists as $point_list) { 204 + foreach ($point_list as $x => $point) { 205 + $must_define[$x] = $x; 206 + } 207 + } 208 + 209 + ksort($must_define); 210 + 211 + return $must_define; 212 + } 213 + 214 + private function getFunctionDatapoints( 215 + PhabricatorChartDataQuery $data_query, 216 + array $functions) { 217 + 218 + assert_instances_of($functions, 'PhabricatorChartFunction'); 219 + 220 + $points = array(); 221 + foreach ($functions as $idx => $function) { 222 + $points[$idx] = array(); 223 + 224 + $datapoints = $function->newDatapoints($data_query); 225 + foreach ($datapoints as $point) { 226 + $x_value = $point['x']; 227 + $points[$idx][$x_value] = $point; 228 + } 229 + } 230 + 231 + return $points; 232 + } 233 + 234 + private function getGeometry( 235 + PhabricatorChartDataQuery $data_query, 236 + array $point_lists) { 237 + 238 + $must_define = $this->getAllXValuesAsMap($data_query, $point_lists); 239 + 240 + foreach ($point_lists as $idx => $points) { 241 + 242 + $missing = array(); 243 + foreach ($must_define as $x) { 244 + if (!isset($points[$x])) { 245 + $missing[$x] = true; 246 + } 247 + } 248 + 249 + if (!$missing) { 250 + continue; 251 + } 252 + 253 + $values = array_keys($points); 254 + $cursor = -1; 255 + $length = count($values); 256 + 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++; 262 + } 263 + 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. 267 + 268 + // If it's between two defined points, we average them. 269 + 270 + if ($cursor < 0) { 271 + $y = 0; 272 + } else if ($cursor + 1 < $length) { 273 + $xmin = $values[$cursor]; 274 + $xmax = $values[$cursor + 1]; 275 + 276 + $ymin = $points[$xmin]['y']; 277 + $ymax = $points[$xmax]['y']; 278 + 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']; 286 + } 287 + 288 + $point_lists[$idx][$x] = array( 289 + 'x' => $x, 290 + 'y' => $y, 291 + ); 292 + } 293 + 294 + ksort($point_lists[$idx]); 295 + } 296 + 297 + return $point_lists; 298 + } 225 299 226 300 }
+49 -11
src/applications/project/chart/PhabricatorProjectBurndownChartEngine.php
··· 29 29 } 30 30 31 31 $functions = array(); 32 + $stacks = array(); 33 + 32 34 if ($project_phids) { 33 35 foreach ($project_phids as $project_phid) { 34 36 $function = $this->newFunction( ··· 42 44 )); 43 45 44 46 $function->getFunctionLabel() 47 + ->setKey('moved-in') 45 48 ->setName(pht('Tasks Moved Into Project')) 46 49 ->setColor('rgba(128, 128, 200, 1)') 47 50 ->setFillColor('rgba(128, 128, 200, 0.15)'); ··· 51 54 $function = $this->newFunction( 52 55 array( 53 56 'accumulate', 57 + array( 58 + 'compose', 59 + array('fact', 'tasks.open-count.status.project', $project_phid), 60 + array('min', 0), 61 + ), 62 + )); 63 + 64 + $function->getFunctionLabel() 65 + ->setKey('reopened') 66 + ->setName(pht('Tasks Reopened')) 67 + ->setColor('rgba(128, 128, 200, 1)') 68 + ->setFillColor('rgba(128, 128, 200, 0.15)'); 69 + 70 + $functions[] = $function; 71 + 72 + $function = $this->newFunction( 73 + array( 74 + 'accumulate', 54 75 array('fact', 'tasks.open-count.create.project', $project_phid), 55 76 )); 56 77 57 78 $function->getFunctionLabel() 79 + ->setKey('created') 58 80 ->setName(pht('Tasks Created')) 59 81 ->setColor('rgba(0, 0, 200, 1)') 60 82 ->setFillColor('rgba(0, 0, 200, 0.15)'); ··· 66 88 'accumulate', 67 89 array( 68 90 'compose', 69 - array('fact', 'tasks.open-count.assign.project', $project_phid), 91 + array('fact', 'tasks.open-count.status.project', $project_phid), 70 92 array('max', 0), 71 93 ), 72 94 )); 73 95 74 96 $function->getFunctionLabel() 75 - ->setName(pht('Tasks Moved Out of Project')) 76 - ->setColor('rgba(128, 200, 128, 1)') 77 - ->setFillColor('rgba(128, 200, 128, 0.15)'); 97 + ->setKey('closed') 98 + ->setName(pht('Tasks Closed')) 99 + ->setColor('rgba(0, 200, 0, 1)') 100 + ->setFillColor('rgba(0, 200, 0, 0.15)'); 78 101 79 102 $functions[] = $function; 80 103 81 104 $function = $this->newFunction( 82 105 array( 83 106 'accumulate', 84 - array('fact', 'tasks.open-count.status.project', $project_phid), 107 + array( 108 + 'compose', 109 + array('fact', 'tasks.open-count.assign.project', $project_phid), 110 + array('max', 0), 111 + ), 85 112 )); 86 113 87 114 $function->getFunctionLabel() 88 - ->setName(pht('Tasks Closed')) 89 - ->setColor('rgba(0, 200, 0, 1)') 90 - ->setFillColor('rgba(0, 200, 0, 0.15)'); 115 + ->setKey('moved-out') 116 + ->setName(pht('Tasks Moved Out of Project')) 117 + ->setColor('rgba(128, 200, 128, 1)') 118 + ->setFillColor('rgba(128, 200, 128, 0.15)'); 91 119 92 120 $functions[] = $function; 121 + 122 + $stacks[] = array('created', 'reopened', 'moved-in'); 123 + $stacks[] = array('closed', 'moved-out'); 93 124 } 94 125 } else { 95 126 $function = $this->newFunction( ··· 99 130 )); 100 131 101 132 $function->getFunctionLabel() 102 - ->setName(pht('Tasks Created')) 133 + ->setKey('open') 134 + ->setName(pht('Open Tasks')) 103 135 ->setColor('rgba(0, 0, 200, 1)') 104 136 ->setFillColor('rgba(0, 0, 200, 0.15)'); 105 137 ··· 112 144 )); 113 145 114 146 $function->getFunctionLabel() 115 - ->setName(pht('Tasks Closed')) 147 + ->setKey('closed') 148 + ->setName(pht('Closed Tasks')) 116 149 ->setColor('rgba(0, 200, 0, 1)') 117 150 ->setFillColor('rgba(0, 200, 0, 0.15)'); 118 151 ··· 121 154 122 155 $datasets = array(); 123 156 124 - $datasets[] = id(new PhabricatorChartStackedAreaDataset()) 157 + $dataset = id(new PhabricatorChartStackedAreaDataset()) 125 158 ->setFunctions($functions); 126 159 160 + if ($stacks) { 161 + $dataset->setStacks($stacks); 162 + } 163 + 164 + $datasets[] = $dataset; 127 165 $chart->attachDatasets($datasets); 128 166 } 129 167