waveform-analysis 0.1.71 → 0.1.73

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (122) hide show
  1. package/README.md +260 -40
  2. package/dist/assets/{workerRuntime-o5An0M-W.js → workerRuntime-5p-EI-GL.js} +1 -1
  3. package/dist/index.cjs +2 -1
  4. package/dist/index.js +3874 -2778
  5. package/dist/style.css +1 -1
  6. package/dist/types/components/WaveformChart.vue.d.ts +24 -6
  7. package/dist/types/components/annotation/WaveformAnnotationLayer.vue.d.ts +2 -0
  8. package/dist/types/components/annotation/useWaveformChartAnnotations.d.ts +0 -1
  9. package/dist/types/components/controls/WaveformToolbar.vue.d.ts +7 -0
  10. package/dist/types/components/controls/chartExportOptions.d.ts +5 -0
  11. package/dist/types/components/controls/chartSnapshot.d.ts +2 -0
  12. package/dist/types/components/controls/snapshotTitle.d.ts +2 -0
  13. package/dist/types/components/controls/toolbarRegistry.d.ts +21 -0
  14. package/dist/types/components/controls/useChartCommands.d.ts +42 -0
  15. package/dist/types/components/controls/useChartImageExport.d.ts +17 -0
  16. package/dist/types/components/controls/useControlMode.d.ts +8 -0
  17. package/dist/types/components/core/latestTaskScheduler.d.ts +1 -1
  18. package/dist/types/components/core/layout.d.ts +6 -3
  19. package/dist/types/components/core/niceScale.d.ts +34 -0
  20. package/dist/types/components/core/trackLayoutBuilder.d.ts +4 -1
  21. package/dist/types/components/core/trackPagination.d.ts +1 -1
  22. package/dist/types/components/core/types.d.ts +5 -0
  23. package/dist/types/components/core/useWaveformChartController.d.ts +24 -6
  24. package/dist/types/components/core/useWaveformChartLifecycle.d.ts +2 -2
  25. package/dist/types/components/core/useWaveformData.d.ts +2 -0
  26. package/dist/types/components/core/useWaveformLayout.d.ts +4 -1
  27. package/dist/types/components/core/useWaveformMeasurements.d.ts +2 -0
  28. package/dist/types/components/core/useWaveformRenderSampling.d.ts +1 -0
  29. package/dist/types/components/core/useWaveformViewportRestore.d.ts +5 -0
  30. package/dist/types/components/core/waveformChartTypes.d.ts +5 -0
  31. package/dist/types/components/core/waveformSamplingSession.d.ts +24 -0
  32. package/dist/types/components/core/yAxisLabelStyle.d.ts +3 -0
  33. package/dist/types/components/core/yAxisLayoutMetrics.d.ts +2 -1
  34. package/dist/types/components/core/yAxisPadding.d.ts +7 -0
  35. package/dist/types/components/core/yAxisTitleLayout.d.ts +2 -0
  36. package/dist/types/components/interaction/useExtremaTickViewport.d.ts +5 -0
  37. package/dist/types/components/interaction/useWaveformViewport.d.ts +1 -0
  38. package/dist/types/components/rendering/WaveformLegend.vue.d.ts +2 -2
  39. package/dist/types/components/rendering/WaveformTrack.vue.d.ts +2 -3
  40. package/dist/types/components/rendering/WaveformTrackAxes.vue.d.ts +1 -1
  41. package/dist/types/components/rendering/WaveformTrackBackdrop.vue.d.ts +1 -0
  42. package/dist/types/components/rendering/compactFrame.d.ts +5 -0
  43. package/dist/types/components/rendering/yAxisTickVisibility.d.ts +11 -0
  44. package/dist/types/core/lineBoundary.d.ts +4 -0
  45. package/dist/types/core/rendering.d.ts +4 -0
  46. package/dist/types/core/renderingStrategies.d.ts +2 -0
  47. package/dist/types/core/workerSampling/outputBoundaries.d.ts +4 -0
  48. package/dist/types/data/simulatedWaveforms.d.ts +1 -1
  49. package/dist/types/data/sx2Channel.d.ts +10 -0
  50. package/dist/types/demo/DemoYAxisPaddingControls.vue.d.ts +11 -0
  51. package/dist/types/demo/types.d.ts +7 -0
  52. package/dist/types/index.d.ts +2 -1
  53. package/dist/types/types/chart.d.ts +32 -8
  54. package/dist/types/types/controls.d.ts +69 -0
  55. package/dist/types/types/index.d.ts +2 -1
  56. package/dist/types/utils/formatters.d.ts +1 -0
  57. package/package.json +7 -6
  58. package/src/App.vue +61 -14
  59. package/src/components/WaveformChart.css +10 -1
  60. package/src/components/WaveformChart.vue +6 -0
  61. package/src/components/WaveformChartView.vue +27 -26
  62. package/src/components/annotation/WaveformAnnotationLayer.vue +6 -1
  63. package/src/components/annotation/useWaveformChartAnnotations.ts +7 -18
  64. package/src/components/controls/WaveformToolbar.vue +247 -0
  65. package/src/components/controls/chartExportOptions.ts +32 -0
  66. package/src/components/controls/chartSnapshot.ts +132 -0
  67. package/src/components/controls/snapshotTitle.ts +73 -0
  68. package/src/components/controls/toolbarRegistry.ts +92 -0
  69. package/src/components/controls/useChartCommands.ts +294 -0
  70. package/src/components/controls/useChartImageExport.ts +155 -0
  71. package/src/components/controls/useControlMode.ts +30 -0
  72. package/src/components/core/latestTaskScheduler.ts +14 -2
  73. package/src/components/core/layout.ts +49 -36
  74. package/src/components/core/niceScale.ts +151 -0
  75. package/src/components/core/trackLayoutBuilder.ts +41 -17
  76. package/src/components/core/trackPagination.ts +3 -3
  77. package/src/components/core/types.ts +5 -0
  78. package/src/components/core/useWaveformChartController.ts +94 -3
  79. package/src/components/core/useWaveformChartLifecycle.ts +11 -111
  80. package/src/components/core/useWaveformData.ts +4 -0
  81. package/src/components/core/useWaveformLayout.ts +25 -5
  82. package/src/components/core/useWaveformMeasurements.ts +63 -0
  83. package/src/components/core/useWaveformRenderSampling.ts +80 -59
  84. package/src/components/core/useWaveformViewportRestore.ts +105 -0
  85. package/src/components/core/useWaveformYAxisLayout.ts +3 -2
  86. package/src/components/core/waveformChartTypes.ts +5 -0
  87. package/src/components/core/waveformSamplingSession.ts +49 -0
  88. package/src/components/core/workerSamplingClient.ts +38 -6
  89. package/src/components/core/yAxisLabelStyle.ts +10 -0
  90. package/src/components/core/yAxisLayoutMetrics.ts +11 -9
  91. package/src/components/core/yAxisPadding.ts +60 -0
  92. package/src/components/core/yAxisTitleLayout.ts +11 -12
  93. package/src/components/interaction/useExtremaTickViewport.ts +43 -0
  94. package/src/components/interaction/useWaveformViewport.ts +11 -2
  95. package/src/components/rendering/WaveformTrack.css +1 -2
  96. package/src/components/rendering/WaveformTrack.vue +5 -12
  97. package/src/components/rendering/WaveformTrackAxes.vue +66 -19
  98. package/src/components/rendering/WaveformTrackBackdrop.vue +8 -3
  99. package/src/components/rendering/compactFrame.ts +31 -0
  100. package/src/components/rendering/yAxisTickVisibility.ts +156 -0
  101. package/src/core/lineBoundary.ts +24 -0
  102. package/src/core/rendering.ts +48 -25
  103. package/src/core/renderingStrategies.ts +19 -13
  104. package/src/core/waveformPointSource.ts +23 -1
  105. package/src/core/workerSampling/outputBoundaries.ts +32 -0
  106. package/src/core/workerSampling/repository.ts +26 -18
  107. package/src/core/workerSampling/repositoryDataset.ts +33 -1
  108. package/src/core/workerSampling/workerRuntime.ts +13 -3
  109. package/src/data/simulatedWaveforms.ts +67 -2
  110. package/src/data/sx2Channel.ts +315 -0
  111. package/src/demo/DemoChartHost.vue +5 -1
  112. package/src/demo/DemoGridFrameControls.vue +2 -0
  113. package/src/demo/DemoViewControls.vue +5 -2
  114. package/src/demo/DemoYAxisPaddingControls.vue +53 -0
  115. package/src/demo/embedBridge.ts +85 -0
  116. package/src/demo/types.ts +7 -0
  117. package/src/index.ts +19 -0
  118. package/src/types/chart.ts +34 -8
  119. package/src/types/controls.ts +80 -0
  120. package/src/types/index.ts +18 -0
  121. package/src/utils/formatters.ts +2 -1
  122. package/src/views/WasmSamplingDemo.vue +8 -1
@@ -1,12 +1,14 @@
1
1
  <script setup lang="ts">
2
2
  import { axisBottom, axisLeft, axisRight, select } from 'd3'
3
- import { nextTick, onMounted, ref, watch } from 'vue'
3
+ import { computed, nextTick, onMounted, onBeforeUnmount, ref, watch } from 'vue'
4
4
 
5
5
  import type { WaveformAxesOptions } from '../../types'
6
6
  import { formatXAxisLabel } from '../../utils'
7
7
  import { formatYAxisTickLabel } from '../core/layout'
8
8
  import type { DisplaySeries, TrackLayout, WaveformYAxisLayout } from '../core/types'
9
- import { Y_AXIS_LABEL_BAND_WIDTH } from '../core/yAxisConstants'
9
+ import { updateMaximumTickVisibility } from './yAxisTickVisibility'
10
+ import { sameTick } from '../core/yAxisPadding'
11
+ import { resolveYAxisLabelBandWidth, resolveYAxisLabelFontSize } from '../core/yAxisLabelStyle'
10
12
  import { formatChannelLabel } from './channelLabel'
11
13
 
12
14
  interface Props {
@@ -19,10 +21,20 @@ interface Props {
19
21
  showUnits?: boolean
20
22
  showLabelUnits?: boolean
21
23
  hideYAxisTitles?: boolean
22
- containYAxisEndpoints?: boolean
24
+ hideOverlappingMaximumLabel?: boolean
23
25
  }
24
26
 
25
27
  const props = withDefaults(defineProps<Props>(), { showUnits: true })
28
+ const labelBandWidth = computed(() => resolveYAxisLabelBandWidth(props.axes?.y?.labelTextStyle))
29
+ const labelTextStyle = computed(() => {
30
+ const style = props.axes?.y?.labelTextStyle
31
+ return {
32
+ fontSize: `${resolveYAxisLabelFontSize(style)}px`,
33
+ fontWeight: style?.fontWeight ?? 600,
34
+ fontFamily: style?.fontFamily,
35
+ fontStyle: style?.fontStyle ?? 'normal',
36
+ }
37
+ })
26
38
  const xAxisElement = ref<SVGGElement>()
27
39
  const yAxisElements = ref<SVGGElement[]>([])
28
40
 
@@ -45,6 +57,7 @@ function renderAxes() {
45
57
  const element = yAxisElements.value[index]
46
58
  if (!element) return
47
59
  const unit = props.showUnits ? axis.seriesList[0]?.unit : undefined
60
+ const highestTick = Math.max(...axis.tickValues)
48
61
  const yAxis = (axis.side === 'left' ? axisLeft(axis.scale) : axisRight(axis.scale))
49
62
  .tickFormat((value) =>
50
63
  formatYAxisTickLabel(
@@ -62,24 +75,39 @@ function renderAxes() {
62
75
 
63
76
  const selection = select(element)
64
77
  selection.call(yAxis)
65
- const lowestTick = Math.min(...axis.tickValues)
66
- const highestTick = props.containYAxisEndpoints ? Math.max(...axis.tickValues) : undefined
67
78
  selection
68
79
  .selectAll<SVGTextElement, number>('.tick text')
69
80
  .attr('y', null)
70
- .attr('dy', (value) => (value === lowestTick || value === highestTick ? '0' : '0.32em'))
71
- .attr('dominant-baseline', (value) =>
72
- value === lowestTick
73
- ? 'text-after-edge'
74
- : value === highestTick
75
- ? 'text-before-edge'
76
- : null,
81
+ .attr('dy', '0')
82
+ .attr('dominant-baseline', 'central')
83
+ .attr('data-axis-maximum-label', (value) =>
84
+ props.hideOverlappingMaximumLabel && sameTick(value, highestTick) ? 'true' : null,
85
+ )
86
+ .attr('data-axis-minimum-label', (value) =>
87
+ props.hideOverlappingMaximumLabel && sameTick(value, Math.min(...axis.tickValues))
88
+ ? 'true'
89
+ : null,
77
90
  )
91
+ selection
92
+ .attr('data-label-domain-min', axis.scale.domain()[0])
93
+ .attr('data-label-domain-max', axis.scale.domain()[1])
94
+ .attr('data-label-unit', unit ?? null)
95
+ selection
96
+ .selectAll<SVGGElement, number>('.tick')
97
+ .attr('data-maximum-tick', (value) =>
98
+ axis.maximumTick !== undefined && sameTick(value, axis.maximumTick) ? 'true' : null,
99
+ )
100
+ .attr('data-minimum-tick', (value) =>
101
+ axis.minimumTick !== undefined && sameTick(value, axis.minimumTick) ? 'true' : null,
102
+ )
103
+ .attr('data-tick-value', (value) => value)
104
+ .attr('data-tick-y', (value) => axis.scale(value))
78
105
  selection
79
106
  .selectAll('path.domain')
80
107
  .attr('display', props.axes?.y?.lineVisible === false ? 'none' : null)
81
108
  })
82
109
 
110
+ scheduleVisibility()
83
111
  if (!xAxisElement.value) return
84
112
  const selection = select(xAxisElement.value)
85
113
  selection.call(
@@ -103,7 +131,24 @@ function renderAxes() {
103
131
  .attr('display', props.axes?.x?.lineVisible === false ? 'none' : null)
104
132
  }
105
133
 
134
+ let disposed = false
135
+ function scheduleVisibility() {
136
+ if (disposed) return
137
+ const apply = () => {
138
+ const svg = yAxisElements.value[0]?.ownerSVGElement
139
+ if (svg) updateMaximumTickVisibility(svg)
140
+ }
141
+ apply()
142
+ }
143
+ onBeforeUnmount(() => {
144
+ disposed = true
145
+ document.fonts?.removeEventListener?.('loadingdone', scheduleVisibility)
146
+ })
106
147
  onMounted(async () => {
148
+ document.fonts?.addEventListener?.('loadingdone', scheduleVisibility)
149
+ void document.fonts?.ready.then(() => {
150
+ if (!disposed) scheduleVisibility()
151
+ })
107
152
  await nextTick()
108
153
  renderAxes()
109
154
  })
@@ -116,10 +161,10 @@ watch(
116
161
  () => props.track.yAxisTickValues,
117
162
  () => props.timeUnit,
118
163
  () => props.showUnits,
119
- () => props.containYAxisEndpoints,
120
164
  () => props.axes?.x?.labelFormatter,
121
165
  () => props.axes?.x?.lineVisible,
122
166
  () => props.axes?.y?.lineVisible,
167
+ () => props.hideOverlappingMaximumLabel,
123
168
  ],
124
169
  async () => {
125
170
  await nextTick()
@@ -191,15 +236,16 @@ watch(
191
236
  >
192
237
  <rect
193
238
  class="waveform-track__y-axis-label-bg waveform-chart__y-axis-label-bg"
194
- :x="track.yAxes[0].labelX - Y_AXIS_LABEL_BAND_WIDTH / 2"
239
+ :x="track.yAxes[0].labelX - labelBandWidth / 2"
195
240
  :y="track.height / 2 - 40"
196
- :width="Y_AXIS_LABEL_BAND_WIDTH"
241
+ :width="labelBandWidth"
197
242
  height="80"
198
243
  rx="2"
199
244
  />
200
245
  <text
201
246
  class="waveform-track__y-axis-label waveform-chart__y-axis-label"
202
- :fill="track.series?.color"
247
+ :fill="axes?.y?.labelTextStyle?.color || track.series?.color"
248
+ :style="labelTextStyle"
203
249
  :transform="`translate(${track.yAxes[0].labelX}, ${track.height / 2}) rotate(-90)`"
204
250
  text-anchor="middle"
205
251
  dominant-baseline="central"
@@ -218,15 +264,16 @@ watch(
218
264
  >
219
265
  <rect
220
266
  class="waveform-track__y-axis-label-bg waveform-chart__y-axis-label-bg"
221
- :x="axis.labelX - Y_AXIS_LABEL_BAND_WIDTH / 2"
267
+ :x="axis.labelX - labelBandWidth / 2"
222
268
  :y="track.height / 2 - 40"
223
- :width="Y_AXIS_LABEL_BAND_WIDTH"
269
+ :width="labelBandWidth"
224
270
  height="80"
225
271
  rx="2"
226
272
  />
227
273
  <text
228
274
  class="waveform-track__y-axis-label waveform-chart__y-axis-label"
229
- :fill="axis.seriesList[0].color"
275
+ :fill="axes?.y?.labelTextStyle?.color || axis.seriesList[0].color"
276
+ :style="labelTextStyle"
230
277
  :transform="`translate(${axis.labelX}, ${track.height / 2}) rotate(-90)`"
231
278
  text-anchor="middle"
232
279
  dominant-baseline="central"
@@ -6,6 +6,7 @@ interface Props {
6
6
  clipPathId: string
7
7
  innerWidth: number
8
8
  cleanView: boolean
9
+ showEmptyFrame?: boolean
9
10
  frameNumber?: string | number
10
11
  frameStyle: {
11
12
  backgroundColor: string
@@ -17,7 +18,7 @@ defineProps<Props>()
17
18
 
18
19
  <template>
19
20
  <rect
20
- v-if="!track.isEmpty && !cleanView"
21
+ v-if="(!track.isEmpty || showEmptyFrame) && !cleanView"
21
22
  class="waveform-track__plot-background waveform-chart__plot-background"
22
23
  :width="track.width ?? innerWidth"
23
24
  :height="track.height"
@@ -26,7 +27,7 @@ defineProps<Props>()
26
27
  />
27
28
 
28
29
  <g
29
- v-if="!track.isEmpty && !cleanView"
30
+ v-if="(!track.isEmpty || showEmptyFrame) && !cleanView"
30
31
  :clip-path="`url(#${clipPathId}-${track.index})`"
31
32
  aria-hidden="true"
32
33
  >
@@ -89,7 +90,11 @@ defineProps<Props>()
89
90
  </g>
90
91
 
91
92
  <text
92
- v-if="!track.isEmpty && track.hasVisibleSeries && frameNumber !== undefined && !cleanView"
93
+ v-if="
94
+ (track.hasVisibleSeries || (track.isEmpty && showEmptyFrame)) &&
95
+ frameNumber !== undefined &&
96
+ !cleanView
97
+ "
93
98
  class="waveform-track__watermark waveform-chart__watermark"
94
99
  :x="(track.width ?? innerWidth) / 2"
95
100
  :y="track.height / 2"
@@ -0,0 +1,31 @@
1
+ import type { TrackLayout } from '../core/types'
2
+
3
+ type Frame = Pick<TrackLayout, 'left' | 'top' | 'width' | 'height' | 'isEmpty'>
4
+
5
+ /** Keep the lower frame's top edge as the single owner of a shared boundary. */
6
+ export function compactFramePath(track: Frame, tracks: Frame[], inset = 0): string | undefined {
7
+ let uncovered: [number, number][] = [[0, track.width]]
8
+ for (const below of tracks) {
9
+ if (below === track) continue
10
+ if (Math.abs(track.top + track.height - below.top) > 1e-6) continue
11
+ const start = Math.max(0, below.left - track.left)
12
+ const end = Math.min(track.width, below.left + below.width - track.left)
13
+ if (end <= start) continue
14
+ uncovered = uncovered.flatMap(([left, right]): [number, number][] => {
15
+ if (end <= left || start >= right) return [[left, right]]
16
+ const parts: [number, number][] = []
17
+ if (left < start) parts.push([left, start])
18
+ if (right > end) parts.push([end, right])
19
+ return parts
20
+ })
21
+ }
22
+ if (uncovered.length === 1 && uncovered[0][0] === 0 && uncovered[0][1] === track.width)
23
+ return undefined
24
+ inset = Number.isFinite(inset) ? Math.max(0, inset) : 0
25
+ const rightEdge = Math.max(inset, track.width - inset)
26
+ const bottomEdge = Math.max(inset, track.height - inset)
27
+ const bottom = uncovered
28
+ .map(([left, right]) => `M${Math.max(inset, left)},${bottomEdge}H${Math.min(rightEdge, right)}`)
29
+ .join('')
30
+ return `M${inset},${bottomEdge}V${inset}H${rightEdge}V${bottomEdge}${bottom}`
31
+ }
@@ -0,0 +1,156 @@
1
+ import { sameTick } from '../core/yAxisPadding'
2
+ import { formatYAxisTickLabel } from '../core/layout'
3
+
4
+ interface Box {
5
+ left: number
6
+ right: number
7
+ top: number
8
+ bottom: number
9
+ }
10
+
11
+ function textBox(text: SVGTextElement): Box {
12
+ const rect = text.getBoundingClientRect()
13
+ if (rect.width && rect.height) return rect
14
+ const height = Number.parseFloat(getComputedStyle(text).fontSize) || 11
15
+ const width = (text.textContent?.length ?? 0) * height * 0.65
16
+ let x = Number(text.getAttribute('x')) || 0
17
+ let y = Number(text.getAttribute('y')) || 0
18
+ for (let parent: Element | null = text.parentElement; parent; parent = parent.parentElement) {
19
+ const match = parent
20
+ .getAttribute('transform')
21
+ ?.match(/translate\(([-\d.e+]+)[ ,]+([-\d.e+]+)\)/)
22
+ if (match) {
23
+ x += Number(match[1])
24
+ y += Number(match[2])
25
+ }
26
+ }
27
+ const startAligned = text.getAttribute('text-anchor') === 'start'
28
+ return {
29
+ left: startAligned ? x : x - width,
30
+ right: startAligned ? x + width : x,
31
+ top: y - height / 2,
32
+ bottom: y + height / 2,
33
+ }
34
+ }
35
+
36
+ export function labelsOverlap(a: Box, b: Box): boolean {
37
+ return a.left < b.right && b.left < a.right && a.top < b.bottom + 4 && b.top < a.bottom + 4
38
+ }
39
+
40
+ export function linesOverlap(a: number, b: number, widthA = 1, widthB = 1): boolean {
41
+ return Math.abs(a - b) < (widthA + widthB) / 2 + 1
42
+ }
43
+
44
+ function hide(element: Element) {
45
+ element.setAttribute('display', 'none')
46
+ element.setAttribute('data-maximum-hidden', 'true')
47
+ }
48
+
49
+ function strokeWidth(element: Element): number {
50
+ return Number.parseFloat(getComputedStyle(element).strokeWidth) || 1
51
+ }
52
+
53
+ function formatCompactLabels(svg: SVGSVGElement) {
54
+ svg.querySelectorAll<SVGTextElement>('text[data-axis-maximum-label]').forEach((maximum) => {
55
+ const axis = maximum.parentElement!.parentElement!
56
+ const labels = [...axis.querySelectorAll<SVGTextElement>('.tick text')]
57
+ const value = (text: SVGTextElement) =>
58
+ Number(text.parentElement!.getAttribute('data-tick-value'))
59
+ const visibleValues = labels
60
+ .filter((text) => text.getAttribute('display') !== 'none')
61
+ .map(value)
62
+ const domain: [number, number] = [
63
+ Number(axis.getAttribute('data-label-domain-min')),
64
+ Number(axis.getAttribute('data-label-domain-max')),
65
+ ]
66
+ labels.forEach((text) => {
67
+ text.textContent = formatYAxisTickLabel(
68
+ value(text),
69
+ domain,
70
+ visibleValues,
71
+ axis.getAttribute('data-label-unit') ?? undefined,
72
+ axis.getAttribute('data-y-axis-side') === 'right' ? 'right' : 'left',
73
+ )
74
+ })
75
+ })
76
+ }
77
+
78
+ /** Apply after axes and grid are in the DOM; never feed the filtered set back into layout. */
79
+ export function updateMaximumTickVisibility(svg: SVGSVGElement) {
80
+ svg.querySelectorAll('[data-maximum-hidden]').forEach((el) => {
81
+ el.removeAttribute('display')
82
+ el.removeAttribute('data-maximum-hidden')
83
+ })
84
+ svg.querySelectorAll('.waveform-track__axis--y .tick text').forEach((text) => {
85
+ text.removeAttribute('display')
86
+ })
87
+ formatCompactLabels(svg)
88
+ const minimumLabels = [...svg.querySelectorAll<SVGTextElement>('text[data-axis-minimum-label]')]
89
+ svg.querySelectorAll<SVGTextElement>('text[data-axis-maximum-label]').forEach((maximum) => {
90
+ const box = textBox(maximum)
91
+ if (minimumLabels.some((minimum) => labelsOverlap(box, textBox(minimum)))) hide(maximum)
92
+ })
93
+ svg.querySelectorAll<SVGGElement>('.waveform-track__axis--y .tick').forEach((tick) => {
94
+ const position = Number(tick.getAttribute('data-tick-y'))
95
+ const height = Number(tick.closest('.waveform-track')?.getAttribute('data-track-height'))
96
+ const line = tick.querySelector('line')
97
+ if (sameTick(position, 0) || sameTick(position, height)) line?.setAttribute('display', 'none')
98
+ else line?.removeAttribute('display')
99
+ })
100
+ const minimumTicks = [...svg.querySelectorAll<SVGGElement>('.tick[data-minimum-tick]')]
101
+ const maximumTicks = [...svg.querySelectorAll<SVGGElement>('.tick[data-maximum-tick]')]
102
+ const labels = [...svg.querySelectorAll<SVGTextElement>('.waveform-track__axis--y .tick text')]
103
+ minimumTicks.forEach((tick) => {
104
+ const text = tick.querySelector('text')
105
+ if (!text) return
106
+ maximumTicks.forEach((maximum) => {
107
+ const other = maximum.querySelector('text')
108
+ if (other && other !== text && labelsOverlap(textBox(text), textBox(other))) hide(other)
109
+ })
110
+ })
111
+ const extremeTicks = [...new Set([...minimumTicks, ...maximumTicks])]
112
+ extremeTicks.forEach((tick) => {
113
+ const text = tick.querySelector('text')
114
+ const line = tick.querySelector('line')
115
+ if (!text || !line) return
116
+ const box = textBox(text)
117
+ labels.forEach((other) => {
118
+ if (text.getAttribute('display') === 'none') return
119
+ if (other.getAttribute('display') === 'none') return
120
+ if (other.parentElement?.matches('[data-minimum-tick], [data-maximum-tick]')) return
121
+ if (!tick.hasAttribute('data-minimum-tick') && other.hasAttribute('data-axis-minimum-label'))
122
+ return
123
+ if (labelsOverlap(box, textBox(other))) hide(other)
124
+ })
125
+ const axis = tick.parentElement!
126
+ const value = Number(tick.getAttribute('data-tick-value'))
127
+ const position = Number(tick.getAttribute('data-tick-y'))
128
+ axis
129
+ .querySelectorAll<SVGGElement>('.tick:not([data-maximum-tick]):not([data-minimum-tick])')
130
+ .forEach((other) => {
131
+ const otherLine = other.querySelector('line')
132
+ if (
133
+ otherLine &&
134
+ linesOverlap(
135
+ position,
136
+ Number(other.getAttribute('data-tick-y')),
137
+ strokeWidth(line),
138
+ strokeWidth(otherLine),
139
+ )
140
+ )
141
+ hide(otherLine)
142
+ })
143
+ // Only the primary Y axis owns the existing horizontal grid.
144
+ if (axis.getAttribute('data-y-axis-index') !== '0') return
145
+ const track = axis.closest('.waveform-track')!
146
+ track.querySelectorAll('[data-grid-direction="horizontal"]').forEach((gridLine) => {
147
+ const gridY = Number(gridLine.getAttribute('y1'))
148
+ if (linesOverlap(position, gridY, strokeWidth(line), strokeWidth(gridLine))) hide(gridLine)
149
+ })
150
+ if (tick.hasAttribute('data-maximum-tick'))
151
+ tick.setAttribute('data-maximum-value', String(value))
152
+ if (tick.hasAttribute('data-minimum-tick'))
153
+ tick.setAttribute('data-minimum-value', String(value))
154
+ })
155
+ formatCompactLabels(svg)
156
+ }
@@ -0,0 +1,24 @@
1
+ import type { WaveformPointRange } from './waveformPointSource'
2
+
3
+ /** Outside neighbors join the curve without changing visible-point statistics. */
4
+ export function lineBoundaryIndexes(range: WaveformPointRange, length: number): number[] {
5
+ if (!length || (range.start === range.end && (range.start === 0 || range.end === length)))
6
+ return []
7
+ return [range.start > 0 ? range.start - 1 : -1, range.end < length ? range.end : -1].filter(
8
+ (index) => index >= 0,
9
+ )
10
+ }
11
+
12
+ export function withLineBoundaries<T>(
13
+ points: T[],
14
+ range: WaveformPointRange,
15
+ length: number,
16
+ pointAt: (index: number) => T,
17
+ ): T[] {
18
+ const boundaries = lineBoundaryIndexes(range, length)
19
+ return [
20
+ ...boundaries.filter((index) => index < range.start).map(pointAt),
21
+ ...points,
22
+ ...boundaries.filter((index) => index >= range.end).map(pointAt),
23
+ ]
24
+ }
@@ -1,3 +1,5 @@
1
+ import { pointSourceFromPoints, type WaveformPointSource } from './waveformPointSource'
2
+ import { withLineBoundaries } from './lineBoundary'
1
3
  import { bisector } from 'd3'
2
4
 
3
5
  import type { WaveformPoint } from '../types'
@@ -20,6 +22,7 @@ const acceptAllPoints = () => true
20
22
 
21
23
  interface PointSeriesSource {
22
24
  points: WaveformPoint[]
25
+ source?: WaveformPointSource
23
26
  }
24
27
 
25
28
  interface SeriesRenderSelectionOptions {
@@ -58,9 +61,10 @@ export function hasMinimumVisibleXValues(
58
61
  const required = Math.ceil(minimum)
59
62
  const xValues = new Set<number>()
60
63
  for (const series of seriesList) {
61
- const range = resolveVisiblePointRange(series.points, domain)
64
+ const source = series.source ?? pointSourceFromPoints(series.points)
65
+ const range = source.visibleRange(domain)
62
66
  for (let index = range.start; index < range.end; index += 1) {
63
- xValues.add(series.points[index].x)
67
+ xValues.add(source.pointAt(index)!.x)
64
68
  if (xValues.size >= required) return true
65
69
  }
66
70
  }
@@ -68,18 +72,20 @@ export function hasMinimumVisibleXValues(
68
72
  }
69
73
 
70
74
  function selectRenderablePointsInRange(
71
- points: WaveformPoint[],
75
+ points: WaveformPointSource,
72
76
  range: VisiblePointRange,
73
77
  domain: [number, number],
74
78
  width: number,
75
79
  options: ResolvedWaveformRenderingOptions,
76
80
  ): WaveformPoint[] {
81
+ if (range.start === range.end && (range.start === 0 || range.end === points.length)) return []
77
82
  const start = Math.max(0, range.start - 1)
78
83
  const end = Math.min(points.length, range.end + 1)
79
84
  const visibleCount = end - start
80
85
  if (visibleCount <= 0) return []
81
86
  return resolveRenderablePointSelectionStrategy({ visibleCount, width, options })({
82
- points,
87
+ points: [],
88
+ source: points,
83
89
  range,
84
90
  domain,
85
91
  width,
@@ -96,25 +102,25 @@ function shouldDeferLineSampling(
96
102
  }
97
103
 
98
104
  function selectSamplingPlaceholderPoints(
99
- points: WaveformPoint[],
105
+ points: WaveformPointSource,
100
106
  range: VisiblePointRange,
101
107
  width: number,
102
108
  options: ResolvedWaveformRenderingOptions,
103
109
  ): WaveformPoint[] {
104
- const start = Math.max(0, range.start - 1)
105
- const end = Math.min(points.length, range.end + 1)
110
+ const { start, end } = range
106
111
  const count = end - start
107
- if (count <= 0) return []
108
- if (options.sampling.strategy === 'none') return points.slice(start, end)
109
112
  const target =
110
113
  options.sampling.maxPointCount ??
111
114
  Math.max(1, Math.floor(width * options.sampling.maxPointsPerPixel))
112
- if (count <= target) return points.slice(start, end)
113
- if (target === 1) return [points[start]!]
114
- return Array.from({ length: target }, (_, index) => {
115
- const offset = Math.round((index * (count - 1)) / (target - 1))
116
- return points[start + offset]!
117
- })
115
+ const interior =
116
+ count <= target || options.sampling.strategy === 'none'
117
+ ? points.pointsInRange(start, end)
118
+ : Array.from({ length: target }, (_, index) =>
119
+ points.pointAt(
120
+ start + (target === 1 ? 0 : Math.round((index * (count - 1)) / (target - 1))),
121
+ )!,
122
+ )
123
+ return withLineBoundaries(interior, range, points.length, (index) => points.pointAt(index)!)
118
124
  }
119
125
 
120
126
  /**
@@ -129,7 +135,7 @@ export function selectRenderablePoints(
129
135
  ): WaveformPoint[] {
130
136
  if (!points.length || width <= 0) return []
131
137
  return selectRenderablePointsInRange(
132
- points,
138
+ pointSourceFromPoints(points),
133
139
  resolveVisiblePointRange(points, domain),
134
140
  domain,
135
141
  width,
@@ -138,7 +144,7 @@ export function selectRenderablePoints(
138
144
  }
139
145
 
140
146
  function selectDecorationPointsInRange(
141
- points: WaveformPoint[],
147
+ points: WaveformPointSource,
142
148
  range: VisiblePointRange,
143
149
  domain: [number, number],
144
150
  width: number,
@@ -148,10 +154,10 @@ function selectDecorationPointsInRange(
148
154
  priorityPredicate?: (point: WaveformPoint) => boolean,
149
155
  ): WaveformPoint[] {
150
156
  if (!downsample || minSpacing === 0) {
151
- if (predicate === acceptAllPoints) return points.slice(range.start, range.end)
157
+ if (predicate === acceptAllPoints) return points.pointsInRange(range.start, range.end)
152
158
  const visiblePoints: WaveformPoint[] = []
153
159
  for (let index = range.start; index < range.end; index += 1) {
154
- if (predicate(points[index])) visiblePoints.push(points[index])
160
+ if (predicate(points.pointAt(index)!)) visiblePoints.push(points.pointAt(index)!)
155
161
  }
156
162
  return visiblePoints
157
163
  }
@@ -161,7 +167,7 @@ function selectDecorationPointsInRange(
161
167
  const span = domainEnd - domainStart
162
168
  if (span <= 0) {
163
169
  for (let index = range.start; index < range.end; index += 1) {
164
- if (predicate(points[index])) return [points[index]]
170
+ if (predicate(points.pointAt(index)!)) return [points.pointAt(index)!]
165
171
  }
166
172
  return []
167
173
  }
@@ -208,7 +214,7 @@ function selectDecorationPointsInRange(
208
214
  }
209
215
 
210
216
  for (let index = range.start; index < range.end; index += 1) {
211
- const point = points[index]
217
+ const point = points.pointAt(index)!
212
218
  if (!predicate(point)) continue
213
219
  first ??= point
214
220
  last = point
@@ -252,7 +258,7 @@ export function selectDecorationPoints(
252
258
  ): WaveformPoint[] {
253
259
  if (!points.length || width <= 0) return []
254
260
  return selectDecorationPointsInRange(
255
- points,
261
+ pointSourceFromPoints(points),
256
262
  resolveVisiblePointRange(points, domain),
257
263
  domain,
258
264
  width,
@@ -268,8 +274,8 @@ function hasPointError(point: WaveformPoint): boolean {
268
274
  return lower !== 0 || upper !== 0
269
275
  }
270
276
 
271
- export function selectSeriesRenderPoints(
272
- points: WaveformPoint[],
277
+ export function selectSeriesRenderSourcePoints(
278
+ points: WaveformPointSource,
273
279
  domain: [number, number],
274
280
  width: number,
275
281
  rendering: ResolvedWaveformRenderingOptions,
@@ -278,7 +284,7 @@ export function selectSeriesRenderPoints(
278
284
  if (!points.length || width <= 0) {
279
285
  return { linePoints: [], pointRenderPoints: [], errorBarRenderPoints: [] }
280
286
  }
281
- const range = resolveVisiblePointRange(points, domain)
287
+ const range = points.visibleRange(domain)
282
288
  const linePoints = selection.lineVisible
283
289
  ? (selection.linePointOverride ??
284
290
  (shouldDeferLineSampling(range.end - range.start, rendering)
@@ -329,3 +335,20 @@ export function selectSeriesRenderPoints(
329
335
  : [],
330
336
  }
331
337
  }
338
+
339
+ /** Public array adapter; internal layout passes its normalized source directly. */
340
+ export function selectSeriesRenderPoints(
341
+ points: WaveformPoint[],
342
+ domain: [number, number],
343
+ width: number,
344
+ rendering: ResolvedWaveformRenderingOptions,
345
+ selection: SeriesRenderSelectionOptions,
346
+ ): SeriesRenderPointSelection {
347
+ return selectSeriesRenderSourcePoints(
348
+ pointSourceFromPoints(points),
349
+ domain,
350
+ width,
351
+ rendering,
352
+ selection,
353
+ )
354
+ }