This is an automated email from the ASF dual-hosted git repository. shenyi pushed a commit to branch line-optimize in repository https://gitbox.apache.org/repos/asf/incubator-echarts.git
commit 71a590b4b84d9dd34163f5af3cd4b3848ee78168 Author: pissang <[email protected]> AuthorDate: Wed Sep 16 23:35:10 2020 +0800 line: optimize memory cost and initialize time for the large line. --- src/chart/helper/LargeSymbolDraw.ts | 6 +- src/chart/helper/SymbolDraw.ts | 18 +- src/chart/line.ts | 2 +- src/chart/line/LineView.ts | 128 ++++++++------ src/chart/line/helper.ts | 2 +- src/chart/line/lineAnimationDiff.ts | 126 ++++++++------ src/chart/line/poly.ts | 302 +++++++++++----------------------- src/layout/points.ts | 12 +- src/{chart/line.ts => util/vendor.ts} | 27 ++- test/largeLine.html | 30 ++-- 10 files changed, 304 insertions(+), 349 deletions(-) diff --git a/src/chart/helper/LargeSymbolDraw.ts b/src/chart/helper/LargeSymbolDraw.ts index 381beea..432df1f 100644 --- a/src/chart/helper/LargeSymbolDraw.ts +++ b/src/chart/helper/LargeSymbolDraw.ts @@ -190,7 +190,7 @@ class LargeSymbolDraw { }); symbolEl.setShape({ - points: data.getLayout('symbolPoints') + points: data.getLayout('points') }); this._setCommon(symbolEl, data, false, opt); this.group.add(symbolEl); @@ -203,7 +203,7 @@ class LargeSymbolDraw { return; } - let points = data.getLayout('symbolPoints'); + let points = data.getLayout('points'); this.group.eachChild(function (child: LargeSymbolPath) { if (child.startIndex != null) { const len = (child.endIndex - child.startIndex) * 2; @@ -251,7 +251,7 @@ class LargeSymbolDraw { } symbolEl.setShape({ - points: data.getLayout('symbolPoints') + points: data.getLayout('points') }); this._setCommon(symbolEl, data, !!this._incremental, opt); } diff --git a/src/chart/helper/SymbolDraw.ts b/src/chart/helper/SymbolDraw.ts index fd864e6..94c6b3d 100644 --- a/src/chart/helper/SymbolDraw.ts +++ b/src/chart/helper/SymbolDraw.ts @@ -25,7 +25,6 @@ import type Displayable from 'zrender/src/graphic/Displayable'; import { StageHandlerProgressParams, LabelOption, - ColorString, SymbolOptionMixin, ItemStyleOption, ZRColor, @@ -42,6 +41,7 @@ import { getLabelStatesModels } from '../../label/labelStyle'; interface UpdateOpt { isIgnore?(idx: number): boolean + getSymbolPoint?(idx: number): number[] clipShape?: CoordinateSystemClipArea } @@ -157,6 +157,8 @@ class SymbolDraw { private _seriesScope: SymbolDrawSeriesScope; + private _getSymbolPoint: UpdateOpt['getSymbolPoint']; + constructor(SymbolCtor?: SymbolLikeCtor) { this._SymbolCtor = SymbolCtor || SymbolClz; } @@ -174,6 +176,11 @@ class SymbolDraw { const seriesScope = makeSeriesScope(data); + const getSymbolPoint = opt.getSymbolPoint || function (idx: number) { + return data.getItemLayout(idx); + }; + + // There is no oldLineData only when first rendering or switching from // stream mode to normal mode, where previous elements should be removed. if (!oldData) { @@ -182,7 +189,7 @@ class SymbolDraw { data.diff(oldData) .add(function (newIdx) { - const point = data.getItemLayout(newIdx) as number[]; + const point = getSymbolPoint(newIdx); if (symbolNeedsDraw(data, point, newIdx, opt)) { const symbolEl = new SymbolCtor(data, newIdx, seriesScope); symbolEl.setPosition(point); @@ -193,7 +200,7 @@ class SymbolDraw { .update(function (newIdx, oldIdx) { let symbolEl = oldData.getItemGraphicEl(oldIdx) as SymbolLike; - const point = data.getItemLayout(newIdx) as number[]; + const point = getSymbolPoint(newIdx) as number[]; if (!symbolNeedsDraw(data, point, newIdx, opt)) { group.remove(symbolEl); return; @@ -223,6 +230,7 @@ class SymbolDraw { }) .execute(); + this._getSymbolPoint = getSymbolPoint; this._data = data; }; @@ -234,8 +242,8 @@ class SymbolDraw { const data = this._data; if (data) { // Not use animation - data.eachItemGraphicEl(function (el, idx) { - const point = data.getItemLayout(idx); + data.eachItemGraphicEl((el, idx) => { + const point = this._getSymbolPoint(idx); el.setPosition(point); el.markRedraw(); }); diff --git a/src/chart/line.ts b/src/chart/line.ts index 541b481..53be4a6 100644 --- a/src/chart/line.ts +++ b/src/chart/line.ts @@ -28,7 +28,7 @@ import dataSample from '../processor/dataSample'; // In case developer forget to include grid component import '../component/gridSimple'; -echarts.registerLayout(layoutPoints('line')); +echarts.registerLayout(layoutPoints('line', true)); // Down sample after filter echarts.registerProcessor( diff --git a/src/chart/line/LineView.ts b/src/chart/line/LineView.ts index 2c77752..4616e2f 100644 --- a/src/chart/line/LineView.ts +++ b/src/chart/line/LineView.ts @@ -20,7 +20,6 @@ // FIXME step not support polar import * as zrUtil from 'zrender/src/core/util'; -import {fromPoints} from 'zrender/src/core/bbox'; import SymbolDraw from '../helper/SymbolDraw'; import SymbolClz from '../helper/Symbol'; import lineAnimationDiff from './lineAnimationDiff'; @@ -43,6 +42,8 @@ import type Axis2D from '../../coord/cartesian/Axis2D'; import { CoordinateSystemClipArea } from '../../coord/CoordinateSystem'; import { setStatesStylesFromModel, setStatesFlag, enableHoverEmphasis } from '../../util/states'; import { getECData } from '../../util/ecData'; +import { createFloat32Array } from '../../util/vendor'; +import { createSymbol } from '../../util/symbol'; type PolarArea = ReturnType<Polar['getArea']>; @@ -52,29 +53,46 @@ interface SymbolExtended extends SymbolClz { __temp: boolean } -function isPointsSame(points1: number[][], points2: number[][]) { +function isPointsSame(points1: ArrayLike<number>, points2: ArrayLike<number>) { if (points1.length !== points2.length) { return; } for (let i = 0; i < points1.length; i++) { - const p1 = points1[i]; - const p2 = points2[i]; - if (p1[0] !== p2[0] || p1[1] !== p2[1]) { + if (points1[i] !== points2[i]) { return; } } return true; } -function getBoundingDiff(points1: number[][], points2: number[][]): number { - const min1 = [] as number[]; - const max1 = [] as number[]; +function bboxFromPoints(points: ArrayLike<number>) { + 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++]; + if (!isNaN(x)) { + minX = Math.min(x, minX); + maxX = Math.max(x, maxX); + } + if (!isNaN(y)) { + minY = Math.min(y, minY); + maxY = Math.max(y, maxY); + } + } + return [ + [minX, minY], + [maxX, maxY] + ]; +} - const min2 = [] as number[]; - const max2 = [] as number[]; +function getBoundingDiff(points1: ArrayLike<number>, points2: ArrayLike<number>): number { - fromPoints(points1, min1, max1); - fromPoints(points2, min2, max2); + const [min1, max1] = bboxFromPoints(points1); + const [min2, max2] = bboxFromPoints(points2); // Get a max value from each corner of two boundings. return Math.max( @@ -99,56 +117,61 @@ function getStackedOnPoints( return []; } - const points = []; - for (let idx = 0, len = data.count(); idx < len; idx++) { - points.push(getStackedOnPoint(dataCoordInfo, coordSys, data, idx)); + const len = data.count(); + const points = createFloat32Array(len * 2); + for (let idx = 0; idx < len; idx++) { + const pt = getStackedOnPoint(dataCoordInfo, coordSys, data, idx); + points[idx * 2] = pt[0]; + points[idx * 2 + 1] = pt[1]; } return points; } function turnPointsIntoStep( - points: number[][], + points: ArrayLike<number>, coordSys: Cartesian2D | Polar, stepTurnAt: 'start' | 'end' | 'middle' -) { +): number[] { const baseAxis = coordSys.getBaseAxis(); const baseIndex = baseAxis.dim === 'x' || baseAxis.dim === 'radius' ? 0 : 1; - const stepPoints = []; + const stepPoints: number[] = []; let i = 0; - for (; i < points.length - 1; i++) { - const nextPt = points[i + 1]; - const pt = points[i]; - stepPoints.push(pt); + const stepPt: number[] = []; + const pt: number[] = []; + const nextPt: number[] = []; + for (; i < points.length - 2; i += 2) { + nextPt[0] = points[i + 2]; + nextPt[1] = points[i + 3]; + pt[0] = points[i]; + pt[1] = points[i + 1]; + stepPoints.push(pt[0], pt[1]); - const stepPt = []; switch (stepTurnAt) { case 'end': stepPt[baseIndex] = nextPt[baseIndex]; stepPt[1 - baseIndex] = pt[1 - baseIndex]; - // default is start - stepPoints.push(stepPt); + stepPoints.push(stepPt[0], stepPt[1]); break; case 'middle': - // default is start const middle = (pt[baseIndex] + nextPt[baseIndex]) / 2; const stepPt2 = []; stepPt[baseIndex] = stepPt2[baseIndex] = middle; stepPt[1 - baseIndex] = pt[1 - baseIndex]; stepPt2[1 - baseIndex] = nextPt[1 - baseIndex]; - stepPoints.push(stepPt); - stepPoints.push(stepPt2); + stepPoints.push(stepPt[0], stepPt[1]); + stepPoints.push(stepPt2[0], stepPt[1]); break; default: + // default is start stepPt[baseIndex] = pt[baseIndex]; stepPt[1 - baseIndex] = nextPt[1 - baseIndex]; - // default is start - stepPoints.push(stepPt); + stepPoints.push(stepPt[0], stepPt[1]); } } // Last points - points[i] && stepPoints.push(points[i]); + stepPoints.push(points[i++], points[i++]); return stepPoints; } @@ -365,8 +388,8 @@ class LineView extends ChartView { _polyline: ECPolyline; _polygon: ECPolygon; - _stackedOnPoints: number[][]; - _points: number[][]; + _stackedOnPoints: ArrayLike<number>; + _points: ArrayLike<number>; _step: LineSeriesOption['step']; _valueOrigin: LineSeriesOption['areaStyle']['origin']; @@ -392,7 +415,7 @@ class LineView extends ChartView { const lineStyleModel = seriesModel.getModel('lineStyle'); const areaStyleModel = seriesModel.getModel('areaStyle'); - let points = data.mapArray(data.getItemLayout); + let points = data.getLayout('points') as number[] || []; const isCoordSysPolar = coordSys.type === 'polar'; const prevCoordSys = this._coordSys; @@ -458,7 +481,10 @@ class LineView extends ChartView { ) { showSymbol && symbolDraw.updateData(data, { isIgnore: isIgnoreFunc, - clipShape: clipShapeForSymbol + clipShape: clipShapeForSymbol, + getSymbolPoint(idx) { + return [points[idx * 2], points[idx * 2 + 1]]; + } }); if (step) { @@ -495,7 +521,10 @@ class LineView extends ChartView { // because points are not changed showSymbol && symbolDraw.updateData(data, { isIgnore: isIgnoreFunc, - clipShape: clipShapeForSymbol + clipShape: clipShapeForSymbol, + getSymbolPoint(idx) { + return [points[idx * 2], points[idx * 2 + 1]]; + } }); // Stop symbol animation and sync with line points @@ -629,25 +658,27 @@ class LineView extends ChartView { this._changePolyState('emphasis'); if (!(dataIndex instanceof Array) && dataIndex != null && dataIndex >= 0) { + const points = data.getLayout('points'); let symbol = data.getItemGraphicEl(dataIndex) as SymbolClz; if (!symbol) { // Create a temporary symbol if it is not exists - const pt = data.getItemLayout(dataIndex) as number[]; - if (!pt) { + const x = points[dataIndex * 2]; + const y = points[dataIndex * 2 + 1]; + if (isNaN(x) || isNaN(y)) { // Null data return; } // fix #11360: should't draw symbol outside clipShapeForSymbol - if (this._clipShapeForSymbol && !this._clipShapeForSymbol.contain(pt[0], pt[1])) { + if (this._clipShapeForSymbol && !this._clipShapeForSymbol.contain(x, y)) { return; } symbol = new SymbolClz(data, dataIndex); - symbol.setPosition(pt); + symbol.x = x; + symbol.y = y; symbol.setZ( seriesModel.get('zlevel'), seriesModel.get('z') ); - symbol.ignore = isNaN(pt[0]) || isNaN(pt[1]); (symbol as SymbolExtended).__temp = true; data.setItemGraphicEl(dataIndex, symbol); @@ -705,7 +736,7 @@ class LineView extends ChartView { polygon && setStatesFlag(polygon, toState); } - _newPolyline(points: number[][]) { + _newPolyline(points: ArrayLike<number>) { let polyline = this._polyline; // Remove previous created polyline if (polyline) { @@ -714,7 +745,7 @@ class LineView extends ChartView { polyline = new ECPolyline({ shape: { - points: points + points }, segmentIgnoreThreshold: 2, z2: 10 @@ -727,7 +758,7 @@ class LineView extends ChartView { return polyline; } - _newPolygon(points: number[][], stackedOnPoints: number[][]) { + _newPolygon(points: ArrayLike<number>, stackedOnPoints: ArrayLike<number>) { let polygon = this._polygon; // Remove previous created polygon if (polygon) { @@ -736,7 +767,7 @@ class LineView extends ChartView { polygon = new ECPolygon({ shape: { - points: points, + points, stackedOnPoints: stackedOnPoints }, segmentIgnoreThreshold: 2 @@ -754,7 +785,7 @@ class LineView extends ChartView { // FIXME Two value axis _updateAnimation( data: List, - stackedOnPoints: number[][], + stackedOnPoints: ArrayLike<number>, coordSys: Cartesian2D | Polar, api: ExtensionAPI, step: LineSeriesOption['step'], @@ -855,9 +886,12 @@ class LineView extends ChartView { if (polyline.animators && polyline.animators.length) { polyline.animators[0].during(function () { + const points = (polyline.shape as any).__points; for (let i = 0; i < updatedDataInfo.length; i++) { const el = updatedDataInfo[i].el; - el.setPosition((polyline.shape as any).__points[updatedDataInfo[i].ptIdx]); + const offset = updatedDataInfo[i].ptIdx * 2; + el.x = points[offset]; + el.y = points[offset + 1]; el.markRedraw(); } }); diff --git a/src/chart/line/helper.ts b/src/chart/line/helper.ts index edaccc5..200824f 100644 --- a/src/chart/line/helper.ts +++ b/src/chart/line/helper.ts @@ -111,7 +111,7 @@ export function getStackedOnPoint( coordSys: Cartesian2D | Polar, data: List, idx: number - ) { +) { let value = NaN; if (dataCoordInfo.stacked) { value = data.get(data.getCalculationInfo('stackedOverDimension'), idx) as number; diff --git a/src/chart/line/lineAnimationDiff.ts b/src/chart/line/lineAnimationDiff.ts index 9e164ef..84cc409 100644 --- a/src/chart/line/lineAnimationDiff.ts +++ b/src/chart/line/lineAnimationDiff.ts @@ -22,6 +22,7 @@ import List from '../../data/List'; import type Cartesian2D from '../../coord/cartesian/Cartesian2D'; import type Polar from '../../coord/polar/Polar'; import { LineSeriesOption } from './LineSeries'; +import { createFloat32Array } from '../../util/vendor'; interface DiffItem { cmd: '+' | '=' | '-' @@ -49,7 +50,7 @@ function diffData(oldData: List, newData: List) { export default function ( oldData: List, newData: List, - oldStackedOnPoints: number[][], newStackedOnPoints: number[][], + oldStackedOnPoints: ArrayLike<number>, newStackedOnPoints: ArrayLike<number>, oldCoordSys: Cartesian2D | Polar, newCoordSys: Cartesian2D | Polar, oldValueOrigin: LineSeriesOption['areaStyle']['origin'], newValueOrigin: LineSeriesOption['areaStyle']['origin'] @@ -64,11 +65,11 @@ export default function ( // // FIXME One data ? // diff = arrayDiff(oldIdList, newIdList); - const currPoints: number[][] = []; - const nextPoints: number[][] = []; + const currPoints: number[] = []; + const nextPoints: number[] = []; // Points for stacking base line - const currStackedPoints: number[][] = []; - const nextStackedPoints: number[][] = []; + const currStackedPoints: number[] = []; + const nextStackedPoints: number[] = []; const status = []; const sortedIndices: number[] = []; @@ -77,62 +78,78 @@ export default function ( const newDataOldCoordInfo = prepareDataCoordInfo(oldCoordSys, newData, oldValueOrigin); const oldDataNewCoordInfo = prepareDataCoordInfo(newCoordSys, oldData, newValueOrigin); + const oldPoints = oldData.getLayout('points') as number[] || []; + const newPoints = newData.getLayout('points') as number[] || []; + for (let i = 0; i < diff.length; i++) { const diffItem = diff[i]; let pointAdded = true; + let oldIdx2: number; + let newIdx2: number; + // FIXME, animation is not so perfect when dataZoom window moves fast // Which is in case remvoing or add more than one data in the tail or head switch (diffItem.cmd) { case '=': - let currentPt = oldData.getItemLayout(diffItem.idx) as number[]; - const nextPt = newData.getItemLayout(diffItem.idx1) as number[]; + oldIdx2 = diffItem.idx * 2; + newIdx2 = diffItem.idx1 * 2; + let currentX = oldPoints[oldIdx2]; + let currentY = oldPoints[oldIdx2 + 1]; + const nextX = newPoints[newIdx2]; + const nextY = newPoints[newIdx2 + 1]; + // If previous data is NaN, use next point directly - if (isNaN(currentPt[0]) || isNaN(currentPt[1])) { - currentPt = nextPt.slice(); + if (isNaN(currentX) || isNaN(currentY)) { + currentX = nextX; + currentY = nextY; } - currPoints.push(currentPt); - nextPoints.push(nextPt); + currPoints.push(currentX, currentY); + nextPoints.push(nextX, nextY); - currStackedPoints.push(oldStackedOnPoints[diffItem.idx]); - nextStackedPoints.push(newStackedOnPoints[diffItem.idx1]); + currStackedPoints.push(oldStackedOnPoints[oldIdx2], oldStackedOnPoints[oldIdx2 + 1]); + nextStackedPoints.push(newStackedOnPoints[newIdx2], newStackedOnPoints[newIdx2 + 1]); rawIndices.push(newData.getRawIndex(diffItem.idx1)); break; case '+': - const idxAdd = diffItem.idx; - currPoints.push( - oldCoordSys.dataToPoint([ - newData.get(newDataOldCoordInfo.dataDimsForPoint[0], idxAdd), - newData.get(newDataOldCoordInfo.dataDimsForPoint[1], idxAdd) - ]) - ); - - nextPoints.push((newData.getItemLayout(idxAdd) as number[]).slice()); - - currStackedPoints.push( - getStackedOnPoint(newDataOldCoordInfo, oldCoordSys, newData, idxAdd) - ); - nextStackedPoints.push(newStackedOnPoints[idxAdd]); - - rawIndices.push(newData.getRawIndex(idxAdd)); + const newIdx = diffItem.idx; + const newDataDimsForPoint = newDataOldCoordInfo.dataDimsForPoint; + const oldPt = oldCoordSys.dataToPoint([ + newData.get(newDataDimsForPoint[0], newIdx), + newData.get(newDataDimsForPoint[1], newIdx) + ]); + newIdx2 = newIdx * 2; + currPoints.push(oldPt[0], oldPt[1]); + + nextPoints.push(newPoints[newIdx2], newPoints[newIdx2 + 1]); + + const stackedOnPoint = getStackedOnPoint(newDataOldCoordInfo, oldCoordSys, newData, newIdx); + + currStackedPoints.push(stackedOnPoint[0], stackedOnPoint[1]); + nextStackedPoints.push(newStackedOnPoints[newIdx2], newStackedOnPoints[newIdx2 + 1]); + + rawIndices.push(newData.getRawIndex(newIdx)); break; case '-': - const idxMinus = diffItem.idx; - const rawIndex = oldData.getRawIndex(idxMinus); + const oldIdx = diffItem.idx; + const rawIndex = oldData.getRawIndex(oldIdx); + const oldDataDimsForPoint = oldDataNewCoordInfo.dataDimsForPoint; + oldIdx2 = oldIdx * 2; // Data is replaced. In the case of dynamic data queue // FIXME FIXME FIXME - if (rawIndex !== idxMinus) { - currPoints.push(oldData.getItemLayout(idxMinus) as number[]); - nextPoints.push(newCoordSys.dataToPoint([ - oldData.get(oldDataNewCoordInfo.dataDimsForPoint[0], idxMinus), - oldData.get(oldDataNewCoordInfo.dataDimsForPoint[1], idxMinus) - ])); - - currStackedPoints.push(oldStackedOnPoints[idxMinus]); - nextStackedPoints.push( - getStackedOnPoint(oldDataNewCoordInfo, newCoordSys, oldData, idxMinus) - ); + if (rawIndex !== oldIdx) { + const newPt = newCoordSys.dataToPoint([ + oldData.get(oldDataDimsForPoint[0], oldIdx), + oldData.get(oldDataDimsForPoint[1], oldIdx) + ]); + const newStackedOnPt = getStackedOnPoint(oldDataNewCoordInfo, newCoordSys, oldData, oldIdx); + + currPoints.push(oldPoints[oldIdx2], oldPoints[oldIdx2 + 1]); + nextPoints.push(newPt[0], newPt[1]); + + currStackedPoints.push(oldStackedOnPoints[oldIdx2], oldStackedOnPoints[oldIdx2 + 1]); + nextStackedPoints.push(newStackedOnPt[0], newStackedOnPt[1]); rawIndices.push(rawIndex); } @@ -154,20 +171,27 @@ export default function ( return rawIndices[a] - rawIndices[b]; }); - const sortedCurrPoints = []; - const sortedNextPoints = []; + const len = currPoints.length; + const sortedCurrPoints = createFloat32Array(len); + const sortedNextPoints = createFloat32Array(len); - const sortedCurrStackedPoints = []; - const sortedNextStackedPoints = []; + const sortedCurrStackedPoints = createFloat32Array(len); + const sortedNextStackedPoints = createFloat32Array(len); const sortedStatus = []; for (let i = 0; i < sortedIndices.length; i++) { const idx = sortedIndices[i]; - sortedCurrPoints[i] = currPoints[idx]; - sortedNextPoints[i] = nextPoints[idx]; - - sortedCurrStackedPoints[i] = currStackedPoints[idx]; - sortedNextStackedPoints[i] = nextStackedPoints[idx]; + const i2 = i * 2; + const idx2 = idx * 2; + sortedCurrPoints[i2] = currPoints[idx2]; + sortedCurrPoints[i2 + 1] = currPoints[idx2 + 1]; + sortedNextPoints[i2] = nextPoints[idx2]; + sortedNextPoints[i2 + 1] = nextPoints[idx2 + 1]; + + sortedCurrStackedPoints[i2] = currStackedPoints[idx2]; + sortedCurrStackedPoints[i2 + 1] = currStackedPoints[idx2 + 1]; + sortedNextStackedPoints[i2] = nextStackedPoints[idx2]; + sortedNextStackedPoints[i2 + 1] = nextStackedPoints[idx2 + 1]; sortedStatus[i] = status[idx]; } diff --git a/src/chart/line/poly.ts b/src/chart/line/poly.ts index adb61eb..1c9a4ce 100644 --- a/src/chart/line/poly.ts +++ b/src/chart/line/poly.ts @@ -20,94 +20,21 @@ // Poly path support NaN point import Path, { PathProps } from 'zrender/src/graphic/Path'; -import * as vec2 from 'zrender/src/core/vector'; -const vec2Min = vec2.min; -const vec2Max = vec2.max; +const mathMin = Math.min; +const mathMax = Math.max; -const scaleAndAdd = vec2.scaleAndAdd; -const v2Copy = vec2.copy; - -// Temporary variable -const v: number[] = []; -const cp0: number[] = []; -const cp1: number[] = []; - -function isPointNull(p: number[]) { - return isNaN(p[0]) || isNaN(p[1]); +function isPointNull(x: number, y: number) { + return isNaN(x) || isNaN(y); } - -function drawSegment( - ctx: CanvasRenderingContext2D, - points: number[][], - start: number, - segLen: number, - allLen: number, - dir: number, - smoothMin: number[], - smoothMax: number[], - smooth: number, - smoothMonotone: 'x' | 'y' | 'none', - connectNulls: boolean -) { - return ((smoothMonotone === 'none' || !smoothMonotone) ? drawNonMono : drawMono)( - ctx, - points, - start, - segLen, - allLen, - dir, - smoothMin, - smoothMax, - smooth, - smoothMonotone, - connectNulls - ); -} - -/** - * Check if points is in monotone. - * - * @param {number[][]} points Array of points which is in [x, y] form - * @param {string} smoothMonotone 'x', 'y', or 'none', stating for which - * dimension that is checking. - * If is 'none', `drawNonMono` should be - * called. - * If is undefined, either being monotone - * in 'x' or 'y' will call `drawMono`. - */ -// function isMono(points, smoothMonotone) { -// if (points.length <= 1) { -// return true; -// } - -// let dim = smoothMonotone === 'x' ? 0 : 1; -// let last = points[0][dim]; -// let lastDiff = 0; -// for (let i = 1; i < points.length; ++i) { -// let diff = points[i][dim] - last; -// if (!isNaN(diff) && !isNaN(lastDiff) -// && diff !== 0 && lastDiff !== 0 -// && ((diff >= 0) !== (lastDiff >= 0)) -// ) { -// return false; -// } -// if (!isNaN(diff) && diff !== 0) { -// lastDiff = diff; -// last = points[i][dim]; -// } -// } -// return true; -// } - /** - * Draw smoothed line in monotone, in which only vertical or horizontal bezier - * control points will be used. This should be used when points are monotone - * either in x or y dimension. + * Draw smoothed line in non-monotone, in may cause undesired curve in extreme + * situations. This should be used when points are non-monotone neither in x or + * y dimension. */ -function drawMono( +function drawSegment( ctx: CanvasRenderingContext2D, - points: number[][], + points: ArrayLike<number>, start: number, segLen: number, allLen: number, @@ -118,15 +45,23 @@ function drawMono( smoothMonotone: 'x' | 'y' | 'none', connectNulls: boolean ) { - let prevIdx = 0; + let px: number; + let py: number; + let cpx0: number; + let cpy0: number; + let cpx1: number; + let cpy1: number; let idx = start; let k = 0; for (; k < segLen; k++) { - const p = points[idx]; + + const x = points[idx * 2]; + const y = points[idx * 2 + dir]; + if (idx >= allLen || idx < 0) { break; } - if (isPointNull(p)) { + if (isPointNull(x, y)) { if (connectNulls) { idx += dir; continue; @@ -135,165 +70,118 @@ function drawMono( } if (idx === start) { - ctx[dir > 0 ? 'moveTo' : 'lineTo'](p[0], p[1]); + ctx[dir > 0 ? 'moveTo' : 'lineTo'](x, y); + cpx0 = x; + cpy0 = y; } else { - if (smooth > 0) { - const prevP = points[prevIdx]; - const dim = smoothMonotone === 'y' ? 1 : 0; - - // Length of control point to p, either in x or y, but not both - const ctrlLen = (p[dim] - prevP[dim]) * smooth; - - v2Copy(cp0, prevP); - cp0[dim] = prevP[dim] + ctrlLen; - - v2Copy(cp1, p); - cp1[dim] = p[dim] - ctrlLen; - - ctx.bezierCurveTo( - cp0[0], cp0[1], - cp1[0], cp1[1], - p[0], p[1] - ); - } - else { - ctx.lineTo(p[0], p[1]); - } - } - - prevIdx = idx; - idx += dir; - } + const dx = x - px; + const dy = y - py; - return k; -} - -/** - * Draw smoothed line in non-monotone, in may cause undesired curve in extreme - * situations. This should be used when points are non-monotone neither in x or - * y dimension. - */ -function drawNonMono( - ctx: CanvasRenderingContext2D, - points: number[][], - start: number, - segLen: number, - allLen: number, - dir: number, - smoothMin: number[], - smoothMax: number[], - smooth: number, - smoothMonotone: 'x' | 'y' | 'none', - connectNulls: boolean -) { - let prevIdx = 0; - let idx = start; - let k = 0; - for (; k < segLen; k++) { - const p = points[idx]; - if (idx >= allLen || idx < 0) { - break; - } - if (isPointNull(p)) { - if (connectNulls) { + // Ignore tiny segment. + if ((dx * dx + dy * dy) < 1) { idx += dir; continue; } - break; - } - if (idx === start) { - ctx[dir > 0 ? 'moveTo' : 'lineTo'](p[0], p[1]); - v2Copy(cp0, p); - } - else { if (smooth > 0) { let nextIdx = idx + dir; - let nextP = points[nextIdx]; + let nextX = points[nextIdx * 2]; + let nextY = points[nextIdx * 2 + 1]; if (connectNulls) { // Find next point not null - while (nextP && isPointNull(points[nextIdx])) { + while (isPointNull(nextX, nextY) && (nextIdx < (segLen + start)) || (dir < 0 && nextIdx >= start)) { nextIdx += dir; - nextP = points[nextIdx]; + nextX = points[nextIdx * 2]; + nextY = points[nextIdx * 2 + 1]; } } let ratioNextSeg = 0.5; - const prevP = points[prevIdx]; - nextP = points[nextIdx]; - // Last point - if (!nextP || isPointNull(nextP)) { - v2Copy(cp1, p); + let vx: number = 0; + let vy: number = 0; + // Is last point + if ((dir > 0 && nextIdx >= (segLen + start)) || (dir < 0 && nextIdx < start)) { + cpx1 = x; + cpy1 = y; } else { - // If next data is null in not connect case - if (isPointNull(nextP) && !connectNulls) { - nextP = p; - } - - vec2.sub(v, nextP, prevP); + vx = nextX - px; + vy = nextY - py; + const dx0 = x - px; + const dx1 = nextX - x; + const dy0 = y - py; + const dy1 = nextY - y; let lenPrevSeg; let lenNextSeg; - if (smoothMonotone === 'x' || smoothMonotone === 'y') { - const dim = smoothMonotone === 'x' ? 0 : 1; - lenPrevSeg = Math.abs(p[dim] - prevP[dim]); - lenNextSeg = Math.abs(p[dim] - nextP[dim]); + if (smoothMonotone === 'x') { + lenPrevSeg = Math.abs(dx0); + lenNextSeg = Math.abs(dx1); + } + else if (smoothMonotone === 'y') { + lenPrevSeg = Math.abs(dy0); + lenNextSeg = Math.abs(dy1); } else { - lenPrevSeg = vec2.dist(p, prevP); - lenNextSeg = vec2.dist(p, nextP); + lenPrevSeg = Math.sqrt(dx0 * dx0 + dy0 * dy0); + lenNextSeg = Math.sqrt(dx1 * dx1 + dy1 * dy1); } // Use ratio of seg length ratioNextSeg = lenNextSeg / (lenNextSeg + lenPrevSeg); - scaleAndAdd(cp1, p, v, -smooth * (1 - ratioNextSeg)); + cpx1 = x - vx * smooth * (1 - ratioNextSeg); + cpy1 = y - vy * smooth * (1 - ratioNextSeg); } // Smooth constraint - vec2Min(cp0, cp0, smoothMax); - vec2Max(cp0, cp0, smoothMin); - vec2Min(cp1, cp1, smoothMax); - vec2Max(cp1, cp1, smoothMin); - - ctx.bezierCurveTo( - cp0[0], cp0[1], - cp1[0], cp1[1], - p[0], p[1] - ); + cpx0 = mathMin(cpx0, smoothMax[0]); + cpy0 = mathMin(cpy0, smoothMax[1]); + cpx0 = mathMax(cpx0, smoothMin[0]); + cpy0 = mathMax(cpy0, smoothMin[1]); + + cpx1 = mathMin(cpx1, smoothMax[0]); + cpy1 = mathMin(cpy1, smoothMax[1]); + cpx1 = mathMax(cpx1, smoothMin[0]); + cpy1 = mathMax(cpy1, smoothMin[1]); + + ctx.bezierCurveTo(cpx0, cpy0, cpx1, cpy1, x, y); + // cp0 of next segment - scaleAndAdd(cp0, p, v, smooth * ratioNextSeg); + cpx0 = x + vx * smooth * ratioNextSeg; + cpy0 = y + vy * smooth * ratioNextSeg; } else { - ctx.lineTo(p[0], p[1]); + ctx.lineTo(x, y); } } - prevIdx = idx; + px = x; + py = y; idx += dir; } return k; } -function getBoundingBox(points: number[][], smoothConstraint?: boolean) { +function getBoundingBox(points: ArrayLike<number>, smoothConstraint?: boolean) { const ptMin = [Infinity, Infinity]; const ptMax = [-Infinity, -Infinity]; if (smoothConstraint) { - for (let i = 0; i < points.length; i++) { - const pt = points[i]; - if (pt[0] < ptMin[0]) { - ptMin[0] = pt[0]; + for (let i = 0; i < points.length;) { + const x = points[i++]; + const y = points[i++]; + if (x < ptMin[0]) { + ptMin[0] = x; } - if (pt[1] < ptMin[1]) { - ptMin[1] = pt[1]; + if (y < ptMin[1]) { + ptMin[1] = y; } - if (pt[0] > ptMax[0]) { - ptMax[0] = pt[0]; + if (x > ptMax[0]) { + ptMax[0] = x; } - if (pt[1] > ptMax[1]) { - ptMax[1] = pt[1]; + if (y > ptMax[1]) { + ptMax[1] = y; } } } @@ -304,7 +192,7 @@ function getBoundingBox(points: number[][], smoothConstraint?: boolean) { } class ECPolylineShape { - points: number[][]; + points: ArrayLike<number>; smooth = 0; smoothConstraint = true; smoothMonotone: 'x' | 'y' | 'none'; @@ -346,13 +234,13 @@ export class ECPolyline extends Path<ECPolylineProps> { if (shape.connectNulls) { // Must remove first and last null values avoid draw error in polygon - for (; len > 0; len--) { - if (!isPointNull(points[len - 1])) { + for (; len > 0; len -= 2) { + if (!isPointNull(points[len - 2], points[len - 1])) { break; } } - for (; i < len; i++) { - if (!isPointNull(points[i])) { + for (; i < len; i += 2) { + if (!isPointNull(points[i], points[i + 1])) { break; } } @@ -368,7 +256,7 @@ export class ECPolyline extends Path<ECPolylineProps> { } class ECPolygonShape extends ECPolylineShape { // Offset between stacked base points and points - stackedOnPoints: number[][]; + stackedOnPoints: ArrayLike<number>; stackedOnSmooth: number; } @@ -401,13 +289,13 @@ export class ECPolygon extends Path { if (shape.connectNulls) { // Must remove first and last null values avoid draw error in polygon - for (; len > 0; len--) { - if (!isPointNull(points[len - 1])) { + for (; len > 0; len -= 2) { + if (!isPointNull(points[len - 2], points[len - 1])) { break; } } - for (; i < len; i++) { - if (!isPointNull(points[i])) { + for (; i < len; i += 2) { + if (!isPointNull(points[i], points[i + 1])) { break; } } @@ -425,7 +313,7 @@ export class ECPolygon extends Path { ); i += k + 1; - ctx.closePath(); + // ctx.closePath(); } } } \ No newline at end of file diff --git a/src/layout/points.ts b/src/layout/points.ts index 0808e10..e819581 100644 --- a/src/layout/points.ts +++ b/src/layout/points.ts @@ -24,8 +24,10 @@ import createRenderPlanner from '../chart/helper/createRenderPlanner'; import {isDimensionStacked} from '../data/helper/dataStackHelper'; import SeriesModel from '../model/Series'; import { StageHandler, ParsedValueNumeric } from '../util/types'; +import { createFloat32Array } from '../util/vendor'; -export default function (seriesType?: string): StageHandler { + +export default function (seriesType: string, forceStoreInTypedArray?: boolean): StageHandler { return { seriesType: seriesType, @@ -35,7 +37,7 @@ export default function (seriesType?: string): StageHandler { const data = seriesModel.getData(); const coordSys = seriesModel.coordinateSystem; const pipelineContext = seriesModel.pipelineContext; - const isLargeRender = pipelineContext.large; + const useTypedArray = forceStoreInTypedArray || pipelineContext.large; if (!coordSys) { return; @@ -58,7 +60,7 @@ export default function (seriesType?: string): StageHandler { return dimLen && { progress(params, data) { const segCount = params.end - params.start; - const points = isLargeRender && new Float32Array(segCount * dimLen); + const points = useTypedArray && createFloat32Array(segCount * dimLen); const tmpIn: ParsedValueNumeric[] = []; const tmpOut: number[] = []; @@ -76,7 +78,7 @@ export default function (seriesType?: string): StageHandler { point = !isNaN(x) && !isNaN(y) && coordSys.dataToPoint(tmpIn, null, tmpOut); } - if (isLargeRender) { + if (useTypedArray) { points[offset++] = point ? point[0] : NaN; points[offset++] = point ? point[1] : NaN; } @@ -85,7 +87,7 @@ export default function (seriesType?: string): StageHandler { } } - isLargeRender && data.setLayout('symbolPoints', points); + useTypedArray && data.setLayout('points', points); } }; } diff --git a/src/chart/line.ts b/src/util/vendor.ts similarity index 60% copy from src/chart/line.ts copy to src/util/vendor.ts index 541b481..ab4db32 100644 --- a/src/chart/line.ts +++ b/src/util/vendor.ts @@ -17,21 +17,18 @@ * under the License. */ -import * as echarts from '../echarts'; +import { isArray } from 'zrender/src/core/util'; -import './line/LineSeries'; -import './line/LineView'; +/* global Float32Array */ +const supportFloat32Array = typeof Float32Array !== 'undefined'; -import layoutPoints from '../layout/points'; -import dataSample from '../processor/dataSample'; +const Float32ArrayCtor = !supportFloat32Array ? Array : Float32Array; -// In case developer forget to include grid component -import '../component/gridSimple'; - -echarts.registerLayout(layoutPoints('line')); - -// Down sample after filter -echarts.registerProcessor( - echarts.PRIORITY.PROCESSOR.STATISTIC, - dataSample('line') -); +export function createFloat32Array(arg: number | number[]): number[] | Float32Array { + if (isArray(arg)) { + // Return self directly if don't support TypedArray. + return supportFloat32Array ? new Float32Array(arg) : arg; + } + // Else is number + return new Float32ArrayCtor(arg); +} \ No newline at end of file diff --git a/test/largeLine.html b/test/largeLine.html index dba5cd3..9a8cd9b 100644 --- a/test/largeLine.html +++ b/test/largeLine.html @@ -47,8 +47,8 @@ under the License. ], function (echarts) { var myChart; var lineCount = 20; - var pointCount = 1000; - var chartCount = 50; + var pointCount = 10000; + var chartCount = 20; var option = { tooltip : { @@ -92,8 +92,7 @@ under the License. var oneDay = 24 * 3600 * 1000; var base = +new Date(1897, 9, 3); for (var j = 0; j < pointCount; j++) { - var now = new Date(base += oneDay); - date.push([now.getFullYear(), now.getMonth() + 1, now.getDate()].join('-')); + date.push(base += oneDay); } for (var i = 0; i < lineCount; i++) { var y = Math.random() * 1000; @@ -101,11 +100,9 @@ under the License. for (var j = 0; j < pointCount; j++) { y += Math.round(10 + Math.random() * (-10 - 10)); values.push( - [ - date[j], - // Math.random() < 0.1 ? '-' : y - y - ] + date[j], + // Math.random() < 0.1 ? '-' : y + y ); } @@ -113,10 +110,14 @@ under the License. option.series.push({ name: 'line' + i, type: 'line', - sampling: 'average', hoverAnimation: false, showSymbol: false, - data: values, + dimensions: ['date', 'value'], + encode: { + x: 'date', + y: 'value' + }, + data: new Float64Array(values), lineStyle: lineStyle }); } @@ -131,9 +132,10 @@ under the License. myChart = echarts.init(document.getElementById('chart'+n)); myChart.setOption(option, true); } - var end = new Date(); - - document.getElementById('timing').innerHTML = 'Graphs loaded in ' + ( end - start ) + ' ms.'; + setTimeout(function () { + var end = new Date(); + document.getElementById('timing').innerHTML = 'Graphs loaded in ' + ( end - start ) + ' ms.'; + }); }; refresh(); --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
