Files
scrap/webui/src/components/MiniSparkline.vue
Gilles Soulier cf7c415e22 before claude
2026-01-17 13:40:26 +01:00

88 lines
2.0 KiB
Vue

<script setup lang="ts">
import { computed, PropType } from "vue";
const props = defineProps({
points: {
type: Array as PropType<number[]>,
default: () => [],
},
width: {
type: Number,
default: 280,
},
height: {
type: Number,
default: 14,
},
padding: {
type: Number,
default: 4,
},
});
const validPoints = computed(() =>
(props.points || [])
.map((value) => (Number.isFinite(value) ? Number(value) : null))
.filter((value): value is number => value !== null)
);
const pointRange = computed(() => {
const points = validPoints.value;
if (points.length === 0) {
return { min: 0, max: 1 };
}
const min = Math.min(...points);
const max = Math.max(...points);
return { min, max };
});
const svgPoints = computed(() => {
const points = validPoints.value;
const { min, max } = pointRange.value;
if (points.length === 0) {
return "";
}
const delta = max - min || 1;
const availableWidth = props.width - props.padding * 2;
const availableHeight = props.height - props.padding * 2;
const step = points.length > 1 ? availableWidth / (points.length - 1) : 0;
return points
.map((value, index) => {
const x = props.padding + step * index;
const normalized = (value - min) / delta;
const y = props.padding + availableHeight * (1 - normalized);
return `${x},${y}`;
})
.join(" ");
});
const hasPoints = computed(() => validPoints.value.length > 1);
</script>
<template>
<div class="mini-sparkline">
<svg
:width="width"
:height="height"
:viewBox="`0 0 ${width} ${height}`"
role="presentation"
aria-hidden="true"
>
<polyline
v-if="hasPoints"
:points="svgPoints"
class="sparkline-polyline"
fill="none"
/>
<line
v-else
:x1="padding"
:y1="height / 2"
:x2="width - padding"
:y2="height / 2"
class="sparkline-polyline"
/>
</svg>
</div>
</template>