8000 Merge https://github.com/matplotlib/matplotlib into confidence_ellipse · matplotlib/matplotlib@b48f97d · GitHub
[go: up one dir, main page]

Skip to content

Commit b48f97d

Browse files
author
Carsten
committed
Merge https://github.com/matplotlib/matplotlib into confidence_ellipse
2 parents 9661375 + 684a1ea commit b48f97d

File tree

9 files changed

+191
-63
lines changed

9 files changed

+191
-63
lines changed
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
`EngFormatter` now accepts `usetex`, `useMathText` as keyword only arguments
2+
````````````````````````````````````````````````````````````````````````````
3+
4+
A public API has been added to `EngFormatter` to control how the numbers in the
5+
ticklabels will be rendered. By default, ``useMathText`` evaluates to
6+
``rcParams['axes.formatter.use_mathtext']`` and ``usetex`` evaluates to
7+
``rcParams['text.usetex']``.
8+
9+
If either is ``True`` then the numbers will be encapsulated by ``$`` signs.
10+
When using ``TeX`` this implies that the numbers will be shown in TeX's math
11+
font. When using mathtext, the ``$`` signs around numbers will ensure unicode
12+
rendering (as implied by mathtext). This will make sure that the minus signs
13+
in the ticks are rendered as the unicode-minus (U+2212) when using mathtext
14+
(without relying on the ``fix_minus`` method).

lib/matplotlib/backends/backend_pgf.py

Lines changed: 11 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818
_Backend, FigureCanvasBase, FigureManagerBase, GraphicsContextBase,
1919
RendererBase)
2020
from matplotlib.backends.backend_mixed import MixedModeRenderer
21-
from matplotlib.cbook import is_writable_file_like
2221
from matplotlib.path import Path
2322
from matplotlib.figure import Figure
2423
from matplotlib._pylab_helpers import Gcf
@@ -385,8 +384,8 @@ def __init__(self, figure, fh, dummy=False):
385384
Matplotlib figure to initialize height, width and dpi from.
386385
fh : file-like
387386
File handle for the output of the drawing commands.
388-
389387
"""
388+
390389
RendererBase.__init__(self)
391390
self.dpi = figure.dpi
392391
self.fh = fh
@@ -842,16 +841,10 @@ def print_pgf(self, fname_or_fh, *args, **kwargs):
842841
if kwargs.get("dryrun", False):
843842
self._print_pgf_to_fh(None, *args, **kwargs)
844843
return
845-
846-
# figure out where the pgf is to be written to
847-
if isinstance(fname_or_fh, str):
848-
with open(fname_or_fh, "w", encoding="utf-8") as fh:
849-
self._print_pgf_to_fh(fh, *args, **kwargs)
850-
elif is_writable_file_like(fname_or_fh):
851-
fh = codecs.getwriter("utf-8")(fname_or_fh)
852-
self._print_pgf_to_fh(fh, *args, **kwargs)
853-
else:
854-
raise ValueError("filename must be a path")
844+
with cbook.open_file_cm(fname_or_fh, "w", encoding="utf-8") as file:
845+
if not cbook.file_requires_unicode(file):
846+
file = codecs.getwriter("utf-8")(file)
847+
self._print_pgf_to_fh(file, *args, **kwargs)
855848

856849
def _print_pdf_to_fh(self, fh, *args, **kwargs):
857850
w, h = self.figure.get_figwidth(), self.figure.get_figheight()
@@ -896,21 +889,12 @@ def _print_pdf_to_fh(self, fh, *args, **kwargs):
896889
TmpDirCleaner.add(tmpdir)
897890

898891
def print_pdf(self, fname_or_fh, *args, **kwargs):
899-
"""
900-
Use LaTeX to compile a Pgf generated figure to PDF.
901-
"""
892+
"""Use LaTeX to compile a Pgf generated figure to PDF."""
902893
if kwargs.get("dryrun", False):
903894
self._print_pgf_to_fh(None, *args, **kwargs)
904895
return
905-
906-
# figure out where the pdf is to be written to
907-
if isinstance(fname_or_fh, str):
908-
with open(fname_or_fh, "wb") as fh:
909-
self._print_pdf_to_fh(fh, *args, **kwargs)
910-
elif is_writable_file_like(fname_or_fh):
911-
self._print_pdf_to_fh(fname_or_fh, *args, **kwargs)
912-
else:
913-
raise ValueError("filename must be a path or a file-like object")
896+
with cbook.open_file_cm(fname_or_fh, "wb") as file:
897+
self._print_pdf_to_fh(file, *args, **kwargs)
914898

915899
def _print_png_to_fh(self, fh, *args, **kwargs):
916900
converter = make_pdf_to_png_converter()
@@ -933,20 +917,12 @@ def _print_png_to_fh(self, fh, *args, **kwargs):
933917
TmpDirCleaner.add(tmpdir)
934918

935919
def print_png(self, fname_or_fh, *args, **kwargs):
936-
"""
937-
Use LaTeX to compile a pgf figure to pdf and convert it to png.
938-
"""
920+
"""Use LaTeX to compile a pgf figure to pdf and convert it to png."""
939921
if kwargs.get("dryrun", False):
940922
self._print_pgf_to_fh(None, *args, **kwargs)
941923
return
942-
943-
if isinstance(fname_or_fh, str):
944-
with open(fname_or_fh, "wb") as fh:
945-
self._print_png_to_fh(fh, *args, **kwargs)
946-
elif is_writable_file_like(fname_or_fh):
947-
self._print_png_to_fh(fname_or_fh, *args, **kwargs)
948-
else:
949-
raise ValueError("filename must be a path or a file-like object")
924+
with cbook.open_file_cm(fname_or_fh, "wb") as file:
925+
self._print_png_to_fh(file, *args, **kwargs)
950926

951927
def get_renderer(self):
952928
return RendererPgf(self.figure, None, dummy=True)

lib/matplotlib/cbook/__init__.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -360,14 +360,14 @@ def is_hashable(obj):
360360

361361

362362
def is_writable_file_like(obj):
363-
"""return true if *obj* looks like a file object with a *write* method"""
363+
"""Return whether *obj* looks like a file object with a *write* method."""
364364
return callable(getattr(obj, 'write', None))
365365

366366

367367
def file_requires_unicode(x):
368368
"""
369-
Returns `True` if the given writable file-like object requires Unicode
370-
to be written to it.
369+
Returns whether the given writable file-like object requires Unicode to be
370+
written to it.
371371
"""
372372
try:
373373
x.write(b'')

lib/matplotlib/image.py

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -744,9 +744,7 @@ def set_interpolation(self, s):
744744
self.stale = True
745745

746746
def can_composite(self):
747-
"""
748-
Returns `True` if the image can be composited with its neighbors.
749-
"""
747+
"""Return whether the image can be composited with its neighbors."""
750748
trans = self.get_transform()
751749
return (
752750
self._interpolation != 'none' and
@@ -755,19 +753,20 @@ def can_composite(self):
755753

756754
def set_resample(self, v):
757755
"""
758-
Set whether or not image resampling is used.
756+
Set whether image resampling is used.
759757
760758
Parameters
761759
----------
762-
v : bool
760+
v : bool or None
761+
If None, use :rc:`image.resample` = True.
763762
"""
764763
if v is None:
765764
v = rcParams['image.resample']
766765
self._resample = v
767766
self.stale = True
768767

769768
def get_resample(self):
770-
"""Return the image resample boolean."""
769+
"""Return whether image resampling is used."""
771770
return self._resample
772771

773772
def set_filternorm(self, filternorm):

lib/matplotlib/tests/test_path.py

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,83 @@ def test_path_deepcopy():
273273
copy.deepcopy(path2)
274274

275275

276+
def test_path_intersect_path():
277+
# test for the range of intersection angles
278+
base_angles = np.array([0, 15, 30, 45, 60, 75, 90, 105, 120, 135])
279+
angles = np.concatenate([base_angles, base_angles + 1, base_angles - 1])
280+
eps_array = [1e-5, 1e-8, 1e-10, 1e-12]
281+
282+
for phi in angles:
283+
284+
transform = transforms.Affine2D().rotate(np.deg2rad(phi))
285+
286+
# a and b intersect at angle phi
287+
a = Path([(-2, 0), (2, 0)])
288+
b = transform.transform_path(a)
289+
assert a.intersects_path(b) and b.intersects_path(a)
290+
291+
# a and b touch at angle phi at (0, 0)
292+
a = Path([(0, 0), (2, 0)])
293+
b = transform.transform_path(a)
294+
assert a.intersects_path(b) and b.intersects_path(a)
295+
296+
# a and b are orthogonal and intersect at (0, 3)
297+
a = transform.transform_path(Path([(0, 1), (0, 3)]))
298+
b = transform.transform_path(Path([(1, 3), (0, 3)]))
299+
assert a.intersects_path(b) and b.intersects_path(a)
300+
301+
# a and b are collinear and intersect at (0, 3)
302+
a = transform.transform_path(Path([(0, 1), (0, 3)]))
303+
b = transform.transform_path(Path([(0, 5), (0, 3)]))
304+
assert a.intersects_path(b) and b.intersects_path(a)
305+
306+
# self-intersect
307+
assert a.intersects_path(a)
308+
309+
# a contains b
310+
a = transform.transform_path(Path([(0, 0), (5, 5)]))
311+
b = transform.transform_path(Path([(1, 1), (3, 3)]))
312+
assert a.intersects_path(b) and b.intersects_path(a)
313+
314+
# a and b are collinear but do not intersect
315+
a = transform.transform_path(Path([(0, 1), (0, 5)]))
316+
b = transform.transform_path(Path([(3, 0), (3, 3)]))
317+
assert not a.intersects_path(b) and not b.intersects_path(a)
318+
319+
# a and b are on the same line but do not intersect
320+
a = transform.transform_path(Path([(0, 1), (0, 5)]))
321+
b = transform.transform_path(Path([(0, 6), (0, 7)]))
322+
assert not a.intersects_path(b) and not b.intersects_path(a)
323+
324+
# Note: 1e-13 is the absolute tolerance error used for
325+
# `isclose` function from src/_path.h
326+
327+
# a and b are parallel but do not touch
328+
for eps in eps_array:
329+
a = transform.transform_path(Path([(0, 1), (0, 5)]))
330+
b = transform.transform_path(Path([(0 + eps, 1), (0 + eps, 5)]))
331+
assert not a.intersects_path(b) and not b.intersects_path(a)
332+
333+
# a and b are on the same line but do not intersect (really close)
334+
for eps in eps_array:
335+
a = transform.transform_path(Path([(0, 1), (0, 5)]))
336+
b = transform.transform_path(Path([(0, 5 + eps), (0, 7)]))
337+
assert not a.intersects_path(b) and not b.intersects_path(a)
338+
339+
# a and b are on the same line and intersect (really close)
340+
for eps in eps_array:
341+
a = transform.transform_path(Path([(0, 1), (0, 5)]))
342+
b = transform.transform_path(Path([(0, 5 - eps), (0, 7)]))
343+
assert a.intersects_path(b) and b.intersects_path(a)
344+
345+
# b is the same as a but with an extra point
346+
a = transform.transform_path(Path([(0, 1), (0, 5)]))
347+
b = transform.transform_path(Path([(0, 1), (0, 2), (0, 5)]))
348+
assert a.intersects_path(b) and b.intersects_path(a)
349+
350+
return
351+
352+
276353
@pytest.mark.parametrize('offset', range(-720, 361, 45))
277354
def test_full_arc(offset):
278355
low = offset

lib/matplotlib/tests/test_ticker.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -786,6 +786,20 @@ def test_params(self, input, expected):
786786
assert _formatter(input) == _exp_output
787787

788788

789+
def test_engformatter_usetex_useMathText():
790+
fig, ax = plt.subplots()
791+
ax.plot([0, 500, 1000], [0, 500, 1000])
792+
ax.set_xticks([0, 500, 1000])
793+
for formatter in (mticker.EngFormatter(usetex=True),
794+
mticker.EngFormatter(useMathText=True)):
795+
ax.xaxis.set_major_formatter(formatter)
796+
fig.canvas.draw()
797+
x_tick_label_text = [labl.get_text() for labl in ax.get_xticklabels()]
798+
# Checking if the dollar `$` signs have been inserted around numbers
799+
# in tick labels.
800+
assert x_tick_label_text == ['$0$', '$500$', '$1$ k']
801+
802+
789803
class TestPercentFormatter(object):
790804
percent_data = [
791805
# Check explicitly set decimals over different intervals and values

lib/matplotlib/tests/test_usetex.py

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -32,18 +32,3 @@ def test_usetex():
3232
fontsize=24)
3333
ax.set_xticks([])
3434
ax.set_yticks([])
35-
36-
37-
@needs_usetex
38-
def test_usetex_engformatter():
39-
matplotlib.rcParams['text.usetex'] = True
40-
fig, ax = plt.subplots()
41-
ax.plot([0, 500, 1000], [0, 500, 1000])
42-
ax.set_xticks([0, 500, 1000])
43-
formatter = EngFormatter()
44-
ax.xaxis.set_major_formatter(formatter)
45-
fig.canvas.draw()
46-
x_tick_label_text = [label.get_text() for label in ax.get_xticklabels()]
47-
# Checking if the dollar `$` signs have been inserted around numbers
48-
# in tick label text.
49-
assert x_tick_label_text == ['$0$', '$500$', '$1$ k']

lib/matplotlib/ticker.py

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1208,7 +1208,8 @@ class EngFormatter(Formatter):
12081208
24: "Y"
12091209
}
12101210

1211-
def __init__(self, unit="", places=None, sep=" "):
1211+
def __init__(self, unit="", places=None, sep=" ", *, usetex=None,
1212+
useMathText=None):
12121213
"""
12131214
Parameters
12141215
----------
@@ -1234,10 +1235,42 @@ def __init__(self, unit="", places=None, sep=" "):
12341235
* ``sep="\\N{THIN SPACE}"`` (``U+2009``);
12351236
* ``sep="\\N{NARROW NO-BREAK SPACE}"`` (``U+202F``);
12361237
* ``sep="\\N{NO-BREAK SPACE}"`` (``U+00A0``).
1238+
1239+
usetex : bool (default: None)
1240+
To enable/disable the use of TeX's math mode for rendering the
1241+
numbers in the formatter.
1242+
1243+
useMathText : bool (default: None)
1244+
To enable/disable the use mathtext for rendering the numbers in
1245+
the formatter.
12371246
"""
12381247
self.unit = unit
12391248
self.places = places
12401249
self.sep = sep
1250+
self.set_usetex(usetex)
1251+
self.set_useMathText(useMathText)
1252+
1253+
def get_usetex(self):
1254+
return self._usetex
1255+
1256+
def set_usetex(self, val):
1257+
if val is None:
1258+
self._usetex = rcParams['text.usetex']
1259+
else:
1260+
self._usetex = val
1261+
1262+
usetex = property(fget=get_usetex, fset=set_usetex)
1263+
1264+
def get_useMathText(self):
1265+
return self._useMathText
1266+
1267+
def set_useMathText(self, val):
1268+
if val is None:
1269+
self._useMathText = rcParams['axes.formatter.use_mathtext']
1270+
else:
1271+
self._useMathText = val
1272+
1273+
useMathText = property(fget=get_useMathText, fset=set_useMathText)
12411274

12421275
def __call__(self, x, pos=None):
12431276
s = "%s%s" % (self.format_eng(x), self.unit)
@@ -1289,7 +1322,7 @@ def format_eng(self, num):
12891322
pow10 += 3
12901323

12911324
prefix = self.ENG_PREFIXES[int(pow10)]
1292-
if rcParams['text.usetex']:
1325+
if self._usetex or self._useMathText:
12931326
formatted = "${mant:{fmt}}${sep}{prefix}".format(
12941327
mant=mant, sep=self.sep, prefix=prefix, fmt=fmt)
12951328
else:

src/_path.h

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -813,6 +813,14 @@ int count_bboxes_overlapping_bbox(agg::rect_d &a, BBoxArray &bboxes)
813813
return count;
814814
}
815815

816+
817+
inline bool isclose(double a, double b, double rtol, double atol)
818+
{
819+
// as per python's math.isclose
820+
return fabs(a-b) <= fmax(rtol * fmax(fabs(a), fabs(b)), atol);
821+
}
822+
823+
816824
inline bool segments_intersect(const double &x1,
817825
const double &y1,
818826
const double &x2,
@@ -822,8 +830,27 @@ inline bool segments_intersect(const double &x1,
822830
const double &x4,
823831
const double &y4)
824832
{
833+
// relative and absolute tolerance values are chosen empirically
834+
// it looks the atol value matters here bacause of round-off errors
835+
const double rtol = 1e-10;
836+
const double atol = 1e-13;
837+
// determinant
825838
double den = ((y4 - y3) * (x2 - x1)) - ((x4 - x3) * (y2 - y1));
826-
if (den == 0.0) {
839+
840+
if (isclose(den, 0.0, rtol, atol)) { // collinear segments
841+
if (x1 == x2 && x2 == x3) { // segments have infinite slope (vertical lines)
842+
// and lie on the same line
843+
return (fmin(y1, y2) <= fmin(y3, y4) && fmin(y3, y4) <= fmax(y1, y2)) ||
844+
(fmin(y3, y4) <= fmin(y1, y2) && fmin(y1, y2) <= fmax(y3, y4));
845+
}
846+
else {
847+
double intercept = (y1*x2 - y2*x1)*(x4 - x3) - (y3*x4 - y4*x3)*(x1 - x2);
848+
if (isclose(intercept, 0.0, rtol, atol)) { // segments lie on the same line
849+
return (fmin(x1, x2) <= fmin(x3, x4) && fmin(x3, x4) <= fmax(x1, x2)) ||
850+
(fmin(x3, x4) <= fmin(x1, x2) && fmin(x1, x2) <= fmax(x3, x4));
851+
}
852+
}
853+
827854
return false;
828855
}
829856

@@ -833,7 +860,10 @@ inline bool segments_intersect(const double &x1,
833860
double u1 = n1 / den;
834861
double u2 = n2 / den;
835862

836-
return (u1 >= 0.0 && u1 <= 1.0 && u2 >= 0.0 && u2 <= 1.0);
863+
return ((u1 > 0.0 || isclose(u1, 0.0, rtol, atol)) &&
864+
(u1 < 1.0 || isclose(u1, 1.0, rtol, atol)) &&
865+
(u2 > 0.0 || isclose(u2, 0.0, rtol, atol)) &&
866+
(u2 < 1.0 || isclose(u2, 1.0, rtol, atol)));
837867
}
838868

839869
template <class PathIterator1, class PathIterator2>

0 commit comments

Comments
 (0)
0