class _Base: """Base class for axis helper.""" def update_lim(self, axes): pass delta1 = _api.deprecated("3.6")(property(lambda self: 0.00001, lambda self, value: None)) delta2 = _api.deprecated("3.6")(property(lambda self: 0.00001, lambda self, value: None))
class __getattr__: IDLE_DELAY = _api.deprecated("3.1", obj_type="", removal="3.6")(property(lambda self: 5)) cursord = _api.deprecated("3.5", obj_type="")(property( lambda self: { cursors.MOVE: wx.CURSOR_HAND, cursors.HAND: wx.CURSOR_HAND, cursors.POINTER: wx.CURSOR_ARROW, cursors.SELECT_REGION: wx.CURSOR_CROSS, cursors.WAIT: wx.CURSOR_WAIT, cursors.RESIZE_HORIZONTAL: wx.CURSOR_SIZEWE, cursors.RESIZE_VERTICAL: wx.CURSOR_SIZENS, }))
class RubberbandTk(backend_tools.RubberbandBase): def draw_rubberband(self, x0, y0, x1, y1): NavigationToolbar2Tk.draw_rubberband( self._make_classic_style_pseudo_toolbar(), None, x0, y0, x1, y1) def remove_rubberband(self): NavigationToolbar2Tk.remove_rubberband( self._make_classic_style_pseudo_toolbar()) lastrect = _api.deprecated("3.6")( property(lambda self: self.figure.canvas._rubberband_rect))
class __getattr__: locals().update({ name: _api.deprecated("3.4")( property(lambda self, _mod=mod, _name=name: getattr(_mod, _name))) for mod, names in [(_mathtext, ["SHRINK_FACTOR", "GROW_FACTOR", "NUM_SIZE_LEVELS"]), (_mathtext_data, [ "latex_to_bakoma", "latex_to_cmex", "latex_to_standard", "stix_virtual_fonts", "tex2uni" ])] for name in names })
class __getattr__: @_api.deprecated("3.5", obj_type="") @property def cursord(self): try: new_cursor = functools.partial(Gdk.Cursor.new_from_name, Gdk.Display.get_default()) return { Cursors.MOVE: new_cursor("move"), Cursors.HAND: new_cursor("pointer"), Cursors.POINTER: new_cursor("default"), Cursors.SELECT_REGION: new_cursor("crosshair"), Cursors.WAIT: new_cursor("wait"), } except TypeError as exc: return {} icon_filename = _api.deprecated("3.6", obj_type="")( property(lambda self: "matplotlib.png" if sys.platform == "win32" else "matplotlib.svg")) window_icon = _api.deprecated("3.6", obj_type="")( property(lambda self: str( cbook._get_data_path("images", __getattr__("icon_filename")))))
import sys import textwrap import traceback from docutils.parsers.rst import directives, Directive from docutils.parsers.rst.directives.images import Image import jinja2 # Sphinx dependency. import matplotlib from matplotlib.backend_bases import FigureManagerBase import matplotlib.pyplot as plt from matplotlib import _api, _pylab_helpers, cbook, sphinxext matplotlib.use("agg") align = _api.deprecated( "3.4", alternative="docutils.parsers.rst.directives.images.Image.align")( Image.align) __version__ = 2 # ----------------------------------------------------------------------------- # Registration hook # ----------------------------------------------------------------------------- def _option_boolean(arg): if not arg or not arg.strip(): # no argument given, assume used as a flag return True elif arg.strip().lower() in ('no', '0', 'false'):
class RendererPS(_backend_pdf_ps.RendererPDFPSBase): """ The renderer handles all the drawing primitives using a graphics context instance that controls the colors/styles. """ _afm_font_dir = cbook._get_data_path("fonts/afm") _use_afm_rc_name = "ps.useafm" mathtext_parser = _api.deprecated("3.4")( property(lambda self: MathTextParser("PS"))) def __init__(self, width, height, pswriter, imagedpi=72): # Although postscript itself is dpi independent, we need to inform the # image code about a requested dpi to generate high resolution images # and them scale them before embedding them. super().__init__(width, height) self._pswriter = pswriter if mpl.rcParams['text.usetex']: self.textcnt = 0 self.psfrag = [] self.imagedpi = imagedpi # current renderer state (None=uninitialised) self.color = None self.linewidth = None self.linejoin = None self.linecap = None self.linedash = None self.fontname = None self.fontsize = None self._hatches = {} self.image_magnification = imagedpi / 72 self._clip_paths = {} self._path_collection_id = 0 self._character_tracker = _backend_pdf_ps.CharacterTracker() def set_color(self, r, g, b, store=True): if (r, g, b) != self.color: if r == g and r == b: self._pswriter.write("%1.3f setgray\n" % r) else: self._pswriter.write("%1.3f %1.3f %1.3f setrgbcolor\n" % (r, g, b)) if store: self.color = (r, g, b) def set_linewidth(self, linewidth, store=True): linewidth = float(linewidth) if linewidth != self.linewidth: self._pswriter.write("%1.3f setlinewidth\n" % linewidth) if store: self.linewidth = linewidth @staticmethod def _linejoin_cmd(linejoin): # Support for directly passing integer values is for backcompat. linejoin = { 'miter': 0, 'round': 1, 'bevel': 2, 0: 0, 1: 1, 2: 2 }[linejoin] return f"{linejoin:d} setlinejoin\n" def set_linejoin(self, linejoin, store=True): if linejoin != self.linejoin: self._pswriter.write(self._linejoin_cmd(linejoin)) if store: self.linejoin = linejoin @staticmethod def _linecap_cmd(linecap): # Support for directly passing integer values is for backcompat. linecap = { 'butt': 0, 'round': 1, 'projecting': 2, 0: 0, 1: 1, 2: 2 }[linecap] return f"{linecap:d} setlinecap\n" def set_linecap(self, linecap, store=True): if linecap != self.linecap: self._pswriter.write(self._linecap_cmd(linecap)) if store: self.linecap = linecap def set_linedash(self, offset, seq, store=True): if self.linedash is not None: oldo, oldseq = self.linedash if np.array_equal(seq, oldseq) and oldo == offset: return if seq is not None and len(seq): s = "[%s] %d setdash\n" % (_nums_to_str(*seq), offset) self._pswriter.write(s) else: self._pswriter.write("[] 0 setdash\n") if store: self.linedash = (offset, seq) def set_font(self, fontname, fontsize, store=True): if (fontname, fontsize) != (self.fontname, self.fontsize): self._pswriter.write(f"/{fontname} {fontsize:1.3f} selectfont\n") if store: self.fontname = fontname self.fontsize = fontsize def create_hatch(self, hatch): sidelen = 72 if hatch in self._hatches: return self._hatches[hatch] name = 'H%d' % len(self._hatches) linewidth = mpl.rcParams['hatch.linewidth'] pageheight = self.height * 72 self._pswriter.write(f"""\ << /PatternType 1 /PaintType 2 /TilingType 2 /BBox[0 0 {sidelen:d} {sidelen:d}] /XStep {sidelen:d} /YStep {sidelen:d} /PaintProc {{ pop {linewidth:f} setlinewidth {self._convert_path( Path.hatch(hatch), Affine2D().scale(sidelen), simplify=False)} gsave fill grestore stroke }} bind >> matrix 0.0 {pageheight:f} translate makepattern /{name} exch def """) self._hatches[hatch] = name return name def get_image_magnification(self): """ Get the factor by which to magnify images passed to draw_image. Allows a backend to have images at a different resolution to other artists. """ return self.image_magnification def _convert_path(self, path, transform, clip=False, simplify=None): if clip: clip = (0.0, 0.0, self.width * 72.0, self.height * 72.0) else: clip = None return _path.convert_to_string(path, transform, clip, simplify, None, 6, [b"m", b"l", b"", b"c", b"cl"], True).decode("ascii") def _get_clip_cmd(self, gc): clip = [] rect = gc.get_clip_rectangle() if rect is not None: clip.append("%s clipbox\n" % _nums_to_str(*rect.size, *rect.p0)) path, trf = gc.get_clip_path() if path is not None: key = (path, id(trf)) custom_clip_cmd = self._clip_paths.get(key) if custom_clip_cmd is None: custom_clip_cmd = "c%x" % len(self._clip_paths) self._pswriter.write(f"""\ /{custom_clip_cmd} {{ {self._convert_path(path, trf, simplify=False)} clip newpath }} bind def """) self._clip_paths[key] = custom_clip_cmd clip.append(f"{custom_clip_cmd}\n") return "".join(clip) def draw_image(self, gc, x, y, im, transform=None): # docstring inherited h, w = im.shape[:2] imagecmd = "false 3 colorimage" data = im[::-1, :, :3] # Vertically flipped rgb values. # data.tobytes().hex() has no spaces, so can be linewrapped by simply # splitting data every nchars. It's equivalent to textwrap.fill only # much faster. nchars = 128 data = data.tobytes().hex() hexlines = "\n".join([ data[n * nchars:(n + 1) * nchars] for n in range(math.ceil(len(data) / nchars)) ]) if transform is None: matrix = "1 0 0 1 0 0" xscale = w / self.image_magnification yscale = h / self.image_magnification else: matrix = " ".join(map(str, transform.frozen().to_values())) xscale = 1.0 yscale = 1.0 self._pswriter.write(f"""\ gsave {self._get_clip_cmd(gc)} {x:f} {y:f} translate [{matrix}] concat {xscale:f} {yscale:f} scale /DataString {w:d} string def {w:d} {h:d} 8 [ {w:d} 0 0 -{h:d} 0 {h:d} ] {{ currentfile DataString readhexstring pop }} bind {imagecmd} {hexlines} grestore """) def draw_path(self, gc, path, transform, rgbFace=None): # docstring inherited clip = rgbFace is None and gc.get_hatch_path() is None simplify = path.should_simplify and clip ps = self._convert_path(path, transform, clip=clip, simplify=simplify) self._draw_ps(ps, gc, rgbFace) def draw_markers(self, gc, marker_path, marker_trans, path, trans, rgbFace=None): # docstring inherited if debugPS: self._pswriter.write('% draw_markers \n') ps_color = (None if _is_transparent(rgbFace) else '%1.3f setgray' % rgbFace[0] if rgbFace[0] == rgbFace[1] == rgbFace[2] else '%1.3f %1.3f %1.3f setrgbcolor' % rgbFace[:3]) # construct the generic marker command: # don't want the translate to be global ps_cmd = ['/o {', 'gsave', 'newpath', 'translate'] lw = gc.get_linewidth() alpha = (gc.get_alpha() if gc.get_forced_alpha() or len(gc.get_rgb()) == 3 else gc.get_rgb()[3]) stroke = lw > 0 and alpha > 0 if stroke: ps_cmd.append('%.1f setlinewidth' % lw) ps_cmd.append(self._linejoin_cmd(gc.get_joinstyle())) ps_cmd.append(self._linecap_cmd(gc.get_capstyle())) ps_cmd.append( self._convert_path(marker_path, marker_trans, simplify=False)) if rgbFace: if stroke: ps_cmd.append('gsave') if ps_color: ps_cmd.extend([ps_color, 'fill']) if stroke: ps_cmd.append('grestore') if stroke: ps_cmd.append('stroke') ps_cmd.extend(['grestore', '} bind def']) for vertices, code in path.iter_segments(trans, clip=(0, 0, self.width * 72, self.height * 72), simplify=False): if len(vertices): x, y = vertices[-2:] ps_cmd.append("%g %g o" % (x, y)) ps = '\n'.join(ps_cmd) self._draw_ps(ps, gc, rgbFace, fill=False, stroke=False) def draw_path_collection(self, gc, master_transform, paths, all_transforms, offsets, offsetTrans, facecolors, edgecolors, linewidths, linestyles, antialiaseds, urls, offset_position): # Is the optimization worth it? Rough calculation: # cost of emitting a path in-line is # (len_path + 2) * uses_per_path # cost of definition+use is # (len_path + 3) + 3 * uses_per_path len_path = len(paths[0].vertices) if len(paths) > 0 else 0 uses_per_path = self._iter_collection_uses_per_path( paths, all_transforms, offsets, facecolors, edgecolors) should_do_optimization = \ len_path + 3 * uses_per_path + 3 < (len_path + 2) * uses_per_path if not should_do_optimization: return RendererBase.draw_path_collection( self, gc, master_transform, paths, all_transforms, offsets, offsetTrans, facecolors, edgecolors, linewidths, linestyles, antialiaseds, urls, offset_position) path_codes = [] for i, (path, transform) in enumerate( self._iter_collection_raw_paths(master_transform, paths, all_transforms)): name = 'p%x_%x' % (self._path_collection_id, i) path_bytes = self._convert_path(path, transform, simplify=False) self._pswriter.write(f"""\ /{name} {{ newpath translate {path_bytes} }} bind def """) path_codes.append(name) for xo, yo, path_id, gc0, rgbFace in self._iter_collection( gc, master_transform, all_transforms, path_codes, offsets, offsetTrans, facecolors, edgecolors, linewidths, linestyles, antialiaseds, urls, offset_position): ps = "%g %g %s" % (xo, yo, path_id) self._draw_ps(ps, gc0, rgbFace) self._path_collection_id += 1 def draw_tex(self, gc, x, y, s, prop, angle, *, mtext=None): # docstring inherited if not hasattr(self, "psfrag"): _log.warning( "The PS backend determines usetex status solely based on " "rcParams['text.usetex'] and does not support having " "usetex=True only for some elements; this element will thus " "be rendered as if usetex=False.") self.draw_text(gc, x, y, s, prop, angle, False, mtext) return w, h, bl = self.get_text_width_height_descent(s, prop, ismath="TeX") fontsize = prop.get_size_in_points() thetext = 'psmarker%d' % self.textcnt color = '%1.3f,%1.3f,%1.3f' % gc.get_rgb()[:3] fontcmd = { 'sans-serif': r'{\sffamily %s}', 'monospace': r'{\ttfamily %s}' }.get(mpl.rcParams['font.family'][0], r'{\rmfamily %s}') s = fontcmd % s tex = r'\color[rgb]{%s} %s' % (color, s) # Stick to the bottom alignment. pos = _nums_to_str(x, y - bl) self.psfrag.append(r'\psfrag{%s}[bl][bl][1][%f]{\fontsize{%f}{%f}%s}' % (thetext, angle, fontsize, fontsize * 1.25, tex)) self._pswriter.write(f"""\ gsave {pos} moveto ({thetext}) show grestore """) self.textcnt += 1 def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None): # docstring inherited if debugPS: self._pswriter.write("% text\n") if _is_transparent(gc.get_rgb()): return # Special handling for fully transparent. if ismath == 'TeX': return self.draw_tex(gc, x, y, s, prop, angle) if ismath: return self.draw_mathtext(gc, x, y, s, prop, angle) if mpl.rcParams['ps.useafm']: font = self._get_font_afm(prop) scale = 0.001 * prop.get_size_in_points() thisx = 0 last_name = None # kerns returns 0 for None. xs_names = [] for c in s: name = uni2type1.get(ord(c), f"uni{ord(c):04X}") try: width = font.get_width_from_char_name(name) except KeyError: name = 'question' width = font.get_width_char('?') kern = font.get_kern_dist_from_name(last_name, name) last_name = name thisx += kern * scale xs_names.append((thisx, name)) thisx += width * scale else: font = self._get_font_ttf(prop) font.set_text(s, 0, flags=LOAD_NO_HINTING) self._character_tracker.track(font, s) xs_names = [(item.x, font.get_glyph_name(item.glyph_idx)) for item in _text_helpers.layout(s, font)] self.set_color(*gc.get_rgb()) ps_name = (font.postscript_name.encode("ascii", "replace").decode("ascii")) self.set_font(ps_name, prop.get_size_in_points()) thetext = "\n".join(f"{x:f} 0 m /{name:s} glyphshow" for x, name in xs_names) self._pswriter.write(f"""\ gsave {self._get_clip_cmd(gc)} {x:f} {y:f} translate {angle:f} rotate {thetext} grestore """) def draw_mathtext(self, gc, x, y, s, prop, angle): """Draw the math text using matplotlib.mathtext.""" if debugPS: self._pswriter.write("% mathtext\n") width, height, descent, glyphs, rects = \ self._text2path.mathtext_parser.parse( s, 72, prop, _force_standard_ps_fonts=mpl.rcParams["ps.useafm"]) self.set_color(*gc.get_rgb()) self._pswriter.write(f"gsave\n" f"{x:f} {y:f} translate\n" f"{angle:f} rotate\n") lastfont = None for font, fontsize, num, ox, oy in glyphs: self._character_tracker.track(font, chr(num)) if (font.postscript_name, fontsize) != lastfont: lastfont = font.postscript_name, fontsize self._pswriter.write( f"/{font.postscript_name} {fontsize} selectfont\n") symbol_name = (font.get_name_char(chr(num)) if isinstance( font, AFM) else font.get_glyph_name(font.get_char_index(num))) self._pswriter.write(f"{ox:f} {oy:f} moveto\n" f"/{symbol_name} glyphshow\n") for ox, oy, w, h in rects: self._pswriter.write(f"{ox} {oy} {w} {h} rectfill\n") self._pswriter.write("grestore\n") def draw_gouraud_triangle(self, gc, points, colors, trans): self.draw_gouraud_triangles(gc, points.reshape((1, 3, 2)), colors.reshape((1, 3, 4)), trans) def draw_gouraud_triangles(self, gc, points, colors, trans): assert len(points) == len(colors) assert points.ndim == 3 assert points.shape[1] == 3 assert points.shape[2] == 2 assert colors.ndim == 3 assert colors.shape[1] == 3 assert colors.shape[2] == 4 shape = points.shape flat_points = points.reshape((shape[0] * shape[1], 2)) flat_points = trans.transform(flat_points) flat_colors = colors.reshape((shape[0] * shape[1], 4)) points_min = np.min(flat_points, axis=0) - (1 << 12) points_max = np.max(flat_points, axis=0) + (1 << 12) factor = np.ceil((2**32 - 1) / (points_max - points_min)) xmin, ymin = points_min xmax, ymax = points_max streamarr = np.empty(shape[0] * shape[1], dtype=[('flags', 'u1'), ('points', '2>u4'), ('colors', '3u1')]) streamarr['flags'] = 0 streamarr['points'] = (flat_points - points_min) * factor streamarr['colors'] = flat_colors[:, :3] * 255.0 stream = quote_ps_string(streamarr.tobytes()) self._pswriter.write(f"""\ gsave << /ShadingType 4 /ColorSpace [/DeviceRGB] /BitsPerCoordinate 32 /BitsPerComponent 8 /BitsPerFlag 8 /AntiAlias true /Decode [ {xmin:f} {xmax:f} {ymin:f} {ymax:f} 0 1 0 1 0 1 ] /DataSource ({stream}) >> shfill grestore """) def _draw_ps(self, ps, gc, rgbFace, fill=True, stroke=True, command=None): """ Emit the PostScript snippet 'ps' with all the attributes from 'gc' applied. 'ps' must consist of PostScript commands to construct a path. The fill and/or stroke kwargs can be set to False if the 'ps' string already includes filling and/or stroking, in which case _draw_ps is just supplying properties and clipping. """ # local variable eliminates all repeated attribute lookups write = self._pswriter.write if debugPS and command: write("% " + command + "\n") mightstroke = (gc.get_linewidth() > 0 and not _is_transparent(gc.get_rgb())) if not mightstroke: stroke = False if _is_transparent(rgbFace): fill = False hatch = gc.get_hatch() if mightstroke: self.set_linewidth(gc.get_linewidth()) self.set_linejoin(gc.get_joinstyle()) self.set_linecap(gc.get_capstyle()) self.set_linedash(*gc.get_dashes()) self.set_color(*gc.get_rgb()[:3]) write('gsave\n') write(self._get_clip_cmd(gc)) # Jochen, is the strip necessary? - this could be a honking big string write(ps.strip()) write("\n") if fill: if stroke or hatch: write("gsave\n") self.set_color(*rgbFace[:3], store=False) write("fill\n") if stroke or hatch: write("grestore\n") if hatch: hatch_name = self.create_hatch(hatch) write("gsave\n") write("%f %f %f " % gc.get_hatch_color()[:3]) write("%s setpattern fill grestore\n" % hatch_name) if stroke: write("stroke\n") write("grestore\n")
class __getattr__: NO_ESCAPE = _api.deprecated("3.6", obj_type="")(property(lambda self: _NO_ESCAPE)) re_mathsep = _api.deprecated("3.6", obj_type="")( property(lambda self: _split_math.__self__))
class Axis(maxis.XAxis): """An Axis class for the 3D plots.""" # These points from the unit cube make up the x, y and z-planes _PLANES = ( (0, 3, 7, 4), (1, 2, 6, 5), # yz planes (0, 1, 5, 4), (3, 2, 6, 7), # xz planes (0, 1, 2, 3), (4, 5, 6, 7), # xy planes ) # Some properties for the axes _AXINFO = { 'x': { 'i': 0, 'tickdir': 1, 'juggled': (1, 0, 2), 'color': (0.95, 0.95, 0.95, 0.5) }, 'y': { 'i': 1, 'tickdir': 0, 'juggled': (0, 1, 2), 'color': (0.90, 0.90, 0.90, 0.5) }, 'z': { 'i': 2, 'tickdir': 0, 'juggled': (0, 2, 1), 'color': (0.925, 0.925, 0.925, 0.5) }, } def _old_init(self, adir, v_intervalx, d_intervalx, axes, *args, rotate_label=None, **kwargs): return locals() def _new_init(self, axes, *, rotate_label=None, **kwargs): return locals() def __init__(self, *args, **kwargs): params = _api.select_matching_signature( [self._old_init, self._new_init], *args, **kwargs) if "adir" in params: _api.warn_deprecated( "3.6", message=f"The signature of 3D Axis constructors has " f"changed in %(since)s; the new signature is " f"{inspect.signature(type(self).__init__)}", pending=True) if params["adir"] != self.axis_name: raise ValueError(f"Cannot instantiate {type(self).__name__} " f"with adir={params['adir']!r}") axes = params["axes"] rotate_label = params["rotate_label"] args = params.get("args", ()) kwargs = params["kwargs"] name = self.axis_name # This is a temporary member variable. # Do not depend on this existing in future releases! self._axinfo = self._AXINFO[name].copy() if rcParams['_internal.classic_mode']: self._axinfo.update({ 'label': { 'va': 'center', 'ha': 'center' }, 'tick': { 'inward_factor': 0.2, 'outward_factor': 0.1, 'linewidth': { True: rcParams['lines.linewidth'], # major False: rcParams['lines.linewidth'], # minor } }, 'axisline': { 'linewidth': 0.75, 'color': (0, 0, 0, 1) }, 'grid': { 'color': (0.9, 0.9, 0.9, 1), 'linewidth': 1.0, 'linestyle': '-', }, }) else: self._axinfo.update({ 'label': { 'va': 'center', 'ha': 'center' }, 'tick': { 'inward_factor': 0.2, 'outward_factor': 0.1, 'linewidth': { True: ( # major rcParams['xtick.major.width'] if name in 'xz' else rcParams['ytick.major.width']), False: ( # minor rcParams['xtick.minor.width'] if name in 'xz' else rcParams['ytick.minor.width']), } }, 'axisline': { 'linewidth': rcParams['axes.linewidth'], 'color': rcParams['axes.edgecolor'], }, 'grid': { 'color': rcParams['grid.color'], 'linewidth': rcParams['grid.linewidth'], 'linestyle': rcParams['grid.linestyle'], }, }) super().__init__(axes, *args, **kwargs) # data and viewing intervals for this direction if "d_intervalx" in params: self.set_data_interval(*params["d_intervalx"]) if "v_intervalx" in params: self.set_view_interval(*params["v_intervalx"]) self.set_rotate_label(rotate_label) self._init3d() # Inline after init3d deprecation elapses. __init__.__signature__ = inspect.signature(_new_init) adir = _api.deprecated("3.6", pending=True)(property(lambda self: self.axis_name)) def _init3d(self): self.line = mlines.Line2D( xdata=(0, 0), ydata=(0, 0), linewidth=self._axinfo['axisline']['linewidth'], color=self._axinfo['axisline']['color'], antialiased=True) # Store dummy data in Polygon object self.pane = mpatches.Polygon(np.array([[0, 0], [0, 1], [1, 0], [0, 0]]), closed=False, alpha=0.8, facecolor='k', edgecolor='k') self.set_pane_color(self._axinfo['color']) self.axes._set_artist_props(self.line) self.axes._set_artist_props(self.pane) self.gridlines = art3d.Line3DCollection([]) self.axes._set_artist_props(self.gridlines) self.axes._set_artist_props(self.label) self.axes._set_artist_props(self.offsetText) # Need to be able to place the label at the correct location self.label._transform = self.axes.transData self.offsetText._transform = self.axes.transData @_api.deprecated("3.6", pending=True) def init3d(self): # After deprecation elapses, inline _init3d to __init__. self._init3d() def get_major_ticks(self, numticks=None): ticks = super().get_major_ticks(numticks) for t in ticks: for obj in [ t.tick1line, t.tick2line, t.gridline, t.label1, t.label2 ]: obj.set_transform(self.axes.transData) return ticks def get_minor_ticks(self, numticks=None): ticks = super().get_minor_ticks(numticks) for t in ticks: for obj in [ t.tick1line, t.tick2line, t.gridline, t.label1, t.label2 ]: obj.set_transform(self.axes.transData) return ticks def set_pane_pos(self, xys): xys = np.asarray(xys) xys = xys[:, :2] self.pane.xy = xys self.stale = True def set_pane_color(self, color): """Set pane color to a RGBA tuple.""" self._axinfo['color'] = color self.pane.set_edgecolor(color) self.pane.set_facecolor(color) self.pane.set_alpha(color[-1]) self.stale = True def set_rotate_label(self, val): """ Whether to rotate the axis label: True, False or None. If set to None the label will be rotated if longer than 4 chars. """ self._rotate_label = val self.stale = True def get_rotate_label(self, text): if self._rotate_label is not None: return self._rotate_label else: return len(text) > 4 def _get_coord_info(self, renderer): mins, maxs = np.array([ self.axes.get_xbound(), self.axes.get_ybound(), self.axes.get_zbound(), ]).T # Get the mean value for each bound: centers = 0.5 * (maxs + mins) # Add a small offset between min/max point and the edge of the # plot: deltas = (maxs - mins) / 12 mins -= 0.25 * deltas maxs += 0.25 * deltas # Project the bounds along the current position of the cube: bounds = mins[0], maxs[0], mins[1], maxs[1], mins[2], maxs[2] bounds_proj = self.axes.tunit_cube(bounds, self.axes.M) # Determine which one of the parallel planes are higher up: highs = np.zeros(3, dtype=bool) for i in range(3): mean_z0 = np.mean(bounds_proj[self._PLANES[2 * i], 2]) mean_z1 = np.mean(bounds_proj[self._PLANES[2 * i + 1], 2]) highs[i] = mean_z0 < mean_z1 return mins, maxs, centers, deltas, bounds_proj, highs def _get_axis_line_edge_points(self, minmax, maxmin): """Get the edge points for the black bolded axis line.""" # When changing vertical axis some of the axes has to be # moved to the other plane so it looks the same as if the z-axis # was the vertical axis. mb = [minmax, maxmin] mb_rev = mb[::-1] mm = [[mb, mb_rev, mb_rev], [mb_rev, mb_rev, mb], [mb, mb, mb]] mm = mm[self.axes._vertical_axis][self._axinfo["i"]] juggled = self._axinfo["juggled"] edge_point_0 = mm[0].copy() edge_point_0[juggled[0]] = mm[1][juggled[0]] edge_point_1 = edge_point_0.copy() edge_point_1[juggled[1]] = mm[1][juggled[1]] return edge_point_0, edge_point_1 def _get_tickdir(self): """ Get the direction of the tick. Returns ------- tickdir : int Index which indicates which coordinate the tick line will align with. """ # TODO: Move somewhere else where it's triggered less: tickdirs_base = [v["tickdir"] for v in self._AXINFO.values()] info_i = [v["i"] for v in self._AXINFO.values()] i = self._axinfo["i"] j = self.axes._vertical_axis - 2 # tickdir = [[1, 2, 1], [2, 2, 0], [1, 0, 0]][i] tickdir = np.roll(info_i, -j)[np.roll(tickdirs_base, j)][i] return tickdir def draw_pane(self, renderer): renderer.open_group('pane3d', gid=self.get_gid()) mins, maxs, centers, deltas, tc, highs = self._get_coord_info(renderer) info = self._axinfo index = info['i'] if not highs[index]: plane = self._PLANES[2 * index] else: plane = self._PLANES[2 * index + 1] xys = [tc[p] for p in plane] self.set_pane_pos(xys) self.pane.draw(renderer) renderer.close_group('pane3d') @artist.allow_rasterization def draw(self, renderer): self.label._transform = self.axes.transData renderer.open_group("axis3d", gid=self.get_gid()) ticks = self._update_ticks() # Get general axis information: info = self._axinfo index = info["i"] juggled = info["juggled"] mins, maxs, centers, deltas, tc, highs = self._get_coord_info(renderer) minmax = np.where(highs, maxs, mins) maxmin = np.where(~highs, maxs, mins) # Create edge points for the black bolded axis line: edgep1, edgep2 = self._get_axis_line_edge_points(minmax, maxmin) # Project the edge points along the current position and # create the line: pep = proj3d.proj_trans_points([edgep1, edgep2], self.axes.M) pep = np.asarray(pep) self.line.set_data(pep[0], pep[1]) self.line.draw(renderer) # Grid points where the planes meet xyz0 = np.tile(minmax, (len(ticks), 1)) xyz0[:, index] = [tick.get_loc() for tick in ticks] # Draw labels # The transAxes transform is used because the Text object # rotates the text relative to the display coordinate system. # Therefore, if we want the labels to remain parallel to the # axis regardless of the aspect ratio, we need to convert the # edge points of the plane to display coordinates and calculate # an angle from that. # TODO: Maybe Text objects should handle this themselves? dx, dy = (self.axes.transAxes.transform([pep[0:2, 1]]) - self.axes.transAxes.transform([pep[0:2, 0]]))[0] lxyz = 0.5 * (edgep1 + edgep2) # A rough estimate; points are ambiguous since 3D plots rotate reltoinches = self.figure.dpi_scale_trans.inverted() ax_inches = reltoinches.transform(self.axes.bbox.size) ax_points_estimate = sum(72. * ax_inches) deltas_per_point = 48 / ax_points_estimate default_offset = 21. labeldeltas = ((self.labelpad + default_offset) * deltas_per_point * deltas) axmask = [True, True, True] axmask[index] = False lxyz = move_from_center(lxyz, centers, labeldeltas, axmask) tlx, tly, tlz = proj3d.proj_transform(*lxyz, self.axes.M) self.label.set_position((tlx, tly)) if self.get_rotate_label(self.label.get_text()): angle = art3d._norm_text_angle(np.rad2deg(np.arctan2(dy, dx))) self.label.set_rotation(angle) self.label.set_va(info['label']['va']) self.label.set_ha(info['label']['ha']) self.label.draw(renderer) # Draw Offset text # Which of the two edge points do we want to # use for locating the offset text? if juggled[2] == 2: outeredgep = edgep1 outerindex = 0 else: outeredgep = edgep2 outerindex = 1 pos = move_from_center(outeredgep, centers, labeldeltas, axmask) olx, oly, olz = proj3d.proj_transform(*pos, self.axes.M) self.offsetText.set_text(self.major.formatter.get_offset()) self.offsetText.set_position((olx, oly)) angle = art3d._norm_text_angle(np.rad2deg(np.arctan2(dy, dx))) self.offsetText.set_rotation(angle) # Must set rotation mode to "anchor" so that # the alignment point is used as the "fulcrum" for rotation. self.offsetText.set_rotation_mode('anchor') # ---------------------------------------------------------------------- # Note: the following statement for determining the proper alignment of # the offset text. This was determined entirely by trial-and-error # and should not be in any way considered as "the way". There are # still some edge cases where alignment is not quite right, but this # seems to be more of a geometry issue (in other words, I might be # using the wrong reference points). # # (TT, FF, TF, FT) are the shorthand for the tuple of # (centpt[info['tickdir']] <= pep[info['tickdir'], outerindex], # centpt[index] <= pep[index, outerindex]) # # Three-letters (e.g., TFT, FTT) are short-hand for the array of bools # from the variable 'highs'. # --------------------------------------------------------------------- centpt = proj3d.proj_transform(*centers, self.axes.M) if centpt[info['tickdir']] > pep[info['tickdir'], outerindex]: # if FT and if highs has an even number of Trues if (centpt[index] <= pep[index, outerindex] and np.count_nonzero(highs) % 2 == 0): # Usually, this means align right, except for the FTT case, # in which offset for axis 1 and 2 are aligned left. if highs.tolist() == [False, True, True] and index in (1, 2): align = 'left' else: align = 'right' else: # The FF case align = 'left' else: # if TF and if highs has an even number of Trues if (centpt[index] > pep[index, outerindex] and np.count_nonzero(highs) % 2 == 0): # Usually mean align left, except if it is axis 2 if index == 2: align = 'right' else: align = 'left' else: # The TT case align = 'right' self.offsetText.set_va('center') self.offsetText.set_ha(align) self.offsetText.draw(renderer) if self.axes._draw_grid and len(ticks): # Grid lines go from the end of one plane through the plane # intersection (at xyz0) to the end of the other plane. The first # point (0) differs along dimension index-2 and the last (2) along # dimension index-1. lines = np.stack([xyz0, xyz0, xyz0], axis=1) lines[:, 0, index - 2] = maxmin[index - 2] lines[:, 2, index - 1] = maxmin[index - 1] self.gridlines.set_segments(lines) self.gridlines.set_color(info['grid']['color']) self.gridlines.set_linewidth(info['grid']['linewidth']) self.gridlines.set_linestyle(info['grid']['linestyle']) self.gridlines.do_3d_projection() self.gridlines.draw(renderer) # Draw ticks: tickdir = self._get_tickdir() tickdelta = deltas[tickdir] if highs[tickdir]: ticksign = 1 else: ticksign = -1 for tick in ticks: # Get tick line positions pos = edgep1.copy() pos[index] = tick.get_loc() pos[tickdir] = ( edgep1[tickdir] + info['tick']['outward_factor'] * ticksign * tickdelta) x1, y1, z1 = proj3d.proj_transform(*pos, self.axes.M) pos[tickdir] = ( edgep1[tickdir] - info['tick']['inward_factor'] * ticksign * tickdelta) x2, y2, z2 = proj3d.proj_transform(*pos, self.axes.M) # Get position of label default_offset = 8. # A rough estimate labeldeltas = ((tick.get_pad() + default_offset) * deltas_per_point * deltas) axmask = [True, True, True] axmask[index] = False pos[tickdir] = edgep1[tickdir] pos = move_from_center(pos, centers, labeldeltas, axmask) lx, ly, lz = proj3d.proj_transform(*pos, self.axes.M) tick_update_position(tick, (x1, x2), (y1, y2), (lx, ly)) tick.tick1line.set_linewidth( info['tick']['linewidth'][tick._major]) tick.draw(renderer) renderer.close_group('axis3d') self.stale = False # TODO: Get this to work (more) properly when mplot3d supports the # transforms framework. def get_tightbbox(self, renderer, *, for_layout_only=False): # docstring inherited if not self.get_visible(): return # We have to directly access the internal data structures # (and hope they are up to date) because at draw time we # shift the ticks and their labels around in (x, y) space # based on the projection, the current view port, and their # position in 3D space. If we extend the transforms framework # into 3D we would not need to do this different book keeping # than we do in the normal axis major_locs = self.get_majorticklocs() minor_locs = self.get_minorticklocs() ticks = [ *self.get_minor_ticks(len(minor_locs)), *self.get_major_ticks(len(major_locs)) ] view_low, view_high = self.get_view_interval() if view_low > view_high: view_low, view_high = view_high, view_low interval_t = self.get_transform().transform([view_low, view_high]) ticks_to_draw = [] for tick in ticks: try: loc_t = self.get_transform().transform(tick.get_loc()) except AssertionError: # Transform.transform doesn't allow masked values but # some scales might make them, so we need this try/except. pass else: if mtransforms._interval_contains_close(interval_t, loc_t): ticks_to_draw.append(tick) ticks = ticks_to_draw bb_1, bb_2 = self._get_ticklabel_bboxes(ticks, renderer) other = [] if self.line.get_visible(): other.append(self.line.get_window_extent(renderer)) if (self.label.get_visible() and not for_layout_only and self.label.get_text()): other.append(self.label.get_window_extent(renderer)) return mtransforms.Bbox.union([*bb_1, *bb_2, *other]) d_interval = _api.deprecated( "3.6", alternative="get_data_interval", pending=True)(property( lambda self: self.get_data_interval(), lambda self, minmax: self.set_data_interval(*minmax))) v_interval = _api.deprecated( "3.6", alternative="get_view_interval", pending=True)(property( lambda self: self.get_view_interval(), lambda self, minmax: self.set_view_interval(*minmax)))
class PdfPages: """ A multi-page PDF file using the pgf backend Examples -------- >>> import matplotlib.pyplot as plt >>> # Initialize: >>> with PdfPages('foo.pdf') as pdf: ... # As many times as you like, create a figure fig and save it: ... fig = plt.figure() ... pdf.savefig(fig) ... # When no figure is specified the current figure is saved ... pdf.savefig() """ __slots__ = ( '_output_name', 'keep_empty', '_n_figures', '_file', '_info_dict', '_metadata', ) metadata = _api.deprecated('3.3')(property(lambda self: self._metadata)) def __init__(self, filename, *, keep_empty=True, metadata=None): """ Create a new PdfPages object. Parameters ---------- filename : str or path-like Plots using `PdfPages.savefig` will be written to a file at this location. Any older file with the same name is overwritten. keep_empty : bool, default: True If set to False, then empty pdf files will be deleted automatically when closed. metadata : dict, optional Information dictionary object (see PDF reference section 10.2.1 'Document Information Dictionary'), e.g.: ``{'Creator': 'My software', 'Author': 'Me', 'Title': 'Awesome'}``. The standard keys are 'Title', 'Author', 'Subject', 'Keywords', 'Creator', 'Producer', 'CreationDate', 'ModDate', and 'Trapped'. Values have been predefined for 'Creator', 'Producer' and 'CreationDate'. They can be removed by setting them to `None`. """ self._output_name = filename self._n_figures = 0 self.keep_empty = keep_empty self._metadata = (metadata or {}).copy() if metadata: for key in metadata: canonical = { 'creationdate': 'CreationDate', 'moddate': 'ModDate', }.get(key.lower(), key.lower().title()) if canonical != key: _api.warn_deprecated( '3.3', message='Support for setting PDF metadata keys ' 'case-insensitively is deprecated since %(since)s and ' 'will be removed %(removal)s; ' f'set {canonical} instead of {key}.') self._metadata[canonical] = self._metadata.pop(key) self._info_dict = _create_pdf_info_dict('pgf', self._metadata) self._file = BytesIO() def _write_header(self, width_inches, height_inches): hyperref_options = ','.join( _metadata_to_str(k, v) for k, v in self._info_dict.items()) latex_preamble = get_preamble() latex_fontspec = get_fontspec() latex_header = r"""\PassOptionsToPackage{{ pdfinfo={{ {metadata} }} }}{{hyperref}} \RequirePackage{{hyperref}} \documentclass[12pt]{{minimal}} \usepackage[ paperwidth={width}in, paperheight={height}in, margin=0in ]{{geometry}} {preamble} {fontspec} \usepackage{{pgf}} \setlength{{\parindent}}{{0pt}} \begin{{document}}%% """.format( width=width_inches, height=height_inches, preamble=latex_preamble, fontspec=latex_fontspec, metadata=hyperref_options, ) self._file.write(latex_header.encode('utf-8')) def __enter__(self): return self def __exit__(self, exc_type, exc_val, exc_tb): self.close() def close(self): """ Finalize this object, running LaTeX in a temporary directory and moving the final pdf file to *filename*. """ self._file.write(rb'\end{document}\n') if self._n_figures > 0: self._run_latex() elif self.keep_empty: open(self._output_name, 'wb').close() self._file.close() def _run_latex(self): texcommand = mpl.rcParams["pgf.texsystem"] with TemporaryDirectory() as tmpdir: tex_source = pathlib.Path(tmpdir, "pdf_pages.tex") tex_source.write_bytes(self._file.getvalue()) cbook._check_and_log_subprocess( [texcommand, "-interaction=nonstopmode", "-halt-on-error", tex_source], _log, cwd=tmpdir) shutil.move(tex_source.with_suffix(".pdf"), self._output_name) def savefig(self, figure=None, **kwargs): """ Save a `.Figure` to this file as a new page. Any other keyword arguments are passed to `~.Figure.savefig`. Parameters ---------- figure : `.Figure` or int, optional Specifies what figure is saved to file. If not specified, the active figure is saved. If a `.Figure` instance is provided, this figure is saved. If an int is specified, the figure instance to save is looked up by number. """ if not isinstance(figure, Figure): if figure is None: manager = Gcf.get_active() else: manager = Gcf.get_fig_manager(figure) if manager is None: raise ValueError("No figure {}".format(figure)) figure = manager.canvas.figure try: orig_canvas = figure.canvas figure.canvas = FigureCanvasPgf(figure) width, height = figure.get_size_inches() if self._n_figures == 0: self._write_header(width, height) else: # \pdfpagewidth and \pdfpageheight exist on pdftex, xetex, and # luatex<0.85; they were renamed to \pagewidth and \pageheight # on luatex>=0.85. self._file.write( br'\newpage' br'\ifdefined\pdfpagewidth\pdfpagewidth' br'\else\pagewidth\fi=%ain' br'\ifdefined\pdfpageheight\pdfpageheight' br'\else\pageheight\fi=%ain' b'%%\n' % (width, height) ) figure.savefig(self._file, format="pgf", **kwargs) self._n_figures += 1 finally: figure.canvas = orig_canvas def get_pagecount(self): """Return the current number of pages in the multipage pdf file.""" return self._n_figures
_mathtext.ship(0, 0, box) return (self.width, self.height + self.depth, self.depth, self.glyphs, self.rects) for _cls_name in [ "Fonts", *[c.__name__ for c in _mathtext.Fonts.__subclasses__()], "FontConstantsBase", *[c.__name__ for c in _mathtext.FontConstantsBase.__subclasses__()], "Node", *[c.__name__ for c in _mathtext.Node.__subclasses__()], "Ship", "Parser", ]: globals()[_cls_name] = _api.deprecated("3.4")(type( _cls_name, (getattr(_mathtext, _cls_name), ), {})) class MathTextWarning(Warning): pass @_api.deprecated("3.3") class GlueSpec: """See `Glue`.""" def __init__(self, width=0., stretch=0., stretch_order=0, shrink=0., shrink_order=0):
class FigureFrameWx(wx.Frame): def __init__(self, num, fig, *, canvas_class=None): # On non-Windows platform, explicitly set the position - fix # positioning bug on some Linux platforms if wx.Platform == '__WXMSW__': pos = wx.DefaultPosition else: pos = wx.Point(20, 20) super().__init__(parent=None, id=-1, pos=pos) # Frame will be sized later by the Fit method _log.debug("%s - __init__()", type(self)) _set_frame_icon(self) # The parameter will become required after the deprecation elapses. if canvas_class is not None: self.canvas = canvas_class(self, -1, fig) else: _api.warn_deprecated( "3.6", message="The canvas_class parameter will become " "required after the deprecation period starting in Matplotlib " "%(since)s elapses.") self.canvas = self.get_canvas(fig) # Auto-attaches itself to self.canvas.manager manager = FigureManagerWx(self.canvas, num, self) toolbar = self.canvas.manager.toolbar if toolbar is not None: self.SetToolBar(toolbar) # On Windows, canvas sizing must occur after toolbar addition; # otherwise the toolbar further resizes the canvas. w, h = map(math.ceil, fig.bbox.size) self.canvas.SetInitialSize(wx.Size(w, h)) self.canvas.SetMinSize((2, 2)) self.canvas.SetFocus() self.Fit() self.Bind(wx.EVT_CLOSE, self._on_close) sizer = _api.deprecated("3.6", alternative="frame.GetSizer()")( property(lambda self: self.GetSizer())) figmgr = _api.deprecated("3.6", alternative="frame.canvas.manager")( property(lambda self: self.canvas.manager)) num = _api.deprecated("3.6", alternative="frame.canvas.manager.num")( property(lambda self: self.canvas.manager.num)) toolbar = _api.deprecated("3.6", alternative="frame.GetToolBar()")( property(lambda self: self.GetToolBar())) toolmanager = _api.deprecated( "3.6", alternative="frame.canvas.manager.toolmanager")( property(lambda self: self.canvas.manager.toolmanager)) @_api.deprecated("3.6", alternative="the canvas_class constructor parameter") def get_canvas(self, fig): return FigureCanvasWx(self, -1, fig) @_api.deprecated("3.6", alternative="frame.canvas.manager") def get_figure_manager(self): _log.debug("%s - get_figure_manager()", type(self)) return self.canvas.manager def _on_close(self, event): _log.debug("%s - on_close()", type(self)) self.canvas.close_event() self.canvas.stop_event_loop() # set FigureManagerWx.frame to None to prevent repeated attempts to # close this frame from FigureManagerWx.destroy() self.canvas.manager.frame = None # remove figure manager from Gcf.figs Gcf.destroy(self.canvas.manager) try: # See issue 2941338. self.canvas.mpl_disconnect(self.canvas.toolbar._id_drag) except AttributeError: # If there's no toolbar. pass # Carry on with close event propagation, frame & children destruction event.Skip()
class NavigationToolbar2Tk(NavigationToolbar2, tk.Frame): window = _api.deprecated("3.6", alternative="self.master")( property(lambda self: self.master)) def __init__(self, canvas, window=None, *, pack_toolbar=True): """ Parameters ---------- canvas : `FigureCanvas` The figure canvas on which to operate. window : tk.Window The tk.Window which owns this toolbar. pack_toolbar : bool, default: True If True, add the toolbar to the parent's pack manager's packing list during initialization with ``side="bottom"`` and ``fill="x"``. If you want to use the toolbar with a different layout manager, use ``pack_toolbar=False``. """ if window is None: window = canvas.get_tk_widget().master tk.Frame.__init__(self, master=window, borderwidth=2, width=int(canvas.figure.bbox.width), height=50) self._buttons = {} for text, tooltip_text, image_file, callback in self.toolitems: if text is None: # Add a spacer; return value is unused. self._Spacer() else: self._buttons[text] = button = self._Button( text, str(cbook._get_data_path(f"images/{image_file}.png")), toggle=callback in ["zoom", "pan"], command=getattr(self, callback), ) if tooltip_text is not None: ToolTip.createToolTip(button, tooltip_text) self._label_font = tkinter.font.Font(root=window, size=10) # This filler item ensures the toolbar is always at least two text # lines high. Otherwise the canvas gets redrawn as the mouse hovers # over images because those use two-line messages which resize the # toolbar. label = tk.Label(master=self, font=self._label_font, text='\N{NO-BREAK SPACE}\n\N{NO-BREAK SPACE}') label.pack(side=tk.RIGHT) self.message = tk.StringVar(master=self) self._message_label = tk.Label(master=self, font=self._label_font, textvariable=self.message, justify=tk.RIGHT) self._message_label.pack(side=tk.RIGHT) NavigationToolbar2.__init__(self, canvas) if pack_toolbar: self.pack(side=tk.BOTTOM, fill=tk.X) def _rescale(self): """ Scale all children of the toolbar to current DPI setting. Before this is called, the Tk scaling setting will have been updated to match the new DPI. Tk widgets do not update for changes to scaling, but all measurements made after the change will match the new scaling. Thus this function re-applies all the same sizes in points, which Tk will scale correctly to pixels. """ for widget in self.winfo_children(): if isinstance(widget, (tk.Button, tk.Checkbutton)): if hasattr(widget, '_image_file'): # Explicit class because ToolbarTk calls _rescale. NavigationToolbar2Tk._set_image_for_button(self, widget) else: # Text-only button is handled by the font setting instead. pass elif isinstance(widget, tk.Frame): widget.configure(height='22p', pady='1p') widget.pack_configure(padx='4p') elif isinstance(widget, tk.Label): pass # Text is handled by the font setting instead. else: _log.warning('Unknown child class %s', widget.winfo_class) self._label_font.configure(size=10) def _update_buttons_checked(self): # sync button checkstates to match active mode for text, mode in [('Zoom', _Mode.ZOOM), ('Pan', _Mode.PAN)]: if text in self._buttons: if self.mode == mode: self._buttons[text].select() # NOT .invoke() else: self._buttons[text].deselect() def pan(self, *args): super().pan(*args) self._update_buttons_checked() def zoom(self, *args): super().zoom(*args) self._update_buttons_checked() def set_message(self, s): self.message.set(s) def draw_rubberband(self, event, x0, y0, x1, y1): # Block copied from remove_rubberband for backend_tools convenience. if self.canvas._rubberband_rect: self.canvas._tkcanvas.delete(self.canvas._rubberband_rect) height = self.canvas.figure.bbox.height y0 = height - y0 y1 = height - y1 self.canvas._rubberband_rect = self.canvas._tkcanvas.create_rectangle( x0, y0, x1, y1) def remove_rubberband(self): if self.canvas._rubberband_rect: self.canvas._tkcanvas.delete(self.canvas._rubberband_rect) self.canvas._rubberband_rect = None lastrect = _api.deprecated("3.6")( property(lambda self: self.canvas._rubberband_rect)) def _set_image_for_button(self, button): """ Set the image for a button based on its pixel size. The pixel size is determined by the DPI scaling of the window. """ if button._image_file is None: return # Allow _image_file to be relative to Matplotlib's "images" data # directory. path_regular = cbook._get_data_path('images', button._image_file) path_large = path_regular.with_name( path_regular.name.replace('.png', '_large.png')) size = button.winfo_pixels('18p') # Nested functions because ToolbarTk calls _Button. def _get_color(color_name): # `winfo_rgb` returns an (r, g, b) tuple in the range 0-65535 return button.winfo_rgb(button.cget(color_name)) def _is_dark(color): if isinstance(color, str): color = _get_color(color) return max(color) < 65535 / 2 def _recolor_icon(image, color): image_data = np.asarray(image).copy() black_mask = (image_data[..., :3] == 0).all(axis=-1) image_data[black_mask, :3] = color return Image.fromarray(image_data, mode="RGBA") # Use the high-resolution (48x48 px) icon if it exists and is needed with Image.open(path_large if ( size > 24 and path_large.exists()) else path_regular) as im: image = ImageTk.PhotoImage(im.resize((size, size)), master=self) button._ntimage = image # create a version of the icon with the button's text color foreground = (255 / 65535) * np.array( button.winfo_rgb(button.cget("foreground"))) im_alt = _recolor_icon(im, foreground) image_alt = ImageTk.PhotoImage(im_alt.resize((size, size)), master=self) button._ntimage_alt = image_alt if _is_dark("background"): button.configure(image=image_alt) else: button.configure(image=image) # Checkbuttons may switch the background to `selectcolor` in the # checked state, so check separately which image it needs to use in # that state to still ensure enough contrast with the background. if (isinstance(button, tk.Checkbutton) and button.cget("selectcolor") != ""): if self._windowingsystem != "x11": selectcolor = "selectcolor" else: # On X11, selectcolor isn't used directly for indicator-less # buttons. See `::tk::CheckEnter` in the Tk button.tcl source # code for details. r1, g1, b1 = _get_color("selectcolor") r2, g2, b2 = _get_color("activebackground") selectcolor = ((r1 + r2) / 2, (g1 + g2) / 2, (b1 + b2) / 2) if _is_dark(selectcolor): button.configure(selectimage=image_alt) else: button.configure(selectimage=image) button.configure(height='18p', width='18p') def _Button(self, text, image_file, toggle, command): if not toggle: b = tk.Button( master=self, text=text, command=command, relief="flat", overrelief="groove", borderwidth=1, ) else: # There is a bug in tkinter included in some python 3.6 versions # that without this variable, produces a "visual" toggling of # other near checkbuttons # https://bugs.python.org/issue29402 # https://bugs.python.org/issue25684 var = tk.IntVar(master=self) b = tk.Checkbutton(master=self, text=text, command=command, indicatoron=False, variable=var, offrelief="flat", overrelief="groove", borderwidth=1) b.var = var b._image_file = image_file if image_file is not None: # Explicit class because ToolbarTk calls _Button. NavigationToolbar2Tk._set_image_for_button(self, b) else: b.configure(font=self._label_font) b.pack(side=tk.LEFT) return b def _Spacer(self): # Buttons are also 18pt high. s = tk.Frame(master=self, height='18p', relief=tk.RIDGE, bg='DarkGray') s.pack(side=tk.LEFT, padx='3p') return s def save_figure(self, *args): filetypes = self.canvas.get_supported_filetypes().copy() default_filetype = self.canvas.get_default_filetype() # Tk doesn't provide a way to choose a default filetype, # so we just have to put it first default_filetype_name = filetypes.pop(default_filetype) sorted_filetypes = ([(default_filetype, default_filetype_name)] + sorted(filetypes.items())) tk_filetypes = [(name, '*.%s' % ext) for ext, name in sorted_filetypes] # adding a default extension seems to break the # asksaveasfilename dialog when you choose various save types # from the dropdown. Passing in the empty string seems to # work - JDH! # defaultextension = self.canvas.get_default_filetype() defaultextension = '' initialdir = os.path.expanduser(mpl.rcParams['savefig.directory']) initialfile = self.canvas.get_default_filename() fname = tkinter.filedialog.asksaveasfilename( master=self.canvas.get_tk_widget().master, title='Save the figure', filetypes=tk_filetypes, defaultextension=defaultextension, initialdir=initialdir, initialfile=initialfile, ) if fname in ["", ()]: return # Save dir for next time, unless empty str (i.e., use cwd). if initialdir != "": mpl.rcParams['savefig.directory'] = (os.path.dirname(str(fname))) try: # This method will handle the delegation to the correct type self.canvas.figure.savefig(fname) except Exception as e: tkinter.messagebox.showerror("Error saving file", str(e)) def set_history_buttons(self): state_map = {True: tk.NORMAL, False: tk.DISABLED} can_back = self._nav_stack._pos > 0 can_forward = self._nav_stack._pos < len(self._nav_stack._elements) - 1 if "Back" in self._buttons: self._buttons['Back']['state'] = state_map[can_back] if "Forward" in self._buttons: self._buttons['Forward']['state'] = state_map[can_forward]
class NavigationToolbar2GTK3(_NavigationToolbar2GTK, Gtk.Toolbar): @_api.delete_parameter("3.6", "window") def __init__(self, canvas, window=None): self._win = window GObject.GObject.__init__(self) self.set_style(Gtk.ToolbarStyle.ICONS) self._gtk_ids = {} for text, tooltip_text, image_file, callback in self.toolitems: if text is None: self.insert(Gtk.SeparatorToolItem(), -1) continue image = Gtk.Image.new_from_gicon( Gio.Icon.new_for_string( str( cbook._get_data_path('images', f'{image_file}-symbolic.svg'))), Gtk.IconSize.LARGE_TOOLBAR) self._gtk_ids[text] = button = (Gtk.ToggleToolButton() if callback in ['zoom', 'pan'] else Gtk.ToolButton()) button.set_label(text) button.set_icon_widget(image) # Save the handler id, so that we can block it as needed. button._signal_handler = button.connect('clicked', getattr(self, callback)) button.set_tooltip_text(tooltip_text) self.insert(button, -1) # This filler item ensures the toolbar is always at least two text # lines high. Otherwise the canvas gets redrawn as the mouse hovers # over images because those use two-line messages which resize the # toolbar. toolitem = Gtk.ToolItem() self.insert(toolitem, -1) label = Gtk.Label() label.set_markup( '<small>\N{NO-BREAK SPACE}\n\N{NO-BREAK SPACE}</small>') toolitem.set_expand(True) # Push real message to the right. toolitem.add(label) toolitem = Gtk.ToolItem() self.insert(toolitem, -1) self.message = Gtk.Label() self.message.set_justify(Gtk.Justification.RIGHT) toolitem.add(self.message) self.show_all() _NavigationToolbar2GTK.__init__(self, canvas) win = _api.deprecated("3.6")(property(lambda self: self._win)) def save_figure(self, *args): dialog = Gtk.FileChooserDialog( title="Save the figure", parent=self.canvas.get_toplevel(), action=Gtk.FileChooserAction.SAVE, buttons=(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, Gtk.STOCK_SAVE, Gtk.ResponseType.OK), ) for name, fmts \ in self.canvas.get_supported_filetypes_grouped().items(): ff = Gtk.FileFilter() ff.set_name(name) for fmt in fmts: ff.add_pattern(f'*.{fmt}') dialog.add_filter(ff) if self.canvas.get_default_filetype() in fmts: dialog.set_filter(ff) @functools.partial(dialog.connect, "notify::filter") def on_notify_filter(*args): name = dialog.get_filter().get_name() fmt = self.canvas.get_supported_filetypes_grouped()[name][0] dialog.set_current_name( str(Path(dialog.get_current_name()).with_suffix(f'.{fmt}'))) dialog.set_current_folder(mpl.rcParams["savefig.directory"]) dialog.set_current_name(self.canvas.get_default_filename()) dialog.set_do_overwrite_confirmation(True) response = dialog.run() fname = dialog.get_filename() ff = dialog.get_filter() # Doesn't autoadjust to filename :/ fmt = self.canvas.get_supported_filetypes_grouped()[ff.get_name()][0] dialog.destroy() if response != Gtk.ResponseType.OK: return # Save dir for next time, unless empty str (which means use cwd). if mpl.rcParams['savefig.directory']: mpl.rcParams['savefig.directory'] = os.path.dirname(fname) try: self.canvas.figure.savefig(fname, format=fmt) except Exception as e: dialog = Gtk.MessageDialog(parent=self.canvas.get_toplevel(), message_format=str(e), type=Gtk.MessageType.ERROR, buttons=Gtk.ButtonsType.OK) dialog.run() dialog.destroy()
class NavigationToolbar2GTK4(_NavigationToolbar2GTK, Gtk.Box): @_api.delete_parameter("3.6", "window") def __init__(self, canvas, window=None): self._win = window Gtk.Box.__init__(self) self.add_css_class('toolbar') self._gtk_ids = {} for text, tooltip_text, image_file, callback in self.toolitems: if text is None: self.append(Gtk.Separator()) continue image = Gtk.Image.new_from_gicon( Gio.Icon.new_for_string( str( cbook._get_data_path('images', f'{image_file}-symbolic.svg')))) self._gtk_ids[text] = button = (Gtk.ToggleButton() if callback in [ 'zoom', 'pan' ] else Gtk.Button()) button.set_child(image) button.add_css_class('flat') button.add_css_class('image-button') # Save the handler id, so that we can block it as needed. button._signal_handler = button.connect('clicked', getattr(self, callback)) button.set_tooltip_text(tooltip_text) self.append(button) # This filler item ensures the toolbar is always at least two text # lines high. Otherwise the canvas gets redrawn as the mouse hovers # over images because those use two-line messages which resize the # toolbar. label = Gtk.Label() label.set_markup( '<small>\N{NO-BREAK SPACE}\n\N{NO-BREAK SPACE}</small>') label.set_hexpand(True) # Push real message to the right. self.append(label) self.message = Gtk.Label() self.message.set_justify(Gtk.Justification.RIGHT) self.append(self.message) _NavigationToolbar2GTK.__init__(self, canvas) win = _api.deprecated("3.6")(property(lambda self: self._win)) def save_figure(self, *args): dialog = Gtk.FileChooserNative(title='Save the figure', transient_for=self.canvas.get_root(), action=Gtk.FileChooserAction.SAVE, modal=True) self._save_dialog = dialog # Must keep a reference. ff = Gtk.FileFilter() ff.set_name('All files') ff.add_pattern('*') dialog.add_filter(ff) dialog.set_filter(ff) formats = [] default_format = None for i, (name, fmts) in enumerate( self.canvas.get_supported_filetypes_grouped().items()): ff = Gtk.FileFilter() ff.set_name(name) for fmt in fmts: ff.add_pattern(f'*.{fmt}') dialog.add_filter(ff) formats.append(name) if self.canvas.get_default_filetype() in fmts: default_format = i # Setting the choice doesn't always work, so make sure the default # format is first. formats = [ formats[default_format], *formats[:default_format], *formats[default_format + 1:] ] dialog.add_choice('format', 'File format', formats, formats) dialog.set_choice('format', formats[default_format]) dialog.set_current_folder( Gio.File.new_for_path( os.path.expanduser(mpl.rcParams['savefig.directory']))) dialog.set_current_name(self.canvas.get_default_filename()) @functools.partial(dialog.connect, 'response') def on_response(dialog, response): file = dialog.get_file() fmt = dialog.get_choice('format') fmt = self.canvas.get_supported_filetypes_grouped()[fmt][0] dialog.destroy() self._save_dialog = None if response != Gtk.ResponseType.ACCEPT: return # Save dir for next time, unless empty str (which means use cwd). if mpl.rcParams['savefig.directory']: parent = file.get_parent() mpl.rcParams['savefig.directory'] = parent.get_path() try: self.canvas.figure.savefig(file.get_path(), format=fmt) except Exception as e: msg = Gtk.MessageDialog(transient_for=self.canvas.get_root(), message_type=Gtk.MessageType.ERROR, buttons=Gtk.ButtonsType.OK, modal=True, text=str(e)) msg.show() dialog.show()
class __getattr__: LUTSIZE = _api.deprecated( "3.5", obj_type="", alternative="rcParams['image.lut']")( property(lambda self: _LUTSIZE))
class ScalarMappable: """ A mixin class to map scalar data to RGBA. The ScalarMappable applies data normalization before returning RGBA colors from the given colormap. """ def __init__(self, norm=None, cmap=None): """ Parameters ---------- norm : `matplotlib.colors.Normalize` (or subclass thereof) The normalizing object which scales data, typically into the interval ``[0, 1]``. If *None*, *norm* defaults to a *colors.Normalize* object which initializes its scaling based on the first data processed. cmap : str or `~matplotlib.colors.Colormap` The colormap used to map normalized data values to RGBA colors. """ self._A = None self._norm = None # So that the setter knows we're initializing. self.set_norm(norm) # The Normalize instance of this ScalarMappable. self.cmap = None # So that the setter knows we're initializing. self.set_cmap(cmap) # The Colormap instance of this ScalarMappable. #: The last colorbar associated with this ScalarMappable. May be None. self.colorbar = None self.callbacks = cbook.CallbackRegistry() callbacksSM = _api.deprecated("3.5", alternative="callbacks")( property(lambda self: self.callbacks)) def _scale_norm(self, norm, vmin, vmax): """ Helper for initial scaling. Used by public functions that create a ScalarMappable and support parameters *vmin*, *vmax* and *norm*. This makes sure that a *norm* will take precedence over *vmin*, *vmax*. Note that this method does not set the norm. """ if vmin is not None or vmax is not None: self.set_clim(vmin, vmax) if norm is not None: raise ValueError( "Passing parameters norm and vmin/vmax simultaneously is " "not supported. Please pass vmin/vmax directly to the " "norm when creating it.") # always resolve the autoscaling so we have concrete limits # rather than deferring to draw time. self.autoscale_None() def to_rgba(self, x, alpha=None, bytes=False, norm=True): """ Return a normalized rgba array corresponding to *x*. In the normal case, *x* is a 1D or 2D sequence of scalars, and the corresponding ndarray of rgba values will be returned, based on the norm and colormap set for this ScalarMappable. There is one special case, for handling images that are already rgb or rgba, such as might have been read from an image file. If *x* is an ndarray with 3 dimensions, and the last dimension is either 3 or 4, then it will be treated as an rgb or rgba array, and no mapping will be done. The array can be uint8, or it can be floating point with values in the 0-1 range; otherwise a ValueError will be raised. If it is a masked array, the mask will be ignored. If the last dimension is 3, the *alpha* kwarg (defaulting to 1) will be used to fill in the transparency. If the last dimension is 4, the *alpha* kwarg is ignored; it does not replace the pre-existing alpha. A ValueError will be raised if the third dimension is other than 3 or 4. In either case, if *bytes* is *False* (default), the rgba array will be floats in the 0-1 range; if it is *True*, the returned rgba array will be uint8 in the 0 to 255 range. If norm is False, no normalization of the input data is performed, and it is assumed to be in the range (0-1). """ # First check for special case, image input: try: if x.ndim == 3: if x.shape[2] == 3: if alpha is None: alpha = 1 if x.dtype == np.uint8: alpha = np.uint8(alpha * 255) m, n = x.shape[:2] xx = np.empty(shape=(m, n, 4), dtype=x.dtype) xx[:, :, :3] = x xx[:, :, 3] = alpha elif x.shape[2] == 4: xx = x else: raise ValueError("Third dimension must be 3 or 4") if xx.dtype.kind == 'f': if norm and (xx.max() > 1 or xx.min() < 0): raise ValueError("Floating point image RGB values " "must be in the 0..1 range.") if bytes: xx = (xx * 255).astype(np.uint8) elif xx.dtype == np.uint8: if not bytes: xx = xx.astype(np.float32) / 255 else: raise ValueError("Image RGB array must be uint8 or " "floating point; found %s" % xx.dtype) return xx except AttributeError: # e.g., x is not an ndarray; so try mapping it pass # This is the normal case, mapping a scalar array: x = ma.asarray(x) if norm: x = self.norm(x) rgba = self.cmap(x, alpha=alpha, bytes=bytes) return rgba def set_array(self, A): """ Set the value array from array-like *A*. Parameters ---------- A : array-like or None The values that are mapped to colors. The base class `.ScalarMappable` does not make any assumptions on the dimensionality and shape of the value array *A*. """ if A is None: self._A = None return A = cbook.safe_masked_invalid(A, copy=True) if not np.can_cast(A.dtype, float, "same_kind"): raise TypeError(f"Image data of dtype {A.dtype} cannot be " "converted to float") self._A = A def get_array(self): """ Return the array of values, that are mapped to colors. The base class `.ScalarMappable` does not make any assumptions on the dimensionality and shape of the array. """ return self._A def get_cmap(self): """Return the `.Colormap` instance.""" return self.cmap def get_clim(self): """ Return the values (min, max) that are mapped to the colormap limits. """ return self.norm.vmin, self.norm.vmax def set_clim(self, vmin=None, vmax=None): """ Set the norm limits for image scaling. Parameters ---------- vmin, vmax : float The limits. The limits may also be passed as a tuple (*vmin*, *vmax*) as a single positional argument. .. ACCEPTS: (vmin: float, vmax: float) """ # If the norm's limits are updated self.changed() will be called # through the callbacks attached to the norm if vmax is None: try: vmin, vmax = vmin except (TypeError, ValueError): pass if vmin is not None: self.norm.vmin = colors._sanitize_extrema(vmin) if vmax is not None: self.norm.vmax = colors._sanitize_extrema(vmax) def get_alpha(self): """ Returns ------- float Always returns 1. """ # This method is intended to be overridden by Artist sub-classes return 1. def set_cmap(self, cmap): """ Set the colormap for luminance data. Parameters ---------- cmap : `.Colormap` or str or None """ in_init = self.cmap is None cmap = get_cmap(cmap) self.cmap = cmap if not in_init: self.changed() # Things are not set up properly yet. @property def norm(self): return self._norm @norm.setter def norm(self, norm): _api.check_isinstance((colors.Normalize, None), norm=norm) if norm is None: norm = colors.Normalize() if norm is self.norm: # We aren't updating anything return in_init = self.norm is None # Remove the current callback and connect to the new one if not in_init: self.norm.callbacks.disconnect(self._id_norm) self._norm = norm self._id_norm = self.norm.callbacks.connect('changed', self.changed) if not in_init: self.changed() def set_norm(self, norm): """ Set the normalization instance. Parameters ---------- norm : `.Normalize` or None Notes ----- If there are any colorbars using the mappable for this norm, setting the norm of the mappable will reset the norm, locator, and formatters on the colorbar to default. """ self.norm = norm def autoscale(self): """ Autoscale the scalar limits on the norm instance using the current array """ if self._A is None: raise TypeError('You must first set_array for mappable') # If the norm's limits are updated self.changed() will be called # through the callbacks attached to the norm self.norm.autoscale(self._A) def autoscale_None(self): """ Autoscale the scalar limits on the norm instance using the current array, changing only limits that are None """ if self._A is None: raise TypeError('You must first set_array for mappable') # If the norm's limits are updated self.changed() will be called # through the callbacks attached to the norm self.norm.autoscale_None(self._A) def changed(self): """ Call this whenever the mappable is changed to notify all the callbackSM listeners to the 'changed' signal. """ self.callbacks.process('changed', self) self.stale = True
class LatexManager: """ The LatexManager opens an instance of the LaTeX application for determining the metrics of text elements. The LaTeX environment can be modified by setting fonts and/or a custom preamble in `.rcParams`. """ @staticmethod def _build_latex_header(): latex_preamble = get_preamble() latex_fontspec = get_fontspec() # Create LaTeX header with some content, else LaTeX will load some math # fonts later when we don't expect the additional output on stdout. # TODO: is this sufficient? latex_header = [ r"\documentclass{minimal}", # Include TeX program name as a comment for cache invalidation. # TeX does not allow this to be the first line. rf"% !TeX program = {mpl.rcParams['pgf.texsystem']}", # Test whether \includegraphics supports interpolate option. r"\usepackage{graphicx}", latex_preamble, latex_fontspec, r"\begin{document}", r"text $math \mu$", # force latex to load fonts now r"\typeout{pgf_backend_query_start}", ] return "\n".join(latex_header) @classmethod def _get_cached_or_new(cls): """ Return the previous LatexManager if the header and tex system did not change, or a new instance otherwise. """ return cls._get_cached_or_new_impl(cls._build_latex_header()) @classmethod @functools.lru_cache(1) def _get_cached_or_new_impl(cls, header): # Helper for _get_cached_or_new. return cls() def _stdin_writeln(self, s): if self.latex is None: self._setup_latex_process() self.latex.stdin.write(s) self.latex.stdin.write("\n") self.latex.stdin.flush() def _expect(self, s): s = list(s) chars = [] while True: c = self.latex.stdout.read(1) chars.append(c) if chars[-len(s):] == s: break if not c: self.latex.kill() self.latex = None raise LatexError("LaTeX process halted", "".join(chars)) return "".join(chars) def _expect_prompt(self): return self._expect("\n*") def __init__(self): # create a tmp directory for running latex, register it for deletion self._tmpdir = TemporaryDirectory() self.tmpdir = self._tmpdir.name self._finalize_tmpdir = weakref.finalize(self, self._tmpdir.cleanup) # test the LaTeX setup to ensure a clean startup of the subprocess self.texcommand = mpl.rcParams["pgf.texsystem"] self.latex_header = LatexManager._build_latex_header() latex_end = "\n\\makeatletter\n\\@@end\n" try: latex = subprocess.Popen([self.texcommand, "-halt-on-error"], stdin=subprocess.PIPE, stdout=subprocess.PIPE, encoding="utf-8", cwd=self.tmpdir) except FileNotFoundError as err: raise RuntimeError( f"{self.texcommand} not found. Install it or change " f"rcParams['pgf.texsystem'] to an available TeX " f"implementation.") from err except OSError as err: raise RuntimeError("Error starting process %r" % self.texcommand) from err test_input = self.latex_header + latex_end stdout, stderr = latex.communicate(test_input) if latex.returncode != 0: raise LatexError( "LaTeX returned an error, probably missing font " "or error in preamble.", stdout) self.latex = None # Will be set up on first use. # Per-instance cache. self._get_box_metrics = functools.lru_cache()(self._get_box_metrics) str_cache = _api.deprecated("3.5")(property(lambda self: {})) def _setup_latex_process(self): # Open LaTeX process for real work; register it for deletion. On # Windows, we must ensure that the subprocess has quit before being # able to delete the tmpdir in which it runs; in order to do so, we # must first `kill()` it, and then `communicate()` with it. self.latex = subprocess.Popen([self.texcommand, "-halt-on-error"], stdin=subprocess.PIPE, stdout=subprocess.PIPE, encoding="utf-8", cwd=self.tmpdir) def finalize_latex(latex): latex.kill() latex.communicate() self._finalize_latex = weakref.finalize(self, finalize_latex, self.latex) # write header with 'pgf_backend_query_start' token self._stdin_writeln(self._build_latex_header()) # read all lines until our 'pgf_backend_query_start' token appears self._expect("*pgf_backend_query_start") self._expect_prompt() def get_width_height_descent(self, text, prop): """ Get the width, total height, and descent (in TeX points) for a text typeset by the current LaTeX environment. """ return self._get_box_metrics(_escape_and_apply_props(text, prop)) def _get_box_metrics(self, tex): """ Get the width, total height and descent (in TeX points) for a TeX command's output in the current LaTeX environment. """ # This method gets wrapped in __init__ for per-instance caching. self._stdin_writeln( # Send textbox to TeX & request metrics typeout. r"\sbox0{%s}\typeout{\the\wd0,\the\ht0,\the\dp0}" % tex) try: answer = self._expect_prompt() except LatexError as err: raise ValueError("Error measuring {!r}\nLaTeX Output:\n{}".format( tex, err.latex_output)) from err try: # Parse metrics from the answer string. Last line is prompt, and # next-to-last-line is blank line from \typeout. width, height, offset = answer.splitlines()[-3].split(",") except Exception as err: raise ValueError("Error measuring {!r}\nLaTeX Output:\n{}".format( tex, answer)) from err w, h, o = float(width[:-2]), float(height[:-2]), float(offset[:-2]) # The height returned from LaTeX goes from base to top; # the height Matplotlib expects goes from bottom to top. return w, h + o, o
class NavigationToolbar2GTK3(NavigationToolbar2, Gtk.Toolbar): ctx = _api.deprecated("3.3")(property( lambda self: self.canvas.get_property("window").cairo_create())) def __init__(self, canvas, window): self.win = window GObject.GObject.__init__(self) self.set_style(Gtk.ToolbarStyle.ICONS) self._gtk_ids = {} for text, tooltip_text, image_file, callback in self.toolitems: if text is None: self.insert(Gtk.SeparatorToolItem(), -1) continue image = Gtk.Image.new_from_gicon( Gio.Icon.new_for_string( str( cbook._get_data_path('images', f'{image_file}-symbolic.svg'))), Gtk.IconSize.LARGE_TOOLBAR) self._gtk_ids[text] = tbutton = (Gtk.ToggleToolButton() if callback in ['zoom', 'pan'] else Gtk.ToolButton()) tbutton.set_label(text) tbutton.set_icon_widget(image) self.insert(tbutton, -1) # Save the handler id, so that we can block it as needed. tbutton._signal_handler = tbutton.connect('clicked', getattr(self, callback)) tbutton.set_tooltip_text(tooltip_text) toolitem = Gtk.SeparatorToolItem() self.insert(toolitem, -1) toolitem.set_draw(False) toolitem.set_expand(True) # This filler item ensures the toolbar is always at least two text # lines high. Otherwise the canvas gets redrawn as the mouse hovers # over images because those use two-line messages which resize the # toolbar. toolitem = Gtk.ToolItem() self.insert(toolitem, -1) label = Gtk.Label() label.set_markup( '<small>\N{NO-BREAK SPACE}\n\N{NO-BREAK SPACE}</small>') toolitem.add(label) toolitem = Gtk.ToolItem() self.insert(toolitem, -1) self.message = Gtk.Label() toolitem.add(self.message) self.show_all() NavigationToolbar2.__init__(self, canvas) def set_message(self, s): escaped = GLib.markup_escape_text(s) self.message.set_markup(f'<small>{escaped}</small>') def set_cursor(self, cursor): window = self.canvas.get_property("window") if window is not None: window.set_cursor(cursord[cursor]) Gtk.main_iteration() def draw_rubberband(self, event, x0, y0, x1, y1): height = self.canvas.figure.bbox.height y1 = height - y1 y0 = height - y0 rect = [int(val) for val in (x0, y0, x1 - x0, y1 - y0)] self.canvas._draw_rubberband(rect) def remove_rubberband(self): self.canvas._draw_rubberband(None) def _update_buttons_checked(self): for name, active in [("Pan", "PAN"), ("Zoom", "ZOOM")]: button = self._gtk_ids.get(name) if button: with button.handler_block(button._signal_handler): button.set_active(self.mode.name == active) def pan(self, *args): super().pan(*args) self._update_buttons_checked() def zoom(self, *args): super().zoom(*args) self._update_buttons_checked() def save_figure(self, *args): dialog = Gtk.FileChooserDialog( title="Save the figure", parent=self.canvas.get_toplevel(), action=Gtk.FileChooserAction.SAVE, buttons=(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, Gtk.STOCK_SAVE, Gtk.ResponseType.OK), ) for name, fmts \ in self.canvas.get_supported_filetypes_grouped().items(): ff = Gtk.FileFilter() ff.set_name(name) for fmt in fmts: ff.add_pattern("*." + fmt) dialog.add_filter(ff) if self.canvas.get_default_filetype() in fmts: dialog.set_filter(ff) @functools.partial(dialog.connect, "notify::filter") def on_notify_filter(*args): name = dialog.get_filter().get_name() fmt = self.canvas.get_supported_filetypes_grouped()[name][0] dialog.set_current_name( str(Path(dialog.get_current_name()).with_suffix("." + fmt))) dialog.set_current_folder(mpl.rcParams["savefig.directory"]) dialog.set_current_name(self.canvas.get_default_filename()) dialog.set_do_overwrite_confirmation(True) response = dialog.run() fname = dialog.get_filename() ff = dialog.get_filter() # Doesn't autoadjust to filename :/ fmt = self.canvas.get_supported_filetypes_grouped()[ff.get_name()][0] dialog.destroy() if response != Gtk.ResponseType.OK: return # Save dir for next time, unless empty str (which means use cwd). if mpl.rcParams['savefig.directory']: mpl.rcParams['savefig.directory'] = os.path.dirname(fname) try: self.canvas.figure.savefig(fname, format=fmt) except Exception as e: error_msg_gtk(str(e), parent=self) def set_history_buttons(self): can_backward = self._nav_stack._pos > 0 can_forward = self._nav_stack._pos < len(self._nav_stack._elements) - 1 if 'Back' in self._gtk_ids: self._gtk_ids['Back'].set_sensitive(can_backward) if 'Forward' in self._gtk_ids: self._gtk_ids['Forward'].set_sensitive(can_forward)
class __getattr__: ETS = _api.deprecated("3.5")(property(lambda self: dict( pyqt5=(QT_API_PYQT5, 5), pyside2=(QT_API_PYSIDE2, 5)))) QT_RC_MAJOR_VERSION = _api.deprecated("3.5")( property(lambda self: int(QtCore.qVersion().split(".")[0])))
class Dvi: """ A reader for a dvi ("device-independent") file, as produced by TeX. The current implementation can only iterate through pages in order, and does not even attempt to verify the postamble. This class can be used as a context manager to close the underlying file upon exit. Pages can be read via iteration. Here is an overly simple way to extract text without trying to detect whitespace:: >>> with matplotlib.dviread.Dvi('input.dvi', 72) as dvi: ... for page in dvi: ... print(''.join(chr(t.glyph) for t in page.text)) """ # dispatch table _dtable = [None] * 256 _dispatch = partial(_dispatch, _dtable) def __init__(self, filename, dpi): """ Read the data from the file named *filename* and convert TeX's internal units to units of *dpi* per inch. *dpi* only sets the units and does not limit the resolution. Use None to return TeX's internal units. """ _log.debug('Dvi: %s', filename) self.file = open(filename, 'rb') self.dpi = dpi self.fonts = {} self.state = _dvistate.pre baseline = _api.deprecated("3.5")(property(lambda self: None)) def __enter__(self): """Context manager enter method, does nothing.""" return self def __exit__(self, etype, evalue, etrace): """ Context manager exit method, closes the underlying file if it is open. """ self.close() def __iter__(self): """ Iterate through the pages of the file. Yields ------ Page Details of all the text and box objects on the page. The Page tuple contains lists of Text and Box tuples and the page dimensions, and the Text and Box tuples contain coordinates transformed into a standard Cartesian coordinate system at the dpi value given when initializing. The coordinates are floating point numbers, but otherwise precision is not lost and coordinate values are not clipped to integers. """ while self._read(): yield self._output() def close(self): """Close the underlying file if it is open.""" if not self.file.closed: self.file.close() def _output(self): """ Output the text and boxes belonging to the most recent page. page = dvi._output() """ minx, miny, maxx, maxy = np.inf, np.inf, -np.inf, -np.inf maxy_pure = -np.inf for elt in self.text + self.boxes: if isinstance(elt, Box): x, y, h, w = elt e = 0 # zero depth else: # glyph x, y, font, g, w = elt h, e = font._height_depth_of(g) minx = min(minx, x) miny = min(miny, y - h) maxx = max(maxx, x + w) maxy = max(maxy, y + e) maxy_pure = max(maxy_pure, y) if self._baseline_v is not None: maxy_pure = self._baseline_v # This should normally be the case. self._baseline_v = None if not self.text and not self.boxes: # Avoid infs/nans from inf+/-inf. return Page(text=[], boxes=[], width=0, height=0, descent=0) if self.dpi is None: # special case for ease of debugging: output raw dvi coordinates return Page(text=self.text, boxes=self.boxes, width=maxx - minx, height=maxy_pure - miny, descent=maxy - maxy_pure) # convert from TeX's "scaled points" to dpi units d = self.dpi / (72.27 * 2**16) descent = (maxy - maxy_pure) * d text = [ Text((x - minx) * d, (maxy - y) * d - descent, f, g, w * d) for (x, y, f, g, w) in self.text ] boxes = [ Box((x - minx) * d, (maxy - y) * d - descent, h * d, w * d) for (x, y, h, w) in self.boxes ] return Page(text=text, boxes=boxes, width=(maxx - minx) * d, height=(maxy_pure - miny) * d, descent=descent) def _read(self): """ Read one page from the file. Return True if successful, False if there were no more pages. """ # Pages appear to start with the sequence # bop (begin of page) # xxx comment # <push, ..., pop> # if using chemformula # down # push # down # <push, push, xxx, right, xxx, pop, pop> # if using xcolor # down # push # down (possibly multiple) # push <= here, v is the baseline position. # etc. # (dviasm is useful to explore this structure.) # Thus, we use the vertical position at the first time the stack depth # reaches 3, while at least three "downs" have been executed (excluding # those popped out (corresponding to the chemformula preamble)), as the # baseline (the "down" count is necessary to handle xcolor). down_stack = [0] self._baseline_v = None while True: byte = self.file.read(1)[0] self._dtable[byte](self, byte) name = self._dtable[byte].__name__ if name == "_push": down_stack.append(down_stack[-1]) elif name == "_pop": down_stack.pop() elif name == "_down": down_stack[-1] += 1 if (self._baseline_v is None and len(getattr(self, "stack", [])) == 3 and down_stack[-1] >= 4): self._baseline_v = self.v if byte == 140: # end of page return True if self.state is _dvistate.post_post: # end of file self.close() return False def _arg(self, nbytes, signed=False): """ Read and return an integer argument *nbytes* long. Signedness is determined by the *signed* keyword. """ buf = self.file.read(nbytes) value = buf[0] if signed and value >= 0x80: value = value - 0x100 for b in buf[1:]: value = 0x100 * value + b return value @_dispatch(min=0, max=127, state=_dvistate.inpage) def _set_char_immediate(self, char): self._put_char_real(char) self.h += self.fonts[self.f]._width_of(char) @_dispatch(min=128, max=131, state=_dvistate.inpage, args=('olen1', )) def _set_char(self, char): self._put_char_real(char) self.h += self.fonts[self.f]._width_of(char) @_dispatch(132, state=_dvistate.inpage, args=('s4', 's4')) def _set_rule(self, a, b): self._put_rule_real(a, b) self.h += b @_dispatch(min=133, max=136, state=_dvistate.inpage, args=('olen1', )) def _put_char(self, char): self._put_char_real(char) def _put_char_real(self, char): font = self.fonts[self.f] if font._vf is None: self.text.append( Text(self.h, self.v, font, char, font._width_of(char))) else: scale = font._scale for x, y, f, g, w in font._vf[char].text: newf = DviFont(scale=_mul2012(scale, f._scale), tfm=f._tfm, texname=f.texname, vf=f._vf) self.text.append( Text(self.h + _mul2012(x, scale), self.v + _mul2012(y, scale), newf, g, newf._width_of(g))) self.boxes.extend([ Box(self.h + _mul2012(x, scale), self.v + _mul2012(y, scale), _mul2012(a, scale), _mul2012(b, scale)) for x, y, a, b in font._vf[char].boxes ]) @_dispatch(137, state=_dvistate.inpage, args=('s4', 's4')) def _put_rule(self, a, b): self._put_rule_real(a, b) def _put_rule_real(self, a, b): if a > 0 and b > 0: self.boxes.append(Box(self.h, self.v, a, b)) @_dispatch(138) def _nop(self, _): pass @_dispatch(139, state=_dvistate.outer, args=('s4', ) * 11) def _bop(self, c0, c1, c2, c3, c4, c5, c6, c7, c8, c9, p): self.state = _dvistate.inpage self.h, self.v, self.w, self.x, self.y, self.z = 0, 0, 0, 0, 0, 0 self.stack = [] self.text = [] # list of Text objects self.boxes = [] # list of Box objects @_dispatch(140, state=_dvistate.inpage) def _eop(self, _): self.state = _dvistate.outer del self.h, self.v, self.w, self.x, self.y, self.z, self.stack @_dispatch(141, state=_dvistate.inpage) def _push(self, _): self.stack.append((self.h, self.v, self.w, self.x, self.y, self.z)) @_dispatch(142, state=_dvistate.inpage) def _pop(self, _): self.h, self.v, self.w, self.x, self.y, self.z = self.stack.pop() @_dispatch(min=143, max=146, state=_dvistate.inpage, args=('slen1', )) def _right(self, b): self.h += b @_dispatch(min=147, max=151, state=_dvistate.inpage, args=('slen', )) def _right_w(self, new_w): if new_w is not None: self.w = new_w self.h += self.w @_dispatch(min=152, max=156, state=_dvistate.inpage, args=('slen', )) def _right_x(self, new_x): if new_x is not None: self.x = new_x self.h += self.x @_dispatch(min=157, max=160, state=_dvistate.inpage, args=('slen1', )) def _down(self, a): self.v += a @_dispatch(min=161, max=165, state=_dvistate.inpage, args=('slen', )) def _down_y(self, new_y): if new_y is not None: self.y = new_y self.v += self.y @_dispatch(min=166, max=170, state=_dvistate.inpage, args=('slen', )) def _down_z(self, new_z): if new_z is not None: self.z = new_z self.v += self.z @_dispatch(min=171, max=234, state=_dvistate.inpage) def _fnt_num_immediate(self, k): self.f = k @_dispatch(min=235, max=238, state=_dvistate.inpage, args=('olen1', )) def _fnt_num(self, new_f): self.f = new_f @_dispatch(min=239, max=242, args=('ulen1', )) def _xxx(self, datalen): special = self.file.read(datalen) _log.debug( 'Dvi._xxx: encountered special: %s', ''.join([ chr(ch) if 32 <= ch < 127 else '<%02x>' % ch for ch in special ])) @_dispatch(min=243, max=246, args=('olen1', 'u4', 'u4', 'u4', 'u1', 'u1')) def _fnt_def(self, k, c, s, d, a, l): self._fnt_def_real(k, c, s, d, a, l) def _fnt_def_real(self, k, c, s, d, a, l): n = self.file.read(a + l) fontname = n[-l:].decode('ascii') tfm = _tfmfile(fontname) if c != 0 and tfm.checksum != 0 and c != tfm.checksum: raise ValueError('tfm checksum mismatch: %s' % n) try: vf = _vffile(fontname) except FileNotFoundError: vf = None self.fonts[k] = DviFont(scale=s, tfm=tfm, texname=n, vf=vf) @_dispatch(247, state=_dvistate.pre, args=('u1', 'u4', 'u4', 'u4', 'u1')) def _pre(self, i, num, den, mag, k): self.file.read(k) # comment in the dvi file if i != 2: raise ValueError("Unknown dvi format %d" % i) if num != 25400000 or den != 7227 * 2**16: raise ValueError("Nonstandard units in dvi file") # meaning: TeX always uses those exact values, so it # should be enough for us to support those # (There are 72.27 pt to an inch so 7227 pt = # 7227 * 2**16 sp to 100 in. The numerator is multiplied # by 10^5 to get units of 10**-7 meters.) if mag != 1000: raise ValueError("Nonstandard magnification in dvi file") # meaning: LaTeX seems to frown on setting \mag, so # I think we can assume this is constant self.state = _dvistate.outer @_dispatch(248, state=_dvistate.outer) def _post(self, _): self.state = _dvistate.post_post # TODO: actually read the postamble and finale? # currently post_post just triggers closing the file @_dispatch(249) def _post_post(self, _): raise NotImplementedError @_dispatch(min=250, max=255) def _malformed(self, offset): raise ValueError(f"unknown command: byte {250 + offset}")
class __getattr__: qApp = _api.deprecated( "3.6", alternative="QtWidgets.QApplication.instance()")( property(lambda self: QtWidgets.QApplication.instance()))
class __getattr__: STYLE_FILE_PATTERN = _api.deprecated("3.5", obj_type="")( property(lambda self: re.compile(r'([\S]+).%s$' % STYLE_EXTENSION)))
class TexManager: """ Convert strings to dvi files using TeX, caching the results to a directory. Repeated calls to this constructor always return the same instance. """ # Caches. texcache = os.path.join(mpl.get_cachedir(), 'tex.cache') grey_arrayd = {} font_family = 'serif' font_families = ('serif', 'sans-serif', 'cursive', 'monospace') font_info = { 'new century schoolbook': ('pnc', r'\renewcommand{\rmdefault}{pnc}'), 'bookman': ('pbk', r'\renewcommand{\rmdefault}{pbk}'), 'times': ('ptm', r'\usepackage{mathptmx}'), 'palatino': ('ppl', r'\usepackage{mathpazo}'), 'zapf chancery': ('pzc', r'\usepackage{chancery}'), 'cursive': ('pzc', r'\usepackage{chancery}'), 'charter': ('pch', r'\usepackage{charter}'), 'serif': ('cmr', ''), 'sans-serif': ('cmss', ''), 'helvetica': ('phv', r'\usepackage{helvet}'), 'avant garde': ('pag', r'\usepackage{avant}'), 'courier': ('pcr', r'\usepackage{courier}'), # Loading the type1ec package ensures that cm-super is installed, which # is necessary for unicode computer modern. (It also allows the use of # computer modern at arbitrary sizes, but that's just a side effect.) 'monospace': ('cmtt', r'\usepackage{type1ec}'), 'computer modern roman': ('cmr', r'\usepackage{type1ec}'), 'computer modern sans serif': ('cmss', r'\usepackage{type1ec}'), 'computer modern typewriter': ('cmtt', r'\usepackage{type1ec}')} cachedir = _api.deprecated( "3.3", alternative="matplotlib.get_cachedir()")( property(lambda self: mpl.get_cachedir())) rgba_arrayd = _api.deprecated("3.3")(property(lambda self: {})) _fonts = {} # Only for deprecation period. serif = _api.deprecated("3.3")(property( lambda self: self._fonts.get("serif", ('cmr', '')))) sans_serif = _api.deprecated("3.3")(property( lambda self: self._fonts.get("sans-serif", ('cmss', '')))) cursive = _api.deprecated("3.3")(property( lambda self: self._fonts.get("cursive", ('pzc', r'\usepackage{chancery}')))) monospace = _api.deprecated("3.3")(property( lambda self: self._fonts.get("monospace", ('cmtt', '')))) @functools.lru_cache() # Always return the same instance. def __new__(cls): Path(cls.texcache).mkdir(parents=True, exist_ok=True) return object.__new__(cls) def get_font_config(self): ff = rcParams['font.family'] if len(ff) == 1 and ff[0].lower() in self.font_families: self.font_family = ff[0].lower() else: _log.info('font.family must be one of (%s) when text.usetex is ' 'True. serif will be used by default.', ', '.join(self.font_families)) self.font_family = 'serif' fontconfig = [self.font_family] for font_family in self.font_families: for font in rcParams['font.' + font_family]: if font.lower() in self.font_info: self._fonts[font_family] = self.font_info[font.lower()] _log.debug('family: %s, font: %s, info: %s', font_family, font, self.font_info[font.lower()]) break else: _log.debug('%s font is not compatible with usetex.', font) else: _log.info('No LaTeX-compatible font found for the %s font ' 'family in rcParams. Using default.', font_family) self._fonts[font_family] = self.font_info[font_family] fontconfig.append(self._fonts[font_family][0]) # Add a hash of the latex preamble to fontconfig so that the # correct png is selected for strings rendered with same font and dpi # even if the latex preamble changes within the session preamble_bytes = self.get_custom_preamble().encode('utf-8') fontconfig.append(hashlib.md5(preamble_bytes).hexdigest()) # The following packages and commands need to be included in the latex # file's preamble: cmd = [self._fonts['serif'][1], self._fonts['sans-serif'][1], self._fonts['monospace'][1]] if self.font_family == 'cursive': cmd.append(self._fonts['cursive'][1]) self._font_preamble = '\n'.join([r'\usepackage{type1cm}', *cmd]) return ''.join(fontconfig) def get_basefile(self, tex, fontsize, dpi=None): """ Return a filename based on a hash of the string, fontsize, and dpi. """ s = ''.join([tex, self.get_font_config(), '%f' % fontsize, self.get_custom_preamble(), str(dpi or '')]) return os.path.join( self.texcache, hashlib.md5(s.encode('utf-8')).hexdigest()) def get_font_preamble(self): """ Return a string containing font configuration for the tex preamble. """ return self._font_preamble def get_custom_preamble(self): """Return a string containing user additions to the tex preamble.""" return rcParams['text.latex.preamble'] def _get_preamble(self): return "\n".join([ r"\documentclass{article}", # Pass-through \mathdefault, which is used in non-usetex mode to # use the default text font but was historically suppressed in # usetex mode. r"\newcommand{\mathdefault}[1]{#1}", self._font_preamble, r"\usepackage[utf8]{inputenc}", r"\DeclareUnicodeCharacter{2212}{\ensuremath{-}}", # geometry is loaded before the custom preamble as convert_psfrags # relies on a custom preamble to change the geometry. r"\usepackage[papersize=72in, margin=1in]{geometry}", self.get_custom_preamble(), # textcomp is loaded last (if not already loaded by the custom # preamble) in order not to clash with custom packages (e.g. # newtxtext) which load it with different options. r"\makeatletter" r"\@ifpackageloaded{textcomp}{}{\usepackage{textcomp}}" r"\makeatother", ]) def make_tex(self, tex, fontsize): """ Generate a tex file to render the tex string at a specific font size. Return the file name. """ basefile = self.get_basefile(tex, fontsize) texfile = '%s.tex' % basefile fontcmd = {'sans-serif': r'{\sffamily %s}', 'monospace': r'{\ttfamily %s}'}.get(self.font_family, r'{\rmfamily %s}') Path(texfile).write_text( r""" %s \pagestyle{empty} \begin{document} %% The empty hbox ensures that a page is printed even for empty inputs, except %% when using psfrag which gets confused by it. \fontsize{%f}{%f}%% \ifdefined\psfrag\else\hbox{}\fi%% %s \end{document} """ % (self._get_preamble(), fontsize, fontsize * 1.25, fontcmd % tex), encoding='utf-8') return texfile _re_vbox = re.compile( r"MatplotlibBox:\(([\d.]+)pt\+([\d.]+)pt\)x([\d.]+)pt") @_api.deprecated("3.3") def make_tex_preview(self, tex, fontsize): """ Generate a tex file to render the tex string at a specific font size. It uses the preview.sty to determine the dimension (width, height, descent) of the output. Return the file name. """ basefile = self.get_basefile(tex, fontsize) texfile = '%s.tex' % basefile fontcmd = {'sans-serif': r'{\sffamily %s}', 'monospace': r'{\ttfamily %s}'}.get(self.font_family, r'{\rmfamily %s}') # newbox, setbox, immediate, etc. are used to find the box # extent of the rendered text. Path(texfile).write_text( r""" %s \usepackage[active,showbox,tightpage]{preview} %% we override the default showbox as it is treated as an error and makes %% the exit status not zero \def\showbox#1%% {\immediate\write16{MatplotlibBox:(\the\ht#1+\the\dp#1)x\the\wd#1}} \begin{document} \begin{preview} {\fontsize{%f}{%f}%s} \end{preview} \end{document} """ % (self._get_preamble(), fontsize, fontsize * 1.25, fontcmd % tex), encoding='utf-8') return texfile def _run_checked_subprocess(self, command, tex, *, cwd=None): _log.debug(cbook._pformat_subprocess(command)) try: report = subprocess.check_output( command, cwd=cwd if cwd is not None else self.texcache, stderr=subprocess.STDOUT) except FileNotFoundError as exc: raise RuntimeError( 'Failed to process string with tex because {} could not be ' 'found'.format(command[0])) from exc except subprocess.CalledProcessError as exc: raise RuntimeError( '{prog} was not able to process the following string:\n' '{tex!r}\n\n' 'Here is the full report generated by {prog}:\n' '{exc}\n\n'.format( prog=command[0], tex=tex.encode('unicode_escape'), exc=exc.output.decode('utf-8'))) from exc _log.debug(report) return report def make_dvi(self, tex, fontsize): """ Generate a dvi file containing latex's layout of tex string. Return the file name. """ if dict.__getitem__(rcParams, 'text.latex.preview'): return self.make_dvi_preview(tex, fontsize) basefile = self.get_basefile(tex, fontsize) dvifile = '%s.dvi' % basefile if not os.path.exists(dvifile): texfile = self.make_tex(tex, fontsize) # Generate the dvi in a temporary directory to avoid race # conditions e.g. if multiple processes try to process the same tex # string at the same time. Having tmpdir be a subdirectory of the # final output dir ensures that they are on the same filesystem, # and thus replace() works atomically. with TemporaryDirectory(dir=Path(dvifile).parent) as tmpdir: self._run_checked_subprocess( ["latex", "-interaction=nonstopmode", "--halt-on-error", texfile], tex, cwd=tmpdir) (Path(tmpdir) / Path(dvifile).name).replace(dvifile) return dvifile @_api.deprecated("3.3") def make_dvi_preview(self, tex, fontsize): """ Generate a dvi file containing latex's layout of tex string. It calls make_tex_preview() method and store the size information (width, height, descent) in a separate file. Return the file name. """ basefile = self.get_basefile(tex, fontsize) dvifile = '%s.dvi' % basefile baselinefile = '%s.baseline' % basefile if not os.path.exists(dvifile) or not os.path.exists(baselinefile): texfile = self.make_tex_preview(tex, fontsize) report = self._run_checked_subprocess( ["latex", "-interaction=nonstopmode", "--halt-on-error", texfile], tex) # find the box extent information in the latex output # file and store them in ".baseline" file m = TexManager._re_vbox.search(report.decode("utf-8")) with open(basefile + '.baseline', "w") as fh: fh.write(" ".join(m.groups())) for fname in glob.glob(basefile + '*'): if not fname.endswith(('dvi', 'tex', 'baseline')): try: os.remove(fname) except OSError: pass return dvifile def make_png(self, tex, fontsize, dpi): """ Generate a png file containing latex's rendering of tex string. Return the file name. """ basefile = self.get_basefile(tex, fontsize, dpi) pngfile = '%s.png' % basefile # see get_rgba for a discussion of the background if not os.path.exists(pngfile): dvifile = self.make_dvi(tex, fontsize) cmd = ["dvipng", "-bg", "Transparent", "-D", str(dpi), "-T", "tight", "-o", pngfile, dvifile] # When testing, disable FreeType rendering for reproducibility; but # dvipng 1.16 has a bug (fixed in f3ff241) that breaks --freetype0 # mode, so for it we keep FreeType enabled; the image will be # slightly off. if (getattr(mpl, "_called_from_pytest", False) and mpl._get_executable_info("dvipng").version != "1.16"): cmd.insert(1, "--freetype0") self._run_checked_subprocess(cmd, tex) return pngfile def get_grey(self, tex, fontsize=None, dpi=None): """Return the alpha channel.""" if not fontsize: fontsize = rcParams['font.size'] if not dpi: dpi = rcParams['savefig.dpi'] key = tex, self.get_font_config(), fontsize, dpi alpha = self.grey_arrayd.get(key) if alpha is None: pngfile = self.make_png(tex, fontsize, dpi) rgba = mpl.image.imread(os.path.join(self.texcache, pngfile)) self.grey_arrayd[key] = alpha = rgba[:, :, -1] return alpha def get_rgba(self, tex, fontsize=None, dpi=None, rgb=(0, 0, 0)): """Return latex's rendering of the tex string as an rgba array.""" alpha = self.get_grey(tex, fontsize, dpi) rgba = np.empty((*alpha.shape, 4)) rgba[..., :3] = mpl.colors.to_rgb(rgb) rgba[..., -1] = alpha return rgba def get_text_width_height_descent(self, tex, fontsize, renderer=None): """Return width, height and descent of the text.""" if tex.strip() == '': return 0, 0, 0 dpi_fraction = renderer.points_to_pixels(1.) if renderer else 1 if dict.__getitem__(rcParams, 'text.latex.preview'): # use preview.sty basefile = self.get_basefile(tex, fontsize) baselinefile = '%s.baseline' % basefile if not os.path.exists(baselinefile): dvifile = self.make_dvi_preview(tex, fontsize) with open(baselinefile) as fh: l = fh.read().split() height, depth, width = [float(l1) * dpi_fraction for l1 in l] return width, height + depth, depth else: # use dviread. dvifile = self.make_dvi(tex, fontsize) with dviread.Dvi(dvifile, 72 * dpi_fraction) as dvi: page, = dvi # A total height (including the descent) needs to be returned. return page.width, page.height + page.descent, page.descent