/* * 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 = []; } interface LargeLinesPathProps extends PathProps { shape?: Partial } interface LargeLinesCommonOption { polyline?: boolean lineStyle?: LineStyleOption & { curveness?: number } } /** * Data which can support large lines. */ type LargeLinesData = SeriesData & { 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;