waveform-analysis 0.1.30 → 0.1.31

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 (41) hide show
  1. package/README.md +8 -0
  2. package/dist/index.cjs +1 -1
  3. package/dist/index.js +815 -601
  4. package/dist/style.css +1 -1
  5. package/dist/types/components/core/useWaveformChartController.d.ts +2 -14
  6. package/dist/types/components/core/useWaveformChartLifecycle.d.ts +4 -1
  7. package/dist/types/components/core/useWaveformData.d.ts +1 -1
  8. package/dist/types/components/core/waveformChartTypes.d.ts +6 -3
  9. package/dist/types/components/interaction/pointerCapture.d.ts +1 -0
  10. package/dist/types/components/interaction/viewportInteractionState.d.ts +39 -0
  11. package/dist/types/components/interaction/zoomConstraints.d.ts +14 -0
  12. package/dist/types/core/rendering.d.ts +1 -4
  13. package/dist/types/core/renderingStrategies.d.ts +22 -0
  14. package/dist/types/demo/types.d.ts +0 -1
  15. package/package.json +5 -1
  16. package/src/App.vue +0 -4
  17. package/src/components/WaveformChartView.vue +2 -2
  18. package/src/components/core/useWaveformChartController.ts +7 -2
  19. package/src/components/core/useWaveformChartLifecycle.ts +47 -3
  20. package/src/components/core/useWaveformData.ts +6 -1
  21. package/src/components/core/waveformChartTypes.ts +4 -3
  22. package/src/components/interaction/WaveformTooltip.test.ts +39 -0
  23. package/src/components/interaction/WaveformTooltip.vue +72 -22
  24. package/src/components/interaction/pointerCapture.ts +10 -0
  25. package/src/components/interaction/useWaveformHover.ts +1 -1
  26. package/src/components/interaction/useWaveformViewport.ts +86 -74
  27. package/src/components/interaction/useWaveformZoom.ts +46 -14
  28. package/src/components/interaction/viewportInteractionState.test.ts +101 -0
  29. package/src/components/interaction/viewportInteractionState.ts +89 -0
  30. package/src/components/interaction/zoomConstraints.ts +167 -0
  31. package/src/components/waveformChartCases/viewportLifecycle.test.ts +132 -0
  32. package/src/components/waveformChartCases/zoomPointConstraints.test.ts +248 -0
  33. package/src/core/rendering.ts +11 -81
  34. package/src/core/renderingStrategies.test.ts +90 -0
  35. package/src/core/renderingStrategies.ts +131 -0
  36. package/src/data/simulatedWaveforms.ts +9 -10
  37. package/src/demo/DemoChartHost.vue +1 -2
  38. package/src/demo/types.ts +0 -1
  39. package/src/utils/formatters.test.ts +4 -3
  40. package/src/utils/formatters.ts +1 -2
  41. package/src/utils/sampling.ts +2 -1
package/dist/style.css CHANGED
@@ -1,2 +1,2 @@
1
- .waveform-annotation-layer[data-v-7254383e]{pointer-events:none}.waveform-annotation-layer--interactive .waveform-annotation[data-v-7254383e]{pointer-events:auto;cursor:grab;touch-action:none}.waveform-annotation-layer--interactive .waveform-annotation[data-v-7254383e]:active{cursor:grabbing}.waveform-annotation__arrow[data-v-7254383e]{stroke-width:2px;stroke-linecap:round;pointer-events:none}.waveform-annotation__box[data-v-7254383e]{stroke-width:1px;rx:3}.waveform-annotation-layer--interactive .waveform-annotation__box[data-v-7254383e]{pointer-events:auto}.waveform-annotation__text[data-v-7254383e]{pointer-events:none;-webkit-user-select:none;user-select:none}.waveform-annotation-context-menu[data-v-c70c98a1]{z-index:30;background:#fff;border:1px solid #dfe5ef;border-radius:4px;min-width:112px;padding:4px;display:grid;position:absolute;box-shadow:0 4px 14px #10182829}.waveform-annotation-context-menu button[data-v-c70c98a1]{color:#344054;text-align:left;white-space:nowrap;cursor:pointer;background:0 0;border:0;border-radius:3px;padding:7px 10px;font-size:12px}.waveform-annotation-context-menu button[data-v-c70c98a1]:hover{background:#f2f4f7}.waveform-annotation-editor[data-v-52818b45]{z-index:40;background:#10182852;place-items:center;padding:20px;animation:.16s ease-out waveform-annotation-editor-fade-in-52818b45;display:grid;position:absolute;inset:0}.waveform-annotation-editor__panel[data-v-52818b45]{color:#344054;background:#fff;border:1px solid #e4e7ec;border-radius:12px;gap:20px;width:min(440px,100%);max-height:100%;padding:22px;font-size:13px;animation:.18s ease-out waveform-annotation-editor-rise-in-52818b45;display:grid;overflow-y:auto;box-shadow:0 20px 50px #10182838}.waveform-annotation-editor__header[data-v-52818b45]{justify-content:space-between;align-items:flex-start;gap:16px;display:flex}.waveform-annotation-editor__header h2[data-v-52818b45]{color:#101828;margin:0;font-size:18px;line-height:1.3}.waveform-annotation-editor__header p[data-v-52818b45]{color:#667085;margin:5px 0 0;font-size:12px}.waveform-annotation-editor__close[data-v-52818b45]{color:#667085;cursor:pointer;background:0 0;border:0;border-radius:6px;flex:0 0 30px;place-items:center;width:30px;height:30px;padding:0;font-size:22px;line-height:1;display:inline-grid}.waveform-annotation-editor__close[data-v-52818b45]:hover{color:#101828;background:#f2f4f7}.waveform-annotation-editor__coordinates[data-v-52818b45]{grid-template-columns:repeat(2,minmax(0,1fr));gap:8px;display:grid}.waveform-annotation-editor__coordinates span[data-v-52818b45],.waveform-annotation-editor__series[data-v-52818b45]{background:#f8fafc;border:1px solid #eaecf0;border-radius:7px;align-items:center;gap:8px;min-width:0;padding:9px 10px;display:flex}.waveform-annotation-editor__coordinates span[data-v-52818b45]{color:#667085}.waveform-annotation-editor__coordinates b[data-v-52818b45]{color:#1677ff;font-size:11px}.waveform-annotation-editor__coordinates code[data-v-52818b45]{color:#344054;text-overflow:ellipsis;white-space:nowrap;min-width:0;font-family:SFMono-Regular,Consolas,monospace;font-size:11px;overflow:hidden}.waveform-annotation-editor__series[data-v-52818b45]{gap:10px}.waveform-annotation-editor__series i[data-v-52818b45]{border:1px solid #0000001f;border-radius:2px;flex:0 0 12px;width:12px;height:12px}.waveform-annotation-editor__series-label[data-v-52818b45]{gap:1px;min-width:0;display:grid}.waveform-annotation-editor__series select[data-v-52818b45]{color:#344054;min-width:0;max-width:190px;font:inherit;background:#fff;border:1px solid #d0d5dd;border-radius:5px;margin-left:auto;padding:5px 7px}.waveform-annotation-editor__series small[data-v-52818b45]{color:#667085;font-size:10px}.waveform-annotation-editor__series b[data-v-52818b45]{color:#344054;text-overflow:ellipsis;white-space:nowrap;font-size:12px;overflow:hidden}.waveform-annotation-editor__series code[data-v-52818b45]{color:#667085;margin-left:auto;font-size:11px}.waveform-annotation-editor__field[data-v-52818b45],.waveform-annotation-editor__color-field[data-v-52818b45]{gap:4px;display:grid}.waveform-annotation-editor__label-row[data-v-52818b45]{justify-content:space-between;align-items:center;gap:8px;display:flex}.waveform-annotation-editor__label-row b[data-v-52818b45],.waveform-annotation-editor__colors legend[data-v-52818b45]{color:#344054;font-size:12px;font-weight:600}.waveform-annotation-editor__label-row small[data-v-52818b45]{color:#98a2b3;font-size:11px}.waveform-annotation-editor textarea[data-v-52818b45]{box-sizing:border-box;color:#1d2939;width:100%;min-height:92px;font:inherit;resize:vertical;background:#fff;border:1px solid #d0d5dd;border-radius:7px;padding:10px 11px;line-height:1.5}.waveform-annotation-editor textarea[data-v-52818b45]:focus{border-color:#1677ff;outline:3px solid #1677ff24}.waveform-annotation-editor__colors[data-v-52818b45]{border:0;border-top:1px solid #eaecf0;grid-template-columns:repeat(3,minmax(0,1fr));gap:12px;min-width:0;margin:0;padding:14px 0 0;display:grid}.waveform-annotation-editor__colors legend[data-v-52818b45]{grid-column:1/-1;padding:0}.waveform-annotation-editor__color-field[data-v-52818b45]{color:#667085;text-align:left;place-items:stretch stretch;min-width:0;font-size:11px}.waveform-annotation-editor__color-field[data-v-52818b45] .vc-color-wrap{border:1px solid #cfd5df;border-radius:6px;width:100%;height:34px;margin-right:0;box-shadow:inset 0 0 0 1px #ffffff61}.waveform-annotation-editor__color-field[data-v-52818b45] .vc-color-wrap:hover{border-color:#98a2b3;box-shadow:0 0 0 3px #1677ff1f}.waveform-annotation-editor__actions[data-v-52818b45]{border-top:1px solid #eaecf0;justify-content:flex-end;gap:8px;padding-top:8px;display:flex}.waveform-annotation-editor__actions button[data-v-52818b45]{color:#475467;cursor:pointer;background:#fff;border:1px solid #d0d5dd;border-radius:6px;height:34px;padding:0 12px}.waveform-annotation-editor__actions button.is-primary[data-v-52818b45]{color:#fff;background:#1677ff;border-color:#1677ff}.waveform-annotation-editor__actions button[data-v-52818b45]:disabled{opacity:.45;cursor:not-allowed}@keyframes waveform-annotation-editor-fade-in-52818b45{0%{opacity:0}to{opacity:1}}@keyframes waveform-annotation-editor-rise-in-52818b45{0%{opacity:0;transform:translateY(8px)scale(.98)}to{opacity:1;transform:translateY(0)scale(1)}}@media (width<=420px){.waveform-annotation-editor[data-v-52818b45]{padding:12px}.waveform-annotation-editor__panel[data-v-52818b45]{gap:16px;padding:18px}.waveform-annotation-editor__coordinates[data-v-52818b45]{grid-template-columns:1fr}}.waveform-tooltip[data-v-b8b5592a]{box-sizing:border-box;z-index:2;color:#333;pointer-events:none;background:#fff;border:1px solid #d9d9d9;border-radius:2px;gap:3px;min-width:180px;max-width:238px;padding:8px 10px;font:12px/1.45 ui-monospace,SFMono-Regular,Consolas,monospace;display:grid;position:absolute;box-shadow:0 4px 12px #1018281f}.waveform-tooltip__time[data-v-b8b5592a]{border-bottom:1px solid #f0f0f0;padding-bottom:4px}.waveform-tooltip__series[data-v-b8b5592a]{grid-template-columns:8px minmax(0,1fr) auto;align-items:center;gap:6px;display:grid}.waveform-tooltip__series i[data-v-b8b5592a]{border-radius:50%;width:8px;height:8px}.waveform-tooltip__series strong[data-v-b8b5592a]{text-overflow:ellipsis;white-space:nowrap;font-weight:600;overflow:hidden}.waveform-tooltip__value[data-v-b8b5592a]{white-space:nowrap}.waveform-tooltip__series small[data-v-b8b5592a]{color:#667085;white-space:nowrap}.waveform-track__line[data-v-42a3fd47]{fill:none;stroke-width:1.5px;stroke-linejoin:round;stroke-linecap:round}.waveform-track__error-bar[data-v-42a3fd47]{fill:none}.waveform-track__point[data-v-42a3fd47],.waveform-track__error-bar[data-v-42a3fd47]{pointer-events:none}.waveform-track{isolation:isolate;pointer-events:none}.waveform-track__y-axis-label-bg{fill:#fff;opacity:.9;pointer-events:none}.waveform-track__y-axis-label{pointer-events:none;font-size:12px;font-weight:500}.waveform-track__grid{pointer-events:none}.waveform-track__grid--major line{stroke:#dfe5ef;stroke-width:1px}.waveform-track__grid--minor line{stroke:#f2f5fa;stroke-width:1px}.waveform-track__plot-frame,.waveform-track__plot-background{pointer-events:none}.waveform-track__watermark{fill:#1677ff1a;pointer-events:none;-webkit-user-select:none;user-select:none;font-family:Consolas,Monaco,Courier New,monospace}.waveform-track__overlay{fill:#0000;cursor:crosshair;pointer-events:all;touch-action:none}.waveform-track__no-visible-series{fill:#8c8c8c;pointer-events:none;font:13px sans-serif}.waveform-track__overlay.is-zoomable{cursor:grab}.waveform-track__overlay.is-zoomable:active{cursor:grabbing}.waveform-track__overlay.is-annotating{cursor:crosshair}.waveform-track__overlay.is-presentation{cursor:default;pointer-events:none}.waveform-track__zero-line{fill:none;pointer-events:none}.waveform-track__axis-endpoint{fill:#667085;pointer-events:none;font-size:11px}.waveform-track__axis-exponent{fill:#666;font-family:sans-serif;font-size:10px}.waveform-track__axis path,.waveform-track__axis line{stroke:#1f2937}.waveform-track__axis text{fill:#667085;font-size:11px}.waveform-legend[data-v-2ae12e71]{pointer-events:none;overflow:hidden}.waveform-legend__viewport[data-v-2ae12e71]{box-sizing:border-box;width:100%;height:100%;padding:8px;display:flex;overflow:hidden}.waveform-legend__viewport--top-left[data-v-2ae12e71]{justify-content:flex-start;align-items:flex-start}.waveform-legend__viewport--top[data-v-2ae12e71]{justify-content:center;align-items:flex-start}.waveform-legend__viewport--top-right[data-v-2ae12e71]{justify-content:flex-end;align-items:flex-start}.waveform-legend__viewport--right[data-v-2ae12e71]{justify-content:flex-end;align-items:center}.waveform-legend__viewport--bottom-right[data-v-2ae12e71]{justify-content:flex-end;align-items:flex-end}.waveform-legend__viewport--bottom[data-v-2ae12e71]{justify-content:center;align-items:flex-end}.waveform-legend__viewport--bottom-left[data-v-2ae12e71]{justify-content:flex-start;align-items:flex-end}.waveform-legend__viewport--left[data-v-2ae12e71]{justify-content:flex-start;align-items:center}.waveform-legend__panel[data-v-2ae12e71]{color:#344054;border:1px solid #d0d5dde6;border-radius:4px;flex:0 auto;gap:5px 12px;max-width:100%;max-height:100%;padding:5px 7px;font:12px/1.35 sans-serif;display:flex;overflow:hidden}.waveform-legend__panel--interactive[data-v-2ae12e71]{pointer-events:auto}.waveform-legend__panel--horizontal[data-v-2ae12e71]{flex-flow:wrap;align-items:center}.waveform-legend__panel--vertical[data-v-2ae12e71]{flex-flow:column;align-items:stretch}.waveform-legend__item[data-v-2ae12e71]{min-width:0;max-width:160px;color:inherit;font:inherit;text-align:left;white-space:nowrap;appearance:none;background:0 0;border:0;align-items:center;gap:6px;padding:0;display:flex}.waveform-legend__item[data-v-2ae12e71]:disabled{opacity:1}.waveform-legend__panel--interactive .waveform-legend__item[data-v-2ae12e71]{cursor:pointer}.waveform-legend__panel--interactive .waveform-legend__item[data-v-2ae12e71]:focus-visible{outline-offset:2px;outline:2px solid #1677ff}.waveform-legend__item.is-hidden[data-v-2ae12e71]{opacity:.45}.waveform-legend__item.is-hidden .waveform-legend__label[data-v-2ae12e71]{text-decoration:line-through}.waveform-legend__swatch[data-v-2ae12e71]{stroke-linecap:round;stroke-linejoin:round;flex:0 0 26px;width:26px;height:16px;overflow:visible}.waveform-legend__label[data-v-2ae12e71]{text-overflow:ellipsis;min-width:0;overflow:hidden}.waveform-track__crosshair[data-v-a6f82099]{pointer-events:none}.waveform-track__crosshair line[data-v-a6f82099]{stroke:#57617b;stroke-width:1px;stroke-dasharray:4 3}.waveform-chart{box-sizing:border-box;color:#475467;background:#fff;border:1px solid #00000014;border-radius:6px;min-width:0;min-height:180px;position:relative;overflow:hidden}.waveform-chart--clean{border-color:#0000}.waveform-chart__pagination{z-index:2;position:absolute;bottom:6px;right:10px}.waveform-chart__title-area{background:#fff;align-items:center;width:100%;min-width:0;padding:0 24px;display:flex;position:relative;overflow:hidden}.waveform-chart__title-measure{visibility:hidden;pointer-events:none;display:block;position:absolute}.waveform-chart__title-visual{flex:none;min-width:0;position:relative;overflow:visible}.waveform-chart__title-text{transform-origin:50%;line-height:1.2;display:block;position:absolute;top:50%;left:50%;overflow:visible}.waveform-chart__pagination .ant-pagination-item,.waveform-chart__pagination .ant-pagination-prev .ant-pagination-item-link,.waveform-chart__pagination .ant-pagination-next .ant-pagination-item-link{background:#fff;border-color:#d9d9d9}.waveform-chart__pagination .ant-pagination-item-active{border-color:#1677ff}.waveform-chart__grid-slot-placeholder{fill:#fafbfc;stroke:#e4e7ec;stroke-dasharray:4 4;pointer-events:none}.waveform-chart__svg{max-width:100%;display:block}.waveform-chart__overlay{fill:#0000;cursor:crosshair;touch-action:none}.waveform-chart__overlay.is-zoomable{cursor:crosshair}.waveform-chart--panning .waveform-chart__overlay.is-zoomable{cursor:grabbing}.waveform-chart__zoom-selection{fill:#1677ff24;stroke:#1677ff;stroke-width:1px;pointer-events:none}.waveform-chart__overlay.is-annotating{cursor:crosshair}.waveform-chart--presentation .waveform-chart__overlay{cursor:default;pointer-events:none}.waveform-chart__label,.waveform-chart__empty{fill:#595959;font-size:12px}
1
+ .waveform-annotation-layer[data-v-7254383e]{pointer-events:none}.waveform-annotation-layer--interactive .waveform-annotation[data-v-7254383e]{pointer-events:auto;cursor:grab;touch-action:none}.waveform-annotation-layer--interactive .waveform-annotation[data-v-7254383e]:active{cursor:grabbing}.waveform-annotation__arrow[data-v-7254383e]{stroke-width:2px;stroke-linecap:round;pointer-events:none}.waveform-annotation__box[data-v-7254383e]{stroke-width:1px;rx:3}.waveform-annotation-layer--interactive .waveform-annotation__box[data-v-7254383e]{pointer-events:auto}.waveform-annotation__text[data-v-7254383e]{pointer-events:none;-webkit-user-select:none;user-select:none}.waveform-annotation-context-menu[data-v-c70c98a1]{z-index:30;background:#fff;border:1px solid #dfe5ef;border-radius:4px;min-width:112px;padding:4px;display:grid;position:absolute;box-shadow:0 4px 14px #10182829}.waveform-annotation-context-menu button[data-v-c70c98a1]{color:#344054;text-align:left;white-space:nowrap;cursor:pointer;background:0 0;border:0;border-radius:3px;padding:7px 10px;font-size:12px}.waveform-annotation-context-menu button[data-v-c70c98a1]:hover{background:#f2f4f7}.waveform-annotation-editor[data-v-52818b45]{z-index:40;background:#10182852;place-items:center;padding:20px;animation:.16s ease-out waveform-annotation-editor-fade-in-52818b45;display:grid;position:absolute;inset:0}.waveform-annotation-editor__panel[data-v-52818b45]{color:#344054;background:#fff;border:1px solid #e4e7ec;border-radius:12px;gap:20px;width:min(440px,100%);max-height:100%;padding:22px;font-size:13px;animation:.18s ease-out waveform-annotation-editor-rise-in-52818b45;display:grid;overflow-y:auto;box-shadow:0 20px 50px #10182838}.waveform-annotation-editor__header[data-v-52818b45]{justify-content:space-between;align-items:flex-start;gap:16px;display:flex}.waveform-annotation-editor__header h2[data-v-52818b45]{color:#101828;margin:0;font-size:18px;line-height:1.3}.waveform-annotation-editor__header p[data-v-52818b45]{color:#667085;margin:5px 0 0;font-size:12px}.waveform-annotation-editor__close[data-v-52818b45]{color:#667085;cursor:pointer;background:0 0;border:0;border-radius:6px;flex:0 0 30px;place-items:center;width:30px;height:30px;padding:0;font-size:22px;line-height:1;display:inline-grid}.waveform-annotation-editor__close[data-v-52818b45]:hover{color:#101828;background:#f2f4f7}.waveform-annotation-editor__coordinates[data-v-52818b45]{grid-template-columns:repeat(2,minmax(0,1fr));gap:8px;display:grid}.waveform-annotation-editor__coordinates span[data-v-52818b45],.waveform-annotation-editor__series[data-v-52818b45]{background:#f8fafc;border:1px solid #eaecf0;border-radius:7px;align-items:center;gap:8px;min-width:0;padding:9px 10px;display:flex}.waveform-annotation-editor__coordinates span[data-v-52818b45]{color:#667085}.waveform-annotation-editor__coordinates b[data-v-52818b45]{color:#1677ff;font-size:11px}.waveform-annotation-editor__coordinates code[data-v-52818b45]{color:#344054;text-overflow:ellipsis;white-space:nowrap;min-width:0;font-family:SFMono-Regular,Consolas,monospace;font-size:11px;overflow:hidden}.waveform-annotation-editor__series[data-v-52818b45]{gap:10px}.waveform-annotation-editor__series i[data-v-52818b45]{border:1px solid #0000001f;border-radius:2px;flex:0 0 12px;width:12px;height:12px}.waveform-annotation-editor__series-label[data-v-52818b45]{gap:1px;min-width:0;display:grid}.waveform-annotation-editor__series select[data-v-52818b45]{color:#344054;min-width:0;max-width:190px;font:inherit;background:#fff;border:1px solid #d0d5dd;border-radius:5px;margin-left:auto;padding:5px 7px}.waveform-annotation-editor__series small[data-v-52818b45]{color:#667085;font-size:10px}.waveform-annotation-editor__series b[data-v-52818b45]{color:#344054;text-overflow:ellipsis;white-space:nowrap;font-size:12px;overflow:hidden}.waveform-annotation-editor__series code[data-v-52818b45]{color:#667085;margin-left:auto;font-size:11px}.waveform-annotation-editor__field[data-v-52818b45],.waveform-annotation-editor__color-field[data-v-52818b45]{gap:4px;display:grid}.waveform-annotation-editor__label-row[data-v-52818b45]{justify-content:space-between;align-items:center;gap:8px;display:flex}.waveform-annotation-editor__label-row b[data-v-52818b45],.waveform-annotation-editor__colors legend[data-v-52818b45]{color:#344054;font-size:12px;font-weight:600}.waveform-annotation-editor__label-row small[data-v-52818b45]{color:#98a2b3;font-size:11px}.waveform-annotation-editor textarea[data-v-52818b45]{box-sizing:border-box;color:#1d2939;width:100%;min-height:92px;font:inherit;resize:vertical;background:#fff;border:1px solid #d0d5dd;border-radius:7px;padding:10px 11px;line-height:1.5}.waveform-annotation-editor textarea[data-v-52818b45]:focus{border-color:#1677ff;outline:3px solid #1677ff24}.waveform-annotation-editor__colors[data-v-52818b45]{border:0;border-top:1px solid #eaecf0;grid-template-columns:repeat(3,minmax(0,1fr));gap:12px;min-width:0;margin:0;padding:14px 0 0;display:grid}.waveform-annotation-editor__colors legend[data-v-52818b45]{grid-column:1/-1;padding:0}.waveform-annotation-editor__color-field[data-v-52818b45]{color:#667085;text-align:left;place-items:stretch stretch;min-width:0;font-size:11px}.waveform-annotation-editor__color-field[data-v-52818b45] .vc-color-wrap{border:1px solid #cfd5df;border-radius:6px;width:100%;height:34px;margin-right:0;box-shadow:inset 0 0 0 1px #ffffff61}.waveform-annotation-editor__color-field[data-v-52818b45] .vc-color-wrap:hover{border-color:#98a2b3;box-shadow:0 0 0 3px #1677ff1f}.waveform-annotation-editor__actions[data-v-52818b45]{border-top:1px solid #eaecf0;justify-content:flex-end;gap:8px;padding-top:8px;display:flex}.waveform-annotation-editor__actions button[data-v-52818b45]{color:#475467;cursor:pointer;background:#fff;border:1px solid #d0d5dd;border-radius:6px;height:34px;padding:0 12px}.waveform-annotation-editor__actions button.is-primary[data-v-52818b45]{color:#fff;background:#1677ff;border-color:#1677ff}.waveform-annotation-editor__actions button[data-v-52818b45]:disabled{opacity:.45;cursor:not-allowed}@keyframes waveform-annotation-editor-fade-in-52818b45{0%{opacity:0}to{opacity:1}}@keyframes waveform-annotation-editor-rise-in-52818b45{0%{opacity:0;transform:translateY(8px)scale(.98)}to{opacity:1;transform:translateY(0)scale(1)}}@media (width<=420px){.waveform-annotation-editor[data-v-52818b45]{padding:12px}.waveform-annotation-editor__panel[data-v-52818b45]{gap:16px;padding:18px}.waveform-annotation-editor__coordinates[data-v-52818b45]{grid-template-columns:1fr}}.waveform-tooltip[data-v-93daa641]{box-sizing:border-box;z-index:2;color:#333;pointer-events:none;background:#fff;border:1px solid #d9d9d9;border-radius:2px;gap:3px;width:max-content;max-width:min(320px,100% - 16px);padding:8px 10px;font:12px/1.45 ui-monospace,SFMono-Regular,Consolas,monospace;display:grid;position:absolute;box-shadow:0 4px 12px #1018281f}.waveform-tooltip__time[data-v-93daa641]{border-bottom:1px solid #f0f0f0;padding-bottom:4px}.waveform-tooltip__series[data-v-93daa641]{grid-template-columns:8px minmax(0,1fr);align-items:start;gap:6px;display:grid}.waveform-tooltip__series i[data-v-93daa641]{border-radius:50%;flex:none;width:8px;height:8px;margin-top:4px}.waveform-tooltip__series-content[data-v-93daa641]{overflow-wrap:anywhere;white-space:normal;min-width:0}.waveform-tooltip__series strong[data-v-93daa641]{font-weight:600}.waveform-tooltip__value[data-v-93daa641]{overflow-wrap:anywhere}.waveform-tooltip__series small[data-v-93daa641]{color:#667085}.waveform-track__line[data-v-42a3fd47]{fill:none;stroke-width:1.5px;stroke-linejoin:round;stroke-linecap:round}.waveform-track__error-bar[data-v-42a3fd47]{fill:none}.waveform-track__point[data-v-42a3fd47],.waveform-track__error-bar[data-v-42a3fd47]{pointer-events:none}.waveform-track{isolation:isolate;pointer-events:none}.waveform-track__y-axis-label-bg{fill:#fff;opacity:.9;pointer-events:none}.waveform-track__y-axis-label{pointer-events:none;font-size:12px;font-weight:500}.waveform-track__grid{pointer-events:none}.waveform-track__grid--major line{stroke:#dfe5ef;stroke-width:1px}.waveform-track__grid--minor line{stroke:#f2f5fa;stroke-width:1px}.waveform-track__plot-frame,.waveform-track__plot-background{pointer-events:none}.waveform-track__watermark{fill:#1677ff1a;pointer-events:none;-webkit-user-select:none;user-select:none;font-family:Consolas,Monaco,Courier New,monospace}.waveform-track__overlay{fill:#0000;cursor:crosshair;pointer-events:all;touch-action:none}.waveform-track__no-visible-series{fill:#8c8c8c;pointer-events:none;font:13px sans-serif}.waveform-track__overlay.is-zoomable{cursor:grab}.waveform-track__overlay.is-zoomable:active{cursor:grabbing}.waveform-track__overlay.is-annotating{cursor:crosshair}.waveform-track__overlay.is-presentation{cursor:default;pointer-events:none}.waveform-track__zero-line{fill:none;pointer-events:none}.waveform-track__axis-endpoint{fill:#667085;pointer-events:none;font-size:11px}.waveform-track__axis-exponent{fill:#666;font-family:sans-serif;font-size:10px}.waveform-track__axis path,.waveform-track__axis line{stroke:#1f2937}.waveform-track__axis text{fill:#667085;font-size:11px}.waveform-legend[data-v-2ae12e71]{pointer-events:none;overflow:hidden}.waveform-legend__viewport[data-v-2ae12e71]{box-sizing:border-box;width:100%;height:100%;padding:8px;display:flex;overflow:hidden}.waveform-legend__viewport--top-left[data-v-2ae12e71]{justify-content:flex-start;align-items:flex-start}.waveform-legend__viewport--top[data-v-2ae12e71]{justify-content:center;align-items:flex-start}.waveform-legend__viewport--top-right[data-v-2ae12e71]{justify-content:flex-end;align-items:flex-start}.waveform-legend__viewport--right[data-v-2ae12e71]{justify-content:flex-end;align-items:center}.waveform-legend__viewport--bottom-right[data-v-2ae12e71]{justify-content:flex-end;align-items:flex-end}.waveform-legend__viewport--bottom[data-v-2ae12e71]{justify-content:center;align-items:flex-end}.waveform-legend__viewport--bottom-left[data-v-2ae12e71]{justify-content:flex-start;align-items:flex-end}.waveform-legend__viewport--left[data-v-2ae12e71]{justify-content:flex-start;align-items:center}.waveform-legend__panel[data-v-2ae12e71]{color:#344054;border:1px solid #d0d5dde6;border-radius:4px;flex:0 auto;gap:5px 12px;max-width:100%;max-height:100%;padding:5px 7px;font:12px/1.35 sans-serif;display:flex;overflow:hidden}.waveform-legend__panel--interactive[data-v-2ae12e71]{pointer-events:auto}.waveform-legend__panel--horizontal[data-v-2ae12e71]{flex-flow:wrap;align-items:center}.waveform-legend__panel--vertical[data-v-2ae12e71]{flex-flow:column;align-items:stretch}.waveform-legend__item[data-v-2ae12e71]{min-width:0;max-width:160px;color:inherit;font:inherit;text-align:left;white-space:nowrap;appearance:none;background:0 0;border:0;align-items:center;gap:6px;padding:0;display:flex}.waveform-legend__item[data-v-2ae12e71]:disabled{opacity:1}.waveform-legend__panel--interactive .waveform-legend__item[data-v-2ae12e71]{cursor:pointer}.waveform-legend__panel--interactive .waveform-legend__item[data-v-2ae12e71]:focus-visible{outline-offset:2px;outline:2px solid #1677ff}.waveform-legend__item.is-hidden[data-v-2ae12e71]{opacity:.45}.waveform-legend__item.is-hidden .waveform-legend__label[data-v-2ae12e71]{text-decoration:line-through}.waveform-legend__swatch[data-v-2ae12e71]{stroke-linecap:round;stroke-linejoin:round;flex:0 0 26px;width:26px;height:16px;overflow:visible}.waveform-legend__label[data-v-2ae12e71]{text-overflow:ellipsis;min-width:0;overflow:hidden}.waveform-track__crosshair[data-v-a6f82099]{pointer-events:none}.waveform-track__crosshair line[data-v-a6f82099]{stroke:#57617b;stroke-width:1px;stroke-dasharray:4 3}.waveform-chart{box-sizing:border-box;color:#475467;background:#fff;border:1px solid #00000014;border-radius:6px;min-width:0;min-height:180px;position:relative;overflow:hidden}.waveform-chart--clean{border-color:#0000}.waveform-chart__pagination{z-index:2;position:absolute;bottom:6px;right:10px}.waveform-chart__title-area{background:#fff;align-items:center;width:100%;min-width:0;padding:0 24px;display:flex;position:relative;overflow:hidden}.waveform-chart__title-measure{visibility:hidden;pointer-events:none;display:block;position:absolute}.waveform-chart__title-visual{flex:none;min-width:0;position:relative;overflow:visible}.waveform-chart__title-text{transform-origin:50%;line-height:1.2;display:block;position:absolute;top:50%;left:50%;overflow:visible}.waveform-chart__pagination .ant-pagination-item,.waveform-chart__pagination .ant-pagination-prev .ant-pagination-item-link,.waveform-chart__pagination .ant-pagination-next .ant-pagination-item-link{background:#fff;border-color:#d9d9d9}.waveform-chart__pagination .ant-pagination-item-active{border-color:#1677ff}.waveform-chart__grid-slot-placeholder{fill:#fafbfc;stroke:#e4e7ec;stroke-dasharray:4 4;pointer-events:none}.waveform-chart__svg{max-width:100%;display:block}.waveform-chart__overlay{fill:#0000;cursor:crosshair;touch-action:none}.waveform-chart__overlay.is-zoomable{cursor:crosshair}.waveform-chart--panning .waveform-chart__overlay.is-zoomable{cursor:grabbing}.waveform-chart__zoom-selection{fill:#1677ff24;stroke:#1677ff;stroke-width:1px;pointer-events:none}.waveform-chart__overlay.is-annotating{cursor:crosshair}.waveform-chart--presentation .waveform-chart__overlay{cursor:default;pointer-events:none}.waveform-chart__label,.waveform-chart__empty{fill:#595959;font-size:12px}
2
2
  /*$vite$:1*/
@@ -1,7 +1,7 @@
1
1
  import { type ComponentPublicInstance } from 'vue';
2
2
  import { getPageSize } from './grid';
3
3
  import type { WaveformHoverState } from './types';
4
- import type { ResolvedWaveformChartProps, WaveformChartEmit } from './waveformChartTypes';
4
+ import type { ResolvedWaveformChartProps, ViewportSelectionState, WaveformChartEmit } from './waveformChartTypes';
5
5
  export declare function useWaveformChartController(props: ResolvedWaveformChartProps, emit: WaveformChartEmit): {
6
6
  margin: {
7
7
  top: number;
@@ -11,19 +11,7 @@ export declare function useWaveformChartController(props: ResolvedWaveformChartP
11
11
  };
12
12
  getPageSize: typeof getPageSize;
13
13
  currentPage: number;
14
- selection: {
15
- trackIndex: number;
16
- independent: boolean;
17
- overlay: SVGRectElement;
18
- startX: number;
19
- startY: number;
20
- currentX: number;
21
- currentY: number;
22
- pointerId: number;
23
- mode: "box" | "pan";
24
- xDomain: [number, number];
25
- yDomains: Record<string, [number, number]>;
26
- } | null;
14
+ selection: ViewportSelectionState | null;
27
15
  pointerInsideChart: boolean;
28
16
  hoverState: import("vue").ShallowReactive<WaveformHoverState>;
29
17
  annotationInteraction: {
@@ -2,7 +2,7 @@ import { type ZoomTransform } from 'd3';
2
2
  import { type ComputedRef, type Ref, type ShallowRef } from 'vue';
3
3
  import type { AnnotationSeriesCandidate } from '../annotation';
4
4
  import type { useWaveformAnnotationInteraction } from '../annotation';
5
- import type { DisplaySeries, DisplayTrack } from './types';
5
+ import type { DisplaySeries, DisplayTrack, TrackLayout } from './types';
6
6
  import type { ResolvedWaveformChartProps, WaveformChartEmit } from './waveformChartTypes';
7
7
  interface LifecycleContext {
8
8
  props: ResolvedWaveformChartProps;
@@ -28,6 +28,7 @@ interface LifecycleContext {
28
28
  }>;
29
29
  chartSeries: ComputedRef<DisplaySeries[]>;
30
30
  chartTracks: ComputedRef<DisplayTrack[]>;
31
+ trackLayouts: ComputedRef<TrackLayout[]>;
31
32
  innerWidth: ComputedRef<number>;
32
33
  innerHeight: ComputedRef<number>;
33
34
  activeInteractionMode: ComputedRef<string | undefined>;
@@ -35,6 +36,7 @@ interface LifecycleContext {
35
36
  internalHiddenSeriesIds: Ref<Set<string>>;
36
37
  independentTransforms: ShallowRef<ZoomTransform[]>;
37
38
  independentYDomains: Ref<Record<number, [number, number]>>;
39
+ resolveInitialTrackDomain: (track: TrackLayout) => [number, number];
38
40
  annotationInteraction: ReturnType<typeof useWaveformAnnotationInteraction>;
39
41
  editorSeriesOptions: Ref<AnnotationSeriesCandidate[]>;
40
42
  isPresentationMode: ComputedRef<boolean>;
@@ -48,6 +50,7 @@ interface LifecycleContext {
48
50
  }
49
51
  export declare function useWaveformChartLifecycle(context: LifecycleContext): {
50
52
  goToPage: (page: number) => void;
53
+ handleBeforeDataReferenceChange: () => void;
51
54
  handleDataReferenceChange: () => void;
52
55
  };
53
56
  export {};
@@ -15,4 +15,4 @@ export interface PreparedWaveformSeries {
15
15
  hasErrorPoints: boolean;
16
16
  }
17
17
  export declare function prepareWaveformSeries(data: WaveformData): PreparedWaveformSeries[];
18
- export declare function usePreparedWaveformSeries(data: () => WaveformData, onDataChange: () => void): import("vue").ShallowRef<PreparedWaveformSeries[], PreparedWaveformSeries[]>;
18
+ export declare function usePreparedWaveformSeries(data: () => WaveformData, onBeforeDataChange: () => void, onDataChange: () => void): import("vue").ShallowRef<PreparedWaveformSeries[], PreparedWaveformSeries[]>;
@@ -55,17 +55,20 @@ export interface WaveformChartEmit {
55
55
  (event: 'annotation-delete', annotation: WaveformAnnotation): void;
56
56
  (event: 'page-change', page: number, pageCount: number): void;
57
57
  }
58
- export interface ViewportSelectionState {
58
+ interface ViewportSelectionBase {
59
59
  trackIndex: number;
60
60
  independent: boolean;
61
- overlay: SVGRectElement;
62
61
  startX: number;
63
62
  startY: number;
64
63
  currentX: number;
65
64
  currentY: number;
66
65
  pointerId: number;
67
- mode: 'box' | 'pan';
68
66
  xDomain: [number, number];
69
67
  yDomains: Record<string, [number, number]>;
70
68
  }
69
+ export type ViewportSelectionState = (ViewportSelectionBase & {
70
+ kind: 'box';
71
+ }) | (ViewportSelectionBase & {
72
+ kind: 'pan';
73
+ });
71
74
  export {};
@@ -0,0 +1 @@
1
+ export declare function tryReleasePointerCapture(target: SVGRectElement | null | undefined, pointerId: number): void;
@@ -0,0 +1,39 @@
1
+ import type { ViewportSelectionState } from '../core/waveformChartTypes';
2
+ export interface ViewportGestureStart {
3
+ trackIndex: number;
4
+ independent: boolean;
5
+ startX: number;
6
+ startY: number;
7
+ pointerId: number;
8
+ kind: 'box' | 'pan';
9
+ xDomain: [number, number];
10
+ yDomains: Record<string, [number, number]>;
11
+ }
12
+ export interface ViewportGesturePosition {
13
+ currentX: number;
14
+ currentY: number;
15
+ }
16
+ export type ViewportInteractionEvent = {
17
+ type: 'begin';
18
+ gesture: ViewportGestureStart;
19
+ } | {
20
+ type: 'move';
21
+ pointerId: number;
22
+ position: ViewportGesturePosition;
23
+ } | {
24
+ type: 'finish';
25
+ pointerId: number;
26
+ position: ViewportGesturePosition;
27
+ } | {
28
+ type: 'cancel';
29
+ pointerId?: number;
30
+ } | {
31
+ type: 'reset';
32
+ };
33
+ export interface ViewportInteractionTransition {
34
+ state: ViewportSelectionState | null;
35
+ accepted: boolean;
36
+ completed: ViewportSelectionState | null;
37
+ }
38
+ export declare function transitionViewportInteraction(state: ViewportSelectionState | null, event: ViewportInteractionEvent): ViewportInteractionTransition;
39
+ export declare function reduceViewportInteraction(state: ViewportSelectionState | null, event: ViewportInteractionEvent): ViewportSelectionState | null;
@@ -0,0 +1,14 @@
1
+ import { type ZoomTransform } from 'd3';
2
+ import type { WaveformPoint } from '../../types';
3
+ interface PointSeriesSource {
4
+ points: WaveformPoint[];
5
+ }
6
+ export type ZoomSeriesGroup = readonly PointSeriesSource[];
7
+ interface ZoomConstraintOptions {
8
+ minZoomSpan?: number;
9
+ minVisiblePoints?: number;
10
+ }
11
+ export declare function resolveMinimumZoomSpan(boundary: [number, number], groups: readonly ZoomSeriesGroup[], options: ZoomConstraintOptions): number;
12
+ export declare function constrainZoomDomain(domain: [number, number], boundary: [number, number], groups: readonly ZoomSeriesGroup[], options: ZoomConstraintOptions): [number, number];
13
+ export declare function transformForDomain(domain: [number, number], baseDomain: [number, number], width: number): ZoomTransform;
14
+ export {};
@@ -1,10 +1,7 @@
1
1
  import type { WaveformPoint } from '@/types';
2
2
  import type { ResolvedWaveformRenderingOptions } from './renderingOptions';
3
+ import { type VisiblePointRange } from './renderingStrategies';
3
4
  export { DEFAULT_WAVEFORM_RENDERING_OPTIONS, resolveWaveformRenderingOptions, type ResolvedWaveformRenderingOptions, } from './renderingOptions';
4
- interface VisiblePointRange {
5
- start: number;
6
- end: number;
7
- }
8
5
  interface PointSeriesSource {
9
6
  points: WaveformPoint[];
10
7
  }
@@ -0,0 +1,22 @@
1
+ import type { WaveformPoint } from '../types';
2
+ import type { ResolvedWaveformRenderingOptions } from './renderingOptions';
3
+ export interface VisiblePointRange {
4
+ start: number;
5
+ end: number;
6
+ }
7
+ export interface RenderablePointSelectionContext {
8
+ points: WaveformPoint[];
9
+ range: VisiblePointRange;
10
+ domain: [number, number];
11
+ width: number;
12
+ options: ResolvedWaveformRenderingOptions;
13
+ }
14
+ export type RenderablePointSelectionStrategy = (context: RenderablePointSelectionContext) => WaveformPoint[];
15
+ export declare const completePointSelectionStrategy: RenderablePointSelectionStrategy;
16
+ export declare const peakPreservingPointSelectionStrategy: RenderablePointSelectionStrategy;
17
+ export interface RenderablePointSelectionStrategyRequest {
18
+ visibleCount: number;
19
+ width: number;
20
+ options: ResolvedWaveformRenderingOptions;
21
+ }
22
+ export declare function resolveRenderablePointSelectionStrategy(request: RenderablePointSelectionStrategyRequest): RenderablePointSelectionStrategy;
@@ -67,7 +67,6 @@ export interface DemoControlPanelModel {
67
67
  }
68
68
  export interface DemoChartModel {
69
69
  data: WaveformData;
70
- minZoomSpan?: number;
71
70
  initialXDomain?: [number, number];
72
71
  displayMode: WaveformDisplayMode;
73
72
  overlayMode: WaveformOverlayMode;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "waveform-analysis",
3
- "version": "0.1.30",
3
+ "version": "0.1.31",
4
4
  "main": "./dist/index.cjs",
5
5
  "module": "./dist/index.js",
6
6
  "types": "./dist/types/index.d.ts",
@@ -43,6 +43,7 @@
43
43
  "eslint-plugin-vue": "10.9.2",
44
44
  "globals": "17.7.0",
45
45
  "jsdom": "29.1.1",
46
+ "oxlint": "1.77.0",
46
47
  "prettier": "3.9.5",
47
48
  "typescript": "~6.0.0",
48
49
  "typescript-eslint": "8.64.0",
@@ -61,7 +62,10 @@
61
62
  "typecheck": "vue-tsc -b",
62
63
  "check:file-length": "node scripts/check-file-length.mjs",
63
64
  "lint": "eslint . --max-warnings=0",
65
+ "lint:oxlint": "oxlint . --deny-warnings",
66
+ "lint:all": "pnpm lint:oxlint && pnpm lint",
64
67
  "format": "prettier --write .",
68
+ "format:check": "prettier --check .",
65
69
  "test": "vitest run",
66
70
  "test:coverage": "vitest run --coverage"
67
71
  }
package/src/App.vue CHANGED
@@ -141,9 +141,6 @@ const [initialXMinimum, initialXMaximum] = initialXValues.reduce<[number, number
141
141
  ([minimum, maximum], value) => [Math.min(minimum, value), Math.max(maximum, value)],
142
142
  [Number.POSITIVE_INFINITY, Number.NEGATIVE_INFINITY],
143
143
  )
144
- const initialXSpan = initialXMaximum - initialXMinimum
145
- const minZoomSpan =
146
- Number.isFinite(initialXSpan) && initialXSpan > 0 ? initialXSpan / 40 : undefined
147
144
  const initialXDomainValue: [number, number] | undefined =
148
145
  Number.isFinite(initialXMinimum) && Number.isFinite(initialXMaximum)
149
146
  ? [initialXMinimum, initialXMaximum]
@@ -353,7 +350,6 @@ const controlPanelModel = reactive({
353
350
 
354
351
  const chartModel = reactive({
355
352
  data: displayChartData,
356
- minZoomSpan,
357
353
  initialXDomain,
358
354
  displayMode,
359
355
  overlayMode,
@@ -110,7 +110,7 @@ const {
110
110
  {
111
111
  'waveform-chart--clean': isCleanView,
112
112
  'waveform-chart--presentation': isPresentationMode,
113
- 'waveform-chart--panning': selection?.mode === 'pan',
113
+ 'waveform-chart--panning': selection?.kind === 'pan',
114
114
  },
115
115
  ]"
116
116
  :style="containerStyle"
@@ -247,7 +247,7 @@ const {
247
247
  />
248
248
 
249
249
  <rect
250
- v-if="selectionBox && selection?.mode === 'box'"
250
+ v-if="selectionBox && selection?.kind === 'box'"
251
251
  class="waveform-chart__zoom-selection"
252
252
  :x="selectionBox.x"
253
253
  :y="selectionBox.y"
@@ -78,12 +78,14 @@ export function useWaveformChartController(
78
78
  },
79
79
  { deep: true },
80
80
  )
81
- const selection = ref<ViewportSelectionState | null>(null)
81
+ const selection = shallowRef<ViewportSelectionState | null>(null)
82
82
  const spacePressed = ref(false)
83
83
  const pointerInsideChart = ref(false)
84
+ let handleBeforeDataReferenceChange: () => void = () => undefined
84
85
  let handleDataReferenceChange: () => void = () => undefined
85
86
  const preparedSeries = usePreparedWaveformSeries(
86
87
  () => props.data,
88
+ () => handleBeforeDataReferenceChange(),
87
89
  () => handleDataReferenceChange(),
88
90
  )
89
91
 
@@ -127,6 +129,7 @@ export function useWaveformChartController(
127
129
  const {
128
130
  chartSeries,
129
131
  chartTracks,
132
+ trackLayouts,
130
133
  gridOptions,
131
134
  pageCount,
132
135
  pagedTracks,
@@ -138,7 +141,6 @@ export function useWaveformChartController(
138
141
  initialXDomain,
139
142
  sharedZoomDomain,
140
143
  resolveInitialTrackDomain,
141
- trackLayouts,
142
144
  annotationLayoutsForTrack,
143
145
  resolveSeriesYScale,
144
146
  } = layout
@@ -254,6 +256,7 @@ export function useWaveformChartController(
254
256
  gridOptions,
255
257
  chartSeries,
256
258
  chartTracks,
259
+ trackLayouts,
257
260
  innerWidth,
258
261
  innerHeight,
259
262
  activeInteractionMode,
@@ -261,6 +264,7 @@ export function useWaveformChartController(
261
264
  internalHiddenSeriesIds,
262
265
  independentTransforms,
263
266
  independentYDomains,
267
+ resolveInitialTrackDomain,
264
268
  annotationInteraction,
265
269
  editorSeriesOptions,
266
270
  isPresentationMode,
@@ -272,6 +276,7 @@ export function useWaveformChartController(
272
276
  cancelPendingHover: hover.cancelPendingHover,
273
277
  clearZoomBindings: zoom.clearZoomBindings,
274
278
  })
279
+ handleBeforeDataReferenceChange = lifecycle.handleBeforeDataReferenceChange
275
280
  handleDataReferenceChange = lifecycle.handleDataReferenceChange
276
281
 
277
282
  return reactive({
@@ -11,8 +11,9 @@ import {
11
11
 
12
12
  import type { AnnotationSeriesCandidate } from '../annotation'
13
13
  import type { useWaveformAnnotationInteraction } from '../annotation'
14
- import type { DisplaySeries, DisplayTrack } from './types'
14
+ import type { DisplaySeries, DisplayTrack, TrackLayout } from './types'
15
15
  import type { ResolvedWaveformChartProps, WaveformChartEmit } from './waveformChartTypes'
16
+ import { constrainZoomDomain, transformForDomain } from '../interaction/zoomConstraints'
16
17
 
17
18
  interface LifecycleContext {
18
19
  props: ResolvedWaveformChartProps
@@ -35,6 +36,7 @@ interface LifecycleContext {
35
36
  gridOptions: ComputedRef<{ rowCount: number; columnCount: number }>
36
37
  chartSeries: ComputedRef<DisplaySeries[]>
37
38
  chartTracks: ComputedRef<DisplayTrack[]>
39
+ trackLayouts: ComputedRef<TrackLayout[]>
38
40
  innerWidth: ComputedRef<number>
39
41
  innerHeight: ComputedRef<number>
40
42
  activeInteractionMode: ComputedRef<string | undefined>
@@ -42,6 +44,7 @@ interface LifecycleContext {
42
44
  internalHiddenSeriesIds: Ref<Set<string>>
43
45
  independentTransforms: ShallowRef<ZoomTransform[]>
44
46
  independentYDomains: Ref<Record<number, [number, number]>>
47
+ resolveInitialTrackDomain: (track: TrackLayout) => [number, number]
45
48
  annotationInteraction: ReturnType<typeof useWaveformAnnotationInteraction>
46
49
  editorSeriesOptions: Ref<AnnotationSeriesCandidate[]>
47
50
  isPresentationMode: ComputedRef<boolean>
@@ -87,6 +90,7 @@ export function useWaveformChartLifecycle(context: LifecycleContext) {
87
90
  gridOptions,
88
91
  chartSeries,
89
92
  chartTracks,
93
+ trackLayouts,
90
94
  innerWidth,
91
95
  innerHeight,
92
96
  activeInteractionMode,
@@ -94,6 +98,7 @@ export function useWaveformChartLifecycle(context: LifecycleContext) {
94
98
  internalHiddenSeriesIds,
95
99
  independentTransforms,
96
100
  independentYDomains,
101
+ resolveInitialTrackDomain,
97
102
  annotationInteraction,
98
103
  editorSeriesOptions,
99
104
  isPresentationMode,
@@ -139,6 +144,27 @@ export function useWaveformChartLifecycle(context: LifecycleContext) {
139
144
  emit('page-change', nextPage, pageCount.value)
140
145
  }
141
146
 
147
+ let pendingIndependentXDomains: Array<[number, number] | undefined> | undefined
148
+
149
+ function handleBeforeDataReferenceChange() {
150
+ if (props.displayMode !== 'independent') return
151
+ pendingIndependentXDomains = trackLayouts.value.map((track) => {
152
+ const configuredDomain =
153
+ props.initialXDomains?.[track.series.trackId ?? track.series.id] ??
154
+ props.initialXDomains?.[track.series.id] ??
155
+ props.initialXDomain
156
+ if (
157
+ !configuredDomain ||
158
+ !Number.isFinite(configuredDomain[0]) ||
159
+ !Number.isFinite(configuredDomain[1]) ||
160
+ configuredDomain[0] === configuredDomain[1]
161
+ ) {
162
+ return undefined
163
+ }
164
+ return track.xScale.domain() as [number, number]
165
+ })
166
+ }
167
+
142
168
  function handleDataReferenceChange() {
143
169
  if (props.displayMode === 'independent') {
144
170
  const currentTransforms = independentTransforms.value
@@ -148,7 +174,24 @@ export function useWaveformChartLifecycle(context: LifecycleContext) {
148
174
  }
149
175
  clearHover()
150
176
  editorSeriesOptions.value = []
151
- void nextTick(configureZoom)
177
+ void nextTick(() => {
178
+ if (props.displayMode === 'independent' && pendingIndependentXDomains) {
179
+ const nextTransforms = chartTracks.value.map(() => zoomIdentity)
180
+ trackLayouts.value.forEach((track) => {
181
+ const previousDomain = pendingIndependentXDomains?.[track.index]
182
+ if (!previousDomain) return
183
+ const boundary = resolveInitialTrackDomain(track)
184
+ const domain = constrainZoomDomain(previousDomain, boundary, [track.seriesList], props)
185
+ nextTransforms[track.index] = transformForDomain(domain, boundary, track.width)
186
+ })
187
+ independentTransforms.value = nextTransforms
188
+ pendingIndependentXDomains = undefined
189
+ void nextTick(configureZoom)
190
+ return
191
+ }
192
+ pendingIndependentXDomains = undefined
193
+ configureZoom()
194
+ })
152
195
  }
153
196
 
154
197
  watch(
@@ -158,6 +201,7 @@ export function useWaveformChartLifecycle(context: LifecycleContext) {
158
201
  () => props.zoomable,
159
202
  isPresentationMode,
160
203
  () => props.minZoomSpan,
204
+ () => props.minVisiblePoints,
161
205
  () => props.initialXDomain,
162
206
  () => props.initialXDomains,
163
207
  () => props.displayMode,
@@ -324,5 +368,5 @@ export function useWaveformChartLifecycle(context: LifecycleContext) {
324
368
  editorSeriesOptions.value = []
325
369
  })
326
370
 
327
- return { goToPage, handleDataReferenceChange }
371
+ return { goToPage, handleBeforeDataReferenceChange, handleDataReferenceChange }
328
372
  }
@@ -62,9 +62,14 @@ export function prepareWaveformSeries(data: WaveformData): PreparedWaveformSerie
62
62
  })
63
63
  }
64
64
 
65
- export function usePreparedWaveformSeries(data: () => WaveformData, onDataChange: () => void) {
65
+ export function usePreparedWaveformSeries(
66
+ data: () => WaveformData,
67
+ onBeforeDataChange: () => void,
68
+ onDataChange: () => void,
69
+ ) {
66
70
  const preparedSeries = shallowRef<PreparedWaveformSeries[]>(prepareWaveformSeries(data()))
67
71
  watch(data, (nextData) => {
72
+ onBeforeDataChange()
68
73
  preparedSeries.value = prepareWaveformSeries(nextData)
69
74
  onDataChange()
70
75
  })
@@ -95,16 +95,17 @@ export interface WaveformChartEmit {
95
95
  (event: 'page-change', page: number, pageCount: number): void
96
96
  }
97
97
 
98
- export interface ViewportSelectionState {
98
+ interface ViewportSelectionBase {
99
99
  trackIndex: number
100
100
  independent: boolean
101
- overlay: SVGRectElement
102
101
  startX: number
103
102
  startY: number
104
103
  currentX: number
105
104
  currentY: number
106
105
  pointerId: number
107
- mode: 'box' | 'pan'
108
106
  xDomain: [number, number]
109
107
  yDomains: Record<string, [number, number]>
110
108
  }
109
+
110
+ export type ViewportSelectionState =
111
+ (ViewportSelectionBase & { kind: 'box' }) | (ViewportSelectionBase & { kind: 'pan' })
@@ -38,9 +38,48 @@ describe('WaveformTooltip', () => {
38
38
  const tooltip = mountTooltip(100, 200).get('.waveform-tooltip')
39
39
 
40
40
  expect(tooltip.attributes('style')).toContain('left: 8px')
41
+ expect(tooltip.attributes('style')).toContain('max-width: 184px')
41
42
  expect(tooltip.attributes('style')).not.toContain('right:')
42
43
  })
43
44
 
45
+ it('keeps short content content-sized while exposing the available width cap', () => {
46
+ const tooltip = mountTooltip(100).get('.waveform-tooltip')
47
+
48
+ expect(tooltip.attributes('style')).toContain('left: 112px')
49
+ expect(tooltip.attributes('style')).toContain('max-width: 280px')
50
+ })
51
+
52
+ it('keeps long series content in a wrapping content container', () => {
53
+ const longName = 'ENG8KJXAc-very-long-series-name-10001'
54
+ const pointWithErrors = { x: 1, y: 12, error: 1, upperError: 2 }
55
+ const wrapper = mount(WaveformTooltip, {
56
+ props: {
57
+ visible: true,
58
+ position: { x: 100, y: 100 },
59
+ timeUnit: 'ms',
60
+ hoveredPoint: pointWithErrors,
61
+ seriesPoints: [
62
+ {
63
+ trackIndex: 0,
64
+ name: longName,
65
+ color: '#f00',
66
+ unit: 'very-long-unit',
67
+ point: pointWithErrors,
68
+ },
69
+ ],
70
+ containerWidth: 400,
71
+ containerHeight: 300,
72
+ },
73
+ })
74
+
75
+ const tooltip = wrapper.get('.waveform-tooltip')
76
+ expect(tooltip.get('.waveform-tooltip__series-content').text()).toContain(longName)
77
+ expect(tooltip.get('.waveform-tooltip__series-content').classes()).toContain(
78
+ 'waveform-tooltip__series-content',
79
+ )
80
+ expect(tooltip.attributes('style')).toContain('max-width: 280px')
81
+ })
82
+
44
83
  it('shows resolved asymmetric errors beside the hovered value', () => {
45
84
  const pointWithErrors = { x: 1, y: 12, error: 1, upperError: 2 }
46
85
  const wrapper = mount(WaveformTooltip, {