This gist contains a purely conceptual map projection used for
#30DayMapChallenge 2025 — Day 19 (Projections).
The idea is to take standard geographic coordinates (longitude, latitude) and wrap the Earth into a spiral / nautilus shell:
- South Pole → spiral core
- North Pole → outer ring
- Parallels become spiral arms
- Meridians get bent and twisted as they move outward
Nothing is preserved (not area, not distance, not direction) — this is deliberately a “projection as art / thought experiment” rather than a useful cartographic projection.
Let:
- Longitude
$( \lambda \in [-180^\circ, 180^\circ] )$ - Latitude
$( \varphi \in [-90^\circ, 90^\circ] )$
We first normalize latitude to a unit interval:
This gives:
-
$( u = 0 )$ at the South Pole -
$( u = 0.5 )$ at the Equator -
$( u = 1 )$ at the North Pole
Radius is then defined as:
Where:
-
R_SCALE= overall radius of the map -
EXPONENT = αcontrols how quickly radius grows with latitude-
$( \alpha < 1 )$ : more space for mid / high latitudes -
$( \alpha > 1 )$ : more emphasis near the South Pole
-
Angle has two parts:
- A standard azimuthal term from longitude:
- A latitude-dependent twist that makes parallels spiral instead of staying circular:
Where:
SPIRAL_TURNS = Tis the number of extra rotations from South → NorthT = 0→ standard azimuthal-like mappingT > 0→ nautilus-style spiral
Finally, spiral-plane coordinates are:
This mapping is implemented in lonlat_to_spiral():
def lonlat_to_spiral(lon, lat, r_scale=R_SCALE, exponent=EXPONENT, turns=SPIRAL_TURNS):
lon = np.asarray(lon)
lat = np.asarray(lat)
# 1) normalize latitude
u = (lat + 90.0) / 180.0
u = np.clip(u, 0.0, 1.0)
# 2) radius
r = (u ** exponent) * r_scale
# 3) base angle from longitude
theta_lon = np.deg2rad(lon) + np.pi
# 4) extra spiral twist
theta = theta_lon + 2 * np.pi * turns * u
# 5) cartesian coords
x = r * np.cos(theta)
y = r * np.sin(theta)
return x, yInstead of bundling shapefiles or calling shapereader directly, the script uses
cartopy.feature.NaturalEarthFeature to fetch and cache the Natural Earth
admin-0 countries dataset (1:110m). The geometries are then wrapped in a
GeoDataFrame:
from cartopy import feature as cfeature
ne_countries = cfeature.NaturalEarthFeature(
category="cultural",
name="admin_0_countries",
scale="110m",
)
geoms = list(ne_countries.geometries())
world = gpd.GeoDataFrame({"geometry": geoms}, crs="EPSG:4326")To make the spiral more intuitive, each country is coloured by the latitude of its centroid:
- Reproject to a projected CRS (World Cylindrical Equal Area)
- Compute centroids in projected space
- Transform centroids back to EPSG:4326 and read their latitude
world_proj = world.to_crs("ESRI:54034")
cent_proj = world_proj.geometry.centroid
cent_ll = gpd.GeoSeries(cent_proj, crs=world_proj.crs).to_crs("EPSG:4326")
world["lat_centroid"] = cent_ll.yIn the plot, lat_centroid is mapped to a coolwarm colour scale and a
colorbar is added as a legend (Latitude (°)).
shapely.ops.transform is used to wrap the lonlat_to_spiral() function and
apply it to each geometry:
from shapely.ops import transform
def spiral_geometry(geom):
def _f(x, y, z=None):
xs, ys = lonlat_to_spiral(x, y)
return xs, ys
return transform(_f, geom)
world_spiral = world.copy()
world_spiral["geometry"] = world_spiral["geometry"].apply(spiral_geometry)Once projected, the geometries plot like any other GeoPandas GeoDataFrame.
A simple synthetic graticule (parallels and meridians) is constructed in geographic coordinates, then passed through the same transformation:
def build_graticule():
grat_lines = []
# Parallels
for lat in range(-80, 81, 20):
lons = np.linspace(-180, 180, 361)
lats = np.full_like(lons, lat, dtype=float)
x, y = lonlat_to_spiral(lons, lats)
grat_lines.append((x, y))
# Meridians
for lon in range(-180, 181, 30):
lats = np.linspace(-90, 90, 361)
lons = np.full_like(lats, lon, dtype=float)
x, y = lonlat_to_spiral(lons, lats)
grat_lines.append((x, y))
return grat_linesBecause of the spiral angle term, the parallels curl into spiral arms, and the meridians bend and twist as they move outward.
The final plot uses:
- Dark background (
fig.patch.set_facecolor("black")) world_spiral.plot(...)to draw the countries- Light, semi-transparent graticule lines
- A colourbar for latitude
- Text labels for South Pole (core) and North Pole (outer ring)
- Title, subtitle, and bottom attribution
The output is saved as day19_projections.png.
Required Python packages:
numpymatplotlibgeopandasshapelycartopy
Run:
python day19_spiral_earth.py(or execute the cell if you drop it into a Jupyter notebook).
The script will:
- Fetch Natural Earth via
cartopy.feature.NaturalEarthFeature(first run only) - Apply the Spiral Earth projection
- Produce
day19_projections.pngin the working directory.
At the top of the script:
R_SCALE = 10.0
EXPONENT = 0.7
SPIRAL_TURNS = 1.25You can experiment with:
SPIRAL_TURNS— tighter or looser spiral coils (e.g. 1.0, 2.0, 3.0)EXPONENT— redistribute radial space between low and high latitudes- Colormap — swap
"coolwarm"for another Matplotlib colormap
These changes let you keep the same basic idea (latitude → radius, spiral angle) while exploring different visual behaviors of the projection.