/** * Highcharts Drilldown plugin * * Author: Torstein Honsi * License: MIT License * * Demo: http://jsfiddle.net/highcharts/Vf3yT/ */ /*global HighchartsAdapter*/ (function (H) { "use strict"; var noop = function () {}, defaultOptions = H.getOptions(), each = H.each, extend = H.extend, format = H.format, pick = H.pick, wrap = H.wrap, Chart = H.Chart, seriesTypes = H.seriesTypes, PieSeries = seriesTypes.pie, ColumnSeries = seriesTypes.column, fireEvent = HighchartsAdapter.fireEvent, inArray = HighchartsAdapter.inArray, dupes = []; // Utilities function tweenColors(startColor, endColor, pos) { var rgba = [ Math.round(startColor[0] + (endColor[0] - startColor[0]) * pos), Math.round(startColor[1] + (endColor[1] - startColor[1]) * pos), Math.round(startColor[2] + (endColor[2] - startColor[2]) * pos), startColor[3] + (endColor[3] - startColor[3]) * pos ]; return 'rgba(' + rgba.join(',') + ')'; } // Add language extend(defaultOptions.lang, { drillUpText: '◁ Back to {series.name}' }); defaultOptions.drilldown = { activeAxisLabelStyle: { cursor: 'pointer', color: '#0d233a', fontWeight: 'bold', textDecoration: 'underline' }, activeDataLabelStyle: { cursor: 'pointer', color: '#0d233a', fontWeight: 'bold', textDecoration: 'underline' }, animation: { duration: 500 }, drillUpButton: { position: { align: 'right', x: -10, y: 10 } // relativeTo: 'plotBox' // theme } }; /** * A general fadeIn method */ H.SVGRenderer.prototype.Element.prototype.fadeIn = function (animation) { this .attr({ opacity: 0.1, visibility: 'inherit' }) .animate({ opacity: pick(this.newOpacity, 1) // newOpacity used in maps }, animation || { duration: 250 }); }; Chart.prototype.addSeriesAsDrilldown = function (point, ddOptions) { this.addSingleSeriesAsDrilldown(point, ddOptions); this.applyDrilldown(); }; Chart.prototype.addSingleSeriesAsDrilldown = function (point, ddOptions) { var oldSeries = point.series, xAxis = oldSeries.xAxis, yAxis = oldSeries.yAxis, newSeries, color = point.color || oldSeries.color, pointIndex, levelSeries = [], levelSeriesOptions = [], level, levelNumber; levelNumber = oldSeries.levelNumber || 0; ddOptions = extend({ color: color }, ddOptions); pointIndex = inArray(point, oldSeries.points); // Record options for all current series each(oldSeries.chart.series, function (series) { if (series.xAxis === xAxis) { levelSeries.push(series); levelSeriesOptions.push(series.userOptions); series.levelNumber = series.levelNumber || levelNumber; // #3182 } }); // Add a record of properties for each drilldown level level = { levelNumber: levelNumber, seriesOptions: oldSeries.userOptions, levelSeriesOptions: levelSeriesOptions, levelSeries: levelSeries, shapeArgs: point.shapeArgs, bBox: point.graphic.getBBox(), color: color, lowerSeriesOptions: ddOptions, pointOptions: oldSeries.options.data[pointIndex], pointIndex: pointIndex, oldExtremes: { xMin: xAxis && xAxis.userMin, xMax: xAxis && xAxis.userMax, yMin: yAxis && yAxis.userMin, yMax: yAxis && yAxis.userMax } }; // Generate and push it to a lookup array if (!this.drilldownLevels) { this.drilldownLevels = []; } this.drilldownLevels.push(level); newSeries = level.lowerSeries = this.addSeries(ddOptions, false); newSeries.levelNumber = levelNumber + 1; if (xAxis) { xAxis.oldPos = xAxis.pos; xAxis.userMin = xAxis.userMax = null; yAxis.userMin = yAxis.userMax = null; } // Run fancy cross-animation on supported and equal types if (oldSeries.type === newSeries.type) { newSeries.animate = newSeries.animateDrilldown || noop; newSeries.options.animation = true; } }; Chart.prototype.applyDrilldown = function () { var drilldownLevels = this.drilldownLevels, levelToRemove; if (drilldownLevels && drilldownLevels.length > 0) { // #3352, async loading levelToRemove = drilldownLevels[drilldownLevels.length - 1].levelNumber; each(this.drilldownLevels, function (level) { if (level.levelNumber === levelToRemove) { each(level.levelSeries, function (series) { if (series.levelNumber === levelToRemove) { // Not removed, not added as part of a multi-series drilldown series.remove(false); } }); } }); } this.redraw(); this.showDrillUpButton(); }; Chart.prototype.getDrilldownBackText = function () { var drilldownLevels = this.drilldownLevels, lastLevel; if (drilldownLevels && drilldownLevels.length > 0) { // #3352, async loading lastLevel = drilldownLevels[drilldownLevels.length - 1]; lastLevel.series = lastLevel.seriesOptions; return format(this.options.lang.drillUpText, lastLevel); } }; Chart.prototype.showDrillUpButton = function () { var chart = this, backText = this.getDrilldownBackText(), buttonOptions = chart.options.drilldown.drillUpButton, attr, states; if (!this.drillUpButton) { attr = buttonOptions.theme; states = attr && attr.states; this.drillUpButton = this.renderer.button( backText, null, null, function () { chart.drillUp(); }, attr, states && states.hover, states && states.select ) .attr({ align: buttonOptions.position.align, zIndex: 9 }) .add() .align(buttonOptions.position, false, buttonOptions.relativeTo || 'plotBox'); } else { this.drillUpButton.attr({ text: backText }) .align(); } }; Chart.prototype.drillUp = function () { var chart = this, drilldownLevels = chart.drilldownLevels, levelNumber = drilldownLevels[drilldownLevels.length - 1].levelNumber, i = drilldownLevels.length, chartSeries = chart.series, seriesI = chartSeries.length, level, oldSeries, newSeries, oldExtremes, addSeries = function (seriesOptions) { var addedSeries; each(chartSeries, function (series) { if (series.userOptions === seriesOptions) { addedSeries = series; } }); addedSeries = addedSeries || chart.addSeries(seriesOptions, false); if (addedSeries.type === oldSeries.type && addedSeries.animateDrillupTo) { addedSeries.animate = addedSeries.animateDrillupTo; } if (seriesOptions === level.seriesOptions) { newSeries = addedSeries; } }; while (i--) { level = drilldownLevels[i]; if (level.levelNumber === levelNumber) { drilldownLevels.pop(); // Get the lower series by reference or id oldSeries = level.lowerSeries; if (!oldSeries.chart) { // #2786 while (seriesI--) { if (chartSeries[seriesI].options.id === level.lowerSeriesOptions.id) { oldSeries = chartSeries[seriesI]; break; } } } oldSeries.xData = []; // Overcome problems with minRange (#2898) each(level.levelSeriesOptions, addSeries); fireEvent(chart, 'drillup', { seriesOptions: level.seriesOptions }); if (newSeries.type === oldSeries.type) { newSeries.drilldownLevel = level; newSeries.options.animation = chart.options.drilldown.animation; if (oldSeries.animateDrillupFrom) { oldSeries.animateDrillupFrom(level); } } newSeries.levelNumber = levelNumber; oldSeries.remove(false); // Reset the zoom level of the upper series if (newSeries.xAxis) { oldExtremes = level.oldExtremes; newSeries.xAxis.setExtremes(oldExtremes.xMin, oldExtremes.xMax, false); newSeries.yAxis.setExtremes(oldExtremes.yMin, oldExtremes.yMax, false); } } } this.redraw(); if (this.drilldownLevels.length === 0) { this.drillUpButton = this.drillUpButton.destroy(); } else { this.drillUpButton.attr({ text: this.getDrilldownBackText() }) .align(); } dupes.length = []; // #3315 }; ColumnSeries.prototype.supportsDrilldown = true; /** * When drilling up, keep the upper series invisible until the lower series has * moved into place */ ColumnSeries.prototype.animateDrillupTo = function (init) { if (!init) { var newSeries = this, level = newSeries.drilldownLevel; each(this.points, function (point) { point.graphic.hide(); if (point.dataLabel) { point.dataLabel.hide(); } if (point.connector) { point.connector.hide(); } }); // Do dummy animation on first point to get to complete setTimeout(function () { each(newSeries.points, function (point, i) { // Fade in other points var verb = i === (level && level.pointIndex) ? 'show' : 'fadeIn', inherit = verb === 'show' ? true : undefined; point.graphic[verb](inherit); if (point.dataLabel) { point.dataLabel[verb](inherit); } if (point.connector) { point.connector[verb](inherit); } }); }, Math.max(this.chart.options.drilldown.animation.duration - 50, 0)); // Reset this.animate = noop; } }; ColumnSeries.prototype.animateDrilldown = function (init) { var series = this, drilldownLevels = this.chart.drilldownLevels, animateFrom = this.chart.drilldownLevels[this.chart.drilldownLevels.length - 1].shapeArgs, animationOptions = this.chart.options.drilldown.animation; if (!init) { each(drilldownLevels, function (level) { if (series.userOptions === level.lowerSeriesOptions) { animateFrom = level.shapeArgs; } }); animateFrom.x += (this.xAxis.oldPos - this.xAxis.pos); each(this.points, function (point) { if (point.graphic) { point.graphic .attr(animateFrom) .animate(point.shapeArgs, animationOptions); } if (point.dataLabel) { point.dataLabel.fadeIn(animationOptions); } }); this.animate = null; } }; /** * When drilling up, pull out the individual point graphics from the lower series * and animate them into the origin point in the upper series. */ ColumnSeries.prototype.animateDrillupFrom = function (level) { var animationOptions = this.chart.options.drilldown.animation, group = this.group, series = this; // Cancel mouse events on the series group (#2787) each(series.trackerGroups, function (key) { if (series[key]) { // we don't always have dataLabelsGroup series[key].on('mouseover'); } }); delete this.group; each(this.points, function (point) { var graphic = point.graphic, startColor = H.Color(point.color).rgba, endColor = H.Color(level.color).rgba, complete = function () { graphic.destroy(); if (group) { group = group.destroy(); } }; if (graphic) { delete point.graphic; if (animationOptions) { /*jslint unparam: true*/ graphic.animate(level.shapeArgs, H.merge(animationOptions, { step: function (val, fx) { if (fx.prop === 'start' && startColor.length === 4 && endColor.length === 4) { this.attr({ fill: tweenColors(startColor, endColor, fx.pos) }); } }, complete: complete })); /*jslint unparam: false*/ } else { graphic.attr(level.shapeArgs); complete(); } } }); }; if (PieSeries) { extend(PieSeries.prototype, { supportsDrilldown: true, animateDrillupTo: ColumnSeries.prototype.animateDrillupTo, animateDrillupFrom: ColumnSeries.prototype.animateDrillupFrom, animateDrilldown: function (init) { var level = this.chart.drilldownLevels[this.chart.drilldownLevels.length - 1], animationOptions = this.chart.options.drilldown.animation, animateFrom = level.shapeArgs, start = animateFrom.start, angle = animateFrom.end - start, startAngle = angle / this.points.length, startColor = H.Color(level.color).rgba; if (!init) { each(this.points, function (point, i) { var endColor = H.Color(point.color).rgba; /*jslint unparam: true*/ point.graphic .attr(H.merge(animateFrom, { start: start + i * startAngle, end: start + (i + 1) * startAngle }))[animationOptions ? 'animate' : 'attr'](point.shapeArgs, H.merge(animationOptions, { step: function (val, fx) { if (fx.prop === 'start' && startColor.length === 4 && endColor.length === 4) { this.attr({ fill: tweenColors(startColor, endColor, fx.pos) }); } } })); /*jslint unparam: false*/ }); this.animate = null; } } }); } H.Point.prototype.doDrilldown = function (_holdRedraw) { var series = this.series, chart = series.chart, drilldown = chart.options.drilldown, i = (drilldown.series || []).length, seriesOptions; while (i-- && !seriesOptions) { if (drilldown.series[i].id === this.drilldown && inArray(this.drilldown, dupes) === -1) { seriesOptions = drilldown.series[i]; dupes.push(this.drilldown); } } // Fire the event. If seriesOptions is undefined, the implementer can check for // seriesOptions, and call addSeriesAsDrilldown async if necessary. fireEvent(chart, 'drilldown', { point: this, seriesOptions: seriesOptions }); if (seriesOptions) { if (_holdRedraw) { chart.addSingleSeriesAsDrilldown(this, seriesOptions); } else { chart.addSeriesAsDrilldown(this, seriesOptions); } } }; wrap(H.Point.prototype, 'init', function (proceed, series, options, x) { var point = proceed.call(this, series, options, x), chart = series.chart, tick = series.xAxis && series.xAxis.ticks[x], tickLabel = tick && tick.label; if (point.drilldown) { // Add the click event to the point H.addEvent(point, 'click', function () { point.doDrilldown(); }); /*wrap(point, 'importEvents', function (proceed) { // wrapping importEvents makes point.click event work if (!this.hasImportedEvents) { proceed.call(this); H.addEvent(this, 'click', function () { this.doDrilldown(); }); } });*/ // Make axis labels clickable if (tickLabel) { if (!tickLabel.basicStyles) { tickLabel.basicStyles = H.merge(tickLabel.styles); } tickLabel .addClass('highcharts-drilldown-axis-label') .css(chart.options.drilldown.activeAxisLabelStyle) .on('click', function () { each(tickLabel.ddPoints, function (point) { if (point.doDrilldown) { point.doDrilldown(true); } }); chart.applyDrilldown(); }); if (!tickLabel.ddPoints) { tickLabel.ddPoints = []; } tickLabel.ddPoints.push(point); } } else if (tickLabel && tickLabel.basicStyles) { tickLabel.styles = {}; // reset for full overwrite of styles tickLabel.css(tickLabel.basicStyles); } return point; }); wrap(H.Series.prototype, 'drawDataLabels', function (proceed) { var css = this.chart.options.drilldown.activeDataLabelStyle; proceed.call(this); each(this.points, function (point) { if (point.drilldown && point.dataLabel) { point.dataLabel .attr({ 'class': 'highcharts-drilldown-data-label' }) .css(css) .on('click', function () { point.doDrilldown(); }); } }); }); // Mark the trackers with a pointer var type, drawTrackerWrapper = function (proceed) { proceed.call(this); each(this.points, function (point) { if (point.drilldown && point.graphic) { point.graphic .attr({ 'class': 'highcharts-drilldown-point' }) .css({ cursor: 'pointer' }); } }); }; for (type in seriesTypes) { if (seriesTypes[type].prototype.supportsDrilldown) { wrap(seriesTypes[type].prototype, 'drawTracker', drawTrackerWrapper); } } }(Highcharts));