echarts ContinuousView 源码

  • 2022-10-20
  • 浏览 (503)

echarts ContinuousView 代码

文件路径:/src/component/visualMap/ContinuousView.ts

/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements.  See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership.  The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License.  You may obtain a copy of the License at
*
*   http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied.  See the License for the
* specific language governing permissions and limitations
* under the License.
*/

import * as zrUtil from 'zrender/src/core/util';
import LinearGradient, { LinearGradientObject } from 'zrender/src/graphic/LinearGradient';
import * as eventTool from 'zrender/src/core/event';
import VisualMapView from './VisualMapView';
import * as graphic from '../../util/graphic';
import * as numberUtil from '../../util/number';
import sliderMove from '../helper/sliderMove';
import * as helper from './helper';
import * as modelUtil from '../../util/model';
import VisualMapModel from './VisualMapModel';
import ContinuousModel from './ContinuousModel';
import GlobalModel from '../../model/Global';
import ExtensionAPI from '../../core/ExtensionAPI';
import Element, { ElementEvent } from 'zrender/src/Element';
import { TextVerticalAlign, TextAlign } from 'zrender/src/core/types';
import { ColorString, Payload } from '../../util/types';
import { parsePercent } from 'zrender/src/contain/text';
import { setAsHighDownDispatcher } from '../../util/states';
import { createSymbol } from '../../util/symbol';
import ZRImage from 'zrender/src/graphic/Image';
import { ECData, getECData } from '../../util/innerStore';
import { createTextStyle } from '../../label/labelStyle';
import { findEventDispatcher } from '../../util/event';

const linearMap = numberUtil.linearMap;
const each = zrUtil.each;
const mathMin = Math.min;
const mathMax = Math.max;

// Arbitrary value
const HOVER_LINK_SIZE = 12;
const HOVER_LINK_OUT = 6;

type Orient = VisualMapModel['option']['orient'];

type ShapeStorage = {
    handleThumbs: graphic.Path[]
    handleLabelPoints: number[][]
    handleLabels: graphic.Text[]

    inRange: graphic.Polygon
    outOfRange: graphic.Polygon

    mainGroup: graphic.Group

    indicator: graphic.Path
    indicatorLabel: graphic.Text
    indicatorLabelPoint: number[]
};

type TargetDataIndices = ReturnType<ContinuousModel['findTargetDataIndices']>;

type BarVisual = {
    barColor: LinearGradient,
    barPoints: number[][]
    handlesColor: ColorString[]
};

type Direction = 'left' | 'right' | 'top' | 'bottom';
// Notice:
// Any "interval" should be by the order of [low, high].
// "handle0" (handleIndex === 0) maps to
// low data value: this._dataInterval[0] and has low coord.
// "handle1" (handleIndex === 1) maps to
// high data value: this._dataInterval[1] and has high coord.
// The logic of transform is implemented in this._createBarGroup.

class ContinuousView extends VisualMapView {
    static type = 'visualMap.continuous';
    type = ContinuousView.type;

    visualMapModel: ContinuousModel;

    private _shapes = {} as ShapeStorage;

    private _dataInterval: number[] = [];

    private _handleEnds: number[] = [];

    private _orient: Orient;

    private _useHandle: boolean;

    private _hoverLinkDataIndices: TargetDataIndices = [];

    private _dragging: boolean;

    private _hovering: boolean;

    private _firstShowIndicator: boolean;

    private _api: ExtensionAPI;


    doRender(
        visualMapModel: ContinuousModel,
        ecModel: GlobalModel,
        api: ExtensionAPI,
        payload: {type: string, from: string}
    ) {
        this._api = api;

        if (!payload || payload.type !== 'selectDataRange' || payload.from !== this.uid) {
            this._buildView();
        }
    }

    private _buildView() {
        this.group.removeAll();

        const visualMapModel = this.visualMapModel;
        const thisGroup = this.group;

        this._orient = visualMapModel.get('orient');
        this._useHandle = visualMapModel.get('calculable');

        this._resetInterval();

        this._renderBar(thisGroup);

        const dataRangeText = visualMapModel.get('text');
        this._renderEndsText(thisGroup, dataRangeText, 0);
        this._renderEndsText(thisGroup, dataRangeText, 1);

        // Do this for background size calculation.
        this._updateView(true);

        // After updating view, inner shapes is built completely,
        // and then background can be rendered.
        this.renderBackground(thisGroup);

        // Real update view
        this._updateView();

        this._enableHoverLinkToSeries();
        this._enableHoverLinkFromSeries();

        this.positionGroup(thisGroup);
    }

    private _renderEndsText(group: graphic.Group, dataRangeText: string[], endsIndex?: 0 | 1) {
        if (!dataRangeText) {
            return;
        }

        // Compatible with ec2, text[0] map to high value, text[1] map low value.
        let text = dataRangeText[1 - endsIndex];
        text = text != null ? text + '' : '';

        const visualMapModel = this.visualMapModel;
        const textGap = visualMapModel.get('textGap');
        const itemSize = visualMapModel.itemSize;

        const barGroup = this._shapes.mainGroup;
        const position = this._applyTransform(
            [
                itemSize[0] / 2,
                endsIndex === 0 ? -textGap : itemSize[1] + textGap
            ],
            barGroup
        ) as number[];
        const align = this._applyTransform(
            endsIndex === 0 ? 'bottom' : 'top',
            barGroup
        );
        const orient = this._orient;
        const textStyleModel = this.visualMapModel.textStyleModel;

        this.group.add(new graphic.Text({
            style: createTextStyle(textStyleModel, {
                x: position[0],
                y: position[1],
                verticalAlign: orient === 'horizontal' ? 'middle' : align as TextVerticalAlign,
                align: orient === 'horizontal' ? align as TextAlign : 'center',
                text
            })
        }));
    }

    private _renderBar(targetGroup: graphic.Group) {
        const visualMapModel = this.visualMapModel;
        const shapes = this._shapes;
        const itemSize = visualMapModel.itemSize;
        const orient = this._orient;
        const useHandle = this._useHandle;
        const itemAlign = helper.getItemAlign(visualMapModel, this.api, itemSize);
        const mainGroup = shapes.mainGroup = this._createBarGroup(itemAlign);

        const gradientBarGroup = new graphic.Group();
        mainGroup.add(gradientBarGroup);

        // Bar
        gradientBarGroup.add(shapes.outOfRange = createPolygon());
        gradientBarGroup.add(shapes.inRange = createPolygon(
            null,
            useHandle ? getCursor(this._orient) : null,
            zrUtil.bind(this._dragHandle, this, 'all', false),
            zrUtil.bind(this._dragHandle, this, 'all', true)
        ));

        // A border radius clip.
        gradientBarGroup.setClipPath(new graphic.Rect({
            shape: {
                x: 0,
                y: 0,
                width: itemSize[0],
                height: itemSize[1],
                r: 3
            }
        }));

        const textRect = visualMapModel.textStyleModel.getTextRect('国');
        const textSize = mathMax(textRect.width, textRect.height);

        // Handle
        if (useHandle) {
            shapes.handleThumbs = [];
            shapes.handleLabels = [];
            shapes.handleLabelPoints = [];

            this._createHandle(visualMapModel, mainGroup, 0, itemSize, textSize, orient);
            this._createHandle(visualMapModel, mainGroup, 1, itemSize, textSize, orient);
        }

        this._createIndicator(visualMapModel, mainGroup, itemSize, textSize, orient);

        targetGroup.add(mainGroup);
    }

    private _createHandle(
        visualMapModel: ContinuousModel,
        mainGroup: graphic.Group,
        handleIndex: 0 | 1,
        itemSize: number[],
        textSize: number,
        orient: Orient
    ) {
        const onDrift = zrUtil.bind(this._dragHandle, this, handleIndex, false);
        const onDragEnd = zrUtil.bind(this._dragHandle, this, handleIndex, true);
        const handleSize = parsePercent(visualMapModel.get('handleSize'), itemSize[0]);
        const handleThumb = createSymbol(
            visualMapModel.get('handleIcon'),
            -handleSize / 2, -handleSize / 2, handleSize, handleSize,
            null, true
        );
        const cursor = getCursor(this._orient);
        handleThumb.attr({
            cursor: cursor,
            draggable: true,
            drift: onDrift,
            ondragend: onDragEnd,
            onmousemove(e) {
                eventTool.stop(e.event);
            }
        });
        handleThumb.x = itemSize[0] / 2;

        handleThumb.useStyle(visualMapModel.getModel('handleStyle').getItemStyle());
        (handleThumb as graphic.Path).setStyle({
            strokeNoScale: true,
            strokeFirst: true
        });
        (handleThumb as graphic.Path).style.lineWidth *= 2;

        handleThumb.ensureState('emphasis').style = visualMapModel.getModel(['emphasis', 'handleStyle']).getItemStyle();
        setAsHighDownDispatcher(handleThumb, true);

        mainGroup.add(handleThumb);

        // Text is always horizontal layout but should not be effected by
        // transform (orient/inverse). So label is built separately but not
        // use zrender/graphic/helper/RectText, and is located based on view
        // group (according to handleLabelPoint) but not barGroup.
        const textStyleModel = this.visualMapModel.textStyleModel;
        const handleLabel = new graphic.Text({
            cursor: cursor,
            draggable: true,
            drift: onDrift,
            onmousemove(e) {
                // Fot mobile devicem, prevent screen slider on the button.
                eventTool.stop(e.event);
            },
            ondragend: onDragEnd,
            style: createTextStyle(textStyleModel, {
                x: 0,
                y: 0,
                text: ''
            })
        });
        handleLabel.ensureState('blur').style = {
            opacity: 0.1
        };
        handleLabel.stateTransition = { duration: 200 };

        this.group.add(handleLabel);

        const handleLabelPoint = [handleSize, 0];

        const shapes = this._shapes;
        shapes.handleThumbs[handleIndex] = handleThumb;
        shapes.handleLabelPoints[handleIndex] = handleLabelPoint;
        shapes.handleLabels[handleIndex] = handleLabel;
    }

    private _createIndicator(
        visualMapModel: ContinuousModel,
        mainGroup: graphic.Group,
        itemSize: number[],
        textSize: number,
        orient: Orient
    ) {
        const scale = parsePercent(visualMapModel.get('indicatorSize'), itemSize[0]);
        const indicator = createSymbol(
            visualMapModel.get('indicatorIcon'),
            -scale / 2, -scale / 2, scale, scale,
            null, true
        );
        indicator.attr({
            cursor: 'move',
            invisible: true,
            silent: true,
            x: itemSize[0] / 2
        });
        const indicatorStyle = visualMapModel.getModel('indicatorStyle').getItemStyle();
        if (indicator instanceof ZRImage) {
            const pathStyle = indicator.style;
            indicator.useStyle(zrUtil.extend({
                // TODO other properties like x, y ?
                image: pathStyle.image,
                x: pathStyle.x, y: pathStyle.y,
                width: pathStyle.width, height: pathStyle.height
            }, indicatorStyle));
        }
        else {
            indicator.useStyle(indicatorStyle);
        }

        mainGroup.add(indicator);

        const textStyleModel = this.visualMapModel.textStyleModel;
        const indicatorLabel = new graphic.Text({
            silent: true,
            invisible: true,
            style: createTextStyle(textStyleModel, {
                x: 0,
                y: 0,
                text: ''
            })
        });
        this.group.add(indicatorLabel);

        const indicatorLabelPoint = [
            (orient === 'horizontal' ? textSize / 2 : HOVER_LINK_OUT) + itemSize[0] / 2,
            0
        ];

        const shapes = this._shapes;
        shapes.indicator = indicator;
        shapes.indicatorLabel = indicatorLabel;
        shapes.indicatorLabelPoint = indicatorLabelPoint;

        this._firstShowIndicator = true;
    }

    private _dragHandle(
        handleIndex: 0 | 1 | 'all',
        isEnd?: boolean,
        // dx is event from ondragend if isEnd is true. It's not used
        dx?: number | ElementEvent,
        dy?: number
    ) {
        if (!this._useHandle) {
            return;
        }

        this._dragging = !isEnd;

        if (!isEnd) {
            // Transform dx, dy to bar coordination.
            const vertex = this._applyTransform([dx as number, dy], this._shapes.mainGroup, true) as number[];
            this._updateInterval(handleIndex, vertex[1]);

            this._hideIndicator();
            // Considering realtime, update view should be executed
            // before dispatch action.
            this._updateView();
        }

        // dragEnd do not dispatch action when realtime.
        if (isEnd === !this.visualMapModel.get('realtime')) { // jshint ignore:line
            this.api.dispatchAction({
                type: 'selectDataRange',
                from: this.uid,
                visualMapId: this.visualMapModel.id,
                selected: this._dataInterval.slice()
            });
        }

        if (isEnd) {
            !this._hovering && this._clearHoverLinkToSeries();
        }
        else if (useHoverLinkOnHandle(this.visualMapModel)) {
            this._doHoverLinkToSeries(this._handleEnds[handleIndex as 0 | 1], false);
        }
    }

    private _resetInterval() {
        const visualMapModel = this.visualMapModel;

        const dataInterval = this._dataInterval = visualMapModel.getSelected();
        const dataExtent = visualMapModel.getExtent();
        const sizeExtent = [0, visualMapModel.itemSize[1]];

        this._handleEnds = [
            linearMap(dataInterval[0], dataExtent, sizeExtent, true),
            linearMap(dataInterval[1], dataExtent, sizeExtent, true)
        ];
    }

    /**
     * @private
     * @param {(number|string)} handleIndex 0 or 1 or 'all'
     * @param {number} dx
     * @param {number} dy
     */
    private _updateInterval(handleIndex: 0 | 1 | 'all', delta: number) {
        delta = delta || 0;
        const visualMapModel = this.visualMapModel;
        const handleEnds = this._handleEnds;
        const sizeExtent = [0, visualMapModel.itemSize[1]];

        sliderMove(
            delta,
            handleEnds,
            sizeExtent,
            handleIndex,
            // cross is forbiden
            0
        );

        const dataExtent = visualMapModel.getExtent();
        // Update data interval.
        this._dataInterval = [
            linearMap(handleEnds[0], sizeExtent, dataExtent, true),
            linearMap(handleEnds[1], sizeExtent, dataExtent, true)
        ];
    }

    private _updateView(forSketch?: boolean) {
        const visualMapModel = this.visualMapModel;
        const dataExtent = visualMapModel.getExtent();
        const shapes = this._shapes;

        const outOfRangeHandleEnds = [0, visualMapModel.itemSize[1]];
        const inRangeHandleEnds = forSketch ? outOfRangeHandleEnds : this._handleEnds;

        const visualInRange = this._createBarVisual(
            this._dataInterval, dataExtent, inRangeHandleEnds, 'inRange'
        );
        const visualOutOfRange = this._createBarVisual(
            dataExtent, dataExtent, outOfRangeHandleEnds, 'outOfRange'
        );

        shapes.inRange
            .setStyle({
                fill: visualInRange.barColor
                // opacity: visualInRange.opacity
            })
            .setShape('points', visualInRange.barPoints);
        shapes.outOfRange
            .setStyle({
                fill: visualOutOfRange.barColor
                // opacity: visualOutOfRange.opacity
            })
            .setShape('points', visualOutOfRange.barPoints);

        this._updateHandle(inRangeHandleEnds, visualInRange);
    }

    private _createBarVisual(
        dataInterval: number[],
        dataExtent: number[],
        handleEnds: number[],
        forceState: ContinuousModel['stateList'][number]
    ): BarVisual {
        const opts = {
            forceState: forceState,
            convertOpacityToAlpha: true
        };
        const colorStops = this._makeColorGradient(dataInterval, opts);

        const symbolSizes = [
            this.getControllerVisual(dataInterval[0], 'symbolSize', opts) as number,
            this.getControllerVisual(dataInterval[1], 'symbolSize', opts) as number
        ];
        const barPoints = this._createBarPoints(handleEnds, symbolSizes);

        return {
            barColor: new LinearGradient(0, 0, 0, 1, colorStops),
            barPoints: barPoints,
            handlesColor: [
                colorStops[0].color,
                colorStops[colorStops.length - 1].color
            ]
        };
    }

    private _makeColorGradient(
        dataInterval: number[],
        opts: {
            forceState?: ContinuousModel['stateList'][number]
            convertOpacityToAlpha?: boolean
        }
    ) {
        // Considering colorHue, which is not linear, so we have to sample
        // to calculate gradient color stops, but not only caculate head
        // and tail.
        const sampleNumber = 100; // Arbitrary value.
        const colorStops: LinearGradientObject['colorStops'] = [];
        const step = (dataInterval[1] - dataInterval[0]) / sampleNumber;

        colorStops.push({
            color: this.getControllerVisual(dataInterval[0], 'color', opts) as ColorString,
            offset: 0
        });

        for (let i = 1; i < sampleNumber; i++) {
            const currValue = dataInterval[0] + step * i;
            if (currValue > dataInterval[1]) {
                break;
            }
            colorStops.push({
                color: this.getControllerVisual(currValue, 'color', opts) as ColorString,
                offset: i / sampleNumber
            });
        }

        colorStops.push({
            color: this.getControllerVisual(dataInterval[1], 'color', opts) as ColorString,
            offset: 1
        });

        return colorStops;
    }

    private _createBarPoints(handleEnds: number[], symbolSizes: number[]) {
        const itemSize = this.visualMapModel.itemSize;

        return [
            [itemSize[0] - symbolSizes[0], handleEnds[0]],
            [itemSize[0], handleEnds[0]],
            [itemSize[0], handleEnds[1]],
            [itemSize[0] - symbolSizes[1], handleEnds[1]]
        ];
    }

    private _createBarGroup(itemAlign: helper.ItemAlign) {
        const orient = this._orient;
        const inverse = this.visualMapModel.get('inverse');

        return new graphic.Group(
            (orient === 'horizontal' && !inverse)
            ? {scaleX: itemAlign === 'bottom' ? 1 : -1, rotation: Math.PI / 2}
            : (orient === 'horizontal' && inverse)
            ? {scaleX: itemAlign === 'bottom' ? -1 : 1, rotation: -Math.PI / 2}
            : (orient === 'vertical' && !inverse)
            ? {scaleX: itemAlign === 'left' ? 1 : -1, scaleY: -1}
            : {scaleX: itemAlign === 'left' ? 1 : -1}
        );
    }

    private _updateHandle(handleEnds: number[], visualInRange: BarVisual) {
        if (!this._useHandle) {
            return;
        }

        const shapes = this._shapes;
        const visualMapModel = this.visualMapModel;
        const handleThumbs = shapes.handleThumbs;
        const handleLabels = shapes.handleLabels;
        const itemSize = visualMapModel.itemSize;
        const dataExtent = visualMapModel.getExtent();

        each([0, 1], function (handleIndex) {
            const handleThumb = handleThumbs[handleIndex];
            handleThumb.setStyle('fill', visualInRange.handlesColor[handleIndex]);
            handleThumb.y = handleEnds[handleIndex];

            const val = linearMap(handleEnds[handleIndex], [0, itemSize[1]], dataExtent, true);
            const symbolSize = this.getControllerVisual(val, 'symbolSize') as number;

            handleThumb.scaleX = handleThumb.scaleY = symbolSize / itemSize[0];
            handleThumb.x = itemSize[0] - symbolSize / 2;

            // Update handle label position.
            const textPoint = graphic.applyTransform(
                shapes.handleLabelPoints[handleIndex],
                graphic.getTransform(handleThumb, this.group)
            );
            handleLabels[handleIndex].setStyle({
                x: textPoint[0],
                y: textPoint[1],
                text: visualMapModel.formatValueText(this._dataInterval[handleIndex]),
                verticalAlign: 'middle',
                align: this._orient === 'vertical' ? this._applyTransform(
                    'left',
                    shapes.mainGroup
                ) as TextAlign : 'center'
            });
        }, this);
    }

    private _showIndicator(
        cursorValue: number,
        textValue: number,
        rangeSymbol?: string,
        halfHoverLinkSize?: number
    ) {
        const visualMapModel = this.visualMapModel;
        const dataExtent = visualMapModel.getExtent();
        const itemSize = visualMapModel.itemSize;
        const sizeExtent = [0, itemSize[1]];

        const shapes = this._shapes;
        const indicator = shapes.indicator;
        if (!indicator) {
            return;
        }

        indicator.attr('invisible', false);

        const opts = {convertOpacityToAlpha: true};
        const color = this.getControllerVisual(cursorValue, 'color', opts) as ColorString;
        const symbolSize = this.getControllerVisual(cursorValue, 'symbolSize') as number;
        const y = linearMap(cursorValue, dataExtent, sizeExtent, true);
        const x = itemSize[0] - symbolSize / 2;

        const oldIndicatorPos = { x: indicator.x, y: indicator.y };
        // Update handle label position.
        indicator.y = y;
        indicator.x = x;
        const textPoint = graphic.applyTransform(
            shapes.indicatorLabelPoint,
            graphic.getTransform(indicator, this.group)
        );

        const indicatorLabel = shapes.indicatorLabel;
        indicatorLabel.attr('invisible', false);
        const align = this._applyTransform('left', shapes.mainGroup);
        const orient = this._orient;
        const isHorizontal = orient === 'horizontal';
        indicatorLabel.setStyle({
            text: (rangeSymbol ? rangeSymbol : '') + visualMapModel.formatValueText(textValue),
            verticalAlign: isHorizontal ? align as TextVerticalAlign : 'middle',
            align: isHorizontal ? 'center' : align as TextAlign
        });

        const indicatorNewProps = {
            x: x,
            y: y,
            style: {
                fill: color
            }
        };
        const labelNewProps = {
            style: {
                x: textPoint[0],
                y: textPoint[1]
            }
        };

        if (visualMapModel.ecModel.isAnimationEnabled() && !this._firstShowIndicator) {
            const animationCfg = {
                duration: 100,
                easing: 'cubicInOut',
                additive: true
            } as const;
            indicator.x = oldIndicatorPos.x;
            indicator.y = oldIndicatorPos.y;
            indicator.animateTo(indicatorNewProps, animationCfg);
            indicatorLabel.animateTo(labelNewProps, animationCfg);
        }
        else {
            indicator.attr(indicatorNewProps);
            indicatorLabel.attr(labelNewProps);
        }

        this._firstShowIndicator = false;

        const handleLabels = this._shapes.handleLabels;
        if (handleLabels) {
            for (let i = 0; i < handleLabels.length; i++) {
                // Fade out handle labels.
                // NOTE: Must use api enter/leave on emphasis/blur/select state. Or the global states manager will change it.
                this._api.enterBlur(handleLabels[i]);
            }
        }
    }

    private _enableHoverLinkToSeries() {
        const self = this;
        this._shapes.mainGroup

            .on('mousemove', function (e) {
                self._hovering = true;

                if (!self._dragging) {
                    const itemSize = self.visualMapModel.itemSize;
                    const pos = self._applyTransform(
                        [e.offsetX, e.offsetY], self._shapes.mainGroup, true, true
                    );
                    // For hover link show when hover handle, which might be
                    // below or upper than sizeExtent.
                    pos[1] = mathMin(mathMax(0, pos[1]), itemSize[1]);
                    self._doHoverLinkToSeries(
                        pos[1],
                        0 <= pos[0] && pos[0] <= itemSize[0]
                    );
                }
            })

            .on('mouseout', function () {
                // When mouse is out of handle, hoverLink still need
                // to be displayed when realtime is set as false.
                self._hovering = false;
                !self._dragging && self._clearHoverLinkToSeries();
            });
    }

    private _enableHoverLinkFromSeries() {
        const zr = this.api.getZr();

        if (this.visualMapModel.option.hoverLink) {
            zr.on('mouseover', this._hoverLinkFromSeriesMouseOver, this);
            zr.on('mouseout', this._hideIndicator, this);
        }
        else {
            this._clearHoverLinkFromSeries();
        }
    }

    private _doHoverLinkToSeries(cursorPos: number, hoverOnBar?: boolean) {
        const visualMapModel = this.visualMapModel;
        const itemSize = visualMapModel.itemSize;

        if (!visualMapModel.option.hoverLink) {
            return;
        }

        const sizeExtent = [0, itemSize[1]];
        const dataExtent = visualMapModel.getExtent();

        // For hover link show when hover handle, which might be below or upper than sizeExtent.
        cursorPos = mathMin(mathMax(sizeExtent[0], cursorPos), sizeExtent[1]);

        const halfHoverLinkSize = getHalfHoverLinkSize(visualMapModel, dataExtent, sizeExtent);
        const hoverRange = [cursorPos - halfHoverLinkSize, cursorPos + halfHoverLinkSize];
        const cursorValue = linearMap(cursorPos, sizeExtent, dataExtent, true);
        const valueRange = [
            linearMap(hoverRange[0], sizeExtent, dataExtent, true),
            linearMap(hoverRange[1], sizeExtent, dataExtent, true)
        ];
        // Consider data range is out of visualMap range, see test/visualMap-continuous.html,
        // where china and india has very large population.
        hoverRange[0] < sizeExtent[0] && (valueRange[0] = -Infinity);
        hoverRange[1] > sizeExtent[1] && (valueRange[1] = Infinity);

        // Do not show indicator when mouse is over handle,
        // otherwise labels overlap, especially when dragging.
        if (hoverOnBar) {
            if (valueRange[0] === -Infinity) {
                this._showIndicator(cursorValue, valueRange[1], '< ', halfHoverLinkSize);
            }
            else if (valueRange[1] === Infinity) {
                this._showIndicator(cursorValue, valueRange[0], '> ', halfHoverLinkSize);
            }
            else {
                this._showIndicator(cursorValue, cursorValue, '≈ ', halfHoverLinkSize);
            }
        }

        // When realtime is set as false, handles, which are in barGroup,
        // also trigger hoverLink, which help user to realize where they
        // focus on when dragging. (see test/heatmap-large.html)
        // When realtime is set as true, highlight will not show when hover
        // handle, because the label on handle, which displays a exact value
        // but not range, might mislead users.
        const oldBatch = this._hoverLinkDataIndices;
        let newBatch: TargetDataIndices = [];
        if (hoverOnBar || useHoverLinkOnHandle(visualMapModel)) {
            newBatch = this._hoverLinkDataIndices = visualMapModel.findTargetDataIndices(valueRange);
        }

        const resultBatches = modelUtil.compressBatches(oldBatch, newBatch);

        this._dispatchHighDown('downplay', helper.makeHighDownBatch(resultBatches[0], visualMapModel));
        this._dispatchHighDown('highlight', helper.makeHighDownBatch(resultBatches[1], visualMapModel));
    }

    private _hoverLinkFromSeriesMouseOver(e: ElementEvent) {
        let ecData: ECData;

        findEventDispatcher(e.target, target => {
            const currECData = getECData(target);
            if (currECData.dataIndex != null) {
                ecData = currECData;
                return true;
            }
        }, true);

        if (!ecData) {
            return;
        }

        const dataModel = this.ecModel.getSeriesByIndex(ecData.seriesIndex);

        const visualMapModel = this.visualMapModel;
        if (!visualMapModel.isTargetSeries(dataModel)) {
            return;
        }

        const data = dataModel.getData(ecData.dataType);
        const value = data.getStore().get(visualMapModel.getDataDimensionIndex(data), ecData.dataIndex) as number;

        if (!isNaN(value)) {
            this._showIndicator(value, value);
        }
    }

    private _hideIndicator() {
        const shapes = this._shapes;
        shapes.indicator && shapes.indicator.attr('invisible', true);
        shapes.indicatorLabel && shapes.indicatorLabel.attr('invisible', true);

        const handleLabels = this._shapes.handleLabels;
        if (handleLabels) {
            for (let i = 0; i < handleLabels.length; i++) {
                // Fade out handle labels.
                // NOTE: Must use api enter/leave on emphasis/blur/select state. Or the global states manager will change it.
                this._api.leaveBlur(handleLabels[i]);
            }
        }
    }

    private _clearHoverLinkToSeries() {
        this._hideIndicator();

        const indices = this._hoverLinkDataIndices;
        this._dispatchHighDown('downplay', helper.makeHighDownBatch(indices, this.visualMapModel));

        indices.length = 0;
    }

    private _clearHoverLinkFromSeries() {
        this._hideIndicator();

        const zr = this.api.getZr();
        zr.off('mouseover', this._hoverLinkFromSeriesMouseOver);
        zr.off('mouseout', this._hideIndicator);
    }
    private _applyTransform(vertex: number[], element: Element, inverse?: boolean, global?: boolean): number[]
    private _applyTransform(vertex: Direction, element: Element, inverse?: boolean, global?: boolean): Direction
    private _applyTransform(
        vertex: number[] | Direction,
        element: Element,
        inverse?: boolean,
        global?: boolean
    ) {
        const transform = graphic.getTransform(element, global ? null : this.group);

        return zrUtil.isArray(vertex)
            ? graphic.applyTransform(vertex, transform, inverse)
            : graphic.transformDirection(vertex, transform, inverse);
    }

 // TODO: TYPE more specified payload types.
    private _dispatchHighDown(type: 'highlight' | 'downplay', batch: Payload['batch']) {
        batch && batch.length && this.api.dispatchAction({
            type: type,
            batch: batch
        });
    }

    /**
     * @override
     */
    dispose() {
        this._clearHoverLinkFromSeries();
        this._clearHoverLinkToSeries();
    }

    /**
     * @override
     */
    remove() {
        this._clearHoverLinkFromSeries();
        this._clearHoverLinkToSeries();
    }

}

function createPolygon(
    points?: number[][],
    cursor?: string,
    onDrift?: (x: number, y: number) => void,
    onDragEnd?: () => void
) {
    return new graphic.Polygon({
        shape: {points: points},
        draggable: !!onDrift,
        cursor: cursor,
        drift: onDrift,
        onmousemove(e) {
            // Fot mobile devicem, prevent screen slider on the button.
            eventTool.stop(e.event);
        },
        ondragend: onDragEnd
    });
}

function getHalfHoverLinkSize(visualMapModel: ContinuousModel, dataExtent: number[], sizeExtent: number[]) {
    let halfHoverLinkSize = HOVER_LINK_SIZE / 2;
    const hoverLinkDataSize = visualMapModel.get('hoverLinkDataSize');
    if (hoverLinkDataSize) {
        halfHoverLinkSize = linearMap(hoverLinkDataSize, dataExtent, sizeExtent, true) / 2;
    }
    return halfHoverLinkSize;
}

function useHoverLinkOnHandle(visualMapModel: ContinuousModel) {
    const hoverLinkOnHandle = visualMapModel.get('hoverLinkOnHandle');
    return !!(hoverLinkOnHandle == null ? visualMapModel.get('realtime') : hoverLinkOnHandle);
}

function getCursor(orient: Orient) {
    return orient === 'vertical' ? 'ns-resize' : 'ew-resize';
}

export default ContinuousView;

相关信息

echarts 源码目录

相关文章

echarts ContinuousModel 源码

echarts PiecewiseModel 源码

echarts PiecewiseView 源码

echarts VisualMapModel 源码

echarts VisualMapView 源码

echarts helper 源码

echarts install 源码

echarts installCommon 源码

echarts installVisualMapContinuous 源码

echarts installVisualMapPiecewise 源码

0  赞