Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/matomo-org/matomo.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'plugins/CoreVisualizations')
-rw-r--r--plugins/CoreVisualizations/JqplotDataGenerator/Evolution.php41
-rw-r--r--plugins/CoreVisualizations/Visualizations/Sparkline.php4
-rw-r--r--plugins/CoreVisualizations/Visualizations/Sparklines.php147
-rw-r--r--plugins/CoreVisualizations/Visualizations/Sparklines/Config.php354
-rw-r--r--plugins/CoreVisualizations/javascripts/jqplotEvolutionGraph.js35
-rw-r--r--plugins/CoreVisualizations/templates/_dataTableViz_sparklines.twig31
-rw-r--r--plugins/CoreVisualizations/templates/macros.twig32
-rw-r--r--plugins/CoreVisualizations/tests/Integration/SparklinesConfigTest.php128
-rw-r--r--plugins/CoreVisualizations/tests/Unit/SparklinesConfigTest.php130
9 files changed, 830 insertions, 72 deletions
diff --git a/plugins/CoreVisualizations/JqplotDataGenerator/Evolution.php b/plugins/CoreVisualizations/JqplotDataGenerator/Evolution.php
index 5675153c34..5114167364 100644
--- a/plugins/CoreVisualizations/JqplotDataGenerator/Evolution.php
+++ b/plugins/CoreVisualizations/JqplotDataGenerator/Evolution.php
@@ -12,7 +12,6 @@ use Piwik\Archive\DataTableFactory;
use Piwik\Common;
use Piwik\DataTable;
use Piwik\DataTable\Row;
-use Piwik\Menu\MenuMain;
use Piwik\Plugins\CoreVisualizations\JqplotDataGenerator;
use Piwik\Url;
@@ -76,7 +75,6 @@ class Evolution extends JqplotDataGenerator
$periodLabel = reset($dataTables)->getMetadata(DataTableFactory::TABLE_METADATA_PERIOD_INDEX)->getLabel();
$axisXOnClick = array();
- $queryStringAsHash = $this->getQueryStringAsHash();
foreach ($dataTable->getDataTables() as $metadataDataTable) {
$dateInUrl = $metadataDataTable->getMetadata(DataTableFactory::TABLE_METADATA_PERIOD_INDEX)->getDateStart();
$parameters = array(
@@ -85,16 +83,7 @@ class Evolution extends JqplotDataGenerator
'date' => $dateInUrl->toString(),
'segment' => \Piwik\API\Request::getRawSegmentFromRequest()
);
- $hash = '';
- if (!empty($queryStringAsHash)) {
- $hash = '#' . Url::getQueryStringFromParameters($queryStringAsHash + $parameters);
- }
- $link = 'index.php?' .
- Url::getQueryStringFromParameters(array(
- 'module' => 'CoreHome',
- 'action' => 'index',
- ) + $parameters)
- . $hash;
+ $link = Url::getQueryStringFromParameters($parameters);
$axisXOnClick[] = $link;
}
$visualization->setAxisXOnClick($axisXOnClick);
@@ -144,34 +133,6 @@ class Evolution extends JqplotDataGenerator
return $label;
}
- /**
- * We link the graph dots to the same report as currently being displayed (only the date would change).
- *
- * In some cases the widget is loaded within a report that doesn't exist as such.
- * For example, the dashboards loads the 'Last visits graph' widget which can't be directly linked to.
- * Instead, the graph must link back to the dashboard.
- *
- * In other cases, like Visitors>Overview or the Goals graphs, we can link the graph clicks to the same report.
- *
- * To detect whether or not we can link to a report, we simply check if the current URL from which it was loaded
- * belongs to the menu or not. If it doesn't belong to the menu, we do not append the hash to the URL,
- * which results in loading the dashboard.
- *
- * @return array Query string array to append to the URL hash or false if the dashboard should be displayed
- */
- private function getQueryStringAsHash()
- {
- $queryString = Url::getArrayFromCurrentQueryString();
- $piwikParameters = array('idSite', 'date', 'period', 'XDEBUG_SESSION_START', 'KEY');
- foreach ($piwikParameters as $parameter) {
- unset($queryString[$parameter]);
- }
- if (MenuMain::getInstance()->isUrlFound($queryString)) {
- return $queryString;
- }
- return false;
- }
-
private function isLinkEnabled()
{
static $linkEnabled;
diff --git a/plugins/CoreVisualizations/Visualizations/Sparkline.php b/plugins/CoreVisualizations/Visualizations/Sparkline.php
index 2ca75bbcfe..3c1dbf5a56 100644
--- a/plugins/CoreVisualizations/Visualizations/Sparkline.php
+++ b/plugins/CoreVisualizations/Visualizations/Sparkline.php
@@ -25,7 +25,7 @@ class Sparkline extends ViewDataTable
* @see ViewDataTable::main()
* @return mixed
*/
- protected function buildView()
+ public function render()
{
// If period=range, we force the sparkline to draw daily data points
$period = Common::getRequestVar('period');
@@ -58,7 +58,7 @@ class Sparkline extends ViewDataTable
$graph->main();
- return $graph;
+ return $graph->render();
}
/**
diff --git a/plugins/CoreVisualizations/Visualizations/Sparklines.php b/plugins/CoreVisualizations/Visualizations/Sparklines.php
new file mode 100644
index 0000000000..3b576c885e
--- /dev/null
+++ b/plugins/CoreVisualizations/Visualizations/Sparklines.php
@@ -0,0 +1,147 @@
+<?php
+/**
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ *
+ */
+namespace Piwik\Plugins\CoreVisualizations\Visualizations;
+
+use Piwik\DataTable;
+use Piwik\Metrics;
+use Piwik\Plugin\ViewDataTable;
+use Piwik\Url;
+use Piwik\View;
+
+/**
+ * Reads the requested DataTable from the API and prepares data for the Sparklines view. It can display any amount
+ * of sparklines. Within a reporting page sparklines are shown in 2 columns, in a dashboard or when exported as a widget
+ * the sparklines are shown in one column.
+ *
+ * The sparklines view currently only supports requesting columns from the same API (the API method of the defining
+ * report) via {Sparklines\Config::addSparklineMetric($columns = array('nb_visits', 'nb_unique_visitors'))}.
+ *
+ * Example:
+ * $view->config->addSparklineMetric('nb_visits'); // if an array of metrics given, they will be displayed comma separated
+ * $view->config->addTranslation('nb_visits', 'Visits');
+ * Results in: [sparkline image] X visits
+ * Data is fetched from the configured $view->requestConfig->apiMethodToRequestDataTable.
+ *
+ * In case you want to add any custom sparklines from any other API method you can call
+ * {@link Sparklines\Config::addSparkline()}.
+ *
+ * Example:
+ * $sparklineUrlParams = array('columns' => array('nb_visits));
+ * $evolution = array('currentValue' => 5, 'pastValue' => 10, 'tooltip' => 'Foo bar');
+ * $view->config->addSparkline($sparklineUrlParams, $value = 5, $description = 'Visits', $evolution);
+ *
+ * @property Sparklines\Config $config
+ */
+class Sparklines extends ViewDataTable
+{
+ const ID = 'sparklines';
+
+ public static function getDefaultConfig()
+ {
+ return new Sparklines\Config();
+ }
+
+ /**
+ * @see ViewDataTable::main()
+ * @return mixed
+ */
+ public function render()
+ {
+ $view = new View('@CoreVisualizations/_dataTableViz_sparklines.twig');
+
+ $columnsList = array();
+ if ($this->config->hasSparklineMetrics()) {
+ foreach ($this->config->getSparklineMetrics() as $cols) {
+ $columnsList = array_merge($cols['columns'], $columnsList);
+ }
+ }
+
+ $this->requestConfig->request_parameters_to_modify['columns'] = $columnsList;
+ $this->requestConfig->request_parameters_to_modify['format_metrics'] = '1';
+
+ if (!empty($this->requestConfig->apiMethodToRequestDataTable)) {
+ $this->fetchConfiguredSparklines();
+ }
+
+ $view->sparklines = $this->config->getSortedSparklines();
+
+ return $view->render();
+ }
+
+ private function fetchConfiguredSparklines()
+ {
+ $data = $this->loadDataTableFromAPI();
+
+ $this->applyFilters($data);
+
+ if (!$this->config->hasSparklineMetrics()) {
+ foreach ($data->getColumns() as $column) {
+ $this->config->addSparklineMetric($column);
+ }
+ }
+
+ $translations = $this->config->translations;
+
+ $firstRow = $data->getFirstRow();
+
+ foreach ($this->config->getSparklineMetrics() as $sparklineMetric) {
+ $column = $sparklineMetric['columns'];
+ $order = $sparklineMetric['order'];
+
+ if ($column === 'label') {
+ continue;
+ }
+
+ if (empty($column)) {
+ $this->config->addPlaceholder($order);
+ continue;
+ }
+
+ if (!is_array($column)) {
+ $column = array($column);
+ }
+
+ $values = array();
+ $descriptions = array();
+
+ foreach ($column as $col) {
+ $value = $firstRow->getColumn($col);
+
+ if ($value === false) {
+ $value = 0;
+ }
+
+ $values[] = $value;
+ $descriptions[] = isset($translations[$col]) ? $translations[$col] : $col;
+ }
+
+ $sparklineUrlParams = array(
+ 'columns' => $column,
+ 'module' => $this->requestConfig->getApiModuleToRequest(),
+ 'action' => $this->requestConfig->getApiMethodToRequest()
+ );
+
+ $this->config->addSparkline($sparklineUrlParams, $values, $descriptions, null, $order);
+ }
+ }
+
+ private function applyFilters(DataTable\DataTableInterface $table)
+ {
+ foreach ($this->config->getPriorityFilters() as $filter) {
+ $table->filter($filter[0], $filter[1]);
+ }
+
+ // queue other filters so they can be applied later if queued filters are disabled
+ foreach ($this->config->getPresentationFilters() as $filter) {
+ $table->queueFilter($filter[0], $filter[1]);
+ }
+
+ $table->applyQueuedFilters();
+ }
+}
diff --git a/plugins/CoreVisualizations/Visualizations/Sparklines/Config.php b/plugins/CoreVisualizations/Visualizations/Sparklines/Config.php
new file mode 100644
index 0000000000..ca54a6d564
--- /dev/null
+++ b/plugins/CoreVisualizations/Visualizations/Sparklines/Config.php
@@ -0,0 +1,354 @@
+<?php
+/**
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ *
+ */
+
+namespace Piwik\Plugins\CoreVisualizations\Visualizations\Sparklines;
+use Piwik\Common;
+use Piwik\DataTable\Filter\CalculateEvolutionFilter;
+use Piwik\Metrics;
+use Piwik\NoAccessException;
+use Piwik\Period\Range;
+use Piwik\Site;
+use Piwik\Url;
+
+/**
+ * DataTable Visualization that derives from Sparklines.
+ */
+class Config extends \Piwik\ViewDataTable\Config
+{
+ /**
+ * Holds metrics / column names that will be used to fetch data from the configured $requestConfig API.
+ * Default value: array
+ */
+ private $sparkline_metrics = array();
+
+ /**
+ * Holds the actual sparkline entries based on fetched data that will be used in the template.
+ * @var array
+ */
+ private $sparklines = array();
+
+ public function __construct()
+ {
+ parent::__construct();
+
+ $this->translations = Metrics::getDefaultMetricTranslations();
+ }
+
+ /**
+ * @ignore
+ * @return array
+ */
+ public function getSparklineMetrics()
+ {
+ return $this->sparkline_metrics;
+ }
+
+ /**
+ * @ignore
+ * @return bool
+ */
+ public function hasSparklineMetrics()
+ {
+ return !empty($this->sparkline_metrics);
+ }
+
+ /**
+ * Removes an existing sparkline entry. Especially useful in dataTable filters in case sparklines should be not
+ * displayed depending on the fetched data.
+ *
+ * Example:
+ * $config->addSparklineMetric('nb_users');
+ * $config->filters[] = function ($dataTable) use ($config) {
+ * if ($dataTable->getFirstRow()->getColumn('nb_users') == 0) {
+ * // do not show a sparkline if there are no recorded users
+ * $config->removeSparklineMetric('nb_users');
+ * }
+ * }
+ *
+ * @param array|string $metricNames The name of the metrics in the same format they were used when added via
+ * {@link addSparklineMetric}
+ */
+ public function removeSparklineMetric($metricNames)
+ {
+ foreach ($this->sparkline_metrics as $index => $metric) {
+ if ($metric['columns'] === $metricNames) {
+ array_splice($this->sparkline_metrics, $index, 1);
+
+ break;
+ }
+ }
+ }
+
+ /**
+ * Replaces an existing sparkline entry with different columns. Especially useful in dataTable filters in case
+ * sparklines should be not displayed depending on the fetched data.
+ *
+ * Example:
+ * $config->addSparklineMetric('nb_users');
+ * $config->filters[] = function ($dataTable) use ($config) {
+ * if ($dataTable->getFirstRow()->getColumn('nb_users') == 0) {
+ * // instead of showing the sparklines for users, show a placeholder if there are no recorded users
+ * $config->replaceSparklineMetric(array('nb_users'), '');
+ * }
+ * }
+ *
+ * @param array|string $metricNames The name of the metrics in the same format they were used when added via
+ * {@link addSparklineMetric}
+ * @param array|string $replacementColumns The removed columns will be replaced with these columns
+ */
+ public function replaceSparklineMetric($metricNames, $replacementColumns)
+ {
+ foreach ($this->sparkline_metrics as $index => $metric) {
+ if ($metric['columns'] === $metricNames) {
+ $this->sparkline_metrics[$index]['columns'] = $replacementColumns;
+ }
+ }
+ }
+
+ /**
+ * Adds a new sparkline.
+ *
+ * It will show a sparkline image, the value of the resolved metric name and a descrption. Optionally, multiple
+ * values can be shown after a sparkline image by passing multiple metric names
+ * (eg array('nb_visits', 'nb_actions')). The data will be requested from the configured api method see
+ * {@link Piwik\ViewDataTable\RequestConfig::$apiMethodToRequestDataTable}.
+ *
+ * Example:
+ * $config->addSparklineMetric('nb_visits');
+ * $config->addTranslation('nb_visits', 'Visits');
+ * Results in: [sparkline image] X visits
+ *
+ * Example:
+ * $config->addSparklineMetric(array('nb_visits', 'nb_actions'));
+ * $config->addTranslations(array('nb_visits' => 'Visits', 'nb_actions' => 'Actions'));
+ * Results in: [sparkline image] X visits, Y actions
+ *
+ * @param string|array $metricName Either one metric name (eg 'nb_visits') or an array of metric names
+ * @param int|null $order Defines the order. The lower the order the earlier the sparkline will be displayed.
+ * By default the sparkline will be appended to the end.
+ */
+ public function addSparklineMetric($metricName, $order = null)
+ {
+ $this->sparkline_metrics[] = array(
+ 'columns' => $metricName,
+ 'order' => $order
+ );
+ }
+
+ /**
+ * Adds a placeholder. In this case nothing will be shown, neither a sparkline nor any description. This can be
+ * useful if you want to have some kind of separator. Eg if you want to have a sparkline on the left side but
+ * not sparkline on the right side.
+ *
+ * @param int|null $order Defines the order. The lower the order the earlier the sparkline will be displayed.
+ * By default the sparkline will be appended to the end.
+ */
+ public function addPlaceholder($order = null)
+ {
+ $this->sparklines[] = array(
+ 'url' => '',
+ 'metrics' => array(),
+ 'order' => $this->getSparklineOrder($order)
+ );
+ }
+
+ /**
+ * Add a new sparkline to be displayed to the view.
+ *
+ * Each sparkline can consist of one or multiple metrics. One metric consists of a value and a description. By
+ * default the value is shown first, then the description. The description can optionally contain a '%s' in case
+ * the value shall be displayed within the description. If multiple metrics are given, they will be separated by
+ * a comma.
+ *
+ * @param array $requestParamsForSparkline You need to at least set a module / action eg
+ * array('columns' => array('nb_visit'), 'module' => '', 'action' => '')
+ * @param int|float|string|array $value Either the metric value or an array of values.
+ * @param string|array $description Either one description or an array of descriptions. If an array, both
+ * $value and $description need the same amount of array entries.
+ * $description[0] should be the description for $value[0].
+ * $description should be already translated. If $value should appear
+ * somewhere within the text a `%s` can be used in the translation.
+ * @param array|null $evolution Optional array containing at least the array keys 'currentValue' and
+ * 'pastValue' which are needed to calculate the correct percentage.
+ * An optional 'tooltip' can be set as well. Eg
+ * array('currentValue' => 10, 'pastValue' => 20,
+ * 'tooltip' => '10 visits in 2015-07-26 compared to 20 visits in 2015-07-25')
+ * @param int $order Defines the order. The lower the order the earlier the sparkline will be
+ * displayed. By default the sparkline will be appended to the end.
+ * @throws \Exception In case an evolution parameter is set but has wrong data structure
+ */
+ public function addSparkline($requestParamsForSparkline, $value, $description, $evolution = null, $order = null)
+ {
+ $metrics = array();
+
+ if (is_array($value)) {
+ $values = $value;
+ } else {
+ $values = array($value);
+ }
+
+ if (!is_array($description)) {
+ $description = array($description);
+ }
+
+ if (count($values) === count($description)) {
+ foreach ($values as $index => $value) {
+ $metrics[] = array(
+ 'value' => $value,
+ 'description' => $description[$index]
+ );
+ }
+ } else {
+ $msg = 'The number of values and descriptions need to be the same to add a sparkline. ';
+ $msg .= 'Values: ' . implode(', ', $values). ' Descriptions: ' . implode(', ', $description);
+ throw new \Exception($msg);
+ }
+
+ if (empty($metrics)) {
+ return;
+ }
+
+ $sparkline = array(
+ 'url' => $this->getUrlSparkline($requestParamsForSparkline),
+ 'metrics' => $metrics,
+ 'order' => $this->getSparklineOrder($order)
+ );
+
+ if (!empty($evolution)) {
+ if (!is_array($evolution) ||
+ !array_key_exists('currentValue', $evolution) ||
+ !array_key_exists('pastValue', $evolution)) {
+ throw new \Exception('In order to show an evolution in the sparklines view a currentValue and pastValue array key needs to be present');
+ }
+
+ $evolutionPercent = CalculateEvolutionFilter::calculate($evolution['currentValue'], $evolution['pastValue'], $precision = 1);
+
+ // do not display evolution if evolution percent is 0 and current value is 0
+ if ($evolutionPercent != 0 || $evolution['currentValue'] != 0) {
+ $sparkline['evolution'] = array(
+ 'percent' => $evolutionPercent,
+ 'tooltip' => !empty($evolution['tooltip']) ? $evolution['tooltip'] : null
+ );
+ }
+
+ }
+
+ $this->sparklines[] = $sparkline;
+ }
+
+ /**
+ * @return array
+ * @ignore
+ */
+ public function getSortedSparklines()
+ {
+ usort($this->sparklines, function ($a, $b) {
+ if ($a['order'] == $b['order']) {
+ return 0;
+ }
+ return ($a['order'] < $b['order']) ? -1 : 1;
+ });
+
+ return $this->sparklines;
+ }
+
+ private function getSparklineOrder($order)
+ {
+ if (!isset($order)) {
+ // make sure to append to the end if nothing set (in the order they are added)
+ $order = 999 + count($this->sparklines);
+ }
+
+ return (int) $order;
+ }
+
+ /**
+ * Returns a URL to a sparkline image for a report served by the current plugin.
+ *
+ * The result of this URL should be used with the [sparkline()](/api-reference/Piwik/View#twig) twig function.
+ *
+ * The current site ID and period will be used.
+ *
+ * @param array $customParameters The array of query parameter name/value pairs that
+ * should be set in result URL.
+ * @return string The generated URL.
+ */
+ private function getUrlSparkline($customParameters = array())
+ {
+ $customParameters['viewDataTable'] = 'sparkline';
+
+ $params = $this->getGraphParamsModified($customParameters);
+
+ // convert array values to comma separated
+ foreach ($params as &$value) {
+ if (is_array($value)) {
+ $value = rawurlencode(implode(',', $value));
+ }
+ }
+ $url = Url::getCurrentQueryStringWithParametersModified($params);
+ return $url;
+ }
+
+ /**
+ * Returns the array of new processed parameters once the parameters are applied.
+ * For example: if you set range=last30 and date=2008-03-10,
+ * the date element of the returned array will be "2008-02-10,2008-03-10"
+ *
+ * Parameters you can set:
+ * - range: last30, previous10, etc.
+ * - date: YYYY-MM-DD, today, yesterday
+ * - period: day, week, month, year
+ *
+ * @param array $paramsToSet array( 'date' => 'last50', 'viewDataTable' =>'sparkline' )
+ * @throws \Piwik\NoAccessException
+ * @return array
+ */
+ private function getGraphParamsModified($paramsToSet = array())
+ {
+ if (!isset($paramsToSet['period'])) {
+ $period = Common::getRequestVar('period');
+ } else {
+ $period = $paramsToSet['period'];
+ }
+
+ if ($period == 'range') {
+ return $paramsToSet;
+ }
+
+ if (!isset($paramsToSet['range'])) {
+ $range = 'last30';
+ } else {
+ $range = $paramsToSet['range'];
+ }
+
+ if (!isset($paramsToSet['idSite'])) {
+ $idSite = Common::getRequestVar('idSite');
+ } else {
+ $idSite = $paramsToSet['idSite'];
+ }
+
+ if (!isset($paramsToSet['date'])) {
+ $endDate = Common::getRequestVar('date', 'yesterday', 'string');
+ } else {
+ $endDate = $paramsToSet['date'];
+ }
+
+ $site = new Site($idSite);
+
+ if (is_null($site)) {
+ throw new NoAccessException("Website not initialized, check that you are logged in and/or using the correct token_auth.");
+ }
+
+ $paramDate = Range::getRelativeToEndDate($period, $range, $endDate, $site);
+
+ $params = array_merge($paramsToSet, array('date' => $paramDate));
+ return $params;
+ }
+
+}
diff --git a/plugins/CoreVisualizations/javascripts/jqplotEvolutionGraph.js b/plugins/CoreVisualizations/javascripts/jqplotEvolutionGraph.js
index 3b62e9197e..b77add4962 100644
--- a/plugins/CoreVisualizations/javascripts/jqplotEvolutionGraph.js
+++ b/plugins/CoreVisualizations/javascripts/jqplotEvolutionGraph.js
@@ -90,36 +90,7 @@
&& typeof self.jqplotParams.axes.xaxis.onclick[lastTick] == 'string') {
var url = self.jqplotParams.axes.xaxis.onclick[lastTick];
- if (url && -1 === url.indexOf('#')) {
- var module = broadcast.getValueFromHash('module');
- var action = broadcast.getValueFromHash('action');
- var idGoal = broadcast.getValueFromHash('idGoal');
- var idSite = broadcast.getValueFromUrl('idSite', url);
- var period = broadcast.getValueFromUrl('period', url);
- var date = broadcast.getValueFromUrl('date', url);
-
- if (module && action) {
- url += '#module=' + module + '&action=' + action;
-
- if (idSite) {
- url += '&idSite=' + idSite;
- }
-
- if (idGoal) {
- url += '&idGoal=' + idGoal;
- }
-
- if (period) {
- url += '&period=' + period;
- }
-
- if (period) {
- url += '&date=' + date;
- }
- }
- }
-
- piwikHelper.redirectToUrl(url);
+ broadcast.propagateNewPage(url);
}
})
.on('jqplotPiwikTickOver', function (e, tick) {
@@ -161,6 +132,10 @@
render: function () {
JqplotGraphDataTablePrototype.render.call(this);
+
+ if (initializeSparklines) {
+ initializeSparklines();
+ }
}
});
diff --git a/plugins/CoreVisualizations/templates/_dataTableViz_sparklines.twig b/plugins/CoreVisualizations/templates/_dataTableViz_sparklines.twig
new file mode 100644
index 0000000000..359c0f9768
--- /dev/null
+++ b/plugins/CoreVisualizations/templates/_dataTableViz_sparklines.twig
@@ -0,0 +1,31 @@
+{% import '@CoreVisualizations/macros.twig' as macros %}
+
+{% if not isWidget %}
+<div class="row">
+ <div class="col-md-6">
+{% endif %}
+
+ {% for key, sparkline in sparklines %}
+ {% if key is even %}
+ {{ macros.singleSparkline(sparkline) }}
+ {% endif %}
+ {% endfor %}
+
+{% if not isWidget %}
+ </div>
+ <div class="col-md-6">
+{% endif %}
+
+ {% for key, sparkline in sparklines %}
+ {% if key is odd %}
+ {{ macros.singleSparkline(sparkline) }}
+ {% endif %}
+ {% endfor %}
+
+{% if not isWidget %}
+ </div>
+</div>
+{% endif %}
+
+{% include "_sparklineFooter.twig" %}
+
diff --git a/plugins/CoreVisualizations/templates/macros.twig b/plugins/CoreVisualizations/templates/macros.twig
new file mode 100644
index 0000000000..ffd1885be1
--- /dev/null
+++ b/plugins/CoreVisualizations/templates/macros.twig
@@ -0,0 +1,32 @@
+{% macro singleSparkline(sparkline) %}
+ <div class="sparkline">
+ {% if sparkline.url %}{{ sparkline(sparkline.url)|raw }}{% endif %}
+ {% for metric in sparkline.metrics %}
+ {% if '%s' in metric.description -%}
+ {{ metric.description|translate("<strong>"~metric.value~"</strong>")|raw }}
+ {%- else %}
+ <strong>{{ metric.value }}</strong> {{ metric.description }}
+ {%- endif %}{% if not loop.last %}, {% endif %}
+ {% endfor %}
+ {% if sparkline.evolution is defined %}
+
+ {% set evolutionPretty = sparkline.evolution.percent %}
+
+ {% if sparkline.evolution.percent < 0 %}
+ {% set evolutionClass = 'negative-evolution' %}
+ {% set evolutionIcon = 'arrow_down.png' %}
+ {% elseif sparkline.evolution.percent == 0 %}
+ {% set evolutionClass = 'neutral-evolution' %}
+ {% set evolutionIcon = 'stop.png' %}
+ {% else %}
+ {% set evolutionClass = 'positive-evolution' %}
+ {% set evolutionIcon = 'arrow_up.png' %}
+ {% set evolutionPretty = '+' ~ sparkline.evolution.percent %}
+ {% endif %}
+
+ <span class="metricEvolution" title="{{ sparkline.evolution.tooltip }}"><img
+ style="padding-right:4px" src="plugins/MultiSites/images/{{ evolutionIcon }}"/>
+ <strong class="{{ evolutionClass }}">{{ evolutionPretty }}</strong></span>
+ {% endif %}
+ </div>
+{% endmacro %}
diff --git a/plugins/CoreVisualizations/tests/Integration/SparklinesConfigTest.php b/plugins/CoreVisualizations/tests/Integration/SparklinesConfigTest.php
new file mode 100644
index 0000000000..3541344f04
--- /dev/null
+++ b/plugins/CoreVisualizations/tests/Integration/SparklinesConfigTest.php
@@ -0,0 +1,128 @@
+<?php
+/**
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+
+namespace Piwik\Plugins\CoreVisualizations\tests\Integration;
+
+use Piwik\Plugins\CoreVisualizations\Visualizations\Sparklines\Config;
+use Piwik\Tests\Framework\Fixture;
+use Piwik\Tests\Framework\Mock\FakeAccess;
+use Piwik\Tests\Framework\TestCase\IntegrationTestCase;
+
+/**
+ * @group CoreVisualizations
+ * @group SparklinesConfigTest
+ * @group Plugins
+ */
+class SparklinesConfigTest extends IntegrationTestCase
+{
+ /**
+ * @var Config
+ */
+ private $config;
+
+ public function setUp()
+ {
+ parent::setUp();
+ FakeAccess::$superUser = true;
+
+ if (!Fixture::siteCreated(1)) {
+ Fixture::createWebsite('2014-01-01 00:00:00');
+ }
+
+ $this->config = new Config();
+ }
+
+ public function test_addSparkline_shouldAddAMinimalSparklineWithOneValueAndUseDefaultOrder()
+ {
+ $this->config->addSparkline($this->sparklineParams(), $value = 10, $description = 'Visits');
+
+ $expectedSparkline = array(
+ 'url' => '?period=day&date=2012-03-06,2012-04-04&idSite=1&module=CoreHome&action=renderMe&viewDataTable=sparkline',
+ 'metrics' => array (
+ array ('value' => 10, 'description' => 'Visits'),
+ ),
+ 'order' => 999
+ );
+
+ $this->assertSame(array($expectedSparkline), $this->config->getSortedSparklines());
+ }
+
+ public function test_addSparkline_shouldAddSparklineWithMultipleValues()
+ {
+ $this->config->addSparkline($this->sparklineParams(), $values = array(10, 20), $description = array('Visits', 'Actions'));
+
+ $sparklines = $this->config->getSortedSparklines();
+
+ $this->assertSame(array (
+ array ('value' => 10, 'description' => 'Visits'),
+ array ('value' => 20, 'description' => 'Actions'),
+ ), $sparklines[0]['metrics']);
+ }
+
+ /**
+ * @expectedException \Exception
+ * @expectedExceptionMessage Values: 10, 20, 30 Descriptions: Visits, Actions
+ */
+ public function test_addSparkline_shouldThrowAnException_IfValuesDoesNotMatchAmountOfDescriptions()
+ {
+ $this->config->addSparkline($this->sparklineParams(), $values = array(10, 20, 30), $description = array('Visits', 'Actions'));
+ }
+
+ public function test_addSparkline_shouldAddEvolution()
+ {
+ $evolution = array('currentValue' => 10, 'pastValue' => 21,
+ 'tooltip' => '1 visit compared to 2 visits');
+ $this->config->addSparkline($this->sparklineParams(), $value = 10, $description = 'Visits', $evolution);
+
+ $sparklines = $this->config->getSortedSparklines();
+
+ $this->assertSame(array (
+ 'percent' => '-52.4%',
+ 'tooltip' => '1 visit compared to 2 visits'
+ ), $sparklines[0]['evolution']);
+ }
+
+ public function test_addSparkline_shouldAddOrder()
+ {
+ $this->config->addSparkline($this->sparklineParams(), $value = 10, $description = 'Visits', $evolution = null, $order = '42');
+
+ $sparklines = $this->config->getSortedSparklines();
+
+ $this->assertSame(42, $sparklines[0]['order']);
+ }
+
+ public function test_addSparkline_shouldBeAbleToBuildSparklineUrlBasedOnGETparams()
+ {
+ $oldGet = $_GET;
+ $_GET = $this->sparklineParams();
+ $this->config->addSparkline(array('columns' => 'nb_visits'), $value = 10, $description = 'Visits');
+ $_GET = $oldGet;
+
+ $sparklines = $this->config->getSortedSparklines();
+
+ $this->assertSame('?columns=nb_visits&viewDataTable=sparkline&date=2012-03-06,2012-04-04', $sparklines[0]['url']);
+ }
+
+ private function sparklineParams($params = array())
+ {
+ $params['period'] = 'day';
+ $params['date'] = '2012-04-04';
+ $params['idSite'] = '1';
+ $params['module'] = 'CoreHome';
+ $params['action'] = 'renderMe';
+
+ return $params;
+ }
+
+ public function provideContainerConfig()
+ {
+ return array(
+ 'Piwik\Access' => new FakeAccess()
+ );
+ }
+}
diff --git a/plugins/CoreVisualizations/tests/Unit/SparklinesConfigTest.php b/plugins/CoreVisualizations/tests/Unit/SparklinesConfigTest.php
new file mode 100644
index 0000000000..516f19f8e1
--- /dev/null
+++ b/plugins/CoreVisualizations/tests/Unit/SparklinesConfigTest.php
@@ -0,0 +1,130 @@
+<?php
+/**
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+
+namespace Piwik\Plugins\CoreVisualizations\tests\Unit;
+use Piwik\Plugins\CoreVisualizations\Visualizations\Sparklines\Config;
+
+/**
+ * @group CoreVisualizations
+ * @group SparklinesConfigTest
+ * @group Sparklines
+ * @group Plugins
+ */
+class SparklinesConfigTest extends \PHPUnit_Framework_TestCase
+{
+ /**
+ * @var Config
+ */
+ private $config;
+
+ public function setUp()
+ {
+ $this->config = new Config();
+ }
+
+ public function test_hasSparklineMetrics_shouldNotHaveSparklineMetrics_ByDefault()
+ {
+ $this->assertFalse($this->config->hasSparklineMetrics());
+ }
+
+ public function test_hasSparklineMetrics_shouldHaveSparklineMetrics_IfAtLeastOneWasAdded()
+ {
+ $this->config->addSparklineMetric('nb_visits');
+
+ $this->assertTrue($this->config->hasSparklineMetrics());
+ }
+
+ public function test_getSparklineMetrics_shouldNotHaveSparklineMetrics_ByDefault()
+ {
+ $this->assertSame(array(), $this->config->getSparklineMetrics());
+ }
+
+ public function test_addSparklineMetric_getSparklineMetrics_shouldReturnAllAddedSparklineMetrics()
+ {
+ $this->addFewSparklines();
+
+ $this->assertSame(array(
+ array('columns' => 'nb_visits', 'order' => null),
+ array('columns' => 'nb_unique_visitors', 'order' => 99),
+ array('columns' => array('nb_downloads', 'nb_outlinks'), 'order' => null),
+ ), $this->config->getSparklineMetrics());
+ }
+
+ public function test_removeSparklineMetric_shouldRemoveMetric_IfOnlySingleMetricIsGiven()
+ {
+ $this->addFewSparklines();
+
+ $this->config->removeSparklineMetric('nb_unique_visitors');
+
+ $this->assertSame(array(
+ array('columns' => 'nb_visits', 'order' => null),
+ array('columns' => array('nb_downloads', 'nb_outlinks'), 'order' => null),
+ ), $this->config->getSparklineMetrics());
+ }
+
+ public function test_removeSparklineMetric_shouldRemoveMetric_IfMultipleMetricsAreGiven()
+ {
+ $this->addFewSparklines();
+
+ $this->config->removeSparklineMetric(array('nb_downloads', 'nb_outlinks'));
+
+ $this->assertSame(array(
+ array('columns' => 'nb_visits', 'order' => null),
+ array('columns' => 'nb_unique_visitors', 'order' => 99),
+ ), $this->config->getSparklineMetrics());
+ }
+
+ public function test_replaceSparklineMetric_shouldBeAbleToReplaceColumns_IfSingleMetricIsGiven()
+ {
+ $this->addFewSparklines();
+
+ $this->config->replaceSparklineMetric('nb_unique_visitors', '');
+
+ $this->assertSame(array(
+ array('columns' => 'nb_visits', 'order' => null),
+ array('columns' => '', 'order' => 99),
+ array('columns' => array('nb_downloads', 'nb_outlinks'), 'order' => null),
+ ), $this->config->getSparklineMetrics());
+ }
+
+ public function test_replaceSparklineMetric_shouldBeAbleToReplaceColumns_IfMultipleMetricsAreGiven()
+ {
+ $this->addFewSparklines();
+
+ $this->config->replaceSparklineMetric(array('nb_downloads', 'nb_outlinks'), '');
+
+ $this->assertSame(array(
+ array('columns' => 'nb_visits', 'order' => null),
+ array('columns' => 'nb_unique_visitors', 'order' => 99),
+ array('columns' => '', 'order' => null),
+ ), $this->config->getSparklineMetrics());
+ }
+
+ public function test_addPlaceholder_getSortedSparklines()
+ {
+ $this->config->addPlaceholder();
+ $this->config->addPlaceholder($order = 10);
+ $this->config->addPlaceholder();
+ $this->config->addPlaceholder($order = 3);
+
+ $this->assertSame(array(
+ array('url' => '', 'metrics' => array(), 'order' => 3),
+ array('url' => '', 'metrics' => array(), 'order' => 10),
+ array('url' => '', 'metrics' => array(), 'order' => 999),
+ array('url' => '', 'metrics' => array(), 'order' => 1001),
+ ), $this->config->getSortedSparklines());
+ }
+
+ private function addFewSparklines()
+ {
+ $this->config->addSparklineMetric('nb_visits');
+ $this->config->addSparklineMetric('nb_unique_visitors', 99);
+ $this->config->addSparklineMetric(array('nb_downloads', 'nb_outlinks'));
+ }
+
+}