Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit d8ed7f4

Browse filesBrowse files
RobertAugustynowiczhenryhu123
authored andcommitted
Implement Axes.label_lines
Co-authored-by: Robert Augustynowicz <robert.augustynowicz@mail.utoronto.ca> Co-authored-by: Omar Chehab <omarchehab98@gmail.com> Co-authored-by: Jinyang Hu <jinyang.hu@mail.utoronto.ca> Co-authored-by: Dennis Tismenko <dtismenkodeveloper@gmail.com> Co-authored-by: Jinming Zhang <jinming.zhang@mail.utoronto.ca> Closes: #12939
1 parent ab2d200 commit d8ed7f4
Copy full SHA for d8ed7f4

File tree

7 files changed

+278
-0
lines changed
Filter options

7 files changed

+278
-0
lines changed
+12Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
New `~.axes.Axes.label_lines` method
2+
------------------------------------
3+
4+
A new `~.axes.Axes.label_lines` method has been added to label the end of lines on an axes.
5+
Previously, the user had to go through the hassle of positioning each label individually
6+
like the Bachelors degrees by gender example.
7+
8+
https://matplotlib.org/gallery/showcase/bachelors_degrees_by_gender.html
9+
10+
Now, to achieve the same effect, a user can simply call
11+
12+
ax.label_lines()
+44Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
"""
2+
====================================
3+
Line labels using pre-defined labels
4+
====================================
5+
6+
Defining line labels with plots.
7+
"""
8+
9+
10+
import numpy as np
11+
import matplotlib.pyplot as plt
12+
13+
# Make some fake data.
14+
a = b = np.arange(0, 3, .02)
15+
c = np.exp(a)
16+
d = c[::-1]
17+
18+
# Create plots with pre-defined labels.
19+
fig, ax = plt.subplots()
20+
ax.spines['top'].set_visible(False)
21+
ax.spines['right'].set_visible(False)
22+
ax.plot(a, c, 'k--', label='Model length')
23+
ax.plot(a, d, 'k:', label='Data length')
24+
ax.plot(a, c + d, 'k', label='Total message length')
25+
26+
ax.label_lines()
27+
28+
plt.show()
29+
30+
#############################################################################
31+
#
32+
# ------------
33+
#
34+
# References
35+
# """"""""""
36+
#
37+
# The use of the following functions, methods, classes and modules is shown
38+
# in this example:
39+
40+
import matplotlib
41+
matplotlib.axes.Axes.plot
42+
matplotlib.pyplot.plot
43+
matplotlib.axes.Axes.label_lines
44+
matplotlib.pyplot.label_lines

‎lib/matplotlib/axes/_axes.py

Copy file name to clipboardExpand all lines: lib/matplotlib/axes/_axes.py
+206Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -753,6 +753,212 @@ def text(self, x, y, s, fontdict=None, **kwargs):
753753
self._add_text(t)
754754
return t
755755

756+
def label_lines(self, *args, **kwargs):
757+
"""
758+
Place labels at the end of lines on the chart.
759+
760+
Call signatures::
761+
762+
label_lines()
763+
label_lines(labels)
764+
label_lines(handles, labels)
765+
766+
The call signatures correspond to three different ways of how to use
767+
this method.
768+
769+
The simplest way to use line_labels is without parameters. After the
770+
lines are created with their labels, the user can call this function
771+
which automatically applies the stored labels by the corresponding
772+
lines. This would be most effectively used after the complete creation
773+
of the line chart.
774+
775+
The second way to call this method is by specifying the first parameter
776+
which is labels. In doing so you would be able to name the lines if
777+
they lack names, or rename them as needed. This process occurs in the
778+
order in which the lines were added to the chart.
779+
780+
The Final way to use this method is specifying both the handles and the
781+
labels. In doing so you can specify names for select lines leaving the
782+
rest blank. This would be useful when users would want to specify
783+
select pieces of data to monitor when data clumps occur.
784+
785+
Parameters
786+
----------
787+
handles : sequence of `.Artist`, optional
788+
A list of Artists (lines) to be added to the line labels.
789+
Use this together with *labels*, if you need full control on what
790+
is shown in the line labels and the automatic mechanism described
791+
above is not sufficient.
792+
793+
The length of handles and labels should be the same in this
794+
case. If they are not, they are truncated to the smaller length.
795+
796+
labels : list of str, optional
797+
A list of labels to show next to the artists.
798+
Use this together with *handles*, if you need full control on what
799+
is shown in the line labels and the automatic mechanism described
800+
above is not sufficient.
801+
802+
Returns
803+
-------
804+
None
805+
806+
Notes
807+
-----
808+
Only line handles are supported by this method.
809+
810+
Examples
811+
--------
812+
.. plot:: gallery/text_labels_and_annotations/label_lines.py
813+
"""
814+
handles, labels, extra_args, kwargs = mlegend._parse_legend_args(
815+
[self],
816+
*args,
817+
**kwargs)
818+
if len(extra_args):
819+
raise TypeError(
820+
'label_lines only accepts two nonkeyword arguments')
821+
822+
self._line_labels = []
823+
824+
def get_last_data_point(handle):
825+
last_x = handle.get_xdata()[-1]
826+
last_y = handle.get_ydata()[-1]
827+
return last_x, last_y
828+
829+
# Get last data point for each handle
830+
xys = np.array([get_last_data_point(x) for x in handles])
831+
832+
# Find the largest last x and y value
833+
data_maxx, data_maxy = np.max(xys, axis=0)
834+
# Find the smallest last y value
835+
data_miny = np.min(xys, axis=0)[1]
836+
837+
# Get the figure width and height in pixels
838+
fig_dpi_transform = self.figure.dpi_scale_trans.inverted()
839+
fig_bbox = self.get_window_extent().transformed(fig_dpi_transform)
840+
fig_width_px = fig_bbox.width * self.figure.dpi
841+
fig_height_px = fig_bbox.height * self.figure.dpi
842+
843+
# Get the figure width and height in points
844+
fig_minx, fig_maxx = self.get_xbound()
845+
fig_miny, fig_maxy = self.get_ybound()
846+
fig_width_pt = abs(fig_maxx - fig_minx)
847+
fig_height_pt = abs(fig_maxy - fig_miny)
848+
849+
# Space to the left of the text converted from pixels
850+
margin_left = 8 * (fig_width_pt / fig_width_px)
851+
# Space to the top and bottom of the text converted from pixels
852+
margin_vertical = 2 * (fig_height_pt / fig_height_px)
853+
854+
text_fontsize = 10
855+
# Height of the text converted from pixels
856+
text_height = text_fontsize * (fig_height_pt / fig_height_px)
857+
858+
# Height of each bucket
859+
bucket_height = text_height + 2 * margin_vertical
860+
# Total number of buckets
861+
buckets_total = 1 + int((fig_maxy - fig_miny - text_height) /
862+
bucket_height)
863+
# Bit array to track which buckets are used
864+
buckets_map = 0
865+
866+
def get_bucket_index(y):
867+
return int((y - fig_miny) / bucket_height)
868+
869+
# How many text SHOULD be in this bucket
870+
bucket_densities = [0] * buckets_total
871+
for xy in xys:
872+
data_x, data_y = xy
873+
ideal_bucket = get_bucket_index(data_y)
874+
if ideal_bucket >= 0 and ideal_bucket < buckets_total:
875+
bucket_densities[ideal_bucket] += 1
876+
877+
def in_viewport(args):
878+
xy = args[2]
879+
x, y = xy
880+
return fig_minx < x < fig_maxx and fig_miny < y < fig_maxy
881+
882+
def by_y(args):
883+
xy = args[2]
884+
x, y = xy
885+
return y
886+
887+
bucket_offset = None
888+
prev_ideal_bucket = -1
889+
# Iterate over all the handles, labels, and xy data sorted by y asc
890+
for handle, label, xy in sorted(filter(in_viewport,
891+
zip(handles, labels, xys)),
892+
key=by_y):
893+
data_x, data_y = xy
894+
895+
ideal_bucket = get_bucket_index(data_y)
896+
if ideal_bucket != prev_ideal_bucket:
897+
# If a bucket is dense, then we want to center the text around
898+
# the bucket. SSo, find out how many empty buckets there are
899+
# below the current bucket
900+
bucket_density = bucket_densities[ideal_bucket]
901+
ideal_offset = bucket_density // 2
902+
empty_buckets_below = 0
903+
for i in range(ideal_bucket + 1,
904+
ideal_bucket - ideal_offset + 1,
905+
-1):
906+
# If reached bottom or bucket is not empty
907+
if i == 0 or buckets_map & (1 << i) == 1:
908+
break
909+
empty_buckets_below += 1
910+
bucket_offset = -min(ideal_offset, empty_buckets_below)
911+
prev_ideal_bucket = ideal_bucket
912+
913+
bucket = ideal_bucket + bucket_offset
914+
text_x, text_y = None, None
915+
916+
# Find the nearest empty bucket
917+
for index in range(buckets_total):
918+
# 0 <= bucket_index <= buckets_total
919+
bucket_index = max(0, min(bucket + index, buckets_total))
920+
bucket_mask = 1 << bucket_index
921+
# If this bucket is empty
922+
if buckets_map & bucket_mask == 0:
923+
# Mark this bucket as used
924+
buckets_map |= bucket_mask
925+
text_x = data_maxx + margin_left
926+
text_y = bucket_index * bucket_height + fig_miny
927+
break
928+
929+
# If a bucket was found for the text
930+
if text_x is not None and text_y is not None:
931+
text_color = handle.get_color()
932+
line_label = self.annotate(label, (data_maxx, data_y),
933+
xytext=(text_x, text_y),
934+
fontsize=text_fontsize,
935+
color=text_color,
936+
annotation_clip=True)
937+
self._line_labels.append(line_label)
938+
939+
# A bucket is not necessarily aligned with the data point
940+
# If space allows, center the text around the data point
941+
# TODO(omarchehab98): group fuzzing
942+
# for line_label in self._line_labels:
943+
# datax, datay = line_label.xy
944+
# targety = datay - text_height / 2
945+
# textx, texty = line_label.get_position()
946+
# direction = 1 if targety > texty else -1
947+
# target_bucket = get_bucket_index(targety) + direction
948+
# if (get_bucket_index(targety) == get_bucket_index(texty) and
949+
# (target_bucket == -1 or
950+
# buckets_map & (1 << target_bucket) == 0)):
951+
# line_label.set_position((textx, targety))
952+
953+
def has_label_lines(self):
954+
return self._line_labels is not None
955+
956+
def refresh_label_lines(self, *args, **kwargs):
957+
for label in self._line_labels:
958+
label.remove()
959+
self._line_labels = None
960+
self.label_lines(*args, **kwargs)
961+
756962
@cbook._rename_parameter("3.3", "s", "text")
757963
@docstring.dedent_interpd
758964
def annotate(self, text, xy, *args, **kwargs):

‎lib/matplotlib/axes/_base.py

Copy file name to clipboardExpand all lines: lib/matplotlib/axes/_base.py
+2Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -570,6 +570,8 @@ def __init__(self, fig, rect,
570570

571571
self._layoutbox = None
572572
self._poslayoutbox = None
573+
574+
self._line_labels = None
573575

574576
def __getstate__(self):
575577
# The renderer should be re-created by the figure, and then cached at

‎lib/matplotlib/backend_bases.py

Copy file name to clipboardExpand all lines: lib/matplotlib/backend_bases.py
+7Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3058,6 +3058,11 @@ def release_zoom(self, event):
30583058
def draw(self):
30593059
"""Redraw the canvases, update the locators."""
30603060
for a in self.canvas.figure.get_axes():
3061+
# TODO(henryhu123): Is this the right place do recompute the line
3062+
# labels?
3063+
if a.has_label_lines():
3064+
a.refresh_label_lines()
3065+
30613066
xaxis = getattr(a, 'xaxis', None)
30623067
yaxis = getattr(a, 'yaxis', None)
30633068
locators = []
@@ -3088,6 +3093,8 @@ def _update_view(self):
30883093
# Restore both the original and modified positions
30893094
ax._set_position(pos_orig, 'original')
30903095
ax._set_position(pos_active, 'active')
3096+
if ax.has_label_lines():
3097+
ax.refresh_label_lines()
30913098
self.canvas.draw_idle()
30923099

30933100
def save_figure(self, *args):

‎lib/matplotlib/pyplot.py

Copy file name to clipboardExpand all lines: lib/matplotlib/pyplot.py
+6Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2559,6 +2559,12 @@ def imshow(
25592559
return __ret
25602560

25612561

2562+
# Autogenerated by boilerplate.py. Do not edit as changes will be lost.
2563+
@_copy_docstring_and_deprecators(Axes.label_lines)
2564+
def label_lines(*args, **kwargs):
2565+
return gca().label_lines(*args, **kwargs)
2566+
2567+
25622568
# Autogenerated by boilerplate.py. Do not edit as changes will be lost.
25632569
@_copy_docstring_and_deprecators(Axes.legend)
25642570
def legend(*args, **kwargs):

‎tools/boilerplate.py

Copy file name to clipboardExpand all lines: tools/boilerplate.py
+1Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,7 @@ def boilerplate_gen():
227227
'hist2d',
228228
'hlines',
229229
'imshow',
230+
'label_lines',
230231
'legend',
231232
'locator_params',
232233
'loglog',

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.