Uh oh!
There was an error while loading.Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork7.9k
Implement line labels feature#17035
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 ourterms of service andprivacy statement. We’ll occasionally send you account related emails.
Already on GitHub?Sign in to your account
base:main
Are you sure you want to change the base?
Uh oh!
There was an error while loading.Please reload this page.
Changes fromall commits
File filter
Filter by extension
Conversations
Uh oh!
There was an error while loading.Please reload this page.
Jump to
Uh oh!
There was an error while loading.Please reload this page.
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
New `~.axes.Axes.label_lines` method | ||
------------------------------------ | ||
A new `~.axes.Axes.label_lines` method has been added to label the end of lines on an axes. | ||
Previously, the user had to go through the hassle of positioning each label individually | ||
like the Bachelors degrees by gender example. | ||
https://matplotlib.org/gallery/showcase/bachelors_degrees_by_gender.html | ||
Now, to achieve the same effect, a user can simply call | ||
ax.label_lines() |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
""" | ||
==================================== | ||
Line labels using pre-defined labels | ||
==================================== | ||
Defining line labels with plots. | ||
""" | ||
import numpy as np | ||
import matplotlib.pyplot as plt | ||
# Make some fake data. | ||
a = b = np.arange(0, 3, .02) | ||
c = np.exp(a) | ||
d = c[::-1] | ||
# Create plots with pre-defined labels. | ||
fig, ax = plt.subplots() | ||
ax.spines['top'].set_visible(False) | ||
ax.spines['right'].set_visible(False) | ||
ax.plot(a, c, 'k--', label='Model length') | ||
ax.plot(a, d, 'k:', label='Data length') | ||
ax.plot(a, c + d, 'k', label='Total message length') | ||
ax.label_lines() | ||
plt.show() | ||
############################################################################# | ||
# | ||
# ------------ | ||
# | ||
# References | ||
# """""""""" | ||
# | ||
# The use of the following functions, methods, classes and modules is shown | ||
# in this example: | ||
#import matplotlib | ||
#matplotlib.axes.Axes.plot | ||
#matplotlib.pyplot.plot | ||
#matplotlib.axes.Axes.label_lines | ||
#matplotlib.pyplot.label_lines |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -767,6 +767,179 @@ def text(self, x, y, s, fontdict=None, **kwargs): | ||
self._add_text(t) | ||
return t | ||
def label_lines(self, *args, **kwargs): | ||
""" | ||
Place labels at the end of lines on the chart. | ||
Call signatures:: | ||
label_lines() | ||
label_lines(labels) | ||
label_lines(handles, labels) | ||
The call signatures correspond to three different ways of how to use | ||
this method. | ||
The simplest way to use line_labels is without parameters. After the | ||
lines are created with their labels, the user can call this function | ||
which automatically applies the stored labels by the corresponding | ||
lines. This would be most effectively used after the complete creation | ||
of the line chart. | ||
The second way to call this method is by specifying the first parameter | ||
which is labels. In doing so you would be able to name the lines if | ||
they lack names, or rename them as needed. This process occurs in the | ||
order in which the lines were added to the chart. | ||
The Final way to use this method is specifying both the handles and the | ||
labels. In doing so you can specify names for select lines leaving the | ||
rest blank. This would be useful when users would want to specify | ||
select pieces of data to monitor when data clumps occur. | ||
Parameters | ||
---------- | ||
handles : sequence of `.Artist`, optional | ||
A list of Artists (lines) to be added to the line labels. | ||
Use this together with *labels*, if you need full control on what | ||
is shown in the line labels and the automatic mechanism described | ||
above is not sufficient. | ||
The length of handles and labels should be the same in this | ||
case. If they are not, they are truncated to the smaller length. | ||
labels : list of str, optional | ||
A list of labels to show next to the artists. | ||
Use this together with *handles*, if you need full control on what | ||
is shown in the line labels and the automatic mechanism described | ||
above is not sufficient. | ||
Returns | ||
------- | ||
None | ||
Notes | ||
----- | ||
Only line handles are supported by this method. | ||
Examples | ||
-------- | ||
.. plot:: gallery/text_labels_and_annotations/label_lines.py | ||
""" | ||
handles, labels, extra_args, kwargs = mlegend._parse_legend_args( | ||
[self], | ||
*args, | ||
**kwargs) | ||
if len(extra_args): | ||
raise TypeError( | ||
'label_lines only accepts two nonkeyword arguments') | ||
self._line_labels = [] | ||
def get_last_data_point(handle): | ||
last_x = handle.get_xdata()[-1] | ||
last_y = handle.get_ydata()[-1] | ||
return last_x, last_y | ||
xys = np.array([get_last_data_point(x) for x in handles]) | ||
data_maxx, data_maxy = np.max(xys, axis=0) | ||
data_miny = np.min(xys, axis=0)[1] | ||
fig_dpi_transform = self.figure.dpi_scale_trans.inverted() | ||
fig_bbox = self.get_window_extent().transformed(fig_dpi_transform) | ||
fig_width_px = fig_bbox.width * self.figure.dpi | ||
fig_height_px = fig_bbox.height * self.figure.dpi | ||
fig_minx, fig_maxx = self.get_xbound() | ||
fig_miny, fig_maxy = self.get_ybound() | ||
fig_width_pt = abs(fig_maxx - fig_minx) | ||
fig_height_pt = abs(fig_maxy - fig_miny) | ||
margin_left = 8 * (fig_width_pt / fig_width_px) | ||
margin_vertical = 2 * (fig_height_pt / fig_height_px) | ||
text_fontsize = 10 | ||
text_height = text_fontsize * (fig_height_pt / fig_height_px) | ||
bucket_height = text_height + 2 * margin_vertical | ||
buckets_total = 1 + int((fig_maxy - fig_miny - text_height) / | ||
bucket_height) | ||
buckets_map = 0 | ||
def get_bucket_index(y): | ||
return int((y - fig_miny) / bucket_height) | ||
bucket_densities = [0] * buckets_total | ||
for xy in xys: | ||
data_x, data_y = xy | ||
ideal_bucket = get_bucket_index(data_y) | ||
if ideal_bucket >= 0 and ideal_bucket < buckets_total: | ||
bucket_densities[ideal_bucket] += 1 | ||
def in_viewport(args): | ||
xy = args[2] | ||
x, y = xy | ||
return fig_minx < x < fig_maxx and fig_miny < y < fig_maxy | ||
def by_y(args): | ||
xy = args[2] | ||
x, y = xy | ||
return y | ||
bucket_offset = None | ||
prev_ideal_bucket = -1 | ||
for handle, label, xy in sorted(filter(in_viewport, | ||
zip(handles, labels, xys)), | ||
key=by_y): | ||
data_x, data_y = xy | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others.Learn more. Can you explain this? Why is this more complicated than putting the text at the end of each line? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others.Learn more. Our main goal with this implementation is to ensure readability for the user at minimal cost for them. The given examples such as Bachelor's degrees by gender were created by individually placing text objects and using offsets so that they do not overlap. Therefore to try and automatically format the texts in such a way that there is no overlap, we decided to create this bitmap system for spacing rather than simply placing a text at the end of each line. This need for automatic spacing is exemplified by the the same graph from the feature request, where it lacks the offsets. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others.Learn more. @jklymak basically, if you place the text at the end of each line. In some cases the text will overlap, consider the bachelor's degree example posted by my colleague above. ContributorAuthor
| ||
ideal_bucket = get_bucket_index(data_y) | ||
if (ideal_bucket != prev_ideal_bucket and | ||
ideal_bucket < len(bucket_densities)): | ||
bucket_density = bucket_densities[ideal_bucket] | ||
ideal_offset = bucket_density // 2 | ||
empty_buckets_below = 0 | ||
for i in range(ideal_bucket + 1, | ||
ideal_bucket - ideal_offset + 1, | ||
-1): | ||
if i == 0 or buckets_map & (1 << i) == 1: | ||
break | ||
empty_buckets_below += 1 | ||
bucket_offset = -min(ideal_offset, empty_buckets_below) | ||
prev_ideal_bucket = ideal_bucket | ||
bucket = ideal_bucket + bucket_offset | ||
text_x, text_y = None, None | ||
for index in range(buckets_total): | ||
bucket_index = max(0, min(bucket + index, buckets_total)) | ||
bucket_mask = 1 << bucket_index | ||
if buckets_map & bucket_mask == 0: | ||
buckets_map |= bucket_mask | ||
text_x = data_maxx + margin_left | ||
text_y = bucket_index * bucket_height + fig_miny | ||
break | ||
if text_x is not None and text_y is not None: | ||
text_color = handle.get_color() | ||
y_axes = self.transLimits.transform((text_x, text_y))[1] | ||
line_label = self.annotate(label, (data_maxx, data_y), | ||
textcoords='axes fraction', | ||
xytext=(1, y_axes), | ||
fontsize=text_fontsize, | ||
color=text_color, | ||
annotation_clip=True) | ||
self._line_labels.append(line_label) | ||
def has_label_lines(self): | ||
return self._line_labels is not None | ||
def refresh_label_lines(self, *args, **kwargs): | ||
for label in self._line_labels: | ||
label.remove() | ||
self._line_labels = None | ||
self.label_lines(*args, **kwargs) | ||
@cbook._rename_parameter("3.3", "s", "text") | ||
@docstring.dedent_interpd | ||
def annotate(self, text, xy, *args, **kwargs): | ||
Uh oh!
There was an error while loading.Please reload this page.