mirror of
https://we.phorge.it/source/phorge.git
synced 2025-02-16 16:58:38 +01:00
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
This commit is contained in:
parent
769e745a3f
commit
080e132aa7
10 changed files with 315 additions and 16 deletions
|
@ -8301,7 +8301,7 @@ phutil_register_library_map(array(
|
|||
'PhabricatorAccessLog' => 'Phobject',
|
||||
'PhabricatorAccessLogConfigOptions' => 'PhabricatorApplicationConfigOptions',
|
||||
'PhabricatorAccessibilitySetting' => 'PhabricatorSelectSetting',
|
||||
'PhabricatorAccumulateChartFunction' => 'PhabricatorChartFunction',
|
||||
'PhabricatorAccumulateChartFunction' => 'PhabricatorHigherOrderChartFunction',
|
||||
'PhabricatorActionListView' => 'AphrontTagView',
|
||||
'PhabricatorActionView' => 'AphrontView',
|
||||
'PhabricatorActivitySettingsPanel' => 'PhabricatorSettingsPanel',
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<?php
|
||||
|
||||
final class PhabricatorAccumulateChartFunction
|
||||
extends PhabricatorChartFunction {
|
||||
extends PhabricatorHigherOrderChartFunction {
|
||||
|
||||
const FUNCTIONKEY = 'accumulate';
|
||||
|
||||
|
@ -13,14 +13,6 @@ final class PhabricatorAccumulateChartFunction
|
|||
);
|
||||
}
|
||||
|
||||
public function getDomain() {
|
||||
return $this->getArgument('x')->getDomain();
|
||||
}
|
||||
|
||||
public function newInputValues(PhabricatorChartDataQuery $query) {
|
||||
return $this->getArgument('x')->newInputValues($query);
|
||||
}
|
||||
|
||||
public function evaluateFunction(array $xv) {
|
||||
// First, we're going to accumulate the underlying function. Then
|
||||
// we'll map the inputs through the accumulation.
|
||||
|
|
|
@ -75,4 +75,35 @@ abstract class PhabricatorChartDataset
|
|||
PhabricatorChartDataQuery $data_query);
|
||||
|
||||
|
||||
final public function getTabularDisplayData(
|
||||
PhabricatorChartDataQuery $data_query) {
|
||||
$results = array();
|
||||
|
||||
$functions = $this->getFunctions();
|
||||
foreach ($functions as $function) {
|
||||
$datapoints = $function->newDatapoints($data_query);
|
||||
|
||||
$refs = $function->getDataRefs(ipull($datapoints, 'x'));
|
||||
|
||||
foreach ($datapoints as $key => $point) {
|
||||
$x = $point['x'];
|
||||
|
||||
if (isset($refs[$x])) {
|
||||
$xrefs = $refs[$x];
|
||||
} else {
|
||||
$xrefs = array();
|
||||
}
|
||||
|
||||
$datapoints[$key]['refs'] = $xrefs;
|
||||
}
|
||||
|
||||
$results[] = array(
|
||||
'data' => $datapoints,
|
||||
);
|
||||
}
|
||||
|
||||
return id(new PhabricatorChartDisplayData())
|
||||
->setWireData($results);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -180,6 +180,8 @@ abstract class PhabricatorChartFunction
|
|||
}
|
||||
|
||||
abstract public function evaluateFunction(array $xv);
|
||||
abstract public function getDataRefs(array $xv);
|
||||
abstract public function loadRefs(array $refs);
|
||||
|
||||
public function getDomain() {
|
||||
return null;
|
||||
|
|
|
@ -70,4 +70,22 @@ final class PhabricatorComposeChartFunction
|
|||
return $yv;
|
||||
}
|
||||
|
||||
public function getDataRefs(array $xv) {
|
||||
// TODO: This is not entirely correct. The correct implementation would
|
||||
// map "x -> y" at each stage of composition and pull and aggregate all
|
||||
// the datapoint refs. In practice, we currently never compose functions
|
||||
// with a data function somewhere in the middle, so just grabbing the first
|
||||
// result is close enough.
|
||||
|
||||
// In the future, we may: notably, "x -> shift(-1 month) -> ..." to
|
||||
// generate a month-over-month overlay is a sensible operation which will
|
||||
// source data from the middle of a function composition.
|
||||
|
||||
foreach ($this->getFunctionArguments() as $function) {
|
||||
return $function->getDataRefs($xv);
|
||||
}
|
||||
|
||||
return array();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ final class PhabricatorFactChartFunction
|
|||
|
||||
private $fact;
|
||||
private $map;
|
||||
private $refs;
|
||||
|
||||
protected function newArguments() {
|
||||
$key_argument = $this->newArgument()
|
||||
|
@ -51,13 +52,15 @@ final class PhabricatorFactChartFunction
|
|||
|
||||
$data = queryfx_all(
|
||||
$conn,
|
||||
'SELECT value, epoch FROM %T WHERE %LA ORDER BY epoch ASC',
|
||||
'SELECT id, value, epoch FROM %T WHERE %LA ORDER BY epoch ASC',
|
||||
$table_name,
|
||||
$where);
|
||||
|
||||
$map = array();
|
||||
$refs = array();
|
||||
if ($data) {
|
||||
foreach ($data as $row) {
|
||||
$ref = (string)$row['id'];
|
||||
$value = (int)$row['value'];
|
||||
$epoch = (int)$row['epoch'];
|
||||
|
||||
|
@ -66,10 +69,17 @@ final class PhabricatorFactChartFunction
|
|||
}
|
||||
|
||||
$map[$epoch] += $value;
|
||||
|
||||
if (!isset($refs[$epoch])) {
|
||||
$refs[$epoch] = array();
|
||||
}
|
||||
|
||||
$refs[$epoch][] = $ref;
|
||||
}
|
||||
}
|
||||
|
||||
$this->map = $map;
|
||||
$this->refs = $refs;
|
||||
}
|
||||
|
||||
public function getDomain() {
|
||||
|
@ -99,4 +109,60 @@ final class PhabricatorFactChartFunction
|
|||
return $yv;
|
||||
}
|
||||
|
||||
public function getDataRefs(array $xv) {
|
||||
return array_select_keys($this->refs, $xv);
|
||||
}
|
||||
|
||||
public function loadRefs(array $refs) {
|
||||
$fact = $this->fact;
|
||||
|
||||
$datapoint_table = $fact->newDatapoint();
|
||||
$conn = $datapoint_table->establishConnection('r');
|
||||
|
||||
$dimension_table = new PhabricatorFactObjectDimension();
|
||||
|
||||
$where = array();
|
||||
|
||||
$where[] = qsprintf(
|
||||
$conn,
|
||||
'p.id IN (%Ld)',
|
||||
$refs);
|
||||
|
||||
|
||||
$rows = queryfx_all(
|
||||
$conn,
|
||||
'SELECT
|
||||
p.id id,
|
||||
p.value,
|
||||
od.objectPHID objectPHID,
|
||||
dd.objectPHID dimensionPHID
|
||||
FROM %R p
|
||||
LEFT JOIN %R od ON od.id = p.objectID
|
||||
LEFT JOIN %R dd ON dd.id = p.dimensionID
|
||||
WHERE %LA',
|
||||
$datapoint_table,
|
||||
$dimension_table,
|
||||
$dimension_table,
|
||||
$where);
|
||||
$rows = ipull($rows, null, 'id');
|
||||
|
||||
$results = array();
|
||||
|
||||
foreach ($refs as $ref) {
|
||||
if (!isset($rows[$ref])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$row = $rows[$ref];
|
||||
|
||||
$results[$ref] = array(
|
||||
'objectPHID' => $row['objectPHID'],
|
||||
'dimensionPHID' => $row['dimensionPHID'],
|
||||
'value' => (float)$row['value'],
|
||||
);
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -32,4 +32,38 @@ abstract class PhabricatorHigherOrderChartFunction
|
|||
return array_keys($map);
|
||||
}
|
||||
|
||||
public function getDataRefs(array $xv) {
|
||||
$refs = array();
|
||||
|
||||
foreach ($this->getFunctionArguments() as $function) {
|
||||
$function_refs = $function->getDataRefs($xv);
|
||||
|
||||
$function_refs = array_select_keys($function_refs, $xv);
|
||||
if (!$function_refs) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($function_refs as $x => $ref_list) {
|
||||
if (!isset($refs[$x])) {
|
||||
$refs[$x] = array();
|
||||
}
|
||||
foreach ($ref_list as $ref) {
|
||||
$refs[$x][] = $ref;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $refs;
|
||||
}
|
||||
|
||||
public function loadRefs(array $refs) {
|
||||
$results = array();
|
||||
|
||||
foreach ($this->getFunctionArguments() as $function) {
|
||||
$results += $function->loadRefs($refs);
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,4 +1,14 @@
|
|||
<?php
|
||||
|
||||
abstract class PhabricatorPureChartFunction
|
||||
extends PhabricatorChartFunction {}
|
||||
extends PhabricatorChartFunction {
|
||||
|
||||
public function getDataRefs(array $xv) {
|
||||
return array();
|
||||
}
|
||||
|
||||
public function loadRefs(array $refs) {
|
||||
return array();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -33,10 +33,12 @@ final class PhabricatorFactChartController extends PhabricatorFactController {
|
|||
}
|
||||
|
||||
$chart_view = $engine->newChartView();
|
||||
return $this->newChartResponse($chart_view);
|
||||
$tabular_view = $engine->newTabularView();
|
||||
|
||||
return $this->newChartResponse($chart_view, $tabular_view);
|
||||
}
|
||||
|
||||
private function newChartResponse($chart_view) {
|
||||
private function newChartResponse($chart_view, $tabular_view) {
|
||||
$box = id(new PHUIObjectBoxView())
|
||||
->setHeaderText(pht('Chart'))
|
||||
->appendChild($chart_view);
|
||||
|
@ -50,7 +52,11 @@ final class PhabricatorFactChartController extends PhabricatorFactController {
|
|||
return $this->newPage()
|
||||
->setTitle($title)
|
||||
->setCrumbs($crumbs)
|
||||
->appendChild($box);
|
||||
->appendChild(
|
||||
array(
|
||||
$box,
|
||||
$tabular_view,
|
||||
));
|
||||
}
|
||||
|
||||
private function newDemoChart() {
|
||||
|
|
|
@ -109,7 +109,143 @@ final class PhabricatorChartRenderingEngine
|
|||
return $chart_view;
|
||||
}
|
||||
|
||||
public function newTabularView() {
|
||||
$viewer = $this->getViewer();
|
||||
$tabular_data = $this->newTabularData();
|
||||
|
||||
$ref_keys = array();
|
||||
foreach ($tabular_data['datasets'] as $tabular_dataset) {
|
||||
foreach ($tabular_dataset as $function) {
|
||||
foreach ($function['data'] as $point) {
|
||||
foreach ($point['refs'] as $ref) {
|
||||
$ref_keys[$ref] = $ref;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$chart = $this->getStoredChart();
|
||||
|
||||
$ref_map = array();
|
||||
foreach ($chart->getDatasets() as $dataset) {
|
||||
foreach ($dataset->getFunctions() as $function) {
|
||||
// If we aren't looking for anything else, bail out.
|
||||
if (!$ref_keys) {
|
||||
break 2;
|
||||
}
|
||||
|
||||
$function_refs = $function->loadRefs($ref_keys);
|
||||
|
||||
$ref_map += $function_refs;
|
||||
|
||||
// Remove the ref keys that we found data for from the list of keys
|
||||
// we are looking for. If any function gives us data for a given ref,
|
||||
// that's satisfactory.
|
||||
foreach ($function_refs as $ref_key => $ref_data) {
|
||||
unset($ref_keys[$ref_key]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$phids = array();
|
||||
foreach ($ref_map as $ref => $ref_data) {
|
||||
if (isset($ref_data['objectPHID'])) {
|
||||
$phids[] = $ref_data['objectPHID'];
|
||||
}
|
||||
}
|
||||
|
||||
$handles = $viewer->loadHandles($phids);
|
||||
|
||||
$tabular_view = array();
|
||||
foreach ($tabular_data['datasets'] as $tabular_data) {
|
||||
foreach ($tabular_data as $function) {
|
||||
$rows = array();
|
||||
foreach ($function['data'] as $point) {
|
||||
$ref_views = array();
|
||||
|
||||
$xv = date('Y-m-d h:i:s', $point['x']);
|
||||
$yv = $point['y'];
|
||||
|
||||
$point_refs = array();
|
||||
foreach ($point['refs'] as $ref) {
|
||||
if (!isset($ref_map[$ref])) {
|
||||
continue;
|
||||
}
|
||||
$point_refs[$ref] = $ref_map[$ref];
|
||||
}
|
||||
|
||||
if (!$point_refs) {
|
||||
$rows[] = array(
|
||||
$xv,
|
||||
$yv,
|
||||
);
|
||||
} else {
|
||||
foreach ($point_refs as $ref => $ref_data) {
|
||||
$ref_value = $ref_data['value'];
|
||||
$ref_link = $handles[$ref_data['objectPHID']]
|
||||
->renderLink();
|
||||
|
||||
$view_uri = urisprintf(
|
||||
'/fact/object/%s/',
|
||||
$ref_data['objectPHID']);
|
||||
|
||||
$ref_button = id(new PHUIButtonView())
|
||||
->setIcon('fa-table')
|
||||
->setTag('a')
|
||||
->setColor('grey')
|
||||
->setHref($view_uri)
|
||||
->setText(pht('View Data'));
|
||||
|
||||
$rows[] = array(
|
||||
$xv,
|
||||
$yv,
|
||||
$ref_value,
|
||||
$ref_link,
|
||||
$ref_button,
|
||||
);
|
||||
|
||||
$xv = null;
|
||||
$yv = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$table = id(new AphrontTableView($rows))
|
||||
->setHeaders(
|
||||
array(
|
||||
pht('X'),
|
||||
pht('Y'),
|
||||
pht('Raw'),
|
||||
pht('Refs'),
|
||||
null,
|
||||
))
|
||||
->setColumnClasses(
|
||||
array(
|
||||
'n',
|
||||
'n',
|
||||
'n',
|
||||
'wide',
|
||||
null,
|
||||
));
|
||||
|
||||
$tabular_view[] = id(new PHUIObjectBoxView())
|
||||
->setHeaderText(pht('Function'))
|
||||
->setTable($table);
|
||||
}
|
||||
}
|
||||
|
||||
return $tabular_view;
|
||||
}
|
||||
|
||||
public function newChartData() {
|
||||
return $this->newWireData(false);
|
||||
}
|
||||
|
||||
public function newTabularData() {
|
||||
return $this->newWireData(true);
|
||||
}
|
||||
|
||||
private function newWireData($is_tabular) {
|
||||
$chart = $this->getStoredChart();
|
||||
$chart_key = $chart->getChartKey();
|
||||
|
||||
|
@ -151,7 +287,11 @@ final class PhabricatorChartRenderingEngine
|
|||
$wire_datasets = array();
|
||||
$ranges = array();
|
||||
foreach ($datasets as $dataset) {
|
||||
$display_data = $dataset->getChartDisplayData($data_query);
|
||||
if ($is_tabular) {
|
||||
$display_data = $dataset->getTabularDisplayData($data_query);
|
||||
} else {
|
||||
$display_data = $dataset->getChartDisplayData($data_query);
|
||||
}
|
||||
|
||||
$ranges[] = $display_data->getRange();
|
||||
$wire_datasets[] = $display_data->getWireData();
|
||||
|
|
Loading…
Add table
Reference in a new issue