Zum Inhalt

touchstone/gui/static/sparkline.js

/**
 * sparkline.js — build-free inline-SVG sparkline for RAM/CPU time series.
 *
 * Usage: add `data-sparkline` to any <svg> element; its `data-ram` / `data-cpu`
 * attributes carry JSON arrays ([number|null, ...]).  null values are treated as
 * gaps (no line drawn) so old bundles without CPU data show no CPU trace.
 *
 * Wired by initSparklines() called on DOMContentLoaded.
 */

"use strict";

(function () {
  /** Return a polyline path-string for a series of [x,y] points, skipping nulls. */
  function polylinePath(xs, ys) {
    if (!xs || xs.length === 0) return "";
    var segments = [];
    var inSeg = false;
    for (var i = 0; i < xs.length; i++) {
      if (ys[i] == null || isNaN(ys[i])) {
        inSeg = false;
        continue;
      }
      if (!inSeg) {
        segments.push("M" + xs[i].toFixed(1) + "," + ys[i].toFixed(1));
        inSeg = true;
      } else {
        segments.push("L" + xs[i].toFixed(1) + "," + ys[i].toFixed(1));
      }
    }
    return segments.join(" ");
  }

  /** Normalise a series to [0, height] range, returns null-preserved float array. */
  function normalise(vals, height, padPx) {
    var nums = vals.filter(function (v) { return v != null && !isNaN(v); });
    if (nums.length === 0) return vals.map(function () { return null; });
    var min = Math.min.apply(null, nums);
    var max = Math.max.apply(null, nums);
    var range = max - min;
    return vals.map(function (v) {
      if (v == null || isNaN(v)) return null;
      var frac = range > 0 ? (v - min) / range : 0.5;
      // top is y=padPx, bottom is y=(height-padPx): invert frac
      return (height - padPx) - frac * (height - 2 * padPx);
    });
  }

  function renderSparkline(svg) {
    var ramRaw = [];
    var cpuRaw = [];
    try { ramRaw = JSON.parse(svg.dataset.ram || "[]"); } catch (e) {}
    try { cpuRaw = JSON.parse(svg.dataset.cpu || "[]"); } catch (e) {}

    var W = parseFloat(svg.getAttribute("width") || "200");
    var H = parseFloat(svg.getAttribute("height") || "40");
    var PAD = 3;

    var xs = ramRaw.length > 0
      ? ramRaw.map(function (_, i) { return PAD + (i / (ramRaw.length - 1 || 1)) * (W - 2 * PAD); })
      : [PAD, W - PAD];

    var cpuXs = cpuRaw.length > 0
      ? cpuRaw.map(function (_, i) { return PAD + (i / (cpuRaw.length - 1 || 1)) * (W - 2 * PAD); })
      : [PAD, W - PAD];

    var ramYs = normalise(ramRaw, H, PAD);
    var cpuYs = normalise(cpuRaw, H, PAD);

    // Clear existing content
    while (svg.firstChild) svg.removeChild(svg.firstChild);

    // RAM line (blue)
    var ramPath = polylinePath(xs, ramYs);
    if (ramPath) {
      var polyEl = document.createElementNS("http://www.w3.org/2000/svg", "path");
      polyEl.setAttribute("d", ramPath);
      polyEl.setAttribute("fill", "none");
      polyEl.setAttribute("stroke", "#2563eb");
      polyEl.setAttribute("stroke-width", "1.5");
      polyEl.setAttribute("stroke-linejoin", "round");
      polyEl.setAttribute("stroke-linecap", "round");
      svg.appendChild(polyEl);
    }

    // CPU line (orange), only when data is present (no all-null series)
    var cpuPath = polylinePath(cpuXs, cpuYs);
    if (cpuPath) {
      var cpuEl = document.createElementNS("http://www.w3.org/2000/svg", "path");
      cpuEl.setAttribute("d", cpuPath);
      cpuEl.setAttribute("fill", "none");
      cpuEl.setAttribute("stroke", "#d97706");
      cpuEl.setAttribute("stroke-width", "1.5");
      cpuEl.setAttribute("stroke-linejoin", "round");
      cpuEl.setAttribute("stroke-linecap", "round");
      svg.appendChild(cpuEl);
    }

    // Max / Ø labels injected into sibling [data-sparkline-labels]
    var labelsEl = svg.parentElement && svg.parentElement.querySelector("[data-sparkline-labels]");
    if (labelsEl) {
      var ramNums = ramRaw.filter(function (v) { return v != null && !isNaN(v); });
      var cpuNums = cpuRaw.filter(function (v) { return v != null && !isNaN(v); });
      var parts = [];
      if (ramNums.length > 0) {
        var ramMax = (Math.max.apply(null, ramNums) / 1024).toFixed(1);
        var ramAvg = (ramNums.reduce(function (a, b) { return a + b; }, 0) / ramNums.length / 1024).toFixed(1);
        parts.push("RAM Max " + ramMax + " GB · Ø " + ramAvg + " GB");
      }
      if (cpuNums.length > 0) {
        var cpuMax = Math.max.apply(null, cpuNums).toFixed(0);
        var cpuAvg = (cpuNums.reduce(function (a, b) { return a + b; }, 0) / cpuNums.length).toFixed(0);
        parts.push("CPU Max " + cpuMax + "% · Ø " + cpuAvg + "%");
      }
      labelsEl.textContent = parts.join(" · ");
    }
  }

  function initSparklines() {
    var svgs = document.querySelectorAll("svg[data-sparkline]");
    for (var i = 0; i < svgs.length; i++) {
      renderSparkline(svgs[i]);
    }
  }

  if (document.readyState === "loading") {
    document.addEventListener("DOMContentLoaded", initSparklines);
  } else {
    initSparklines();
  }
})();