1
0
Fork 0
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:
epriestley 2019-09-16 11:19:24 -07:00
parent 769e745a3f
commit 080e132aa7
10 changed files with 315 additions and 16 deletions

View file

@ -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',

View file

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

View file

@ -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);
}
}

View file

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

View file

@ -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();
}
}

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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();
}
}

View file

@ -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() {

View file

@ -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();