| /* | 
 | * 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 * as graphic from '../../util/graphic'; | 
 | import { toggleHoverEmphasis } from '../../util/states'; | 
 | import HeatmapLayer from './HeatmapLayer'; | 
 | import * as zrUtil from 'zrender/src/core/util'; | 
 | import ChartView from '../../view/Chart'; | 
 | import HeatmapSeriesModel, { HeatmapDataItemOption } from './HeatmapSeries'; | 
 | import type GlobalModel from '../../model/Global'; | 
 | import type ExtensionAPI from '../../core/ExtensionAPI'; | 
 | import type VisualMapModel from '../../component/visualMap/VisualMapModel'; | 
 | import type PiecewiseModel from '../../component/visualMap/PiecewiseModel'; | 
 | import type ContinuousModel from '../../component/visualMap/ContinuousModel'; | 
 | import { CoordinateSystem, isCoordinateSystemType } from '../../coord/CoordinateSystem'; | 
 | import { StageHandlerProgressParams, Dictionary, OptionDataValue } from '../../util/types'; | 
 | import type Cartesian2D from '../../coord/cartesian/Cartesian2D'; | 
 | import type Calendar from '../../coord/calendar/Calendar'; | 
 | import { setLabelStyle, getLabelStatesModels } from '../../label/labelStyle'; | 
 | import type Element from 'zrender/src/Element'; | 
 |  | 
 | // Coord can be 'geo' 'bmap' 'amap' 'leaflet'... | 
 | interface GeoLikeCoordSys extends CoordinateSystem { | 
 |     dimensions: ['lng', 'lat'] | 
 |     getViewRect(): graphic.BoundingRect | 
 | } | 
 |  | 
 | function getIsInPiecewiseRange( | 
 |     dataExtent: number[], | 
 |     pieceList: ReturnType<PiecewiseModel['getPieceList']>, | 
 |     selected: Dictionary<boolean> | 
 | ) { | 
 |     const dataSpan = dataExtent[1] - dataExtent[0]; | 
 |     pieceList = zrUtil.map(pieceList, function (piece) { | 
 |         return { | 
 |             interval: [ | 
 |                 (piece.interval[0] - dataExtent[0]) / dataSpan, | 
 |                 (piece.interval[1] - dataExtent[0]) / dataSpan | 
 |             ] | 
 |         }; | 
 |     }); | 
 |     const len = pieceList.length; | 
 |     let lastIndex = 0; | 
 |  | 
 |     return function (val: number) { | 
 |         let i; | 
 |         // Try to find in the location of the last found | 
 |         for (i = lastIndex; i < len; i++) { | 
 |             const interval = pieceList[i].interval; | 
 |             if (interval[0] <= val && val <= interval[1]) { | 
 |                 lastIndex = i; | 
 |                 break; | 
 |             } | 
 |         } | 
 |         if (i === len) { // Not found, back interation | 
 |             for (i = lastIndex - 1; i >= 0; i--) { | 
 |                 const interval = pieceList[i].interval; | 
 |                 if (interval[0] <= val && val <= interval[1]) { | 
 |                     lastIndex = i; | 
 |                     break; | 
 |                 } | 
 |             } | 
 |         } | 
 |         return i >= 0 && i < len && selected[i]; | 
 |     }; | 
 | } | 
 |  | 
 | function getIsInContinuousRange(dataExtent: number[], range: number[]) { | 
 |     const dataSpan = dataExtent[1] - dataExtent[0]; | 
 |     range = [ | 
 |         (range[0] - dataExtent[0]) / dataSpan, | 
 |         (range[1] - dataExtent[0]) / dataSpan | 
 |     ]; | 
 |     return function (val: number) { | 
 |         return val >= range[0] && val <= range[1]; | 
 |     }; | 
 | } | 
 |  | 
 | function isGeoCoordSys(coordSys: CoordinateSystem): coordSys is GeoLikeCoordSys { | 
 |     const dimensions = coordSys.dimensions; | 
 |     // Not use coordSys.type === 'geo' because coordSys maybe extended | 
 |     return dimensions[0] === 'lng' && dimensions[1] === 'lat'; | 
 | } | 
 |  | 
 | class HeatmapView extends ChartView { | 
 |  | 
 |     static readonly type = 'heatmap'; | 
 |     readonly type = HeatmapView.type; | 
 |  | 
 |     private _hmLayer: HeatmapLayer; | 
 |  | 
 |     private _progressiveEls: Element[]; | 
 |  | 
 |     render(seriesModel: HeatmapSeriesModel, ecModel: GlobalModel, api: ExtensionAPI) { | 
 |         let visualMapOfThisSeries; | 
 |         ecModel.eachComponent('visualMap', function (visualMap: VisualMapModel) { | 
 |             visualMap.eachTargetSeries(function (targetSeries) { | 
 |                 if (targetSeries === seriesModel) { | 
 |                     visualMapOfThisSeries = visualMap; | 
 |                 } | 
 |             }); | 
 |         }); | 
 |  | 
 |         if (__DEV__) { | 
 |             if (!visualMapOfThisSeries) { | 
 |                 throw new Error('Heatmap must use with visualMap'); | 
 |             } | 
 |         } | 
 |  | 
 |         // Clear previously rendered progressive elements. | 
 |         this._progressiveEls = null; | 
 |  | 
 |         this.group.removeAll(); | 
 |  | 
 |         const coordSys = seriesModel.coordinateSystem; | 
 |         if (coordSys.type === 'cartesian2d' || coordSys.type === 'calendar') { | 
 |             this._renderOnCartesianAndCalendar(seriesModel, api, 0, seriesModel.getData().count()); | 
 |         } | 
 |         else if (isGeoCoordSys(coordSys)) { | 
 |             this._renderOnGeo( | 
 |                 coordSys, seriesModel, visualMapOfThisSeries, api | 
 |             ); | 
 |         } | 
 |     } | 
 |  | 
 |     incrementalPrepareRender(seriesModel: HeatmapSeriesModel, ecModel: GlobalModel, api: ExtensionAPI) { | 
 |         this.group.removeAll(); | 
 |     } | 
 |  | 
 |     incrementalRender( | 
 |         params: StageHandlerProgressParams, | 
 |         seriesModel: HeatmapSeriesModel, | 
 |         ecModel: GlobalModel, | 
 |         api: ExtensionAPI | 
 |     ) { | 
 |         const coordSys = seriesModel.coordinateSystem; | 
 |         if (coordSys) { | 
 |             // geo does not support incremental rendering? | 
 |             if (isGeoCoordSys(coordSys)) { | 
 |                 this.render(seriesModel, ecModel, api); | 
 |             } | 
 |             else { | 
 |                 this._progressiveEls = []; | 
 |                 this._renderOnCartesianAndCalendar(seriesModel, api, params.start, params.end, true); | 
 |             } | 
 |         } | 
 |     } | 
 |  | 
 |     eachRendered(cb: (el: Element) => boolean | void) { | 
 |         graphic.traverseElements(this._progressiveEls || this.group, cb); | 
 |     } | 
 |  | 
 |     _renderOnCartesianAndCalendar( | 
 |         seriesModel: HeatmapSeriesModel, | 
 |         api: ExtensionAPI, | 
 |         start: number, | 
 |         end: number, | 
 |         incremental?: boolean | 
 |     ) { | 
 |  | 
 |         const coordSys = seriesModel.coordinateSystem as Cartesian2D | Calendar; | 
 |         const isCartesian2d = isCoordinateSystemType<Cartesian2D>(coordSys, 'cartesian2d'); | 
 |         let width; | 
 |         let height; | 
 |         let xAxisExtent; | 
 |         let yAxisExtent; | 
 |  | 
 |         if (isCartesian2d) { | 
 |             const xAxis = coordSys.getAxis('x'); | 
 |             const yAxis = coordSys.getAxis('y'); | 
 |  | 
 |             if (__DEV__) { | 
 |                 if (!(xAxis.type === 'category' && yAxis.type === 'category')) { | 
 |                     throw new Error('Heatmap on cartesian must have two category axes'); | 
 |                 } | 
 |                 if (!(xAxis.onBand && yAxis.onBand)) { | 
 |                     throw new Error('Heatmap on cartesian must have two axes with boundaryGap true'); | 
 |                 } | 
 |             } | 
 |  | 
 |             // add 0.5px to avoid the gaps | 
 |             width = xAxis.getBandWidth() + .5; | 
 |             height = yAxis.getBandWidth() + .5; | 
 |             xAxisExtent = xAxis.scale.getExtent(); | 
 |             yAxisExtent = yAxis.scale.getExtent(); | 
 |         } | 
 |  | 
 |         const group = this.group; | 
 |         const data = seriesModel.getData(); | 
 |  | 
 |         let emphasisStyle = seriesModel.getModel(['emphasis', 'itemStyle']).getItemStyle(); | 
 |         let blurStyle = seriesModel.getModel(['blur', 'itemStyle']).getItemStyle(); | 
 |         let selectStyle = seriesModel.getModel(['select', 'itemStyle']).getItemStyle(); | 
 |         let borderRadius = seriesModel.get(['itemStyle', 'borderRadius']); | 
 |         let labelStatesModels = getLabelStatesModels(seriesModel); | 
 |         const emphasisModel = seriesModel.getModel('emphasis'); | 
 |         let focus = emphasisModel.get('focus'); | 
 |         let blurScope = emphasisModel.get('blurScope'); | 
 |         let emphasisDisabled = emphasisModel.get('disabled'); | 
 |  | 
 |         const dataDims = isCartesian2d | 
 |             ? [ | 
 |                 data.mapDimension('x'), | 
 |                 data.mapDimension('y'), | 
 |                 data.mapDimension('value') | 
 |             ] | 
 |             : [ | 
 |                 data.mapDimension('time'), | 
 |                 data.mapDimension('value') | 
 |             ]; | 
 |  | 
 |         for (let idx = start; idx < end; idx++) { | 
 |             let rect; | 
 |             const style = data.getItemVisual(idx, 'style'); | 
 |  | 
 |             if (isCartesian2d) { | 
 |                 const dataDimX = data.get(dataDims[0], idx); | 
 |                 const dataDimY = data.get(dataDims[1], idx); | 
 |  | 
 |                 // Ignore empty data and out of extent data | 
 |                 if (isNaN(data.get(dataDims[2], idx) as number) | 
 |                     || isNaN(dataDimX as number) | 
 |                     || isNaN(dataDimY as number) | 
 |                     || dataDimX < xAxisExtent[0] | 
 |                     || dataDimX > xAxisExtent[1] | 
 |                     || dataDimY < yAxisExtent[0] | 
 |                     || dataDimY > yAxisExtent[1] | 
 |                 ) { | 
 |                     continue; | 
 |                 } | 
 |  | 
 |                 const point = coordSys.dataToPoint([ | 
 |                     dataDimX, | 
 |                     dataDimY | 
 |                 ]); | 
 |  | 
 |                 rect = new graphic.Rect({ | 
 |                     shape: { | 
 |                         x: point[0] - width / 2, | 
 |                         y: point[1] - height / 2, | 
 |                         width, | 
 |                         height | 
 |                     }, | 
 |                     style | 
 |                 }); | 
 |             } | 
 |             else { | 
 |                 // Ignore empty data | 
 |                 if (isNaN(data.get(dataDims[1], idx) as number)) { | 
 |                     continue; | 
 |                 } | 
 |  | 
 |                 rect = new graphic.Rect({ | 
 |                     z2: 1, | 
 |                     shape: coordSys.dataToRect([data.get(dataDims[0], idx)]).contentShape, | 
 |                     style | 
 |                 }); | 
 |             } | 
 |  | 
 |             // Optimization for large dataset | 
 |             if (data.hasItemOption) { | 
 |                 const itemModel = data.getItemModel<HeatmapDataItemOption>(idx); | 
 |                 const emphasisModel = itemModel.getModel('emphasis'); | 
 |                 emphasisStyle = emphasisModel.getModel('itemStyle').getItemStyle(); | 
 |                 blurStyle = itemModel.getModel(['blur', 'itemStyle']).getItemStyle(); | 
 |                 selectStyle = itemModel.getModel(['select', 'itemStyle']).getItemStyle(); | 
 |  | 
 |                 // Each item value struct in the data would be firstly | 
 |                 // { | 
 |                 //     itemStyle: { borderRadius: [30, 30] }, | 
 |                 //     value: [2022, 02, 22] | 
 |                 // } | 
 |                 borderRadius = itemModel.get(['itemStyle', 'borderRadius']); | 
 |  | 
 |                 focus = emphasisModel.get('focus'); | 
 |                 blurScope = emphasisModel.get('blurScope'); | 
 |                 emphasisDisabled = emphasisModel.get('disabled'); | 
 |  | 
 |                 labelStatesModels = getLabelStatesModels(itemModel); | 
 |             } | 
 |  | 
 |             rect.shape.r = borderRadius; | 
 |  | 
 |             const rawValue = seriesModel.getRawValue(idx) as OptionDataValue[]; | 
 |             let defaultText = '-'; | 
 |             if (rawValue && rawValue[2] != null) { | 
 |                 defaultText = rawValue[2] + ''; | 
 |             } | 
 |  | 
 |             setLabelStyle( | 
 |                 rect, labelStatesModels, | 
 |                 { | 
 |                     labelFetcher: seriesModel, | 
 |                     labelDataIndex: idx, | 
 |                     defaultOpacity: style.opacity, | 
 |                     defaultText: defaultText | 
 |                 } | 
 |             ); | 
 |  | 
 |             rect.ensureState('emphasis').style = emphasisStyle; | 
 |             rect.ensureState('blur').style = blurStyle; | 
 |             rect.ensureState('select').style = selectStyle; | 
 |  | 
 |             toggleHoverEmphasis(rect, focus, blurScope, emphasisDisabled); | 
 |  | 
 |             rect.incremental = incremental; | 
 |             // PENDING | 
 |             if (incremental) { | 
 |                 // Rect must use hover layer if it's incremental. | 
 |                 rect.states.emphasis.hoverLayer = true; | 
 |             } | 
 |  | 
 |             group.add(rect); | 
 |             data.setItemGraphicEl(idx, rect); | 
 |  | 
 |             if (this._progressiveEls) { | 
 |                 this._progressiveEls.push(rect); | 
 |             } | 
 |         } | 
 |     } | 
 |  | 
 |     _renderOnGeo( | 
 |         geo: GeoLikeCoordSys, | 
 |         seriesModel: HeatmapSeriesModel, | 
 |         visualMapModel: VisualMapModel, | 
 |         api: ExtensionAPI | 
 |     ) { | 
 |         const inRangeVisuals = visualMapModel.targetVisuals.inRange; | 
 |         const outOfRangeVisuals = visualMapModel.targetVisuals.outOfRange; | 
 |         // if (!visualMapping) { | 
 |         //     throw new Error('Data range must have color visuals'); | 
 |         // } | 
 |  | 
 |         const data = seriesModel.getData(); | 
 |         const hmLayer = this._hmLayer || (this._hmLayer || new HeatmapLayer()); | 
 |         hmLayer.blurSize = seriesModel.get('blurSize'); | 
 |         hmLayer.pointSize = seriesModel.get('pointSize'); | 
 |         hmLayer.minOpacity = seriesModel.get('minOpacity'); | 
 |         hmLayer.maxOpacity = seriesModel.get('maxOpacity'); | 
 |  | 
 |         const rect = geo.getViewRect().clone(); | 
 |         const roamTransform = geo.getRoamTransform(); | 
 |         rect.applyTransform(roamTransform); | 
 |  | 
 |         // Clamp on viewport | 
 |         const x = Math.max(rect.x, 0); | 
 |         const y = Math.max(rect.y, 0); | 
 |         const x2 = Math.min(rect.width + rect.x, api.getWidth()); | 
 |         const y2 = Math.min(rect.height + rect.y, api.getHeight()); | 
 |         const width = x2 - x; | 
 |         const height = y2 - y; | 
 |  | 
 |         const dims = [ | 
 |             data.mapDimension('lng'), | 
 |             data.mapDimension('lat'), | 
 |             data.mapDimension('value') | 
 |         ]; | 
 |  | 
 |         const points = data.mapArray(dims, function (lng: number, lat: number, value: number) { | 
 |             const pt = geo.dataToPoint([lng, lat]); | 
 |             pt[0] -= x; | 
 |             pt[1] -= y; | 
 |             pt.push(value); | 
 |             return pt; | 
 |         }); | 
 |  | 
 |         const dataExtent = visualMapModel.getExtent(); | 
 |         const isInRange = visualMapModel.type === 'visualMap.continuous' | 
 |             ? getIsInContinuousRange(dataExtent, (visualMapModel as ContinuousModel).option.range) | 
 |             : getIsInPiecewiseRange( | 
 |                 dataExtent, | 
 |                 (visualMapModel as PiecewiseModel).getPieceList(), | 
 |                 (visualMapModel as PiecewiseModel).option.selected | 
 |             ); | 
 |  | 
 |         hmLayer.update( | 
 |             points, width, height, | 
 |             inRangeVisuals.color.getNormalizer(), | 
 |             { | 
 |                 inRange: inRangeVisuals.color.getColorMapper(), | 
 |                 outOfRange: outOfRangeVisuals.color.getColorMapper() | 
 |             }, | 
 |             isInRange | 
 |         ); | 
 |         const img = new graphic.Image({ | 
 |             style: { | 
 |                 width: width, | 
 |                 height: height, | 
 |                 x: x, | 
 |                 y: y, | 
 |                 image: hmLayer.canvas | 
 |             }, | 
 |             silent: true | 
 |         }); | 
 |         this.group.add(img); | 
 |     } | 
 | } | 
 |  | 
 | export default HeatmapView; |