blob: f8c8c8e693d6529837a76967d46a35818109f7b5 [file] [log] [blame]
/*
* 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, ParsedModelFinder, ModelFinder,
parseFinder as modelUtilParseFinder,
ParsedModelFinderKnown
} from '../../util/model';
const COORD_CONVERTS = ['dataToPoint', 'pointToData'] as const;
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);
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
) => {
values: BrushAreaRange,
xyMinMax: BrushDimensionMinMax[]
};
const coordConvert: Record<BrushType, ConvertCoord> = {
lineX: curry(axisConvert, 0),
lineY: curry(axisConvert, 1),
rect: function (to, coordSys, rangeOrCoordRange: BrushDimensionMinMax[]): {
values: BrushDimensionMinMax[],
xyMinMax: BrushDimensionMinMax[]
} {
const xminymin = coordSys[COORD_CONVERTS[to]]([rangeOrCoordRange[0][0], rangeOrCoordRange[1][0]]);
const xmaxymax = coordSys[COORD_CONVERTS[to]]([rangeOrCoordRange[0][1], rangeOrCoordRange[1][1]]);
const values = [
formatMinMax([xminymin[0], xmaxymax[0]]),
formatMinMax([xminymin[1], xmaxymax[1]])
];
return {values: values, xyMinMax: values};
},
polygon: function (to, coordSys, rangeOrCoordRange: BrushDimensionMinMax[]): {
values: BrushDimensionMinMax[],
xyMinMax: BrushDimensionMinMax[]
} {
const xyMinMax = [[Infinity, -Infinity], [Infinity, -Infinity]];
const values = map(rangeOrCoordRange, function (item) {
const p = coordSys[COORD_CONVERTS[to]](item);
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]))
: 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;