@progress/kendo-charts
Version:
Kendo UI platform-independent Charts library
1,602 lines (1,297 loc) • 72.4 kB
JavaScript
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) {