/**
* Matomo - free/libre analytics platform
*
* DataTable UI class for JqplotGraph.
*
* @link http://www.jqplot.com
* @link https://matomo.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*/
function rowEvolutionGetMetricNameFromRow(tr)
{
return $(tr).find('td [data-name]').text().trim();
}
(function ($, require) {
var exports = require('piwik/UI'),
DataTable = exports.DataTable,
dataTablePrototype = DataTable.prototype,
getLabelFontFamily = function () {
if (!window.piwik.jqplotLabelFont) {
window.piwik.jqplotLabelFont = $('
').hide().appendTo('body').css('font-family');
}
return window.piwik.jqplotLabelFont || 'Arial';
}
;
exports.getLabelFontFamily = getLabelFontFamily;
/**
* DataTable UI class for jqPlot graph datatable visualizations.
*
* @constructor
*/
exports.JqplotGraphDataTable = function (element) {
DataTable.call(this, element);
};
$.extend(exports.JqplotGraphDataTable.prototype, dataTablePrototype, {
/**
* Initializes this class.
*/
init: function () {
dataTablePrototype.init.call(this);
var graphElement = $('.piwik-graph', this.$element);
if (!graphElement.length) {
return;
}
this._lang = {
noData: _pk_translate('General_NoDataForGraph'),
exportTitle: _pk_translate('General_ExportAsImage'),
exportText: _pk_translate('General_SaveImageOnYourComputer'),
metricsToPlot: _pk_translate('General_MetricsToPlot'),
metricToPlot: _pk_translate('General_MetricToPlot'),
recordsToPlot: _pk_translate('General_RecordsToPlot')
};
// set a unique ID for the graph element (required by jqPlot)
this.targetDivId = this.workingDivId + 'Chart';
graphElement.attr('id', this.targetDivId);
try {
var graphData = JSON.parse(graphElement.attr('data-data'));
} catch (e) {
console.error('JSON.parse Error: "' + e + "\" in:\n" + graphElement.attr('data-data'));
return;
}
this.data = graphData.data;
this._setJqplotParameters(graphData.params);
if (this.props.display_percentage_in_tooltip) {
this._setTooltipPercentages();
}
this._bindEvents();
// add external series toggle if it should be added
if (this.props.external_series_toggle) {
this.addExternalSeriesToggle(
window[this.props.external_series_toggle], // get the function w/ string name
this.props.external_series_toggle_show_all == 1
);
}
// render the graph (setTimeout is required, otherwise the graph will not
// render initially)
var self = this;
setTimeout(function () { self.render(); }, 1);
},
_setJqplotParameters: function (params) {
defaultParams = {
grid: {
drawGridLines: false,
borderWidth: 0,
shadow: false
},
title: {
show: false
},
axesDefaults: {
pad: 1.0,
tickRenderer: $.jqplot.CanvasAxisTickRenderer,
tickOptions: {
showMark: false,
fontSize: '11px',
fontFamily: getLabelFontFamily()
},
rendererOptions: {
drawBaseline: false
}
},
axes: {
yaxis: {
tickOptions: {
formatString: '%s',
formatter: $.jqplot.NumberFormatter
}
}
}
};
this.jqplotParams = $.extend(true, {}, defaultParams, params);
this._setColors();
},
_setTooltipPercentages: function () {
this.tooltip = {percentages: []};
for (var seriesIdx = 0; seriesIdx != this.data.length; ++seriesIdx) {
var series = this.data[seriesIdx];
var sum = 0;
$.each(series, function(index, value) {
if ($.isArray(value) && value[1]) {
sum = sum + value[1];
} else if (!$.isArray(value)) {
sum = sum + value;
}
});
var percentages = this.tooltip.percentages[seriesIdx] = [];
for (var valueIdx = 0; valueIdx != series.length; ++valueIdx) {
var value = series[valueIdx];
if ($.isArray(value) && value[1]) {
value = value[1];
}
percentages[valueIdx] = sum > 0 ? Math.round(100 * value / sum) : 0;
}
}
},
_bindEvents: function () {
var self = this;
var target = $('#' + this.targetDivId);
// tooltip show/hide
target.on('jqplotDataHighlight', function (e, seriesIndex, valueIndex) {
self._showDataPointTooltip(this, seriesIndex, valueIndex);
})
.on('jqplotDataUnhighlight', function () {
self._destroyDataPointTooltip($(this));
});
// handle window resize
this._plotWidth = target.innerWidth();
target.on('resizeGraph', function () { // TODO: shouldn't be a triggerable event.
self._resizeGraph();
});
// export as image
target.on('piwikExportAsImage', function () {
self.exportAsImage(target, self._lang);
});
// manage resources
target.on('piwikDestroyPlot', function () {
if (self._resizeListener) {
$(window).off('resize', self._resizeListener);
}
self._plot.destroy();
for (var i = 0; i < $.jqplot.visiblePlots.length; i++) {
if ($.jqplot.visiblePlots[i] === self) {
$.jqplot.visiblePlots[i] = null;
}
}
});
this.$element.closest('.widgetContent').on('widget:resize', function () {
self._resizeGraph();
});
},
_resizeGraph: function () {
var width = $('#' + this.targetDivId).innerWidth();
if (width > 0 && Math.abs(this._plotWidth - width) >= 5) {
this._plotWidth = width;
this.render();
}
},
_setWindowResizeListener: function () {
var self = this;
var timeout = false;
this._resizeListener = function () {
if (timeout) {
window.clearTimeout(timeout);
}
timeout = window.setTimeout(function () { $('#' + self.targetDivId).trigger('resizeGraph'); }, 300);
};
$(window).on('resize', this._resizeListener);
},
_destroyDataPointTooltip: function ($element) {
if ($element.is( ":data('ui-tooltip')" )) {
$element.tooltip('destroy');
}
},
_showDataPointTooltip: function (element, seriesIndex, valueIndex) {
// empty
},
changeSeries: function (columns, rows) {
this.showLoading();
columns = columns || [];
if (typeof columns == 'string') {
columns = columns.split(',');
}
rows = rows || [];
if (typeof rows == 'string') {
rows = rows.split(',');
}
var dataTable = $('#' + this.workingDivId).data('uiControlObject');
dataTable.param.columns = columns.join(',');
dataTable.param.rows = rows.join(',');
delete dataTable.param.filter_limit;
delete dataTable.param.totalRows;
if (dataTable.param.filter_sort_column != 'label') {
dataTable.param.filter_sort_column = columns[0];
}
dataTable.param.disable_generic_filters = '0';
dataTable.reloadAjaxDataTable(false);
},
destroyPlot: function () {
var target = $('#' + this.targetDivId);
target.trigger('piwikDestroyPlot');
if (target.data('oldHeight') > 0) {
// handle replot after empty report
target.height(target.data('oldHeight'));
target.data('oldHeight', 0);
target.innerHTML = '';
}
},
showLoading: function () {
var target = $('#' + this.targetDivId);
var loading = $(document.createElement('div')).addClass('jqplot-loading');
loading.css({
width: target.innerWidth() + 'px',
height: target.innerHeight() + 'px',
opacity: 0
});
target.prepend(loading);
loading.css({opacity: .7});
},
/**
* This method sums up total width of all tick according to currently
* set font-family, font-size and font-weight. It is achieved by
* creating span elements with ticks and adding their width.
* Rendered ticks have to be visible to get their real width. But it
* is too fast for user to notice it. If total ticks width is bigger
* than container width then half of ticks is being cut out and their
* width is tested again. Until their total width is smaller than chart
* div. There is a failsafe so check will be performed no more than 20
* times, which is I think more than enough. Each tick have its own
* gutter, by default width of 5 px from each side so they are more
* readable.
*
* @param $targetDiv
* @private
*/
_checkTicksWidth: function($targetDiv){
if(typeof this.jqplotParams.axes.xaxis.ticksOriginal === 'undefined' || this.jqplotParams.axes.xaxis.ticksOriginal === {}){
this.jqplotParams.axes.xaxis.ticksOriginal = this.jqplotParams.axes.xaxis.ticks.slice();
}
var ticks = this.jqplotParams.axes.xaxis.ticks = this.jqplotParams.axes.xaxis.ticksOriginal.slice();
var divWidth = $targetDiv.width();
var tickOptions = $.extend(true, {}, this.jqplotParams.axesDefaults.tickOptions, this.jqplotParams.axes.xaxis.tickOptions);
var gutter = tickOptions.gutter || 5;
var sumWidthOfTicks = Number.MAX_VALUE;
var $labelTestChamber = {};
var tick = "";
var $body = $("body");
var maxRunsFailsafe = 20;
var ticksCount = 0;
var key = 0;
while(sumWidthOfTicks > divWidth && maxRunsFailsafe > 0) {
sumWidthOfTicks = 0;
for (key = 0; key < ticks.length; key++) {
tick = ticks[key];
if (tick !== " " && tick !== "") {
$labelTestChamber = $("", {
style: 'font-size: ' + (tickOptions.fontSize || '11px') + '; font-family: ' + (tickOptions.fontFamily || 'Arial, Helvetica, sans-serif') + ';' + (tickOptions.fontWeight || 'normal') + ';' + 'clear: both; float: none;',
text: tick
}).appendTo($body);
sumWidthOfTicks += ($labelTestChamber.width() + gutter*2);
$labelTestChamber.remove();
}
}
ticksCount = 0;
if (sumWidthOfTicks > divWidth) {
for (key = 0; key < ticks.length; key++) {
tick = ticks[key];
if (tick !== " " && tick !== "") {
if (ticksCount % 2 == 1) {
ticks[key] = " ";
}
ticksCount++;
}
}
}
maxRunsFailsafe--;
}
},
/** Generic render function */
render: function () {
if (this.data.length == 0) { // sanity check
return;
}
var targetDivId = this.workingDivId + 'Chart';
var lang = this._lang;
var dataTableDiv = $('#' + this.workingDivId);
// if the plot has already been rendered, get rid of the existing plot
var target = $('#' + targetDivId);
if (target.find('canvas').length > 0) {
this.destroyPlot();
}
// handle replot
// this has be bound before the check for an empty graph.
// otherwise clicking on sparklines won't work anymore after an empty
// report has been displayed.
var self = this;
// before drawing a jqplot chart, check if all labels ticks will fit
// into it
if( this.param.viewDataTable === "graphBar"
|| this.param.viewDataTable === "graphVerticalBar"
|| this.param.viewDataTable === "graphEvolution" ) {
self._checkTicksWidth(target);
}
// create jqplot chart
try {
var plot = self._plot = $.jqplot(targetDivId, this.data, this.jqplotParams);
} catch (e) {
// this is thrown when refreshing piwik in the browser
if (e != "No plot target specified") {
throw e;
}
}
self._setWindowResizeListener();
var self = this;
// TODO: this code destroys plots when a page is switched. there must be a better way of managing memory.
if (typeof $.jqplot.visiblePlots == 'undefined') {
$.jqplot.visiblePlots = [];
var $rootScope = piwikHelper.getAngularDependency('$rootScope');
$rootScope.$on('piwikPageChange', function () {
for (var i = 0; i < $.jqplot.visiblePlots.length; i++) {
if ($.jqplot.visiblePlots[i] == null) {
continue;
}
$.jqplot.visiblePlots[i].destroyPlot();
}
$.jqplot.visiblePlots = [];
});
}
if (typeof plot != 'undefined') {
$.jqplot.visiblePlots.push(self);
}
},
/** Export the chart as an image */
exportAsImage: function (container, lang) {
var pixelRatio = window.devicePixelRatio || 1;
var exportCanvas = document.createElement('canvas');
exportCanvas.width = Math.round(container.width() * pixelRatio);
exportCanvas.height = Math.round(container.height() * pixelRatio);
if (!exportCanvas.getContext) {
alert("Sorry, not supported in your browser. Please upgrade your browser :)");
return;
}
var exportCtx = exportCanvas.getContext('2d');
var canvases = container.find('canvas');
for (var i = 0; i < canvases.length; i++) {
var canvas = canvases.eq(i);
var position = canvas.position();
var parent = canvas.parent();
if (parent.hasClass('jqplot-axis')) {
var addPosition = parent.position();
position.left += addPosition.left;
position.top += addPosition.top + parseInt(parent.css('marginTop'), 10);
}
exportCtx.drawImage(canvas[0], Math.round(position.left * pixelRatio), Math.round(position.top * pixelRatio));
}
var exported = exportCanvas.toDataURL("image/png");
var img = document.createElement('img');
img.src = exported;
img = $(img).css({
width: Math.round(exportCanvas.width / pixelRatio) + 'px',
height: Math.round(exportCanvas.height / pixelRatio) + 'px'
});
var popover = $(document.createElement('div'));
popover.append(''
+ lang.exportText + '
').append($(img));
popover.dialog({
title: lang.exportTitle,
modal: true,
width: 'auto',
position: ['center', 'center'],
resizable: false,
autoOpen: true,
open: function (event, ui) {
$('.ui-widget-overlay').on('click.popover', function () {
popover.dialog('close');
});
},
close: function (event, ui) {
$(this).dialog("destroy").remove();
}
});
},
// ------------------------------------------------------------
// HELPER METHODS
// ------------------------------------------------------------
/** Generate ticks in y direction */
setYTicks: function () {
var $tempAxisElement = $('').attr('class', 'jqplot-axis jqplot-y2axis').css({'visibility': 'hidden', 'display': 'inline-block'});
$('')
.css('font-size', this.jqplotParams.axesDefaults.fontSize)
.css('font-family', this.jqplotParams.axesDefaults.fontFamily)
.appendTo($tempAxisElement);
$('body').append($tempAxisElement);
// default axis
this.setYTicksForAxis('yaxis', this.jqplotParams.axes.yaxis);
// other axes: y2axis, y3axis...
var axisLength = 10;
for (var i = 2; typeof this.jqplotParams.axes['y' + i + 'axis'] != 'undefined'; i++) {
this.setYTicksForAxis('y' + i + 'axis', this.jqplotParams.axes['y' + i + 'axis']);
axisLength += getAxisWidth(this.jqplotParams.axes['y' + i + 'axis']);
}
var axesShown = {};
this.jqplotParams.series.forEach(function (series) {
axesShown[series.yaxis] = true;
});
var hasMultipleAxes = Object.keys(axesShown).length > 1;
// only adjust width if more than one axis exists AND more than one series shown
if (hasMultipleAxes) {
$('.piwik-graph', this.$element).css('width', 'calc(100% - ' + axisLength + 'px)');
} else {
$('.piwik-graph', this.$element).css('width', '');
}
$tempAxisElement.remove();
function getAxisWidth(axis) {
var maxWidth = 0;
axis.ticks.forEach(function (tick) {
var tickFormatted = $.jqplot.NumberFormatter(axis.tickOptions.formatString || '%s', tick);
$tempAxisElement.find('span').text(tickFormatted);
maxWidth = Math.max(maxWidth, $tempAxisElement.width());
});
return maxWidth;
}
},
setYTicksForAxis: function (axisName, axis) {
// calculate maximum x value of all data sets
var maxCrossDataSets = 0;
for (var i = 0; i < this.data.length; i++) {
if (this.jqplotParams.series[i].yaxis == axisName) {
var maxValue = Math.max.apply(Math, this.data[i]);
if (maxValue > maxCrossDataSets) {
maxCrossDataSets = maxValue;
}
maxCrossDataSets = parseFloat(maxCrossDataSets);
}
}
// add little padding on top
maxCrossDataSets += Math.max(1, Math.round(maxCrossDataSets * .03));
// round to the nearest multiple of ten
if (maxCrossDataSets > 15) {
maxCrossDataSets = maxCrossDataSets + 10 - maxCrossDataSets % 10;
}
if (maxCrossDataSets == 0) {
maxCrossDataSets = 1;
}
// make sure percent axes don't go above 100%
if (axis.tickOptions.formatString.substring(2, 3) == '%' && maxCrossDataSets > 100) {
maxCrossDataSets = 100;
}
// calculate y-values for ticks
var ticks = [];
var numberOfTicks = 2;
var tickDistance = Math.ceil(maxCrossDataSets / numberOfTicks);
for (var i = 0; i <= numberOfTicks; i++) {
ticks.push(i * tickDistance);
}
axis.ticks = ticks;
},
/** Get a formatted y values (with unit) */
formatY: function (value, seriesIndex) {
var floatVal = parseFloat(value);
var intVal = parseInt(value, 10);
if (Math.abs(floatVal - intVal) >= 0.005) {
value = Math.round(floatVal * 100) / 100;
} else if (parseFloat(intVal) == floatVal) {
value = intVal;
} else {
value = floatVal;
}
var axisId = this.jqplotParams.series[seriesIndex].yaxis;
var formatString = this.jqplotParams.axes[axisId].tickOptions.formatString;
return $.jqplot.NumberFormatter(formatString, value);
},
/**
* Add an external series toggle.
* As opposed to addSeriesPicker, the external series toggle can only show/hide
* series that are already loaded.
*
* @param seriesPickerClass a subclass of JQPlotExternalSeriesToggle
* @param initiallyShowAll
*/
addExternalSeriesToggle: function (seriesPickerClass, initiallyShowAll) {
new seriesPickerClass(this.targetDivId, this, initiallyShowAll);
if (!initiallyShowAll) {
var initialMetrics = 0;
var $rowEvolution = $('#'+this.targetDivId).closest('.rowevolution');
var newData = [];
var newSeries = [];
if ($rowEvolution.data('initialMetrics')) {
initialMetrics = $rowEvolution.data('initialMetrics');
if (angular.isArray(initialMetrics)) {
for (var j = 0; j < initialMetrics.length; j++) {
// find index of series and data
for (var k = 0; k < this.jqplotParams.series.length; k++) {
if (this.jqplotParams.series[k]
&& this.jqplotParams.series[k].label
&& this.jqplotParams.series[k].label === initialMetrics[j]) {
newData.push(this.data[k]);
newSeries.push(this.jqplotParams.series[k]);
break;
}
}
}
}
}
if (newData.length) {
// restore original selection
this.data = newData;
this.jqplotParams.series = newSeries;
} else {
// initially, show only the first series
this.data = [this.data[0]];
this.jqplotParams.series = [this.jqplotParams.series[0]];
}
this.setYTicks();
}
},
/**
* Sets the colors used to render this graph.
*/
_setColors: function () {
var colorManager = piwik.ColorManager;
var viewDataTable = $('#' + this.workingDivId).data('uiControlObject').param['viewDataTable'];
var graphType = '';
if (viewDataTable == 'graphEvolution' || viewDataTable == 'graphStackedBarEvolution') {
graphType = 'evolution';
} else if (viewDataTable == 'graphPie') {
graphType = 'pie';
} else if (viewDataTable == 'graphVerticalBar') {
graphType = 'bar';
}
var namespace = graphType + '-graph-colors';
this._setSeriesColors(namespace);
this.jqplotParams.grid.background = colorManager.getColor(namespace, 'grid-background');
this.jqplotParams.grid.borderColor = colorManager.getColor(namespace, 'grid-border');
this.tickColor = colorManager.getColor(namespace, 'ticks');
this.singleMetricColor = colorManager.getColor(namespace, 'single-metric-label')
},
_setSeriesColors: function (namespace) {
var colorManager = piwik.ColorManager,
seriesColorNames = ['series0', 'series1', 'series2', 'series3', 'series4', 'series5',
'series6', 'series7', 'series8', 'series9', 'series10'];
var comparisonService = piwikHelper.getAngularDependency('piwikComparisonsService');
if (comparisonService.isComparing() && typeof this.jqplotParams.series[0].seriesIndex !== 'undefined') {
namespace = 'comparison-series-color';
seriesColorNames = [];
this.jqplotParams.series.forEach(function (s) {
var seriesColorName = comparisonService.getSeriesColorName(s.seriesIndex, s.metricIndex);
seriesColorNames.push(seriesColorName);
});
}
this.jqplotParams.seriesColors = colorManager.getColors(namespace, seriesColorNames, true);
}
});
DataTable.registerFooterIconHandler('graphPie', DataTable.switchToGraph);
DataTable.registerFooterIconHandler('graphVerticalBar', DataTable.switchToGraph);
DataTable.registerFooterIconHandler('graphEvolution', DataTable.switchToGraph);
})(jQuery, require);
// ----------------------------------------------------------------
// EXTERNAL SERIES TOGGLE
// Use external dom elements and their events to show/hide series
// ----------------------------------------------------------------
function JQPlotExternalSeriesToggle(targetDivId, jqplotObject, initiallyShowAll) {
this.init(targetDivId, originalConfig, initiallyShowAll);
}
JQPlotExternalSeriesToggle.prototype = {
init: function (targetDivId, jqplotObject, initiallyShowAll) {
this.targetDivId = targetDivId;
this.jqplotObject = jqplotObject;
this.originalData = jqplotObject.data;
this.originalSeries = jqplotObject.jqplotParams.series;
this.originalAxes = jqplotObject.jqplotParams.axes;
this.originalParams = jqplotObject.jqplotParams;
this.originalSeriesColors = jqplotObject.jqplotParams.seriesColors;
this.initiallyShowAll = initiallyShowAll;
this.activated = [];
this.target = $('#' + targetDivId);
this.attachEvents();
},
// can be overridden
attachEvents: function () {},
// show a single series
showSeries: function (i) {
this.activated = [i];
this.replot();
},
// toggle a series (make plotting multiple series possible)
toggleSeries: function (i) {
if (this.activated.indexOf(i) > -1) {
// need to remove the metric
if (this.activated.length > 1) {
// prevent removing the only visible metric
this.activated.splice(this.activated.indexOf(i), 1);
}
} else {
this.activated.push(i);
}
this.replot();
},
replot: function () {
this.beforeReplot();
// build new config and replot
var usedAxes = [];
var config = {data: this.originalData, params: this.originalParams};
config.data = [];
config.params.series = [];
config.params.axes = {xaxis: this.originalAxes.xaxis};
config.params.seriesColors = [];
for (var j = 0; j < this.activated.length; j++) {
// find index of series and data
for (var k = 0; k < this.originalSeries.length; k++) {
if (this.originalSeries[k]
&& this.originalSeries[k].label
&& this.originalSeries[k].label === this.activated[j]) {
config.data.push(this.originalData[k]);
config.params.seriesColors.push(this.originalSeriesColors[k]);
config.params.series.push($.extend(true, {}, this.originalSeries[k]));
// build array of used axes
var axis = this.originalSeries[k].yaxis;
if ($.inArray(axis, usedAxes) == -1) {
usedAxes.push(axis);
}
break;
}
}
}
// build new axes config
var replaceAxes = {};
for (j = 0; j < usedAxes.length; j++) {
var originalAxisName = usedAxes[j];
var newAxisName = (j == 0 ? 'yaxis' : 'y' + (j + 1) + 'axis');
replaceAxes[originalAxisName] = newAxisName;
config.params.axes[newAxisName] = this.originalAxes[originalAxisName];
}
// replace axis names in series config
for (j = 0; j < config.params.series.length; j++) {
var series = config.params.series[j];
series.yaxis = replaceAxes[series.yaxis];
}
this.jqplotObject.data = config.data;
this.jqplotObject.jqplotParams = config.params;
this.jqplotObject.setYTicks();
this.jqplotObject.render();
},
// can be overridden
beforeReplot: function () {}
};
// ROW EVOLUTION SERIES TOGGLE
function RowEvolutionSeriesToggle(targetDivId, jqplotData, initiallyShowAll) {
this.init(targetDivId, jqplotData, initiallyShowAll);
}
RowEvolutionSeriesToggle.prototype = JQPlotExternalSeriesToggle.prototype;
RowEvolutionSeriesToggle.prototype.attachEvents = function () {
var self = this;
var $rowEvolution = this.target.closest('.rowevolution');
this.seriesPickers = $rowEvolution.find('table.metrics tr');
var initialMetrics = [];
if ($rowEvolution.data('initialMetrics')) {
initialMetrics = [];
var savedMetrics = $rowEvolution.data('initialMetrics');
var existingMetricsInSeries = [];
var m = 0;
for (m = 0; m < this.originalSeries.length; m++) {
existingMetricsInSeries.push(this.originalSeries[m].label);
}
for (m = 0; m < savedMetrics.length; m++) {
if (existingMetricsInSeries.indexOf(savedMetrics[m]) > -1) {
// only if it exists... for example unique visitors etc might not be available for some metrics,
// then we need to make sure to highlight the default first metric for example
initialMetrics.push(savedMetrics[m]);
}
}
}
this.seriesPickers.each(function (i) {
var el = $(this);
el.off('click').on('click', function (e) {
var metricName = rowEvolutionGetMetricNameFromRow(this);
// we are storing this info on the element as the series picker and the jqplot object gets recreated whenever
// we change a period so we cannot persist the selection there.
if (e.shiftKey) {
self.toggleSeries(metricName);
document.getSelection().removeAllRanges(); // make sure chrome doesn't select text
} else {
self.showSeries(metricName);
}
$rowEvolution.data('initialMetrics', self.activated);
return false;
});
var label = rowEvolutionGetMetricNameFromRow(el);
var metricExists = false;
for (var k = 0; k < self.originalSeries.length; k++) {
if (self.originalSeries[k] && labelMatches(self.originalSeries[k].label, label)) {
metricExists = true;
}
}
if (!metricExists) {
el.hide();
} else if (
(initialMetrics.length === 0 && i == 0)
|| (initialMetrics.length > 0 && initialMetrics.indexOf(label) > -1)
|| self.initiallyShowAll) {
// show the active series
// if initiallyShowAll, all are active; otherwise only the first one
if (!el.hasClass('hiddenByDefault')) {
el.show();
}
el.find('td').css('opacity', '');
self.activated.push(rowEvolutionGetMetricNameFromRow(el));
} else {
if (!el.hasClass('hiddenByDefault')) {
el.show();
}
// fade out the others
el.find('td').css('opacity', .5);
}
// prevent selecting in ie & opera (they don't support doing this via css)
if ($.browser.msie) {
this.ondrag = function () { return false; };
this.onselectstart = function () { return false; };
} else if ($.browser.opera) {
$(this).attr('unselectable', 'on');
}
// the API outputs the label double encoded when it shouldn't. so when looking for a matching label we have
// to check if one is double encoded.
function labelMatches(lhs, rhs) {
return lhs === rhs || piwikHelper.htmlDecode(lhs) === rhs || lhs === piwikHelper.htmlDecode(rhs);
}
});
};
RowEvolutionSeriesToggle.prototype.beforeReplot = function () {
var self = this;
// fade out if not activated
this.seriesPickers.find('td').css('opacity', .5);
this.seriesPickers.each(function (i) {
var name = rowEvolutionGetMetricNameFromRow(this);
if (self.activated.indexOf(name) > -1) {
$(this).find('td').css('opacity', 1);
}
});
};
// ------------------------------------------------------------
// PIWIK NUMBERFORMATTER PLUGIN FOR JQPLOT
// ------------------------------------------------------------
(function($){
$.jqplot.NumberFormatter = function (format, value) {
if (!$.isNumeric(value)) {
return format.replace(/%s/, value);
}
return format.replace(/%s/, NumberFormatter.formatNumber(value));
}
})(jQuery);
// ------------------------------------------------------------
// PIWIK TICKS PLUGIN FOR JQPLOT
// Handle ticks the piwik way...
// ------------------------------------------------------------
(function ($) {
$.jqplot.PiwikTicks = function (options) {
// canvas for the grid
this.piwikTicksCanvas = null;
// canvas for the highlight
this.piwikHighlightCanvas = null;
// renderer used to draw the marker of the highlighted point
this.markerRenderer = new $.jqplot.MarkerRenderer({
shadow: false
});
// the x tick the mouse is over
this.currentXTick = false;
// show the highlight around markers
this.showHighlight = false;
// show the grid
this.showGrid = false;
// show the ticks
this.showTicks = false;
$.extend(true, this, options);
};
$.jqplot.PiwikTicks.init = function (target, data, opts) {
// add plugin as an attribute to the plot
var options = opts || {};
this.plugins.piwikTicks = new $.jqplot.PiwikTicks(options.piwikTicks);
if (typeof $.jqplot.PiwikTicks.init.eventsBound == 'undefined') {
$.jqplot.PiwikTicks.init.eventsBound = true;
$.jqplot.eventListenerHooks.push(['jqplotMouseMove', handleMouseMove]);
$.jqplot.eventListenerHooks.push(['jqplotMouseLeave', handleMouseLeave]);
}
};
// draw the grid
// called with context of plot
$.jqplot.PiwikTicks.postDraw = function () {
var c = this.plugins.piwikTicks;
// highligh canvas
if (c.showHighlight) {
c.piwikHighlightCanvas = new $.jqplot.GenericCanvas();
this.eventCanvas._elem.before(c.piwikHighlightCanvas.createElement(
this._gridPadding, 'jqplot-piwik-highlight-canvas', this._plotDimensions, this));
c.piwikHighlightCanvas.setContext();
}
// grid canvas
if (c.showTicks) {
var dimensions = this._plotDimensions;
dimensions.height += 6;
c.piwikTicksCanvas = new $.jqplot.GenericCanvas();
this.series[0].shadowCanvas._elem.before(c.piwikTicksCanvas.createElement(
this._gridPadding, 'jqplot-piwik-ticks-canvas', dimensions, this));
c.piwikTicksCanvas.setContext();
var ctx = c.piwikTicksCanvas._ctx;
var ticks = this.data[0];
var totalWidth = ctx.canvas.width;
var tickWidth = totalWidth / ticks.length;
var xaxisLabels = this.axes.xaxis.ticks;
for (var i = 0; i < ticks.length; i++) {
var pos = Math.round(i * tickWidth + tickWidth / 2);
var full = xaxisLabels[i] && xaxisLabels[i] != ' ';
drawLine(ctx, pos, full, c.showGrid, c.tickColor);
}
}
};
$.jqplot.preInitHooks.push($.jqplot.PiwikTicks.init);
$.jqplot.postDrawHooks.push($.jqplot.PiwikTicks.postDraw);
// draw a 1px line
function drawLine(ctx, x, full, showGrid, color) {
ctx.save();
ctx.strokeStyle = color;
ctx.beginPath();
ctx.lineWidth = 2;
var top = 0;
if ((full && !showGrid) || !full) {
top = ctx.canvas.height - 5;
}
ctx.moveTo(x, top);
ctx.lineTo(x, full ? ctx.canvas.height : ctx.canvas.height - 2);
ctx.stroke();
// canvas renders line slightly too large
ctx.clearRect(x, 0, x + 1, ctx.canvas.height);
ctx.restore();
}
// trigger the event jqplotPiwikTickOver when the mouse enters
// and new tick. this is used for tooltips.
function handleMouseMove(ev, gridpos, datapos, neighbor, plot) {
var c = plot.plugins.piwikTicks;
var tick = Math.floor(datapos.xaxis + 0.5) - 1;
if (tick !== c.currentXTick) {
c.currentXTick = tick;
plot.target.trigger('jqplotPiwikTickOver', [tick]);
highlight(plot, tick);
}
}
function handleMouseLeave(ev, gridpos, datapos, neighbor, plot) {
unHighlight(plot);
plot.plugins.piwikTicks.currentXTick = false;
}
// highlight a marker
function highlight(plot, tick) {
var c = plot.plugins.piwikTicks;
if (!c.showHighlight) {
return;
}
unHighlight(plot);
for (var i = 0; i < plot.series.length; i++) {
var series = plot.series[i];
var seriesMarkerRenderer = series.markerRenderer;
c.markerRenderer.style = seriesMarkerRenderer.style;
c.markerRenderer.size = seriesMarkerRenderer.size + 5;
var rgba = $.jqplot.getColorComponents(seriesMarkerRenderer.color);
var newrgb = [rgba[0], rgba[1], rgba[2]];
var alpha = rgba[3] * .4;
c.markerRenderer.color = 'rgba(' + newrgb[0] + ',' + newrgb[1] + ',' + newrgb[2] + ',' + alpha + ')';
c.markerRenderer.init();
var position = series.gridData[tick];
if (typeof position !== 'undefined') {
c.markerRenderer.draw(position[0], position[1], c.piwikHighlightCanvas._ctx);
}
}
}
function unHighlight(plot) {
var canvas = plot.plugins.piwikTicks.piwikHighlightCanvas;
if (canvas !== null) {
var ctx = canvas._ctx;
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
}
}
})(jQuery);
// ------------------------------------------------------------
// LEGEND PLUGIN FOR JQPLOT
// Render legend on canvas
// ------------------------------------------------------------
(function ($) {
$.jqplot.CanvasLegendRenderer = function (options) {
// canvas for the legend
this.legendCanvas = null;
// is it a legend for a single metric only (pie chart)?
this.singleMetric = false;
// render the legend?
this.show = false;
$.extend(true, this, options);
};
$.jqplot.CanvasLegendRenderer.init = function (target, data, opts) {
// add plugin as an attribute to the plot
var options = opts || {};
this.plugins.canvasLegend = new $.jqplot.CanvasLegendRenderer(options.canvasLegend);
// add padding above the grid
// legend will be put there
if (this.plugins.canvasLegend.show) {
options.gridPadding = {
top: 21, right: 0
};
}
};
// render the legend
$.jqplot.CanvasLegendRenderer.postDraw = function () {
var plot = this;
var legend = plot.plugins.canvasLegend;
if (!legend.show) {
return;
}
// initialize legend canvas
var padding = {top: 0, right: this._gridPadding.right, bottom: 0, left: this._gridPadding.left};
var dimensions = {width: this._plotDimensions.width, height: this._gridPadding.top};
var width = this._plotDimensions.width - this._gridPadding.left - this._gridPadding.right;
legend.legendCanvas = new $.jqplot.GenericCanvas();
this.eventCanvas._elem.before(legend.legendCanvas.createElement(
padding, 'jqplot-legend-canvas', dimensions, plot));
legend.legendCanvas.setContext();
var ctx = legend.legendCanvas._ctx;
ctx.save();
ctx.font = '11px ' + require('piwik/UI').getLabelFontFamily()
// render series names
var x = 0;
var series = plot.legend._series;
for (var i = 0; i < series.length; i++) {
var s = series[i];
var label;
if (legend.labels && legend.labels[i]) {
label = legend.labels[i];
} else {
label = s.label.toString();
}
ctx.fillStyle = s.color;
if (legend.singleMetric) {
ctx.fillStyle = legend.singleMetricColor;
}
ctx.fillRect(x, 10, 10, 2);
x += 15;
var nextX = x + ctx.measureText(label).width + 20;
if (nextX + 70 > width) {
ctx.fillText("[...]", x, 15);
x += ctx.measureText("[...]").width + 20;
break;
}
ctx.fillText(label, x, 15);
x = nextX;
}
legend.width = x;
ctx.restore();
};
$.jqplot.preInitHooks.push($.jqplot.CanvasLegendRenderer.init);
$.jqplot.postDrawHooks.push($.jqplot.CanvasLegendRenderer.postDraw);
})(jQuery);
// ------------------------------------------------------------
// SERIES PICKER
// ------------------------------------------------------------
(function ($, require) {
$.jqplot.preInitHooks.push(function (target, data, options) {
// create the series picker
var dataTable = $('#' + target).closest('.dataTable').data('uiControlObject');
if (!dataTable) { // if we're not dealing w/ a DataTable visualization, don't add the series picker
return;
}
var SeriesPicker = require('piwik/DataTableVisualizations/Widgets').SeriesPicker;
var seriesPicker = new SeriesPicker(dataTable);
// handle placeSeriesPicker event
var plot = this;
$(seriesPicker).bind('placeSeriesPicker', function () {
this.domElem.css('margin-left', plot._gridPadding.left + 'px');
$('.jqplot-legend-canvas').css({paddingLeft: '34px'});
plot.baseCanvas._elem.before(this.domElem);
});
// handle seriesPicked event
$(seriesPicker).bind('seriesPicked', function (e, columns, rows) {
dataTable.changeSeries(columns, rows);
});
this.plugins.seriesPicker = seriesPicker;
});
$.jqplot.postDrawHooks.push(function () {
this.plugins.seriesPicker.init();
});
})(jQuery, require);
// ------------------------------------------------------------
// PIE CHART LEGEND PLUGIN FOR JQPLOT
// Render legend inside the pie graph
// ------------------------------------------------------------
(function ($) {
$.jqplot.PieLegend = function (options) {
// canvas for the legend
this.pieLegendCanvas = null;
// render the legend?
this.show = false;
$.extend(true, this, options);
};
$.jqplot.PieLegend.init = function (target, data, opts) {
// add plugin as an attribute to the plot
var options = opts || {};
this.plugins.pieLegend = new $.jqplot.PieLegend(options.pieLegend);
};
// render the legend
$.jqplot.PieLegend.postDraw = function () {
var plot = this;
var legend = plot.plugins.pieLegend;
if (!legend.show) {
return;
}
var series = plot.series[0];
var angles = series._sliceAngles;
var radius = series._diameter / 2;
var center = series._center;
var colors = this.seriesColors;
// concentric line angles
var lineAngles = [];
for (var i = 0; i < angles.length; i++) {
lineAngles.push((angles[i][0] + angles[i][1]) / 2 + Math.PI / 2);
}
// labels
var labels = [];
var data = series._plotData;
for (i = 0; i < data.length; i++) {
labels.push(data[i][0]);
}
// initialize legend canvas
legend.pieLegendCanvas = new $.jqplot.GenericCanvas();
plot.series[0].canvas._elem.before(legend.pieLegendCanvas.createElement(
plot._gridPadding, 'jqplot-pie-legend-canvas', plot._plotDimensions, plot));
legend.pieLegendCanvas.setContext();
var ctx = legend.pieLegendCanvas._ctx;
ctx.save();
ctx.font = '11px ' + require('piwik/UI').getLabelFontFamily()
// render labels
var height = legend.pieLegendCanvas._elem.height();
var x1, x2, y1, y2, lastY2 = false, right, lastRight = false;
for (i = 0; i < labels.length; i++) {
var label = labels[i];
ctx.strokeStyle = colors[i % colors.length];
ctx.lineCap = 'round';
ctx.lineWidth = 1;
// concentric line
x1 = center[0] + Math.sin(lineAngles[i]) * (radius);
y1 = center[1] - Math.cos(lineAngles[i]) * (radius);
x2 = center[0] + Math.sin(lineAngles[i]) * (radius + 7);
y2 = center[1] - Math.cos(lineAngles[i]) * (radius + 7);
right = x2 > center[0];
// move close labels
if (lastY2 !== false && lastRight == right && (
(right && y2 - lastY2 < 13) ||
(!right && lastY2 - y2 < 13))) {
if (x1 > center[0]) {
// move down if the label is in the right half of the graph
y2 = lastY2 + 13;
} else {
// move up if in left halt
y2 = lastY2 - 13;
}
}
if (y2 < 4 || y2 + 4 > height) {
continue;
}
ctx.beginPath();
ctx.moveTo(x1, y1);
ctx.lineTo(x2, y2);
ctx.closePath();
ctx.stroke();
// horizontal line
ctx.beginPath();
ctx.moveTo(x2, y2);
if (right) {
ctx.lineTo(x2 + 5, y2);
} else {
ctx.lineTo(x2 - 5, y2);
}
ctx.closePath();
ctx.stroke();
lastY2 = y2;
lastRight = right;
// text
if (right) {
var x = x2 + 9;
} else {
var x = x2 - 9 - ctx.measureText(label).width;
}
ctx.fillStyle = legend.labelColor;
ctx.fillText(label, x, y2 + 3);
}
ctx.restore();
};
$.jqplot.preInitHooks.push($.jqplot.PieLegend.init);
$.jqplot.postDrawHooks.push($.jqplot.PieLegend.postDraw);
})(jQuery, require);