UNPKG

highcharts

Version:
611 lines (610 loc) 18.6 kB
/* * * * (c) 2009-2024 Highsoft AS * * Authors: Øystein Moseng, Torstein Hønsi, Jon A. Nygård * * License: www.highcharts.com/license * * !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!! * * */ 'use strict'; import DraggableChart from './DraggableChart.js'; const { flipResizeSide } = DraggableChart; import U from '../../Core/Utilities.js'; const { isNumber, merge, pick } = U; /* * * * Constants * * */ // Line series - only draggableX/Y, no drag handles const line = { x: { axis: 'x', move: true }, y: { axis: 'y', move: true } }; // Flag series - same as line/scatter const flags = line; // Column series - x can be moved, y can only be resized. Note extra // functionality for handling upside down columns (below threshold). const column = { x: { axis: 'x', move: true }, y: { axis: 'y', move: false, resize: true, // Force guideBox start coordinates beforeResize: (guideBox, pointVals, point) => { // We need to ensure that guideBox always starts at threshold. // We flip whether or not we update the top or bottom of the guide // box at threshold, but if we drag the mouse fast, the top has not // reached threshold before we cross over and update the bottom. const plotThreshold = pick(point.yBottom, // Added support for stacked series. (#18741) point.series.translatedThreshold), plotY = guideBox.attr('y'), threshold = isNumber(point.stackY) ? (point.stackY - (point.y || 0)) : point.series.options.threshold || 0, y = threshold + pointVals.y; let height, diff; if (point.series.yAxis.reversed ? y < threshold : y >= threshold) { // Above threshold - always set height to hit the threshold height = guideBox.attr('height'); diff = plotThreshold ? plotThreshold - plotY - height : 0; guideBox.attr({ height: Math.max(0, Math.round(height + diff)) }); } else { // Below - always set y to start at threshold guideBox.attr({ y: Math.round(plotY + (plotThreshold ? plotThreshold - plotY : 0)) }); } }, // Flip the side of the resize handle if column is below threshold. // Make sure we remove the handle on the other side. resizeSide: (pointVals, point) => { const chart = point.series.chart, dragHandles = chart.dragHandles, side = pointVals.y >= (point.series.options.threshold || 0) ? 'top' : 'bottom', flipSide = flipResizeSide(side); // Force remove handle on other side if (dragHandles && dragHandles[flipSide]) { dragHandles[flipSide].destroy(); delete dragHandles[flipSide]; } return side; }, // Position handle at bottom if column is below threshold handlePositioner: (point) => { const bBox = (point.shapeArgs || (point.graphic && point.graphic.getBBox()) || {}), reversed = point.series.yAxis.reversed, threshold = point.series.options.threshold || 0, y = point.y || 0, bottom = (!reversed && y >= threshold) || (reversed && y < threshold); return { x: bBox.x || 0, y: bottom ? (bBox.y || 0) : (bBox.y || 0) + (bBox.height || 0) }; }, // Horizontal handle handleFormatter: (point) => { const shapeArgs = point.shapeArgs || {}, radius = shapeArgs.r || 0, // Rounding of bar corners width = shapeArgs.width || 0, centerX = width / 2; return [ // Left wick ['M', radius, 0], ['L', centerX - 5, 0], // Circle ['A', 1, 1, 0, 0, 0, centerX + 5, 0], ['A', 1, 1, 0, 0, 0, centerX - 5, 0], // Right wick ['M', centerX + 5, 0], ['L', width - radius, 0] ]; } } }; // Boxplot series - move x, resize or move low/q1/q3/high const boxplot = { x: column.x, /** * Allow low value to be dragged individually. * * @type {boolean} * @default true * @requires modules/draggable-points * @apioption plotOptions.boxplot.dragDrop.draggableLow */ low: { optionName: 'draggableLow', axis: 'y', move: true, resize: true, resizeSide: 'bottom', handlePositioner: (point) => ({ x: point.shapeArgs.x || 0, y: point.lowPlot }), handleFormatter: column.y.handleFormatter, propValidate: (val, point) => (val <= point.q1) }, /** * Allow Q1 value to be dragged individually. * * @type {boolean} * @default true * @requires modules/draggable-points * @apioption plotOptions.boxplot.dragDrop.draggableQ1 */ q1: { optionName: 'draggableQ1', axis: 'y', move: true, resize: true, resizeSide: 'bottom', handlePositioner: (point) => ({ x: point.shapeArgs.x || 0, y: point.q1Plot }), handleFormatter: column.y.handleFormatter, propValidate: (val, point) => (val <= point.median && val >= point.low) }, median: { // Median cannot be dragged individually, just move the whole // point for this. axis: 'y', move: true }, /** * Allow Q3 value to be dragged individually. * * @type {boolean} * @default true * @requires modules/draggable-points * @apioption plotOptions.boxplot.dragDrop.draggableQ3 */ q3: { optionName: 'draggableQ3', axis: 'y', move: true, resize: true, resizeSide: 'top', handlePositioner: (point) => ({ x: point.shapeArgs.x || 0, y: point.q3Plot }), handleFormatter: column.y.handleFormatter, propValidate: (val, point) => (val <= point.high && val >= point.median) }, /** * Allow high value to be dragged individually. * * @type {boolean} * @default true * @requires modules/draggable-points * @apioption plotOptions.boxplot.dragDrop.draggableHigh */ high: { optionName: 'draggableHigh', axis: 'y', move: true, resize: true, resizeSide: 'top', handlePositioner: (point) => ({ x: point.shapeArgs.x || 0, y: point.highPlot }), handleFormatter: column.y.handleFormatter, propValidate: (val, point) => (val >= point.q3) } }; // Errorbar series - move x, resize or move low/high const errorbar = { x: column.x, low: { ...boxplot.low, propValidate: (val, point) => (val <= point.high) }, high: { ...boxplot.high, propValidate: (val, point) => (val >= point.low) } }; /** * @exclude draggableQ1, draggableQ3 * @optionparent plotOptions.errorbar.dragDrop */ // Bullet graph, x/y same as column, but also allow target to be dragged. const bullet = { x: column.x, y: column.y, /** * Allow target value to be dragged individually. * * @type {boolean} * @default true * @requires modules/draggable-points * @apioption plotOptions.bullet.dragDrop.draggableTarget */ target: { optionName: 'draggableTarget', axis: 'y', move: true, resize: true, resizeSide: 'top', handlePositioner: (point) => { const bBox = point.targetGraphic.getBBox(); return { x: point.barX, y: bBox.y + bBox.height / 2 }; }, handleFormatter: column.y.handleFormatter } }; // OHLC series - move x, resize or move open/high/low/close const ohlc = { x: column.x, /** * Allow low value to be dragged individually. * * @type {boolean} * @default true * @requires modules/draggable-points * @apioption plotOptions.ohlc.dragDrop.draggableLow */ low: { optionName: 'draggableLow', axis: 'y', move: true, resize: true, resizeSide: 'bottom', handlePositioner: (point) => ({ x: point.shapeArgs.x, y: point.plotLow }), handleFormatter: column.y.handleFormatter, propValidate: (val, point) => (val <= point.open && val <= point.close) }, /** * Allow high value to be dragged individually. * * @type {boolean} * @default true * @requires modules/draggable-points * @apioption plotOptions.ohlc.dragDrop.draggableHigh */ high: { optionName: 'draggableHigh', axis: 'y', move: true, resize: true, resizeSide: 'top', handlePositioner: (point) => ({ x: point.shapeArgs.x, y: point.plotHigh }), handleFormatter: column.y.handleFormatter, propValidate: (val, point) => (val >= point.open && val >= point.close) }, /** * Allow open value to be dragged individually. * * @type {boolean} * @default true * @requires modules/draggable-points * @apioption plotOptions.ohlc.dragDrop.draggableOpen */ open: { optionName: 'draggableOpen', axis: 'y', move: true, resize: true, resizeSide: (point) => (point.open >= point.close ? 'top' : 'bottom'), handlePositioner: (point) => ({ x: point.shapeArgs.x, y: point.plotOpen }), handleFormatter: column.y.handleFormatter, propValidate: (val, point) => (val <= point.high && val >= point.low) }, /** * Allow close value to be dragged individually. * * @type {boolean} * @default true * @requires modules/draggable-points * @apioption plotOptions.ohlc.dragDrop.draggableClose */ close: { optionName: 'draggableClose', axis: 'y', move: true, resize: true, resizeSide: (point) => (point.open >= point.close ? 'bottom' : 'top'), handlePositioner: (point) => ({ x: point.shapeArgs.x, y: point.plotClose }), handleFormatter: column.y.handleFormatter, propValidate: (val, point) => (val <= point.high && val >= point.low) } }; // Waterfall - mostly as column, but don't show drag handles for sum points const waterfall = { x: column.x, y: merge(column.y, { handleFormatter: (point) => (point.isSum || point.isIntermediateSum ? null : column?.y?.handleFormatter?.(point) || null) }) }; // Columnrange series - move x, resize or move low/high const columnrange = { x: { axis: 'x', move: true }, /** * Allow low value to be dragged individually. * * @type {boolean} * @default true * @requires modules/draggable-points * @apioption plotOptions.columnrange.dragDrop.draggableLow */ low: { optionName: 'draggableLow', axis: 'y', move: true, resize: true, resizeSide: 'bottom', handlePositioner: (point) => { const bBox = (point.shapeArgs || point.graphic.getBBox()); return { x: bBox.x || 0, y: (bBox.y || 0) + (bBox.height || 0) }; }, handleFormatter: column.y.handleFormatter, propValidate: (val, point) => (val <= point.high) }, /** * Allow high value to be dragged individually. * * @type {boolean} * @default true * @requires modules/draggable-points * @apioption plotOptions.columnrange.dragDrop.draggableHigh */ high: { optionName: 'draggableHigh', axis: 'y', move: true, resize: true, resizeSide: 'top', handlePositioner: (point) => { const bBox = (point.shapeArgs || point.graphic.getBBox()); return { x: bBox.x || 0, y: bBox.y || 0 }; }, handleFormatter: column.y.handleFormatter, propValidate: (val, point) => (val >= point.low) } }; // Arearange series - move x, resize or move low/high const arearange = { x: columnrange.x, /** * Allow low value to be dragged individually. * * @type {boolean} * @default true * @requires modules/draggable-points * @apioption plotOptions.arearange.dragDrop.draggableLow */ low: { optionName: 'draggableLow', axis: 'y', move: true, resize: true, resizeSide: 'bottom', handlePositioner: (point) => { const bBox = (point.graphics && point.graphics[0] && point.graphics[0].getBBox()); return bBox ? { x: bBox.x + bBox.width / 2, y: bBox.y + bBox.height / 2 } : { x: -999, y: -999 }; }, handleFormatter: arearangeHandleFormatter, propValidate: columnrange.low.propValidate }, /** * Allow high value to be dragged individually. * * @type {boolean} * @default true * @requires modules/draggable-points * @apioption plotOptions.arearange.dragDrop.draggableHigh */ high: { optionName: 'draggableHigh', axis: 'y', move: true, resize: true, resizeSide: 'top', handlePositioner: (point) => { const bBox = (point.graphics && point.graphics[1] && point.graphics[1].getBBox()); return bBox ? { x: bBox.x + bBox.width / 2, y: bBox.y + bBox.height / 2 } : { x: -999, y: -999 }; }, handleFormatter: arearangeHandleFormatter, propValidate: columnrange.high.propValidate } }; // Xrange - resize/move x/x2, and move y const xrange = { y: { axis: 'y', move: true }, /** * Allow x value to be dragged individually. * * @type {boolean} * @default true * @requires modules/draggable-points * @apioption plotOptions.xrange.dragDrop.draggableX1 */ x: { optionName: 'draggableX1', axis: 'x', move: true, resize: true, resizeSide: 'left', handlePositioner: (point) => (xrangeHandlePositioner(point, 'x')), handleFormatter: horizHandleFormatter, propValidate: (val, point) => (val <= point.x2) }, /** * Allow x2 value to be dragged individually. * * @type {boolean} * @default true * @requires modules/draggable-points * @apioption plotOptions.xrange.dragDrop.draggableX2 */ x2: { optionName: 'draggableX2', axis: 'x', move: true, resize: true, resizeSide: 'right', handlePositioner: (point) => (xrangeHandlePositioner(point, 'x2')), handleFormatter: horizHandleFormatter, propValidate: (val, point) => (val >= point.x) } }; // Gantt - same as xrange, but with aliases const gantt = { y: xrange.y, /** * Allow start value to be dragged individually. * * @type {boolean} * @default true * @requires modules/draggable-points * @apioption plotOptions.gantt.dragDrop.draggableStart */ start: merge(xrange.x, { optionName: 'draggableStart', // Do not allow individual drag handles for milestones validateIndividualDrag: (point) => (!point.milestone) }), /** * Allow end value to be dragged individually. * * @type {boolean} * @default true * @requires modules/draggable-points * @apioption plotOptions.gantt.dragDrop.draggableEnd */ end: merge(xrange.x2, { optionName: 'draggableEnd', // Do not allow individual drag handles for milestones validateIndividualDrag: (point) => (!point.milestone) }) }; /* * * * Functions * * */ /** * Use a circle covering the marker as drag handle. * @private */ function arearangeHandleFormatter(point) { const radius = point.graphic ? point.graphic.getBBox().width / 2 + 1 : 4; return [ ['M', 0 - radius, 0], ['a', radius, radius, 0, 1, 0, radius * 2, 0], ['a', radius, radius, 0, 1, 0, radius * -2, 0] ]; } /** * 90deg rotated column handle path, used in multiple series types. * @private */ function horizHandleFormatter(point) { const shapeArgs = point.shapeArgs || point.graphic.getBBox(), top = shapeArgs.r || 0, // Rounding of bar corners bottom = shapeArgs.height - top, centerY = shapeArgs.height / 2; return [ // Top wick ['M', 0, top], ['L', 0, centerY - 5], // Circle ['A', 1, 1, 0, 0, 0, 0, centerY + 5], ['A', 1, 1, 0, 0, 0, 0, centerY - 5], // Bottom wick ['M', 0, centerY + 5], ['L', 0, bottom] ]; } /** * Handle positioner logic is the same for x and x2 apart from the x value. * shapeArgs does not take yAxis reversed etc into account, so we use * axis.toPixels to handle positioning. * @private */ function xrangeHandlePositioner(point, xProp) { const series = point.series, xAxis = series.xAxis, yAxis = series.yAxis, inverted = series.chart.inverted, offsetY = series.columnMetrics ? series.columnMetrics.offset : -point.shapeArgs.height / 2; // Using toPixels handles axis.reversed, but doesn't take // chart.inverted into account. let newX = xAxis.toPixels(point[xProp], true), newY = yAxis.toPixels(point.y, true); // Handle chart inverted if (inverted) { newX = xAxis.len - newX; newY = yAxis.len - newY; } newY += offsetY; // (#12872) return { x: Math.round(newX), y: Math.round(newY) }; } /* * * * Default Export * * */ const DragDropProps = { arearange, boxplot, bullet, column, columnrange, errorbar, flags, gantt, line, ohlc, waterfall, xrange }; export default DragDropProps;