328 lines
12 KiB
JavaScript
328 lines
12 KiB
JavaScript
|
|
||
|
/*
|
||
|
* 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.
|
||
|
*/
|
||
|
|
||
|
var echarts = require("../../echarts");
|
||
|
|
||
|
var zrUtil = require("zrender/lib/core/util");
|
||
|
|
||
|
var BoundingRect = require("zrender/lib/core/BoundingRect");
|
||
|
|
||
|
var visualSolution = require("../../visual/visualSolution");
|
||
|
|
||
|
var selector = require("./selector");
|
||
|
|
||
|
var throttleUtil = require("../../util/throttle");
|
||
|
|
||
|
var BrushTargetManager = require("../helper/BrushTargetManager");
|
||
|
|
||
|
/*
|
||
|
* 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.
|
||
|
*/
|
||
|
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
|
||
|
});
|
||
|
});
|
||
|
layoutCovers(ecModel);
|
||
|
});
|
||
|
|
||
|
function layoutCovers(ecModel) {
|
||
|
ecModel.eachComponent({
|
||
|
mainType: 'brush'
|
||
|
}, function (brushModel) {
|
||
|
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]);
|
||
|
}
|
||
|
|
||
|
exports.layoutCovers = layoutCovers;
|