| /* | |
| * 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 Batch by color | |
| import * as graphic from '../../util/graphic'; | |
| import * as lineContain from 'zrender/src/contain/line'; | |
| import * as quadraticContain from 'zrender/src/contain/quadratic'; | |
| import { PathProps } from 'zrender/src/graphic/Path'; | |
| import SeriesData from '../../data/SeriesData'; | |
| import { StageHandlerProgressParams, LineStyleOption, ColorString } from '../../util/types'; | |
| import Model from '../../model/Model'; | |
| import { getECData } from '../../util/innerStore'; | |
| import Element from 'zrender/src/Element'; | |
| class LargeLinesPathShape { | |
| polyline = false; | |
| curveness = 0; | |
| segs: ArrayLike<number> = []; | |
| } | |
| interface LargeLinesPathProps extends PathProps { | |
| shape?: Partial<LargeLinesPathShape> | |
| } | |
| interface LargeLinesCommonOption { | |
| polyline?: boolean | |
| lineStyle?: LineStyleOption & { | |
| curveness?: number | |
| } | |
| } | |
| /** | |
| * Data which can support large lines. | |
| */ | |
| type LargeLinesData = SeriesData<Model<LargeLinesCommonOption> & { | |
| seriesIndex?: number | |
| }>; | |
| class LargeLinesPath extends graphic.Path { | |
| shape: LargeLinesPathShape; | |
| __startIndex: number; | |
| private _off: number = 0; | |
| hoverDataIdx: number = -1; | |
| notClear: boolean; | |
| constructor(opts?: LargeLinesPathProps) { | |
| super(opts); | |
| } | |
| reset() { | |
| this.notClear = false; | |
| this._off = 0; | |
| } | |
| getDefaultStyle() { | |
| return { | |
| stroke: '#000', | |
| fill: null as ColorString | |
| }; | |
| } | |
| getDefaultShape() { | |
| return new LargeLinesPathShape(); | |
| } | |
| buildPath(ctx: CanvasRenderingContext2D, shape: LargeLinesPathShape) { | |
| const segs = shape.segs; | |
| const curveness = shape.curveness; | |
| let i; | |
| if (shape.polyline) { | |
| for (i = this._off; i < segs.length;) { | |
| const count = segs[i++]; | |
| if (count > 0) { | |
| ctx.moveTo(segs[i++], segs[i++]); | |
| for (let k = 1; k < count; k++) { | |
| ctx.lineTo(segs[i++], segs[i++]); | |
| } | |
| } | |
| } | |
| } | |
| else { | |
| for (i = this._off; i < segs.length;) { | |
| const x0 = segs[i++]; | |
| const y0 = segs[i++]; | |
| const x1 = segs[i++]; | |
| const y1 = segs[i++]; | |
| ctx.moveTo(x0, y0); | |
| if (curveness > 0) { | |
| const x2 = (x0 + x1) / 2 - (y0 - y1) * curveness; | |
| const y2 = (y0 + y1) / 2 - (x1 - x0) * curveness; | |
| ctx.quadraticCurveTo(x2, y2, x1, y1); | |
| } | |
| else { | |
| ctx.lineTo(x1, y1); | |
| } | |
| } | |
| } | |
| if (this.incremental) { | |
| this._off = i; | |
| this.notClear = true; | |
| } | |
| } | |
| findDataIndex(x: number, y: number) { | |
| const shape = this.shape; | |
| const segs = shape.segs; | |
| const curveness = shape.curveness; | |
| const lineWidth = this.style.lineWidth; | |
| if (shape.polyline) { | |
| let dataIndex = 0; | |
| for (let i = 0; i < segs.length;) { | |
| const count = segs[i++]; | |
| if (count > 0) { | |
| const x0 = segs[i++]; | |
| const y0 = segs[i++]; | |
| for (let k = 1; k < count; k++) { | |
| const x1 = segs[i++]; | |
| const y1 = segs[i++]; | |
| if (lineContain.containStroke(x0, y0, x1, y1, lineWidth, x, y)) { | |
| return dataIndex; | |
| } | |
| } | |
| } | |
| dataIndex++; | |
| } | |
| } | |
| else { | |
| let dataIndex = 0; | |
| for (let i = 0; i < segs.length;) { | |
| const x0 = segs[i++]; | |
| const y0 = segs[i++]; | |
| const x1 = segs[i++]; | |
| const y1 = segs[i++]; | |
| if (curveness > 0) { | |
| const x2 = (x0 + x1) / 2 - (y0 - y1) * curveness; | |
| const y2 = (y0 + y1) / 2 - (x1 - x0) * curveness; | |
| if (quadraticContain.containStroke( | |
| x0, y0, x2, y2, x1, y1, lineWidth, x, y | |
| )) { | |
| return dataIndex; | |
| } | |
| } | |
| else { | |
| if (lineContain.containStroke( | |
| x0, y0, x1, y1, lineWidth, x, y | |
| )) { | |
| return dataIndex; | |
| } | |
| } | |
| dataIndex++; | |
| } | |
| } | |
| 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.segs; | |
| 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, minY, maxX, maxY); | |
| } | |
| return rect; | |
| } | |
| } | |
| class LargeLineDraw { | |
| group = new graphic.Group(); | |
| private _newAdded: LargeLinesPath[]; | |
| /** | |
| * Update symbols draw by new data | |
| */ | |
| updateData(data: LargeLinesData) { | |
| this._clear(); | |
| const lineEl = this._create(); | |
| lineEl.setShape({ | |
| segs: data.getLayout('linesPoints') | |
| }); | |
| this._setCommon(lineEl, data); | |
| }; | |
| /** | |
| * @override | |
| */ | |
| incrementalPrepareUpdate(data: LargeLinesData) { | |
| this.group.removeAll(); | |
| this._clear(); | |
| }; | |
| /** | |
| * @override | |
| */ | |
| incrementalUpdate(taskParams: StageHandlerProgressParams, data: LargeLinesData) { | |
| const lastAdded = this._newAdded[0]; | |
| const linePoints = data.getLayout('linesPoints'); | |
| const oldSegs = lastAdded && lastAdded.shape.segs; | |
| // 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 (oldSegs && oldSegs.length < 2e4) { | |
| const oldLen = oldSegs.length; | |
| const newSegs = new Float32Array(oldLen + linePoints.length); | |
| // Concat two array | |
| newSegs.set(oldSegs); | |
| newSegs.set(linePoints, oldLen); | |
| lastAdded.setShape({ | |
| segs: newSegs | |
| }); | |
| } | |
| else { | |
| // Clear | |
| this._newAdded = []; | |
| const lineEl = this._create(); | |
| lineEl.incremental = true; | |
| lineEl.setShape({ | |
| segs: linePoints | |
| }); | |
| this._setCommon(lineEl, data); | |
| lineEl.__startIndex = taskParams.start; | |
| } | |
| } | |
| /** | |
| * @override | |
| */ | |
| remove() { | |
| this._clear(); | |
| } | |
| eachRendered(cb: (el: Element) => boolean | void) { | |
| this._newAdded[0] && cb(this._newAdded[0]); | |
| } | |
| private _create() { | |
| const lineEl = new LargeLinesPath({ | |
| cursor: 'default', | |
| ignoreCoarsePointer: true | |
| }); | |
| this._newAdded.push(lineEl); | |
| this.group.add(lineEl); | |
| return lineEl; | |
| } | |
| private _setCommon(lineEl: LargeLinesPath, data: LargeLinesData, isIncremental?: boolean) { | |
| const hostModel = data.hostModel; | |
| lineEl.setShape({ | |
| polyline: hostModel.get('polyline'), | |
| curveness: hostModel.get(['lineStyle', 'curveness']) | |
| }); | |
| lineEl.useStyle( | |
| hostModel.getModel('lineStyle').getLineStyle() | |
| ); | |
| lineEl.style.strokeNoScale = true; | |
| const style = data.getVisual('style'); | |
| if (style && style.stroke) { | |
| lineEl.setStyle('stroke', style.stroke); | |
| } | |
| lineEl.setStyle('fill', null); | |
| const ecData = getECData(lineEl); | |
| // Enable tooltip | |
| // PENDING May have performance issue when path is extremely large | |
| ecData.seriesIndex = hostModel.seriesIndex; | |
| lineEl.on('mousemove', function (e) { | |
| ecData.dataIndex = null; | |
| const dataIndex = lineEl.hoverDataIdx; | |
| if (dataIndex > 0) { | |
| // Provide dataIndex for tooltip | |
| ecData.dataIndex = dataIndex + lineEl.__startIndex; | |
| } | |
| }); | |
| }; | |
| private _clear() { | |
| this._newAdded = []; | |
| this.group.removeAll(); | |
| }; | |
| } | |
| export default LargeLineDraw; |