waveform-analysis 0.1.43 → 0.1.46

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 (31) hide show
  1. package/dist/index.cjs +1 -1
  2. package/dist/index.js +163 -161
  3. package/dist/style.css +1 -1
  4. package/dist/types/components/core/constants.d.ts +2 -2
  5. package/dist/types/components/core/types.d.ts +2 -1
  6. package/dist/types/components/core/yAxisConstants.d.ts +2 -2
  7. package/dist/types/components/interaction/WaveformTooltip.vue.d.ts +1 -1
  8. package/package.json +1 -1
  9. package/src/App.test.ts +2 -7
  10. package/src/components/WaveformChart.css +1 -1
  11. package/src/components/core/constants.ts +3 -3
  12. package/src/components/core/layout.test.ts +3 -3
  13. package/src/components/core/layout.ts +2 -2
  14. package/src/components/core/trackLayoutBuilder.ts +2 -2
  15. package/src/components/core/types.ts +2 -1
  16. package/src/components/core/useWaveformChartController.ts +1 -0
  17. package/src/components/core/yAxisConstants.ts +2 -2
  18. package/src/components/interaction/WaveformHoverHost.vue +3 -1
  19. package/src/components/interaction/WaveformTooltip.test.ts +23 -0
  20. package/src/components/interaction/WaveformTooltip.vue +9 -6
  21. package/src/components/interaction/useWaveformHover.ts +19 -14
  22. package/src/components/rendering/WaveformHoverLayer.vue +5 -11
  23. package/src/components/rendering/WaveformTrackAxes.vue +5 -4
  24. package/src/components/waveformChartCases/dataLifecycle.test.ts +1 -1
  25. package/src/components/waveformChartCases/frameGridAndFormatting.test.ts +10 -0
  26. package/src/components/waveformChartCases/incompleteDataHover.test.ts +67 -0
  27. package/src/components/waveformChartCases/layoutAndDomains.test.ts +5 -5
  28. package/src/components/waveformChartCases/legendVisibility.test.ts +1 -1
  29. package/src/components/waveformChartCases/multiAxisAndPagination.test.ts +1 -1
  30. package/src/data/simulatedWaveforms.test.ts +7 -2
  31. 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
  */
@@ -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;
@@ -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
  /** 是否显示 */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "waveform-analysis",
3
- "version": "0.1.43",
3
+ "version": "0.1.46",
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
 
@@ -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轴指数标签间距(像素)
@@ -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)) {
@@ -162,7 +162,7 @@ export function buildTrackLayouts(options: BuildTrackLayoutsOptions): TrackLayou
162
162
  labelX,
163
163
  scale,
164
164
  majorTicks,
165
- minorTicks: buildMinorTicks(majorTicks),
165
+ minorTicks: buildMinorTicks(majorTicks, 2),
166
166
  tickValues,
167
167
  seriesList: group.seriesList,
168
168
  }
@@ -248,7 +248,7 @@ export function buildTrackLayouts(options: BuildTrackLayoutsOptions): TrackLayou
248
248
  yScale,
249
249
  yAxes,
250
250
  xMajorTicks,
251
- xMinorTicks: buildMinorTicks(xMajorTicks, 5, domain),
251
+ xMinorTicks: buildMinorTicks(xMajorTicks, 3, domain),
252
252
  yMajorTicks,
253
253
  yMinorTicks: yAxes[0]?.minorTicks ?? [],
254
254
  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
 
@@ -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)
@@ -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>
@@ -62,18 +62,21 @@ export function useWaveformHover(context: HoverContext) {
62
62
  const commitHover = (
63
63
  nextPoints: HoveredSeriesPoint[],
64
64
  trackIndex: number | null,
65
+ queryX: number,
65
66
  position: { x: number; y: number },
66
67
  ) => {
67
68
  if (isPresentationMode.value) return
68
69
  if (!hoveredPointsMatch(nextPoints)) hoverState.points = nextPoints
69
70
  hoverState.trackIndex = trackIndex
71
+ hoverState.queryX = queryX
70
72
  hoverState.position = position
71
- emit('point-hover', hoverState.points[0]?.point ?? null)
73
+ emit('point-hover', hoverState.points.find((point) => point.point)?.point ?? null)
72
74
  }
73
75
  const clearHover = () => {
74
76
  cancelPendingHover()
75
77
  hoverState.points = []
76
78
  hoverState.trackIndex = null
79
+ hoverState.queryX = null
77
80
  emit('point-hover', null)
78
81
  }
79
82
  const handlePointerLeave = () => {
@@ -82,7 +85,7 @@ export function useWaveformHover(context: HoverContext) {
82
85
  const createHoveredSeriesPoint = (
83
86
  series: DisplaySeries,
84
87
  trackIndex: number,
85
- point: WaveformPoint,
88
+ point: WaveformPoint | null,
86
89
  ): HoveredSeriesPoint => ({
87
90
  id: series.id,
88
91
  shotNo: series.shotNo,
@@ -106,8 +109,12 @@ export function useWaveformHover(context: HoverContext) {
106
109
  clearHover()
107
110
  return true
108
111
  }
109
- const nearestPoint = (series: DisplaySeries, xValue: number): WaveformPoint | undefined =>
110
- series.points[pointBisector.center(series.points, xValue)]
112
+ const nearestPoint = (series: DisplaySeries, xValue: number): WaveformPoint | undefined => {
113
+ const firstPoint = series.points[0]
114
+ const lastPoint = series.points.at(-1)
115
+ if (!firstPoint || !lastPoint || xValue < firstPoint.x || xValue > lastPoint.x) return undefined
116
+ return series.points[pointBisector.center(series.points, xValue)]
117
+ }
111
118
 
112
119
  const handleIndependentPointerMove = (event: PointerEvent, trackIndex: number) => {
113
120
  if (isPresentationMode.value) return
@@ -125,11 +132,10 @@ export function useWaveformHover(context: HoverContext) {
125
132
  const currentTrack = trackLayouts.value[trackIndex]
126
133
  if (!currentTrack || !currentTrack.hasVisibleSeries || currentTrack !== track) return
127
134
  const xValue = track.xScale.invert(Math.max(0, Math.min(innerWidth.value, pointerX)))
128
- const nextPoints = track.seriesList.flatMap((series) => {
129
- const point = nearestPoint(series, xValue)
130
- return point ? [createHoveredSeriesPoint(series, trackIndex, point)] : []
131
- })
132
- commitHover(nextPoints, trackIndex, {
135
+ const nextPoints = track.seriesList.map((series) =>
136
+ createHoveredSeriesPoint(series, trackIndex, nearestPoint(series, xValue) ?? null),
137
+ )
138
+ commitHover(nextPoints, trackIndex, xValue, {
133
139
  x: resolvedChartLeftMargin.value + track.left + pointerX,
134
140
  y: titleAreaHeight.value + chartTopMargin.value + track.top + pointerY,
135
141
  })
@@ -155,12 +161,11 @@ export function useWaveformHover(context: HoverContext) {
155
161
  )
156
162
  const xValue = referenceTrack.xScale.invert(localPointerX)
157
163
  const nextPoints = trackLayouts.value.flatMap((track) =>
158
- track.seriesList.flatMap((series) => {
159
- const point = nearestPoint(series, xValue)
160
- return point ? [createHoveredSeriesPoint(series, track.index, point)] : []
161
- }),
164
+ track.seriesList.map((series) =>
165
+ createHoveredSeriesPoint(series, track.index, nearestPoint(series, xValue) ?? null),
166
+ ),
162
167
  )
163
- commitHover(nextPoints, null, {
168
+ commitHover(nextPoints, null, xValue, {
164
169
  x: resolvedChartLeftMargin.value + pointerX,
165
170
  y: titleAreaHeight.value + chartTopMargin.value + pointerY,
166
171
  })
@@ -1,7 +1,7 @@
1
1
  <script setup lang="ts">
2
2
  import { computed } from 'vue'
3
3
 
4
- import type { HoveredSeriesPoint, TrackLayout, WaveformHoverState } from '../core/types'
4
+ import type { TrackLayout, WaveformHoverState } from '../core/types'
5
5
 
6
6
  const props = defineProps<{
7
7
  state: WaveformHoverState
@@ -11,23 +11,17 @@ const props = defineProps<{
11
11
  }>()
12
12
 
13
13
  interface Crosshair {
14
- point: HoveredSeriesPoint
15
14
  track: TrackLayout
16
15
  x: number
17
16
  }
18
17
 
19
18
  const crosshairs = computed<Crosshair[]>(() => {
20
- if (!props.visible) return []
21
-
22
- const pointByTrack = new Map<number, HoveredSeriesPoint>()
23
- props.state.points.forEach((point) => {
24
- if (!pointByTrack.has(point.trackIndex)) pointByTrack.set(point.trackIndex, point)
25
- })
19
+ if (!props.visible || props.state.queryX === null) return []
26
20
 
27
21
  return props.tracks.flatMap((track) => {
28
- const point = pointByTrack.get(track.index)
29
- return point && !track.isEmpty && track.hasVisibleSeries
30
- ? [{ point, track, x: track.xScale(point.point.x) }]
22
+ const trackIsActive = props.state.trackIndex === null || props.state.trackIndex === track.index
23
+ return trackIsActive && !track.isEmpty && track.hasVisibleSeries
24
+ ? [{ track, x: track.xScale(props.state.queryX!) }]
31
25
  : []
32
26
  })
33
27
  })
@@ -5,6 +5,7 @@ import { nextTick, onMounted, ref, watch } from 'vue'
5
5
  import type { WaveformAxesOptions } from '../../types'
6
6
  import { formatScientificAxisLabel, formatXAxisLabel } from '../../utils'
7
7
  import type { DisplaySeries, TrackLayout, WaveformYAxisLayout } from '../core/types'
8
+ import { Y_AXIS_LABEL_BAND_WIDTH } from '../core/yAxisConstants'
8
9
 
9
10
  interface Props {
10
11
  track: TrackLayout
@@ -169,9 +170,9 @@ watch(
169
170
  >
170
171
  <rect
171
172
  class="waveform-track__y-axis-label-bg waveform-chart__y-axis-label-bg"
172
- :x="track.yAxisLabelX - 12"
173
+ :x="track.yAxisLabelX - Y_AXIS_LABEL_BAND_WIDTH / 2"
173
174
  :y="track.height / 2 - 40"
174
- width="24"
175
+ :width="Y_AXIS_LABEL_BAND_WIDTH"
175
176
  height="80"
176
177
  rx="2"
177
178
  />
@@ -194,9 +195,9 @@ watch(
194
195
  >
195
196
  <rect
196
197
  class="waveform-track__y-axis-label-bg waveform-chart__y-axis-label-bg"
197
- :x="axis.labelX - 12"
198
+ :x="axis.labelX - Y_AXIS_LABEL_BAND_WIDTH / 2"
198
199
  :y="track.height / 2 - 40"
199
- width="24"
200
+ :width="Y_AXIS_LABEL_BAND_WIDTH"
200
201
  height="80"
201
202
  rx="2"
202
203
  />
@@ -170,7 +170,7 @@ describe('WaveformChart', () => {
170
170
  )
171
171
  expect(labelX).toBeLessThan(-46)
172
172
  expect(Number(wrapper.get('.waveform-chart__y-axis-label-bg').attributes('x'))).toBe(
173
- labelX - 12,
173
+ labelX - 6,
174
174
  )
175
175
  expect(yAxisLabels.every((label) => !label.text().includes('(T)'))).toBe(true)
176
176
  expect(wrapper.findAll('.waveform-chart__track-label')).toHaveLength(0)
@@ -22,6 +22,16 @@ describe('WaveformChart', () => {
22
22
 
23
23
  expect(wrapper.findAll('.waveform-chart__grid--major line').length).toBeGreaterThan(0)
24
24
  expect(wrapper.findAll('.waveform-chart__grid--minor line').length).toBeGreaterThan(0)
25
+ expect(
26
+ wrapper.findAll('.waveform-chart__grid--major [data-grid-direction="horizontal"]'),
27
+ ).toHaveLength(5)
28
+ expect(
29
+ wrapper.findAll('.waveform-chart__grid--minor [data-grid-direction="horizontal"]'),
30
+ ).toHaveLength(4)
31
+ expect(
32
+ wrapper.findAll('.waveform-chart__grid--minor [data-grid-direction="vertical"]'),
33
+ ).not.toHaveLength(0)
34
+ expect(getComputedStyle(wrapper.element).borderTopWidth).toBe('1.2px')
25
35
  expect(wrapper.find('.waveform-chart__plot-frame').exists()).toBe(true)
26
36
  expect(wrapper.get('.waveform-chart__plot-frame').attributes()).toMatchObject({
27
37
  fill: 'none',
@@ -0,0 +1,67 @@
1
+ import { flushPromises } from '@vue/test-utils'
2
+ import { describe, expect, it } from 'vitest'
3
+
4
+ import { flushAnimationFrames } from '../../test/setup'
5
+ import { mountSizedChart } from '../../test/waveformChart'
6
+
7
+ describe('WaveformChart incomplete data hover', () => {
8
+ it('keeps the shared crosshair at the query time and marks out-of-range series unavailable', async () => {
9
+ const wrapper = await mountSizedChart(
10
+ {
11
+ kind: 'series',
12
+ series: [
13
+ {
14
+ name: '完整数据',
15
+ data: {
16
+ kind: 'points',
17
+ points: [
18
+ { x: -1, y: 1 },
19
+ { x: -0.5, y: 2 },
20
+ { x: 1, y: 3 },
21
+ ],
22
+ },
23
+ },
24
+ {
25
+ name: '从零开始',
26
+ data: {
27
+ kind: 'points',
28
+ points: [
29
+ { x: 0, y: 10 },
30
+ { x: 1, y: 20 },
31
+ ],
32
+ },
33
+ },
34
+ ],
35
+ },
36
+ { displayMode: 'separated' },
37
+ )
38
+ const overlay = wrapper.get('.waveform-chart__overlay--shared')
39
+ const overlayWidth = Number(overlay.attributes('width'))
40
+ Object.defineProperty(overlay.element, 'getBoundingClientRect', {
41
+ value: () => ({ left: 0, top: 0, width: overlayWidth, height: 290 }),
42
+ })
43
+
44
+ overlay.element.dispatchEvent(
45
+ new MouseEvent('pointermove', {
46
+ clientX: overlayWidth / 4,
47
+ clientY: 120,
48
+ bubbles: true,
49
+ }),
50
+ )
51
+ flushAnimationFrames()
52
+ await flushPromises()
53
+
54
+ const crosshairPositions = wrapper
55
+ .findAll('.waveform-chart__crosshair line')
56
+ .map((line) => Number(line.attributes('x1')))
57
+ expect(crosshairPositions).toHaveLength(2)
58
+ expect(crosshairPositions[0]).toBeCloseTo(overlayWidth / 4)
59
+ expect(crosshairPositions[1]).toBeCloseTo(overlayWidth / 4)
60
+ const tooltipRows = wrapper.findAll('.waveform-chart__tooltip-series')
61
+ expect(tooltipRows).toHaveLength(2)
62
+ expect(tooltipRows[0].text()).toContain('(x:-500 y:2)')
63
+ expect(tooltipRows[1].text()).toContain('无数据')
64
+ expect(tooltipRows[1].text()).not.toContain('x:')
65
+ expect(wrapper.emitted('point-hover')?.at(-1)).toEqual([{ x: -0.5, y: 2 }])
66
+ })
67
+ })
@@ -53,10 +53,10 @@ describe('WaveformChart', () => {
53
53
  tracks[0].get('.waveform-chart__y-axis-label-bg').attributes('x'),
54
54
  )
55
55
 
56
- expect(labelX).toBe(-74)
57
- expect(labelBackgroundX).toBe(labelX - 12)
58
- expect(Number(wrapper.attributes('data-chart-left-margin'))).toBe(90)
59
- expect(secondLeft - firstWidth).toBeGreaterThanOrEqual(90)
56
+ expect(labelX).toBe(-62)
57
+ expect(labelBackgroundX).toBe(labelX - 6)
58
+ expect(Number(wrapper.attributes('data-chart-left-margin'))).toBe(80)
59
+ expect(secondLeft - firstWidth).toBeGreaterThanOrEqual(72)
60
60
  })
61
61
 
62
62
  it('keeps a tick-only gutter when channel labels are empty', async () => {
@@ -82,7 +82,7 @@ describe('WaveformChart', () => {
82
82
  const secondLeft = Number(tracks[1].attributes('data-track-left'))
83
83
 
84
84
  expect(wrapper.findAll('.waveform-chart__y-axis-label')).toHaveLength(0)
85
- expect(Number(wrapper.attributes('data-chart-left-margin'))).toBe(64)
85
+ expect(Number(wrapper.attributes('data-chart-left-margin'))).toBe(80)
86
86
  expect(secondLeft - firstWidth).toBeGreaterThanOrEqual(60)
87
87
  })
88
88
 
@@ -343,7 +343,7 @@ describe('WaveformChart', () => {
343
343
  const secondLeft = Number(tracks[1].attributes('data-track-left'))
344
344
  const labelX = Number(tracks[1].attributes('data-y-axis-label-x'))
345
345
 
346
- expect(secondLeft - (firstLeft + firstWidth)).toBeGreaterThanOrEqual(Math.abs(labelX) + 16)
346
+ expect(secondLeft - (firstLeft + firstWidth)).toBeGreaterThanOrEqual(Math.abs(labelX) + 6)
347
347
  expect(tracks[1].find('.waveform-chart__y-axis-label-bg').exists()).toBe(true)
348
348
  }
349
349
  })