UNPKG

devextreme

Version:

HTML5 JavaScript Component Suite for Responsive Web Development

540 lines (536 loc) • 21.8 kB
/** * DevExtreme (esm/viz/core/annotations.js) * Version: 24.2.6 * Build date: Mon Mar 17 2025 * * Copyright (c) 2012 - 2025 Developer Express Inc. ALL RIGHTS RESERVED * Read about DevExtreme licensing here: https://js.devexpress.com/Licensing/ */ import domAdapter from "../../core/dom_adapter"; import { isDefined, isFunction } from "../../core/utils/type"; import { Tooltip } from "../core/tooltip"; import { extend } from "../../core/utils/extend"; import { patchFontOptions } from "./utils"; import { Plaque } from "./plaque"; import pointerEvents from "../../common/core/events/pointer"; import { start as dragEventStart, move as dragEventMove, end as dragEventEnd } from "../../common/core/events/drag"; import { addNamespace } from "../../common/core/events/utils/index"; import eventsEngine from "../../common/core/events/core/events_engine"; const getDocument = domAdapter.getDocument; const EVENT_NS = "annotations"; const DOT_EVENT_NS = "." + EVENT_NS; const POINTER_ACTION = addNamespace([pointerEvents.down, pointerEvents.move], EVENT_NS); const POINTER_UP_EVENT_NAME = addNamespace(pointerEvents.up, EVENT_NS); const DRAG_START_EVENT_NAME = dragEventStart + DOT_EVENT_NS; const DRAG_EVENT_NAME = dragEventMove + DOT_EVENT_NS; const DRAG_END_EVENT_NAME = dragEventEnd + DOT_EVENT_NS; function coreAnnotation(options, contentTemplate) { return { draw: function(widget, group) { const annotationGroup = widget._renderer.g().append(group).css(patchFontOptions(options.font)); if (this.plaque) { this.plaque.clear() } this.plaque = new Plaque(extend(true, {}, options, { cornerRadius: (options.border || {}).cornerRadius }), widget, annotationGroup, contentTemplate, widget._isAnnotationBounded(options)); this.plaque.draw(widget._getAnnotationCoords(this)); if (options.allowDragging) { annotationGroup.on(DRAG_START_EVENT_NAME, { immediate: true }, (e => { this._dragOffsetX = this.plaque.x - e.pageX; this._dragOffsetY = this.plaque.y - e.pageY })).on(DRAG_EVENT_NAME, (e => { this.plaque.move(e.pageX + this._dragOffsetX, e.pageY + this._dragOffsetY) })).on(DRAG_END_EVENT_NAME, (e => { this.offsetX = (this.offsetX || 0) + e.offset.x; this.offsetY = (this.offsetY || 0) + e.offset.y })) } }, hitTest(x, y) { return this.plaque.hitTest(x, y) }, showTooltip(tooltip, _ref) { let { x: x, y: y } = _ref; const that = this; const options = that.options; if (tooltip.annotation !== that) { tooltip.setTemplate(options.tooltipTemplate); const callback = result => { result && (tooltip.annotation = that) }; callback(tooltip.show(options, { x: x, y: y }, { target: options }, options.customizeTooltip, callback)) } else if (!tooltip.isCursorOnTooltip(x, y)) { tooltip.move(x, y) } } } } function getTemplateFunction(options, widget) { let template; if ("text" === options.type) { template = function(item, groupElement) { const text = widget._renderer.text(item.text).attr({ class: item.cssClass }).append({ element: groupElement }); if (item.width > 0 || item.height > 0) { text.setMaxSize(item.width, item.height, { wordWrap: item.wordWrap, textOverflow: item.textOverflow }) } } } else if ("image" === options.type) { template = function(item, groupElement) { const { width: width, height: height, url: url, location: location } = item.image || {}; const { width: outerWidth, height: outerHeight } = item; const imageWidth = outerWidth > 0 ? Math.min(width, outerWidth) : width; const imageHeight = outerHeight > 0 ? Math.min(height, outerHeight) : height; widget._renderer.image(0, 0, imageWidth, imageHeight, url, location || "center").append({ element: groupElement }) } } else if ("custom" === options.type) { template = options.template } return template } function getImageObject(image) { return "string" === typeof image ? { url: image } : image } export let createAnnotations = function(widget, items) { let commonAnnotationSettings = arguments.length > 2 && void 0 !== arguments[2] ? arguments[2] : {}; let customizeAnnotation = arguments.length > 3 ? arguments[3] : void 0; let pullOptions = arguments.length > 4 ? arguments[4] : void 0; const commonImageOptions = getImageObject(commonAnnotationSettings.image); return items.reduce(((arr, item) => { const currentImageOptions = getImageObject(item.image); const customizedItem = isFunction(customizeAnnotation) ? customizeAnnotation(item) : {}; if (customizedItem) { customizedItem.image = getImageObject(customizedItem.image) } const options = extend(true, {}, commonAnnotationSettings, item, { image: commonImageOptions }, { image: currentImageOptions }, customizedItem); const templateFunction = getTemplateFunction(options, widget); const annotation = templateFunction && extend(true, pullOptions(options), coreAnnotation(options, widget._getTemplate(templateFunction))); annotation && arr.push(annotation); return arr }), []) }; const chartPlugin = { name: "annotations_chart", init() {}, dispose() {}, members: { _getAnnotationCoords(annotation) { var _axis, _axis2; const coords = { offsetX: annotation.offsetX, offsetY: annotation.offsetY }; const argCoordName = this._options.silent("rotated") ? "y" : "x"; const valCoordName = this._options.silent("rotated") ? "x" : "y"; const argAxis = this.getArgumentAxis(); const argument = argAxis.validateUnit(annotation.argument); let axis = this.getValueAxis(annotation.axis); let series; let pane = null === (_axis = axis) || void 0 === _axis ? void 0 : _axis.pane; if (annotation.series) { var _series; series = this.series.filter((s => s.name === annotation.series))[0]; axis = null === (_series = series) || void 0 === _series ? void 0 : _series.getValueAxis(); isDefined(axis) && (pane = axis.pane) } if (isDefined(argument)) { if (series) { const center = series.getPointCenterByArg(argument); center && (coords[argCoordName] = center[argCoordName]) } else { coords[argCoordName] = argAxis.getTranslator().translate(argument) }!isDefined(pane) && (pane = argAxis.pane) } const value = null === (_axis2 = axis) || void 0 === _axis2 ? void 0 : _axis2.validateUnit(annotation.value); if (isDefined(value)) { var _axis3; coords[valCoordName] = null === (_axis3 = axis) || void 0 === _axis3 ? void 0 : _axis3.getTranslator().translate(value); !isDefined(pane) && isDefined(axis) && (pane = axis.pane) } coords.canvas = this._getCanvasForPane(pane); if (isDefined(coords[argCoordName]) && !isDefined(value)) { var _series2; if (!isDefined(axis) && !isDefined(series)) { coords[valCoordName] = argAxis.getAxisPosition() } else if (isDefined(axis) && !isDefined(series)) { coords[valCoordName] = this._argumentAxes.filter((a => a.pane === axis.pane))[0].getAxisPosition() } else if (null !== (_series2 = series) && void 0 !== _series2 && _series2.checkSeriesViewportCoord(argAxis, coords[argCoordName])) { coords[valCoordName] = series.getSeriesPairCoord(coords[argCoordName], true) } } if (!isDefined(argument) && isDefined(coords[valCoordName])) { if (isDefined(axis) && !isDefined(series)) { coords[argCoordName] = axis.getAxisPosition() } else if (isDefined(series)) { if (series.checkSeriesViewportCoord(axis, coords[valCoordName])) { coords[argCoordName] = series.getSeriesPairCoord(coords[valCoordName], false) } } } return coords }, _annotationsPointerEventHandler(event) { if (this._disposed) { return } const originalEvent = event.originalEvent || {}; const touch = originalEvent.touches && originalEvent.touches[0] || {}; const rootOffset = this._renderer.getRootOffset(); const coords = { x: touch.pageX || originalEvent.pageX || event.pageX, y: touch.pageY || originalEvent.pageY || event.pageY }; const annotation = this._annotations.items.filter((a => a.hitTest(coords.x - rootOffset.left, coords.y - rootOffset.top)))[0]; if (!annotation || !annotation.options.tooltipEnabled) { this._annotations.hideTooltip(); return } this._clear(); if (annotation.options.allowDragging && event.type === pointerEvents.down) { this._annotations._hideToolTipForDrag = true } if (!this._annotations._hideToolTipForDrag) { annotation.showTooltip(this._annotations.tooltip, coords); event.stopPropagation() } }, _isAnnotationBounded: options => isDefined(options.value) || isDefined(options.argument), _pullOptions: options => ({ type: options.type, name: options.name, x: options.x, y: options.y, value: options.value, argument: options.argument, axis: options.axis, series: options.series, options: options, offsetX: options.offsetX, offsetY: options.offsetY }), _forceAnnotationRender() { this._change(["FORCE_RENDER"]) }, _clear() { this.hideTooltip(); this.clearHover() } } }; const polarChartPlugin = { name: "annotations_polar_chart", init() {}, dispose() {}, members: { _getAnnotationCoords(annotation) { const coords = { offsetX: annotation.offsetX, offsetY: annotation.offsetY, canvas: this._calcCanvas() }; const argAxis = this.getArgumentAxis(); let argument = argAxis.validateUnit(annotation.argument); const value = this.getValueAxis().validateUnit(annotation.value); const radius = annotation.radius; const angle = annotation.angle; let pointCoords; let series; if (annotation.series) { series = this.series.filter((s => s.name === annotation.series))[0] } extend(true, coords, this.getXYFromPolar(angle, radius, argument, value)); if (isDefined(series)) { if (isDefined(coords.angle) && !isDefined(value) && !isDefined(radius)) { if (!isDefined(argument)) { argument = argAxis.getTranslator().from(isFinite(angle) ? this.getActualAngle(angle) : coords.angle) } pointCoords = series.getSeriesPairCoord({ argument: argument, angle: -coords.angle }, true) } else if (isDefined(coords.radius) && !isDefined(argument) && !isDefined(angle)) { pointCoords = series.getSeriesPairCoord({ radius: coords.radius }, false) } if (isDefined(pointCoords)) { coords.x = pointCoords.x; coords.y = pointCoords.y } } if (annotation.series && !isDefined(pointCoords)) { coords.x = coords.y = void 0 } return coords }, _annotationsPointerEventHandler: chartPlugin.members._annotationsPointerEventHandler, _isAnnotationBounded: chartPlugin.members._isAnnotationBounded, _pullOptions(options) { const polarOptions = extend({}, { radius: options.radius, angle: options.angle }, chartPlugin.members._pullOptions(options)); delete polarOptions.axis; return polarOptions }, _forceAnnotationRender: chartPlugin.members._forceAnnotationRender, _clear: chartPlugin.members._clear } }; const vectorMapPlugin = { name: "annotations_vector_map", init() {}, dispose() { this._annotations._offTracker(); this._annotations._offTracker = null }, members: { _getAnnotationCoords(annotation) { const coords = { offsetX: annotation.offsetX, offsetY: annotation.offsetY }; coords.canvas = this._projection.getCanvas(); if (annotation.coordinates) { const data = this._projection.toScreenPoint(annotation.coordinates); coords.x = data[0]; coords.y = data[1] } return coords }, _annotationsPointerEventHandler: chartPlugin.members._annotationsPointerEventHandler, _isAnnotationBounded: options => isDefined(options.coordinates), _pullOptions(options) { const vectorMapOptions = extend({}, { coordinates: options.coordinates }, chartPlugin.members._pullOptions(options)); delete vectorMapOptions.axis; delete vectorMapOptions.series; delete vectorMapOptions.argument; delete vectorMapOptions.value; return vectorMapOptions }, _forceAnnotationRender() { this._change(["EXTRA_ELEMENTS"]) }, _getAnnotationStyles: () => ({ "text-anchor": "start" }), _clear() {} }, extenders: { _prepareExtraElements() { const that = this; const renderElements = () => { that._renderExtraElements() }; that._annotations._offTracker = that._tracker.on({ move: renderElements, zoom: renderElements, end: renderElements }) } } }; const pieChartPlugin = { name: "annotations_pie_chart", init() {}, dispose() {}, members: { _getAnnotationCoords(annotation) { let series; const coords = { offsetX: annotation.offsetX, offsetY: annotation.offsetY, canvas: this._canvas }; if (annotation.argument) { if (annotation.series) { series = this.getSeriesByName(annotation.series) } else { series = this.series[0] } const argument = series.getPointsByArg(annotation.argument)[0]; const { x: x, y: y } = argument.getAnnotationCoords(annotation.location); coords.x = x; coords.y = y } return coords }, _isAnnotationBounded: options => options.argument, _annotationsPointerEventHandler: chartPlugin.members._annotationsPointerEventHandler, _pullOptions(options) { const pieChartOptions = extend({}, { location: options.location }, chartPlugin.members._pullOptions(options)); delete pieChartOptions.axis; return pieChartOptions }, _clear: chartPlugin.members._clear, _forceAnnotationRender: chartPlugin.members._forceAnnotationRender } }; const corePlugin = { name: "annotations_core", init() { this._annotations = { items: [], _hideToolTipForDrag: false, tooltip: new Tooltip({ cssClass: `${this._rootClassPrefix}-annotation-tooltip`, eventTrigger: this._eventTrigger, widgetRoot: this.element(), widget: this }), hideTooltip() { this.tooltip.annotation = null; this.tooltip.hide() }, clearItems() { this.items.forEach((i => i.plaque.clear())); this.items = [] } }; this._annotations.tooltip.setRendererOptions(this._getRendererOptions()) }, dispose() { this._annotationsGroup.linkRemove().linkOff(); eventsEngine.off(getDocument(), DOT_EVENT_NS); this._annotationsGroup.off(DOT_EVENT_NS); this._annotations.tooltip && this._annotations.tooltip.dispose() }, extenders: { _createHtmlStructure() { this._annotationsGroup = this._renderer.g().attr({ class: `${this._rootClassPrefix}-annotations` }).css(this._getAnnotationStyles()).linkOn(this._renderer.root, "annotations").linkAppend(); eventsEngine.on(getDocument(), POINTER_ACTION, (e => { if (this._disposed) { return } if (!this._annotations.tooltip.isCursorOnTooltip(e.pageX, e.pageY)) { this._annotations.hideTooltip() } })); eventsEngine.on(getDocument(), POINTER_UP_EVENT_NAME, (event => { this._annotations._hideToolTipForDrag = false; this._annotationsPointerEventHandler(event) })); this._annotationsGroup.on(POINTER_ACTION, this._annotationsPointerEventHandler.bind(this)) }, _renderExtraElements() { this._annotationsGroup.clear(); this._annotations.items.forEach((item => item.draw(this, this._annotationsGroup))) }, _stopCurrentHandling() { this._annotations.hideTooltip() } }, members: { _buildAnnotations() { this._annotations.clearItems(); const items = this._getOption("annotations", true); if (!(null !== items && void 0 !== items && items.length)) { return } this._annotations.items = createAnnotations(this, items, this._getOption("commonAnnotationSettings"), this._getOption("customizeAnnotation", true), this._pullOptions) }, _setAnnotationTooltipOptions() { const tooltipOptions = extend({}, this._getOption("tooltip")); tooltipOptions.contentTemplate = tooltipOptions.customizeTooltip = void 0; this._annotations.tooltip.update(tooltipOptions) }, _getAnnotationCoords: () => ({}), _pullOptions: () => ({}), _getAnnotationStyles: () => ({}) }, customize(constructor) { constructor.addChange({ code: "ANNOTATIONITEMS", handler() { this._requestChange(["ANNOTATIONS"]) }, isOptionChange: true, option: "annotations" }); constructor.addChange({ code: "ANNOTATIONSSETTINGS", handler() { this._requestChange(["ANNOTATIONS"]) }, isOptionChange: true, option: "commonAnnotationSettings" }); constructor.addChange({ code: "ANNOTATIONS", handler() { this._buildAnnotations(); this._setAnnotationTooltipOptions(); this._forceAnnotationRender() }, isThemeDependent: true, isOptionChange: true }) }, fontFields: ["commonAnnotationSettings.font"] }; export const plugins = { core: corePlugin, chart: chartPlugin, polarChart: polarChartPlugin, vectorMap: vectorMapPlugin, pieChart: pieChartPlugin };