diff --git a/lib/matplotlib/backends/backend_pdf.py b/lib/matplotlib/backends/backend_pdf.py index 1e4d312dcaf1..a8aabe910167 100644 --- a/lib/matplotlib/backends/backend_pdf.py +++ b/lib/matplotlib/backends/backend_pdf.py @@ -1187,10 +1187,10 @@ def writeImages(self): img.flipud_out() - def markerObject(self, path, trans, fillp, lw): + def markerObject(self, path, trans, fillp, lw, joinstyle, capstyle): """Return name of a marker XObject representing the given path.""" pathops = self.pathOperations(path, trans, simplify=False) - key = (tuple(pathops), bool(fillp)) + key = (tuple(pathops), bool(fillp), joinstyle, capstyle) result = self.markers.get(key) if result is None: name = Name('M%d' % len(self.markers)) @@ -1204,12 +1204,14 @@ def markerObject(self, path, trans, fillp, lw): return name def writeMarkers(self): - for (pathops, fillp),(name, ob, bbox, lw) in self.markers.iteritems(): + for (pathops, fillp, joinstyle, capstyle),(name, ob, bbox, lw) in self.markers.iteritems(): bbox = bbox.padded(lw * 0.5) self.beginStream( ob.id, None, {'Type': Name('XObject'), 'Subtype': Name('Form'), 'BBox': list(bbox.extents) }) + self.output(GraphicsContextPdf.joinstyles[joinstyle], Op.setlinejoin) + self.output(GraphicsContextPdf.capstyles[capstyle], Op.setlinecap) self.output(*pathops) if fillp: self.output(Op.fill_stroke) @@ -1402,7 +1404,7 @@ def draw_image(self, gc, x, y, im, dx=None, dy=None, transform=None): h = 72.0*h/self.image_dpi else: h = dy - + imob = self.file.imageObject(im) if transform is None: @@ -1416,7 +1418,7 @@ def draw_image(self, gc, x, y, im, dx=None, dy=None, transform=None): tr1, tr2, tr3, tr4, tr5, tr6, Op.concat_matrix, w, 0, 0, h, x, y, Op.concat_matrix, imob, Op.use_xobject, Op.grestore) - + def draw_path(self, gc, path, transform, rgbFace=None): self.check_gc(gc, rgbFace) @@ -1438,7 +1440,8 @@ def draw_markers(self, gc, marker_path, marker_trans, path, trans, rgbFace=None) output = self.file.output marker = self.file.markerObject( - marker_path, marker_trans, fillp, self.gc._linewidth) + marker_path, marker_trans, fillp, self.gc._linewidth, + gc.get_joinstyle(), gc.get_capstyle()) output(Op.gsave) lastx, lasty = 0, 0 diff --git a/lib/matplotlib/backends/backend_ps.py b/lib/matplotlib/backends/backend_ps.py index 58ee164e59bf..dfadb52cfd03 100644 --- a/lib/matplotlib/backends/backend_ps.py +++ b/lib/matplotlib/backends/backend_ps.py @@ -72,7 +72,7 @@ def gs_exe(self): self._cached["gs_exe"] = gs_exe return gs_exe - + @property def gs_version(self): """ @@ -97,7 +97,7 @@ def supports_ps2write(self): True if the installed ghostscript supports ps2write device. """ return self.gs_version[0] >= 9 - + ps_backend_helper = PsBackendHelper() papersize = {'letter': (8.5,11), @@ -582,13 +582,29 @@ def draw_markers(self, gc, marker_path, marker_trans, path, trans, rgbFace=None) # construct the generic marker command: ps_cmd = ['/o {', 'gsave', 'newpath', 'translate'] # dont want the translate to be global + + lw = gc.get_linewidth() + stroke = lw != 0.0 + if stroke: + ps_cmd.append('%.1f setlinewidth' % lw) + jint = gc.get_joinstyle() + ps_cmd.append('%d setlinejoin' % jint) + cint = gc.get_capstyle() + ps_cmd.append('%d setlinecap' % cint) + ps_cmd.append(self._convert_path(marker_path, marker_trans, simplify=False)) if rgbFace: - ps_cmd.extend(['gsave', ps_color, 'fill', 'grestore']) + if stroke: + ps_cmd.append('gsave') + ps_cmd.extend([ps_color, 'fill']) + if stroke: + ps_cmd.append('grestore') - ps_cmd.extend(['stroke', 'grestore', '} bind def']) + if stroke: + ps_cmd.append('stroke') + ps_cmd.extend(['grestore', '} bind def']) for vertices, code in path.iter_segments(trans, simplify=False): if len(vertices): diff --git a/lib/matplotlib/backends/backend_svg.py b/lib/matplotlib/backends/backend_svg.py index 04ea015fc95a..6ca4437fc180 100644 --- a/lib/matplotlib/backends/backend_svg.py +++ b/lib/matplotlib/backends/backend_svg.py @@ -370,7 +370,7 @@ def _write_hatches(self): writer.end('pattern') writer.end('defs') - def _get_style(self, gc, rgbFace): + def _get_style_dict(self, gc, rgbFace): """ return the style string. style is generated from the GraphicsContext and rgbFace @@ -403,7 +403,10 @@ def _get_style(self, gc, rgbFace): if gc.get_capstyle() != 'projecting': attrib['stroke-linecap'] = _capstyle_d[gc.get_capstyle()] - return generate_css(attrib) + return attrib + + def _get_style(self, gc, rgbFace): + return generate_css(self._get_style_dict(gc, rgbFace)) def _get_clip(self, gc): cliprect = gc.get_clip_rectangle() @@ -536,12 +539,18 @@ def draw_markers(self, gc, marker_path, marker_trans, path, trans, rgbFace=None) marker_path, marker_trans + Affine2D().scale(1.0, -1.0), simplify=False) - dictkey = (path_data) + style = self._get_style_dict(gc, rgbFace) + dictkey = (path_data, generate_css(style)) oid = self._markers.get(dictkey) + for key in style.keys(): + if not key.startswith('stroke'): + del style[key] + style = generate_css(style) + if oid is None: oid = self._make_id('m', dictkey) writer.start('defs') - writer.element('path', id=oid, d=path_data) + writer.element('path', id=oid, d=path_data, style=style) writer.end('defs') self._markers[dictkey] = oid diff --git a/lib/matplotlib/lines.py b/lib/matplotlib/lines.py index 825ea85d924a..19bb108b2d45 100644 --- a/lib/matplotlib/lines.py +++ b/lib/matplotlib/lines.py @@ -541,11 +541,16 @@ def draw(self, renderer): if type(snap) == float: snap = renderer.points_to_pixels(self._markersize) >= snap gc.set_snap(snap) + gc.set_joinstyle(marker.get_joinstyle()) + gc.set_capstyle(marker.get_capstyle()) marker_path = marker.get_path() marker_trans = marker.get_transform() w = renderer.points_to_pixels(self._markersize) - if marker.get_marker() != ',': # Don't scale for pixels + if marker.get_marker() != ',': + # Don't scale for pixels, and don't stroke them marker_trans = marker_trans.scale(w) + else: + gc.set_linewidth(0) renderer.draw_markers( gc, marker_path, marker_trans, subsampled, affine.frozen(), rgbFace) diff --git a/lib/matplotlib/markers.py b/lib/matplotlib/markers.py index cd0116031aa7..fea9150d1a71 100644 --- a/lib/matplotlib/markers.py +++ b/lib/matplotlib/markers.py @@ -53,11 +53,11 @@ class MarkerStyle: For backward compatibility, the form (*verts*, 0) is also accepted, but it is equivalent to just *verts* for giving a raw set of vertices that define the shape. -""" - +""" + # TODO: Automatically generate this accepts = """ACCEPTS: [ %s | ``'$...$'`` | *tuple* | *Nx2 array* ]""" - + markers = { '.' : 'point', ',' : 'pixel', @@ -100,12 +100,12 @@ class MarkerStyle: # is calculated in the _set_* functions. filled_markers = ( 'o', 'v', '^', '<', '>', '8', 's', 'p', '*', 'h', 'H', 'D', 'd') - + fillstyles = ('full', 'left' , 'right' , 'bottom' , 'top') # TODO: Is this ever used as a non-constant? _point_size_reduction = 0.5 - + def __init__(self, marker=None, fillstyle='full'): self._fillstyle = fillstyle self.set_marker(marker) @@ -117,15 +117,17 @@ def _recache(self): self._alt_path = None self._alt_transform = None self._snap_threshold = None + self._joinstyle = 'round' + self._capstyle = 'butt' self._filled = True self._marker_function() - + def __nonzero__(self): return len(self._path.vertices) - + def is_filled(self): return self._filled - + def get_fillstyle(self): return self._fillstyle @@ -134,7 +136,13 @@ def set_fillstyle(self, fillstyle): assert fillstyle in self.fillstyles self._fillstyle = fillstyle self._recache() - + + def get_joinstyle(self): + return self._joinstyle + + def get_capstyle(self): + return self._capstyle + def get_marker(self): return self._marker @@ -173,7 +181,7 @@ def get_alt_transform(self): def get_snap_threshold(self): return self._snap_threshold - + def _set_nothing(self): self._filled = False @@ -182,14 +190,14 @@ def _set_custom_marker(self, path): rescale = max(np.max(np.abs(verts[:,0])), np.max(np.abs(verts[:,1]))) self._transform = Affine2D().scale(1.0 / rescale) self._path = path - + def _set_path_marker(self): self._set_custom_marker(self._marker) - + def _set_vertices(self): path = Path(verts) self._set_custom_marker(path) - + def _set_tuple_marker(self): marker = self._marker if is_numlike(marker[0]): @@ -200,11 +208,14 @@ def _set_tuple_marker(self): symstyle = marker[1] if symstyle == 0: self._path = Path.unit_regular_polygon(numsides) + self._joinstyle = 'miter' elif symstyle == 1: self._path = Path.unit_regular_star(numsides) + self._joinstyle = 'bevel' elif symstyle == 2: self._path = Path.unit_regular_asterisk(numsides) self._filled = False + self._joinstyle = 'bevel' elif symstyle == 3: self._path = Path.unit_circle() self._transform = Affine2D().scale(0.5).rotate_deg(rotation) @@ -212,7 +223,7 @@ def _set_tuple_marker(self): verts = np.asarray(marker[0]) path = Path(verts) self._set_custom_marker(path) - + def _set_mathtext_path(self): """ Draws mathtext markers '$...$' using TextPath object. @@ -222,7 +233,7 @@ def _set_mathtext_path(self): from matplotlib.patches import PathPatch from matplotlib.text import TextPath from matplotlib.font_manager import FontProperties - + # again, the properties could be initialised just once outside # this function # Font size is irrelevant here, it will be rescaled based on @@ -232,7 +243,7 @@ def _set_mathtext_path(self): usetex=rcParams['text.usetex']) if len(text.vertices) == 0: return - + xmin, ymin = text.vertices.min(axis=0) xmax, ymax = text.vertices.max(axis=0) width = xmax - xmin @@ -263,9 +274,18 @@ def _set_circle(self, reduction = 1.0): def _set_pixel(self): self._path = Path.unit_rectangle() - self._transform = Affine2D().translate(-0.5, 0.5) - self._snap_threshold = False - + # Ideally, you'd want -0.5, -0.5 here, but then the snapping + # algorithm in the Agg backend will round this to a 2x2 + # rectangle from (-1, -1) to (1, 1). By offsetting it + # slightly, we can force it to be (0, -1) to (1, 0), which + # both makes it only be a single pixel and places it correctly + # with 1-width stroking (i.e. the ticks). This hack is the + # best of a number of bad alternatives, mainly because the + # backends are not aware of what marker is actually being used + # beyond just its path data. + self._transform = Affine2D().translate(-0.49999, -0.50001) + self._snap_threshold = None + def _set_point(self): self._set_circle(reduction = self._point_size_reduction) @@ -297,7 +317,7 @@ def _set_triangle(self, rot, skip): self._triangle_path_l, self._triangle_path_d, self._triangle_path_r] - + if fs=='top': self._path = mpaths[(0+skip) % 4] self._alt_path = mpaths[(2+skip) % 4] @@ -313,6 +333,8 @@ def _set_triangle(self, rot, skip): self._alt_transform = self._transform + self._joinstyle = 'miter' + def _set_triangle_up(self): return self._set_triangle(0.0, 0) @@ -345,6 +367,8 @@ def _set_square(self): self._transform.rotate_deg(rotate) self._alt_transform = self._transform + self._joinstyle = 'miter' + def _set_diamond(self): self._transform = Affine2D().translate(-0.5, -0.5).rotate_deg(45) self._snap_threshold = 5.0 @@ -362,7 +386,9 @@ def _set_diamond(self): self._transform.rotate_deg(rotate) self._alt_transform = self._transform - + + self._joinstyle = 'miter' + def _set_thin_diamond(self): self._set_diamond() self._transform.scale(0.6, 1.0) @@ -370,7 +396,7 @@ def _set_thin_diamond(self): def _set_pentagon(self): self._transform = Affine2D().scale(0.5) self._snap_threshold = 5.0 - + polypath = Path.unit_regular_polygon(5) fs = self.get_fillstyle() @@ -397,6 +423,8 @@ def _set_pentagon(self): self._alt_path = mpath_alt self._alt_transform = self._transform + self._joinstyle = 'miter' + def _set_star(self): self._transform = Affine2D().scale(0.5) self._snap_threshold = 5.0 @@ -426,10 +454,12 @@ def _set_star(self): self._alt_path = mpath_alt self._alt_transform = self._transform + self._joinstyle = 'bevel' + def _set_hexagon1(self): self._transform = Affine2D().scale(0.5) self._snap_threshold = 5.0 - + fs = self.get_fillstyle() polypath = Path.unit_regular_polygon(6) @@ -458,10 +488,12 @@ def _set_hexagon1(self): self._alt_path = mpath_alt self._alt_transform = self._transform + self._joinstyle = 'miter' + def _set_hexagon2(self): self._transform = Affine2D().scale(0.5).rotate_deg(30) self._snap_threshold = 5.0 - + fs = self.get_fillstyle() polypath = Path.unit_regular_polygon(6) @@ -490,10 +522,12 @@ def _set_hexagon2(self): self._alt_path = mpath_alt self._alt_transform = self._transform + self._joinstyle = 'miter' + def _set_octagon(self): self._transform = Affine2D().scale(0.5) self._snap_threshold = 5.0 - + fs = self.get_fillstyle() polypath = Path.unit_regular_polygon(8) @@ -514,6 +548,8 @@ def _set_octagon(self): self._path = self._alt_path = half self._alt_transform = self._transform.frozen().rotate_deg(180.0) + self._joinstyle = 'miter' + _line_marker_path = Path([[0.0, -1.0], [0.0, 1.0]]) def _set_vline(self): self._transform = Affine2D().scale(0.5) @@ -539,14 +575,14 @@ def _set_tickright(self): self._snap_threshold = 1.0 self._filled = False self._path = self._tickhoriz_path - + _tickvert_path = Path([[-0.0, 0.0], [-0.0, 1.0]]) def _set_tickup(self): self._transform = Affine2D().scale(1.0, 1.0) self._snap_threshold = 1.0 self._filled = False self._path = self._tickvert_path - + def _set_tickdown(self): self._transform = Affine2D().scale(1.0, -1.0) self._snap_threshold = 1.0 @@ -599,24 +635,28 @@ def _set_caretdown(self): self._snap_threshold = 3.0 self._filled = False self._path = self._caret_path + self._joinstyle = 'miter' def _set_caretup(self): self._transform = Affine2D().scale(0.5).rotate_deg(180) self._snap_threshold = 3.0 self._filled = False self._path = self._caret_path + self._joinstyle = 'miter' def _set_caretleft(self): self._transform = Affine2D().scale(0.5).rotate_deg(270) self._snap_threshold = 3.0 self._filled = False self._path = self._caret_path + self._joinstyle = 'miter' def _set_caretright(self): self._transform = Affine2D().scale(0.5).rotate_deg(90) self._snap_threshold = 3.0 self._filled = False self._path = self._caret_path + self._joinstyle = 'miter' _x_path = Path([[-1.0, -1.0], [1.0, 1.0], [-1.0, 1.0], [1.0, -1.0]],