repos
/ orchard main

orchard

mirror

Every site I host, in one repo, along with the Cloudflare Tunnel and Caddy that front them. It's all Go, Vite, and SQLite, and it runs on a desktop at home with nothing listening on an inbound port.

blogbuncaddycloudflare-tunneldockergogolanghomelabhtml-templatemonorepoself-hostedseosqlitestatic-sitetypstuptime-monitoringviteweb-analytics

5.3 KB · 187 lines · JavaScript Raw History
  1import Chart from "chart.js/auto";
  2import {
  3  applyDefaults,
  4  fontStack,
  5  foldToPalette,
  6  ink,
  7  palette,
  8  series,
  9  status,
 10  tooltipStyle,
 11} from "./chart_theme.js";
 12
 13const paletteBorders = palette;
 14
 15applyDefaults(Chart);
 16
 17// Labels are padded to a common width so the four doughnut legends line up in
 18// the sidebar, which only works because the legend font is monospace.
 19let maxLabelLength = 0;
 20if (document.getElementById("chart-total-events-by-browser-data")) {
 21  const browserData = JSON.parse(document.getElementById("chart-total-events-by-browser-data").innerHTML);
 22  const deviceData = JSON.parse(document.getElementById("chart-total-events-by-device-data").innerHTML);
 23  const screenSizeData = JSON.parse(document.getElementById("chart-total-events-by-screen-size-data").innerHTML);
 24  const platformData = JSON.parse(document.getElementById("chart-total-events-by-platform-data").innerHTML);
 25  const allData = [...browserData, ...deviceData, ...screenSizeData, ...platformData];
 26  for (let i = 0; i < allData.length; i++) {
 27    if (allData[i].label.length > maxLabelLength) {
 28      maxLabelLength = allData[i].label.length;
 29    }
 30  }
 31}
 32
 33function padLabels(data) {
 34  for (let i = 0; i < data.length; i++) {
 35    data[i].label = data[i].label + " ".repeat(Math.max(0, maxLabelLength - data[i].label.length));
 36  }
 37  return data;
 38}
 39
 40const doughnutOptions = {
 41  responsive: true,
 42  aspectRatio: 1.8,
 43  animation: { animateRotate: false },
 44  cutout: "60%",
 45  plugins: {
 46    tooltip: tooltipStyle,
 47    legend: {
 48      position: "right",
 49      labels: {
 50        boxWidth: 8,
 51        boxHeight: 8,
 52        padding: 8,
 53        color: ink.legend,
 54        font: { family: fontStack, size: 11 },
 55      },
 56    },
 57  },
 58};
 59
 60// The placeholder keeps the card's footprint about the same, so a chart with no
 61// data does not reflow the ones beside it.
 62function renderEmpty(canvas) {
 63  const placeholder = document.createElement("div");
 64  placeholder.className = "chart-empty";
 65  placeholder.textContent = "no data yet";
 66  Object.assign(placeholder.style, {
 67    display: "flex",
 68    alignItems: "center",
 69    justifyContent: "center",
 70    height: "100%",
 71    minHeight: "120px",
 72    color: ink.ticks,
 73    fontFamily: fontStack,
 74    fontSize: "11px",
 75    letterSpacing: "0.05em",
 76    textTransform: "uppercase",
 77  });
 78  canvas.replaceWith(placeholder);
 79}
 80
 81function hasData(rows) {
 82  return rows.some((d) => (d.count || 0) > 0);
 83}
 84
 85function renderDoughnut(canvasId, dataId) {
 86  document.addEventListener("DOMContentLoaded", function () {
 87    const canvas = document.getElementById(canvasId);
 88    if (!canvas) return;
 89    const raw = JSON.parse(document.getElementById(dataId).innerHTML);
 90    if (!hasData(raw)) {
 91      renderEmpty(canvas);
 92      return;
 93    }
 94    const data = padLabels(foldToPalette(raw));
 95    new Chart(canvas.getContext("2d"), {
 96      type: "doughnut",
 97      data: {
 98        labels: data.map((d) => d.label),
 99        datasets: [
100          {
101            data: data.map((d) => d.count),
102            backgroundColor: palette,
103            borderColor: "rgba(14, 13, 10, 0.9)",
104            borderWidth: 2,
105          },
106        ],
107      },
108      options: doughnutOptions,
109    });
110  });
111}
112
113document.addEventListener("DOMContentLoaded", function () {
114  const canvas = document.getElementById("chart-total-events");
115  if (!canvas) return;
116  const data = JSON.parse(document.getElementById("chart-total-events-data").innerHTML);
117  if (!hasData(data)) {
118    renderEmpty(canvas);
119    return;
120  }
121  const ctx = canvas.getContext("2d");
122
123  const gradient = ctx.createLinearGradient(0, 0, 0, 320);
124  gradient.addColorStop(0, "rgba(87, 179, 120, 0.35)");
125  gradient.addColorStop(1, "rgba(87, 179, 120, 0.01)");
126
127  new Chart(ctx, {
128    type: "line",
129    data: {
130      labels: data.map((d) => d.label),
131      datasets: [
132        {
133          label: "events",
134          data: data.map((d) => d.count),
135          backgroundColor: gradient,
136          borderColor: series[0],
137          pointBackgroundColor: series[0],
138          pointBorderColor: "rgba(14, 13, 10, 1)",
139          pointRadius: 3,
140          pointHoverRadius: 5,
141          borderWidth: 2,
142          tension: 0.25,
143          fill: true,
144        },
145      ],
146    },
147    options: {
148      responsive: true,
149      maintainAspectRatio: false,
150      animation: { duration: 0 },
151      plugins: {
152        tooltip: { ...tooltipStyle, mode: "index", intersect: false },
153        legend: {
154          display: false,
155        },
156      },
157      scales: {
158        x: {
159          ticks: {
160            autoSkip: true,
161            maxTicksLimit: 10,
162            maxRotation: 0,
163            color: ink.ticks,
164            font: { family: fontStack, size: 10 },
165          },
166          grid: { color: ink.grid, drawTicks: false },
167          border: { color: ink.border },
168        },
169        y: {
170          beginAtZero: true,
171          ticks: {
172            color: ink.ticks,
173            font: { family: fontStack, size: 10 },
174          },
175          grid: { color: ink.grid, drawTicks: false },
176          border: { display: false },
177        },
178      },
179    },
180  });
181});
182
183renderDoughnut("chart-total-events-by-browser", "chart-total-events-by-browser-data");
184renderDoughnut("chart-total-events-by-device", "chart-total-events-by-device-data");
185renderDoughnut("chart-total-events-by-screen-size", "chart-total-events-by-screen-size-data");
186renderDoughnut("chart-total-events-by-platform", "chart-total-events-by-platform-data");