{"spec_id":"contour-map-geographic","library":"altair","language":"python","code":"\"\"\" anyplot.ai\ncontour-map-geographic: Contour Lines on Geographic Map\nLibrary: altair 6.1.0 | Python 3.13.13\nQuality: 81/100 | Updated: 2026-05-20\n\"\"\"\n\nimport os\nimport sys\n\n\n# Prevent altair.py (this file) from shadowing the installed altair package\n_script_dir = os.path.dirname(os.path.abspath(__file__))\nsys.path = [p for p in sys.path if p and os.path.abspath(p) != _script_dir]\n\nimport altair as alt\nimport numpy as np\nimport pandas as pd\nfrom contourpy import contour_generator\nfrom PIL import Image\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\"\nBASEMAP_STROKE = \"#888880\" if THEME == \"light\" else \"#666660\"\n\n# Data — Mercator-uniform latitude grid eliminates stripe artefacts\nnp.random.seed(42)\n\n# Convert latitude bounds to Mercator y, space uniformly, invert back to lat\n_y_lo = np.log(np.tan(np.pi / 4 + 30 * np.pi / 360))\n_y_hi = np.log(np.tan(np.pi / 4 + 72 * np.pi / 360))\n_y_vals = np.linspace(_y_lo, _y_hi, 110)\nlat_range = 2 * (np.arctan(np.exp(_y_vals)) - np.pi / 4) * 180 / np.pi\n\nlon_range = np.linspace(-25, 55, 160)\nlon_grid, lat_grid = np.meshgrid(lon_range, lat_range)\n\ntemperature = (\n    30\n    - 0.6 * (lat_grid - 30)\n    + 3 * np.sin((lon_grid + 10) / 15)\n    + 2 * np.cos(lat_grid / 10)\n    - 5 * np.exp(-((lat_grid - 47) ** 2 + (lon_grid - 10) ** 2) / 100)\n    - 3 * np.exp(-((lat_grid - 65) ** 2 + (lon_grid - 25) ** 2) / 150)\n    + np.random.normal(0, 0.3, lon_grid.shape)\n)\ntemperature = np.clip(temperature, -15, 35)\n\ndf_fill = pd.DataFrame(\n    {\"longitude\": lon_grid.flatten(), \"latitude\": lat_grid.flatten(), \"temperature\": temperature.flatten()}\n)\n\n# True contour paths from contourpy (operates in geographic coordinate space)\ncontour_levels = [-5, 0, 5, 10, 15, 20, 25]\ngen = contour_generator(x=lon_range, y=lat_range, z=temperature)\n\nline_rows = []\nseg_counter = 0\nfor level in contour_levels:\n    for seg in gen.lines(level):\n        if len(seg) >= 3:\n            for order, (lon, lat) in enumerate(seg):\n                line_rows.append(\n                    {\n                        \"longitude\": float(lon),\n                        \"latitude\": float(lat),\n                        \"level\": float(level),\n                        \"seg_id\": seg_counter,\n                        \"order\": order,\n                    }\n                )\n            seg_counter += 1\n\nline_df = pd.DataFrame(line_rows).sort_values([\"seg_id\", \"order\"])\n\n# One contour label per level: pick the point nearest lon=15°E within visible range\n# Visible latitude range is approx 36-64°N at scale=400, center=(15°N,52°N), height=310\nlabel_rows = []\nfor lvl in [-5, 5, 15, 25]:\n    subset = line_df[\n        (line_df[\"level\"] == float(lvl))\n        & (line_df[\"longitude\"] > 5)\n        & (line_df[\"longitude\"] < 40)\n        & (line_df[\"latitude\"] > 35)\n        & (line_df[\"latitude\"] < 64)\n    ]\n    if len(subset) > 0:\n        idx = (subset[\"longitude\"] - 15).abs().idxmin()\n        row = line_df.loc[idx]\n        label_rows.append(\n            {\"longitude\": float(row[\"longitude\"]), \"latitude\": float(row[\"latitude\"]), \"label\": f\"{int(lvl)}°C\"}\n        )\n\nlabel_df = pd.DataFrame(label_rows)\n\n# Chart construction\nW, H = 600, 310\nproj = {\"type\": \"mercator\", \"scale\": 400, \"center\": [15, 52]}\n\ncountries = alt.topo_feature(\"https://cdn.jsdelivr.net/npm/world-atlas@2/countries-110m.json\", \"countries\")\n\n# Temperature raster — mark_square size tuned to Mercator-uniform y step\n# y_step = (y_hi - y_lo) / 109 * scale = 1.294/109*400 ≈ 4.75 px; size=40 (side=6.3) covers it\nheat = (\n    alt.Chart(df_fill)\n    .mark_square(size=40, opacity=0.90)\n    .encode(\n        longitude=\"longitude:Q\",\n        latitude=\"latitude:Q\",\n        color=alt.Color(\n            \"temperature:Q\",\n            scale=alt.Scale(scheme=\"brownbluegreen\", domain=[-10, 30]),\n            legend=alt.Legend(\n                title=\"Temp (°C)\",\n                titleFontSize=12,\n                labelFontSize=10,\n                gradientLength=200,\n                gradientThickness=16,\n                orient=\"right\",\n            ),\n        ),\n        tooltip=[\n            alt.Tooltip(\"longitude:Q\", format=\".1f\", title=\"Lon\"),\n            alt.Tooltip(\"latitude:Q\", format=\".1f\", title=\"Lat\"),\n            alt.Tooltip(\"temperature:Q\", format=\".1f\", title=\"Temp (°C)\"),\n        ],\n    )\n    .project(**proj)\n    .properties(width=W, height=H)\n)\n\n# Country borders overlay on top of temperature fill for geographic context\nborders = (\n    alt.Chart(countries)\n    .mark_geoshape(filled=False, stroke=BASEMAP_STROKE, strokeWidth=0.7)\n    .project(**proj)\n    .properties(width=W, height=H)\n)\n\n# True smooth contour isolines\nisolines = (\n    alt.Chart(line_df)\n    .mark_line(color=INK, strokeWidth=0.9, opacity=0.75)\n    .encode(longitude=\"longitude:Q\", latitude=\"latitude:Q\", detail=\"seg_id:N\", order=\"order:Q\")\n    .project(**proj)\n    .properties(width=W, height=H)\n)\n\n# Temperature labels at selected contour levels\niso_labels = (\n    alt.Chart(label_df)\n    .mark_text(fontSize=13, fontWeight=\"bold\", fill=INK, stroke=PAGE_BG, strokeWidth=2)\n    .encode(longitude=\"longitude:Q\", latitude=\"latitude:Q\", text=\"label:N\")\n    .project(**proj)\n    .properties(width=W, height=H)\n)\n\nchart = (\n    alt.layer(heat, borders, isolines, iso_labels)\n    .properties(\n        width=W,\n        height=H,\n        background=PAGE_BG,\n        title=alt.Title(\n            \"contour-map-geographic · python · altair · anyplot.ai\", fontSize=16, anchor=\"middle\", color=INK\n        ),\n    )\n    .configure_view(fill=PAGE_BG, stroke=INK_SOFT, strokeWidth=0.5)\n    .configure_axis(labelColor=INK_SOFT, titleColor=INK)\n    .configure_legend(fillColor=ELEVATED_BG, strokeColor=INK_SOFT, labelColor=INK_SOFT, titleColor=INK)\n)\n\n# Save PNG then pad to exact 3200×1800\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(f\"vl-convert produced {_w}×{_h}, exceeds {TW}×{TH}. Shrink chart width/height and re-render.\")\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"}