@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.) hq.recaptime.dev/wiki/Phorge
phorge phabricator
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

Separate the "configuration" and "evaluation" phases of chart functions

Summary:
Depends on D20446. Currently, chart functions are both configured through arguments and evaluated through arguments. This sort of conflates things and makes some logic more difficult than it should be.

Instead:

- Function arguments are used to configure function behavior. For example, `scale(2)` configures a function which does `f(x) => 2 * x`.
- Evaluation is now separate, after configuration.

We can get rid of "sourceFunction" (which was basically marking one argument as "this is the thing that gets piped in" in a weird magical way) and "canEvaluate()" and "impulse".

Sequences of functions are achieved with `compose(u, v, w)`, which configures a function `f(x) => w(v(u(x)))` (note order is left-to right, like piping `x | u | v | w` to produce `y`).

The new flow is:

- Every chartable function is `compose(...)` at top level, and composes one or more functions. `compose(x)` is longhand for `id(x)`. This just gives us a root/anchor node.
- Figure out a domain, through various means.
- Ask the function for a list of good input X values in that domain. This lets function chains which include a "fact" with distinct datapoints tell us that we should evaluate those datapoints.
- Pipe those X values through the function.
- We get Y values out.
- Draw those points.

Also:

- Adds `accumluate()`.
- Adds `sum()`, which is now easy to implement.
- Adds `compose()`.
- All functions can now always evaluate everywhere, they just return `null` if they are not defined at a given X.
- Adds repeatable arguments for `compose(f, g, ...)` and `sum(f, g, ...)`.

Test Plan: {F6409890}

Reviewers: amckinley

Reviewed By: amckinley

Subscribers: yelirekim

Differential Revision: https://secure.phabricator.com/D20454

+561 -306
+8 -2
src/__phutil_library_map__.php
··· 2107 2107 'PhabricatorAccessLog' => 'infrastructure/log/PhabricatorAccessLog.php', 2108 2108 'PhabricatorAccessLogConfigOptions' => 'applications/config/option/PhabricatorAccessLogConfigOptions.php', 2109 2109 'PhabricatorAccessibilitySetting' => 'applications/settings/setting/PhabricatorAccessibilitySetting.php', 2110 + 'PhabricatorAccumulateChartFunction' => 'applications/fact/chart/PhabricatorAccumulateChartFunction.php', 2110 2111 'PhabricatorActionListView' => 'view/layout/PhabricatorActionListView.php', 2111 2112 'PhabricatorActionView' => 'view/layout/PhabricatorActionView.php', 2112 2113 'PhabricatorActivitySettingsPanel' => 'applications/settings/panel/PhabricatorActivitySettingsPanel.php', ··· 2695 2696 'PhabricatorCommitSearchEngine' => 'applications/audit/query/PhabricatorCommitSearchEngine.php', 2696 2697 'PhabricatorCommitTagsField' => 'applications/repository/customfield/PhabricatorCommitTagsField.php', 2697 2698 'PhabricatorCommonPasswords' => 'applications/auth/constants/PhabricatorCommonPasswords.php', 2699 + 'PhabricatorComposeChartFunction' => 'applications/fact/chart/PhabricatorComposeChartFunction.php', 2698 2700 'PhabricatorConduitAPIController' => 'applications/conduit/controller/PhabricatorConduitAPIController.php', 2699 2701 'PhabricatorConduitApplication' => 'applications/conduit/application/PhabricatorConduitApplication.php', 2700 2702 'PhabricatorConduitCallManagementWorkflow' => 'applications/conduit/management/PhabricatorConduitCallManagementWorkflow.php', ··· 3425 3427 'PhabricatorHeraldContentSource' => 'applications/herald/contentsource/PhabricatorHeraldContentSource.php', 3426 3428 'PhabricatorHexdumpDocumentEngine' => 'applications/files/document/PhabricatorHexdumpDocumentEngine.php', 3427 3429 'PhabricatorHighSecurityRequestExceptionHandler' => 'aphront/handler/PhabricatorHighSecurityRequestExceptionHandler.php', 3430 + 'PhabricatorHigherOrderChartFunction' => 'applications/fact/chart/PhabricatorHigherOrderChartFunction.php', 3428 3431 'PhabricatorHomeApplication' => 'applications/home/application/PhabricatorHomeApplication.php', 3429 3432 'PhabricatorHomeConstants' => 'applications/home/constants/PhabricatorHomeConstants.php', 3430 3433 'PhabricatorHomeController' => 'applications/home/controller/PhabricatorHomeController.php', ··· 4725 4728 'PhabricatorSubscriptionsUIEventListener' => 'applications/subscriptions/events/PhabricatorSubscriptionsUIEventListener.php', 4726 4729 'PhabricatorSubscriptionsUnsubscribeEmailCommand' => 'applications/subscriptions/command/PhabricatorSubscriptionsUnsubscribeEmailCommand.php', 4727 4730 'PhabricatorSubtypeEditEngineExtension' => 'applications/transactions/engineextension/PhabricatorSubtypeEditEngineExtension.php', 4731 + 'PhabricatorSumChartFunction' => 'applications/fact/chart/PhabricatorSumChartFunction.php', 4728 4732 'PhabricatorSupportApplication' => 'applications/support/application/PhabricatorSupportApplication.php', 4729 4733 'PhabricatorSyntaxHighlighter' => 'infrastructure/markup/PhabricatorSyntaxHighlighter.php', 4730 4734 'PhabricatorSyntaxHighlightingConfigOptions' => 'applications/config/option/PhabricatorSyntaxHighlightingConfigOptions.php', ··· 4952 4956 'PhabricatorWorkingCopyDiscoveryTestCase' => 'applications/repository/engine/__tests__/PhabricatorWorkingCopyDiscoveryTestCase.php', 4953 4957 'PhabricatorWorkingCopyPullTestCase' => 'applications/repository/engine/__tests__/PhabricatorWorkingCopyPullTestCase.php', 4954 4958 'PhabricatorWorkingCopyTestCase' => 'applications/repository/engine/__tests__/PhabricatorWorkingCopyTestCase.php', 4955 - 'PhabricatorXChartFunction' => 'applications/fact/chart/PhabricatorXChartFunction.php', 4956 4959 'PhabricatorXHPASTDAO' => 'applications/phpast/storage/PhabricatorXHPASTDAO.php', 4957 4960 'PhabricatorXHPASTParseTree' => 'applications/phpast/storage/PhabricatorXHPASTParseTree.php', 4958 4961 'PhabricatorXHPASTViewController' => 'applications/phpast/controller/PhabricatorXHPASTViewController.php', ··· 7987 7990 'PhabricatorAccessLog' => 'Phobject', 7988 7991 'PhabricatorAccessLogConfigOptions' => 'PhabricatorApplicationConfigOptions', 7989 7992 'PhabricatorAccessibilitySetting' => 'PhabricatorSelectSetting', 7993 + 'PhabricatorAccumulateChartFunction' => 'PhabricatorChartFunction', 7990 7994 'PhabricatorActionListView' => 'AphrontTagView', 7991 7995 'PhabricatorActionView' => 'AphrontView', 7992 7996 'PhabricatorActivitySettingsPanel' => 'PhabricatorSettingsPanel', ··· 8691 8695 'PhabricatorCommitSearchEngine' => 'PhabricatorApplicationSearchEngine', 8692 8696 'PhabricatorCommitTagsField' => 'PhabricatorCommitCustomField', 8693 8697 'PhabricatorCommonPasswords' => 'Phobject', 8698 + 'PhabricatorComposeChartFunction' => 'PhabricatorHigherOrderChartFunction', 8694 8699 'PhabricatorConduitAPIController' => 'PhabricatorConduitController', 8695 8700 'PhabricatorConduitApplication' => 'PhabricatorApplication', 8696 8701 'PhabricatorConduitCallManagementWorkflow' => 'PhabricatorConduitManagementWorkflow', ··· 9520 9525 'PhabricatorHeraldContentSource' => 'PhabricatorContentSource', 9521 9526 'PhabricatorHexdumpDocumentEngine' => 'PhabricatorDocumentEngine', 9522 9527 'PhabricatorHighSecurityRequestExceptionHandler' => 'PhabricatorRequestExceptionHandler', 9528 + 'PhabricatorHigherOrderChartFunction' => 'PhabricatorChartFunction', 9523 9529 'PhabricatorHomeApplication' => 'PhabricatorApplication', 9524 9530 'PhabricatorHomeConstants' => 'PhabricatorHomeController', 9525 9531 'PhabricatorHomeController' => 'PhabricatorController', ··· 11053 11059 'PhabricatorSubscriptionsUIEventListener' => 'PhabricatorEventListener', 11054 11060 'PhabricatorSubscriptionsUnsubscribeEmailCommand' => 'MetaMTAEmailTransactionCommand', 11055 11061 'PhabricatorSubtypeEditEngineExtension' => 'PhabricatorEditEngineExtension', 11062 + 'PhabricatorSumChartFunction' => 'PhabricatorHigherOrderChartFunction', 11056 11063 'PhabricatorSupportApplication' => 'PhabricatorApplication', 11057 11064 'PhabricatorSyntaxHighlighter' => 'Phobject', 11058 11065 'PhabricatorSyntaxHighlightingConfigOptions' => 'PhabricatorApplicationConfigOptions', ··· 11323 11330 'PhabricatorWorkingCopyDiscoveryTestCase' => 'PhabricatorWorkingCopyTestCase', 11324 11331 'PhabricatorWorkingCopyPullTestCase' => 'PhabricatorWorkingCopyTestCase', 11325 11332 'PhabricatorWorkingCopyTestCase' => 'PhabricatorTestCase', 11326 - 'PhabricatorXChartFunction' => 'PhabricatorChartFunction', 11327 11333 'PhabricatorXHPASTDAO' => 'PhabricatorLiskDAO', 11328 11334 'PhabricatorXHPASTParseTree' => 'PhabricatorXHPASTDAO', 11329 11335 'PhabricatorXHPASTViewController' => 'PhabricatorController',
+81
src/applications/fact/chart/PhabricatorAccumulateChartFunction.php
··· 1 + <?php 2 + 3 + final class PhabricatorAccumulateChartFunction 4 + extends PhabricatorChartFunction { 5 + 6 + const FUNCTIONKEY = 'accumulate'; 7 + 8 + protected function newArguments() { 9 + return array( 10 + $this->newArgument() 11 + ->setName('x') 12 + ->setType('function'), 13 + ); 14 + } 15 + 16 + public function getDomain() { 17 + return $this->getArgument('x')->getDomain(); 18 + } 19 + 20 + public function newInputValues(PhabricatorChartDataQuery $query) { 21 + return $this->getArgument('x')->newInputValues($query); 22 + } 23 + 24 + public function evaluateFunction(array $xv) { 25 + // First, we're going to accumulate the underlying function. Then 26 + // we'll map the inputs through the accumulation. 27 + 28 + $datasource = $this->getArgument('x'); 29 + 30 + // Use an unconstrained query to pull all the data from the underlying 31 + // source. We need to accumulate data since the beginning of time to 32 + // figure out the right Y-intercept -- otherwise, we'll always start at 33 + // "0" wherever our domain begins. 34 + $empty_query = new PhabricatorChartDataQuery(); 35 + 36 + $datasource_xv = $datasource->newInputValues($empty_query); 37 + if (!$datasource_xv) { 38 + // TODO: Maybe this should just be an error? 39 + $datasource_xv = $xv; 40 + } 41 + 42 + $yv = $datasource->evaluateFunction($datasource_xv); 43 + 44 + $map = array_combine($datasource_xv, $yv); 45 + 46 + $accumulator = 0; 47 + foreach ($map as $x => $y) { 48 + $accumulator += $y; 49 + $map[$x] = $accumulator; 50 + } 51 + 52 + // The value of "accumulate(x)" is the largest datapoint in the map which 53 + // is no larger than "x". 54 + 55 + $map_x = array_keys($map); 56 + $idx = -1; 57 + $max = count($map_x) - 1; 58 + 59 + $yv = array(); 60 + 61 + $value = 0; 62 + foreach ($xv as $x) { 63 + // While the next "x" we need to evaluate the function at lies to the 64 + // right of the next datapoint, move the current datapoint forward until 65 + // we're at the rightmost datapoint which is not larger than "x". 66 + while ($idx < $max) { 67 + if ($map_x[$idx + 1] > $x) { 68 + break; 69 + } 70 + 71 + $idx++; 72 + $value = $map[$map_x[$idx]]; 73 + } 74 + 75 + $yv[] = $value; 76 + } 77 + 78 + return $yv; 79 + } 80 + 81 + }
+44
src/applications/fact/chart/PhabricatorChartDataQuery.php
··· 34 34 return $this->limit; 35 35 } 36 36 37 + public function selectInputValues(array $xv) { 38 + $result = array(); 39 + 40 + $x_min = $this->getMinimumValue(); 41 + $x_max = $this->getMaximumValue(); 42 + $limit = $this->getLimit(); 43 + 44 + if ($x_min !== null) { 45 + foreach ($xv as $key => $x) { 46 + if ($x < $x_min) { 47 + unset($xv[$key]); 48 + } 49 + } 50 + } 51 + 52 + if ($x_max !== null) { 53 + foreach ($xv as $key => $x) { 54 + if ($x > $x_max) { 55 + unset($xv[$key]); 56 + } 57 + } 58 + } 59 + 60 + // If we have too many data points, throw away some of the data. 61 + 62 + // TODO: This doesn't work especially well right now. 63 + 64 + if ($limit !== null) { 65 + $count = count($xv); 66 + if ($count > $limit) { 67 + $ii = 0; 68 + $every = ceil($count / $limit); 69 + foreach ($xv as $key => $x) { 70 + $ii++; 71 + if (($ii % $every) && ($ii != $count)) { 72 + unset($xv[$key]); 73 + } 74 + } 75 + } 76 + } 77 + 78 + return array_values($xv); 79 + } 80 + 37 81 }
+72 -87
src/applications/fact/chart/PhabricatorChartFunction.php
··· 3 3 abstract class PhabricatorChartFunction 4 4 extends Phobject { 5 5 6 - private $xAxis; 7 - private $yAxis; 8 - 9 6 private $argumentParser; 10 - private $sourceFunction; 11 7 12 8 final public function getFunctionKey() { 13 9 return $this->getPhobjectClassConstant('FUNCTIONKEY', 32); ··· 44 40 $parser->setHaveAllArguments(true); 45 41 $parser->parseArguments(); 46 42 47 - $source_argument = $parser->getSourceFunctionArgument(); 48 - if ($source_argument) { 49 - $source_function = $this->getArgument($source_argument->getName()); 50 - $this->setSourceFunction($source_function); 51 - } 52 - 53 43 return $this; 54 44 } 55 45 56 - abstract protected function newArguments(); 46 + public function getSubfunctions() { 47 + $result = array(); 48 + $result[] = $this; 57 49 58 - final protected function newArgument() { 59 - return new PhabricatorChartFunctionArgument(); 60 - } 50 + foreach ($this->getFunctionArguments() as $argument) { 51 + foreach ($argument->getSubfunctions() as $subfunction) { 52 + $result[] = $subfunction; 53 + } 54 + } 61 55 62 - final protected function getArgument($key) { 63 - return $this->getArgumentParser()->getArgumentValue($key); 56 + return $result; 64 57 } 65 58 66 - final protected function getArgumentParser() { 67 - if (!$this->argumentParser) { 68 - $parser = id(new PhabricatorChartFunctionArgumentParser()) 69 - ->setFunction($this); 59 + public function getFunctionArguments() { 60 + $results = array(); 70 61 71 - $this->argumentParser = $parser; 62 + $parser = $this->getArgumentParser(); 63 + foreach ($parser->getAllArguments() as $argument) { 64 + if ($argument->getType() !== 'function') { 65 + continue; 66 + } 67 + 68 + $name = $argument->getName(); 69 + $value = $this->getArgument($name); 70 + 71 + if (!is_array($value)) { 72 + $results[] = $value; 73 + } else { 74 + foreach ($value as $arg_value) { 75 + $results[] = $arg_value; 76 + } 77 + } 72 78 } 73 - return $this->argumentParser; 74 - } 75 79 76 - public function loadData() { 77 - return; 80 + return $results; 78 81 } 79 82 80 - protected function setSourceFunction(PhabricatorChartFunction $source) { 81 - $this->sourceFunction = $source; 82 - return $this; 83 - } 83 + public function newDatapoints(PhabricatorChartDataQuery $query) { 84 + $xv = $this->newInputValues($query); 84 85 85 - protected function getSourceFunction() { 86 - return $this->sourceFunction; 87 - } 86 + if ($xv === null) { 87 + $xv = $this->newDefaultInputValues($query); 88 + } 89 + 90 + $xv = $query->selectInputValues($xv); 91 + 92 + $n = count($xv); 93 + $yv = $this->evaluateFunction($xv); 88 94 89 - final public function setXAxis(PhabricatorChartAxis $x_axis) { 90 - $this->xAxis = $x_axis; 91 - return $this; 92 - } 95 + $points = array(); 96 + for ($ii = 0; $ii < $n; $ii++) { 97 + $y = $yv[$ii]; 98 + 99 + if ($y === null) { 100 + continue; 101 + } 102 + 103 + $points[] = array( 104 + 'x' => $xv[$ii], 105 + 'y' => $y, 106 + ); 107 + } 93 108 94 - final public function getXAxis() { 95 - return $this->xAxis; 109 + return $points; 96 110 } 97 111 98 - final public function setYAxis(PhabricatorChartAxis $y_axis) { 99 - $this->yAxis = $y_axis; 100 - return $this; 101 - } 112 + abstract protected function newArguments(); 102 113 103 - final public function getYAxis() { 104 - return $this->yAxis; 114 + final protected function newArgument() { 115 + return new PhabricatorChartFunctionArgument(); 105 116 } 106 117 107 - protected function canEvaluateFunction() { 108 - return false; 118 + final protected function getArgument($key) { 119 + return $this->getArgumentParser()->getArgumentValue($key); 109 120 } 110 121 111 - protected function evaluateFunction($x) { 112 - throw new PhutilMethodNotImplementedException(); 113 - } 122 + final protected function getArgumentParser() { 123 + if (!$this->argumentParser) { 124 + $parser = id(new PhabricatorChartFunctionArgumentParser()) 125 + ->setFunction($this); 114 126 115 - public function hasDomain() { 116 - if ($this->canEvaluateFunction()) { 117 - return false; 127 + $this->argumentParser = $parser; 118 128 } 119 - 120 - throw new PhutilMethodNotImplementedException(); 129 + return $this->argumentParser; 121 130 } 122 131 123 - public function getDatapoints(PhabricatorChartDataQuery $query) { 124 - if ($this->canEvaluateFunction()) { 125 - $points = $this->newSourceDatapoints($query); 126 - foreach ($points as $key => $point) { 127 - $y = $point['y']; 128 - $y = $this->evaluateFunction($y); 129 - $points[$key]['y'] = $y; 130 - } 132 + abstract public function evaluateFunction(array $xv); 131 133 132 - return $points; 133 - } 134 - 135 - return $this->newDatapoints($query); 134 + public function getDomain() { 135 + return null; 136 136 } 137 137 138 - protected function newDatapoints(PhabricatorChartDataQuery $query) { 139 - throw new PhutilMethodNotImplementedException(); 138 + public function newInputValues(PhabricatorChartDataQuery $query) { 139 + return null; 140 140 } 141 141 142 - protected function newSourceDatapoints(PhabricatorChartDataQuery $query) { 143 - $source = $this->getSourceFunction(); 144 - if ($source) { 145 - return $source->getDatapoints($query); 146 - } 147 - 148 - return $this->newDefaultDatapoints($query); 142 + public function loadData() { 143 + return; 149 144 } 150 145 151 - protected function newDefaultDatapoints(PhabricatorChartDataQuery $query) { 146 + protected function newDefaultInputValues(PhabricatorChartDataQuery $query) { 152 147 $x_min = $query->getMinimumValue(); 153 148 $x_max = $query->getMaximumValue(); 154 149 $limit = $query->getLimit(); 155 150 156 - $points = array(); 157 - $steps = $this->newLinearSteps($x_min, $x_max, $limit); 158 - foreach ($steps as $step) { 159 - $points[] = array( 160 - 'x' => $step, 161 - 'y' => $step, 162 - ); 163 - } 164 - 165 - return $points; 151 + return $this->newLinearSteps($x_min, $x_max, $limit); 166 152 } 167 153 168 154 protected function newLinearSteps($src, $dst, $count) { ··· 213 199 214 200 return $steps; 215 201 } 216 - 217 202 }
+10 -10
src/applications/fact/chart/PhabricatorChartFunctionArgument.php
··· 5 5 6 6 private $name; 7 7 private $type; 8 - private $isSourceFunction; 8 + private $repeatable; 9 9 10 10 public function setName($name) { 11 11 $this->name = $name; ··· 14 14 15 15 public function getName() { 16 16 return $this->name; 17 + } 18 + 19 + public function setRepeatable($repeatable) { 20 + $this->repeatable = $repeatable; 21 + return $this; 22 + } 23 + 24 + public function getRepeatable() { 25 + return $this->repeatable; 17 26 } 18 27 19 28 public function setType($type) { ··· 38 47 39 48 public function getType() { 40 49 return $this->type; 41 - } 42 - 43 - public function setIsSourceFunction($is_source_function) { 44 - $this->isSourceFunction = $is_source_function; 45 - return $this; 46 - } 47 - 48 - public function getIsSourceFunction() { 49 - return $this->isSourceFunction; 50 50 } 51 51 52 52 public function newValue($value) {
+59 -42
src/applications/fact/chart/PhabricatorChartFunctionArgumentParser.php
··· 11 11 private $argumentMap = array(); 12 12 private $argumentPosition = 0; 13 13 private $argumentValues = array(); 14 + private $repeatableArgument = null; 14 15 15 16 public function setFunction(PhabricatorChartFunction $function) { 16 17 $this->function = $function; ··· 55 56 $name)); 56 57 } 57 58 59 + if ($this->repeatableArgument) { 60 + if ($spec->getRepeatable()) { 61 + throw new Exception( 62 + pht( 63 + 'Chart function "%s" emitted multiple repeatable argument '. 64 + 'specifications ("%s" and "%s"). Only one argument may be '. 65 + 'repeatable and it must be the last argument.', 66 + $this->getFunctionArgumentSignature(), 67 + $name, 68 + $this->repeatableArgument->getName())); 69 + } else { 70 + throw new Exception( 71 + pht( 72 + 'Chart function "%s" emitted a repeatable argument ("%s"), then '. 73 + 'another argument ("%s"). No arguments are permitted after a '. 74 + 'repeatable argument.', 75 + $this->getFunctionArgumentSignature(), 76 + $this->repeatableArgument->getName(), 77 + $name)); 78 + } 79 + } 80 + 81 + if ($spec->getRepeatable()) { 82 + $this->repeatableArgument = $spec; 83 + } 84 + 58 85 $this->argumentMap[$name] = $spec; 59 86 $this->unparsedArguments[] = $spec; 60 87 ··· 72 99 return $this; 73 100 } 74 101 102 + public function getAllArguments() { 103 + return array_values($this->argumentMap); 104 + } 105 + 75 106 public function parseArguments() { 76 107 $have_count = count($this->rawArguments); 77 108 $want_count = count($this->argumentMap); 78 109 79 110 if ($this->haveAllArguments) { 80 - if ($want_count !== $have_count) { 111 + if ($this->repeatableArgument) { 112 + if ($want_count > $have_count) { 113 + throw new Exception( 114 + pht( 115 + 'Function "%s" expects %s or more argument(s), but only %s '. 116 + 'argument(s) were provided.', 117 + $this->getFunctionArgumentSignature(), 118 + $want_count, 119 + $have_count)); 120 + } 121 + } else if ($want_count !== $have_count) { 81 122 throw new Exception( 82 123 pht( 83 124 'Function "%s" expects %s argument(s), but %s argument(s) were '. ··· 104 145 105 146 $raw_argument = array_shift($this->unconsumedArguments); 106 147 $this->argumentPosition++; 148 + 149 + $is_repeatable = $argument->getRepeatable(); 150 + 151 + // If this argument is repeatable and we have more arguments, add it 152 + // back to the end of the list so we can continue parsing. 153 + if ($is_repeatable && $this->unconsumedArguments) { 154 + $this->unparsedArguments[] = $argument; 155 + } 107 156 108 157 try { 109 158 $value = $argument->newValue($raw_argument); ··· 118 167 $ex->getMessage())); 119 168 } 120 169 121 - $this->argumentValues[$name] = $value; 170 + if ($is_repeatable) { 171 + if (!isset($this->argumentValues[$name])) { 172 + $this->argumentValues[$name] = array(); 173 + } 174 + $this->argumentValues[$name][] = $value; 175 + } else { 176 + $this->argumentValues[$name] = $value; 177 + } 122 178 } 123 179 } 124 180 ··· 141 197 $argument_list[] = $key; 142 198 } 143 199 144 - if (!$this->haveAllArguments) { 200 + if (!$this->haveAllArguments || $this->repeatableArgument) { 145 201 $argument_list[] = '...'; 146 202 } 147 203 ··· 149 205 '%s(%s)', 150 206 $this->getFunction()->getFunctionKey(), 151 207 implode(', ', $argument_list)); 152 - } 153 - 154 - public function getSourceFunctionArgument() { 155 - $required_type = 'function'; 156 - 157 - $sources = array(); 158 - foreach ($this->argumentMap as $key => $argument) { 159 - if (!$argument->getIsSourceFunction()) { 160 - continue; 161 - } 162 - 163 - if ($argument->getType() !== $required_type) { 164 - throw new Exception( 165 - pht( 166 - 'Function "%s" defines an argument "%s" which is marked as a '. 167 - 'source function, but the type of this argument is not "%s".', 168 - $this->getFunctionArgumentSignature(), 169 - $argument->getName(), 170 - $required_type)); 171 - } 172 - 173 - $sources[$key] = $argument; 174 - } 175 - 176 - if (!$sources) { 177 - return null; 178 - } 179 - 180 - if (count($sources) > 1) { 181 - throw new Exception( 182 - pht( 183 - 'Function "%s" defines more than one argument as a source '. 184 - 'function (arguments: %s). Functions must have zero or one '. 185 - 'source function.', 186 - $this->getFunctionArgumentSignature(), 187 - implode(', ', array_keys($sources)))); 188 - } 189 - 190 - return head($sources); 191 208 } 192 209 193 210 }
+73
src/applications/fact/chart/PhabricatorComposeChartFunction.php
··· 1 + <?php 2 + 3 + final class PhabricatorComposeChartFunction 4 + extends PhabricatorHigherOrderChartFunction { 5 + 6 + const FUNCTIONKEY = 'compose'; 7 + 8 + protected function newArguments() { 9 + return array( 10 + $this->newArgument() 11 + ->setName('f') 12 + ->setType('function') 13 + ->setRepeatable(true), 14 + ); 15 + } 16 + 17 + public function evaluateFunction(array $xv) { 18 + $original_positions = array_keys($xv); 19 + $remaining_positions = $original_positions; 20 + foreach ($this->getFunctionArguments() as $function) { 21 + $xv = $function->evaluateFunction($xv); 22 + 23 + // If a function evaluates to "null" at some positions, we want to return 24 + // "null" at those positions and stop evaluating the function. 25 + 26 + // We also want to pass "evaluateFunction()" a natural list containing 27 + // only values it should evaluate: keys should not be important and we 28 + // should not pass "null". This simplifies implementation of functions. 29 + 30 + // To do this, first create a map from original input positions to 31 + // function return values. 32 + $xv = array_combine($remaining_positions, $xv); 33 + 34 + // If a function evaluated to "null" at any position where we evaluated 35 + // it, the result will be "null". We remove the position from the 36 + // vector so we stop evaluating it. 37 + foreach ($xv as $x => $y) { 38 + if ($y !== null) { 39 + continue; 40 + } 41 + 42 + unset($xv[$x]); 43 + } 44 + 45 + // Store the remaining original input positions for the next round, then 46 + // throw away the array keys so we're passing the next function a natural 47 + // list with only non-"null" values. 48 + $remaining_positions = array_keys($xv); 49 + $xv = array_values($xv); 50 + 51 + // If we have no more inputs to evaluate, we can bail out early rather 52 + // than passing empty vectors to functions for evaluation. 53 + if (!$xv) { 54 + break; 55 + } 56 + } 57 + 58 + 59 + $yv = array(); 60 + $xv = array_combine($remaining_positions, $xv); 61 + foreach ($original_positions as $position) { 62 + if (isset($xv[$position])) { 63 + $y = $xv[$position]; 64 + } else { 65 + $y = null; 66 + } 67 + $yv[$position] = $y; 68 + } 69 + 70 + return $yv; 71 + } 72 + 73 + }
+9 -5
src/applications/fact/chart/PhabricatorConstantChartFunction.php
··· 13 13 ); 14 14 } 15 15 16 - protected function canEvaluateFunction() { 17 - return true; 18 - } 16 + public function evaluateFunction(array $xv) { 17 + $n = $this->getArgument('n'); 19 18 20 - protected function evaluateFunction($x) { 21 - return $this->getArgument('n'); 19 + $yv = array(); 20 + 21 + foreach ($xv as $x) { 22 + $yv[] = $n; 23 + } 24 + 25 + return $yv; 22 26 } 23 27 24 28 }
+8 -11
src/applications/fact/chart/PhabricatorCosChartFunction.php
··· 6 6 const FUNCTIONKEY = 'cos'; 7 7 8 8 protected function newArguments() { 9 - return array( 10 - $this->newArgument() 11 - ->setName('x') 12 - ->setType('function') 13 - ->setIsSourceFunction(true), 14 - ); 9 + return array(); 15 10 } 16 11 17 - protected function canEvaluateFunction() { 18 - return true; 19 - } 12 + public function evaluateFunction(array $xv) { 13 + $yv = array(); 14 + 15 + foreach ($xv as $x) { 16 + $yv[] = cos(deg2rad($x)); 17 + } 20 18 21 - protected function evaluateFunction($x) { 22 - return cos(deg2rad($x)); 19 + return $yv; 23 20 } 24 21 25 22 }
+29 -56
src/applications/fact/chart/PhabricatorFactChartFunction.php
··· 6 6 const FUNCTIONKEY = 'fact'; 7 7 8 8 private $fact; 9 - private $datapoints; 9 + private $map; 10 10 11 11 protected function newArguments() { 12 12 $key_argument = $this->newArgument() ··· 44 44 return; 45 45 } 46 46 47 - $points = array(); 47 + $map = array(); 48 + foreach ($data as $row) { 49 + $value = (int)$row['value']; 50 + $epoch = (int)$row['epoch']; 51 + 52 + if (!isset($map[$epoch])) { 53 + $map[$epoch] = 0; 54 + } 48 55 49 - $sum = 0; 50 - foreach ($data as $key => $row) { 51 - $sum += (int)$row['value']; 52 - $points[] = array( 53 - 'x' => (int)$row['epoch'], 54 - 'y' => $sum, 55 - ); 56 + $map[$epoch] += $value; 56 57 } 57 58 58 - $this->datapoints = $points; 59 + $this->map = $map; 59 60 } 60 61 61 - public function getDatapoints(PhabricatorChartDataQuery $query) { 62 - $points = $this->datapoints; 63 - if (!$points) { 64 - return array(); 65 - } 62 + public function getDomain() { 63 + return array( 64 + head_key($this->map), 65 + last_key($this->map), 66 + ); 67 + } 66 68 67 - $x_min = $query->getMinimumValue(); 68 - $x_max = $query->getMaximumValue(); 69 - $limit = $query->getLimit(); 69 + public function newInputValues(PhabricatorChartDataQuery $query) { 70 + return array_keys($this->map); 71 + } 70 72 71 - if ($x_min !== null) { 72 - foreach ($points as $key => $point) { 73 - if ($point['x'] < $x_min) { 74 - unset($points[$key]); 75 - } 76 - } 77 - } 73 + public function evaluateFunction(array $xv) { 74 + $map = $this->map; 78 75 79 - if ($x_max !== null) { 80 - foreach ($points as $key => $point) { 81 - if ($point['x'] > $x_max) { 82 - unset($points[$key]); 83 - } 84 - } 85 - } 76 + $yv = array(); 86 77 87 - // If we have too many data points, throw away some of the data. 88 - if ($limit !== null) { 89 - $count = count($points); 90 - if ($count > $limit) { 91 - $ii = 0; 92 - $every = ceil($count / $limit); 93 - foreach ($points as $key => $point) { 94 - $ii++; 95 - if (($ii % $every) && ($ii != $count)) { 96 - unset($points[$key]); 97 - } 98 - } 78 + foreach ($xv as $x) { 79 + if (isset($map[$x])) { 80 + $yv[] = $map[$x]; 81 + } else { 82 + $yv[] = null; 99 83 } 100 84 } 101 85 102 - return $points; 103 - } 104 - 105 - public function hasDomain() { 106 - return true; 107 - } 108 - 109 - public function getDomain() { 110 - // TODO: We can examine the data to fit a better domain. 111 - 112 - $now = PhabricatorTime::getNow(); 113 - return array($now - phutil_units('90 days in seconds'), $now); 86 + return $yv; 114 87 } 115 88 116 89 }
+56
src/applications/fact/chart/PhabricatorHigherOrderChartFunction.php
··· 1 + <?php 2 + 3 + abstract class PhabricatorHigherOrderChartFunction 4 + extends PhabricatorChartFunction { 5 + 6 + public function getDomain() { 7 + $minv = array(); 8 + $maxv = array(); 9 + foreach ($this->getFunctionArguments() as $function) { 10 + $domain = $function->getDomain(); 11 + if ($domain !== null) { 12 + list($min, $max) = $domain; 13 + $minv[] = $min; 14 + $maxv[] = $max; 15 + } 16 + } 17 + 18 + if (!$minv && !$maxv) { 19 + return null; 20 + } 21 + 22 + $min = null; 23 + $max = null; 24 + 25 + if ($minv) { 26 + $min = min($minv); 27 + } 28 + 29 + if ($maxv) { 30 + $max = max($maxv); 31 + } 32 + 33 + return array($min, $max); 34 + } 35 + 36 + public function newInputValues(PhabricatorChartDataQuery $query) { 37 + $map = array(); 38 + foreach ($this->getFunctionArguments() as $function) { 39 + $xv = $function->newInputValues($query); 40 + if ($xv !== null) { 41 + foreach ($xv as $x) { 42 + $map[$x] = true; 43 + } 44 + } 45 + } 46 + 47 + if (!$map) { 48 + return null; 49 + } 50 + 51 + ksort($map); 52 + 53 + return array_keys($map); 54 + } 55 + 56 + }
+9 -9
src/applications/fact/chart/PhabricatorScaleChartFunction.php
··· 8 8 protected function newArguments() { 9 9 return array( 10 10 $this->newArgument() 11 - ->setName('x') 12 - ->setType('function') 13 - ->setIsSourceFunction(true), 14 - $this->newArgument() 15 11 ->setName('scale') 16 12 ->setType('number'), 17 13 ); 18 14 } 19 15 20 - protected function canEvaluateFunction() { 21 - return true; 22 - } 16 + public function evaluateFunction(array $xv) { 17 + $scale = $this->getArgument('scale'); 23 18 24 - protected function evaluateFunction($x) { 25 - return $x * $this->getArgument('scale'); 19 + $yv = array(); 20 + 21 + foreach ($xv as $x) { 22 + $yv[] = $x * $scale; 23 + } 24 + 25 + return $yv; 26 26 } 27 27 28 28 }
+9 -9
src/applications/fact/chart/PhabricatorShiftChartFunction.php
··· 8 8 protected function newArguments() { 9 9 return array( 10 10 $this->newArgument() 11 - ->setName('x') 12 - ->setType('function') 13 - ->setIsSourceFunction(true), 14 - $this->newArgument() 15 11 ->setName('shift') 16 12 ->setType('number'), 17 13 ); 18 14 } 19 15 20 - protected function canEvaluateFunction() { 21 - return true; 22 - } 16 + public function evaluateFunction(array $xv) { 17 + $shift = $this->getArgument('shift'); 23 18 24 - protected function evaluateFunction($x) { 25 - return $x * $this->getArgument('shift'); 19 + $yv = array(); 20 + 21 + foreach ($xv as $x) { 22 + $yv[] = $x + $shift; 23 + } 24 + 25 + return $yv; 26 26 } 27 27 28 28 }
+8 -11
src/applications/fact/chart/PhabricatorSinChartFunction.php
··· 6 6 const FUNCTIONKEY = 'sin'; 7 7 8 8 protected function newArguments() { 9 - return array( 10 - $this->newArgument() 11 - ->setName('x') 12 - ->setType('function') 13 - ->setIsSourceFunction(true), 14 - ); 9 + return array(); 15 10 } 16 11 17 - protected function canEvaluateFunction() { 18 - return true; 19 - } 12 + public function evaluateFunction(array $xv) { 13 + $yv = array(); 14 + 15 + foreach ($xv as $x) { 16 + $yv[] = sin(deg2rad($x)); 17 + } 20 18 21 - protected function evaluateFunction($x) { 22 - return sin(deg2rad($x)); 19 + return $yv; 23 20 } 24 21 25 22 }
+40
src/applications/fact/chart/PhabricatorSumChartFunction.php
··· 1 + <?php 2 + 3 + final class PhabricatorSumChartFunction 4 + extends PhabricatorHigherOrderChartFunction { 5 + 6 + const FUNCTIONKEY = 'sum'; 7 + 8 + protected function newArguments() { 9 + return array( 10 + $this->newArgument() 11 + ->setName('f') 12 + ->setType('function') 13 + ->setRepeatable(true), 14 + ); 15 + } 16 + 17 + public function evaluateFunction(array $xv) { 18 + $fv = array(); 19 + foreach ($this->getFunctionArguments() as $function) { 20 + $fv[] = $function->evaluateFunction($xv); 21 + } 22 + 23 + $n = count($xv); 24 + $yv = array_fill(0, $n, null); 25 + 26 + foreach ($fv as $f) { 27 + for ($ii = 0; $ii < $n; $ii++) { 28 + if ($f[$ii] !== null) { 29 + if (!isset($yv[$ii])) { 30 + $yv[$ii] = 0; 31 + } 32 + $yv[$ii] += $f[$ii]; 33 + } 34 + } 35 + } 36 + 37 + return $yv; 38 + } 39 + 40 + }
-20
src/applications/fact/chart/PhabricatorXChartFunction.php
··· 1 - <?php 2 - 3 - final class PhabricatorXChartFunction 4 - extends PhabricatorChartFunction { 5 - 6 - const FUNCTIONKEY = 'x'; 7 - 8 - protected function newArguments() { 9 - return array(); 10 - } 11 - 12 - protected function canEvaluateFunction() { 13 - return true; 14 - } 15 - 16 - protected function evaluateFunction($x) { 17 - return $x; 18 - } 19 - 20 - }
+46 -44
src/applications/fact/controller/PhabricatorFactChartController.php
··· 12 12 $is_chart_mode = ($mode === 'chart'); 13 13 $is_draw_mode = ($mode === 'draw'); 14 14 15 - $functions = array(); 15 + $argvs = array(); 16 16 17 - $functions[] = id(new PhabricatorFactChartFunction()) 18 - ->setArguments(array('tasks.count.create')); 17 + $argvs[] = array('fact', 'tasks.count.create'); 19 18 20 - $functions[] = id(new PhabricatorFactChartFunction()) 21 - ->setArguments(array('tasks.open-count.create')); 19 + $argvs[] = array('constant', 360); 20 + 21 + $argvs[] = array('fact', 'tasks.open-count.create'); 22 22 23 - $x_function = id(new PhabricatorXChartFunction()) 24 - ->setArguments(array()); 23 + $argvs[] = array( 24 + 'sum', 25 + array( 26 + 'accumulate', 27 + array('fact', 'tasks.count.create'), 28 + ), 29 + array( 30 + 'accumulate', 31 + array('fact', 'tasks.open-count.create'), 32 + ), 33 + ); 25 34 26 - $functions[] = id(new PhabricatorConstantChartFunction()) 27 - ->setArguments(array(360)); 35 + $argvs[] = array( 36 + 'compose', 37 + array('scale', 0.001), 38 + array('cos'), 39 + array('scale', 100), 40 + array('shift', 800), 41 + ); 28 42 29 - $functions[] = id(new PhabricatorSinChartFunction()) 30 - ->setArguments(array($x_function)); 43 + $functions = array(); 44 + foreach ($argvs as $argv) { 45 + $functions[] = id(new PhabricatorComposeChartFunction()) 46 + ->setArguments(array($argv)); 47 + } 31 48 32 - $cos_function = id(new PhabricatorCosChartFunction()) 33 - ->setArguments(array($x_function)); 49 + $subfunctions = array(); 50 + foreach ($functions as $function) { 51 + foreach ($function->getSubfunctions() as $subfunction) { 52 + $subfunctions[] = $subfunction; 53 + } 54 + } 34 55 35 - $functions[] = id(new PhabricatorShiftChartFunction()) 36 - ->setArguments( 37 - array( 38 - array( 39 - 'scale', 40 - array( 41 - 'cos', 42 - array( 43 - 'scale', 44 - array('x'), 45 - 0.001, 46 - ), 47 - ), 48 - 10, 49 - ), 50 - 200, 51 - )); 56 + foreach ($subfunctions as $subfunction) { 57 + $subfunction->loadData(); 58 + } 52 59 53 60 list($domain_min, $domain_max) = $this->getDomain($functions); 54 61 ··· 63 70 64 71 $datasets = array(); 65 72 foreach ($functions as $function) { 66 - $function->setXAxis($axis); 67 - 68 - $function->loadData(); 69 - 70 - $points = $function->getDatapoints($data_query); 73 + $points = $function->newDatapoints($data_query); 71 74 72 75 $x = array(); 73 76 $y = array(); ··· 157 160 private function getDomain(array $functions) { 158 161 $domain_min_list = null; 159 162 $domain_max_list = null; 163 + 160 164 foreach ($functions as $function) { 161 - if ($function->hasDomain()) { 162 - $domain = $function->getDomain(); 165 + $domain = $function->getDomain(); 163 166 164 - list($domain_min, $domain_max) = $domain; 167 + list($function_min, $function_max) = $domain; 165 168 166 - if ($domain_min !== null) { 167 - $domain_min_list[] = $domain_min; 168 - } 169 + if ($function_min !== null) { 170 + $domain_min_list[] = $function_min; 171 + } 169 172 170 - if ($domain_max !== null) { 171 - $domain_max_list[] = $domain_max; 172 - } 173 + if ($function_max !== null) { 174 + $domain_max_list[] = $function_max; 173 175 } 174 176 } 175 177