Custom Matplotlib Legend for Arbitrary Row/Column Arrangement via Transposed Handle/Label Reordering
To control legend entry ordering independent of Matplotlib's default column-major rendering, we can reprocess the handles and labels before passing them to the Legend class, leveraging matplotlib.legend._get_legend_handles_labels to automatically retrieve plot elements if needed. For example, a default ncols=2 legend with 6 entries will render:
0 3
1 4
2 5
To switch to row-major order like:
0 1
2 3
4 5
We first reshape the flat lists into a 2D grid, transpose it, then flatten again. Adding None placeholders ensures even grid transpostiion works correctly even when entry counts don’t divide perfectly by column numbers.
from typing import List, Any, Optional
import matplotlib.pyplot as plt
from matplotlib.legend import Legend, _get_legend_handles_labels
import matplotlib.axes as mpl_axes
def _pad_and_transpose_sequence(seq: List[Any], target_cols: int) -> List[Any]:
# Split sequence into target_rows × target_cols grid, pad incomplete last row with Nones
target_rows = (len(seq) + target_cols - 1) // target_cols
padded_grid = []
for idx in range(target_rows):
start = idx * target_cols
end = start + target_cols
row = seq[start:end] + [None] * (target_cols - len(seq[start:end]))
padded_grid.append(row)
# Transpose grid and flatten while filtering out Nones
transposed_flat = []
for col_idx in range(target_cols):
for row_idx in range(target_rows):
elem = padded_grid[row_idx][col_idx]
if elem is not None:
transposed_flat.append(elem)
return transposed_flat
class RowMajorLegend(Legend):
def __init__(self, ax: mpl_axes.Axes,
handles: Optional[List[Any]] = None,
labels: Optional[List[str]] = None,
arrange_by_row: bool = False,
loc: Optional[str] = None,
ncols: int = 1,
**kwargs):
# Validate handle/label pairing
if (handles is None) != (labels is None):
raise ValueError("Either both handles and labels must be provided, or neither to auto-retrieve.")
# Auto-fetch plot elements if no handles/labels given
if not handles and not labels:
handles, labels = _get_legend_handles_labels(axs=[ax])
# Apply row-major rearrangement if requested
if arrange_by_row and ncols > 1:
handles = _pad_and_transpose_sequence(handles, ncols)
labels = _pad_and_transpose_sequence(labels, ncols)
super().__init__(parent=ax, handles=handles, labels=labels, loc=loc, ncols=ncols, **kwargs)
if __name__ == "__main__":
# Create sample plot
fig, ax = plt.subplots(figsize=(8, 5))
scatter_handles = [
ax.scatter(range(10), [x * (i + 1) for x in range(10)], label=f"Dataset {i}")
for i in range(6)
]
# Initialize custom row-major legend
custom_legend = RowMajorLegend(
ax, arrange_by_row=True, loc="upper left",
ncols=2, framealpha=0.9, borderpad=1.2
)
ax.add_artist(custom_legend)
plt.tight_layout()
plt.show()
The ncols parameter still controls the visual number of columns, and row-major arrangement balances empty slots across the bottom row. This approach retains full access to all Legend initiailzation parameters, including titles, while avoiding the complexity of creating multiple independent Legend instances.