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