UNPKG

@progress/kendo-charts

Version:

Kendo UI platform-independent Charts library

1,602 lines (1,297 loc) 72.4 kB
import { drawing as draw, throttle, geometry } from '@progress/kendo-drawing'; import { RootElement, Title, CategoryAxis, Point } from '../core'; import Highlight from './highlight'; import Pannable from './pan-and-zoom/pannable'; import ZoomSelection from './pan-and-zoom/zoom-selection'; import MousewheelZoom from './pan-and-zoom/mousewheel-zoom'; import Legend from './legend/legend'; import PlotAreaFactory from './plotarea/plotarea-factory'; import Selection from './selection'; import SeriesBinder from './series-binder'; import Tooltip from './tooltip/tooltip'; import SharedTooltip from './tooltip/shared-tooltip'; import CategoricalPlotArea from './plotarea/categorical-plotarea'; import PlotAreaBase from './plotarea/plotarea-base'; import { ChartService, DomEventsBuilder } from '../services'; import isDateAxis from './utils/is-date-axis'; import { ChartPane, ChartPlotArea, findAxisByName } from './api-elements'; import { X, Y, VALUE, DEFAULT_WIDTH, DEFAULT_HEIGHT, DEFAULT_SERIES_OPACITY, ARROW_DOWN, ARROW_UP, ARROW_LEFT, ARROW_RIGHT, ARIA_ACTIVE_DESCENDANT, TAB, TOP, LEFT, BLACK, ENTER, ESCAPE, BOTTOM } from '../common/constants'; import { addClass, removeClass, setDefaultOptions, deepExtend, defined, find, isObject, isFunction, elementSize, elementOffset, elementScale, elementStyles, eventCoordinates, bindEvents, unbindEvents, mousewheelDelta, FontLoader, inArray, round, valueOrDefault, isString, cycleUp, cycleDown, hasOwnProperty, hasClasses } from '../common'; import { DRAG_START, DRAG, DRAG_END, ZOOM_START, ZOOM, ZOOM_END, SELECT_START, SELECT, SELECT_END, PLOT_AREA_HOVER, PLOT_AREA_LEAVE, RENDER, CATEGORY, PIE, DONUT, FUNNEL, PYRAMID, COLUMN, MOUSEWHEEL, MOUSEWHEEL_DELAY, MOUSEWHEEL_ZOOM_RATE, SHOW_TOOLTIP, SERIES_HOVER, SERIES_OVER, SERIES_LEAVE, SERIES_CLICK, DRILLDOWN, LEGEND_ITEM_CLICK } from './constants'; import './animations'; import './register-charts'; import { parseDateCategory } from './utils'; const AXIS_NAMES = [ CATEGORY, VALUE, X, Y ]; const MOUSEDOWN = "mousedown"; const MOUSEMOVE = "mousemove"; const CONTEXTMENU = "contextmenu"; const MOUSELEAVE = "mouseleave"; const KEYDOWN = "keydown"; const FOCUS = "focus"; const BLUR = "blur"; const MOUSEMOVE_DELAY = 20; const NO_DATA_OVERLAY_TOP_CLASS = "k-chart-overlay-top"; class Chart { constructor(element, userOptions, themeOptions, context = {}) { this.observers = []; this.addObserver(context.observer); this.chartService = new ChartService(this, context); this.chartService.theme = themeOptions; this._initElement(element); const options = deepExtend({}, this.options, userOptions); this._originalOptions = deepExtend({}, options); this._theme = themeOptions; this._initTheme(options, themeOptions); this._focusState = {}; this._initHandlers(); this._createSurface = context.createSurface || draw.Surface.create.bind(draw.Surface); this._initSurface(); this.bindCategories(); FontLoader.preloadFonts(userOptions, () => { this.fontLoaded = true; if (!this._destroyed) { this.trigger('init'); this._redraw(); this._attachEvents(); this._restoreOverlayElement(); } }); } _initElement(element) { this._setElementClass(element); element.style.position = "relative"; element.tabIndex = element.getAttribute("tabindex") ? element.getAttribute("tabindex") : 0; // To support user agents and assistive technologies based on the ARIA 1.0 specification, authors may wish to include the document role as a fallback value, in the form role="graphics-document document". element.setAttribute("role", "graphics-document document"); for (let i = element.childNodes.length - 1; i >= 0; i--) { const child = element.childNodes[i]; if (!hasClasses(child, "k-chart-overlay")) { element.removeChild(child); } else { // this is necessary if the overlay is rendered server-side, e.g. in blazor // but drawing the surface clears the contents of the element // and thus the no data overlay is lost this.overlayElement = child; } } this.element = element; } _setElementClass(element) { addClass(element, "k-chart"); } _restoreOverlayElement() { if (!this.overlayElement) { return; } if (this._hasSeriesData()) { this.overlayElement.style.display = "none"; } else { if (!this.options.title || (this.options.title && this.options.title.position !== BOTTOM)) { addClass(this.overlayElement, NO_DATA_OVERLAY_TOP_CLASS); } else { removeClass(this.overlayElement, NO_DATA_OVERLAY_TOP_CLASS); } this.overlayElement.style.display = ""; } if (this.overlayElement.parentElement !== this.element) { this.element.appendChild(this.overlayElement); } } _hasSeriesData() { const series = this.options.series || []; const hasData = series.length > 0 && series.some(x => x.data && x.data.length > 0); return hasData; } _initTheme(options, themeOptions) { const seriesCopies = []; const series = options.series || []; for (let i = 0; i < series.length; i++) { seriesCopies.push(Object.assign({}, series[i])); } options.series = seriesCopies; resolveAxisAliases(options); this.applyDefaults(options, themeOptions); // Clean up default if not overriden by data attributes if (options.seriesColors === null) { delete options.seriesColors; } if (isString(options.title)) { options.title = { text: options.title }; } this.options = deepExtend({}, themeOptions, options); this.applySeriesColors(); } getSize() { const chartArea = this.options.chartArea || {}; const width = chartArea.width ? parseInt(chartArea.width, 10) : Math.floor(this.element.offsetWidth); const height = chartArea.height ? parseInt(chartArea.height, 10) : Math.floor(this.element.offsetHeight); return { width: width, height: height }; } resize(force) { const size = this.getSize(); const currentSize = this._size; const hasSize = size.width > 0 || size.height > 0; if (force || hasSize && (!currentSize || size.width !== currentSize.width || size.height !== currentSize.height)) { this._size = size; this._resize(size, force); this.trigger("resize", size); } else if (hasSize && this._selections && find(this._selections, s => !s.visible)) { this._destroySelections(); this._setupSelection(); } } _resize() { this._noTransitionsRedraw(); } redraw(paneName) { this.applyDefaults(this.options); this.applySeriesColors(); if (paneName) { const plotArea = this._model._plotArea; const pane = plotArea.findPane(paneName); plotArea.redraw(pane); } else { this._redraw(); } } getAxis(name) { return findAxisByName(name, this._plotArea.axes); } findAxisByName(name) { return this.getAxis(name); } findPaneByName(name) { const panes = this._plotArea.panes; for (let idx = 0; idx < panes.length; idx++) { if (panes[idx].options.name === name) { return new ChartPane(panes[idx]); } } } findPaneByIndex(idx) { const panes = this._plotArea.panes; if (panes[idx]) { return new ChartPane(panes[idx]); } } plotArea() { return new ChartPlotArea(this._plotArea); } toggleHighlight(show, filter) { const plotArea = this._plotArea; const firstSeries = (plotArea.srcSeries || plotArea.series || [])[0]; let points; if (isFunction(filter)) { points = plotArea.filterPoints(filter); } else { let seriesName, categoryName; if (isObject(filter)) { seriesName = filter.series; categoryName = filter.category; } else { seriesName = categoryName = filter; } if (firstSeries.type === DONUT) { points = pointByCategoryName(plotArea.pointsBySeriesName(seriesName), categoryName); } else if (inArray(firstSeries.type, [ PIE, FUNNEL, PYRAMID ])) { points = pointByCategoryName((plotArea.charts[0] || {}).points, categoryName); } else { points = plotArea.pointsBySeriesName(seriesName); } } if (points) { this.togglePointsHighlight(show, points); } } togglePointsHighlight(show, points) { const highlight = this._highlight; for (let idx = 0; idx < points.length; idx++) { highlight.togglePointHighlight(points[idx], show); } } showTooltip(filter) { const shared = this._sharedTooltip(); const { _tooltip: tooltip, _plotArea: plotArea } = this; let point, categoryIndex; if (isFunction(filter)) { point = plotArea.findPoint(filter); if (point && shared) { categoryIndex = point.categoryIx; } } else if (shared && defined(filter)) { categoryIndex = plotArea.categoryAxis.categoryIndex(filter); } if (shared) { if (categoryIndex >= 0) { const points = this._plotArea.pointsByCategoryIndex(categoryIndex); tooltip.showAt(points); } } else if (point) { tooltip.show(point); } } hideTooltip() { this._tooltip.hide(); } _initSurface() { const surface = this.surface; const wrap = this._surfaceWrap(); const chartArea = this.options.chartArea || {}; if (chartArea.width) { elementSize(wrap, { width: chartArea.width }); } if (chartArea.height) { elementSize(wrap, { height: chartArea.height }); } if (!surface || surface.options.type !== this.options.renderAs) { this._destroySurface(); this.surface = this._createSurface(wrap, { type: this.options.renderAs }); this.surface.bind("mouseenter", this._surfaceMouseenterHandler); this.surface.bind("mouseleave", this._surfaceMouseleaveHandler); } else { this.surface.clear(); this.surface.resize(); } // Override the surface _kendoExportVisual in order to accept export options with size. this.element._kendoExportVisual = this._kendoExportVisual.bind(this); } _surfaceWrap() { return this.element; } _redraw() { const model = this._getModel(); this._size = { width: model.options.width, height: model.options.height }; this._destroyView(); this._setElementAccessibilityAttributes(); this._model = model; this._plotArea = model._plotArea; this._legend = model._legend; model.renderVisual(); const transitions = this.options.transitions; if (transitions !== false) { model.traverse(function(element) { if (element.animation) { const loading = (transitions && transitions !== true) ? transitions.loading : transitions; element.animation.options = Object.assign({}, element.animation.options, loading); element.animation.setup(); } }); } this._initSurface(); this.surface.draw(model.visual); if (transitions !== false) { model.traverse(function(element) { if (element.animation) { element.animation.play(); } }); } this._tooltip = this._createTooltip(); this._highlight = new Highlight(); this._setupSelection(); this._createPannable(); this._createZoomSelection(); this._createMousewheelZoom(); this._setComputedStyles(); this.trigger(RENDER); triggerPaneRender(this._plotArea.panes); if (!this._navState) { this._cancelDomEvents(); } this._redrawFocusHighlight(); } _setComputedStyles() { const titleHeight = this.titleHeight(); this.element.style.setProperty('--kendo-chart-computed-title-height', `${titleHeight}px`); } _redrawFocusHighlight() { if (this._destroyed) { return; } const { _focusState: { legendInFocus, preserveHighlight } } = this; if (legendInFocus && preserveHighlight) { this._focusElement(this._getFocusedLegendItem(), false); this._focusState.preserveHighlight = false; } } _setElementAccessibilityAttributes() { let titleOptions = this.options.title; let title = isString(titleOptions) ? titleOptions : (titleOptions.description || titleOptions.text); if (title) { this.element.setAttribute("aria-roledescription", title); } } _kendoExportVisual(size) { if (size && size.width && size.height) { const chartArea = this._originalOptions.chartArea || {}; const exportOptions = { width: chartArea.width || size.width, height: chartArea.height || size.height }; return this.exportVisual(exportOptions); } return this.exportVisual(); } exportVisual(exportOptions) { let visual; if (exportOptions && (exportOptions.width || exportOptions.height || exportOptions.options)) { const currentOptions = this.options; const options = deepExtend({}, exportOptions.options, { chartArea: { width: exportOptions.width, height: exportOptions.height } }); clearMissingValues(this._originalOptions, options); this.options = deepExtend({}, this._originalOptions, options); this._initTheme(this.options, this._theme); this.bindCategories(); const model = this._getModel(); model.renderVisual(); triggerPaneRender(model._plotArea.panes); visual = model.visual; this.options = currentOptions; } else { visual = this.surface.exportVisual(); } return visual; } _sharedTooltip() { return this._plotArea instanceof CategoricalPlotArea && this.options.tooltip && this.options.tooltip.shared; } _createPannable() { const options = this.options; if (options.pannable !== false) { this._pannable = new Pannable(this._plotArea, options.pannable); } } _createZoomSelection() { const zoomable = this.options.zoomable; const selection = (zoomable || {}).selection; if (zoomable !== false && selection !== false) { this._zoomSelection = new ZoomSelection(this, selection); } } _createMousewheelZoom() { const zoomable = this.options.zoomable; const mousewheel = (zoomable || {}).mousewheel; if (zoomable !== false && mousewheel !== false) { this._mousewheelZoom = new MousewheelZoom(this, mousewheel); } } _toggleDragZoomEvents() { const pannable = this.options.pannable; const zoomable = this.options.zoomable; const selection = (zoomable || {}).selection; const mousewheel = (zoomable || {}).mousewheel; const allowDrag = !pannable && (zoomable === false || selection === false) && !this.requiresHandlers([ DRAG_START, DRAG, DRAG_END ]); const allowZoom = (zoomable === false || mousewheel === false) && !this.requiresHandlers([ ZOOM_START, ZOOM, ZOOM_END ]); const element = this.element; if (this._dragZoomEnabled && allowDrag && allowZoom) { element.style.touchAction = this._touchAction || ''; this._dragZoomEnabled = false; } else if (!this._dragZoomEnabled && !(allowDrag && allowZoom)) { element.style.touchAction = "none"; this._dragZoomEnabled = true; } this._toggleDomEvents(!allowDrag, !allowZoom); } _toggleDomEvents(drag, zoom) { const domEvents = this.domEvents; if (!domEvents) { return; } if (domEvents.toggleDrag) { domEvents.toggleDrag(drag); } if (domEvents.toggleZoom) { domEvents.toggleZoom(zoom); } } _createTooltip() { const { options: { tooltip: tooltipOptions } } = this; let tooltip; if (this._sharedTooltip()) { tooltip = this._createSharedTooltip(tooltipOptions); } else { tooltip = new Tooltip(this.chartService, tooltipOptions); } return tooltip; } _createSharedTooltip(options) { return new SharedTooltip(this._plotArea, options); } applyDefaults(options, themeOptions) { applyAxisDefaults(options, themeOptions); applySeriesDefaults(options, themeOptions); } applySeriesColors() { const options = this.options; const series = options.series; const colors = options.seriesColors || []; for (let i = 0; i < series.length; i++) { const currentSeries = series[i]; const seriesColor = colors[i % colors.length]; const defaults = currentSeries._defaults; currentSeries.color = currentSeries.color || seriesColor; if (defaults) { defaults.color = defaults.color || seriesColor; } } } _getModel() { const options = this.options; const plotArea = this._createPlotArea(); const model = new RootElement(this._modelOptions()); model.chart = this; model._plotArea = plotArea; const title = Title.buildTitle(options.title); const subtitle = Title.buildTitle(options.subtitle, { align: options.title.align, position: options.title.position }); model.append.apply(model, Title.orderTitles([title, subtitle])); if (options.legend && options.legend.visible) { const legend = new Legend(plotArea.options.legend, this.chartService); model.append(legend); model._legend = legend; } model.append(plotArea); model.reflow(); this._setTitleBox(title, subtitle); return model; } _setTitleBox(title, subtitle) { if (!title && !subtitle) { return; } this._titleBox = (title || subtitle).box.clone(); const titlePosition = title ? title.options.position : ''; const subtitlePosition = subtitle ? subtitle.options.position : ''; const samePosition = titlePosition === subtitlePosition; const subtitleAtTop = subtitlePosition !== BOTTOM; if (samePosition && subtitle) { this._titleBox.wrap(subtitle.box); } else if (title && subtitle && subtitleAtTop) { this._titleBox = subtitle.box.clone(); } } _modelOptions() { const options = this.options; const size = this.getSize(); return deepExtend({ transitions: options.transitions, width: size.width || DEFAULT_WIDTH, height: size.height || DEFAULT_HEIGHT }, options.chartArea); } _createPlotArea(skipSeries) { const options = this.options; const plotArea = PlotAreaFactory.current.create(skipSeries ? [] : options.series, options, this.chartService); return plotArea; } _setupSelection() { const { _plotArea: { axes } } = this; const selections = this._selections = []; for (let i = 0; i < axes.length; i++) { const axis = axes[i]; const options = axis.options; if (axis instanceof CategoryAxis && options.select && !options.vertical) { const range = axis.range(); const selection = new Selection(this, axis, deepExtend({ min: range.min, max: range.max }, options.select) ); selections.push(selection); } } } _selectStart(e) { return this.trigger(SELECT_START, e); } _select(e) { return this.trigger(SELECT, e); } _selectEnd(e) { return this.trigger(SELECT_END, e); } _initHandlers() { this._clickHandler = this._click.bind(this); this._keydownHandler = this._keydown.bind(this); this._focusHandler = this._focus.bind(this); this._blurHandler = this._blur.bind(this); this._mousedownHandler = this._mousedown.bind(this); this._mousewheelHandler = this._mousewheel.bind(this); this._mouseleaveHandler = this._mouseleave.bind(this); this._surfaceMouseenterHandler = this._mouseover.bind(this); this._surfaceMouseleaveHandler = this._mouseout.bind(this); this._mousemoveThrottled = throttle( this._mousemove.bind(this), MOUSEMOVE_DELAY ); } addObserver(observer) { if (observer) { this.observers.push(observer); } } removeObserver(observer) { const index = this.observers.indexOf(observer); if (index >= 0) { this.observers.splice(index, 1); } } requiresHandlers(eventNames) { const observers = this.observers; for (let idx = 0; idx < observers.length; idx++) { if (observers[idx].requiresHandlers(eventNames)) { return true; } } } trigger(name, args = {}) { args.sender = this; if (name === SHOW_TOOLTIP) { args.anchor.point = this._toDocumentCoordinates(args.anchor.point); } else if (name === SERIES_OVER) { this._updateDrilldownPoint(args.point); } else if (name === SERIES_LEAVE) { this._resetDrilldownPoint(); } else if (name === SERIES_CLICK) { this._focusPoint(args.point); this._startDrilldown(args.point); } else if (name === LEGEND_ITEM_CLICK) { this._focusLegendItem(args); } const observers = this.observers; let isDefaultPrevented = false; for (let idx = 0; idx < observers.length; idx++) { if (observers[idx].trigger(name, args)) { isDefaultPrevented = true; } } return isDefaultPrevented; } titleHeight() { if (!this._titleBox) { return 0; } return this._titleBox.height(); } _attachEvents() { const element = this.element; this._touchAction = element.style.touchAction; bindEvents(element, { [ CONTEXTMENU ]: this._clickHandler, [ MOUSEWHEEL ]: this._mousewheelHandler, [ MOUSELEAVE ]: this._mouseleaveHandler, [ KEYDOWN ]: this._keydownHandler, [ MOUSEDOWN ]: this._mousedownHandler, [ FOCUS ]: this._focusHandler, [ BLUR]: this._blurHandler }); if (this._shouldAttachMouseMove()) { bindEvents(element, { [ MOUSEMOVE ]: this._mousemoveThrottled }); } this.domEvents = DomEventsBuilder.create(this.element, { start: this._start.bind(this), move: this._move.bind(this), end: this._end.bind(this), tap: this._tap.bind(this), gesturestart: this._gesturestart.bind(this), gesturechange: this._gesturechange.bind(this), gestureend: this._gestureend.bind(this) }); this._toggleDragZoomEvents(); } _mouseleave(e) { if (this._hoveredPoint) { this._hoveredPoint.out(this, e); this._hoveredPoint = null; } if (this._plotAreaHovered) { this._plotAreaHovered = false; this.trigger(PLOT_AREA_LEAVE); } if (this._hasInactiveOpacity() && this._activeChartInstance) { this._applySeriesOpacity(this._activeChartInstance.children, null, true); this._updateSeriesOpacity(null, true); } } _cancelDomEvents() { if (this.domEvents && this.domEvents.cancel) { this.domEvents.cancel(); } } _gesturestart(e) { if (this._mousewheelZoom && !this._stopChartHandlers(e)) { this._gestureDistance = e.distance; this._unsetActivePoint(); this._clearFocusedElement(); this.surface.suspendTracking(); } } _gestureend(e) { if (this._zooming && !this._stopChartHandlers(e)) { if (this.surface) { this.surface.resumeTracking(); } this._zooming = false; this.trigger(ZOOM_END, {}); } } _gesturechange(e) { const mousewheelZoom = this._mousewheelZoom; if (mousewheelZoom && !this._stopChartHandlers(e)) { e.preventDefault(); const previousGestureDistance = this._gestureDistance; let scaleDelta = -e.distance / previousGestureDistance + 1; if (Math.abs(scaleDelta) >= 0.1) { scaleDelta = Math.round(scaleDelta * 10); this._gestureDistance = e.distance; const args = { delta: scaleDelta, axisRanges: axisRanges(this._plotArea.axes), originalEvent: e }; if (this._zooming || !this.trigger(ZOOM_START, args)) { const coords = this._eventCoordinates(e); if (!this._zooming) { this._zooming = true; } const ranges = args.axisRanges = mousewheelZoom.updateRanges(scaleDelta, coords); if (ranges && !this.trigger(ZOOM, args)) { mousewheelZoom.zoom(); } } } } } _mouseout(e) { if (e.element) { const element = this._drawingChartElement(e.element, e); if (element && element.leave) { element.leave(this, e.originalEvent); } } } _start(e) { const coords = this._eventCoordinates(e); if (this._stopChartHandlers(e) || !this._plotArea.backgroundContainsPoint(coords)) { return; } if (this.requiresHandlers([ DRAG_START, DRAG, DRAG_END ])) { this._startNavigation(e, coords, DRAG_START); } if (this._pannable && this._pannable.start(e)) { this.surface.suspendTracking(); this._unsetActivePoint(); this._clearFocusedElement(); this._suppressHover = true; this.chartService.panning = true; } if (this._zoomSelection) { if (this._zoomSelection.start(e)) { this.trigger(ZOOM_START, { axisRanges: axisRanges(this._plotArea.axes), originalEvent: e }); } } } _move(e) { let { _navState: state, _pannable: pannable } = this; if (this._stopChartHandlers(e)) { return; } if (pannable) { const ranges = pannable.move(e); if (ranges && !this.trigger(DRAG, { axisRanges: ranges, originalEvent: e })) { pannable.pan(); } } else if (state) { const ranges = {}; const axes = state.axes; for (let i = 0; i < axes.length; i++) { const currentAxis = axes[i]; const axisName = currentAxis.options.name; if (axisName) { const axis = currentAxis.options.vertical ? e.y : e.x; const delta = axis.startLocation - axis.location; if (delta !== 0) { ranges[currentAxis.options.name] = currentAxis.translateRange(delta); } } } state.axisRanges = ranges; this.trigger(DRAG, { axisRanges: ranges, originalEvent: e }); } if (this._zoomSelection) { this._zoomSelection.move(e); } } _end(e) { if (this._stopChartHandlers(e)) { return; } const pannable = this._pannable; if (pannable && pannable.end(e)) { this.surface.resumeTracking(); this.trigger(DRAG_END, { axisRanges: axisRanges(this._plotArea.axes), originalEvent: e }); this._suppressHover = false; this.chartService.panning = false; } else { this._endNavigation(e, DRAG_END); } if (this._zoomSelection) { const ranges = this._zoomSelection.end(e); if (ranges && !this.trigger(ZOOM, { axisRanges: ranges, originalEvent: e })) { this._zoomSelection.zoom(); this.trigger(ZOOM_END, { axisRanges: ranges, originalEvent: e }); } } } _stopChartHandlers(e) { const selections = this._selections || []; if (!selections.length) { return false; } const coords = this._eventCoordinates(e); const pane = this._plotArea.paneByPoint(coords); if (pane) { for (let idx = 0; idx < selections.length; idx++) { if (selections[idx].onPane(pane)) { return true; } } } } _mousewheelZoomRate() { const zoomable = this.options.zoomable; const mousewheel = (zoomable || {}).mousewheel || {}; return valueOrDefault(mousewheel.rate, MOUSEWHEEL_ZOOM_RATE); } _mousewheel(e) { const delta = mousewheelDelta(e); const mousewheelZoom = this._mousewheelZoom; const coords = this._eventCoordinates(e); if (this._stopChartHandlers(e) || !this._plotArea.backgroundContainsPoint(coords)) { return; } if (mousewheelZoom) { const args = { delta: delta, axisRanges: axisRanges(this._plotArea.axes), originalEvent: e }; if (this._zooming || !this.trigger(ZOOM_START, args)) { e.preventDefault(); if (!this._zooming) { this._unsetActivePoint(); this._clearFocusedElement(); this.surface.suspendTracking(); this._zooming = true; } if (this._mwTimeout) { clearTimeout(this._mwTimeout); } args.axisRanges = mousewheelZoom.updateRanges(delta, coords); if (args.axisRanges && !this.trigger(ZOOM, args)) { mousewheelZoom.zoom(); } this._mwTimeout = setTimeout(() => { this.trigger(ZOOM_END, args); this._zooming = false; if (this.surface) { this.surface.resumeTracking(); } }, MOUSEWHEEL_DELAY); } } else { let state = this._navState; if (!state) { const prevented = this._startNavigation(e, coords, ZOOM_START); if (!prevented) { state = this._navState; } } if (state) { const totalDelta = state.totalDelta || delta; state.totalDelta = totalDelta + delta; const axes = this._navState.axes; const ranges = {}; for (let i = 0; i < axes.length; i++) { const currentAxis = axes[i]; const axisName = currentAxis.options.name; if (axisName) { ranges[axisName] = currentAxis.scaleRange(-totalDelta * this._mousewheelZoomRate(), coords); } } this.trigger(ZOOM, { delta: delta, axisRanges: ranges, originalEvent: e }); if (this._mwTimeout) { clearTimeout(this._mwTimeout); } this._mwTimeout = setTimeout(() => { this._endNavigation(e, ZOOM_END); }, MOUSEWHEEL_DELAY); } } } _startNavigation(e, coords, chartEvent) { const plotArea = this._model._plotArea; const pane = plotArea.findPointPane(coords); const axes = plotArea.axes.slice(0); if (!pane) { return; } const ranges = axisRanges(axes); const prevented = this.trigger(chartEvent, { axisRanges: ranges, originalEvent: e }); if (prevented) { this._cancelDomEvents(); } else { this._suppressHover = true; this._unsetActivePoint(); this._clearFocusedElement(); this._navState = { axisRanges: ranges, pane: pane, axes: axes }; } } _endNavigation(e, chartEvent) { if (this._navState) { this.trigger(chartEvent, { axisRanges: this._navState.axisRanges, originalEvent: e }); this._suppressHover = false; this._navState = null; } } _getChartElement(e, match) { const element = this.surface.eventTarget(e); if (element) { return this._drawingChartElement(element, e, match); } } _drawingChartElement(element, e, match) { let current = element; let chartElement; while (current && !chartElement) { chartElement = current.chartElement; current = current.parent; } if (chartElement) { if (chartElement.aliasFor) { chartElement = chartElement.aliasFor(e, this._eventCoordinates(e)); } if (match) { chartElement = chartElement.closest(match); if (chartElement && chartElement.aliasFor) { chartElement = chartElement.aliasFor(); } } return chartElement; } } _eventCoordinates(e) { const coordinates = eventCoordinates(e); return this._toModelCoordinates(coordinates.x, coordinates.y); } _elementPadding() { if (!this._padding) { const { paddingLeft, paddingTop } = elementStyles(this.element, [ "paddingLeft", "paddingTop" ]); this._padding = { top: paddingTop, left: paddingLeft }; } return this._padding; } _toDocumentCoordinates(point) { const padding = this._elementPadding(); const offset = elementOffset(this.element); return { left: round(point.x + padding.left + offset.left), top: round(point.y + padding.top + offset.top) }; } // TODO: Breaking change due to peer version change // Reuse by exposing _surfacePoint on Surface _toModelCoordinates(clientX, clientY) { const element = this.element; const offset = elementOffset(element); const padding = this._elementPadding(); const inverseTransform = elementScale(element).invert(); const point = new geometry.Point( clientX - offset.left - padding.left, clientY - offset.top - padding.top ).transform(inverseTransform); return new Point(point.x, point.y); } _tap(e) { const drawingElement = this.surface.eventTarget(e); const element = this._drawingChartElement(drawingElement, e); const sharedTooltip = this._sharedTooltip(); if (!this._startHover(drawingElement, e) && !sharedTooltip) { this._unsetActivePoint(); } if (sharedTooltip) { this._trackSharedTooltip(this._eventCoordinates(e), e, true); } this._propagateClick(element, e); //part of fix for hover issue on windows touch this.handlingTap = true; setTimeout(() => { this.handlingTap = false; }, 0); } _click(e) { const element = this._getChartElement(e); this._propagateClick(element, e); } _propagateClick(element, e) { let current = element; while (current) { if (current.click) { current.click(this, e); } current = current.parent; } } _isLegendBeforeChart() { const { options: { legend: { position: legendPosition } }, _legend: legend } = this; return legend && legend.hasItems() && (legendPosition === TOP || legendPosition === LEFT); } _focus() { if (!this._preventInitialPointFocus) { if (this._isLegendBeforeChart()) { this._focusFirstLegendItem(); } else { this._focusFirstPoint(); } } this._preventInitialPointFocus = false; } _keydown(e) { const { _focusState: { legendInFocus, focusedElement }, _legend: legend } = this; if (e.key === TAB) { this._clearFocusedElement(); const isLegendBeforeChart = this._isLegendBeforeChart(); if (legendInFocus && isLegendBeforeChart !== e.shiftKey) { this._navigatePoints(e); } else if (!legendInFocus && isLegendBeforeChart === e.shiftKey && legend.hasItems()) { this._navigateLegend(e); } } else if (e.key === ESCAPE) { if (focusedElement) { e.stopPropagation(); } if (this._tooltip && this._tooltip.visible) { this._hideTooltip(); } else { this._blur(); } } else if (e.key === ENTER) { if (focusedElement) { this._focusState.preserveHighlight = true; this._propagateClick(focusedElement, e); this._focusElement(focusedElement); } } else if (!legendInFocus) { this._navigatePoints(e); } else { this._navigateLegend(e); } } _navigatePoints(e) { const { _focusState: focusState, _plotArea: plotArea } = this; focusState.legendInFocus = false; if (!focusState.focusedElement) { this._focusFirstPoint(); e.preventDefault(); return; } const moveFocus = (point) => { focusState.focusedPoint = point; this._focusElement(focusState.focusedPoint); this._displayTooltip(point); e.preventDefault(); }; switch (e.key) { case ARROW_RIGHT: moveFocus(plotArea.getPointToTheRight(focusState.focusedPoint)); break; case ARROW_LEFT: moveFocus(plotArea.getPointToTheLeft(focusState.focusedPoint)); break; case ARROW_DOWN: moveFocus(plotArea.getPointBelow(focusState.focusedPoint)); break; case ARROW_UP: moveFocus(plotArea.getPointAbove(focusState.focusedPoint)); break; default: break; } } _navigateLegend(e) { const { _focusState: focusState, _legend: legend, chartService: { rtl: rtl } } = this; focusState.legendInFocus = true; if (!focusState.focusedElement) { this._focusFirstLegendItem(); e.preventDefault(); return; } const itemsLength = legend.getItems().length; const moveFocus = (cycleFunc) => { focusState.focusedLegendItemIndex = cycleFunc( focusState.focusedLegendItemIndex, itemsLength ); this._focusElement(this._getFocusedLegendItem()); e.preventDefault(); }; switch (e.key) { case ARROW_UP: case ARROW_LEFT: moveFocus(rtl ? cycleUp : cycleDown); break; case ARROW_DOWN: case ARROW_RIGHT: moveFocus(rtl ? cycleDown : cycleUp); break; default: break; } } _focusFirstPoint() { const point = this._focusState.focusedPoint = this._plotArea.getFirstPoint(); if (point) { this._focusElement(point); this._displayTooltip(point); } } _hasFocus() { return this.element.ownerDocument.activeElement === this.element; } _mousedown() { if (!this._hasFocus()) { this._preventInitialPointFocus = true; } } _focusChart() { if (!this._hasFocus()) { this._preventInitialPointFocus = true; this.element.focus(); } } _focusPoint(point) { this._focusState.focusedPoint = point; this._focusChart(); this._focusElement(point, true); } _focusFirstLegendItem() { const { _focusState: focusState } = this; focusState.focusedLegendItemIndex = 0; this._focusElement(this._getFocusedLegendItem()); focusState.legendInFocus = true; this._hideTooltip(); } _focusLegendItem(args) { const { _focusState: focusState } = this; focusState.focusedLegendItemIndex = this._legend .getItems() .findIndex(x => x.options.series.index === args.seriesIndex && x.options.pointIndex === args.pointIndex); focusState.legendInFocus = true; this._focusChart(); this._focusElement(this._getFocusedLegendItem(), true); } _getFocusedLegendItem() { const { _focusState: focusState, _legend: legend } = this; return legend.getItems()[focusState.focusedLegendItemIndex]; } _focusElement(element, omitHighlight) { const { _focusState: focusState } = this; this._clearFocusedElement(); if (!element) { return; } focusState.focusedElement = element; this._setElementActiveDescendant(element); if (!omitHighlight) { element.focusVisual(); if (focusState.legendInFocus) { const options = element.options; this._showSeriesInactiveOpacity(options.series.index, options.pointIndex); } else { this._showInactiveOpacity(element); } } } _clearFocusedElement() { const { _focusState: focusState } = this; if (!focusState) { return; } if (focusState.focusedElement && focusState.focusedElement.clearFocusFromVisual) { focusState.focusedElement.clearFocusFromVisual(); this._clearElementActiveDescendant(); } focusState.focusedElement = null; } _setElementActiveDescendant(element) { if (this.options.renderAs === "canvas") { this._pseudoFocusedElement = this._createPseudoFocusedElement(element); this.element.append(this._pseudoFocusedElement); } this.element.setAttribute(ARIA_ACTIVE_DESCENDANT, element._id); } _clearElementActiveDescendant() { if (this._pseudoFocusedElement) { this._pseudoFocusedElement.remove(); this._pseudoFocusedElement = null; } this.element.removeAttribute(ARIA_ACTIVE_DESCENDANT); } _createPseudoFocusedElement(element) { const pseudoElement = document.createElement("div"); const accessibilityOptions = element.options.accessibility; pseudoElement.id = element._id; pseudoElement.setAttribute("aria-label", element.getAriaLabelText()); pseudoElement.setAttribute("role", accessibilityOptions.role); pseudoElement.setAttribute("aria-roledescription", accessibilityOptions.ariaRoleDescription); const checked = accessibilityOptions.ariaChecked; if (defined(checked)) { pseudoElement.setAttribute("aria-checked", checked); } return pseudoElement; } _blur() { this._focusState.legendInFocus = false; this._clearFocusedElement(); this._hideInactiveOpacity(); } _startHover(element, e) { if (this._suppressHover) { return false; } let point = this._drawingChartElement(element, e, function(element) { return (element.hover || element.over) && !(element instanceof PlotAreaBase); }); const activePoint = this._activePoint; this._updateHoveredPoint(point, e); if (point && activePoint !== point && point.hover) { this._activePoint = point; if (!this._sharedTooltip() && !point.hover(this, e)) { this._displayTooltip(point); this._showInactiveOpacity(point); } } return point; } _displayTooltip(point) { const tooltipOptions = deepExtend({}, this.options.tooltip, point.options.tooltip); if (tooltipOptions.visible) { if (this._sharedTooltip() && point.box) { this._trackSharedTooltip(point.box.center(), {}); } else { this._tooltip.show(point); } } } _hideTooltip() { if (this._tooltip) { this._tooltip.hide(); } } _displayInactiveOpacity(activePoint, multipleSeries, highlightPoints) { const chartInstance = this._activeChartInstance = this._chartInstanceFromPoint(activePoint); if (!chartInstance) { return; } if (multipleSeries) { this._updateSeriesOpacity(activePoint); this._applySeriesOpacity(chartInstance.children, null, true); this._applySeriesOpacity(chartInstance.children, activePoint.series); this._highlight.show(highlightPoints || activePoint); } else { let inactivePoints; if (!chartInstance.supportsPointInactiveOpacity()) { this._highlight.show(activePoint); return; } inactivePoints = this._getInactivePoints(activePoint, chartInstance); if (inactivePoints && inactivePoints.length) { this._highlight.show(inactivePoints, 1 - this._getInactiveOpacityForSeries(activePoint.series)); } } } _getInactivePoints(activePoint, chartInstance) { let allPoints = this._getAllPointsOfType(chartInstance, activePoint.constructor); return allPoints.filter(point => point !== activePoint); } _getAllPointsOfType(container, type) { let points = []; for (let i = 0; i < container.children.length; i++) { const element = container.children[i]; if (element.constructor === type) { points.push(element); } else if (element.children && element.children.length) { points = points.concat(this._getAllPointsOfType(element, type)); } } return points; } _updateHoveredPoint(point, e) { const hoveredPoint = this._hoveredPoint; if (hoveredPoint && hoveredPoint !== point) { hoveredPoint.out(this, e); this._hoveredPoint = null; } if (point && hoveredPoint !== point && point.over) { this._hoveredPoint = point; point.over(this, e); } } _updateDrilldownPoint(point) { if (!point || !point.series) { return; } const { fields } = SeriesBinder.current.bindPoint(point.series, null, point.dataItem); if (fields.drilldown) { this._drilldownState = { cursor: this.element.style.cursor }; this.element.style.cursor = 'pointer'; } } _resetDrilldownPoint() { if (this._drilldownState) { this.element.style.cursor = this._drilldownState.cursor; this._drilldownState = null; } } _startDrilldown(point) { if (!point || !point.series) { return; } const series = point.series; const { fields } = SeriesBinder.current.bindPoint(series, null, point.dataItem); const value = fields.drilldown; if (value) {