{"spec_id":"lollipop-basic","library":"d3","language":"javascript","code":"// anyplot.ai\n// lollipop-basic: Basic Lollipop Chart\n// Library: d3 7.9.0 | JavaScript 22.23.0\n// Quality: 91/100 | Created: 2026-07-01\n//# anyplot-orientation: landscape\n\nconst t = window.ANYPLOT_TOKENS;\nconst { width, height } = window.ANYPLOT_SIZE;\nconst margin = { top: 80, right: 130, bottom: 70, left: 215 };\nconst iw = width - margin.left - margin.right;\nconst ih = height - margin.top - margin.bottom;\n\n// Data: World's most-visited museums (annual visitors, millions, 2019)\n// sorted ascending — scaleBand range([ih,0]) maps first→bottom, last→top\nconst museums = [\n  { name: \"Musée d'Orsay\",          visitors: 4.0 },\n  { name: \"Smithsonian Air & Space\", visitors: 4.5 },\n  { name: \"Tokyo National Museum\",   visitors: 4.8 },\n  { name: \"National Gallery\",        visitors: 5.7 },\n  { name: \"Tate Modern\",             visitors: 5.9 },\n  { name: \"British Museum\",          visitors: 6.2 },\n  { name: \"Metropolitan Museum\",     visitors: 6.5 },\n  { name: \"Vatican Museums\",         visitors: 6.8 },\n  { name: \"Nat'l Museum of China\",   visitors: 8.6 },\n  { name: \"Louvre\",                  visitors: 9.6 },\n];\n\n// SVG\nconst svg = d3.select(\"#container\").append(\"svg\")\n  .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, 11]).range([0, iw]);\nconst y = d3.scaleBand()\n  .domain(museums.map(d => d.name))\n  .range([ih, 0])\n  .padding(0.45);\n\n// Vertical gridlines — idiomatic D3 data join\ng.selectAll(\".grid\")\n  .data(x.ticks(6))\n  .join(\"line\")\n  .attr(\"class\", \"grid\")\n  .attr(\"x1\", d => x(d)).attr(\"x2\", d => x(d))\n  .attr(\"y1\", 0).attr(\"y2\", ih)\n  .attr(\"stroke\", t.grid)\n  .attr(\"stroke-width\", 1);\n\n// Axes\nconst xAxisG = g.append(\"g\").attr(\"transform\", `translate(0,${ih})`).call(\n  d3.axisBottom(x).ticks(6).tickFormat(d => `${d}M`)\n);\nconst yAxisG = g.append(\"g\").call(d3.axisLeft(y).tickSize(0).tickPadding(10));\n\nxAxisG.selectAll(\"text\").attr(\"fill\", t.inkSoft).style(\"font-size\", \"14px\");\nxAxisG.selectAll(\"line\").attr(\"stroke\", t.grid);\nxAxisG.select(\".domain\").attr(\"stroke\", t.inkSoft);\n\n// Semi-bold Louvre label for typographic hierarchy (top-ranked entry)\nyAxisG.selectAll(\"text\")\n  .attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"14px\")\n  .style(\"font-weight\", d => d === \"Louvre\" ? \"600\" : \"400\");\nyAxisG.select(\".domain\").attr(\"stroke\", t.inkSoft);\n\n// Stems\ng.selectAll(\".stem\")\n  .data(museums)\n  .join(\"line\")\n  .attr(\"class\", \"stem\")\n  .attr(\"x1\", 0)\n  .attr(\"x2\", d => x(d.visitors))\n  .attr(\"y1\", d => y(d.name) + y.bandwidth() / 2)\n  .attr(\"y2\", d => y(d.name) + y.bandwidth() / 2)\n  .attr(\"stroke\", t.palette[0])\n  .attr(\"stroke-width\", 2.5)\n  .attr(\"stroke-opacity\", 0.65);\n\n// Dots — Louvre at r=13 (vs r=10) signals its outlier status\ng.selectAll(\".dot\")\n  .data(museums)\n  .join(\"circle\")\n  .attr(\"class\", \"dot\")\n  .attr(\"cx\", d => x(d.visitors))\n  .attr(\"cy\", d => y(d.name) + y.bandwidth() / 2)\n  .attr(\"r\", d => d.name === \"Louvre\" ? 13 : 10)\n  .attr(\"fill\", t.palette[0])\n  .attr(\"stroke\", t.pageBg)\n  .attr(\"stroke-width\", 2.5);\n\n// Value labels — 14px to meet the floor; Louvre label semi-bold\ng.selectAll(\".val\")\n  .data(museums)\n  .join(\"text\")\n  .attr(\"class\", \"val\")\n  .attr(\"x\", d => x(d.visitors) + 18)\n  .attr(\"y\", d => y(d.name) + y.bandwidth() / 2)\n  .attr(\"dy\", \"0.35em\")\n  .attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"14px\")\n  .style(\"font-weight\", d => d.name === \"Louvre\" ? \"600\" : \"400\")\n  .text(d => `${d.visitors}M`);\n\n// Focal-point annotation on the Louvre outlier\nconst louvre = museums[museums.length - 1];\ng.append(\"text\")\n  .attr(\"x\", x(louvre.visitors) + 18)\n  .attr(\"y\", y(louvre.name) + y.bandwidth() / 2 - 22)\n  .attr(\"fill\", t.palette[0])\n  .style(\"font-size\", \"12px\")\n  .style(\"font-style\", \"italic\")\n  .text(\"world's most-visited\");\n\n// X-axis label\ng.append(\"text\")\n  .attr(\"x\", iw / 2)\n  .attr(\"y\", ih + 55)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"14px\")\n  .text(\"Annual Visitors (millions, 2019)\");\n\n// Title\nsvg.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\", \"22px\")\n  .style(\"font-weight\", \"600\")\n  .text(\"lollipop-basic · javascript · d3 · anyplot.ai\");\n"}