Pārlūkot izejas kodu

move stats related javascript code to its own file

Pouria Ezzati 1 gadu atpakaļ
vecāks
revīzija
4c316b5cba
3 mainītis faili ar 451 papildinājumiem un 446 dzēšanām
  1. 1 0
      server/views/stats.hbs
  2. 0 446
      static/scripts/main.js
  3. 450 0
      static/scripts/stats.js

+ 1 - 0
server/views/stats.hbs

@@ -21,4 +21,5 @@
 {{> footer}}
 {{#extend "scripts"}}
   <script src="/libs/chart.min.js"></script>
+  <script src="/scripts/stats.js"></script>
 {{/extend}}

+ 0 - 446
static/scripts/main.js

@@ -269,452 +269,6 @@ function canSendVerificationEmail() {
     checkbox.classList.add('hidden');
 }
 
-// create views chart label
-function createViewsChartLabel(ctx) {
-  const period = ctx.dataset.period;
-  let labels = [];
-
-  if (period === "day") {
-    const nowHour = new Date().getHours();
-    for (let i = 23; i >= 0; --i) {
-      let h = nowHour - i;
-      if (h < 0) h = 24 + h;
-      labels.push(`${Math.floor(h)}:00`);
-    }
-  }
-
-  if (period === "week") {
-    const nowDay = new Date().getDate();
-    for (let i = 6; i >= 0; --i) {
-      const date = new Date(new Date().setDate(nowDay - i));
-      labels.push(`${date.getDate()} ${date.toLocaleString("default",{month:"short"})}`);
-    }
-  }
-
-  if (period === "month") {
-    const nowDay = new Date().getDate();  
-    for (let i = 29; i >= 0; --i) {
-      const date = new Date(new Date().setDate(nowDay - i));
-      labels.push(`${date.getDate()} ${date.toLocaleString("default",{month:"short"})}`);
-    }
-  }
-
-  if (period === "year") {
-    const nowMonth = new Date().getMonth();  
-    for (let i = 11; i >= 0; --i) {
-      const date = new Date(new Date().setMonth(nowMonth - i));
-      labels.push(`${date.toLocaleString("default",{month:"short"})} ${date.toLocaleString("default",{year:"numeric"})}`);
-    }
-  }
-
-  return labels;
-}
-
-// create views chart
-function createViewsChart() {
-  const canvases = document.querySelectorAll("canvas.visits");
-  if (!canvases || !canvases.length) return;
-
-  canvases.forEach(ctx => {
-    const data = JSON.parse(ctx.dataset.data);
-    const period = ctx.dataset.period;
-  
-    const labels = createViewsChartLabel(ctx);
-    const maxTicksLimitX = period === "year" ? 6 : period === "month" ? 15 : 12;
-  
-    const gradient = ctx.getContext("2d").createLinearGradient(0, 0, 0, 300);
-    gradient.addColorStop(0, "rgba(179, 157, 219, 0.95)");   
-    gradient.addColorStop(1, "rgba(179, 157, 219, 0.05)");
-    
-    new Chart(ctx, {
-      type: "line",
-      data: {
-        labels: labels,
-        datasets: [{
-          label: "Views",
-          data,
-          tension: 0.3,
-  
-          elements: {
-            point: {
-              pointRadius: 0,
-              pointHoverRadius: 4
-            }
-          },
-          fill: {
-            target: "start",
-          },
-          backgroundColor: gradient,
-          borderColor: "rgb(179, 157, 219)",
-          borderWidth: 1,
-        }]
-      },
-      options: {
-        plugins: {
-          legend: {
-            display: false,
-          },
-          tooltip: {
-            backgroundColor: "rgba(255, 255, 255, 0.95)",
-            titleColor: "#333",
-            titleFont: { weight: "normal", size: 15 },
-            bodyFont: { weight: "normal", size: 16 },
-            bodyColor: "rgb(179, 157, 219)",
-            padding: 12,
-            cornerRadius: 2,
-            borderColor: "rgba(0, 0, 0, 0.1)",
-            borderWidth: 1,
-            displayColors: false,
-          }
-        },
-        responsive: true,
-        interaction: {
-          intersect: false,
-          usePointStyle: true,
-          mode: "index",
-        },
-        scales: {
-          y: {
-            grace: "10%",
-            beginAtZero: true,
-            ticks: {
-              maxTicksLimit: 5
-            }
-          },
-          x: {
-            ticks: {
-              maxTicksLimit: maxTicksLimitX,
-            }
-          }
-        }  
-      }
-    });
-
-    // reset the display: block style that chart.js applies automatically
-    ctx.style.display = "";
-  });
-}
-
-// beautify browser lables
-function beautifyBrowserName(name) {
-  if (name === "firefox") return "Firefox";
-  if (name === "chrome") return "Chrome";
-  if (name === "edge") return "Edge";
-  if (name === "opera") return "Opera";
-  if (name === "safari") return "Safari";
-  if (name === "other") return "Other";
-  if (name === "ie") return "IE";
-  return name;
-}
-
-// create browsers chart
-function createBrowsersChart() {
-  const canvases = document.querySelectorAll("canvas.browsers");
-  if (!canvases || !canvases.length) return;
-
-  canvases.forEach(ctx => {
-    const data = JSON.parse(ctx.dataset.data);
-    const period = ctx.dataset.period;
-
-    const gradient = ctx.getContext("2d").createLinearGradient(500, 0, 0, 0);
-    const gradientHover = ctx.getContext("2d").createLinearGradient(500, 0, 0, 0);
-    gradient.addColorStop(0, "rgba(179, 157, 219, 0.95)");   
-    gradient.addColorStop(1, "rgba(179, 157, 219, 0.05)");
-    gradientHover.addColorStop(0, "rgba(179, 157, 219, 0.9)");   
-    gradientHover.addColorStop(1, "rgba(179, 157, 219, 0.4)");
-
-    new Chart(ctx, {
-      type: "bar",
-      data: {
-        labels: data.map(d => beautifyBrowserName(d.name)),
-        datasets: [{
-          label: "Views",
-          data: data.map(d => d.value),
-          backgroundColor: gradient,
-          borderColor: "rgba(179, 157, 219, 1)",
-          borderWidth: 1,
-          hoverBackgroundColor: gradientHover,
-          hoverBorderWidth: 2
-        }]
-      },
-      options: {
-        indexAxis: "y",
-        plugins: {
-          legend: {
-            display: false,
-          },
-          tooltip: {
-            backgroundColor: "rgba(255, 255, 255, 0.95)",
-            titleColor: "#333",
-            titleFont: { weight: "normal", size: 15 },
-            bodyFont: { weight: "normal", size: 16 },
-            bodyColor: "rgb(179, 157, 219)",
-            padding: 12,
-            cornerRadius: 2,
-            borderColor: "rgba(0, 0, 0, 0.1)",
-            borderWidth: 1,
-            displayColors: false,
-          }
-        },
-        responsive: true,
-        interaction: {
-          intersect: false,
-          mode: "index",
-          axis: "y"
-        },
-        scales: {
-          x: {
-            grace: "5%",
-            beginAtZero: true,
-            ticks: {
-              maxTicksLimit: 6,
-            }
-          }
-        }  
-      }
-    });
-
-    // reset the display: block style that chart.js applies automatically
-    ctx.style.display = "";
-  });
-}
-
-// create referrers chart
-function createReferrersChart() {
-  const canvases = document.querySelectorAll("canvas.referrers");
-  if (!canvases || !canvases.length) return;
-
-  canvases.forEach(ctx => {
-    const data = JSON.parse(ctx.dataset.data);
-    const period = ctx.dataset.period;
-    let max = Array.from(data).sort((a, b) => a.value > b.value ? -1 : 1)[0];
-
-    let tooltipEnabled = true;
-    let hoverBackgroundColor = "rgba(179, 157, 219, 1)";
-    let hoverBorderWidth = 2;
-    let borderColor = "rgba(179, 157, 219, 1)";
-    if (data.length === 0) {
-      data.push({ name: "No views.", value: 1 });
-      max = { value: 1000 };
-      tooltipEnabled = false;
-      hoverBackgroundColor = "rgba(179, 157, 219, 0.1)";
-      hoverBorderWidth = 1;
-      borderColor = "rgba(179, 157, 219, 0.2)";
-    }
-
-    new Chart(ctx, {
-      type: "doughnut",
-      data: {
-        labels: data.map(d => d.name.replace(/\[dot\]/g, ".")),
-        datasets: [{
-          label: "Views",
-          data: data.map(d => d.value),
-          backgroundColor: data.map(d => `rgba(179, 157, 219, ${Math.max((d.value / max.value) - 0.2, 0.1).toFixed(2)})`),
-          borderWidth: 1,
-          borderColor,
-          hoverBackgroundColor,
-          hoverBorderWidth,
-        }]
-      },
-      options: {
-        plugins: {
-          legend: {
-            position: "left",
-            labels: {
-              boxWidth: 25,
-              font: { size: 11 }
-            }
-          },
-          tooltip: {
-            enabled: tooltipEnabled,
-            backgroundColor: "rgba(255, 255, 255, 0.95)",
-            titleColor: "#333",
-            titleFont: { weight: "normal", size: 15 },
-            bodyFont: { weight: "normal", size: 16 },
-            bodyColor: "rgb(179, 157, 219)",
-            padding: 12,
-            cornerRadius: 2,
-            borderColor: "rgba(0, 0, 0, 0.1)",
-            borderWidth: 1,
-            displayColors: false,
-          }
-        },
-        responsive: false,
-      }
-    });
-
-    // reset the display: block style that chart.js applies automatically
-    ctx.style.display = "";
-  });
-}
-
-
-// beautify browser lables
-function beautifyOsName(name) {
-  if (name === "android") return "Android";
-  if (name === "ios") return "iOS";
-  if (name === "linux") return "Linux";
-  if (name === "macos") return "macOS";
-  if (name === "windows") return "Windows";
-  if (name === "other") return "Other";
-  return name;
-}
-
-
-// create operation systems chart
-function createOsChart() {
-  const canvases = document.querySelectorAll("canvas.os");
-  if (!canvases || !canvases.length) return;
-
-  canvases.forEach(ctx => {
-    const data = JSON.parse(ctx.dataset.data);
-    const period = ctx.dataset.period;
-
-    const gradient = ctx.getContext("2d").createLinearGradient(500, 0, 0, 0);
-    const gradientHover = ctx.getContext("2d").createLinearGradient(500, 0, 0, 0);
-    gradient.addColorStop(0, "rgba(179, 157, 219, 0.95)");   
-    gradient.addColorStop(1, "rgba(179, 157, 219, 0.05)");
-    gradientHover.addColorStop(0, "rgba(179, 157, 219, 0.9)");   
-    gradientHover.addColorStop(1, "rgba(179, 157, 219, 0.4)");
-
-    new Chart(ctx, {
-      type: "bar",
-      data: {
-        labels: data.map(d => beautifyOsName(d.name)),
-        datasets: [{
-          label: "Views",
-          data: data.map(d => d.value),
-          backgroundColor: gradient,
-          borderColor: "rgba(179, 157, 219, 1)",
-          borderWidth: 1,
-          hoverBackgroundColor: gradientHover,
-          hoverBorderWidth: 2
-        }]
-      },
-      options: {
-        indexAxis: "y",
-        plugins: {
-          legend: {
-            display: false,
-          },
-          tooltip: {
-            backgroundColor: "rgba(255, 255, 255, 0.95)",
-            titleColor: "#333",
-            titleFont: { weight: "normal", size: 15 },
-            bodyFont: { weight: "normal", size: 16 },
-            bodyColor: "rgb(179, 157, 219)",
-            padding: 12,
-            cornerRadius: 2,
-            borderColor: "rgba(0, 0, 0, 0.1)",
-            borderWidth: 1,
-            displayColors: false,
-          }
-        },
-        responsive: true,
-        interaction: {
-          intersect: false,
-          mode: "index",
-          axis: "y"
-        },
-        scales: {
-          x: {
-            grace:"5%",
-            beginAtZero: true,
-            ticks: {
-              maxTicksLimit: 6,
-            }
-          }
-        }  
-      }
-    });
-
-    // reset the display: block style that chart.js applies automatically
-    ctx.style.display = "";
-  });
-}
-
-// add data to the map
-function feedMapData(period) {
-  const map = document.querySelector("svg.map");
-  const paths = map.querySelectorAll("path");
-  if (!map || !paths || !paths.length) return;
-
-  let data = JSON.parse(map.dataset[period || "day"]);
-  if (!data) return;
-
-  let max = data.sort((a, b) => a.value > b.value ? -1 : 1)[0];
-
-  if (!max) max = { value: 1 }
-  
-  data = data.reduce((a, c) => ({ ...a, [c.name]: c.value }), {});
-  
-  for (let i = 0; i < paths.length; ++i) {
-    const id = paths[i].dataset.id;
-    const views = data[id] || 0;
-    paths[i].dataset.views = views;
-    const colorLevel = Math.ceil((views / max.value) * 6);  
-    const classList = paths[i].classList;
-    for (let j = 1; j < 7; j++) {
-      paths[i].classList.remove(`color-${j}`);
-    }
-    paths[i].classList.add(`color-${colorLevel}`)
-    paths[i].dataset.views = views;
-  }
-}
-
-// handle map tooltip hover
-function mapTooltipHoverOver() {
-  const tooltip = document.querySelector("#map-tooltip");
-  if (!tooltip) return;
-  if (!event.target.dataset.id) return mapTooltipHoverOut();
-  if (!tooltip.classList.contains("active")) {
-    tooltip.classList.add("visible");
-  }
-  tooltip.dataset.tooltip = `${event.target.ariaLabel}: ${event.target.dataset.views || 0}`;
-  const rect = event.target.getBoundingClientRect();
-  tooltip.style.top = rect.top + (rect.height / 2) + "px";
-  tooltip.style.left = rect.left + (rect.width / 2) + "px";
-  event.target.classList.add("active");
-}
-function mapTooltipHoverOut() {
-  const tooltip = document.querySelector("#map-tooltip");
-  const map = document.querySelector("svg.map");
-  const paths = map.querySelectorAll("path");
-  if (!tooltip || !map) return;
-  tooltip.classList.remove("visible");
-  for (let i = 0; i < paths.length; ++i) {
-    paths[i].classList.remove("active");
-  }
-}
-
-// create stats charts
-function createCharts() {
-  createViewsChart();
-  createBrowsersChart();
-  createReferrersChart();
-  createOsChart();
-  feedMapData();
-}
-
-// change stats period for showing charts and data
-function changeStatsPeriod(event) {
-  const period = event.target.dataset.period;
-  if (!period) return;
-  const canvases = document.querySelector("#stats").querySelectorAll("[data-period]");
-  const buttons = document.querySelector("#stats").querySelectorAll(".nav");
-  if (!buttons || !canvases) return;
-  buttons.forEach(b => b.disabled = false);
-  event.target.disabled = true;
-  canvases.forEach(canvas => {
-    if (canvas.dataset.period === period) {
-      canvas.classList.remove("hidden");
-    } else {
-      canvas.classList.add("hidden");
-    }
-  });
-  feedMapData(period);
-}
-
 // htmx prefetch extension
 // https://github.com/bigskysoftware/htmx-extensions/blob/main/src/preload/README.md
 htmx.defineExtension('preload', {

+ 450 - 0
static/scripts/stats.js

@@ -0,0 +1,450 @@
+// create views chart label
+function createViewsChartLabel(ctx) {
+  const period = ctx.dataset.period;
+  let labels = [];
+
+  if (period === "day") {
+    const nowHour = new Date().getHours();
+    for (let i = 23; i >= 0; --i) {
+      let h = nowHour - i;
+      if (h < 0) h = 24 + h;
+      labels.push(`${Math.floor(h)}:00`);
+    }
+  }
+
+  if (period === "week") {
+    const nowDay = new Date().getDate();
+    for (let i = 6; i >= 0; --i) {
+      const date = new Date(new Date().setDate(nowDay - i));
+      labels.push(`${date.getDate()} ${date.toLocaleString("default",{month:"short"})}`);
+    }
+  }
+
+  if (period === "month") {
+    const nowDay = new Date().getDate();  
+    for (let i = 29; i >= 0; --i) {
+      const date = new Date(new Date().setDate(nowDay - i));
+      labels.push(`${date.getDate()} ${date.toLocaleString("default",{month:"short"})}`);
+    }
+  }
+
+  if (period === "year") {
+    const nowMonth = new Date().getMonth();  
+    for (let i = 11; i >= 0; --i) {
+      const date = new Date(new Date().setMonth(nowMonth - i));
+      labels.push(`${date.toLocaleString("default",{month:"short"})} ${date.toLocaleString("default",{year:"numeric"})}`);
+    }
+  }
+
+  return labels;
+}
+
+// change stats period for showing charts and data
+function changeStatsPeriod(event) {
+  const period = event.target.dataset.period;
+  if (!period) return;
+  const canvases = document.querySelector("#stats").querySelectorAll("[data-period]");
+  const buttons = document.querySelector("#stats").querySelectorAll(".nav");
+  if (!buttons || !canvases) return;
+  buttons.forEach(b => b.disabled = false);
+  event.target.disabled = true;
+  canvases.forEach(canvas => {
+    if (canvas.dataset.period === period) {
+      canvas.classList.remove("hidden");
+    } else {
+      canvas.classList.add("hidden");
+    }
+  });
+  feedMapData(period);
+}
+
+// beautify browser lables
+function beautifyBrowserName(name) {
+  if (name === "firefox") return "Firefox";
+  if (name === "chrome") return "Chrome";
+  if (name === "edge") return "Edge";
+  if (name === "opera") return "Opera";
+  if (name === "safari") return "Safari";
+  if (name === "other") return "Other";
+  if (name === "ie") return "IE";
+  return name;
+}
+
+
+// create views chart
+function createViewsChart() {
+  const canvases = document.querySelectorAll("canvas.visits");
+  if (!canvases || !canvases.length) return;
+
+  canvases.forEach(ctx => {
+    const data = JSON.parse(ctx.dataset.data);
+    const period = ctx.dataset.period;
+  
+    const labels = createViewsChartLabel(ctx);
+    const maxTicksLimitX = period === "year" ? 6 : period === "month" ? 15 : 12;
+  
+    const gradient = ctx.getContext("2d").createLinearGradient(0, 0, 0, 300);
+    gradient.addColorStop(0, "rgba(179, 157, 219, 0.95)");   
+    gradient.addColorStop(1, "rgba(179, 157, 219, 0.05)");
+    
+    new Chart(ctx, {
+      type: "line",
+      data: {
+        labels: labels,
+        datasets: [{
+          label: "Views",
+          data,
+          tension: 0.3,
+  
+          elements: {
+            point: {
+              pointRadius: 0,
+              pointHoverRadius: 4
+            }
+          },
+          fill: {
+            target: "start",
+          },
+          backgroundColor: gradient,
+          borderColor: "rgb(179, 157, 219)",
+          borderWidth: 1,
+        }]
+      },
+      options: {
+        plugins: {
+          legend: {
+            display: false,
+          },
+          tooltip: {
+            backgroundColor: "rgba(255, 255, 255, 0.95)",
+            titleColor: "#333",
+            titleFont: { weight: "normal", size: 15 },
+            bodyFont: { weight: "normal", size: 16 },
+            bodyColor: "rgb(179, 157, 219)",
+            padding: 12,
+            cornerRadius: 2,
+            borderColor: "rgba(0, 0, 0, 0.1)",
+            borderWidth: 1,
+            displayColors: false,
+          }
+        },
+        responsive: true,
+        interaction: {
+          intersect: false,
+          usePointStyle: true,
+          mode: "index",
+        },
+        scales: {
+          y: {
+            grace: "10%",
+            beginAtZero: true,
+            ticks: {
+              maxTicksLimit: 5
+            }
+          },
+          x: {
+            ticks: {
+              maxTicksLimit: maxTicksLimitX,
+            }
+          }
+        }  
+      }
+    });
+
+    // reset the display: block style that chart.js applies automatically
+    ctx.style.display = "";
+  });
+}
+
+// create browsers chart
+function createBrowsersChart() {
+  const canvases = document.querySelectorAll("canvas.browsers");
+  if (!canvases || !canvases.length) return;
+
+  canvases.forEach(ctx => {
+    const data = JSON.parse(ctx.dataset.data);
+    const period = ctx.dataset.period;
+
+    const gradient = ctx.getContext("2d").createLinearGradient(500, 0, 0, 0);
+    const gradientHover = ctx.getContext("2d").createLinearGradient(500, 0, 0, 0);
+    gradient.addColorStop(0, "rgba(179, 157, 219, 0.95)");   
+    gradient.addColorStop(1, "rgba(179, 157, 219, 0.05)");
+    gradientHover.addColorStop(0, "rgba(179, 157, 219, 0.9)");   
+    gradientHover.addColorStop(1, "rgba(179, 157, 219, 0.4)");
+
+    new Chart(ctx, {
+      type: "bar",
+      data: {
+        labels: data.map(d => beautifyBrowserName(d.name)),
+        datasets: [{
+          label: "Views",
+          data: data.map(d => d.value),
+          backgroundColor: gradient,
+          borderColor: "rgba(179, 157, 219, 1)",
+          borderWidth: 1,
+          hoverBackgroundColor: gradientHover,
+          hoverBorderWidth: 2
+        }]
+      },
+      options: {
+        indexAxis: "y",
+        plugins: {
+          legend: {
+            display: false,
+          },
+          tooltip: {
+            backgroundColor: "rgba(255, 255, 255, 0.95)",
+            titleColor: "#333",
+            titleFont: { weight: "normal", size: 15 },
+            bodyFont: { weight: "normal", size: 16 },
+            bodyColor: "rgb(179, 157, 219)",
+            padding: 12,
+            cornerRadius: 2,
+            borderColor: "rgba(0, 0, 0, 0.1)",
+            borderWidth: 1,
+            displayColors: false,
+          }
+        },
+        responsive: true,
+        interaction: {
+          intersect: false,
+          mode: "index",
+          axis: "y"
+        },
+        scales: {
+          x: {
+            grace: "5%",
+            beginAtZero: true,
+            ticks: {
+              maxTicksLimit: 6,
+            }
+          }
+        }  
+      }
+    });
+
+    // reset the display: block style that chart.js applies automatically
+    ctx.style.display = "";
+  });
+}
+
+// create referrers chart
+function createReferrersChart() {
+  const canvases = document.querySelectorAll("canvas.referrers");
+  if (!canvases || !canvases.length) return;
+
+  canvases.forEach(ctx => {
+    const data = JSON.parse(ctx.dataset.data);
+    const period = ctx.dataset.period;
+    let max = Array.from(data).sort((a, b) => a.value > b.value ? -1 : 1)[0];
+
+    let tooltipEnabled = true;
+    let hoverBackgroundColor = "rgba(179, 157, 219, 1)";
+    let hoverBorderWidth = 2;
+    let borderColor = "rgba(179, 157, 219, 1)";
+    if (data.length === 0) {
+      data.push({ name: "No views.", value: 1 });
+      max = { value: 1000 };
+      tooltipEnabled = false;
+      hoverBackgroundColor = "rgba(179, 157, 219, 0.1)";
+      hoverBorderWidth = 1;
+      borderColor = "rgba(179, 157, 219, 0.2)";
+    }
+
+    new Chart(ctx, {
+      type: "doughnut",
+      data: {
+        labels: data.map(d => d.name.replace(/\[dot\]/g, ".")),
+        datasets: [{
+          label: "Views",
+          data: data.map(d => d.value),
+          backgroundColor: data.map(d => `rgba(179, 157, 219, ${Math.max((d.value / max.value) - 0.2, 0.1).toFixed(2)})`),
+          borderWidth: 1,
+          borderColor,
+          hoverBackgroundColor,
+          hoverBorderWidth,
+        }]
+      },
+      options: {
+        plugins: {
+          legend: {
+            position: "left",
+            labels: {
+              boxWidth: 25,
+              font: { size: 11 }
+            }
+          },
+          tooltip: {
+            enabled: tooltipEnabled,
+            backgroundColor: "rgba(255, 255, 255, 0.95)",
+            titleColor: "#333",
+            titleFont: { weight: "normal", size: 15 },
+            bodyFont: { weight: "normal", size: 16 },
+            bodyColor: "rgb(179, 157, 219)",
+            padding: 12,
+            cornerRadius: 2,
+            borderColor: "rgba(0, 0, 0, 0.1)",
+            borderWidth: 1,
+            displayColors: false,
+          }
+        },
+        responsive: false,
+      }
+    });
+
+    // reset the display: block style that chart.js applies automatically
+    ctx.style.display = "";
+  });
+}
+
+
+// beautify browser lables
+function beautifyOsName(name) {
+  if (name === "android") return "Android";
+  if (name === "ios") return "iOS";
+  if (name === "linux") return "Linux";
+  if (name === "macos") return "macOS";
+  if (name === "windows") return "Windows";
+  if (name === "other") return "Other";
+  return name;
+}
+
+
+// create operation systems chart
+function createOsChart() {
+  const canvases = document.querySelectorAll("canvas.os");
+  if (!canvases || !canvases.length) return;
+
+  canvases.forEach(ctx => {
+    const data = JSON.parse(ctx.dataset.data);
+    const period = ctx.dataset.period;
+
+    const gradient = ctx.getContext("2d").createLinearGradient(500, 0, 0, 0);
+    const gradientHover = ctx.getContext("2d").createLinearGradient(500, 0, 0, 0);
+    gradient.addColorStop(0, "rgba(179, 157, 219, 0.95)");   
+    gradient.addColorStop(1, "rgba(179, 157, 219, 0.05)");
+    gradientHover.addColorStop(0, "rgba(179, 157, 219, 0.9)");   
+    gradientHover.addColorStop(1, "rgba(179, 157, 219, 0.4)");
+
+    new Chart(ctx, {
+      type: "bar",
+      data: {
+        labels: data.map(d => beautifyOsName(d.name)),
+        datasets: [{
+          label: "Views",
+          data: data.map(d => d.value),
+          backgroundColor: gradient,
+          borderColor: "rgba(179, 157, 219, 1)",
+          borderWidth: 1,
+          hoverBackgroundColor: gradientHover,
+          hoverBorderWidth: 2
+        }]
+      },
+      options: {
+        indexAxis: "y",
+        plugins: {
+          legend: {
+            display: false,
+          },
+          tooltip: {
+            backgroundColor: "rgba(255, 255, 255, 0.95)",
+            titleColor: "#333",
+            titleFont: { weight: "normal", size: 15 },
+            bodyFont: { weight: "normal", size: 16 },
+            bodyColor: "rgb(179, 157, 219)",
+            padding: 12,
+            cornerRadius: 2,
+            borderColor: "rgba(0, 0, 0, 0.1)",
+            borderWidth: 1,
+            displayColors: false,
+          }
+        },
+        responsive: true,
+        interaction: {
+          intersect: false,
+          mode: "index",
+          axis: "y"
+        },
+        scales: {
+          x: {
+            grace:"5%",
+            beginAtZero: true,
+            ticks: {
+              maxTicksLimit: 6,
+            }
+          }
+        }  
+      }
+    });
+
+    // reset the display: block style that chart.js applies automatically
+    ctx.style.display = "";
+  });
+}
+
+// add data to the map
+function feedMapData(period) {
+  const map = document.querySelector("svg.map");
+  const paths = map.querySelectorAll("path");
+  if (!map || !paths || !paths.length) return;
+
+  let data = JSON.parse(map.dataset[period || "day"]);
+  if (!data) return;
+
+  let max = data.sort((a, b) => a.value > b.value ? -1 : 1)[0];
+
+  if (!max) max = { value: 1 }
+  
+  data = data.reduce((a, c) => ({ ...a, [c.name]: c.value }), {});
+  
+  for (let i = 0; i < paths.length; ++i) {
+    const id = paths[i].dataset.id;
+    const views = data[id] || 0;
+    paths[i].dataset.views = views;
+    const colorLevel = Math.ceil((views / max.value) * 6);  
+    const classList = paths[i].classList;
+    for (let j = 1; j < 7; j++) {
+      paths[i].classList.remove(`color-${j}`);
+    }
+    paths[i].classList.add(`color-${colorLevel}`)
+    paths[i].dataset.views = views;
+  }
+}
+
+// handle map tooltip hover
+function mapTooltipHoverOver() {
+  const tooltip = document.querySelector("#map-tooltip");
+  if (!tooltip) return;
+  if (!event.target.dataset.id) return mapTooltipHoverOut();
+  if (!tooltip.classList.contains("active")) {
+    tooltip.classList.add("visible");
+  }
+  tooltip.dataset.tooltip = `${event.target.ariaLabel}: ${event.target.dataset.views || 0}`;
+  const rect = event.target.getBoundingClientRect();
+  tooltip.style.top = rect.top + (rect.height / 2) + "px";
+  tooltip.style.left = rect.left + (rect.width / 2) + "px";
+  event.target.classList.add("active");
+}
+function mapTooltipHoverOut() {
+  const tooltip = document.querySelector("#map-tooltip");
+  const map = document.querySelector("svg.map");
+  const paths = map.querySelectorAll("path");
+  if (!tooltip || !map) return;
+  tooltip.classList.remove("visible");
+  for (let i = 0; i < paths.length; ++i) {
+    paths[i].classList.remove("active");
+  }
+}
+
+// create stats charts
+function createCharts() {
+  if (Chart === undefined) {
+    setTimeout(function() { createCharts() }, 100);
+    return;
+  }
+  createViewsChart();
+  createBrowsersChart();
+  createReferrersChart();
+  createOsChart();
+  feedMapData();
+}