{"spec_id":"map-projections","library":"letsplot","language":"python","code":"\"\"\" anyplot.ai\nmap-projections: World Map with Different Projections\nLibrary: letsplot 4.10.1 | Python 3.13.13\nQuality: 83/100 | Updated: 2026-05-23\n\"\"\"\n\nimport os\n\nimport numpy as np\nimport pandas as pd\nfrom lets_plot import (\n    LetsPlot,\n    aes,\n    coord_map,\n    element_blank,\n    element_rect,\n    element_text,\n    geom_path,\n    geom_point,\n    geom_polygon,\n    geom_text,\n    ggplot,\n    ggsize,\n    labs,\n    scale_color_identity,\n    theme,\n    theme_minimal,\n)\nfrom lets_plot.export import ggsave\nfrom PIL import Image as PILImage\n\n\nLetsPlot.setup_html()\n\nTHEME = os.getenv(\"ANYPLOT_THEME\", \"light\")\nPAGE_BG = \"#FAF8F1\" if THEME == \"light\" else \"#1A1A17\"\nINK = \"#1A1A17\" if THEME == \"light\" else \"#F0EFE8\"\nINK_SOFT = \"#4A4A44\" if THEME == \"light\" else \"#B8B7B0\"\n\nIMPRINT = [\"#009E73\", \"#C475FD\", \"#AE3030\", \"#4467A3\", \"#99B314\", \"#954477\", \"#BD8233\"]\nLAND_COLOR = IMPRINT[0]  # brand green — first categorical series\nTISSOT_COLOR = IMPRINT[1]  # purple — canonical second series\n\nnp.random.seed(42)\n\n# Data: Generate graticule lines (lat/lon grid)\ngraticule_rows = []\nline_id = 0\n\nfor lat in range(-90, 91, 30):\n    lons = np.linspace(-180, 180, 180)\n    for lon in lons:\n        graticule_rows.append({\"lon\": lon, \"lat\": lat, \"line_id\": line_id})\n    line_id += 1\n\nfor lon in range(-180, 181, 60):\n    lats = np.linspace(-90, 90, 180)\n    for lat in lats:\n        graticule_rows.append({\"lon\": lon, \"lat\": lat, \"line_id\": line_id})\n    line_id += 1\n\ngraticule = pd.DataFrame(graticule_rows)\n\n# Data: Continent outlines\ncontinent_rows = []\n\nafrica_coords = [\n    (-17, 15),\n    (-5, 5),\n    (10, 4),\n    (12, 0),\n    (42, 12),\n    (51, 11),\n    (51, -1),\n    (40, -11),\n    (35, -22),\n    (27, -33),\n    (18, -35),\n    (15, -30),\n    (12, -18),\n    (20, -20),\n    (35, -10),\n    (40, 0),\n    (30, 5),\n    (35, 15),\n    (30, 30),\n    (10, 37),\n    (0, 36),\n    (-5, 35),\n    (-17, 15),\n]\nfor lon, lat in africa_coords:\n    continent_rows.append({\"lon\": lon, \"lat\": lat, \"continent\": \"Africa\"})\n\neurope_coords = [\n    (-10, 36),\n    (-8, 43),\n    (0, 43),\n    (3, 42),\n    (8, 44),\n    (13, 45),\n    (14, 42),\n    (19, 42),\n    (25, 37),\n    (28, 41),\n    (40, 41),\n    (50, 45),\n    (55, 50),\n    (60, 55),\n    (50, 60),\n    (60, 70),\n    (25, 71),\n    (15, 68),\n    (5, 62),\n    (10, 55),\n    (8, 52),\n    (-5, 50),\n    (-10, 44),\n    (-10, 36),\n]\nfor lon, lat in europe_coords:\n    continent_rows.append({\"lon\": lon, \"lat\": lat, \"continent\": \"Europe\"})\n\nasia_coords = [\n    (60, 35),\n    (70, 25),\n    (75, 15),\n    (80, 8),\n    (95, 6),\n    (103, 2),\n    (105, 12),\n    (110, 20),\n    (120, 22),\n    (125, 32),\n    (130, 35),\n    (135, 35),\n    (140, 40),\n    (145, 45),\n    (142, 52),\n    (145, 60),\n    (160, 62),\n    (170, 65),\n    (180, 68),\n    (170, 70),\n    (140, 70),\n    (120, 72),\n    (100, 72),\n    (80, 68),\n    (65, 55),\n    (50, 45),\n    (55, 38),\n    (60, 35),\n]\nfor lon, lat in asia_coords:\n    continent_rows.append({\"lon\": lon, \"lat\": lat, \"continent\": \"Asia\"})\n\nna_coords = [\n    (-170, 66),\n    (-165, 62),\n    (-140, 60),\n    (-130, 55),\n    (-125, 48),\n    (-124, 42),\n    (-118, 34),\n    (-105, 25),\n    (-97, 26),\n    (-85, 22),\n    (-82, 23),\n    (-80, 25),\n    (-83, 30),\n    (-88, 30),\n    (-92, 29),\n    (-97, 28),\n    (-97, 24),\n    (-92, 18),\n    (-87, 15),\n    (-82, 9),\n    (-78, 9),\n    (-75, 10),\n    (-72, 12),\n    (-62, 10),\n    (-60, 14),\n    (-62, 18),\n    (-65, 18),\n    (-67, 18),\n    (-72, 21),\n    (-75, 20),\n    (-80, 23),\n    (-80, 25),\n    (-82, 30),\n    (-76, 35),\n    (-75, 38),\n    (-72, 41),\n    (-70, 44),\n    (-67, 45),\n    (-65, 48),\n    (-70, 47),\n    (-60, 47),\n    (-55, 50),\n    (-58, 52),\n    (-63, 58),\n    (-70, 60),\n    (-85, 65),\n    (-95, 68),\n    (-110, 70),\n    (-130, 70),\n    (-145, 68),\n    (-155, 70),\n    (-165, 65),\n    (-170, 66),\n]\nfor lon, lat in na_coords:\n    continent_rows.append({\"lon\": lon, \"lat\": lat, \"continent\": \"North America\"})\n\nsa_coords = [\n    (-80, 10),\n    (-75, 10),\n    (-70, 12),\n    (-62, 10),\n    (-60, 5),\n    (-52, 4),\n    (-50, 0),\n    (-48, -2),\n    (-44, -3),\n    (-38, -5),\n    (-35, -8),\n    (-35, -15),\n    (-38, -18),\n    (-40, -22),\n    (-43, -23),\n    (-47, -25),\n    (-48, -28),\n    (-52, -33),\n    (-58, -38),\n    (-65, -42),\n    (-68, -48),\n    (-72, -52),\n    (-68, -55),\n    (-64, -55),\n    (-58, -52),\n    (-65, -45),\n    (-70, -38),\n    (-72, -30),\n    (-70, -20),\n    (-75, -15),\n    (-81, -5),\n    (-80, 0),\n    (-78, 2),\n    (-77, 8),\n    (-80, 10),\n]\nfor lon, lat in sa_coords:\n    continent_rows.append({\"lon\": lon, \"lat\": lat, \"continent\": \"South America\"})\n\naus_coords = [\n    (115, -20),\n    (120, -18),\n    (128, -15),\n    (130, -12),\n    (135, -12),\n    (137, -16),\n    (140, -17),\n    (142, -11),\n    (145, -15),\n    (148, -20),\n    (153, -25),\n    (153, -30),\n    (151, -34),\n    (147, -38),\n    (143, -38),\n    (138, -35),\n    (130, -32),\n    (125, -35),\n    (118, -35),\n    (114, -32),\n    (114, -26),\n    (115, -20),\n]\nfor lon, lat in aus_coords:\n    continent_rows.append({\"lon\": lon, \"lat\": lat, \"continent\": \"Australia\"})\n\nantarctica_coords = [\n    (-60, -62),\n    (-45, -68),\n    (-20, -70),\n    (0, -70),\n    (30, -68),\n    (60, -66),\n    (90, -66),\n    (120, -66),\n    (150, -67),\n    (170, -72),\n    (180, -78),\n    (170, -80),\n    (140, -75),\n    (100, -74),\n    (60, -72),\n    (30, -74),\n    (0, -78),\n    (-30, -76),\n    (-60, -74),\n    (-90, -74),\n    (-120, -72),\n    (-150, -74),\n    (-170, -78),\n    (-180, -76),\n    (-160, -68),\n    (-130, -64),\n    (-100, -66),\n    (-80, -63),\n    (-60, -62),\n]\nfor lon, lat in antarctica_coords:\n    continent_rows.append({\"lon\": lon, \"lat\": lat, \"continent\": \"Antarctica\"})\n\ncontinents = pd.DataFrame(continent_rows)\n\n# Data: Tissot indicatrices — equal-area circles reveal Mercator distortion\ntissot_rows = []\ncircle_id = 0\nbase_radius = 8\n\nfor lat in [-60, -30, 0, 30, 60]:\n    for lon in range(-150, 151, 50):\n        angles = np.linspace(0, 2 * np.pi, 72)\n        for angle in angles:\n            dlat = base_radius * np.cos(angle)\n            dlon = base_radius * np.sin(angle) / max(np.cos(np.radians(lat)), 0.5)\n            tissot_rows.append({\"lon\": lon + dlon, \"lat\": lat + dlat, \"circle_id\": circle_id})\n        circle_id += 1\n\ntissot = pd.DataFrame(tissot_rows)\n\n# Manual legend in South Atlantic gap between lat=-30 and lat=-60 Tissot rows\nlegend_data = pd.DataFrame(\n    {\n        \"x\": [-170, -170],\n        \"y\": [-43, -51],\n        \"label\": [\"Land Masses\", \"Tissot Indicatrices\"],\n        \"color\": [LAND_COLOR, TISSOT_COLOR],\n    }\n)\n\n# Plot\nplot = (\n    ggplot()\n    + geom_path(\n        data=graticule,\n        mapping=aes(x=\"lon\", y=\"lat\", group=\"line_id\"),\n        color=INK_SOFT,\n        size=0.3,\n        linetype=\"dashed\",\n        alpha=0.4,\n    )\n    + geom_polygon(\n        data=continents,\n        mapping=aes(x=\"lon\", y=\"lat\", group=\"continent\"),\n        fill=LAND_COLOR,\n        color=INK,\n        alpha=0.85,\n        size=0.5,\n    )\n    + geom_polygon(\n        data=tissot,\n        mapping=aes(x=\"lon\", y=\"lat\", group=\"circle_id\"),\n        fill=TISSOT_COLOR,\n        color=INK,\n        alpha=0.65,\n        size=0.3,\n    )\n    + geom_point(data=legend_data, mapping=aes(x=\"x\", y=\"y\", color=\"color\"), size=5, shape=15)\n    + scale_color_identity()\n    + geom_text(data=legend_data, mapping=aes(x=\"x\", y=\"y\", label=\"label\"), hjust=0, nudge_x=8, size=8, color=INK_SOFT)\n    + coord_map(xlim=[-180, 180], ylim=[-85, 85])\n    + labs(\n        title=\"map-projections · python · letsplot · anyplot.ai\",\n        x=\"Longitude (°)\",\n        y=\"Latitude (°)\",\n        caption=\"Tissot indicatrices: equal-area circles grow toward the poles in Mercator projection\",\n    )\n    + theme_minimal()\n    + theme(\n        plot_title=element_text(size=16, face=\"bold\", hjust=0.5, color=INK),\n        axis_title=element_text(size=12, color=INK),\n        axis_text=element_text(size=10, color=INK_SOFT),\n        plot_caption=element_text(size=9, hjust=0.5, color=INK_SOFT),\n        legend_position=\"none\",\n        panel_grid_major=element_blank(),\n        panel_grid_minor=element_blank(),\n        plot_background=element_rect(fill=PAGE_BG, color=PAGE_BG),\n        panel_background=element_rect(fill=PAGE_BG),\n    )\n    + ggsize(800, 450)\n)\n\n# Save\nggsave(plot, filename=f\"plot-{THEME}.png\", path=\".\", scale=4)\nggsave(plot, filename=f\"plot-{THEME}.html\", path=\".\")\n\n# Flatten transparent margins to solid PAGE_BG (lets-plot leaves outer margins transparent)\nimg = PILImage.open(f\"plot-{THEME}.png\").convert(\"RGBA\")\nbg = PILImage.new(\"RGBA\", img.size, PAGE_BG)\nbg.paste(img, mask=img.split()[3])\nbg.convert(\"RGB\").save(f\"plot-{THEME}.png\")\n"}