From 6fa0064a192254337946161ff347e250b4409418 Mon Sep 17 00:00:00 2001 From: Michael Droettboom Date: Mon, 6 Feb 2012 14:26:16 -0500 Subject: [PATCH 1/6] Fix single pixel markers. The pixel shape is now optimized so it does the right thing in the Agg backend. For raster backends, linejoin and cap styles is now correctly passed to and used in the backend. --- lib/matplotlib/backends/backend_pdf.py | 15 +++-- lib/matplotlib/backends/backend_ps.py | 9 ++- lib/matplotlib/backends/backend_svg.py | 17 ++++-- lib/matplotlib/lines.py | 2 + lib/matplotlib/markers.py | 85 ++++++++++++++++++-------- 5 files changed, 90 insertions(+), 38 deletions(-) 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..c868b29621fc 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,6 +582,11 @@ 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 + 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)) 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..19f8ac2085cf 100644 --- a/lib/matplotlib/lines.py +++ b/lib/matplotlib/lines.py @@ -541,6 +541,8 @@ 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) diff --git a/lib/matplotlib/markers.py b/lib/matplotlib/markers.py index cd0116031aa7..0b18f8d8bfee 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,11 @@ 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._transform = Affine2D().translate(-0.5, -0.5) \ + .scale(0.5, 0.5).translate(0.5, 0.5) self._snap_threshold = False - + self._joinstyle = 'miter' + def _set_point(self): self._set_circle(reduction = self._point_size_reduction) @@ -297,7 +310,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 +326,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 +360,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 +379,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 +389,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 +416,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 +447,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 +481,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 +515,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 +541,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 +568,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 +628,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]], From 342007cb99ff7d5532e4e16aa674bd35cf229c91 Mon Sep 17 00:00:00 2001 From: Jae-Joon Lee Date: Wed, 29 Feb 2012 17:00:01 +0900 Subject: [PATCH 2/6] add offset of 0.5 when marker is stroked so that snapping works better in agg backend --- src/_backend_agg.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/_backend_agg.cpp b/src/_backend_agg.cpp index f7a986fc2f95..2e7f4c6ff6fc 100644 --- a/src/_backend_agg.cpp +++ b/src/_backend_agg.cpp @@ -650,7 +650,7 @@ RendererAgg::draw_markers(const Py::Tuple& args) // Deal with the difference in y-axis direction marker_trans *= agg::trans_affine_scaling(1.0, -1.0); trans *= agg::trans_affine_scaling(1.0, -1.0); - trans *= agg::trans_affine_translation(0.0, (double)height); + trans *= agg::trans_affine_translation(0.5, (double)height+0.5); PathIterator marker_path(marker_path_obj); transformed_path_t marker_path_transformed(marker_path, marker_trans); From fabf327b03bb162b009be0e4945a0d11ca10f14d Mon Sep 17 00:00:00 2001 From: Michael Droettboom Date: Wed, 29 Feb 2012 10:44:25 -0500 Subject: [PATCH 3/6] Respect stroke width on markers in the Ps backend --- lib/matplotlib/backends/backend_ps.py | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/lib/matplotlib/backends/backend_ps.py b/lib/matplotlib/backends/backend_ps.py index c868b29621fc..dfadb52cfd03 100644 --- a/lib/matplotlib/backends/backend_ps.py +++ b/lib/matplotlib/backends/backend_ps.py @@ -582,18 +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 - jint = gc.get_joinstyle() - ps_cmd.append('%d setlinejoin' % jint) - cint = gc.get_capstyle() - ps_cmd.append('%d setlinecap' % cint) + + 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): From 752ad0e3203da53d1730e5d73e5ee057f68e553b Mon Sep 17 00:00:00 2001 From: Michael Droettboom Date: Wed, 29 Feb 2012 10:45:42 -0500 Subject: [PATCH 4/6] Another attempt at getting pixel markers to work correctly. --- lib/matplotlib/lines.py | 5 ++++- lib/matplotlib/markers.py | 4 +--- src/_backend_agg.cpp | 2 +- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/lib/matplotlib/lines.py b/lib/matplotlib/lines.py index 19f8ac2085cf..19bb108b2d45 100644 --- a/lib/matplotlib/lines.py +++ b/lib/matplotlib/lines.py @@ -546,8 +546,11 @@ def draw(self, renderer): 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 0b18f8d8bfee..922976eec326 100644 --- a/lib/matplotlib/markers.py +++ b/lib/matplotlib/markers.py @@ -274,10 +274,8 @@ def _set_circle(self, reduction = 1.0): def _set_pixel(self): self._path = Path.unit_rectangle() - self._transform = Affine2D().translate(-0.5, -0.5) \ - .scale(0.5, 0.5).translate(0.5, 0.5) + self._transform = Affine2D().translate(-0.49999, -0.50001) self._snap_threshold = False - self._joinstyle = 'miter' def _set_point(self): self._set_circle(reduction = self._point_size_reduction) diff --git a/src/_backend_agg.cpp b/src/_backend_agg.cpp index 2e7f4c6ff6fc..f7a986fc2f95 100644 --- a/src/_backend_agg.cpp +++ b/src/_backend_agg.cpp @@ -650,7 +650,7 @@ RendererAgg::draw_markers(const Py::Tuple& args) // Deal with the difference in y-axis direction marker_trans *= agg::trans_affine_scaling(1.0, -1.0); trans *= agg::trans_affine_scaling(1.0, -1.0); - trans *= agg::trans_affine_translation(0.5, (double)height+0.5); + trans *= agg::trans_affine_translation(0.0, (double)height); PathIterator marker_path(marker_path_obj); transformed_path_t marker_path_transformed(marker_path, marker_trans); From 92e3b635e7d9c7a6909d6198bf613c3b7f5427a7 Mon Sep 17 00:00:00 2001 From: Michael Droettboom Date: Wed, 29 Feb 2012 13:09:22 -0500 Subject: [PATCH 5/6] Fix typo. --- lib/matplotlib/markers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/matplotlib/markers.py b/lib/matplotlib/markers.py index 922976eec326..7207e25fb9dd 100644 --- a/lib/matplotlib/markers.py +++ b/lib/matplotlib/markers.py @@ -275,7 +275,7 @@ def _set_circle(self, reduction = 1.0): def _set_pixel(self): self._path = Path.unit_rectangle() self._transform = Affine2D().translate(-0.49999, -0.50001) - self._snap_threshold = False + self._snap_threshold = None def _set_point(self): self._set_circle(reduction = self._point_size_reduction) From b05aa16106b25fadb127ea1f96516c916b664a72 Mon Sep 17 00:00:00 2001 From: Michael Droettboom Date: Wed, 29 Feb 2012 13:14:41 -0500 Subject: [PATCH 6/6] Comment why the offset of the pixel rectangle is surprising. --- lib/matplotlib/markers.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/lib/matplotlib/markers.py b/lib/matplotlib/markers.py index 7207e25fb9dd..fea9150d1a71 100644 --- a/lib/matplotlib/markers.py +++ b/lib/matplotlib/markers.py @@ -274,6 +274,15 @@ def _set_circle(self, reduction = 1.0): def _set_pixel(self): self._path = Path.unit_rectangle() + # 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