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