From 3ef3f5b5a29afae0664702630f0c0935964a9436 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Thu, 7 Jul 2016 00:04:27 -0400 Subject: [PATCH] FIX: do dash scaling early When scaling the dash pattern by the linewidth do the scaling at artist creation / value set time rather than at draw time. closes #6592 closes #6588 closes #6590 Closes #6693 closes #5430 --- lib/matplotlib/backend_bases.py | 14 +-- lib/matplotlib/collections.py | 99 +++++++++++++++----- lib/matplotlib/lines.py | 110 +++++++++++++++-------- lib/matplotlib/patches.py | 22 +++-- lib/matplotlib/tests/test_collections.py | 14 +++ 5 files changed, 178 insertions(+), 81 deletions(-) diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py index 8d70711511ca..91bc30d6e313 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -871,16 +871,7 @@ def get_dashes(self): Default value is None """ - if rcParams['_internal.classic_mode']: - return self._dashes - else: - scale = max(1.0, self.get_linewidth()) - offset, dashes = self._dashes - if offset is not None: - offset = offset * scale - if dashes is not None: - dashes = [x * scale for x in dashes] - return offset, dashes + return self._dashes def get_forced_alpha(self): """ @@ -1062,10 +1053,7 @@ def set_linestyle(self, style): `lines.dotted_pattern`. One may also specify customized dash styles by providing a tuple of (offset, dash pairs). """ - offset, dashes = lines.get_dash_pattern(style) - self._linestyle = style - self.set_dashes(offset, dashes) def set_url(self, url): """ diff --git a/lib/matplotlib/collections.py b/lib/matplotlib/collections.py index 43293c2b0c40..1050d3e721ae 100644 --- a/lib/matplotlib/collections.py +++ b/lib/matplotlib/collections.py @@ -13,9 +13,16 @@ import six from six.moves import zip +try: + from math import gcd +except ImportError: + # LPy workaround + from fractions import gcd import warnings + import numpy as np import numpy.ma as ma + import matplotlib as mpl import matplotlib.cbook as cbook import matplotlib.colors as mcolors @@ -24,13 +31,11 @@ import matplotlib.transforms as transforms import matplotlib.artist as artist from matplotlib.artist import allow_rasterization -import matplotlib.backend_bases as backend_bases import matplotlib.path as mpath from matplotlib import _path import matplotlib.mlab as mlab import matplotlib.lines as mlines - CIRCLE_AREA_FACTOR = 1.0 / np.sqrt(np.pi) @@ -117,6 +122,14 @@ def __init__(self, """ artist.Artist.__init__(self) cm.ScalarMappable.__init__(self, norm, cmap) + # list of un-scaled dash patterns + # this is needed scaling the dash pattern by linewidth + self._us_linestyles = [(None, None)] + # list of dash patterns + self._linestyles = [(None, None)] + # list of unbroadcast/scaled linewidths + self._us_lw = [0] + self._linewidths = [0] self.set_edgecolor(edgecolors) self.set_facecolor(facecolors) @@ -313,7 +326,7 @@ def draw(self, renderer): if do_single_path_optimization: gc.set_foreground(tuple(edgecolors[0])) gc.set_linewidth(self._linewidths[0]) - gc.set_linestyle(self._linestyles[0]) + gc.set_dashes(*self._linestyles[0]) gc.set_antialiased(self._antialiaseds[0]) gc.set_url(self._urls[0]) renderer.draw_markers( @@ -489,8 +502,12 @@ def set_linewidth(self, lw): lw = mpl.rcParams['lines.linewidth'] else: lw = 0 + # get the un-scaled/broadcast lw + self._us_lw = self._get_value(lw) - self._linewidths = self._get_value(lw) + # scale all of the dash patterns. + self._linewidths, self._linestyles = self._bcast_lwls( + self._us_lw, self._us_linestyles) self.stale = True def set_linewidths(self, lw): @@ -534,29 +551,63 @@ def set_linestyle(self, ls): try: if cbook.is_string_like(ls) and cbook.is_hashable(ls): ls = cbook.ls_mapper.get(ls, ls) - dashes = [mlines.get_dash_pattern(ls)] - elif cbook.iterable(ls): + dashes = [mlines._get_dash_pattern(ls)] + else: try: - dashes = [] - for x in ls: - if cbook.is_string_like(x): - x = cbook.ls_mapper.get(x, x) - dashes.append(mlines.get_dash_pattern(x)) - elif cbook.iterable(x) and len(x) == 2: - dashes.append(x) - else: - raise ValueError() + dashes = [mlines._get_dash_pattern(ls)] except ValueError: - if len(ls) == 2: - dashes = [ls] - else: - raise ValueError() - else: - raise ValueError() + dashes = [mlines._get_dash_pattern(x) for x in ls] + except ValueError: - raise ValueError('Do not know how to convert %s to dashes' % ls) - self._linestyles = dashes - self.stale = True + raise ValueError( + 'Do not know how to convert {!r} to dashes'.format(ls)) + + # get the list of raw 'unscaled' dash patterns + self._us_linestyles = dashes + + # broadcast and scale the lw and dash patterns + self._linewidths, self._linestyles = self._bcast_lwls( + self._us_lw, self._us_linestyles) + + @staticmethod + def _bcast_lwls(linewidths, dashes): + '''Internal helper function to broadcast + scale ls/lw + + In the collection drawing code the linewidth and linestyle are + cycled through as circular buffers (via v[i % len(v)]). Thus, + if we are going to scale the dash pattern at set time (not + draw time) we need to do the broadcasting now and expand both + lists to be the same length. + + Parameters + ---------- + linewidths : list + line widths of collection + + dashes : list + dash specification (offset, (dash pattern tuple)) + + Returns + ------- + linewidths, dashes : list + Will be the same length, dashes are scaled by paired linewidth + + ''' + if mpl.rcParams['_internal.classic_mode']: + return linewidths, dashes + # make sure they are the same length so we can zip them + if len(dashes) != len(linewidths): + l_dashes = len(dashes) + l_lw = len(linewidths) + GCD = gcd(l_dashes, l_lw) + dashes = list(dashes) * (l_lw // GCD) + linewidths = list(linewidths) * (l_dashes // GCD) + + # scale the dash patters + dashes = [mlines._scale_dashes(o, d, lw) + for (o, d), lw in zip(dashes, linewidths)] + + return linewidths, dashes def set_linestyles(self, ls): """alias for set_linestyle""" diff --git a/lib/matplotlib/lines.py b/lib/matplotlib/lines.py index 507cff846840..df4b27cdef18 100644 --- a/lib/matplotlib/lines.py +++ b/lib/matplotlib/lines.py @@ -13,11 +13,11 @@ import numpy as np from numpy import ma -from matplotlib import verbose from . import artist, colors as mcolors from .artist import Artist from .cbook import (iterable, is_string_like, is_numlike, ls_mapper_r, - pts_to_prestep, pts_to_poststep, pts_to_midstep) + pts_to_prestep, pts_to_poststep, pts_to_midstep, ls_mapper, + is_hashable) from .path import Path from .transforms import Bbox, TransformedPath, IdentityTransform @@ -36,24 +36,49 @@ from matplotlib import _path -def get_dash_pattern(style): - """ - Given a dash pattern name from 'solid', 'dashed', 'dashdot' or - 'dotted', returns the (offset, dashes) pattern. +def _get_dash_pattern(style): + """Convert linestyle -> dash pattern + """ - if style == 'solid': + # go from short hand -> full strings + if is_string_like(style) and is_hashable(style): + style = ls_mapper.get(style, style) + # un-dashed styles + if style in ['solid', 'None']: offset, dashes = None, None + # dashed styles elif style in ['dashed', 'dashdot', 'dotted']: offset = 0 dashes = tuple(rcParams['lines.{}_pattern'.format(style)]) + # elif isinstance(style, tuple): offset, dashes = style else: raise ValueError('Unrecognized linestyle: %s' % str(style)) + # normalize offset to be positive and shorter than the dash cycle + if dashes is not None and offset is not None: + dsum = sum(dashes) + if dsum: + offset %= dsum + return offset, dashes +def _scale_dashes(offset, dashes, lw): + if rcParams['_internal.classic_mode']: + return offset, dashes + scale = max(1.0, lw) + scaled_offset = scaled_dashes = None + if offset is not None: + scaled_offset = offset * scale + if dashes is not None: + scaled_dashes = [x * scale if x is not None else None + for x in dashes] + + return scaled_offset, scaled_dashes + + def segment_hits(cx, cy, x, y, radius): """ Determine if any line segments are within radius of a @@ -360,10 +385,15 @@ def __init__(self, xdata, ydata, self._linestyles = None self._drawstyle = None - self._linewidth = None + self._linewidth = linewidth + # scaled dash + offset self._dashSeq = None self._dashOffset = 0 + # unscaled dash + offset + # this is needed scaling the dash pattern by linewidth + self._us_dashSeq = None + self._us_dashOffset = 0 self.set_linestyle(linestyle) self.set_drawstyle(drawstyle) @@ -1013,9 +1043,13 @@ def set_linewidth(self, w): ACCEPTS: float value in points """ w = float(w) + if self._linewidth != w: self.stale = True self._linewidth = w + # rescale the dashes + offset + self._dashOffset, self._dashSeq = _scale_dashes( + self._us_dashOffset, self._us_dashSeq, self._linewidth) def _split_drawstyle_linestyle(self, ls): '''Split drawstyle from linestyle string @@ -1093,31 +1127,31 @@ def set_linestyle(self, ls): ls : { ``'-'``, ``'--'``, ``'-.'``, ``':'``} and more see description The line style. """ - if not is_string_like(ls): - if len(ls) != 2: - raise ValueError() - - self.set_dashes(ls[1]) - self._dashOffset = ls[0] - self._linestyle = "--" - return - ds, ls = self._split_drawstyle_linestyle(ls) - if ds is not None: - self.set_drawstyle(ds) - - if ls in [' ', '', 'none']: - ls = 'None' - - if ls not in self._lineStyles: - try: - ls = ls_mapper_r[ls] - except KeyError: - raise ValueError(("You passed in an invalid linestyle, " - "`{0}`. See " - "docs of Line2D.set_linestyle for " - "valid values.").format(ls)) + if is_string_like(ls): + ds, ls = self._split_drawstyle_linestyle(ls) + if ds is not None: + self.set_drawstyle(ds) + + if ls in [' ', '', 'none']: + ls = 'None' + + if ls not in self._lineStyles: + try: + ls = ls_mapper_r[ls] + except KeyError: + raise ValueError(("You passed in an invalid linestyle, " + "`{0}`. See " + "docs of Line2D.set_linestyle for " + "valid values.").format(ls)) + self._linestyle = ls + else: + self._linestyle = '--' - self._linestyle = ls + # get the unscaled dashes + self._us_dashOffset, self._us_dashSeq = _get_dash_pattern(ls) + # compute the linewidth scaled dashes + self._dashOffset, self._dashSeq = _scale_dashes( + self._us_dashOffset, self._us_dashSeq, self._linewidth) @docstring.dedent_interpd def set_marker(self, marker): @@ -1227,10 +1261,7 @@ def set_dashes(self, seq): if seq == (None, None) or len(seq) == 0: self.set_linestyle('-') else: - self.set_linestyle('--') - if self._dashSeq != seq: - self.stale = True - self._dashSeq = seq # TODO: offset ignored for now + self.set_linestyle((0, seq)) def _draw_lines(self, renderer, gc, path, trans): self._lineFunc(renderer, gc, path, trans) @@ -1258,21 +1289,22 @@ def _draw_steps_mid(self, renderer, gc, path, trans): def _draw_solid(self, renderer, gc, path, trans): gc.set_linestyle('solid') + gc.set_dashes(self._dashOffset, self._dashSeq) renderer.draw_path(gc, path, trans) def _draw_dashed(self, renderer, gc, path, trans): gc.set_linestyle('dashed') - if self._dashSeq is not None: - gc.set_dashes(self._dashOffset, self._dashSeq) - + gc.set_dashes(self._dashOffset, self._dashSeq) renderer.draw_path(gc, path, trans) def _draw_dash_dot(self, renderer, gc, path, trans): gc.set_linestyle('dashdot') + gc.set_dashes(self._dashOffset, self._dashSeq) renderer.draw_path(gc, path, trans) def _draw_dotted(self, renderer, gc, path, trans): gc.set_linestyle('dotted') + gc.set_dashes(self._dashOffset, self._dashSeq) renderer.draw_path(gc, path, trans) def update_from(self, other): diff --git a/lib/matplotlib/patches.py b/lib/matplotlib/patches.py index 0b802260a541..7a8ff77c7ba7 100644 --- a/lib/matplotlib/patches.py +++ b/lib/matplotlib/patches.py @@ -17,6 +17,7 @@ from matplotlib import docstring import matplotlib.transforms as transforms from matplotlib.path import Path +import matplotlib.lines as mlines from matplotlib.bezier import split_bezier_intersecting_with_closedpath from matplotlib.bezier import get_intersection, inside_circle, get_parallels @@ -122,10 +123,13 @@ def __init__(self, else: self.set_edgecolor(edgecolor) self.set_facecolor(facecolor) + # unscaled dashes. Needed to scale dash patterns by lw + self._us_dashes = None + self._linewidth = 0 self.set_fill(fill) - self.set_linewidth(linewidth) self.set_linestyle(linestyle) + self.set_linewidth(linewidth) self.set_antialiased(antialiased) self.set_hatch(hatch) self.set_capstyle(capstyle) @@ -361,7 +365,11 @@ def set_linewidth(self, w): w = 0 self._linewidth = float(w) - + # scale the dash pattern by the linewidth + offset, ls = self._us_dashes + self._dashes = mlines._scale_dashes(offset, + ls, + self._linewidth)[1] self.stale = True def set_lw(self, lw): @@ -400,9 +408,13 @@ def set_linestyle(self, ls): """ if ls is None: ls = "solid" - - ls = cbook.ls_mapper.get(ls, ls) self._linestyle = ls + # get the unscalled dash pattern + offset, ls = self._us_dashes = mlines._get_dash_pattern(ls) + # scale the dash pattern by the linewidth + self._dashes = mlines._scale_dashes(offset, + ls, + self._linewidth)[1] self.stale = True def set_ls(self, ls): @@ -510,7 +522,7 @@ def draw(self, renderer): if self._edgecolor[3] == 0: lw = 0 gc.set_linewidth(lw) - gc.set_linestyle(self._linestyle) + gc.set_dashes(0, self._dashes) gc.set_capstyle(self._capstyle) gc.set_joinstyle(self._joinstyle) diff --git a/lib/matplotlib/tests/test_collections.py b/lib/matplotlib/tests/test_collections.py index 2feed20a0610..635d3fafd304 100644 --- a/lib/matplotlib/tests/test_collections.py +++ b/lib/matplotlib/tests/test_collections.py @@ -640,6 +640,20 @@ def test_pandas_indexing(): Collection(antialiaseds=aa) +@cleanup(style='default') +def test_lslw_bcast(): + col = mcollections.PathCollection([]) + col.set_linestyles(['-', '-']) + col.set_linewidths([1, 2, 3]) + + assert col.get_linestyles() == [(None, None)] * 6 + assert col.get_linewidths() == [1, 2, 3] * 2 + + col.set_linestyles(['-', '-', '-']) + assert col.get_linestyles() == [(None, None)] * 3 + assert col.get_linewidths() == [1, 2, 3] + + if __name__ == '__main__': import nose nose.runmodule(argv=['-s', '--with-doctest'], exit=False)