{"spec_id":"dashboard-metrics-tiles","library":"d3","language":"javascript","code":"// anyplot.ai\n// dashboard-metrics-tiles: Real-Time Dashboard Tiles\n// Library: d3 7.9.0 | JavaScript 22.23.2\n// Quality: 91/100 | Updated: 2026-09-02\n\nconst t = window.ANYPLOT_TOKENS;\nconst { width, height } = window.ANYPLOT_SIZE;\n\nconst GOOD = t.palette[0]; // #009E73 — brand green, also \"favorable / good\" anchor\nconst WARNING = t.amber; // #DDCC77\nconst CRITICAL = t.palette[4]; // #AE3030 — matte red, semantic anchor for bad/error\nconst STATUS_COLOR = { good: GOOD, warning: WARNING, critical: CRITICAL };\n// Shape-coded per status (circle/triangle/square) so meaning survives without color — CVD-safe.\nconst STATUS_SYMBOL = {\n  good: d3.symbolCircle,\n  warning: d3.symbolTriangle,\n  critical: d3.symbolSquare,\n};\n\n// --- Data (in-memory, deterministic) ----------------------------------------\n// A snapshot of an operations dashboard: current value + recent history per\n// metric. `favorable` says whether this metric's change direction is good\n// news (independent of `status`, which reflects the current absolute level).\nconst metrics = [\n  {\n    name: \"CPU Usage\",\n    value: 45,\n    unit: \"%\",\n    history: [58, 55, 60, 52, 54, 49, 51, 47, 50, 46, 48, 45, 47, 45],\n    changePercent: -5.2,\n    favorable: true,\n    status: \"good\",\n  },\n  {\n    name: \"Memory\",\n    value: 72,\n    unit: \"%\",\n    history: [58, 60, 62, 61, 64, 66, 65, 68, 67, 70, 69, 71, 70, 72],\n    changePercent: 8.1,\n    favorable: false,\n    status: \"warning\",\n  },\n  {\n    name: \"Response Time\",\n    value: 120,\n    unit: \"ms\",\n    history: [\n      155, 150, 148, 142, 138, 135, 130, 128, 125, 122, 124, 121, 118, 120,\n    ],\n    changePercent: -15.3,\n    favorable: true,\n    status: \"good\",\n  },\n  {\n    name: \"Error Rate\",\n    value: 0.8,\n    unit: \"%\",\n    history: [\n      0.5, 0.6, 0.5, 0.7, 0.6, 0.8, 0.7, 0.9, 0.7, 0.8, 0.9, 0.7, 0.8, 0.8,\n    ],\n    changePercent: 2.1,\n    favorable: false,\n    status: \"warning\",\n  },\n  {\n    name: \"Throughput\",\n    value: 1240,\n    unit: \"req/s\",\n    history: [\n      980, 1010, 1040, 1020, 1080, 1100, 1090, 1150, 1130, 1180, 1200, 1190,\n      1220, 1240,\n    ],\n    changePercent: 12.4,\n    favorable: true,\n    status: \"good\",\n  },\n  {\n    name: \"Disk I/O\",\n    value: 89,\n    unit: \"%\",\n    history: [65, 68, 70, 72, 75, 74, 78, 80, 82, 81, 85, 84, 87, 89],\n    changePercent: 18.7,\n    favorable: false,\n    status: \"critical\",\n  },\n];\n\nconst formatValue = (m) =>\n  m.unit === \"req/s\" ? d3.format(\",\")(m.value) : String(m.value);\n\n// --- Grid geometry ------------------------------------------------------------\nconst cols = 3;\nconst rows = 2;\nconst outerMargin = { top: 112, right: 40, bottom: 40, left: 40 };\nconst gap = 24;\nconst gridW = width - outerMargin.left - outerMargin.right;\nconst gridH = height - outerMargin.top - outerMargin.bottom;\nconst tileW = (gridW - gap * (cols - 1)) / cols;\nconst tileH = (gridH - gap * (rows - 1)) / rows;\n\nconst accentW = 6;\nconst padLeft = 22;\nconst padRight = 22;\nconst padTop = 24;\nconst padBottom = 20;\nconst contentX = accentW + padLeft;\nconst contentW = tileW - contentX - padRight;\n\n// --- SVG mount ----------------------------------------------------------------\nconst svg = d3\n  .select(\"#container\")\n  .append(\"svg\")\n  .attr(\"width\", width)\n  .attr(\"height\", height);\n\n// --- Title ---------------------------------------------------------------------\nsvg\n  .append(\"text\")\n  .attr(\"x\", width / 2)\n  .attr(\"y\", 48)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"26px\")\n  .style(\"font-weight\", \"600\")\n  .text(\"dashboard-metrics-tiles · javascript · d3 · anyplot.ai\");\n\n// --- Status legend --------------------------------------------------------------\nconst legend = [\n  { label: \"Good\", color: GOOD, symbol: STATUS_SYMBOL.good },\n  { label: \"Warning\", color: WARNING, symbol: STATUS_SYMBOL.warning },\n  { label: \"Critical\", color: CRITICAL, symbol: STATUS_SYMBOL.critical },\n];\nconst legendItemW = 110;\nconst legendW = legend.length * legendItemW;\nconst legendG = svg\n  .append(\"g\")\n  .attr(\"transform\", `translate(${(width - legendW) / 2}, 84)`);\nconst legendItems = legendG\n  .selectAll(\"g\")\n  .data(legend)\n  .join(\"g\")\n  .attr(\"transform\", (d, i) => `translate(${i * legendItemW}, 0)`);\nlegendItems\n  .append(\"path\")\n  .attr(\"transform\", \"translate(6,0)\")\n  .attr(\"d\", (d) => d3.symbol().type(d.symbol).size(110)())\n  .attr(\"fill\", (d) => d.color);\nlegendItems\n  .append(\"text\")\n  .attr(\"x\", 18)\n  .attr(\"y\", 5)\n  .attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"15px\")\n  .text((d) => d.label);\n\n// --- Tiles ----------------------------------------------------------------------\nconst tiles = svg\n  .selectAll(\"g.tile\")\n  .data(metrics)\n  .join(\"g\")\n  .attr(\"class\", \"tile\")\n  .attr(\"transform\", (d, i) => {\n    const col = i % cols;\n    const row = Math.floor(i / cols);\n    const x = outerMargin.left + col * (tileW + gap);\n    const y = outerMargin.top + row * (tileH + gap);\n    return `translate(${x},${y})`;\n  });\n\n// The critical-status tile gets a tinted surface + stronger border so the eye\n// lands there first — a deliberate visual-weight difference, not a size change\n// (tile footprint stays identical, preserving the uniform grid).\ntiles\n  .append(\"rect\")\n  .attr(\"width\", tileW)\n  .attr(\"height\", tileH)\n  .attr(\"rx\", 14)\n  .attr(\"fill\", (d) =>\n    d.status === \"critical\"\n      ? d3.interpolateRgb(t.elevatedBg, CRITICAL)(0.06)\n      : t.elevatedBg,\n  )\n  .attr(\"stroke\", (d) => (d.status === \"critical\" ? CRITICAL : t.grid))\n  .attr(\"stroke-width\", (d) => (d.status === \"critical\" ? 2.5 : 1));\n\ntiles\n  .append(\"rect\")\n  .attr(\"x\", 0)\n  .attr(\"width\", (d) => (d.status === \"critical\" ? accentW + 4 : accentW))\n  .attr(\"height\", tileH)\n  .attr(\"rx\", 3)\n  .attr(\"fill\", (d) => STATUS_COLOR[d.status]);\n\n// Status icon — shape-coded (circle/triangle/square), redundant with the\n// accent-bar color so status still reads for color-vision-deficient viewers (VQ-04).\nconst labelY = padTop + 18;\ntiles\n  .append(\"path\")\n  .attr(\"transform\", (d) => `translate(${contentX + 7},${labelY - 6})`)\n  .attr(\"d\", (d) => d3.symbol().type(STATUS_SYMBOL[d.status]).size(90)())\n  .attr(\"fill\", (d) => STATUS_COLOR[d.status]);\n\n// Metric label\ntiles\n  .append(\"text\")\n  .attr(\"x\", contentX + 20)\n  .attr(\"y\", labelY)\n  .attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"19px\")\n  .text((d) => d.name);\n\n// Change indicator — arrow direction from sign, color from favorability\nconst changeGroup = tiles\n  .append(\"g\")\n  .attr(\"transform\", `translate(0,${labelY})`);\nchangeGroup\n  .append(\"text\")\n  .attr(\"x\", tileW - padRight)\n  .attr(\"y\", 0)\n  .attr(\"text-anchor\", \"end\")\n  .attr(\"fill\", (d) => (d.favorable ? GOOD : CRITICAL))\n  .style(\"font-size\", \"20px\")\n  .style(\"font-weight\", \"700\")\n  .text(\n    (d) =>\n      `${d.changePercent >= 0 ? \"▲\" : \"▼\"} ${Math.abs(d.changePercent).toFixed(1)}%`,\n  );\n\n// Value display (big number + unit)\nconst valueBaseline = padTop + 18 + 74;\nconst valueText = tiles\n  .append(\"text\")\n  .attr(\"x\", contentX)\n  .attr(\"y\", valueBaseline)\n  .attr(\"fill\", t.ink)\n  .style(\"font-weight\", \"700\");\nvalueText\n  .append(\"tspan\")\n  .style(\"font-size\", \"54px\")\n  .text((d) => formatValue(d));\nvalueText\n  .append(\"tspan\")\n  .attr(\"dx\", 8)\n  .style(\"font-size\", \"22px\")\n  .style(\"font-weight\", \"500\")\n  .attr(\"fill\", t.inkSoft)\n  .text((d) => d.unit);\n\n// --- Sparklines -------------------------------------------------------------\nconst sparkTop = valueBaseline + 26;\nconst sparkBottom = tileH - padBottom;\nconst sparkH = sparkBottom - sparkTop;\n\ntiles.each(function (d) {\n  const tile = d3.select(this);\n  const x = d3\n    .scaleLinear()\n    .domain([0, d.history.length - 1])\n    .range([0, contentW]);\n  const yMin = d3.min(d.history);\n  const yMax = d3.max(d.history);\n  const pad = (yMax - yMin) * 0.15 || 1;\n  const y = d3\n    .scaleLinear()\n    .domain([yMin - pad, yMax + pad])\n    .range([sparkH, 0]);\n  const color = STATUS_COLOR[d.status];\n\n  const area = d3\n    .area()\n    .x((v, i) => x(i))\n    .y0(sparkH)\n    .y1((v) => y(v))\n    .curve(d3.curveMonotoneX);\n  const line = d3\n    .line()\n    .x((v, i) => x(i))\n    .y((v) => y(v))\n    .curve(d3.curveMonotoneX);\n\n  const spark = tile\n    .append(\"g\")\n    .attr(\"transform\", `translate(${contentX},${sparkTop})`);\n  spark\n    .append(\"path\")\n    .datum(d.history)\n    .attr(\"d\", area)\n    .attr(\"fill\", color)\n    .attr(\"opacity\", 0.15);\n  spark\n    .append(\"path\")\n    .datum(d.history)\n    .attr(\"d\", line)\n    .attr(\"fill\", \"none\")\n    .attr(\"stroke\", color)\n    .attr(\"stroke-width\", 2.5);\n  spark\n    .append(\"circle\")\n    .attr(\"cx\", x(d.history.length - 1))\n    .attr(\"cy\", y(d.history[d.history.length - 1]))\n    .attr(\"r\", 5.5)\n    .attr(\"fill\", color)\n    .attr(\"stroke\", t.elevatedBg)\n    .attr(\"stroke-width\", 2);\n});\n"}