|  | /* | 
|  | * 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; |