echarts axisTrigger 源码
echarts axisTrigger 代码
文件路径:/src/component/axisPointer/axisTrigger.ts
/*
* 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 {makeInner, ModelFinderObject} from '../../util/model';
import * as modelHelper from './modelHelper';
import findPointFromSeries from './findPointFromSeries';
import GlobalModel from '../../model/Global';
import ExtensionAPI from '../../core/ExtensionAPI';
import { Dictionary, Payload, CommonAxisPointerOption, HighlightPayload, DownplayPayload } from '../../util/types';
import AxisPointerModel, { AxisPointerOption } from './AxisPointerModel';
import { each, curry, bind, extend, Curry1 } from 'zrender/src/core/util';
import { ZRenderType } from 'zrender/src/zrender';
const inner = makeInner<{
axisPointerLastHighlights: Dictionary<BatchItem>
}, ZRenderType>();
type AxisValue = CommonAxisPointerOption['value'];
interface DataIndex {
seriesIndex: number
dataIndex: number
dataIndexInside: number
}
type BatchItem = DataIndex;
export interface DataByAxis {
// TODO: TYPE Value type
value: string | number
axisIndex: number
axisDim: string
axisType: string
axisId: string
seriesDataIndices: DataIndex[]
valueLabelOpt: {
precision: AxisPointerOption['label']['precision']
formatter: AxisPointerOption['label']['formatter']
}
}
export interface DataByCoordSys {
coordSysId: string
coordSysIndex: number
coordSysType: string
coordSysMainType: string
dataByAxis: DataByAxis[]
}
interface DataByCoordSysCollection {
list: DataByCoordSys[]
map: Dictionary<DataByCoordSys>
}
type CollectedCoordInfo = ReturnType<typeof modelHelper['collect']>;
type CollectedAxisInfo = CollectedCoordInfo['axesInfo'][string];
interface AxisTriggerPayload extends Payload {
currTrigger?: 'click' | 'mousemove' | 'leave'
/**
* x and y, which are mandatory, specify a point to trigger axisPointer and tooltip.
*/
x?: number
/**
* x and y, which are mandatory, specify a point to trigger axisPointer and tooltip.
*/
y?: number
/**
* finder, optional, restrict target axes.
*/
seriesIndex?: number
dataIndex: number
axesInfo?: {
// 'x'|'y'|'angle'
axisDim?: string
axisIndex?: number
value?: AxisValue
}[]
dispatchAction: ExtensionAPI['dispatchAction']
}
type ShowValueMap = Dictionary<{
value: AxisValue
payloadBatch: BatchItem[]
}>;
/**
* Basic logic: check all axis, if they do not demand show/highlight,
* then hide/downplay them.
*
* @return content of event obj for echarts.connect.
*/
export default function axisTrigger(
payload: AxisTriggerPayload,
ecModel: GlobalModel,
api: ExtensionAPI
) {
const currTrigger = payload.currTrigger;
let point = [payload.x, payload.y];
const finder = payload;
const dispatchAction = payload.dispatchAction || bind(api.dispatchAction, api);
const coordSysAxesInfo = (ecModel.getComponent('axisPointer') as AxisPointerModel)
.coordSysAxesInfo as CollectedCoordInfo;
// Pending
// See #6121. But we are not able to reproduce it yet.
if (!coordSysAxesInfo) {
return;
}
if (illegalPoint(point)) {
// Used in the default behavior of `connection`: use the sample seriesIndex
// and dataIndex. And also used in the tooltipView trigger.
point = findPointFromSeries({
seriesIndex: finder.seriesIndex,
// Do not use dataIndexInside from other ec instance.
// FIXME: auto detect it?
dataIndex: finder.dataIndex
}, ecModel).point;
}
const isIllegalPoint = illegalPoint(point);
// Axis and value can be specified when calling dispatchAction({type: 'updateAxisPointer'}).
// Notice: In this case, it is difficult to get the `point` (which is necessary to show
// tooltip, so if point is not given, we just use the point found by sample seriesIndex
// and dataIndex.
const inputAxesInfo = finder.axesInfo;
const axesInfo = coordSysAxesInfo.axesInfo;
const shouldHide = currTrigger === 'leave' || illegalPoint(point);
const outputPayload = {} as AxisTriggerPayload;
const showValueMap: ShowValueMap = {};
const dataByCoordSys: DataByCoordSysCollection = {
list: [],
map: {}
};
const updaters = {
showPointer: curry(showPointer, showValueMap),
showTooltip: curry(showTooltip, dataByCoordSys)
};
// Process for triggered axes.
each(coordSysAxesInfo.coordSysMap, function (coordSys, coordSysKey) {
// If a point given, it must be contained by the coordinate system.
const coordSysContainsPoint = isIllegalPoint || coordSys.containPoint(point);
each(coordSysAxesInfo.coordSysAxesInfo[coordSysKey], function (axisInfo, key) {
const axis = axisInfo.axis;
const inputAxisInfo = findInputAxisInfo(inputAxesInfo, axisInfo);
// If no inputAxesInfo, no axis is restricted.
if (!shouldHide && coordSysContainsPoint && (!inputAxesInfo || inputAxisInfo)) {
let val = inputAxisInfo && inputAxisInfo.value;
if (val == null && !isIllegalPoint) {
val = axis.pointToData(point);
}
val != null && processOnAxis(axisInfo, val, updaters, false, outputPayload);
}
});
});
// Process for linked axes.
const linkTriggers: Dictionary<AxisValue> = {};
each(axesInfo, function (tarAxisInfo, tarKey) {
const linkGroup = tarAxisInfo.linkGroup;
// If axis has been triggered in the previous stage, it should not be triggered by link.
if (linkGroup && !showValueMap[tarKey]) {
each(linkGroup.axesInfo, function (srcAxisInfo, srcKey) {
const srcValItem = showValueMap[srcKey];
// If srcValItem exist, source axis is triggered, so link to target axis.
if (srcAxisInfo !== tarAxisInfo && srcValItem) {
let val = srcValItem.value;
linkGroup.mapper && (val = tarAxisInfo.axis.scale.parse(linkGroup.mapper(
val, makeMapperParam(srcAxisInfo), makeMapperParam(tarAxisInfo)
)));
linkTriggers[tarAxisInfo.key] = val;
}
});
}
});
each(linkTriggers, function (val, tarKey) {
processOnAxis(axesInfo[tarKey], val, updaters, true, outputPayload);
});
updateModelActually(showValueMap, axesInfo, outputPayload);
dispatchTooltipActually(dataByCoordSys, point, payload, dispatchAction);
dispatchHighDownActually(axesInfo, dispatchAction, api);
return outputPayload;
}
function processOnAxis(
axisInfo: CollectedCoordInfo['axesInfo'][string],
newValue: AxisValue,
updaters: {
showPointer: Curry1<typeof showPointer, ShowValueMap>
showTooltip: Curry1<typeof showTooltip, DataByCoordSysCollection>
},
noSnap: boolean,
outputFinder: ModelFinderObject
) {
const axis = axisInfo.axis;
if (axis.scale.isBlank() || !axis.containData(newValue)) {
return;
}
if (!axisInfo.involveSeries) {
updaters.showPointer(axisInfo, newValue);
return;
}
// Heavy calculation. So put it after axis.containData checking.
const payloadInfo = buildPayloadsBySeries(newValue, axisInfo);
const payloadBatch = payloadInfo.payloadBatch;
const snapToValue = payloadInfo.snapToValue;
// Fill content of event obj for echarts.connect.
// By default use the first involved series data as a sample to connect.
if (payloadBatch[0] && outputFinder.seriesIndex == null) {
extend(outputFinder, payloadBatch[0]);
}
// If no linkSource input, this process is for collecting link
// target, where snap should not be accepted.
if (!noSnap && axisInfo.snap) {
if (axis.containData(snapToValue) && snapToValue != null) {
newValue = snapToValue;
}
}
updaters.showPointer(axisInfo, newValue, payloadBatch);
// Tooltip should always be snapToValue, otherwise there will be
// incorrect "axis value ~ series value" mapping displayed in tooltip.
updaters.showTooltip(axisInfo, payloadInfo, snapToValue);
}
function buildPayloadsBySeries(value: AxisValue, axisInfo: CollectedAxisInfo) {
const axis = axisInfo.axis;
const dim = axis.dim;
let snapToValue = value;
const payloadBatch: BatchItem[] = [];
let minDist = Number.MAX_VALUE;
let minDiff = -1;
each(axisInfo.seriesModels, function (series, idx) {
const dataDim = series.getData().mapDimensionsAll(dim);
let seriesNestestValue;
let dataIndices;
if (series.getAxisTooltipData) {
const result = series.getAxisTooltipData(dataDim, value, axis);
dataIndices = result.dataIndices;
seriesNestestValue = result.nestestValue;
}
else {
dataIndices = series.getData().indicesOfNearest(
dataDim[0],
value as number,
// Add a threshold to avoid find the wrong dataIndex
// when data length is not same.
// false,
axis.type === 'category' ? 0.5 : null
);
if (!dataIndices.length) {
return;
}
seriesNestestValue = series.getData().get(dataDim[0], dataIndices[0]);
}
if (seriesNestestValue == null || !isFinite(seriesNestestValue)) {
return;
}
const diff = value as number - seriesNestestValue;
const dist = Math.abs(diff);
// Consider category case
if (dist <= minDist) {
if (dist < minDist || (diff >= 0 && minDiff < 0)) {
minDist = dist;
minDiff = diff;
snapToValue = seriesNestestValue;
payloadBatch.length = 0;
}
each(dataIndices, function (dataIndex) {
payloadBatch.push({
seriesIndex: series.seriesIndex,
dataIndexInside: dataIndex,
dataIndex: series.getData().getRawIndex(dataIndex)
});
});
}
});
return {
payloadBatch: payloadBatch,
snapToValue: snapToValue
};
}
function showPointer(
showValueMap: ShowValueMap,
axisInfo: CollectedAxisInfo,
value: AxisValue,
payloadBatch?: BatchItem[]
) {
showValueMap[axisInfo.key] = {
value: value,
payloadBatch: payloadBatch
};
}
function showTooltip(
dataByCoordSys: DataByCoordSysCollection,
axisInfo: CollectedCoordInfo['axesInfo'][string],
payloadInfo: { payloadBatch: BatchItem[] },
value: AxisValue
) {
const payloadBatch = payloadInfo.payloadBatch;
const axis = axisInfo.axis;
const axisModel = axis.model;
const axisPointerModel = axisInfo.axisPointerModel;
// If no data, do not create anything in dataByCoordSys,
// whose length will be used to judge whether dispatch action.
if (!axisInfo.triggerTooltip || !payloadBatch.length) {
return;
}
const coordSysModel = axisInfo.coordSys.model;
const coordSysKey = modelHelper.makeKey(coordSysModel);
let coordSysItem = dataByCoordSys.map[coordSysKey];
if (!coordSysItem) {
coordSysItem = dataByCoordSys.map[coordSysKey] = {
coordSysId: coordSysModel.id,
coordSysIndex: coordSysModel.componentIndex,
coordSysType: coordSysModel.type,
coordSysMainType: coordSysModel.mainType,
dataByAxis: []
};
dataByCoordSys.list.push(coordSysItem);
}
coordSysItem.dataByAxis.push({
axisDim: axis.dim,
axisIndex: axisModel.componentIndex,
axisType: axisModel.type,
axisId: axisModel.id,
value: value as number,
// Caustion: viewHelper.getValueLabel is actually on "view stage", which
// depends that all models have been updated. So it should not be performed
// here. Considering axisPointerModel used here is volatile, which is hard
// to be retrieve in TooltipView, we prepare parameters here.
valueLabelOpt: {
precision: axisPointerModel.get(['label', 'precision']),
formatter: axisPointerModel.get(['label', 'formatter'])
},
seriesDataIndices: payloadBatch.slice()
});
}
function updateModelActually(
showValueMap: ShowValueMap,
axesInfo: Dictionary<CollectedAxisInfo>,
outputPayload: AxisTriggerPayload
) {
const outputAxesInfo: AxisTriggerPayload['axesInfo'] = outputPayload.axesInfo = [];
// Basic logic: If no 'show' required, 'hide' this axisPointer.
each(axesInfo, function (axisInfo, key) {
const option = axisInfo.axisPointerModel.option;
const valItem = showValueMap[key];
if (valItem) {
!axisInfo.useHandle && (option.status = 'show');
option.value = valItem.value;
// For label formatter param and highlight.
option.seriesDataIndices = (valItem.payloadBatch || []).slice();
}
// When always show (e.g., handle used), remain
// original value and status.
else {
// If hide, value still need to be set, consider
// click legend to toggle axis blank.
!axisInfo.useHandle && (option.status = 'hide');
}
// If status is 'hide', should be no info in payload.
option.status === 'show' && outputAxesInfo.push({
axisDim: axisInfo.axis.dim,
axisIndex: axisInfo.axis.model.componentIndex,
value: option.value
});
});
}
function dispatchTooltipActually(
dataByCoordSys: DataByCoordSysCollection,
point: number[],
payload: AxisTriggerPayload,
dispatchAction: ExtensionAPI['dispatchAction']
) {
// Basic logic: If no showTip required, hideTip will be dispatched.
if (illegalPoint(point) || !dataByCoordSys.list.length) {
dispatchAction({type: 'hideTip'});
return;
}
// In most case only one axis (or event one series is used). It is
// convinient to fetch payload.seriesIndex and payload.dataIndex
// dirtectly. So put the first seriesIndex and dataIndex of the first
// axis on the payload.
const sampleItem = ((dataByCoordSys.list[0].dataByAxis[0] || {}).seriesDataIndices || [])[0] || {} as DataIndex;
dispatchAction({
type: 'showTip',
escapeConnect: true,
x: point[0],
y: point[1],
tooltipOption: payload.tooltipOption,
position: payload.position,
dataIndexInside: sampleItem.dataIndexInside,
dataIndex: sampleItem.dataIndex,
seriesIndex: sampleItem.seriesIndex,
dataByCoordSys: dataByCoordSys.list
});
}
function dispatchHighDownActually(
axesInfo: Dictionary<CollectedAxisInfo>,
dispatchAction: ExtensionAPI['dispatchAction'],
api: ExtensionAPI
) {
// FIXME
// highlight status modification shoule be a stage of main process?
// (Consider confilct (e.g., legend and axisPointer) and setOption)
const zr = api.getZr();
const highDownKey = 'axisPointerLastHighlights' as const;
const lastHighlights = inner(zr)[highDownKey] || {};
const newHighlights: Dictionary<BatchItem> = inner(zr)[highDownKey] = {};
// Update highlight/downplay status according to axisPointer model.
// Build hash map and remove duplicate incidentally.
each(axesInfo, function (axisInfo, key) {
const option = axisInfo.axisPointerModel.option;
option.status === 'show' && each(option.seriesDataIndices, function (batchItem) {
const key = batchItem.seriesIndex + ' | ' + batchItem.dataIndex;
newHighlights[key] = batchItem;
});
});
// Diff.
const toHighlight: BatchItem[] = [];
const toDownplay: BatchItem[] = [];
each(lastHighlights, function (batchItem, key) {
!newHighlights[key] && toDownplay.push(batchItem);
});
each(newHighlights, function (batchItem, key) {
!lastHighlights[key] && toHighlight.push(batchItem);
});
toDownplay.length && api.dispatchAction({
type: 'downplay',
escapeConnect: true,
// Not blur others when highlight in axisPointer.
notBlur: true,
batch: toDownplay
} as DownplayPayload);
toHighlight.length && api.dispatchAction({
type: 'highlight',
escapeConnect: true,
// Not blur others when highlight in axisPointer.
notBlur: true,
batch: toHighlight
} as HighlightPayload);
}
function findInputAxisInfo(
inputAxesInfo: AxisTriggerPayload['axesInfo'],
axisInfo: CollectedAxisInfo
) {
for (let i = 0; i < (inputAxesInfo || []).length; i++) {
const inputAxisInfo = inputAxesInfo[i];
if (axisInfo.axis.dim === inputAxisInfo.axisDim
&& axisInfo.axis.model.componentIndex === inputAxisInfo.axisIndex
) {
return inputAxisInfo;
}
}
}
function makeMapperParam(axisInfo: CollectedAxisInfo) {
const axisModel = axisInfo.axis.model;
const item = {} as {
axisDim: string
axisIndex: number
axisId: string
axisName: string
// TODO `dim`AxisIndex, `dim`AxisName, `dim`AxisId?
};
const dim = item.axisDim = axisInfo.axis.dim;
item.axisIndex = (item as any)[dim + 'AxisIndex'] = axisModel.componentIndex;
item.axisName = (item as any)[dim + 'AxisName'] = axisModel.name;
item.axisId = (item as any)[dim + 'AxisId'] = axisModel.id;
return item;
}
function illegalPoint(point?: number[]) {
return !point || point[0] == null || isNaN(point[0]) || point[1] == null || isNaN(point[1]);
}
相关信息
相关文章
echarts CartesianAxisPointer 源码
0
赞
热门推荐
-
2、 - 优质文章
-
3、 gate.io
-
8、 golang
-
9、 openharmony
-
10、 Vue中input框自动聚焦