{"spec_id":"climograph-walter-lieth","library":"bokeh","language":"python","code":"\"\"\" anyplot.ai\nclimograph-walter-lieth: Walter-Lieth Climate Diagram\nLibrary: bokeh 3.9.1 | Python 3.13.13\nQuality: 91/100 | Created: 2026-06-15\n\"\"\"\n\nimport io\nimport os\nimport sys\nimport time\nfrom pathlib import Path\n\n\n# Remove current dir from sys.path so bokeh.py doesn't shadow the bokeh package\nsys.path = [p for p in sys.path if p != \"\" and not (os.path.isfile(os.path.join(p, \"bokeh.py\")) if p else False)]\n\nimport numpy as np\nfrom bokeh.io import output_file, save\nfrom bokeh.models import CustomJSTickFormatter, FixedTicker, Label, LinearAxis, Range1d, Span\nfrom bokeh.plotting import figure\nfrom PIL import Image\nfrom selenium import webdriver\nfrom selenium.webdriver.chrome.options import Options\n\n\n# Theme tokens\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 palette — semantic assignments for Walter-Lieth\nTEMP_COLOR = \"#AE3030\"  # matte red (heat / temperature)\nPRECIP_COLOR = \"#4467A3\"  # blue (water / precipitation)\n\n# Data — Mediterranean coastal station \"Valora\" (fictional), 1991-2020 normals\nSTATION_NAME = \"Valora\"\nELEVATION_M = 85\ntemperature = np.array([9.2, 10.1, 12.5, 15.3, 18.9, 23.5, 26.8, 27.1, 23.8, 18.5, 13.2, 10.0])\nprecipitation = np.array([62.0, 48.0, 41.0, 38.0, 26.0, 8.0, 3.0, 5.0, 28.0, 65.0, 72.0, 71.0])\n\nannual_mean_temp = temperature.mean()\nannual_total_precip = precipitation.sum()\n\n# Walter-Lieth scaling: 10 deg-C <-> 20 mm  =>  temp-scale precip = precip / 2\nx_idx = np.arange(12, dtype=float)\ntemp_equiv = precipitation / 2.0  # precipitation mapped to left (temp) axis\n\n# Fine interpolation for smooth fill polygons at curve crossings\nx_fine = np.linspace(0, 11, 1200)\ntemp_fine = np.interp(x_fine, x_idx, temperature)\ntequiv_fine = np.interp(x_fine, x_idx, temp_equiv)\n\n# Humid fill: where temp_equiv > temp (precipitation curve above temperature line)\nhumid_mask = tequiv_fine > temp_fine\ny1_humid = np.where(humid_mask, tequiv_fine, temp_fine)\ny2_humid = temp_fine\n\n# Arid fill: where temp > temp_equiv (temperature line above precipitation curve)\narid_mask = temp_fine > tequiv_fine\ny1_arid = np.where(arid_mask, temp_fine, tequiv_fine)\ny2_arid = tequiv_fine\n\n# Axis ranges — left (temperature) and right (precipitation = 2x temperature)\nT_MIN, T_MAX = -5.0, 40.0\nP_MIN, P_MAX = T_MIN * 2, T_MAX * 2  # -10, 80 mm\n\n# Title length scaling\ntitle_str = \"climograph-walter-lieth · python · bokeh · anyplot.ai\"\nn = len(title_str)\nratio = 67 / n if n > 67 else 1.0\ntitle_size = f\"{max(34, round(50 * ratio))}pt\"\n\n# Build figure\np = figure(\n    width=3200,\n    height=1800,\n    x_range=Range1d(-0.5, 11.5),\n    y_range=Range1d(T_MIN, T_MAX),\n    toolbar_location=None,\n    min_border_bottom=160,\n    min_border_left=200,\n    min_border_top=110,\n    min_border_right=230,\n)\n\n# Theme\np.background_fill_color = PAGE_BG\np.border_fill_color = PAGE_BG\np.outline_line_color = None\n\n# Precipitation right axis (range 2x the temperature range for 1:2 Walter-Lieth scaling)\np.extra_y_ranges = {\"precip\": Range1d(P_MIN, P_MAX)}\nright_ax = LinearAxis(\n    y_range_name=\"precip\",\n    axis_label=\"Precipitation (mm)\",\n    ticker=FixedTicker(ticks=[0, 20, 40, 60, 80]),\n    axis_label_text_font_size=\"42pt\",\n    axis_label_text_color=PRECIP_COLOR,\n    major_label_text_font_size=\"34pt\",\n    major_label_text_color=PRECIP_COLOR,\n    axis_line_color=PRECIP_COLOR,\n    major_tick_line_color=PRECIP_COLOR,\n    minor_tick_line_color=PRECIP_COLOR,\n)\np.add_layout(right_ax, \"right\")\n\n# Humid fill (blue — precipitation curve above temperature line)\n# Higher alpha in dark theme to compensate for low contrast on near-black background\nHUMID_ALPHA = 0.32 if THEME == \"dark\" else 0.20\nARID_ALPHA = 0.38 if THEME == \"dark\" else 0.20\np.varea(x=x_fine, y1=y1_humid, y2=y2_humid, fill_color=PRECIP_COLOR, fill_alpha=HUMID_ALPHA)\n\n# Arid fill (red — temperature line above precipitation curve)\np.varea(x=x_fine, y1=y1_arid, y2=y2_arid, fill_color=TEMP_COLOR, fill_alpha=ARID_ALPHA)\n\n# Precipitation line (plotted in temperature-scale units on left axis)\np.line(x=x_idx, y=temp_equiv, line_color=PRECIP_COLOR, line_width=5, legend_label=\"Precipitation (mm)\")\n\n# Temperature line\np.line(x=x_idx, y=temperature, line_color=TEMP_COLOR, line_width=5, legend_label=\"Temperature (°C)\")\n\n# Dots on lines for month markers\np.scatter(x=x_idx, y=temperature, fill_color=TEMP_COLOR, line_color=PAGE_BG, size=16)\np.scatter(x=x_idx, y=temp_equiv, fill_color=PRECIP_COLOR, line_color=PAGE_BG, size=16)\n\n# Zero-degree reference line\np.add_layout(Span(location=0, dimension=\"width\", line_color=INK_SOFT, line_width=2, line_dash=\"dashed\"))\n\n# Frost indicator — blue blocks below x-axis for months with mean temp < 0\nfor i, t in enumerate(temperature):\n    if t < 0:\n        p.varea(x=[i - 0.45, i + 0.45], y1=[T_MIN, T_MIN], y2=[0.0, 0.0], fill_color=PRECIP_COLOR, fill_alpha=0.45)\n\n# Title\np.title.text = title_str\np.title.text_font_size = title_size\np.title.text_color = INK\np.title.text_font_style = \"bold\"\n\n# Station metadata annotation — placed inside the plot at the top center\n# (the area above y≈28°C in summer months is free of data in a Mediterranean chart)\nstation_header = (\n    f\"{STATION_NAME}  ·  {ELEVATION_M} m a.s.l.  ·  \"\n    f\"T = {annual_mean_temp:.1f}°C  ·  \"\n    f\"P = {annual_total_precip:.0f} mm/yr  ·  1991-2020\"\n)\np.add_layout(\n    Label(\n        x=0.1,\n        y=37.0,\n        text=station_header,\n        text_font_size=\"32pt\",\n        text_color=INK,\n        background_fill_color=ELEVATED_BG,\n        background_fill_alpha=0.90,\n        border_line_color=INK_SOFT,\n        border_line_alpha=0.50,\n        padding=12,\n    )\n)\n\n# Left axis (temperature — colored red to match line)\np.yaxis.axis_label = \"Temperature (°C)\"\np.yaxis.axis_label_text_font_size = \"42pt\"\np.yaxis.axis_label_text_color = TEMP_COLOR\np.yaxis.major_label_text_font_size = \"34pt\"\np.yaxis.major_label_text_color = TEMP_COLOR\np.yaxis.axis_line_color = TEMP_COLOR\np.yaxis.major_tick_line_color = TEMP_COLOR\np.yaxis.minor_tick_line_color = TEMP_COLOR\np.yaxis.ticker = FixedTicker(ticks=[0, 10, 20, 30, 40])\n\n# X-axis (months)\np.xaxis.axis_label = \"Month\"\np.xaxis.axis_label_text_font_size = \"42pt\"\np.xaxis.axis_label_text_color = INK\np.xaxis.major_label_text_font_size = \"34pt\"\np.xaxis.major_label_text_color = INK_SOFT\np.xaxis.axis_line_color = INK_SOFT\np.xaxis.major_tick_line_color = INK_SOFT\np.xaxis.minor_tick_line_color = INK_SOFT\np.xaxis.ticker = FixedTicker(ticks=list(range(12)))\np.xaxis.formatter = CustomJSTickFormatter(\n    code=\"\"\"\n    const labels = ['Jan','Feb','Mar','Apr','May','Jun',\n                    'Jul','Aug','Sep','Oct','Nov','Dec'];\n    return (tick >= 0 && tick <= 11) ? labels[tick] : '';\n\"\"\"\n)\n\n# Grid\np.xgrid.grid_line_color = INK\np.xgrid.grid_line_alpha = 0.10\np.xgrid.grid_line_width = 1\np.ygrid.grid_line_color = INK\np.ygrid.grid_line_alpha = 0.10\np.ygrid.grid_line_width = 1\n\n# Legend\np.legend.location = \"top_right\"\np.legend.background_fill_color = ELEVATED_BG\np.legend.border_line_color = INK_SOFT\np.legend.label_text_color = INK_SOFT\np.legend.label_text_font_size = \"34pt\"\np.legend.padding = 20\np.legend.spacing = 14\n\n# Humid/arid period labels inside plot (bottom center)\np.add_layout(\n    Label(\n        x=2.8,\n        y=T_MIN + 1.5,\n        text=\"humid\",\n        text_font_size=\"30pt\",\n        text_color=PRECIP_COLOR,\n        text_alpha=0.70,\n        text_font_style=\"italic\",\n    )\n)\np.add_layout(\n    Label(\n        x=5.6,\n        y=T_MIN + 1.5,\n        text=\"arid\",\n        text_font_size=\"30pt\",\n        text_color=TEMP_COLOR,\n        text_alpha=0.70,\n        text_font_style=\"italic\",\n    )\n)\n\n# Save interactive HTML\noutput_file(f\"plot-{THEME}.html\")\nsave(p)\n\n# Screenshot via Selenium — window taller than target so browser overhead\n# does not clip the figure; PIL crops to the exact 3200x1800 canvas.\nFIG_W, FIG_H = 3200, 1800\nWIN_W, WIN_H = FIG_W, FIG_H + 200\nopts = Options()\nfor arg in (\n    \"--headless=new\",\n    \"--no-sandbox\",\n    \"--disable-dev-shm-usage\",\n    \"--disable-gpu\",\n    f\"--window-size={WIN_W},{WIN_H}\",\n    \"--hide-scrollbars\",\n):\n    opts.add_argument(arg)\ndriver = webdriver.Chrome(options=opts)\ndriver.set_window_size(WIN_W, WIN_H)\ndriver.get(f\"file://{Path(f'plot-{THEME}.html').resolve()}\")\ntime.sleep(3)\nraw = driver.get_screenshot_as_png()\ndriver.quit()\nImage.open(io.BytesIO(raw)).crop((0, 0, FIG_W, FIG_H)).save(f\"plot-{THEME}.png\")\n"}