Page MenuHomePhabricator

D20483.id48865.diff
No OneTemporary

D20483.id48865.diff

diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php
--- a/src/__phutil_library_map__.php
+++ b/src/__phutil_library_map__.php
@@ -2653,6 +2653,8 @@
'PhabricatorChangesetResponse' => 'infrastructure/diff/PhabricatorChangesetResponse.php',
'PhabricatorChartAxis' => 'applications/fact/chart/PhabricatorChartAxis.php',
'PhabricatorChartDataQuery' => 'applications/fact/chart/PhabricatorChartDataQuery.php',
+ 'PhabricatorChartDataset' => 'applications/fact/chart/PhabricatorChartDataset.php',
+ 'PhabricatorChartEngine' => 'applications/fact/engine/PhabricatorChartEngine.php',
'PhabricatorChartFunction' => 'applications/fact/chart/PhabricatorChartFunction.php',
'PhabricatorChartFunctionArgument' => 'applications/fact/chart/PhabricatorChartFunctionArgument.php',
'PhabricatorChartFunctionArgumentParser' => 'applications/fact/chart/PhabricatorChartFunctionArgumentParser.php',
@@ -8640,6 +8642,8 @@
'PhabricatorChangesetResponse' => 'AphrontProxyResponse',
'PhabricatorChartAxis' => 'Phobject',
'PhabricatorChartDataQuery' => 'Phobject',
+ 'PhabricatorChartDataset' => 'Phobject',
+ 'PhabricatorChartEngine' => 'Phobject',
'PhabricatorChartFunction' => 'Phobject',
'PhabricatorChartFunctionArgument' => 'Phobject',
'PhabricatorChartFunctionArgumentParser' => 'Phobject',
diff --git a/src/applications/fact/application/PhabricatorFactApplication.php b/src/applications/fact/application/PhabricatorFactApplication.php
--- a/src/applications/fact/application/PhabricatorFactApplication.php
+++ b/src/applications/fact/application/PhabricatorFactApplication.php
@@ -30,7 +30,9 @@
return array(
'/fact/' => array(
'' => 'PhabricatorFactHomeController',
- '(?<mode>chart|draw)/' => 'PhabricatorFactChartController',
+ 'chart/' => 'PhabricatorFactChartController',
+ 'chart/(?P<chartKey>[^/]+)/(?:(?P<mode>draw)/)?' =>
+ 'PhabricatorFactChartController',
'object/(?<phid>[^/]+)/' => 'PhabricatorFactObjectController',
),
);
diff --git a/src/applications/fact/chart/PhabricatorChartDataset.php b/src/applications/fact/chart/PhabricatorChartDataset.php
new file mode 100644
--- /dev/null
+++ b/src/applications/fact/chart/PhabricatorChartDataset.php
@@ -0,0 +1,37 @@
+<?php
+
+final class PhabricatorChartDataset
+ extends Phobject {
+
+ private $function;
+
+ public function getFunction() {
+ return $this->function;
+ }
+
+ public static function newFromDictionary(array $map) {
+ PhutilTypeSpec::checkMap(
+ $map,
+ array(
+ 'function' => 'list<wild>',
+ ));
+
+ $dataset = new self();
+
+ $dataset->function = id(new PhabricatorComposeChartFunction())
+ ->setArguments(array($map['function']));
+
+ return $dataset;
+ }
+
+ public function toDictionary() {
+ // Since we wrap the raw value in a "compose(...)", when deserializing,
+ // we need to unwrap it when serializing.
+ $function_raw = head($this->getFunction()->toDictionary());
+
+ return array(
+ 'function' => $function_raw,
+ );
+ }
+
+}
diff --git a/src/applications/fact/chart/PhabricatorChartFunction.php b/src/applications/fact/chart/PhabricatorChartFunction.php
--- a/src/applications/fact/chart/PhabricatorChartFunction.php
+++ b/src/applications/fact/chart/PhabricatorChartFunction.php
@@ -43,6 +43,10 @@
return $this;
}
+ public function toDictionary() {
+ return $this->getArgumentParser()->getRawArguments();
+ }
+
public function getSubfunctions() {
$result = array();
$result[] = $this;
diff --git a/src/applications/fact/chart/PhabricatorChartFunctionArgumentParser.php b/src/applications/fact/chart/PhabricatorChartFunctionArgumentParser.php
--- a/src/applications/fact/chart/PhabricatorChartFunctionArgumentParser.php
+++ b/src/applications/fact/chart/PhabricatorChartFunctionArgumentParser.php
@@ -103,6 +103,10 @@
return array_values($this->argumentMap);
}
+ public function getRawArguments() {
+ return $this->rawArguments;
+ }
+
public function parseArguments() {
$have_count = count($this->rawArguments);
$want_count = count($this->argumentMap);
diff --git a/src/applications/fact/controller/PhabricatorFactChartController.php b/src/applications/fact/controller/PhabricatorFactChartController.php
--- a/src/applications/fact/controller/PhabricatorFactChartController.php
+++ b/src/applications/fact/controller/PhabricatorFactChartController.php
@@ -5,13 +5,60 @@
public function handleRequest(AphrontRequest $request) {
$viewer = $request->getViewer();
+ $chart_key = $request->getURIData('chartKey');
+ if ($chart_key === null) {
+ return $this->newDemoChart();
+ }
+
+ $chart = id(new PhabricatorFactChart())->loadOneWhere(
+ 'chartKey = %s',
+ $chart_key);
+ if (!$chart) {
+ return new Aphront404Response();
+ }
+
+ $engine = id(new PhabricatorChartEngine())
+ ->setViewer($viewer)
+ ->setChart($chart);
+
// When drawing a chart, we send down a placeholder piece of HTML first,
// then fetch the data via async request. Determine if we're drawing
// the structure or actually pulling the data.
$mode = $request->getURIData('mode');
- $is_chart_mode = ($mode === 'chart');
$is_draw_mode = ($mode === 'draw');
+ // TODO: For now, always pull the data. We'll throw it away if we're just
+ // drawing the frame, but this makes errors easier to debug.
+ $chart_data = $engine->newChartData();
+
+ if ($is_draw_mode) {
+ return id(new AphrontAjaxResponse())->setContent($chart_data);
+ }
+
+ $chart_view = $engine->newChartView();
+ return $this->newChartResponse($chart_view);
+ }
+
+ private function newChartResponse($chart_view) {
+ $box = id(new PHUIObjectBoxView())
+ ->setHeaderText(pht('Chart'))
+ ->appendChild($chart_view);
+
+ $crumbs = $this->buildApplicationCrumbs()
+ ->addTextCrumb(pht('Chart'))
+ ->setBorder(true);
+
+ $title = pht('Chart');
+
+ return $this->newPage()
+ ->setTitle($title)
+ ->setCrumbs($crumbs)
+ ->appendChild($box);
+ }
+
+ private function newDemoChart() {
+ $viewer = $this->getViewer();
+
$argvs = array();
$argvs[] = array('fact', 'tasks.count.create');
@@ -40,165 +87,24 @@
array('shift', 800),
);
- $functions = array();
- foreach ($argvs as $argv) {
- $functions[] = id(new PhabricatorComposeChartFunction())
- ->setArguments(array($argv));
- }
-
- $subfunctions = array();
- foreach ($functions as $function) {
- foreach ($function->getSubfunctions() as $subfunction) {
- $subfunctions[] = $subfunction;
- }
- }
-
- foreach ($subfunctions as $subfunction) {
- $subfunction->loadData();
- }
-
- list($domain_min, $domain_max) = $this->getDomain($functions);
-
- $axis = id(new PhabricatorChartAxis())
- ->setMinimumValue($domain_min)
- ->setMaximumValue($domain_max);
-
- $data_query = id(new PhabricatorChartDataQuery())
- ->setMinimumValue($domain_min)
- ->setMaximumValue($domain_max)
- ->setLimit(2000);
-
$datasets = array();
- foreach ($functions as $function) {
- $points = $function->newDatapoints($data_query);
-
- $x = array();
- $y = array();
-
- foreach ($points as $point) {
- $x[] = $point['x'];
- $y[] = $point['y'];
- }
-
- $datasets[] = array(
- 'x' => $x,
- 'y' => $y,
- 'color' => '#ff00ff',
- );
- }
-
-
- $y_min = 0;
- $y_max = 0;
- foreach ($datasets as $dataset) {
- if (!$dataset['y']) {
- continue;
- }
-
- $y_min = min($y_min, min($dataset['y']));
- $y_max = max($y_max, max($dataset['y']));
- }
-
- $chart_data = array(
- 'datasets' => $datasets,
- 'xMin' => $domain_min,
- 'xMax' => $domain_max,
- 'yMin' => $y_min,
- 'yMax' => $y_max,
- );
-
- // TODO: Move this back up, it's just down here for now to make
- // debugging easier so the main page throws a more visible exception when
- // something goes wrong.
- if ($is_chart_mode) {
- return $this->newChartResponse();
- }
-
- return id(new AphrontAjaxResponse())->setContent($chart_data);
- }
-
- private function newChartResponse() {
- $request = $this->getRequest();
- $chart_node_id = celerity_generate_unique_node_id();
-
- $chart_view = phutil_tag(
- 'div',
- array(
- 'id' => $chart_node_id,
- 'style' => 'background: #ffffff; '.
- 'height: 480px; ',
- ),
- '');
-
- $data_uri = $request->getRequestURI();
- $data_uri->setPath('/fact/draw/');
-
- Javelin::initBehavior(
- 'line-chart',
- array(
- 'chartNodeID' => $chart_node_id,
- 'dataURI' => (string)$data_uri,
- ));
-
- $box = id(new PHUIObjectBoxView())
- ->setHeaderText(pht('Chart'))
- ->appendChild($chart_view);
-
- $crumbs = $this->buildApplicationCrumbs()
- ->addTextCrumb(pht('Chart'))
- ->setBorder(true);
-
- $title = pht('Chart');
-
- return $this->newPage()
- ->setTitle($title)
- ->setCrumbs($crumbs)
- ->appendChild($box);
-
- }
-
- private function getDomain(array $functions) {
- $domain_min_list = null;
- $domain_max_list = null;
-
- foreach ($functions as $function) {
- $domain = $function->getDomain();
-
- list($function_min, $function_max) = $domain;
-
- if ($function_min !== null) {
- $domain_min_list[] = $function_min;
- }
-
- if ($function_max !== null) {
- $domain_max_list[] = $function_max;
- }
- }
-
- $domain_min = null;
- $domain_max = null;
-
- if ($domain_min_list) {
- $domain_min = min($domain_min_list);
- }
-
- if ($domain_max_list) {
- $domain_max = max($domain_max_list);
+ foreach ($argvs as $argv) {
+ $datasets[] = PhabricatorChartDataset::newFromDictionary(
+ array(
+ 'function' => $argv,
+ ));
}
- // If we don't have any domain data from the actual functions, pick a
- // plausible domain automatically.
+ $chart = id(new PhabricatorFactChart())
+ ->setDatasets($datasets);
- if ($domain_max === null) {
- $domain_max = PhabricatorTime::getNow();
- }
+ $engine = id(new PhabricatorChartEngine())
+ ->setViewer($viewer)
+ ->setChart($chart);
- if ($domain_min === null) {
- $domain_min = $domain_max - phutil_units('365 days in seconds');
- }
+ $chart = $engine->getStoredChart();
- return array($domain_min, $domain_max);
+ return id(new AphrontRedirectResponse())->setURI($chart->getURI());
}
-
}
diff --git a/src/applications/fact/controller/PhabricatorFactChartController.php b/src/applications/fact/engine/PhabricatorChartEngine.php
copy from src/applications/fact/controller/PhabricatorFactChartController.php
copy to src/applications/fact/engine/PhabricatorChartEngine.php
--- a/src/applications/fact/controller/PhabricatorFactChartController.php
+++ b/src/applications/fact/engine/PhabricatorChartEngine.php
@@ -1,49 +1,107 @@
<?php
-final class PhabricatorFactChartController extends PhabricatorFactController {
+final class PhabricatorChartEngine
+ extends Phobject {
- public function handleRequest(AphrontRequest $request) {
- $viewer = $request->getViewer();
+ private $viewer;
+ private $chart;
+ private $storedChart;
- // When drawing a chart, we send down a placeholder piece of HTML first,
- // then fetch the data via async request. Determine if we're drawing
- // the structure or actually pulling the data.
- $mode = $request->getURIData('mode');
- $is_chart_mode = ($mode === 'chart');
- $is_draw_mode = ($mode === 'draw');
+ public function setViewer(PhabricatorUser $viewer) {
+ $this->viewer = $viewer;
+ return $this;
+ }
+
+ public function getViewer() {
+ return $this->viewer;
+ }
- $argvs = array();
+ public function setChart(PhabricatorFactChart $chart) {
+ $this->chart = $chart;
+ return $this;
+ }
- $argvs[] = array('fact', 'tasks.count.create');
+ public function getChart() {
+ return $this->chart;
+ }
- $argvs[] = array('constant', 360);
+ 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);
+ }
- $argvs[] = array('fact', 'tasks.open-count.create');
+ $this->storedChart = $chart;
+ }
- $argvs[] = array(
- 'sum',
+ 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(
- 'accumulate',
- array('fact', 'tasks.count.create'),
+ 'id' => $chart_node_id,
+ 'style' => 'background: #ffffff; '.
+ 'height: 480px; ',
),
+ '');
+
+ $data_uri = urisprintf('/fact/chart/%s/draw/', $chart_key);
+
+ Javelin::initBehavior(
+ 'line-chart',
array(
- 'accumulate',
- array('fact', 'tasks.open-count.create'),
- ),
- );
+ 'chartNodeID' => $chart_node_id,
+ 'dataURI' => (string)$data_uri,
+ ));
- $argvs[] = array(
- 'compose',
- array('scale', 0.001),
- array('cos'),
- array('scale', 100),
- array('shift', 800),
- );
+ return $chart_view;
+ }
+
+ public function newChartData() {
+ $chart = $this->getStoredChart();
+ $chart_key = $chart->getChartKey();
+
+ $datasets = $chart->getDatasets();
$functions = array();
- foreach ($argvs as $argv) {
- $functions[] = id(new PhabricatorComposeChartFunction())
- ->setArguments(array($argv));
+ foreach ($datasets as $dataset) {
+ $functions[] = $dataset->getFunction();
}
$subfunctions = array();
@@ -107,54 +165,7 @@
'yMax' => $y_max,
);
- // TODO: Move this back up, it's just down here for now to make
- // debugging easier so the main page throws a more visible exception when
- // something goes wrong.
- if ($is_chart_mode) {
- return $this->newChartResponse();
- }
-
- return id(new AphrontAjaxResponse())->setContent($chart_data);
- }
-
- private function newChartResponse() {
- $request = $this->getRequest();
- $chart_node_id = celerity_generate_unique_node_id();
-
- $chart_view = phutil_tag(
- 'div',
- array(
- 'id' => $chart_node_id,
- 'style' => 'background: #ffffff; '.
- 'height: 480px; ',
- ),
- '');
-
- $data_uri = $request->getRequestURI();
- $data_uri->setPath('/fact/draw/');
-
- Javelin::initBehavior(
- 'line-chart',
- array(
- 'chartNodeID' => $chart_node_id,
- 'dataURI' => (string)$data_uri,
- ));
-
- $box = id(new PHUIObjectBoxView())
- ->setHeaderText(pht('Chart'))
- ->appendChild($chart_view);
-
- $crumbs = $this->buildApplicationCrumbs()
- ->addTextCrumb(pht('Chart'))
- ->setBorder(true);
-
- $title = pht('Chart');
-
- return $this->newPage()
- ->setTitle($title)
- ->setCrumbs($crumbs)
- ->appendChild($box);
-
+ return $chart_data;
}
private function getDomain(array $functions) {
@@ -200,5 +211,4 @@
return array($domain_min, $domain_max);
}
-
}
diff --git a/src/applications/fact/storage/PhabricatorFactChart.php b/src/applications/fact/storage/PhabricatorFactChart.php
--- a/src/applications/fact/storage/PhabricatorFactChart.php
+++ b/src/applications/fact/storage/PhabricatorFactChart.php
@@ -7,6 +7,8 @@
protected $chartKey;
protected $chartParameters = array();
+ private $datasets;
+
protected function getConfiguration() {
return array(
self::CONFIG_SERIALIZATION => array(
@@ -33,6 +35,12 @@
return idx($this->chartParameters, $key, $default);
}
+ public function newChartKey() {
+ $digest = serialize($this->chartParameters);
+ $digest = PhabricatorHash::digestForIndex($digest);
+ return $digest;
+ }
+
public function save() {
if ($this->getID()) {
throw new Exception(
@@ -41,14 +49,46 @@
'overwrite an existing chart configuration.'));
}
- $digest = serialize($this->chartParameters);
- $digest = PhabricatorHash::digestForIndex($digest);
-
- $this->chartKey = $digest;
+ $this->chartKey = $this->newChartKey();
return parent::save();
}
+ public function setDatasets(array $datasets) {
+ assert_instances_of($datasets, 'PhabricatorChartDataset');
+
+ $dataset_list = array();
+ foreach ($datasets as $dataset) {
+ $dataset_list[] = $dataset->toDictionary();
+ }
+
+ $this->setChartParameter('datasets', $dataset_list);
+ $this->datasets = null;
+
+ return $this;
+ }
+
+ public function getDatasets() {
+ if ($this->datasets === null) {
+ $this->datasets = $this->newDatasets();
+ }
+ return $this->datasets;
+ }
+
+ private function newDatasets() {
+ $datasets = $this->getChartParameter('datasets', array());
+
+ foreach ($datasets as $key => $dataset) {
+ $datasets[$key] = PhabricatorChartDataset::newFromDictionary($dataset);
+ }
+
+ return $datasets;
+ }
+
+ public function getURI() {
+ return urisprintf('/fact/chart/%s/', $this->getChartKey());
+ }
+
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getCapabilities() {

File Metadata

Mime Type
text/plain
Expires
Sat, Mar 22, 11:45 AM (3 d, 11 h ago)
Storage Engine
blob
Storage Format
Encrypted (AES-256-CBC)
Storage Handle
7713081
Default Alt Text
D20483.id48865.diff (18 KB)

Event Timeline