UNPKG

ag-charts-community

Version:

Advanced Charting / Charts supporting Javascript / Typescript / React / Angular / Vue

1,070 lines (1,053 loc) 39 kB
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; import { Scene } from "../scene/scene"; import { Group } from "../scene/group"; import { Padding } from "../util/padding"; import { Shape } from "../scene/shape/shape"; import { Rect } from "../scene/shape/rect"; import { Legend } from "./legend"; import { BBox } from "../scene/bbox"; import { find } from "../util/array"; import { SizeMonitor } from "../util/sizeMonitor"; import { Observable, reactive } from "../util/observable"; import { createId } from "../util/id"; import { placeLabels } from "../util/labelPlacement"; const defaultTooltipCss = ` .ag-chart-tooltip { display: table; position: absolute; user-select: none; pointer-events: none; white-space: nowrap; z-index: 99999; font: 12px Verdana, sans-serif; color: black; background: rgb(244, 244, 244); border-radius: 5px; box-shadow: 0 0 1px rgba(3, 3, 3, 0.7), 0.5vh 0.5vh 1vh rgba(3, 3, 3, 0.25); } .ag-chart-tooltip-hidden { top: -10000px !important; } .ag-chart-tooltip-title { font-weight: bold; padding: 7px; border-top-left-radius: 5px; border-top-right-radius: 5px; color: white; background-color: #888888; border-top-left-radius: 5px; border-top-right-radius: 5px; } .ag-chart-tooltip-content { padding: 7px; line-height: 1.7em; border-bottom-left-radius: 5px; border-bottom-right-radius: 5px; overflow: hidden; } .ag-chart-tooltip-content:empty { padding: 0; height: 7px; } .ag-chart-tooltip-arrow::before { content: ""; position: absolute; top: 100%; left: 50%; transform: translateX(-50%); border: 6px solid #989898; border-left-color: transparent; border-right-color: transparent; border-top-color: #989898; border-bottom-color: transparent; width: 0; height: 0; margin: 0 auto; } .ag-chart-tooltip-arrow::after { content: ""; position: absolute; top: 100%; left: 50%; transform: translateX(-50%); border: 5px solid black; border-left-color: transparent; border-right-color: transparent; border-top-color: rgb(244, 244, 244); border-bottom-color: transparent; width: 0; height: 0; margin: 0 auto; } .ag-chart-wrapper { box-sizing: border-box; overflow: hidden; } `; export function toTooltipHtml(input, defaults) { if (typeof input === 'string') { return input; } defaults = defaults || {}; const { content = defaults.content || '', title = defaults.title || undefined, color = defaults.color || 'white', backgroundColor = defaults.backgroundColor || '#888' } = input; const titleHtml = title ? `<div class="${Chart.defaultTooltipClass}-title" style="color: ${color}; background-color: ${backgroundColor}">${title}</div>` : ''; return `${titleHtml}<div class="${Chart.defaultTooltipClass}-content">${content}</div>`; } export class ChartTooltip extends Observable { constructor(chart, document) { super(); this.enabled = true; this.class = Chart.defaultTooltipClass; this.delay = 0; /** * If `true`, the tooltip will be shown for the marker closest to the mouse cursor. * Only has effect on series with markers. */ this.tracking = true; this.showTimeout = 0; this.constrained = false; this.chart = chart; this.class = ''; const tooltipRoot = document.body; const element = document.createElement('div'); this.element = tooltipRoot.appendChild(element); // Detect when the chart becomes invisible and hide the tooltip as well. if (window.IntersectionObserver) { const target = this.chart.scene.canvas.element; const observer = new IntersectionObserver(entries => { for (const entry of entries) { if (entry.target === target && entry.intersectionRatio === 0) { this.toggle(false); } } }, { root: tooltipRoot }); observer.observe(target); this.observer = observer; } } destroy() { const { parentNode } = this.element; if (parentNode) { parentNode.removeChild(this.element); } if (this.observer) { this.observer.unobserve(this.chart.scene.canvas.element); } } isVisible() { const { element } = this; if (element.classList) { // if not IE11 return !element.classList.contains(Chart.defaultTooltipClass + '-hidden'); } // IE11 part. const classes = element.getAttribute('class'); if (classes) { return classes.split(' ').indexOf(Chart.defaultTooltipClass + '-hidden') < 0; } return false; } updateClass(visible, constrained) { const classList = [Chart.defaultTooltipClass, this.class]; if (visible !== true) { classList.push(`${Chart.defaultTooltipClass}-hidden`); } if (constrained !== true) { classList.push(`${Chart.defaultTooltipClass}-arrow`); } this.element.setAttribute('class', classList.join(' ')); } /** * Shows tooltip at the given event's coordinates. * If the `html` parameter is missing, moves the existing tooltip to the new position. */ show(meta, html, instantly = false) { const el = this.element; if (html !== undefined) { el.innerHTML = html; } else if (!el.innerHTML) { return; } let left = meta.pageX - el.clientWidth / 2; let top = meta.pageY - el.clientHeight - 8; this.constrained = false; if (this.chart.container) { const tooltipRect = el.getBoundingClientRect(); const minLeft = 0; const maxLeft = window.innerWidth - tooltipRect.width - 1; if (left < minLeft) { left = minLeft; this.updateClass(true, this.constrained = true); } else if (left > maxLeft) { left = maxLeft; this.updateClass(true, this.constrained = true); } if (top < window.pageYOffset) { top = meta.pageY + 20; this.updateClass(true, this.constrained = true); } } el.style.left = `${Math.round(left)}px`; el.style.top = `${Math.round(top)}px`; if (this.delay > 0 && !instantly) { this.toggle(false); this.showTimeout = window.setTimeout(() => { this.toggle(true); }, this.delay); return; } this.toggle(true); } toggle(visible) { if (!visible) { window.clearTimeout(this.showTimeout); if (this.chart.lastPick && !this.delay) { this.chart.dehighlightDatum(); this.chart.lastPick = undefined; } } this.updateClass(visible, this.constrained); } } __decorate([ reactive() ], ChartTooltip.prototype, "enabled", void 0); __decorate([ reactive() ], ChartTooltip.prototype, "class", void 0); __decorate([ reactive() ], ChartTooltip.prototype, "delay", void 0); __decorate([ reactive() ], ChartTooltip.prototype, "tracking", void 0); export class Chart extends Observable { constructor(document = window.document) { super(); this.id = createId(this); this.background = new Rect(); this.legend = new Legend(); this.legendAutoPadding = new Padding(); this.captionAutoPadding = 0; // top padding only this._container = undefined; this._data = []; this._autoSize = false; this.padding = new Padding(20); this._axes = []; this._series = []; this._axesChanged = false; this._seriesChanged = false; this.layoutCallbackId = 0; this._performLayout = () => { this.layoutCallbackId = 0; this.background.width = this.width; this.background.height = this.height; this.performLayout(); if (!this.layoutPending) { this.fireEvent({ type: 'layoutDone' }); } }; this.dataCallbackId = 0; this.nodeData = new Map(); this.updateCallbackId = 0; this.legendBBox = new BBox(0, 0, 0, 0); this._onMouseDown = this.onMouseDown.bind(this); this._onMouseMove = this.onMouseMove.bind(this); this._onMouseUp = this.onMouseUp.bind(this); this._onMouseOut = this.onMouseOut.bind(this); this._onClick = this.onClick.bind(this); this.pointerInsideLegend = false; const root = new Group(); const background = this.background; background.fill = 'white'; root.appendChild(background); const element = this._element = document.createElement('div'); element.setAttribute('class', 'ag-chart-wrapper'); const scene = new Scene(document); this.scene = scene; scene.root = root; scene.container = element; this.autoSize = true; this.padding.addEventListener('layoutChange', this.scheduleLayout, this); const { legend } = this; legend.addEventListener('layoutChange', this.scheduleLayout, this); legend.item.label.addPropertyListener('formatter', this.updateLegend, this); legend.addPropertyListener('position', this.onLegendPositionChange, this); this.tooltip = new ChartTooltip(this, document); this.tooltip.addPropertyListener('class', () => this.tooltip.toggle()); if (Chart.tooltipDocuments.indexOf(document) < 0) { const styleElement = document.createElement('style'); styleElement.innerHTML = defaultTooltipCss; // Make sure the default tooltip style goes before other styles so it can be overridden. document.head.insertBefore(styleElement, document.head.querySelector('style')); Chart.tooltipDocuments.push(document); } this.setupDomListeners(scene.canvas.element); this.addPropertyListener('title', this.onCaptionChange); this.addPropertyListener('subtitle', this.onCaptionChange); this.addEventListener('layoutChange', this.scheduleLayout); } set container(value) { if (this._container !== value) { const { parentNode } = this.element; if (parentNode != null) { parentNode.removeChild(this.element); } if (value) { value.appendChild(this.element); } this._container = value; } } get container() { return this._container; } set data(data) { this._data = data; this.series.forEach(series => series.data = data); } get data() { return this._data; } set width(value) { this.autoSize = false; if (this.width !== value) { this.scene.resize(value, this.height); this.fireEvent({ type: 'layoutChange' }); } } get width() { return this.scene.width; } set height(value) { this.autoSize = false; if (this.height !== value) { this.scene.resize(this.width, value); this.fireEvent({ type: 'layoutChange' }); } } get height() { return this.scene.height; } set autoSize(value) { if (this._autoSize !== value) { this._autoSize = value; const { style } = this.element; if (value) { const chart = this; // capture `this` for IE11 SizeMonitor.observe(this.element, size => { if (size.width !== chart.width || size.height !== chart.height) { chart.scene.resize(size.width, size.height); chart.fireEvent({ type: 'layoutChange' }); } }); style.display = 'block'; style.width = '100%'; style.height = '100%'; } else { SizeMonitor.unobserve(this.element); style.display = 'inline-block'; style.width = 'auto'; style.height = 'auto'; } } } get autoSize() { return this._autoSize; } download(fileName) { this.scene.download(fileName); } destroy() { this.tooltip.destroy(); SizeMonitor.unobserve(this.element); this.container = undefined; this.cleanupDomListeners(this.scene.canvas.element); this.scene.container = undefined; } onLegendPositionChange() { this.legendAutoPadding.clear(); this.layoutPending = true; } onCaptionChange(event) { const { value, oldValue } = event; if (oldValue) { oldValue.removeEventListener('change', this.scheduleLayout, this); this.scene.root.removeChild(oldValue.node); } if (value) { value.addEventListener('change', this.scheduleLayout, this); this.scene.root.appendChild(value.node); } } get element() { return this._element; } set axes(values) { this._axes.forEach(axis => this.detachAxis(axis)); // make linked axes go after the regular ones (simulates stable sort by `linkedTo` property) this._axes = values.filter(a => !a.linkedTo).concat(values.filter(a => a.linkedTo)); this._axes.forEach(axis => this.attachAxis(axis)); this.axesChanged = true; } get axes() { return this._axes; } attachAxis(axis) { this.scene.root.insertBefore(axis.group, this.seriesRoot); } detachAxis(axis) { this.scene.root.removeChild(axis.group); } set series(values) { this.removeAllSeries(); values.forEach(series => this.addSeries(series)); } get series() { return this._series; } scheduleLayout() { this.layoutPending = true; } scheduleData() { // To prevent the chart from thinking the cursor is over the same node // after a change to data (the nodes are reused on data changes). this.dehighlightDatum(); this.dataPending = true; } addSeries(series, before) { const { series: allSeries, seriesRoot } = this; const canAdd = allSeries.indexOf(series) < 0; if (canAdd) { const beforeIndex = before ? allSeries.indexOf(before) : -1; if (beforeIndex >= 0) { allSeries.splice(beforeIndex, 0, series); seriesRoot.insertBefore(series.group, before.group); } else { allSeries.push(series); seriesRoot.append(series.group); } this.initSeries(series); this.seriesChanged = true; this.axesChanged = true; return true; } return false; } initSeries(series) { series.chart = this; if (!series.data) { series.data = this.data; } series.addEventListener('layoutChange', this.scheduleLayout, this); series.addEventListener('dataChange', this.scheduleData, this); series.addEventListener('legendChange', this.updateLegend, this); series.addEventListener('nodeClick', this.onSeriesNodeClick, this); } freeSeries(series) { series.chart = undefined; series.removeEventListener('layoutChange', this.scheduleLayout, this); series.removeEventListener('dataChange', this.scheduleData, this); series.removeEventListener('legendChange', this.updateLegend, this); series.removeEventListener('nodeClick', this.onSeriesNodeClick, this); } addSeriesAfter(series, after) { const { series: allSeries, seriesRoot } = this; const canAdd = allSeries.indexOf(series) < 0; if (canAdd) { const afterIndex = after ? this.series.indexOf(after) : -1; if (afterIndex >= 0) { if (afterIndex + 1 < allSeries.length) { seriesRoot.insertBefore(series.group, allSeries[afterIndex + 1].group); } else { seriesRoot.append(series.group); } this.initSeries(series); allSeries.splice(afterIndex + 1, 0, series); } else { if (allSeries.length > 0) { seriesRoot.insertBefore(series.group, allSeries[0].group); } else { seriesRoot.append(series.group); } this.initSeries(series); allSeries.unshift(series); } this.seriesChanged = true; this.axesChanged = true; } return false; } removeSeries(series) { const index = this.series.indexOf(series); if (index >= 0) { this.series.splice(index, 1); this.freeSeries(series); this.seriesRoot.removeChild(series.group); this.seriesChanged = true; return true; } return false; } removeAllSeries() { this.series.forEach(series => { this.freeSeries(series); this.seriesRoot.removeChild(series.group); }); this._series = []; // using `_series` instead of `series` to prevent infinite recursion this.seriesChanged = true; } assignSeriesToAxes() { this.axes.forEach(axis => { const axisName = axis.direction + 'Axis'; const boundSeries = []; this.series.forEach(series => { if (series[axisName] === axis) { boundSeries.push(series); } }); axis.boundSeries = boundSeries; }); this.seriesChanged = false; } assignAxesToSeries(force = false) { // This method has to run before `assignSeriesToAxes`. const directionToAxesMap = {}; this.axes.forEach(axis => { const direction = axis.direction; const directionAxes = directionToAxesMap[direction] || (directionToAxesMap[direction] = []); directionAxes.push(axis); }); this.series.forEach(series => { series.directions.forEach(direction => { const axisName = direction + 'Axis'; if (!series[axisName] || force) { const directionAxes = directionToAxesMap[direction]; if (directionAxes) { const axis = this.findMatchingAxis(directionAxes, series.getKeys(direction)); if (axis) { series[axisName] = axis; } } } }); }); this.axesChanged = false; } findMatchingAxis(directionAxes, directionKeys) { for (let i = 0; i < directionAxes.length; i++) { const axis = directionAxes[i]; const axisKeys = axis.keys; if (!axisKeys.length) { return axis; } else if (directionKeys) { for (let j = 0; j < directionKeys.length; j++) { if (axisKeys.indexOf(directionKeys[j]) >= 0) { return axis; } } } } } set axesChanged(value) { this._axesChanged = value; } get axesChanged() { return this._axesChanged; } set seriesChanged(value) { this._seriesChanged = value; if (value) { this.dataPending = true; } } get seriesChanged() { return this._seriesChanged; } set layoutPending(value) { if (value) { if (!(this.layoutCallbackId || this.dataPending)) { this.layoutCallbackId = requestAnimationFrame(this._performLayout); this.series.forEach(s => s.nodeDataPending = true); } } else if (this.layoutCallbackId) { cancelAnimationFrame(this.layoutCallbackId); this.layoutCallbackId = 0; } } /** * Only `true` while we are waiting for the layout to start. * This will be `false` if the layout has already started and is ongoing. */ get layoutPending() { return !!this.layoutCallbackId; } set dataPending(value) { if (this.dataCallbackId) { clearTimeout(this.dataCallbackId); this.dataCallbackId = 0; } if (value) { this.dataCallbackId = window.setTimeout(() => { this.dataPending = false; this.processData(); }, 0); } } get dataPending() { return !!this.dataCallbackId; } processData() { this.layoutPending = false; if (this.axesChanged) { this.assignAxesToSeries(true); this.assignSeriesToAxes(); } if (this.seriesChanged) { this.assignSeriesToAxes(); } this.series.forEach(s => s.processData()); this.updateLegend(); // sets legend data which schedules a layout this.layoutPending = true; } createNodeData() { this.nodeData.clear(); this.series.forEach(s => { const data = s.visible ? s.createNodeData() : []; this.nodeData.set(s, data); }); } placeLabels() { const series = []; const data = []; this.nodeData.forEach((d, s) => { if (s.visible && s.label.enabled) { series.push(s); data.push(s.getLabelData()); } }); const { seriesRect } = this; const labels = seriesRect ? placeLabels(data, { x: 0, y: 0, width: seriesRect.width, height: seriesRect.height }) : []; return new Map(labels.map((l, i) => [series[i], l])); } updateLegend() { const legendData = []; this.series.filter(s => s.showInLegend).forEach(series => series.listSeriesItems(legendData)); const { formatter } = this.legend.item.label; if (formatter) { legendData.forEach(datum => datum.label.text = formatter({ id: datum.id, itemId: datum.itemId, value: datum.label.text })); } this.legend.data = legendData; } set updatePending(value) { if (this.updateCallbackId) { clearTimeout(this.updateCallbackId); this.updateCallbackId = 0; } if (value && !this.layoutPending) { this.updateCallbackId = window.setTimeout(() => { this.update(); }, 0); } } get updatePending() { return !!this.updateCallbackId; } update() { this.updatePending = false; this.series.forEach(series => { if (series.updatePending) { series.update(); } }); } positionCaptions() { const { title, subtitle } = this; let titleVisible = false; let subtitleVisible = false; const spacing = 10; let paddingTop = spacing; if (title && title.enabled) { title.node.x = this.width / 2; title.node.y = paddingTop; titleVisible = true; const titleBBox = title.node.computeBBox(); // make sure to set node's x/y, then computeBBox if (titleBBox) { paddingTop = titleBBox.y + titleBBox.height; } if (subtitle && subtitle.enabled) { subtitle.node.x = this.width / 2; subtitle.node.y = paddingTop + spacing; subtitleVisible = true; const subtitleBBox = subtitle.node.computeBBox(); if (subtitleBBox) { paddingTop = subtitleBBox.y + subtitleBBox.height; } } } if (title) { title.node.visible = titleVisible; } if (subtitle) { subtitle.node.visible = subtitleVisible; } this.captionAutoPadding = Math.floor(paddingTop); } positionLegend() { if (!this.legend.enabled || !this.legend.data.length) { return; } const { legend, captionAutoPadding, legendAutoPadding } = this; const width = this.width; const height = this.height - captionAutoPadding; const legendGroup = legend.group; const legendSpacing = legend.spacing; let translationX = 0; let translationY = 0; let legendBBox; switch (legend.position) { case 'bottom': legend.performLayout(width - legendSpacing * 2, 0); legendBBox = legendGroup.computeBBox(); legendGroup.visible = legendBBox.height < Math.floor((height * 0.5)); // Remove legend if it takes up more than 50% of the chart height. if (legendGroup.visible) { translationX = (width - legendBBox.width) / 2 - legendBBox.x; translationY = captionAutoPadding + height - legendBBox.height - legendBBox.y - legendSpacing; legendAutoPadding.bottom = legendBBox.height; } else { legendAutoPadding.bottom = 0; } break; case 'top': legend.performLayout(width - legendSpacing * 2, 0); legendBBox = legendGroup.computeBBox(); legendGroup.visible = legendBBox.height < Math.floor((height * 0.5)); if (legendGroup.visible) { translationX = (width - legendBBox.width) / 2 - legendBBox.x; translationY = captionAutoPadding + legendSpacing - legendBBox.y; legendAutoPadding.top = legendBBox.height; } else { legendAutoPadding.top = 0; } break; case 'left': legend.performLayout(0, height - legendSpacing * 2); legendBBox = legendGroup.computeBBox(); legendGroup.visible = legendBBox.width < Math.floor((width * 0.5)); // Remove legend if it takes up more than 50% of the chart width. if (legendGroup.visible) { translationX = legendSpacing - legendBBox.x; translationY = captionAutoPadding + (height - legendBBox.height) / 2 - legendBBox.y; legendAutoPadding.left = legendBBox.width; } else { legendAutoPadding.left = 0; } break; default: // case 'right': legend.performLayout(0, height - legendSpacing * 2); legendBBox = legendGroup.computeBBox(); legendGroup.visible = legendBBox.width < Math.floor((width * 0.5)); if (legendGroup.visible) { translationX = width - legendBBox.width - legendBBox.x - legendSpacing; translationY = captionAutoPadding + (height - legendBBox.height) / 2 - legendBBox.y; legendAutoPadding.right = legendBBox.width; } else { legendAutoPadding.right = 0; } break; } if (legendGroup.visible) { // Round off for pixel grid alignment to work properly. legendGroup.translationX = Math.floor(translationX + legendGroup.translationX); legendGroup.translationY = Math.floor(translationY + legendGroup.translationY); this.legendBBox = legendGroup.computeBBox(); } } setupDomListeners(chartElement) { chartElement.addEventListener('mousedown', this._onMouseDown); chartElement.addEventListener('mousemove', this._onMouseMove); chartElement.addEventListener('mouseup', this._onMouseUp); chartElement.addEventListener('mouseout', this._onMouseOut); chartElement.addEventListener('click', this._onClick); } cleanupDomListeners(chartElement) { chartElement.removeEventListener('mousedown', this._onMouseDown); chartElement.removeEventListener('mousemove', this._onMouseMove); chartElement.removeEventListener('mouseup', this._onMouseUp); chartElement.removeEventListener('mouseout', this._onMouseOut); chartElement.removeEventListener('click', this._onClick); } getSeriesRect() { return this.seriesRect; } // x/y are local canvas coordinates in CSS pixels, not actual pixels pickSeriesNode(x, y) { if (!(this.seriesRect && this.seriesRect.containsPoint(x, y))) { return undefined; } const allSeries = this.series; let node = undefined; for (let i = allSeries.length - 1; i >= 0; i--) { const series = allSeries[i]; if (!series.visible) { continue; } node = series.pickGroup.pickNode(x, y); if (node) { return { series, node }; } } } // Provided x/y are in canvas coordinates. pickClosestSeriesNodeDatum(x, y) { if (!this.seriesRect || !this.seriesRect.containsPoint(x, y)) { return undefined; } const allSeries = this.series; function getDistance(p1, p2) { return Math.sqrt(Math.pow((p1.x - p2.x), 2) + Math.pow((p1.y - p2.y), 2)); } let minDistance = Infinity; let closestDatum; for (let i = allSeries.length - 1; i >= 0; i--) { const series = allSeries[i]; if (!series.visible) { continue; } const hitPoint = series.group.transformPoint(x, y); series.getNodeData().forEach(datum => { if (!datum.point) { return; } const distance = getDistance(hitPoint, datum.point); if (distance < minDistance) { minDistance = distance; closestDatum = datum; } }); } return closestDatum; } onMouseMove(event) { this.handleLegendMouseMove(event); if (this.tooltip.enabled) { if (this.tooltip.delay > 0) { this.tooltip.toggle(false); } this.handleTooltip(event); } } handleTooltip(event) { const { lastPick, tooltip: { tracking: tooltipTracking } } = this; const { offsetX, offsetY } = event; const pick = this.pickSeriesNode(offsetX, offsetY); let nodeDatum; if (pick && pick.node instanceof Shape) { const { node } = pick; nodeDatum = node.datum; if (lastPick && lastPick.datum === nodeDatum) { lastPick.node = node; lastPick.event = event; } // Marker nodes will have the `point` info in their datums. // Highlight if not a marker node or, if not in the tracking mode, highlight markers too. if ((!node.datum.point || !tooltipTracking)) { if (!lastPick // cursor moved from empty space to a node || lastPick.node !== node) { // cursor moved from one node to another this.onSeriesDatumPick(event, node.datum, node, event); } else if (pick.series.tooltip.enabled) { // cursor moved within the same node this.tooltip.show(event); } // A non-marker node (takes precedence over marker nodes) was highlighted. // Or we are not in the tracking mode. // Either way, we are done at this point. return; } } let hideTooltip = false; // In tracking mode a tooltip is shown for the closest rendered datum. // This makes it easier to show tooltips when markers are small and/or plentiful // and also gives the ability to show tooltips even when the series were configured // to not render markers. if (tooltipTracking) { const closestDatum = this.pickClosestSeriesNodeDatum(offsetX, offsetY); if (closestDatum && closestDatum.point) { const { x, y } = closestDatum.point; const { canvas } = this.scene; const point = closestDatum.series.group.inverseTransformPoint(x, y); const canvasRect = canvas.element.getBoundingClientRect(); this.onSeriesDatumPick({ pageX: Math.round(canvasRect.left + window.pageXOffset + point.x), pageY: Math.round(canvasRect.top + window.pageYOffset + point.y) }, closestDatum, nodeDatum === closestDatum && pick ? pick.node : undefined, event); } else { hideTooltip = true; } } if (lastPick && (hideTooltip || !tooltipTracking)) { // Cursor moved from a non-marker node to empty space. this.dehighlightDatum(); this.tooltip.toggle(false); this.lastPick = undefined; } } onMouseDown(event) { } onMouseUp(event) { } onMouseOut(event) { this.tooltip.toggle(false); } onClick(event) { if (this.checkSeriesNodeClick()) { return; } if (this.checkLegendClick(event)) { return; } this.fireEvent({ type: 'click', event }); } checkSeriesNodeClick() { const { lastPick } = this; if (lastPick && lastPick.event && lastPick.node) { const { event, datum } = lastPick; datum.series.fireNodeClickEvent(event, datum); return true; } return false; } onSeriesNodeClick(event) { this.fireEvent(Object.assign(Object.assign({}, event), { type: 'seriesNodeClick' })); } checkLegendClick(event) { const datum = this.legend.getDatumForPoint(event.offsetX, event.offsetY); if (datum) { const { id, itemId, enabled } = datum; const series = find(this.series, series => series.id === id); if (series) { series.toggleSeriesItem(itemId, !enabled); if (enabled) { this.tooltip.toggle(false); } this.legend.fireEvent({ type: 'click', event, itemId, enabled: !enabled }); return true; } } return false; } handleLegendMouseMove(event) { if (!this.legend.enabled) { return; } const { offsetX, offsetY } = event; const datum = this.legend.getDatumForPoint(offsetX, offsetY); const pointerInsideLegend = this.legendBBox.containsPoint(offsetX, offsetY); if (pointerInsideLegend) { if (!this.pointerInsideLegend) { this.pointerInsideLegend = true; } } else if (this.pointerInsideLegend) { this.pointerInsideLegend = false; // Dehighlight if the pointer was inside the legend and is now leaving it. if (this.highlightedDatum) { this.highlightedDatum = undefined; this.series.forEach(s => s.updatePending = true); } return; } const oldHighlightedDatum = this.highlightedDatum; if (datum) { const { id, itemId, enabled } = datum; if (enabled) { const series = find(this.series, series => series.id === id); if (series) { this.highlightedDatum = { series, itemId, datum: undefined }; } } } // Careful to only schedule updates when necessary. if ((this.highlightedDatum && !oldHighlightedDatum) || (this.highlightedDatum && oldHighlightedDatum && (this.highlightedDatum.series !== oldHighlightedDatum.series || this.highlightedDatum.itemId !== oldHighlightedDatum.itemId))) { this.series.forEach(s => s.updatePending = true); } } onSeriesDatumPick(meta, datum, node, event) { const { lastPick } = this; if (lastPick) { if (lastPick.datum === datum) { return; } this.dehighlightDatum(); } this.lastPick = { datum, node, event }; this.highlightDatum(datum); const html = datum.series.tooltip.enabled && datum.series.getTooltipHtml(datum); if (html) { this.tooltip.show(meta, html); } } highlightDatum(datum) { this.scene.canvas.element.style.cursor = datum.series.cursor; this.highlightedDatum = datum; this.series.forEach(s => s.updatePending = true); } dehighlightDatum() { this.scene.canvas.element.style.cursor = 'default'; this.highlightedDatum = undefined; this.series.forEach(s => s.updatePending = true); } } Chart.defaultTooltipClass = 'ag-chart-tooltip'; Chart.tooltipDocuments = []; __decorate([ reactive('layoutChange') ], Chart.prototype, "title", void 0); __decorate([ reactive('layoutChange') ], Chart.prototype, "subtitle", void 0); //# sourceMappingURL=chart.js.map