UNPKG

highcharts

Version:
812 lines (811 loc) 27.1 kB
/* * * * !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!! * * */ 'use strict'; import Annotation from '../Annotation.js'; import ControlPoint from '../ControlPoint.js'; import U from '../../../Core/Utilities.js'; const { defined, extend, isNumber, merge, pick } = U; /* * * * * Functions * * */ /** * @private */ function average() { let average = 0, pointsTotal = 0, pointsAmount = 0; const series = this.chart.series, ext = getExtremes(this.xAxisMin, this.xAxisMax, this.yAxisMin, this.yAxisMax); series.forEach((s) => { if (s.visible && s.options.id !== 'highcharts-navigator-series') { s.points.forEach((point) => { if (isPointWithinExtremes(point, ext) && isNumber(point.y)) { pointsTotal += point.y; pointsAmount++; } }); } }); if (pointsAmount > 0) { average = pointsTotal / pointsAmount; } return average; } /** * @private */ function isPointWithinExtremes(point, ext) { return (!point.isNull && isNumber(point.y) && point.x > ext.xAxisMin && point.x <= ext.xAxisMax && point.y > ext.yAxisMin && point.y <= ext.yAxisMax); } /** * @private */ function bins() { const series = this.chart.series, ext = getExtremes(this.xAxisMin, this.xAxisMax, this.yAxisMin, this.yAxisMax); let bins = 0; series.forEach((s) => { if (s.visible && s.options.id !== 'highcharts-navigator-series') { s.points.forEach((point) => { if (isPointWithinExtremes(point, ext)) { bins++; } }); } }); return bins; } /** * Default formatter of label's content * @private */ function defaultFormatter() { return 'Min: ' + this.min + '<br>Max: ' + this.max + '<br>Average: ' + this.average.toFixed(2) + '<br>Bins: ' + this.bins; } /** * Set values for xAxisMin, xAxisMax, yAxisMin, yAxisMax, also * when chart is inverted * @private */ function getExtremes(xAxisMin, xAxisMax, yAxisMin, yAxisMax) { return { xAxisMin: Math.min(xAxisMax, xAxisMin), xAxisMax: Math.max(xAxisMax, xAxisMin), yAxisMin: Math.min(yAxisMax, yAxisMin), yAxisMax: Math.max(yAxisMax, yAxisMin) }; } /** * Set current xAxisMin, xAxisMax, yAxisMin, yAxisMax. * Calculations of measure values (min, max, average, bins). * @private * @param {Highcharts.Axis} axis * X or y axis reference * @param {number} value * Point's value (x or y) * @param {number} offset * Amount of pixels */ function getPointPos(axis, value, offset) { return axis.toValue(axis.toPixels(value) + offset); } /** * Set starting points * @private */ function init() { const options = this.options.typeOptions, chart = this.chart, inverted = chart.inverted, xAxis = chart.xAxis[options.xAxis], yAxis = chart.yAxis[options.yAxis], bg = options.background, width = inverted ? bg.height : bg.width, height = inverted ? bg.width : bg.height, selectType = options.selectType, top = inverted ? xAxis.left : yAxis.top, // #13664 left = inverted ? yAxis.top : xAxis.left; // #13664 this.startXMin = options.point.x; this.startYMin = options.point.y; if (isNumber(width)) { this.startXMax = this.startXMin + width; } else { this.startXMax = getPointPos(xAxis, this.startXMin, parseFloat(width)); } if (isNumber(height)) { this.startYMax = this.startYMin - height; } else { this.startYMax = getPointPos(yAxis, this.startYMin, parseFloat(height)); } // X / y selection type if (selectType === 'x') { this.startYMin = yAxis.toValue(top); this.startYMax = yAxis.toValue(top + yAxis.len); } else if (selectType === 'y') { this.startXMin = xAxis.toValue(left); this.startXMax = xAxis.toValue(left + xAxis.len); } } /** * @private */ function max() { const series = this.chart.series, ext = getExtremes(this.xAxisMin, this.xAxisMax, this.yAxisMin, this.yAxisMax); let max = -Infinity, isCalculated = false; // To avoid Infinity in formatter series.forEach((s) => { if (s.visible && s.options.id !== 'highcharts-navigator-series') { s.points.forEach((point) => { if (isNumber(point.y) && point.y > max && isPointWithinExtremes(point, ext)) { max = point.y; isCalculated = true; } }); } }); if (!isCalculated) { max = 0; } return max; } /** * Definitions of calculations (min, max, average, bins) * @private */ function min() { const series = this.chart.series, ext = getExtremes(this.xAxisMin, this.xAxisMax, this.yAxisMin, this.yAxisMax); let min = Infinity, isCalculated = false; // To avoid Infinity in formatter series.forEach((s) => { if (s.visible && s.options.id !== 'highcharts-navigator-series') { s.points.forEach((point) => { if (isNumber(point.y) && point.y < min && isPointWithinExtremes(point, ext)) { min = point.y; isCalculated = true; } }); } }); if (!isCalculated) { min = 0; } return min; } /** * Set current xAxisMin, xAxisMax, yAxisMin, yAxisMax. * Calculations of measure values (min, max, average, bins). * @private * @param {boolean} [resize] * Flag if shape is resized. */ function recalculate(resize) { const options = this.options.typeOptions, xAxis = this.chart.xAxis[options.xAxis], yAxis = this.chart.yAxis[options.yAxis], offsetX = this.offsetX, offsetY = this.offsetY; this.xAxisMin = getPointPos(xAxis, this.startXMin, offsetX); this.xAxisMax = getPointPos(xAxis, this.startXMax, offsetX); this.yAxisMin = getPointPos(yAxis, this.startYMin, offsetY); this.yAxisMax = getPointPos(yAxis, this.startYMax, offsetY); this.min = min.call(this); this.max = max.call(this); this.average = average.call(this); this.bins = bins.call(this); if (resize) { this.resize(0, 0); } } /** * Update position of start points * (startXMin, startXMax, startYMin, startYMax) * @private * @param {boolean} redraw * Flag if shape is redraw * @param {boolean} resize * Flag if shape is resized * @param {number} cpIndex * Index of controlPoint */ function updateStartPoints(redraw, resize, cpIndex, dx, dy) { const options = this.options.typeOptions, selectType = options.selectType, xAxis = this.chart.xAxis[options.xAxis], yAxis = this.chart.yAxis[options.yAxis], startXMin = this.startXMin, startXMax = this.startXMax, startYMin = this.startYMin, startYMax = this.startYMax, offsetX = this.offsetX, offsetY = this.offsetY; if (resize) { if (selectType === 'x') { if (cpIndex === 0) { this.startXMin = getPointPos(xAxis, startXMin, dx); } else { this.startXMax = getPointPos(xAxis, startXMax, dx); } } else if (selectType === 'y') { if (cpIndex === 0) { this.startYMin = getPointPos(yAxis, startYMin, dy); } else { this.startYMax = getPointPos(yAxis, startYMax, dy); } } else { this.startXMax = getPointPos(xAxis, startXMax, dx); this.startYMax = getPointPos(yAxis, startYMax, dy); } } if (redraw) { this.startXMin = getPointPos(xAxis, startXMin, offsetX); this.startXMax = getPointPos(xAxis, startXMax, offsetX); this.startYMin = getPointPos(yAxis, startYMin, offsetY); this.startYMax = getPointPos(yAxis, startYMax, offsetY); this.offsetX = 0; this.offsetY = 0; } this.options.typeOptions.point = { x: this.startXMin, y: this.startYMin }; // We need to update userOptions as well as they are used in // the Annotation.update() method to initialize the annotation, #19121. this.userOptions.typeOptions.point = { x: this.startXMin, y: this.startYMin }; } /* * * * Class * * */ class Measure extends Annotation { /* * * * Functions * * */ /** * Init annotation object. * @private */ init(annotationOrChart, userOptions, index) { super.init(annotationOrChart, userOptions, index); this.offsetX = 0; this.offsetY = 0; this.resizeX = 0; this.resizeY = 0; init.call(this); this.addValues(); this.addShapes(); } /** * Overrides default setter to get axes from typeOptions. * @private */ setClipAxes() { this.clipXAxis = this.chart.xAxis[this.options.typeOptions.xAxis]; this.clipYAxis = this.chart.yAxis[this.options.typeOptions.yAxis]; } /** * Get points configuration objects for shapes. * @private */ shapePointsOptions() { const options = this.options.typeOptions, xAxis = options.xAxis, yAxis = options.yAxis; return [ { x: this.xAxisMin, y: this.yAxisMin, xAxis: xAxis, yAxis: yAxis }, { x: this.xAxisMax, y: this.yAxisMin, xAxis: xAxis, yAxis: yAxis }, { x: this.xAxisMax, y: this.yAxisMax, xAxis: xAxis, yAxis: yAxis }, { x: this.xAxisMin, y: this.yAxisMax, xAxis: xAxis, yAxis: yAxis }, { command: 'Z' } ]; } addControlPoints() { const inverted = this.chart.inverted, options = this.options.controlPointOptions, selectType = this.options.typeOptions.selectType; if (!defined(this.userOptions.controlPointOptions?.style?.cursor)) { if (selectType === 'x') { options.style.cursor = inverted ? 'ns-resize' : 'ew-resize'; } else if (selectType === 'y') { options.style.cursor = inverted ? 'ew-resize' : 'ns-resize'; } } let controlPoint = new ControlPoint(this.chart, this, this.options.controlPointOptions, 0); this.controlPoints.push(controlPoint); // Add extra controlPoint for horizontal and vertical range if (selectType !== 'xy') { controlPoint = new ControlPoint(this.chart, this, this.options.controlPointOptions, 1); this.controlPoints.push(controlPoint); } } /** * Add label with calculated values (min, max, average, bins). * @private * @param {boolean} [resize] * The flag for resize shape */ addValues(resize) { const typeOptions = this.options.typeOptions, formatter = typeOptions.label.formatter; // Set xAxisMin, xAxisMax, yAxisMin, yAxisMax recalculate.call(this, resize); if (!typeOptions.label.enabled) { return; } if (this.labels.length > 0) { (this.labels[0]).text = ((formatter && formatter.call(this)) || defaultFormatter.call(this)); } else { this.initLabel(extend({ shape: 'rect', backgroundColor: 'none', color: 'black', borderWidth: 0, dashStyle: 'Dash', overflow: 'allow', align: 'left', y: 0, x: 0, verticalAlign: 'top', crop: true, xAxis: 0, yAxis: 0, point: function (target) { const annotation = target.annotation, options = target.options; return { x: annotation.xAxisMin, y: annotation.yAxisMin, xAxis: pick(typeOptions.xAxis, options.xAxis), yAxis: pick(typeOptions.yAxis, options.yAxis) }; }, text: ((formatter && formatter.call(this)) || defaultFormatter.call(this)) }, typeOptions.label), void 0); } } /** * Crosshair, background (rect). * @private */ addShapes() { this.addCrosshairs(); this.addBackground(); } /** * Add background shape. * @private */ addBackground() { const shapePoints = this.shapePointsOptions(); if (typeof shapePoints[0].x === 'undefined') { return; } this.initShape(extend({ type: 'path', points: shapePoints, className: 'highcharts-measure-background' }, this.options.typeOptions.background), 2); } /** * Add internal crosshair shapes (on top and bottom). * @private */ addCrosshairs() { const chart = this.chart, options = this.options.typeOptions, point = this.options.typeOptions.point, xAxis = chart.xAxis[options.xAxis], yAxis = chart.yAxis[options.yAxis], inverted = chart.inverted, defaultOptions = { point: point, type: 'path' }; let xAxisMin = xAxis.toPixels(this.xAxisMin), xAxisMax = xAxis.toPixels(this.xAxisMax), yAxisMin = yAxis.toPixels(this.yAxisMin), yAxisMax = yAxis.toPixels(this.yAxisMax), pathH = [], pathV = [], crosshairOptionsX, crosshairOptionsY, temp; if (inverted) { temp = xAxisMin; xAxisMin = yAxisMin; yAxisMin = temp; temp = xAxisMax; xAxisMax = yAxisMax; yAxisMax = temp; } // Horizontal line if (options.crosshairX.enabled) { pathH = [[ 'M', xAxisMin, yAxisMin + ((yAxisMax - yAxisMin) / 2) ], [ 'L', xAxisMax, yAxisMin + ((yAxisMax - yAxisMin) / 2) ]]; } // Vertical line if (options.crosshairY.enabled) { pathV = [[ 'M', xAxisMin + ((xAxisMax - xAxisMin) / 2), yAxisMin ], [ 'L', xAxisMin + ((xAxisMax - xAxisMin) / 2), yAxisMax ]]; } // Update existed crosshair if (this.shapes.length > 0) { this.shapes[0].options.d = pathH; this.shapes[1].options.d = pathV; } else { // Add new crosshairs crosshairOptionsX = merge(defaultOptions, { className: 'highcharts-measure-crosshair-x' }, options.crosshairX); crosshairOptionsY = merge(defaultOptions, { className: 'highcharts-measure-crosshair-y' }, options.crosshairY); this.initShape(extend({ d: pathH }, crosshairOptionsX), 0); this.initShape(extend({ d: pathV }, crosshairOptionsY), 1); } } onDrag(e) { const translation = this.mouseMoveToTranslation(e), selectType = this.options.typeOptions.selectType, x = selectType === 'y' ? 0 : translation.x, y = selectType === 'x' ? 0 : translation.y; this.translate(x, y); this.offsetX += x; this.offsetY += y; // Animation, resize, setStartPoints this.redraw(false, false, true); } /** * Translate start or end ("left" or "right") side of the measure. * Update start points (startXMin, startXMax, startYMin, startYMax) * @private * @param {number} dx * the amount of x translation * @param {number} dy * the amount of y translation * @param {number} cpIndex * index of control point * @param {Highcharts.AnnotationDraggableValue} selectType * x / y / xy */ resize(dx, dy, cpIndex, selectType) { // Background shape const bckShape = this.shapes[2]; if (selectType === 'x') { if (cpIndex === 0) { bckShape.translatePoint(dx, 0, 0); bckShape.translatePoint(dx, dy, 3); } else { bckShape.translatePoint(dx, 0, 1); bckShape.translatePoint(dx, dy, 2); } } else if (selectType === 'y') { if (cpIndex === 0) { bckShape.translatePoint(0, dy, 0); bckShape.translatePoint(0, dy, 1); } else { bckShape.translatePoint(0, dy, 2); bckShape.translatePoint(0, dy, 3); } } else { bckShape.translatePoint(dx, 0, 1); bckShape.translatePoint(dx, dy, 2); bckShape.translatePoint(0, dy, 3); } updateStartPoints.call(this, false, true, cpIndex, dx, dy); this.options.typeOptions.background.height = Math.abs(this.startYMax - this.startYMin); this.options.typeOptions.background.width = Math.abs(this.startXMax - this.startXMin); } /** * Redraw event which render elements and update start points if needed. * @private * @param {boolean} animation * @param {boolean} [resize] * flag if resized * @param {boolean} [setStartPoints] * update position of start points */ redraw(animation, resize, setStartPoints) { this.linkPoints(); if (!this.graphic) { this.render(); } if (setStartPoints) { updateStartPoints.call(this, true, false); } // #11174 - clipBox was not recalculate during resize / redraw if (this.clipRect) { this.clipRect.animate(this.getClipBox()); } this.addValues(resize); this.addCrosshairs(); this.redrawItems(this.shapes, animation); this.redrawItems(this.labels, animation); const backgroundOptions = this.options.typeOptions.background; if (backgroundOptions?.strokeWidth && this.shapes[2]?.graphic) { const offset = (backgroundOptions.strokeWidth) / 2; const background = this.shapes[2]; const path = background.graphic.pathArray; const p1 = path[0]; const p2 = path[1]; const p3 = path[2]; const p4 = path[3]; p1[1] = (p1[1] || 0) + offset; p2[1] = (p2[1] || 0) - offset; p3[1] = (p3[1] || 0) - offset; p4[1] = (p4[1] || 0) + offset; p1[2] = (p1[2] || 0) + offset; p2[2] = (p2[2] || 0) + offset; p3[2] = (p3[2] || 0) - offset; p4[2] = (p4[2] || 0) - offset; background.graphic.attr({ d: path }); } // Redraw control point to run positioner this.controlPoints.forEach((controlPoint) => controlPoint.redraw()); } translate(dx, dy) { this.shapes.forEach((item) => item.translate(dx, dy)); } } Measure.prototype.defaultOptions = merge(Annotation.prototype.defaultOptions, /** * A measure annotation. * * @extends annotations.crookedLine * @excluding labels, labelOptions, shapes, shapeOptions * @sample highcharts/annotations-advanced/measure/ * Measure * @product highstock * @optionparent annotations.measure */ { typeOptions: { /** * Decides in what dimensions the user can resize by dragging the * mouse. Can be one of x, y or xy. */ selectType: 'xy', /** * This number defines which xAxis the point is connected to. * It refers to either the axis id or the index of the axis * in the xAxis array. */ xAxis: 0, /** * This number defines which yAxis the point is connected to. * It refers to either the axis id or the index of the axis * in the yAxis array. */ yAxis: 0, background: { /** * The color of the rectangle. */ fill: 'rgba(130, 170, 255, 0.4)', /** * The width of border. */ strokeWidth: 0, /** * The color of border. */ stroke: void 0 }, /** * Configure a crosshair that is horizontally placed in middle of * rectangle. * */ crosshairX: { /** * Enable or disable the horizontal crosshair. * */ enabled: true, /** * The Z index of the crosshair in annotation. */ zIndex: 6, /** * The dash or dot style of the crosshair's line. For possible * values, see * [this demonstration](https://jsfiddle.net/gh/get/library/pure/highcharts/highcharts/tree/master/samples/highcharts/plotoptions/series-dashstyle-all/). * * @type {Highcharts.DashStyleValue} * @default Dash */ dashStyle: 'Dash', /** * The marker-end defines the arrowhead that will be drawn * at the final vertex of the given crosshair's path. * * @type {string} * @default arrow */ markerEnd: 'arrow' }, /** * Configure a crosshair that is vertically placed in middle of * rectangle. */ crosshairY: { /** * Enable or disable the vertical crosshair. * */ enabled: true, /** * The Z index of the crosshair in annotation. */ zIndex: 6, /** * The dash or dot style of the crosshair's line. For possible * values, see * [this demonstration](https://jsfiddle.net/gh/get/library/pure/highcharts/highcharts/tree/master/samples/highcharts/plotoptions/series-dashstyle-all/). * * @type {Highcharts.DashStyleValue} * @default Dash * @apioption annotations.measure.typeOptions.crosshairY.dashStyle * */ dashStyle: 'Dash', /** * The marker-end defines the arrowhead that will be drawn * at the final vertex of the given crosshair's path. * * @type {string} * @default arrow * @validvalue ["none", "arrow"] * */ markerEnd: 'arrow' }, label: { /** * Enable or disable the label text (min, max, average, * bins values). * * Defaults to true. */ enabled: true, /** * CSS styles for the measure label. * * @type {Highcharts.CSSObject} * @default {"color": "#666666", "fontSize": "11px"} */ style: { fontSize: '0.7em', color: "#666666" /* Palette.neutralColor60 */ }, /** * Formatter function for the label text. * * Available data are: * * <table> * * <tbody> * * <tr> * * <td>`this.min`</td> * * <td>The minimum value of the points in the selected * range.</td> * * </tr> * * <tr> * * <td>`this.max`</td> * * <td>The maximum value of the points in the selected * range.</td> * * </tr> * * <tr> * * <td>`this.average`</td> * * <td>The average value of the points in the selected * range.</td> * * </tr> * * <tr> * * <td>`this.bins`</td> * * <td>The amount of the points in the selected range.</td> * * </tr> * * </table> * * @type {Function} * */ formatter: void 0 } }, controlPointOptions: { positioner: function (target) { const cpIndex = this.index, chart = target.chart, options = target.options, typeOptions = options.typeOptions, selectType = typeOptions.selectType, controlPointOptions = options.controlPointOptions, inverted = chart.inverted, xAxis = chart.xAxis[typeOptions.xAxis], yAxis = chart.yAxis[typeOptions.yAxis], ext = getExtremes(target.xAxisMin, target.xAxisMax, target.yAxisMin, target.yAxisMax); let targetX = target.xAxisMax, targetY = target.yAxisMax, x, y; if (selectType === 'x') { targetY = (ext.yAxisMax + ext.yAxisMin) / 2; // First control point if (cpIndex === 0) { targetX = target.xAxisMin; } } if (selectType === 'y') { targetX = ext.xAxisMin + ((ext.xAxisMax - ext.xAxisMin) / 2); // First control point if (cpIndex === 0) { targetY = target.yAxisMin; } } if (inverted) { x = yAxis.toPixels(targetY); y = xAxis.toPixels(targetX); } else { x = xAxis.toPixels(targetX); y = yAxis.toPixels(targetY); } return { x: x - (controlPointOptions.width / 2), y: y - (controlPointOptions.height / 2) }; }, events: { drag: function (e, target) { const translation = this.mouseMoveToTranslation(e), selectType = target.options.typeOptions.selectType, index = this.index, x = selectType === 'y' ? 0 : translation.x, y = selectType === 'x' ? 0 : translation.y; target.resize(x, y, index, selectType); target.resizeX += x; target.resizeY += y; target.redraw(false, true); } } } }); Annotation.types.measure = Measure; /* * * * Default Export * * */ export default Measure;