Thanks to visit codestin.com
Credit goes to github.com

Skip to content

[ENH]: Rotate legend #22860

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
amunozj opened this issue Apr 17, 2022 · 8 comments
Open

[ENH]: Rotate legend #22860

amunozj opened this issue Apr 17, 2022 · 8 comments

Comments

@amunozj
Copy link

amunozj commented Apr 17, 2022

Problem

I'm trying to plot a legend that is rotated 90 degrees so that it neatly arranges along a long vertical axis I have, but there is no rotation in the legend definition. It would be great if the legend was not forced to have horizontal text and have all elements displayed horizontally.

Someone mocked what this would look like: https://i.stack.imgur.com/FqCgO.png

Proposed solution

if legend had a rotation parameter that would rotate the bbox around the anchor, it would be amazing!! Thank you so much for your hard work!

@timhoffm
Copy link
Member

FYI: #16005 solves a similar problem but with another approach. Would that be sufficient for you? IMHO vertical legends only make sense with very few (or even just one?) elements.

@amunozj
Copy link
Author

amunozj commented Apr 18, 2022

Thank you so much Tim! Not exactly. While I agree generally with your statement when it comes to publishing figures, I have my own reasons why I want to manipulate the legend this way. This functionality would only empower users, not necessarily encourage bad design.

Simply a rotation parameter like in the text method, would be amazing!

@jklymak
Copy link
Member

jklymak commented Apr 18, 2022

@amunozj Given the complexity of the legend artist, I would be very surprised if this were easy, however, anyone is welcome to try.

@amunozj
Copy link
Author

amunozj commented Apr 18, 2022

Thank you. I was trying to apply a transformation to the bbox, but my understanding of bounding boxes and transformations is very limited :( Would something like this work?

@jklymak
Copy link
Member

jklymak commented Apr 18, 2022

It would work if the children somehow inherit the parents transform But I am not sure that is the case.

@Mickychen00
Copy link

Does anyone find a good solution? Same feature request.

@Europium-152
Copy link

It would be very nice to have this feature. My current solution is to export the figure as a pdf and edit the pdf.

@malfatti
Copy link

I'm using a solution based on manipulating the legend after it is drawn. Basically we can rotate and move the text based on the position we get from the handles. The X and Y position of the text in relation to the handle can be controlled through the XPad and YPad arguments; the rotation can be set to 90 or 270 and anything else is passed to .legend(). Therefore, you can have fine control of the final legend box (bbox_to_anchor= for example). You can also tweak the axes limits to fit the legend without covering/mixing with the data.

This would be much simpler and more precise if the handlers could report (x,y) coordinates from its center that could be matched to the text .set_x() and .set_y() coordinates.

import matplotlib
import matplotlib.pyplot as plt
import numpy as np


def LegendVertical(Ax, Rotation=90, XPad=0, YPad=0, **LegendArgs):
    if Rotation not in (90,270):
        raise NotImplementedError('Rotation must be 90 or 270.')

    # Extra spacing between labels is needed to fit the rotated labels;
    # and since the frame will not adjust to the rotated labels, it is
    # disabled by default
    DefaultLoc = 'center left' if Rotation==90 else 'center right'
    ArgsDefaults = dict(loc=DefaultLoc, labelspacing=4, frameon=False)
    Args = {**ArgsDefaults, **LegendArgs}

    Handles, Labels = Ax.get_legend_handles_labels()
    if Rotation==90:
        # Reverse entries
        Handles, Labels = (reversed(_) for _ in (Handles, Labels))
    AxLeg = Ax.legend(Handles, Labels, **Args)

    LegTexts = AxLeg.get_texts()
    LegHandles = AxLeg.legend_handles

    for L,Leg in enumerate(LegHandles):
        if type(Leg) == matplotlib.patches.Rectangle:
            BBounds = np.ravel(Leg.get_bbox())
            BBounds[2:] = BBounds[2:][::-1]
            Leg.set_bounds(BBounds)

            LegPos = (
                # Ideally,
                #    `(BBounds[0]+(BBounds[2]/2)) - AxLeg.handletextpad`
                # should be at the horizontal center of the legend patch,
                # but for some reason it is not. Therefore the user will
                # need to specify some padding.
                (BBounds[0]+(BBounds[2]/2)) - AxLeg.handletextpad + XPad,

                # Similarly, `(BBounds[1]+BBounds[3])` should be at the vertical
                # top of the legend patch, but it is not.
                (BBounds[1]+BBounds[3])+YPad
            )

        elif type(Leg) == matplotlib.lines.Line2D:
            LegXY = Leg.get_xydata()[:,::-1]
            Leg.set_data(*(LegXY[:,_] for _ in (0,1)))

            LegPos = (
                LegXY[0,0] - AxLeg.handletextpad + XPad,
                max(LegXY[:,1]) + YPad
            )

        elif type(Leg) == matplotlib.collections.PathCollection:
            LegPos = (
                Leg.get_offsets()[0][0] + XPad,
                Leg.get_offsets()[0][1] + YPad,
            )
        else:
            raise NotImplementedError('Legends should contain Rectangle, Line2D or PathCollection.')

        PText = LegTexts[L]
        PText.set_verticalalignment('bottom')
        PText.set_rotation(Rotation)
        PText.set_x(LegPos[0])
        PText.set_y(LegPos[1])

    return(None)


Fig, Axes = plt.subplots(1, 3, constrained_layout=True)

Axes[0].bar(0, 10, label='First')
Axes[0].bar(1, 20, label='Second')
Axes[0].bar(2, 30, label='Third')

Axes[1].plot([1,2,1,2], label='First')
Axes[1].plot([3,4,3,4], label='Second')
Axes[1].plot([5,6,5,6], label='Third')

Scatters = [(np.random.uniform(-0.5,0.5,(2,10))+_).tolist() for _ in range(3)]
Labels = ('First','Second','Third')
for S,Scatter in enumerate(Scatters):
    Axes[2].scatter(*Scatter, label=Labels[S])

for Ax in Axes:
    Ax.set_xlim(-2,4)
    LegendVertical(Ax, 90, XPad=-45, YPad=12)
    # # or
    # LegendVertical(Ax, 270, XPad=-45, YPad=12)

Fig.savefig('VertLeg.png')
plt.show()

VertLeg

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

6 participants