/*
|
* 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 BoundingRect from 'zrender/src/core/BoundingRect';
|
import * as visualSolution from '../../visual/visualSolution';
|
import { BrushSelectableArea, makeBrushCommonSelectorForSeries } from './selector';
|
import * as throttleUtil from '../../util/throttle';
|
import BrushTargetManager from '../helper/BrushTargetManager';
|
import GlobalModel from '../../model/Global';
|
import ExtensionAPI from '../../core/ExtensionAPI';
|
import { Payload } from '../../util/types';
|
import BrushModel, { BrushAreaParamInternal } from './BrushModel';
|
import SeriesModel from '../../model/Series';
|
import ParallelSeriesModel from '../../chart/parallel/ParallelSeries';
|
import { ZRenderType } from 'zrender/src/zrender';
|
import { BrushType, BrushDimensionMinMax } from '../helper/BrushController';
|
|
type BrushVisualState = 'inBrush' | 'outOfBrush';
|
|
const STATE_LIST = ['inBrush', 'outOfBrush'] as const;
|
const DISPATCH_METHOD = '__ecBrushSelect' as const;
|
const DISPATCH_FLAG = '__ecInBrushSelectEvent' as const;
|
|
interface BrushGlobalDispatcher extends ZRenderType {
|
[DISPATCH_FLAG]: boolean;
|
[DISPATCH_METHOD]: typeof doDispatch;
|
}
|
|
interface BrushSelectedItem {
|
brushId: string;
|
brushIndex: number;
|
brushName: string;
|
areas: BrushAreaParamInternal[];
|
selected: {
|
seriesId: string;
|
seriesIndex: number;
|
seriesName: string;
|
dataIndex: number[];
|
}[]
|
};
|
|
export function layoutCovers(ecModel: GlobalModel): void {
|
ecModel.eachComponent({mainType: 'brush'}, function (brushModel: BrushModel) {
|
const brushTargetManager = brushModel.brushTargetManager = new BrushTargetManager(brushModel.option, ecModel);
|
brushTargetManager.setInputRanges(brushModel.areas, ecModel);
|
});
|
}
|
|
/**
|
* Register the visual encoding if this modules required.
|
*/
|
export default function brushVisual(ecModel: GlobalModel, api: ExtensionAPI, payload: Payload) {
|
|
const brushSelected: BrushSelectedItem[] = [];
|
let throttleType;
|
let throttleDelay;
|
|
ecModel.eachComponent({mainType: 'brush'}, function (brushModel: BrushModel) {
|
payload && payload.type === 'takeGlobalCursor' && brushModel.setBrushOption(
|
payload.key === 'brush' ? payload.brushOption : {brushType: false}
|
);
|
});
|
|
layoutCovers(ecModel);
|
|
|
ecModel.eachComponent({mainType: 'brush'}, function (brushModel: BrushModel, brushIndex) {
|
|
const thisBrushSelected: BrushSelectedItem = {
|
brushId: brushModel.id,
|
brushIndex: brushIndex,
|
brushName: brushModel.name,
|
areas: zrUtil.clone(brushModel.areas),
|
selected: []
|
};
|
// Every brush component exists in event params, convenient
|
// for user to find by index.
|
brushSelected.push(thisBrushSelected);
|
|
const brushOption = brushModel.option;
|
const brushLink = brushOption.brushLink;
|
const linkedSeriesMap: {[seriesIndex: number]: 0 | 1} = [];
|
const selectedDataIndexForLink: {[dataIndex: number]: 0 | 1} = [];
|
const rangeInfoBySeries: {[seriesIndex: number]: BrushSelectableArea[]} = [];
|
let hasBrushExists = false;
|
|
if (!brushIndex) { // Only the first throttle setting works.
|
throttleType = brushOption.throttleType;
|
throttleDelay = brushOption.throttleDelay;
|
}
|
|
// Add boundingRect and selectors to range.
|
const areas: BrushSelectableArea[] = zrUtil.map(brushModel.areas, function (area) {
|
const builder = boundingRectBuilders[area.brushType];
|
const selectableArea = zrUtil.defaults(
|
{boundingRect: builder ? builder(area) : void 0},
|
area
|
) as BrushSelectableArea;
|
selectableArea.selectors = makeBrushCommonSelectorForSeries(selectableArea);
|
return selectableArea;
|
});
|
|
const visualMappings = visualSolution.createVisualMappings(
|
brushModel.option, STATE_LIST, function (mappingOption) {
|
mappingOption.mappingMethod = 'fixed';
|
}
|
);
|
|
zrUtil.isArray(brushLink) && zrUtil.each(brushLink, function (seriesIndex) {
|
linkedSeriesMap[seriesIndex] = 1;
|
});
|
|
function linkOthers(seriesIndex: number): boolean {
|
return brushLink === 'all' || !!linkedSeriesMap[seriesIndex];
|
}
|
|
// If no supported brush or no brush on the series,
|
// all visuals should be in original state.
|
function brushed(rangeInfoList: BrushSelectableArea[]): boolean {
|
return !!rangeInfoList.length;
|
}
|
|
/**
|
* Logic for each series: (If the logic has to be modified one day, do it carefully!)
|
*
|
* ( brushed ┬ && ┬hasBrushExist ┬ && linkOthers ) => StepA: ┬record, ┬ StepB: ┬visualByRecord.
|
* !brushed┘ ├hasBrushExist ┤ └nothing,┘ ├visualByRecord.
|
* └!hasBrushExist┘ └nothing.
|
* ( !brushed && ┬hasBrushExist ┬ && linkOthers ) => StepA: nothing, StepB: ┬visualByRecord.
|
* └!hasBrushExist┘ └nothing.
|
* ( brushed ┬ && !linkOthers ) => StepA: nothing, StepB: ┬visualByCheck.
|
* !brushed┘ └nothing.
|
* ( !brushed && !linkOthers ) => StepA: nothing, StepB: nothing.
|
*/
|
|
// Step A
|
ecModel.eachSeries(function (seriesModel, seriesIndex) {
|
const rangeInfoList: BrushSelectableArea[] = rangeInfoBySeries[seriesIndex] = [];
|
|
seriesModel.subType === 'parallel'
|
? stepAParallel(seriesModel as ParallelSeriesModel, seriesIndex)
|
: stepAOthers(seriesModel, seriesIndex, rangeInfoList);
|
});
|
|
function stepAParallel(seriesModel: ParallelSeriesModel, seriesIndex: number): void {
|
const coordSys = seriesModel.coordinateSystem;
|
hasBrushExists = hasBrushExists || coordSys.hasAxisBrushed();
|
|
linkOthers(seriesIndex) && coordSys.eachActiveState(
|
seriesModel.getData(),
|
function (activeState, dataIndex) {
|
activeState === 'active' && (selectedDataIndexForLink[dataIndex] = 1);
|
}
|
);
|
}
|
|
function stepAOthers(
|
seriesModel: SeriesModel, seriesIndex: number, rangeInfoList: BrushSelectableArea[]
|
): void {
|
if (!seriesModel.brushSelector || brushModelNotControll(brushModel, seriesIndex)) {
|
return;
|
}
|
|
zrUtil.each(areas, function (area) {
|
if (brushModel.brushTargetManager.controlSeries(area, seriesModel, ecModel)) {
|
rangeInfoList.push(area);
|
}
|
hasBrushExists = hasBrushExists || brushed(rangeInfoList);
|
});
|
|
if (linkOthers(seriesIndex) && brushed(rangeInfoList)) {
|
const data = seriesModel.getData();
|
data.each(function (dataIndex) {
|
if (checkInRange(seriesModel, rangeInfoList, data, dataIndex)) {
|
selectedDataIndexForLink[dataIndex] = 1;
|
}
|
});
|
}
|
}
|
|
// Step B
|
ecModel.eachSeries(function (seriesModel, seriesIndex) {
|
const seriesBrushSelected: BrushSelectedItem['selected'][0] = {
|
seriesId: seriesModel.id,
|
seriesIndex: seriesIndex,
|
seriesName: seriesModel.name,
|
dataIndex: []
|
};
|
// Every series exists in event params, convenient
|
// for user to find series by seriesIndex.
|
thisBrushSelected.selected.push(seriesBrushSelected);
|
|
const rangeInfoList = rangeInfoBySeries[seriesIndex];
|
|
const data = seriesModel.getData();
|
const getValueState = linkOthers(seriesIndex)
|
? function (dataIndex: number): BrushVisualState {
|
return selectedDataIndexForLink[dataIndex]
|
? (seriesBrushSelected.dataIndex.push(data.getRawIndex(dataIndex)), 'inBrush')
|
: 'outOfBrush';
|
}
|
: function (dataIndex: number): BrushVisualState {
|
return checkInRange(seriesModel, rangeInfoList, data, dataIndex)
|
? (seriesBrushSelected.dataIndex.push(data.getRawIndex(dataIndex)), 'inBrush')
|
: 'outOfBrush';
|
};
|
|
// If no supported brush or no brush, all visuals are in original state.
|
(linkOthers(seriesIndex) ? hasBrushExists : brushed(rangeInfoList))
|
&& visualSolution.applyVisual(
|
STATE_LIST, visualMappings, data, getValueState
|
);
|
});
|
|
});
|
|
dispatchAction(api, throttleType, throttleDelay, brushSelected, payload);
|
};
|
|
function dispatchAction(
|
api: ExtensionAPI,
|
throttleType: throttleUtil.ThrottleType,
|
throttleDelay: number,
|
brushSelected: BrushSelectedItem[],
|
payload: Payload
|
): void {
|
// This event will not be triggered when `setOpion`, otherwise dead lock may
|
// triggered when do `setOption` in event listener, which we do not find
|
// satisfactory way to solve yet. Some considered resolutions:
|
// (a) Diff with prevoius selected data ant only trigger event when changed.
|
// But store previous data and diff precisely (i.e., not only by dataIndex, but
|
// also detect value changes in selected data) might bring complexity or fragility.
|
// (b) Use spectial param like `silent` to suppress event triggering.
|
// But such kind of volatile param may be weird in `setOption`.
|
if (!payload) {
|
return;
|
}
|
|
const zr = api.getZr() as BrushGlobalDispatcher;
|
if (zr[DISPATCH_FLAG]) {
|
return;
|
}
|
|
if (!zr[DISPATCH_METHOD]) {
|
zr[DISPATCH_METHOD] = doDispatch;
|
}
|
|
const fn = throttleUtil.createOrUpdate(zr, DISPATCH_METHOD, throttleDelay, throttleType);
|
|
fn(api, brushSelected);
|
}
|
|
function doDispatch(api: ExtensionAPI, brushSelected: BrushSelectedItem[]): void {
|
if (!api.isDisposed()) {
|
const zr = api.getZr() as BrushGlobalDispatcher;
|
zr[DISPATCH_FLAG] = true;
|
api.dispatchAction({
|
type: 'brushSelect',
|
batch: brushSelected
|
});
|
zr[DISPATCH_FLAG] = false;
|
}
|
}
|
|
function checkInRange(
|
seriesModel: SeriesModel,
|
rangeInfoList: BrushSelectableArea[],
|
data: ReturnType<SeriesModel['getData']>,
|
dataIndex: number
|
) {
|
for (let i = 0, len = rangeInfoList.length; i < len; i++) {
|
const area = rangeInfoList[i];
|
if (seriesModel.brushSelector(
|
dataIndex, data, area.selectors, area
|
)) {
|
return true;
|
}
|
}
|
}
|
|
function brushModelNotControll(brushModel: BrushModel, seriesIndex: number): boolean {
|
const seriesIndices = brushModel.option.seriesIndex;
|
return seriesIndices != null
|
&& seriesIndices !== 'all'
|
&& (
|
zrUtil.isArray(seriesIndices)
|
? zrUtil.indexOf(seriesIndices, seriesIndex) < 0
|
: seriesIndex !== seriesIndices
|
);
|
}
|
|
type AreaBoundingRectBuilder = (area: BrushAreaParamInternal) => BoundingRect;
|
const boundingRectBuilders: Partial<Record<BrushType, AreaBoundingRectBuilder>> = {
|
|
rect: function (area) {
|
return getBoundingRectFromMinMax(area.range as BrushDimensionMinMax[]);
|
},
|
|
polygon: function (area) {
|
let minMax;
|
const range = area.range as BrushDimensionMinMax[];
|
|
for (let i = 0, len = range.length; i < len; i++) {
|
minMax = minMax || [[Infinity, -Infinity], [Infinity, -Infinity]];
|
const rg = range[i];
|
rg[0] < minMax[0][0] && (minMax[0][0] = rg[0]);
|
rg[0] > minMax[0][1] && (minMax[0][1] = rg[0]);
|
rg[1] < minMax[1][0] && (minMax[1][0] = rg[1]);
|
rg[1] > minMax[1][1] && (minMax[1][1] = rg[1]);
|
}
|
|
return minMax && getBoundingRectFromMinMax(minMax);
|
}
|
};
|
|
|
function getBoundingRectFromMinMax(minMax: BrushDimensionMinMax[]): BoundingRect {
|
return new BoundingRect(
|
minMax[0][0],
|
minMax[1][0],
|
minMax[0][1] - minMax[0][0],
|
minMax[1][1] - minMax[1][0]
|
);
|
}
|