@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<?php
2
3final class PhabricatorChartRenderingEngine
4 extends Phobject {
5
6 private $viewer;
7 private $chart;
8 private $storedChart;
9
10 public function setViewer(PhabricatorUser $viewer) {
11 $this->viewer = $viewer;
12 return $this;
13 }
14
15 public function getViewer() {
16 return $this->viewer;
17 }
18
19 public function setChart(PhabricatorFactChart $chart) {
20 $this->chart = $chart;
21 return $this;
22 }
23
24 public function getChart() {
25 return $this->chart;
26 }
27
28 /**
29 * Load the chart by its key
30 * @param string $chart_key 12-character string identifier of chart to load
31 * @return PhabricatorFactChart
32 */
33 public function loadChart($chart_key) {
34 $chart = id(new PhabricatorFactChart())->loadOneWhere(
35 'chartKey = %s',
36 $chart_key);
37
38 if ($chart) {
39 $this->setChart($chart);
40 }
41
42 return $chart;
43 }
44
45 /**
46 * Get the relative URI of the chart
47 * @param string $chart_key 12-character string identifier of chart to load
48 * @return string Relative URI of the chart, e.g. "fact/chart/a1b2c3d4e5f6/"
49 */
50 public static function getChartURI($chart_key) {
51 return id(new PhabricatorFactChart())
52 ->setChartKey($chart_key)
53 ->getURI();
54 }
55
56 /**
57 * @return PhabricatorFactChart
58 */
59 public function getStoredChart() {
60 if (!$this->storedChart) {
61 $chart = $this->getChart();
62 $chart_key = $chart->getChartKey();
63 if (!$chart_key) {
64 $chart_key = $chart->newChartKey();
65
66 $stored_chart = id(new PhabricatorFactChart())->loadOneWhere(
67 'chartKey = %s',
68 $chart_key);
69 if ($stored_chart) {
70 $chart = $stored_chart;
71 } else {
72 $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
73
74 try {
75 $chart->save();
76 } catch (AphrontDuplicateKeyQueryException $ex) {
77 $chart = id(new PhabricatorFactChart())->loadOneWhere(
78 'chartKey = %s',
79 $chart_key);
80 if (!$chart) {
81 throw new Exception(
82 pht(
83 'Failed to load chart with key "%s" after key collision. '.
84 'This should not be possible.',
85 $chart_key));
86 }
87 }
88
89 unset($unguarded);
90 }
91 $this->setChart($chart);
92 }
93
94 $this->storedChart = $chart;
95 }
96
97 return $this->storedChart;
98 }
99
100 /**
101 * @return PhutilSafeHTML
102 */
103 public function newChartView() {
104 $chart = $this->getStoredChart();
105 $chart_key = $chart->getChartKey();
106
107 $chart_node_id = celerity_generate_unique_node_id();
108
109 $chart_view = phutil_tag(
110 'div',
111 array(
112 'id' => $chart_node_id,
113 'class' => 'chart-hardpoint',
114 ));
115
116 $data_uri = urisprintf('/fact/chart/%s/draw/', $chart_key);
117
118 Javelin::initBehavior(
119 'line-chart',
120 array(
121 'chartNodeID' => $chart_node_id,
122 'dataURI' => (string)$data_uri,
123 ));
124
125 return $chart_view;
126 }
127
128 public function newTabularView() {
129 $viewer = $this->getViewer();
130 $tabular_data = $this->newTabularData();
131
132 $ref_keys = array();
133 foreach ($tabular_data['datasets'] as $tabular_dataset) {
134 foreach ($tabular_dataset as $function) {
135 foreach ($function['data'] as $point) {
136 foreach ($point['refs'] as $ref) {
137 $ref_keys[$ref] = $ref;
138 }
139 }
140 }
141 }
142
143 $chart = $this->getStoredChart();
144
145 $ref_map = array();
146 foreach ($chart->getDatasets() as $dataset) {
147 foreach ($dataset->getFunctions() as $function) {
148 // If we aren't looking for anything else, bail out.
149 if (!$ref_keys) {
150 break 2;
151 }
152
153 $function_refs = $function->loadRefs($ref_keys);
154
155 $ref_map += $function_refs;
156
157 // Remove the ref keys that we found data for from the list of keys
158 // we are looking for. If any function gives us data for a given ref,
159 // that's satisfactory.
160 foreach ($function_refs as $ref_key => $ref_data) {
161 unset($ref_keys[$ref_key]);
162 }
163 }
164 }
165
166 $phids = array();
167 foreach ($ref_map as $ref => $ref_data) {
168 if (isset($ref_data['objectPHID'])) {
169 $phids[] = $ref_data['objectPHID'];
170 }
171 }
172
173 $handles = $viewer->loadHandles($phids);
174
175 $tabular_view = array();
176 foreach ($tabular_data['datasets'] as $tabular_data) {
177 foreach ($tabular_data as $function) {
178 $rows = array();
179 foreach ($function['data'] as $point) {
180 $ref_views = array();
181
182 $xv = date('Y-m-d h:i:s', $point['x']);
183 $yv = $point['y'];
184
185 $point_refs = array();
186 foreach ($point['refs'] as $ref) {
187 if (!isset($ref_map[$ref])) {
188 continue;
189 }
190 $point_refs[$ref] = $ref_map[$ref];
191 }
192
193 if (!$point_refs) {
194 $rows[] = array(
195 $xv,
196 $yv,
197 null,
198 null,
199 null,
200 );
201 } else {
202 foreach ($point_refs as $ref => $ref_data) {
203 $ref_value = $ref_data['value'];
204 $ref_link = $handles[$ref_data['objectPHID']]
205 ->renderLink();
206
207 $view_uri = urisprintf(
208 '/fact/object/%s/',
209 $ref_data['objectPHID']);
210
211 $ref_button = id(new PHUIButtonView())
212 ->setIcon('fa-table')
213 ->setTag('a')
214 ->setColor('grey')
215 ->setHref($view_uri)
216 ->setText(pht('View Data'));
217
218 $rows[] = array(
219 $xv,
220 $yv,
221 $ref_value,
222 $ref_link,
223 $ref_button,
224 );
225
226 $xv = null;
227 $yv = null;
228 }
229 }
230 }
231
232 $table = id(new AphrontTableView($rows))
233 ->setHeaders(
234 array(
235 pht('X'),
236 pht('Y'),
237 pht('Raw'),
238 pht('Refs'),
239 null,
240 ))
241 ->setColumnClasses(
242 array(
243 'n',
244 'n',
245 'n',
246 'wide',
247 null,
248 ));
249
250 $tabular_view[] = id(new PHUIObjectBoxView())
251 ->setHeaderText(pht('Function'))
252 ->setTable($table);
253 }
254 }
255
256 return $tabular_view;
257 }
258
259 public function newChartData() {
260 return $this->newWireData(false);
261 }
262
263 public function newTabularData() {
264 return $this->newWireData(true);
265 }
266
267 private function newWireData($is_tabular) {
268 $chart = $this->getStoredChart();
269 $chart_key = $chart->getChartKey();
270
271 $chart_engine = PhabricatorChartEngine::newFromChart($chart)
272 ->setViewer($this->getViewer());
273 $chart_engine->buildChart($chart);
274
275 $datasets = $chart->getDatasets();
276
277 $functions = array();
278 foreach ($datasets as $dataset) {
279 foreach ($dataset->getFunctions() as $function) {
280 $functions[] = $function;
281 }
282 }
283
284 $subfunctions = array();
285 foreach ($functions as $function) {
286 foreach ($function->getSubfunctions() as $subfunction) {
287 $subfunctions[] = $subfunction;
288 }
289 }
290
291 foreach ($subfunctions as $subfunction) {
292 $subfunction->loadData();
293 }
294
295 $domain = $this->getDomain($functions);
296
297 $axis = id(new PhabricatorChartAxis())
298 ->setMinimumValue($domain->getMin())
299 ->setMaximumValue($domain->getMax());
300
301 $data_query = id(new PhabricatorChartDataQuery())
302 ->setMinimumValue($domain->getMin())
303 ->setMaximumValue($domain->getMax())
304 ->setLimit(2000);
305
306 $wire_datasets = array();
307 $ranges = array();
308 foreach ($datasets as $dataset) {
309 if ($is_tabular) {
310 $display_data = $dataset->getTabularDisplayData($data_query);
311 } else {
312 $display_data = $dataset->getChartDisplayData($data_query);
313 }
314
315 $ranges[] = $display_data->getRange();
316 $wire_datasets[] = $display_data->getWireData();
317 }
318
319 $range = $this->getRange($ranges);
320
321 $chart_data = array(
322 'datasets' => $wire_datasets,
323 'xMin' => $domain->getMin(),
324 'xMax' => $domain->getMax(),
325 'yMin' => $range->getMin(),
326 'yMax' => $range->getMax(),
327 );
328
329 return $chart_data;
330 }
331
332 private function getDomain(array $functions) {
333 $domains = array();
334 foreach ($functions as $function) {
335 $domains[] = $function->getDomain();
336 }
337
338 $domain = PhabricatorChartInterval::newFromIntervalList($domains);
339
340 // If we don't have any domain data from the actual functions, pick a
341 // plausible domain automatically.
342
343 if ($domain->getMax() === null) {
344 $domain->setMax(PhabricatorTime::getNow());
345 }
346
347 if ($domain->getMin() === null) {
348 $domain->setMin($domain->getMax() - phutil_units('365 days in seconds'));
349 }
350
351 return $domain;
352 }
353
354 private function getRange(array $ranges) {
355 $range = PhabricatorChartInterval::newFromIntervalList($ranges);
356
357 // Start the Y axis at 0 unless the chart has negative values.
358 $min = $range->getMin();
359 if ($min === null || $min >= 0) {
360 $range->setMin(0);
361 }
362
363 // If there's no maximum value, just pick a plausible default.
364 $max = $range->getMax();
365 if ($max === null) {
366 $range->setMax($range->getMin() + 100);
367 }
368
369 return $range;
370 }
371
372}