@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

Track chart datapoints from their sources and provide a tabular view of chart data

Summary: Depends on D20815. Ref T13279. Give datapoints "refs", which allow us to figure out where particular datapoints came from even after the point is transformed by functions. For now, show the raw points in a table below the chart.

Test Plan: Viewed chart data, saw reasonable-looking numbers.

Subscribers: yelirekim

Maniphest Tasks: T13279

Differential Revision: https://secure.phabricator.com/D20816

+315 -16
+1 -1
src/__phutil_library_map__.php
··· 8301 8301 'PhabricatorAccessLog' => 'Phobject', 8302 8302 'PhabricatorAccessLogConfigOptions' => 'PhabricatorApplicationConfigOptions', 8303 8303 'PhabricatorAccessibilitySetting' => 'PhabricatorSelectSetting', 8304 - 'PhabricatorAccumulateChartFunction' => 'PhabricatorChartFunction', 8304 + 'PhabricatorAccumulateChartFunction' => 'PhabricatorHigherOrderChartFunction', 8305 8305 'PhabricatorActionListView' => 'AphrontTagView', 8306 8306 'PhabricatorActionView' => 'AphrontView', 8307 8307 'PhabricatorActivitySettingsPanel' => 'PhabricatorSettingsPanel',
+1 -9
src/applications/fact/chart/PhabricatorAccumulateChartFunction.php
··· 1 1 <?php 2 2 3 3 final class PhabricatorAccumulateChartFunction 4 - extends PhabricatorChartFunction { 4 + extends PhabricatorHigherOrderChartFunction { 5 5 6 6 const FUNCTIONKEY = 'accumulate'; 7 7 ··· 11 11 ->setName('x') 12 12 ->setType('function'), 13 13 ); 14 - } 15 - 16 - public function getDomain() { 17 - return $this->getArgument('x')->getDomain(); 18 - } 19 - 20 - public function newInputValues(PhabricatorChartDataQuery $query) { 21 - return $this->getArgument('x')->newInputValues($query); 22 14 } 23 15 24 16 public function evaluateFunction(array $xv) {
+31
src/applications/fact/chart/PhabricatorChartDataset.php
··· 75 75 PhabricatorChartDataQuery $data_query); 76 76 77 77 78 + final public function getTabularDisplayData( 79 + PhabricatorChartDataQuery $data_query) { 80 + $results = array(); 81 + 82 + $functions = $this->getFunctions(); 83 + foreach ($functions as $function) { 84 + $datapoints = $function->newDatapoints($data_query); 85 + 86 + $refs = $function->getDataRefs(ipull($datapoints, 'x')); 87 + 88 + foreach ($datapoints as $key => $point) { 89 + $x = $point['x']; 90 + 91 + if (isset($refs[$x])) { 92 + $xrefs = $refs[$x]; 93 + } else { 94 + $xrefs = array(); 95 + } 96 + 97 + $datapoints[$key]['refs'] = $xrefs; 98 + } 99 + 100 + $results[] = array( 101 + 'data' => $datapoints, 102 + ); 103 + } 104 + 105 + return id(new PhabricatorChartDisplayData()) 106 + ->setWireData($results); 107 + } 108 + 78 109 }
+2
src/applications/fact/chart/PhabricatorChartFunction.php
··· 180 180 } 181 181 182 182 abstract public function evaluateFunction(array $xv); 183 + abstract public function getDataRefs(array $xv); 184 + abstract public function loadRefs(array $refs); 183 185 184 186 public function getDomain() { 185 187 return null;
+18
src/applications/fact/chart/PhabricatorComposeChartFunction.php
··· 70 70 return $yv; 71 71 } 72 72 73 + public function getDataRefs(array $xv) { 74 + // TODO: This is not entirely correct. The correct implementation would 75 + // map "x -> y" at each stage of composition and pull and aggregate all 76 + // the datapoint refs. In practice, we currently never compose functions 77 + // with a data function somewhere in the middle, so just grabbing the first 78 + // result is close enough. 79 + 80 + // In the future, we may: notably, "x -> shift(-1 month) -> ..." to 81 + // generate a month-over-month overlay is a sensible operation which will 82 + // source data from the middle of a function composition. 83 + 84 + foreach ($this->getFunctionArguments() as $function) { 85 + return $function->getDataRefs($xv); 86 + } 87 + 88 + return array(); 89 + } 90 + 73 91 }
+67 -1
src/applications/fact/chart/PhabricatorFactChartFunction.php
··· 7 7 8 8 private $fact; 9 9 private $map; 10 + private $refs; 10 11 11 12 protected function newArguments() { 12 13 $key_argument = $this->newArgument() ··· 51 52 52 53 $data = queryfx_all( 53 54 $conn, 54 - 'SELECT value, epoch FROM %T WHERE %LA ORDER BY epoch ASC', 55 + 'SELECT id, value, epoch FROM %T WHERE %LA ORDER BY epoch ASC', 55 56 $table_name, 56 57 $where); 57 58 58 59 $map = array(); 60 + $refs = array(); 59 61 if ($data) { 60 62 foreach ($data as $row) { 63 + $ref = (string)$row['id']; 61 64 $value = (int)$row['value']; 62 65 $epoch = (int)$row['epoch']; 63 66 ··· 66 69 } 67 70 68 71 $map[$epoch] += $value; 72 + 73 + if (!isset($refs[$epoch])) { 74 + $refs[$epoch] = array(); 75 + } 76 + 77 + $refs[$epoch][] = $ref; 69 78 } 70 79 } 71 80 72 81 $this->map = $map; 82 + $this->refs = $refs; 73 83 } 74 84 75 85 public function getDomain() { ··· 97 107 } 98 108 99 109 return $yv; 110 + } 111 + 112 + public function getDataRefs(array $xv) { 113 + return array_select_keys($this->refs, $xv); 114 + } 115 + 116 + public function loadRefs(array $refs) { 117 + $fact = $this->fact; 118 + 119 + $datapoint_table = $fact->newDatapoint(); 120 + $conn = $datapoint_table->establishConnection('r'); 121 + 122 + $dimension_table = new PhabricatorFactObjectDimension(); 123 + 124 + $where = array(); 125 + 126 + $where[] = qsprintf( 127 + $conn, 128 + 'p.id IN (%Ld)', 129 + $refs); 130 + 131 + 132 + $rows = queryfx_all( 133 + $conn, 134 + 'SELECT 135 + p.id id, 136 + p.value, 137 + od.objectPHID objectPHID, 138 + dd.objectPHID dimensionPHID 139 + FROM %R p 140 + LEFT JOIN %R od ON od.id = p.objectID 141 + LEFT JOIN %R dd ON dd.id = p.dimensionID 142 + WHERE %LA', 143 + $datapoint_table, 144 + $dimension_table, 145 + $dimension_table, 146 + $where); 147 + $rows = ipull($rows, null, 'id'); 148 + 149 + $results = array(); 150 + 151 + foreach ($refs as $ref) { 152 + if (!isset($rows[$ref])) { 153 + continue; 154 + } 155 + 156 + $row = $rows[$ref]; 157 + 158 + $results[$ref] = array( 159 + 'objectPHID' => $row['objectPHID'], 160 + 'dimensionPHID' => $row['dimensionPHID'], 161 + 'value' => (float)$row['value'], 162 + ); 163 + } 164 + 165 + return $results; 100 166 } 101 167 102 168 }
+34
src/applications/fact/chart/PhabricatorHigherOrderChartFunction.php
··· 32 32 return array_keys($map); 33 33 } 34 34 35 + public function getDataRefs(array $xv) { 36 + $refs = array(); 37 + 38 + foreach ($this->getFunctionArguments() as $function) { 39 + $function_refs = $function->getDataRefs($xv); 40 + 41 + $function_refs = array_select_keys($function_refs, $xv); 42 + if (!$function_refs) { 43 + continue; 44 + } 45 + 46 + foreach ($function_refs as $x => $ref_list) { 47 + if (!isset($refs[$x])) { 48 + $refs[$x] = array(); 49 + } 50 + foreach ($ref_list as $ref) { 51 + $refs[$x][] = $ref; 52 + } 53 + } 54 + } 55 + 56 + return $refs; 57 + } 58 + 59 + public function loadRefs(array $refs) { 60 + $results = array(); 61 + 62 + foreach ($this->getFunctionArguments() as $function) { 63 + $results += $function->loadRefs($refs); 64 + } 65 + 66 + return $results; 67 + } 68 + 35 69 }
+11 -1
src/applications/fact/chart/PhabricatorPureChartFunction.php
··· 1 1 <?php 2 2 3 3 abstract class PhabricatorPureChartFunction 4 - extends PhabricatorChartFunction {} 4 + extends PhabricatorChartFunction { 5 + 6 + public function getDataRefs(array $xv) { 7 + return array(); 8 + } 9 + 10 + public function loadRefs(array $refs) { 11 + return array(); 12 + } 13 + 14 + }
+9 -3
src/applications/fact/controller/PhabricatorFactChartController.php
··· 33 33 } 34 34 35 35 $chart_view = $engine->newChartView(); 36 - return $this->newChartResponse($chart_view); 36 + $tabular_view = $engine->newTabularView(); 37 + 38 + return $this->newChartResponse($chart_view, $tabular_view); 37 39 } 38 40 39 - private function newChartResponse($chart_view) { 41 + private function newChartResponse($chart_view, $tabular_view) { 40 42 $box = id(new PHUIObjectBoxView()) 41 43 ->setHeaderText(pht('Chart')) 42 44 ->appendChild($chart_view); ··· 50 52 return $this->newPage() 51 53 ->setTitle($title) 52 54 ->setCrumbs($crumbs) 53 - ->appendChild($box); 55 + ->appendChild( 56 + array( 57 + $box, 58 + $tabular_view, 59 + )); 54 60 } 55 61 56 62 private function newDemoChart() {
+141 -1
src/applications/fact/engine/PhabricatorChartRenderingEngine.php
··· 109 109 return $chart_view; 110 110 } 111 111 112 + public function newTabularView() { 113 + $viewer = $this->getViewer(); 114 + $tabular_data = $this->newTabularData(); 115 + 116 + $ref_keys = array(); 117 + foreach ($tabular_data['datasets'] as $tabular_dataset) { 118 + foreach ($tabular_dataset as $function) { 119 + foreach ($function['data'] as $point) { 120 + foreach ($point['refs'] as $ref) { 121 + $ref_keys[$ref] = $ref; 122 + } 123 + } 124 + } 125 + } 126 + 127 + $chart = $this->getStoredChart(); 128 + 129 + $ref_map = array(); 130 + foreach ($chart->getDatasets() as $dataset) { 131 + foreach ($dataset->getFunctions() as $function) { 132 + // If we aren't looking for anything else, bail out. 133 + if (!$ref_keys) { 134 + break 2; 135 + } 136 + 137 + $function_refs = $function->loadRefs($ref_keys); 138 + 139 + $ref_map += $function_refs; 140 + 141 + // Remove the ref keys that we found data for from the list of keys 142 + // we are looking for. If any function gives us data for a given ref, 143 + // that's satisfactory. 144 + foreach ($function_refs as $ref_key => $ref_data) { 145 + unset($ref_keys[$ref_key]); 146 + } 147 + } 148 + } 149 + 150 + $phids = array(); 151 + foreach ($ref_map as $ref => $ref_data) { 152 + if (isset($ref_data['objectPHID'])) { 153 + $phids[] = $ref_data['objectPHID']; 154 + } 155 + } 156 + 157 + $handles = $viewer->loadHandles($phids); 158 + 159 + $tabular_view = array(); 160 + foreach ($tabular_data['datasets'] as $tabular_data) { 161 + foreach ($tabular_data as $function) { 162 + $rows = array(); 163 + foreach ($function['data'] as $point) { 164 + $ref_views = array(); 165 + 166 + $xv = date('Y-m-d h:i:s', $point['x']); 167 + $yv = $point['y']; 168 + 169 + $point_refs = array(); 170 + foreach ($point['refs'] as $ref) { 171 + if (!isset($ref_map[$ref])) { 172 + continue; 173 + } 174 + $point_refs[$ref] = $ref_map[$ref]; 175 + } 176 + 177 + if (!$point_refs) { 178 + $rows[] = array( 179 + $xv, 180 + $yv, 181 + ); 182 + } else { 183 + foreach ($point_refs as $ref => $ref_data) { 184 + $ref_value = $ref_data['value']; 185 + $ref_link = $handles[$ref_data['objectPHID']] 186 + ->renderLink(); 187 + 188 + $view_uri = urisprintf( 189 + '/fact/object/%s/', 190 + $ref_data['objectPHID']); 191 + 192 + $ref_button = id(new PHUIButtonView()) 193 + ->setIcon('fa-table') 194 + ->setTag('a') 195 + ->setColor('grey') 196 + ->setHref($view_uri) 197 + ->setText(pht('View Data')); 198 + 199 + $rows[] = array( 200 + $xv, 201 + $yv, 202 + $ref_value, 203 + $ref_link, 204 + $ref_button, 205 + ); 206 + 207 + $xv = null; 208 + $yv = null; 209 + } 210 + } 211 + } 212 + 213 + $table = id(new AphrontTableView($rows)) 214 + ->setHeaders( 215 + array( 216 + pht('X'), 217 + pht('Y'), 218 + pht('Raw'), 219 + pht('Refs'), 220 + null, 221 + )) 222 + ->setColumnClasses( 223 + array( 224 + 'n', 225 + 'n', 226 + 'n', 227 + 'wide', 228 + null, 229 + )); 230 + 231 + $tabular_view[] = id(new PHUIObjectBoxView()) 232 + ->setHeaderText(pht('Function')) 233 + ->setTable($table); 234 + } 235 + } 236 + 237 + return $tabular_view; 238 + } 239 + 112 240 public function newChartData() { 241 + return $this->newWireData(false); 242 + } 243 + 244 + public function newTabularData() { 245 + return $this->newWireData(true); 246 + } 247 + 248 + private function newWireData($is_tabular) { 113 249 $chart = $this->getStoredChart(); 114 250 $chart_key = $chart->getChartKey(); 115 251 ··· 151 287 $wire_datasets = array(); 152 288 $ranges = array(); 153 289 foreach ($datasets as $dataset) { 154 - $display_data = $dataset->getChartDisplayData($data_query); 290 + if ($is_tabular) { 291 + $display_data = $dataset->getTabularDisplayData($data_query); 292 + } else { 293 + $display_data = $dataset->getChartDisplayData($data_query); 294 + } 155 295 156 296 $ranges[] = $display_data->getRange(); 157 297 $wire_datasets[] = $display_data->getWireData();