perf: 优化波形核心算法与图框网格控制
This commit is contained in:
58
src/core/data.test.ts
Normal file
58
src/core/data.test.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { normalizeWaveformData } from './data'
|
||||
|
||||
describe('waveform data normalization', () => {
|
||||
it('builds sample points in one pass while preserving source indexes', () => {
|
||||
expect(
|
||||
normalizeWaveformData({
|
||||
kind: 'samples',
|
||||
values: [1, Number.NaN, 3],
|
||||
sampleRate: 2,
|
||||
startTime: 1,
|
||||
}),
|
||||
).toEqual([
|
||||
{ x: 1, y: 1 },
|
||||
{ x: 2, y: 3 },
|
||||
])
|
||||
})
|
||||
|
||||
it('skips sorting already ordered points and normalizes errors', () => {
|
||||
const sortSpy = vi.spyOn(Array.prototype, 'sort')
|
||||
try {
|
||||
const result = normalizeWaveformData({
|
||||
kind: 'points',
|
||||
points: [
|
||||
{ x: 0, y: 1, error: -1, upperError: 2 },
|
||||
{ x: 1, y: 2, lowerError: 3 },
|
||||
],
|
||||
})
|
||||
|
||||
expect(sortSpy).not.toHaveBeenCalled()
|
||||
expect(result).toEqual([
|
||||
{ x: 0, y: 1, upperError: 2 },
|
||||
{ x: 1, y: 2, lowerError: 3 },
|
||||
])
|
||||
} finally {
|
||||
sortSpy.mockRestore()
|
||||
}
|
||||
})
|
||||
|
||||
it('sorts only unordered points and preserves duplicate-x order', () => {
|
||||
expect(
|
||||
normalizeWaveformData({
|
||||
kind: 'points',
|
||||
points: [
|
||||
{ x: 2, y: 20 },
|
||||
{ x: 1, y: 10 },
|
||||
{ x: 1, y: 11 },
|
||||
{ x: Number.NaN, y: 12 },
|
||||
],
|
||||
}),
|
||||
).toEqual([
|
||||
{ x: 1, y: 10 },
|
||||
{ x: 1, y: 11 },
|
||||
{ x: 2, y: 20 },
|
||||
])
|
||||
})
|
||||
})
|
||||
@@ -46,15 +46,27 @@ export function normalizeWaveformData(data: SingleWaveformData): WaveformPoint[]
|
||||
if (!Number.isFinite(data.sampleRate) || data.sampleRate <= 0) return []
|
||||
|
||||
const startTime = Number.isFinite(data.startTime) ? (data.startTime ?? 0) : 0
|
||||
return data.values.flatMap((value, index) =>
|
||||
Number.isFinite(value) ? [{ x: startTime + index / data.sampleRate, y: value }] : [],
|
||||
)
|
||||
const points: WaveformPoint[] = []
|
||||
for (let index = 0; index < data.values.length; index += 1) {
|
||||
const value = data.values[index]
|
||||
if (!Number.isFinite(value)) continue
|
||||
points.push({ x: startTime + index / data.sampleRate, y: value })
|
||||
}
|
||||
return points
|
||||
}
|
||||
|
||||
return data.points
|
||||
.filter((point) => Number.isFinite(point.x) && Number.isFinite(point.y))
|
||||
.map(normalizeWaveformPoint)
|
||||
.sort((left, right) => left.x - right.x)
|
||||
const points: WaveformPoint[] = []
|
||||
let previousX = Number.NEGATIVE_INFINITY
|
||||
let sorted = true
|
||||
for (const point of data.points) {
|
||||
if (!Number.isFinite(point.x) || !Number.isFinite(point.y)) continue
|
||||
const normalized = normalizeWaveformPoint(point)
|
||||
if (normalized.x < previousX) sorted = false
|
||||
previousX = normalized.x
|
||||
points.push(normalized)
|
||||
}
|
||||
if (!sorted) points.sort((left, right) => left.x - right.x)
|
||||
return points
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -2,9 +2,11 @@ import { describe, expect, it } from 'vitest'
|
||||
|
||||
import type { WaveformPoint } from '../types'
|
||||
import {
|
||||
hasMinimumVisibleXValues,
|
||||
resolveWaveformRenderingOptions,
|
||||
selectDecorationPoints,
|
||||
selectRenderablePoints,
|
||||
selectSeriesRenderPoints,
|
||||
} from './rendering'
|
||||
|
||||
describe('waveform rendering selection', () => {
|
||||
@@ -122,4 +124,66 @@ describe('waveform rendering selection', () => {
|
||||
expect(selected.filter(hasError)).toEqual(priorityPoints.filter(hasError))
|
||||
expect(selected.length).toBeLessThanOrEqual(Math.ceil(100 / 20) + 2)
|
||||
})
|
||||
|
||||
it('shares visible-range selection for a dense 100k-point series', () => {
|
||||
const densePoints = Array.from({ length: 100_000 }, (_, index): WaveformPoint => ({
|
||||
x: index,
|
||||
y: index === 50_001 ? 10_000 : Math.sin(index / 20),
|
||||
error: index % 1_000 === 0 ? 1 : undefined,
|
||||
}))
|
||||
const selected = selectSeriesRenderPoints(
|
||||
densePoints,
|
||||
[99_999, 0],
|
||||
500,
|
||||
resolveWaveformRenderingOptions({ downsampleThreshold: 100 }),
|
||||
{
|
||||
lineVisible: true,
|
||||
pointVisible: true,
|
||||
errorBarVisible: true,
|
||||
hasErrorPoints: true,
|
||||
},
|
||||
)
|
||||
|
||||
expect(selected.linePoints.length).toBeLessThanOrEqual(2_002)
|
||||
expect(selected.linePoints).toContain(densePoints[50_001])
|
||||
expect(selected.linePoints[0]).toBe(densePoints[0])
|
||||
expect(selected.linePoints.at(-1)).toBe(densePoints.at(-1))
|
||||
expect(selected.pointRenderPoints.length).toBeLessThanOrEqual(52)
|
||||
expect(selected.errorBarRenderPoints.every((point) => (point.error ?? 0) > 0)).toBe(true)
|
||||
})
|
||||
|
||||
it('counts unique visible x values across series and reversed domains', () => {
|
||||
const first = {
|
||||
points: [
|
||||
{ x: 0, y: 0 },
|
||||
{ x: 1, y: 1 },
|
||||
],
|
||||
}
|
||||
const second = {
|
||||
points: [
|
||||
{ x: 0, y: 2 },
|
||||
{ x: 1, y: 3 },
|
||||
{ x: 2, y: 4 },
|
||||
],
|
||||
}
|
||||
|
||||
expect(hasMinimumVisibleXValues([first, second], [2, 0], 3)).toBe(true)
|
||||
expect(hasMinimumVisibleXValues([first, second], [0, 2], 4)).toBe(false)
|
||||
expect(hasMinimumVisibleXValues([first, second], [1, 1], 1)).toBe(true)
|
||||
expect(hasMinimumVisibleXValues([first, second], [3, 4], 1)).toBe(false)
|
||||
})
|
||||
|
||||
it('stops scanning a 100k-point series after reaching the minimum', () => {
|
||||
const source = Array.from({ length: 100_000 }, (_, index) => ({ x: index, y: index }))
|
||||
let pointReads = 0
|
||||
const points = new Proxy(source, {
|
||||
get(target, property, receiver) {
|
||||
if (typeof property === 'string' && /^\d+$/.test(property)) pointReads += 1
|
||||
return Reflect.get(target, property, receiver)
|
||||
},
|
||||
})
|
||||
|
||||
expect(hasMinimumVisibleXValues([{ points }], [0, 99_999], 5)).toBe(true)
|
||||
expect(pointReads).toBeLessThan(100)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { bisector } from 'd3'
|
||||
|
||||
import type { WaveformPoint, WaveformRenderingOptions } from '@/types'
|
||||
import { resolveWaveformPointErrors } from './data'
|
||||
|
||||
export interface ResolvedWaveformRenderingOptions {
|
||||
downsample: boolean
|
||||
@@ -19,6 +20,59 @@ export const DEFAULT_WAVEFORM_RENDERING_OPTIONS: ResolvedWaveformRenderingOption
|
||||
}
|
||||
|
||||
const pointBisector = bisector((point: WaveformPoint) => point.x)
|
||||
const acceptAllPoints = () => true
|
||||
|
||||
interface VisiblePointRange {
|
||||
start: number
|
||||
end: number
|
||||
}
|
||||
|
||||
interface PointSeriesSource {
|
||||
points: WaveformPoint[]
|
||||
}
|
||||
|
||||
interface SeriesRenderSelectionOptions {
|
||||
lineVisible: boolean
|
||||
pointVisible: boolean
|
||||
errorBarVisible: boolean
|
||||
hasErrorPoints: boolean
|
||||
}
|
||||
|
||||
export interface SeriesRenderPointSelection {
|
||||
linePoints: WaveformPoint[]
|
||||
pointRenderPoints: WaveformPoint[]
|
||||
errorBarRenderPoints: WaveformPoint[]
|
||||
}
|
||||
|
||||
export function resolveVisiblePointRange(
|
||||
points: WaveformPoint[],
|
||||
domain: [number, number],
|
||||
): VisiblePointRange {
|
||||
const domainStart = Math.min(domain[0], domain[1])
|
||||
const domainEnd = Math.max(domain[0], domain[1])
|
||||
return {
|
||||
start: pointBisector.left(points, domainStart),
|
||||
end: pointBisector.right(points, domainEnd),
|
||||
}
|
||||
}
|
||||
|
||||
export function hasMinimumVisibleXValues(
|
||||
seriesList: readonly PointSeriesSource[],
|
||||
domain: [number, number],
|
||||
minimum: number,
|
||||
): boolean {
|
||||
if (!Number.isFinite(minimum) || minimum <= 0) return true
|
||||
const required = Math.ceil(minimum)
|
||||
const xValues = new Set<number>()
|
||||
for (const series of seriesList) {
|
||||
const range = resolveVisiblePointRange(series.points, domain)
|
||||
for (let index = range.start; index < range.end; index += 1) {
|
||||
xValues.add(series.points[index].x)
|
||||
if (xValues.size >= required) return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
export function resolveWaveformRenderingOptions(
|
||||
options?: WaveformRenderingOptions,
|
||||
@@ -52,24 +106,17 @@ function pushUniquePoint(target: WaveformPoint[], point: WaveformPoint | undefin
|
||||
if (point && target[target.length - 1] !== point) target.push(point)
|
||||
}
|
||||
|
||||
/**
|
||||
* Select the visible source range and preserve first/min/max/last values in each X bucket.
|
||||
* Source points must be sorted by X.
|
||||
*/
|
||||
export function selectRenderablePoints(
|
||||
function selectRenderablePointsInRange(
|
||||
points: WaveformPoint[],
|
||||
range: VisiblePointRange,
|
||||
domain: [number, number],
|
||||
width: number,
|
||||
options: ResolvedWaveformRenderingOptions,
|
||||
): WaveformPoint[] {
|
||||
if (!points.length || width <= 0) return []
|
||||
|
||||
const domainStart = Math.min(domain[0], domain[1])
|
||||
const domainEnd = Math.max(domain[0], domain[1])
|
||||
const visibleStart = pointBisector.left(points, domainStart)
|
||||
const visibleEnd = pointBisector.right(points, domainEnd)
|
||||
const start = Math.max(0, visibleStart - 1)
|
||||
const end = Math.min(points.length, visibleEnd + 1)
|
||||
const start = Math.max(0, range.start - 1)
|
||||
const end = Math.min(points.length, range.end + 1)
|
||||
const visibleCount = end - start
|
||||
if (visibleCount <= 0) return []
|
||||
if (!options.downsample || visibleCount <= options.downsampleThreshold) {
|
||||
@@ -82,22 +129,45 @@ export function selectRenderablePoints(
|
||||
|
||||
const result: WaveformPoint[] = []
|
||||
const span = domainEnd - domainStart || 1
|
||||
const bucketIndexes = Array.from({ length: 4 }, () => -1)
|
||||
let activeBucket = -1
|
||||
let firstIndex = -1
|
||||
let lastIndex = -1
|
||||
let minimumIndex = -1
|
||||
let maximumIndex = -1
|
||||
|
||||
const addBucketIndex = (index: number, count: number) => {
|
||||
if (index < 0) return count
|
||||
for (let position = 0; position < count; position += 1) {
|
||||
if (bucketIndexes[position] === index) return count
|
||||
}
|
||||
bucketIndexes[count] = index
|
||||
return count + 1
|
||||
}
|
||||
|
||||
const flushBucket = () => {
|
||||
if (firstIndex < 0) return
|
||||
const indexes = [firstIndex, minimumIndex, maximumIndex, lastIndex]
|
||||
.filter((index, position, source) => index >= 0 && source.indexOf(index) === position)
|
||||
.sort((left, right) => left - right)
|
||||
indexes.forEach((index) => pushUniquePoint(result, points[index]))
|
||||
let count = 0
|
||||
count = addBucketIndex(firstIndex, count)
|
||||
count = addBucketIndex(minimumIndex, count)
|
||||
count = addBucketIndex(maximumIndex, count)
|
||||
count = addBucketIndex(lastIndex, count)
|
||||
for (let index = 1; index < count; index += 1) {
|
||||
const value = bucketIndexes[index]
|
||||
let position = index - 1
|
||||
while (position >= 0 && bucketIndexes[position] > value) {
|
||||
bucketIndexes[position + 1] = bucketIndexes[position]
|
||||
position -= 1
|
||||
}
|
||||
bucketIndexes[position + 1] = value
|
||||
}
|
||||
for (let index = 0; index < count; index += 1) {
|
||||
pushUniquePoint(result, points[bucketIndexes[index]])
|
||||
}
|
||||
}
|
||||
|
||||
pushUniquePoint(result, points[start])
|
||||
for (let index = Math.max(start, visibleStart); index < Math.min(end, visibleEnd); index += 1) {
|
||||
for (let index = range.start; index < range.end; index += 1) {
|
||||
const point = points[index]
|
||||
const bucket = Math.min(
|
||||
bucketCount - 1,
|
||||
@@ -121,33 +191,61 @@ export function selectRenderablePoints(
|
||||
return result
|
||||
}
|
||||
|
||||
/** Selects real source points for discrete decorations without using line-extrema sampling. */
|
||||
export function selectDecorationPoints(
|
||||
/**
|
||||
* Select the visible source range and preserve first/min/max/last values in each X bucket.
|
||||
* Source points must be sorted by X.
|
||||
*/
|
||||
export function selectRenderablePoints(
|
||||
points: WaveformPoint[],
|
||||
domain: [number, number],
|
||||
width: number,
|
||||
options: ResolvedWaveformRenderingOptions,
|
||||
): WaveformPoint[] {
|
||||
if (!points.length || width <= 0) return []
|
||||
return selectRenderablePointsInRange(
|
||||
points,
|
||||
resolveVisiblePointRange(points, domain),
|
||||
domain,
|
||||
width,
|
||||
options,
|
||||
)
|
||||
}
|
||||
|
||||
function selectDecorationPointsInRange(
|
||||
points: WaveformPoint[],
|
||||
range: VisiblePointRange,
|
||||
domain: [number, number],
|
||||
width: number,
|
||||
minSpacing: number,
|
||||
downsample: boolean,
|
||||
predicate: (point: WaveformPoint) => boolean = () => true,
|
||||
predicate: (point: WaveformPoint) => boolean,
|
||||
priorityPredicate?: (point: WaveformPoint) => boolean,
|
||||
): WaveformPoint[] {
|
||||
if (!points.length || width <= 0) return []
|
||||
if (!downsample || minSpacing === 0) {
|
||||
if (predicate === acceptAllPoints) return points.slice(range.start, range.end)
|
||||
const visiblePoints: WaveformPoint[] = []
|
||||
for (let index = range.start; index < range.end; index += 1) {
|
||||
if (predicate(points[index])) visiblePoints.push(points[index])
|
||||
}
|
||||
return visiblePoints
|
||||
}
|
||||
|
||||
const domainStart = Math.min(domain[0], domain[1])
|
||||
const domainEnd = Math.max(domain[0], domain[1])
|
||||
const visibleStart = pointBisector.left(points, domainStart)
|
||||
const visibleEnd = pointBisector.right(points, domainEnd)
|
||||
if (!downsample || minSpacing === 0) {
|
||||
return points.slice(visibleStart, visibleEnd).filter(predicate)
|
||||
}
|
||||
|
||||
const span = domainEnd - domainStart
|
||||
if (span <= 0) {
|
||||
const point = points.slice(visibleStart, visibleEnd).find(predicate)
|
||||
return point ? [point] : []
|
||||
for (let index = range.start; index < range.end; index += 1) {
|
||||
if (predicate(points[index])) return [points[index]]
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
const toPixel = (point: WaveformPoint) => ((point.x - domainStart) / span) * width
|
||||
const bucketCount = Math.max(1, Math.ceil(width / minSpacing))
|
||||
const bucketWidth = width / bucketCount
|
||||
let bucketPoints: Array<WaveformPoint | undefined> | undefined
|
||||
let bucketDistances: number[] | undefined
|
||||
let priorityBucketPoints: Array<WaveformPoint | undefined> | undefined
|
||||
let priorityBucketDistances: number[] | undefined
|
||||
const sparsePoints: WaveformPoint[] = []
|
||||
let alreadySparse = true
|
||||
let first: WaveformPoint | undefined
|
||||
@@ -155,44 +253,10 @@ export function selectDecorationPoints(
|
||||
let previousPixel = Number.NEGATIVE_INFINITY
|
||||
let candidateCount = 0
|
||||
|
||||
for (let index = visibleStart; index < visibleEnd; index += 1) {
|
||||
const point = points[index]
|
||||
if (!predicate(point)) continue
|
||||
first ??= point
|
||||
last = point
|
||||
candidateCount += 1
|
||||
if (!alreadySparse) continue
|
||||
const pixel = toPixel(point)
|
||||
if (pixel - previousPixel < minSpacing) {
|
||||
alreadySparse = false
|
||||
sparsePoints.length = 0
|
||||
continue
|
||||
const recordBucketPoint = (point: WaveformPoint, pixel: number) => {
|
||||
if (!bucketPoints || !bucketDistances || !priorityBucketPoints || !priorityBucketDistances) {
|
||||
return
|
||||
}
|
||||
sparsePoints.push(point)
|
||||
previousPixel = pixel
|
||||
}
|
||||
if (candidateCount <= 2) {
|
||||
if (!first) return []
|
||||
return last && last !== first ? [first, last] : [first]
|
||||
}
|
||||
if (alreadySparse) return sparsePoints
|
||||
|
||||
const bucketCount = Math.max(1, Math.ceil(width / minSpacing))
|
||||
const bucketWidth = width / bucketCount
|
||||
const bucketPoints: Array<WaveformPoint | undefined> = Array.from({ length: bucketCount })
|
||||
const bucketDistances = Array.from({ length: bucketCount }, () => Number.POSITIVE_INFINITY)
|
||||
const priorityBucketPoints: Array<WaveformPoint | undefined> = Array.from({
|
||||
length: bucketCount,
|
||||
})
|
||||
const priorityBucketDistances = Array.from(
|
||||
{ length: bucketCount },
|
||||
() => Number.POSITIVE_INFINITY,
|
||||
)
|
||||
|
||||
for (let index = visibleStart; index < visibleEnd; index += 1) {
|
||||
const point = points[index]
|
||||
if (!predicate(point)) continue
|
||||
const pixel = Math.max(0, Math.min(width, toPixel(point)))
|
||||
const bucket = Math.min(bucketCount - 1, Math.floor(pixel / bucketWidth))
|
||||
const center = (bucket + 0.5) * bucketWidth
|
||||
const distance = Math.abs(pixel - center)
|
||||
@@ -206,10 +270,133 @@ export function selectDecorationPoints(
|
||||
}
|
||||
}
|
||||
|
||||
const selected = bucketPoints
|
||||
.map((point, index) => priorityBucketPoints[index] ?? point)
|
||||
const initializeBuckets = () => {
|
||||
bucketPoints = Array.from({ length: bucketCount })
|
||||
bucketDistances = Array.from({ length: bucketCount }, () => Number.POSITIVE_INFINITY)
|
||||
priorityBucketPoints = Array.from({ length: bucketCount })
|
||||
priorityBucketDistances = Array.from({ length: bucketCount }, () => Number.POSITIVE_INFINITY)
|
||||
for (const point of sparsePoints) {
|
||||
const pixel = Math.max(0, Math.min(width, ((point.x - domainStart) / span) * width))
|
||||
recordBucketPoint(point, pixel)
|
||||
}
|
||||
}
|
||||
|
||||
for (let index = range.start; index < range.end; index += 1) {
|
||||
const point = points[index]
|
||||
if (!predicate(point)) continue
|
||||
first ??= point
|
||||
last = point
|
||||
candidateCount += 1
|
||||
const pixel = Math.max(0, Math.min(width, ((point.x - domainStart) / span) * width))
|
||||
if (alreadySparse) {
|
||||
if (pixel - previousPixel < minSpacing) {
|
||||
alreadySparse = false
|
||||
initializeBuckets()
|
||||
sparsePoints.length = 0
|
||||
} else {
|
||||
sparsePoints.push(point)
|
||||
previousPixel = pixel
|
||||
}
|
||||
}
|
||||
if (!alreadySparse) recordBucketPoint(point, pixel)
|
||||
}
|
||||
if (candidateCount <= 2) {
|
||||
if (!first) return []
|
||||
return last && last !== first ? [first, last] : [first]
|
||||
}
|
||||
if (alreadySparse) return sparsePoints
|
||||
|
||||
const selected = (bucketPoints ?? [])
|
||||
.map((point, index) => priorityBucketPoints?.[index] ?? point)
|
||||
.filter((point): point is WaveformPoint => point !== undefined)
|
||||
if (first && selected[0] !== first) selected.unshift(first)
|
||||
if (last && selected.at(-1) !== last) selected.push(last)
|
||||
return selected
|
||||
}
|
||||
|
||||
/** Selects real source points for discrete decorations without using line-extrema sampling. */
|
||||
export function selectDecorationPoints(
|
||||
points: WaveformPoint[],
|
||||
domain: [number, number],
|
||||
width: number,
|
||||
minSpacing: number,
|
||||
downsample: boolean,
|
||||
predicate: (point: WaveformPoint) => boolean = acceptAllPoints,
|
||||
priorityPredicate?: (point: WaveformPoint) => boolean,
|
||||
): WaveformPoint[] {
|
||||
if (!points.length || width <= 0) return []
|
||||
return selectDecorationPointsInRange(
|
||||
points,
|
||||
resolveVisiblePointRange(points, domain),
|
||||
domain,
|
||||
width,
|
||||
minSpacing,
|
||||
downsample,
|
||||
predicate,
|
||||
priorityPredicate,
|
||||
)
|
||||
}
|
||||
|
||||
function hasPointError(point: WaveformPoint): boolean {
|
||||
const { lower, upper } = resolveWaveformPointErrors(point)
|
||||
return lower !== 0 || upper !== 0
|
||||
}
|
||||
|
||||
export function selectSeriesRenderPoints(
|
||||
points: WaveformPoint[],
|
||||
domain: [number, number],
|
||||
width: number,
|
||||
rendering: ResolvedWaveformRenderingOptions,
|
||||
selection: SeriesRenderSelectionOptions,
|
||||
): SeriesRenderPointSelection {
|
||||
if (!points.length || width <= 0) {
|
||||
return { linePoints: [], pointRenderPoints: [], errorBarRenderPoints: [] }
|
||||
}
|
||||
const range = resolveVisiblePointRange(points, domain)
|
||||
const linePoints = selection.lineVisible
|
||||
? selectRenderablePointsInRange(points, range, domain, width, rendering)
|
||||
: []
|
||||
const errorBarVisible = selection.errorBarVisible && selection.hasErrorPoints
|
||||
if (selection.pointVisible && errorBarVisible) {
|
||||
const sharedPoints = selectDecorationPointsInRange(
|
||||
points,
|
||||
range,
|
||||
domain,
|
||||
width,
|
||||
Math.max(rendering.pointMinSpacing, rendering.errorBarMinSpacing),
|
||||
rendering.downsample,
|
||||
acceptAllPoints,
|
||||
hasPointError,
|
||||
)
|
||||
return {
|
||||
linePoints,
|
||||
pointRenderPoints: sharedPoints,
|
||||
errorBarRenderPoints: sharedPoints.filter(hasPointError),
|
||||
}
|
||||
}
|
||||
return {
|
||||
linePoints,
|
||||
pointRenderPoints: selection.pointVisible
|
||||
? selectDecorationPointsInRange(
|
||||
points,
|
||||
range,
|
||||
domain,
|
||||
width,
|
||||
rendering.pointMinSpacing,
|
||||
rendering.downsample,
|
||||
acceptAllPoints,
|
||||
)
|
||||
: [],
|
||||
errorBarRenderPoints: errorBarVisible
|
||||
? selectDecorationPointsInRange(
|
||||
points,
|
||||
range,
|
||||
domain,
|
||||
width,
|
||||
rendering.errorBarMinSpacing,
|
||||
rendering.downsample,
|
||||
hasPointError,
|
||||
)
|
||||
: [],
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user