From 5e23ba2f1175ff971203001b9f0eb035446b3a8d Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Sat, 21 Mar 2015 18:04:18 -0400 Subject: [PATCH 01/24] WIP : first pass at adding a Cycler Helper-class to compose arbitrary, complex, property cycles. An alternate implementation + documentation forth coming. --- lib/matplotlib/cycler.py | 102 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 102 insertions(+) create mode 100644 lib/matplotlib/cycler.py diff --git a/lib/matplotlib/cycler.py b/lib/matplotlib/cycler.py new file mode 100644 index 000000000000..cf34508edd65 --- /dev/null +++ b/lib/matplotlib/cycler.py @@ -0,0 +1,102 @@ +from __future__ import (absolute_import, division, print_function, + unicode_literals) + +import six +from itertools import product, cycle +from six.moves import zip + + +class Cycler(object): + """ + A class to handle cycling multiple artist properties. + + This class has two compositions methods '+' for 'inner' + products of the cycles and '*' for outer products of the + cycles. + + Parameters + ---------- + left : Cycler or None + The 'left' cycler + + right : Cycler or None + The 'right' cycler + + op : func or None + Function which composes the 'left' and 'right' cyclers. + + """ + def __init__(self, left, right=None, op=None): + self._left = left + self._right = right + self._op = op + l_key = left.keys if left is not None else set() + r_key = right.keys if right is not None else set() + if l_key & r_key: + raise ValueError("Can not compose overlapping cycles") + self._keys = l_key | r_key + + @property + def keys(self): + return self._keys + + def finite_iter(self): + """ + Return a finite iterator over the configurations in + this cycle. + """ + if self._right is None: + try: + return self._left.finite_iter() + except AttributeError: + return iter(self._left) + return self._compose() + + def _compose(self): + """ + Compose the 'left' and 'right' components of this cycle + with the proper operation (zip or product as of now) + """ + for a, b in self._op(self._left.finite_iter(), + self._right.finite_iter()): + out = dict() + out.update(a) + out.update(b) + yield out + + @classmethod + def from_iter(cls, label, itr): + """ + Class method to create 'base' Cycler objects + that do not have a 'right' or 'op' and for which + the 'left' object is not another Cycler. + + Parameters + ---------- + label : str + The property key. + + itr : iterable + Finite length iterable of the property values. + + Returns + ------- + cycler : Cycler + New 'base' `Cycler` + """ + ret = cls(None) + ret._left = list({label: v} for v in itr) + ret._keys = set([label]) + return ret + + def __iter__(self): + return cycle(self.finite_iter()) + + def __add__(self, other): + return Cycler(self, other, zip) + + def __mul__(self, other): + return Cycler(self, other, product) + + def __len__(self): + return len(list(self.finite_iter())) From 5539698582638303e06be85364d0497c39e2821c Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Sat, 21 Mar 2015 19:02:37 -0400 Subject: [PATCH 02/24] WIP : version 2 of cycler classes I think I like this version better. More classes, but less implicit magic. --- lib/matplotlib/cycler.py | 135 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 135 insertions(+) diff --git a/lib/matplotlib/cycler.py b/lib/matplotlib/cycler.py index cf34508edd65..57a270ef97b1 100644 --- a/lib/matplotlib/cycler.py +++ b/lib/matplotlib/cycler.py @@ -100,3 +100,138 @@ def __mul__(self, other): def __len__(self): return len(list(self.finite_iter())) + + +class _base_cycler(object): + """ + Helper base-class to provide composition logic to + `SingleCycler` and `CompoundCycler`. + + This class + does not have a `__init__` method which will result + in a usable object. + """ + def __iter__(self): + return cycle(self.finite_iter()) + + def __add__(self, other): + return CompoundCycler(self, other, zip) + + def __mul__(self, other): + return CompoundCycler(self, other, product) + + +class SingleCycler(_base_cycler): + """ + Class to hold the cycle for a single parameter and handle the + composition. + + Parameters + ---------- + label : str + The name of the property this cycles over + itr : iterable + Finite length iterable of the property values. + """ + def __init__(self, label, itr): + self._itr = itr + self._label = label + + def finite_iter(self): + """ + Return a finite iterator over the configurations in + this cycle. + + Returns + ------- + gen : generator + A generator that yields dictionaries keyed on the property + name of the values to be used + + """ + return ({self._label: v} for v in self._itr) + + def __len__(self): + return len(self._itr) + + @property + def keys(self): + """ + The properties that this cycle loops over + """ + return set([self._label]) + + +class CompoundCycler(_base_cycler): + """ + A class to handle cycling multiple artist properties. + + This class has two compositions methods '+' for 'inner' + products of the cycles and '*' for outer products of the + cycles. + + This objects should not be created directly, but instead + result of composition of existing `SingleCycler` and + `CompoundCycler` objects. + + Parameters + ---------- + left : _base_cycler + The 'left' cycler + + right : _base_cycler + The 'right' cycler + + op : function + Function which composes the 'left' and 'right' cyclers. + + """ + def __init__(self, left, right, op): + self._left = left + self._right = right + self._op = op + l_key = left.keys + r_key = right.keys + if l_key & r_key: + raise ValueError("Can not compose overlapping cycles") + self._keys = l_key | r_key + + def finite_iter(self): + """ + Return a finite iterator over the configurations in + this cycle. + + Returns + ------- + gen : generator + A generator that yields dictionaries keyed on the property + name of the values to be used + """ + return self._compose() + + def _compose(self): + """ + Private function to handle the logic of merging the dictionaries + of the left and right cycles. + + Yields + ------ + ret : dict + A dictionary keyed on the property name of the values to be used + """ + for a, b in self._op(self._left.finite_iter(), + self._right.finite_iter()): + out = dict() + out.update(a) + out.update(b) + yield out + + def __len__(self): + return len(list(self.finite_iter())) + + @property + def keys(self): + """ + The properties that this cycle loops over + """ + return self._keys From 958b0b63f53a173bc424e94e3f8637b350ce1f4f Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Sat, 21 Mar 2015 23:52:43 -0400 Subject: [PATCH 03/24] MNT : tweak Cycler API - make class method private - provide simple user facing factory method --- lib/matplotlib/cycler.py | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/lib/matplotlib/cycler.py b/lib/matplotlib/cycler.py index 57a270ef97b1..aa2b7c4d7978 100644 --- a/lib/matplotlib/cycler.py +++ b/lib/matplotlib/cycler.py @@ -65,7 +65,7 @@ def _compose(self): yield out @classmethod - def from_iter(cls, label, itr): + def _from_iter(cls, label, itr): """ Class method to create 'base' Cycler objects that do not have a 'right' or 'op' and for which @@ -102,6 +102,27 @@ def __len__(self): return len(list(self.finite_iter())) +def cycler(label, itr): + """ + Create a new `Cycler` object from a property name and + iterable of values. + + Parameters + ---------- + label : str + The property key. + + itr : iterable + Finite length iterable of the property values. + + Returns + ------- + cycler : Cycler + New `Cycler` for the given property + """ + return Cycler._from_iter(label, itr) + + class _base_cycler(object): """ Helper base-class to provide composition logic to From 6b0696defe0a99b0f716823a4b6f5cd1b4a97a51 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Sat, 21 Mar 2015 23:53:16 -0400 Subject: [PATCH 04/24] MNT : Cycler-family API tweaks - make _base_cycler public as BaseCycler --- lib/matplotlib/cycler.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/matplotlib/cycler.py b/lib/matplotlib/cycler.py index aa2b7c4d7978..bd434a3c22d9 100644 --- a/lib/matplotlib/cycler.py +++ b/lib/matplotlib/cycler.py @@ -123,7 +123,7 @@ def cycler(label, itr): return Cycler._from_iter(label, itr) -class _base_cycler(object): +class BaseCycler(object): """ Helper base-class to provide composition logic to `SingleCycler` and `CompoundCycler`. @@ -142,7 +142,7 @@ def __mul__(self, other): return CompoundCycler(self, other, product) -class SingleCycler(_base_cycler): +class SingleCycler(BaseCycler): """ Class to hold the cycle for a single parameter and handle the composition. @@ -183,7 +183,7 @@ def keys(self): return set([self._label]) -class CompoundCycler(_base_cycler): +class CompoundCycler(BaseCycler): """ A class to handle cycling multiple artist properties. @@ -197,10 +197,10 @@ class CompoundCycler(_base_cycler): Parameters ---------- - left : _base_cycler + left : BaseCycler The 'left' cycler - right : _base_cycler + right : BaseCycler The 'right' cycler op : function From 2f57756804bb8713408e6682e40a516ec7f6ac26 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Sun, 22 Mar 2015 00:19:57 -0400 Subject: [PATCH 05/24] ENH : added in-place operations - added `__imul__` and `__iadd__` - added paranoia copy functions to make sure that child Cyclers can't be changed after they are composed (which would mess up the key tracking logic) --- lib/matplotlib/cycler.py | 47 ++++++++++++++++++++++++++++++++++------ 1 file changed, 40 insertions(+), 7 deletions(-) diff --git a/lib/matplotlib/cycler.py b/lib/matplotlib/cycler.py index bd434a3c22d9..9a9ecb0181c7 100644 --- a/lib/matplotlib/cycler.py +++ b/lib/matplotlib/cycler.py @@ -4,6 +4,27 @@ import six from itertools import product, cycle from six.moves import zip +import copy + + +def _process_keys(left, right): + """ + Helper function to compose cycler keys + + Parameters + ---------- + left, right : Cycler or None + The cyclers to be composed + Returns + ------- + keys : set + The keys in the composition of the two cyclers + """ + l_key = left.keys if left is not None else set() + r_key = right.keys if right is not None else set() + if l_key & r_key: + raise ValueError("Can not compose overlapping cycles") + return l_key | r_key class Cycler(object): @@ -27,14 +48,10 @@ class Cycler(object): """ def __init__(self, left, right=None, op=None): - self._left = left - self._right = right + self._keys = _process_keys(left, right) + self._left = copy.copy(left) + self._right = copy.copy(right) self._op = op - l_key = left.keys if left is not None else set() - r_key = right.keys if right is not None else set() - if l_key & r_key: - raise ValueError("Can not compose overlapping cycles") - self._keys = l_key | r_key @property def keys(self): @@ -101,6 +118,22 @@ def __mul__(self, other): def __len__(self): return len(list(self.finite_iter())) + def __iadd__(self, other): + old_self = copy.copy(self) + self._keys = _process_keys(old_self, other) + self._left = old_self + self._op = zip + self._right = copy.copy(other) + return self + + def __imul__(self, other): + old_self = copy.copy(self) + self._keys = _process_keys(old_self, other) + self._left = old_self + self._op = product + self._right = copy.copy(other) + return self + def cycler(label, itr): """ From 4aef726ffdc889de01af3d5cd22455aadad286fe Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Sun, 22 Mar 2015 00:21:13 -0400 Subject: [PATCH 06/24] ENH : added verbose repr --- lib/matplotlib/cycler.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/lib/matplotlib/cycler.py b/lib/matplotlib/cycler.py index 9a9ecb0181c7..f4dcbd2aa6b8 100644 --- a/lib/matplotlib/cycler.py +++ b/lib/matplotlib/cycler.py @@ -134,6 +134,17 @@ def __imul__(self, other): self._right = copy.copy(other) return self + def __repr__(self): + op_map = {zip: '+', product: '*'} + if self._right is None: + lab = list(self.keys)[0] + itr = list(v[lab] for v in self.finite_iter()) + return "cycler({lab!r}, {itr!r})".format(lab=lab, itr=itr) + else: + op = op_map.get(self._op, '?') + msg = "({left!r} {op} {right!r})" + return msg.format(left=self._left, op=op, right=self._right) + def cycler(label, itr): """ From 105fcd68d62623dcc340173ca7cf6ff63ebb5e38 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Sun, 22 Mar 2015 00:31:21 -0400 Subject: [PATCH 07/24] MNT : make keys property return a copy Don't open the chance for outside users to mess with contents of the `_keys` attribute. --- lib/matplotlib/cycler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/matplotlib/cycler.py b/lib/matplotlib/cycler.py index f4dcbd2aa6b8..59ece962c50d 100644 --- a/lib/matplotlib/cycler.py +++ b/lib/matplotlib/cycler.py @@ -55,7 +55,7 @@ def __init__(self, left, right=None, op=None): @property def keys(self): - return self._keys + return set(self._keys) def finite_iter(self): """ From a0c387acce99fcaa6c3108db88f130b6b2f77be8 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Sun, 22 Mar 2015 00:32:16 -0400 Subject: [PATCH 08/24] MNT : simplify repr --- lib/matplotlib/cycler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/matplotlib/cycler.py b/lib/matplotlib/cycler.py index 59ece962c50d..bcc0563b5b88 100644 --- a/lib/matplotlib/cycler.py +++ b/lib/matplotlib/cycler.py @@ -137,7 +137,7 @@ def __imul__(self, other): def __repr__(self): op_map = {zip: '+', product: '*'} if self._right is None: - lab = list(self.keys)[0] + lab = self.keys.pop() itr = list(v[lab] for v in self.finite_iter()) return "cycler({lab!r}, {itr!r})".format(lab=lab, itr=itr) else: From 23557765f33a831de44ae4d154e35a605b8add97 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Sun, 22 Mar 2015 00:32:30 -0400 Subject: [PATCH 09/24] ENH : make `cycler` deal with Cycler input - if multi-property cycler, raise - if already the Cycler we want, return copy - if cycler of a different property, extract the values and continue This behavior follows the pattern of numpy to copy in `np.array`. --- lib/matplotlib/cycler.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/lib/matplotlib/cycler.py b/lib/matplotlib/cycler.py index bcc0563b5b88..85602a957c00 100644 --- a/lib/matplotlib/cycler.py +++ b/lib/matplotlib/cycler.py @@ -164,6 +164,18 @@ def cycler(label, itr): cycler : Cycler New `Cycler` for the given property """ + if isinstance(itr, Cycler): + keys = itr.keys + if len(keys) != 1: + msg = "Can not create Cycler from a multi-property Cycler" + raise ValueError(msg) + + if label in keys: + return copy.copy(itr) + else: + lab = keys.pop() + itr = list(v[lab] for v in itr.finite_iter()) + return Cycler._from_iter(label, itr) From 74e61e1db1ee8c053aae328ae925501ee4934bb8 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Sun, 22 Mar 2015 23:17:03 -0400 Subject: [PATCH 10/24] MNT : remove second version of Cycler classes --- lib/matplotlib/cycler.py | 135 --------------------------------------- 1 file changed, 135 deletions(-) diff --git a/lib/matplotlib/cycler.py b/lib/matplotlib/cycler.py index 85602a957c00..8be7d0ef025f 100644 --- a/lib/matplotlib/cycler.py +++ b/lib/matplotlib/cycler.py @@ -177,138 +177,3 @@ def cycler(label, itr): itr = list(v[lab] for v in itr.finite_iter()) return Cycler._from_iter(label, itr) - - -class BaseCycler(object): - """ - Helper base-class to provide composition logic to - `SingleCycler` and `CompoundCycler`. - - This class - does not have a `__init__` method which will result - in a usable object. - """ - def __iter__(self): - return cycle(self.finite_iter()) - - def __add__(self, other): - return CompoundCycler(self, other, zip) - - def __mul__(self, other): - return CompoundCycler(self, other, product) - - -class SingleCycler(BaseCycler): - """ - Class to hold the cycle for a single parameter and handle the - composition. - - Parameters - ---------- - label : str - The name of the property this cycles over - itr : iterable - Finite length iterable of the property values. - """ - def __init__(self, label, itr): - self._itr = itr - self._label = label - - def finite_iter(self): - """ - Return a finite iterator over the configurations in - this cycle. - - Returns - ------- - gen : generator - A generator that yields dictionaries keyed on the property - name of the values to be used - - """ - return ({self._label: v} for v in self._itr) - - def __len__(self): - return len(self._itr) - - @property - def keys(self): - """ - The properties that this cycle loops over - """ - return set([self._label]) - - -class CompoundCycler(BaseCycler): - """ - A class to handle cycling multiple artist properties. - - This class has two compositions methods '+' for 'inner' - products of the cycles and '*' for outer products of the - cycles. - - This objects should not be created directly, but instead - result of composition of existing `SingleCycler` and - `CompoundCycler` objects. - - Parameters - ---------- - left : BaseCycler - The 'left' cycler - - right : BaseCycler - The 'right' cycler - - op : function - Function which composes the 'left' and 'right' cyclers. - - """ - def __init__(self, left, right, op): - self._left = left - self._right = right - self._op = op - l_key = left.keys - r_key = right.keys - if l_key & r_key: - raise ValueError("Can not compose overlapping cycles") - self._keys = l_key | r_key - - def finite_iter(self): - """ - Return a finite iterator over the configurations in - this cycle. - - Returns - ------- - gen : generator - A generator that yields dictionaries keyed on the property - name of the values to be used - """ - return self._compose() - - def _compose(self): - """ - Private function to handle the logic of merging the dictionaries - of the left and right cycles. - - Yields - ------ - ret : dict - A dictionary keyed on the property name of the values to be used - """ - for a, b in self._op(self._left.finite_iter(), - self._right.finite_iter()): - out = dict() - out.update(a) - out.update(b) - yield out - - def __len__(self): - return len(list(self.finite_iter())) - - @property - def keys(self): - """ - The properties that this cycle loops over - """ - return self._keys From e6910e4db632e9ff317f8b7bac85d8c78a0ec983 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Sun, 22 Mar 2015 23:34:04 -0400 Subject: [PATCH 11/24] ENH : add to_list method Return a list of the style dictionaries yielded by this Cycler --- lib/matplotlib/cycler.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/lib/matplotlib/cycler.py b/lib/matplotlib/cycler.py index 8be7d0ef025f..ffe9b77a0dea 100644 --- a/lib/matplotlib/cycler.py +++ b/lib/matplotlib/cycler.py @@ -69,6 +69,18 @@ def finite_iter(self): return iter(self._left) return self._compose() + def to_list(self): + """ + Return a list of the dictionaries yielded by + this Cycler. + + Returns + ------- + cycle : list + All of the dictionaries yielded by this Cycler in order. + """ + return list(self.finite_iter()) + def _compose(self): """ Compose the 'left' and 'right' components of this cycle From 1532863ae9176f71b7a1c51e9ff6ef00cba6642c Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Sun, 22 Mar 2015 23:40:19 -0400 Subject: [PATCH 12/24] PRF : make __len__ more efficient If we know the length of the left and right and the op, we can directly calculate what the length will be with out actually iterating over everything. --- lib/matplotlib/cycler.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/lib/matplotlib/cycler.py b/lib/matplotlib/cycler.py index ffe9b77a0dea..44386ef83157 100644 --- a/lib/matplotlib/cycler.py +++ b/lib/matplotlib/cycler.py @@ -4,6 +4,7 @@ import six from itertools import product, cycle from six.moves import zip +from operator import mul import copy @@ -128,7 +129,12 @@ def __mul__(self, other): return Cycler(self, other, product) def __len__(self): - return len(list(self.finite_iter())) + op_dict = {zip: min, product: mul} + if self._right is None: + return len(self._left) + l_len = len(self._left) + r_len = len(self._right) + return op_dict[self._op](l_len, r_len) def __iadd__(self, other): old_self = copy.copy(self) From d5ec6d8cef088b0e56a3e3face01f280ad14bd26 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Wed, 22 Apr 2015 00:39:46 -0400 Subject: [PATCH 13/24] TST : first round of tests for Cycler Covers everything but the repr Tests '+' behavior which may be changed. --- lib/matplotlib/tests/test_cycler.py | 83 +++++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 lib/matplotlib/tests/test_cycler.py diff --git a/lib/matplotlib/tests/test_cycler.py b/lib/matplotlib/tests/test_cycler.py new file mode 100644 index 000000000000..52e93c26ad80 --- /dev/null +++ b/lib/matplotlib/tests/test_cycler.py @@ -0,0 +1,83 @@ +from __future__ import (absolute_import, division, print_function, + unicode_literals) + +import six +from six.moves import zip +from matplotlib.cycler import cycler +from nose.tools import assert_equal, assert_raises +from itertools import product +from operator import add, iadd, mul, imul + + +def _cycler_helper(c, length, keys, values): + assert_equal(len(c), length) + assert_equal(len(c), len(list(c.finite_iter()))) + assert_equal(len(c), len(c.to_list())) + assert_equal(c.keys, set(keys)) + + for k, vals in zip(keys, values): + for v, v_target in zip(c, vals): + assert_equal(v[k], v_target) + + +def test_creation(): + c = cycler('c', 'rgb') + yield _cycler_helper, c, 3, ['c'], [['r', 'g', 'b']] + c = cycler('c', list('rgb')) + yield _cycler_helper, c, 3, ['c'], [['r', 'g', 'b']] + + +def test_compose(): + c1 = cycler('c', 'rgb') + c2 = cycler('lw', range(3)) + c3 = cycler('lw', range(15)) + # addition + yield _cycler_helper, c1+c2, 3, ['c', 'lw'], [list('rgb'), range(3)] + yield _cycler_helper, c2+c1, 3, ['c', 'lw'], [list('rgb'), range(3)] + # miss-matched add lengths + yield _cycler_helper, c1+c3, 3, ['c', 'lw'], [list('rgb'), range(3)] + yield _cycler_helper, c3+c1, 3, ['c', 'lw'], [list('rgb'), range(3)] + + # multiplication + target = zip(*product(list('rgb'), range(3))) + yield (_cycler_helper, c1 * c2, 9, ['c', 'lw'], target) + + target = zip(*product(range(3), list('rgb'))) + yield (_cycler_helper, c2 * c1, 9, ['lw', 'c'], target) + + target = zip(*product(range(15), list('rgb'))) + yield (_cycler_helper, c3 * c1, 45, ['lw', 'c'], target) + + +def test_inplace(): + c1 = cycler('c', 'rgb') + c2 = cycler('lw', range(3)) + c2 += c1 + yield _cycler_helper, c2, 3, ['c', 'lw'], [list('rgb'), range(3)] + + c3 = cycler('c', 'rgb') + c4 = cycler('lw', range(3)) + c3 *= c4 + target = zip(*product(list('rgb'), range(3))) + yield (_cycler_helper, c3, 9, ['c', 'lw'], target) + + +def test_constructor(): + c1 = cycler('c', 'rgb') + c2 = cycler('ec', c1) + yield _cycler_helper, c1+c2, 3, ['c', 'ec'], [['r', 'g', 'b']]*2 + c3 = cycler('c', c1) + yield _cycler_helper, c3+c2, 3, ['c', 'ec'], [['r', 'g', 'b']]*2 + + +def test_failures(): + c1 = cycler('c', 'rgb') + c2 = cycler('c', c1) + assert_raises(ValueError, add, c1, c2) + assert_raises(ValueError, iadd, c1, c2) + assert_raises(ValueError, mul, c1, c2) + assert_raises(ValueError, imul, c1, c2) + + c3 = cycler('ec', c1) + + assert_raises(ValueError, cycler, 'c', c2 + c3) From 0da274f6e5e792f57228c3fa9b101d59d47eca13 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Sun, 26 Apr 2015 21:54:30 -0400 Subject: [PATCH 14/24] API : make __iter__ be finite It is far simpler for users that want infinite cycling to wrap the ``Cycler`` in a call to `itertools.cycle` than it is for the user that wants a finite iterator to sort out how to get the finite version. This simplifies the API and makes it a bit more guessable/intuitive. --- lib/matplotlib/cycler.py | 38 ++++++----------------------- lib/matplotlib/tests/test_cycler.py | 3 +-- 2 files changed, 9 insertions(+), 32 deletions(-) diff --git a/lib/matplotlib/cycler.py b/lib/matplotlib/cycler.py index 44386ef83157..9c65b474cb67 100644 --- a/lib/matplotlib/cycler.py +++ b/lib/matplotlib/cycler.py @@ -2,7 +2,7 @@ unicode_literals) import six -from itertools import product, cycle +from itertools import product from six.moves import zip from operator import mul import copy @@ -58,37 +58,12 @@ def __init__(self, left, right=None, op=None): def keys(self): return set(self._keys) - def finite_iter(self): - """ - Return a finite iterator over the configurations in - this cycle. - """ - if self._right is None: - try: - return self._left.finite_iter() - except AttributeError: - return iter(self._left) - return self._compose() - - def to_list(self): - """ - Return a list of the dictionaries yielded by - this Cycler. - - Returns - ------- - cycle : list - All of the dictionaries yielded by this Cycler in order. - """ - return list(self.finite_iter()) - def _compose(self): """ Compose the 'left' and 'right' components of this cycle with the proper operation (zip or product as of now) """ - for a, b in self._op(self._left.finite_iter(), - self._right.finite_iter()): + for a, b in self._op(self._left, self._right): out = dict() out.update(a) out.update(b) @@ -120,7 +95,10 @@ def _from_iter(cls, label, itr): return ret def __iter__(self): - return cycle(self.finite_iter()) + if self._right is None: + return iter(self._left) + + return self._compose() def __add__(self, other): return Cycler(self, other, zip) @@ -156,7 +134,7 @@ def __repr__(self): op_map = {zip: '+', product: '*'} if self._right is None: lab = self.keys.pop() - itr = list(v[lab] for v in self.finite_iter()) + itr = list(v[lab] for v in self) return "cycler({lab!r}, {itr!r})".format(lab=lab, itr=itr) else: op = op_map.get(self._op, '?') @@ -192,6 +170,6 @@ def cycler(label, itr): return copy.copy(itr) else: lab = keys.pop() - itr = list(v[lab] for v in itr.finite_iter()) + itr = list(v[lab] for v in itr) return Cycler._from_iter(label, itr) diff --git a/lib/matplotlib/tests/test_cycler.py b/lib/matplotlib/tests/test_cycler.py index 52e93c26ad80..c6325b2bba89 100644 --- a/lib/matplotlib/tests/test_cycler.py +++ b/lib/matplotlib/tests/test_cycler.py @@ -11,8 +11,7 @@ def _cycler_helper(c, length, keys, values): assert_equal(len(c), length) - assert_equal(len(c), len(list(c.finite_iter()))) - assert_equal(len(c), len(c.to_list())) + assert_equal(len(c), len(list(c))) assert_equal(c.keys, set(keys)) for k, vals in zip(keys, values): From 52b5dfd01f5c6a7533bb59d4d529d94369b1a3b0 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Mon, 27 Apr 2015 00:49:58 -0400 Subject: [PATCH 15/24] DOC/WIP : first draft a docs for cycler --- doc/conf.py | 1 + doc/users/cycler.rst | 154 +++++++++++++++++++++++++++++++++++++++++++ doc/users/index.rst | 5 +- 3 files changed, 156 insertions(+), 4 deletions(-) create mode 100644 doc/users/cycler.rst diff --git a/doc/conf.py b/doc/conf.py index 571def2e02c1..366c894ab9df 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -43,6 +43,7 @@ extensions.append('matplotlib.sphinxext.ipython_console_highlighting') else: print("Using IPython's ipython_console_highlighting directive") + extensions.append('IPython.sphinxext.ipython_directive') extensions.append('IPython.sphinxext.ipython_console_highlighting') try: diff --git a/doc/users/cycler.rst b/doc/users/cycler.rst new file mode 100644 index 000000000000..5722bbd35de1 --- /dev/null +++ b/doc/users/cycler.rst @@ -0,0 +1,154 @@ +.. _cycler_guide: + +========================== + Style/kwarg cycler Guide +========================== + +.. currentmodule:: matplotlib.cycler + +When plotting more than one line it is common to want to be able to cycle over one +or more artist styles. For simple cases than can be done with out too much trouble: + +.. plot:: + :include-source: + + fig, ax = plt.subplots(tight_layout=True) + x = np.linspace(0, 2*np.pi, 1024) + + for i, (lw, c) in enumerate(zip(range(4), ['r', 'g', 'b', 'k'])): + ax.plot(x, np.sin(x - i * np.pi / 4), + label=r'$\phi = {{{0}}} \pi / 4$'.format(i), + lw=lw + 1, + c=c) + + ax.set_xlim([0, 2*np.pi]) + ax.set_title(r'$y=\sin(\theta + \phi)$') + ax.set_ylabel(r'[arb]') + ax.set_xlabel(r'$\theta$ [rad]') + + ax.legend(loc=0) + +However, if you want to do something more complicated: + +.. plot:: + :include-source: + + fig, ax = plt.subplots(tight_layout=True) + x = np.linspace(0, 2*np.pi, 1024) + + for i, (lw, c) in enumerate(zip(range(4), ['r', 'g', 'b', 'k'])): + if i % 2: + ls = '-' + else: + ls = '--' + ax.plot(x, np.sin(x - i * np.pi / 4), + label=r'$\phi = {{{0}}} \pi / 4$'.format(i), + lw=lw + 1, + c=c, + ls=ls) + + ax.set_xlim([0, 2*np.pi]) + ax.set_title(r'$y=\sin(\theta + \phi)$') + ax.set_ylabel(r'[arb]') + ax.set_xlabel(r'$\theta$ [rad]') + + ax.legend(loc=0) + +the plotting logic can quickly become very involved. To address this and allow easy +cycling over arbitrary ``kwargs`` the `~matplotlib.cycler.Cycler` class, a composable +kwarg iterator, was developed. + + +`~matplotlib.cycler.Cycler` +=========================== + +The public API of `Cycler` consists of a class +`~matplotlib.cycler.Cycler` and a factory function +`~matplotlib.cycler.cycler`. The class takes care of the composition and iteration logic while +the function provides a simple interface for creating 'base' `Cycler` objects. + +.. autosummary:: + :toctree: generated/ + + Cycler + cycler + + +A 'base' `Cycler` object is some what useful + +.. ipython:: python + + from matplotlib.cycler import cycler + + + single_cycle = cycler('c', ['r', 'g', 'b']) + + print(single_cycle) + + for v in single_cycle: + print(v) + + len(single_cycle) + + +.. plot:: + :include-source: + + from matplotlib.cycler import cycler + from itertools import cycle + + fig, (ax1, ax2) = plt.subplots(1, 2, tight_layout=True, figsize=(8, 4)) + x = np.arange(10) + + single_cycle = cycler('c', ['r', 'g', 'b']) + + for i, sty in enumerate(single_cycle): + ax1.plot(x, x*(i+1), **sty) + + + for i, sty in zip(range(1, 10), cycle(single_cycle)): + ax2.plot(x, x*i, **sty) + +However they are most useful when composed. They can be added + +.. ipython:: python + + from __future__ import print_function + from matplotlib.cycler import cycler + + color_cycle = cycler('c', ['r', 'g', 'b']) + lw_cycle = cycler('lw', range(1, 5)) + add_cycle = color_cycle + lw_cycle + + print(color_cycle) + print(lw_cycle) + print('added cycle: ', add_cycle) + + print('len A: {}, len B: {}, len A + B: {}'.format(len(color_cycle), len(lw_cycle), len(add_cycle))) + + for v in add_cycle: + print(v) + +or multiplied + +.. ipython:: python + + from __future__ import print_function + from matplotlib.cycler import cycler + + color_cycle = cycler('c', ['r', 'g', 'b']) + lw_cycle = cycler('lw', range(1, 5)) + + prod_cycle = color_cycle * lw_cycle + + print(color_cycle) + print(lw_cycle) + print('multiplied cycle: ', prod_cycle) + + print('len A: {}, len B: {}, len A * B: {}'.format(len(color_cycle), len(lw_cycle), len(prod_cycle))) + + for v in prod_cycle: + print(v) + + +stuff diff --git a/doc/users/index.rst b/doc/users/index.rst index eca6241e139e..141c8741d2a2 100644 --- a/doc/users/index.rst +++ b/doc/users/index.rst @@ -14,13 +14,10 @@ User's Guide intro.rst configuration.rst + cycler.rst beginner.rst developer.rst whats_new.rst github_stats.rst license.rst credits.rst - - - - From d4195f75f109758f80fb96a19b8b90b1101a95bd Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Mon, 4 May 2015 23:38:46 -0400 Subject: [PATCH 16/24] WIP : more edits to docs --- doc/users/cycler.rst | 79 ++++++++++++++++++++++++++------------------ 1 file changed, 46 insertions(+), 33 deletions(-) diff --git a/doc/users/cycler.rst b/doc/users/cycler.rst index 5722bbd35de1..a3b9ba511989 100644 --- a/doc/users/cycler.rst +++ b/doc/users/cycler.rst @@ -76,21 +76,6 @@ the function provides a simple interface for creating 'base' `Cycler` objects. A 'base' `Cycler` object is some what useful -.. ipython:: python - - from matplotlib.cycler import cycler - - - single_cycle = cycler('c', ['r', 'g', 'b']) - - print(single_cycle) - - for v in single_cycle: - print(v) - - len(single_cycle) - - .. plot:: :include-source: @@ -109,46 +94,74 @@ A 'base' `Cycler` object is some what useful for i, sty in zip(range(1, 10), cycle(single_cycle)): ax2.plot(x, x*i, **sty) -However they are most useful when composed. They can be added .. ipython:: python from __future__ import print_function from matplotlib.cycler import cycler + color_cycle = cycler('c', ['r', 'g', 'b']) + + color_cycle + + for v in color_cycle: + print(v) + + len(color_cycle) + + + +However they are most useful when composed. They can be added + +.. ipython:: python + lw_cycle = cycler('lw', range(1, 5)) add_cycle = color_cycle + lw_cycle - print(color_cycle) - print(lw_cycle) - print('added cycle: ', add_cycle) - - print('len A: {}, len B: {}, len A + B: {}'.format(len(color_cycle), len(lw_cycle), len(add_cycle))) + lw_cycle + add_cycle for v in add_cycle: print(v) + len(add_cycle) + or multiplied .. ipython:: python - from __future__ import print_function - from matplotlib.cycler import cycler - - color_cycle = cycler('c', ['r', 'g', 'b']) - lw_cycle = cycler('lw', range(1, 5)) - prod_cycle = color_cycle * lw_cycle - print(color_cycle) - print(lw_cycle) - print('multiplied cycle: ', prod_cycle) - - print('len A: {}, len B: {}, len A * B: {}'.format(len(color_cycle), len(lw_cycle), len(prod_cycle))) + color_cycle + lw_cycle + prod_cycle for v in prod_cycle: print(v) + len(prod_cycle) + +The result of composition is another `Cycler` object which allows very +complicated cycles to be defined very succinctly -stuff +.. ipython:: python + + +.. plot:: + :include-source: + + from matplotlib.cycler import cycler + from itertools import cycle + + fig, (ax1, ax2) = plt.subplots(1, 2, tight_layout=True, figsize=(8, 4)) + x = np.arange(10) + + single_cycle = cycler('c', ['r', 'g', 'b']) + + for i, sty in enumerate(single_cycle): + ax1.plot(x, x*(i+1), **sty) + + + for i, sty in zip(range(1, 10), cycle(single_cycle)): + ax2.plot(x, x*i, **sty) From 3e8bd8e832c452cd28ecee97bf2bd0b0af7297c8 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Tue, 5 May 2015 22:20:21 -0400 Subject: [PATCH 17/24] ENH : add simplify Add a method to return an equivalent Cycler composed using only addition composition. --- lib/matplotlib/cycler.py | 47 +++++++++++++++++++++++++++-- lib/matplotlib/tests/test_cycler.py | 13 ++++++++ 2 files changed, 58 insertions(+), 2 deletions(-) diff --git a/lib/matplotlib/cycler.py b/lib/matplotlib/cycler.py index 9c65b474cb67..3d21192a20ef 100644 --- a/lib/matplotlib/cycler.py +++ b/lib/matplotlib/cycler.py @@ -3,8 +3,8 @@ import six from itertools import product -from six.moves import zip -from operator import mul +from six.moves import zip, reduce +from operator import mul, add import copy @@ -141,6 +141,49 @@ def __repr__(self): msg = "({left!r} {op} {right!r})" return msg.format(left=self._left, op=op, right=self._right) + def _transpose(self): + """ + Internal helper function which iterates through the + styles and returns a dict of lists instead of a list of + dicts. This is needed for multiplying by integers and + for __getitem__ + + Returns + ------- + trans : dict + dict of lists for the styles + """ + + # TODO : sort out if this is a bottle neck, if there is a better way + # and if we care. + + keys = self.keys + out = {k: list() for k in keys} + + for d in self: + for k in keys: + out[k].append(d[k]) + return out + + def simplify(self): + """ + Simplify the Cycler and return as a composition only + sums (no multiplications) + + Returns + ------- + simple : Cycler + An equivalent cycler using only summation + """ + # TODO: sort out if it is worth the effort to make sure this is + # balanced. Currently it is is + # (((a + b) + c) + d) vs + # ((a + b) + (c + d)) + # I would believe that there is some performance implications + + trans = self._transpose() + return reduce(add, (cycler(k, v) for k, v in six.iteritems(trans))) + def cycler(label, itr): """ diff --git a/lib/matplotlib/tests/test_cycler.py b/lib/matplotlib/tests/test_cycler.py index c6325b2bba89..26b416466317 100644 --- a/lib/matplotlib/tests/test_cycler.py +++ b/lib/matplotlib/tests/test_cycler.py @@ -19,6 +19,10 @@ def _cycler_helper(c, length, keys, values): assert_equal(v[k], v_target) +def _cycles_equal(c1, c2): + assert_equal(list(c1), list(c2)) + + def test_creation(): c = cycler('c', 'rgb') yield _cycler_helper, c, 3, ['c'], [['r', 'g', 'b']] @@ -80,3 +84,12 @@ def test_failures(): c3 = cycler('ec', c1) assert_raises(ValueError, cycler, 'c', c2 + c3) + + +def test_simplify(): + c1 = cycler('c', 'rgb') + c2 = cycler('ec', c1) + c3 = c1 * c2 + c4 = c1 + c2 + yield _cycles_equal, c3, c3.simplify() + yield _cycles_equal, c4, c4.simplify() From 6fea7a46446327c6de8ba551cfc2b2d3b42ae5df Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Tue, 5 May 2015 22:54:54 -0400 Subject: [PATCH 18/24] ENH : add integer multiplication Add the ability to multiply a Cycler by an integer to increase the length of the cycle. This has the side effect of simplifying the Cycler to only use addition composition --- lib/matplotlib/cycler.py | 12 +++++++++++- lib/matplotlib/tests/test_cycler.py | 21 ++++++++++++++++++--- 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/lib/matplotlib/cycler.py b/lib/matplotlib/cycler.py index 3d21192a20ef..2ce1473fe6e0 100644 --- a/lib/matplotlib/cycler.py +++ b/lib/matplotlib/cycler.py @@ -104,7 +104,17 @@ def __add__(self, other): return Cycler(self, other, zip) def __mul__(self, other): - return Cycler(self, other, product) + if isinstance(other, Cycler): + return Cycler(self, other, product) + elif isinstance(other, int): + trans = self._transpose() + return reduce(add, (cycler(k, v*other) + for k, v in six.iteritems(trans))) + else: + return NotImplemented + + def __rmul__(self, other): + return self * other def __len__(self): op_dict = {zip: min, product: mul} diff --git a/lib/matplotlib/tests/test_cycler.py b/lib/matplotlib/tests/test_cycler.py index 26b416466317..03a05b6e408a 100644 --- a/lib/matplotlib/tests/test_cycler.py +++ b/lib/matplotlib/tests/test_cycler.py @@ -88,8 +88,23 @@ def test_failures(): def test_simplify(): c1 = cycler('c', 'rgb') + c2 = cycler('ec', c1) + for c in [c1 * c2, c2 * c1, c1 + c2]: + yield _cycles_equal, c, c.simplify() + + +def test_multiply(): + c1 = cycler('c', 'rgb') + yield _cycler_helper, 2*c1, 6, ['c'], ['rgb'*2] + c2 = cycler('ec', c1) c3 = c1 * c2 - c4 = c1 + c2 - yield _cycles_equal, c3, c3.simplify() - yield _cycles_equal, c4, c4.simplify() + + yield _cycles_equal, 2*c3, c3*2 + + +def test_mul_fails(): + c1 = cycler('c', 'rgb') + assert_raises(TypeError, mul, c1, 2.0) + assert_raises(TypeError, mul, c1, 'a') + assert_raises(TypeError, mul, c1, []) From 2ffc5e1e7b308287fd7f7b13505324b2d1da020e Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Tue, 5 May 2015 22:59:35 -0400 Subject: [PATCH 19/24] TST : test commutativity of cycler addition --- lib/matplotlib/tests/test_cycler.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/matplotlib/tests/test_cycler.py b/lib/matplotlib/tests/test_cycler.py index 03a05b6e408a..56399c0dfda8 100644 --- a/lib/matplotlib/tests/test_cycler.py +++ b/lib/matplotlib/tests/test_cycler.py @@ -37,9 +37,11 @@ def test_compose(): # addition yield _cycler_helper, c1+c2, 3, ['c', 'lw'], [list('rgb'), range(3)] yield _cycler_helper, c2+c1, 3, ['c', 'lw'], [list('rgb'), range(3)] + yield _cycles_equal, c2+c1, c1+c2 # miss-matched add lengths yield _cycler_helper, c1+c3, 3, ['c', 'lw'], [list('rgb'), range(3)] yield _cycler_helper, c3+c1, 3, ['c', 'lw'], [list('rgb'), range(3)] + yield _cycles_equal, c3+c1, c1+c3 # multiplication target = zip(*product(list('rgb'), range(3))) From 126d7df45a5cde9cdff8c04c6665b956d3ab683d Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Tue, 5 May 2015 23:23:29 -0400 Subject: [PATCH 20/24] ENH : implement `__getitem__` for Cycler Implement `__getitem__` for Cycler by broadcasting down to the lists in the transposed data. Raises on trying to use anything else but a slice. Numpy-style fancy slicing might be worth adding. Adding integer slicing is probably of minilmal value. --- lib/matplotlib/cycler.py | 9 +++++++++ lib/matplotlib/tests/test_cycler.py | 17 ++++++++++++++++- 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/lib/matplotlib/cycler.py b/lib/matplotlib/cycler.py index 2ce1473fe6e0..ebd2322d7641 100644 --- a/lib/matplotlib/cycler.py +++ b/lib/matplotlib/cycler.py @@ -94,6 +94,15 @@ def _from_iter(cls, label, itr): ret._keys = set([label]) return ret + def __getitem__(self, key): + # TODO : maybe add numpy style fancy slicing + if isinstance(key, slice): + trans = self._transpose() + return reduce(add, (cycler(k, v[key]) + for k, v in six.iteritems(trans))) + else: + raise ValueError("Can only use slices with Cycler.__getitem__") + def __iter__(self): if self._right is None: return iter(self._left) diff --git a/lib/matplotlib/tests/test_cycler.py b/lib/matplotlib/tests/test_cycler.py index 56399c0dfda8..0c1b07d0d735 100644 --- a/lib/matplotlib/tests/test_cycler.py +++ b/lib/matplotlib/tests/test_cycler.py @@ -3,7 +3,7 @@ import six from six.moves import zip -from matplotlib.cycler import cycler +from matplotlib.cycler import cycler, Cycler from nose.tools import assert_equal, assert_raises from itertools import product from operator import add, iadd, mul, imul @@ -110,3 +110,18 @@ def test_mul_fails(): assert_raises(TypeError, mul, c1, 2.0) assert_raises(TypeError, mul, c1, 'a') assert_raises(TypeError, mul, c1, []) + + +def test_getitem(): + c1 = cycler('lw', range(15)) + for slc in (slice(None, None, None), + slice(None, None, -1), + slice(1, 5, None), + slice(0, 5, 2)): + yield _cycles_equal, c1[slc], cycler('lw', range(15)[slc]) + + +def test_fail_getime(): + c1 = cycler('lw', range(15)) + assert_raises(ValueError, Cycler.__getitem__, c1, 0) + assert_raises(ValueError, Cycler.__getitem__, c1, [0, 1]) From a56ea63d1bec846cd4b7ad9b3f1d6fe1ae216a98 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Tue, 5 May 2015 23:37:14 -0400 Subject: [PATCH 21/24] API : Only allow addition of equal length cycles Now that we have both multiplication and slicing, it is easy to get correct length cyclers to add together --- lib/matplotlib/cycler.py | 3 +++ lib/matplotlib/tests/test_cycler.py | 5 ++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/lib/matplotlib/cycler.py b/lib/matplotlib/cycler.py index ebd2322d7641..dfcae2a2af86 100644 --- a/lib/matplotlib/cycler.py +++ b/lib/matplotlib/cycler.py @@ -110,6 +110,9 @@ def __iter__(self): return self._compose() def __add__(self, other): + if len(self) != len(other): + raise ValueError("Can only add equal length cycles, " + "not {0} and {1}".format(len(self), len(other))) return Cycler(self, other, zip) def __mul__(self, other): diff --git a/lib/matplotlib/tests/test_cycler.py b/lib/matplotlib/tests/test_cycler.py index 0c1b07d0d735..39bb244fae6e 100644 --- a/lib/matplotlib/tests/test_cycler.py +++ b/lib/matplotlib/tests/test_cycler.py @@ -39,9 +39,8 @@ def test_compose(): yield _cycler_helper, c2+c1, 3, ['c', 'lw'], [list('rgb'), range(3)] yield _cycles_equal, c2+c1, c1+c2 # miss-matched add lengths - yield _cycler_helper, c1+c3, 3, ['c', 'lw'], [list('rgb'), range(3)] - yield _cycler_helper, c3+c1, 3, ['c', 'lw'], [list('rgb'), range(3)] - yield _cycles_equal, c3+c1, c1+c3 + assert_raises(ValueError, add, c1, c3) + assert_raises(ValueError, add, c3, c1) # multiplication target = zip(*product(list('rgb'), range(3))) From 58610f49bea768e9ec1aca42e857247591c9a6ca Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Tue, 5 May 2015 23:38:42 -0400 Subject: [PATCH 22/24] DOC : more edits to docs --- doc/users/cycler.rst | 64 ++++++++++++++++++++++++++++---------------- 1 file changed, 41 insertions(+), 23 deletions(-) diff --git a/doc/users/cycler.rst b/doc/users/cycler.rst index a3b9ba511989..8bfe85f2e9ee 100644 --- a/doc/users/cycler.rst +++ b/doc/users/cycler.rst @@ -1,10 +1,30 @@ .. _cycler_guide: +.. currentmodule:: matplotlib.cycler ========================== Style/kwarg cycler Guide ========================== -.. currentmodule:: matplotlib.cycler +`~matplotlib.cycler.Cycler` API +=============================== + +.. autosummary:: + :toctree: generated/ + + cycler + Cycler + + +The public API of `Cycler` consists of a class +`~matplotlib.cycler.Cycler` and a factory function +`~matplotlib.cycler.cycler`. The class takes care of the composition +and iteration logic while the function provides a simple interface for +creating 'base' `Cycler` objects. + + +Motivation +========== + When plotting more than one line it is common to want to be able to cycle over one or more artist styles. For simple cases than can be done with out too much trouble: @@ -58,23 +78,11 @@ the plotting logic can quickly become very involved. To address this and allow cycling over arbitrary ``kwargs`` the `~matplotlib.cycler.Cycler` class, a composable kwarg iterator, was developed. +`Cycler` Usage +============== -`~matplotlib.cycler.Cycler` -=========================== - -The public API of `Cycler` consists of a class -`~matplotlib.cycler.Cycler` and a factory function -`~matplotlib.cycler.cycler`. The class takes care of the composition and iteration logic while -the function provides a simple interface for creating 'base' `Cycler` objects. - -.. autosummary:: - :toctree: generated/ - - Cycler - cycler - - -A 'base' `Cycler` object is some what useful +A 'base' `Cycler` object is some what useful and can be used to easily +cycle over a single style .. plot:: :include-source: @@ -85,13 +93,13 @@ A 'base' `Cycler` object is some what useful fig, (ax1, ax2) = plt.subplots(1, 2, tight_layout=True, figsize=(8, 4)) x = np.arange(10) - single_cycle = cycler('c', ['r', 'g', 'b']) + color_cycle = cycler('c', ['r', 'g', 'b']) - for i, sty in enumerate(single_cycle): + for i, sty in enumerate(color_cycle): ax1.plot(x, x*(i+1), **sty) - for i, sty in zip(range(1, 10), cycle(single_cycle)): + for i, sty in zip(range(1, 10), cycle(color_cycle)): ax2.plot(x, x*i, **sty) @@ -111,6 +119,16 @@ A 'base' `Cycler` object is some what useful len(color_cycle) + fig, (ax1, ax2) = plt.subplots(1, 2, tight_layout=True, figsize=(8, 4)) + x = np.arange(10) + + + for i, sty in enumerate(color_cycle): + ax1.plot(x, x*(i+1), **sty) + + + + However they are most useful when composed. They can be added @@ -157,11 +175,11 @@ complicated cycles to be defined very succinctly fig, (ax1, ax2) = plt.subplots(1, 2, tight_layout=True, figsize=(8, 4)) x = np.arange(10) - single_cycle = cycler('c', ['r', 'g', 'b']) + color_cycle = cycler('c', ['r', 'g', 'b']) - for i, sty in enumerate(single_cycle): + for i, sty in enumerate(color_cycle): ax1.plot(x, x*(i+1), **sty) - for i, sty in zip(range(1, 10), cycle(single_cycle)): + for i, sty in zip(range(1, 10), cycle(color_cycle)): ax2.plot(x, x*i, **sty) From c76b976330dec0f09a393303432baf0f521a6d0f Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Wed, 6 May 2015 21:18:17 -0400 Subject: [PATCH 23/24] DOC : more work on documentation --- doc/users/cycler.rst | 182 +++++++++++++++++++++++++++++++++---------- 1 file changed, 141 insertions(+), 41 deletions(-) diff --git a/doc/users/cycler.rst b/doc/users/cycler.rst index 8bfe85f2e9ee..8dd761884fbf 100644 --- a/doc/users/cycler.rst +++ b/doc/users/cycler.rst @@ -81,90 +81,151 @@ kwarg iterator, was developed. `Cycler` Usage ============== -A 'base' `Cycler` object is some what useful and can be used to easily -cycle over a single style +Basic +----- -.. plot:: - :include-source: +A 'base' `Cycler` object is somewhat useful and can be used to easily +cycle over a single style. To create a base `Cycler` use the `cycler` +function to link a key/style/kwarg to series of values. The key can be +any hashable object (as it will eventually be used as the key in a `dict`). + +.. ipython:: python + from __future__ import print_function from matplotlib.cycler import cycler - from itertools import cycle - fig, (ax1, ax2) = plt.subplots(1, 2, tight_layout=True, figsize=(8, 4)) - x = np.arange(10) - color_cycle = cycler('c', ['r', 'g', 'b']) + color_cycle = cycler('color', ['r', 'g', 'b']) + color_cycle - for i, sty in enumerate(color_cycle): - ax1.plot(x, x*(i+1), **sty) +The `Cycler` object knows it's length and keys: +.. ipython:: python - for i, sty in zip(range(1, 10), cycle(color_cycle)): - ax2.plot(x, x*i, **sty) + len(color_cycle) + color_cycle.keys + +Iterating over this object will yield a series of `dicts` keyed on +the key with a single value from the series .. ipython:: python - from __future__ import print_function - from matplotlib.cycler import cycler + for v in color_cycle: + print(v) +Basic `Cycler` objects can be passed as the second argument to `cycler` +which is copy cyclers to a new key. - color_cycle = cycler('c', ['r', 'g', 'b']) +.. ipython:: python - color_cycle + cycler('ec', color_cycle) - for v in color_cycle: - print(v) - len(color_cycle) +Composition +----------- +A single `Cycler` is not all that useful, they can just as easily be +replaced by a single `for` loop. Fortunately, `Cycler` objects can be +composed to easily generate complex, multi-key cycles. - fig, (ax1, ax2) = plt.subplots(1, 2, tight_layout=True, figsize=(8, 4)) - x = np.arange(10) +Addition +~~~~~~~~ +Equal length `Cycler` s with different keys can be added to get the +'inner' product of two cycles - for i, sty in enumerate(color_cycle): - ax1.plot(x, x*(i+1), **sty) +.. ipython:: python + + lw_cycle = cycler('lw', range(1, 4)) + + wc = lw_cycle + color_cycle +The result has the same length and has keys which are the union of the +two input `Cycler` s. +.. ipython:: python + + len(wc) + wc.keys + +and iterating over the result is the zip of the two input cycles +.. ipython:: python + for s in wc: + print(s) -However they are most useful when composed. They can be added +As with arithmetic, addition is commutative .. ipython:: python - lw_cycle = cycler('lw', range(1, 5)) - add_cycle = color_cycle + lw_cycle + for a, b in zip(lw_cycle + color_cycle, color_cycle + lw_cycle): + print(a == b) - lw_cycle - add_cycle - for v in add_cycle: - print(v) +Multiplication +~~~~~~~~~~~~~~ - len(add_cycle) +Any pair of `Cycler` can be multiplied -or multiplied +.. ipython:: python + + m_cycle = cycler('marker', ['s', 'o']) + + m_c = m_cycle * color_cycle + +which gives the 'outer product' of the two cycles (same as +:func:`itertools.prod` ) .. ipython:: python - prod_cycle = color_cycle * lw_cycle + len(m_c) + m_c.keys + for s in m_c: + print(s) + +Note that unlike addition, multiplication is not commutative (like +matrices) + +.. ipython:: python + + c_m = color_cycle * m_cycle + for a, b in zip(c_m, m_c): + print(a, b) + + + + +Integer Multiplication +~~~~~~~~~~~~~~~~~~~~~~ + +`Cycler` s can also be multiplied by integer values to increase the length. + +.. ipython:: python + + color_cycle * 2 + 2 * color_cycle - color_cycle - lw_cycle - prod_cycle - for v in prod_cycle: - print(v) - len(prod_cycle) +Slicing +------- -The result of composition is another `Cycler` object which allows very -complicated cycles to be defined very succinctly +Cycles can be sliced with `silce` objects .. ipython:: python + color_cycle[::-1] + color_cycle[:2] + color_cycle[1:] + +to return a sub-set of the cycle as a new `Cycler`. They can also be multiplied +by scalars to make fixed length periodic cycles + +Examples +-------- + .. plot:: :include-source: @@ -183,3 +244,42 @@ complicated cycles to be defined very succinctly for i, sty in zip(range(1, 10), cycle(color_cycle)): ax2.plot(x, x*i, **sty) + + +.. plot:: + :include-source: + + from matplotlib.cycler import cycler + from itertools import cycle + + fig, (ax1, ax2) = plt.subplots(1, 2, tight_layout=True, figsize=(8, 4)) + x = np.arange(10) + + color_cycle = cycler('c', ['r', 'g', 'b']) + + for i, sty in enumerate(color_cycle): + ax1.plot(x, x*(i+1), **sty) + + + for i, sty in zip(range(1, 10), cycle(color_cycle)): + ax2.plot(x, x*i, **sty) + + +Exceptions +---------- + + +A `ValueError` is raised if unequal length `Cycler` s are added together + +.. ipython:: python + :okexcept: + + color_cycle + ls_cycle + +or if two cycles which have overlapping keys are composed + +.. ipython:: python + :okexcept: + + color_cycle + color_cycle + color_cycle * color_cycle From 14df70f14eccfac02473ee6d52a989fc7f120320 Mon Sep 17 00:00:00 2001 From: danielballan Date: Thu, 7 May 2015 12:03:28 -0400 Subject: [PATCH 24/24] ENH: Add a rich display hook to Cycler. --- lib/matplotlib/cycler.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/lib/matplotlib/cycler.py b/lib/matplotlib/cycler.py index dfcae2a2af86..1af3c3e6a142 100644 --- a/lib/matplotlib/cycler.py +++ b/lib/matplotlib/cycler.py @@ -163,6 +163,19 @@ def __repr__(self): msg = "({left!r} {op} {right!r})" return msg.format(left=self._left, op=op, right=self._right) + def _repr_html_(self): + # an table showing the value of each key through a full cycle + output = "" + for key in self.keys: + output += "".format(key=key) + for d in iter(self): + output += "" + for val in d.values(): + output += "".format(val=val) + output += "" + output += "
{key!r}
{val!r}
" + return output + def _transpose(self): """ Internal helper function which iterates through the