| /* |
| * 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] |
| ); |
| } |