Skip to content
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

Feature request: manually supply labels to labelLines #126

Open
3 of 4 tasks
lwelzel opened this issue Mar 24, 2023 · 3 comments
Open
3 of 4 tasks

Feature request: manually supply labels to labelLines #126

lwelzel opened this issue Mar 24, 2023 · 3 comments

Comments

@lwelzel
Copy link

lwelzel commented Mar 24, 2023

I would like to add custom labels to the labelLines method. This is already supported via labelLine, however, adding the same functionality to labelLines would save some boilerplate code.

Feature description

  • add labels as arg to labelLines
  • use labels if supplied for allLabels instead of the labels from ax.get_legend_handles_labels()
  • labels should either be a list of length lines/all_lines or a string
  • test solution + test cases

Suggested solution

I think the implementation should not be too difficult, but I am a first time user of this module so I dont know if I am missing something. I am replacing allLabels with the custom labels and check if everything seems to line up. I see that there are already check for the label content so I did not touch any code not in core.py.

# in \labellines\core.py
# ln 84 ff

def labelLines(
    lines=None,
    labels=None,
    # ...
    **kwargs,
):
    """Label all lines with their respective legends.
    Parameters
    ----------
    lines : list of matplotlib lines, optional.
       Lines to label. If empty, label all lines that have a label.
    labels : list of strings, optional.
        Labels for each line. Must match lines.
    # ...
    """
    # ...

    # !! new code after ln 131 ff

    # if lines has been supplied and contains Line2D objects use those for all_lines, 
    # disregard any other Line2D objects in ax
    if lines is not None:
        assert np.all([isinstance(l, Line2D) for l in lines]), \
            f"Objects in lines must be matplotlib.line.Line2D objects.\n" \
            f"\t Object type at lines[0]: {type(lines[0])} "
        all_lines = lines
    else:  # old code here that iterates over the handles from the figure
        all_lines = []
        for h in handles:
            if isinstance(h, ErrorbarContainer):
                line = h.lines[0]
            else:
                line = h

            # If the user provided a list of lines to label, only label those
            if (lines is not None) and (line not in lines):
                continue
            all_lines.append(line)

    # !! new code after ln 141 ff

    if labels is not None:
        assert len(labels) == len(all_lines) or isinstance(labels, str), \
            f"Number of labels must be equal to one or the number of lines to label.\n" \
            f"\t len(labels): {len(labels)}, len(lines to label): {len(all_lines)}"

        assert lines is not None, f"If labels is supplied manually lines must also be supplied manually."
        allLabels = labels
        # TODO: unsure: if lines is not supplied but labels is supplied raise exception
        #  to make sure that each line gets the right label

    # Check that the lines passed to the function have all a label
    # other code is unchanged
    # ...

Additional context

Issues with suggested solution

  1. TODO above is partially addressed below (ln 141)
  2. supplying lines and labels together is required with this change. This should not be needed but makes sure that the user labels the each label is assigned to the correct line.

Notes

To be even more fancy a string formatter should be able to be passed as labels together with values /or with reference to the data in Line2D to automatically format the labels on the lines, but I have not gotten around to this yet. This could also build on the mpl methods for string formatting.

@cphyc
Copy link
Owner

cphyc commented Mar 24, 2023

Hey,

thanks for the detailed request and the suggested change. Is there any advantage of modifying the labelLines function rather than passing label arguments to the lines to be labelled in the first place?

fig, ax = plt.subplots()
ax.plot([1, 2, 3], [0, 1, 0], label="Label 1")
ax.plot([1, 2, 3], [0, -1, 0], label="Label 2")

labelLines(ax.get_lines())

The (historic) reason why labelLine has a label argument is that it is called by labelLines with the label extracted from each individual line.

@lwelzel
Copy link
Author

lwelzel commented Mar 24, 2023

Hi, thanks for the quick response and great library!

My use case is essentially de-cluttering the legend for plots with many lines where each line varies 2 or more parameters which are not on the plot axes, see an example below. If I now want to give each line an label in the legend, however I want to show additional/different on the lines themselves (e.g. in the example the radius annotations for each blop), the current interface would require me to overplot/hide lines after creating them (I think).
For me, passing the labels to the .plot routine should also work when using drop_label option, however in my case having the higher-level access to all lines was more convenient.

radius_gradients

Thanks a bunch again!

@cphyc
Copy link
Owner

cphyc commented Mar 24, 2023

I am sorry but I'm not sure I understand. Is your problem that you are trying to annotate multiple times each line with a different label?

If so, I now see how that would be a convenient option to have :)

Please feel free to open a pull-request with your proposed change!

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

No branches or pull requests

2 participants