Fixed up sensor cards
This commit is contained in:
@@ -11,12 +11,16 @@ const W = 600, H = 200, PAD = 30;
|
||||
|
||||
watch([() => props.metricKey, mode], async ([key]) => {
|
||||
if(!key) return;
|
||||
const now = new Date(), past = new Date(), future = new Date();
|
||||
past.setDate(past.getDate() - 3);
|
||||
future.setDate(future.getDate() + 3);
|
||||
rows.value = mode.value === 'hourly'
|
||||
? await api.hourly(past, future)
|
||||
: await api.daily(past, future);
|
||||
const past = new Date(), future = new Date();
|
||||
if(mode.value === 'hourly') {
|
||||
past.setHours(past.getHours() - 24);
|
||||
future.setHours(future.getHours() + 24);
|
||||
rows.value = await api.hourly(past, future);
|
||||
} else {
|
||||
past.setDate(past.getDate() - 3);
|
||||
future.setDate(future.getDate() + 3);
|
||||
rows.value = await api.daily(past, future);
|
||||
}
|
||||
}, {immediate: true});
|
||||
|
||||
const now = new Date();
|
||||
@@ -29,34 +33,52 @@ const points = computed(() => {
|
||||
})).filter(p => p.v != null);
|
||||
});
|
||||
|
||||
const {minT, maxT, minV, maxV, path, currentX} = computed(() => {
|
||||
const chart = computed(() => {
|
||||
const pts = points.value;
|
||||
if(!pts.length) return {minT:0, maxT:1, minV:0, maxV:1, path:'', currentX: 0};
|
||||
if(!pts.length) return {minT:0, maxT:1, minV:0, maxV:1, path:{hist:'',fut:''}, currentX:0, dot:null as any, yLabels:[] as any[]};
|
||||
|
||||
const ts = pts.map(p => p.t), vs = pts.map(p => p.v as number);
|
||||
const minT = Math.min(...ts), maxT = Math.max(...ts);
|
||||
const minV = Math.min(...vs), maxV = Math.max(...vs);
|
||||
const rawMin = Math.min(...vs), rawMax = Math.max(...vs);
|
||||
const pad = (rawMax - rawMin) * 0.1 || 1;
|
||||
const minV = rawMin - pad, maxV = rawMax + pad;
|
||||
|
||||
const xOf = (t: number) => PAD + ((t - minT) / (maxT - minT || 1)) * (W - PAD * 2);
|
||||
const yOf = (v: number) => H - PAD - ((v - minV) / (maxV - minV || 1)) * (H - PAD * 2);
|
||||
const nowT = now.getTime();
|
||||
const currentX = xOf(nowT);
|
||||
const currentX = Math.max(PAD, Math.min(W - PAD, xOf(nowT)));
|
||||
|
||||
// split historic vs future
|
||||
const hist = pts.filter(p => p.t <= nowT);
|
||||
const fut = pts.filter(p => p.t >= nowT);
|
||||
const histPath = hist.map((p, i) => `${i===0?'M':'L'} ${xOf(p.t).toFixed(1)} ${yOf(p.v as number).toFixed(1)}`).join(' ');
|
||||
const futPath = fut.map((p, i) => `${i===0?'M':'L'} ${xOf(p.t).toFixed(1)} ${yOf(p.v as number).toFixed(1)}`).join(' ');
|
||||
const path = {hist: histPath, fut: futPath};
|
||||
return {minT, maxT, minV, maxV, path, currentX};
|
||||
}).value;
|
||||
|
||||
const currentDot = computed(() => {
|
||||
const pts = points.value;
|
||||
if(!pts.length || !props.metricKey) return null;
|
||||
const nowT = now.getTime();
|
||||
const closest = pts.reduce((a, b) => Math.abs(a.t - nowT) < Math.abs(b.t - nowT) ? a : b);
|
||||
const xOf = (t: number) => PAD + ((t - minT) / (maxT - minT || 1)) * (W - PAD * 2);
|
||||
const yOf = (v: number) => H - PAD - ((v - minV) / (maxV - minV || 1)) * (H - PAD * 2);
|
||||
return {x: xOf(closest.t), y: yOf(closest.v as number), v: closest.v};
|
||||
// stitch current point into both paths so lines meet
|
||||
const nearest = pts.reduce((a, b) => Math.abs(a.t - nowT) < Math.abs(b.t - nowT) ? a : b);
|
||||
const dot = {x: xOf(nearest.t), y: yOf(nearest.v as number), v: nearest.v};
|
||||
|
||||
const toPath = (arr: typeof pts) =>
|
||||
arr.map((p, i) => `${i===0?'M':'L'} ${xOf(p.t).toFixed(1)} ${yOf(p.v as number).toFixed(1)}`).join(' ');
|
||||
|
||||
// y-axis labels (5 ticks)
|
||||
const yLabels = Array.from({length: 5}, (_, i) => {
|
||||
const frac = i / 4;
|
||||
const v = minV + frac * (maxV - minV);
|
||||
return {y: yOf(v), label: v.toFixed(1)};
|
||||
});
|
||||
|
||||
// x-axis labels
|
||||
const xLabels: {x: number, label: string}[] = [];
|
||||
const span = maxT - minT;
|
||||
const tickCount = mode.value === 'hourly' ? 9 : 7;
|
||||
for(let i = 0; i <= tickCount; i++) {
|
||||
const t = minT + (i / tickCount) * span;
|
||||
const d = new Date(t);
|
||||
const label = mode.value === 'hourly'
|
||||
? d.toLocaleTimeString([], {hour: '2-digit', minute: '2-digit'})
|
||||
: d.toLocaleDateString([], {month: 'short', day: 'numeric'});
|
||||
xLabels.push({x: xOf(t), label});
|
||||
}
|
||||
|
||||
return {minV, maxV, path: {hist: toPath(hist), fut: toPath(fut)}, currentX, dot, yLabels, xLabels};
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -85,8 +107,7 @@ const currentDot = computed(() => {
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 16px;
|
||||
|
||||
.title { font-size: 16px; font-weight: 700; color: var(--text); }
|
||||
.title { font-size: 16px; font-weight: 700; color: var(--text); text-transform: capitalize; }
|
||||
.close-btn {
|
||||
background: none;
|
||||
border: 1px solid var(--border);
|
||||
@@ -103,7 +124,6 @@ const currentDot = computed(() => {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
margin-bottom: 16px;
|
||||
|
||||
button {
|
||||
font-size: 11px;
|
||||
padding: 4px 12px;
|
||||
@@ -115,7 +135,17 @@ const currentDot = computed(() => {
|
||||
&.active { background: var(--accent); color: #fff; border-color: var(--accent); }
|
||||
}
|
||||
}
|
||||
.current-val {
|
||||
font-size: 13px;
|
||||
color: var(--text-muted);
|
||||
margin-bottom: 12px;
|
||||
span { color: var(--accent); font-weight: 700; }
|
||||
}
|
||||
svg { width: 100%; overflow: visible; }
|
||||
.tick-label {
|
||||
font-size: 9px;
|
||||
fill: var(--text-muted);
|
||||
}
|
||||
</style>
|
||||
|
||||
<template>
|
||||
@@ -124,27 +154,44 @@ svg { width: 100%; overflow: visible; }
|
||||
<div v-if="metricKey" class="backdrop" @click.self="emit('close')">
|
||||
<div class="modal">
|
||||
<div class="modal-header">
|
||||
<span class="title">{{ metricKey }}</span>
|
||||
<span class="title">{{ metricKey?.replace(/_/g, ' ') }}</span>
|
||||
<button class="close-btn" @click="emit('close')">✕</button>
|
||||
</div>
|
||||
|
||||
<div class="mode-toggle">
|
||||
<button :class="{active: mode === 'hourly'}" @click="mode = 'hourly'">Hourly</button>
|
||||
<button :class="{active: mode === 'daily'}" @click="mode = 'daily'">Daily</button>
|
||||
<button :class="{active: mode === 'hourly'}" @click="mode = 'hourly'">Hourly ±24h</button>
|
||||
<button :class="{active: mode === 'daily'}" @click="mode = 'daily'">Daily ±3d</button>
|
||||
</div>
|
||||
|
||||
<div v-if="chart.dot" class="current-val">Now: <span>{{ (chart.dot.v as number).toFixed(2) }}</span></div>
|
||||
|
||||
<svg :viewBox="`0 0 ${W} ${H}`" preserveAspectRatio="none">
|
||||
<!-- grid -->
|
||||
<!-- y-axis grid lines & labels -->
|
||||
<template v-for="tick in chart.yLabels" :key="tick.y">
|
||||
<line :x1="PAD" :y1="tick.y" :x2="W-PAD" :y2="tick.y" stroke="var(--border)" stroke-width="0.5" opacity="0.5"/>
|
||||
<text :x="PAD - 4" :y="tick.y + 3" text-anchor="end" class="tick-label">{{ tick.label }}</text>
|
||||
</template>
|
||||
|
||||
<!-- x-axis labels -->
|
||||
<template v-for="tick in chart.xLabels" :key="tick.x">
|
||||
<text :x="tick.x" :y="H - 4" text-anchor="middle" class="tick-label">{{ tick.label }}</text>
|
||||
</template>
|
||||
|
||||
<!-- axes -->
|
||||
<line :x1="PAD" :y1="PAD" :x2="PAD" :y2="H-PAD" stroke="var(--border)" stroke-width="1"/>
|
||||
<line :x1="PAD" :y1="H-PAD" :x2="W-PAD" :y2="H-PAD" stroke="var(--border)" stroke-width="1"/>
|
||||
|
||||
<!-- now line -->
|
||||
<line :x1="currentX" :y1="PAD" :x2="currentX" :y2="H-PAD" stroke="var(--accent)" stroke-width="1" stroke-dasharray="3 2" opacity="0.5"/>
|
||||
<line :x1="chart.currentX" :y1="PAD" :x2="chart.currentX" :y2="H-PAD"
|
||||
stroke="var(--accent)" stroke-width="1" stroke-dasharray="3 2" opacity="0.6"/>
|
||||
|
||||
<!-- historic solid -->
|
||||
<path :d="path.hist" fill="none" stroke="var(--accent)" stroke-width="2"/>
|
||||
<path :d="chart.path.hist" fill="none" stroke="var(--accent)" stroke-width="2"/>
|
||||
<!-- future dashed -->
|
||||
<path :d="path.fut" fill="none" stroke="var(--accent)" stroke-width="2" stroke-dasharray="5 3" opacity="0.6"/>
|
||||
<path :d="chart.path.fut" fill="none" stroke="var(--accent)" stroke-width="2" stroke-dasharray="5 3" opacity="0.5"/>
|
||||
|
||||
<!-- current dot -->
|
||||
<circle v-if="currentDot" :cx="currentDot.x" :cy="currentDot.y" r="4" fill="var(--accent)"/>
|
||||
<circle v-if="chart.dot" :cx="chart.dot.x" :cy="chart.dot.y" r="4" fill="var(--accent)"/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user