Subversion-Projekte lars-tiefland.cienc

Revision

Blame | Letzte Änderung | Log anzeigen | RSS feed

/**
 * Highcharts Drilldown plugin
 * 
 * Author: Torstein Honsi
 * License: MIT License
 *
 * Demo: http://jsfiddle.net/highcharts/Vf3yT/
 */

/*global Highcharts,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,
                Tick = H.Tick,
                fireEvent = HighchartsAdapter.fireEvent,
                inArray = HighchartsAdapter.inArray,
                ddSeriesId = 1;

        // Utilities
        /*
         * Return an intermediate color between two colors, according to pos where 0
         * is the from color and 1 is the to color. This method is copied from ColorAxis.js
         * and should always be kept updated, until we get AMD support.
         */
        function tweenColors(from, to, pos) {
                // Check for has alpha, because rgba colors perform worse due to lack of
                // support in WebKit.
                var hasAlpha,
                        ret;

                // Unsupported color, return to-color (#3920)
                if (!to.rgba.length || !from.rgba.length) {
                        ret = to.raw || 'none';

                // Interpolate
                } else {
                        from = from.rgba;
                        to = to.rgba;
                        hasAlpha = (to[3] !== 1 || from[3] !== 1);
                        ret = (hasAlpha ? 'rgba(' : 'rgb(') + 
                                Math.round(to[0] + (from[0] - to[0]) * (1 - pos)) + ',' + 
                                Math.round(to[1] + (from[1] - to[1]) * (1 - pos)) + ',' + 
                                Math.round(to[2] + (from[2] - to[2]) * (1 - pos)) + 
                                (hasAlpha ? (',' + (to[3] + (from[3] - to[3]) * (1 - pos))) : '') + ')';
                }
                return ret;
        }
        /**
         * Handle animation of the color attributes directly
         */
        each(['fill', 'stroke'], function (prop) {
                HighchartsAdapter.addAnimSetter(prop, function (fx) {
                        fx.elem.attr(prop, tweenColors(H.Color(fx.start), H.Color(fx.end), fx.pos));
                });
        });

        // 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,
                        last;

                if (!this.drilldownLevels) {
                        this.drilldownLevels = [];
                }
                
                levelNumber = oldSeries.options._levelNumber || 0;

                // See if we can reuse the registered series from last run
                last = this.drilldownLevels[this.drilldownLevels.length - 1];
                if (last && last.levelNumber !== levelNumber) {
                        last = undefined;
                }
                
                        
                ddOptions = extend({
                        color: color,
                        _ddSeriesId: ddSeriesId++
                }, ddOptions);
                pointIndex = inArray(point, oldSeries.points);

                // Record options for all current series
                each(oldSeries.chart.series, function (series) {
                        if (series.xAxis === xAxis && !series.isDrilling) {
                                series.options._ddSeriesId = series.options._ddSeriesId || ddSeriesId++;
                                series.options._colorIndex = series.userOptions._colorIndex;
                                series.options._levelNumber = series.options._levelNumber || levelNumber; // #3182

                                if (last) {
                                        levelSeries = last.levelSeries;
                                        levelSeriesOptions = last.levelSeriesOptions;
                                } else {
                                        levelSeries.push(series);
                                        levelSeriesOptions.push(series.options);
                                }
                        }
                });

                // Add a record of properties for each drilldown level
                level = {
                        levelNumber: levelNumber,
                        seriesOptions: oldSeries.options,
                        levelSeriesOptions: levelSeriesOptions,
                        levelSeries: levelSeries,
                        shapeArgs: point.shapeArgs,
                        bBox: point.graphic ? point.graphic.getBBox() : {}, // no graphic in line series with markers disabled
                        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
                        }
                };

                // Push it to the lookup array
                this.drilldownLevels.push(level);

                newSeries = level.lowerSeries = this.addSeries(ddOptions, false);
                newSeries.options._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.options && series.options._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,
                        level,
                        oldSeries,
                        newSeries,
                        oldExtremes,
                        addSeries = function (seriesOptions) {
                                var addedSeries;
                                each(chartSeries, function (series) {
                                        if (series.options._ddSeriesId === seriesOptions._ddSeriesId) {
                                                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
                                        seriesI = chartSeries.length; // #2919
                                        while (seriesI--) {
                                                if (chartSeries[seriesI].options.id === level.lowerSeriesOptions.id && 
                                                                chartSeries[seriesI].options._levelNumber === levelNumber + 1) { // #3867
                                                        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.chart) { // #2919
                                                oldSeries.animateDrillupFrom(level);
                                        }
                                }
                                newSeries.options._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();
                }

                this.ddDupes.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) {
                                if (point.graphic) { // #3407
                                        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 () {
                                if (newSeries.points) { // May be destroyed in the meantime, #3389
                                        each(newSeries.points, function (point, i) {  
                                                // Fade in other points                   
                                                var verb = i === (level && level.pointIndex) ? 'show' : 'fadeIn',
                                                        inherit = verb === 'show' ? true : undefined;
                                                if (point.graphic) { // #3407
                                                        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,
                        animationOptions = this.chart.options.drilldown.animation,
                        xAxis = this.xAxis;
                        
                if (!init) {
                        each(drilldownLevels, function (level) {
                                if (series.options._ddSeriesId === level.lowerSeriesOptions._ddSeriesId) {
                                        animateFrom = level.shapeArgs;
                                        animateFrom.fill = level.color;
                                }
                        });

                        animateFrom.x += (pick(xAxis.oldPos, xAxis.pos) - xAxis.pos);

                        each(this.points, function (point) {
                                if (point.graphic) {
                                        point.graphic
                                                .attr(animateFrom)
                                                .animate(
                                                        extend(point.shapeArgs, { fill: point.color }), 
                                                        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,
                                complete = function () {
                                        graphic.destroy();
                                        if (group) {
                                                group = group.destroy();
                                        }
                                };

                        if (graphic) {
                        
                                delete point.graphic;

                                if (animationOptions) {
                                        graphic.animate(
                                                extend(level.shapeArgs, { fill: level.color }),
                                                H.merge(animationOptions, { complete: complete })
                                        );
                                } 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;

                                if (!init) {
                                        each(this.points, function (point, i) {
                                                point.graphic
                                                        .attr(H.merge(animateFrom, {
                                                                start: start + i * startAngle,
                                                                end: start + (i + 1) * startAngle,
                                                                fill: level.color
                                                        }))[animationOptions ? 'animate' : 'attr'](
                                                                extend(point.shapeArgs, { fill: point.color }), 
                                                                animationOptions
                                                        );
                                        });
                                        this.animate = null;
                                }
                        }
                });
        }
        
        H.Point.prototype.doDrilldown = function (_holdRedraw, category) {
                var series = this.series,
                        chart = series.chart,
                        drilldown = chart.options.drilldown,
                        i = (drilldown.series || []).length,
                        seriesOptions;

                if (!chart.ddDupes) {
                        chart.ddDupes = [];
                }
                
                while (i-- && !seriesOptions) {
                        if (drilldown.series[i].id === this.drilldown && inArray(this.drilldown, chart.ddDupes) === -1) {
                                seriesOptions = drilldown.series[i];
                                chart.ddDupes.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,
                        category: category,
                        points: category !== undefined && this.series.xAxis.ddPoints[category].slice(0)
                });
                
                if (seriesOptions) {
                        if (_holdRedraw) {
                                chart.addSingleSeriesAsDrilldown(this, seriesOptions);
                        } else {
                                chart.addSeriesAsDrilldown(this, seriesOptions);
                        }
                }
        };

        /**
         * Drill down to a given category. This is the same as clicking on an axis label.
         */
        H.Axis.prototype.drilldownCategory = function (x) {
                var key,
                        point,
                        ddPointsX = this.ddPoints[x];
                for (key in ddPointsX) {
                        point = ddPointsX[key];
                        if (point && point.series && point.series.visible && point.doDrilldown) { // #3197
                                point.doDrilldown(true, x);
                        }
                }
                this.chart.applyDrilldown();
        };

        /**
         * Create and return a collection of points associated with the X position. Reset it for each level.
         */     
        H.Axis.prototype.getDDPoints = function (x, levelNumber) {
                var ddPoints = this.ddPoints;
                if (!ddPoints) {
                        this.ddPoints = ddPoints = {};
                }
                if (!ddPoints[x]) {
                        ddPoints[x] = [];
                }
                if (ddPoints[x].levelNumber !== levelNumber) {
                        ddPoints[x].length = 0; // reset
                }
                return ddPoints[x];
        };


        /**
         * Make a tick label drillable, or remove drilling on update
         */
        Tick.prototype.drillable = function () {
                var pos = this.pos,
                        label = this.label,
                        axis = this.axis,
                        ddPointsX = axis.ddPoints && axis.ddPoints[pos];

                if (label && ddPointsX && ddPointsX.length) {
                        if (!label.basicStyles) {
                                label.basicStyles = H.merge(label.styles);
                        }
                        label
                                .addClass('highcharts-drilldown-axis-label')
                                .css(axis.chart.options.drilldown.activeAxisLabelStyle)
                                .on('click', function () {
                                        axis.drilldownCategory(pos);
                                });

                } else if (label && label.basicStyles) {
                        label.styles = {}; // reset for full overwrite of styles
                        label.css(label.basicStyles);
                        label.on('click', null); // #3806                       
                }
        };

        /**
         * Always keep the drillability updated (#3951)
         */
        wrap(Tick.prototype, 'addLabel', function (proceed) {
                proceed.call(this);
                this.drillable();
        });
        

        /**
         * On initialization of each point, identify its label and make it clickable. Also, provide a
         * list of points associated to that label.
         */
        wrap(H.Point.prototype, 'init', function (proceed, series, options, x) {
                var point = proceed.call(this, series, options, x),
                        xAxis = series.xAxis,
                        tick = xAxis && xAxis.ticks[x],
                        ddPointsX = xAxis && xAxis.getDDPoints(x, series.options._levelNumber);

                if (point.drilldown) {
                        
                        // Add the click event to the point 
                        H.addEvent(point, 'click', function () {
                                if (series.xAxis && series.chart.options.drilldown.allowPointDrilldown === false) {
                                        series.xAxis.drilldownCategory(x);
                                } else {
                                        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();
                                        });
                                }
                        });*/


                        // Register drilldown points on this X value
                        if (ddPointsX) {
                                ddPointsX.push(point);
                                ddPointsX.levelNumber = series.options._levelNumber;
                        }

                }

                // Add or remove click handler and style on the tick label
                if (tick) {
                        tick.drillable();
                }

                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);
                        }
                });
        });

        // 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));