{"spec_id":"horizon-basic","library":"chartjs","language":"javascript","code":"// anyplot.ai\n// horizon-basic: Horizon Chart\n// Library: chartjs 4.4.7 | JavaScript 22.23.2\n// Quality: 88/100 | Created: 2026-08-18\n//# anyplot-orientation: landscape\n\n// Chart.js has no native \"horizon chart\" type, so this snippet builds one out\n// of core primitives: for each service row and each magnitude band, a small\n// transparent-background line chart is layered (via absolutely-positioned\n// canvases) with the y-scale clamped to that band's [min, max) window and\n// `fill: \"start\"`. A value inside the window fills proportionally; a value\n// below it collapses to zero height; a value above it saturates the full\n// band height (Chart.js/canvas clip the polygon at the row's pixel bounds).\n// Stacking bands from low-index to high-index (drawn in that order) produces\n// the classic \"fold and overlay\" horizon look with plain Chart.js Line charts\n// — no custom draw plugin, no other library.\n\nconst t = window.ANYPLOT_TOKENS;\nconst SIZE = window.ANYPLOT_SIZE;\n\n// --- Data (in-memory, deterministic) ----------------------------------------\n// Ten backend services; CPU deviation (percentage points) from a 50% baseline,\n// sampled every 5 minutes over a 10-hour window — 120 points per series.\nfunction lcg(seed) {\n  let s = seed >>> 0;\n  return () => {\n    s = (Math.imul(s, 1664525) + 1013904223) >>> 0;\n    return s / 4294967296;\n  };\n}\n\nconst SERVICES = [\n  \"api-gateway\",\n  \"auth-service\",\n  \"cache-primary\",\n  \"cache-replica\",\n  \"db-primary\",\n  \"db-replica\",\n  \"queue-worker\",\n  \"search-index\",\n  \"cdn-edge\",\n  \"websocket-gw\",\n];\nconst N_POINTS = 121; // 5-minute samples over a 10-hour window (00:00..10:00 inclusive)\n\nconst labels = Array.from({ length: N_POINTS }, (_, i) => {\n  const totalMin = i * 5;\n  const h = String(Math.floor(totalMin / 60)).padStart(2, \"0\");\n  const m = String(totalMin % 60).padStart(2, \"0\");\n  return `${h}:${m}`;\n});\n\nconst series = SERVICES.map((name, k) => {\n  const rnd = lcg(1000 + k * 37);\n  const amplitude = 15 + (k % 4) * 5;\n  const period = 30 + (k % 3) * 10;\n  const phase = k * 0.7;\n  const values = Array.from({ length: N_POINTS }, (_, i) => {\n    const wave = amplitude * Math.sin((2 * Math.PI * i) / period + phase);\n    const noise = (rnd() - 0.5) * 10;\n    const spike = rnd() < 0.04 ? (rnd() < 0.5 ? 1 : -1) * amplitude * 1.4 : 0;\n    return wave + noise + spike;\n  });\n  const peak = Math.max(...values.map(Math.abs));\n  return { name, values, peak };\n});\n\n// Rows are ordered by peak magnitude (most volatile first) instead of the\n// data-generation order — gives the dense 10-row layout a clear focal point\n// at the top instead of leaving every row visually equal-weight.\nconst orderedSeries = [...series].sort((a, b) => b.peak - a.peak);\n\n// --- Band color ramps (Imprint diverging, matching spec's \"blue up / red down\") ---\nconst N_BANDS = 3; // per polarity — 6 bands total, within the spec's \"2-4 bands\" note\nconst BAND_HEIGHT = 9; // deviation points per band\n\nfunction hexToRgb(hex) {\n  const n = parseInt(hex.slice(1), 16);\n  return [(n >> 16) & 255, (n >> 8) & 255, n & 255];\n}\nfunction mix(hexA, hexB, frac) {\n  const a = hexToRgb(hexA);\n  const b = hexToRgb(hexB);\n  const c = a.map((v, i) => Math.round(v + (b[i] - v) * frac));\n  return `rgb(${c[0]}, ${c[1]}, ${c[2]})`;\n}\n\nconst posColors = Array.from({ length: N_BANDS }, (_, i) => mix(t.pageBg, t.div[2], (i + 1) / N_BANDS));\nconst negColors = Array.from({ length: N_BANDS }, (_, i) => mix(t.pageBg, t.div[0], (i + 1) / N_BANDS));\n\n// --- Layout ------------------------------------------------------------------\n// Append a wrapper instead of restyling #container directly — the harness sets\n// #container's own inline width/height/position, which a `cssText` overwrite\n// would clobber and collapse the mount to its content height.\nconst root = document.createElement(\"div\");\nroot.style.cssText = `box-sizing:border-box; width:100%; height:100%; padding:26px 34px 20px 34px; display:flex; flex-direction:column; font-family:inherit;`;\ndocument.getElementById(\"container\").appendChild(root);\n\nconst title = document.createElement(\"div\");\ntitle.textContent = \"horizon-basic · javascript · chartjs · anyplot.ai\";\ntitle.style.cssText = `color:${t.ink}; font-size:22px; font-weight:600; margin-bottom:5px;`;\nroot.appendChild(title);\n\nconst subtitle = document.createElement(\"div\");\nsubtitle.textContent = \"CPU deviation from 50% baseline · 10 services · 5-minute samples over 10 hours\";\nsubtitle.style.cssText = `color:${t.inkSoft}; font-size:16px; margin-bottom:12px;`;\nroot.appendChild(subtitle);\n\n// Small legend explaining the folded color-intensity bands.\nfunction legendGroup(label, colors) {\n  const wrap = document.createElement(\"div\");\n  wrap.style.cssText = \"display:flex; align-items:center; gap:8px;\";\n  const text = document.createElement(\"span\");\n  text.textContent = label;\n  wrap.appendChild(text);\n  const bar = document.createElement(\"div\");\n  bar.style.cssText = \"display:flex; height:16px; border-radius:2px; overflow:hidden;\";\n  colors.forEach((c) => {\n    const seg = document.createElement(\"div\");\n    seg.style.cssText = `width:26px; height:100%; background:${c};`;\n    bar.appendChild(seg);\n  });\n  wrap.appendChild(bar);\n  return wrap;\n}\nconst legend = document.createElement(\"div\");\nlegend.style.cssText = `display:flex; align-items:center; gap:24px; margin-bottom:14px; font-size:15px; color:${t.inkSoft};`;\nlegend.appendChild(legendGroup(\"above baseline, increasing →\", posColors));\nlegend.appendChild(legendGroup(\"below baseline, increasing →\", negColors));\nroot.appendChild(legend);\n\nconst LABEL_WIDTH = 160;\n\nconst rowsWrap = document.createElement(\"div\");\nrowsWrap.style.cssText = \"display:flex; flex-direction:column; flex:1 1 auto; min-height:0; gap:5px;\";\nroot.appendChild(rowsWrap);\n\nfunction makeBandLayer(parent, values, bandIndex, color) {\n  const canvas = document.createElement(\"canvas\");\n  canvas.style.cssText = \"position:absolute; inset:0; width:100%; height:100%;\";\n  parent.appendChild(canvas);\n  new Chart(canvas, {\n    type: \"line\",\n    data: {\n      labels,\n      datasets: [\n        {\n          data: values,\n          borderWidth: 0,\n          fill: \"start\",\n          backgroundColor: color,\n          pointRadius: 0,\n          tension: 0,\n        },\n      ],\n    },\n    options: {\n      responsive: true,\n      maintainAspectRatio: false,\n      animation: false,\n      layout: { padding: 0 },\n      plugins: { legend: { display: false }, title: { display: false }, tooltip: { display: false } },\n      scales: {\n        x: { type: \"category\", offset: false, display: false, grid: { display: false } },\n        y: {\n          min: bandIndex * BAND_HEIGHT,\n          max: (bandIndex + 1) * BAND_HEIGHT,\n          display: false,\n          grid: { display: false },\n        },\n      },\n    },\n  });\n}\n\norderedSeries.forEach((s) => {\n  const row = document.createElement(\"div\");\n  row.style.cssText = \"display:flex; align-items:stretch; flex:1 1 auto; min-height:0;\";\n\n  const label = document.createElement(\"div\");\n  label.textContent = s.name;\n  label.style.cssText = `flex:0 0 ${LABEL_WIDTH}px; width:${LABEL_WIDTH}px; display:flex; align-items:center; justify-content:flex-end; color:${t.ink}; font-size:16px; padding-right:12px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;`;\n  row.appendChild(label);\n\n  const plotWrap = document.createElement(\"div\");\n  plotWrap.style.cssText = `position:relative; flex:1 1 auto; min-width:0; background:${t.grid}; border-radius:2px;`;\n  row.appendChild(plotWrap);\n\n  const posValues = s.values.map((v) => Math.max(v, 0));\n  const negValues = s.values.map((v) => Math.max(-v, 0));\n\n  for (let i = 0; i < N_BANDS; i++) makeBandLayer(plotWrap, posValues, i, posColors[i]);\n  for (let i = 0; i < N_BANDS; i++) makeBandLayer(plotWrap, negValues, i, negColors[i]);\n\n  rowsWrap.appendChild(row);\n});\n\n// --- X-axis time labels (shared across all rows, drawn once at the bottom) --\nconst axisRow = document.createElement(\"div\");\naxisRow.style.cssText = \"display:flex; margin-top:6px;\";\nconst axisSpacer = document.createElement(\"div\");\naxisSpacer.style.cssText = `flex:0 0 ${LABEL_WIDTH}px;`;\naxisRow.appendChild(axisSpacer);\nconst axisLabels = document.createElement(\"div\");\naxisLabels.style.cssText = `flex:1 1 auto; display:flex; justify-content:space-between; color:${t.inkSoft}; font-size:15px;`;\nconst N_TICKS = 6;\nfor (let i = 0; i < N_TICKS; i++) {\n  const idx = Math.round((i / (N_TICKS - 1)) * (N_POINTS - 1));\n  const span = document.createElement(\"span\");\n  span.textContent = labels[idx];\n  axisLabels.appendChild(span);\n}\naxisRow.appendChild(axisLabels);\nroot.appendChild(axisRow);\n"}