| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749 | /* * * *  Experimental Timeline Series. *  Note: This API is in alpha stage and will be changed before final release. * *  (c) 2010-2019 Highsoft AS * *  Author: Daniel Studencki * *  License: www.highcharts.com/license * * */'use strict';import H from '../parts/Globals.js';var addEvent = H.addEvent,    extend = H.extend,    defined = H.defined,    LegendSymbolMixin = H.LegendSymbolMixin,    TrackerMixin = H.TrackerMixin,    merge = H.merge,    pick = H.pick,    Point = H.Point,    Series = H.Series,    undocumentedSeriesType = H.seriesType;/* * * The timeline series type. * * @private * @class * @name Highcharts.seriesTypes.timeline * * @augments Highcharts.Series */undocumentedSeriesType('timeline', 'line'    /* * * The timeline series presents given events along a drawn line. * * @sample highcharts/series-timeline/alternate-labels Timeline series * * @extends      plotOptions.line * @since        7.0.0 * @product      highcharts * @excluding    animationLimit, boostThreshold, connectEnds, connectNulls, *               cropThreshold, dashStyle, findNearestPointBy, *               getExtremesFromAll, lineWidth, negativeColor, pointInterval, *               pointIntervalUnit, pointPlacement, pointStart, softThreshold, *               stacking, step, threshold, turboThreshold, zoneAxis, zones * @optionparent plotOptions.timeline */    , {        colorByPoint: true,        stickyTracking: false,        ignoreHiddenPoint: true,        legendType: 'point',        lineWidth: 0,        tooltip: {            headerFormat: '<span style="color:{point.color}">● </span>' +            '<span style="font-weight: bold;">{point.point.date}</span><br/>',            pointFormat: '{point.description}'        },        states: {            hover: {                lineWidthPlus: 5,                halo: {                    size: 0                }            }        },        dataLabels: {            enabled: true,            allowOverlap: true,            /* *         * The width of the line connecting the data label to the point.         *         *         * In styled mode, the connector stroke width is given in the         * `.highcharts-data-label-connector` class.         *         * @type {Number}         * @default 1         * @sample {highcharts} highcharts/series-timeline/connector-styles         *         Custom connector width and color         */            connectorWidth: 1,            /* *         * The color of the line connecting the data label to the point.         *         * In styled mode, the connector stroke is given in the         * `.highcharts-data-label-connector` class.         *         * @type {String}         * @sample {highcharts} highcharts/series-timeline/connector-styles         *         Custom connector width and color         */            connectorColor: '#000000',            backgroundColor: '#ffffff',            /* *         * @type      {Highcharts.FormatterCallbackFunction<object>}         * @default function () {         *   var format;         *         *   if (!this.series.chart.styledMode) {         *       format = '<span style="color:' + this.point.color +         *           '">● </span><span style="font-weight: bold;" > ' +         *           (this.point.date || '') + '</span><br/>' +         *           (this.point.label || '');         *   } else {         *       format = '<span>● </span>' +         *           '<span>' + (this.point.date || '') +         *           '</span><br/>' + (this.point.label || '');         *   }         *   return format;         * }         * @apioption plotOptions.timeline.dataLabels.formatter         */            formatter: function () {                var format;                if (!this.series.chart.styledMode) {                    format = '<span style="color:' + this.point.color +                    '">● </span><span style="font-weight: bold;" > ' +                    (this.point.date || '') + '</span><br/>' +                    (this.point.label || '');                } else {                    format = '<span>● </span>' +                    '<span>' + (this.point.date || '') +                    '</span><br/>' + (this.point.label || '');                }                return format;            },            borderWidth: 1,            borderColor: '#666666',            /* *         * A pixel value defining the distance between the data label         * and the point. Negative numbers puts the label on top         * of the point.         *         * @type {Number}         * @default 100         */            distance: 100,            /* *         * Whether to position data labels alternately. For example, if         * [distance](#plotOptions.timeline.dataLabels.distance) is set         * equal to `100`, then the first data label 's distance will be         * set equal to `100`, the second one equal to `-100`, and so on.         *         * @type {Boolean}         * @default true         * @sample {highcharts} highcharts/series-timeline/alternate-disabled         *         Alternate disabled         */            alternate: true,            verticalAlign: 'middle',            color: '#333333'        },        marker: {            enabledThreshold: 0,            symbol: 'square',            height: 15        }    }    /* * * @lends Highcharts.Series# */    , {        requireSorting: false,        trackerGroups: ['markerGroup', 'dataLabelsGroup'],        // Use a simple symbol from LegendSymbolMixin        drawLegendSymbol: LegendSymbolMixin.drawRectangle,        // Use a group of trackers from TrackerMixin        drawTracker: TrackerMixin.drawTrackerPoint,        init: function () {            var series = this;            Series.prototype.init.apply(series, arguments);            // Distribute data labels before rendering them. Distribution is            // based on the 'dataLabels.distance' and 'dataLabels.alternate'            // property.            addEvent(series, 'drawDataLabels', function () {                // Delete the oldTextWidth parameter, in order to force                // adjusting data label wrapper box width. It's needed only when                // useHTML is enabled. This prevents the data label text getting                // out of the box range.                if (series.options.dataLabels.useHTML) {                    series.points.forEach(function (p) {                        if (p.visible && p.dataLabel) {                            delete p.dataLabel.text.oldTextWidth;                        }                    });                }                // Distribute data labels basing on defined algorithm.                series.distributeDL();            });            addEvent(series, 'afterDrawDataLabels', function () {                var seriesOptions = series.options,                    options = seriesOptions.dataLabels,                    hasRendered = series.hasRendered || 0,                    defer = pick(options.defer, !!seriesOptions.animation),                    connectorsGroup = series.connectorsGroup,                    dataLabel;                // Create (or redraw) the group for all connectors.                connectorsGroup = series.plotGroup(                    'connectorsGroup',                    'data-labels-connectors',                    defer && !hasRendered ? 'hidden' : 'visible',                    options.zIndex || 5                );                // Draw or align connector for each point.                series.points.forEach(function (point) {                    dataLabel = point.dataLabel;                    if (dataLabel) {                        // Within this wrap method is necessary to save the                        // current animation params, because the data label                        // target position (after animation) is needed to align                        // connectors.                        dataLabel.animate = function (params) {                            if (this.targetPosition) {                                this.targetPosition = params;                            }                            return H.SVGElement.prototype.animate.apply(                                this,                                arguments                            );                        };                        // Initialize the targetPosition field within data label                        // object. It's necessary because there is need to know                        // expected position of specific data label, when                        // aligning connectors. This field is overrided inside                        // of SVGElement.animate() wrapped  method.                        if (!dataLabel.targetPosition) {                            dataLabel.targetPosition = {};                        }                        return !point.connector ?                            point.drawConnector() :                            point.alignConnector();                    }                });                // Animate connectors group. It's animated in the same way like                // dataLabels, and also depends on dataLabels.defer parameter.                if (defer) {                    connectorsGroup.attr({                        opacity: +hasRendered                    });                    if (!hasRendered) {                        addEvent(series, 'afterAnimate', function () {                            if (series.visible) {                                connectorsGroup.show(true);                            }                            connectorsGroup[                                seriesOptions.animation ? 'animate' : 'attr'                            ]({                                opacity: 1                            }, {                                duration: 200                            });                        });                    }                }            });        },        alignDataLabel: function (point, dataLabel) {            var series = this,                isInverted = series.chart.inverted,                visiblePoints = series.visibilityMap.filter(function (point) {                    return point;                }),                visiblePointsCount = series.visiblePointsCount,                pointIndex = visiblePoints.indexOf(point),                isFirstOrLast = !pointIndex ||                pointIndex === visiblePointsCount - 1,                dataLabelsOptions = series.options.dataLabels,                userDLOptions = point.userDLOptions || {},                // Define multiplier which is used to calculate data label                // width. If data labels are alternate, they have two times more                // space to adapt (excepting first and last ones, which has only                // one and half), than in case of placing all data labels side                // by side.                multiplier = dataLabelsOptions.alternate ?                    (isFirstOrLast ? 1.5 : 2) :                    1,                distance,                availableSpace = Math.floor(                    series.xAxis.len / visiblePointsCount                ),                pad = dataLabel.padding,                targetDLWidth,                styles;            // Adjust data label width to the currently available space.            if (point.visible) {                distance = Math.abs(                    userDLOptions.x || point.options.dataLabels.x                );                if (isInverted) {                    targetDLWidth = (                        (distance - pad) * 2 - (point.itemHeight / 2)                    );                    styles = {                        width: targetDLWidth,                        // Apply ellipsis when data label height is exceeded.                        textOverflow: dataLabel.width / targetDLWidth *                        dataLabel.height / 2 > availableSpace * multiplier ?                            'ellipsis' : 'none'                    };                } else {                    styles = {                        width: userDLOptions.width ||                        dataLabelsOptions.width ||                        availableSpace * multiplier - (pad * 2)                    };                }                dataLabel.css(styles);                if (!series.chart.styledMode) {                    dataLabel.shadow({});                }            }            Series.prototype.alignDataLabel.apply(series, arguments);        },        processData: function () {            var series = this,                xMap = [],                base,                visiblePoints = 0,                i;            series.visibilityMap = series.getVisibilityMap();            // Calculate currently visible points.            series.visibilityMap.forEach(function (point) {                if (point) {                    visiblePoints++;                }            });            series.visiblePointsCount = visiblePoints;            base = series.xAxis.options.max / visiblePoints;            // Generate xData map.            for (i = 1; i <= visiblePoints; i++) {                xMap.push(                    (base * i) - (base / 2)                );            }            // Set all hidden points y values as negatives, in order to move            // them away from plot area. It is necessary to avoid hiding data            // labels, when dataLabels.allowOverlap is set to false.            series.visibilityMap.forEach(function (vis, i) {                if (!vis) {                    xMap.splice(i, 0, series.yData[i] === null ? null : -99);                }            });            series.xData = xMap;            series.yData = xMap.map(function (data) {                return defined(data) ? 1 : null;            });            Series.prototype.processData.call(this, arguments);        },        generatePoints: function () {            var series = this;            Series.prototype.generatePoints.apply(series);            series.points.forEach(function (point, i) {                point.applyOptions({                    x: series.xData[i]                });            });        },        getVisibilityMap: function () {            var series = this,                map = (series.data.length ?                    series.data : series.userOptions.data                ).map(function (point) {                    return (                        point &&                        point.visible !== false &&                        !point.isNull                    ) ? point : false;                });            return map;        },        distributeDL: function () {            var series = this,                dataLabelsOptions = series.options.dataLabels,                options,                pointDLOptions,                newOptions = {},                visibilityIndex = 1,                distance = dataLabelsOptions.distance;            series.points.forEach(function (point) {                if (point.visible && !point.isNull) {                    options = point.options;                    pointDLOptions = point.options.dataLabels;                    if (!series.hasRendered) {                        point.userDLOptions = merge({}, pointDLOptions);                    }                    newOptions[series.chart.inverted ? 'x' : 'y'] =                    dataLabelsOptions.alternate && visibilityIndex % 2 ?                        -distance : distance;                    options.dataLabels = merge(newOptions, point.userDLOptions);                    visibilityIndex++;                }            });        },        markerAttribs: function (point, state) {            var series = this,                seriesMarkerOptions = series.options.marker,                seriesStateOptions,                pointMarkerOptions = point.marker || {},                symbol = (                    pointMarkerOptions.symbol || seriesMarkerOptions.symbol                ),                pointStateOptions,                width = pick(                    pointMarkerOptions.width,                    seriesMarkerOptions.width,                    series.xAxis.len / series.visiblePointsCount                ),                height = pick(                    pointMarkerOptions.height,                    seriesMarkerOptions.height                ),                radius = 0,                attribs;            // Handle hover and select states            if (state) {                seriesStateOptions = seriesMarkerOptions.states[state] || {};                pointStateOptions = pointMarkerOptions.states &&                pointMarkerOptions.states[state] || {};                radius = pick(                    pointStateOptions.radius,                    seriesStateOptions.radius,                    radius + (                        seriesStateOptions.radiusPlus ||                    0                    )                );            }            point.hasImage = symbol && symbol.indexOf('url') === 0;            attribs = {                x: Math.floor(point.plotX) - (width / 2) - (radius / 2),                y: point.plotY - (height / 2) - (radius / 2),                width: width + radius,                height: height + radius            };            return attribs;        },        bindAxes: function () {            var series = this,                timelineXAxis = {                    gridLineWidth: 0,                    lineWidth: 0,                    min: 0,                    dataMin: 0,                    minPadding: 0,                    max: 100,                    dataMax: 100,                    maxPadding: 0,                    title: null,                    tickPositions: []                },                timelineYAxis = {                    gridLineWidth: 0,                    min: 0.5,                    dataMin: 0.5,                    minPadding: 0,                    max: 1.5,                    dataMax: 1.5,                    maxPadding: 0,                    title: null,                    labels: {                        enabled: false                    }                };            Series.prototype.bindAxes.call(series);            extend(series.xAxis.options, timelineXAxis);            extend(series.yAxis.options, timelineYAxis);        }    }    /* * * @lends Highcharts.Point# */    , {        init: function () {            var point = Point.prototype.init.apply(this, arguments);            point.name = pick(point.name, point.date, 'Event');            point.y = 1;            return point;        },        // The setVisible method is taken from Pie series prototype, in order to        // prevent importing whole Pie series.        setVisible: function (vis, redraw) {            var point = this,                series = point.series,                chart = series.chart,                ignoreHiddenPoint = series.options.ignoreHiddenPoint;            redraw = pick(redraw, ignoreHiddenPoint);            if (vis !== point.visible) {                // If called without an argument, toggle visibility                point.visible = point.options.visible = vis =                vis === undefined ? !point.visible : vis;                // update userOptions.data                series.options.data[series.data.indexOf(point)] = point.options;                // Show and hide associated elements. This is performed                // regardless of redraw or not, because chart.redraw only                // handles full series.                ['graphic', 'dataLabel', 'connector'].forEach(                    function (key) {                        if (point[key]) {                            point[key][vis ? 'show' : 'hide'](true);                        }                    }                );                if (point.legendItem) {                    chart.legend.colorizeItem(point, vis);                }                // #4170, hide halo after hiding point                if (!vis && point.state === 'hover') {                    point.setState('');                }                // Handle ignore hidden slices                if (ignoreHiddenPoint) {                    series.isDirty = true;                }                if (redraw) {                    chart.redraw();                }            }        },        setState: function () {            var proceed = Series.prototype.pointClass.prototype.setState;            // Prevent triggering the setState method on null points.            if (!this.isNull) {                proceed.apply(this, arguments);            }        },        getConnectorPath: function () {            var point = this,                chart = point.series.chart,                xAxisLen = point.series.xAxis.len,                inverted = chart.inverted,                direction = inverted ? 'x2' : 'y2',                dl = point.dataLabel,                targetDLPos = dl.targetPosition,                coords = {                    x1: point.plotX,                    y1: point.plotY,                    x2: point.plotX,                    y2: targetDLPos.y || dl.y                },                negativeDistance = (                    coords[direction] < point.series.yAxis.len / 2                ),                path;            // Recalculate coords when the chart is inverted.            if (inverted) {                coords = {                    x1: point.plotY,                    y1: xAxisLen - point.plotX,                    x2: targetDLPos.x || dl.x,                    y2: xAxisLen - point.plotX                };            }            // Subtract data label width or height from expected coordinate so            // that the connector would start from the appropriate edge.            if (negativeDistance) {                coords[direction] += dl[inverted ? 'width' : 'height'];            }            path = chart.renderer.crispLine([                'M',                coords.x1,                coords.y1,                'L',                coords.x2,                coords.y2            ], dl.options.connectorWidth || 1);            return path;        },        drawConnector: function () {            var point = this,                series = point.series,                dlOptions = point.dataLabel.options = merge(                    {}, series.options.dataLabels,                    point.options.dataLabels                );            point.connector = series.chart.renderer                .path(point.getConnectorPath())                .add(series.connectorsGroup);            if (!series.chart.styledMode) {                point.connector.attr({                    stroke: dlOptions.connectorColor,                    'stroke-width': dlOptions.connectorWidth,                    opacity: point.dataLabel.opacity                });            }        },        alignConnector: function () {            var point = this,                connector = point.connector,                bBox = connector.getBBox(),                isVisible = bBox.y > 0;            connector[isVisible ? 'animate' : 'attr']({                d: point.getConnectorPath()            });        }    });// Hide/show connector related with a specific data label, after overlapping// detected.addEvent(H.Chart, 'afterHideOverlappingLabels', function () {    var series = this.series,        dataLabel,        connector;    series.forEach(function (series) {        if (series.points) {            series.points.forEach(function (point) {                dataLabel = point.dataLabel;                connector = point.connector;                if (                    dataLabel &&                    dataLabel.targetPosition &&                    connector                ) {                    connector.attr({                        opacity: dataLabel.targetPosition.opacity ||                            dataLabel.newOpacity                    });                }            });        }    });});/* * * The `timeline` series. If the [type](#series.timeline.type) option is * not specified, it is inherited from [chart.type](#chart.type). * * @extends   series,plotOptions.timeline * @excluding animationLimit, boostThreshold, connectEnds, connectNulls, *            cropThreshold, dashStyle, dataParser, dataURL, findNearestPointBy, *            getExtremesFromAll, lineWidth, negativeColor, *            pointInterval, pointIntervalUnit, pointPlacement, pointStart, *            softThreshold, stacking, stack, step, threshold, turboThreshold, *            zoneAxis, zones * @product   highcharts * @apioption series.timeline *//* * * An array of data points for the series. For the `timeline` series type, * points can be given with three general parameters, `date`, `label`, * and `description`: * * Example: * * ```js * series: [{ *    type: 'timeline', *    data: [{ *        date: 'Jan 2018', *        label: 'Some event label', *        description: 'Description to show in tooltip' *    }] * }] * ``` * * @sample {highcharts} highcharts/series-timeline/alternate-labels *         Alternate labels * * @type      {Array<number|*>} * @extends   series.line.data * @excluding marker, x, y * @product   highcharts * @apioption series.timeline.data *//* * * The date of event. * * @type      {string} * @product   highcharts * @apioption series.timeline.data.date *//* * * The label of event. * * @type      {string} * @product   highcharts * @apioption series.timeline.data.label *//* * * The description of event. This description will be shown in tooltip. * * @type      {string} * @product   highcharts * @apioption series.timeline.data.description */
 |