{"spec_id":"scatter-connected-temporal","library":"d3","language":"javascript","code":"// anyplot.ai\n// scatter-connected-temporal: Connected Scatter Plot with Temporal Path\n// Library: d3 7.9.0 | JavaScript 22.22.3\n// Quality: 93/100 | Updated: 2026-06-10\n\nconst t = window.ANYPLOT_TOKENS;\nconst { width, height } = window.ANYPLOT_SIZE;\nconst margin = { top: 90, right: 180, bottom: 90, left: 110 };\nconst iw = width - margin.left - margin.right;\nconst ih = height - margin.top - margin.bottom;\n\n// GDP per capita (thousands USD, 2015 prices) vs. Life Expectancy (years), 1992–2021\nconst data = [\n  { year: 1992, gdp: 23.0, le: 75.3 }, { year: 1993, gdp: 23.8, le: 75.5 },\n  { year: 1994, gdp: 25.2, le: 75.7 }, { year: 1995, gdp: 26.1, le: 75.9 },\n  { year: 1996, gdp: 27.3, le: 76.1 }, { year: 1997, gdp: 28.8, le: 76.4 },\n  { year: 1998, gdp: 29.7, le: 76.7 }, { year: 1999, gdp: 31.0, le: 77.0 },\n  { year: 2000, gdp: 32.8, le: 77.2 }, { year: 2001, gdp: 32.1, le: 77.3 },\n  { year: 2002, gdp: 31.6, le: 77.5 }, { year: 2003, gdp: 32.7, le: 77.5 },\n  { year: 2004, gdp: 34.5, le: 77.7 }, { year: 2005, gdp: 36.1, le: 77.9 },\n  { year: 2006, gdp: 38.0, le: 78.1 }, { year: 2007, gdp: 39.8, le: 78.2 },\n  { year: 2008, gdp: 40.2, le: 78.4 }, { year: 2009, gdp: 37.1, le: 78.6 },\n  { year: 2010, gdp: 38.6, le: 78.8 }, { year: 2011, gdp: 40.0, le: 79.0 },\n  { year: 2012, gdp: 40.8, le: 79.1 }, { year: 2013, gdp: 42.0, le: 79.3 },\n  { year: 2014, gdp: 43.7, le: 79.4 }, { year: 2015, gdp: 44.7, le: 79.6 },\n  { year: 2016, gdp: 45.3, le: 79.6 }, { year: 2017, gdp: 47.1, le: 79.7 },\n  { year: 2018, gdp: 49.4, le: 79.8 }, { year: 2019, gdp: 51.1, le: 80.0 },\n  { year: 2020, gdp: 48.2, le: 79.2 }, { year: 2021, gdp: 52.4, le: 79.5 },\n];\n\n// SVG scaffold\nconst svg = d3.select(\"#container\").append(\"svg\").attr(\"width\", width).attr(\"height\", height);\nconst defs = svg.append(\"defs\");\n\n// Legend gradient (green → blue for the legend bar)\nconst legendGrad = defs.append(\"linearGradient\")\n  .attr(\"id\", \"temporal-legend-grad\")\n  .attr(\"x1\", \"0%\").attr(\"x2\", \"100%\");\nlegendGrad.append(\"stop\").attr(\"offset\", \"0%\").attr(\"stop-color\", t.seq[0]);\nlegendGrad.append(\"stop\").attr(\"offset\", \"100%\").attr(\"stop-color\", t.seq[1]);\n\nconst g = svg.append(\"g\").attr(\"transform\", `translate(${margin.left},${margin.top})`);\n\n// Scales\nconst xScale = d3.scaleLinear().domain(d3.extent(data, d => d.gdp)).nice().range([0, iw]);\nconst yScale = d3.scaleLinear().domain(d3.extent(data, d => d.le)).nice().range([ih, 0]);\n\n// Path-stroke gradient via gradientUnits=\"userSpaceOnUse\" (idiomatic SVG/D3 technique)\n// Projects color linearly from the 1992 start point to the 2021 end point in data space\nconst pathGrad = defs.append(\"linearGradient\")\n  .attr(\"id\", \"path-temporal-grad\")\n  .attr(\"gradientUnits\", \"userSpaceOnUse\")\n  .attr(\"x1\", xScale(data[0].gdp))\n  .attr(\"y1\", yScale(data[0].le))\n  .attr(\"x2\", xScale(data[data.length - 1].gdp))\n  .attr(\"y2\", yScale(data[data.length - 1].le));\npathGrad.append(\"stop\").attr(\"offset\", \"0%\").attr(\"stop-color\", t.seq[0]);\npathGrad.append(\"stop\").attr(\"offset\", \"100%\").attr(\"stop-color\", t.seq[1]);\n\n// Gridlines (both axes — subtle)\nxScale.ticks(6).forEach(v => {\n  g.append(\"line\")\n    .attr(\"x1\", xScale(v)).attr(\"x2\", xScale(v))\n    .attr(\"y1\", 0).attr(\"y2\", ih)\n    .attr(\"stroke\", t.grid).attr(\"stroke-width\", 1);\n});\nyScale.ticks(6).forEach(v => {\n  g.append(\"line\")\n    .attr(\"x1\", 0).attr(\"x2\", iw)\n    .attr(\"y1\", yScale(v)).attr(\"y2\", yScale(v))\n    .attr(\"stroke\", t.grid).attr(\"stroke-width\", 1);\n});\n\n// Connecting path with userSpaceOnUse gradient stroke — single <path> element\nconst lineGen = d3.line()\n  .x(d => xScale(d.gdp))\n  .y(d => yScale(d.le));\n\ng.append(\"path\")\n  .datum(data)\n  .attr(\"d\", lineGen)\n  .attr(\"fill\", \"none\")\n  .attr(\"stroke\", \"url(#path-temporal-grad)\")\n  .attr(\"stroke-width\", 2.5)\n  .attr(\"stroke-opacity\", 0.85);\n\n// Temporal color interpolator (for marker fills, mirrors gradient)\nconst tempColor = frac => d3.interpolateRgb(t.seq[0], t.seq[1])(frac);\n\n// Data point markers (larger endpoint circles for 1992 and 2021)\ng.selectAll(\"circle\").data(data).join(\"circle\")\n  .attr(\"cx\", d => xScale(d.gdp))\n  .attr(\"cy\", d => yScale(d.le))\n  .attr(\"r\", (d, i) => (i === 0 || i === data.length - 1) ? 9 : 5)\n  .attr(\"fill\", (d, i) => tempColor(i / (data.length - 1)))\n  .attr(\"stroke\", t.pageBg)\n  .attr(\"stroke-width\", 2);\n\n// Year labels at key economic moments\nconst labelConfig = [\n  { year: 1992, dx: -46, dy: 6 },\n  { year: 2000, dx: 10, dy: -13 },\n  { year: 2007, dx: 10, dy: -13 },\n  { year: 2009, dx: -46, dy: 19 },\n  { year: 2015, dx: 10, dy: -13 },\n  { year: 2020, dx: 6, dy: 22 },\n  { year: 2021, dx: 12, dy: -13 },\n];\ndata.forEach(d => {\n  const cfg = labelConfig.find(c => c.year === d.year);\n  if (!cfg) return;\n  const isEndpoint = d.year === 1992 || d.year === 2021;\n  g.append(\"text\")\n    .attr(\"x\", xScale(d.gdp) + cfg.dx)\n    .attr(\"y\", yScale(d.le) + cfg.dy)\n    .attr(\"fill\", isEndpoint ? t.ink : t.inkSoft)\n    .style(\"font-size\", isEndpoint ? \"13px\" : \"12px\")\n    .style(\"font-weight\", isEndpoint ? \"700\" : \"400\")\n    .text(d.year);\n});\n\n// Narrative event annotations with dashed leader lines\nconst d2009 = data.find(d => d.year === 2009);\nconst d2020 = data.find(d => d.year === 2020);\n\n// Financial Crisis (2009) — leader line angling down-left, away from year label\nconst fc2009x = xScale(d2009.gdp) - 68;\nconst fc2009y = yScale(d2009.le) + 60;\ng.append(\"line\")\n  .attr(\"x1\", xScale(d2009.gdp) - 8).attr(\"y1\", yScale(d2009.le) + 12)\n  .attr(\"x2\", fc2009x).attr(\"y2\", fc2009y - 14)\n  .attr(\"stroke\", t.inkSoft).attr(\"stroke-width\", 1).attr(\"stroke-dasharray\", \"4,3\");\ng.append(\"text\")\n  .attr(\"x\", fc2009x).attr(\"y\", fc2009y)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.inkSoft).style(\"font-size\", \"11px\").style(\"font-style\", \"italic\")\n  .text(\"Financial Crisis\");\n\n// COVID-19 (2020) — leader line going straight down, away from year label\nconst cv2020x = xScale(d2020.gdp) - 42;\nconst cv2020y = yScale(d2020.le) + 60;\ng.append(\"line\")\n  .attr(\"x1\", xScale(d2020.gdp) - 8).attr(\"y1\", yScale(d2020.le) + 12)\n  .attr(\"x2\", cv2020x).attr(\"y2\", cv2020y - 14)\n  .attr(\"stroke\", t.inkSoft).attr(\"stroke-width\", 1).attr(\"stroke-dasharray\", \"4,3\");\ng.append(\"text\")\n  .attr(\"x\", cv2020x).attr(\"y\", cv2020y)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.inkSoft).style(\"font-size\", \"11px\").style(\"font-style\", \"italic\")\n  .text(\"COVID-19\");\n\n// Axes\nconst xAxis = g.append(\"g\")\n  .attr(\"transform\", `translate(0,${ih})`)\n  .call(d3.axisBottom(xScale).ticks(6).tickFormat(d => `$${d3.format(\".0f\")(d)}k`));\nconst yAxis = g.append(\"g\")\n  .call(d3.axisLeft(yScale).ticks(6).tickFormat(d => `${d3.format(\".0f\")(d)} yrs`));\n\n[xAxis, yAxis].forEach(ax => {\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).attr(\"y\", ih + 60)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink).style(\"font-size\", \"16px\")\n  .text(\"GDP per Capita (USD thousands, 2015 prices)\");\ng.append(\"text\")\n  .attr(\"transform\", \"rotate(-90)\")\n  .attr(\"x\", -(ih / 2)).attr(\"y\", -78)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink).style(\"font-size\", \"16px\")\n  .text(\"Life Expectancy (years)\");\n\n// Temporal legend in the right margin — no overlap with data path\nconst lgW = 150, lgH = 10;\nconst lgX = iw + 16, lgY = ih / 2 - lgH / 2;\ng.append(\"text\")\n  .attr(\"x\", lgX + lgW / 2).attr(\"y\", lgY - 14)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.inkSoft).style(\"font-size\", \"11px\")\n  .text(\"Temporal direction  →\");\ng.append(\"rect\")\n  .attr(\"x\", lgX).attr(\"y\", lgY)\n  .attr(\"width\", lgW).attr(\"height\", lgH)\n  .attr(\"rx\", 4)\n  .attr(\"fill\", \"url(#temporal-legend-grad)\");\ng.append(\"text\")\n  .attr(\"x\", lgX).attr(\"y\", lgY + lgH + 14)\n  .attr(\"fill\", t.inkSoft).style(\"font-size\", \"11px\")\n  .text(\"1992\");\ng.append(\"text\")\n  .attr(\"x\", lgX + lgW).attr(\"y\", lgY + lgH + 14)\n  .attr(\"text-anchor\", \"end\")\n  .attr(\"fill\", t.inkSoft).style(\"font-size\", \"11px\")\n  .text(\"2021\");\n\n// Title\nconst title = \"GDP & Life Expectancy · scatter-connected-temporal · javascript · d3 · anyplot.ai\";\nconst titleFontSize = Math.max(13, Math.round(22 * 67 / title.length));\nsvg.append(\"text\")\n  .attr(\"x\", width / 2).attr(\"y\", 52)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", `${titleFontSize}px`)\n  .style(\"font-weight\", \"600\")\n  .text(title);\n"}