{"spec_id":"contour-map-geographic","library":"letsplot","language":"python","code":"\"\"\" anyplot.ai\ncontour-map-geographic: Contour Lines on Geographic Map\nLibrary: letsplot 4.10.1 | Python 3.13.13\nQuality: 86/100 | Updated: 2026-05-20\n\"\"\"\n\nimport os\n\nimport numpy as np\nimport pandas as pd\nfrom lets_plot import (\n    LetsPlot,\n    aes,\n    coord_fixed,\n    element_blank,\n    element_line,\n    element_rect,\n    element_text,\n    geom_contour,\n    geom_contourf,\n    geom_path,\n    geom_point,\n    geom_text,\n    ggplot,\n    ggsize,\n    labs,\n    scale_fill_gradient2,\n    theme,\n)\nfrom lets_plot.export import ggsave\n\n\nLetsPlot.setup_html()\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\"\nCONTOUR_COLOR = \"#333333\" if THEME == \"light\" else \"#CCCCCC\"\nLABEL_COLOR = \"#1A1A17\" if THEME == \"light\" else \"#F0EFE8\"\nCOAST_COLOR = \"#4467A3\"  # Okabe-Ito blue — works on both themes\nGRID_MAJOR = \"#CCCAC3\" if THEME == \"light\" else \"#2E2E2B\"\nGRID_MINOR = \"#E0DDD7\" if THEME == \"light\" else \"#252522\"\nCITY_COLOR = \"#C475FD\"  # Okabe-Ito vermillion — reference cities\nANNO_BG = \"#FFFDF6\" if THEME == \"light\" else \"#242420\"\n\n# Data — synthetic elevation for Western US mountain region\nnp.random.seed(42)\n\nlat_range = np.linspace(35, 45, 50)\nlon_range = np.linspace(-120, -105, 60)\nlon_grid, lat_grid = np.meshgrid(lon_range, lat_range)\n\nelevation = np.zeros_like(lat_grid)\n\npeaks = [\n    (40.5, -111.5, 2800, 2.0),  # Wasatch Range near Salt Lake City\n    (39.0, -114.0, 2600, 1.8),  # Nevada peak (Great Basin)\n    (43.5, -110.5, 3200, 2.5),  # Tetons area\n    (37.5, -118.5, 3500, 2.2),  # Sierra Nevada\n    (41.0, -106.5, 2900, 2.0),  # Rocky Mountains\n]\n\nfor peak_lat, peak_lon, peak_height, spread in peaks:\n    distance = np.sqrt((lat_grid - peak_lat) ** 2 + (lon_grid - peak_lon) ** 2)\n    elevation += peak_height * np.exp(-(distance**2) / (2 * spread**2))\n\nelevation += 1200 + np.random.randn(*elevation.shape) * 30\n\ndf = pd.DataFrame({\"lon\": lon_grid.flatten(), \"lat\": lat_grid.flatten(), \"elevation\": elevation.flatten()})\n\nelev_min = int(np.floor(elevation.min() / 500) * 500)\nelev_max = int(np.ceil(elevation.max() / 500) * 500)\nelev_mid = (elev_min + elev_max) / 2\n\n# Contour labels at 500m intervals for better readability\ncontour_levels = [1500, 2000, 2500, 3000, 3500, 4000, 4500, 5000]\n\nlabel_data = []\nused_positions = []\n\nfor level in contour_levels:\n    candidates = df[\n        (abs(df[\"elevation\"] - level) < 80)\n        & (df[\"lon\"] > lon_range[0] + 2.0)  # 2° left buffer to prevent text clipping\n        & (df[\"lon\"] < lon_range[-1] - 7.0)  # 7° right buffer to avoid panel clipping\n        & (df[\"lat\"] > lat_range[0] + 0.5)\n        & (df[\"lat\"] < lat_range[-1] - 0.5)\n    ].copy()\n    if len(candidates) == 0:\n        continue\n\n    best_candidate = None\n    best_dist = 0\n\n    for _, row in candidates.iterrows():\n        min_dist = float(\"inf\")\n        for used_lon, used_lat in used_positions:\n            dist = np.sqrt((row[\"lon\"] - used_lon) ** 2 + (row[\"lat\"] - used_lat) ** 2)\n            min_dist = min(min_dist, dist)\n\n        if len(used_positions) == 0 or min_dist > best_dist:\n            best_dist = min_dist\n            best_candidate = row\n\n    if best_candidate is not None and (len(used_positions) == 0 or best_dist > 1.5):\n        label_data.append({\"lon\": best_candidate[\"lon\"], \"lat\": best_candidate[\"lat\"], \"label\": f\"{int(level)}m\"})\n        used_positions.append((best_candidate[\"lon\"], best_candidate[\"lat\"]))\n\nlabel_df = pd.DataFrame(label_data) if label_data else pd.DataFrame({\"lon\": [], \"lat\": [], \"label\": []})\n\n# Geographic features — Pacific coastline (more detail for realism)\ncoast_df = pd.DataFrame(\n    {\n        \"lon\": [-120, -120, -120, -120, -119.8, -119.5, -119.2, -118.8, -118.5, -118.2, -118],\n        \"lat\": [35, 36, 37.5, 39, 40, 41, 42, 43, 43.8, 44.5, 45],\n    }\n)\n\n# State border segments (CA/NV, NV/UT, ID/UT, WY/MT/ID borders)\nborder_segments = [\n    {\"lon\": [-120, -120, -117, -114], \"lat\": [42, 39, 36, 35]},\n    {\"lon\": [-114, -114], \"lat\": [35, 42]},\n    {\"lon\": [-111, -111], \"lat\": [41, 45]},\n    {\"lon\": [-117, -111], \"lat\": [45, 45]},\n]\nborder_dfs = [pd.DataFrame(seg) for seg in border_segments]\n\n# Mountain range name annotations — storytelling layer\nrange_labels = pd.DataFrame(\n    {\n        \"lon\": [-118.8, -111.2, -110.8, -106.8],\n        \"lat\": [37.2, 40.3, 43.8, 41.3],\n        \"label\": [\"Sierra\\nNevada\", \"Wasatch\\nRange\", \"Teton\\nRange\", \"Rocky\\nMts.\"],\n    }\n)\n\n# Reference cities inside the plot bounds for geographic context\ncities = pd.DataFrame(\n    {\n        \"lon\": [-119.81, -115.14, -111.89, -116.20],\n        \"lat\": [39.53, 36.18, 40.76, 43.62],\n        \"label\": [\"Reno\", \"Las Vegas\", \"SLC\", \"Boise\"],\n    }\n)\n\n# Build plot\nplot = (\n    ggplot(df, aes(x=\"lon\", y=\"lat\", z=\"elevation\"))\n    + geom_contourf(aes(fill=\"..level..\"), bins=12, alpha=0.9)\n    + geom_contour(color=CONTOUR_COLOR, size=0.5, bins=12, alpha=0.7)\n    + scale_fill_gradient2(\n        low=\"#1a5e1a\",\n        mid=\"#c4a35a\",\n        high=\"#f5f5f5\",\n        midpoint=elev_mid,\n        limits=[elev_min, elev_max],\n        name=\"Elevation (m)\",\n    )\n    + geom_path(data=coast_df, mapping=aes(x=\"lon\", y=\"lat\"), color=COAST_COLOR, size=2.0, alpha=0.9, inherit_aes=False)\n)\n\nfor border_df in border_dfs:\n    plot = plot + geom_path(\n        data=border_df, mapping=aes(x=\"lon\", y=\"lat\"), color=INK_SOFT, size=0.8, linetype=\"dashed\", inherit_aes=False\n    )\n\nif len(label_df) > 0:\n    plot = plot + geom_text(\n        data=label_df,\n        mapping=aes(x=\"lon\", y=\"lat\", label=\"label\"),\n        color=LABEL_COLOR,\n        size=10,  # increased from 9 for mobile legibility\n        fontface=\"bold\",\n        inherit_aes=False,\n    )\n\n# Mountain range annotations — italic, slightly smaller for hierarchy\nplot = plot + geom_text(\n    data=range_labels,\n    mapping=aes(x=\"lon\", y=\"lat\", label=\"label\"),\n    color=INK_SOFT,\n    size=8,\n    fontface=\"italic\",\n    inherit_aes=False,\n)\n\n# City reference markers + labels\nplot = plot + geom_point(\n    data=cities,\n    mapping=aes(x=\"lon\", y=\"lat\"),\n    color=CITY_COLOR,\n    size=4,\n    shape=21,\n    fill=ANNO_BG,\n    stroke=1.5,\n    inherit_aes=False,\n)\nplot = plot + geom_text(\n    data=cities,\n    mapping=aes(x=\"lon\", y=\"lat\", label=\"label\"),\n    color=CITY_COLOR,\n    size=8,\n    nudge_y=0.4,\n    fontface=\"bold\",\n    inherit_aes=False,\n)\n\n# Theme-adaptive chrome — no heavy base theme; fully custom for map-style clarity\nanyplot_theme = theme(\n    plot_background=element_rect(fill=PAGE_BG, color=PAGE_BG),\n    panel_background=element_rect(fill=PAGE_BG),\n    panel_border=element_blank(),  # geographic maps conventionally use no heavy frame\n    panel_grid_major=element_line(color=GRID_MAJOR, size=0.3),\n    panel_grid_minor=element_line(color=GRID_MINOR, size=0.2),\n    axis_title=element_text(color=INK, size=12),\n    axis_text=element_text(color=INK_SOFT, size=10),\n    axis_line=element_line(color=INK_SOFT, size=0.5),\n    plot_title=element_text(color=INK, size=16),\n    legend_background=element_rect(fill=ELEVATED_BG, color=INK_SOFT),\n    legend_text=element_text(color=INK_SOFT, size=10),\n    legend_title=element_text(color=INK, size=12),\n    legend_position=\"right\",\n)\n\n# Geographic aspect ratio correction for ~40°N latitude\ngeo_ratio = 1.0 / np.cos(np.radians(40.0))\n\nplot = (\n    plot\n    + labs(title=\"contour-map-geographic · python · letsplot · anyplot.ai\", x=\"Longitude (°W)\", y=\"Latitude (°N)\")\n    + anyplot_theme\n    + ggsize(800, 450)\n    + coord_fixed(ratio=geo_ratio)\n)\n\n# Save — theme-suffixed filenames\nggsave(plot, f\"plot-{THEME}.png\", path=\".\", scale=4)\nggsave(plot, f\"plot-{THEME}.html\", path=\".\")\n"}