| /* |
| * 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; |