| /* | 
 | * 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 SeriesData from '../../data/SeriesData'; | 
 | import * as numberUtil from '../../util/number'; | 
 | import * as markerHelper from './markerHelper'; | 
 | import LineDraw from '../../chart/helper/LineDraw'; | 
 | import MarkerView from './MarkerView'; | 
 | import {getStackedDimension} from '../../data/helper/dataStackHelper'; | 
 | import { CoordinateSystem, isCoordinateSystemType } from '../../coord/CoordinateSystem'; | 
 | import MarkLineModel, { MarkLine2DDataItemOption, MarkLineOption } from './MarkLineModel'; | 
 | import { ScaleDataValue, ColorString } from '../../util/types'; | 
 | import SeriesModel from '../../model/Series'; | 
 | import { getECData } from '../../util/innerStore'; | 
 | import ExtensionAPI from '../../core/ExtensionAPI'; | 
 | import Cartesian2D from '../../coord/cartesian/Cartesian2D'; | 
 | import GlobalModel from '../../model/Global'; | 
 | import MarkerModel from './MarkerModel'; | 
 | import { | 
 |     isArray, | 
 |     retrieve, | 
 |     retrieve2, | 
 |     clone, | 
 |     extend, | 
 |     logError, | 
 |     merge, | 
 |     map, | 
 |     curry, | 
 |     filter, | 
 |     HashMap, | 
 |     isNumber | 
 | } from 'zrender/src/core/util'; | 
 | import { makeInner } from '../../util/model'; | 
 | import { LineDataVisual } from '../../visual/commonVisualTypes'; | 
 | import { getVisualFromData } from '../../visual/helper'; | 
 | import Axis2D from '../../coord/cartesian/Axis2D'; | 
 | import SeriesDimensionDefine from '../../data/SeriesDimensionDefine'; | 
 |  | 
 | // Item option for configuring line and each end of symbol. | 
 | // Line option. be merged from configuration of two ends. | 
 | type MarkLineMergedItemOption = MarkLine2DDataItemOption[number]; | 
 |  | 
 | const inner = makeInner<{ | 
 |     // from data | 
 |     from: SeriesData<MarkLineModel> | 
 |     // to data | 
 |     to: SeriesData<MarkLineModel> | 
 | }, MarkLineModel>(); | 
 |  | 
 | const markLineTransform = function ( | 
 |     seriesModel: SeriesModel, | 
 |     coordSys: CoordinateSystem, | 
 |     mlModel: MarkLineModel, | 
 |     item: MarkLineOption['data'][number] | 
 | ) { | 
 |     const data = seriesModel.getData(); | 
 |  | 
 |     let itemArray: MarkLineMergedItemOption[]; | 
 |     if (!isArray(item)) { | 
 |         // Special type markLine like 'min', 'max', 'average', 'median' | 
 |         const mlType = item.type; | 
 |         if ( | 
 |             mlType === 'min' || mlType === 'max' || mlType === 'average' || mlType === 'median' | 
 |             // In case | 
 |             // data: [{ | 
 |             //   yAxis: 10 | 
 |             // }] | 
 |             || (item.xAxis != null || item.yAxis != null) | 
 |         ) { | 
 |  | 
 |             let valueAxis; | 
 |             let value; | 
 |  | 
 |             if (item.yAxis != null || item.xAxis != null) { | 
 |                 valueAxis = coordSys.getAxis(item.yAxis != null ? 'y' : 'x'); | 
 |                 value = retrieve(item.yAxis, item.xAxis); | 
 |             } | 
 |             else { | 
 |                 const axisInfo = markerHelper.getAxisInfo(item, data, coordSys, seriesModel); | 
 |                 valueAxis = axisInfo.valueAxis; | 
 |                 const valueDataDim = getStackedDimension(data, axisInfo.valueDataDim); | 
 |                 value = markerHelper.numCalculate(data, valueDataDim, mlType); | 
 |             } | 
 |             const valueIndex = valueAxis.dim === 'x' ? 0 : 1; | 
 |             const baseIndex = 1 - valueIndex; | 
 |  | 
 |             // Normized to 2d data with start and end point | 
 |             const mlFrom = clone(item) as MarkLine2DDataItemOption[number]; | 
 |             const mlTo = { | 
 |                 coord: [] | 
 |             } as MarkLine2DDataItemOption[number]; | 
 |  | 
 |             mlFrom.type = null; | 
 |  | 
 |             mlFrom.coord = []; | 
 |             mlFrom.coord[baseIndex] = -Infinity; | 
 |             mlTo.coord[baseIndex] = Infinity; | 
 |  | 
 |             const precision = mlModel.get('precision'); | 
 |             if (precision >= 0 && isNumber(value)) { | 
 |                 value = +value.toFixed(Math.min(precision, 20)); | 
 |             } | 
 |  | 
 |             mlFrom.coord[valueIndex] = mlTo.coord[valueIndex] = value; | 
 |  | 
 |             itemArray = [mlFrom, mlTo, { // Extra option for tooltip and label | 
 |                 type: mlType, | 
 |                 valueIndex: item.valueIndex, | 
 |                 // Force to use the value of calculated value. | 
 |                 value: value | 
 |             }]; | 
 |         } | 
 |         else { | 
 |             // Invalid data | 
 |             if (__DEV__) { | 
 |                 logError('Invalid markLine data.'); | 
 |             } | 
 |             itemArray = []; | 
 |         } | 
 |     } | 
 |     else { | 
 |         itemArray = item; | 
 |     } | 
 |  | 
 |     const normalizedItem = [ | 
 |         markerHelper.dataTransform(seriesModel, itemArray[0]), | 
 |         markerHelper.dataTransform(seriesModel, itemArray[1]), | 
 |         extend({}, itemArray[2]) | 
 |     ]; | 
 |  | 
 |     // Avoid line data type is extended by from(to) data type | 
 |     normalizedItem[2].type = normalizedItem[2].type || null; | 
 |  | 
 |     // Merge from option and to option into line option | 
 |     merge(normalizedItem[2], normalizedItem[0]); | 
 |     merge(normalizedItem[2], normalizedItem[1]); | 
 |  | 
 |     return normalizedItem; | 
 | }; | 
 |  | 
 | function isInfinity(val: ScaleDataValue) { | 
 |     return !isNaN(val as number) && !isFinite(val as number); | 
 | } | 
 |  | 
 | // If a markLine has one dim | 
 | function ifMarkLineHasOnlyDim( | 
 |     dimIndex: number, | 
 |     fromCoord: ScaleDataValue[], | 
 |     toCoord: ScaleDataValue[], | 
 |     coordSys: CoordinateSystem | 
 | ) { | 
 |     const otherDimIndex = 1 - dimIndex; | 
 |     const dimName = coordSys.dimensions[dimIndex]; | 
 |     return isInfinity(fromCoord[otherDimIndex]) && isInfinity(toCoord[otherDimIndex]) | 
 |         && fromCoord[dimIndex] === toCoord[dimIndex] && coordSys.getAxis(dimName).containData(fromCoord[dimIndex]); | 
 | } | 
 |  | 
 | function markLineFilter( | 
 |     coordSys: CoordinateSystem, | 
 |     item: MarkLine2DDataItemOption | 
 | ) { | 
 |     if (coordSys.type === 'cartesian2d') { | 
 |         const fromCoord = item[0].coord; | 
 |         const toCoord = item[1].coord; | 
 |         // In case | 
 |         // { | 
 |         //  markLine: { | 
 |         //    data: [{ yAxis: 2 }] | 
 |         //  } | 
 |         // } | 
 |         if ( | 
 |             fromCoord && toCoord | 
 |             && (ifMarkLineHasOnlyDim(1, fromCoord, toCoord, coordSys) | 
 |             || ifMarkLineHasOnlyDim(0, fromCoord, toCoord, coordSys)) | 
 |         ) { | 
 |             return true; | 
 |         } | 
 |     } | 
 |     return markerHelper.dataFilter(coordSys, item[0]) | 
 |         && markerHelper.dataFilter(coordSys, item[1]); | 
 | } | 
 |  | 
 | function updateSingleMarkerEndLayout( | 
 |     data: SeriesData<MarkLineModel>, | 
 |     idx: number, | 
 |     isFrom: boolean, | 
 |     seriesModel: SeriesModel, | 
 |     api: ExtensionAPI | 
 | ) { | 
 |     const coordSys = seriesModel.coordinateSystem; | 
 |     const itemModel = data.getItemModel<MarkLine2DDataItemOption[number]>(idx); | 
 |  | 
 |     let point; | 
 |     const xPx = numberUtil.parsePercent(itemModel.get('x'), api.getWidth()); | 
 |     const yPx = numberUtil.parsePercent(itemModel.get('y'), api.getHeight()); | 
 |     if (!isNaN(xPx) && !isNaN(yPx)) { | 
 |         point = [xPx, yPx]; | 
 |     } | 
 |     else { | 
 |         // Chart like bar may have there own marker positioning logic | 
 |         if (seriesModel.getMarkerPosition) { | 
 |             // Use the getMarkerPosition | 
 |             point = seriesModel.getMarkerPosition( | 
 |                 data.getValues(data.dimensions, idx) | 
 |             ); | 
 |         } | 
 |         else { | 
 |             const dims = coordSys.dimensions; | 
 |             const x = data.get(dims[0], idx); | 
 |             const y = data.get(dims[1], idx); | 
 |             point = coordSys.dataToPoint([x, y]); | 
 |         } | 
 |         // Expand line to the edge of grid if value on one axis is Inifnity | 
 |         // In case | 
 |         //  markLine: { | 
 |         //    data: [{ | 
 |         //      yAxis: 2 | 
 |         //      // or | 
 |         //      type: 'average' | 
 |         //    }] | 
 |         //  } | 
 |         if (isCoordinateSystemType<Cartesian2D>(coordSys, 'cartesian2d')) { | 
 |             // TODO: TYPE ts@4.1 may still infer it as Axis instead of Axis2D. Not sure if it's a bug | 
 |             const xAxis = coordSys.getAxis('x') as Axis2D; | 
 |             const yAxis = coordSys.getAxis('y') as Axis2D; | 
 |             const dims = coordSys.dimensions; | 
 |             if (isInfinity(data.get(dims[0], idx))) { | 
 |                 point[0] = xAxis.toGlobalCoord(xAxis.getExtent()[isFrom ? 0 : 1]); | 
 |             } | 
 |             else if (isInfinity(data.get(dims[1], idx))) { | 
 |                 point[1] = yAxis.toGlobalCoord(yAxis.getExtent()[isFrom ? 0 : 1]); | 
 |             } | 
 |         } | 
 |  | 
 |         // Use x, y if has any | 
 |         if (!isNaN(xPx)) { | 
 |             point[0] = xPx; | 
 |         } | 
 |         if (!isNaN(yPx)) { | 
 |             point[1] = yPx; | 
 |         } | 
 |     } | 
 |  | 
 |     data.setItemLayout(idx, point); | 
 | } | 
 |  | 
 | class MarkLineView extends MarkerView { | 
 |  | 
 |     static type = 'markLine'; | 
 |     type = MarkLineView.type; | 
 |  | 
 |     markerGroupMap: HashMap<LineDraw>; | 
 |  | 
 |     updateTransform(markLineModel: MarkLineModel, ecModel: GlobalModel, api: ExtensionAPI) { | 
 |         ecModel.eachSeries(function (seriesModel) { | 
 |             const mlModel = MarkerModel.getMarkerModelFromSeries(seriesModel, 'markLine') as MarkLineModel; | 
 |             if (mlModel) { | 
 |                 const mlData = mlModel.getData(); | 
 |                 const fromData = inner(mlModel).from; | 
 |                 const toData = inner(mlModel).to; | 
 |                 // Update visual and layout of from symbol and to symbol | 
 |                 fromData.each(function (idx) { | 
 |                     updateSingleMarkerEndLayout(fromData, idx, true, seriesModel, api); | 
 |                     updateSingleMarkerEndLayout(toData, idx, false, seriesModel, api); | 
 |                 }); | 
 |                 // Update layout of line | 
 |                 mlData.each(function (idx) { | 
 |                     mlData.setItemLayout(idx, [ | 
 |                         fromData.getItemLayout(idx), | 
 |                         toData.getItemLayout(idx) | 
 |                     ]); | 
 |                 }); | 
 |  | 
 |                 this.markerGroupMap.get(seriesModel.id).updateLayout(); | 
 |  | 
 |             } | 
 |         }, this); | 
 |     } | 
 |  | 
 |     renderSeries( | 
 |         seriesModel: SeriesModel, | 
 |         mlModel: MarkLineModel, | 
 |         ecModel: GlobalModel, | 
 |         api: ExtensionAPI | 
 |     ) { | 
 |         const coordSys = seriesModel.coordinateSystem; | 
 |         const seriesId = seriesModel.id; | 
 |         const seriesData = seriesModel.getData(); | 
 |  | 
 |         const lineDrawMap = this.markerGroupMap; | 
 |         const lineDraw = lineDrawMap.get(seriesId) | 
 |             || lineDrawMap.set(seriesId, new LineDraw()); | 
 |         this.group.add(lineDraw.group); | 
 |  | 
 |         const mlData = createList(coordSys, seriesModel, mlModel); | 
 |  | 
 |         const fromData = mlData.from; | 
 |         const toData = mlData.to; | 
 |         const lineData = mlData.line as SeriesData<MarkLineModel, LineDataVisual>; | 
 |  | 
 |         inner(mlModel).from = fromData; | 
 |         inner(mlModel).to = toData; | 
 |         // Line data for tooltip and formatter | 
 |         mlModel.setData(lineData); | 
 |  | 
 |         // TODO | 
 |         // Functionally, `symbolSize` & `symbolOffset` can also be 2D array now. | 
 |         // But the related logic and type definition are not finished yet. | 
 |         // Finish it if required | 
 |         let symbolType = mlModel.get('symbol'); | 
 |         let symbolSize = mlModel.get('symbolSize'); | 
 |         let symbolRotate = mlModel.get('symbolRotate'); | 
 |         let symbolOffset = mlModel.get('symbolOffset'); | 
 |         // TODO: support callback function like markPoint | 
 |         if (!isArray(symbolType)) { | 
 |             symbolType = [symbolType, symbolType]; | 
 |         } | 
 |         if (!isArray(symbolSize)) { | 
 |             symbolSize = [symbolSize, symbolSize]; | 
 |         } | 
 |         if (!isArray(symbolRotate)) { | 
 |             symbolRotate = [symbolRotate, symbolRotate]; | 
 |         } | 
 |         if (!isArray(symbolOffset)) { | 
 |             symbolOffset = [symbolOffset, symbolOffset]; | 
 |         } | 
 |  | 
 |         // Update visual and layout of from symbol and to symbol | 
 |         mlData.from.each(function (idx) { | 
 |             updateDataVisualAndLayout(fromData, idx, true); | 
 |             updateDataVisualAndLayout(toData, idx, false); | 
 |         }); | 
 |  | 
 |         // Update visual and layout of line | 
 |         lineData.each(function (idx) { | 
 |             const lineStyle = lineData.getItemModel<MarkLineMergedItemOption>(idx) | 
 |                 .getModel('lineStyle').getLineStyle(); | 
 |             // lineData.setItemVisual(idx, { | 
 |             //     color: lineColor || fromData.getItemVisual(idx, 'color') | 
 |             // }); | 
 |             lineData.setItemLayout(idx, [ | 
 |                 fromData.getItemLayout(idx), | 
 |                 toData.getItemLayout(idx) | 
 |             ]); | 
 |  | 
 |             if (lineStyle.stroke == null) { | 
 |                 lineStyle.stroke = fromData.getItemVisual(idx, 'style').fill; | 
 |             } | 
 |  | 
 |             lineData.setItemVisual(idx, { | 
 |                 fromSymbolKeepAspect: fromData.getItemVisual(idx, 'symbolKeepAspect'), | 
 |                 fromSymbolOffset: fromData.getItemVisual(idx, 'symbolOffset'), | 
 |                 fromSymbolRotate: fromData.getItemVisual(idx, 'symbolRotate'), | 
 |                 fromSymbolSize: fromData.getItemVisual(idx, 'symbolSize') as number, | 
 |                 fromSymbol: fromData.getItemVisual(idx, 'symbol'), | 
 |                 toSymbolKeepAspect: toData.getItemVisual(idx, 'symbolKeepAspect'), | 
 |                 toSymbolOffset: toData.getItemVisual(idx, 'symbolOffset'), | 
 |                 toSymbolRotate: toData.getItemVisual(idx, 'symbolRotate'), | 
 |                 toSymbolSize: toData.getItemVisual(idx, 'symbolSize') as number, | 
 |                 toSymbol: toData.getItemVisual(idx, 'symbol'), | 
 |                 style: lineStyle | 
 |             }); | 
 |         }); | 
 |  | 
 |         lineDraw.updateData(lineData); | 
 |  | 
 |         // Set host model for tooltip | 
 |         // FIXME | 
 |         mlData.line.eachItemGraphicEl(function (el) { | 
 |             getECData(el).dataModel = mlModel; | 
 |  | 
 |             el.traverse(function (child) { | 
 |                 getECData(child).dataModel = mlModel; | 
 |             }); | 
 |         }); | 
 |  | 
 |         function updateDataVisualAndLayout( | 
 |             data: SeriesData<MarkLineModel>, | 
 |             idx: number, | 
 |             isFrom: boolean | 
 |         ) { | 
 |             const itemModel = data.getItemModel<MarkLineMergedItemOption>(idx); | 
 |  | 
 |             updateSingleMarkerEndLayout( | 
 |                 data, idx, isFrom, seriesModel, api | 
 |             ); | 
 |  | 
 |             const style = itemModel.getModel('itemStyle').getItemStyle(); | 
 |             if (style.fill == null) { | 
 |                 style.fill = getVisualFromData(seriesData, 'color') as ColorString; | 
 |             } | 
 |  | 
 |             data.setItemVisual(idx, { | 
 |                 symbolKeepAspect: itemModel.get('symbolKeepAspect'), | 
 |                 // `0` should be considered as a valid value, so use `retrieve2` instead of `||` | 
 |                 symbolOffset: retrieve2( | 
 |                     itemModel.get('symbolOffset', true), | 
 |                     (symbolOffset as (string | number)[])[isFrom ? 0 : 1] | 
 |                 ), | 
 |                 symbolRotate: retrieve2( | 
 |                     itemModel.get('symbolRotate', true), | 
 |                     (symbolRotate as number[])[isFrom ? 0 : 1] | 
 |                 ), | 
 |                 // TODO: when 2d array is supported, it should ignore parent | 
 |                 symbolSize: retrieve2( | 
 |                     itemModel.get('symbolSize'), | 
 |                     (symbolSize as number[])[isFrom ? 0 : 1] | 
 |                 ), | 
 |                 symbol: retrieve2( | 
 |                     itemModel.get('symbol', true), | 
 |                     (symbolType as string[])[isFrom ? 0 : 1] | 
 |                 ), | 
 |                 style | 
 |             }); | 
 |         } | 
 |  | 
 |         this.markKeep(lineDraw); | 
 |  | 
 |         lineDraw.group.silent = mlModel.get('silent') || seriesModel.get('silent'); | 
 |     } | 
 | } | 
 |  | 
 | function createList(coordSys: CoordinateSystem, seriesModel: SeriesModel, mlModel: MarkLineModel) { | 
 |  | 
 |     let coordDimsInfos: SeriesDimensionDefine[]; | 
 |     if (coordSys) { | 
 |         coordDimsInfos = map(coordSys && coordSys.dimensions, function (coordDim) { | 
 |             const info = seriesModel.getData().getDimensionInfo( | 
 |                 seriesModel.getData().mapDimension(coordDim) | 
 |             ) || {}; | 
 |             // In map series data don't have lng and lat dimension. Fallback to same with coordSys | 
 |             return extend(extend({}, info), { | 
 |                 name: coordDim, | 
 |                 // DON'T use ordinalMeta to parse and collect ordinal. | 
 |                 ordinalMeta: null | 
 |             }); | 
 |         }); | 
 |     } | 
 |     else { | 
 |         coordDimsInfos = [{ | 
 |             name: 'value', | 
 |             type: 'float' | 
 |         }]; | 
 |     } | 
 |  | 
 |     const fromData = new SeriesData(coordDimsInfos, mlModel); | 
 |     const toData = new SeriesData(coordDimsInfos, mlModel); | 
 |     // No dimensions | 
 |     const lineData = new SeriesData([], mlModel); | 
 |  | 
 |     let optData = map(mlModel.get('data'), curry( | 
 |         markLineTransform, seriesModel, coordSys, mlModel | 
 |     )); | 
 |     if (coordSys) { | 
 |         optData = filter( | 
 |             optData, curry(markLineFilter, coordSys) | 
 |         ); | 
 |     } | 
 |  | 
 |     const dimValueGetter = markerHelper.createMarkerDimValueGetter(!!coordSys, coordDimsInfos); | 
 |  | 
 |     fromData.initData( | 
 |         map(optData, function (item) { | 
 |             return item[0]; | 
 |         }), | 
 |         null, | 
 |         dimValueGetter | 
 |     ); | 
 |     toData.initData( | 
 |         map(optData, function (item) { | 
 |             return item[1]; | 
 |         }), | 
 |         null, | 
 |         dimValueGetter | 
 |     ); | 
 |     lineData.initData( | 
 |         map(optData, function (item) { | 
 |             return item[2]; | 
 |         }) | 
 |     ); | 
 |     lineData.hasItemOption = true; | 
 |  | 
 |     return { | 
 |         from: fromData, | 
 |         to: toData, | 
 |         line: lineData | 
 |     }; | 
 | } | 
 |  | 
 | export default MarkLineView; |