{"spec_id":"lightcurve-transit","library":"bokeh","language":"python","code":"\"\"\" anyplot.ai\nlightcurve-transit: Astronomical Light Curve\nLibrary: bokeh 3.9.1 | Python 3.13.14\nQuality: 90/100 | Updated: 2026-06-20\n\"\"\"\n\nimport base64\nimport os\nimport time\nfrom pathlib import Path\n\nimport numpy as np\nfrom bokeh.io import output_file, save\nfrom bokeh.models import (\n    Band,\n    BoxAnnotation,\n    ColumnDataSource,\n    CustomJSTickFormatter,\n    HoverTool,\n    Label,\n    Range1d,\n    Span,\n    Whisker,\n)\nfrom bokeh.plotting import figure\nfrom selenium import webdriver\nfrom selenium.webdriver.chrome.options import Options\n\n\n# Theme tokens — Imprint palette chrome\nTHEME = os.getenv(\"ANYPLOT_THEME\", \"light\")\nPAGE_BG = \"#FAF8F1\" if THEME == \"light\" else \"#1A1A17\"\nELEVATED_BG = \"#FFFDF6\" if THEME == \"light\" else \"#242420\"\nINK = \"#1A1A17\" if THEME == \"light\" else \"#F0EFE8\"\nINK_SOFT = \"#4A4A44\" if THEME == \"light\" else \"#B8B7B0\"\nINK_MUTED = \"#6B6A63\" if THEME == \"light\" else \"#A8A79F\"\n\n# Imprint categorical palette — canonical order, theme-independent\nBRAND = \"#009E73\"  # position 1 — always first series (photometric data)\nMODEL_COLOR = \"#C475FD\"  # position 2 — transit model curve\n\n# Data — simulated phase-folded exoplanet transit (hot Jupiter style)\nnp.random.seed(42)\nn_points = 600\nphase = np.sort(np.random.uniform(0.0, 1.0, n_points))\n\ntransit_center = 0.5\ntransit_depth = 0.012  # ~1.2% flux dip\nhalf_duration = 0.06\n\ndist = np.abs(phase - transit_center)\nin_transit = dist < half_duration\nmodel_flux = np.ones(n_points)\nmodel_flux[in_transit] = 1.0 - transit_depth * (0.5 + 0.5 * np.cos(np.pi * dist[in_transit] / half_duration))\n\nflux_err = np.random.uniform(0.001, 0.002, n_points)\nflux = model_flux + np.random.normal(0, 1, n_points) * flux_err\n\n# Smooth model curve for the overlay\nphase_model = np.linspace(0.0, 1.0, 3000)\ndist_m = np.abs(phase_model - transit_center)\nin_transit_m = dist_m < half_duration\nmodel_smooth = np.ones(3000)\nmodel_smooth[in_transit_m] = 1.0 - transit_depth * (0.5 + 0.5 * np.cos(np.pi * dist_m[in_transit_m] / half_duration))\n\nsource_data = ColumnDataSource(\n    data={\"phase\": phase, \"flux\": flux, \"flux_err\": flux_err, \"upper\": flux + flux_err, \"lower\": flux - flux_err}\n)\nsource_model = ColumnDataSource(\n    data={\"phase\": phase_model, \"model\": model_smooth, \"upper\": model_smooth + 0.0008, \"lower\": model_smooth - 0.0008}\n)\n\ny_min = min(flux.min(), model_smooth.min()) - 0.003\ny_max = max(flux.max(), model_smooth.max()) + 0.003\n\n# Plot\ntitle = \"lightcurve-transit · python · bokeh · anyplot.ai\"\n\np = figure(\n    width=3200,\n    height=1800,\n    title=title,\n    x_axis_label=\"Orbital Phase\",\n    y_axis_label=\"Relative Flux\",\n    x_range=Range1d(-0.02, 1.02),\n    y_range=Range1d(y_min, y_max),\n    toolbar_location=None,\n    min_border_bottom=160,\n    min_border_left=180,\n    min_border_top=110,\n    min_border_right=50,\n)\n\n# Theme chrome\np.background_fill_color = PAGE_BG\np.border_fill_color = PAGE_BG\np.outline_line_color = None\n\n# Transit window highlight (subtle shaded box)\ntransit_box = BoxAnnotation(\n    left=transit_center - half_duration, right=transit_center + half_duration, fill_color=BRAND, fill_alpha=0.06\n)\np.add_layout(transit_box)\n\n# Baseline reference at flux = 1.0\nbaseline = Span(location=1.0, dimension=\"width\", line_color=INK_SOFT, line_width=2, line_dash=[8, 6], line_alpha=0.4)\np.add_layout(baseline)\n\n# Confidence band around transit model\nband = Band(\n    base=\"phase\",\n    upper=\"upper\",\n    lower=\"lower\",\n    source=source_model,\n    fill_color=MODEL_COLOR,\n    fill_alpha=0.12,\n    line_color=None,\n)\np.add_layout(band)\n\n# Error bars — increased alpha for visibility\nwhisker = Whisker(\n    base=\"phase\", upper=\"upper\", lower=\"lower\", source=source_data, line_color=BRAND, line_alpha=0.3, line_width=1\n)\nwhisker.upper_head.size = 0\nwhisker.lower_head.size = 0\np.add_layout(whisker)\n\n# Data scatter — photometric measurements\nscatter_r = p.scatter(\n    x=\"phase\",\n    y=\"flux\",\n    source=source_data,\n    size=8,\n    color=BRAND,\n    alpha=0.45,\n    line_color=PAGE_BG,\n    line_width=0.5,\n    legend_label=\"Photometric Data\",\n)\n\n# Transit model curve\np.line(\n    x=\"phase\",\n    y=\"model\",\n    source=source_model,\n    line_color=MODEL_COLOR,\n    line_width=4,\n    line_alpha=0.9,\n    legend_label=\"Transit Model\",\n)\n\n# Transit depth annotation — vertical line + label quantifying the dip\np.line(\n    x=[transit_center + half_duration + 0.018] * 2,\n    y=[1.0, 1.0 - transit_depth],\n    line_color=MODEL_COLOR,\n    line_width=3,\n    line_alpha=0.7,\n)\np.scatter(\n    x=[transit_center + half_duration + 0.018] * 2,\n    y=[1.0, 1.0 - transit_depth],\n    size=10,\n    color=MODEL_COLOR,\n    marker=\"diamond\",\n    alpha=0.8,\n)\ndepth_label = Label(\n    x=transit_center + half_duration + 0.025,\n    y=1.0 - transit_depth / 2,\n    text=f\"Transit depth\\n{transit_depth * 100:.1f}%\",\n    text_font_size=\"30pt\",\n    text_color=INK,\n    text_font_style=\"italic\",\n    text_align=\"left\",\n)\np.add_layout(depth_label)\n\n# HoverTool for interactive exploration\nhover = HoverTool(\n    renderers=[scatter_r],\n    tooltips=[(\"Phase\", \"@phase{0.0000}\"), (\"Flux\", \"@flux{0.00000}\"), (\"Error\", \"±@flux_err{0.00000}\")],\n    mode=\"mouse\",\n)\np.add_tools(hover)\n\n# 4-decimal y-axis formatter for scientific precision\np.yaxis.formatter = CustomJSTickFormatter(code=\"return tick.toFixed(4);\")\n\n# Typography — standard 3200×1800 sizes\np.title.text_font_size = \"50pt\"\np.title.text_color = INK\np.title.text_font_style = \"bold\"\np.xaxis.axis_label_text_font_size = \"42pt\"\np.yaxis.axis_label_text_font_size = \"42pt\"\np.xaxis.axis_label_text_color = INK\np.yaxis.axis_label_text_color = INK\np.xaxis.axis_label_standoff = 20\np.yaxis.axis_label_standoff = 20\np.xaxis.major_label_text_font_size = \"34pt\"\np.yaxis.major_label_text_font_size = \"34pt\"\np.xaxis.major_label_text_color = INK_SOFT\np.yaxis.major_label_text_color = INK_SOFT\n\n# Axis lines and ticks\np.xaxis.axis_line_color = INK_SOFT\np.yaxis.axis_line_color = INK_SOFT\np.xaxis.major_tick_line_color = INK_SOFT\np.yaxis.major_tick_line_color = INK_SOFT\np.xaxis.minor_tick_line_color = None\np.yaxis.minor_tick_line_color = None\n\n# Grid — y-axis only, very subtle\np.xgrid.grid_line_color = None\np.ygrid.grid_line_color = INK\np.ygrid.grid_line_alpha = 0.15\n\n# Legend\np.legend.location = \"top_left\"\np.legend.label_text_font_size = \"34pt\"\np.legend.label_text_color = INK_SOFT\np.legend.background_fill_color = ELEVATED_BG\np.legend.border_line_color = INK_SOFT\np.legend.padding = 20\np.legend.spacing = 10\np.legend.glyph_height = 50\np.legend.glyph_width = 50\n\n# Save HTML\noutput_file(f\"plot-{THEME}.html\")\nsave(p)\n\n# Screenshot with headless Chrome (Selenium — export_png is not available in CI)\nW, H = 3200, 1800\nopts = Options()\nfor arg in (\n    \"--headless=new\",\n    \"--no-sandbox\",\n    \"--disable-dev-shm-usage\",\n    \"--disable-gpu\",\n    f\"--window-size={W},{H}\",\n    \"--hide-scrollbars\",\n):\n    opts.add_argument(arg)\ndriver = webdriver.Chrome(options=opts)\ndriver.set_window_size(W, H)\ndriver.get(f\"file://{Path(f'plot-{THEME}.html').resolve()}\")\ntime.sleep(3)\n# CDP screenshot at exact canvas dimensions — avoids viewport-height caps in headless Chrome\nresult = driver.execute_cdp_cmd(\n    \"Page.captureScreenshot\", {\"format\": \"png\", \"clip\": {\"x\": 0, \"y\": 0, \"width\": W, \"height\": H, \"scale\": 1}}\n)\nwith open(f\"plot-{THEME}.png\", \"wb\") as fh:\n    fh.write(base64.b64decode(result[\"data\"]))\ndriver.quit()\n"}