{"spec_id":"contour-map-geographic","library":"pygal","language":"python","code":"\"\"\" anyplot.ai\ncontour-map-geographic: Contour Lines on Geographic Map\nLibrary: pygal 3.1.0 | Python 3.13.13\nQuality: 87/100 | Updated: 2026-05-20\n\"\"\"\n\nimport os\nimport sys\nfrom pathlib import Path\n\n\nsys.path = [p for p in sys.path if p != str(Path(__file__).parent)]\n\nimport cairosvg\nimport matplotlib.cm as mpl_cm\nimport matplotlib.colors as mpl_colors\nimport numpy as np\nimport pygal as pygal_lib\nfrom pygal.style import Style\n\n\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# Data — North Pacific sea-level pressure field (hPa)\nnp.random.seed(42)\n\nlon_min, lon_max = 120, 240  # 120°E to 120°W\nlat_min, lat_max = 20, 65\n\nn_grid = 40\nlon_grid = np.linspace(lon_min, lon_max, n_grid)\nlat_grid = np.linspace(lat_min, lat_max, n_grid)\nLON, LAT = np.meshgrid(lon_grid, lat_grid)\n\n# Background pressure gradient (higher in subtropics, lower at poles)\npressure_base = 1020 - (LAT - lat_min) * 0.25\n\n# North Pacific High (subtropical high pressure ridge)\nhp_lon, hp_lat = 200, 35\ndist_hp = np.sqrt(((LON - hp_lon) / 25) ** 2 + ((LAT - hp_lat) / 12) ** 2)\npressure_base += 12 * np.exp(-(dist_hp**2))\n\n# Aleutian Low (subpolar low pressure center)\nal_lon, al_lat = 185, 53\ndist_al = np.sqrt(((LON - al_lon) / 20) ** 2 + ((LAT - al_lat) / 10) ** 2)\npressure_base -= 18 * np.exp(-(dist_al**2))\n\n# Secondary low off Asian coast\nal2_lon, al2_lat = 145, 45\ndist_al2 = np.sqrt(((LON - al2_lon) / 15) ** 2 + ((LAT - al2_lat) / 10) ** 2)\npressure_base -= 8 * np.exp(-(dist_al2**2))\n\nPRESSURE = pressure_base + np.random.normal(0, 0.3, pressure_base.shape)\n\n# Isobar levels every 4 hPa\ncontour_levels = np.arange(996, 1033, 4)\n\n# Simplified coastlines: North Pacific rim (lon, lat)\ncoastlines = [\n    # Japan main islands\n    [\n        (130, 31),\n        (131, 33),\n        (130, 34),\n        (132, 34),\n        (135, 35),\n        (137, 37),\n        (140, 38),\n        (141, 40),\n        (141, 43),\n        (145, 44),\n        (141, 45),\n        (130, 31),\n    ],\n    # Kamchatka + Kuril arc\n    [(152, 47), (155, 52), (158, 56), (163, 58), (167, 54), (165, 50), (152, 47)],\n    # Aleutian Islands (simplified)\n    [(167, 54), (172, 53), (178, 52), (188, 52), (195, 54), (200, 57), (210, 56), (220, 57), (225, 56)],\n    # Alaska\n    [(195, 57), (200, 59), (205, 60), (210, 60), (215, 60), (220, 60), (225, 59), (230, 58), (235, 55), (237, 53)],\n    # North American west coast\n    [\n        (237, 53),\n        (236, 50),\n        (235, 48),\n        (234, 47),\n        (235, 45),\n        (236, 42),\n        (236, 38),\n        (238, 35),\n        (240, 32),\n        (240, 25),\n        (237, 22),\n        (234, 20),\n    ],\n    # Asian mainland coast (rough)\n    [\n        (120, 22),\n        (122, 24),\n        (120, 27),\n        (121, 30),\n        (122, 32),\n        (124, 34),\n        (127, 37),\n        (129, 38),\n        (130, 40),\n        (130, 43),\n        (132, 46),\n        (135, 48),\n        (138, 48),\n        (141, 46),\n        (143, 47),\n        (147, 48),\n        (150, 47),\n        (152, 47),\n    ],\n]\n\n# Pressure colormap: viridis sequential (perceptually uniform, style-guide compliant)\np_min_cb, p_max_cb = 996, 1032\nn_colors = 10\npressure_colors = [mpl_colors.to_hex(mpl_cm.viridis(i / (n_colors - 1))) for i in range(n_colors)]\n\ncustom_style = Style(\n    background=PAGE_BG,\n    plot_background=PAGE_BG,\n    foreground=INK,\n    foreground_strong=INK,\n    foreground_subtle=INK_MUTED,\n    guide_stroke_color=\"rgba(128,128,128,0.15)\",\n    guide_stroke_dasharray=\"\",\n    colors=(INK_MUTED,) * (len(coastlines) + 1),\n    title_font_size=52,\n    label_font_size=56,\n    major_label_font_size=44,\n    legend_font_size=44,\n    value_font_size=36,\n    stroke_width=2.5,\n)\n\nchart = pygal_lib.XY(\n    width=3200,\n    height=1800,\n    style=custom_style,\n    title=\"North Pacific Pressure Isobars · contour-map-geographic · python · pygal · anyplot.ai\",\n    x_title=\"Longitude (°E)\",\n    y_title=\"Latitude (°N)\",\n    show_legend=False,\n    stroke=True,\n    dots_size=0,\n    show_x_guides=False,\n    show_y_guides=False,\n    explicit_size=True,\n    print_values=False,\n    xrange=(lon_min, lon_max),\n    range=(lat_min, lat_max),\n    margin=100,\n    margin_top=160,\n    margin_bottom=160,\n    margin_left=240,\n    margin_right=260,\n)\n\nfor coords in coastlines:\n    chart.add(None, coords, stroke=True, dots_size=0, show_dots=False, fill=False)\n\n# Plot area pixel coordinates (matching pygal internal layout)\nplot_x = 240\nplot_y = 160\nplot_width = 3200 - 240 - 260\nplot_height = 1800 - 160 - 160\n\nsvg_parts = []\n\n# Filled pressure cells\ncell_w = plot_width / (n_grid - 1)\ncell_h = plot_height / (n_grid - 1)\n\nfor i in range(n_grid - 1):\n    for j in range(n_grid - 1):\n        avg_p = (PRESSURE[i, j] + PRESSURE[i, j + 1] + PRESSURE[i + 1, j] + PRESSURE[i + 1, j + 1]) / 4\n        color_idx = int((avg_p - p_min_cb) / 4)\n        color_idx = max(0, min(color_idx, len(pressure_colors) - 1))\n        color = pressure_colors[color_idx]\n        px = plot_x + (lon_grid[j] - lon_min) / (lon_max - lon_min) * plot_width\n        py = plot_y + plot_height - (lat_grid[i + 1] - lat_min) / (lat_max - lat_min) * plot_height\n        svg_parts.append(\n            f'<rect x=\"{px:.1f}\" y=\"{py:.1f}\" width=\"{cell_w + 1:.1f}\" '\n            f'height=\"{cell_h + 1:.1f}\" fill=\"{color}\" fill-opacity=\"0.55\" stroke=\"none\"/>'\n        )\n\n# Isobar contour lines via marching squares\nfor level in contour_levels:\n    is_major = level % 8 == 0\n    line_color = INK if is_major else INK_SOFT\n    line_width = 3.5 if is_major else 1.8\n\n    all_segments = []\n    for i in range(n_grid - 1):\n        for j in range(n_grid - 1):\n            z00, z01 = PRESSURE[i, j], PRESSURE[i, j + 1]\n            z10, z11 = PRESSURE[i + 1, j], PRESSURE[i + 1, j + 1]\n\n            case = 0\n            if z00 >= level:\n                case |= 1\n            if z01 >= level:\n                case |= 2\n            if z11 >= level:\n                case |= 4\n            if z10 >= level:\n                case |= 8\n\n            if case == 0 or case == 15:\n                continue\n\n            x0 = plot_x + (lon_grid[j] - lon_min) / (lon_max - lon_min) * plot_width\n            y0 = plot_y + plot_height - (lat_grid[i + 1] - lat_min) / (lat_max - lat_min) * plot_height\n            x1 = plot_x + (lon_grid[j + 1] - lon_min) / (lon_max - lon_min) * plot_width\n            y1 = plot_y + plot_height - (lat_grid[i] - lat_min) / (lat_max - lat_min) * plot_height\n\n            t_left = 0.5 if abs(z10 - z00) < 1e-10 else (level - z00) / (z10 - z00)\n            t_right = 0.5 if abs(z11 - z01) < 1e-10 else (level - z01) / (z11 - z01)\n            t_top = 0.5 if abs(z11 - z10) < 1e-10 else (level - z10) / (z11 - z10)\n            t_bottom = 0.5 if abs(z01 - z00) < 1e-10 else (level - z00) / (z01 - z00)\n\n            left = (x0, y0 - cell_h * t_left)\n            right = (x1, y1 + cell_h * t_right)\n            top = (x0 + cell_w * t_top, y0 - cell_h)\n            bottom = (x0 + cell_w * t_bottom, y0)\n\n            if case in [1, 14]:\n                all_segments.append((left, bottom))\n            elif case in [2, 13]:\n                all_segments.append((bottom, right))\n            elif case in [3, 12]:\n                all_segments.append((left, right))\n            elif case in [4, 11]:\n                all_segments.append((right, top))\n            elif case == 5:\n                all_segments.append((left, top))\n                all_segments.append((bottom, right))\n            elif case in [6, 9]:\n                all_segments.append((bottom, top))\n            elif case in [7, 8]:\n                all_segments.append((left, top))\n            elif case == 10:\n                all_segments.append((left, bottom))\n                all_segments.append((right, top))\n\n    # Chain segments into polylines\n    tolerance = 1.5\n    polylines = []\n    used = [False] * len(all_segments)\n\n    for idx, seg in enumerate(all_segments):\n        if used[idx]:\n            continue\n        used[idx] = True\n        chain = list(seg)\n        extended = True\n        while extended:\n            extended = False\n            for j, other in enumerate(all_segments):\n                if used[j]:\n                    continue\n                p0, p1 = other\n                if abs(chain[-1][0] - p0[0]) < tolerance and abs(chain[-1][1] - p0[1]) < tolerance:\n                    chain.append(p1)\n                    used[j] = True\n                    extended = True\n                elif abs(chain[-1][0] - p1[0]) < tolerance and abs(chain[-1][1] - p1[1]) < tolerance:\n                    chain.append(p0)\n                    used[j] = True\n                    extended = True\n                elif abs(chain[0][0] - p1[0]) < tolerance and abs(chain[0][1] - p1[1]) < tolerance:\n                    chain.insert(0, p0)\n                    used[j] = True\n                    extended = True\n                elif abs(chain[0][0] - p0[0]) < tolerance and abs(chain[0][1] - p0[1]) < tolerance:\n                    chain.insert(0, p1)\n                    used[j] = True\n                    extended = True\n        polylines.append(chain)\n\n    for chain in polylines:\n        if len(chain) < 2:\n            continue\n        path_data = f\"M {chain[0][0]:.1f} {chain[0][1]:.1f}\"\n        for pt in chain[1:]:\n            path_data += f\" L {pt[0]:.1f} {pt[1]:.1f}\"\n        svg_parts.append(\n            f'<path d=\"{path_data}\" fill=\"none\" stroke=\"{line_color}\" '\n            f'stroke-width=\"{line_width}\" stroke-opacity=\"0.9\" stroke-linejoin=\"round\" stroke-linecap=\"round\"/>'\n        )\n\n# Isobar labels at representative positions\nlabel_positions = [\n    (135, 30, 1016),\n    (150, 55, 1004),\n    (185, 52, 1000),\n    (200, 37, 1024),\n    (215, 55, 1008),\n    (230, 45, 1016),\n    (155, 38, 1012),\n]\nfor lon_l, lat_l, pval in label_positions:\n    if lon_min <= lon_l <= lon_max and lat_min <= lat_l <= lat_max:\n        px = plot_x + (lon_l - lon_min) / (lon_max - lon_min) * plot_width\n        py = plot_y + plot_height - (lat_l - lat_min) / (lat_max - lat_min) * plot_height\n        svg_parts.append(\n            f'<rect x=\"{px - 48}\" y=\"{py - 22}\" width=\"96\" height=\"40\" '\n            f'fill=\"{ELEVATED_BG}\" fill-opacity=\"0.90\" rx=\"5\" stroke=\"{INK_MUTED}\" stroke-width=\"1\"/>'\n        )\n        svg_parts.append(\n            f'<text x=\"{px}\" y=\"{py + 7}\" text-anchor=\"middle\" fill=\"{INK}\" '\n            f'style=\"font-size:30px;font-weight:bold;font-family:sans-serif\">{pval}</text>'\n        )\n\n# Colorbar\ncb_width = 48\ncb_height = plot_height * 0.72\ncb_x = plot_x + plot_width + 55\ncb_y = plot_y + (plot_height - cb_height) / 2\n\nn_cb = len(pressure_colors)\nseg_h = cb_height / n_cb\nfor i, color in enumerate(pressure_colors[::-1]):\n    seg_y = cb_y + i * seg_h\n    svg_parts.append(f'<rect x=\"{cb_x}\" y=\"{seg_y:.1f}\" width=\"{cb_width}\" height=\"{seg_h + 1:.1f}\" fill=\"{color}\"/>')\n\nsvg_parts.append(\n    f'<rect x=\"{cb_x}\" y=\"{cb_y}\" width=\"{cb_width}\" height=\"{cb_height}\" fill=\"none\" stroke=\"{INK}\" stroke-width=\"2\"/>'\n)\n\ncb_vals = list(range(p_max_cb, p_min_cb - 1, -4))\nfor i, val in enumerate(cb_vals):\n    label_y = cb_y + i * seg_h + seg_h / 2 + 11\n    svg_parts.append(\n        f'<text x=\"{cb_x + cb_width + 12}\" y=\"{label_y:.1f}\" fill=\"{INK}\" '\n        f'style=\"font-size:30px;font-family:sans-serif\">{val}</text>'\n    )\n\nsvg_parts.append(\n    f'<text x=\"{cb_x + cb_width / 2}\" y=\"{cb_y - 22}\" text-anchor=\"middle\" fill=\"{INK}\" '\n    f'style=\"font-size:32px;font-weight:bold;font-family:sans-serif\">hPa</text>'\n)\n\n# Pressure center labels (H/L): data-encoding colors, domain-conventional\nHIGH_COLOR = \"#C475FD\"  # Okabe-Ito vermillion — high pressure\nLOW_COLOR = \"#4467A3\"  # Okabe-Ito blue — low pressure\ncenters = [(200, 35, \"H\", HIGH_COLOR), (185, 53, \"L\", LOW_COLOR), (145, 45, \"L\", LOW_COLOR)]\nfor lon_c, lat_c, label, color in centers:\n    if lon_min <= lon_c <= lon_max and lat_min <= lat_c <= lat_max:\n        px = plot_x + (lon_c - lon_min) / (lon_max - lon_min) * plot_width\n        py = plot_y + plot_height - (lat_c - lat_min) / (lat_max - lat_min) * plot_height\n        svg_parts.append(\n            f'<text x=\"{px}\" y=\"{py + 18}\" text-anchor=\"middle\" fill=\"{color}\" '\n            f'style=\"font-size:56px;font-weight:bold;font-family:sans-serif;opacity:0.85\">{label}</text>'\n        )\n\ncustom_svg = \"\\n\".join(svg_parts)\n\nchart.add(\"\", [(lon_min, lat_min)])\n\nbase_svg = chart.render(is_unicode=True)\noutput_svg = base_svg.replace(\"</svg>\", f\"{custom_svg}\\n</svg>\")\n\ncairosvg.svg2png(bytestring=output_svg.encode(\"utf-8\"), write_to=f\"plot-{THEME}.png\")\n\nwith open(f\"plot-{THEME}.html\", \"w\") as f:\n    f.write(output_svg)\n"}