{"spec_id":"climograph-walter-lieth","library":"altair","language":"python","code":"\"\"\" anyplot.ai\nclimograph-walter-lieth: Walter-Lieth Climate Diagram\nLibrary: altair 6.2.1 | Python 3.13.13\nQuality: 86/100 | Created: 2026-06-15\n\"\"\"\n\nimport importlib\nimport os\nimport sys\n\n\n# Drop script directory from sys.path so `altair` resolves to the package, not this file\nsys.path[:] = [p for p in sys.path if os.path.abspath(p or \".\") != os.path.dirname(os.path.abspath(__file__))]\nalt = importlib.import_module(\"altair\")\nnp = importlib.import_module(\"numpy\")\npd = importlib.import_module(\"pandas\")\nImage = importlib.import_module(\"PIL.Image\")\n\n# Theme tokens — Imprint palette chrome\nTHEME = os.getenv(\"ANYPLOT_THEME\", \"light\")\n\n# Theme-conditional fill opacities — dark background needs higher opacity for visibility\nARID_OPACITY = 0.45 if THEME == \"dark\" else 0.22\nHUMID_OPACITY = 0.40 if THEME == \"dark\" else 0.22\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# Semantic colour assignments (Walter-Lieth convention)\n# Imprint position 5 (matte red) — temperature/heat\nTEMP_COLOR = \"#AE3030\"\n# Imprint position 3 (blue) — precipitation/water\nPRECIP_COLOR = \"#4467A3\"\n# Imprint position 6 (cyan) — frost/ice\nFROST_COLOR = \"#2ABCCD\"\n\n# Station: Ankara, Turkey — climate normals 1991–2020\nSTATION = \"Ankara, Turkey\"\nELEVATION = 891\nPERIOD = \"1991–2020\"\nMONTHS = [\"Jan\", \"Feb\", \"Mar\", \"Apr\", \"May\", \"Jun\", \"Jul\", \"Aug\", \"Sep\", \"Oct\", \"Nov\", \"Dec\"]\n\ntemp_vals = np.array([-0.2, 1.3, 5.8, 11.3, 16.0, 20.0, 23.4, 23.4, 18.3, 12.5, 6.3, 2.0])\nprecip_vals = np.array([39, 32, 37, 43, 51, 37, 15, 10, 19, 28, 37, 43])\n\nannual_temp = round(float(np.mean(temp_vals)), 1)\nannual_precip = int(np.sum(precip_vals))\n\n# Walter-Lieth: 10 °C ↔ 20 mm — divide precip by 2 for temp-scale comparison\nprecip_t = precip_vals / 2.0\nPERHUMID_THRESH = 50.0  # °C-equiv of 100 mm\n\n# Y-axis domains (1:2 ratio enforced between temp and precip axes)\nTEMP_MIN, TEMP_MAX = -10, 40\nPRECIP_MIN, PRECIP_MAX = -20, 80  # exactly 2× temp domain\n\n# Build DataFrame\ndf = pd.DataFrame({\"month\": MONTHS, \"temp\": temp_vals, \"precip\": precip_vals.astype(float), \"precip_t\": precip_t})\n\n# Fill zones in temperature-equivalent units\narid_mask = df[\"temp\"] > df[\"precip_t\"]\nhumid_mask = df[\"precip_t\"] > df[\"temp\"]\nperhumid_mask = df[\"precip_t\"] > PERHUMID_THRESH\n\ndf[\"arid_top\"] = np.where(arid_mask, df[\"temp\"], np.nan)\ndf[\"arid_bot\"] = np.where(arid_mask, df[\"precip_t\"], np.nan)\ndf[\"humid_top\"] = np.where(humid_mask, np.minimum(df[\"precip_t\"], PERHUMID_THRESH), np.nan)\ndf[\"humid_bot\"] = np.where(humid_mask, df[\"temp\"], np.nan)\ndf[\"phum_top\"] = np.where(perhumid_mask, df[\"precip_t\"], np.nan)\ndf[\"phum_bot\"] = np.where(perhumid_mask, PERHUMID_THRESH, np.nan)\n# Frost indicator with minimum visual height (2 °C) so near-zero months remain legible\nFROST_MIN_H = 2.0\ndf[\"frost_top\"] = np.where(df[\"temp\"] < 0, 0.0, np.nan)\ndf[\"frost_bot\"] = np.where(df[\"temp\"] < 0, np.minimum(df[\"temp\"], -FROST_MIN_H), np.nan)\n\n# Scales\ntemp_scale = alt.Scale(domain=[TEMP_MIN, TEMP_MAX])\nprecip_scale = alt.Scale(domain=[PRECIP_MIN, PRECIP_MAX])\n\n# Shared x encoding\nx_enc = alt.X(\n    \"month:O\",\n    sort=MONTHS,\n    axis=alt.Axis(title=None, labelFontSize=10, labelColor=INK_SOFT, domainColor=INK_SOFT, tickColor=INK_SOFT),\n)\n\n# Left Y-axis — defined by the temperature-line layer\nleft_axis = alt.Axis(\n    title=\"Temperature (°C)\",\n    titleColor=TEMP_COLOR,\n    orient=\"left\",\n    labelColor=INK_SOFT,\n    domainColor=INK_SOFT,\n    tickColor=INK_SOFT,\n    gridColor=INK,\n    gridOpacity=0.1,\n    values=[-10, 0, 10, 20, 30, 40],\n    labelFontSize=10,\n    titleFontSize=11,\n)\n\n# Right Y-axis — defined by the invisible anchor layer\nright_axis = alt.Axis(\n    title=\"Precipitation (mm)\",\n    titleColor=PRECIP_COLOR,\n    orient=\"right\",\n    labelColor=INK_SOFT,\n    domainColor=INK_SOFT,\n    tickColor=INK_SOFT,\n    gridOpacity=0,\n    values=[0, 20, 40, 60, 80],\n    labelFontSize=10,\n    titleFontSize=11,\n)\n\nbase = alt.Chart(df)\n\n# Frost indicator — cyan bars below 0 °C for frost months\nfrost_fill = base.mark_bar(color=FROST_COLOR, opacity=0.45, width={\"band\": 0.9}).encode(\n    x=x_enc, y=alt.Y(\"frost_bot:Q\", scale=temp_scale, axis=None), y2=alt.Y2(\"frost_top:Q\")\n)\n\n# Arid fill — red area between temperature (top) and precip_t (bottom)\narid_fill = base.mark_area(color=TEMP_COLOR, opacity=ARID_OPACITY).encode(\n    x=x_enc, y=alt.Y(\"arid_bot:Q\", scale=temp_scale, axis=None), y2=alt.Y2(\"arid_top:Q\")\n)\n\n# Humid fill — blue area between precip_t (top) and temperature (bottom), ≤ 100 mm\nhumid_fill = base.mark_area(color=PRECIP_COLOR, opacity=HUMID_OPACITY).encode(\n    x=x_enc, y=alt.Y(\"humid_bot:Q\", scale=temp_scale, axis=None), y2=alt.Y2(\"humid_top:Q\")\n)\n\n# Perhumid fill — solid blue area above 100 mm threshold (compressed scale zone)\nphum_fill = base.mark_area(color=PRECIP_COLOR, opacity=0.65).encode(\n    x=x_enc, y=alt.Y(\"phum_bot:Q\", scale=temp_scale, axis=None), y2=alt.Y2(\"phum_top:Q\")\n)\n\n# 0 °C reference line\nzero_rule = (\n    alt.Chart(pd.DataFrame({\"y\": [0.0]}))\n    .mark_rule(color=INK_SOFT, strokeWidth=0.9, strokeDash=[4, 4])\n    .encode(y=alt.Y(\"y:Q\", scale=temp_scale, axis=None))\n)\n\n# Precipitation curve drawn on temperature-equivalent scale\nprecip_line = base.mark_line(color=PRECIP_COLOR, strokeWidth=2.8, point=alt.OverlayMarkDef(size=40)).encode(\n    x=x_enc,\n    y=alt.Y(\"precip_t:Q\", scale=temp_scale, axis=None),\n    tooltip=[alt.Tooltip(\"month:O\", title=\"Month\"), alt.Tooltip(\"precip:Q\", title=\"Precipitation (mm)\", format=\".0f\")],\n)\n\n# Temperature curve — also anchors the left Y-axis definition\ntemp_line = base.mark_line(color=TEMP_COLOR, strokeWidth=2.8, point=alt.OverlayMarkDef(size=40)).encode(\n    x=x_enc,\n    y=alt.Y(\"temp:Q\", scale=temp_scale, axis=left_axis),\n    tooltip=[alt.Tooltip(\"month:O\", title=\"Month\"), alt.Tooltip(\"temp:Q\", title=\"Temperature (°C)\", format=\".1f\")],\n)\n\n# Invisible layer that anchors the right precipitation axis\nright_layer = base.mark_point(opacity=0, size=0).encode(\n    x=x_enc, y=alt.Y(\"precip:Q\", scale=precip_scale, axis=right_axis)\n)\n\n# Title\ntitle_str = \"climograph-walter-lieth · python · altair · anyplot.ai\"\nn_chars = len(title_str)\ntitle_fs = round(16 * min(1.0, 67 / n_chars))\nsubtitle_str = f\"{STATION}  ·  {ELEVATION} m a.s.l.  ·  {PERIOD}  ·  T̄ = {annual_temp} °C  ·  P = {annual_precip} mm\"\n\n# Flat layer: all temp-scale layers use axis=None (except temp_line which defines\n# the left Y-axis); right_layer carries the independent right Y-axis.\n# resolve_scale(y='independent') gives each layer its own Y scale — layers with\n# axis=None show nothing; only temp_line (left) and right_layer (right) show axes.\nchart = (\n    alt.layer(frost_fill, arid_fill, humid_fill, phum_fill, zero_rule, precip_line, temp_line, right_layer)\n    .resolve_scale(y=\"independent\")\n    .properties(\n        width=620,\n        height=300,\n        background=PAGE_BG,\n        title=alt.Title(\n            text=title_str,\n            subtitle=subtitle_str,\n            fontSize=title_fs,\n            subtitleFontSize=10,\n            color=INK,\n            subtitleColor=INK_MUTED,\n            anchor=\"start\",\n        ),\n    )\n    .configure_view(fill=PAGE_BG, stroke=INK_SOFT, strokeWidth=0.5)\n    .configure_axis(labelColor=INK_SOFT, titleColor=INK, domainColor=INK_SOFT, tickColor=INK_SOFT)\n    .configure_legend(fillColor=ELEVATED_BG, strokeColor=INK_SOFT, labelColor=INK_SOFT, titleColor=INK)\n)\n\n# Save — pad to exact 3200 × 1800 target\nTW, TH = 3200, 1800\nchart.save(f\"plot-{THEME}.png\", scale_factor=4.0)\n\n_img = Image.open(f\"plot-{THEME}.png\").convert(\"RGB\")\n_w, _h = _img.size\nif _w > TW or _h > TH:\n    raise SystemExit(\n        f\"altair vl-convert produced {_w}×{_h}, exceeds target {TW}×{TH}. \"\n        f\"Shrink chart .properties(width=, height=) values and re-render.\"\n    )\nif _w < TW or _h < TH:\n    _canvas = Image.new(\"RGB\", (TW, TH), PAGE_BG)\n    _canvas.paste(_img, ((TW - _w) // 2, (TH - _h) // 2))\n    _canvas.save(f\"plot-{THEME}.png\")\n\nchart.save(f\"plot-{THEME}.html\")\n"}