waveform-analysis 0.1.43 → 0.1.47

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 (42) hide show
  1. package/README.md +37 -30
  2. package/dist/index.cjs +1 -1
  3. package/dist/index.js +456 -461
  4. package/dist/style.css +1 -1
  5. package/dist/types/components/core/constants.d.ts +2 -2
  6. package/dist/types/components/core/grid.d.ts +3 -0
  7. package/dist/types/components/core/types.d.ts +4 -2
  8. package/dist/types/components/core/yAxisConstants.d.ts +2 -2
  9. package/dist/types/components/interaction/WaveformTooltip.vue.d.ts +1 -1
  10. package/dist/types/components/interaction/useWaveformViewport.d.ts +2 -1
  11. package/package.json +1 -1
  12. package/src/App.test.ts +2 -7
  13. package/src/components/WaveformChart.css +1 -1
  14. package/src/components/WaveformChartView.vue +2 -2
  15. package/src/components/core/constants.ts +3 -3
  16. package/src/components/core/grid.test.ts +13 -1
  17. package/src/components/core/grid.ts +14 -4
  18. package/src/components/core/layout.test.ts +3 -3
  19. package/src/components/core/layout.ts +2 -2
  20. package/src/components/core/trackLayoutBuilder.ts +14 -23
  21. package/src/components/core/types.ts +4 -2
  22. package/src/components/core/useWaveformChartController.ts +1 -0
  23. package/src/components/core/useWaveformLayout.ts +12 -6
  24. package/src/components/core/yAxisConstants.ts +2 -2
  25. package/src/components/interaction/WaveformHoverHost.vue +3 -1
  26. package/src/components/interaction/WaveformTooltip.test.ts +23 -0
  27. package/src/components/interaction/WaveformTooltip.vue +9 -6
  28. package/src/components/interaction/useWaveformHover.ts +21 -15
  29. package/src/components/interaction/useWaveformViewport.ts +20 -15
  30. package/src/components/interaction/useWaveformZoom.ts +1 -1
  31. package/src/components/rendering/WaveformHoverLayer.vue +5 -11
  32. package/src/components/rendering/WaveformTrack.vue +1 -0
  33. package/src/components/rendering/WaveformTrackAxes.vue +8 -6
  34. package/src/components/waveformChartCases/dataLifecycle.test.ts +1 -1
  35. package/src/components/waveformChartCases/frameGridAndFormatting.test.ts +10 -0
  36. package/src/components/waveformChartCases/incompleteDataHover.test.ts +67 -0
  37. package/src/components/waveformChartCases/layoutAndDomains.test.ts +5 -5
  38. package/src/components/waveformChartCases/legendVisibility.test.ts +1 -1
  39. package/src/components/waveformChartCases/multiAxisAndPagination.test.ts +50 -1
  40. package/src/components/waveformChartCases/trackOrder.test.ts +131 -0
  41. package/src/data/simulatedWaveforms.test.ts +7 -2
  42. package/src/data/simulatedWaveforms.ts +12 -7
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-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}
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-b9ed144d]{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-b9ed144d]{grid-template-columns:8px minmax(0,1fr);align-items:start;gap:6px;display:grid}.waveform-tooltip__series i[data-v-b9ed144d]{border-radius:50%;flex:none;width:8px;height:8px;margin-top:4px}.waveform-tooltip__series-content[data-v-b9ed144d]{flex-wrap:nowrap;align-items:baseline;gap:12px;min-width:0;display:flex}.waveform-tooltip__series-label[data-v-b9ed144d]{text-overflow:ellipsis;white-space:nowrap;flex:auto;min-width:0;overflow:hidden}.waveform-tooltip__value[data-v-b9ed144d]{white-space:nowrap;flex:none}.waveform-tooltip__series strong[data-v-b9ed144d]{font-weight:600}.waveform-tooltip__value[data-v-b9ed144d]{color:#555}.waveform-tooltip__series small[data-v-b9ed144d]{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-044318c2]{pointer-events:none}.waveform-track__crosshair line[data-v-044318c2]{stroke:#57617b;stroke-width:1px;stroke-dasharray:4 3}.waveform-chart{box-sizing:border-box;color:#475467;background:#fff;border:1.2px 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*/
@@ -37,11 +37,11 @@ export declare const Y_AXIS_OUTER_PADDING = 4;
37
37
  /**
38
38
  * Y轴标签间距(像素)
39
39
  */
40
- export declare const Y_AXIS_LABEL_GAP = 6;
40
+ export declare const Y_AXIS_LABEL_GAP = 0;
41
41
  /**
42
42
  * Y轴标签带宽度(像素)
43
43
  */
44
- export declare const Y_AXIS_LABEL_BAND_WIDTH = 24;
44
+ export declare const Y_AXIS_LABEL_BAND_WIDTH = 12;
45
45
  /**
46
46
  * Y轴指数标签间距(像素)
47
47
  */
@@ -7,6 +7,8 @@ export interface WaveformGridOptions {
7
7
  columnCount?: number;
8
8
  showPagination?: boolean;
9
9
  fillIncompleteLastRow?: boolean;
10
+ /** Stable grid slots. Empty IDs remain as empty chart frames and keep their page position. */
11
+ trackOrder?: string[];
10
12
  trackLines?: WaveformGridTrackLines;
11
13
  }
12
14
  export interface WaveformGridLineOptions {
@@ -29,6 +31,7 @@ export interface NormalizedWaveformGridOptions {
29
31
  columnCount: number;
30
32
  showPagination: boolean;
31
33
  fillIncompleteLastRow: boolean;
34
+ trackOrder?: string[];
32
35
  trackLines: Record<string, NormalizedWaveformGridLineOptions>;
33
36
  }
34
37
  export interface GridCellGeometry {
@@ -58,11 +58,12 @@ export interface HoveredSeriesPoint {
58
58
  unit?: string;
59
59
  color: string;
60
60
  trackIndex: number;
61
- point: WaveformPoint;
61
+ point: WaveformPoint | null;
62
62
  }
63
63
  export interface WaveformHoverState {
64
64
  points: HoveredSeriesPoint[];
65
65
  trackIndex: number | null;
66
+ queryX: number | null;
66
67
  position: {
67
68
  x: number;
68
69
  y: number;
@@ -75,7 +76,8 @@ export interface TrackLayout {
75
76
  /** Stable track key derived from trackId, or from the series id when trackId is omitted. */
76
77
  id: string;
77
78
  index: number;
78
- series: DisplaySeries;
79
+ /** Primary visible series, if this grid slot has waveform data. */
80
+ series: DisplaySeries | null;
79
81
  /** Visible series used by rendering and interaction code. */
80
82
  seriesList: DisplaySeries[];
81
83
  /** Complete series list used by the legend. */
@@ -1,6 +1,6 @@
1
1
  export declare const Y_AXIS_CHARACTER_WIDTH = 7;
2
2
  export declare const Y_AXIS_TICK_PADDING = 7;
3
3
  export declare const Y_AXIS_OUTER_PADDING = 4;
4
- export declare const Y_AXIS_LABEL_GAP = 6;
5
- export declare const Y_AXIS_LABEL_BAND_WIDTH = 24;
4
+ export declare const Y_AXIS_LABEL_GAP = 0;
5
+ export declare const Y_AXIS_LABEL_BAND_WIDTH = 12;
6
6
  export declare function resolveYAxisSides(axisCount: number): Array<'left' | 'right'>;
@@ -5,7 +5,7 @@ interface SeriesPoint {
5
5
  shotNo?: string;
6
6
  color: string;
7
7
  unit?: string;
8
- point: WaveformPoint;
8
+ point: WaveformPoint | null;
9
9
  }
10
10
  interface Props {
11
11
  /** 是否显示 */
@@ -1,7 +1,8 @@
1
1
  import { type ZoomTransform } from 'd3';
2
2
  import { type ComputedRef, type Ref, type ShallowRef } from 'vue';
3
3
  import type { DisplayTrack, TrackLayout } from '../core/types';
4
- import type { ResolvedWaveformChartProps, ViewportSelectionState, WaveformChartEmit } from '../core/waveformChartTypes';
4
+ import type { ResolvedWaveformChartProps, ViewportSelectionState } from '../core/waveformChartTypes';
5
+ import type { WaveformChartEmit } from '../core/waveformChartTypes';
5
6
  import type { AnnotationSeriesCandidate } from '../annotation';
6
7
  interface ViewportContext {
7
8
  props: ResolvedWaveformChartProps;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "waveform-analysis",
3
- "version": "0.1.43",
3
+ "version": "0.1.47",
4
4
  "main": "./dist/index.cjs",
5
5
  "module": "./dist/index.js",
6
6
  "types": "./dist/types/index.d.ts",
package/src/App.test.ts CHANGED
@@ -18,23 +18,19 @@ describe('App workspace layout', { timeout: 20_000 }, () => {
18
18
  ? data.series[0].data.points.length
19
19
  : 0
20
20
  const initialPointCount = pointCount(chart.props('data') as WaveformData)
21
-
22
21
  chart.vm.$emit('zoom-end', { start: 0, end: 0.001 })
23
22
  await wrapper.get('[aria-label="重置波形视图"]').trigger('click')
24
23
  await vi.advanceTimersByTimeAsync(100)
25
24
  await flushPromises()
26
-
27
25
  expect(pointCount(chart.props('data') as WaveformData)).toBe(initialPointCount)
28
26
  } finally {
29
27
  wrapper.unmount()
30
28
  vi.useRealTimers()
31
29
  }
32
30
  })
33
-
34
31
  it('places controls in the sidebar beside the chart', async () => {
35
32
  const wrapper = mount(App)
36
33
  await flushPromises()
37
-
38
34
  const panel = wrapper.get('#waveform-control-panel')
39
35
  const frameControls = panel.get('.frame-style-controls')
40
36
  expect(panel.find('h1').exists()).toBe(false)
@@ -88,7 +84,6 @@ describe('App workspace layout', { timeout: 20_000 }, () => {
88
84
  expect(legendColorPicker.props('disableAlpha')).toBe(false)
89
85
  expect(panel.text()).not.toContain('数据摘要')
90
86
  expect(wrapper.get('.chart-panel').find('.waveform-chart').exists()).toBe(true)
91
-
92
87
  wrapper.unmount()
93
88
  })
94
89
 
@@ -262,8 +257,8 @@ describe('App workspace layout', { timeout: 20_000 }, () => {
262
257
  simulatedSeries.forEach((item) => {
263
258
  expect(item.data.kind).toBe('points')
264
259
  if (item.data.kind === 'points') {
265
- expect(item.data.points).toHaveLength(1000)
266
- expect(item.data.points[0]?.x).toBe(-5)
260
+ expect(item.data.points).toHaveLength(item.name === '阶跃响应' ? 500 : 1000)
261
+ expect(item.data.points[0]?.x).toBeCloseTo(item.name === '阶跃响应' ? 5 / 999 : -5, 10)
267
262
  }
268
263
  })
269
264
 
@@ -6,7 +6,7 @@
6
6
  overflow: hidden;
7
7
  color: #475467;
8
8
  background: #fff;
9
- border: 1px solid rgb(0 0 0 / 8%);
9
+ border: 1.2px solid rgb(0 0 0 / 8%);
10
10
  border-radius: 6px;
11
11
  }
12
12
 
@@ -222,7 +222,7 @@ function handleChartPointerLeave() {
222
222
  <!-- 轨道渲染 -->
223
223
  <WaveformTrack
224
224
  v-for="track in trackLayouts"
225
- :key="`${track.index}-${track.series.name}`"
225
+ :key="`${track.index}-${track.id}`"
226
226
  :track="track"
227
227
  :clip-path-id="clipPathId"
228
228
  :inner-width="innerWidth"
@@ -277,7 +277,7 @@ function handleChartPointerLeave() {
277
277
  <g v-if="!isCleanView" class="waveform-chart__legend-layer">
278
278
  <g
279
279
  v-for="track in trackLayouts"
280
- :key="`legend-${track.index}-${track.series.name}`"
280
+ :key="`legend-${track.index}-${track.id}`"
281
281
  class="waveform-chart__legend-track"
282
282
  :data-legend-track-index="track.index"
283
283
  :data-legend-track-id="track.id"
@@ -5,7 +5,7 @@
5
5
  // ==================== 布局常量 ====================
6
6
 
7
7
  /** 图表边距 */
8
- export const margin = { top: 18, right: 24, bottom: 52, left: 64 }
8
+ export const margin = { top: 18, right: 24, bottom: 52, left: 80 }
9
9
 
10
10
  /** Distance from the drawing SVG bottom edge to the X-axis title baseline. */
11
11
  export const X_AXIS_TITLE_BOTTOM_OFFSET = 12
@@ -44,12 +44,12 @@ export const Y_AXIS_OUTER_PADDING = 4
44
44
  /**
45
45
  * Y轴标签间距(像素)
46
46
  */
47
- export const Y_AXIS_LABEL_GAP = 6
47
+ export const Y_AXIS_LABEL_GAP = 0
48
48
 
49
49
  /**
50
50
  * Y轴标签带宽度(像素)
51
51
  */
52
- export const Y_AXIS_LABEL_BAND_WIDTH = 24
52
+ export const Y_AXIS_LABEL_BAND_WIDTH = 12
53
53
 
54
54
  /**
55
55
  * Y轴指数标签间距(像素)
@@ -16,6 +16,7 @@ describe('waveform grid helpers', () => {
16
16
  columnCount: 1,
17
17
  showPagination: true,
18
18
  fillIncompleteLastRow: false,
19
+ trackOrder: [],
19
20
  trackLines: {},
20
21
  })
21
22
  expect(normalizeGridOptions({ rowCount: 0, columnCount: 99 })).toEqual({
@@ -23,10 +24,17 @@ describe('waveform grid helpers', () => {
23
24
  columnCount: 10,
24
25
  showPagination: true,
25
26
  fillIncompleteLastRow: false,
27
+ trackOrder: [],
26
28
  trackLines: {},
27
29
  })
28
30
  })
29
31
 
32
+ it('normalizes track order by trimming, dropping blanks, and removing duplicates', () => {
33
+ expect(
34
+ normalizeGridOptions({ trackOrder: [' voltage ', '', 'current', 'voltage', ' '] }),
35
+ ).toMatchObject({ trackOrder: ['voltage', 'current'] })
36
+ })
37
+
30
38
  it('normalizes per-track grid line visibility with visible defaults', () => {
31
39
  expect(
32
40
  normalizeGridOptions({
@@ -170,7 +178,11 @@ describe('waveform grid helpers', () => {
170
178
 
171
179
  it('keeps full pages and the disabled option byte-for-byte compatible', () => {
172
180
  const base = normalizeGridOptions({ rowCount: 2, columnCount: 2 })
173
- const filled = normalizeGridOptions({ rowCount: 2, columnCount: 2, fillIncompleteLastRow: true })
181
+ const filled = normalizeGridOptions({
182
+ rowCount: 2,
183
+ columnCount: 2,
184
+ fillIncompleteLastRow: true,
185
+ })
174
186
  const fullSlots = [true, true, true, true]
175
187
  expect(resolveGridCellGeometry(400, 300, filled, 'independent', fullSlots)).toEqual(
176
188
  resolveGridCellGeometry(400, 300, base, 'independent', fullSlots),
@@ -9,6 +9,8 @@ export interface WaveformGridOptions {
9
9
  columnCount?: number
10
10
  showPagination?: boolean
11
11
  fillIncompleteLastRow?: boolean
12
+ /** Stable grid slots. Empty IDs remain as empty chart frames and keep their page position. */
13
+ trackOrder?: string[]
12
14
  trackLines?: WaveformGridTrackLines
13
15
  }
14
16
 
@@ -35,6 +37,7 @@ export interface NormalizedWaveformGridOptions {
35
37
  columnCount: number
36
38
  showPagination: boolean
37
39
  fillIncompleteLastRow: boolean
40
+ trackOrder?: string[]
38
41
  trackLines: Record<string, NormalizedWaveformGridLineOptions>
39
42
  }
40
43
 
@@ -59,6 +62,15 @@ const normalizeCount = (value: unknown, fallback: number) => {
59
62
  return Math.min(GRID_MAX_COUNT, Math.max(GRID_MIN_COUNT, Math.floor(numeric)))
60
63
  }
61
64
 
65
+ const normalizeTrackOrder = (trackOrder: unknown): string[] => {
66
+ if (!Array.isArray(trackOrder)) return []
67
+ const normalized = trackOrder
68
+ .filter((trackId): trackId is string => typeof trackId === 'string')
69
+ .map((trackId) => trackId.trim())
70
+ .filter(Boolean)
71
+ return Array.from(new Set(normalized))
72
+ }
73
+
62
74
  export function normalizeGridOptions(options?: WaveformGridOptions): NormalizedWaveformGridOptions {
63
75
  const trackLines = Object.fromEntries(
64
76
  Object.entries(options?.trackLines ?? {}).map(([trackId, lines]) => [
@@ -82,6 +94,7 @@ export function normalizeGridOptions(options?: WaveformGridOptions): NormalizedW
82
94
  columnCount: normalizeCount(options?.columnCount, 1),
83
95
  showPagination: options?.showPagination ?? true,
84
96
  fillIncompleteLastRow: options?.fillIncompleteLastRow ?? false,
97
+ trackOrder: normalizeTrackOrder(options?.trackOrder),
85
98
  trackLines,
86
99
  }
87
100
  }
@@ -148,10 +161,7 @@ export function resolveGridCellGeometry(
148
161
  }
149
162
  const totalVerticalGap = Math.max(0, rowCount - 1) * defaultGap
150
163
  const totalAxisBand = axisRows.size * X_AXIS_BAND
151
- const plotHeight = Math.max(
152
- 1,
153
- (innerHeight - totalVerticalGap - totalAxisBand) / rowCount,
154
- )
164
+ const plotHeight = Math.max(1, (innerHeight - totalVerticalGap - totalAxisBand) / rowCount)
155
165
 
156
166
  return Array.from({ length: filledLastRow ? seriesCount : pageSize }, (_, slotIndex) => {
157
167
  const row = Math.floor(slotIndex / options.columnCount)
@@ -253,8 +253,8 @@ describe('multi-value Y-axis grouping', () => {
253
253
  labelOffset: Math.abs(labelX - x),
254
254
  })),
255
255
  ).toEqual([
256
- { side: 'left', labelOffset: 67 },
257
- { side: 'right', labelOffset: 67 },
256
+ { side: 'left', labelOffset: 55 },
257
+ { side: 'right', labelOffset: 55 },
258
258
  ])
259
259
  })
260
260
 
@@ -262,7 +262,7 @@ describe('multi-value Y-axis grouping', () => {
262
262
  const [group] = buildYAxisSeriesGroups(track([series('long', -1e120, 1e120)]), 'multi-axis')
263
263
 
264
264
  expect(group).toBeDefined()
265
- expect(measureYAxisGroupClearance(group!)).toBe(90)
265
+ expect(measureYAxisGroupClearance(group!)).toBe(72)
266
266
  })
267
267
  })
268
268
 
@@ -17,8 +17,8 @@ export { MAX_MULTI_Y_AXIS_COUNT } from './constants'
17
17
  const Y_AXIS_CHARACTER_WIDTH = 7
18
18
  const Y_AXIS_TICK_PADDING = 7
19
19
  const Y_AXIS_OUTER_PADDING = 4
20
- const Y_AXIS_LABEL_GAP = 6
21
- const Y_AXIS_LABEL_BAND_WIDTH = 24
20
+ const Y_AXIS_LABEL_GAP = 0
21
+ const Y_AXIS_LABEL_BAND_WIDTH = 12
22
22
 
23
23
  export function resolveYAxisTickCount(_plotHeight: number, splitNumber?: number): number {
24
24
  if (typeof splitNumber === 'number' && Number.isFinite(splitNumber)) {
@@ -26,7 +26,7 @@ import {
26
26
  type NormalizedWaveformGridOptions,
27
27
  } from './grid'
28
28
  import { axisTextMetrics, resolveYAxisSeriesGroups, resolveYAxisTickCount } from './layout'
29
- import type { DisplaySeries, DisplayTrack, TrackLayout, WaveformYAxisLayout } from './types'
29
+ import type { DisplayTrack, TrackLayout, WaveformYAxisLayout } from './types'
30
30
  import { applyXDomainStrategy } from './xDomain'
31
31
  import {
32
32
  Y_AXIS_LABEL_BAND_WIDTH,
@@ -81,33 +81,24 @@ export function buildTrackLayouts(options: BuildTrackLayoutsOptions): TrackLayou
81
81
  const bottomCells = getBottomRowCellIndexes(visibleCells, options.grid.columnCount)
82
82
 
83
83
  return visibleCells.flatMap((cell, index) => {
84
- const isEmpty = !cell.series
85
- if (isEmpty && (options.displayMode !== 'compact' || !options.showCompactEmptyTracks)) return []
86
- const emptySeries: DisplaySeries = {
84
+ const isEmpty = !cell.series || cell.series.series.length === 0
85
+ if (!cell.series && (options.displayMode !== 'compact' || !options.showCompactEmptyTracks))
86
+ return []
87
+ const displayTrack: DisplayTrack = cell.series ?? {
87
88
  id: `empty-grid-slot-${cell.slotIndex}`,
88
- name: '',
89
- color: 'transparent',
90
- lineType: 'linear',
91
- lineStyle: 'solid',
92
- pointType: 'none',
93
- errorBar: { visible: false, width: 1.5, capWidth: 8 },
94
- points: [],
89
+ series: [],
90
+ visibleSeries: [],
95
91
  xDomain: [0, 1],
96
92
  yDomain: [0, 1],
97
- hasErrorPoints: false,
98
- }
99
- const displayTrack: DisplayTrack = cell.series ?? {
100
- id: emptySeries.id,
101
- series: [emptySeries],
102
- visibleSeries: [emptySeries],
103
- xDomain: emptySeries.xDomain,
104
- yDomain: emptySeries.yDomain,
105
93
  }
106
94
  const hasVisibleSeries = !isEmpty && displayTrack.visibleSeries.length > 0
107
- const series = displayTrack.visibleSeries[0] ?? displayTrack.series[0] ?? emptySeries
95
+ const series = displayTrack.visibleSeries[0] ?? displayTrack.series[0] ?? null
108
96
  const baseXScale =
109
97
  options.displayMode === 'independent'
110
- ? scaleLinear(resolveIndependentXDomain(displayTrack, series.id, options), [0, cell.width])
98
+ ? scaleLinear(
99
+ resolveIndependentXDomain(displayTrack, series?.id ?? displayTrack.id, options),
100
+ [0, cell.width],
101
+ )
111
102
  : scaleLinear(options.sharedZoomDomain, [0, cell.width])
112
103
  const transform =
113
104
  options.displayMode === 'independent'
@@ -162,7 +153,7 @@ export function buildTrackLayouts(options: BuildTrackLayoutsOptions): TrackLayou
162
153
  labelX,
163
154
  scale,
164
155
  majorTicks,
165
- minorTicks: buildMinorTicks(majorTicks),
156
+ minorTicks: buildMinorTicks(majorTicks, 2),
166
157
  tickValues,
167
158
  seriesList: group.seriesList,
168
159
  }
@@ -248,7 +239,7 @@ export function buildTrackLayouts(options: BuildTrackLayoutsOptions): TrackLayou
248
239
  yScale,
249
240
  yAxes,
250
241
  xMajorTicks,
251
- xMinorTicks: buildMinorTicks(xMajorTicks, 5, domain),
242
+ xMinorTicks: buildMinorTicks(xMajorTicks, 3, domain),
252
243
  yMajorTicks,
253
244
  yMinorTicks: yAxes[0]?.minorTicks ?? [],
254
245
  yAxisTickValues,
@@ -69,12 +69,13 @@ export interface HoveredSeriesPoint {
69
69
  unit?: string
70
70
  color: string
71
71
  trackIndex: number
72
- point: WaveformPoint
72
+ point: WaveformPoint | null
73
73
  }
74
74
 
75
75
  export interface WaveformHoverState {
76
76
  points: HoveredSeriesPoint[]
77
77
  trackIndex: number | null
78
+ queryX: number | null
78
79
  position: { x: number; y: number }
79
80
  }
80
81
 
@@ -85,7 +86,8 @@ export interface TrackLayout {
85
86
  /** Stable track key derived from trackId, or from the series id when trackId is omitted. */
86
87
  id: string
87
88
  index: number
88
- series: DisplaySeries
89
+ /** Primary visible series, if this grid slot has waveform data. */
90
+ series: DisplaySeries | null
89
91
  /** Visible series used by rendering and interaction code. */
90
92
  seriesList: DisplaySeries[]
91
93
  /** Complete series list used by the legend. */
@@ -59,6 +59,7 @@ export function useWaveformChartController(
59
59
  const hoverState = shallowReactive<WaveformHoverState>({
60
60
  points: [],
61
61
  trackIndex: null,
62
+ queryX: null,
62
63
  position: { x: 0, y: 0 },
63
64
  })
64
65
  const suppressHoverUntilMove = ref(false)
@@ -75,6 +75,7 @@ export function useWaveformLayout(context: LayoutContext) {
75
75
  (index === 0 ? props.lineColor : channelColors[index % channelColors.length]),
76
76
  })),
77
77
  )
78
+ const gridOptions = computed(() => normalizeGridOptions(props.grid))
78
79
  const chartTracks = computed<DisplayTrack[]>(() => {
79
80
  const groupedSeries = new Map<string, DisplaySeries[]>()
80
81
  chartSeries.value.forEach((series) => {
@@ -83,7 +84,13 @@ export function useWaveformLayout(context: LayoutContext) {
83
84
  if (trackSeries) trackSeries.push(series)
84
85
  else groupedSeries.set(trackId, [series])
85
86
  })
86
- return Array.from(groupedSeries, ([id, series]) => {
87
+ const trackOrder = gridOptions.value.trackOrder ?? []
88
+ const orderedTrackIds = [
89
+ ...trackOrder,
90
+ ...Array.from(groupedSeries.keys()).filter((trackId) => !trackOrder.includes(trackId)),
91
+ ]
92
+ return orderedTrackIds.map((id) => {
93
+ const series = groupedSeries.get(id) ?? []
87
94
  const visibleSeries = series.filter((item) => !hiddenSeriesIdSet.value.has(item.id))
88
95
  const xDomainValues: number[] = []
89
96
  const yDomainValues: number[] = []
@@ -95,12 +102,11 @@ export function useWaveformLayout(context: LayoutContext) {
95
102
  id,
96
103
  series,
97
104
  visibleSeries,
98
- xDomain: paddedDomain(xDomainValues),
99
- yDomain: paddedDomain(yDomainValues),
105
+ xDomain: xDomainValues.length ? paddedDomain(xDomainValues) : [0, 1],
106
+ yDomain: yDomainValues.length ? paddedDomain(yDomainValues) : [0, 1],
100
107
  }
101
108
  })
102
109
  })
103
- const gridOptions = computed(() => normalizeGridOptions(props.grid))
104
110
  const renderingOptions = computed(() => resolveWaveformRenderingOptions(props.rendering))
105
111
  const pageCount = computed(() => getPageCount(chartTracks.value.length, gridOptions.value))
106
112
  const pagedTracks = computed(() =>
@@ -245,8 +251,8 @@ export function useWaveformLayout(context: LayoutContext) {
245
251
  })
246
252
  const resolveInitialTrackDomain = (track: TrackLayout): [number, number] => {
247
253
  const configuredDomain =
248
- props.initialXDomains?.[track.series.trackId ?? track.series.id] ??
249
- props.initialXDomains?.[track.series.id] ??
254
+ props.initialXDomains?.[track.series?.trackId ?? track.series?.id ?? track.id] ??
255
+ (track.series ? props.initialXDomains?.[track.series.id] : undefined) ??
250
256
  props.initialXDomain
251
257
  if (
252
258
  configuredDomain &&
@@ -1,8 +1,8 @@
1
1
  export const Y_AXIS_CHARACTER_WIDTH = 7
2
2
  export const Y_AXIS_TICK_PADDING = 7
3
3
  export const Y_AXIS_OUTER_PADDING = 4
4
- export const Y_AXIS_LABEL_GAP = 6
5
- export const Y_AXIS_LABEL_BAND_WIDTH = 24
4
+ export const Y_AXIS_LABEL_GAP = 0
5
+ export const Y_AXIS_LABEL_BAND_WIDTH = 12
6
6
 
7
7
  export function resolveYAxisSides(axisCount: number): Array<'left' | 'right'> {
8
8
  if (axisCount >= 4) return ['left', 'left', 'right', 'right']
@@ -12,7 +12,9 @@ const props = defineProps<{
12
12
  containerHeight: number
13
13
  }>()
14
14
 
15
- const hoveredPoint = computed(() => props.state.points[0]?.point ?? null)
15
+ const hoveredPoint = computed(
16
+ () => props.state.points.find((seriesPoint) => seriesPoint.point)?.point ?? null,
17
+ )
16
18
  </script>
17
19
 
18
20
  <template>
@@ -170,4 +170,27 @@ describe('WaveformTooltip', () => {
170
170
 
171
171
  expect(wrapper.get('.waveform-tooltip__series-label').text()).toBe('未配置炮号: 温度')
172
172
  })
173
+
174
+ it('uses each series sample time and marks unavailable series without synthesizing values', () => {
175
+ const wrapper = mount(WaveformTooltip, {
176
+ props: {
177
+ visible: true,
178
+ position: { x: 10, y: 10 },
179
+ timeUnit: 's',
180
+ hoveredPoint: { x: 1, y: 12 },
181
+ seriesPoints: [
182
+ { trackIndex: 0, name: '完整数据', color: '#f00', point: { x: 1, y: 12 } },
183
+ { trackIndex: 1, name: '异步采样', color: '#0a0', point: { x: 1.25, y: 8 } },
184
+ { trackIndex: 2, name: '缺失数据', color: '#00f', point: null },
185
+ ],
186
+ containerWidth: 400,
187
+ containerHeight: 300,
188
+ },
189
+ })
190
+
191
+ const values = wrapper
192
+ .findAll('.waveform-tooltip__value')
193
+ .map((value) => value.text())
194
+ expect(values).toEqual(['(x:1 y:12)', '(x:1.25 y:8)', '无数据'])
195
+ })
173
196
  })
@@ -9,7 +9,7 @@ interface SeriesPoint {
9
9
  shotNo?: string
10
10
  color: string
11
11
  unit?: string
12
- point: WaveformPoint
12
+ point: WaveformPoint | null
13
13
  }
14
14
 
15
15
  interface Props {
@@ -45,10 +45,12 @@ function estimateLineCount(text: string, width: number): number {
45
45
  }
46
46
 
47
47
  function formatSeriesText(seriesPoint: SeriesPoint): string {
48
- const timeText = formatTooltipTime(props.hoveredPoint?.x ?? seriesPoint.point.x, props.timeUnit)
48
+ const valueText = seriesPoint.point
49
+ ? `(x:${formatTooltipTime(seriesPoint.point.x, props.timeUnit)} y:${formatTooltipNumber(seriesPoint.point.y)})`
50
+ : '无数据'
49
51
  return `${seriesPoint.shotNo?.trim() || '未配置炮号'}: ${seriesPoint.name}${
50
52
  seriesPoint.unit ? `(${seriesPoint.unit})` : ''
51
- } (x:${timeText} y:${formatTooltipNumber(seriesPoint.point.y)})`
53
+ } ${valueText}`
52
54
  }
53
55
 
54
56
  function estimateTooltipHeight(width: number): number {
@@ -114,9 +116,10 @@ const tooltipStyle = computed(() => {
114
116
  <span class="waveform-tooltip__series-label">{{ seriesPoint.shotNo?.trim() || '未配置炮号' }}: {{
115
117
  seriesPoint.name
116
118
  }}<template v-if="seriesPoint.unit">({{ seriesPoint.unit }})</template></span>
117
- <span class="waveform-tooltip__value">(x:{{ formatTooltipTime(hoveredPoint.x, timeUnit) }} y:{{
118
- formatTooltipNumber(seriesPoint.point.y)
119
- }})</span>
119
+ <span v-if="seriesPoint.point" class="waveform-tooltip__value">(x:{{
120
+ formatTooltipTime(seriesPoint.point.x, timeUnit)
121
+ }} y:{{ formatTooltipNumber(seriesPoint.point.y) }})</span>
122
+ <span v-else class="waveform-tooltip__value waveform-tooltip__value--missing">无数据</span>
120
123
  </span>
121
124
  </span>
122
125
  </div>