@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.

at recaptime-dev/main 323 lines 9.2 kB view raw
1<?php 2 3final class PhabricatorChartStackedAreaDataset 4 extends PhabricatorChartDataset { 5 6 const DATASETKEY = 'stacked-area'; 7 8 private $stacks; 9 10 /** 11 * @param array<array> $stacks One or more arrays with 12 * PhabricatorChartFunctionLabel names of each PhabricatorChartFunction, 13 * grouped by stacking 14 * (e.g. [["created","reopened","moved-in"],["closed","moved-out"]]) 15 */ 16 public function setStacks(array $stacks) { 17 $this->stacks = $stacks; 18 return $this; 19 } 20 21 public function getStacks() { 22 return $this->stacks; 23 } 24 25 /** 26 * @param PhabricatorChartDataQuery $data_query 27 * @return PhabricatorChartDisplayData 28 */ 29 protected function newChartDisplayData( 30 PhabricatorChartDataQuery $data_query) { 31 32 $functions = $this->getFunctions(); 33 $functions = mpull($functions, null, 'getKey'); 34 35 $stacks = $this->getStacks(); 36 37 if (!$stacks) { 38 $stacks = array( 39 array_reverse(array_keys($functions), true), 40 ); 41 } 42 43 $series = array(); 44 $raw_points = array(); 45 46 foreach ($stacks as $stack) { 47 $stack_functions = array_select_keys($functions, $stack); 48 49 $function_points = $this->getFunctionDatapoints( 50 $data_query, 51 $stack_functions); 52 53 $stack_points = $function_points; 54 55 $function_points = $this->getGeometry( 56 $data_query, 57 $function_points); 58 59 $baseline = array(); 60 foreach ($function_points as $function_idx => $points) { 61 $bounds = array(); 62 foreach ($points as $x => $point) { 63 if (!isset($baseline[$x])) { 64 $baseline[$x] = 0; 65 } 66 67 $y0 = $baseline[$x]; 68 $baseline[$x] += $point['y']; 69 $y1 = $baseline[$x]; 70 71 $bounds[] = array( 72 'x' => $x, 73 'y0' => $y0, 74 'y1' => $y1, 75 ); 76 77 if (isset($stack_points[$function_idx][$x])) { 78 $stack_points[$function_idx][$x]['y1'] = $y1; 79 } 80 } 81 82 $series[$function_idx] = $bounds; 83 } 84 85 $raw_points += $stack_points; 86 } 87 88 $series = array_select_keys($series, array_keys($functions)); 89 $series = array_values($series); 90 91 $raw_points = array_select_keys($raw_points, array_keys($functions)); 92 $raw_points = array_values($raw_points); 93 94 $range_min = null; 95 $range_max = null; 96 97 foreach ($series as $geometry_list) { 98 foreach ($geometry_list as $geometry_item) { 99 $y0 = $geometry_item['y0']; 100 $y1 = $geometry_item['y1']; 101 102 if ($range_min === null) { 103 $range_min = $y0; 104 } 105 $range_min = min($range_min, $y0, $y1); 106 107 if ($range_max === null) { 108 $range_max = $y1; 109 } 110 $range_max = max($range_max, $y0, $y1); 111 } 112 } 113 114 // We're going to group multiple events into a single point if they have 115 // X values that are very close to one another. 116 // 117 // If the Y values are also close to one another (these points are near 118 // one another in a horizontal line), it can be hard to select any 119 // individual point with the mouse. 120 // 121 // Even if the Y values are not close together (the points are on a 122 // fairly steep slope up or down), it's usually better to be able to 123 // mouse over a single point at the top or bottom of the slope and get 124 // a summary of what's going on. 125 126 $domain_max = $data_query->getMaximumValue(); 127 $domain_min = $data_query->getMinimumValue(); 128 $resolution = ($domain_max - $domain_min) / 100; 129 130 $events = array(); 131 foreach ($raw_points as $function_idx => $points) { 132 $event_list = array(); 133 134 $event_group = array(); 135 $head_event = null; 136 foreach ($points as $point) { 137 $x = $point['x']; 138 139 if ($head_event === null) { 140 // We don't have any points yet, so start a new group. 141 $head_event = $x; 142 $event_group[] = $point; 143 } else if (($x - $head_event) <= $resolution) { 144 // This point is close to the first point in this group, so 145 // add it to the existing group. 146 $event_group[] = $point; 147 } else { 148 // This point is not close to the first point in the group, 149 // so create a new group. 150 $event_list[] = $event_group; 151 $head_event = $x; 152 $event_group = array($point); 153 } 154 } 155 156 if ($event_group) { 157 $event_list[] = $event_group; 158 } 159 160 $event_spec = array(); 161 foreach ($event_list as $key => $event_points) { 162 // NOTE: We're using the last point as the representative point so 163 // that you can learn about a section of a chart by hovering over 164 // the point to right of the section, which is more intuitive than 165 // other points. 166 $event = last($event_points); 167 168 $event = $event + array( 169 'n' => count($event_points), 170 ); 171 172 $event_list[$key] = $event; 173 } 174 175 $events[] = $event_list; 176 } 177 178 $wire_labels = array(); 179 foreach ($functions as $function_key => $function) { 180 $label = $function->getFunctionLabel(); 181 $wire_labels[] = $label->toWireFormat(); 182 } 183 184 $result = array( 185 'type' => $this->getDatasetTypeKey(), 186 'data' => $series, 187 'events' => $events, 188 'labels' => $wire_labels, 189 ); 190 191 return id(new PhabricatorChartDisplayData()) 192 ->setWireData($result) 193 ->setRange(new PhabricatorChartInterval($range_min, $range_max)); 194 } 195 196 private function getAllXValuesAsMap( 197 PhabricatorChartDataQuery $data_query, 198 array $point_lists) { 199 200 // We need to define every function we're drawing at every point where 201 // any of the functions we're drawing are defined. If we don't, we'll 202 // end up with weird gaps or overlaps between adjacent areas, and won't 203 // know how much we need to lift each point above the baseline when 204 // stacking the functions on top of one another. 205 206 $must_define = array(); 207 208 $min = $data_query->getMinimumValue(); 209 $max = $data_query->getMaximumValue(); 210 $must_define[$max] = $max; 211 $must_define[$min] = $min; 212 213 foreach ($point_lists as $point_list) { 214 foreach ($point_list as $x => $point) { 215 $must_define[$x] = $x; 216 } 217 } 218 219 ksort($must_define); 220 221 return $must_define; 222 } 223 224 /** 225 * @param PhabricatorChartDataQuery $data_query 226 * @param array<PhabricatorChartFunction> $functions 227 */ 228 private function getFunctionDatapoints( 229 PhabricatorChartDataQuery $data_query, 230 array $functions) { 231 232 assert_instances_of($functions, PhabricatorChartFunction::class); 233 234 $points = array(); 235 foreach ($functions as $idx => $function) { 236 $points[$idx] = array(); 237 238 $datapoints = $function->newDatapoints($data_query); 239 foreach ($datapoints as $point) { 240 $x_value = $point['x']; 241 $points[$idx][$x_value] = $point; 242 } 243 } 244 245 return $points; 246 } 247 248 /** 249 * @param PhabricatorChartDataQuery $data_query 250 * @param array<string<int<array<string,int>,array<string,int>>>> $point_lists 251 * The key is the stack (the PhabricatorChartFunctionLabel name of the 252 * PhabricatorChartFunction (e.g. "created" or "moved-in")) and its value 253 * is an array of keys which are date epochs and their values are another 254 * array of x:date epoch and y:incremental integer pairs: 255 * array <string<epoch<string:int,string:int>>> 256 */ 257 private function getGeometry( 258 PhabricatorChartDataQuery $data_query, 259 array $point_lists) { 260 261 $must_define = $this->getAllXValuesAsMap($data_query, $point_lists); 262 263 foreach ($point_lists as $idx => $points) { 264 265 $missing = array(); 266 foreach ($must_define as $x) { 267 if (!isset($points[$x])) { 268 $missing[$x] = true; 269 } 270 } 271 272 if (!$missing) { 273 continue; 274 } 275 276 $values = array_keys($points); 277 $cursor = -1; 278 $length = count($values); 279 280 foreach ($missing as $x => $ignored) { 281 // Move the cursor forward until we find the last point before "x" 282 // which is defined. 283 while ($cursor + 1 < $length && $values[$cursor + 1] < $x) { 284 $cursor++; 285 } 286 287 // If this new point is to the left of all defined points, we'll 288 // assume the value is 0. If the point is to the right of all defined 289 // points, we assume the value is the same as the last known value. 290 291 // If it's between two defined points, we average them. 292 293 if ($cursor < 0) { 294 $y = 0; 295 } else if ($cursor + 1 < $length) { 296 $xmin = $values[$cursor]; 297 $xmax = $values[$cursor + 1]; 298 299 $ymin = $points[$xmin]['y']; 300 $ymax = $points[$xmax]['y']; 301 302 // Fill in the missing point by creating a linear interpolation 303 // between the two adjacent points. 304 $distance = ($x - $xmin) / ($xmax - $xmin); 305 $y = $ymin + (($ymax - $ymin) * $distance); 306 } else { 307 $xmin = $values[$cursor]; 308 $y = $points[$xmin]['y']; 309 } 310 311 $point_lists[$idx][$x] = array( 312 'x' => $x, 313 'y' => $y, 314 ); 315 } 316 317 ksort($point_lists[$idx]); 318 } 319 320 return $point_lists; 321 } 322 323}