| /* | |
| * 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. | |
| */ | |
| /* global Float32Array */ | |
| // TODO Batch by color | |
| import * as graphic from '../../util/graphic'; | |
| import {createSymbol} from '../../util/symbol'; | |
| import SeriesData from '../../data/SeriesData'; | |
| import { PathProps } from 'zrender/src/graphic/Path'; | |
| import PathProxy from 'zrender/src/core/PathProxy'; | |
| import SeriesModel from '../../model/Series'; | |
| import { StageHandlerProgressParams } from '../../util/types'; | |
| import { CoordinateSystemClipArea } from '../../coord/CoordinateSystem'; | |
| import { getECData } from '../../util/innerStore'; | |
| import Element from 'zrender/src/Element'; | |
| const BOOST_SIZE_THRESHOLD = 4; | |
| class LargeSymbolPathShape { | |
| points: ArrayLike<number>; | |
| size: number[]; | |
| } | |
| type LargeSymbolPathProps = PathProps & { | |
| shape?: Partial<LargeSymbolPathShape> | |
| startIndex?: number | |
| endIndex?: number | |
| }; | |
| type ECSymbol = ReturnType<typeof createSymbol>; | |
| class LargeSymbolPath extends graphic.Path<LargeSymbolPathProps> { | |
| shape: LargeSymbolPathShape; | |
| symbolProxy: ECSymbol; | |
| softClipShape: CoordinateSystemClipArea; | |
| startIndex: number; | |
| endIndex: number; | |
| private _ctx: CanvasRenderingContext2D; | |
| private _off: number = 0; | |
| hoverDataIdx: number = -1; | |
| notClear: boolean; | |
| constructor(opts?: LargeSymbolPathProps) { | |
| super(opts); | |
| } | |
| getDefaultShape() { | |
| return new LargeSymbolPathShape(); | |
| } | |
| setColor: ECSymbol['setColor']; | |
| reset() { | |
| this.notClear = false; | |
| this._off = 0; | |
| } | |
| buildPath(path: PathProxy | CanvasRenderingContext2D, shape: LargeSymbolPathShape) { | |
| const points = shape.points; | |
| const size = shape.size; | |
| const symbolProxy = this.symbolProxy; | |
| const symbolProxyShape = symbolProxy.shape; | |
| const ctx = (path as PathProxy).getContext | |
| ? (path as PathProxy).getContext() | |
| : path as CanvasRenderingContext2D; | |
| const canBoost = ctx && size[0] < BOOST_SIZE_THRESHOLD; | |
| const softClipShape = this.softClipShape; | |
| let i; | |
| // Do draw in afterBrush. | |
| if (canBoost) { | |
| this._ctx = ctx; | |
| return; | |
| } | |
| this._ctx = null; | |
| for (i = this._off; i < points.length;) { | |
| const x = points[i++]; | |
| const y = points[i++]; | |
| if (isNaN(x) || isNaN(y)) { | |
| continue; | |
| } | |
| if (softClipShape && !softClipShape.contain(x, y)) { | |
| continue; | |
| } | |
| symbolProxyShape.x = x - size[0] / 2; | |
| symbolProxyShape.y = y - size[1] / 2; | |
| symbolProxyShape.width = size[0]; | |
| symbolProxyShape.height = size[1]; | |
| symbolProxy.buildPath(path, symbolProxyShape, true); | |
| } | |
| if (this.incremental) { | |
| this._off = i; | |
| this.notClear = true; | |
| } | |
| } | |
| afterBrush() { | |
| const shape = this.shape; | |
| const points = shape.points; | |
| const size = shape.size; | |
| const ctx = this._ctx; | |
| const softClipShape = this.softClipShape; | |
| let i; | |
| if (!ctx) { | |
| return; | |
| } | |
| // PENDING If style or other canvas status changed? | |
| for (i = this._off; i < points.length;) { | |
| const x = points[i++]; | |
| const y = points[i++]; | |
| if (isNaN(x) || isNaN(y)) { | |
| continue; | |
| } | |
| if (softClipShape && !softClipShape.contain(x, y)) { | |
| continue; | |
| } | |
| // fillRect is faster than building a rect path and draw. | |
| // And it support light globalCompositeOperation. | |
| ctx.fillRect( | |
| x - size[0] / 2, y - size[1] / 2, | |
| size[0], size[1] | |
| ); | |
| } | |
| if (this.incremental) { | |
| this._off = i; | |
| this.notClear = true; | |
| } | |
| } | |
| findDataIndex(x: number, y: number) { | |
| // TODO ??? | |
| // Consider transform | |
| const shape = this.shape; | |
| const points = shape.points; | |
| const size = shape.size; | |
| const w = Math.max(size[0], 4); | |
| const h = Math.max(size[1], 4); | |
| // Not consider transform | |
| // Treat each element as a rect | |
| // top down traverse | |
| for (let idx = points.length / 2 - 1; idx >= 0; idx--) { | |
| const i = idx * 2; | |
| const x0 = points[i] - w / 2; | |
| const y0 = points[i + 1] - h / 2; | |
| if (x >= x0 && y >= y0 && x <= x0 + w && y <= y0 + h) { | |
| return idx; | |
| } | |
| } | |
| return -1; | |
| } | |
| contain(x: number, y: number): boolean { | |
| const localPos = this.transformCoordToLocal(x, y); | |
| const rect = this.getBoundingRect(); | |
| x = localPos[0]; | |
| y = localPos[1]; | |
| if (rect.contain(x, y)) { | |
| // Cache found data index. | |
| const dataIdx = this.hoverDataIdx = this.findDataIndex(x, y); | |
| return dataIdx >= 0; | |
| } | |
| this.hoverDataIdx = -1; | |
| return false; | |
| } | |
| getBoundingRect() { | |
| // Ignore stroke for large symbol draw. | |
| let rect = this._rect; | |
| if (!rect) { | |
| const shape = this.shape; | |
| const points = shape.points; | |
| const size = shape.size; | |
| const w = size[0]; | |
| const h = size[1]; | |
| let minX = Infinity; | |
| let minY = Infinity; | |
| let maxX = -Infinity; | |
| let maxY = -Infinity; | |
| for (let i = 0; i < points.length;) { | |
| const x = points[i++]; | |
| const y = points[i++]; | |
| minX = Math.min(x, minX); | |
| maxX = Math.max(x, maxX); | |
| minY = Math.min(y, minY); | |
| maxY = Math.max(y, maxY); | |
| } | |
| rect = this._rect = new graphic.BoundingRect( | |
| minX - w / 2, | |
| minY - h / 2, | |
| maxX - minX + w, | |
| maxY - minY + h | |
| ); | |
| } | |
| return rect; | |
| } | |
| } | |
| interface UpdateOpt { | |
| clipShape?: CoordinateSystemClipArea | |
| } | |
| class LargeSymbolDraw { | |
| group = new graphic.Group(); | |
| // New add element in this frame of progressive render. | |
| private _newAdded: LargeSymbolPath[]; | |
| /** | |
| * Update symbols draw by new data | |
| */ | |
| updateData(data: SeriesData, opt?: UpdateOpt) { | |
| this._clear(); | |
| const symbolEl = this._create(); | |
| symbolEl.setShape({ | |
| points: data.getLayout('points') | |
| }); | |
| this._setCommon(symbolEl, data, opt); | |
| } | |
| updateLayout(data: SeriesData) { | |
| let points = data.getLayout('points'); | |
| this.group.eachChild(function (child: LargeSymbolPath) { | |
| if (child.startIndex != null) { | |
| const len = (child.endIndex - child.startIndex) * 2; | |
| const byteOffset = child.startIndex * 4 * 2; | |
| points = new Float32Array(points.buffer, byteOffset, len); | |
| } | |
| child.setShape('points', points); | |
| // Reset draw cursor. | |
| child.reset(); | |
| }); | |
| } | |
| incrementalPrepareUpdate(data: SeriesData) { | |
| this._clear(); | |
| } | |
| incrementalUpdate(taskParams: StageHandlerProgressParams, data: SeriesData, opt: UpdateOpt) { | |
| const lastAdded = this._newAdded[0]; | |
| const points = data.getLayout('points'); | |
| const oldPoints = lastAdded && lastAdded.shape.points; | |
| // Merging the exists. Each element has 1e4 points. | |
| // Consider the performance balance between too much elements and too much points in one shape(may affect hover optimization) | |
| if (oldPoints && oldPoints.length < 2e4) { | |
| const oldLen = oldPoints.length; | |
| const newPoints = new Float32Array(oldLen + points.length); | |
| // Concat two array | |
| newPoints.set(oldPoints); | |
| newPoints.set(points, oldLen); | |
| // Update endIndex | |
| lastAdded.endIndex = taskParams.end; | |
| lastAdded.setShape({ points: newPoints }); | |
| } | |
| else { | |
| // Clear | |
| this._newAdded = []; | |
| const symbolEl = this._create(); | |
| symbolEl.startIndex = taskParams.start; | |
| symbolEl.endIndex = taskParams.end; | |
| symbolEl.incremental = true; | |
| symbolEl.setShape({ | |
| points | |
| }); | |
| this._setCommon(symbolEl, data, opt); | |
| } | |
| } | |
| eachRendered(cb: (el: Element) => boolean | void) { | |
| this._newAdded[0] && cb(this._newAdded[0]); | |
| } | |
| private _create() { | |
| const symbolEl = new LargeSymbolPath({ | |
| cursor: 'default' | |
| }); | |
| symbolEl.ignoreCoarsePointer = true; | |
| this.group.add(symbolEl); | |
| this._newAdded.push(symbolEl); | |
| return symbolEl; | |
| } | |
| private _setCommon( | |
| symbolEl: LargeSymbolPath, | |
| data: SeriesData, | |
| opt: UpdateOpt | |
| ) { | |
| const hostModel = data.hostModel; | |
| opt = opt || {}; | |
| const size = data.getVisual('symbolSize'); | |
| symbolEl.setShape('size', (size instanceof Array) ? size : [size, size]); | |
| symbolEl.softClipShape = opt.clipShape || null; | |
| // Create symbolProxy to build path for each data | |
| symbolEl.symbolProxy = createSymbol( | |
| data.getVisual('symbol'), 0, 0, 0, 0 | |
| ); | |
| // Use symbolProxy setColor method | |
| symbolEl.setColor = symbolEl.symbolProxy.setColor; | |
| const extrudeShadow = symbolEl.shape.size[0] < BOOST_SIZE_THRESHOLD; | |
| symbolEl.useStyle( | |
| // Draw shadow when doing fillRect is extremely slow. | |
| hostModel.getModel('itemStyle').getItemStyle( | |
| extrudeShadow ? ['color', 'shadowBlur', 'shadowColor'] : ['color'] | |
| ) | |
| ); | |
| const globalStyle = data.getVisual('style'); | |
| const visualColor = globalStyle && globalStyle.fill; | |
| if (visualColor) { | |
| symbolEl.setColor(visualColor); | |
| } | |
| const ecData = getECData(symbolEl); | |
| // Enable tooltip | |
| // PENDING May have performance issue when path is extremely large | |
| ecData.seriesIndex = (hostModel as SeriesModel).seriesIndex; | |
| symbolEl.on('mousemove', function (e) { | |
| ecData.dataIndex = null; | |
| const dataIndex = symbolEl.hoverDataIdx; | |
| if (dataIndex >= 0) { | |
| // Provide dataIndex for tooltip | |
| ecData.dataIndex = dataIndex + (symbolEl.startIndex || 0); | |
| } | |
| }); | |
| } | |
| remove() { | |
| this._clear(); | |
| } | |
| private _clear() { | |
| this._newAdded = []; | |
| this.group.removeAll(); | |
| } | |
| } | |
| export default LargeSymbolDraw; |