echarts BrushTargetManager 源码

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

echarts BrushTargetManager 代码

文件路径:/src/component/helper/BrushTargetManager.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 { each, indexOf, curry, assert, map, createHashMap } from 'zrender/src/core/util';
import * as graphic from '../../util/graphic';
import * as brushHelper from './brushHelper';
import {
    BrushPanelConfig, BrushControllerEvents, BrushType,
    BrushAreaRange, BrushDimensionMinMax
} from './BrushController';
import ExtensionAPI from '../../core/ExtensionAPI';
import GridModel from '../../coord/cartesian/GridModel';
import GeoModel from '../../coord/geo/GeoModel';
import { CoordinateSystemMaster } from '../../coord/CoordinateSystem';
import Cartesian2D from '../../coord/cartesian/Cartesian2D';
import Geo from '../../coord/geo/Geo';
import GlobalModel from '../../model/Global';
import { BrushAreaParam, BrushAreaParamInternal } from '../brush/BrushModel';
import SeriesModel from '../../model/Series';
import { Dictionary } from '../../util/types';
import {
    ModelFinderObject, ModelFinder,
    parseFinder as modelUtilParseFinder,
    ParsedModelFinderKnown
} from '../../util/model';

type COORD_CONVERTS_INDEX = 0 | 1;

// FIXME
// how to genarialize to more coordinate systems.
const INCLUDE_FINDER_MAIN_TYPES = [
    'grid', 'xAxis', 'yAxis', 'geo', 'graph',
    'polar', 'radiusAxis', 'angleAxis', 'bmap'
];

type BrushableCoordinateSystem = Cartesian2D | Geo;
type BrushTargetBuilderKey = 'grid' | 'geo';

/**
 * There can be multiple axes in a single targetInfo. Consider the case
 * of `grid` component, a targetInfo represents a grid which contains one or more
 * cartesian and one or more axes. And consider the case of parallel system,
 * which has multiple axes in a coordinate system.
 */
interface BrushTargetInfo {
    panelId: string;
    coordSysModel: CoordinateSystemMaster['model'];
    // Use the first one as the representitive coordSys.
    // A representitive cartesian in grid (first cartesian by default).
    coordSys: BrushableCoordinateSystem;
    // All cartesians.
    coordSyses: BrushableCoordinateSystem[];
    getPanelRect: GetPanelRect,
}
export interface BrushTargetInfoCartesian2D extends BrushTargetInfo {
    gridModel: GridModel;
    coordSys: Cartesian2D;
    coordSyses: Cartesian2D[];
    xAxisDeclared: boolean;
    yAxisDeclared: boolean;
}
export interface BrushTargetInfoGeo extends BrushTargetInfo {
    geoModel: GeoModel,
    coordSysModel: GeoModel,
    coordSys: Geo,
    coordSyses: Geo[],
}
type GetPanelRect = () => graphic.BoundingRect;


class BrushTargetManager {

    private _targetInfoList: BrushTargetInfo[] = [];

    /**
     * @param finder contains Index/Id/Name of xAxis/yAxis/geo/grid
     *        Each can be {number|Array.<number>}. like: {xAxisIndex: [3, 4]}
     * @param opt.include include coordinate system types.
     */
    constructor(
        finder: ModelFinderObject,
        ecModel: GlobalModel,
        opt?: {include?: BrushTargetBuilderKey[]}
    ) {
        const foundCpts = parseFinder(ecModel, finder);

        each(targetInfoBuilders, (builder, type) => {
            if (!opt || !opt.include || indexOf(opt.include, type) >= 0) {
                builder(foundCpts, this._targetInfoList);
            }
        });
    }

    setOutputRanges(
        areas: BrushControllerEvents['brush']['areas'],
        ecModel: GlobalModel
    ): BrushAreaParam[] {
        this.matchOutputRanges(areas, ecModel, function (
            area: BrushAreaParam,
            coordRange: ReturnType<ConvertCoord>['values'],
            coordSys: BrushableCoordinateSystem
        ) {
            (area.coordRanges || (area.coordRanges = [])).push(coordRange);
            // area.coordRange is the first of area.coordRanges
            if (!area.coordRange) {
                area.coordRange = coordRange;
                // In 'category' axis, coord to pixel is not reversible, so we can not
                // rebuild range by coordRange accrately, which may bring trouble when
                // brushing only one item. So we use __rangeOffset to rebuilding range
                // by coordRange. And this it only used in brush component so it is no
                // need to be adapted to coordRanges.
                const result = coordConvert[area.brushType](0, coordSys, coordRange);
                area.__rangeOffset = {
                    offset: diffProcessor[area.brushType](result.values, area.range, [1, 1]),
                    xyMinMax: result.xyMinMax
                };
            }
        });
        return areas;
    }

    matchOutputRanges<T extends (
        Parameters<BrushTargetManager['findTargetInfo']>[0] & {
            brushType: BrushType;
            range: BrushAreaRange;
        }
    )>(
        areas: T[],
        ecModel: GlobalModel,
        cb: (
            area: T,
            coordRange: ReturnType<ConvertCoord>['values'],
            coordSys: BrushableCoordinateSystem,
            ecModel: GlobalModel
        ) => void
    ) {
        each(areas, function (area) {
            const targetInfo = this.findTargetInfo(area, ecModel);

            if (targetInfo && targetInfo !== true) {
                each(
                    targetInfo.coordSyses,
                    function (coordSys) {
                        const result = coordConvert[area.brushType](1, coordSys, area.range, true);
                        cb(area, result.values, coordSys, ecModel);
                    }
                );
            }
        }, this);
    }

    /**
     * the `areas` is `BrushModel.areas`.
     * Called in layout stage.
     * convert `area.coordRange` to global range and set panelId to `area.range`.
     */
    setInputRanges(
        areas: BrushAreaParamInternal[],
        ecModel: GlobalModel
    ): void {
        each(areas, function (area) {
            const targetInfo = this.findTargetInfo(area, ecModel);

            if (__DEV__) {
                assert(
                    !targetInfo || targetInfo === true || area.coordRange,
                    'coordRange must be specified when coord index specified.'
                );
                assert(
                    !targetInfo || targetInfo !== true || area.range,
                    'range must be specified in global brush.'
                );
            }

            area.range = area.range || [];

            // convert coordRange to global range and set panelId.
            if (targetInfo && targetInfo !== true) {
                area.panelId = targetInfo.panelId;
                // (1) area.range shoule always be calculate from coordRange but does
                // not keep its original value, for the sake of the dataZoom scenario,
                // where area.coordRange remains unchanged but area.range may be changed.
                // (2) Only support converting one coordRange to pixel range in brush
                // component. So do not consider `coordRanges`.
                // (3) About __rangeOffset, see comment above.
                const result = coordConvert[area.brushType](0, targetInfo.coordSys, area.coordRange);
                const rangeOffset = area.__rangeOffset;
                area.range = rangeOffset
                    ? diffProcessor[area.brushType](
                        result.values,
                        rangeOffset.offset,
                        getScales(result.xyMinMax, rangeOffset.xyMinMax)
                    )
                    : result.values;
            }
        }, this);
    }

    makePanelOpts(
        api: ExtensionAPI,
        getDefaultBrushType?: (targetInfo: BrushTargetInfo) => BrushType
    ): BrushPanelConfig[] {
        return map(this._targetInfoList, function (targetInfo) {
            const rect = targetInfo.getPanelRect();
            return {
                panelId: targetInfo.panelId,
                defaultBrushType: getDefaultBrushType ? getDefaultBrushType(targetInfo) : null,
                clipPath: brushHelper.makeRectPanelClipPath(rect),
                isTargetByCursor: brushHelper.makeRectIsTargetByCursor(
                    rect, api, targetInfo.coordSysModel
                ),
                getLinearBrushOtherExtent: brushHelper.makeLinearBrushOtherExtent(rect)
            };
        });
    }

    controlSeries(area: BrushAreaParamInternal, seriesModel: SeriesModel, ecModel: GlobalModel): boolean {
        // Check whether area is bound in coord, and series do not belong to that coord.
        // If do not do this check, some brush (like lineX) will controll all axes.
        const targetInfo = this.findTargetInfo(area, ecModel);
        return targetInfo === true || (
            targetInfo && indexOf(
                targetInfo.coordSyses, seriesModel.coordinateSystem as BrushableCoordinateSystem
            ) >= 0
        );
    }

    /**
     * If return Object, a coord found.
     * If reutrn true, global found.
     * Otherwise nothing found.
     */
    findTargetInfo(
        area: ModelFinderObject & {
            panelId?: string
        },
        ecModel: GlobalModel
    ): BrushTargetInfo | true {
        const targetInfoList = this._targetInfoList;
        const foundCpts = parseFinder(ecModel, area);

        for (let i = 0; i < targetInfoList.length; i++) {
            const targetInfo = targetInfoList[i];
            const areaPanelId = area.panelId;
            if (areaPanelId) {
                if (targetInfo.panelId === areaPanelId) {
                    return targetInfo;
                }
            }
            else {
                for (let j = 0; j < targetInfoMatchers.length; j++) {
                    if (targetInfoMatchers[j](foundCpts, targetInfo)) {
                        return targetInfo;
                    }
                }
            }
        }

        return true;
    }

}

function formatMinMax(minMax: BrushDimensionMinMax): BrushDimensionMinMax {
    minMax[0] > minMax[1] && minMax.reverse();
    return minMax;
}

function parseFinder(
    ecModel: GlobalModel, finder: ModelFinder
): ParsedModelFinderKnown {
    return modelUtilParseFinder(
        ecModel, finder, {includeMainTypes: INCLUDE_FINDER_MAIN_TYPES}
    );
}

type TargetInfoBuilder = (
    foundCpts: ParsedModelFinderKnown, targetInfoList: BrushTargetInfo[]
) => void;
const targetInfoBuilders: Record<BrushTargetBuilderKey, TargetInfoBuilder> = {

    grid: function (foundCpts, targetInfoList) {
        const xAxisModels = foundCpts.xAxisModels;
        const yAxisModels = foundCpts.yAxisModels;
        const gridModels = foundCpts.gridModels;
        // Remove duplicated.
        const gridModelMap = createHashMap<GridModel>();
        const xAxesHas = {} as Dictionary<boolean>;
        const yAxesHas = {} as Dictionary<boolean>;

        if (!xAxisModels && !yAxisModels && !gridModels) {
            return;
        }

        each(xAxisModels, function (axisModel) {
            const gridModel = axisModel.axis.grid.model;
            gridModelMap.set(gridModel.id, gridModel);
            xAxesHas[gridModel.id] = true;
        });
        each(yAxisModels, function (axisModel) {
            const gridModel = axisModel.axis.grid.model;
            gridModelMap.set(gridModel.id, gridModel);
            yAxesHas[gridModel.id] = true;
        });
        each(gridModels, function (gridModel) {
            gridModelMap.set(gridModel.id, gridModel);
            xAxesHas[gridModel.id] = true;
            yAxesHas[gridModel.id] = true;
        });

        gridModelMap.each(function (gridModel) {
            const grid = gridModel.coordinateSystem;
            const cartesians = [] as Cartesian2D[];

            each(grid.getCartesians(), function (cartesian, index) {
                if (indexOf(xAxisModels, cartesian.getAxis('x').model) >= 0
                    || indexOf(yAxisModels, cartesian.getAxis('y').model) >= 0
                ) {
                    cartesians.push(cartesian);
                }
            });
            targetInfoList.push({
                panelId: 'grid--' + gridModel.id,
                gridModel: gridModel,
                coordSysModel: gridModel,
                // Use the first one as the representitive coordSys.
                coordSys: cartesians[0],
                coordSyses: cartesians,
                getPanelRect: panelRectBuilders.grid,
                xAxisDeclared: xAxesHas[gridModel.id],
                yAxisDeclared: yAxesHas[gridModel.id]
            } as BrushTargetInfoCartesian2D);
        });
    },

    geo: function (foundCpts, targetInfoList) {
        each(foundCpts.geoModels, function (geoModel: GeoModel) {
            const coordSys = geoModel.coordinateSystem;
            targetInfoList.push({
                panelId: 'geo--' + geoModel.id,
                geoModel: geoModel,
                coordSysModel: geoModel,
                coordSys: coordSys,
                coordSyses: [coordSys],
                getPanelRect: panelRectBuilders.geo
            } as BrushTargetInfoGeo);
        });
    }
};

type TargetInfoMatcher = (
    foundCpts: ParsedModelFinderKnown, targetInfo: BrushTargetInfo
) => boolean;
const targetInfoMatchers: TargetInfoMatcher[] = [

    // grid
    function (foundCpts, targetInfo) {
        const xAxisModel = foundCpts.xAxisModel;
        const yAxisModel = foundCpts.yAxisModel;
        let gridModel = foundCpts.gridModel;

        !gridModel && xAxisModel && (gridModel = xAxisModel.axis.grid.model);
        !gridModel && yAxisModel && (gridModel = yAxisModel.axis.grid.model);

        return gridModel && gridModel === (targetInfo as BrushTargetInfoCartesian2D).gridModel;
    },

    // geo
    function (foundCpts, targetInfo) {
        const geoModel = foundCpts.geoModel;
        return geoModel && geoModel === (targetInfo as BrushTargetInfoGeo).geoModel;
    }
];

type PanelRectBuilder = (this: BrushTargetInfo) => graphic.BoundingRect;
const panelRectBuilders: Record<BrushTargetBuilderKey, PanelRectBuilder> = {

    grid: function (this: BrushTargetInfoCartesian2D) {
        // grid is not Transformable.
        return this.coordSys.master.getRect().clone();
    },

    geo: function (this: BrushTargetInfoGeo) {
        const coordSys = this.coordSys;
        const rect = coordSys.getBoundingRect().clone();
        // geo roam and zoom transform
        rect.applyTransform(graphic.getTransform(coordSys));
        return rect;
    }
};

type ConvertCoord = (
    to: COORD_CONVERTS_INDEX,
    coordSys: BrushableCoordinateSystem,
    rangeOrCoordRange: BrushAreaRange,
    clamp?: boolean
) => {
    values: BrushAreaRange,
    xyMinMax: BrushDimensionMinMax[]
};
const coordConvert: Record<BrushType, ConvertCoord> = {

    lineX: curry(axisConvert, 0),

    lineY: curry(axisConvert, 1),

    rect: function (to, coordSys, rangeOrCoordRange: BrushDimensionMinMax[], clamp): {
        values: BrushDimensionMinMax[],
        xyMinMax: BrushDimensionMinMax[]
    } {
        const xminymin = to
            ? coordSys.pointToData([rangeOrCoordRange[0][0], rangeOrCoordRange[1][0]], clamp)
            : coordSys.dataToPoint([rangeOrCoordRange[0][0], rangeOrCoordRange[1][0]], clamp);
        const xmaxymax = to
            ? coordSys.pointToData([rangeOrCoordRange[0][1], rangeOrCoordRange[1][1]], clamp)
            : coordSys.dataToPoint([rangeOrCoordRange[0][1], rangeOrCoordRange[1][1]], clamp);
        const values = [
            formatMinMax([xminymin[0], xmaxymax[0]]),
            formatMinMax([xminymin[1], xmaxymax[1]])
        ];
        return {values: values, xyMinMax: values};
    },

    polygon: function (to, coordSys, rangeOrCoordRange: BrushDimensionMinMax[], clamp): {
        values: BrushDimensionMinMax[],
        xyMinMax: BrushDimensionMinMax[]
    } {
        const xyMinMax = [[Infinity, -Infinity], [Infinity, -Infinity]];
        const values = map(rangeOrCoordRange, function (item) {
            const p = to ? coordSys.pointToData(item, clamp) : coordSys.dataToPoint(item, clamp);
            xyMinMax[0][0] = Math.min(xyMinMax[0][0], p[0]);
            xyMinMax[1][0] = Math.min(xyMinMax[1][0], p[1]);
            xyMinMax[0][1] = Math.max(xyMinMax[0][1], p[0]);
            xyMinMax[1][1] = Math.max(xyMinMax[1][1], p[1]);
            return p;
        });
        return {values: values, xyMinMax: xyMinMax};
    }
};

function axisConvert(
    axisNameIndex: 0 | 1,
    to: COORD_CONVERTS_INDEX,
    coordSys: Cartesian2D,
    rangeOrCoordRange: BrushDimensionMinMax
): {
    values: BrushDimensionMinMax,
    xyMinMax: BrushDimensionMinMax[]
} {
    if (__DEV__) {
        assert(
            coordSys.type === 'cartesian2d',
            'lineX/lineY brush is available only in cartesian2d.'
        );
    }

    const axis = coordSys.getAxis(['x', 'y'][axisNameIndex]);
    const values = formatMinMax(map([0, 1], function (i) {
        return to
            ? axis.coordToData(axis.toLocalCoord(rangeOrCoordRange[i]), true)
            : axis.toGlobalCoord(axis.dataToCoord(rangeOrCoordRange[i]));
    }));
    const xyMinMax = [];
    xyMinMax[axisNameIndex] = values;
    xyMinMax[1 - axisNameIndex] = [NaN, NaN];

    return {values: values, xyMinMax: xyMinMax};
}


type DiffProcess = (
    values: BrushDimensionMinMax | BrushDimensionMinMax[],
    refer: BrushDimensionMinMax | BrushDimensionMinMax[],
    scales: ReturnType<typeof getScales>
) => BrushDimensionMinMax | BrushDimensionMinMax[];

const diffProcessor: Record<BrushType, DiffProcess> = {

    lineX: curry(axisDiffProcessor, 0),

    lineY: curry(axisDiffProcessor, 1),

    rect: function (
        values: BrushDimensionMinMax[], refer: BrushDimensionMinMax[], scales: ReturnType<typeof getScales>
    ): BrushDimensionMinMax[] {
        return [
            [values[0][0] - scales[0] * refer[0][0], values[0][1] - scales[0] * refer[0][1]],
            [values[1][0] - scales[1] * refer[1][0], values[1][1] - scales[1] * refer[1][1]]
        ];
    },

    polygon: function (
        values: BrushDimensionMinMax[], refer: BrushDimensionMinMax[], scales: ReturnType<typeof getScales>
    ): BrushDimensionMinMax[] {
        return map(values, function (item, idx) {
            return [item[0] - scales[0] * refer[idx][0], item[1] - scales[1] * refer[idx][1]];
        });
    }
};

function axisDiffProcessor(
    axisNameIndex: 0 | 1,
    values: BrushDimensionMinMax,
    refer: BrushDimensionMinMax,
    scales: ReturnType<typeof getScales>
): BrushDimensionMinMax {
    return [
        values[0] - scales[axisNameIndex] * refer[0],
        values[1] - scales[axisNameIndex] * refer[1]
    ];
}

// We have to process scale caused by dataZoom manually,
// although it might be not accurate.
// Return [0~1, 0~1]
function getScales(xyMinMaxCurr: BrushDimensionMinMax[], xyMinMaxOrigin: BrushDimensionMinMax[]): number[] {
    const sizeCurr = getSize(xyMinMaxCurr);
    const sizeOrigin = getSize(xyMinMaxOrigin);
    const scales = [sizeCurr[0] / sizeOrigin[0], sizeCurr[1] / sizeOrigin[1]];
    isNaN(scales[0]) && (scales[0] = 1);
    isNaN(scales[1]) && (scales[1] = 1);
    return scales;
}

function getSize(xyMinMax: BrushDimensionMinMax[]): number[] {
    return xyMinMax
        ? [xyMinMax[0][1] - xyMinMax[0][0], xyMinMax[1][1] - xyMinMax[1][0]]
        : [NaN, NaN];
}

export default BrushTargetManager;

相关信息

echarts 源码目录

相关文章

echarts BrushController 源码

echarts MapDraw 源码

echarts RoamController 源码

echarts brushHelper 源码

echarts cursorHelper 源码

echarts interactionMutex 源码

echarts listComponent 源码

echarts roamHelper 源码

echarts sliderMove 源码

0  赞