waveform-analysis 0.1.40 → 0.1.42

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 (28) hide show
  1. package/README.md +19 -3
  2. package/dist/index.cjs +1 -1
  3. package/dist/index.js +478 -477
  4. package/dist/style.css +1 -1
  5. package/dist/types/components/core/grid.d.ts +3 -0
  6. package/dist/types/components/core/layout.d.ts +4 -3
  7. package/dist/types/components/core/trackLayoutBuilder.d.ts +1 -0
  8. package/dist/types/components/core/useWaveformChartController.d.ts +1 -0
  9. package/dist/types/components/core/useWaveformChartLifecycle.d.ts +2 -4
  10. package/dist/types/components/core/useWaveformLayout.d.ts +1 -0
  11. package/dist/types/types/chart.d.ts +2 -0
  12. package/package.json +1 -1
  13. package/src/components/annotation/WaveformAnnotationEditor.css +30 -0
  14. package/src/components/annotation/WaveformAnnotationEditor.vue +1 -0
  15. package/src/components/annotation/components.test.ts +14 -1
  16. package/src/components/core/fixedYDomainLayout.test.ts +94 -6
  17. package/src/components/core/grid.test.ts +48 -0
  18. package/src/components/core/grid.ts +20 -7
  19. package/src/components/core/layout.test.ts +31 -5
  20. package/src/components/core/layout.ts +21 -8
  21. package/src/components/core/trackLayoutBuilder.ts +14 -9
  22. package/src/components/core/useWaveformChartLifecycle.ts +25 -15
  23. package/src/components/core/useWaveformLayout.ts +9 -2
  24. package/src/components/waveformChartCases/compactAndSharedHover.test.ts +2 -2
  25. package/src/components/waveformChartCases/dataLifecycle.test.ts +2 -2
  26. package/src/components/waveformChartCases/gridFill.test.ts +95 -0
  27. package/src/components/waveformChartCases/layoutAndDomains.test.ts +23 -3
  28. package/src/types/chart.ts +2 -0
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__content[data-v-8ce79dc8]{color:#344054;gap:20px;font-size:13px;display:grid}.waveform-annotation-editor__coordinates[data-v-8ce79dc8]{grid-template-columns:repeat(2,minmax(0,1fr));gap:8px;display:grid}.waveform-annotation-editor__coordinates span[data-v-8ce79dc8],.waveform-annotation-editor__series[data-v-8ce79dc8]{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-8ce79dc8]{color:#667085}.waveform-annotation-editor__coordinate-input[data-v-8ce79dc8]{color:#667085;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__coordinate-input[data-v-8ce79dc8] .ant-input-number{color:#344054;width:auto;min-width:96px;font-family:SFMono-Regular,Consolas,monospace}.waveform-annotation-editor__coordinate-value[data-v-8ce79dc8]{display:none}.waveform-annotation-editor__coordinate-error[data-v-8ce79dc8]{color:#d92d20;grid-column:1/-1;margin:-12px 0 0;font-size:11px;line-height:1.4}.waveform-annotation-editor__coordinate-hint[data-v-8ce79dc8]{color:#98a2b3;margin:-12px 0 0;font-size:11px;line-height:1.4}.waveform-annotation-editor__coordinates b[data-v-8ce79dc8]{color:#1677ff;font-size:11px}.waveform-annotation-editor__coordinates code[data-v-8ce79dc8]{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-8ce79dc8]{gap:10px}.waveform-annotation-editor__series i[data-v-8ce79dc8]{border:1px solid #0000001f;border-radius:2px;flex:0 0 12px;width:12px;height:12px}.waveform-annotation-editor__series-label[data-v-8ce79dc8]{gap:1px;min-width:0;display:grid}.waveform-annotation-editor__series select[data-v-8ce79dc8]{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-8ce79dc8]{color:#667085;font-size:10px}.waveform-annotation-editor__series b[data-v-8ce79dc8]{color:#344054;text-overflow:ellipsis;white-space:nowrap;font-size:12px;overflow:hidden}.waveform-annotation-editor__series code[data-v-8ce79dc8]{color:#667085;margin-left:auto;font-size:11px}.waveform-annotation-editor__field[data-v-8ce79dc8],.waveform-annotation-editor__color-field[data-v-8ce79dc8]{gap:4px;display:grid}.waveform-annotation-editor__label-row[data-v-8ce79dc8]{justify-content:space-between;align-items:center;gap:8px;display:flex}.waveform-annotation-editor__label-row b[data-v-8ce79dc8],.waveform-annotation-editor__colors legend[data-v-8ce79dc8]{color:#344054;font-size:12px;font-weight:600}.waveform-annotation-editor__label-row small[data-v-8ce79dc8]{color:#98a2b3;font-size:11px}.waveform-annotation-editor textarea[data-v-8ce79dc8]{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-8ce79dc8]:focus{border-color:#1677ff;outline:3px solid #1677ff24}.waveform-annotation-editor__colors[data-v-8ce79dc8]{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-8ce79dc8]{grid-column:1/-1;padding:0}.waveform-annotation-editor__color-field[data-v-8ce79dc8]{color:#667085;text-align:left;place-items:stretch stretch;min-width:0;font-size:11px}.waveform-annotation-editor__color-field[data-v-8ce79dc8] .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-8ce79dc8] .vc-color-wrap:hover{border-color:#98a2b3;box-shadow:0 0 0 3px #1677ff1f}@media (width<=420px){.waveform-annotation-editor__content[data-v-8ce79dc8]{gap:16px}.waveform-annotation-editor__coordinates[data-v-8ce79dc8]{grid-template-columns:1fr}}.waveform-tooltip[data-v-1f55d6c8]{box-sizing:border-box;z-index:2;color:#505050;pointer-events:none;background:#fff;border:1px solid #e3e7eb;border-radius:4px;gap:2px;width:max-content;max-width:min(560px,100% - 16px);padding:9px 12px;font:14px/1.35 Arial,sans-serif;display:grid;position:absolute;box-shadow:0 3px 10px #10182826}.waveform-tooltip__series[data-v-1f55d6c8]{grid-template-columns:8px minmax(0,1fr);align-items:start;gap:6px;display:grid}.waveform-tooltip__series i[data-v-1f55d6c8]{border-radius:50%;flex:none;width:8px;height:8px;margin-top:4px}.waveform-tooltip__series-content[data-v-1f55d6c8]{flex-wrap:nowrap;align-items:baseline;gap:12px;min-width:0;display:flex}.waveform-tooltip__series-label[data-v-1f55d6c8]{text-overflow:ellipsis;white-space:nowrap;flex:auto;min-width:0;overflow:hidden}.waveform-tooltip__value[data-v-1f55d6c8]{white-space:nowrap;flex:none}.waveform-tooltip__series strong[data-v-1f55d6c8]{font-weight:600}.waveform-tooltip__value[data-v-1f55d6c8]{color:#555}.waveform-tooltip__series small[data-v-1f55d6c8]{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}
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-root,.waveform-annotation-editor-root *{box-sizing:border-box}.waveform-annotation-editor-root{color:#344054;text-align:left;text-transform:none;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif;line-height:1.5715}.waveform-annotation-editor-root button,.waveform-annotation-editor-root input,.waveform-annotation-editor-root select,.waveform-annotation-editor-root textarea{letter-spacing:normal;text-transform:none;font-family:inherit}.waveform-annotation-editor-root button{appearance:auto}.waveform-annotation-editor__content[data-v-42466cf9]{color:#344054;gap:20px;font-size:13px;display:grid}.waveform-annotation-editor__coordinates[data-v-42466cf9]{grid-template-columns:repeat(2,minmax(0,1fr));gap:8px;display:grid}.waveform-annotation-editor__coordinates span[data-v-42466cf9],.waveform-annotation-editor__series[data-v-42466cf9]{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-42466cf9]{color:#667085}.waveform-annotation-editor__coordinate-input[data-v-42466cf9]{color:#667085;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__coordinate-input[data-v-42466cf9] .ant-input-number{color:#344054;width:auto;min-width:96px;font-family:SFMono-Regular,Consolas,monospace}.waveform-annotation-editor__coordinate-value[data-v-42466cf9]{display:none}.waveform-annotation-editor__coordinate-error[data-v-42466cf9]{color:#d92d20;grid-column:1/-1;margin:-12px 0 0;font-size:11px;line-height:1.4}.waveform-annotation-editor__coordinate-hint[data-v-42466cf9]{color:#98a2b3;margin:-12px 0 0;font-size:11px;line-height:1.4}.waveform-annotation-editor__coordinates b[data-v-42466cf9]{color:#1677ff;font-size:11px}.waveform-annotation-editor__coordinates code[data-v-42466cf9]{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-42466cf9]{gap:10px}.waveform-annotation-editor__series i[data-v-42466cf9]{border:1px solid #0000001f;border-radius:2px;flex:0 0 12px;width:12px;height:12px}.waveform-annotation-editor__series-label[data-v-42466cf9]{gap:1px;min-width:0;display:grid}.waveform-annotation-editor__series select[data-v-42466cf9]{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-42466cf9]{color:#667085;font-size:10px}.waveform-annotation-editor__series b[data-v-42466cf9]{color:#344054;text-overflow:ellipsis;white-space:nowrap;font-size:12px;overflow:hidden}.waveform-annotation-editor__series code[data-v-42466cf9]{color:#667085;margin-left:auto;font-size:11px}.waveform-annotation-editor__field[data-v-42466cf9],.waveform-annotation-editor__color-field[data-v-42466cf9]{gap:4px;display:grid}.waveform-annotation-editor__label-row[data-v-42466cf9]{justify-content:space-between;align-items:center;gap:8px;display:flex}.waveform-annotation-editor__label-row b[data-v-42466cf9],.waveform-annotation-editor__colors legend[data-v-42466cf9]{color:#344054;font-size:12px;font-weight:600}.waveform-annotation-editor__label-row small[data-v-42466cf9]{color:#98a2b3;font-size:11px}.waveform-annotation-editor textarea[data-v-42466cf9]{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-42466cf9]:focus{border-color:#1677ff;outline:3px solid #1677ff24}.waveform-annotation-editor__colors[data-v-42466cf9]{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-42466cf9]{grid-column:1/-1;padding:0}.waveform-annotation-editor__color-field[data-v-42466cf9]{color:#667085;text-align:left;place-items:stretch stretch;min-width:0;font-size:11px}.waveform-annotation-editor__color-field[data-v-42466cf9] .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-42466cf9] .vc-color-wrap:hover{border-color:#98a2b3;box-shadow:0 0 0 3px #1677ff1f}@media (width<=420px){.waveform-annotation-editor__content[data-v-42466cf9]{gap:16px}.waveform-annotation-editor__coordinates[data-v-42466cf9]{grid-template-columns:1fr}}.waveform-tooltip[data-v-1f55d6c8]{box-sizing:border-box;z-index:2;color:#505050;pointer-events:none;background:#fff;border:1px solid #e3e7eb;border-radius:4px;gap:2px;width:max-content;max-width:min(560px,100% - 16px);padding:9px 12px;font:14px/1.35 Arial,sans-serif;display:grid;position:absolute;box-shadow:0 3px 10px #10182826}.waveform-tooltip__series[data-v-1f55d6c8]{grid-template-columns:8px minmax(0,1fr);align-items:start;gap:6px;display:grid}.waveform-tooltip__series i[data-v-1f55d6c8]{border-radius:50%;flex:none;width:8px;height:8px;margin-top:4px}.waveform-tooltip__series-content[data-v-1f55d6c8]{flex-wrap:nowrap;align-items:baseline;gap:12px;min-width:0;display:flex}.waveform-tooltip__series-label[data-v-1f55d6c8]{text-overflow:ellipsis;white-space:nowrap;flex:auto;min-width:0;overflow:hidden}.waveform-tooltip__value[data-v-1f55d6c8]{white-space:nowrap;flex:none}.waveform-tooltip__series strong[data-v-1f55d6c8]{font-weight:600}.waveform-tooltip__value[data-v-1f55d6c8]{color:#555}.waveform-tooltip__series small[data-v-1f55d6c8]{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*/
@@ -6,6 +6,7 @@ export interface WaveformGridOptions {
6
6
  rowCount?: number;
7
7
  columnCount?: number;
8
8
  showPagination?: boolean;
9
+ fillIncompleteLastRow?: boolean;
9
10
  trackLines?: WaveformGridTrackLines;
10
11
  }
11
12
  export interface WaveformGridLineOptions {
@@ -27,6 +28,7 @@ export interface NormalizedWaveformGridOptions {
27
28
  rowCount: number;
28
29
  columnCount: number;
29
30
  showPagination: boolean;
31
+ fillIncompleteLastRow: boolean;
30
32
  trackLines: Record<string, NormalizedWaveformGridLineOptions>;
31
33
  }
32
34
  export interface GridCellGeometry {
@@ -41,6 +43,7 @@ export interface GridCellGeometry {
41
43
  plotHeight: number;
42
44
  cellHeight: number;
43
45
  xAxisBand: number;
46
+ isLastRow?: boolean;
44
47
  }
45
48
  export declare function normalizeGridOptions(options?: WaveformGridOptions): NormalizedWaveformGridOptions;
46
49
  export declare function getPageSize(options: NormalizedWaveformGridOptions): number;
@@ -2,6 +2,7 @@ import type { WaveformOverlayMode } from '../../types';
2
2
  import type { DisplaySeries, DisplayTrack, TrackLayout } from './types';
3
3
  import { type WaveformYDomain } from './yDomain';
4
4
  export { MAX_MULTI_Y_AXIS_COUNT } from './constants';
5
+ export declare function resolveYAxisTickCount(_plotHeight: number, splitNumber?: number): number;
5
6
  export interface YAxisSeriesGroup {
6
7
  index: number;
7
8
  side: 'left' | 'right';
@@ -11,11 +12,11 @@ export interface YAxisSeriesGroup {
11
12
  }
12
13
  export declare function buildYAxisSeriesGroups(track: DisplayTrack, overlayMode: WaveformOverlayMode): YAxisSeriesGroup[];
13
14
  export declare function resolveYAxisSeriesGroups(track: DisplayTrack, overlayMode: WaveformOverlayMode, yDomain?: WaveformYDomain, yDomains?: Record<string, WaveformYDomain>): YAxisSeriesGroup[];
14
- export declare function axisTextMetrics(domain: [number, number], nice?: boolean, tickValues?: number[], unit?: string): {
15
+ export declare function axisTextMetrics(domain: [number, number], nice?: boolean, tickValues?: number[], unit?: string, tickCount?: number): {
15
16
  tickTextWidth: number;
16
17
  };
17
- export declare function measureYAxisGroupClearance(group: YAxisSeriesGroup): number;
18
- export declare function measureTrackYAxisClearance(track: DisplayTrack, overlayMode: WaveformOverlayMode, yDomain?: WaveformYDomain, yDomains?: Record<string, WaveformYDomain>): {
18
+ export declare function measureYAxisGroupClearance(group: YAxisSeriesGroup, tickCount?: number): number;
19
+ export declare function measureTrackYAxisClearance(track: DisplayTrack, overlayMode: WaveformOverlayMode, yDomain?: WaveformYDomain, yDomains?: Record<string, WaveformYDomain>, tickCount?: number): {
19
20
  left: number;
20
21
  right: number;
21
22
  };
@@ -21,6 +21,7 @@ export interface BuildTrackLayoutsOptions {
21
21
  yDomains?: Record<string, [number, number]>;
22
22
  timeUnit: 's' | 'ms';
23
23
  xAxisLabelFormatter?: WaveformXAxisLabelFormatter;
24
+ yAxisSplitNumber?: number;
24
25
  rendering: ResolvedWaveformRenderingOptions;
25
26
  hideSecondaryLabels: boolean;
26
27
  yAxisLabelX: number;
@@ -272,6 +272,7 @@ export declare function useWaveformChartController(props: ResolvedWaveformChartP
272
272
  plotHeight: number;
273
273
  cellHeight: number;
274
274
  xAxisBand: number;
275
+ isLastRow?: boolean;
275
276
  }[];
276
277
  trackLayouts: import("./types").TrackLayout[];
277
278
  annotationLayoutsForTrack: (track: import("./types").TrackLayout) => import("../annotation").AnnotationTrackLayout[];
@@ -2,6 +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 { NormalizedWaveformGridOptions } from './grid';
5
6
  import type { DisplaySeries, DisplayTrack, TrackLayout } from './types';
6
7
  import type { ResolvedWaveformChartProps, WaveformChartEmit } from './waveformChartTypes';
7
8
  interface LifecycleContext {
@@ -22,10 +23,7 @@ interface LifecycleContext {
22
23
  currentPage: Ref<number>;
23
24
  pageCount: ComputedRef<number>;
24
25
  pagedTracks: ComputedRef<Array<DisplayTrack | undefined>>;
25
- gridOptions: ComputedRef<{
26
- rowCount: number;
27
- columnCount: number;
28
- }>;
26
+ gridOptions: ComputedRef<NormalizedWaveformGridOptions>;
29
27
  chartSeries: ComputedRef<DisplaySeries[]>;
30
28
  chartTracks: ComputedRef<DisplayTrack[]>;
31
29
  trackLayouts: ComputedRef<TrackLayout[]>;
@@ -48,6 +48,7 @@ export declare function useWaveformLayout(context: LayoutContext): {
48
48
  plotHeight: number;
49
49
  cellHeight: number;
50
50
  xAxisBand: number;
51
+ isLastRow?: boolean;
51
52
  }[]>;
52
53
  trackLayouts: ComputedRef<TrackLayout[]>;
53
54
  annotationLayoutsForTrack: (track: TrackLayout) => AnnotationTrackLayout[];
@@ -150,6 +150,8 @@ export interface WaveformAxesOptions {
150
150
  };
151
151
  y?: {
152
152
  lineVisible?: boolean;
153
+ /** Target total number of equal Y-axis ticks, including endpoints. Defaults to 5. */
154
+ splitNumber?: number;
153
155
  };
154
156
  }
155
157
  /** Styling and visibility options for the horizontal zero-value reference line. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "waveform-analysis",
3
- "version": "0.1.40",
3
+ "version": "0.1.42",
4
4
  "main": "./dist/index.cjs",
5
5
  "module": "./dist/index.js",
6
6
  "types": "./dist/types/index.d.ts",
@@ -1,3 +1,33 @@
1
+ /* The modal is teleported to body. Keep its entire subtree namespaced so host
2
+ application resets (for example `button { ... }`) cannot change the editor. */
3
+ :global(.waveform-annotation-editor-root),
4
+ :global(.waveform-annotation-editor-root *) {
5
+ box-sizing: border-box;
6
+ }
7
+
8
+ :global(.waveform-annotation-editor-root) {
9
+ color: #344054;
10
+ font-family:
11
+ -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial,
12
+ 'Noto Sans', sans-serif;
13
+ line-height: 1.5715;
14
+ text-align: left;
15
+ text-transform: none;
16
+ }
17
+
18
+ :global(.waveform-annotation-editor-root button),
19
+ :global(.waveform-annotation-editor-root input),
20
+ :global(.waveform-annotation-editor-root select),
21
+ :global(.waveform-annotation-editor-root textarea) {
22
+ font-family: inherit;
23
+ letter-spacing: normal;
24
+ text-transform: none;
25
+ }
26
+
27
+ :global(.waveform-annotation-editor-root button) {
28
+ appearance: auto;
29
+ }
30
+
1
31
  .waveform-annotation-editor__content {
2
32
  display: grid;
3
33
  gap: 20px;
@@ -126,6 +126,7 @@ function handleSeriesChange(event: Event) {
126
126
  cancel-text="取消"
127
127
  ok-text="保存标注"
128
128
  :ok-button-props="{ disabled: !canConfirm }"
129
+ root-class-name="waveform-annotation-editor-root"
129
130
  wrap-class-name="waveform-annotation-editor"
130
131
  @cancel="emit('cancel')"
131
132
  @ok="confirm"
@@ -18,13 +18,14 @@ const modalStub = defineComponent({
18
18
  'cancelText',
19
19
  'okText',
20
20
  'okButtonProps',
21
+ 'rootClassName',
21
22
  ],
22
23
  emits: ['cancel', 'ok'],
23
24
  setup(props, { emit, slots }) {
24
25
  return () => {
25
26
  const title = slots.title?.() ?? []
26
27
  const titleId = (title[0]?.props as { id?: string } | undefined)?.id
27
- return h('div', { class: 'ant-modal-root' }, [
28
+ return h('div', { class: ['ant-modal-root', props.rootClassName] }, [
28
29
  h(
29
30
  'div',
30
31
  {
@@ -96,6 +97,18 @@ describe('waveform annotation controls', () => {
96
97
  expect(second.get('[role="dialog"]').attributes('aria-labelledby')).toBe(secondTitleId)
97
98
  })
98
99
 
100
+ it('uses a dedicated root class to isolate teleported modal styles', () => {
101
+ const wrapper = mountEditor({
102
+ props: {
103
+ annotation: { id: 'isolated', seriesId: 'a', x: 1, y: 2, text: '说明' },
104
+ mode: 'add',
105
+ },
106
+ })
107
+
108
+ expect(wrapper.get('.ant-modal-root').classes()).toContain('waveform-annotation-editor-root')
109
+ expect(wrapper.get('.ant-modal-wrap').classes()).toContain('waveform-annotation-editor')
110
+ })
111
+
99
112
  it.each(['add', 'edit'] as const)('shows the X-axis snapping hint in %s mode', (mode) => {
100
113
  const wrapper = mountEditor({
101
114
  props: {
@@ -35,7 +35,7 @@ function track(seriesList: DisplaySeries[]): DisplayTrack {
35
35
  }
36
36
 
37
37
  describe('fixed Y-domain layout', () => {
38
- it('uses an exact global fixed domain without applying nice bounds', () => {
38
+ it('expands a global fixed domain to nice equal intervals', () => {
39
39
  const sourceTrack = track([series('a', 0, 100)])
40
40
  const result = buildTrackLayouts({
41
41
  cells: [
@@ -53,7 +53,13 @@ describe('fixed Y-domain layout', () => {
53
53
  series: sourceTrack,
54
54
  },
55
55
  ],
56
- grid: { rowCount: 1, columnCount: 1, showPagination: false, trackLines: {} },
56
+ grid: {
57
+ rowCount: 1,
58
+ columnCount: 1,
59
+ showPagination: false,
60
+ fillIncompleteLastRow: false,
61
+ trackLines: {},
62
+ },
57
63
  displayMode: 'independent',
58
64
  overlayMode: 'single-axis',
59
65
  independentTransforms: [zoomIdentity],
@@ -66,10 +72,92 @@ describe('fixed Y-domain layout', () => {
66
72
  showCompactEmptyTracks: false,
67
73
  })[0]
68
74
 
69
- expect(result?.yScale.domain()).toEqual([3, 97])
70
- expect(result?.yAxes[0]?.tickValues).toContain(3)
71
- expect(result?.yAxes[0]?.tickValues).toContain(97)
72
- expect(result?.seriesPaths[0]?.yScale.domain()).toEqual([3, 97])
75
+ expect(result?.yScale.domain()).toEqual([0, 100])
76
+ expect(result?.yAxes[0]?.tickValues).toContain(0)
77
+ expect(result?.yAxes[0]?.tickValues).toContain(100)
78
+ expect(result?.seriesPaths[0]?.yScale.domain()).toEqual([0, 100])
79
+ })
80
+
81
+ it('uses the configured split number for fixed domains', () => {
82
+ const result = buildTrackLayouts({
83
+ cells: [
84
+ {
85
+ slotIndex: 0,
86
+ row: 0,
87
+ column: 0,
88
+ left: 0,
89
+ top: 0,
90
+ width: 120,
91
+ height: 100,
92
+ plotHeight: 100,
93
+ cellHeight: 130,
94
+ xAxisBand: 30,
95
+ series: track([series('a', 0, 100)]),
96
+ },
97
+ ],
98
+ grid: {
99
+ rowCount: 1,
100
+ columnCount: 1,
101
+ showPagination: false,
102
+ fillIncompleteLastRow: false,
103
+ trackLines: {},
104
+ },
105
+ displayMode: 'independent',
106
+ overlayMode: 'single-axis',
107
+ independentTransforms: [zoomIdentity],
108
+ sharedZoomDomain: [0, 1],
109
+ fixedYDomain: [3, 97],
110
+ yAxisSplitNumber: 5,
111
+ timeUnit: 'ms',
112
+ rendering: DEFAULT_WAVEFORM_RENDERING_OPTIONS,
113
+ hideSecondaryLabels: false,
114
+ yAxisLabelX: -50,
115
+ showCompactEmptyTracks: false,
116
+ })[0]
117
+
118
+ expect(result?.yAxes[0]?.majorTicks).toEqual([0, 25, 50, 75, 100])
119
+ })
120
+
121
+ it('defaults to five ticks and supports a two-tick axis', () => {
122
+ const build = (splitNumber?: number) =>
123
+ buildTrackLayouts({
124
+ cells: [
125
+ {
126
+ slotIndex: 0,
127
+ row: 0,
128
+ column: 0,
129
+ left: 0,
130
+ top: 0,
131
+ width: 120,
132
+ height: 100,
133
+ plotHeight: 100,
134
+ cellHeight: 130,
135
+ xAxisBand: 30,
136
+ series: track([series('a', 3, 97)]),
137
+ },
138
+ ],
139
+ grid: {
140
+ rowCount: 1,
141
+ columnCount: 1,
142
+ showPagination: false,
143
+ fillIncompleteLastRow: false,
144
+ trackLines: {},
145
+ },
146
+ displayMode: 'independent',
147
+ overlayMode: 'single-axis',
148
+ independentTransforms: [zoomIdentity],
149
+ sharedZoomDomain: [0, 1],
150
+ fixedYDomain: [3, 97],
151
+ yAxisSplitNumber: splitNumber,
152
+ timeUnit: 'ms',
153
+ rendering: DEFAULT_WAVEFORM_RENDERING_OPTIONS,
154
+ hideSecondaryLabels: false,
155
+ yAxisLabelX: -50,
156
+ showCompactEmptyTracks: false,
157
+ })[0]?.yAxes[0]?.majorTicks
158
+
159
+ expect(build()).toHaveLength(5)
160
+ expect(build(2)).toEqual([0, 100])
73
161
  })
74
162
 
75
163
  it('resolves track, series, and global fixed-domain precedence', () => {
@@ -15,12 +15,14 @@ describe('waveform grid helpers', () => {
15
15
  rowCount: 2,
16
16
  columnCount: 1,
17
17
  showPagination: true,
18
+ fillIncompleteLastRow: false,
18
19
  trackLines: {},
19
20
  })
20
21
  expect(normalizeGridOptions({ rowCount: 0, columnCount: 99 })).toEqual({
21
22
  rowCount: 1,
22
23
  columnCount: 10,
23
24
  showPagination: true,
25
+ fillIncompleteLastRow: false,
24
26
  trackLines: {},
25
27
  })
26
28
  })
@@ -131,4 +133,50 @@ describe('waveform grid helpers', () => {
131
133
  expect(cells[1].left).toBe(232)
132
134
  expect(cells[2].top).toBe(cells[0].plotHeight + X_AXIS_BAND + 14)
133
135
  })
136
+
137
+ it('fills incomplete final rows without changing page capacity', () => {
138
+ const oneColumn = resolveGridCellGeometry(
139
+ 400,
140
+ 400,
141
+ normalizeGridOptions({ rowCount: 4, columnCount: 1, fillIncompleteLastRow: true }),
142
+ 'separated',
143
+ [true, true, true],
144
+ )
145
+ expect(oneColumn).toHaveLength(3)
146
+ expect(oneColumn.map((cell) => cell.width)).toEqual([400, 400, 400])
147
+ expect(oneColumn.at(-1)?.isLastRow).toBe(true)
148
+
149
+ const twoColumns = resolveGridCellGeometry(
150
+ 400,
151
+ 300,
152
+ normalizeGridOptions({ rowCount: 2, columnCount: 2, fillIncompleteLastRow: true }),
153
+ 'separated',
154
+ [true, true, true],
155
+ )
156
+ expect(twoColumns).toHaveLength(3)
157
+ expect(twoColumns[2]).toMatchObject({ row: 1, column: 0, left: 0, width: 400 })
158
+
159
+ const threeColumns = resolveGridCellGeometry(
160
+ 400,
161
+ 300,
162
+ normalizeGridOptions({ rowCount: 2, columnCount: 3, fillIncompleteLastRow: true }),
163
+ 'separated',
164
+ [true, true, true, true, true],
165
+ )
166
+ expect(threeColumns).toHaveLength(5)
167
+ expect(threeColumns.slice(3).map((cell) => cell.width)).toEqual([192, 192])
168
+ expect(threeColumns.slice(3).map((cell) => cell.left)).toEqual([0, 208])
169
+ })
170
+
171
+ it('keeps full pages and the disabled option byte-for-byte compatible', () => {
172
+ const base = normalizeGridOptions({ rowCount: 2, columnCount: 2 })
173
+ const filled = normalizeGridOptions({ rowCount: 2, columnCount: 2, fillIncompleteLastRow: true })
174
+ const fullSlots = [true, true, true, true]
175
+ expect(resolveGridCellGeometry(400, 300, filled, 'independent', fullSlots)).toEqual(
176
+ resolveGridCellGeometry(400, 300, base, 'independent', fullSlots),
177
+ )
178
+ expect(resolveGridCellGeometry(400, 300, base, 'independent', [true, true, true])).toHaveLength(
179
+ 4,
180
+ )
181
+ })
134
182
  })
@@ -8,6 +8,7 @@ export interface WaveformGridOptions {
8
8
  rowCount?: number
9
9
  columnCount?: number
10
10
  showPagination?: boolean
11
+ fillIncompleteLastRow?: boolean
11
12
  trackLines?: WaveformGridTrackLines
12
13
  }
13
14
 
@@ -33,6 +34,7 @@ export interface NormalizedWaveformGridOptions {
33
34
  rowCount: number
34
35
  columnCount: number
35
36
  showPagination: boolean
37
+ fillIncompleteLastRow: boolean
36
38
  trackLines: Record<string, NormalizedWaveformGridLineOptions>
37
39
  }
38
40
 
@@ -48,6 +50,7 @@ export interface GridCellGeometry {
48
50
  plotHeight: number
49
51
  cellHeight: number
50
52
  xAxisBand: number
53
+ isLastRow?: boolean
51
54
  }
52
55
 
53
56
  const normalizeCount = (value: unknown, fallback: number) => {
@@ -78,6 +81,7 @@ export function normalizeGridOptions(options?: WaveformGridOptions): NormalizedW
78
81
  rowCount: normalizeCount(options?.rowCount, 2),
79
82
  columnCount: normalizeCount(options?.columnCount, 1),
80
83
  showPagination: options?.showPagination ?? true,
84
+ fillIncompleteLastRow: options?.fillIncompleteLastRow ?? false,
81
85
  trackLines,
82
86
  }
83
87
  }
@@ -114,20 +118,23 @@ export function resolveGridCellGeometry(
114
118
  horizontalGap?: number,
115
119
  showXAxis = true,
116
120
  ): GridCellGeometry[] {
121
+ const pageSize = getPageSize(options)
122
+ const seriesCount = slotHasSeries.filter(Boolean).length
123
+ const filledLastRow = options.fillIncompleteLastRow && seriesCount > 0 && seriesCount < pageSize
124
+ const rowCount = filledLastRow ? Math.ceil(seriesCount / options.columnCount) : options.rowCount
117
125
  const defaultGap = getGridGap(displayMode)
118
126
  const columnGap = Number.isFinite(horizontalGap)
119
127
  ? Math.max(0, horizontalGap as number)
120
128
  : defaultGap
121
- const totalHorizontalGap = Math.max(0, options.columnCount - 1) * columnGap
122
129
  const axisRows = new Set<number>()
123
130
  if (!showXAxis) {
124
131
  // Net view uses the full drawing area for waveform pixels.
125
132
  } else if (displayMode === 'independent') {
126
- for (let row = 0; row < options.rowCount; row += 1) axisRows.add(row)
133
+ for (let row = 0; row < rowCount; row += 1) axisRows.add(row)
127
134
  } else if (displayMode === 'compact') {
128
135
  // Compact tracks share one continuous plot stack. Reserve the X-axis band
129
136
  // only beneath the final grid row, including when that row is empty.
130
- axisRows.add(options.rowCount - 1)
137
+ axisRows.add(rowCount - 1)
131
138
  } else {
132
139
  for (let column = 0; column < options.columnCount; column += 1) {
133
140
  for (let slotIndex = getPageSize(options) - 1; slotIndex >= 0; slotIndex -= 1) {
@@ -139,17 +146,22 @@ export function resolveGridCellGeometry(
139
146
  }
140
147
  }
141
148
  }
142
- const totalVerticalGap = Math.max(0, options.rowCount - 1) * defaultGap
149
+ const totalVerticalGap = Math.max(0, rowCount - 1) * defaultGap
143
150
  const totalAxisBand = axisRows.size * X_AXIS_BAND
144
- const width = Math.max(1, (innerWidth - totalHorizontalGap) / options.columnCount)
145
151
  const plotHeight = Math.max(
146
152
  1,
147
- (innerHeight - totalVerticalGap - totalAxisBand) / options.rowCount,
153
+ (innerHeight - totalVerticalGap - totalAxisBand) / rowCount,
148
154
  )
149
155
 
150
- return Array.from({ length: getPageSize(options) }, (_, slotIndex) => {
156
+ return Array.from({ length: filledLastRow ? seriesCount : pageSize }, (_, slotIndex) => {
151
157
  const row = Math.floor(slotIndex / options.columnCount)
152
158
  const column = slotIndex % options.columnCount
159
+ const rowSeriesCount =
160
+ filledLastRow && row === rowCount - 1
161
+ ? seriesCount - row * options.columnCount
162
+ : options.columnCount
163
+ const rowGap = Math.max(0, rowSeriesCount - 1) * columnGap
164
+ const width = Math.max(1, (innerWidth - rowGap) / rowSeriesCount)
153
165
  const xAxisBand = axisRows.has(row) ? X_AXIS_BAND : 0
154
166
  const cellHeight = plotHeight + xAxisBand
155
167
  const top = Array.from({ length: row }, (_, previousRow) => {
@@ -167,6 +179,7 @@ export function resolveGridCellGeometry(
167
179
  plotHeight,
168
180
  cellHeight,
169
181
  xAxisBand,
182
+ isLastRow: row === rowCount - 1,
170
183
  }
171
184
  })
172
185
  }
@@ -9,6 +9,7 @@ import {
9
9
  findClosestTrackAtPointer,
10
10
  MAX_MULTI_Y_AXIS_COUNT,
11
11
  measureYAxisGroupClearance,
12
+ resolveYAxisTickCount,
12
13
  } from './layout'
13
14
 
14
15
  function series(id: string, minimum: number, maximum: number): DisplaySeries {
@@ -64,7 +65,13 @@ function layoutForSeries(
64
65
  series: sourceTrack,
65
66
  },
66
67
  ],
67
- grid: { rowCount: 1, columnCount: 1, showPagination: false, trackLines: {} },
68
+ grid: {
69
+ rowCount: 1,
70
+ columnCount: 1,
71
+ showPagination: false,
72
+ fillIncompleteLastRow: false,
73
+ trackLines: {},
74
+ },
68
75
  displayMode: 'independent',
69
76
  overlayMode: 'single-axis',
70
77
  independentTransforms: [transform],
@@ -106,7 +113,13 @@ describe('multi-value Y-axis grouping', () => {
106
113
  series: sourceTrack,
107
114
  },
108
115
  ],
109
- grid: { rowCount: 1, columnCount: 1, showPagination: false, trackLines: {} },
116
+ grid: {
117
+ rowCount: 1,
118
+ columnCount: 1,
119
+ showPagination: false,
120
+ fillIncompleteLastRow: false,
121
+ trackLines: {},
122
+ },
110
123
  displayMode: 'independent',
111
124
  overlayMode: 'single-axis',
112
125
  independentTransforms: [zoomIdentity],
@@ -119,8 +132,15 @@ describe('multi-value Y-axis grouping', () => {
119
132
  showCompactEmptyTracks: false,
120
133
  })[0]
121
134
 
122
- expect(result?.yScale.domain()).toEqual([25, 75])
123
- expect(result?.seriesPaths[0]?.yScale.domain()).toEqual([25, 75])
135
+ expect(result?.yScale.domain()).toEqual([20, 80])
136
+ expect(result?.seriesPaths[0]?.yScale.domain()).toEqual([20, 80])
137
+ })
138
+
139
+ it('normalizes configured Y-axis split counts', () => {
140
+ expect(resolveYAxisTickCount(100, 0)).toBe(2)
141
+ expect(resolveYAxisTickCount(100, 4.8)).toBe(4)
142
+ expect(resolveYAxisTickCount(100, Number.NaN)).toBe(5)
143
+ expect(resolveYAxisTickCount(220)).toBe(5)
124
144
  })
125
145
 
126
146
  it('keeps every overlaid series on one axis in single-axis mode', () => {
@@ -209,7 +229,13 @@ describe('multi-value Y-axis grouping', () => {
209
229
  series: track([series('left', 1000, 3000), series('right', 1000, 3000)]),
210
230
  },
211
231
  ],
212
- grid: { rowCount: 1, columnCount: 1, showPagination: false, trackLines: {} },
232
+ grid: {
233
+ rowCount: 1,
234
+ columnCount: 1,
235
+ showPagination: false,
236
+ fillIncompleteLastRow: false,
237
+ trackLines: {},
238
+ },
213
239
  displayMode: 'independent',
214
240
  overlayMode: 'multi-axis',
215
241
  independentTransforms: [zoomIdentity],