UNPKG

@amcharts/amcharts5

Version:
619 lines 23.8 kB
import { Container } from "../../core/render/Container"; import { p100 } from "../../core/util/Percent"; import { Graphics } from "../../core/render/Graphics"; import { Grid } from "./axes/Grid"; //import { Animations } from "../core/util/Animation"; import * as $type from "../../core/util/Type"; import * as $utils from "../../core/util/Utils"; import * as $math from "../../core/util/Math"; import * as $array from "../../core/util/Array"; import * as $object from "../../core/util/Object"; /** * Creates a chart cursor for an [[XYChart]]. * * @see {@link https://www.amcharts.com/docs/v5/charts/xy-chart/cursor/} for more info * @important */ export class XYCursor extends Container { constructor() { super(...arguments); this._alwaysShow = false; /** * A [[Grid]] elment that used for horizontal line of the cursor crosshair. * * @default Grid.new() */ this.lineX = this.children.push(Grid.new(this._root, { themeTags: ["x"] })); /** * A [[Grid]] elment that used for horizontal line of the cursor crosshair. * * @default Grid.new() */ this.lineY = this.children.push(Grid.new(this._root, { themeTags: ["y"] })); /** * An element that represents current selection. * * @default Graphics.new() */ this.selection = this.children.push(Graphics.new(this._root, { themeTags: ["selection", "cursor"], layer: 30 })._markC("themeTags", "layer")); this._lastPoint = { x: 0, y: 0 }; this._lastPoint2 = { x: 0, y: 0 }; this._tooltipX = false; this._tooltipY = false; } _afterNew() { this._settings.themeTags = $utils.mergeTags(this._settings.themeTags, ["xy", "cursor"]); super._afterNew(); this._setCAll({ "width": p100, height: p100, isMeasured: true, position: "absolute" }); this.states._createC("hidden", { visible: true, opacity: 0 }); this._drawLines(); this.setPrivateRaw("visible", false); this._disposers.push(this.setTimeout(() => { this.setPrivate("visible", true); }, 500)); this._disposers.push(this.lineX.events.on("positionchanged", () => { this._handleXLine(); })); this._disposers.push(this.lineY.events.on("positionchanged", () => { this._handleYLine(); })); this._disposers.push(this.lineX.events.on("focus", () => this._handleLineFocus())); this._disposers.push(this.lineX.events.on("blur", () => this._handleLineBlur())); this._disposers.push(this.lineY.events.on("focus", () => this._handleLineFocus())); this._disposers.push(this.lineY.events.on("blur", () => this._handleLineBlur())); if ($utils.supports("keyboardevents")) { this._disposers.push($utils.addEventListener(document, "keydown", (ev) => { this._handleLineMove($utils.getEventKey(ev), ev.ctrlKey); })); } } _setUpTouch() { const chart = this.chart; if (chart) { chart.plotContainer._display.cancelTouch = this.get("behavior") != "none" ? true : false; } } _handleXLine() { let x = this.lineX.x(); let visible = true; if (x < 0 || x > this.width()) { visible = false; } this.lineX.setPrivate("visible", visible); } _handleYLine() { let y = this.lineY.y(); let visible = true; if (y < 0 || y > this.height()) { visible = false; } this.lineY.setPrivate("visible", visible); } _handleLineMove(key, ctrlKey) { let dir = ""; const chart = this.chart; let axis; if (chart && chart.xAxes.length) { if (this._root.focused(this.lineX)) { dir = "positionX"; axis = chart.xAxes.getIndex(0); } else if (this._root.focused(this.lineY)) { axis = chart.yAxes.getIndex(0); dir = "positionY"; } let m = 1; if (ctrlKey) { m = 5; } if (axis) { let inversed = axis.get("renderer").get("inversed", false); let step; if (key == "ArrowRight" || key == "ArrowDown") { step = 1; } else if (key == "ArrowLeft" || key == "ArrowUp") { step = -1; } else if (key == "Tab") { step = 0; } if (step != null) { if (inversed) { step *= -1; } this.set(dir, axis.nextPosition(step * m)); } } } } _handleLineFocus() { this._alwaysShow = this.get("alwaysShow", false); this.setAll({ positionX: this.getPrivate("positionX", 0), positionY: this.getPrivate("positionY", 0), alwaysShow: true }); this._handleLineMove("Tab"); } _handleLineBlur() { if (this.lineX.isFocus() || this.lineY.isFocus()) { this.setAll({ positionX: undefined, positionY: undefined, alwaysShow: this._alwaysShow }); } } _prepareChildren() { super._prepareChildren(); if (this.isDirty("xAxis")) { this._tooltipX = false; const xAxis = this.get("xAxis"); if (xAxis) { const tooltip = xAxis.get("tooltip"); if (tooltip) { this._tooltipX = true; this._disposers.push(tooltip.on("pointTo", () => { this._updateXLine(tooltip); })); } } } if (this.isDirty("yAxis")) { this._tooltipY = false; const yAxis = this.get("yAxis"); if (yAxis) { const tooltip = yAxis.get("tooltip"); if (tooltip) { this._tooltipY = true; this._disposers.push(tooltip.on("pointTo", () => { this._updateYLine(tooltip); })); } } } } _handleSyncWith() { const chart = this.chart; if (chart) { const syncWith = this.get("syncWith"); const otherCharts = []; if (syncWith) { $array.each(syncWith, (cursor) => { const chart = cursor.chart; if (chart) { otherCharts.push(chart); } }); } chart._otherCharts = otherCharts; } } _updateChildren() { super._updateChildren(); this._handleSyncWith(); if (this.isDirty("positionX") || this.isDirty("positionY")) { const positionX = this.get("positionX"); const positionY = this.get("positionY"); if (positionX == null && positionY == null) { this.hide(0); } else { this._movePoint = this.toGlobal(this._getPoint(this.get("positionX", 0), this.get("positionY", 0))); this.handleMove(); } } } _updateXLine(tooltip) { let x = $math.round(this._display.toLocal(tooltip.get("pointTo", { x: 0, y: 0 })).x, 3); if (this._toX != x) { this.lineX.animate({ key: "x", to: x, duration: tooltip.get("animationDuration", 0), easing: tooltip.get("animationEasing") }); this._toX = x; } } _updateYLine(tooltip) { let y = $math.round(this._display.toLocal(tooltip.get("pointTo", { x: 0, y: 0 })).y, 3); if (this._toY != y) { this.lineY.animate({ key: "y", to: y, duration: tooltip.get("animationDuration", 0), easing: tooltip.get("animationEasing") }); this._toY = y; } } _drawLines() { this.lineX.set("draw", (display) => { display.moveTo(0, 0); display.lineTo(0, this.height()); }); this.lineY.set("draw", (display) => { display.moveTo(0, 0); display.lineTo(this.width(), 0); }); } updateCursor() { if (this.get("alwaysShow")) { this._movePoint = this.toGlobal(this._getPoint(this.get("positionX", 0), this.get("positionY", 0))); } this.handleMove(); } _setChart(chart) { this.chart = chart; this._handleSyncWith(); const plotContainer = chart.plotContainer; this.events.on("boundschanged", () => { if (this._boundsTimeout) { this._boundsTimeout.dispose(); } this._boundsTimeout = this.setTimeout(() => { this.updateCursor(); }, 50); this._disposers.push(this._boundsTimeout); }); //this._display.interactive = true; if ($utils.supports("touchevents")) { this._disposers.push(plotContainer.events.on("click", (event) => { if ($utils.isTouchEvent(event.originalEvent)) { this._handleMove(event); } })); this._setUpTouch(); } this._disposers.push(plotContainer.events.on("pointerdown", (event) => { this._handleCursorDown(event); })); this._disposers.push(plotContainer.events.on("globalpointerdown", (event) => { this._handleCursorDownGlobal(event); })); this._disposers.push(plotContainer.events.on("globalpointerup", (event) => { this._handleCursorUp(event); if (!event.native && !this.isHidden()) { this._handleMove(event); } })); this._disposers.push(plotContainer.events.on("globalpointermove", (event) => { if (!this.get("syncWith")) { // `_downPoint` counts as dragging too: a press outside the plot // area never populated `plotContainer._downPoints`. if ($object.keys(plotContainer._downPoints).length == 0 && !this._downPoint && !event.native && this.isHidden()) { // Ignore mouse movement if it originates on outside element and // we're not dragging. return; } } this._handleMove(event); if (Math.hypot(this._lastPoint2.x - event.point.x, this._lastPoint2.y - event.point.y) > 1) { this._handleLineBlur(); this._lastPoint2 = event.point; } })); const parent = this.parent; if (parent) { parent.children.moveValue(this.selection); } } _inPlot(point) { const chart = this.chart; if (chart) { return chart.inPlot(point); } return false; } _handleCursorDown(event) { if (event.originalEvent.button == 2) { return; } const rootPoint = event.point; let local = this._display.toLocal(rootPoint); const chart = this.chart; this.selection.set("draw", () => { }); if (chart && this._inPlot(local)) { this._startSelection(local, event); } } /** * Handles a press outside the plot area but within `clickTolerance`: the * press is clamped onto the plot edge and the selection starts there. * In-plot presses are handled by `_handleCursorDown` instead. */ _handleCursorDownGlobal(event) { const tolerance = this.get("clickTolerance", 0); if (tolerance <= 0 || this.get("behavior", "none") == "none") { return; } if (event.originalEvent.button == 2) { return; } const chart = this.chart; if (!chart) { return; } const local = this._display.toLocal(event.point); // In-plot presses are handled by the plot container's own pointerdown. if (this._inPlot(local)) { return; } const w = this.width(); const h = this.height(); // How far outside the plot rectangle the press is, per axis. const dx = Math.max(0, -local.x, local.x - w); const dy = Math.max(0, -local.y, local.y - h); if (dx > tolerance || dy > tolerance) { return; } local.x = $math.fitToRange(local.x, 0, w); local.y = $math.fitToRange(local.y, 0, h); this.selection.set("draw", () => { }); this._startSelection(local, event); } _startSelection(local, event) { this._downPoint = local; if (this.get("behavior") != "none") { this.selection.show(); const type = "selectstarted"; if (this.events.isEnabled(type)) { this.events.dispatch(type, { type: type, target: this, originalEvent: event.originalEvent }); } } let positionX = this._getPosition(local).x; let positionY = this._getPosition(local).y; this.setPrivate("downPositionX", positionX); this.setPrivate("downPositionY", positionY); } _handleCursorUp(event) { // TODO: handle multitouch if (this._downPoint) { const behavior = this.get("behavior", "none"); if (behavior != "none") { if (behavior.charAt(0) === "z") { this.selection.hide(); } const rootPoint = event.point; let local = this._display.toLocal(rootPoint); // The press was clamped into the plot area, so clamp the release // point too - otherwise a click within `clickTolerance` measures // as a drag and dispatches "selectended". local.x = $math.fitToRange(local.x, 0, this.width()); local.y = $math.fitToRange(local.y, 0, this.height()); const downPoint = this._downPoint; const moveThreshold = this.get("moveThreshold", 1); if (local && downPoint) { let dispatch = false; if (behavior === "zoomX" || behavior === "zoomXY" || behavior === "selectX" || behavior === "selectXY") { if (Math.abs(local.x - downPoint.x) > moveThreshold) { dispatch = true; } } if (behavior === "zoomY" || behavior === "zoomXY" || behavior === "selectY" || behavior === "selectXY") { if (Math.abs(local.y - downPoint.y) > moveThreshold) { dispatch = true; } } if (dispatch) { const type = "selectended"; if (this.events.isEnabled(type)) { this.events.dispatch(type, { type: type, target: this, originalEvent: event.originalEvent }); } } else { const type = "selectcancelled"; if (this.events.isEnabled(type)) { this.events.dispatch(type, { type: type, target: this, originalEvent: event.originalEvent }); } } } } } this._downPoint = undefined; } _handleMove(event) { if (this.getPrivate("visible")) { const chart = this.chart; if (chart && $object.keys(chart.plotContainer._downPoints).length > 1) { this._setC("forceHidden", true); return; } else { this._setC("forceHidden", false); } // TODO: handle multitouch const rootPoint = event.point; const lastPoint = this._lastPoint; if (Math.round(lastPoint.x) === Math.round(rootPoint.x) && Math.round(lastPoint.y) === Math.round(rootPoint.y)) { return; } this._lastPoint = rootPoint; this.setPrivate("lastPoint", rootPoint); this.handleMove({ x: rootPoint.x, y: rootPoint.y }, false, event.originalEvent); } } _getPosition(point) { return { x: point.x / this.width(), y: point.y / this.height() }; } /** * Moves the cursor to X/Y coordinates within chart container (`point`). * * If `skipEvent` parameter is set to `true`, the move will not invoke * the `"cursormoved"` event. * * @param point X/Y to move cursor to * @param skipEvent Do not fire "cursormoved" event */ handleMove(point, skipEvent, originalEvent) { if (!point) { point = this._movePoint; } const alwaysShow = this.get("alwaysShow"); if (!point) { this.hide(0); return; } this._movePoint = point; let local = this._display.toLocal(point); let chart = this.chart; if (chart && (this._inPlot(local) || this._downPoint)) { chart._movePoint = point; if (this.isHidden()) { this.show(); const behavior = this.get("behavior", ""); if (behavior.charAt(0) == "z") { this.selection.set("draw", () => { }); } } let x = local.x; let y = local.y; let xyPos = this._getPosition(local); this.setPrivate("point", local); let snapToSeries = this.get("snapToSeries"); if (this._downPoint) { snapToSeries = undefined; } let userPositionX = this.get("positionX"); let positionX = xyPos.x; if ($type.isNumber(userPositionX)) { positionX = userPositionX; } let userPositionY = this.get("positionY"); let positionY = xyPos.y; if ($type.isNumber(userPositionY)) { positionY = userPositionY; } this.setPrivate("positionX", positionX); this.setPrivate("positionY", positionY); const xy = this._getPoint(positionX, positionY); x = xy.x; y = xy.y; chart.xAxes.each((axis) => { axis._handleCursorPosition(positionX, snapToSeries); if (alwaysShow) { axis.handleCursorShow(); } }); chart.yAxes.each((axis) => { axis._handleCursorPosition(positionY, snapToSeries); if (alwaysShow) { axis.handleCursorShow(); } }); if (!skipEvent) { chart._handleCursorPosition(); const type = "cursormoved"; if (this.events.isEnabled(type)) { this.events.dispatch(type, { type: type, target: this, point: point, originalEvent: originalEvent }); } } this._updateLines(x, y); chart.arrangeTooltips(); } else if (!this._downPoint) { if (!alwaysShow) { this.hide(0); const type = "cursorhidden"; if (this.events.isEnabled(type)) { this.events.dispatch(type, { type: type, target: this }); } } } if (this._downPoint && this.get("behavior") != "none") { this._updateSelection(local); } } _getPoint(positionX, positionY) { return { x: this.width() * positionX, y: this.height() * positionY }; } _updateLines(x, y) { if (!this._tooltipX) { this.lineX._setC("x", x); } if (!this._tooltipY) { this.lineY._setC("y", y); } this._drawLines(); } _updateSelection(point) { const selection = this.selection; const behavior = this.get("behavior"); const w = this.width(); const h = this.height(); if (point.x < 0) { point.x = 0; } if (point.x > w) { point.x = w; } if (point.y < 0) { point.y = 0; } if (point.y > h) { point.y = h; } selection.set("draw", (display) => { const downPoint = this._downPoint; if (downPoint) { if (behavior === "zoomXY" || behavior === "selectXY") { display.moveTo(downPoint.x, downPoint.y); display.lineTo(downPoint.x, point.y); display.lineTo(point.x, point.y); display.lineTo(point.x, downPoint.y); display.lineTo(downPoint.x, downPoint.y); } else if (behavior === "zoomX" || behavior === "selectX") { display.moveTo(downPoint.x, 0); display.lineTo(downPoint.x, h); display.lineTo(point.x, h); display.lineTo(point.x, 0); display.lineTo(downPoint.x, 0); } else if (behavior === "zoomY" || behavior === "selectY") { display.moveTo(0, downPoint.y); display.lineTo(w, downPoint.y); display.lineTo(w, point.y); display.lineTo(0, point.y); display.lineTo(0, downPoint.y); } } }); } _onHide() { if (this.isHidden()) { let chart = this.chart; if (chart) { chart.xAxes.each((axis) => { axis.handleCursorHide(); }); chart.yAxes.each((axis) => { axis.handleCursorHide(); }); chart.series.each((series) => { series.handleCursorHide(); }); } } super._onHide(); } _onShow() { if (!this.isHidden()) { let chart = this.chart; if (chart) { chart.xAxes.each((axis) => { axis.handleCursorShow(); }); chart.yAxes.each((axis) => { axis.handleCursorShow(); }); } } super._onShow(); } _dispose() { super._dispose(); this.selection.dispose(); } } XYCursor.className = "XYCursor"; XYCursor.classNames = Container.classNames.concat([XYCursor.className]); //# sourceMappingURL=XYCursor.js.map