From 178bbe830b6fc1086b382869e9e49572cee1a84b Mon Sep 17 00:00:00 2001 From: Michael Waskom Date: Fri, 15 May 2020 19:09:56 -0400 Subject: [PATCH 1/6] Programatically define arbitrary dash specs --- seaborn/core.py | 52 ++++++++++++++++++++++++++++++++++++++ seaborn/relational.py | 15 +++++------ seaborn/tests/test_core.py | 18 ++++++++++++- 3 files changed, 76 insertions(+), 9 deletions(-) diff --git a/seaborn/core.py b/seaborn/core.py index fb432f7507..0ecab9ddc1 100644 --- a/seaborn/core.py +++ b/seaborn/core.py @@ -1,3 +1,4 @@ +import itertools from collections.abc import Iterable, Sequence, Mapping import numpy as np import pandas as pd @@ -228,3 +229,54 @@ def establish_variables_longform(self, data=None, **kwargs): } return plot_data, variables + + +def unique_dashes(n): + """Build an arbitrarily long list of unique dash styles for lines. + + Parameters + ---------- + n : int + Number of unique dash specs to generate. + Returns + ------- + dashes : list of tuples + Valid arguments for the ``dashes`` parameter on + :class:`matplotlib.lines.Line2D`. The first spec is a solid + line (``""``), the remainder are sequences of long and short + dashes. + + """ + + # Start with 5 dash specs that are well distinguishable + dashes = [ + "", + (4, 1.5), + (1, 1), + (3, 1.25, 1.5, 1.25), + (5, 1, 1, 1), + ] + + # Now programatically build as many as we need + q = 3 + while len(dashes) < n: + + # Take combinations of long and short dashes + a = itertools.combinations_with_replacement([3, 1.5], q) + b = itertools.combinations_with_replacement([4, 1], q) + + # Interleave the combinations, reversing one of the streams + segment_list = itertools.chain(*zip( + list(a)[1:-1][::-1], + list(b)[1:-1] + )) + + # Now insert the "off" segments + for segments in segment_list: + off = min(segments) + spec = tuple(itertools.chain(*((on, off) for on in segments))) + dashes.append(spec) + + q += 1 + + return dashes[:n] diff --git a/seaborn/relational.py b/seaborn/relational.py index 09f68137ba..f63be05a48 100644 --- a/seaborn/relational.py +++ b/seaborn/relational.py @@ -7,10 +7,10 @@ import matplotlib as mpl import matplotlib.pyplot as plt -from .core import _VectorPlotter -from . import utils +from .core import (_VectorPlotter, unique_dashes) from .utils import (categorical_order, get_color_cycle, ci_to_errsize, - remove_na, locator_to_legend_entries) + remove_na, locator_to_legend_entries, + ci as ci_func) from .algorithms import bootstrap from .palettes import (color_palette, cubehelix_palette, _parse_cubehelix_args, QUAL_PALETTES) @@ -38,9 +38,6 @@ class _RelationalPlotter(_VectorPlotter): # Defaults for style semantic default_markers = ["o", "X", "s", "P", "D", "^", "v", "p"] - default_dashes = ["", (4, 1.5), (1, 1), - (3, 1, 1.5, 1), (5, 1, 1, 1), - (5, 1, 2, 1, 2, 1)] def categorical_to_palette(self, data, order, palette): """Determine colors when the hue variable is qualitative.""" @@ -250,6 +247,8 @@ def parse_hue(self, data, palette=None, order=None, norm=None): self.cmap = cmap # Update data as it may have changed dtype + # TODO This is messy! We need to rethink the order of operations + # to avoid changing the plot data after we have it. self.plot_data["hue"] = data def parse_size(self, data, sizes=None, order=None, norm=None): @@ -377,7 +376,7 @@ def parse_style(self, data, markers=None, dashes=None, order=None): ) dashes = self.style_to_attributes( - levels, dashes, self.default_dashes, "dashes" + levels, dashes, unique_dashes(len(levels)), "dashes" ) paths = {} @@ -592,7 +591,7 @@ def bootstrapped_cis(vals): return null_ci boots = bootstrap(vals, func=func, n_boot=n_boot, seed=seed) - cis = utils.ci(boots, ci) + cis = ci_func(boots, ci) return pd.Series(cis, ["low", "high"]) # Group and get the aggregation estimate diff --git a/seaborn/tests/test_core.py b/seaborn/tests/test_core.py index 913abe958b..081ec7178d 100644 --- a/seaborn/tests/test_core.py +++ b/seaborn/tests/test_core.py @@ -2,7 +2,10 @@ from numpy.testing import assert_array_equal -from ..core import _VectorPlotter +from ..core import ( + _VectorPlotter, + unique_dashes, +) class TestVectorPlotter: @@ -33,3 +36,16 @@ def test_flat_variables(self, flat_data): assert p.variables["x"] == expected_x_name assert p.variables["y"] == expected_y_name + + +class TestCoreFunc: + + def test_unique_dashes(self): + + n = 24 + dashes = unique_dashes(n) + assert len(set(dashes)) == n + + assert dashes[0] == "" + for spec in dashes[1:]: + assert isinstance(spec, tuple) From 785442f3e3a3340f7d3458121a3693246fd70dab Mon Sep 17 00:00:00 2001 From: Michael Waskom Date: Sat, 16 May 2020 12:47:45 -0400 Subject: [PATCH 2/6] Add unique default markers and update tests --- seaborn/core.py | 67 +++++++++++++++++++++++++++----- seaborn/relational.py | 7 +--- seaborn/tests/test_core.py | 17 +++++++- seaborn/tests/test_relational.py | 31 +++++++++++++-- 4 files changed, 102 insertions(+), 20 deletions(-) diff --git a/seaborn/core.py b/seaborn/core.py index 0ecab9ddc1..ace3173d96 100644 --- a/seaborn/core.py +++ b/seaborn/core.py @@ -1,7 +1,9 @@ import itertools from collections.abc import Iterable, Sequence, Mapping + import numpy as np import pandas as pd +import matplotlib as mpl class _VectorPlotter: @@ -238,17 +240,17 @@ def unique_dashes(n): ---------- n : int Number of unique dash specs to generate. + Returns ------- - dashes : list of tuples + dashes : list of strings or tuples Valid arguments for the ``dashes`` parameter on :class:`matplotlib.lines.Line2D`. The first spec is a solid line (``""``), the remainder are sequences of long and short dashes. """ - - # Start with 5 dash specs that are well distinguishable + # Start with dash specs that are well distinguishable dashes = [ "", (4, 1.5), @@ -258,12 +260,12 @@ def unique_dashes(n): ] # Now programatically build as many as we need - q = 3 + p = 3 while len(dashes) < n: # Take combinations of long and short dashes - a = itertools.combinations_with_replacement([3, 1.5], q) - b = itertools.combinations_with_replacement([4, 1], q) + a = itertools.combinations_with_replacement([3, 1.25], p) + b = itertools.combinations_with_replacement([4, 1], p) # Interleave the combinations, reversing one of the streams segment_list = itertools.chain(*zip( @@ -271,12 +273,57 @@ def unique_dashes(n): list(b)[1:-1] )) - # Now insert the "off" segments + # Now insert the gaps for segments in segment_list: - off = min(segments) - spec = tuple(itertools.chain(*((on, off) for on in segments))) + gap = min(segments) + spec = tuple(itertools.chain(*((seg, gap) for seg in segments))) dashes.append(spec) - q += 1 + p += 1 return dashes[:n] + + +def unique_markers(n): + """Build an arbitrarily long list of unique marker styles for points. + + Parameters + ---------- + n : int + Number of unique marker specs to generate. + + Returns + ------- + markers : list of :class:`matplotlib.markers.MarkerStyle` objects + All markers will be filled. + + """ + # Start with marker specs that are well distinguishable + markers = [ + "o", + "X", + (4, 0, 45), + "P", + (4, 0, 0), + (4, 1, 0), + "^", + (4, 1, 45), + "v", + ] + + # Now generate more from regular polygons of increasing order + s = 5 + while len(markers) < n: + a = 360 / (s + 1) / 2 + markers.extend([ + (s + 1, 1, a), + (s + 1, 0, a), + (s, 1, 0), + (s, 0, 0), + ]) + s += 1 + + # Convert to MarkerStyle object, using only exactly what we need + markers = [mpl.markers.MarkerStyle(m) for m in markers[:n]] + + return markers diff --git a/seaborn/relational.py b/seaborn/relational.py index f63be05a48..5a45e167ab 100644 --- a/seaborn/relational.py +++ b/seaborn/relational.py @@ -7,7 +7,7 @@ import matplotlib as mpl import matplotlib.pyplot as plt -from .core import (_VectorPlotter, unique_dashes) +from .core import (_VectorPlotter, unique_dashes, unique_markers) from .utils import (categorical_order, get_color_cycle, ci_to_errsize, remove_na, locator_to_legend_entries, ci as ci_func) @@ -36,9 +36,6 @@ class _RelationalPlotter(_VectorPlotter): # TODO this should match style of other defaults _default_size_range = 0, 1 - # Defaults for style semantic - default_markers = ["o", "X", "s", "P", "D", "^", "v", "p"] - def categorical_to_palette(self, data, order, palette): """Determine colors when the hue variable is qualitative.""" # -- Identify the order and name of the levels @@ -372,7 +369,7 @@ def parse_style(self, data, markers=None, dashes=None, order=None): levels = order markers = self.style_to_attributes( - levels, markers, self.default_markers, "markers" + levels, markers, unique_markers(len(levels)), "markers" ) dashes = self.style_to_attributes( diff --git a/seaborn/tests/test_core.py b/seaborn/tests/test_core.py index 081ec7178d..fc78f10b2f 100644 --- a/seaborn/tests/test_core.py +++ b/seaborn/tests/test_core.py @@ -1,10 +1,12 @@ import numpy as np +import matplotlib as mpl from numpy.testing import assert_array_equal from ..core import ( _VectorPlotter, unique_dashes, + unique_markers, ) @@ -44,8 +46,21 @@ def test_unique_dashes(self): n = 24 dashes = unique_dashes(n) - assert len(set(dashes)) == n + assert len(dashes) == n + assert len(set(dashes)) == n assert dashes[0] == "" for spec in dashes[1:]: assert isinstance(spec, tuple) + assert not len(spec) % 2 + + def test_unique_markers(self): + + n = 24 + markers = unique_markers(n) + + assert len(markers) == n + assert len(set(markers)) == n + for m in markers: + assert isinstance(m, mpl.markers.MarkerStyle) + assert m.is_filled() diff --git a/seaborn/tests/test_relational.py b/seaborn/tests/test_relational.py index 63ae5d1d85..486011c7e3 100644 --- a/seaborn/tests/test_relational.py +++ b/seaborn/tests/test_relational.py @@ -11,6 +11,11 @@ from ..palettes import color_palette from ..utils import categorical_order +from ..core import ( + unique_dashes, + unique_markers, +) + from ..relational import ( _RelationalPlotter, _LinePlotter, @@ -860,8 +865,17 @@ def test_parse_style(self, long_df): # Test defaults markers, dashes = True, True p.parse_style(p.plot_data["style"], markers, dashes) - assert p.markers == dict(zip(p.style_levels, p.default_markers)) - assert p.dashes == dict(zip(p.style_levels, p.default_dashes)) + + n = len(p.style_levels) + assert p.dashes == dict(zip(p.style_levels, unique_dashes(n))) + + actual_marker_paths = { + k: m.get_path() for k, m in p.markers.items() + } + expected_marker_paths = { + k: m.get_path() for k, m in zip(p.style_levels, unique_markers(n)) + } + assert actual_marker_paths == expected_marker_paths # Test lists markers, dashes = ["o", "s", "d"], [(1, 0), (1, 1), (2, 1, 3, 1)] @@ -880,8 +894,17 @@ def test_parse_style(self, long_df): style_order = np.take(p.style_levels, [1, 2, 0]) markers = dashes = True p.parse_style(p.plot_data["style"], markers, dashes, style_order) - assert p.markers == dict(zip(style_order, p.default_markers)) - assert p.dashes == dict(zip(style_order, p.default_dashes)) + + n = len(style_order) + assert p.dashes == dict(zip(style_order, unique_dashes(n))) + + actual_marker_paths = { + k: m.get_path() for k, m in p.markers.items() + } + expected_marker_paths = { + k: m.get_path() for k, m in zip(style_order, unique_markers(n)) + } + assert actual_marker_paths == expected_marker_paths # Test too many levels with style lists markers, dashes = ["o", "s"], False From 4af3e672ae310d9998d0f2eefee1f680cf5f46ba Mon Sep 17 00:00:00 2001 From: Michael Waskom Date: Sat, 16 May 2020 13:50:39 -0400 Subject: [PATCH 3/6] Don't pass MarkerStyle into plt.plot This fails; see https://github.com/matplotlib/matplotlib/issues/17432 --- seaborn/core.py | 4 ++-- seaborn/tests/test_core.py | 3 +-- seaborn/tests/test_relational.py | 12 ++++++++---- 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/seaborn/core.py b/seaborn/core.py index ace3173d96..fbe517714f 100644 --- a/seaborn/core.py +++ b/seaborn/core.py @@ -324,6 +324,6 @@ def unique_markers(n): s += 1 # Convert to MarkerStyle object, using only exactly what we need - markers = [mpl.markers.MarkerStyle(m) for m in markers[:n]] + # markers = [mpl.markers.MarkerStyle(m) for m in markers[:n]] - return markers + return markers[:n] diff --git a/seaborn/tests/test_core.py b/seaborn/tests/test_core.py index fc78f10b2f..fe2fb37c06 100644 --- a/seaborn/tests/test_core.py +++ b/seaborn/tests/test_core.py @@ -62,5 +62,4 @@ def test_unique_markers(self): assert len(markers) == n assert len(set(markers)) == n for m in markers: - assert isinstance(m, mpl.markers.MarkerStyle) - assert m.is_filled() + assert mpl.markers.MarkerStyle(m).is_filled() diff --git a/seaborn/tests/test_relational.py b/seaborn/tests/test_relational.py index 486011c7e3..ad79736410 100644 --- a/seaborn/tests/test_relational.py +++ b/seaborn/tests/test_relational.py @@ -870,10 +870,12 @@ def test_parse_style(self, long_df): assert p.dashes == dict(zip(p.style_levels, unique_dashes(n))) actual_marker_paths = { - k: m.get_path() for k, m in p.markers.items() + k: mpl.markers.MarkerStyle(m).get_path() + for k, m in p.markers.items() } expected_marker_paths = { - k: m.get_path() for k, m in zip(p.style_levels, unique_markers(n)) + k: mpl.markers.MarkerStyle(m).get_path() + for k, m in zip(p.style_levels, unique_markers(n)) } assert actual_marker_paths == expected_marker_paths @@ -899,10 +901,12 @@ def test_parse_style(self, long_df): assert p.dashes == dict(zip(style_order, unique_dashes(n))) actual_marker_paths = { - k: m.get_path() for k, m in p.markers.items() + k: mpl.markers.MarkerStyle(m).get_path() + for k, m in p.markers.items() } expected_marker_paths = { - k: m.get_path() for k, m in zip(style_order, unique_markers(n)) + k: mpl.markers.MarkerStyle(m).get_path() + for k, m in zip(style_order, unique_markers(n)) } assert actual_marker_paths == expected_marker_paths From b9079e6714fc8327a6a572e943aead9ced5d0b7f Mon Sep 17 00:00:00 2001 From: Michael Waskom Date: Sat, 16 May 2020 13:50:44 -0400 Subject: [PATCH 4/6] Update release notes --- doc/releases/v0.11.0.txt | 4 ++++ seaborn/core.py | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/doc/releases/v0.11.0.txt b/doc/releases/v0.11.0.txt index 3213da1313..5cc8ac6477 100644 --- a/doc/releases/v0.11.0.txt +++ b/doc/releases/v0.11.0.txt @@ -2,10 +2,14 @@ v0.11.0 (Unreleased) -------------------- +- TODO stub for explaining improvements to variable specificiation. Make this good! + - Enforced keyword-only arguments for most parameters of most functions and classes. - Standardized the parameter names for the oldest functions (:func:`distplot`, :func:`kdeplot`, and :func:`rugplot`) to be `x` and `y`, as in other functions. Using the old names will warn now and break in the future. +- Plots with a ``style`` semantic can now generate an infinite number of default dashes and/or markers. Prevously, an error would be raised if the ``style`` variable had more levels than could be mapped using the default lists. The existing defaults were slightly modified as part of this change; if you need to exactly reproduce plots from earlier versions, refer to the `old defaults `_. + - Added a ``tight_layout`` method to :class:`FacetGrid` and :class:`PairGrid`, which runs the :func:`matplotlib.pyplot.tight_layout` algorithm without interference from the external legend. - Added an explicit warning in :func:`swarmplot` when more than 2% of the points are overlap in the "gutters" of the swarm. diff --git a/seaborn/core.py b/seaborn/core.py index fbe517714f..6ed8aa1b51 100644 --- a/seaborn/core.py +++ b/seaborn/core.py @@ -3,7 +3,6 @@ import numpy as np import pandas as pd -import matplotlib as mpl class _VectorPlotter: @@ -294,7 +293,8 @@ def unique_markers(n): Returns ------- - markers : list of :class:`matplotlib.markers.MarkerStyle` objects + markers : list of string or tuples + Values for defining :class:`matplotlib.markers.MarkerStyle` objects. All markers will be filled. """ From 409fa91af49d3d5b258304a6dc4053c4f8061421 Mon Sep 17 00:00:00 2001 From: Michael Waskom Date: Sat, 16 May 2020 15:15:32 -0400 Subject: [PATCH 5/6] Emphasize that default dashes/markers are unique. --- doc/releases/v0.11.0.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/releases/v0.11.0.txt b/doc/releases/v0.11.0.txt index 5cc8ac6477..9cd38a4125 100644 --- a/doc/releases/v0.11.0.txt +++ b/doc/releases/v0.11.0.txt @@ -8,7 +8,7 @@ v0.11.0 (Unreleased) - Standardized the parameter names for the oldest functions (:func:`distplot`, :func:`kdeplot`, and :func:`rugplot`) to be `x` and `y`, as in other functions. Using the old names will warn now and break in the future. -- Plots with a ``style`` semantic can now generate an infinite number of default dashes and/or markers. Prevously, an error would be raised if the ``style`` variable had more levels than could be mapped using the default lists. The existing defaults were slightly modified as part of this change; if you need to exactly reproduce plots from earlier versions, refer to the `old defaults `_. +- Plots with a ``style`` semantic can now generate an infinite number of unique dashes and/or markers by default. Prevously, an error would be raised if the ``style`` variable had more levels than could be mapped using the default lists. The existing defaults were slightly modified as part of this change; if you need to exactly reproduce plots from earlier versions, refer to the `old defaults `_. - Added a ``tight_layout`` method to :class:`FacetGrid` and :class:`PairGrid`, which runs the :func:`matplotlib.pyplot.tight_layout` algorithm without interference from the external legend. From a578745f709a49838b985c7ea5ad354de0a94fe1 Mon Sep 17 00:00:00 2001 From: Michael Waskom Date: Sat, 16 May 2020 15:25:56 -0400 Subject: [PATCH 6/6] Add refs to github PRs --- doc/releases/v0.11.0.txt | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/doc/releases/v0.11.0.txt b/doc/releases/v0.11.0.txt index 9cd38a4125..022b520f0f 100644 --- a/doc/releases/v0.11.0.txt +++ b/doc/releases/v0.11.0.txt @@ -2,18 +2,18 @@ v0.11.0 (Unreleased) -------------------- -- TODO stub for explaining improvements to variable specificiation. Make this good! +- TODO stub for explaining improvements to variable specificiation. Make this good! GH2017 -- Enforced keyword-only arguments for most parameters of most functions and classes. +- Enforced keyword-only arguments for most parameters of most functions and classes. GH2052 -- Standardized the parameter names for the oldest functions (:func:`distplot`, :func:`kdeplot`, and :func:`rugplot`) to be `x` and `y`, as in other functions. Using the old names will warn now and break in the future. +- Standardized the parameter names for the oldest functions (:func:`distplot`, :func:`kdeplot`, and :func:`rugplot`) to be `x` and `y`, as in other functions. Using the old names will warn now and break in the future. GH2060 -- Plots with a ``style`` semantic can now generate an infinite number of unique dashes and/or markers by default. Prevously, an error would be raised if the ``style`` variable had more levels than could be mapped using the default lists. The existing defaults were slightly modified as part of this change; if you need to exactly reproduce plots from earlier versions, refer to the `old defaults `_. +- Plots with a ``style`` semantic can now generate an infinite number of unique dashes and/or markers by default. Prevously, an error would be raised if the ``style`` variable had more levels than could be mapped using the default lists. The existing defaults were slightly modified as part of this change; if you need to exactly reproduce plots from earlier versions, refer to the `old defaults `_. GH2075 -- Added a ``tight_layout`` method to :class:`FacetGrid` and :class:`PairGrid`, which runs the :func:`matplotlib.pyplot.tight_layout` algorithm without interference from the external legend. +- Added a ``tight_layout`` method to :class:`FacetGrid` and :class:`PairGrid`, which runs the :func:`matplotlib.pyplot.tight_layout` algorithm without interference from the external legend. GH2073 -- Added an explicit warning in :func:`swarmplot` when more than 2% of the points are overlap in the "gutters" of the swarm. +- Added an explicit warning in :func:`swarmplot` when more than 2% of the points are overlap in the "gutters" of the swarm. GH2045 -- Added the ``axes_dict`` attribute to :class:`FacetGrid` for named access to the component axes. +- Added the ``axes_dict`` attribute to :class:`FacetGrid` for named access to the component axes. GH2046 -- Made :meth:`FacetGrid.set_axis_labels` clear labels from "interior" axes. +- Made :meth:`FacetGrid.set_axis_labels` clear labels from "interior" axes. GH2046