1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2024-11-22 14:52:41 +01:00

Simplify implementation of "pure" Chart functions

Summary:
Depends on D20445. Ref T13279. I'm not sure what the class tree of functions actually looks like, and I suspect it isn't really a tree, so I'm hesitant to start subclassing. Instead, try adding some `isSomethingSomething()` methods.

We have some different types of functions:

  # Some functions can be evaluated anywhere, like "constant(3)", which always evaluates to 3.
  # Some functions can't be evaluated anywhere, but have values everywhere in some domain. This is most interesting functions, like "number of open tasks". These functions also usually have a distinct set of interesting points, and are constant between those points (any count of anything, like "open points in project" or "tasks closed by alice", etc).
  # Some functions can be evaluated almost nowhere and have only discrete values. This is most of the data we actually store, which is just "+1" when a task is opened and "-1" when a task is closed.

Soon, I'd like to be able to show ("all tasks" - "open tasks") and draw a chart of closed tasks. This is somewhat tricky because the two datasets are of the second class of function (straight lines connecting dots) but their "interesting" x values won't be the same (users don't open and close tasks every second, or at the same time).

The "subtract X Y" function will need to be able to know that `subtract "all tasks" 3` and `subtract "all tasks" "closed tasks"` evaluate slightly differently.

To make this worse, the data we actually //store// is of the third class of function (just the "derivative" of the line chart), then we accumulate it in the application after we pull it out of the database. So the code will need to know that `subtract "derivative of all tasks" "derivative of closed tasks"` is meaningless, or the UI needs to make that clear, or it needs to interpret it to mean "accumulate the derivative into a line first".

Anyway, I'll sort that out in future changes. For now, simplify the easy case of functions in class (1), where they're just actual functions.

Add "shift(function, number)" and "scale(function, number)". These are probably like "mul" and "add" but they can't take two functions -- the second value must always be a constant. Maybe these will go away in the future and become `add(function, constant(3))` or something?

Test Plan: {F6382885}

Reviewers: amckinley

Reviewed By: amckinley

Subscribers: yelirekim

Maniphest Tasks: T13279

Differential Revision: https://secure.phabricator.com/D20446
This commit is contained in:
epriestley 2019-04-17 14:40:36 -07:00
parent edaf17f3fe
commit 70c643c685
11 changed files with 251 additions and 53 deletions

View file

@ -2817,6 +2817,7 @@ phutil_register_library_map(array(
'PhabricatorCoreCreateTransaction' => 'applications/transactions/xaction/PhabricatorCoreCreateTransaction.php',
'PhabricatorCoreTransactionType' => 'applications/transactions/xaction/PhabricatorCoreTransactionType.php',
'PhabricatorCoreVoidTransaction' => 'applications/transactions/xaction/PhabricatorCoreVoidTransaction.php',
'PhabricatorCosChartFunction' => 'applications/fact/chart/PhabricatorCosChartFunction.php',
'PhabricatorCountFact' => 'applications/fact/fact/PhabricatorCountFact.php',
'PhabricatorCountdown' => 'applications/countdown/storage/PhabricatorCountdown.php',
'PhabricatorCountdownApplication' => 'applications/countdown/application/PhabricatorCountdownApplication.php',
@ -4475,6 +4476,7 @@ phutil_register_library_map(array(
'PhabricatorSSHWorkflow' => 'infrastructure/ssh/PhabricatorSSHWorkflow.php',
'PhabricatorSavedQuery' => 'applications/search/storage/PhabricatorSavedQuery.php',
'PhabricatorSavedQueryQuery' => 'applications/search/query/PhabricatorSavedQueryQuery.php',
'PhabricatorScaleChartFunction' => 'applications/fact/chart/PhabricatorScaleChartFunction.php',
'PhabricatorScheduleTaskTriggerAction' => 'infrastructure/daemon/workers/action/PhabricatorScheduleTaskTriggerAction.php',
'PhabricatorScopedEnv' => 'infrastructure/env/PhabricatorScopedEnv.php',
'PhabricatorSearchAbstractDocument' => 'applications/search/index/PhabricatorSearchAbstractDocument.php',
@ -4564,6 +4566,7 @@ phutil_register_library_map(array(
'PhabricatorSetupIssue' => 'applications/config/issue/PhabricatorSetupIssue.php',
'PhabricatorSetupIssueUIExample' => 'applications/uiexample/examples/PhabricatorSetupIssueUIExample.php',
'PhabricatorSetupIssueView' => 'applications/config/view/PhabricatorSetupIssueView.php',
'PhabricatorShiftChartFunction' => 'applications/fact/chart/PhabricatorShiftChartFunction.php',
'PhabricatorShortSite' => 'aphront/site/PhabricatorShortSite.php',
'PhabricatorShowFiletreeSetting' => 'applications/settings/setting/PhabricatorShowFiletreeSetting.php',
'PhabricatorSimpleEditType' => 'applications/transactions/edittype/PhabricatorSimpleEditType.php',
@ -8811,6 +8814,7 @@ phutil_register_library_map(array(
'PhabricatorCoreCreateTransaction' => 'PhabricatorCoreTransactionType',
'PhabricatorCoreTransactionType' => 'PhabricatorModularTransactionType',
'PhabricatorCoreVoidTransaction' => 'PhabricatorModularTransactionType',
'PhabricatorCosChartFunction' => 'PhabricatorChartFunction',
'PhabricatorCountFact' => 'PhabricatorFact',
'PhabricatorCountdown' => array(
'PhabricatorCountdownDAO',
@ -10775,6 +10779,7 @@ phutil_register_library_map(array(
'PhabricatorPolicyInterface',
),
'PhabricatorSavedQueryQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorScaleChartFunction' => 'PhabricatorChartFunction',
'PhabricatorScheduleTaskTriggerAction' => 'PhabricatorTriggerAction',
'PhabricatorScopedEnv' => 'Phobject',
'PhabricatorSearchAbstractDocument' => 'Phobject',
@ -10864,6 +10869,7 @@ phutil_register_library_map(array(
'PhabricatorSetupIssue' => 'Phobject',
'PhabricatorSetupIssueUIExample' => 'PhabricatorUIExample',
'PhabricatorSetupIssueView' => 'AphrontView',
'PhabricatorShiftChartFunction' => 'PhabricatorChartFunction',
'PhabricatorShortSite' => 'PhabricatorSite',
'PhabricatorShowFiletreeSetting' => 'PhabricatorSelectSetting',
'PhabricatorSimpleEditType' => 'PhabricatorEditType',

View file

@ -5,9 +5,9 @@ abstract class PhabricatorChartFunction
private $xAxis;
private $yAxis;
private $limit;
private $argumentParser;
private $sourceFunction;
final public function getFunctionKey() {
return $this->getPhobjectClassConstant('FUNCTIONKEY', 32);
@ -44,6 +44,12 @@ abstract class PhabricatorChartFunction
$parser->setHaveAllArguments(true);
$parser->parseArguments();
$source_argument = $parser->getSourceFunctionArgument();
if ($source_argument) {
$source_function = $this->getArgument($source_argument->getName());
$this->setSourceFunction($source_function);
}
return $this;
}
@ -71,6 +77,15 @@ abstract class PhabricatorChartFunction
return;
}
protected function setSourceFunction(PhabricatorChartFunction $source) {
$this->sourceFunction = $source;
return $this;
}
protected function getSourceFunction() {
return $this->sourceFunction;
}
final public function setXAxis(PhabricatorChartAxis $x_axis) {
$this->xAxis = $x_axis;
return $this;
@ -89,6 +104,67 @@ abstract class PhabricatorChartFunction
return $this->yAxis;
}
protected function canEvaluateFunction() {
return false;
}
protected function evaluateFunction($x) {
throw new PhutilMethodNotImplementedException();
}
public function hasDomain() {
if ($this->canEvaluateFunction()) {
return false;
}
throw new PhutilMethodNotImplementedException();
}
public function getDatapoints(PhabricatorChartDataQuery $query) {
if ($this->canEvaluateFunction()) {
$points = $this->newSourceDatapoints($query);
foreach ($points as $key => $point) {
$y = $point['y'];
$y = $this->evaluateFunction($y);
$points[$key]['y'] = $y;
}
return $points;
}
return $this->newDatapoints($query);
}
protected function newDatapoints(PhabricatorChartDataQuery $query) {
throw new PhutilMethodNotImplementedException();
}
protected function newSourceDatapoints(PhabricatorChartDataQuery $query) {
$source = $this->getSourceFunction();
if ($source) {
return $source->getDatapoints($query);
}
return $this->newDefaultDatapoints($query);
}
protected function newDefaultDatapoints(PhabricatorChartDataQuery $query) {
$x_min = $query->getMinimumValue();
$x_max = $query->getMaximumValue();
$limit = $query->getLimit();
$points = array();
$steps = $this->newLinearSteps($x_min, $x_max, $limit);
foreach ($steps as $step) {
$points[] = array(
'x' => $step,
'y' => $step,
);
}
return $points;
}
protected function newLinearSteps($src, $dst, $count) {
$count = (int)$count;
$src = (int)$src;

View file

@ -5,6 +5,7 @@ final class PhabricatorChartFunctionArgument
private $name;
private $type;
private $isSourceFunction;
public function setName($name) {
$this->name = $name;
@ -39,6 +40,15 @@ final class PhabricatorChartFunctionArgument
return $this->type;
}
public function setIsSourceFunction($is_source_function) {
$this->isSourceFunction = $is_source_function;
return $this;
}
public function getIsSourceFunction() {
return $this->isSourceFunction;
}
public function newValue($value) {
switch ($this->getType()) {
case 'fact-key':

View file

@ -151,4 +151,43 @@ final class PhabricatorChartFunctionArgumentParser
implode(', ', $argument_list));
}
public function getSourceFunctionArgument() {
$required_type = 'function';
$sources = array();
foreach ($this->argumentMap as $key => $argument) {
if (!$argument->getIsSourceFunction()) {
continue;
}
if ($argument->getType() !== $required_type) {
throw new Exception(
pht(
'Function "%s" defines an argument "%s" which is marked as a '.
'source function, but the type of this argument is not "%s".',
$this->getFunctionArgumentSignature(),
$argument->getName(),
$required_type));
}
$sources[$key] = $argument;
}
if (!$sources) {
return null;
}
if (count($sources) > 1) {
throw new Exception(
pht(
'Function "%s" defines more than one argument as a source '.
'function (arguments: %s). Functions must have zero or one '.
'source function.',
$this->getFunctionArgumentSignature(),
implode(', ', array_keys($sources))));
}
return head($sources);
}
}

View file

@ -5,8 +5,6 @@ final class PhabricatorConstantChartFunction
const FUNCTIONKEY = 'constant';
private $value;
protected function newArguments() {
return array(
$this->newArgument()
@ -15,26 +13,12 @@ final class PhabricatorConstantChartFunction
);
}
public function getDatapoints(PhabricatorChartDataQuery $query) {
$x_min = $query->getMinimumValue();
$x_max = $query->getMaximumValue();
$value = $this->getArgument('n');
$points = array();
$steps = $this->newLinearSteps($x_min, $x_max, 2);
foreach ($steps as $step) {
$points[] = array(
'x' => $step,
'y' => $value,
);
}
return $points;
protected function canEvaluateFunction() {
return true;
}
public function hasDomain() {
return false;
protected function evaluateFunction($x) {
return $this->getArgument('n');
}
}

View file

@ -0,0 +1,25 @@
<?php
final class PhabricatorCosChartFunction
extends PhabricatorChartFunction {
const FUNCTIONKEY = 'cos';
protected function newArguments() {
return array(
$this->newArgument()
->setName('x')
->setType('function')
->setIsSourceFunction(true),
);
}
protected function canEvaluateFunction() {
return true;
}
protected function evaluateFunction($x) {
return cos(deg2rad($x));
}
}

View file

@ -0,0 +1,28 @@
<?php
final class PhabricatorScaleChartFunction
extends PhabricatorChartFunction {
const FUNCTIONKEY = 'scale';
protected function newArguments() {
return array(
$this->newArgument()
->setName('x')
->setType('function')
->setIsSourceFunction(true),
$this->newArgument()
->setName('scale')
->setType('number'),
);
}
protected function canEvaluateFunction() {
return true;
}
protected function evaluateFunction($x) {
return $x * $this->getArgument('scale');
}
}

View file

@ -0,0 +1,28 @@
<?php
final class PhabricatorShiftChartFunction
extends PhabricatorChartFunction {
const FUNCTIONKEY = 'shift';
protected function newArguments() {
return array(
$this->newArgument()
->setName('x')
->setType('function')
->setIsSourceFunction(true),
$this->newArgument()
->setName('shift')
->setType('number'),
);
}
protected function canEvaluateFunction() {
return true;
}
protected function evaluateFunction($x) {
return $x * $this->getArgument('shift');
}
}

View file

@ -9,26 +9,17 @@ final class PhabricatorSinChartFunction
return array(
$this->newArgument()
->setName('x')
->setType('function'),
->setType('function')
->setIsSourceFunction(true),
);
}
protected function assignArguments(array $arguments) {
$this->argument = $arguments[0];
protected function canEvaluateFunction() {
return true;
}
public function getDatapoints(PhabricatorChartDataQuery $query) {
$points = $this->getArgument('x')->getDatapoints($query);
foreach ($points as $key => $point) {
$points[$key]['y'] = sin(deg2rad($points[$key]['y']));
}
return $points;
}
public function hasDomain() {
return false;
protected function evaluateFunction($x) {
return sin(deg2rad($x));
}
}

View file

@ -9,25 +9,12 @@ final class PhabricatorXChartFunction
return array();
}
public function getDatapoints(PhabricatorChartDataQuery $query) {
$x_min = $query->getMinimumValue();
$x_max = $query->getMaximumValue();
$limit = $query->getLimit();
$points = array();
$steps = $this->newLinearSteps($x_min, $x_max, $limit);
foreach ($steps as $step) {
$points[] = array(
'x' => $step,
'y' => $step,
);
}
return $points;
protected function canEvaluateFunction() {
return true;
}
public function hasDomain() {
return false;
protected function evaluateFunction($x) {
return $x;
}
}

View file

@ -29,6 +29,27 @@ final class PhabricatorFactChartController extends PhabricatorFactController {
$functions[] = id(new PhabricatorSinChartFunction())
->setArguments(array($x_function));
$cos_function = id(new PhabricatorCosChartFunction())
->setArguments(array($x_function));
$functions[] = id(new PhabricatorShiftChartFunction())
->setArguments(
array(
array(
'scale',
array(
'cos',
array(
'scale',
array('x'),
0.001,
),
),
10,
),
200,
));
list($domain_min, $domain_max) = $this->getDomain($functions);
$axis = id(new PhabricatorChartAxis())
@ -83,6 +104,9 @@ final class PhabricatorFactChartController extends PhabricatorFactController {
'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();
}