heartwood every commit a ring
6.4 KB raw
import Chart from "chart.js/auto";

const accent = {
  green: "#6b9e78",
  greenBright: "#7db88c",
  greenFill: "rgba(107, 158, 120, 0.35)",
  amber: "#c9a84c",
  amberFill: "rgba(201, 168, 76, 0.35)",
  terracotta: "#c47055",
  terracottaFill: "rgba(196, 112, 85, 0.35)",
  slate: "#7eaab8",
  slateFill: "rgba(126, 170, 184, 0.3)",
  grid: "rgba(107, 158, 120, 0.08)",
  ticks: "#847c72",
};

const backgroundColors = [
  accent.greenFill,
  accent.terracottaFill,
  accent.amberFill,
  accent.slateFill,
  "rgba(221, 215, 205, 0.18)",
  "rgba(160, 152, 144, 0.3)",
  "rgba(107, 158, 120, 0.55)",
  "rgba(196, 112, 85, 0.55)",
  "rgba(201, 168, 76, 0.55)",
  "rgba(126, 170, 184, 0.55)",
];

const borderColors = [
  "rgba(107, 158, 120, 0.9)",
  "rgba(196, 112, 85, 0.9)",
  "rgba(201, 168, 76, 0.9)",
  "rgba(126, 170, 184, 0.9)",
  "rgba(221, 215, 205, 0.5)",
  "rgba(160, 152, 144, 0.7)",
  "rgba(107, 158, 120, 1)",
  "rgba(196, 112, 85, 1)",
  "rgba(201, 168, 76, 1)",
  "rgba(126, 170, 184, 1)",
];

const fontStack = '"Monaspace Argon", Consolas, "Liberation Mono", Monaco, "Courier New", monospace';

Chart.defaults.color = accent.ticks;
Chart.defaults.borderColor = accent.grid;
Chart.defaults.font.family = fontStack;
Chart.defaults.font.size = 11;

const tickFont = { size: 11, family: fontStack };
const legendLabel = { boxWidth: 10, boxHeight: 10, font: tickFont, color: "#d1d8d4" };

document.addEventListener("DOMContentLoaded", function () {
  const canvas = document.getElementById("chart-response-times");
  if (!canvas) return;
  const data = JSON.parse(
    document.getElementById("chart-status-response-times-data").innerHTML
  );
  const ctx = canvas.getContext("2d");

  // Total stays the loud green line so old-eye muscle memory still works.
  // The four phase lines are thinner and more muted so they read as
  // breakdown, not as five equal-weight series.
  const series = [
    { key: "total", label: "Total",   color: accent.green,      width: 2,   tension: 0.25 },
    { key: "dns",   label: "DNS",     color: accent.terracotta, width: 1.5, tension: 0.2  },
    { key: "tcp",   label: "TCP",     color: accent.amber,      width: 1.5, tension: 0.2  },
    { key: "tls",   label: "TLS",     color: accent.slate,      width: 1.5, tension: 0.2  },
    { key: "ttfb",  label: "TTFB",    color: "#a09890",         width: 1.5, tension: 0.2  },
  ];

  const chart = new Chart(ctx, {
    type: "line",
    data: {
      labels: data.map((d) => {
        const date = new Date(d.label);
        return `${date.getHours() % 12 || 12}:${date.getMinutes() < 10 ? "0" : ""}${date.getMinutes()} ${date.getHours() >= 12 ? "PM" : "AM"}`;
      }),
      datasets: series.map((s) => ({
        label: s.label,
        // Older rows (pre-migration-0002) have null phase timings — chart.js
        // treats null as a gap in the line, which is exactly what we want.
        data: data.map((d) => (d[s.key] == null ? null : d[s.key])),
        borderColor: s.color,
        backgroundColor: s.color,
        borderWidth: s.width,
        pointRadius: 0,
        pointHoverRadius: 4,
        pointHoverBackgroundColor: s.color,
        tension: s.tension,
        fill: false,
        spanGaps: false,
      })),
    },
    options: {
      responsive: true,
      maintainAspectRatio: false,
      animation: { duration: 0 },
      interaction: { mode: "index", intersect: false },
      plugins: {
        tooltip: {
          mode: "index",
          intersect: false,
          backgroundColor: "rgba(9, 8, 6, 0.95)",
          titleColor: "#ede8e0",
          bodyColor: "#ddd7cd",
          borderColor: "rgba(107, 158, 120, 0.2)",
          borderWidth: 1,
          padding: 10,
          titleFont: tickFont,
          bodyFont: tickFont,
          callbacks: {
            label: (item) =>
              ` ${item.dataset.label}: ${item.parsed.y == null ? "" : item.parsed.y + " ms"}`,
          },
        },
        legend: { position: "top", labels: legendLabel },
      },
      scales: {
        x: {
          grid: { color: accent.grid },
          border: { display: false },
          ticks: { autoSkip: true, maxRotation: 25, font: tickFont, color: accent.ticks },
        },
        y: {
          grid: { color: accent.grid },
          border: { display: false },
          ticks: {
            beginAtZero: true,
            font: tickFont,
            color: accent.ticks,
            callback: (value) => `${value} ms`,
          },
        },
      },
    },
  });
  chart.canvas.parentNode.style.width = "100%";
  chart.canvas.parentNode.style.height = "300px";
});

function buildDoughnut(canvasId, dataId) {
  const canvas = document.getElementById(canvasId);
  if (!canvas) return;
  const data = JSON.parse(document.getElementById(dataId).innerHTML);
  const ctx = canvas.getContext("2d");

  // Color rule: status 200 / Uptime = moss green, non-200 / Downtime =
  // terracotta, everything else pulls from the earthy palette in order.
  const paint = (label) => {
    const name = String(label || "").toLowerCase();
    if (name === "uptime" || name === "200") return [accent.greenFill, "rgba(107, 158, 120, 0.95)"];
    if (name === "downtime") return [accent.terracottaFill, "rgba(196, 112, 85, 0.95)"];
    return null;
  };

  const bg = data.map((d, i) => {
    const p = paint(d.label);
    return p ? p[0] : backgroundColors[i % backgroundColors.length];
  });
  const bd = data.map((d, i) => {
    const p = paint(d.label);
    return p ? p[1] : borderColors[i % borderColors.length];
  });

  new Chart(ctx, {
    type: "doughnut",
    data: {
      labels: data.map((d) => String(d.label)),
      datasets: [
        {
          data: data.map((d) => d.count),
          backgroundColor: bg,
          borderColor: bd,
          borderWidth: 1.5,
        },
      ],
    },
    options: {
      responsive: true,
      aspectRatio: 2,
      animation: { animateRotate: false },
      cutout: "62%",
      plugins: {
        legend: { position: "right", labels: legendLabel },
        tooltip: {
          backgroundColor: "rgba(9, 8, 6, 0.95)",
          titleColor: "#ede8e0",
          bodyColor: "#ddd7cd",
          borderColor: "rgba(107, 158, 120, 0.2)",
          borderWidth: 1,
          padding: 10,
          titleFont: tickFont,
          bodyFont: tickFont,
        },
      },
    },
  });
}

document.addEventListener("DOMContentLoaded", () => buildDoughnut("chart-status-codes", "chart-status-codes-data"));
document.addEventListener("DOMContentLoaded", () => buildDoughnut("chart-uptime", "chart-uptime-data"));