| /* |
| * 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 echarts from '../../echarts'; |
| import * as zrUtil from 'zrender/src/core/util'; |
| import BoundingRect from 'zrender/src/core/BoundingRect'; |
| import * as visualSolution from '../../visual/visualSolution'; |
| import selector from './selector'; |
| import * as throttleUtil from '../../util/throttle'; |
| import BrushTargetManager from '../helper/BrushTargetManager'; |
| var STATE_LIST = ['inBrush', 'outOfBrush']; |
| var DISPATCH_METHOD = '__ecBrushSelect'; |
| var DISPATCH_FLAG = '__ecInBrushSelectEvent'; |
| var PRIORITY_BRUSH = echarts.PRIORITY.VISUAL.BRUSH; |
| /** |
| * Layout for visual, the priority higher than other layout, and before brush visual. |
| */ |
| |
| echarts.registerLayout(PRIORITY_BRUSH, function (ecModel, api, payload) { |
| ecModel.eachComponent({ |
| mainType: 'brush' |
| }, function (brushModel) { |
| payload && payload.type === 'takeGlobalCursor' && brushModel.setBrushOption(payload.key === 'brush' ? payload.brushOption : { |
| brushType: false |
| }); |
| var brushTargetManager = brushModel.brushTargetManager = new BrushTargetManager(brushModel.option, ecModel); |
| brushTargetManager.setInputRanges(brushModel.areas, ecModel); |
| }); |
| }); |
| /** |
| * Register the visual encoding if this modules required. |
| */ |
| |
| echarts.registerVisual(PRIORITY_BRUSH, function (ecModel, api, payload) { |
| var brushSelected = []; |
| var throttleType; |
| var throttleDelay; |
| ecModel.eachComponent({ |
| mainType: 'brush' |
| }, function (brushModel, brushIndex) { |
| var thisBrushSelected = { |
| 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); |
| var brushOption = brushModel.option; |
| var brushLink = brushOption.brushLink; |
| var linkedSeriesMap = []; |
| var selectedDataIndexForLink = []; |
| var rangeInfoBySeries = []; |
| var hasBrushExists = 0; |
| |
| if (!brushIndex) { |
| // Only the first throttle setting works. |
| throttleType = brushOption.throttleType; |
| throttleDelay = brushOption.throttleDelay; |
| } // Add boundingRect and selectors to range. |
| |
| |
| var areas = zrUtil.map(brushModel.areas, function (area) { |
| return bindSelector(zrUtil.defaults({ |
| boundingRect: boundingRectBuilders[area.brushType](area) |
| }, area)); |
| }); |
| var 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) { |
| 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) { |
| 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) { |
| var rangeInfoList = rangeInfoBySeries[seriesIndex] = []; |
| seriesModel.subType === 'parallel' ? stepAParallel(seriesModel, seriesIndex, rangeInfoList) : stepAOthers(seriesModel, seriesIndex, rangeInfoList); |
| }); |
| |
| function stepAParallel(seriesModel, seriesIndex) { |
| var coordSys = seriesModel.coordinateSystem; |
| hasBrushExists |= coordSys.hasAxisBrushed(); |
| linkOthers(seriesIndex) && coordSys.eachActiveState(seriesModel.getData(), function (activeState, dataIndex) { |
| activeState === 'active' && (selectedDataIndexForLink[dataIndex] = 1); |
| }); |
| } |
| |
| function stepAOthers(seriesModel, seriesIndex, rangeInfoList) { |
| var selectorsByBrushType = getSelectorsByBrushType(seriesModel); |
| |
| if (!selectorsByBrushType || brushModelNotControll(brushModel, seriesIndex)) { |
| return; |
| } |
| |
| zrUtil.each(areas, function (area) { |
| selectorsByBrushType[area.brushType] && brushModel.brushTargetManager.controlSeries(area, seriesModel, ecModel) && rangeInfoList.push(area); |
| hasBrushExists |= brushed(rangeInfoList); |
| }); |
| |
| if (linkOthers(seriesIndex) && brushed(rangeInfoList)) { |
| var data = seriesModel.getData(); |
| data.each(function (dataIndex) { |
| if (checkInRange(selectorsByBrushType, rangeInfoList, data, dataIndex)) { |
| selectedDataIndexForLink[dataIndex] = 1; |
| } |
| }); |
| } |
| } // Step B |
| |
| |
| ecModel.eachSeries(function (seriesModel, seriesIndex) { |
| var seriesBrushSelected = { |
| 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); |
| var selectorsByBrushType = getSelectorsByBrushType(seriesModel); |
| var rangeInfoList = rangeInfoBySeries[seriesIndex]; |
| var data = seriesModel.getData(); |
| var getValueState = linkOthers(seriesIndex) ? function (dataIndex) { |
| return selectedDataIndexForLink[dataIndex] ? (seriesBrushSelected.dataIndex.push(data.getRawIndex(dataIndex)), 'inBrush') : 'outOfBrush'; |
| } : function (dataIndex) { |
| return checkInRange(selectorsByBrushType, 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, throttleType, throttleDelay, brushSelected, payload) { |
| // 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; |
| } |
| |
| var zr = api.getZr(); |
| |
| if (zr[DISPATCH_FLAG]) { |
| return; |
| } |
| |
| if (!zr[DISPATCH_METHOD]) { |
| zr[DISPATCH_METHOD] = doDispatch; |
| } |
| |
| var fn = throttleUtil.createOrUpdate(zr, DISPATCH_METHOD, throttleDelay, throttleType); |
| fn(api, brushSelected); |
| } |
| |
| function doDispatch(api, brushSelected) { |
| if (!api.isDisposed()) { |
| var zr = api.getZr(); |
| zr[DISPATCH_FLAG] = true; |
| api.dispatchAction({ |
| type: 'brushSelect', |
| batch: brushSelected |
| }); |
| zr[DISPATCH_FLAG] = false; |
| } |
| } |
| |
| function checkInRange(selectorsByBrushType, rangeInfoList, data, dataIndex) { |
| for (var i = 0, len = rangeInfoList.length; i < len; i++) { |
| var area = rangeInfoList[i]; |
| |
| if (selectorsByBrushType[area.brushType](dataIndex, data, area.selectors, area)) { |
| return true; |
| } |
| } |
| } |
| |
| function getSelectorsByBrushType(seriesModel) { |
| var brushSelector = seriesModel.brushSelector; |
| |
| if (zrUtil.isString(brushSelector)) { |
| var sels = []; |
| zrUtil.each(selector, function (selectorsByElementType, brushType) { |
| sels[brushType] = function (dataIndex, data, selectors, area) { |
| var itemLayout = data.getItemLayout(dataIndex); |
| return selectorsByElementType[brushSelector](itemLayout, selectors, area); |
| }; |
| }); |
| return sels; |
| } else if (zrUtil.isFunction(brushSelector)) { |
| var bSelector = {}; |
| zrUtil.each(selector, function (sel, brushType) { |
| bSelector[brushType] = brushSelector; |
| }); |
| return bSelector; |
| } |
| |
| return brushSelector; |
| } |
| |
| function brushModelNotControll(brushModel, seriesIndex) { |
| var seriesIndices = brushModel.option.seriesIndex; |
| return seriesIndices != null && seriesIndices !== 'all' && (zrUtil.isArray(seriesIndices) ? zrUtil.indexOf(seriesIndices, seriesIndex) < 0 : seriesIndex !== seriesIndices); |
| } |
| |
| function bindSelector(area) { |
| var selectors = area.selectors = {}; |
| zrUtil.each(selector[area.brushType], function (selFn, elType) { |
| // Do not use function binding or curry for performance. |
| selectors[elType] = function (itemLayout) { |
| return selFn(itemLayout, selectors, area); |
| }; |
| }); |
| return area; |
| } |
| |
| var boundingRectBuilders = { |
| lineX: zrUtil.noop, |
| lineY: zrUtil.noop, |
| rect: function (area) { |
| return getBoundingRectFromMinMax(area.range); |
| }, |
| polygon: function (area) { |
| var minMax; |
| var range = area.range; |
| |
| for (var i = 0, len = range.length; i < len; i++) { |
| minMax = minMax || [[Infinity, -Infinity], [Infinity, -Infinity]]; |
| var 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) { |
| return new BoundingRect(minMax[0][0], minMax[1][0], minMax[0][1] - minMax[0][0], minMax[1][1] - minMax[1][0]); |
| } |