| /* | 
 | * 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. | 
 | */ | 
 |  | 
 | // TODO Optimize on polar | 
 |  | 
 | import * as colorUtil from 'zrender/src/tool/color'; | 
 | import SeriesData from '../../data/SeriesData'; | 
 | import * as numberUtil from '../../util/number'; | 
 | import * as graphic from '../../util/graphic'; | 
 | import { toggleHoverEmphasis, setStatesStylesFromModel } from '../../util/states'; | 
 | import * as markerHelper from './markerHelper'; | 
 | import MarkerView from './MarkerView'; | 
 | import { retrieve, mergeAll, map, curry, filter, HashMap, extend, isString } from 'zrender/src/core/util'; | 
 | import { ScaleDataValue, ZRColor } from '../../util/types'; | 
 | import { CoordinateSystem, isCoordinateSystemType } from '../../coord/CoordinateSystem'; | 
 | import MarkAreaModel, { MarkArea2DDataItemOption } from './MarkAreaModel'; | 
 | import SeriesModel from '../../model/Series'; | 
 | import Cartesian2D from '../../coord/cartesian/Cartesian2D'; | 
 | import SeriesDimensionDefine from '../../data/SeriesDimensionDefine'; | 
 | import GlobalModel from '../../model/Global'; | 
 | import ExtensionAPI from '../../core/ExtensionAPI'; | 
 | import MarkerModel from './MarkerModel'; | 
 | import { makeInner } from '../../util/model'; | 
 | import { getVisualFromData } from '../../visual/helper'; | 
 | import { setLabelStyle, getLabelStatesModels } from '../../label/labelStyle'; | 
 | import { getECData } from '../../util/innerStore'; | 
 | import Axis2D from '../../coord/cartesian/Axis2D'; | 
 | import { parseDataValue } from '../../data/helper/dataValueHelper'; | 
 |  | 
 | interface MarkAreaDrawGroup { | 
 |     group: graphic.Group | 
 | } | 
 |  | 
 | const inner = makeInner<{ | 
 |     data: SeriesData<MarkAreaModel> | 
 | }, MarkAreaDrawGroup>(); | 
 |  | 
 | // Merge two ends option into one. | 
 | type MarkAreaMergedItemOption = Omit<MarkArea2DDataItemOption[number], 'coord'> & { | 
 |     coord: MarkArea2DDataItemOption[number]['coord'][] | 
 |     x0: number | string | 
 |     y0: number | string | 
 |     x1: number | string | 
 |     y1: number | string | 
 | }; | 
 |  | 
 | const markAreaTransform = function ( | 
 |     seriesModel: SeriesModel, | 
 |     coordSys: CoordinateSystem, | 
 |     maModel: MarkAreaModel, | 
 |     item: MarkArea2DDataItemOption | 
 | ): MarkAreaMergedItemOption { | 
 |     // item may be null | 
 |     const item0 = item[0]; | 
 |     const item1 = item[1]; | 
 |     if (!item0 || !item1) { | 
 |         return; | 
 |     } | 
 |  | 
 |     const lt = markerHelper.dataTransform(seriesModel, item0); | 
 |     const rb = markerHelper.dataTransform(seriesModel, item1); | 
 |  | 
 |     // FIXME make sure lt is less than rb | 
 |     const ltCoord = lt.coord; | 
 |     const rbCoord = rb.coord; | 
 |     ltCoord[0] = retrieve(ltCoord[0], -Infinity); | 
 |     ltCoord[1] = retrieve(ltCoord[1], -Infinity); | 
 |  | 
 |     rbCoord[0] = retrieve(rbCoord[0], Infinity); | 
 |     rbCoord[1] = retrieve(rbCoord[1], Infinity); | 
 |  | 
 |     // Merge option into one | 
 |     const result: MarkAreaMergedItemOption = mergeAll([{}, lt, rb]); | 
 |  | 
 |     result.coord = [ | 
 |         lt.coord, rb.coord | 
 |     ]; | 
 |     result.x0 = lt.x; | 
 |     result.y0 = lt.y; | 
 |     result.x1 = rb.x; | 
 |     result.y1 = rb.y; | 
 |     return result; | 
 | }; | 
 |  | 
 | function isInfinity(val: ScaleDataValue) { | 
 |     return !isNaN(val as number) && !isFinite(val as number); | 
 | } | 
 |  | 
 | // If a markArea has one dim | 
 | function ifMarkAreaHasOnlyDim( | 
 |     dimIndex: number, | 
 |     fromCoord: ScaleDataValue[], | 
 |     toCoord: ScaleDataValue[], | 
 |     coordSys: CoordinateSystem | 
 | ) { | 
 |     const otherDimIndex = 1 - dimIndex; | 
 |     return isInfinity(fromCoord[otherDimIndex]) && isInfinity(toCoord[otherDimIndex]); | 
 | } | 
 |  | 
 | function markAreaFilter(coordSys: CoordinateSystem, item: MarkAreaMergedItemOption) { | 
 |     const fromCoord = item.coord[0]; | 
 |     const toCoord = item.coord[1]; | 
 |     const item0 = { | 
 |         coord: fromCoord, | 
 |         x: item.x0, | 
 |         y: item.y0 | 
 |     }; | 
 |     const item1 = { | 
 |         coord: toCoord, | 
 |         x: item.x1, | 
 |         y: item.y1 | 
 |     }; | 
 |     if (isCoordinateSystemType<Cartesian2D>(coordSys, 'cartesian2d')) { | 
 |         // In case | 
 |         // { | 
 |         //  markArea: { | 
 |         //    data: [{ yAxis: 2 }] | 
 |         //  } | 
 |         // } | 
 |         if ( | 
 |             fromCoord && toCoord | 
 |             && (ifMarkAreaHasOnlyDim(1, fromCoord, toCoord, coordSys) | 
 |             || ifMarkAreaHasOnlyDim(0, fromCoord, toCoord, coordSys)) | 
 |         ) { | 
 |             return true; | 
 |         } | 
 |         // Directly returning true may also do the work, | 
 |         // because markArea will not be shown automatically | 
 |         // when it's not included in coordinate system. | 
 |         // But filtering ahead can avoid keeping rendering markArea | 
 |         // when there are too many of them. | 
 |         return markerHelper.zoneFilter(coordSys, item0, item1); | 
 |     } | 
 |     return markerHelper.dataFilter(coordSys, item0) | 
 |         || markerHelper.dataFilter(coordSys, item1); | 
 | } | 
 |  | 
 | // dims can be ['x0', 'y0'], ['x1', 'y1'], ['x0', 'y1'], ['x1', 'y0'] | 
 | function getSingleMarkerEndPoint( | 
 |     data: SeriesData<MarkAreaModel>, | 
 |     idx: number, | 
 |     dims: typeof dimPermutations[number], | 
 |     seriesModel: SeriesModel, | 
 |     api: ExtensionAPI | 
 | ) { | 
 |     const coordSys = seriesModel.coordinateSystem; | 
 |     const itemModel = data.getItemModel<MarkAreaMergedItemOption>(idx); | 
 |  | 
 |     let point; | 
 |     const xPx = numberUtil.parsePercent(itemModel.get(dims[0]), api.getWidth()); | 
 |     const yPx = numberUtil.parsePercent(itemModel.get(dims[1]), api.getHeight()); | 
 |     if (!isNaN(xPx) && !isNaN(yPx)) { | 
 |         point = [xPx, yPx]; | 
 |     } | 
 |     else { | 
 |         // Chart like bar may have there own marker positioning logic | 
 |         if (seriesModel.getMarkerPosition) { | 
 |             // Consider the case that user input the right-bottom point first | 
 |             // Pick the larger x and y as 'x1' and 'y1' | 
 |             const pointValue0 = data.getValues(['x0', 'y0'], idx); | 
 |             const pointValue1 = data.getValues(['x1', 'y1'], idx); | 
 |             const clampPointValue0 = coordSys.clampData(pointValue0); | 
 |             const clampPointValue1 = coordSys.clampData(pointValue1); | 
 |             const pointValue = []; | 
 |             if (dims[0] === 'x0') { | 
 |                 pointValue[0] = (clampPointValue0[0] > clampPointValue1[0]) ? pointValue1[0] : pointValue0[0]; | 
 |             } | 
 |             else { | 
 |                 pointValue[0] = (clampPointValue0[0] > clampPointValue1[0]) ? pointValue0[0] : pointValue1[0]; | 
 |             } | 
 |             if (dims[1] === 'y0') { | 
 |                 pointValue[1] = (clampPointValue0[1] > clampPointValue1[1]) ? pointValue1[1] : pointValue0[1]; | 
 |             } | 
 |             else { | 
 |                 pointValue[1] = (clampPointValue0[1] > clampPointValue1[1]) ? pointValue0[1] : pointValue1[1]; | 
 |             } | 
 |             // Use the getMarkerPosition | 
 |             point = seriesModel.getMarkerPosition( | 
 |                 pointValue, dims, true | 
 |             ); | 
 |         } | 
 |         else { | 
 |             const x = data.get(dims[0], idx) as number; | 
 |             const y = data.get(dims[1], idx) as number; | 
 |             const pt = [x, y]; | 
 |             coordSys.clampData && coordSys.clampData(pt, pt); | 
 |             point = coordSys.dataToPoint(pt, true); | 
 |         } | 
 |         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 x = data.get(dims[0], idx) as number; | 
 |             const y = data.get(dims[1], idx) as number; | 
 |             if (isInfinity(x)) { | 
 |                 point[0] = xAxis.toGlobalCoord(xAxis.getExtent()[dims[0] === 'x0' ? 0 : 1]); | 
 |             } | 
 |             else if (isInfinity(y)) { | 
 |                 point[1] = yAxis.toGlobalCoord(yAxis.getExtent()[dims[1] === 'y0' ? 0 : 1]); | 
 |             } | 
 |         } | 
 |  | 
 |         // Use x, y if has any | 
 |         if (!isNaN(xPx)) { | 
 |             point[0] = xPx; | 
 |         } | 
 |         if (!isNaN(yPx)) { | 
 |             point[1] = yPx; | 
 |         } | 
 |     } | 
 |  | 
 |     return point; | 
 | } | 
 |  | 
 | export const dimPermutations = [['x0', 'y0'], ['x1', 'y0'], ['x1', 'y1'], ['x0', 'y1']] as const; | 
 |  | 
 | class MarkAreaView extends MarkerView { | 
 |  | 
 |     static type = 'markArea'; | 
 |     type = MarkAreaView.type; | 
 |  | 
 |     markerGroupMap: HashMap<MarkAreaDrawGroup>; | 
 |  | 
 |     updateTransform(markAreaModel: MarkAreaModel, ecModel: GlobalModel, api: ExtensionAPI) { | 
 |         ecModel.eachSeries(function (seriesModel) { | 
 |             const maModel = MarkerModel.getMarkerModelFromSeries(seriesModel, 'markArea') as MarkAreaModel; | 
 |             if (maModel) { | 
 |                 const areaData = maModel.getData(); | 
 |                 areaData.each(function (idx) { | 
 |                     const points = map(dimPermutations, function (dim) { | 
 |                         return getSingleMarkerEndPoint(areaData, idx, dim, seriesModel, api); | 
 |                     }); | 
 |                     // Layout | 
 |                     areaData.setItemLayout(idx, points); | 
 |                     const el = areaData.getItemGraphicEl(idx) as graphic.Polygon; | 
 |                     el.setShape('points', points); | 
 |                 }); | 
 |             } | 
 |         }, this); | 
 |     } | 
 |  | 
 |     renderSeries( | 
 |         seriesModel: SeriesModel, | 
 |         maModel: MarkAreaModel, | 
 |         ecModel: GlobalModel, | 
 |         api: ExtensionAPI | 
 |     ) { | 
 |         const coordSys = seriesModel.coordinateSystem; | 
 |         const seriesId = seriesModel.id; | 
 |         const seriesData = seriesModel.getData(); | 
 |  | 
 |         const areaGroupMap = this.markerGroupMap; | 
 |         const polygonGroup = areaGroupMap.get(seriesId) | 
 |             || areaGroupMap.set(seriesId, {group: new graphic.Group()}); | 
 |  | 
 |         this.group.add(polygonGroup.group); | 
 |         this.markKeep(polygonGroup); | 
 |  | 
 |         const areaData = createList(coordSys, seriesModel, maModel); | 
 |  | 
 |         // Line data for tooltip and formatter | 
 |         maModel.setData(areaData); | 
 |  | 
 |         // Update visual and layout of line | 
 |         areaData.each(function (idx) { | 
 |             // Layout | 
 |             const points = map(dimPermutations, function (dim) { | 
 |                 return getSingleMarkerEndPoint(areaData, idx, dim, seriesModel, api); | 
 |             }); | 
 |             const xAxisScale = coordSys.getAxis('x').scale; | 
 |             const yAxisScale = coordSys.getAxis('y').scale; | 
 |             const xAxisExtent = xAxisScale.getExtent(); | 
 |             const yAxisExtent = yAxisScale.getExtent(); | 
 |             const xPointExtent = [xAxisScale.parse(areaData.get('x0', idx)), xAxisScale.parse(areaData.get('x1', idx))]; | 
 |             const yPointExtent = [yAxisScale.parse(areaData.get('y0', idx)), yAxisScale.parse(areaData.get('y1', idx))]; | 
 |             numberUtil.asc(xPointExtent); | 
 |             numberUtil.asc(yPointExtent); | 
 |             const overlapped = !(xAxisExtent[0] > xPointExtent[1] || xAxisExtent[1] < xPointExtent[0] | 
 |                                 || yAxisExtent[0] > yPointExtent[1] || yAxisExtent[1] < yPointExtent[0]); | 
 |             // If none of the area is inside coordSys, allClipped is set to be true | 
 |             // in layout so that label will not be displayed. See #12591 | 
 |             const allClipped = !overlapped; | 
 |             areaData.setItemLayout(idx, { | 
 |                 points: points, | 
 |                 allClipped: allClipped | 
 |             }); | 
 |  | 
 |  | 
 |             const style = areaData.getItemModel<MarkAreaMergedItemOption>(idx).getModel('itemStyle').getItemStyle(); | 
 |             const color = getVisualFromData(seriesData, 'color') as ZRColor; | 
 |             if (!style.fill) { | 
 |                 style.fill = color; | 
 |                 if (isString(style.fill)) { | 
 |                     style.fill = colorUtil.modifyAlpha(style.fill, 0.4); | 
 |                 } | 
 |             } | 
 |             if (!style.stroke) { | 
 |                 style.stroke = color; | 
 |             } | 
 |             // Visual | 
 |             areaData.setItemVisual(idx, 'style', style); | 
 |         }); | 
 |  | 
 |  | 
 |         areaData.diff(inner(polygonGroup).data) | 
 |             .add(function (idx) { | 
 |                 const layout = areaData.getItemLayout(idx); | 
 |                 if (!layout.allClipped) { | 
 |                     const polygon = new graphic.Polygon({ | 
 |                         shape: { | 
 |                             points: layout.points | 
 |                         } | 
 |                     }); | 
 |                     areaData.setItemGraphicEl(idx, polygon); | 
 |                     polygonGroup.group.add(polygon); | 
 |                 } | 
 |             }) | 
 |             .update(function (newIdx, oldIdx) { | 
 |                 let polygon = inner(polygonGroup).data.getItemGraphicEl(oldIdx) as graphic.Polygon; | 
 |                 const layout = areaData.getItemLayout(newIdx); | 
 |                 if (!layout.allClipped) { | 
 |                     if (polygon) { | 
 |                         graphic.updateProps(polygon, { | 
 |                             shape: { | 
 |                                 points: layout.points | 
 |                             } | 
 |                         }, maModel, newIdx); | 
 |                     } | 
 |                     else { | 
 |                         polygon = new graphic.Polygon({ | 
 |                             shape: { | 
 |                                 points: layout.points | 
 |                             } | 
 |                         }); | 
 |                     } | 
 |                     areaData.setItemGraphicEl(newIdx, polygon); | 
 |                     polygonGroup.group.add(polygon); | 
 |                 } | 
 |                 else if (polygon) { | 
 |                     polygonGroup.group.remove(polygon); | 
 |                 } | 
 |             }) | 
 |             .remove(function (idx) { | 
 |                 const polygon = inner(polygonGroup).data.getItemGraphicEl(idx); | 
 |                 polygonGroup.group.remove(polygon); | 
 |             }) | 
 |             .execute(); | 
 |  | 
 |         areaData.eachItemGraphicEl(function (polygon: graphic.Polygon, idx) { | 
 |             const itemModel = areaData.getItemModel<MarkAreaMergedItemOption>(idx); | 
 |             const style = areaData.getItemVisual(idx, 'style'); | 
 |             polygon.useStyle(areaData.getItemVisual(idx, 'style')); | 
 |  | 
 |             setLabelStyle( | 
 |                 polygon, getLabelStatesModels(itemModel), | 
 |                 { | 
 |                     labelFetcher: maModel, | 
 |                     labelDataIndex: idx, | 
 |                     defaultText: areaData.getName(idx) || '', | 
 |                     inheritColor: isString(style.fill) | 
 |                         ? colorUtil.modifyAlpha(style.fill, 1) : '#000' | 
 |                 } | 
 |             ); | 
 |  | 
 |             setStatesStylesFromModel(polygon, itemModel); | 
 |  | 
 |             toggleHoverEmphasis(polygon, null, null, itemModel.get(['emphasis', 'disabled'])); | 
 |  | 
 |             getECData(polygon).dataModel = maModel; | 
 |         }); | 
 |  | 
 |         inner(polygonGroup).data = areaData; | 
 |  | 
 |         polygonGroup.group.silent = maModel.get('silent') || seriesModel.get('silent'); | 
 |     } | 
 | } | 
 |  | 
 | function createList( | 
 |     coordSys: CoordinateSystem, | 
 |     seriesModel: SeriesModel, | 
 |     maModel: MarkAreaModel | 
 | ) { | 
 |  | 
 |     let areaData: SeriesData<MarkAreaModel>; | 
 |     let dataDims: SeriesDimensionDefine[]; | 
 |     const dims = ['x0', 'y0', 'x1', 'y1']; | 
 |     if (coordSys) { | 
 |         const coordDimsInfos: SeriesDimensionDefine[] = map(coordSys && coordSys.dimensions, function (coordDim) { | 
 |             const data = seriesModel.getData(); | 
 |             const info = data.getDimensionInfo( | 
 |                 data.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 | 
 |             }); | 
 |         }); | 
 |         dataDims = map(dims, (dim, idx) => ({ | 
 |             name: dim, | 
 |             type: coordDimsInfos[idx % 2].type | 
 |         })); | 
 |         areaData = new SeriesData(dataDims, maModel); | 
 |     } | 
 |     else { | 
 |         dataDims = [{ | 
 |             name: 'value', | 
 |             type: 'float' | 
 |         }]; | 
 |         areaData = new SeriesData(dataDims, maModel); | 
 |     } | 
 |  | 
 |     let optData = map(maModel.get('data'), curry( | 
 |         markAreaTransform, seriesModel, coordSys, maModel | 
 |     )); | 
 |     if (coordSys) { | 
 |         optData = filter( | 
 |             optData, curry(markAreaFilter, coordSys) | 
 |         ); | 
 |     } | 
 |  | 
 |     const dimValueGetter: markerHelper.MarkerDimValueGetter<MarkAreaMergedItemOption> = coordSys | 
 |         ? function (item, dimName, dataIndex, dimIndex) { | 
 |             // TODO should convert to ParsedValue? | 
 |             const rawVal = item.coord[Math.floor(dimIndex / 2)][dimIndex % 2]; | 
 |             return parseDataValue(rawVal, dataDims[dimIndex]); | 
 |         } | 
 |         : function (item, dimName, dataIndex, dimIndex) { | 
 |             return parseDataValue(item.value, dataDims[dimIndex]); | 
 |         }; | 
 |     areaData.initData(optData, null, dimValueGetter); | 
 |     areaData.hasItemOption = true; | 
 |     return areaData; | 
 | } | 
 |  | 
 | export default MarkAreaView; |