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