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 {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 | |
| // convenient to fetch payload.seriesIndex and payload.dataIndex | |
| // directly. 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 should 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' && axisInfo.triggerEmphasis && 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]); | |
| } | |