Search code examples
matplotlibcartopy

Setting up a map which crosses the dateline in cartopy


I received the following email and wanted to make sure the answer to this question was available to everybody:


Hi,

I would like to setup a simple latitude longitude map, using cartopy, which crosses the dateline and shows east Asia on the left hand side with the west of North America on the right. The following google map is roughly what I am after:

https://maps.google.co.uk/?ll=56.559482,-175.253906&spn=47.333523,133.066406&t=m&z=4

Screenshot of google map

Can this be done with Cartopy?


Solution

  • Good question. This is probably something which will come up time-and-time again, so I will go through this step-by-step before actually answering your specific question. For future reference, the following examples were written with cartopy v0.5.

    Firstly, it is important to note that the default "latitude longitude" (or more technically PlateCarree) projection works in the forward range of -180 to 180. This means that you cannot plot the standard PlateCarree projection beyond this. There are several good reasons for this, most of which boil down to the fact that cartopy would have to do a lot more work when projecting both vectors and rasters (simple coastlines for example). Unfortunately the plot you are trying to produce requires precisely this functionality. To put this limitation into pictures, the default PlateCarree projection looks like:

    import cartopy.crs as ccrs
    import matplotlib.pyplot as plt
    
    proj = ccrs.PlateCarree(central_longitude=0)
    
    ax1 = plt.axes(projection=proj)
    ax1.stock_img()
    plt.title('Global')
    
    plt.show()
    

    global plate carree

    Any single rectangle that you can draw on this map can legally be a zoomed in area (there is some slightly more advanced code in here, but the picture is worth a 1000 words):

    import cartopy.crs as ccrs
    import matplotlib.pyplot as plt
    import shapely.geometry as sgeom
    
    box = sgeom.box(minx=-90, maxx=45, miny=15, maxy=70)
    x0, y0, x1, y1 = box.bounds
    
    proj = ccrs.PlateCarree(central_longitude=0)
    
    ax1 = plt.subplot(211, projection=proj)
    ax1.stock_img()
    ax1.add_geometries([box], proj, facecolor='coral', 
                       edgecolor='black', alpha=0.5)
    plt.title('Global')
    
    ax2 = plt.subplot(212, projection=proj)
    ax2.stock_img()
    ax2.set_extent([x0, x1, y0, y1], proj)
    plt.title('Zoomed in area')
    
    plt.show()
    

    global with a second zoomed in map

    Unfortunately the plot you want would require 2 rectangles with this projection:

    import cartopy.crs as ccrs
    import matplotlib.pyplot as plt
    import shapely.geometry as sgeom
    
    box = sgeom.box(minx=120, maxx=260, miny=15, maxy=80)
    
    proj = ccrs.PlateCarree(central_longitude=0)
    
    ax1 = plt.axes(projection=proj)
    ax1.stock_img()
    ax1.add_geometries([box], proj, facecolor='coral', 
                       edgecolor='black', alpha=0.5)
    plt.title('Target area')
    
    plt.show()
    

    target rectangle on global plot

    Hence it is not possible to draw a map that crosses the dateline using the standard PlateCarree definition. Instead we could change the PlateCarree definition's central longitude to allow a single box to be drawn of the area we are targeting:

    import cartopy.crs as ccrs
    import matplotlib.pyplot as plt
    import shapely.geometry as sgeom
    
    box = sgeom.box(minx=120, maxx=260, miny=15, maxy=80)
    x0, y0, x1, y1 = box.bounds
    
    proj = ccrs.PlateCarree(central_longitude=180)
    box_proj = ccrs.PlateCarree(central_longitude=0)
    
    ax1 = plt.subplot(211, projection=proj)
    ax1.stock_img()
    ax1.add_geometries([box], box_proj, facecolor='coral', 
                       edgecolor='black', alpha=0.5)
    plt.title('Global')
    
    ax2 = plt.subplot(212, projection=proj)
    ax2.stock_img()
    ax2.set_extent([x0, x1, y0, y1], box_proj)
    plt.title('Zoomed in area')
    
    plt.show()
    

    Two PlateCarre plots with a central longitude of 180, one global, one zoomed in to the target area

    Hopefully that shows you what it is you have to do to achieve your target map, the code above might be a little complex to achieve your goal, so to simplify slightly, the code I would write to produce the plot you want would be something like:

    import cartopy.feature
    import cartopy.crs as ccrs
    import matplotlib.pyplot as plt    
    
    ax = plt.axes(projection=ccrs.PlateCarree(central_longitude=180))
    ax.set_extent([120, 260, 15, 80], crs=ccrs.PlateCarree())
    
    # add some features to make the map a little more polished
    ax.add_feature(cartopy.feature.LAND)
    ax.add_feature(cartopy.feature.OCEAN)
    ax.coastlines('50m')
    
    plt.show()
    

    final target map

    This was a long answer, hopefully I have not only answered the question, but made some of the more complex details of map production and cartopy more clear to help smooth any future problems you may have.

    Cheers,