.. currentmodule:: cycler =================== Composable cycles =================== .. only:: html :Version: |version| :Date: |today| ====== ==================================== Docs https://matplotlib.org/cycler PyPI https://pypi.python.org/pypi/Cycler GitHub https://github.com/matplotlib/cycler ====== ==================================== :py:mod:`cycler` API ==================== .. autosummary:: :toctree: generated/ cycler Cycler concat The public API of :py:mod:`cycler` consists of a class `Cycler`, a factory function :func:`cycler`, and a concatenation function :func:`concat`. The factory function provides a simple interface for creating 'base' `Cycler` objects while the class takes care of the composition and iteration logic. `Cycler` Usage ============== Base ---- A single entry `Cycler` object can be used to easily cycle over a single style. To create the `Cycler` use the :py:func:`cycler` function to link a key/style/kwarg to series of values. The key must be hashable (as it will eventually be used as the key in a :obj:`dict`). .. ipython:: python from __future__ import print_function from cycler import cycler color_cycle = cycler(color=['r', 'g', 'b']) color_cycle The `Cycler` knows it's length and keys: .. ipython:: python len(color_cycle) color_cycle.keys Iterating over this object will yield a series of :obj:`dict` objects keyed on the label .. ipython:: python for v in color_cycle: print(v) `Cycler` objects can be passed as the argument to :func:`cycler` which returns a new `Cycler` with a new label, but the same values. .. ipython:: python cycler(ec=color_cycle) Iterating over a `Cycler` results in the finite list of entries, to get an infinite cycle, call the `Cycler` object (a-la a generator) .. ipython:: python cc = color_cycle() for j, c in zip(range(5), cc): print(j, c) Composition ----------- A single `Cycler` can just as easily be replaced by a single ``for`` loop. The power of `Cycler` objects is that they can be composed to easily create complex multi-key cycles. Addition ~~~~~~~~ Equal length `Cycler` s with different keys can be added to get the 'inner' product of two cycles .. 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) As with arithmetic, addition is commutative .. ipython:: python lw_c = lw_cycle + color_cycle c_lw = color_cycle + lw_cycle for j, (a, b) in enumerate(zip(lw_c, c_lw)): print('({j}) A: {A!r} B: {B!r}'.format(j=j, A=a, B=b)) For convenience, the :func:`cycler` function can have multiple key-value pairs and will automatically compose them into a single `Cycler` via addition .. ipython:: python wc = cycler(c=['r', 'g', 'b'], lw=range(3)) for s in wc: print(s) Multiplication ~~~~~~~~~~~~~~ Any pair of `Cycler` can be 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.product` ) .. ipython:: python 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 j, (a, b) in enumerate(zip(c_m, m_c)): print('({j}) A: {A!r} B: {B!r}'.format(j=j, A=a, B=b)) Integer Multiplication ~~~~~~~~~~~~~~~~~~~~~~ `Cycler` s can also be multiplied by integer values to increase the length. .. ipython:: python color_cycle * 2 2 * color_cycle Concatenation ~~~~~~~~~~~~~ `Cycler` objects can be concatenated either via the :py:meth:`Cycler.concat` method .. ipython:: python color_cycle.concat(color_cycle) or the top-level :py:func:`concat` function .. ipython:: python from cycler import concat concat(color_cycle, color_cycle) Slicing ------- Cycles can be sliced with :obj:`slice` objects .. ipython:: python color_cycle[::-1] color_cycle[:2] color_cycle[1:] to return a sub-set of the cycle as a new `Cycler`. Inspecting the `Cycler` ----------------------- To inspect the values of the transposed `Cycler` use the `Cycler.by_key` method: .. ipython:: python c_m.by_key() This `dict` can be mutated and used to create a new `Cycler` with the updated values .. ipython:: python bk = c_m.by_key() bk['color'] = ['green'] * len(c_m) cycler(**bk) Examples -------- We can use `Cycler` instances to cycle over one or more ``kwarg`` to `~matplotlib.axes.Axes.plot` : .. plot:: :include-source: from 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, 5), cycle(color_cycle)): ax2.plot(x, x*i, **sty) .. plot:: :include-source: from 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']) ls_cycle = cycler('ls', ['-', '--']) lw_cycle = cycler('lw', range(1, 4)) sty_cycle = ls_cycle * (color_cycle + lw_cycle) for i, sty in enumerate(sty_cycle): ax1.plot(x, x*(i+1), **sty) sty_cycle = (color_cycle + lw_cycle) * ls_cycle for i, sty in enumerate(sty_cycle): ax2.plot(x, x*(i+1), **sty) Persistent Cycles ----------------- It can be useful to associate a given label with a style via dictionary lookup and to dynamically generate that mapping. This can easily be accomplished using a `~collections.defaultdict` .. ipython:: python from cycler import cycler as cy from collections import defaultdict cyl = cy('c', 'rgb') + cy('lw', range(1, 4)) To get a finite set of styles .. ipython:: python finite_cy_iter = iter(cyl) dd_finite = defaultdict(lambda : next(finite_cy_iter)) or repeating .. ipython:: python loop_cy_iter = cyl() dd_loop = defaultdict(lambda : next(loop_cy_iter)) This can be helpful when plotting complex data which has both a classification and a label :: finite_cy_iter = iter(cyl) styles = defaultdict(lambda : next(finite_cy_iter)) for group, label, data in DataSet: ax.plot(data, label=label, **styles[group]) which will result in every ``data`` with the same ``group`` being plotted with the same style. Exceptions ---------- A :obj:`ValueError` is raised if unequal length `Cycler` s are added together .. ipython:: python :okexcept: cycler(c=['r', 'g', 'b']) + cycler(ls=['-', '--']) or if two cycles which have overlapping keys are composed .. ipython:: python :okexcept: color_cycle = cycler(c=['r', 'g', 'b']) color_cycle + color_cycle 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: .. 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 `Cycler` class, a composable kwarg iterator, was developed.