1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2024-12-12 00:26:13 +01:00
phorge-phorge/src/applications/fact/engine/PhabricatorChartRenderingEngine.php
epriestley d4ed5d0428 Make various UX improvements to charts so they're closer to making visual sense
Summary: Ref T13279. Fix some tabular stuff, draw areas better, make the "compose()" API more consistent, unfatal the demo chart, unfatal the project burndown, make the project chart do something roughly physical.

Test Plan: Looked at charts, saw fewer obvious horrors.

Subscribers: yelirekim

Maniphest Tasks: T13279

Differential Revision: https://secure.phabricator.com/D20817
2019-09-17 09:43:21 -07:00

356 lines
8.8 KiB
PHP

<?php
final class PhabricatorChartRenderingEngine
extends Phobject {
private $viewer;
private $chart;
private $storedChart;
public function setViewer(PhabricatorUser $viewer) {
$this->viewer = $viewer;
return $this;
}
public function getViewer() {
return $this->viewer;
}
public function setChart(PhabricatorFactChart $chart) {
$this->chart = $chart;
return $this;
}
public function getChart() {
return $this->chart;
}
public function loadChart($chart_key) {
$chart = id(new PhabricatorFactChart())->loadOneWhere(
'chartKey = %s',
$chart_key);
if ($chart) {
$this->setChart($chart);
}
return $chart;
}
public static function getChartURI($chart_key) {
return id(new PhabricatorFactChart())
->setChartKey($chart_key)
->getURI();
}
public function getStoredChart() {
if (!$this->storedChart) {
$chart = $this->getChart();
$chart_key = $chart->getChartKey();
if (!$chart_key) {
$chart_key = $chart->newChartKey();
$stored_chart = id(new PhabricatorFactChart())->loadOneWhere(
'chartKey = %s',
$chart_key);
if ($stored_chart) {
$chart = $stored_chart;
} else {
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
try {
$chart->save();
} catch (AphrontDuplicateKeyQueryException $ex) {
$chart = id(new PhabricatorFactChart())->loadOneWhere(
'chartKey = %s',
$chart_key);
if (!$chart) {
throw new Exception(
pht(
'Failed to load chart with key "%s" after key collision. '.
'This should not be possible.',
$chart_key));
}
}
unset($unguarded);
}
$this->setChart($chart);
}
$this->storedChart = $chart;
}
return $this->storedChart;
}
public function newChartView() {
$chart = $this->getStoredChart();
$chart_key = $chart->getChartKey();
$chart_node_id = celerity_generate_unique_node_id();
$chart_view = phutil_tag(
'div',
array(
'id' => $chart_node_id,
'class' => 'chart-hardpoint',
));
$data_uri = urisprintf('/fact/chart/%s/draw/', $chart_key);
Javelin::initBehavior(
'line-chart',
array(
'chartNodeID' => $chart_node_id,
'dataURI' => (string)$data_uri,
));
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,
null,
null,
null,
);
} 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();
$chart_engine = PhabricatorChartEngine::newFromChart($chart)
->setViewer($this->getViewer());
$chart_engine->buildChart($chart);
$datasets = $chart->getDatasets();
$functions = array();
foreach ($datasets as $dataset) {
foreach ($dataset->getFunctions() as $function) {
$functions[] = $function;
}
}
$subfunctions = array();
foreach ($functions as $function) {
foreach ($function->getSubfunctions() as $subfunction) {
$subfunctions[] = $subfunction;
}
}
foreach ($subfunctions as $subfunction) {
$subfunction->loadData();
}
$domain = $this->getDomain($functions);
$axis = id(new PhabricatorChartAxis())
->setMinimumValue($domain->getMin())
->setMaximumValue($domain->getMax());
$data_query = id(new PhabricatorChartDataQuery())
->setMinimumValue($domain->getMin())
->setMaximumValue($domain->getMax())
->setLimit(2000);
$wire_datasets = array();
$ranges = array();
foreach ($datasets as $dataset) {
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();
}
$range = $this->getRange($ranges);
$chart_data = array(
'datasets' => $wire_datasets,
'xMin' => $domain->getMin(),
'xMax' => $domain->getMax(),
'yMin' => $range->getMin(),
'yMax' => $range->getMax(),
);
return $chart_data;
}
private function getDomain(array $functions) {
$domains = array();
foreach ($functions as $function) {
$domains[] = $function->getDomain();
}
$domain = PhabricatorChartInterval::newFromIntervalList($domains);
// If we don't have any domain data from the actual functions, pick a
// plausible domain automatically.
if ($domain->getMax() === null) {
$domain->setMax(PhabricatorTime::getNow());
}
if ($domain->getMin() === null) {
$domain->setMin($domain->getMax() - phutil_units('365 days in seconds'));
}
return $domain;
}
private function getRange(array $ranges) {
$range = PhabricatorChartInterval::newFromIntervalList($ranges);
// Start the Y axis at 0 unless the chart has negative values.
$min = $range->getMin();
if ($min === null || $min >= 0) {
$range->setMin(0);
}
// If there's no maximum value, just pick a plausible default.
$max = $range->getMax();
if ($max === null) {
$range->setMax($range->getMin() + 100);
}
return $range;
}
}