echarts BaseAxisPointer 源码

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

echarts BaseAxisPointer 代码

文件路径:/src/component/axisPointer/BaseAxisPointer.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 * as graphic from '../../util/graphic';
import * as axisPointerModelHelper from './modelHelper';
import * as eventTool from 'zrender/src/core/event';
import * as throttleUtil from '../../util/throttle';
import {makeInner} from '../../util/model';
import { AxisPointer } from './AxisPointer';
import { AxisBaseModel } from '../../coord/AxisBaseModel';
import ExtensionAPI from '../../core/ExtensionAPI';
import Displayable, { DisplayableProps } from 'zrender/src/graphic/Displayable';
import Element from 'zrender/src/Element';
import { VerticalAlign, HorizontalAlign, CommonAxisPointerOption } from '../../util/types';
import { PathProps } from 'zrender/src/graphic/Path';
import Model from '../../model/Model';
import { TextProps } from 'zrender/src/graphic/Text';

const inner = makeInner<{
    lastProp?: DisplayableProps
    labelEl?: graphic.Text
    pointerEl?: Displayable
}, Element>();
const clone = zrUtil.clone;
const bind = zrUtil.bind;

type Icon = ReturnType<typeof graphic.createIcon>;
interface Transform {
    x: number,
    y: number,
    rotation: number
}

type AxisValue = CommonAxisPointerOption['value'];

// Not use top level axisPointer model
type AxisPointerModel = Model<CommonAxisPointerOption>;

interface BaseAxisPointer {

    /**
     * Should be implemenented by sub-class if support `handle`.
     */
    getHandleTransform(value: AxisValue, axisModel: AxisBaseModel, axisPointerModel: AxisPointerModel): Transform

    /**
     * * Should be implemenented by sub-class if support `handle`.
     */
    updateHandleTransform(
        transform: Transform,
        delta: number[],
        axisModel: AxisBaseModel,
        axisPointerModel: AxisPointerModel
    ): Transform & {
        cursorPoint: number[]
        tooltipOption?: {
            verticalAlign?: VerticalAlign
            align?: HorizontalAlign
        }
    }

}

export interface AxisPointerElementOptions {
    graphicKey: string

    pointer: PathProps & {
        type: 'Line' | 'Rect' | 'Circle' | 'Sector'
    }

    label: TextProps
}
/**
 * Base axis pointer class in 2D.
 */
class BaseAxisPointer implements AxisPointer {

    private _group: graphic.Group;

    private _lastGraphicKey: string;

    private _handle: Icon;

    private _dragging = false;

    private _lastValue: AxisValue;

    private _lastStatus: CommonAxisPointerOption['status'];

    private _payloadInfo: ReturnType<BaseAxisPointer['updateHandleTransform']>;

    /**
     * If have transition animation
     */
    private _moveAnimation: boolean;

    private _axisModel: AxisBaseModel;
    private _axisPointerModel: AxisPointerModel;
    private _api: ExtensionAPI;

    /**
     * In px, arbitrary value. Do not set too small,
     * no animation is ok for most cases.
     */
    protected animationThreshold = 15;

    /**
     * @implement
     */
    render(axisModel: AxisBaseModel, axisPointerModel: AxisPointerModel, api: ExtensionAPI, forceRender?: boolean) {
        const value = axisPointerModel.get('value');
        const status = axisPointerModel.get('status');

        // Bind them to `this`, not in closure, otherwise they will not
        // be replaced when user calling setOption in not merge mode.
        this._axisModel = axisModel;
        this._axisPointerModel = axisPointerModel;
        this._api = api;

        // Optimize: `render` will be called repeatly during mouse move.
        // So it is power consuming if performing `render` each time,
        // especially on mobile device.
        if (!forceRender
            && this._lastValue === value
            && this._lastStatus === status
        ) {
            return;
        }
        this._lastValue = value;
        this._lastStatus = status;

        let group = this._group;
        const handle = this._handle;

        if (!status || status === 'hide') {
            // Do not clear here, for animation better.
            group && group.hide();
            handle && handle.hide();
            return;
        }
        group && group.show();
        handle && handle.show();

        // Otherwise status is 'show'
        const elOption = {} as AxisPointerElementOptions;
        this.makeElOption(elOption, value, axisModel, axisPointerModel, api);

        // Enable change axis pointer type.
        const graphicKey = elOption.graphicKey;
        if (graphicKey !== this._lastGraphicKey) {
            this.clear(api);
        }
        this._lastGraphicKey = graphicKey;

        const moveAnimation = this._moveAnimation =
            this.determineAnimation(axisModel, axisPointerModel);

        if (!group) {
            group = this._group = new graphic.Group();
            this.createPointerEl(group, elOption, axisModel, axisPointerModel);
            this.createLabelEl(group, elOption, axisModel, axisPointerModel);
            api.getZr().add(group);
        }
        else {
            const doUpdateProps = zrUtil.curry(updateProps, axisPointerModel, moveAnimation);
            this.updatePointerEl(group, elOption, doUpdateProps);
            this.updateLabelEl(group, elOption, doUpdateProps, axisPointerModel);
        }

        updateMandatoryProps(group, axisPointerModel, true);

        this._renderHandle(value);
    }

    /**
     * @implement
     */
    remove(api: ExtensionAPI) {
        this.clear(api);
    }

    /**
     * @implement
     */
    dispose(api: ExtensionAPI) {
        this.clear(api);
    }

    /**
     * @protected
     */
    determineAnimation(axisModel: AxisBaseModel, axisPointerModel: AxisPointerModel): boolean {
        const animation = axisPointerModel.get('animation');
        const axis = axisModel.axis;
        const isCategoryAxis = axis.type === 'category';
        const useSnap = axisPointerModel.get('snap');

        // Value axis without snap always do not snap.
        if (!useSnap && !isCategoryAxis) {
            return false;
        }

        if (animation === 'auto' || animation == null) {
            const animationThreshold = this.animationThreshold;
            if (isCategoryAxis && axis.getBandWidth() > animationThreshold) {
                return true;
            }

            // It is important to auto animation when snap used. Consider if there is
            // a dataZoom, animation will be disabled when too many points exist, while
            // it will be enabled for better visual effect when little points exist.
            if (useSnap) {
                const seriesDataCount = axisPointerModelHelper.getAxisInfo(axisModel).seriesDataCount;
                const axisExtent = axis.getExtent();
                // Approximate band width
                return Math.abs(axisExtent[0] - axisExtent[1]) / seriesDataCount > animationThreshold;
            }

            return false;
        }

        return animation === true;
    }

    /**
     * add {pointer, label, graphicKey} to elOption
     * @protected
     */
    makeElOption(
        elOption: AxisPointerElementOptions,
        value: AxisValue,
        axisModel: AxisBaseModel,
        axisPointerModel: AxisPointerModel,
        api: ExtensionAPI
    ) {
        // Shoule be implemenented by sub-class.
    }

    /**
     * @protected
     */
    createPointerEl(
        group: graphic.Group,
        elOption: AxisPointerElementOptions,
        axisModel: AxisBaseModel,
        axisPointerModel: AxisPointerModel
    ) {
        const pointerOption = elOption.pointer;
        if (pointerOption) {
            const pointerEl = inner(group).pointerEl = new graphic[pointerOption.type](
                clone(elOption.pointer)
            );
            group.add(pointerEl);
        }
    }

    /**
     * @protected
     */
    createLabelEl(
        group: graphic.Group,
        elOption: AxisPointerElementOptions,
        axisModel: AxisBaseModel,
        axisPointerModel: AxisPointerModel
    ) {
        if (elOption.label) {
            const labelEl = inner(group).labelEl = new graphic.Text(
                clone(elOption.label)
            );

            group.add(labelEl);
            updateLabelShowHide(labelEl, axisPointerModel);
        }
    }

    /**
     * @protected
     */
    updatePointerEl(
        group: graphic.Group,
        elOption: AxisPointerElementOptions,
        updateProps: (el: Element, props: PathProps) => void
    ) {
        const pointerEl = inner(group).pointerEl;
        if (pointerEl && elOption.pointer) {
            pointerEl.setStyle(elOption.pointer.style);
            updateProps(pointerEl, {shape: elOption.pointer.shape});
        }
    }

    /**
     * @protected
     */
    updateLabelEl(
        group: graphic.Group,
        elOption: AxisPointerElementOptions,
        updateProps: (el: Element, props: PathProps) => void,
        axisPointerModel: AxisPointerModel
    ) {
        const labelEl = inner(group).labelEl;
        if (labelEl) {
            labelEl.setStyle(elOption.label.style);
            updateProps(labelEl, {
                // Consider text length change in vertical axis, animation should
                // be used on shape, otherwise the effect will be weird.
                // TODOTODO
                // shape: elOption.label.shape,
                x: elOption.label.x,
                y: elOption.label.y
            });

            updateLabelShowHide(labelEl, axisPointerModel);
        }
    }

    /**
     * @private
     */
    _renderHandle(value: AxisValue) {
        if (this._dragging || !this.updateHandleTransform) {
            return;
        }

        const axisPointerModel = this._axisPointerModel;
        const zr = this._api.getZr();
        let handle = this._handle;
        const handleModel = axisPointerModel.getModel('handle');

        const status = axisPointerModel.get('status');
        if (!handleModel.get('show') || !status || status === 'hide') {
            handle && zr.remove(handle);
            this._handle = null;
            return;
        }

        let isInit;
        if (!this._handle) {
            isInit = true;
            handle = this._handle = graphic.createIcon(
                handleModel.get('icon'),
                {
                    cursor: 'move',
                    draggable: true,
                    onmousemove(e) {
                        // Fot mobile devicem, prevent screen slider on the button.
                        eventTool.stop(e.event);
                    },
                    onmousedown: bind(this._onHandleDragMove, this, 0, 0),
                    drift: bind(this._onHandleDragMove, this),
                    ondragend: bind(this._onHandleDragEnd, this)
                }
            );
            zr.add(handle);
        }

        updateMandatoryProps(handle, axisPointerModel, false);

        // update style
        (handle as graphic.Path).setStyle(handleModel.getItemStyle(null, [
            'color', 'borderColor', 'borderWidth', 'opacity',
            'shadowColor', 'shadowBlur', 'shadowOffsetX', 'shadowOffsetY'
        ]));

        // update position
        let handleSize = handleModel.get('size');
        if (!zrUtil.isArray(handleSize)) {
            handleSize = [handleSize, handleSize];
        }
        handle.scaleX = handleSize[0] / 2;
        handle.scaleY = handleSize[1] / 2;

        throttleUtil.createOrUpdate(
            this,
            '_doDispatchAxisPointer',
            handleModel.get('throttle') || 0,
            'fixRate'
        );

        this._moveHandleToValue(value, isInit);
    }

    private _moveHandleToValue(value: AxisValue, isInit?: boolean) {
        updateProps(
            this._axisPointerModel,
            !isInit && this._moveAnimation,
            this._handle,
            getHandleTransProps(this.getHandleTransform(
                value, this._axisModel, this._axisPointerModel
            ))
        );
    }

    private _onHandleDragMove(dx: number, dy: number) {
        const handle = this._handle;
        if (!handle) {
            return;
        }

        this._dragging = true;

        // Persistent for throttle.
        const trans = this.updateHandleTransform(
            getHandleTransProps(handle),
            [dx, dy],
            this._axisModel,
            this._axisPointerModel
        );
        this._payloadInfo = trans;

        handle.stopAnimation();
        (handle as graphic.Path).attr(getHandleTransProps(trans));
        inner(handle).lastProp = null;

        this._doDispatchAxisPointer();
    }

    /**
     * Throttled method.
     */
    _doDispatchAxisPointer() {
        const handle = this._handle;
        if (!handle) {
            return;
        }

        const payloadInfo = this._payloadInfo;
        const axisModel = this._axisModel;
        this._api.dispatchAction({
            type: 'updateAxisPointer',
            x: payloadInfo.cursorPoint[0],
            y: payloadInfo.cursorPoint[1],
            tooltipOption: payloadInfo.tooltipOption,
            axesInfo: [{
                axisDim: axisModel.axis.dim,
                axisIndex: axisModel.componentIndex
            }]
        });
    }

    private _onHandleDragEnd() {
        this._dragging = false;
        const handle = this._handle;
        if (!handle) {
            return;
        }

        const value = this._axisPointerModel.get('value');
        // Consider snap or categroy axis, handle may be not consistent with
        // axisPointer. So move handle to align the exact value position when
        // drag ended.
        this._moveHandleToValue(value);

        // For the effect: tooltip will be shown when finger holding on handle
        // button, and will be hidden after finger left handle button.
        this._api.dispatchAction({
            type: 'hideTip'
        });
    }

    /**
     * @private
     */
    clear(api: ExtensionAPI) {
        this._lastValue = null;
        this._lastStatus = null;

        const zr = api.getZr();
        const group = this._group;
        const handle = this._handle;
        if (zr && group) {
            this._lastGraphicKey = null;
            group && zr.remove(group);
            handle && zr.remove(handle);
            this._group = null;
            this._handle = null;
            this._payloadInfo = null;
        }

        throttleUtil.clear(this, '_doDispatchAxisPointer');
    }

    /**
     * @protected
     */
    doClear() {
        // Implemented by sub-class if necessary.
    }

    buildLabel(xy: number[], wh: number[], xDimIndex: 0 | 1) {
        xDimIndex = xDimIndex || 0;
        return {
            x: xy[xDimIndex],
            y: xy[1 - xDimIndex],
            width: wh[xDimIndex],
            height: wh[1 - xDimIndex]
        };
    }
}


function updateProps(
    animationModel: AxisPointerModel,
    moveAnimation: boolean,
    el: Element,
    props: DisplayableProps
) {
    // Animation optimize.
    if (!propsEqual(inner(el).lastProp, props)) {
        inner(el).lastProp = props;
        moveAnimation
            ? graphic.updateProps(el, props, animationModel as Model<
                // Ignore animation property
                Pick<CommonAxisPointerOption, 'animationDurationUpdate' | 'animationEasingUpdate'>
            >)
            : (el.stopAnimation(), el.attr(props));
    }
}

function propsEqual(lastProps: any, newProps: any) {
    if (zrUtil.isObject(lastProps) && zrUtil.isObject(newProps)) {
        let equals = true;
        zrUtil.each(newProps, function (item, key) {
            equals = equals && propsEqual(lastProps[key], item);
        });
        return !!equals;
    }
    else {
        return lastProps === newProps;
    }
}

function updateLabelShowHide(labelEl: Element, axisPointerModel: AxisPointerModel) {
    labelEl[axisPointerModel.get(['label', 'show']) ? 'show' : 'hide']();
}

function getHandleTransProps(trans: Transform): Transform {
    return {
        x: trans.x || 0,
        y: trans.y || 0,
        rotation: trans.rotation || 0
    };
}

function updateMandatoryProps(
    group: Element,
    axisPointerModel: AxisPointerModel,
    silent?: boolean
) {
    const z = axisPointerModel.get('z');
    const zlevel = axisPointerModel.get('zlevel');

    group && group.traverse(function (el: Displayable) {
        if (el.type !== 'group') {
            z != null && (el.z = z);
            zlevel != null && (el.zlevel = zlevel);
            el.silent = silent;
        }
    });
}

export default BaseAxisPointer;

相关信息

echarts 源码目录

相关文章

echarts AxisPointer 源码

echarts AxisPointerModel 源码

echarts AxisPointerView 源码

echarts CartesianAxisPointer 源码

echarts PolarAxisPointer 源码

echarts SingleAxisPointer 源码

echarts axisTrigger 源码

echarts findPointFromSeries 源码

echarts globalListener 源码

echarts install 源码

0  赞