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