UNPKG

highcharts

Version:
1,026 lines (905 loc) 31 kB
/** * (c) 2010-2018 Torstein Honsi * * License: www.highcharts.com/license */ 'use strict'; import H from './Globals.js'; import './Utilities.js'; import './ColumnSeries.js'; import '../mixins/centered-series.js'; import './Legend.js'; import './Options.js'; import './Point.js'; import './Series.js'; var addEvent = H.addEvent, CenteredSeriesMixin = H.CenteredSeriesMixin, defined = H.defined, each = H.each, extend = H.extend, getStartAndEndRadians = CenteredSeriesMixin.getStartAndEndRadians, inArray = H.inArray, LegendSymbolMixin = H.LegendSymbolMixin, noop = H.noop, pick = H.pick, Point = H.Point, Series = H.Series, seriesType = H.seriesType, seriesTypes = H.seriesTypes, setAnimation = H.setAnimation; /** * Pie series type. * * @private * @class * @name Highcharts.seriesTypes.pie * * @augments Highcharts.Series */ seriesType('pie', 'line' /** * A pie chart is a circular graphic which is divided into slices to illustrate * numerical proportion. * * @sample highcharts/demo/pie-basic/ * Pie chart * * @extends plotOptions.line * @excluding animationLimit, boostThreshold, connectEnds, connectNulls, * cropThreshold, dashStyle, findNearestPointBy, * getExtremesFromAll, lineWidth, marker, negativeColor, * pointInterval, pointIntervalUnit, pointPlacement, * pointStart, softThreshold, stacking, step, threshold, * turboThreshold, zoneAxis, zones * @product highcharts * @optionparent plotOptions.pie */ , { /** * The center of the pie chart relative to the plot area. Can be percentages * or pixel values. The default behaviour (as of 3.0) is to center * the pie so that all slices and data labels are within the plot area. * As a consequence, the pie may actually jump around in a chart with * dynamic values, as the data labels move. In that case, the center * should be explicitly set, for example to `["50%", "50%"]`. * * @sample {highcharts} highcharts/plotoptions/pie-center/ * Centered at 100, 100 * * @type {Array<number|string|null>} * @default [null, null] * @product highcharts */ center: [null, null], /** * @product highcharts */ clip: false, /** * @ignore */ colorByPoint: true, // always true for pies /** * A series specific or series type specific color set to use instead * of the global [colors](#colors). * * @sample {highcharts} highcharts/demo/pie-monochrome/ * Set default colors for all pies * * @type {Array<Highcharts.ColorString>} * @since 3.0 * @product highcharts * @apioption plotOptions.pie.colors */ /** * @extends plotOptions.series.dataLabels * @excluding align, allowOverlap, staggerLines, step * @product highcharts */ dataLabels: { allowOverlap: true, /** * The color of the line connecting the data label to the pie slice. * The default color is the same as the point's color. * * In styled mode, the connector stroke is given in the * `.highcharts-data-label-connector` class. * * @sample {highcharts} highcharts/plotoptions/pie-datalabels-connectorcolor/ * Blue connectors * @sample {highcharts} highcharts/css/pie-point/ * Styled connectors * * @type {Highcharts.ColorString} * @since 2.1 * @product highcharts * @apioption plotOptions.pie.dataLabels.connectorColor */ /** * The distance from the data label to the connector. * * @sample {highcharts} highcharts/plotoptions/pie-datalabels-connectorpadding/ * No padding * * @type {number} * @default 5 * @since 2.1 * @product highcharts * @apioption plotOptions.pie.dataLabels.connectorPadding */ /** * The width of the line connecting the data label to the pie slice. * * * In styled mode, the connector stroke width is given in the * `.highcharts-data-label-connector` class. * * @sample {highcharts} highcharts/plotoptions/pie-datalabels-connectorwidth-disabled/ * Disable the connector * @sample {highcharts} highcharts/css/pie-point/ * Styled connectors * * @type {number} * @default 1 * @since 2.1 * @product highcharts * @apioption plotOptions.pie.dataLabels.connectorWidth */ /** * @sample {highcharts} highcharts/plotOptions/pie-datalabels-overflow * Long labels truncated with an ellipsis * @sample {highcharts} highcharts/plotOptions/pie-datalabels-overflow-wrap * Long labels are wrapped * * @type {Highcharts.CSSObject} * @apioption plotOptions.pie.dataLabels.style */ /** * The distance of the data label from the pie's edge. Negative numbers * put the data label on top of the pie slices. Connectors are only * shown for data labels outside the pie. * * @sample {highcharts} highcharts/plotoptions/pie-datalabels-distance/ * Data labels on top of the pie * * @since 2.1 * @product highcharts */ distance: 30, /** * Enable or disable the data labels. * * @since 2.1 * @product highcharts */ enabled: true, /** * @type {Highcharts.FormatterCallbackFunction} * @default function () { return this.point.name; } * @apioption plotOptions.pie.dataLabels.formatter */ formatter: function () { // #2945 return this.point.isNull ? undefined : this.point.name; }, /** * Whether to render the connector as a soft arc or a line with sharp * break. * * @sample {highcharts} highcharts/plotoptions/pie-datalabels-softconnector-true/ * Soft * @sample {highcharts} highcharts/plotoptions/pie-datalabels-softconnector-false/ * Non soft * * @type {number} * @since 2.1.7 * @product highcharts * @apioption plotOptions.pie.dataLabels.softConnector */ x: 0 }, /** * The end angle of the pie in degrees where 0 is top and 90 is right. * Defaults to `startAngle` plus 360. * * @sample {highcharts} highcharts/demo/pie-semi-circle/ * Semi-circle donut * * @type {number} * @since 1.3.6 * @product highcharts * @apioption plotOptions.pie.endAngle */ /** * Equivalent to [chart.ignoreHiddenSeries](#chart.ignoreHiddenSeries), * this option tells whether the series shall be redrawn as if the * hidden point were `null`. * * The default value changed from `false` to `true` with Highcharts * 3.0. * * @sample {highcharts} highcharts/plotoptions/pie-ignorehiddenpoint/ * True, the hiddden point is ignored * * @since 2.3.0 * @product highcharts */ ignoreHiddenPoint: true, /** * The size of the inner diameter for the pie. A size greater than 0 * renders a donut chart. Can be a percentage or pixel value. Percentages * are relative to the pie size. Pixel values are given as integers. * * * Note: in Highcharts < 4.1.2, the percentage was relative to the plot * area, not the pie size. * * @sample {highcharts} highcharts/plotoptions/pie-innersize-80px/ * 80px inner size * @sample {highcharts} highcharts/plotoptions/pie-innersize-50percent/ * 50% of the plot area * @sample {highcharts} highcharts/demo/3d-pie-donut/ * 3D donut * * @type {number|string} * @default 0 * @since 2.0 * @product highcharts * @apioption plotOptions.pie.innerSize */ /** * @ignore */ legendType: 'point', /** * @ignore */ marker: null, // point options are specified in the base options /** * The minimum size for a pie in response to auto margins. The pie will * try to shrink to make room for data labels in side the plot area, * but only to this size. * * @type {number} * @default 80 * @since 3.0 * @product highcharts * @apioption plotOptions.pie.minSize */ /** * The diameter of the pie relative to the plot area. Can be a percentage * or pixel value. Pixel values are given as integers. The default * behaviour (as of 3.0) is to scale to the plot area and give room * for data labels within the plot area. * [slicedOffset](#plotOptions.pie.slicedOffset) is also included * in the default size calculation. As a consequence, the size * of the pie may vary when points are updated and data labels more * around. In that case it is best to set a fixed value, for example * `"75%"`. * * @sample {highcharts} highcharts/plotoptions/pie-size/ * Smaller pie * * @type {number|string|null} * @product highcharts */ size: null, /** * Whether to display this particular series or series type in the * legend. Since 2.1, pies are not shown in the legend by default. * * @sample {highcharts} highcharts/plotoptions/series-showinlegend/ * One series in the legend, one hidden * * @product highcharts */ showInLegend: false, /** * If a point is sliced, moved out from the center, how many pixels * should it be moved?. * * @sample {highcharts} highcharts/plotoptions/pie-slicedoffset-20/ * 20px offset * * @product highcharts */ slicedOffset: 10, /** * The start angle of the pie slices in degrees where 0 is top and 90 * right. * * @sample {highcharts} highcharts/plotoptions/pie-startangle-90/ * Start from right * * @type {number} * @default 0 * @since 2.3.4 * @product highcharts * @apioption plotOptions.pie.startAngle */ /** * Sticky tracking of mouse events. When true, the `mouseOut` event * on a series isn't triggered until the mouse moves over another series, * or out of the plot area. When false, the `mouseOut` event on a * series is triggered when the mouse leaves the area around the series' * graph or markers. This also implies the tooltip. When `stickyTracking` * is false and `tooltip.shared` is false, the tooltip will be hidden * when moving the mouse between series. * * @product highcharts */ stickyTracking: false, tooltip: { followPointer: true } }, /** @lends seriesTypes.pie.prototype */ { isCartesian: false, requireSorting: false, directTouch: true, noSharedTooltip: true, trackerGroups: ['group', 'dataLabelsGroup'], axisTypes: [], pointAttribs: seriesTypes.column.prototype.pointAttribs, /** * Animate the pies in * * @private * @function Highcharts.seriesTypes.pie#animate * * @param {boolean} [init=false] */ animate: function (init) { var series = this, points = series.points, startAngleRad = series.startAngleRad; if (!init) { each(points, function (point) { var graphic = point.graphic, args = point.shapeArgs; if (graphic) { // start values graphic.attr({ // animate from inner radius (#779) r: point.startR || (series.center[3] / 2), start: startAngleRad, end: startAngleRad }); // animate graphic.animate({ r: args.r, start: args.start, end: args.end }, series.options.animation); } }); // delete this function to allow it only once series.animate = null; } }, /** * Recompute total chart sum and update percentages of points. * * @private * @function Highcharts.seriesTypes.pie#updateTotals */ updateTotals: function () { var i, total = 0, points = this.points, len = points.length, point, ignoreHiddenPoint = this.options.ignoreHiddenPoint; // Get the total sum for (i = 0; i < len; i++) { point = points[i]; total += (ignoreHiddenPoint && !point.visible) ? 0 : point.isNull ? 0 : point.y; } this.total = total; // Set each point's properties for (i = 0; i < len; i++) { point = points[i]; point.percentage = (total > 0 && (point.visible || !ignoreHiddenPoint)) ? point.y / total * 100 : 0; point.total = total; } }, /** * Extend the generatePoints method by adding total and percentage * properties to each point * * @private * @function Highcharts.seriesTypes.pie#generatePoints */ generatePoints: function () { Series.prototype.generatePoints.call(this); this.updateTotals(); }, /** * Do translation for pie slices * * @private * @function Highcharts.seriesTypes.pie#translate * * @param {Array<number>} positions */ translate: function (positions) { this.generatePoints(); var series = this, cumulative = 0, precision = 1000, // issue #172 options = series.options, slicedOffset = options.slicedOffset, connectorOffset = slicedOffset + (options.borderWidth || 0), finalConnectorOffset, start, end, angle, radians = getStartAndEndRadians( options.startAngle, options.endAngle ), startAngleRad = series.startAngleRad = radians.start, endAngleRad = series.endAngleRad = radians.end, circ = endAngleRad - startAngleRad, // 2 * Math.PI, points = series.points, radiusX, // the x component of the radius vector for a given point radiusY, labelDistance = options.dataLabels.distance, ignoreHiddenPoint = options.ignoreHiddenPoint, i, len = points.length, point; // Get positions - either an integer or a percentage string must be // given. If positions are passed as a parameter, we're in a recursive // loop for adjusting space for data labels. if (!positions) { series.center = positions = series.getCenter(); } // Utility for getting the x value from a given y, used for // anticollision logic in data labels. Added point for using specific // points' label distance. series.getX = function (y, left, point) { angle = Math.asin( Math.min( ( (y - positions[1]) / (positions[2] / 2 + point.labelDistance) ), 1 ) ); return positions[0] + (left ? -1 : 1) * (Math.cos(angle) * (positions[2] / 2 + point.labelDistance)); }; // Calculate the geometry for each point for (i = 0; i < len; i++) { point = points[i]; // Used for distance calculation for specific point. point.labelDistance = pick( point.options.dataLabels && point.options.dataLabels.distance, labelDistance ); // Saved for later dataLabels distance calculation. series.maxLabelDistance = Math.max( series.maxLabelDistance || 0, point.labelDistance ); // set start and end angle start = startAngleRad + (cumulative * circ); if (!ignoreHiddenPoint || point.visible) { cumulative += point.percentage / 100; } end = startAngleRad + (cumulative * circ); // set the shape point.shapeType = 'arc'; point.shapeArgs = { x: positions[0], y: positions[1], r: positions[2] / 2, innerR: positions[3] / 2, start: Math.round(start * precision) / precision, end: Math.round(end * precision) / precision }; // The angle must stay within -90 and 270 (#2645) angle = (end + start) / 2; if (angle > 1.5 * Math.PI) { angle -= 2 * Math.PI; } else if (angle < -Math.PI / 2) { angle += 2 * Math.PI; } // Center for the sliced out slice point.slicedTranslation = { translateX: Math.round(Math.cos(angle) * slicedOffset), translateY: Math.round(Math.sin(angle) * slicedOffset) }; // set the anchor point for tooltips radiusX = Math.cos(angle) * positions[2] / 2; radiusY = Math.sin(angle) * positions[2] / 2; point.tooltipPos = [ positions[0] + radiusX * 0.7, positions[1] + radiusY * 0.7 ]; point.half = angle < -Math.PI / 2 || angle > Math.PI / 2 ? 1 : 0; point.angle = angle; // Set the anchor point for data labels. Use point.labelDistance // instead of labelDistance // #1174 // finalConnectorOffset - not override connectorOffset value. finalConnectorOffset = Math.min( connectorOffset, point.labelDistance / 5 ); // #1678 point.labelPos = [ // first break of connector positions[0] + radiusX + Math.cos(angle) * point.labelDistance, positions[1] + radiusY + Math.sin(angle) * point.labelDistance, // second break, right outside pie positions[0] + radiusX + Math.cos(angle) * finalConnectorOffset, positions[1] + radiusY + Math.sin(angle) * finalConnectorOffset, // a/a positions[0] + radiusX, // landing point for connector positions[1] + radiusY, // a/a point.labelDistance < 0 ? // alignment 'center' : point.half ? 'right' : 'left', // alignment angle // center angle ]; } }, /** * @private * @deprecated * @name Highcharts.seriesTypes.pie#drawGraph * @type {null} */ drawGraph: null, /** * Draw the data points * * @private * @function Highcharts.seriesTypes.pie#drawPoints */ drawPoints: function () { var series = this, chart = series.chart, renderer = chart.renderer, groupTranslation, graphic, pointAttr, shapeArgs; // draw the slices each(series.points, function (point) { graphic = point.graphic; if (!point.isNull) { shapeArgs = point.shapeArgs; // If the point is sliced, use special translation, else use // plot area traslation groupTranslation = point.getTranslate(); // Draw the slice if (graphic) { graphic .setRadialReference(series.center) .animate(extend(shapeArgs, groupTranslation)); } else { point.graphic = graphic = renderer[point.shapeType]( shapeArgs ) .setRadialReference(series.center) .attr(groupTranslation) .add(series.group); } graphic.attr({ visibility: point.visible ? 'inherit' : 'hidden' }); graphic.addClass(point.getClassName()); } else if (graphic) { point.graphic = graphic.destroy(); } }); }, /** * @private * @deprecated * @function Highcharts.seriesTypes.pie#searchPoint */ searchPoint: noop, /** * Utility for sorting data labels * * @private * @function Highcharts.seriesTypes.pie#sortByAngle * * @param {Array<Highcharts.Point>} points * * @param {number} sign */ sortByAngle: function (points, sign) { points.sort(function (a, b) { return a.angle !== undefined && (b.angle - a.angle) * sign; }); }, /** * Use a simple symbol from LegendSymbolMixin. * * @private * @borrows Highcharts.LegendSymbolMixin.drawRectangle as Highcharts.seriesTypes.pie#drawLegendSymbol */ drawLegendSymbol: LegendSymbolMixin.drawRectangle, /** * Use the getCenter method from drawLegendSymbol. * * @private * @borrows Highcharts.CenteredSeriesMixin.getCenter as Highcharts.seriesTypes.pie#getCenter */ getCenter: CenteredSeriesMixin.getCenter, /** * Pies don't have point marker symbols. * * @deprecated * @private * @function Highcharts.seriesTypes.pie#getSymbol */ getSymbol: noop }, /** @lends seriesTypes.pie.prototype.pointClass.prototype */ { /** * Initiate the pie slice * * @private * @function Highcharts.seriesTypes.pie#pointClass#init * * @return {Highcharts.Point} */ init: function () { Point.prototype.init.apply(this, arguments); var point = this, toggleSlice; point.name = pick(point.name, 'Slice'); // add event listener for select toggleSlice = function (e) { point.slice(e.type === 'select'); }; addEvent(point, 'select', toggleSlice); addEvent(point, 'unselect', toggleSlice); return point; }, /** * Negative points are not valid (#1530, #3623, #5322) * * @private * @function Highcharts.seriesTypes.pie#pointClass#isValid * * @return {boolean} */ isValid: function () { return H.isNumber(this.y, true) && this.y >= 0; }, /** * Toggle the visibility of the pie slice * * @private * @function Highcharts.seriesTypes.pie#pointClass#setVisible * * @param {boolean} vis * Whether to show the slice or not. If undefined, the visibility is * toggled. * * @param {boolean} [redraw=false] */ 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[inArray(point, series.data)] = point.options; // Show and hide associated elements. This is performed regardless // of redraw or not, because chart.redraw only handles full series. each( ['graphic', 'dataLabel', 'connector', 'shadowGroup'], 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(); } } }, /** * Set or toggle whether the slice is cut out from the pie * * @private * @function Highcharts.seriesTypes.pie#pointClass#slice * * @param {boolean} sliced * When undefined, the slice state is toggled. * * @param {boolean} redraw * Whether to redraw the chart. True by default. */ slice: function (sliced, redraw, animation) { var point = this, series = point.series, chart = series.chart; setAnimation(animation, chart); // redraw is true by default redraw = pick(redraw, true); // if called without an argument, toggle point.sliced = point.options.sliced = sliced = defined(sliced) ? sliced : !point.sliced; // update userOptions.data series.options.data[inArray(point, series.data)] = point.options; point.graphic.animate(this.getTranslate()); }, /** * @private * @function Highcharts.seriesTypes.pie#pointClass#getTranslate * * @return {*} */ getTranslate: function () { return this.sliced ? this.slicedTranslation : { translateX: 0, translateY: 0 }; }, /** * @private * @function Highcharts.seriesTypes.pie#pointClass#haloPath * * @param {number} size * * @return {Highcharts.SVGPathArray} */ haloPath: function (size) { var shapeArgs = this.shapeArgs; return this.sliced || !this.visible ? [] : this.series.chart.renderer.symbols.arc( shapeArgs.x, shapeArgs.y, shapeArgs.r + size, shapeArgs.r + size, { // Substract 1px to ensure the background is not bleeding // through between the halo and the slice (#7495). innerR: this.shapeArgs.r - 1, start: shapeArgs.start, end: shapeArgs.end } ); } }); /** * A `pie` series. If the [type](#series.pie.type) option is not specified, * it is inherited from [chart.type](#chart.type). * * @extends series,plotOptions.pie * @excluding dataParser, dataURL, stack, xAxis, yAxis * @product highcharts * @apioption series.pie */ /** * An array of data points for the series. For the `pie` 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. Example: * * ```js * data: [0, 5, 3, 5] * ``` * * 2. 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.pie.turboThreshold), * this option is not available. * * ```js * data: [{ * y: 1, * name: "Point2", * color: "#00FF00" * }, { * y: 7, * name: "Point1", * color: "#FF00FF" * }]</pre> * * @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<number|*>} * @extends series.line.data * @excluding marker, x * @product highcharts * @apioption series.pie.data */ /** * Fires when the checkbox next to the point name in the legend is clicked. * One parameter, event, is passed to the function. The state of the * checkbox is found by event.checked. The checked item is found by * event.item. Return false to prevent the default action which is to * toggle the select state of the series. * * @sample {highcharts} highcharts/plotoptions/series-events-checkboxclick/ * Alert checkbox status * * @type {Function} * @since 1.2.0 * @product highcharts * @context Highcharts.Point * @apioption plotOptions.pie.events.checkboxClick */ /** * Not applicable to pies, as the legend item is per point. See point. * events. * * @type {Function} * @since 1.2.0 * @product highcharts * @apioption plotOptions.pie.events.legendItemClick */ /** * The sequential index of the data point in the legend. * * @type {number} * @product highcharts * @apioption series.pie.data.legendIndex */ /** * Fires when the legend item belonging to the pie point (slice) is * clicked. The `this` keyword refers to the point itself. One parameter, * `event`, is passed to the function, containing common event information. The * default action is to toggle the visibility of the point. This can be * prevented by calling `event.preventDefault()`. * * @sample {highcharts} highcharts/plotoptions/pie-point-events-legenditemclick/ * Confirm toggle visibility * * @type {Function} * @since 1.2.0 * @product highcharts * @apioption plotOptions.pie.point.events.legendItemClick */ /** * Whether to display a slice offset from the center. * * @sample {highcharts} highcharts/point/sliced/ * One sliced point * * @type {boolean} * @product highcharts * @apioption series.pie.data.sliced */