2025, Oct 16 02:32
दो सबप्लॉट्स के ऊपर केंद्र-संरेखित साझा लीजेंड: Matplotlib में भरोसेमंद तरीका
Matplotlib में कई सबप्लॉट्स के लिए केंद्र-संरेखित साझा लीजेंड का सही तरीका। constrained layout और bbox_inches=tight के साथ स्थिर प्लेसमेंट के टिप्स जानें.
कई सबप्लॉट्स के लिए एक ही साझा लीजेंड इस तरह रखना, कि वह प्लॉट क्षेत्रों की चौड़ाई के साथ बिल्कुल मेल खाए, सुनने में आसान लगता है — जब तक कि फ़िगर का लेआउट आपके पैरों तले खिसकना न शुरू कर दे। अगर आप लीजेंड के लिए फ़िगर-रिलेटिव bbox पर निर्भर हैं, तो Matplotlib के लेआउट इंजन सेव के दौरान axes को खिसका सकते हैं और यहाँ तक कि फ़िगर का आकार भी बदल सकते हैं, और नतीजतन लीजेंड मनचाही जगह पर नहीं पहुँचता।
हमें क्या चाहिए और यह क्यों बिगड़ता है
लक्ष्य है दो सबप्लॉट्स के ऊपर एक साझा, केंद्र-संरेखित लीजेंड, जिसकी चौड़ाई सबप्लॉट्स के ड्रॉइंग क्षेत्रों की संयुक्त चौड़ाई के बराबर हो। फ़िगर-स्तर का लीजेंड, सावधानी से गणित किए गए bbox_to_anchor के साथ, देखने में ठीक लग सकता है। लेकिन जब layout="constrained" सक्षम हो और savefig में bbox_inches="tight" इस्तेमाल किया जाए, तो Matplotlib सेव के समय axes की पोज़िशन और फ़िगर का साइज समायोजित कर देता है। फ़िगर-कोऑर्डिनेट्स से ऐंकर किया गया लीजेंड फिर ग़लत संरेखित हो जाता है और जरूरत से ज्यादा चौड़ा दिखता है।
समस्या दिखाने वाला न्यूनतम उदाहरण
नीचे दिया स्निपेट सबप्लॉट्स की चौड़ाई को कवर करने वाला bbox निकालता है और उसे फ़िगर-स्तरीय लीजेंड में उपयोग करता है। यह पहलू अनुपात बनाए रखते हुए कई आकारों में सेव भी करता है।
import matplotlib.pyplot as plt
canvas, ax_grid = plt.subplots(nrows=1, ncols=2, figsize=(7, 4), layout="constrained")
# नमूना डेटा
xs = [1, 2, 3, 4, 5]
ys_a = [1, 2, 3, 4, 5]
ys_b = [5, 4, 3, 2, 1]
ax_grid[0].plot(xs, ys_a, label="Line 1")
ax_grid[1].plot(xs, ys_a, label="Line 1")
ax_grid[1].plot(xs, ys_b, label="Line 2")
def compute_span_bbox(ax_list):
    # संयुक्त सबप्लॉट चौड़ाई को ढकने वाला फ़िगर-निर्देशांक bbox
    rects = [ax.get_position() for ax in ax_list]
    if rects:
        left = min(r.xmin for r in rects)
        right = max(r.xmax for r in rects)
        return (left, 0.0, right - left, 1.0)
    return None
# सभी axes से अद्वितीय हैंडल/लेबल इकट्ठा करें
all_handles, all_labels = [], []
for ax in ax_grid:
    hs, lbs = ax.get_legend_handles_labels()
    for h, lb in zip(hs, lbs):
        if lb not in all_labels:
            all_handles.append(h)
            all_labels.append(lb)
# सबप्लॉट चौड़ाई पर फैला हुआ, फ़िगर-स्तरीय लीजेंड
canvas.legend(
    all_handles,
    all_labels,
    loc="outside upper center",
    ncol=2,
    bbox_to_anchor=compute_span_bbox(ax_grid),
    mode="expand",
    borderaxespad=-1,
    columnspacing=1.0,
    handletextpad=0.4,
)
# एकाधिक आकार सहेजें, आस्पेक्ट अनुपात बरकरार रखते हुए
sizes = [None, 3.16, 4.21]
base_w, base_h = canvas.get_size_inches()
ratio = base_h / base_w
for w in sizes:
    if w is not None:
        canvas.set_size_inches(w, w * ratio)
        canvas.legends[0].set_bbox_to_anchor(compute_span_bbox(ax_grid))
    canvas.savefig(
        f"mvr_figure_{w}.png" if w is not None else "mvr_figure.png",
        bbox_inches="tight",
    )
आपका सावधानी से निकाला गया bbox लीजेंड क्यों नहीं मानता
दो बातें मिलकर यह खिसकाव पैदा करती हैं। पहली, constrained layout उपयोग करने पर Matplotlib savefig के दौरान axes की पोज़िशन बदलता है ताकि फ़िगर की उपलब्ध जगह का बेहतर इस्तेमाल हो। जब आपने bbox निकाला, उस समय दिख रही axes पोज़िशन रेंडर के समय अंतिम पोज़िशन नहीं होती। दूसरी, bbox_inches="tight" के साथ सेव करने से फ़िगर का आकार और अनुपात कलाकारों को कसकर लपेटने के लिए बदल जाता है। तब फ़िगर-कोऑर्डिनेट्स में ऐंकर किया लीजेंड उसी बदले हुए फ़िगर के सापेक्ष रखा जाता है। अगर आप layout="constrained" हटाएँ और bbox_inches="tight" का उपयोग बंद करें, तो लीजेंड bbox के बताए स्थान पर तो पहुँच जाएगा—पर फ़िगर से आंशिक रूप से बाहर निकल सकता है, और यही वजह है कि ये लेआउट विकल्प आमतौर पर उपयोग किए जाते हैं।
मजबूत समाधान: लीजेंड को उसका अपना axes दें
लेआउट सिस्टम से लड़ने के बजाय उसे अपने पक्ष में काम करने दें। सिर्फ लीजेंड के लिए एक अलग axes जोड़ें। तब constrained layout तीनों axes—लीजेंड कंटेनर और दोनों प्लॉटिंग axes—को एकसमान ढंग से व्यवस्थित करेगा। लीजेंड अपने axes को भरता है, प्लॉट्स की चौड़ाई के बराबर फैलता है, और आकार बदलने पर भी केंद्रित रहता है।
import matplotlib.pyplot as plt
board, panel_map = plt.subplot_mosaic(
    "LL;AB", figsize=(7, 4), layout="constrained", height_ratios=[1, 10]
)
# नमूना डेटा
xs = [1, 2, 3, 4, 5]
ys_a = [1, 2, 3, 4, 5]
ys_b = [5, 4, 3, 2, 1]
panel_map["A"].plot(xs, ys_a, label="Line 1")
panel_map["B"].plot(xs, ys_a, label="Line 1")
panel_map["B"].plot(xs, ys_b, label="Line 2")
# L में लीजेंड के लिए A और B से अद्वितीय हैंडल/लेबल एकत्र करें
legend_handles, legend_labels = [], []
for key in "AB":
    hs, lbs = panel_map[key].get_legend_handles_labels()
    for h, lb in zip(hs, lbs):
        if lb not in legend_labels:
            legend_handles.append(h)
            legend_labels.append(lb)
# लीजेंड को उसके axes के भीतर रखें और उसे फैलने दें
panel_map["L"].legend(
    legend_handles,
    legend_labels,
    ncol=2,
    loc="center",
    mode="expand",
    borderaxespad=0,
    handletextpad=0.4,
)
# लीजेंड axes का फ्रेम छिपाएँ
panel_map["L"].axis("off")
# एकाधिक आकार सहेजें, आस्पेक्ट अनुपात बनाए रखते हुए
sizes = [None, 3.16, 4.21]
base_w, base_h = board.get_size_inches()
ratio = base_h / base_w
for w in sizes:
    if w is not None:
        board.set_size_inches(w, w * ratio)
    board.savefig(
        f"mvr_figure_{w}.png" if w is not None else "mvr_figure.png",
    )
बहुत छोटे फ़िगर एक्सपोर्ट करते समय, constrained layout चेतावनी दे सकता है कि axes के आकार सिकुड़ गए हैं। अत्यधिक छोटा करने पर यह सामान्य है:
UserWarning: constrained_layout लागू नहीं हुआ क्योंकि Axes के आकार शून्य तक सिमट गए। फ़िगर बड़ा करने या Axes की सजावट को छोटा करने का प्रयास करें।
बाकी आकार अपेक्षित रूप से रेंडर होते हैं।
यह समझना क्यों उपयोगी है
लीजेंड प्लेसमेंट का लेआउट और एक्सपोर्ट से सूक्ष्म तालमेल होता है। constrained layout savefig के दौरान axes को समायोजित करता है, और bbox_inches="tight" कलाकारों के अनुरूप कैनवस का आकार बदल देता है। फ़िगर-कोऑर्डिनेट्स में रखे गए किसी भी आर्टिस्ट—जिसमें bbox से ऐंकर किए लीजेंड भी शामिल हैं—अंतिम आउटपुट में खिसक सकते हैं। एक लीजेंड-केवल axes इस समस्या को दरकिनार कर देता है और लेआउट इंजन को सभी पोज़िशन सुसंगत रूप से मैनेज करने देता है।
निष्कर्ष
अगर आपको ऐसा साझा लीजेंड चाहिए जो आपके सबप्लॉट ग्रिड से सटीकता से मेल खाए, तो लीजेंड को लेआउट का प्रथम-श्रेणी सदस्य मानें। उसे अलग axes में रखें, constrained layout को स्पेसिंग सँभालने दें, और ऐसे फ़िगर-रिलेटिव ऐंकर पर निर्भर न रहें जो सेव के दौरान बेमानी हो सकता है। अगर ऐंकर को डिबग करना ही पड़े, तो पहले बिना constrained layout और बिना tight bounding के जाँच लें कि ऐंकर सही है, फिर प्रोडक्शन फ़िगर्स के लिए लीजेंड-axes वाला तरीका अपनाएँ।
यह लेख StackOverflow पर एक प्रश्न (लेखक: BernhardWebstudio) और RuthC के उत्तर पर आधारित है।