repos
/ analytics-django master

analytics-django

mirror archived upstream

Self-hostable website analytics on Django: a straightforward collector API, dashboards, a world map, and PDF reports.

analyticsdjangodockerhandcodedpythonself-hostedsqliteviteweb-analytics

5.9 KB · 180 lines · JavaScript Raw History
  1import Chart from "chart.js/auto";
  2
  3// Palette matches the warm-earth SCSS: mossy green primary, amber secondary,
  4// terracotta accent, plus neutral earth tones for the longer tails of the
  5// doughnut charts. Alpha-blended so adjacent segments stay readable.
  6const palette = [
  7  "rgba(107, 158, 120, 0.75)", // green
  8  "rgba(201, 168, 76, 0.75)",  // amber
  9  "rgba(196, 112, 85, 0.75)",  // terracotta
 10  "rgba(126, 170, 184, 0.75)", // info blue-slate
 11  "rgba(160, 152, 144, 0.75)", // warm grey
 12  "rgba(125, 184, 140, 0.75)", // green bright
 13  "rgba(221, 192, 106, 0.7)",  // amber bright
 14  "rgba(216, 136, 112, 0.7)",  // terracotta bright
 15  "rgba(132, 124, 114, 0.7)",  // gray-400
 16  "rgba(196, 189, 178, 0.7)",  // gray-200
 17];
 18
 19const paletteBorders = palette.map((c) => c.replace(/0?\.\d+\)/, "1)"));
 20
 21const fontStack = "'Monaspace Argon', ui-monospace, 'Cascadia Code', Consolas, monospace";
 22
 23// Common axis / grid styling for the dark theme.
 24Chart.defaults.color = "rgba(221, 215, 205, 0.55)";
 25Chart.defaults.borderColor = "rgba(107, 158, 120, 0.08)";
 26Chart.defaults.font.family = fontStack;
 27Chart.defaults.font.size = 11;
 28
 29const tooltipStyle = {
 30  backgroundColor: "rgba(9, 8, 6, 0.95)",
 31  borderColor: "rgba(107, 158, 120, 0.3)",
 32  borderWidth: 1,
 33  titleColor: "#ede8e0",
 34  bodyColor: "#ddd7cd",
 35  padding: 10,
 36  titleFont: { family: fontStack, size: 12 },
 37  bodyFont: { family: fontStack, size: 11 },
 38  cornerRadius: 4,
 39};
 40
 41// Normalize label widths across the four doughnut charts so their legends line
 42// up in the sidebar. Pad with non-breaking spaces so monospace widths match.
 43let maxLabelLength = 0;
 44if (document.getElementById("chart-total-events-by-browser-data")) {
 45  const browserData = JSON.parse(document.getElementById("chart-total-events-by-browser-data").innerHTML);
 46  const deviceData = JSON.parse(document.getElementById("chart-total-events-by-device-data").innerHTML);
 47  const screenSizeData = JSON.parse(document.getElementById("chart-total-events-by-screen-size-data").innerHTML);
 48  const platformData = JSON.parse(document.getElementById("chart-total-events-by-platform-data").innerHTML);
 49  const allData = [...browserData, ...deviceData, ...screenSizeData, ...platformData];
 50  for (let i = 0; i < allData.length; i++) {
 51    if (allData[i].label.length > maxLabelLength) {
 52      maxLabelLength = allData[i].label.length;
 53    }
 54  }
 55}
 56
 57function padLabels(data) {
 58  for (let i = 0; i < data.length; i++) {
 59    data[i].label = data[i].label + " ".repeat(Math.max(0, maxLabelLength - data[i].label.length));
 60  }
 61  return data;
 62}
 63
 64const doughnutOptions = {
 65  responsive: true,
 66  aspectRatio: 1.8,
 67  animation: { animateRotate: false },
 68  cutout: "60%",
 69  plugins: {
 70    tooltip: tooltipStyle,
 71    legend: {
 72      position: "right",
 73      labels: {
 74        boxWidth: 8,
 75        boxHeight: 8,
 76        padding: 8,
 77        color: "rgba(221, 215, 205, 0.7)",
 78        font: { family: fontStack, size: 11 },
 79      },
 80    },
 81  },
 82};
 83
 84function renderDoughnut(canvasId, dataId) {
 85  document.addEventListener("DOMContentLoaded", function () {
 86    const canvas = document.getElementById(canvasId);
 87    if (!canvas) return;
 88    const raw = JSON.parse(document.getElementById(dataId).innerHTML);
 89    const data = padLabels(raw);
 90    new Chart(canvas.getContext("2d"), {
 91      type: "doughnut",
 92      data: {
 93        labels: data.map((d) => d.label),
 94        datasets: [
 95          {
 96            data: data.map((d) => d.count),
 97            backgroundColor: palette,
 98            borderColor: "rgba(14, 13, 10, 0.9)",
 99            borderWidth: 2,
100          },
101        ],
102      },
103      options: doughnutOptions,
104    });
105  });
106}
107
108document.addEventListener("DOMContentLoaded", function () {
109  const canvas = document.getElementById("chart-total-events");
110  if (!canvas) return;
111  const data = JSON.parse(document.getElementById("chart-total-events-data").innerHTML);
112  const ctx = canvas.getContext("2d");
113
114  // Build a subtle vertical gradient fill so the line chart feels lit from
115  // below without washing out the dark surface.
116  const gradient = ctx.createLinearGradient(0, 0, 0, 320);
117  gradient.addColorStop(0, "rgba(107, 158, 120, 0.35)");
118  gradient.addColorStop(1, "rgba(107, 158, 120, 0.01)");
119
120  new Chart(ctx, {
121    type: "line",
122    data: {
123      labels: data.map((d) => d.label),
124      datasets: [
125        {
126          label: "events",
127          data: data.map((d) => d.count),
128          backgroundColor: gradient,
129          borderColor: "rgba(125, 184, 140, 0.95)",
130          pointBackgroundColor: "rgba(125, 184, 140, 1)",
131          pointBorderColor: "rgba(14, 13, 10, 1)",
132          pointRadius: 3,
133          pointHoverRadius: 5,
134          borderWidth: 2,
135          tension: 0.25,
136          fill: true,
137        },
138      ],
139    },
140    options: {
141      responsive: true,
142      maintainAspectRatio: false,
143      animation: { duration: 0 },
144      plugins: {
145        tooltip: { ...tooltipStyle, mode: "index", intersect: false },
146        legend: {
147          display: false,
148        },
149      },
150      scales: {
151        x: {
152          ticks: {
153            autoSkip: true,
154            maxTicksLimit: 10,
155            maxRotation: 0,
156            color: "rgba(132, 124, 114, 0.85)",
157            font: { family: fontStack, size: 10 },
158          },
159          grid: { color: "rgba(107, 158, 120, 0.04)", drawTicks: false },
160          border: { color: "rgba(107, 158, 120, 0.12)" },
161        },
162        y: {
163          beginAtZero: true,
164          ticks: {
165            color: "rgba(132, 124, 114, 0.85)",
166            font: { family: fontStack, size: 10 },
167          },
168          grid: { color: "rgba(107, 158, 120, 0.06)", drawTicks: false },
169          border: { display: false },
170        },
171      },
172    },
173  });
174});
175
176renderDoughnut("chart-total-events-by-browser", "chart-total-events-by-browser-data");
177renderDoughnut("chart-total-events-by-device", "chart-total-events-by-device-data");
178renderDoughnut("chart-total-events-by-screen-size", "chart-total-events-by-screen-size-data");
179renderDoughnut("chart-total-events-by-platform", "chart-total-events-by-platform-data");