2025, Oct 31 23:02
Cartopy प्रोजेक्शन में xlim/ylim के बिना टाइट, कॉम्पैक्ट मैप
matplotlib और cartopy के साथ प्रोजेक्शन मैप में अनचाही खाली जगह हटाएँ: xlim/ylim से बचें, ax.set_global व bbox_inches='tight' से कॉम्पैक्ट फ़िगर पाएँ.
matplotlib और cartopy के साथ आकाशीय मानचित्र बनाते समय एक आम झुंझलाहट यह होती है कि प्रोजेक्शन और कलरबार के चारों ओर बहुत खाली जगह रह जाती है। कैनवस कभी पूरी तरह नहीं भरता, और पैडिंग घटाने से भी फ़ायदा नहीं होता। यह गाइड बताती है कि ऐसा क्यों होता है और हर प्रोजेक्शन के लिए मार्जिन का अंदाजा लगाए बिना कॉम्पैक्ट फ़िगर कैसे बनाएँ।
बड़े मार्जिन कैसे बनते हैं
जब आप map axes पर xlim और ylim सेट करके फ़िगर सेव करते हैं, तो समस्या साफ दिखती है। मानचित्र चारों ओर से पैड हो जाता है, और वही लेआउट होने पर भी अलग‑अलग प्रोजेक्शन अलग मार्जिन पैदा करते हैं।
import cartopy.crs as ccrs
import matplotlib.pyplot as plt
import numpy as np
from matplotlib.layout_engine import ConstrainedLayoutEngine
def run():
astro_globe = ccrs.Globe(datum=None, ellipse=None,
semimajor_axis=180/np.pi,
semiminor_axis=180/np.pi)
data_crs = ccrs.PlateCarree(globe=astro_globe)
lon_grid, lat_grid = np.mgrid[-179.5:180:1, -89.5:90:1]
noise_img = perlin2d(lon_grid.shape, (1, 1))
for tag, projection_cls in [("ee", ccrs.EqualEarth),
("mw", ccrs.Mollweide),
("lc", ccrs.LambertCylindrical)]:
try:
fig_obj, map_ax = plt.subplots(
figsize=(20, 10),
layout=ConstrainedLayoutEngine(
h_pad=0, w_pad=0, hspace=0, wspace=0
),
subplot_kw={
"xlim": (-180, 180),
"ylim": (-90, 90),
"projection": projection_cls(globe=astro_globe)
},
)
cset = map_ax.contourf(lon_grid, lat_grid, noise_img,
transform=data_crs,
cmap="Greys")
fig_obj.colorbar(cset, shrink=0.5, pad=0.02)
fig_obj.savefig(f"layout_test_{tag}.png")
finally:
plt.close(fig_obj)
def perlin2d(shape, res):
def ease(t):
return 6*t**5 - 15*t**4 + 10*t**3
delta = (res[0] / shape[0], res[1] / shape[1])
d = (shape[0] // res[0], shape[1] // res[1])
grid = np.mgrid[0:res[0]:delta[0],0:res[1]:delta[1]].transpose(1, 2, 0) % 1
angles = 2*np.pi*np.random.rand(res[0]+1, res[1]+1)
grads = np.dstack((np.cos(angles), np.sin(angles)))
g00 = grads[0:-1,0:-1].repeat(d[0], 0).repeat(d[1], 1)
g10 = grads[1:,0:-1].repeat(d[0], 0).repeat(d[1], 1)
g01 = grads[0:-1,1:].repeat(d[0], 0).repeat(d[1], 1)
g11 = grads[1:,1:].repeat(d[0], 0).repeat(d[1], 1)
n00 = np.sum(grid * g00, 2)
n10 = np.sum(np.dstack((grid[:,:,0]-1, grid[:,:,1])) * g10, 2)
n01 = np.sum(np.dstack((grid[:,:,0], grid[:,:,1]-1)) * g01, 2)
n11 = np.sum(np.dstack((grid[:,:,0]-1, grid[:,:,1]-1)) * g11, 2)
t = ease(grid)
n0 = n00*(1-t[:,:,0]) + t[:,:,0]*n10
n1 = n01*(1-t[:,:,0]) + t[:,:,0]*n11
return np.sqrt(2)*((1-t[:,:,1])*n0 + t[:,:,1]*n1)
run()
वास्तव में खाली जगह क्यों पड़ती है
मुख्य गलती यह है कि cartopy के GeoAxes पर xlim और ylim देशांतर और अक्षांश नहीं, बल्कि प्रोजेक्शन स्पेस में अक्षों के कार्टेशियन निर्देशांकों को संदर्भित करते हैं। यह इंटरैक्टिव मोड में आसानी से दिखता है। plt.show के साथ फ़िगर खोलें और कैनवस पर कर्सर घुमाएँ। GUI दो तरह के मान दिखाता है: अक्षों के निर्देशांकों में कच्चे x और y, और उनके अनुरूप longitude और latitude। QtAgg में ये ऊपर दाएँ दिखते हैं, जबकि TkAgg में नीचे दाएँ। EqualEarth के लिए, मैप का फुटप्रिंट लगभग x में ±155 और y में ±75 तक फैला होता है। x को ±180 और y को ±90 पर मजबूर करना प्रोजेक्शन के फुटप्रिंट से मेल नहीं खाता, इसलिए प्रोजेक्टेड आकृति को बनाए रखने के लिए अतिरिक्त स्पेसिंग जुड़ जाती है। यही अवांछित क्षैतिज खाली जगह और अलग‑अलग प्रोजेक्शन में असंगत मार्जिन की वजह है। जो शेष ऊर्ध्वाधर गैप दिखता है, वह इसलिए क्योंकि फ़िगर का बाउंडिंग बॉक्स अभी भी कैनवस पर मौजूद सभी आर्टिस्ट्स के संयुक्त क्षेत्र से बड़ा है।
टाइट, कॉम्पैक्ट मैप के लिए उपाय
xlim और ylim को मजबूर करना बंद करें, अक्षों से पूरे ग्लोब को दिखाने के लिए कहें, और सेव करते समय ऐसा बाउंडिंग बॉक्स इस्तेमाल करें जो कंटेंट से चिपक कर रहे। साफ‑साफ लिमिट हटाने से क्षैतिज पैडिंग गायब हो जाती है। ax.set_global के इस्तेमाल से प्रोजेक्शन भूमध्य रेखा के पास चपटी हुई झलक के बजाय पूरा मानचित्र दिखाता है। bbox_inches='tight' के साथ सेव करने पर फ़िगर आर्टिस्ट्स तक ट्रिम हो जाता है, जिससे बची‑खुची ऊर्ध्वाधर खाली जगह भी हट जाती है।
import cartopy.crs as ccrs
import matplotlib.pyplot as plt
import numpy as np
from matplotlib.layout_engine import ConstrainedLayoutEngine
def run():
astro_globe = ccrs.Globe(datum=None, ellipse=None,
semimajor_axis=180/np.pi,
semiminor_axis=180/np.pi)
data_crs = ccrs.PlateCarree(globe=astro_globe)
lon_grid, lat_grid = np.mgrid[-179.5:180:1, -89.5:90:1]
noise_img = perlin2d(lon_grid.shape, (1, 1))
for tag, projection_cls in [("ee", ccrs.EqualEarth),
("mw", ccrs.Mollweide),
("lc", ccrs.LambertCylindrical)]:
try:
fig_obj, map_ax = plt.subplots(
figsize=(20, 10),
layout=ConstrainedLayoutEngine(
h_pad=0, w_pad=0, hspace=0, wspace=0
),
subplot_kw={
"projection": projection_cls(globe=astro_globe)
},
)
map_ax.set_global()
cset = map_ax.contourf(lon_grid, lat_grid, noise_img,
transform=data_crs,
cmap="Greys")
fig_obj.colorbar(cset, shrink=0.5, pad=0.02)
fig_obj.savefig(
f"layout_test_{tag}.png",
bbox_inches="tight",
pad_inches="layout",
)
finally:
plt.close(fig_obj)
def perlin2d(shape, res):
def ease(t):
return 6*t**5 - 15*t**4 + 10*t**3
delta = (res[0] / shape[0], res[1] / shape[1])
d = (shape[0] // res[0], shape[1] // res[1])
grid = np.mgrid[0:res[0]:delta[0],0:res[1]:delta[1]].transpose(1, 2, 0) % 1
angles = 2*np.pi*np.random.rand(res[0]+1, res[1]+1)
grads = np.dstack((np.cos(angles), np.sin(angles)))
g00 = grads[0:-1,0:-1].repeat(d[0], 0).repeat(d[1], 1)
g10 = grads[1:,0:-1].repeat(d[0], 0).repeat(d[1], 1)
g01 = grads[0:-1,1:].repeat(d[0], 0).repeat(d[1], 1)
g11 = grads[1:,1:].repeat(d[0], 0).repeat(d[1], 1)
n00 = np.sum(grid * g00, 2)
n10 = np.sum(np.dstack((grid[:,:,0]-1, grid[:,:,1])) * g10, 2)
n01 = np.sum(np.dstack((grid[:,:,0], grid[:,:,1]-1)) * g01, 2)
n11 = np.sum(np.dstack((grid[:,:,0]-1, grid[:,:,1]-1)) * g11, 2)
t = ease(grid)
n0 = n00*(1-t[:,:,0]) + t[:,:,0]*n10
n1 = n01*(1-t[:,:,0]) + t[:,:,0]*n11
return np.sqrt(2)*((1-t[:,:,1])*n0 + t[:,:,1]*n1)
run()
यह समझना क्यों ज़रूरी है
Cartopy की projections अपने अलग कार्टेशियन समन्वय स्थान में काम करती हैं, जो देशांतर और अक्षांश से रैखिक रूप से नहीं मिलते। xlim और ylim को lon/lat सीमाएँ मान लेना ऐसे extent थोपता है जो प्रोजेक्शन के स्वाभाविक फुटप्रिंट से मेल नहीं खाते। नतीजा यह होता है कि लेआउट इंजन अनुपात बचाने की कोशिश में अतिरिक्त खाली जगह जोड़ देता है। इस फ़र्क को पहचानना आपको कम्पोज़िशन पर भरोसेमंद नियंत्रण देता है। GUI में कर्सर रीडआउट के जरिए x और y की तुलना lon और lat से करना यही भेद साफ कर देता है। EqualEarth में दिखने वाला फुटप्रिंट लगभग ±155 × ±75 है, इसलिए ±180 × ±90 देना पैडिंग के बिना संभव ही नहीं।
अंतिम सुझाव
आकाशीय मानचित्रों को हर प्रोजेक्शन में कॉम्पैक्ट रखने के लिए GeoAxes पर xlim/ylim मैन्युअली सेट न करें, ax.set_global से पूरा ग्लोब माँगें, और सेव करते समय bbox_inches="tight" को बची हुई जगह काटने दें। यह संयोजन असंगत मार्जिन हटाता है, कलरबार के साथ भी ठीक काम करता है, और हर प्रोजेक्शन के लिए जादुई संख्याएँ ट्यून करने की जरूरत नहीं होती। अगर आपको देखना हो कि axes निर्देशांक lon और lat से कैसे संबंधित हैं, तो फ़िगर इंटरैक्टिव रूप से खोलें और कर्सर घुमाते समय दोनों रीडआउट देखें।