Download plugins/CoreVisualizations/javascripts/jqplot.js from Leon4gr45/matomo: direct link, hf CLI and curl.
- Browser
- Download file 83.4 kB
-
https://huggingface.co/spaces/Leon4gr45/matomo/resolve/main/plugins/CoreVisualizations/javascripts/jqplot.js
- Command line
-
hf download hf://spaces/Leon4gr45/matomo/plugins/CoreVisualizations/javascripts/jqplot.js
-
curl -L -o jqplot.js https://huggingface.co/spaces/Leon4gr45/matomo/resolve/main/plugins/CoreVisualizations/javascripts/jqplot.js
83.4 kB
| /** | |
| * Matomo - free/libre analytics platform | |
| * | |
| * DataTable UI class for JqplotGraph. | |
| * | |
| * @link http://www.jqplot.com | |
| * @link https://matomo.org | |
| * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later | |
| */ | |
| function rowEvolutionGetMetricNameFromRow(tr) | |
| { | |
| return $(tr).find('td [data-name]').text().trim(); | |
| } | |
| function getOrCreateLegendFooter($dataTable) | |
| { | |
| var $legendFooter = $dataTable.find('.jqplot-legend-footer'); | |
| if ($legendFooter.length) { | |
| return $legendFooter; | |
| } | |
| $legendFooter = $('<div class="jqplot-legend-footer"><div class="jqplot-legend-items"></div></div>'); | |
| var $dataTableFeatures = $dataTable.find('.dataTableFeatures'); | |
| var $footerNavigation = $dataTableFeatures.find('.dataTableFooterNavigation'); | |
| if ($footerNavigation.length) { | |
| $legendFooter.insertBefore($footerNavigation); | |
| } else if ($dataTableFeatures.length) { | |
| $dataTableFeatures.append($legendFooter); | |
| } else { | |
| $dataTable.append($legendFooter); | |
| } | |
| return $legendFooter; | |
| } | |
| // The legend footer stacks the legend above the "Choose metrics" picker once the report is | |
| // narrower than this. We can't use a CSS container query here — Matomo's LESS compiler | |
| // (less.php) can't parse @container — so we measure the footer width and toggle the | |
| // `is-narrow` class the stylesheet targets. This tracks the report's own width, so it also | |
| // kicks in when a report is shown as a narrow dashboard widget rather than full screen. | |
| var FOOTER_STACK_MAX_WIDTH = 321; | |
| function updateLegendFooterStacking($legendFooter) | |
| { | |
| if (!$legendFooter || !$legendFooter.length) { | |
| return; | |
| } | |
| // 0 when the footer isn't laid out yet (e.g. display:none); skip so we don't flip-flop | |
| var width = $legendFooter[0].clientWidth; | |
| if (width > 0) { | |
| $legendFooter.toggleClass('is-narrow', width < FOOTER_STACK_MAX_WIDTH); | |
| } | |
| } | |
| var MAX_FOOTER_LEGEND_ROWS = 2; | |
| var FOOTER_LEGEND_ROW_TOLERANCE = 1; | |
| var FOOTER_LEGEND_EXPORT_GRAPH_GAP = 12; | |
| function resetLegendItems($legendItems) | |
| { | |
| $legendItems | |
| .removeClass('jqplot-legend-item-hidden jqplot-legend-item-overflow') | |
| .each(function () { | |
| var $label = $(this).find('.jqplot-legend-label'); | |
| var originalLabel = $label.attr('data-original-label'); | |
| if (typeof originalLabel !== 'undefined') { | |
| $label.text(originalLabel); | |
| } | |
| }); | |
| } | |
| function getLegendRows($legendItems) | |
| { | |
| var rowTops = []; | |
| var rows = []; | |
| $legendItems.each(function () { | |
| var $item = $(this); | |
| var itemTop; | |
| var rowIndex = -1; | |
| if ($item.hasClass('jqplot-legend-item-hidden')) { | |
| return; | |
| } | |
| itemTop = Math.round($item.position().top); | |
| for (var i = 0; i < rowTops.length; i++) { | |
| if (Math.abs(rowTops[i] - itemTop) <= FOOTER_LEGEND_ROW_TOLERANCE) { | |
| rowIndex = i; | |
| break; | |
| } | |
| } | |
| if (rowIndex === -1) { | |
| rowTops.push(itemTop); | |
| rows.push([]); | |
| rowIndex = rows.length - 1; | |
| } | |
| rows[rowIndex].push(this); | |
| }); | |
| return rows; | |
| } | |
| function limitLegendRows($legendContainer, maxRows) | |
| { | |
| var $legendItems = $legendContainer.children('.jqplot-legend-item'); | |
| var rows; | |
| var overflowSource; | |
| var hideItems = false; | |
| resetLegendItems($legendItems); | |
| if (!maxRows || maxRows < 1) { | |
| return; | |
| } | |
| rows = getLegendRows($legendItems); | |
| // Left-align the legend (via CSS) once it wraps past a single row; a single row stays centered. | |
| $legendContainer.closest('.jqplot-legend-footer') | |
| .toggleClass('is-multi-row', rows.length >= 2); | |
| if (rows.length <= maxRows) { | |
| return; | |
| } | |
| // Reserve the last visible legend slot for the overflow marker so the | |
| // ellipsis stays within the allowed rows instead of landing on a hidden row. | |
| overflowSource = rows[maxRows - 1] && rows[maxRows - 1][rows[maxRows - 1].length - 1]; | |
| if (!overflowSource) { | |
| return; | |
| } | |
| $(overflowSource) | |
| .addClass('jqplot-legend-item-overflow') | |
| .find('.jqplot-legend-label') | |
| .text('…'); | |
| $legendItems.each(function () { | |
| if (this === overflowSource) { | |
| hideItems = true; | |
| return; | |
| } | |
| if (hideItems) { | |
| $(this).addClass('jqplot-legend-item-hidden'); | |
| } | |
| }); | |
| } | |
| function getLegendLabelTextForExport(ctx, labelElement, originalLabel, maxWidth) | |
| { | |
| var labelText = labelElement.textContent || ''; | |
| if (!originalLabel || maxWidth <= 0 || labelElement.scrollWidth <= labelElement.clientWidth) { | |
| return labelText; | |
| } | |
| var ellipsis = '…'; | |
| var ellipsisWidth = ctx.measureText(ellipsis).width; | |
| if (ellipsisWidth >= maxWidth) { | |
| return ellipsis; | |
| } | |
| var low = 0; | |
| var high = originalLabel.length; | |
| var bestFit = ''; | |
| while (low <= high) { | |
| var middle = Math.floor((low + high) / 2); | |
| var candidate = originalLabel.slice(0, middle) + ellipsis; | |
| if (ctx.measureText(candidate).width <= maxWidth) { | |
| bestFit = candidate; | |
| low = middle + 1; | |
| } else { | |
| high = middle - 1; | |
| } | |
| } | |
| return bestFit || ellipsis; | |
| } | |
| // Re-apply the footer legend row limit for a given dataTable. The "Choose metrics" | |
| // button shares the footer row with the legend items, so the legend's available | |
| // width is only final once the picker has been placed and its Vue button mounted. | |
| // This is called again after the picker is rendered (and on replot/resize) so the | |
| // items wrap against the real width and stay within MAX_FOOTER_LEGEND_ROWS. | |
| function applyFooterLegendRowLimit($dataTable) | |
| { | |
| var $legendContainer = $dataTable.find('.jqplot-legend-footer.has-legend .jqplot-legend-items'); | |
| if ($legendContainer.length) { | |
| limitLegendRows($legendContainer, MAX_FOOTER_LEGEND_ROWS); | |
| } | |
| } | |
| (function ($, require) { | |
| var exports = require('piwik/UI'), | |
| DataTable = exports.DataTable, | |
| dataTablePrototype = DataTable.prototype, | |
| getLabelFontFamily = function () { | |
| if (!window.piwik.jqplotLabelFont) { | |
| window.piwik.jqplotLabelFont = $('<p/>').hide().appendTo('body').css('font-family'); | |
| } | |
| return window.piwik.jqplotLabelFont || 'Arial'; | |
| } | |
| ; | |
| exports.getLabelFontFamily = getLabelFontFamily; | |
| function getPlotLinesSeriesColorNames() { | |
| var seriesColorNames = []; | |
| for (var seriesIndex = 0; seriesIndex < 8; seriesIndex++) { | |
| seriesColorNames.push('series' + seriesIndex); | |
| } | |
| for (var shade = 1; shade <= 3; shade++) { | |
| for (var shadedSeriesIndex = 0; shadedSeriesIndex < 8; shadedSeriesIndex++) { | |
| seriesColorNames.push('series' + shadedSeriesIndex + '-shade' + shade); | |
| } | |
| } | |
| return seriesColorNames; | |
| } | |
| /** | |
| * 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'), | |
| incompletePeriod: _pk_translate('General_IncompletePeriod'), | |
| invalidatedPeriod: _pk_translate('General_InvalidatedPeriod') | |
| }; | |
| // 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); | |
| this._setDataStates(graphData.dataStates); | |
| 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); | |
| }, | |
| _setDataStates: function (dataStates) { | |
| this.jqplotParams.dataStates = []; | |
| if (Array.isArray(dataStates)) { | |
| this.jqplotParams.dataStates = dataStates; | |
| } | |
| }, | |
| _setJqplotParameters: function (params) { | |
| defaultParams = { | |
| grid: { | |
| 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); | |
| for (var i = 2; typeof this.jqplotParams.axes['y' + i + 'axis'] != 'undefined'; i++) { | |
| this.jqplotParams.axes['y' + i + 'axis'].tickOptions = $.extend(true, {}, { | |
| formatString: '%s', | |
| formatter: $.jqplot.NumberFormatter | |
| }, this.jqplotParams.axes['y' + i + 'axis'].tickOptions); | |
| } | |
| 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(); | |
| }); | |
| this._themeModeChangeListener = function () { | |
| self.refreshTheme(); | |
| }; | |
| window.addEventListener('themeModeChange', this._themeModeChangeListener); | |
| }, | |
| _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); | |
| var dataTable = target.closest('.dataTable'); | |
| var legendFooter = dataTable.find('.jqplot-legend-footer'); | |
| // tear down the outgoing picker before its DOM is removed, so it can't leak | |
| if (this._plot && this._plot.plugins && this._plot.plugins.seriesPicker) { | |
| this._plot.plugins.seriesPicker.destroy(); | |
| } | |
| target.trigger('piwikDestroyPlot'); | |
| if (legendFooter.length) { | |
| legendFooter | |
| .removeClass('has-legend') | |
| .find('.jqplot-legend-items') | |
| .empty(); | |
| } | |
| if (target.data('oldHeight') > 0) { | |
| // handle replot after empty report | |
| target.height(target.data('oldHeight')); | |
| target.data('oldHeight', 0); | |
| target.innerHTML = ''; | |
| } | |
| }, | |
| _destroy: function () { | |
| if (this._themeModeChangeListener) { | |
| window.removeEventListener('themeModeChange', this._themeModeChangeListener); | |
| } | |
| if (this._plot) { | |
| this.destroyPlot(); | |
| } | |
| dataTablePrototype._destroy.call(this); | |
| }, | |
| showLoading: function () { | |
| var target = $('#' + this.targetDivId); | |
| var loading = $(document.createElement('div')).addClass('jqplot-loading'); | |
| loading.append('<span class="matomo-loader"><span></span><span></span><span></span></span>'); | |
| loading.css({ | |
| width: target.innerWidth() + 'px', | |
| height: target.innerHeight() + 'px', | |
| backgroundColor: (this.jqplotParams.grid && this.jqplotParams.grid.background) || '', | |
| 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 = $("<span/>", { | |
| 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 = []; | |
| window.CoreHome.Matomo.on('matomoPageChange', 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 dataTable = container.closest('.dataTable'); | |
| var legendFooter = dataTable.find('.jqplot-legend-footer.has-legend'); | |
| var hasFooterLegend = legendFooter.length > 0; | |
| var legendHeight = hasFooterLegend ? legendFooter[0].getBoundingClientRect().height : 0; | |
| var legendGraphGap = hasFooterLegend ? FOOTER_LEGEND_EXPORT_GRAPH_GAP : 0; | |
| var exportCanvas = document.createElement('canvas'); | |
| var exportWidth = container.outerWidth(); | |
| var dataTableWidth = dataTable.innerWidth(); | |
| if (dataTableWidth) { | |
| exportWidth = Math.max(exportWidth, dataTableWidth); | |
| } | |
| exportCanvas.width = Math.round(exportWidth * pixelRatio); | |
| exportCanvas.height = Math.round((container.height() + legendGraphGap + legendHeight) * pixelRatio); | |
| if (!exportCanvas.getContext) { | |
| alert("Sorry, not supported in your browser. Please upgrade your browser :)"); | |
| return; | |
| } | |
| var exportCtx = exportCanvas.getContext('2d'); | |
| exportCtx.fillStyle = (this.jqplotParams.grid && this.jqplotParams.grid.background) || '#ffffff'; | |
| exportCtx.fillRect(0, 0, exportCanvas.width, exportCanvas.height); | |
| 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)); | |
| } | |
| if (hasFooterLegend) { | |
| this.drawLegendForExport( | |
| exportCtx, | |
| legendFooter, | |
| exportWidth, | |
| container.height() + legendGraphGap, | |
| 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('<div style="font-size: 13px; margin-bottom: 10px;">' | |
| + lang.exportText + '</div>').append($(img)); | |
| popover.dialog({ | |
| title: lang.exportTitle, | |
| modal: true, | |
| width: 'auto', | |
| 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(); | |
| } | |
| }); | |
| }, | |
| // Draw the footer legend onto the export canvas by replaying its rendered DOM | |
| // geometry. This preserves browser-resolved item positioning, wrapping, | |
| // centering and hidden overflow state without maintaining a second legend | |
| // layout implementation for export. Text is redrawn onto canvas, so the | |
| // export follows the rendered label bounds but does not reproduce CSS text | |
| // overflow behaviour pixel-for-pixel. | |
| drawLegendForExport: function (ctx, legendFooter, exportWidth, topOffset, pixelRatio) { | |
| var footerRect = legendFooter[0].getBoundingClientRect(); | |
| var $items = legendFooter.find('.jqplot-legend-item').filter(function () { | |
| var $item = $(this); | |
| return !$item.hasClass('jqplot-legend-item-hidden') && $item.css('display') !== 'none'; | |
| }); | |
| if (!$items.length) { | |
| return; | |
| } | |
| // Center the legend on the rendered bounds of its items, not the whole | |
| // footer: the "Choose metrics" button shares the footer row but is an | |
| // interactive control that is not drawn into the exported image, so it | |
| // must not pull the legend off-centre. | |
| var contentLeft = Infinity; | |
| var contentRight = -Infinity; | |
| $items.each(function () { | |
| var rect = this.getBoundingClientRect(); | |
| contentLeft = Math.min(contentLeft, rect.left); | |
| contentRight = Math.max(contentRight, rect.right); | |
| }); | |
| var offsetX = Math.max(0, (exportWidth - (contentRight - contentLeft)) / 2); | |
| $items.each(function () { | |
| var $item = $(this); | |
| var $swatch = $item.find('.jqplot-legend-swatch'); | |
| var $label = $item.find('.jqplot-legend-label'); | |
| var labelElement = $label[0]; | |
| var originalLabel = $label.attr('data-original-label') || $label.text(); | |
| var labelText = $label.text(); | |
| if (!$swatch.length || !$label.length || !labelText) { | |
| return; | |
| } | |
| // swatch: a circle, positioned and coloured from its rendered box | |
| var swatchRect = $swatch[0].getBoundingClientRect(); | |
| ctx.save(); | |
| ctx.fillStyle = $swatch.css('background-color'); | |
| ctx.beginPath(); | |
| ctx.arc( | |
| (offsetX + (swatchRect.left - contentLeft) + swatchRect.width / 2) * pixelRatio, | |
| (topOffset + (swatchRect.top - footerRect.top) + swatchRect.height / 2) * pixelRatio, | |
| (Math.min(swatchRect.width, swatchRect.height) / 2) * pixelRatio, | |
| 0, | |
| Math.PI * 2 | |
| ); | |
| ctx.fill(); | |
| ctx.restore(); | |
| // label: drawn at its rendered box and clipped to it, so any CSS | |
| // truncation of a long label is preserved in the exported image | |
| var labelRect = $label[0].getBoundingClientRect(); | |
| var labelLeft = offsetX + (labelRect.left - contentLeft); | |
| var labelTop = topOffset + (labelRect.top - footerRect.top); | |
| var fontSize = parseFloat($label.css('font-size')) || 12; | |
| ctx.save(); | |
| ctx.beginPath(); | |
| ctx.rect( | |
| labelLeft * pixelRatio, | |
| labelTop * pixelRatio, | |
| labelRect.width * pixelRatio, | |
| labelRect.height * pixelRatio | |
| ); | |
| ctx.clip(); | |
| ctx.font = ($label.css('font-weight') || '400') + ' ' | |
| + Math.round(fontSize * pixelRatio) + 'px ' | |
| + ($label.css('font-family') || require('piwik/UI').getLabelFontFamily()); | |
| ctx.fillStyle = $label.css('color') || '#666666'; | |
| ctx.textBaseline = 'middle'; | |
| labelText = getLegendLabelTextForExport( | |
| ctx, | |
| labelElement, | |
| originalLabel, | |
| labelRect.width * pixelRatio | |
| ); | |
| ctx.fillText( | |
| labelText, | |
| labelLeft * pixelRatio, | |
| (labelTop + labelRect.height / 2) * pixelRatio | |
| ); | |
| ctx.restore(); | |
| }); | |
| }, | |
| // ------------------------------------------------------------ | |
| // HELPER METHODS | |
| // ------------------------------------------------------------ | |
| /** Generate ticks in y direction */ | |
| setYTicks: function () { | |
| // default axis | |
| this.setYTicksForAxis('yaxis', this.jqplotParams.axes.yaxis); | |
| // other axes: y2axis, y3axis... | |
| for (var i = 2; typeof this.jqplotParams.axes['y' + i + 'axis'] != 'undefined'; i++) { | |
| this.setYTicksForAxis('y' + i + 'axis', this.jqplotParams.axes['y' + i + 'axis']); | |
| } | |
| }, | |
| 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 | |
| && axis.tickOptions.formatString | |
| && axis.tickOptions.formatString.endsWith('%') | |
| && 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 (Array.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'); | |
| // Evolution and bar gridlines use a lighter tick color. | |
| if (graphType === 'evolution' || graphType === 'bar') { | |
| var TICK_OPACITY = 0.5; | |
| var tickRgb = colorManager.getRgb(this.tickColor); | |
| this.tickColor = 'rgba(' + tickRgb[0] + ', ' + tickRgb[1] + ', ' | |
| + tickRgb[2] + ', ' + TICK_OPACITY + ')'; | |
| // Keep gridlines and tick marks the same color. | |
| this.jqplotParams.grid.gridLineColor = this.tickColor; | |
| } | |
| this.singleMetricColor = colorManager.getColor(namespace, 'single-metric-label'); | |
| if (this.jqplotParams.pieLegend) { | |
| this.jqplotParams.pieLegend.labelColor = this.singleMetricColor; | |
| } | |
| if (this.jqplotParams.canvasLegend | |
| && this.jqplotParams.canvasLegend.singleMetric | |
| ) { | |
| this.jqplotParams.canvasLegend.singleMetricColor = this.singleMetricColor; | |
| } | |
| }, | |
| _setSeriesColors: function (namespace) { | |
| var colorManager = piwik.ColorManager, | |
| seriesColorNames; | |
| var comparisonService = window.CoreHome.ComparisonsStoreInstance; | |
| if (comparisonService.isComparing() && typeof this.jqplotParams.series[0].seriesIndex !== 'undefined') { | |
| // Use the shared comparison palette when comparing rows. | |
| namespace = 'comparison-series-color'; | |
| seriesColorNames = []; | |
| this.jqplotParams.series.forEach(function (s) { | |
| var seriesColorName = comparisonService.getSeriesColorName(s.seriesIndex, s.metricIndex); | |
| seriesColorNames.push(seriesColorName); | |
| }); | |
| } else if (namespace === 'evolution-graph-colors' | |
| || namespace === 'bar-graph-colors' | |
| || namespace === 'pie-graph-colors') { | |
| seriesColorNames = getPlotLinesSeriesColorNames(); | |
| } else { | |
| seriesColorNames = ['series0', 'series1', 'series2', 'series3', 'series4', 'series5', | |
| 'series6', 'series7', 'series8', 'series9', 'series10']; | |
| } | |
| this.jqplotParams.seriesColors = colorManager.getColors(namespace, seriesColorNames, true); | |
| }, | |
| refreshTheme: function () { | |
| if (!this.data || !this.data.length || !this.$element | |
| || !$.contains(document.documentElement, this.$element[0])) { | |
| return; | |
| } | |
| this._setColors(); | |
| this.render(); | |
| } | |
| }); | |
| 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] | |
| || piwikHelper.htmlDecode(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); | |
| } | |
| // 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 = 8; | |
| var rgba = $.jqplot.getColorComponents(seriesMarkerRenderer.color); | |
| var newrgb = [rgba[0], rgba[1], rgba[2]]; | |
| var alpha = rgba[3]; | |
| 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 below the graph | |
| // ------------------------------------------------------------ | |
| (function ($) { | |
| $.jqplot.CanvasLegendRenderer = function (options) { | |
| // 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); | |
| if (this.plugins.canvasLegend.show) { | |
| options.gridPadding = $.extend({}, options.gridPadding, { | |
| top: Math.max((options.gridPadding && options.gridPadding.top) || 0, 21) | |
| }); | |
| } | |
| }; | |
| // render the legend | |
| $.jqplot.CanvasLegendRenderer.postDraw = function () { | |
| var plot = this; | |
| var legend = plot.plugins.canvasLegend; | |
| if (!legend.show) { | |
| return; | |
| } | |
| var $target = $(plot.targetId); | |
| var $dataTable = $target.closest('.dataTable'); | |
| var $legendFooter = getOrCreateLegendFooter($dataTable); | |
| var $legendContainer = $legendFooter.find('.jqplot-legend-items'); | |
| if (!$legendContainer.length) { | |
| return; | |
| } | |
| $legendFooter.removeClass('has-legend'); | |
| $legendContainer.empty(); | |
| var series = plot.legend && plot.legend._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 if (typeof s.label !== 'undefined' && s.label !== null) { | |
| label = s.label.toString(); | |
| } | |
| if (!label) { | |
| continue; | |
| } | |
| var color = s.color; | |
| if (legend.singleMetric && legend.singleMetricColor) { | |
| color = legend.singleMetricColor; | |
| } | |
| $('<div/>', {'class': 'jqplot-legend-item'}) | |
| .append( | |
| $('<span/>', {'class': 'jqplot-legend-swatch'}).css('background-color', color), | |
| $('<span/>', { | |
| 'class': 'jqplot-legend-label', | |
| 'data-original-label': label | |
| }).text(label) | |
| ) | |
| .appendTo($legendContainer); | |
| } | |
| if ($legendContainer.children().length) { | |
| $legendFooter.addClass('has-legend'); | |
| limitLegendRows($legendContainer, MAX_FOOTER_LEGEND_ROWS); | |
| } | |
| updateLegendFooterStacking($legendFooter); | |
| }; | |
| $.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); | |
| // render the "Choose metrics" button variant and place it in the legend | |
| // footer instead of above the chart | |
| seriesPicker.useChooseMetricsButton = true; | |
| // handle placeSeriesPicker event | |
| var plot = this; | |
| $(seriesPicker).bind('placeSeriesPicker', function () { | |
| var $dataTable = $(plot.targetId).closest('.dataTable'); | |
| var $legendFooter = getOrCreateLegendFooter($dataTable); | |
| var $pickerSlot = $legendFooter.find('.jqplot-legend-picker'); | |
| if (!$pickerSlot.length) { | |
| $pickerSlot = $('<div class="jqplot-legend-picker"></div>').prependTo($legendFooter); | |
| } | |
| // replace any previously rendered picker so redraws don't stack duplicates | |
| $pickerSlot.empty().append(this.domElem); | |
| $legendFooter.addClass('has-picker'); | |
| updateLegendFooterStacking($legendFooter); | |
| }); | |
| // 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(); | |
| // The legend footer renders before this hook, so its row limit was computed | |
| // without the "Choose metrics" button. Now that the button is placed and | |
| // mounted (and has taken its share of the footer width), recompute the limit | |
| // so the legend items wrap against the real available width. | |
| applyFooterLegendRowLimit($(this.targetId).closest('.dataTable')); | |
| }); | |
| })(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); | |
| // ------------------------------------------------------------ | |
| // MATOMO INCOMPLETE DATA INDICATOR FOR JQPLOT | |
| // Show a dashed line to the last point to indicate incomplete | |
| // ------------------------------------------------------------ | |
| (function ($) { | |
| $.jqplot.LineRenderer.prototype.draw = function(ctx, gd, options, plot) { | |
| var i; | |
| // get a copy of the options, so we don't modify the original object. | |
| var opts = $.extend(true, {}, options); | |
| var shadow = (opts.shadow != undefined) ? opts.shadow : this.shadow; | |
| var showLine = (opts.showLine != undefined) ? opts.showLine : this.showLine; | |
| var fill = (opts.fill != undefined) ? opts.fill : this.fill; | |
| var fillAndStroke = (opts.fillAndStroke != undefined) ? opts.fillAndStroke : this.fillAndStroke; | |
| var xmin, ymin, xmax, ymax; | |
| // Only change in this overridden method, to pass the option to the renderers | |
| if (plot.options.hasOwnProperty('dataStates')) { | |
| opts.dataStates = plot.options.dataStates; | |
| } | |
| if (!Array.isArray(opts.dataStates)) { | |
| opts.dataStates = []; | |
| } | |
| ctx.save(); | |
| if (gd.length) { | |
| if (showLine) { | |
| // if we fill, we'll have to add points to close the curve. | |
| if (fill) { | |
| if (this.fillToZero) { | |
| // have to break line up into shapes at axis crossings | |
| var negativeColor = this.negativeColor; | |
| if (! this.useNegativeColors) { | |
| negativeColor = opts.fillStyle; | |
| } | |
| var isnegative = false; | |
| var posfs = opts.fillStyle; | |
| // if stoking line as well as filling, get a copy of line data. | |
| if (fillAndStroke) { | |
| var fasgd = gd.slice(0); | |
| } | |
| // if not stacked, fill down to axis | |
| if (this.index == 0 || !this._stack) { | |
| var tempgd = []; | |
| var pd = (this.renderer.smooth) ? this.renderer._smoothedPlotData : this._plotData; | |
| this._areaPoints = []; | |
| var pyzero = this._yaxis.series_u2p(this.fillToValue); | |
| var pxzero = this._xaxis.series_u2p(this.fillToValue); | |
| opts.closePath = true; | |
| if (this.fillAxis == 'y') { | |
| tempgd.push([gd[0][0], pyzero]); | |
| this._areaPoints.push([gd[0][0], pyzero]); | |
| for (var i=0; i<gd.length-1; i++) { | |
| tempgd.push(gd[i]); | |
| this._areaPoints.push(gd[i]); | |
| // do we have an axis crossing? | |
| if (pd[i][1] * pd[i+1][1] <= 0) { | |
| if (pd[i][1] < 0) { | |
| isnegative = true; | |
| opts.fillStyle = negativeColor; | |
| } | |
| else { | |
| isnegative = false; | |
| opts.fillStyle = posfs; | |
| } | |
| var xintercept = gd[i][0] + (gd[i+1][0] - gd[i][0]) * (pyzero-gd[i][1])/(gd[i+1][1] - gd[i][1]); | |
| tempgd.push([xintercept, pyzero]); | |
| this._areaPoints.push([xintercept, pyzero]); | |
| // now draw this shape and shadow. | |
| if (shadow) { | |
| this.renderer.shadowRenderer.draw(ctx, tempgd, opts); | |
| } | |
| this.renderer.shapeRenderer.draw(ctx, tempgd, opts); | |
| // now empty temp array and continue | |
| tempgd = [[xintercept, pyzero]]; | |
| // this._areaPoints = [[xintercept, pyzero]]; | |
| } | |
| } | |
| if (pd[gd.length-1][1] < 0) { | |
| isnegative = true; | |
| opts.fillStyle = negativeColor; | |
| } | |
| else { | |
| isnegative = false; | |
| opts.fillStyle = posfs; | |
| } | |
| tempgd.push(gd[gd.length-1]); | |
| this._areaPoints.push(gd[gd.length-1]); | |
| tempgd.push([gd[gd.length-1][0], pyzero]); | |
| this._areaPoints.push([gd[gd.length-1][0], pyzero]); | |
| } | |
| // now draw the last area. | |
| if (shadow) { | |
| this.renderer.shadowRenderer.draw(ctx, tempgd, opts); | |
| } | |
| this.renderer.shapeRenderer.draw(ctx, tempgd, opts); | |
| } | |
| // if stacked, fill to line below | |
| else { | |
| var prev = this._prevGridData; | |
| for (var i=prev.length; i>0; i--) { | |
| gd.push(prev[i-1]); | |
| // this._areaPoints.push(prev[i-1]); | |
| } | |
| if (shadow) { | |
| this.renderer.shadowRenderer.draw(ctx, gd, opts); | |
| } | |
| this._areaPoints = gd; | |
| this.renderer.shapeRenderer.draw(ctx, gd, opts); | |
| } | |
| } | |
| ///////////////////////// | |
| // Not filled to zero | |
| //////////////////////// | |
| else { | |
| // if stoking line as well as filling, get a copy of line data. | |
| if (fillAndStroke) { | |
| var fasgd = gd.slice(0); | |
| } | |
| // if not stacked, fill down to axis | |
| if (this.index == 0 || !this._stack) { | |
| // var gridymin = this._yaxis.series_u2p(this._yaxis.min) - this.gridBorderWidth / 2; | |
| var gridymin = ctx.canvas.height; | |
| // IE doesn't return new length on unshift | |
| gd.unshift([gd[0][0], gridymin]); | |
| var len = gd.length; | |
| gd.push([gd[len - 1][0], gridymin]); | |
| } | |
| // if stacked, fill to line below | |
| else { | |
| var prev = this._prevGridData; | |
| for (var i=prev.length; i>0; i--) { | |
| gd.push(prev[i-1]); | |
| } | |
| } | |
| this._areaPoints = gd; | |
| if (shadow) { | |
| this.renderer.shadowRenderer.draw(ctx, gd, opts); | |
| } | |
| this.renderer.shapeRenderer.draw(ctx, gd, opts); | |
| } | |
| if (fillAndStroke) { | |
| var fasopts = $.extend(true, {}, opts, {fill:false, closePath:false}); | |
| this.renderer.shapeRenderer.draw(ctx, fasgd, fasopts); | |
| // now draw the markers | |
| if (this.markerRenderer.show) { | |
| if (this.renderer.smooth) { | |
| fasgd = this.gridData; | |
| } | |
| for (i=0; i<fasgd.length; i++) { | |
| this.markerRenderer.draw(fasgd[i][0], fasgd[i][1], ctx, opts.markerOptions); | |
| } | |
| } | |
| } | |
| } | |
| else { | |
| if (this.renderer.bands.show) { | |
| var bdat; | |
| var bopts = $.extend(true, {}, opts); | |
| if (this.renderer.bands.showLines) { | |
| bdat = (this.renderer.smooth) ? this.renderer._hiBandSmoothedData : this.renderer._hiBandGridData; | |
| this.renderer.shapeRenderer.draw(ctx, bdat, opts); | |
| bdat = (this.renderer.smooth) ? this.renderer._lowBandSmoothedData : this.renderer._lowBandGridData; | |
| this.renderer.shapeRenderer.draw(ctx, bdat, bopts); | |
| } | |
| if (this.renderer.bands.fill) { | |
| if (this.renderer.smooth) { | |
| bdat = this.renderer._hiBandSmoothedData.concat(this.renderer._lowBandSmoothedData.reverse()); | |
| } | |
| else { | |
| bdat = this.renderer._hiBandGridData.concat(this.renderer._lowBandGridData.reverse()); | |
| } | |
| this._areaPoints = bdat; | |
| bopts.closePath = true; | |
| bopts.fill = true; | |
| bopts.fillStyle = this.renderer.bands.fillColor; | |
| this.renderer.shapeRenderer.draw(ctx, bdat, bopts); | |
| } | |
| } | |
| if (shadow) { | |
| this.renderer.shadowRenderer.draw(ctx, gd, opts); | |
| } | |
| this.renderer.shapeRenderer.draw(ctx, gd, opts); | |
| } | |
| } | |
| // calculate the bounding box | |
| var xmin = xmax = ymin = ymax = null; | |
| for (i=0; i<this._areaPoints.length; i++) { | |
| var p = this._areaPoints[i]; | |
| if (xmin > p[0] || xmin == null) { | |
| xmin = p[0]; | |
| } | |
| if (ymax < p[1] || ymax == null) { | |
| ymax = p[1]; | |
| } | |
| if (xmax < p[0] || xmax == null) { | |
| xmax = p[0]; | |
| } | |
| if (ymin > p[1] || ymin == null) { | |
| ymin = p[1]; | |
| } | |
| } | |
| if (this.type === 'line' && this.renderer.bands.show) { | |
| ymax = this._yaxis.series_u2p(this.renderer.bands._min); | |
| ymin = this._yaxis.series_u2p(this.renderer.bands._max); | |
| } | |
| this._boundingBox = [[xmin, ymax], [xmax, ymin]]; | |
| // now draw the markers | |
| if (this.markerRenderer.show && !fill) { | |
| if (this.renderer.smooth) { | |
| gd = this.gridData; | |
| } | |
| for (i = 0; i < gd.length; i++) { | |
| if (gd[i][0] === null || gd[i][1] === null) { | |
| continue; | |
| } | |
| const markerOptions = opts.markerOptions || {}; | |
| markerOptions.isIncomplete = opts.dataStates[i] && opts.dataStates[i] !== 'complete'; | |
| markerOptions.incompleteFillColor = plot.grid.background; | |
| this.markerRenderer.draw(gd[i][0], gd[i][1], ctx, markerOptions); | |
| } | |
| } | |
| } | |
| ctx.restore(); | |
| }; | |
| $.jqplot.ShapeRenderer.prototype.draw = function(ctx, points, options) { | |
| ctx.save(); | |
| var opts = (options != null) ? options : {}; | |
| var fill = (opts.fill != null) ? opts.fill : this.fill; | |
| var closePath = (opts.closePath != null) ? opts.closePath : this.closePath; | |
| var fillRect = (opts.fillRect != null) ? opts.fillRect : this.fillRect; | |
| var strokeRect = (opts.strokeRect != null) ? opts.strokeRect : this.strokeRect; | |
| var clearRect = (opts.clearRect != null) ? opts.clearRect : this.clearRect; | |
| var isarc = (opts.isarc != null) ? opts.isarc : this.isarc; | |
| var linePattern = (opts.linePattern != null) ? opts.linePattern : this.linePattern; | |
| var ctxPattern = $.jqplot.LinePattern(ctx, linePattern); | |
| ctx.lineWidth = opts.lineWidth || this.lineWidth; | |
| ctx.lineJoin = opts.lineJoin || this.lineJoin; | |
| ctx.lineCap = opts.lineCap || this.lineCap; | |
| ctx.strokeStyle = (opts.strokeStyle || opts.color) || this.strokeStyle; | |
| ctx.fillStyle = opts.fillStyle || this.fillStyle; | |
| ctx.beginPath(); | |
| let dataStates = []; | |
| if (!closePath && !fill && Array.isArray(opts.dataStates)) { | |
| // only do the incomplete visualization for line charts | |
| dataStates = opts.dataStates; | |
| } | |
| if (isarc) { | |
| ctx.arc(points[0], points[1], points[2], points[3], points[4], true); | |
| if (closePath) { | |
| ctx.closePath(); | |
| } | |
| if (fill) { | |
| ctx.fill(); | |
| } | |
| else { | |
| ctx.stroke(); | |
| } | |
| if (opts.isIncomplete && opts.incompleteFillColor) { | |
| // graph lines reach into the point | |
| // render inner point filled with background color to avoid showing them | |
| ctx.beginPath(); | |
| ctx.arc(points[0], points[1], points[2] / 8, points[3], points[4], true); | |
| ctx.strokeStyle = opts.incompleteFillColor; | |
| ctx.stroke(); | |
| ctx.closePath(); | |
| } | |
| ctx.restore(); | |
| return; | |
| } | |
| else if (clearRect) { | |
| ctx.clearRect(points[0], points[1], points[2], points[3]); | |
| ctx.restore(); | |
| return; | |
| } | |
| else if (fillRect || strokeRect) { | |
| if (fillRect) { | |
| ctx.fillRect(points[0], points[1], points[2], points[3]); | |
| } | |
| if (strokeRect) { | |
| ctx.strokeRect(points[0], points[1], points[2], points[3]); | |
| ctx.restore(); | |
| return; | |
| } | |
| } | |
| if (!points || !points.length) { | |
| return; | |
| } | |
| let move = true; | |
| for (let i = 0; i < points.length; i++) { | |
| // skip to the first non-null point and move to it. | |
| if (null === points[i][0] && null === points[i][1]) { | |
| continue; | |
| } | |
| if (move) { | |
| move = false; | |
| ctxPattern.moveTo(points[i][0], points[i][1]); | |
| continue; | |
| } | |
| // draw line to current point or skip if incomplete data point | |
| if (dataStates[i] && 'complete' !== dataStates[i]) { | |
| ctxPattern.moveTo(points[i][0], points[i][1]); | |
| } else { | |
| ctxPattern.lineTo(points[i][0], points[i][1]); | |
| } | |
| } | |
| if (closePath) { | |
| ctxPattern.closePath(); | |
| } | |
| if (fill) { | |
| ctx.fill(); | |
| } else { | |
| ctx.stroke(); | |
| } | |
| // draw dashed lines for incomplete data points | |
| ctx.beginPath(); | |
| ctx.setLineDash([3, 3]); | |
| move = true; | |
| for (let i = 0; i < points.length; i++) { | |
| // skip to the first non-null point and move to it. | |
| if (points[i][0] === null && points[i][1] === null) { | |
| continue; | |
| } | |
| if (move) { | |
| move = false; | |
| ctxPattern.moveTo(points[i][0], points[i][1]); | |
| continue; | |
| } | |
| // draw dashed line to current point or skip if not incomplete data point | |
| if (!dataStates[i] || 'complete' === dataStates[i]) { | |
| ctxPattern.moveTo(points[i][0], points[i][1]); | |
| } else { | |
| ctxPattern.lineTo(points[i][0], points[i][1]); | |
| } | |
| } | |
| ctx.stroke(); | |
| ctx.closePath(); | |
| ctx.restore(); | |
| }; | |
| // Only overriding this method to prevent drawing the shadow for the last line segment | |
| $.jqplot.ShadowRenderer.prototype.draw = function(ctx, points, options) { | |
| ctx.save(); | |
| var opts = (options != null) ? options : {}; | |
| var fill = (opts.fill != null) ? opts.fill : this.fill; | |
| var fillRect = (opts.fillRect != null) ? opts.fillRect : this.fillRect; | |
| var closePath = (opts.closePath != null) ? opts.closePath : this.closePath; | |
| var offset = (opts.offset != null) ? opts.offset : this.offset; | |
| var alpha = (opts.alpha != null) ? opts.alpha : this.alpha; | |
| var depth = (opts.depth != null) ? opts.depth : this.depth; | |
| var isarc = (opts.isarc != null) ? opts.isarc : this.isarc; | |
| var linePattern = (opts.linePattern != null) ? opts.linePattern : this.linePattern; | |
| ctx.lineWidth = (opts.lineWidth != null) ? opts.lineWidth : this.lineWidth; | |
| ctx.lineJoin = (opts.lineJoin != null) ? opts.lineJoin : this.lineJoin; | |
| ctx.lineCap = (opts.lineCap != null) ? opts.lineCap : this.lineCap; | |
| ctx.strokeStyle = opts.strokeStyle || this.strokeStyle || 'rgba(0,0,0,'+alpha+')'; | |
| ctx.fillStyle = opts.fillStyle || this.fillStyle || 'rgba(0,0,0,'+alpha+')'; | |
| let dataStates = []; | |
| if (!closePath && !fill && Array.isArray(opts.dataStates)) { | |
| // only do the incomplete visualization for line charts | |
| dataStates = opts.dataStates; | |
| } | |
| for (let j= 0; j < depth; j++) { | |
| const ctxPattern = $.jqplot.LinePattern(ctx, linePattern); | |
| ctx.translate(Math.cos(this.angle*Math.PI/180)*offset, Math.sin(this.angle*Math.PI/180)*offset); | |
| ctxPattern.beginPath(); | |
| if (isarc) { | |
| ctx.arc(points[0], points[1], points[2], points[3], points[4], true); | |
| } | |
| else if (fillRect) { | |
| ctx.fillRect(points[0], points[1], points[2], points[3]); | |
| } | |
| else if (points && points.length) { | |
| let move = true; | |
| for (let i = 0; i < points.length; i++) { | |
| // skip to the first non-null point and move to it. | |
| if (points[i][0] === null && points[i][1] === null) { | |
| continue; | |
| } | |
| if (move) { | |
| move = false; | |
| ctxPattern.moveTo(points[i][0], points[i][1]); | |
| continue; | |
| } | |
| // draw shadow line to current point or skip if incomplete data point | |
| if (dataStates[i] && 'complete' !== dataStates[i]) { | |
| ctxPattern.moveTo(points[i][0], points[i][1]); | |
| } else { | |
| ctxPattern.lineTo(points[i][0], points[i][1]); | |
| } | |
| } | |
| } | |
| if (closePath) { | |
| ctxPattern.closePath(); | |
| } | |
| if (fill) { | |
| ctx.fill(); | |
| } | |
| else { | |
| ctx.stroke(); | |
| } | |
| } | |
| ctx.restore(); | |
| }; | |
| })(jQuery); | |