UNPKG

devextreme

Version:

HTML5 JavaScript Component Suite for Responsive Web Development

321 lines (318 loc) • 12.7 kB
/** * DevExtreme (esm/viz/chart_components/crosshair.js) * Version: 24.2.7 * Build date: Mon Apr 28 2025 * * Copyright (c) 2012 - 2025 Developer Express Inc. ALL RIGHTS RESERVED * Read about DevExtreme licensing here: https://js.devexpress.com/Licensing/ */ import { patchFontOptions } from "../core/utils"; import { extend } from "../../core/utils/extend"; const math = Math; const mathAbs = math.abs; const mathMin = math.min; const mathMax = math.max; const mathFloor = math.floor; const HORIZONTAL = "horizontal"; const VERTICAL = "vertical"; const LABEL_BACKGROUND_PADDING_X = 8; const LABEL_BACKGROUND_PADDING_Y = 4; const CENTER = "center"; const RIGHT = "right"; const LEFT = "left"; const TOP = "top"; const BOTTOM = "bottom"; export function getMargins() { return { x: 8, y: 4 } } function getRectangleBBox(bBox) { return { x: bBox.x - 8, y: bBox.y - 4, width: bBox.width + 16, height: bBox.height + 8 } } function getLabelCheckerPosition(x, y, isHorizontal, canvas) { const params = isHorizontal ? ["x", "width", "y", "height", y, 0] : ["y", "height", "x", "width", x, 1]; return function(bBox, position, coord) { const labelCoord = { x: coord.x, y: coord.y }; const rectangleBBox = getRectangleBBox(bBox); const delta = isHorizontal ? coord.y - bBox.y - bBox.height / 2 : coord.y - bBox.y; labelCoord.y = isHorizontal || !isHorizontal && position === BOTTOM ? coord.y + delta : coord.y; if (rectangleBBox[params[0]] < 0) { labelCoord[params[0]] -= rectangleBBox[params[0]] } else if (rectangleBBox[params[0]] + rectangleBBox[params[1]] + delta * params[5] > canvas[params[1]]) { labelCoord[params[0]] -= rectangleBBox[params[0]] + rectangleBBox[params[1]] + delta * params[5] - canvas[params[1]] } if (params[4] - rectangleBBox[params[3]] / 2 < 0) { labelCoord[params[2]] -= params[4] - rectangleBBox[params[3]] / 2 } else if (params[4] + rectangleBBox[params[3]] / 2 > canvas[params[3]]) { labelCoord[params[2]] -= params[4] + rectangleBBox[params[3]] / 2 - canvas[params[3]] } return labelCoord } } export function Crosshair(renderer, options, params, group) { this._renderer = renderer; this._crosshairGroup = group; this._options = {}; this.update(options, params) } Crosshair.prototype = { constructor: Crosshair, update: function(options, params) { const canvas = params.canvas; this._canvas = { top: canvas.top, bottom: canvas.height - canvas.bottom, left: canvas.left, right: canvas.width - canvas.right, width: canvas.width, height: canvas.height }; this._axes = params.axes; this._panes = params.panes; this._prepareOptions(options, HORIZONTAL); this._prepareOptions(options, VERTICAL) }, dispose: function() { this._renderer = this._crosshairGroup = this._options = this._axes = this._canvas = this._horizontalGroup = this._verticalGroup = this._horizontal = this._vertical = this._circle = this._panes = null }, _prepareOptions: function(options, direction) { const lineOptions = options[direction + "Line"]; this._options[direction] = { visible: lineOptions.visible, line: { stroke: lineOptions.color || options.color, "stroke-width": lineOptions.width || options.width, dashStyle: lineOptions.dashStyle || options.dashStyle, opacity: lineOptions.opacity || options.opacity, "stroke-linecap": "butt" }, label: extend(true, {}, options.label, lineOptions.label) } }, _createLines: function(options, sharpParam, group) { const lines = []; const canvas = this._canvas; const points = [canvas.left, canvas.top, canvas.left, canvas.top]; for (let i = 0; i < 2; i++) { lines.push(this._renderer.path(points, "line").attr(options).sharp(sharpParam).append(group)) } return lines }, render: function() { const that = this; const renderer = that._renderer; const options = that._options; const verticalOptions = options.vertical; const horizontalOptions = options.horizontal; const extraOptions = horizontalOptions.visible ? horizontalOptions.line : verticalOptions.line; const circleOptions = { stroke: extraOptions.stroke, "stroke-width": extraOptions["stroke-width"], dashStyle: extraOptions.dashStyle, opacity: extraOptions.opacity }; const canvas = that._canvas; that._horizontal = {}; that._vertical = {}; that._circle = renderer.circle(canvas.left, canvas.top, 0).attr(circleOptions).append(that._crosshairGroup); that._horizontalGroup = renderer.g().append(that._crosshairGroup); that._verticalGroup = renderer.g().append(that._crosshairGroup); if (verticalOptions.visible) { that._vertical.lines = that._createLines(verticalOptions.line, "h", that._verticalGroup); that._vertical.labels = that._createLabels(that._axes[0], verticalOptions, false, that._verticalGroup) } if (horizontalOptions.visible) { that._horizontal.lines = that._createLines(horizontalOptions.line, "v", that._horizontalGroup); that._horizontal.labels = that._createLabels(that._axes[1], horizontalOptions, true, that._horizontalGroup) } that.hide() }, _createLabels: function(axes, options, isHorizontal, group) { const canvas = this._canvas; const renderer = this._renderer; let x; let y; let text; const labels = []; let background; let currentLabelPos; const labelOptions = options.label; if (labelOptions.visible) { axes.forEach((function(axis) { const position = axis.getOptions().position; if (axis.getTranslator().getBusinessRange().isEmpty()) { return } currentLabelPos = axis.getLabelsPosition(); if (isHorizontal) { y = canvas.top; x = currentLabelPos } else { x = canvas.left; y = currentLabelPos } const align = position === TOP || position === BOTTOM ? CENTER : position === RIGHT ? LEFT : RIGHT; background = renderer.rect(0, 0, 0, 0).attr({ fill: labelOptions.backgroundColor || options.line.stroke }).append(group); text = renderer.text("0", 0, 0).css(patchFontOptions(options.label.font)).attr({ align: align, class: labelOptions.cssClass }).append(group); labels.push({ text: text, background: background, axis: axis, options: labelOptions, pos: { coord: currentLabelPos, side: position }, startXY: { x: x, y: y } }) })) } return labels }, _updateText: function(value, axisName, labels, point, func) { const that = this; labels.forEach((function(label) { const axis = label.axis; const coord = label.startXY; const textElement = label.text; const backgroundElement = label.background; let text = ""; if (!axis.name || axis.name === axisName) { text = axis.getFormattedValue(value, label.options, point) } if (text) { textElement.attr({ text: text, x: coord.x, y: coord.y }); textElement.attr(func(textElement.getBBox(), label.pos.side, coord)); that._updateLinesCanvas(label); backgroundElement.attr(getRectangleBBox(textElement.getBBox())) } else { textElement.attr({ text: "" }); backgroundElement.attr({ x: 0, y: 0, width: 0, height: 0 }) } })) }, hide: function() { this._crosshairGroup.attr({ visibility: "hidden" }) }, _updateLinesCanvas: function(label) { const position = label.pos.side; const labelCoord = label.pos.coord; const coords = this._linesCanvas; const canvas = this._canvas; coords[position] = coords[position] !== canvas[position] && mathAbs(coords[position] - canvas[position]) < mathAbs(labelCoord - canvas[position]) ? coords[position] : labelCoord }, _updateLines: function(lines, x, y, r, isHorizontal) { const coords = this._linesCanvas; const canvas = this._canvas; const points = isHorizontal ? [ [mathMin(x - r, coords.left), canvas.top, x - r, canvas.top], [x + r, canvas.top, mathMax(coords.right, x + r), canvas.top] ] : [ [canvas.left, mathMin(coords.top, y - r), canvas.left, y - r], [canvas.left, y + r, canvas.left, mathMax(coords.bottom, y + r)] ]; for (let i = 0; i < 2; i++) { lines[i].attr({ points: points[i] }).sharp(isHorizontal ? "v" : "h", isHorizontal ? y === canvas.bottom ? -1 : 1 : x === canvas.right ? -1 : 1) } }, _resetLinesCanvas: function() { const canvas = this._canvas; this._linesCanvas = { left: canvas.left, right: canvas.right, top: canvas.top, bottom: canvas.bottom } }, _getClipRectForPane: function(x, y) { const panes = this._panes; let i; let coords; for (i = 0; i < panes.length; i++) { coords = panes[i].coords; if (coords.left <= x && coords.right >= x && coords.top <= y && coords.bottom >= y) { return panes[i].clipRect } } return { id: null } }, show: function(data) { const that = this; const point = data.point; const pointData = point.getCrosshairData(data.x, data.y); const r = point.getPointRadius(); const horizontal = that._horizontal; const vertical = that._vertical; const rad = !r ? 0 : r + 3; const canvas = that._canvas; const x = mathFloor(pointData.x); const y = mathFloor(pointData.y); if (x >= canvas.left && x <= canvas.right && y >= canvas.top && y <= canvas.bottom) { that._crosshairGroup.attr({ visibility: "visible" }); that._resetLinesCanvas(); that._circle.attr({ cx: x, cy: y, r: rad, "clip-path": that._getClipRectForPane(x, y).id }); if (horizontal.lines) { that._updateText(pointData.yValue, pointData.axis, horizontal.labels, point, getLabelCheckerPosition(x, y, true, canvas)); that._updateLines(horizontal.lines, x, y, rad, true); that._horizontalGroup.attr({ translateY: y - canvas.top }) } if (vertical.lines) { that._updateText(pointData.xValue, pointData.axis, vertical.labels, point, getLabelCheckerPosition(x, y, false, canvas)); that._updateLines(vertical.lines, x, y, rad, false); that._verticalGroup.attr({ translateX: x - canvas.left }) } } else { that.hide() } } };