echarts LargeSymbolDraw 源码

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

echarts LargeSymbolDraw 代码

文件路径:/src/chart/helper/LargeSymbolDraw.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.
*/

/* global Float32Array */

// TODO Batch by color

import * as graphic from '../../util/graphic';
import {createSymbol} from '../../util/symbol';
import SeriesData from '../../data/SeriesData';
import { PathProps } from 'zrender/src/graphic/Path';
import PathProxy from 'zrender/src/core/PathProxy';
import SeriesModel from '../../model/Series';
import { StageHandlerProgressParams } from '../../util/types';
import { CoordinateSystemClipArea } from '../../coord/CoordinateSystem';
import { getECData } from '../../util/innerStore';
import Element from 'zrender/src/Element';

const BOOST_SIZE_THRESHOLD = 4;

class LargeSymbolPathShape {
    points: ArrayLike<number>;
    size: number[];
}

type LargeSymbolPathProps = PathProps & {
    shape?: Partial<LargeSymbolPathShape>
    startIndex?: number
    endIndex?: number
};

type ECSymbol = ReturnType<typeof createSymbol>;

class LargeSymbolPath extends graphic.Path<LargeSymbolPathProps> {

    shape: LargeSymbolPathShape;

    symbolProxy: ECSymbol;

    softClipShape: CoordinateSystemClipArea;

    startIndex: number;
    endIndex: number;

    private _ctx: CanvasRenderingContext2D;
    private _off: number = 0;

    hoverDataIdx: number = -1;

    notClear: boolean;

    constructor(opts?: LargeSymbolPathProps) {
        super(opts);
    }

    getDefaultShape() {
        return new LargeSymbolPathShape();
    }

    setColor: ECSymbol['setColor'];

    reset() {
        this.notClear = false;
        this._off = 0;
    }

    buildPath(path: PathProxy | CanvasRenderingContext2D, shape: LargeSymbolPathShape) {
        const points = shape.points;
        const size = shape.size;

        const symbolProxy = this.symbolProxy;
        const symbolProxyShape = symbolProxy.shape;
        const ctx = (path as PathProxy).getContext
            ? (path as PathProxy).getContext()
            : path as CanvasRenderingContext2D;
        const canBoost = ctx && size[0] < BOOST_SIZE_THRESHOLD;
        const softClipShape = this.softClipShape;
        let i;

        // Do draw in afterBrush.
        if (canBoost) {
            this._ctx = ctx;
            return;
        }

        this._ctx = null;

        for (i = this._off; i < points.length;) {
            const x = points[i++];
            const y = points[i++];

            if (isNaN(x) || isNaN(y)) {
                continue;
            }
            if (softClipShape && !softClipShape.contain(x, y)) {
                continue;
            }

            symbolProxyShape.x = x - size[0] / 2;
            symbolProxyShape.y = y - size[1] / 2;
            symbolProxyShape.width = size[0];
            symbolProxyShape.height = size[1];

            symbolProxy.buildPath(path, symbolProxyShape, true);
        }
        if (this.incremental) {
            this._off = i;
            this.notClear = true;
        }
    }

    afterBrush() {
        const shape = this.shape;
        const points = shape.points;
        const size = shape.size;
        const ctx = this._ctx;
        const softClipShape = this.softClipShape;
        let i;

        if (!ctx) {
            return;
        }

        // PENDING If style or other canvas status changed?
        for (i = this._off; i < points.length;) {
            const x = points[i++];
            const y = points[i++];
            if (isNaN(x) || isNaN(y)) {
                continue;
            }
            if (softClipShape && !softClipShape.contain(x, y)) {
                continue;
            }
            // fillRect is faster than building a rect path and draw.
            // And it support light globalCompositeOperation.
            ctx.fillRect(
                x - size[0] / 2, y - size[1] / 2,
                size[0], size[1]
            );
        }
        if (this.incremental) {
            this._off = i;
            this.notClear = true;
        }
    }

    findDataIndex(x: number, y: number) {
        // TODO ???
        // Consider transform

        const shape = this.shape;
        const points = shape.points;
        const size = shape.size;

        const w = Math.max(size[0], 4);
        const h = Math.max(size[1], 4);

        // Not consider transform
        // Treat each element as a rect
        // top down traverse
        for (let idx = points.length / 2 - 1; idx >= 0; idx--) {
            const i = idx * 2;
            const x0 = points[i] - w / 2;
            const y0 = points[i + 1] - h / 2;
            if (x >= x0 && y >= y0 && x <= x0 + w && y <= y0 + h) {
                return idx;
            }
        }

        return -1;
    }

    contain(x: number, y: number): boolean {
        const localPos = this.transformCoordToLocal(x, y);
        const rect = this.getBoundingRect();
        x = localPos[0];
        y = localPos[1];

        if (rect.contain(x, y)) {
            // Cache found data index.
            const dataIdx = this.hoverDataIdx = this.findDataIndex(x, y);
            return dataIdx >= 0;
        }
        this.hoverDataIdx = -1;
        return false;
    }

    getBoundingRect() {
        // Ignore stroke for large symbol draw.
        let rect = this._rect;
        if (!rect) {
            const shape = this.shape;
            const points = shape.points;
            const size = shape.size;
            const w = size[0];
            const h = size[1];
            let minX = Infinity;
            let minY = Infinity;
            let maxX = -Infinity;
            let maxY = -Infinity;
            for (let i = 0; i < points.length;) {
                const x = points[i++];
                const y = points[i++];
                minX = Math.min(x, minX);
                maxX = Math.max(x, maxX);
                minY = Math.min(y, minY);
                maxY = Math.max(y, maxY);
            }

            rect = this._rect = new graphic.BoundingRect(
                minX - w / 2,
                minY - h / 2,
                maxX - minX + w,
                maxY - minY + h
            );
        }
        return rect;
    }
}

interface UpdateOpt {
    clipShape?: CoordinateSystemClipArea
}

class LargeSymbolDraw {

    group = new graphic.Group();

    // New add element in this frame of progressive render.
    private _newAdded: LargeSymbolPath[];

    /**
     * Update symbols draw by new data
     */
    updateData(data: SeriesData, opt?: UpdateOpt) {
        this._clear();

        const symbolEl = this._create();
        symbolEl.setShape({
            points: data.getLayout('points')
        });
        this._setCommon(symbolEl, data, opt);
    }

    updateLayout(data: SeriesData) {
        let points = data.getLayout('points');
        this.group.eachChild(function (child: LargeSymbolPath) {
            if (child.startIndex != null) {
                const len = (child.endIndex - child.startIndex) * 2;
                const byteOffset = child.startIndex * 4 * 2;
                points = new Float32Array(points.buffer, byteOffset, len);
            }
            child.setShape('points', points);
            // Reset draw cursor.
            child.reset();
        });
    }

    incrementalPrepareUpdate(data: SeriesData) {
        this._clear();
    }

    incrementalUpdate(taskParams: StageHandlerProgressParams, data: SeriesData, opt: UpdateOpt) {
        const lastAdded = this._newAdded[0];
        const points = data.getLayout('points');
        const oldPoints = lastAdded && lastAdded.shape.points;
        // Merging the exists. Each element has 1e4 points.
        // Consider the performance balance between too much elements and too much points in one shape(may affect hover optimization)
        if (oldPoints && oldPoints.length < 2e4) {
            const oldLen = oldPoints.length;
            const newPoints = new Float32Array(oldLen + points.length);
            // Concat two array
            newPoints.set(oldPoints);
            newPoints.set(points, oldLen);
            // Update endIndex
            lastAdded.endIndex = taskParams.end;
            lastAdded.setShape({ points: newPoints });
        }
        else {
            // Clear
            this._newAdded = [];

            const symbolEl = this._create();
            symbolEl.startIndex = taskParams.start;
            symbolEl.endIndex = taskParams.end;
            symbolEl.incremental = true;
            symbolEl.setShape({
                points
            });
            this._setCommon(symbolEl, data, opt);
        }
    }

    eachRendered(cb: (el: Element) => boolean | void) {
        this._newAdded[0] && cb(this._newAdded[0]);
    }

    private _create() {
        const symbolEl = new LargeSymbolPath({
            cursor: 'default'
        });
        symbolEl.ignoreCoarsePointer = true;
        this.group.add(symbolEl);
        this._newAdded.push(symbolEl);
        return symbolEl;
    }

    private _setCommon(
        symbolEl: LargeSymbolPath,
        data: SeriesData,
        opt: UpdateOpt
    ) {
        const hostModel = data.hostModel;

        opt = opt || {};

        const size = data.getVisual('symbolSize');
        symbolEl.setShape('size', (size instanceof Array) ? size : [size, size]);

        symbolEl.softClipShape = opt.clipShape || null;
        // Create symbolProxy to build path for each data
        symbolEl.symbolProxy = createSymbol(
            data.getVisual('symbol'), 0, 0, 0, 0
        );
        // Use symbolProxy setColor method
        symbolEl.setColor = symbolEl.symbolProxy.setColor;

        const extrudeShadow = symbolEl.shape.size[0] < BOOST_SIZE_THRESHOLD;
        symbolEl.useStyle(
            // Draw shadow when doing fillRect is extremely slow.
            hostModel.getModel('itemStyle').getItemStyle(
                extrudeShadow ? ['color', 'shadowBlur', 'shadowColor'] : ['color']
            )
        );

        const globalStyle = data.getVisual('style');
        const visualColor = globalStyle && globalStyle.fill;
        if (visualColor) {
            symbolEl.setColor(visualColor);
        }

        const ecData = getECData(symbolEl);
        // Enable tooltip
        // PENDING May have performance issue when path is extremely large
        ecData.seriesIndex = (hostModel as SeriesModel).seriesIndex;
        symbolEl.on('mousemove', function (e) {
            ecData.dataIndex = null;
            const dataIndex = symbolEl.hoverDataIdx;
            if (dataIndex >= 0) {
                // Provide dataIndex for tooltip
                ecData.dataIndex = dataIndex + (symbolEl.startIndex || 0);
            }
        });
    }

    remove() {
        this._clear();
    }

    private _clear() {
        this._newAdded = [];
        this.group.removeAll();
    }
}


export default LargeSymbolDraw;

相关信息

echarts 源码目录

相关文章

echarts EffectLine 源码

echarts EffectPolyline 源码

echarts EffectSymbol 源码

echarts LargeLineDraw 源码

echarts Line 源码

echarts LineDraw 源码

echarts LinePath 源码

echarts Polyline 源码

echarts Symbol 源码

echarts SymbolDraw 源码

0  赞