{"spec_id":"timeline-basic","library":"d3","language":"javascript","code":"// anyplot.ai\n// timeline-basic: Event Timeline\n// Library: d3 7.9.0 | JavaScript 22.23.2\n// Quality: 91/100 | Created: 2026-09-09\n\nconst t = window.ANYPLOT_TOKENS;\nconst { width, height } = window.ANYPLOT_SIZE;\nconst margin = { left: 70, right: 70 };\nconst centerY = height / 2;\nconst iw = width - margin.left - margin.right;\n\n// --- Data: product roadmap milestones (in-memory, deterministic) -----------\n// `major: true` flags flagship releases that get an emphasized diamond marker,\n// bolder label, and a longer stem — giving the timeline a focal hierarchy\n// instead of a uniform run of same-weight events.\nconst categories = [\"Platform\", \"Mobile\", \"Analytics\", \"Security\"];\nconst events = [\n  {\n    date: new Date(2025, 0, 15),\n    name: \"Platform Beta Launch\",\n    category: \"Platform\",\n    major: true,\n  },\n  {\n    date: new Date(2025, 1, 3),\n    name: \"Design System v1\",\n    category: \"Platform\",\n  },\n  { date: new Date(2025, 1, 20), name: \"Mobile App v1.0\", category: \"Mobile\" },\n  {\n    date: new Date(2025, 2, 10),\n    name: \"Analytics Dashboard\",\n    category: \"Analytics\",\n  },\n  {\n    date: new Date(2025, 3, 5),\n    name: \"SOC 2 Certification\",\n    category: \"Security\",\n  },\n  {\n    date: new Date(2025, 4, 18),\n    name: \"API v2 Release\",\n    category: \"Platform\",\n    major: true,\n  },\n  { date: new Date(2025, 5, 2), name: \"Webhook Support\", category: \"Platform\" },\n  { date: new Date(2025, 5, 30), name: \"Offline Mode\", category: \"Mobile\" },\n  {\n    date: new Date(2025, 7, 12),\n    name: \"Predictive Insights\",\n    category: \"Analytics\",\n    major: true,\n  },\n  {\n    date: new Date(2025, 7, 28),\n    name: \"Custom Reports\",\n    category: \"Analytics\",\n  },\n  {\n    date: new Date(2025, 8, 22),\n    name: \"SSO Integration\",\n    category: \"Security\",\n  },\n  {\n    date: new Date(2025, 9, 30),\n    name: \"Platform GA Release\",\n    category: \"Platform\",\n    major: true,\n  },\n  {\n    date: new Date(2025, 10, 5),\n    name: \"Cross-Platform Sync\",\n    category: \"Mobile\",\n  },\n  {\n    date: new Date(2025, 11, 15),\n    name: \"Zero-Trust Rollout\",\n    category: \"Security\",\n  },\n];\n\nconst color = d3.scaleOrdinal().domain(categories).range(t.palette);\nconst formatDate = d3.timeFormat(\"%b %Y\");\n\n// --- Scale -------------------------------------------------------------------\nconst [minDate, maxDate] = d3.extent(events, (d) => d.date);\nconst pad = (maxDate - minDate) * 0.08;\nconst x = d3\n  .scaleTime()\n  .domain([new Date(+minDate - pad), new Date(+maxDate + pad)])\n  .range([0, iw]);\n\n// --- SVG mount ----------------------------------------------------------------\nconst svg = d3\n  .select(\"#container\")\n  .append(\"svg\")\n  .attr(\"width\", width)\n  .attr(\"height\", height);\nconst g = svg.append(\"g\").attr(\"transform\", `translate(${margin.left},0)`);\n\n// --- Timeline spine + month ticks --------------------------------------------\ng.append(\"line\")\n  .attr(\"x1\", 0)\n  .attr(\"x2\", iw)\n  .attr(\"y1\", centerY)\n  .attr(\"y2\", centerY)\n  .attr(\"stroke\", t.inkSoft)\n  .attr(\"stroke-width\", 2);\n\ng.selectAll(\".month-tick\")\n  .data(x.ticks(d3.timeMonth.every(1)))\n  .join(\"line\")\n  .attr(\"class\", \"month-tick\")\n  .attr(\"x1\", (d) => x(d))\n  .attr(\"x2\", (d) => x(d))\n  .attr(\"y1\", centerY - 6)\n  .attr(\"y2\", centerY + 6)\n  .attr(\"stroke\", t.grid)\n  .attr(\"stroke-width\", 1);\n\n// --- Layout: alternating sides with a deterministic vertical stagger ---------\n// Each side (above/below) gets its own sequence of events in date order. The\n// stem length cycles through 3 fixed tiers by position within that sequence,\n// so any two events that are neighbors on the same side always land on a\n// different tier (guaranteed by construction, not by an estimated text width)\n// and never share a vertical level.\nconst baseStem = 130;\nconst tierStep = 60;\nconst majorBonus = 40;\nconst tierCount = 3;\n\nconst sideIndex = { above: 0, below: 0 };\n\nconst laidOut = events.map((d, i) => {\n  const cx = x(d.date);\n  const side = i % 2 === 0 ? \"above\" : \"below\";\n  const tier = sideIndex[side] % tierCount;\n  sideIndex[side] += 1;\n\n  const stemLen = baseStem + tier * tierStep + (d.major ? majorBonus : 0);\n  return { ...d, cx, side, stemLen };\n});\n\n// --- Event markers: d3-shape symbols distinguish major vs. minor milestones --\nconst majorSymbol = d3.symbol().type(d3.symbolDiamond).size(320);\nconst minorSymbol = d3.symbol().type(d3.symbolCircle).size(100);\n\nconst eventGroups = g\n  .selectAll(\".event\")\n  .data(laidOut)\n  .join(\"g\")\n  .attr(\"class\", \"event\");\n\neventGroups.each(function (d) {\n  const eg = d3.select(this);\n  const above = d.side === \"above\";\n  const c = color(d.category);\n  const tipY = above ? centerY - d.stemLen : centerY + d.stemLen;\n  const gap = d.major ? 20 : 18;\n  const nameY = above ? tipY - gap : tipY + gap + 4;\n  const dateY = above ? nameY - 22 : nameY + 22;\n\n  eg.append(\"line\")\n    .attr(\"x1\", d.cx)\n    .attr(\"x2\", d.cx)\n    .attr(\"y1\", centerY)\n    .attr(\"y2\", tipY)\n    .attr(\"stroke\", c)\n    .attr(\"stroke-width\", d.major ? 3 : 2);\n\n  eg.append(\"path\")\n    .attr(\"d\", d.major ? majorSymbol() : minorSymbol())\n    .attr(\"transform\", `translate(${d.cx},${centerY})`)\n    .attr(\"fill\", c)\n    .attr(\"stroke\", t.pageBg)\n    .attr(\"stroke-width\", d.major ? 3 : 2);\n\n  eg.append(\"text\")\n    .attr(\"x\", d.cx)\n    .attr(\"y\", nameY)\n    .attr(\"text-anchor\", \"middle\")\n    .attr(\"fill\", t.ink)\n    .style(\"font-size\", d.major ? \"18px\" : \"16px\")\n    .style(\"font-weight\", d.major ? \"700\" : \"600\")\n    .text(d.name);\n\n  eg.append(\"text\")\n    .attr(\"x\", d.cx)\n    .attr(\"y\", dateY)\n    .attr(\"text-anchor\", \"middle\")\n    .attr(\"fill\", t.inkSoft)\n    .style(\"font-size\", \"13px\")\n    .text(formatDate(d.date));\n});\n\n// --- Legend -------------------------------------------------------------------\nconst legendItemWidth = 200;\nconst legend = svg\n  .append(\"g\")\n  .attr(\n    \"transform\",\n    `translate(${width / 2 - (categories.length * legendItemWidth) / 2}, ${height - 46})`,\n  );\n\nconst legendItems = legend\n  .selectAll(\".legend-item\")\n  .data(categories)\n  .join(\"g\")\n  .attr(\"class\", \"legend-item\")\n  .attr(\"transform\", (_, i) => `translate(${i * legendItemWidth}, 0)`);\n\nlegendItems\n  .append(\"circle\")\n  .attr(\"cx\", 8)\n  .attr(\"cy\", 0)\n  .attr(\"r\", 8)\n  .attr(\"fill\", (d) => color(d));\n\nlegendItems\n  .append(\"text\")\n  .attr(\"x\", 24)\n  .attr(\"y\", 5)\n  .attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"15px\")\n  .text((d) => d);\n\n// --- Title ----------------------------------------------------------------\nsvg\n  .append(\"text\")\n  .attr(\"x\", width / 2)\n  .attr(\"y\", 70)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"24px\")\n  .style(\"font-weight\", \"600\")\n  .text(\"Product Roadmap · timeline-basic · javascript · d3 · anyplot.ai\");\n"}