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 @@ -2097,6 +2097,7 @@ 'PhabricatorAccessLog' => 'infrastructure/log/PhabricatorAccessLog.php', 'PhabricatorAccessLogConfigOptions' => 'applications/config/option/PhabricatorAccessLogConfigOptions.php', 'PhabricatorAccessibilitySetting' => 'applications/settings/setting/PhabricatorAccessibilitySetting.php', + 'PhabricatorAccumulateChartFunction' => 'applications/fact/chart/PhabricatorAccumulateChartFunction.php', 'PhabricatorActionListView' => 'view/layout/PhabricatorActionListView.php', 'PhabricatorActionView' => 'view/layout/PhabricatorActionView.php', 'PhabricatorActivitySettingsPanel' => 'applications/settings/panel/PhabricatorActivitySettingsPanel.php', @@ -7961,6 +7962,7 @@ 'PhabricatorAccessLog' => 'Phobject', 'PhabricatorAccessLogConfigOptions' => 'PhabricatorApplicationConfigOptions', 'PhabricatorAccessibilitySetting' => 'PhabricatorSelectSetting', + 'PhabricatorAccumulateChartFunction' => 'PhabricatorChartFunction', 'PhabricatorActionListView' => 'AphrontTagView', 'PhabricatorActionView' => 'AphrontView', 'PhabricatorActivitySettingsPanel' => 'PhabricatorSettingsPanel', diff --git a/src/applications/fact/chart/PhabricatorAccumulateChartFunction.php b/src/applications/fact/chart/PhabricatorAccumulateChartFunction.php new file mode 100644 --- /dev/null +++ b/src/applications/fact/chart/PhabricatorAccumulateChartFunction.php @@ -0,0 +1,37 @@ +newArgument() + ->setName('x') + ->setType('function') + ->setFunctionType('impulse') + ->setIsSourceFunction(true), + ); + } + + protected function newDatapoints(PhabricatorChartDataQuery $query) { + $source_function = $this->getSourceFunction(); + + // Use an unconstrained query to pull all the data from the underlying + // source. We need to accumulate data since the beginning of time to + // figure out the right Y-intercept -- otherwise, we'll always start at + // "0" wherever our domain begins. + $empty_query = new PhabricatorChartDataQuery(); + $points = $source_function->getDatapoints($empty_query); + + $accumulator = 0; + foreach ($points as $key => $point) { + $accumulator += $point['y']; + $points[$key]['y'] = $accumulator; + } + + return $query->selectDatapoints($points); + } + +} diff --git a/src/applications/fact/chart/PhabricatorChartDataQuery.php b/src/applications/fact/chart/PhabricatorChartDataQuery.php --- a/src/applications/fact/chart/PhabricatorChartDataQuery.php +++ b/src/applications/fact/chart/PhabricatorChartDataQuery.php @@ -34,4 +34,43 @@ return $this->limit; } + public function selectDatapoints(array $points) { + $x_min = $this->getMinimumValue(); + $x_max = $this->getMaximumValue(); + $limit = $this->getLimit(); + + if ($x_min !== null) { + foreach ($points as $key => $point) { + if ($point['x'] < $x_min) { + unset($points[$key]); + } + } + } + + if ($x_max !== null) { + foreach ($points as $key => $point) { + if ($point['x'] > $x_max) { + unset($points[$key]); + } + } + } + + // If we have too many data points, throw away some of the data. + if ($limit !== null) { + $count = count($points); + if ($count > $limit) { + $ii = 0; + $every = ceil($count / $limit); + foreach ($points as $key => $point) { + $ii++; + if (($ii % $every) && ($ii != $count)) { + unset($points[$key]); + } + } + } + } + + return $points; + } + } 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 @@ -73,6 +73,27 @@ return $this->argumentParser; } + public function selectAllFunctions() { + $result = array(); + $result[] = $this; + + $parser = $this->getArgumentParser(); + foreach ($parser->getAllArguments() as $argument) { + if ($argument->getType() !== 'function') { + continue; + } + + $name = $argument->getName(); + $value = $this->getArgument($name); + + foreach ($value->selectAllFunctions() as $subfunction) { + $result[] = $subfunction; + } + } + + return $result; + } + public function loadData() { return; } @@ -117,6 +138,34 @@ return false; } + if ($this->isImpulseFunction()) { + return true; + } + + $source_function = $this->getSourceFunction(); + if ($source_function) { + return $source_function->hasDomain(); + } + + throw new PhutilMethodNotImplementedException(); + } + + public function isImpulseFunction() { + return false; + } + + public function getDomain() { + // TODO: We can examine the data to fit a better domain. + if ($this->isImpulseFunction()) { + $now = PhabricatorTime::getNow(); + return array($now - phutil_units('90 days in seconds'), $now); + } + + $source_function = $this->getSourceFunction(); + if ($source_function) { + return $source_function->getDomain(); + } + throw new PhutilMethodNotImplementedException(); } diff --git a/src/applications/fact/chart/PhabricatorChartFunctionArgument.php b/src/applications/fact/chart/PhabricatorChartFunctionArgument.php --- a/src/applications/fact/chart/PhabricatorChartFunctionArgument.php +++ b/src/applications/fact/chart/PhabricatorChartFunctionArgument.php @@ -6,6 +6,7 @@ private $name; private $type; private $isSourceFunction; + private $functionType; public function setName($name) { $this->name = $name; @@ -49,6 +50,15 @@ return $this->isSourceFunction; } + public function setFunctionType($function_type) { + $this->functionType = $function_type; + return $this; + } + + public function getFunctionType() { + return $this->functionType; + } + public function newValue($value) { switch ($this->getType()) { case 'fact-key': @@ -119,8 +129,24 @@ implode(', ', array_keys($functions)))); } - return id(clone $functions[$function_name]) + $function = id(clone $functions[$function_name]) ->setArguments($value); + + switch ($this->getFunctionType()) { + case 'impulse': + if (!$function->isImpulseFunction()) { + throw new Exception( + pht( + 'Function must be an impulse function, but "%s" is not '. + 'an impulse function.', + $function_name)); + } + break; + default: + break; + } + + return $function; case 'number': if (!is_float($value) && !is_int($value)) { throw new Exception( 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 @@ -72,6 +72,10 @@ return $this; } + public function getAllArguments() { + return array_values($this->argumentMap); + } + public function parseArguments() { $have_count = count($this->rawArguments); $want_count = count($this->argumentMap); diff --git a/src/applications/fact/chart/PhabricatorFactChartFunction.php b/src/applications/fact/chart/PhabricatorFactChartFunction.php --- a/src/applications/fact/chart/PhabricatorFactChartFunction.php +++ b/src/applications/fact/chart/PhabricatorFactChartFunction.php @@ -22,6 +22,10 @@ return $fact->getFunctionArguments(); } + public function isImpulseFunction() { + return true; + } + public function loadData() { $fact = $this->fact; @@ -44,73 +48,32 @@ return; } - $points = array(); - - $sum = 0; - foreach ($data as $key => $row) { - $sum += (int)$row['value']; - $points[] = array( - 'x' => (int)$row['epoch'], - 'y' => $sum, - ); - } - - $this->datapoints = $points; - } - - public function getDatapoints(PhabricatorChartDataQuery $query) { - $points = $this->datapoints; - if (!$points) { - return array(); - } - - $x_min = $query->getMinimumValue(); - $x_max = $query->getMaximumValue(); - $limit = $query->getLimit(); + $map = array(); + foreach ($data as $row) { + $value = (int)$row['value']; + $epoch = (int)$row['epoch']; - if ($x_min !== null) { - foreach ($points as $key => $point) { - if ($point['x'] < $x_min) { - unset($points[$key]); - } + if (!isset($map[$epoch])) { + $map[$epoch] = 0; } - } - if ($x_max !== null) { - foreach ($points as $key => $point) { - if ($point['x'] > $x_max) { - unset($points[$key]); - } - } + $map[$epoch] += $value; } - // If we have too many data points, throw away some of the data. - if ($limit !== null) { - $count = count($points); - if ($count > $limit) { - $ii = 0; - $every = ceil($count / $limit); - foreach ($points as $key => $point) { - $ii++; - if (($ii % $every) && ($ii != $count)) { - unset($points[$key]); - } - } - } + $points = array(); + foreach ($map as $x => $y) { + $points[] = array( + 'x' => $x, + 'y' => $y, + ); } - return $points; + $this->datapoints = $points; } - public function hasDomain() { - return true; + protected function newDatapoints(PhabricatorChartDataQuery $query) { + return $query->selectDatapoints($this->datapoints); } - public function getDomain() { - // TODO: We can examine the data to fit a better domain. - - $now = PhabricatorTime::getNow(); - return array($now - phutil_units('90 days in seconds'), $now); - } } diff --git a/src/applications/fact/chart/PhabricatorShiftChartFunction.php b/src/applications/fact/chart/PhabricatorShiftChartFunction.php --- a/src/applications/fact/chart/PhabricatorShiftChartFunction.php +++ b/src/applications/fact/chart/PhabricatorShiftChartFunction.php @@ -22,7 +22,7 @@ } protected function evaluateFunction($x) { - return $x * $this->getArgument('shift'); + return $x + $this->getArgument('shift'); } } 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 @@ -14,12 +14,6 @@ $functions = array(); - $functions[] = id(new PhabricatorFactChartFunction()) - ->setArguments(array('tasks.count.create')); - - $functions[] = id(new PhabricatorFactChartFunction()) - ->setArguments(array('tasks.open-count.create')); - $x_function = id(new PhabricatorXChartFunction()) ->setArguments(array()); @@ -42,12 +36,21 @@ array( 'scale', array('x'), - 0.001, + 0.0001, ), ), - 10, + 200, + ), + 75, + )); + + $functions[] = id(new PhabricatorAccumulateChartFunction()) + ->setArguments( + array( + array( + 'fact', + 'tasks.count.create', ), - 200, )); list($domain_min, $domain_max) = $this->getDomain($functions); @@ -61,6 +64,12 @@ ->setMaximumValue($domain_max) ->setLimit(2000); + foreach ($functions as $function) { + foreach ($function->selectAllFunctions() as $subfunction) { + $subfunction->loadData(); + } + } + $datasets = array(); foreach ($functions as $function) { $function->setXAxis($axis);