{"spec_id":"lightcurve-transit","library":"highcharts","language":"javascript","code":"// anyplot.ai\n// lightcurve-transit: Astronomical Light Curve\n// Library: highcharts 12.6.0 | JavaScript 22.22.3\n// Quality: 89/100 | Created: 2026-06-20\n\n//# anyplot-orientation: landscape\n\nconst t = window.ANYPLOT_TOKENS;\n\n// Deterministic LCG — browser has no seeded RNG\nlet _rng = 42;\nfunction rand() {\n    _rng = (Math.imul(1664525, _rng) + 1013904223) >>> 0;\n    return _rng / 4294967296;\n}\nfunction randn() {\n    return Math.sqrt(-2 * Math.log(rand() + 1e-12)) * Math.cos(2 * Math.PI * rand());\n}\n\n// Transit model: simplified Mandel-Agol with smoothstep ingress and quadratic limb darkening\nconst DEPTH = 0.011;             // 1.1% fractional transit depth\nconst T14   = 0.085;             // total transit duration in phase units\nconst T23   = 0.063;             // flat-bottom (full occultation) duration\nconst T_ING = (T14 - T23) / 2;  // ingress / egress duration\n\nfunction transitModel(ph) {\n    const p = Math.abs(ph);\n    if (p >= T14 / 2) return 1.0;\n    if (p <= T23 / 2) {\n        // Limb-darkening curve: slightly brighter at limb-center crossing\n        const x = p / (T23 / 2);\n        return 1.0 - DEPTH * (1.0 - 0.08 * x * x);\n    }\n    // Smoothstep ingress / egress\n    const f = (p - T23 / 2) / T_ING;\n    return 1.0 - DEPTH * (1.0 - f * f * (3.0 - 2.0 * f));\n}\n\n// 220 phase-folded photometric observations\nconst N      = 220;\nconst PH_MIN = -0.28;\nconst PH_MAX =  0.28;\nconst SIGMA  = 0.0024;  // typical photometric precision\n\nconst phases = [], fluxes = [], errs = [];\nfor (let i = 0; i < N; i++) {\n    const ph  = PH_MIN + (PH_MAX - PH_MIN) * i / (N - 1);\n    const err = SIGMA * (0.82 + 0.36 * rand());\n    phases.push(ph);\n    fluxes.push(transitModel(ph) + randn() * SIGMA);\n    errs.push(err);\n}\n\n// Dense model curve for smooth spline overlay\nconst modelPts = [];\nfor (let i = 0; i <= 500; i++) {\n    const ph = PH_MIN + (PH_MAX - PH_MIN) * i / 500;\n    modelPts.push([ph, transitModel(ph)]);\n}\n\n// Y-axis range with padding\nconst allFlux = fluxes.concat(modelPts.map(d => d[1]));\nconst fMin = Math.min(...allFlux);\nconst fMax = Math.max(...allFlux);\nconst vPad = (fMax - fMin) * 0.20;\n\n// Imprint palette — first series is always brand green\nconst obsColor = t.palette[0];  // #009E73 — observed photometry\nconst modColor = t.palette[1];  // #C475FD — fitted transit model\n\n// Title length: 77 chars → fontSize = round(22 × 67/77) = 19px\nconst chart = Highcharts.chart(\"container\", {\n    chart: {\n        backgroundColor: \"transparent\",\n        animation: false,\n        style: { fontFamily: \"inherit\" },\n        marginTop: 92,\n        marginBottom: 80,\n        marginLeft: 96,\n        marginRight: 52,\n    },\n    credits: { enabled: false },\n    colors: t.palette,\n    title: {\n        text: \"Exoplanet Transit · lightcurve-transit · javascript · highcharts · anyplot.ai\",\n        style: { color: t.ink, fontSize: \"19px\", fontWeight: \"600\" },\n        margin: 6,\n    },\n    subtitle: {\n        text: \"Phase-folded photometry with limb-darkened transit model · depth 1.1% · T₁₄ = 0.085 phase units\",\n        style: { color: t.inkSoft, fontSize: \"13px\" },\n    },\n    xAxis: {\n        min: PH_MIN,\n        max: PH_MAX,\n        title: {\n            text: \"Orbital Phase\",\n            style: { color: t.inkSoft, fontSize: \"16px\" },\n            margin: 12,\n        },\n        labels: { style: { color: t.inkSoft, fontSize: \"14px\" } },\n        lineColor: t.inkSoft,\n        tickColor: t.inkSoft,\n        gridLineColor: t.grid,\n        gridLineWidth: 1,\n    },\n    yAxis: {\n        min: fMin - vPad,\n        max: fMax + vPad,\n        title: {\n            text: \"Relative Flux\",\n            style: { color: t.inkSoft, fontSize: \"16px\" },\n            margin: 14,\n        },\n        labels: {\n            style: { color: t.inkSoft, fontSize: \"14px\" },\n            formatter: function () { return this.value.toFixed(3); },\n        },\n        lineColor: t.inkSoft,\n        tickColor: t.inkSoft,\n        gridLineColor: t.grid,\n        gridLineWidth: 1,\n        plotLines: [{\n            value: 1.0,\n            color: t.inkSoft,\n            dashStyle: \"ShortDash\",\n            width: 1,\n            zIndex: 2,\n            label: {\n                text: \"Baseline 1.000\",\n                style: { color: t.inkSoft, fontSize: \"12px\" },\n                align: \"right\",\n                x: -8,\n                y: -6,\n            },\n        }],\n    },\n    legend: {\n        enabled: true,\n        align: \"right\",\n        verticalAlign: \"top\",\n        layout: \"vertical\",\n        itemStyle: { color: t.inkSoft, fontSize: \"14px\", fontWeight: \"normal\" },\n        itemHoverStyle: { color: t.ink },\n        backgroundColor: t.elevatedBg,\n        borderRadius: 4,\n        padding: 10,\n        y: 8,\n    },\n    tooltip: { enabled: false },\n    plotOptions: {\n        series: { animation: false },\n        scatter: {\n            marker: {\n                radius: 3.5,\n                symbol: \"circle\",\n                lineWidth: 0,\n            },\n            states: { hover: { enabled: false } },\n        },\n        spline: {\n            lineWidth: 2.5,\n            marker: { enabled: false },\n            states: { hover: { enabled: false } },\n        },\n    },\n    series: [\n        {\n            type: \"scatter\",\n            name: \"Observed Flux\",\n            color: obsColor,\n            data: phases.map(function (ph, i) { return [ph, fluxes[i]]; }),\n            zIndex: 2,\n        },\n        {\n            type: \"spline\",\n            name: \"Transit Model\",\n            color: modColor,\n            lineWidth: 2.5,\n            data: modelPts,\n            zIndex: 3,\n            marker: { enabled: false },\n        },\n    ],\n});\n\n// Draw per-point error bars via SVG renderer — single combined path for efficiency\nconst xA  = chart.xAxis[0];\nconst yA  = chart.yAxis[0];\nconst ren = chart.renderer;\nconst CAP = 3;  // horizontal cap half-width in CSS px\n\nconst stems = [], caps = [];\nphases.forEach(function (ph, i) {\n    const flux = fluxes[i];\n    const err  = errs[i];\n    const px   = xA.toPixels(ph);\n    const pyT  = yA.toPixels(flux + err);\n    const pyB  = yA.toPixels(flux - err);\n\n    stems.push(\"M\", px, pyT, \"L\", px, pyB);\n    caps.push(\n        \"M\", px - CAP, pyT, \"L\", px + CAP, pyT,\n        \"M\", px - CAP, pyB, \"L\", px + CAP, pyB\n    );\n});\n\nconst errGroup = ren.g(\"error-bars\").add();\nren.path(stems)\n    .attr({ stroke: obsColor, \"stroke-width\": 1, opacity: 0.45 })\n    .add(errGroup);\nren.path(caps)\n    .attr({ stroke: obsColor, \"stroke-width\": 1, opacity: 0.45 })\n    .add(errGroup);\n"}