/* * * (c) 2010-2019 Torstein Honsi * * License: www.highcharts.com/license */ 'use strict'; import H from './Globals.js'; import './Utilities.js'; import './Color.js'; import './Legend.js'; import './Series.js'; import './Options.js'; var color = H.color, LegendSymbolMixin = H.LegendSymbolMixin, pick = H.pick, Series = H.Series, seriesType = H.seriesType; /** * Area series type. * * @private * @class * @name Highcharts.seriesTypes.area * * @augments Highcharts.Series */ seriesType('area', 'line', /** * The area series type. * * @sample {highcharts} highcharts/demo/area-basic/ * Area chart * @sample {highstock} stock/demo/area/ * Area chart * * @extends plotOptions.line * @excluding useOhlcData * @product highcharts highstock * @optionparent plotOptions.area */ { /** * Fill color or gradient for the area. When `null`, the series' `color` * is used with the series' `fillOpacity`. * * In styled mode, the fill color can be set with the `.highcharts-area` * class name. * * @sample {highcharts} highcharts/plotoptions/area-fillcolor-default/ * Null by default * @sample {highcharts} highcharts/plotoptions/area-fillcolor-gradient/ * Gradient * * @type {Highcharts.ColorString|Highcharts.GradientColorObject|Highcharts.PatternObject} * @product highcharts highstock * @apioption plotOptions.area.fillColor */ /** * Fill opacity for the area. When you set an explicit `fillColor`, * the `fillOpacity` is not applied. Instead, you should define the * opacity in the `fillColor` with an rgba color definition. The * `fillOpacity` setting, also the default setting, overrides the alpha * component of the `color` setting. * * In styled mode, the fill opacity can be set with the * `.highcharts-area` class name. * * @sample {highcharts} highcharts/plotoptions/area-fillopacity/ * Automatic fill color and fill opacity of 0.1 * * @type {number} * @default {highcharts} 0.75 * @default {highstock} 0.75 * @product highcharts highstock * @apioption plotOptions.area.fillOpacity */ /** * A separate color for the graph line. By default the line takes the * `color` of the series, but the lineColor setting allows setting a * separate color for the line without altering the `fillColor`. * * In styled mode, the line stroke can be set with the * `.highcharts-graph` class name. * * @sample {highcharts} highcharts/plotoptions/area-linecolor/ * Dark gray line * * @type {Highcharts.ColorString} * @product highcharts highstock * @apioption plotOptions.area.lineColor */ /** * A separate color for the negative part of the area. * * In styled mode, a negative color is set with the `.highcharts-negative` * class name. * * @see [negativeColor](#plotOptions.area.negativeColor) * * @sample {highcharts} highcharts/css/series-negative-color/ * Negative color in styled mode * * @type {Highcharts.ColorString|Highcharts.GradientColorObject|Highcharts.PatternObject} * @since 3.0 * @product highcharts * @apioption plotOptions.area.negativeFillColor */ /** * Whether the whole area or just the line should respond to mouseover * tooltips and other mouse or touch events. * * @sample {highcharts|highstock} highcharts/plotoptions/area-trackbyarea/ * Display the tooltip when the area is hovered * * @type {boolean} * @default false * @since 1.1.6 * @product highcharts highstock * @apioption plotOptions.area.trackByArea */ /** * When this is true, the series will not cause the Y axis to cross * the zero plane (or [threshold](#plotOptions.series.threshold) option) * unless the data actually crosses the plane. * * For example, if `softThreshold` is `false`, a series of 0, 1, 2, * 3 will make the Y axis show negative values according to the `minPadding` * option. If `softThreshold` is `true`, the Y axis starts at 0. * * @since 4.1.9 * @product highcharts highstock */ softThreshold: false, /** * The Y axis value to serve as the base for the area, for distinguishing * between values above and below a threshold. The area between the graph * and the threshold is filled. * * * If a number is given, the Y axis will scale to the threshold. * * If `null`, the scaling behaves like a line series with fill between the * graph and the Y axis minimum. * * If `Infinity` or `-Infinity`, the area between the graph and the * corresponing Y axis extreme is filled (since v6.1.0). * * @sample {highcharts} highcharts/plotoptions/area-threshold/ * A threshold of 100 * @sample {highcharts} highcharts/plotoptions/area-threshold-infinity/ * A threshold of Infinity * * @since 2.0 * @product highcharts highstock */ threshold: 0 }, /** @lends seriesTypes.area.prototype */ { singleStacks: false, // Return an array of stacked points, where null and missing points are // replaced by dummy points in order for gaps to be drawn correctly in // stacks. getStackPoints: function (points) { var series = this, segment = [], keys = [], xAxis = this.xAxis, yAxis = this.yAxis, stack = yAxis.stacks[this.stackKey], pointMap = {}, seriesIndex = series.index, yAxisSeries = yAxis.series, seriesLength = yAxisSeries.length, visibleSeries, upOrDown = pick(yAxis.options.reversedStacks, true) ? 1 : -1, i; points = points || this.points; if (this.options.stacking) { for (i = 0; i < points.length; i++) { // Reset after point update (#7326) points[i].leftNull = points[i].rightNull = null; // Create a map where we can quickly look up the points by // their X values. pointMap[points[i].x] = points[i]; } // Sort the keys (#1651) H.objectEach(stack, function (stackX, x) { // nulled after switching between // grouping and not (#1651, #2336) if (stackX.total !== null) { keys.push(x); } }); keys.sort(function (a, b) { return a - b; }); visibleSeries = yAxisSeries.map(function (s) { return s.visible; }); keys.forEach(function (x, idx) { var y = 0, stackPoint, stackedValues; if (pointMap[x] && !pointMap[x].isNull) { segment.push(pointMap[x]); // Find left and right cliff. -1 goes left, 1 goes // right. [-1, 1].forEach(function (direction) { var nullName = direction === 1 ? 'rightNull' : 'leftNull', cliffName = direction === 1 ? 'rightCliff' : 'leftCliff', cliff = 0, otherStack = stack[keys[idx + direction]]; // If there is a stack next to this one, // to the left or to the right... if (otherStack) { i = seriesIndex; // Can go either up or down, // depending on reversedStacks while (i >= 0 && i < seriesLength) { stackPoint = otherStack.points[i]; if (!stackPoint) { // If the next point in this series // is missing, mark the point // with point.leftNull or // point.rightNull = true. if (i === seriesIndex) { pointMap[x][nullName] = true; // If there are missing points in // the next stack in any of the // series below this one, we need // to substract the missing values // and add a hiatus to the left or // right. } else if (visibleSeries[i]) { stackedValues = stack[x].points[i]; if (stackedValues) { cliff -= stackedValues[1] - stackedValues[0]; } } } // When reversedStacks is true, loop up, // else loop down i += upOrDown; } } pointMap[x][cliffName] = cliff; }); // There is no point for this X value in this series, so we // insert a dummy point in order for the areas to be drawn // correctly. } else { // Loop down the stack to find the series below this // one that has a value (#1991) i = seriesIndex; while (i >= 0 && i < seriesLength) { stackPoint = stack[x].points[i]; if (stackPoint) { y = stackPoint[1]; break; } // When reversedStacks is true, loop up, else loop // down i += upOrDown; } y = yAxis.translate(y, 0, 1, 0, 1); // #6272 segment.push({ isNull: true, plotX: xAxis.translate(x, 0, 0, 0, 1), // #6272 x: x, plotY: y, yBottom: y }); } }); } return segment; }, getGraphPath: function (points) { var getGraphPath = Series.prototype.getGraphPath, graphPath, options = this.options, stacking = options.stacking, yAxis = this.yAxis, topPath, bottomPath, bottomPoints = [], graphPoints = [], seriesIndex = this.index, i, areaPath, plotX, stacks = yAxis.stacks[this.stackKey], threshold = options.threshold, translatedThreshold = yAxis.getThreshold(options.threshold), isNull, yBottom, connectNulls = options.connectNulls || stacking === 'percent', // To display null points in underlying stacked series, this // series graph must be broken, and the area also fall down to // fill the gap left by the null point. #2069 addDummyPoints = function (i, otherI, side) { var point = points[i], stackedValues = stacking && stacks[point.x].points[seriesIndex], nullVal = point[side + 'Null'] || 0, cliffVal = point[side + 'Cliff'] || 0, top, bottom, isNull = true; if (cliffVal || nullVal) { top = (nullVal ? stackedValues[0] : stackedValues[1]) + cliffVal; bottom = stackedValues[0] + cliffVal; isNull = !!nullVal; } else if ( !stacking && points[otherI] && points[otherI].isNull ) { top = bottom = threshold; } // Add to the top and bottom line of the area if (top !== undefined) { graphPoints.push({ plotX: plotX, plotY: top === null ? translatedThreshold : yAxis.getThreshold(top), isNull: isNull, isCliff: true }); bottomPoints.push({ plotX: plotX, plotY: bottom === null ? translatedThreshold : yAxis.getThreshold(bottom), doCurve: false // #1041, gaps in areaspline areas }); } }; // Find what points to use points = points || this.points; // Fill in missing points if (stacking) { points = this.getStackPoints(points); } for (i = 0; i < points.length; i++) { isNull = points[i].isNull; plotX = pick(points[i].rectPlotX, points[i].plotX); yBottom = pick(points[i].yBottom, translatedThreshold); if (!isNull || connectNulls) { if (!connectNulls) { addDummyPoints(i, i - 1, 'left'); } // Skip null point when stacking is false and connectNulls // true if (!(isNull && !stacking && connectNulls)) { graphPoints.push(points[i]); bottomPoints.push({ x: i, plotX: plotX, plotY: yBottom }); } if (!connectNulls) { addDummyPoints(i, i + 1, 'right'); } } } topPath = getGraphPath.call(this, graphPoints, true, true); bottomPoints.reversed = true; bottomPath = getGraphPath.call(this, bottomPoints, true, true); if (bottomPath.length) { bottomPath[0] = 'L'; } areaPath = topPath.concat(bottomPath); // TODO: don't set leftCliff and rightCliff when connectNulls? graphPath = getGraphPath .call(this, graphPoints, false, connectNulls); areaPath.xMap = topPath.xMap; this.areaPath = areaPath; return graphPath; }, // Draw the graph and the underlying area. This method calls the Series // base function and adds the area. The areaPath is calculated in the // getSegmentPath method called from Series.prototype.drawGraph. drawGraph: function () { // Define or reset areaPath this.areaPath = []; // Call the base method Series.prototype.drawGraph.apply(this); // Define local variables var series = this, areaPath = this.areaPath, options = this.options, zones = this.zones, props = [[ 'area', 'highcharts-area', this.color, options.fillColor ]]; // area name, main color, fill color zones.forEach(function (zone, i) { props.push([ 'zone-area-' + i, 'highcharts-area highcharts-zone-area-' + i + ' ' + zone.className, zone.color || series.color, zone.fillColor || options.fillColor ]); }); props.forEach(function (prop) { var areaKey = prop[0], area = series[areaKey], attribs; // Create or update the area if (area) { // update area.endX = series.preventGraphAnimation ? null : areaPath.xMap; area.animate({ d: areaPath }); } else { // create attribs = { zIndex: 0 // #1069 }; if (!series.chart.styledMode) { attribs.fill = pick( prop[3], color(prop[2]) .setOpacity(pick(options.fillOpacity, 0.75)) .get() ); } area = series[areaKey] = series.chart.renderer .path(areaPath) .addClass(prop[1]) .attr(attribs) .add(series.group); area.isArea = true; } area.startX = areaPath.xMap; area.shiftUnit = options.step ? 2 : 1; }); }, drawLegendSymbol: LegendSymbolMixin.drawRectangle }); /** * A `area` series. If the [type](#series.area.type) option is not * specified, it is inherited from [chart.type](#chart.type). * * @extends series,plotOptions.area * @excluding dataParser, dataURL, useOhlcData * @product highcharts highstock * @apioption series.area */ /** * An array of data points for the series. For the `area` series type, * points can be given in the following ways: * * 1. An array of numerical values. In this case, the numerical values will be * interpreted as `y` options. The `x` values will be automatically * calculated, either starting at 0 and incremented by 1, or from * `pointStart` * and `pointInterval` given in the series options. If the * axis has categories, these will be used. Example: * ```js * data: [0, 5, 3, 5] * ``` * * 2. An array of arrays with 2 values. In this case, the values correspond to * `x,y`. If the first value is a string, it is applied as the name of the * point, and the `x` value is inferred. * ```js * data: [ * [0, 9], * [1, 7], * [2, 6] * ] * ``` * * 3. An array of objects with named values. The following snippet shows only a * few settings, see the complete options set below. If the total number of * data points exceeds the series' * [turboThreshold](#series.area.turboThreshold), this option is not * available. * ```js * data: [{ * x: 1, * y: 9, * name: "Point2", * color: "#00FF00" * }, { * x: 1, * y: 6, * name: "Point1", * color: "#FF00FF" * }] * ``` * * @sample {highcharts} highcharts/chart/reflow-true/ * Numerical values * @sample {highcharts} highcharts/series/data-array-of-arrays/ * Arrays of numeric x and y * @sample {highcharts} highcharts/series/data-array-of-arrays-datetime/ * Arrays of datetime x and y * @sample {highcharts} highcharts/series/data-array-of-name-value/ * Arrays of point.name and y * @sample {highcharts} highcharts/series/data-array-of-objects/ * Config objects * * @type {Array|*>} * @extends series.line.data * @product highcharts highstock * @apioption series.area.data */