internal/dashboard/frontend/src/components/SparkChart.vue

<script setup lang="ts">
import { computed } from "vue";

const props = defineProps<{
  data: { x: number[]; y: (number | undefined)[] };
}>();

const points = computed(() => {
  const { x, y } = props.data;
  if (!x?.length || !y?.length || x.length !== y.length) return "";

  const minX = Math.min(...x);
  const maxX = Math.max(...x);
  const minY = Math.min(...y.filter((y) => y !== undefined));
  const maxY = Math.max(...y.filter((y) => y !== undefined));

  const dx = maxX - minX || 1;
  const dy = maxY - minY;

  // Add 10% padding so the line isn't cut off
  const paddingY = 10;
  const paddingX = 2;
  const height = 100 - paddingY * 2;
  const width = 100 - paddingX * 2;

  return x
    .map((val, i) => {
      const yVal = y[i];
      if (yVal === undefined) return null;
      const px = 100 - paddingX - ((val - minX) / dx) * width;
      const py = dy === 0 ? 50 : 100 - paddingY - ((yVal - minY) / dy) * height;
      return `${px.toFixed(2)},${py.toFixed(2)}`;
    })
    .filter((p): p is string => p !== null)
    .join(" ");
});
</script>

<template>
  <div class="text-secondary-600 w-full overflow-hidden">
    <svg viewBox="0 0 100 100" preserveAspectRatio="none" class="h-full w-full">
      <defs>
        <linearGradient id="grad" x1="0%" y1="100%" x2="0%" y2="0%">
          <stop
            offset="0%"
            style="stop-color: var(--color-secondary-600); stop-opacity: 1"
          />
          <stop
            offset="100%"
            style="stop-color: var(--color-secondary-300); stop-opacity: 1"
          />
        </linearGradient>
      </defs>
      <polyline
        fill="none"
        stroke="url(#grad)"
        stroke-width="3"
        stroke-linecap="round"
        stroke-linejoin="round"
        vector-effect="non-scaling-stroke"
        :points="points"
      />
    </svg>
  </div>
</template>