{"spec_id":"lightcurve-transit","library":"d3","language":"javascript","code":"// anyplot.ai\n// lightcurve-transit: Astronomical Light Curve\n// Library: d3 7.9.0 | JavaScript 22.22.3\n// Quality: 89/100 | Created: 2026-06-20\n\n//# anyplot-orientation: landscape\n\nconst t = window.ANYPLOT_TOKENS;\nconst { width, height } = window.ANYPLOT_SIZE;\nconst margin = { top: 80, right: 80, bottom: 100, left: 120 };\nconst iw = width - margin.left - margin.right;\nconst ih = height - margin.top - margin.bottom;\n\n// Deterministic LCG (seed 42) — browser has no seeded RNG\nlet _seed = 42;\nfunction lcg() {\n  _seed = ((_seed * 1664525) + 1013904223) >>> 0;\n  return _seed / 0x100000000;\n}\nfunction randn() {\n  return Math.sqrt(-2 * Math.log(lcg() + 1e-10)) * Math.cos(2 * Math.PI * lcg());\n}\n\n// Transit parameters (shared by model + domain)\nconst tc = 0.5, halfDur = 0.065, depth = 0.011;\n\n// Transit model: trapezoidal with limb-darkening curvature\nfunction transitFlux(phase) {\n  const phi = Math.abs(phase - tc);\n  if (phi > halfDur) return 1.0;\n  const u = phi / halfDur;\n  const depthHere = depth * (1 - 0.15 * u * u);\n  const ingressZone = 0.25;\n  if (u > 1 - ingressZone) {\n    const s = (1 - u) / ingressZone;\n    return 1.0 - depthHere * s * s * (3 - 2 * s);\n  }\n  return 1.0 - depthHere;\n}\n\n// Phase-folded observations: 450 points with Gaussian photometric noise\nconst N = 450;\nconst obs = Array.from({ length: N }, (_, i) => {\n  const phase = i / N;\n  const model = transitFlux(phase);\n  const fluxErr = 0.0025 + lcg() * 0.0010;\n  const flux = model + randn() * 0.0030;\n  return { phase, flux, fluxErr };\n});\n\n// Smooth model curve with ±1σ band (1001 points for clean rendering)\nconst sigma1 = 0.003;\nconst modelCurve = Array.from({ length: 1001 }, (_, i) => {\n  const phase = i / 1000;\n  const flux = transitFlux(phase);\n  return { phase, flux, lo: flux - sigma1, hi: flux + sigma1 };\n});\n\n// SVG\nconst svg = d3.select(\"#container\").append(\"svg\")\n  .attr(\"width\", width).attr(\"height\", height);\n\n// Clip-path: keep data elements inside axes bounds — idiomatic D3 pattern\nsvg.append(\"defs\").append(\"clipPath\")\n  .attr(\"id\", \"plot-area\")\n  .append(\"rect\")\n  .attr(\"x\", 0).attr(\"y\", 0).attr(\"width\", iw).attr(\"height\", ih);\n\nconst g = svg.append(\"g\").attr(\"transform\", `translate(${margin.left},${margin.top})`);\n\n// Scales — tighten Y domain so transit dip fills canvas (avoids 40% empty space)\nconst transitBase = 1.0 - depth; // ~0.989, the transit floor\nconst yMin = transitBase - 0.5 * depth; // ~0.9835\nconst xScale = d3.scaleLinear().domain([0, 1]).range([0, iw]);\nconst yScale = d3.scaleLinear().domain([yMin, 1.009]).nice().range([ih, 0]);\n\n// Transit window background — visual storytelling: highlight the ingress/egress window\ng.append(\"rect\")\n  .attr(\"x\", xScale(tc - halfDur))\n  .attr(\"y\", 0)\n  .attr(\"width\", xScale(tc + halfDur) - xScale(tc - halfDur))\n  .attr(\"height\", ih)\n  .attr(\"fill\", t.palette[1])\n  .attr(\"opacity\", 0.07);\n\n// Dashed vertical at transit midpoint — marks the deepest-flux moment\ng.append(\"line\")\n  .attr(\"x1\", xScale(tc)).attr(\"x2\", xScale(tc))\n  .attr(\"y1\", 0).attr(\"y2\", ih)\n  .attr(\"stroke\", t.palette[1])\n  .attr(\"stroke-width\", 1.2)\n  .attr(\"stroke-dasharray\", \"5,4\")\n  .attr(\"opacity\", 0.6);\n\n// Horizontal grid lines (y-axis only, subtle)\nyScale.ticks(6).forEach((tick) => {\n  g.append(\"line\")\n    .attr(\"x1\", 0).attr(\"x2\", iw)\n    .attr(\"y1\", yScale(tick)).attr(\"y2\", yScale(tick))\n    .attr(\"stroke\", t.grid).attr(\"stroke-width\", 1);\n});\n\n// Clipped group: all data elements stay inside the axes frame\nconst clipG = g.append(\"g\").attr(\"clip-path\", \"url(#plot-area)\");\n\n// ±1σ confidence band around model — d3.area() showcases D3's area generator\nconst bandGen = d3.area()\n  .x((d) => xScale(d.phase))\n  .y0((d) => yScale(d.lo))\n  .y1((d) => yScale(d.hi));\n\nclipG.append(\"path\")\n  .datum(modelCurve)\n  .attr(\"fill\", t.palette[1])\n  .attr(\"opacity\", 0.18)\n  .attr(\"d\", bandGen);\n\n// Error bars — drawn below points; stroke-width 1.3 for legibility in transit region\nclipG.append(\"g\")\n  .selectAll(\"line\")\n  .data(obs)\n  .join(\"line\")\n  .attr(\"x1\", (d) => xScale(d.phase))\n  .attr(\"x2\", (d) => xScale(d.phase))\n  .attr(\"y1\", (d) => yScale(d.flux - d.fluxErr))\n  .attr(\"y2\", (d) => yScale(d.flux + d.fluxErr))\n  .attr(\"stroke\", t.palette[0])\n  .attr(\"stroke-width\", 1.3)\n  .attr(\"opacity\", 0.45);\n\n// Data points — Imprint palette[0] (#009E73) as first series\nclipG.append(\"g\")\n  .selectAll(\"circle\")\n  .data(obs)\n  .join(\"circle\")\n  .attr(\"cx\", (d) => xScale(d.phase))\n  .attr(\"cy\", (d) => yScale(d.flux))\n  .attr(\"r\", 2.5)\n  .attr(\"fill\", t.palette[0])\n  .attr(\"opacity\", 0.80);\n\n// Transit model line — Imprint palette[1] (#C475FD)\nconst lineGen = d3.line()\n  .x((d) => xScale(d.phase))\n  .y((d) => yScale(d.flux));\nclipG.append(\"path\")\n  .datum(modelCurve)\n  .attr(\"fill\", \"none\")\n  .attr(\"stroke\", t.palette[1])\n  .attr(\"stroke-width\", 2.5)\n  .attr(\"d\", lineGen);\n\n// Axes\nconst xAxis = d3.axisBottom(xScale).ticks(5).tickFormat(d3.format(\".1f\"));\nconst yAxis = d3.axisLeft(yScale).ticks(6).tickFormat(d3.format(\".3f\"));\n\nconst gx = g.append(\"g\").attr(\"transform\", `translate(0,${ih})`).call(xAxis);\nconst gy = g.append(\"g\").call(yAxis);\n\nfor (const ax of [gx, gy]) {\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 + 68)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink).style(\"font-size\", \"16px\")\n  .text(\"Orbital Phase\");\n\ng.append(\"text\")\n  .attr(\"transform\", \"rotate(-90)\")\n  .attr(\"x\", -(ih / 2)).attr(\"y\", -88)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink).style(\"font-size\", \"16px\")\n  .text(\"Relative Flux\");\n\n// Legend\nconst lx = iw - 220, ly = 20;\n\n// Transit model: confidence band swatch + line\ng.append(\"rect\")\n  .attr(\"x\", lx - 10).attr(\"y\", ly - 5)\n  .attr(\"width\", 20).attr(\"height\", 10)\n  .attr(\"fill\", t.palette[1]).attr(\"opacity\", 0.18);\ng.append(\"line\")\n  .attr(\"x1\", lx - 10).attr(\"x2\", lx + 10)\n  .attr(\"y1\", ly).attr(\"y2\", ly)\n  .attr(\"stroke\", t.palette[1]).attr(\"stroke-width\", 2.5);\ng.append(\"text\")\n  .attr(\"x\", lx + 16).attr(\"y\", ly + 5)\n  .attr(\"fill\", t.inkSoft).style(\"font-size\", \"14px\")\n  .text(\"Transit Model (±1σ)\");\n\n// Observations\ng.append(\"circle\")\n  .attr(\"cx\", lx).attr(\"cy\", ly + 30)\n  .attr(\"r\", 5)\n  .attr(\"fill\", t.palette[0]).attr(\"opacity\", 0.80);\ng.append(\"text\")\n  .attr(\"x\", lx + 16).attr(\"y\", ly + 35)\n  .attr(\"fill\", t.inkSoft).style(\"font-size\", \"14px\")\n  .text(\"Observations\");\n\n// Title\nsvg.append(\"text\")\n  .attr(\"x\", width / 2).attr(\"y\", 50)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"22px\").style(\"font-weight\", \"600\")\n  .text(\"lightcurve-transit · javascript · d3 · anyplot.ai\");\n"}