Skip to content

Commit 1ed5c91

Browse files
authored
Merge pull request #16171 from anntzer/contour
Contour fixes/improvements
2 parents a129e6e + d7104d9 commit 1ed5c91

13 files changed

+8005
-11124
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
Contour plots now default to using ScalarFormatter
2+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
3+
4+
Pass ``fmt="%1.3f"`` to the contouring call to restore the old default label
5+
format.

lib/matplotlib/contour.py

+29-41
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,7 @@
1717
import matplotlib.font_manager as font_manager
1818
import matplotlib.text as text
1919
import matplotlib.cbook as cbook
20-
import matplotlib.mathtext as mathtext
2120
import matplotlib.patches as mpatches
22-
import matplotlib.texmanager as texmanager
2321
import matplotlib.transforms as mtransforms
2422

2523
# Import needed for adding manual selection capability to clabel
@@ -51,7 +49,7 @@ class ContourLabeler:
5149
"""Mixin to provide labelling capability to `.ContourSet`."""
5250

5351
def clabel(self, levels=None, *,
54-
fontsize=None, inline=True, inline_spacing=5, fmt='%1.3f',
52+
fontsize=None, inline=True, inline_spacing=5, fmt=None,
5553
colors=None, use_clabeltext=False, manual=False,
5654
rightside_up=True, zorder=None):
5755
"""
@@ -92,14 +90,17 @@ def clabel(self, levels=None, *,
9290
This spacing will be exact for labels at locations where the
9391
contour is straight, less so for labels on curved contours.
9492
95-
fmt : str or dict, default: '%1.3f'
96-
A format string for the label.
93+
fmt : `.Formatter` or str or callable or dict, optional
94+
How the levels are formatted:
9795
98-
Alternatively, this can be a dictionary matching contour levels
99-
with arbitrary strings to use for each contour level (i.e.,
100-
fmt[level]=string), or it can be any callable, such as a
101-
`.Formatter` instance, that returns a string when called with a
102-
numeric contour level.
96+
- If a `.Formatter`, it is used to format all levels at once, using
97+
its `.Formatter.format_ticks` method.
98+
- If a str, it is interpreted as a %-style format string.
99+
- If a callable, it is called with one level at a time and should
100+
return the corresponding label.
101+
- If a dict, it should directly map levels to labels.
102+
103+
The default is to use a standard `.ScalarFormatter`.
103104
104105
manual : bool or iterable, default: False
105106
If ``True``, contour labels will be placed manually using
@@ -144,6 +145,9 @@ def clabel(self, levels=None, *,
144145
# labels method (case of automatic label placement) or
145146
# `BlockingContourLabeler` (case of manual label placement).
146147

148+
if fmt is None:
149+
fmt = ticker.ScalarFormatter(useOffset=False)
150+
fmt.create_dummy_axis()
147151
self.labelFmt = fmt
148152
self._use_clabeltext = use_clabeltext
149153
# Detect if manual selection is desired and remove from argument list.
@@ -243,20 +247,12 @@ def get_label_width(self, lev, fmt, fsize):
243247
"""
244248
if not isinstance(lev, str):
245249
lev = self.get_text(lev, fmt)
246-
lev, ismath = text.Text()._preprocess_math(lev)
247-
if ismath == 'TeX':
248-
lw, _, _ = (texmanager.TexManager()
249-
.get_text_width_height_descent(lev, fsize))
250-
elif ismath:
251-
if not hasattr(self, '_mathtext_parser'):
252-
self._mathtext_parser = mathtext.MathTextParser('agg')
253-
_, _, _, _, _, img, _ = self._mathtext_parser.parse(
254-
lev, dpi=72, prop=self.labelFontProps)
255-
_, lw = np.shape(img) # at dpi=72, the units are PostScript points
256-
else:
257-
# width is much less than "font size"
258-
lw = len(lev) * fsize * 0.6
259-
return lw
250+
fig = self.axes.figure
251+
width = (text.Text(0, 0, lev, figure=fig,
252+
size=fsize, fontproperties=self.labelFontProps)
253+
.get_window_extent(fig.canvas.get_renderer()).width)
254+
width *= 72 / fig.dpi
255+
return width
260256

261257
def set_label_props(self, label, text, color):
262258
"""Set the label properties - color, fontsize, text."""
@@ -269,13 +265,14 @@ def get_text(self, lev, fmt):
269265
"""Get the text of the label."""
270266
if isinstance(lev, str):
271267
return lev
268+
elif isinstance(fmt, dict):
269+
return fmt.get(lev, '%1.3f')
270+
elif callable(getattr(fmt, "format_ticks", None)):
271+
return fmt.format_ticks([*self.labelLevelList, lev])[-1]
272+
elif callable(fmt):
273+
return fmt(lev)
272274
else:
273-
if isinstance(fmt, dict):
274-
return fmt.get(lev, '%1.3f')
275-
elif callable(fmt):
276-
return fmt(lev)
277-
else:
278-
return fmt % lev
275+
return fmt % lev
279276

280277
def locate_label(self, linecontour, labelwidth):
281278
"""
@@ -564,23 +561,14 @@ def labels(self, inline, inline_spacing):
564561
paths = con.get_paths()
565562
for segNum, linepath in enumerate(paths):
566563
lc = linepath.vertices # Line contour
567-
slc0 = trans.transform(lc) # Line contour in screen coords
568-
569-
# For closed polygons, add extra point to avoid division by
570-
# zero in print_label and locate_label. Other than these
571-
# functions, this is not necessary and should probably be
572-
# eventually removed.
573-
if _is_closed_polygon(lc):
574-
slc = np.row_stack([slc0, slc0[1:2]])
575-
else:
576-
slc = slc0
564+
slc = trans.transform(lc) # Line contour in screen coords
577565

578566
# Check if long enough for a label
579567
if self.print_label(slc, lw):
580568
x, y, ind = self.locate_label(slc, lw)
581569

582570
rotation, new = self.calc_label_rot_and_inline(
583-
slc0, ind, lw, lc if inline else None, inline_spacing)
571+
slc, ind, lw, lc if inline else None, inline_spacing)
584572

585573
# Actually add the label
586574
add_label(x, y, rotation, lev, cvalue)
Binary file not shown.

0 commit comments

Comments
 (0)