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