{"spec_id":"sparkline-basic","library":"echarts","language":"javascript","code":"// anyplot.ai\n// sparkline-basic: Basic Sparkline\n// Library: echarts 6.1.0 | JavaScript 22.23.2\n// Quality: 89/100 | Created: 2026-09-09\n\n//# anyplot-orientation: landscape\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data: six KPI trend series (deterministic random walks) ---------------\n// Tiny fixed-seed LCG — the browser has no seeded Math.random.\nlet seed = 42;\nconst lcg = () => {\n  seed = (seed * 1664525 + 1013904223) % 4294967296;\n  return seed / 4294967296;\n};\n\nconst POINTS = 26;\n// One shared walk generator (rather than inlining the loop per metric) avoids\n// repeating the same random-walk math six times — DRY across metrics, not the\n// default \"no functions\" style.\nconst walk = (start, drift, volatility) => {\n  const series = [start];\n  for (let i = 1; i < POINTS; i += 1) {\n    series.push(Math.max(series[i - 1] + drift + (lcg() - 0.5) * volatility, 0));\n  }\n  return series;\n};\n\n// goodWhenUp: whether an upward trend is a positive signal for this metric.\nconst metrics = [\n  { label: \"Revenue\", data: walk(180, 3.2, 10), goodWhenUp: true, format: (v) => `$${Math.round(v)}K` },\n  { label: \"Active Users\", data: walk(38, 0.45, 2.5), goodWhenUp: true, format: (v) => `${v.toFixed(1)}K` },\n  { label: \"Server Latency\", data: walk(180, 1.6, 9), goodWhenUp: false, format: (v) => `${Math.round(v)}ms` },\n  { label: \"Conversion Rate\", data: walk(2.8, 0.02, 0.25), goodWhenUp: true, format: (v) => `${v.toFixed(2)}%` },\n  { label: \"Churn Rate\", data: walk(4.2, 0.045, 0.3), goodWhenUp: false, format: (v) => `${v.toFixed(2)}%` },\n  { label: \"New Signups\", data: walk(260, 3.8, 22), goodWhenUp: true, format: (v) => `${Math.round(v)}` },\n];\n\n// Trend direction drives color: green (Imprint brand) for a positive signal,\n// matte red for a negative one — the finance up/down semantic exception.\nconst cards = metrics.map((m) => {\n  const first = m.data[0];\n  const last = m.data[m.data.length - 1];\n  const deltaPct = ((last - first) / first) * 100;\n  const trendUp = deltaPct >= 0;\n  const good = m.goodWhenUp === trendUp;\n  return { ...m, last, deltaPct, trendUp, color: good ? t.palette[0] : t.palette[4] };\n});\n\n// --- Layout: a 2x3 grid of KPI tiles, each an independent mini sparkline ---\n// Positions are in the harness's CSS mount space (1600x900 for landscape,\n// scaled 2x to the 3200x1800 output) — see window.ANYPLOT_SIZE.\nconst SIZE = window.ANYPLOT_SIZE;\nconst COLS = 3;\nconst ROWS = 2;\nconst MARGIN = 50;\nconst GAP = 30;\nconst CONTENT_TOP = 110;\nconst CONTENT_BOTTOM = 50;\nconst CARD_W = (SIZE.width - 2 * MARGIN - (COLS - 1) * GAP) / COLS;\nconst CARD_H = (SIZE.height - CONTENT_TOP - CONTENT_BOTTOM - (ROWS - 1) * GAP) / ROWS;\nconst HEADER_H = 70; // label + value + trend text block above each sparkline\nconst SPARK_RATIO = 5; // width:height for the line itself — spec calls for 4:1-8:1\n\nconst grid = [];\nconst xAxis = [];\nconst yAxis = [];\nconst series = [];\nconst graphic = [];\n\ncards.forEach((card, i) => {\n  const row = Math.floor(i / COLS);\n  const col = i % COLS;\n  const x0 = MARGIN + col * (CARD_W + GAP);\n  const y0 = CONTENT_TOP + row * (CARD_H + GAP);\n  // Keep the sparkline itself compact (per SPARK_RATIO) and center it, with\n  // the freed vertical space as padding, in the space below the text header.\n  const sparkHeight = (CARD_W - 8) / SPARK_RATIO;\n  const sparkTop = y0 + HEADER_H + (CARD_H - HEADER_H - sparkHeight) / 2;\n\n  graphic.push(\n    {\n      type: \"text\",\n      left: x0,\n      top: y0,\n      style: { text: card.label, fontSize: 14, fill: t.inkSoft },\n    },\n    {\n      // `right` anchors the text's own right edge — `left` + textAlign:\"right\"\n      // does not work for auto-sized graphic text (the box == the glyph run).\n      type: \"text\",\n      right: SIZE.width - (x0 + CARD_W),\n      top: y0 - 4,\n      style: { text: card.format(card.last), fontSize: 27, fontWeight: \"bold\", fill: t.ink },\n    },\n    {\n      type: \"text\",\n      right: SIZE.width - (x0 + CARD_W),\n      top: y0 + 32,\n      style: {\n        text: `${card.trendUp ? \"▲\" : \"▼\"} ${Math.abs(card.deltaPct).toFixed(1)}%`,\n        fontSize: 15,\n        fill: card.color,\n      },\n    },\n  );\n\n  grid.push({ left: x0 + 4, top: sparkTop, width: CARD_W - 8, height: sparkHeight });\n  xAxis.push({\n    gridIndex: i,\n    type: \"category\",\n    data: card.data.map((_, idx) => idx),\n    boundaryGap: false,\n    show: false,\n  });\n  yAxis.push({ gridIndex: i, type: \"value\", scale: true, show: false, splitLine: { show: false } });\n  series.push({\n    type: \"line\",\n    xAxisIndex: i,\n    yAxisIndex: i,\n    data: card.data,\n    symbol: \"none\",\n    smooth: 0.25,\n    lineStyle: { width: 1.8, color: card.color },\n    areaStyle: { color: card.color, opacity: 0.14 },\n    markPoint: {\n      symbol: \"circle\",\n      symbolSize: 9,\n      label: { show: false },\n      itemStyle: { color: card.color, borderColor: t.pageBg, borderWidth: 1.5 },\n      data: [\n        { coord: [POINTS - 1, card.last] }, // latest value: filled dot\n        // series max/min: hollow rings, a lighter secondary emphasis\n        { type: \"max\", symbolSize: 6, itemStyle: { color: t.pageBg, borderColor: card.color, borderWidth: 1.5 } },\n        { type: \"min\", symbolSize: 6, itemStyle: { color: t.pageBg, borderColor: card.color, borderWidth: 1.5 } },\n      ],\n    },\n  });\n});\n\n// --- Init ---------------------------------------------------------------\nconst chart = echarts.init(document.getElementById(\"container\"));\n\n// --- Option ---------------------------------------------------------------\nchart.setOption({\n  animation: false,\n  color: t.palette,\n  backgroundColor: \"transparent\",\n  title: {\n    text: \"sparkline-basic · javascript · echarts · anyplot.ai\",\n    left: \"center\",\n    top: 40,\n    textStyle: { color: t.ink, fontSize: 28 },\n  },\n  grid,\n  xAxis,\n  yAxis,\n  series,\n  graphic,\n});\n"}