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';
import { setupStyleObserver } from '../common/style-observer';
import { chartCSSVariables } from './theme/load-theme';

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();
                this._initStyleObserver();
            }
        });
    }

    _initStyleObserver() {
        this._destroyStyleObserver();
        if (this.options.observeStyles) {
            this._stylesObserver = setupStyleObserver(this.element, () => {
                this.trigger("styleChanged");
            }, Object.values(chartCSSVariables));
        }
    }

    _destroyStyleObserver() {
        if (this._stylesObserver) {
            this._stylesObserver.disconnect();
            this._stylesObserver = null;
        }
    }

    _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;
        const motionConfig = this.options.motion || {};
        if (transitions !== false) {
            model.traverse(function(element) {
                if (element.animation) {
                    element.animation.options = Object.assign({}, element.animation.options,
                        {duration: undefined,
                        motionConfig: Object.assign({}, motionConfig)});
                    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;
    }

    _toggleCategoryHighlight(index, axis) {
        const plotArea = this._plotArea;
        const categoryAxis = axis || plotArea.categoryAxis;

        if (this._categoryHighlightIndex === index && this._categoryHighlightAxis === categoryAxis) {
            return;
        }

        this._categoryHighlightIndex = index;
        this._categoryHighlightAxis = categoryAxis;

        const highlightOptions = categoryAxis ? (categoryAxis.options || {}).highlight : null;

        if (!highlightOptions || !highlightOptions.visible || index === null) {
            if (this._categoryHighlight) {
                this._categoryHighlight.visible(false);
            }
            return;
        }

        if (!this._categoryHighlight) {
            this._categoryHighlight = new draw.Path({
                fill: {
                    color: highlightOptions.color,
                    opacity: highlightOptions.opacity
                },
                stroke: highlightOptions.border || null,
                closed: true,
                zIndex: -1
            });
        } else {
            this._categoryHighlight.fill(highlightOptions.color, highlightOptions.opacity);
            this._categoryHighlight.stroke(highlightOptions.border || null);
        }

        const slot = categoryAxis.getSlot(index);
        const rect = slot.toRect();
        let plotBox = plotArea.box;

        if (categoryAxis.pane) {
            plotBox = categoryAxis.pane.chartsBox();
        }

        if (categoryAxis.options.vertical) {
            rect.origin.x = plotBox.x1;
            rect.size.width = plotBox.width();
        } else {
            rect.origin.y = plotBox.y1;
            rect.size.height = plotBox.height();
        }

        const newPath = draw.Path.fromRect(rect);
        this._categoryHighlight.segments.elements(newPath.segments.elements());
        this._categoryHighlight.visible(true);

        const group = (categoryAxis.pane || this._plotArea).visual;
        if (group && this._categoryHighlight.parent !== group) {
            if (this._categoryHighlight.parent) {
                this._categoryHighlight.parent.remove(this._categoryHighlight);
            }
            group.insert(0, this._categoryHighlight);
        }
    }

    _categoryHighlightEnabled() {
        const plotArea = this._plotArea;
        const categoryAxis = plotArea.categoryAxis || {};
        const highlightOptions = (categoryAxis.options || {}).highlight;

        return plotArea instanceof CategoricalPlotArea && highlightOptions && highlightOptions.visible;
    }

    _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, 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) {
            const args = { series, point, value, sender: this };
            this.trigger(DRILLDOWN, args);
        }
    }

    _updateSeriesOpacity(point, resetOpacity) {
        const plotArea = this._plotArea;
        const length = plotArea.series.length;

        for (let i = 0; i < length; i++) {
            const currSeries = plotArea.series[i];
            const defaultOpacity = this._getDefaultOpacityForSeries(currSeries);
            const inactiveOpacity = this._getInactiveOpacityForSeries(currSeries);

            if (!resetOpacity && currSeries !== point.series) {
                currSeries.defaultOpacity = defaultOpacity;
                currSeries.opacity = inactiveOpacity;

                if (currSeries.line) {
                    currSeries.line.opacity = inactiveOpacity;
                }
            } else {
                currSeries.opacity = defaultOpacity;

                if (currSeries.line) {
                    currSeries.line.opacity = defaultOpacity;
                }
            }
        }
    }

    _applySeriesOpacity(elements, activeSeries, reset, series) {
        for (let i = 0; i < elements.length; i++) {
            const element = elements[i];
            const currSeries = element.series || series;
            const shouldHighlight = currSeries && (currSeries.highlight || {}).visible;

            if (shouldHighlight && element.visual) {
                const opacity = series ? series.opacity : element.series.opacity;
                if (currSeries !== activeSeries || reset) {
                    element.visual.opacity(reset ? 1 : opacity);
                }
            }

            if (element.children && element.children.length) {
                this._applySeriesOpacity(element.children, activeSeries, reset, element.series);
            }
        }
    }

    _chartInstanceFromPoint(point) {
        let chartInstance = point.parent;

        while (chartInstance && !chartInstance.plotArea) {
            chartInstance = chartInstance.parent;
        }

        return chartInstance;
    }

    _showInactiveOpacity(point) {
        const multipleSeries = this._plotArea.series.length > 1;
        const hasInactiveOpacity = this._hasInactiveOpacity();

        if (hasInactiveOpacity) {
            this._displayInactiveOpacity(point, multipleSeries);
        } else {
            this._highlight.show(point);
        }
    }

    _hideInactiveOpacity(point) {
        const multipleSeries = this._plotArea.series.length > 1;
        const hasInactiveOpacity = this._hasInactiveOpacity();
        if (hasInactiveOpacity) {
            if (multipleSeries && this._activeChartInstance) {
                this._updateSeriesOpacity(point, true);
                this._applySeriesOpacity(this._activeChartInstance.children, null, true);
                this._activeChartInstance = null;
            }
            this._highlight && this._highlight.hide();
            this._activePoint = null;
        }
    }

    _hasInactiveOpacity() {
        let hasDefaultInactiveOpacity = this.options.seriesDefaults.highlight.inactiveOpacity !== undefined;
        let hasInactiveOpacity = this.options.series.filter(s => s.highlight.inactiveOpacity !== undefined ).length > 0;
        return hasDefaultInactiveOpacity || hasInactiveOpacity;
    }

    _getInactiveOpacityForSeries(series) {
        let defaultInactiveOpacity = this.options.seriesDefaults.highlight.inactiveOpacity;
        let seriesInactiveOpacity = series.highlight.inactiveOpacity;

        if (seriesInactiveOpacity !== undefined) {
            return seriesInactiveOpacity;
        }
        if (defaultInactiveOpacity !== undefined) {
            return defaultInactiveOpacity;
        }
        if (series.opacity !== undefined) {
            return series.opacity;
        }
        return DEFAULT_SERIES_OPACITY;
    }

    _getDefaultOpacityForSeries(series) {
        return series.defaultOpacity || series.opacity || DEFAULT_SERIES_OPACITY;
    }

    _mouseover(e) {
        const point = this._startHover(e.element, e.originalEvent);

        if (point && point.tooltipTracking && !this._mouseMoveTrackHandler && !this._sharedTooltip()) {
            this._mouseMoveTrackHandler = this._mouseMoveTracking.bind(this);
            bindEvents(document, {
                [ MOUSEMOVE ]: this._mouseMoveTrackHandler
            });
        }
    }

    _mouseMoveTracking(e) {
        const { options, _tooltip: tooltip, _highlight: highlight, _activePoint: point } = this;
        const coords = this._eventCoordinates(e);

        if (this._plotArea.box.containsPoint(coords)) {
            if (point && point.tooltipTracking && point.series && point.parent.getNearestPoint) {
                const seriesPoint = point.parent.getNearestPoint(coords.x, coords.y, point.seriesIx);
                if (seriesPoint && seriesPoint !== point) {
                    this._activePoint = seriesPoint;

                    if (!seriesPoint.hover(this, e)) {
                        const tooltipOptions = deepExtend({}, options.tooltip, seriesPoint.options.tooltip);
                        if (tooltipOptions.visible) {
                            tooltip.show(seriesPoint);
                        }

                        highlight.show(seriesPoint);
                    }
                }
            }
        } else {
            unbindEvents(document, {
                [ MOUSEMOVE ]: this._mouseMoveTrackHandler
            });
            this._unsetActivePoint();
            this._clearFocusedElement();
            this._mouseMoveTrackHandler = null;

            this._hideInactiveOpacity(point);
        }
    }

    _mousemove(e) {
        const coords = this._eventCoordinates(e);
        const plotArea = this._plotArea;

        this._trackCrosshairs(coords);

        if (plotArea.hover) {
            const overPlotArea = plotArea.backgroundContainsPoint(coords);
            if (overPlotArea) {
                this._plotAreaHovered = true;
                this._plotArea.hover(this, e);
            } else if (this._plotAreaHovered && !overPlotArea) {
                this._plotAreaHovered = false;
                this.trigger(PLOT_AREA_LEAVE);
            }
        }

        if (this._sharedTooltip()) {
            this._trackSharedTooltip(coords, e);
        }

        if (this._categoryHighlightEnabled()) {
            this._trackCategoryHighlight(coords);
        }
    }

    _trackCategoryHighlight(coords) {
        const { _plotArea: plotArea } = this;
        const categoryAxis = plotArea.categoryAxisByPoint(coords) || plotArea.categoryAxis;

        if (plotArea.backgroundContainsPoint(coords)) {
            const index = categoryAxis.pointCategoryIndex(coords);
            this._toggleCategoryHighlight(index, categoryAxis);
        } else {
            this._toggleCategoryHighlight(null);
        }
    }

    _trackCrosshairs(coords) {
        const crosshairs = this._plotArea.crosshairs;

        for (let i = 0; i < crosshairs.length; i++) {
            const current = crosshairs[i];

            if (current.box.containsPoint(coords)) {
                current.showAt(coords);
            } else {
                current.hide();
            }
        }
    }

    _trackSharedTooltip(coords, e, toggle) {
        if (this._suppressHover) {
            return;
        }

        const { options: { tooltip: tooltipOptions }, _plotArea: plotArea, _plotArea: { categoryAxis }, _tooltip: tooltip, _highlight: highlight } = this;

        if (plotArea.backgroundContainsPoint(coords)) {
            const index = categoryAxis.pointCategoryIndex(coords);
            if (index !== this._tooltipCategoryIx || (!this._sharedHighlight && toggle)) {
                const points = plotArea.pointsByCategoryIndex(index);
                const pointArgs = points.map(function(point) {
                    return point.eventArgs(e);
                });
                const hoverArgs = pointArgs[0] || {};
                hoverArgs.categoryPoints = pointArgs;

                if (points.length > 0 && !this.trigger(SERIES_HOVER, hoverArgs)) {
                    if (tooltipOptions.visible) {
                        tooltip.showAt(points, coords);
                    }

                    highlight.show(points);

                    this._sharedHighlight = true;
                } else {
                    tooltip.hide();
                }

                this._tooltipCategoryIx = index;
            } else if (toggle && this._sharedHighlight) {
                highlight.hide();
                tooltip.hide();
                this._sharedHighlight = false;
            }
        } else if (this._sharedHighlight) {
            highlight.hide();
            tooltip.hide();
            this._tooltipCategoryIx = null;
            this._sharedHighlight = false;
        }
    }

    hideElements(options) {
        const plotArea = this._plotArea;
        this._mousemoveThrottled.cancel();

        plotArea.hideCrosshairs();

        this._unsetActivePoint(options);

        this._toggleCategoryHighlight(null);
    }

    _unsetActivePoint(options) {
        const { _highlight: highlight } = this;

        this._activePoint = null;
        this._hoveredPoint = null;

        if (!options || !options.keepTooltipOpen) {
            this._hideTooltip();
        }

        this._tooltipCategoryIx = null;
        this._sharedHighlight = false;

        if (highlight) {
            highlight.hide();
        }
    }

    _deferRedraw() {
        this._redraw();
    }

    _clearRedrawTimeout() {
        if (this._redrawTimeout) {
            clearInterval(this._redrawTimeout);
            this._redrawTimeout = null;
        }
    }

    bindCategories() {
        const options = this.options;
        const definitions = [].concat(options.categoryAxis);

        for (let axisIx = 0; axisIx < definitions.length; axisIx++) {
            const axis = definitions[axisIx];
            if (axis.autoBind !== false) {
                this.bindCategoryAxisFromSeries(axis, axisIx);
            }
        }
    }

    bindCategoryAxisFromSeries(axis, axisIx) {
        const uniqueCategories = new Set();
        const seriesOnAxis = this.options.series.filter(series => series.categoryAxis === axis.name || (!series.categoryAxis && axisIx === 0));
        const hasCategoryBinding = seriesOnAxis.some(series => Boolean(series.categoryField));
        const seriesWithData = seriesOnAxis.filter(series => series.data && series.data.length > 0);
        const categorySamples = seriesWithData.map(series => SeriesBinder.current.bindPoint(series, 0).fields.category);
        const dateAxis = categorySamples.reduce((result, firstCategory) => result || isDateAxis(axis, firstCategory), false);
        const seriesWithBinding = seriesWithData.filter((series, seriesIx) => series.categoryField || defined(categorySamples[seriesIx]));

        seriesWithBinding.forEach((series) => series.data.forEach((row, index) => {
            let category = SeriesBinder.current.bindPoint(series, index).fields.category;
            if (dateAxis) {
                const date = parseDateCategory(category, row, this.chartService.intl);
                category = date ? date.getTime() : undefined;
            }

            uniqueCategories.add(category);
        }));

        if (uniqueCategories.size > 0) {
            let categories = Array.from(uniqueCategories.values());
            if (dateAxis) {
                categories = categories.sort().map(time => time && new Date(time));
            }

            axis.categories = categories;
        } else if (hasCategoryBinding) {
            axis.categories = [];
        }
    }

    _isBindable(series) {
        const valueFields = SeriesBinder.current.valueFields(series);
        let result = true;

        for (let i = 0; i < valueFields.length; i++) {
            let field = valueFields[i];
            if (field === VALUE) {
                field = "field";
            } else {
                field = field + "Field";
            }

            if (!defined(series[field])) {
                result = false;
                break;
            }
        }

        return result;
    }

    _noTransitionsRedraw() {
        const options = this.options;
        let transitionsState;

        if (options.transitions !== false) {
            transitionsState = options.transitions;
            options.transitions = false;
        }

        this._redraw();

        if (transitionsState) {
            options.transitions = transitionsState;
        }
    }

    _legendItemHover(seriesIndex, pointIndex) {
        this._showSeriesInactiveOpacity(seriesIndex, pointIndex);
    }

    _showSeriesInactiveOpacity(seriesIndex, pointIndex) {
        const { _plotArea: plotArea, _highlight: highlight } = this;
        const currentSeries = (plotArea.srcSeries || plotArea.series)[seriesIndex];
        let items;

        if (inArray(currentSeries.type, [ PIE, DONUT, FUNNEL, PYRAMID ])) {
            items = plotArea.findPoint(function(point) {
                return point.series.index === seriesIndex && point.index === pointIndex;
            });
        } else {
            items = plotArea.pointsBySeriesIndex(seriesIndex);
        }

        if (this._hasInactiveOpacity() && currentSeries.visible && items) {
            const multipleSeries = plotArea.series.length > 1;
            const point = items.length ? items[0] : items;

            this._displayInactiveOpacity(point, multipleSeries, items);
        } else {
            highlight.show(items);
        }
    }

    _shouldAttachMouseMove() {
        return this._plotArea.crosshairs.length || (this._tooltip && this._sharedTooltip()) || this._categoryHighlightEnabled() || this.requiresHandlers([ PLOT_AREA_HOVER, PLOT_AREA_LEAVE ]);
    }

    updateMouseMoveHandler() {
        unbindEvents(this.element, {
            [ MOUSEMOVE ]: this._mousemoveThrottled
        });

        if (this._shouldAttachMouseMove()) {
            bindEvents(this.element, {
                [ MOUSEMOVE ]: this._mousemoveThrottled
            });
        }
    }

    applyTheme(theme) {
        const options = Object.assign({}, this._originalOptions);
        requestAnimationFrame(() => {
            this.applyOptions(options, theme);
            this.noTransitionsRedraw();
        });
    }

    applyOptions(options, theme) {
        clearMissingValues(this._originalOptions, options);
        this._originalOptions = deepExtend(this._originalOptions, options);
        this.options = deepExtend({}, this._originalOptions);

        if (theme) {
            this._theme = theme;
            this.chartService.theme = theme;
        }
        this._initTheme(this.options, this._theme);

        this._toggleDragZoomEvents();
    }

    setOptions(options, theme) {
        this.applyOptions(options, theme);
        this.bindCategories();
        this.redraw();
        this.updateMouseMoveHandler();
        this._restoreOverlayElement();
        this._initStyleObserver();
    }

    setDirection(rtl) {
        this.chartService.rtl = Boolean(rtl);
        if (this.surface && this.surface.type === 'svg') {
            this._destroySurface();
        }
    }

    setIntlService(intl) {
        this.chartService.intl = intl;
    }

    noTransitionsRedraw() {
        this._noTransitionsRedraw();
    }

    destroy() {
        this._destroyed = true;

        unbindEvents(this.element, {
            [ CONTEXTMENU ]: this._clickHandler,
            [ MOUSEWHEEL ]: this._mousewheelHandler,
            [ MOUSEMOVE ]: this._mousemoveThrottled,
            [ MOUSELEAVE ]: this._mouseleaveHandler,
            [ MOUSEDOWN ]: this._mousedownHandler,
            [ KEYDOWN ]: this._keydownHandler,
            [ FOCUS ]: this._focusHandler,
            [ BLUR]: this._blurHandler
        });

        if (this.domEvents) {
            this.domEvents.destroy();
            delete this.domEvents;
        }

        if (this._mouseMoveTrackHandler) {
            unbindEvents(document, {
                [ MOUSEMOVE ]: this._mouseMoveTrackHandler
            });
        }

        this._focusState = null;
        this.element._kendoExportVisual = null;

        this._destroyView();

        this._destroySurface();

        this._clearRedrawTimeout();

        this._destroyStyleObserver();
    }

    _destroySurface() {
        const surface = this.surface;
        if (surface) {
            surface.unbind("mouseenter", this._surfaceMouseenterHandler);
            surface.unbind("mouseleave", this._surfaceMouseleaveHandler);
            surface.destroy();

            this.surface = null;
        }
    }

    _destroySelections() {
        const selections = this._selections;

        if (selections) {
            while (selections.length > 0) {
                selections.shift().destroy();
            }
        }
    }

    _destroyView() {
        const model = this._model;

        if (model) {
            model.destroy();
            this._model = null;
        }

        this._unsetActivePoint();
        this._clearFocusedElement();
        this._resetDrilldownPoint();
        this._destroySelections();

        if (this._tooltip) {
            this._tooltip.destroy();
        }

        if (this._highlight) {
            this._highlight.destroy();
        }

        if (this._zoomSelection) {
            this._zoomSelection.destroy();
            delete this._zoomSelection;
        }

        if (this._pannable) {
            this._pannable.destroy();
            delete this._pannable;
        }

        if (this._mousewheelZoom) {
            this._mousewheelZoom.destroy();
            delete this._mousewheelZoom;
        }
    }
}

function resolveAxisAliases(options) {
    const aliases = AXIS_NAMES;

    for (let idx = 0; idx < aliases.length; idx++) {
        const alias = aliases[idx] + "Axes";
        if (options[alias]) {
            options[aliases[idx] + "Axis"] = options[alias];
            delete options[alias];
        }
    }
}

function pointByCategoryName(points, name) {
    if (points) {
        for (let idx = 0; idx < points.length; idx++) {
            if (points[idx].category === name) {
                return [ points[idx] ];
            }
        }
    }
}

function applyAxisDefaults(options, themeOptions) {
    const themeAxisDefaults = ((themeOptions || {}).axisDefaults) || {};
    let axisName, axisDefaults, axes;

    function mapAxisOptions(axisOptions) {
        const axisColor = (axisOptions || {}).color || axisDefaults.color;
        const result = deepExtend({},
            themeAxisDefaults,
            themeAxisDefaults[axisName],
            axisDefaults,
            axisDefaults[axisName], {
                line: { color: axisColor },
                labels: { color: axisColor },
                title: { color: axisColor }
            },
            axisOptions
        );

        delete result[axisName];

        return result;
    }

    for (let idx = 0; idx < AXIS_NAMES.length; idx++) {
        axisName = AXIS_NAMES[idx] + "Axis";
        axisDefaults = options.axisDefaults || {};
        axes = [].concat(options[axisName]);

        axes = axes.map(mapAxisOptions);

        options[axisName] = axes.length > 1 ? axes : axes[0];
    }
}

function applySeriesDefaults(options, themeOptions) {
    const series = options.series;
    const seriesLength = series.length;
    const seriesDefaults = options.seriesDefaults;
    const commonDefaults = deepExtend({}, options.seriesDefaults);
    const themeSeriesDefaults = themeOptions ? deepExtend({}, themeOptions.seriesDefaults) : {};
    const commonThemeDefaults = deepExtend({}, themeSeriesDefaults);

    cleanupNestedSeriesDefaults(commonDefaults);
    cleanupNestedSeriesDefaults(commonThemeDefaults);

    for (let i = 0; i < seriesLength; i++) {
        const seriesType = series[i].type || options.seriesDefaults.type;

        const baseOptions = deepExtend(
            { data: [] },
            commonThemeDefaults,
            themeSeriesDefaults[seriesType],
            { tooltip: options.tooltip },
            commonDefaults,
            seriesDefaults[seriesType]
        );

        series[i]._defaults = baseOptions;
        series[i] = deepExtend({}, baseOptions, series[i]);
        series[i].data = series[i].data || [];
    }
}

function cleanupNestedSeriesDefaults(seriesDefaults) {
    delete seriesDefaults.bar;
    delete seriesDefaults.column;
    delete seriesDefaults.rangeColumn;
    delete seriesDefaults.line;
    delete seriesDefaults.verticalLine;
    delete seriesDefaults.pie;
    delete seriesDefaults.donut;
    delete seriesDefaults.area;
    delete seriesDefaults.verticalArea;
    delete seriesDefaults.scatter;
    delete seriesDefaults.scatterLine;
    delete seriesDefaults.bubble;
    delete seriesDefaults.candlestick;
    delete seriesDefaults.ohlc;
    delete seriesDefaults.boxPlot;
    delete seriesDefaults.bullet;
    delete seriesDefaults.verticalBullet;
    delete seriesDefaults.polarArea;
    delete seriesDefaults.polarLine;
    delete seriesDefaults.radarArea;
    delete seriesDefaults.radarLine;
    delete seriesDefaults.waterfall;
}


function axisRanges(axes) {
    const ranges = {};

    for (let i = 0; i < axes.length; i++) {
        const axis = axes[i];
        const axisName = axis.options.name;
        if (axisName) {
            ranges[axisName] = axis.range();
        }
    }

    return ranges;
}

const DATA_FIELDS = [ 'data', 'categories' ];

function clearMissingValues(originalOptions, options) {
    for (let field in options) {
        if (!inArray(field, DATA_FIELDS) && hasOwnProperty(options, field)) {
            const fieldValue = options[field];
            const originalValue = originalOptions[field];
            if (defined(originalValue)) {
                const nullValue = fieldValue === null;
                if ((nullValue || !defined(fieldValue))) {
                    delete originalOptions[field];
                    if (nullValue) {
                        delete options[field];
                    }
                } else if (originalValue && isObject(fieldValue)) {
                    if (isObject(originalValue)) {
                        clearMissingValues(originalValue, fieldValue);
                    }
                }
            }
        }
    }
}

function triggerPaneRender(panes) {
    for (let idx = 0; idx < panes.length; idx++) {
        panes[idx].notifyRender();
    }
}

setDefaultOptions(Chart, {
    renderAs: "",
    motion: {},
    chartArea: {},
    legend: {
        visible: true,
        labels: {},
        accessibility: {},
        focusHighlight: {
            border: {
                opacity: 1,
                color: BLACK,
                width: 2
            }
        }
    },
    categoryAxis: {
        highlight: {
            visible: false,
            border: null
        }
    },
    seriesDefaults: {
        type: COLUMN,
        data: [],
        highlight: {
            visible: true
        },
        labels: {},
        negativeValues: {
            visible: false
        },
        accessibility: {},
        focusHighlight: {
            border: {
                opacity: 1,
                width: 2
            },
            zIndex: 200
        }
    },
    series: [],
    seriesColors: null,
    tooltip: {
        visible: false
    },
    transitions: true,
    observeStyles: false,
    valueAxis: {
        narrowRange: false
    },
    plotArea: {},
    title: {},
    xAxis: {},
    yAxis: {},
    panes: [ {} ],
    pannable: false,
    zoomable: false
});

export default Chart;
