|
| 1 | +<script setup lang="ts"> |
| 2 | +import { computed } from "vue"; |
| 3 | +import { useTimelineDiagramStore } from "@/stores/TimelineDiagramStore"; |
| 4 | +import { storeToRefs } from "pinia"; |
| 5 | +import { timeToX, rowToY, BAR_HEIGHT, MIN_BAR_WIDTH, DELIVERY_LINE_HEIGHT, type TimelineBar } from "@/resources/TimelineDiagram/TimelineModel"; |
| 6 | +
|
| 7 | +const props = defineProps<{ |
| 8 | + chartWidth: number; |
| 9 | +}>(); |
| 10 | +
|
| 11 | +const store = useTimelineDiagramStore(); |
| 12 | +const { bars, rowIndexByBarId, visibleMinTime, visibleMaxTime, highlightId, showDeliveryTime, labelWidth } = storeToRefs(store); |
| 13 | +
|
| 14 | +const barPositions = computed(() => |
| 15 | + bars.value.map((bar) => { |
| 16 | + const rowIndex = rowIndexByBarId.value.get(bar.id) ?? 0; |
| 17 | + const y = rowToY(rowIndex); |
| 18 | +
|
| 19 | + // Delivery line: sentMs → processingStartMs |
| 20 | + const deliveryX = labelWidth.value + timeToX(bar.sentMs, visibleMinTime.value, visibleMaxTime.value, props.chartWidth); |
| 21 | + const procStartX = labelWidth.value + timeToX(bar.processingStartMs, visibleMinTime.value, visibleMaxTime.value, props.chartWidth); |
| 22 | + const deliveryWidth = Math.max(procStartX - deliveryX, 0); |
| 23 | +
|
| 24 | + // Processing bar: processingStartMs → processedAtMs |
| 25 | + const procEndX = labelWidth.value + timeToX(bar.processedAtMs, visibleMinTime.value, visibleMaxTime.value, props.chartWidth); |
| 26 | + const procWidth = Math.max(procEndX - procStartX, MIN_BAR_WIDTH); |
| 27 | +
|
| 28 | + return { bar, y, deliveryX, deliveryWidth, procX: procStartX, procWidth }; |
| 29 | + }) |
| 30 | +); |
| 31 | +
|
| 32 | +function barFill(bar: TimelineBar) { |
| 33 | + if (bar.isFailed) return "var(--error)"; |
| 34 | + if (bar.isSelected) return "var(--highlight)"; |
| 35 | + if (highlightId.value === bar.id) return "var(--highlight-background)"; |
| 36 | + return "var(--gray80)"; |
| 37 | +} |
| 38 | +
|
| 39 | +function barTextFill(bar: TimelineBar) { |
| 40 | + if (bar.isFailed || bar.isSelected) return "white"; |
| 41 | + return "var(--gray20)"; |
| 42 | +} |
| 43 | +
|
| 44 | +function deliveryLineFill(bar: TimelineBar) { |
| 45 | + if (bar.isFailed) return "var(--error)"; |
| 46 | + if (bar.isSelected) return "var(--highlight)"; |
| 47 | + if (highlightId.value === bar.id) return "var(--highlight-background)"; |
| 48 | + return "var(--gray95)"; |
| 49 | +} |
| 50 | +
|
| 51 | +function onBarClick(bar: TimelineBar, event: MouseEvent) { |
| 52 | + store.navigateTo(bar, event.shiftKey); |
| 53 | +} |
| 54 | +
|
| 55 | +function onBarEnter(bar: TimelineBar, event: MouseEvent) { |
| 56 | + store.setHighlightId(bar.id); |
| 57 | + updateTooltipPos(bar, event); |
| 58 | +} |
| 59 | +
|
| 60 | +function onBarMove(bar: TimelineBar, event: MouseEvent) { |
| 61 | + updateTooltipPos(bar, event); |
| 62 | +} |
| 63 | +
|
| 64 | +function onBarLeave() { |
| 65 | + store.setHighlightId(undefined); |
| 66 | + store.hideTooltip(); |
| 67 | +} |
| 68 | +
|
| 69 | +function updateTooltipPos(bar: TimelineBar, event: MouseEvent) { |
| 70 | + const wrapper = (event.currentTarget as SVGElement).closest(".wrapper"); |
| 71 | + if (!wrapper) return; |
| 72 | + const rect = wrapper.getBoundingClientRect(); |
| 73 | + store.showTooltip(bar, event.clientX - rect.left + 12, event.clientY - rect.top + 12); |
| 74 | +} |
| 75 | +</script> |
| 76 | + |
| 77 | +<template> |
| 78 | + <g class="timeline-bars"> |
| 79 | + <g v-for="pos in barPositions" :key="pos.bar.id" class="bar-group" @click="onBarClick(pos.bar, $event)" @mouseenter="onBarEnter(pos.bar, $event)" @mousemove="onBarMove(pos.bar, $event)" @mouseleave="onBarLeave"> |
| 80 | + <!-- Delivery time line (thin) --> |
| 81 | + <rect |
| 82 | + v-if="showDeliveryTime && pos.deliveryWidth > 0" |
| 83 | + :x="pos.deliveryX" |
| 84 | + :y="pos.y + BAR_HEIGHT / 2 - DELIVERY_LINE_HEIGHT / 2" |
| 85 | + :width="pos.deliveryWidth" |
| 86 | + :height="DELIVERY_LINE_HEIGHT" |
| 87 | + :fill="deliveryLineFill(pos.bar)" |
| 88 | + class="delivery-line" |
| 89 | + /> |
| 90 | + <!-- Processing time bar --> |
| 91 | + <rect :x="pos.procX" :y="pos.y" :width="pos.procWidth" :height="BAR_HEIGHT" :fill="barFill(pos.bar)" rx="3" ry="3" class="bar-rect" /> |
| 92 | + <!-- Clipped text label inside the processing bar --> |
| 93 | + <clipPath :id="`clip-${pos.bar.id}`"> |
| 94 | + <rect :x="pos.procX" :y="pos.y" :width="pos.procWidth - 4" :height="BAR_HEIGHT" /> |
| 95 | + </clipPath> |
| 96 | + <text v-if="pos.procWidth > 30" :x="pos.procX + 4" :y="pos.y + BAR_HEIGHT / 2 + 4" :fill="barTextFill(pos.bar)" :clip-path="`url(#clip-${pos.bar.id})`" class="bar-label"> |
| 97 | + {{ pos.bar.typeName }} |
| 98 | + </text> |
| 99 | + </g> |
| 100 | + </g> |
| 101 | +</template> |
| 102 | + |
| 103 | +<style scoped> |
| 104 | +.bar-group { |
| 105 | + cursor: pointer; |
| 106 | +} |
| 107 | +.bar-rect { |
| 108 | + transition: opacity 0.15s; |
| 109 | +} |
| 110 | +.delivery-line { |
| 111 | + transition: opacity 0.15s; |
| 112 | +} |
| 113 | +.bar-group:hover .bar-rect, |
| 114 | +.bar-group:hover .delivery-line { |
| 115 | + opacity: 0.85; |
| 116 | +} |
| 117 | +.bar-label { |
| 118 | + font-size: 10px; |
| 119 | + pointer-events: none; |
| 120 | +} |
| 121 | +</style> |
0 commit comments