{"spec_id":"line-styled","library":"d3","language":"javascript","code":"// anyplot.ai\n// line-styled: Styled Line Plot\n// Library: d3 7.9.0 | JavaScript 22.23.2\n// Quality: 91/100 | Created: 2026-09-05\n\nconst t = window.ANYPLOT_TOKENS;\nconst { width, height } = window.ANYPLOT_SIZE;\nconst margin = { top: 80, right: 220, bottom: 80, left: 90 };\nconst iw = width - margin.left - margin.right;\nconst ih = height - margin.top - margin.bottom;\n\n// --- Data (in-memory, deterministic) ----------------------------------------\n// Fixed-seed LCG — the browser has no seeded RNG.\nlet seed = 42;\nfunction rand() {\n  seed = (seed * 1664525 + 1013904223) % 4294967296;\n  return seed / 4294967296;\n}\n\nconst months = 36;\nconst series = [\n  { name: \"North America\", start: 82, drift: 0.55, noise: 2.2, dash: null },\n  { name: \"Europe\", start: 68, drift: 0.35, noise: 2.0, dash: \"8,6\" },\n  { name: \"Asia-Pacific\", start: 45, drift: 0.9, noise: 2.6, dash: \"1,7\" },\n  { name: \"Latin America\", start: 30, drift: 0.4, noise: 1.6, dash: \"10,5,3,5\" },\n];\n\nconst data = series.map((s) => {\n  let value = s.start;\n  const points = [];\n  for (let i = 0; i < months; i += 1) {\n    value += s.drift + (rand() - 0.5) * s.noise;\n    value = Math.max(5, value);\n    points.push({ month: i, value });\n  }\n  return { name: s.name, dash: s.dash, points };\n});\n\n// --- SVG mount ---------------------------------------------------------------\nconst svg = d3.select(\"#container\").append(\"svg\").attr(\"width\", width).attr(\"height\", height);\nconst g = svg.append(\"g\").attr(\"transform\", `translate(${margin.left},${margin.top})`);\n\n// --- Scales --------------------------------------------------------------------\nconst x = d3.scaleLinear().domain([0, months - 1]).range([0, iw]);\nconst y = d3\n  .scaleLinear()\n  .domain([0, d3.max(data, (s) => d3.max(s.points, (p) => p.value))])\n  .nice()\n  .range([ih, 0]);\n\n// --- Gridlines (y-axis only, subtle) --------------------------------------------\ng.append(\"g\")\n  .attr(\"class\", \"grid\")\n  .call(d3.axisLeft(y).tickSize(-iw).tickFormat(\"\"))\n  .call((sel) => sel.select(\".domain\").remove())\n  .selectAll(\"line\")\n  .attr(\"stroke\", t.grid);\n\n// --- Axes ------------------------------------------------------------------\nconst xAxis = g\n  .append(\"g\")\n  .attr(\"transform\", `translate(0,${ih})`)\n  .call(\n    d3\n      .axisBottom(x)\n      .ticks(9)\n      .tickFormat((d) => `M${d}`),\n  );\nconst yAxis = g.append(\"g\").call(d3.axisLeft(y).ticks(6).tickFormat(d3.format(\",.0f\")));\nfor (const ax of [xAxis, yAxis]) {\n  ax.selectAll(\"text\").attr(\"fill\", t.inkSoft).style(\"font-size\", \"14px\");\n  ax.selectAll(\"line\").attr(\"stroke\", t.grid);\n  ax.select(\".domain\").attr(\"stroke\", t.inkSoft);\n}\n\n// --- Axis labels -------------------------------------------------------------\ng.append(\"text\")\n  .attr(\"x\", iw / 2)\n  .attr(\"y\", ih + 56)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"16px\")\n  .text(\"Month\");\n\ng.append(\"text\")\n  .attr(\"transform\", \"rotate(-90)\")\n  .attr(\"x\", -ih / 2)\n  .attr(\"y\", -62)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"16px\")\n  .text(\"Shipment Volume (thousand units)\");\n\n// --- Lines — distinguished by style, not just color -----------------------------\nconst color = d3.scaleOrdinal().domain(series.map((s) => s.name)).range(t.palette);\nconst line = d3\n  .line()\n  .x((d) => x(d.month))\n  .y((d) => y(d.value))\n  .curve(d3.curveMonotoneX);\n\ng.selectAll(\".series-line\")\n  .data(data)\n  .join(\"path\")\n  .attr(\"class\", \"series-line\")\n  .attr(\"fill\", \"none\")\n  .attr(\"stroke\", (d) => color(d.name))\n  .attr(\"stroke-width\", 3.5)\n  .attr(\"stroke-dasharray\", (d) => d.dash)\n  .attr(\"stroke-linecap\", \"round\")\n  .attr(\"d\", (d) => line(d.points));\n\n// --- Data-driven focal annotation (storytelling) ----------------------------\n// Highlight whichever series actually grew the fastest (relative to its own\n// start), computed from the generated points rather than hard-coded.\nconst relativeGrowth = (d) =>\n  (d.points[d.points.length - 1].value - d.points[0].value) / d.points[0].value;\nconst focalSeries = d3.greatest(data, (d) => relativeGrowth(d));\nconst focal = { series: focalSeries, growth: relativeGrowth(focalSeries) };\nconst focalEnd = focal.series.points[focal.series.points.length - 1];\nconst focalColor = color(focal.series.name);\n\n// Soft halo behind the focal line only — draws the eye without changing its\n// stroke width, keeping \"consistent line width across styles\" intact.\ng.insert(\"path\", \".series-line\")\n  .attr(\"fill\", \"none\")\n  .attr(\"stroke\", focalColor)\n  .attr(\"stroke-opacity\", 0.18)\n  .attr(\"stroke-width\", 11)\n  .attr(\"d\", line(focal.series.points));\n\ng.append(\"circle\")\n  .attr(\"cx\", x(focalEnd.month))\n  .attr(\"cy\", y(focalEnd.value))\n  .attr(\"r\", 5)\n  .attr(\"fill\", focalColor);\n\nconst calloutX = iw * 0.14;\nconst calloutY = ih * 0.1;\ng.append(\"line\")\n  .attr(\"x1\", calloutX)\n  .attr(\"y1\", calloutY + 12)\n  .attr(\"x2\", x(focalEnd.month) - 8)\n  .attr(\"y2\", y(focalEnd.value))\n  .attr(\"stroke\", t.inkSoft)\n  .attr(\"stroke-width\", 1)\n  .attr(\"stroke-dasharray\", \"2,3\");\n\ng.append(\"text\")\n  .attr(\"x\", calloutX)\n  .attr(\"y\", calloutY)\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"15px\")\n  .style(\"font-weight\", \"600\")\n  .text(`Fastest growth: ${focal.series.name} +${Math.round(focal.growth * 100)}%`);\n\n// --- Legend (style + color mapping) --------------------------------------------\nconst legend = svg\n  .append(\"g\")\n  .attr(\"transform\", `translate(${margin.left + iw + 40},${margin.top + 20})`);\n\nconst legendRows = legend\n  .selectAll(\".legend-row\")\n  .data(data)\n  .join(\"g\")\n  .attr(\"class\", \"legend-row\")\n  .attr(\"transform\", (d, i) => `translate(0,${i * 44})`);\n\nlegendRows\n  .append(\"line\")\n  .attr(\"x1\", 0)\n  .attr(\"x2\", 40)\n  .attr(\"y1\", 0)\n  .attr(\"y2\", 0)\n  .attr(\"stroke\", (d) => color(d.name))\n  .attr(\"stroke-width\", 3.5)\n  .attr(\"stroke-dasharray\", (d) => d.dash)\n  .attr(\"stroke-linecap\", \"round\");\n\nlegendRows\n  .append(\"text\")\n  .attr(\"x\", 52)\n  .attr(\"y\", 5)\n  .attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"14px\")\n  .text((d) => d.name);\n\n// --- Title -------------------------------------------------------------------\nsvg\n  .append(\"text\")\n  .attr(\"x\", width / 2)\n  .attr(\"y\", 44)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"22px\")\n  .style(\"font-weight\", \"600\")\n  .text(\"line-styled · javascript · d3 · anyplot.ai\");\n"}