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