/** * Piwik - free/libre analytics platform * * DataTable UI class for JqplotGraph. * * @link http://www.jqplot.com * @link http://piwik.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') { 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(); } // tigger the event jqplotPiwikTickOver when the mosue 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);