{"spec_id":"line-markers","library":"d3","language":"javascript","code":"// anyplot.ai\n// line-markers: Line Plot with Markers\n// Library: d3 7.9.0 | JavaScript 22.23.2\n// Quality: 88/100 | Created: 2026-09-05\n\nconst t = window.ANYPLOT_TOKENS;\nconst { width, height } = window.ANYPLOT_SIZE;\nconst margin = { top: 110, right: 284, bottom: 110, left: 120 };\nconst iw = width - margin.left - margin.right;\nconst ih = height - margin.top - margin.bottom;\n\n// --- Data: enzyme activity across a pH gradient, three variants -----------\n// Sparse discrete measurements (13 pH readings per enzyme) where each point\n// is a meaningful lab observation, not a dense sampled curve.\nfunction lcg(seed) {\n  let state = seed;\n  return () => {\n    state = (state * 1664525 + 1013904223) % 4294967296;\n    return state / 4294967296;\n  };\n}\nconst rand = lcg(42);\n\nconst phLevels = [4.0, 4.5, 5.0, 5.5, 6.0, 6.5, 7.0, 7.5, 8.0, 8.5, 9.0, 9.5, 10.0];\n\nfunction bellActivity(ph, optimum, sigma, peak) {\n  const gaussian = peak * Math.exp(-((ph - optimum) ** 2) / (2 * sigma ** 2));\n  const noise = (rand() - 0.5) * 4;\n  return Math.max(0, gaussian + noise);\n}\n\nconst enzymes = [\n  {\n    name: \"Pepsin\",\n    optimum: 5.0,\n    sigma: 1.3,\n    peak: 92,\n    color: t.palette[0],\n    symbol: d3.symbolCircle,\n  },\n  {\n    name: \"Trypsin\",\n    optimum: 7.5,\n    sigma: 1.1,\n    peak: 88,\n    color: t.palette[1],\n    symbol: d3.symbolSquare,\n  },\n  {\n    name: \"Alkaline phosphatase\",\n    optimum: 9.5,\n    sigma: 1.4,\n    peak: 96,\n    color: t.palette[2],\n    symbol: d3.symbolTriangle,\n  },\n];\n\nconst series = enzymes.map((e) => ({\n  ...e,\n  values: phLevels.map((ph) => ({ ph, activity: bellActivity(ph, e.optimum, e.sigma, e.peak) })),\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([4.0, 10.0]).range([0, iw]);\nconst y = d3\n  .scaleLinear()\n  .domain([0, d3.max(series, (s) => d3.max(s.values, (v) => v.activity)) * 1.1])\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  .attr(\"stroke-opacity\", 0.15);\n\n// --- Axes -----------------------------------------------------------------\nconst xAxis = g\n  .append(\"g\")\n  .attr(\"transform\", `translate(0,${ih})`)\n  .call(d3.axisBottom(x).tickValues([4, 5, 6, 7, 8, 9, 10]).tickFormat((d) => d.toFixed(0)));\nconst yAxis = g.append(\"g\").call(d3.axisLeft(y));\nfor (const ax of [xAxis, yAxis]) {\n  ax.selectAll(\"text\").attr(\"fill\", t.inkSoft).style(\"font-size\", \"16px\");\n  ax.selectAll(\"line\").attr(\"stroke\", t.inkSoft);\n  ax.select(\".domain\").attr(\"stroke\", t.inkSoft);\n}\n\n// --- Lines + markers per series -----------------------------------------------\nconst line = d3\n  .line()\n  .x((d) => x(d.ph))\n  .y((d) => y(d.activity));\n\nfor (const s of series) {\n  const peak = s.values.reduce((a, b) => (b.activity > a.activity ? b : a));\n\n  // Soft halo behind the optimum point gives each curve a clear focal point.\n  g.append(\"circle\")\n    .attr(\"cx\", x(peak.ph))\n    .attr(\"cy\", y(peak.activity))\n    .attr(\"r\", 26)\n    .attr(\"fill\", s.color)\n    .attr(\"opacity\", 0.16);\n\n  g.append(\"path\")\n    .datum(s.values)\n    .attr(\"fill\", \"none\")\n    .attr(\"stroke\", s.color)\n    .attr(\"stroke-width\", 3)\n    .attr(\"d\", line);\n\n  g.selectAll(`.marker-${s.name.replace(/\\s/g, \"\")}`)\n    .data(s.values)\n    .join(\"path\")\n    .attr(\"d\", (d) => d3.symbol().type(s.symbol).size(d === peak ? 460 : 260)())\n    .attr(\"transform\", (d) => `translate(${x(d.ph)},${y(d.activity)})`)\n    .attr(\"fill\", s.color)\n    .attr(\"stroke\", t.pageBg)\n    .attr(\"stroke-width\", 2);\n}\n\n// --- Axis labels ------------------------------------------------------------\ng.append(\"text\")\n  .attr(\"x\", iw / 2)\n  .attr(\"y\", ih + 64)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"18px\")\n  .text(\"pH Level\");\n\ng.append(\"text\")\n  .attr(\"transform\", \"rotate(-90)\")\n  .attr(\"x\", -ih / 2)\n  .attr(\"y\", -84)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"18px\")\n  .text(\"Enzyme Activity (%)\");\n\n// --- Legend (symbol + color + line swatch per series) -------------------------\nconst legend = svg\n  .append(\"g\")\n  .attr(\"transform\", `translate(${margin.left + iw + 40},${margin.top + 20})`);\n\nseries.forEach((s, i) => {\n  const row = legend.append(\"g\").attr(\"transform\", `translate(0,${i * 56})`);\n  row\n    .append(\"line\")\n    .attr(\"x1\", 0)\n    .attr(\"x2\", 60)\n    .attr(\"y1\", 0)\n    .attr(\"y2\", 0)\n    .attr(\"stroke\", s.color)\n    .attr(\"stroke-width\", 3);\n  row\n    .append(\"path\")\n    .attr(\"d\", d3.symbol().type(s.symbol).size(260)())\n    .attr(\"transform\", \"translate(30,0)\")\n    .attr(\"fill\", s.color)\n    .attr(\"stroke\", t.pageBg)\n    .attr(\"stroke-width\", 2);\n  row\n    .append(\"text\")\n    .attr(\"x\", 76)\n    .attr(\"y\", 0)\n    .attr(\"dominant-baseline\", \"middle\")\n    .attr(\"fill\", t.inkSoft)\n    .style(\"font-size\", \"16px\")\n    .text(s.name);\n});\n\n// --- Title --------------------------------------------------------------------\nsvg\n  .append(\"text\")\n  .attr(\"x\", width / 2)\n  .attr(\"y\", 56)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"22px\")\n  .style(\"font-weight\", \"600\")\n  .text(\"line-markers · javascript · d3 · anyplot.ai\");\n"}