Keywords: Matplotlib | Dual Y-Axis | Legend Management | twinx | Data Visualization
Abstract: This technical article addresses the challenge of legend integration in Matplotlib dual Y-axis plots created with twinx(). Through detailed analysis of the original code limitations, it systematically presents three effective solutions: manual combination of line objects, automatic retrieval using get_legend_handles_labels(), and figure-level legend functionality. With comprehensive code examples and implementation insights, the article provides complete technical guidance for multi-axis legend management in data visualization.
Problem Background and Challenges
In data visualization practice, there is often a need to display multiple variables with different dimensions in a single plot. Matplotlib's twinx() function provides this capability by creating subplots that share the X-axis but have independent Y-axes. However, this multi-axis structure introduces special challenges for legend management.
The original code example clearly demonstrates this issue: when using ax.legend(), only the line labels corresponding to the primary Y-axis appear in the legend, while labels from lines on the secondary Y-axis created via twinx() are ignored. This phenomenon stems from Matplotlib's legend mechanism design—each axis object maintains its own collection of legend elements.
Solution 1: Manual Combination of Line Objects
The most direct approach involves explicitly collecting all line objects and their labels, then creating a unified legend. The core of this method lies in understanding the storage mechanism of line objects in Matplotlib.
import numpy as np
import matplotlib.pyplot as plt
# Generate sample data
time = np.arange(10)
Swdown = np.random.random(10) * 100 - 10
Rn = np.random.random(10) * 100 - 10
temp = np.random.random(10) * 30
fig = plt.figure()
ax = fig.add_subplot(111)
# Store references to line objects
lns1 = ax.plot(time, Swdown, '-', label='Swdown')
lns2 = ax.plot(time, Rn, '-', label='Rn')
ax2 = ax.twinx()
lns3 = ax2.plot(time, temp, '-r', label='temp')
# Combine all line objects and labels
lns = lns1 + lns2 + lns3
labs = [l.get_label() for l in lns]
ax.legend(lns, labs, loc=0)
# Set plot properties
ax.grid()
ax.set_xlabel("Time (h)")
ax.set_ylabel(r"Radiation ($MJ\,m^{-2}\,d^{-1}$)")
ax2.set_ylabel(r"Temperature ($^\circ$C)")
ax2.set_ylim(0, 35)
ax.set_ylim(-20, 100)
plt.show()
The advantage of this method is complete control over legend content, allowing precise specification of which lines should appear in the legend. It's important to note that the plot() function returns a list containing line objects, even when plotting a single line, so proper list structure handling is essential during combination.
Solution 2: Automatic Legend Element Retrieval
For more complex graphics, manually managing all line objects can become cumbersome. Matplotlib provides the get_legend_handles_labels() method to automatically collect legend-related elements.
import numpy as np
import matplotlib.pyplot as plt
# Generate more realistic data distribution
time = np.linspace(0, 25, 50)
temp = 50 / np.sqrt(2 * np.pi * 3**2) * np.exp(-((time - 13)**2 / (3**2))**2) + 15
Swdown = 400 / np.sqrt(2 * np.pi * 3**2) * np.exp(-((time - 13)**2 / (3**2))**2)
Rn = Swdown - 10
fig = plt.figure()
ax = fig.add_subplot(111)
ax.plot(time, Swdown, '-', label='Swdown')
ax.plot(time, Rn, '-', label='Rn')
ax2 = ax.twinx()
ax2.plot(time, temp, '-r', label='temp')
# Automatically retrieve legend elements from all axes
lines, labels = ax.get_legend_handles_labels()
lines2, labels2 = ax2.get_legend_handles_labels()
ax2.legend(lines + lines2, labels + labels2, loc=0)
ax.grid()
ax.set_xlabel("Time (h)")
ax.set_ylabel(r"Radiation ($MJ\,m^{-2}\,d^{-1}$)")
ax2.set_ylabel(r"Temperature ($^\circ$C)")
ax2.set_ylim(0, 35)
ax.set_ylim(-20, 100)
plt.show()
This method significantly simplifies code maintenance, particularly when graphic structures change frequently. It automates the collection process of line objects, reducing the possibility of human error.
Solution 3: Figure-Level Legend Usage
Starting from Matplotlib version 2.1, figure-level legend functionality was introduced, providing another elegant solution for multi-axis legend management.
import numpy as np
import matplotlib.pyplot as plt
x = np.linspace(0, 10)
y = np.linspace(0, 10)
z = np.sin(x/3)**2 * 98
fig = plt.figure()
ax = fig.add_subplot(111)
ax.plot(x, y, '-', label='Quantity 1')
ax2 = ax.twinx()
ax2.plot(x, z, '-r', label='Quantity 2')
# Use figure-level legend
fig.legend(loc="upper right")
ax.set_xlabel("x [units]")
ax.set_ylabel("Quantity 1")
ax2.set_ylabel("Quantity 2")
plt.show()
Figure-level legends automatically collect all labeled artist objects throughout the entire figure without explicit specification. If legend placement within a specific axis is required, precise positioning can be achieved using the bbox_to_anchor parameter:
fig.legend(loc="upper right", bbox_to_anchor=(1, 1), bbox_transform=ax.transAxes)
Technical Details and Best Practices
Several technical points deserve attention when implementing multi-axis legends. First, understanding that twinx() creates completely independent axis objects is crucial. Although they share the same X-axis range, in Matplotlib's internal structure, they maintain a sibling relationship rather than a parent-child hierarchy.
Regarding legend position selection, data characteristics and reader reading habits should be considered. For scientific data visualization, legends are typically placed in areas that don't obscure important data features. Simultaneously, ensuring that line styles in the legend match the actual plots avoids potential misinterpretation.
In terms of performance, when handling large datasets, figure-level legends may offer better rendering performance than axis-level legends by reducing redundant legend element calculations.
Extended Applications and Related Technologies
Beyond basic dual Y-axis configurations, these legend management techniques apply equally to more complex multi-axis scenarios. For instance, when using twiny() to create dual X-axis plots, or combining twinx() and twiny() to create four-axis plots, the same principles remain applicable.
An important concept mentioned in the reference article is the management of axis object "handles." In complex plotting workflows, properly maintaining references to various axis objects is a prerequisite for correct legend display. This is particularly important when creating graphics within loops or functions, where axis object scope and lifecycle require special attention.
Another noteworthy detail is the placement of axis labels. As shown in the reference article, setting X-axis labels in multi-axis environments requires caution to avoid duplicate or conflicting label displays.
Conclusion and Recommendations
This article systematically introduces three main methods for managing multi-axis legends in Matplotlib. The manual combination method offers maximum flexibility, suitable for scenarios requiring precise control over legend content. The automatic retrieval method simplifies code maintenance, ideal for rapid prototyping. The figure-level legend method provides the most concise syntax, particularly suitable for users of modern Matplotlib versions.
In practical applications, the appropriate method should be selected based on specific requirements. For simple dual-axis plots, figure-level legends are typically the best choice. For complex visualizations requiring fine-grained control, the manual combination method may be more appropriate. Regardless of the chosen method, understanding Matplotlib's axis object model and legend mechanism remains key to achieving effective visualizations.