2025, Oct 17 16:00
How to Swap Legend Order in Plotnine: Control Multiple Legends from Fill and Color with guide_legend
Learn how to control multiple legends in plotnine by setting legend order with guides and guide_legend. Swap fill vs color legends in a bar and line chart.
When mixing multiple aesthetics in plotnine, it’s common to end up with more than one legend. If those legends appear in the wrong order, the plot reads awkwardly. Below is a concise way to control the order without reshaping data or changing geoms.
Goal
Recreate a bar-and-step chart with two separate legends—one for bars (2040) and one for lines (2015)—and swap their order in the legend area.
Reproducible example with the unwanted legend order
# imports
import polars as po
import polars.selectors as sel
from plotnine import *
# data
payload = {
    "Category": ['0-1', '1-5', '5-10', '10-15', '15-20', '20-25', '25-30', '30-35', '35-40', '40-45', '45-50', '50-55', '55-60', '60-65', '65-70', '70-75', '75-80', '80-85', '85-90', '90-95', '95 en ouder'],
    "Mannen 2040": [-540, -910, -1530, -1990, -2120, -2300, -2270, -2340, -2540, -2690, -3140, -3370, -3750, -4200, -5310, -7250, -8310, -8170, -6970, -4610, -1480],
    "Vrouwen 2040": [460, 670, 1050, 1740, 2030, 2450, 3040, 3280, 3370, 3420, 3990, 4390, 4510, 4880, 6170, 8050, 9530, 11080, 11470, 8900, 3350],
    "Mannen 2015": [-420, -660, -980, -1310, -1380, -1550, -1490, -1410, -1460, -1810, -2360, -2690, -2910, -3030, -3430, -3050, -2920, -2540, -1800, -840, -210],
    "Vrouwen 2015": [360, 500, 710, 1180, 1350, 1720, 2070, 2060, 1930, 2060, 2570, 2850, 2870, 2840, 3300, 3070, 3540, 4240, 4400, 2950, 1020]
}
wide_tbl = po.DataFrame(payload)
long_tbl = wide_tbl.unpivot(sel.numeric(), index="Category")
# plot
(
    ggplot() +
    geom_bar(
        data=long_tbl.filter(po.col('variable').is_in(['Mannen 2040', 'Vrouwen 2040'])),
        mapping=aes(x='Category', y='value', fill='variable'),
        stat='identity'
    ) +
    scale_fill_manual(values=['#007BC7', '#CA005D']) +
    geom_step(
        data=long_tbl.filter(po.col('variable').is_in(['Mannen 2015', 'Vrouwen 2015'])),
        mapping=aes(x='Category', y='value', color='variable', group='variable'),
        stat='identity',
        size=1.5,
        direction='mid'
    ) +
    scale_color_manual(values=['#0C163F', '#FFB612']) +
    scale_x_discrete(limits=payload['Category']) +
    scale_y_continuous(
        name="Euro's (x miljoen)",
        limits=[-10_000, 12_500],
        breaks=range(-10_000, 12_500, 2_000),
        labels=list(map(abs, range(-10_000, 12_500, 2_000)))
    ) +
    coord_flip() +
    theme(
        figure_size=(8, 6),
        plot_title=element_text(color="#01689B", size=16, hjust=0.5),
        legend_position="bottom",
        legend_title=element_blank(),
        legend_key=element_blank(),
        panel_background=element_rect(fill="white"),
        panel_grid_major_x=element_line(colour="black", size=0.25),
        axis_ticks_length=5,
        axis_ticks_y=element_line(color="black", size=1.5),
        axis_line_y=element_line(color="black", size=1.5, linetype="solid"),
        axis_title_y=element_blank(),
        axis_title_x=element_text(colour="#007BC7", size=10, hjust=1)
    ) +
    ggtitle("De uitgaven aan zorg voor vrouwen zijn hoger dan\nvoor mannen, en dit verschil neemt toe in de toekomst")
)
What’s happening
Bars use the fill aesthetic and lines use the color aesthetic, so plotnine creates two legends. By default, their order is determined by how the guides are built from the scales. If that order isn’t what you want, you need to declare it explicitly.
Fix: swap legend order with guides
Define the order of each legend using guides and guide_legend. The following change swaps them cleanly without touching the underlying data.
import polars as po
import polars.selectors as sel
from plotnine import *
# data
payload = {
    "Category": ['0-1', '1-5', '5-10', '10-15', '15-20', '20-25', '25-30', '30-35', '35-40', '40-45', '45-50', '50-55', '55-60', '60-65', '65-70', '70-75', '75-80', '80-85', '85-90', '90-95', '95 en ouder'],
    "Mannen 2040": [-540, -910, -1530, -1990, -2120, -2300, -2270, -2340, -2540, -2690, -3140, -3370, -3750, -4200, -5310, -7250, -8310, -8170, -6970, -4610, -1480],
    "Vrouwen 2040": [460, 670, 1050, 1740, 2030, 2450, 3040, 3280, 3370, 3420, 3990, 4390, 4510, 4880, 6170, 8050, 9530, 11080, 11470, 8900, 3350],
    "Mannen 2015": [-420, -660, -980, -1310, -1380, -1550, -1490, -1410, -1460, -1810, -2360, -2690, -2910, -3030, -3430, -3050, -2920, -2540, -1800, -840, -210],
    "Vrouwen 2015": [360, 500, 710, 1180, 1350, 1720, 2070, 2060, 1930, 2060, 2570, 2850, 2870, 2840, 3300, 3070, 3540, 4240, 4400, 2950, 1020]
}
long_tbl = po.DataFrame(payload).unpivot(sel.numeric(), index="Category")
(
    ggplot() +
    geom_bar(
        data=long_tbl.filter(po.col('variable').is_in(['Mannen 2040', 'Vrouwen 2040'])),
        mapping=aes(x='Category', y='value', fill='variable'),
        stat='identity'
    ) +
    scale_fill_manual(values=['#007BC7', '#CA005D'], guide=guide_legend(order=1)) +
    geom_step(
        data=long_tbl.filter(po.col('variable').is_in(['Mannen 2015', 'Vrouwen 2015'])),
        mapping=aes(x='Category', y='value', color='variable', group='variable'),
        stat='identity',
        size=1.5,
        direction='mid'
    ) +
    scale_color_manual(values=['#0C163F', '#FFB612']) +
    scale_x_discrete(limits=payload['Category']) +
    scale_y_continuous(
        name="Euro's (x miljoen)",
        limits=[-10_000, 12_500],
        breaks=range(-10_000, 12_500, 2_000),
        labels=list(map(abs, range(-10_000, 12_500, 2_000)))
    ) +
    coord_flip() +
    theme(
        figure_size=(8, 6),
        plot_title=element_text(color="#01689B", size=16, hjust=0.5),
        legend_position="bottom",
        legend_title=element_blank(),
        legend_key=element_blank(),
        panel_background=element_rect(fill="white"),
        panel_grid_major_x=element_line(colour="black", size=0.25),
        axis_ticks_length=5,
        axis_ticks_y=element_line(color="black", size=1.5),
        axis_line_y=element_line(color="black", size=1.5, linetype="solid"),
        axis_title_y=element_blank(),
        axis_title_x=element_text(colour="#007BC7", size=10, hjust=1)
    ) +
    guides(
        color=guide_legend(order=1),  # 2015 lines
        fill=guide_legend(order=2)    # 2040 bars
    ) +
    ggtitle("De uitgaven aan zorg voor vrouwen zijn hoger dan\nvoor mannen, en dit verschil neemt toe in de toekomst")
)
Why this matters
Clear legends drive quick comprehension. When a plot combines bars and lines, readers expect a stable mapping between visual channels and legend blocks. Controlling legend order avoids misreads and keeps multi-layer visualizations maintainable.
Wrap‑up
If separate aesthetics produce separate legends in plotnine, set their relative positions explicitly. The minimal change is to add guides with guide_legend(order=...). It keeps the data intact, preserves geoms, and communicates exactly what should appear first. That’s a small tweak with a big impact on readability.