Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enh remember the style #16

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
108 changes: 107 additions & 1 deletion cycler.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,30 @@
{'color': 'b', 'linestyle': '-'}
{'color': 'b', 'linestyle': '--'}
{'color': 'b', 'linestyle': '-.'}

RememberTheStyle
================

Helper class for mapping keys -> styles::

from cycler import RememberTheStyle, cycler

rts = RememberTheStyle((cycler('c', 'rgb') +
cycler('ls', ['-', '--', ':']))

rts['cat']
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I find the overridden __getitem__ really non-intuitive and would highly recommend using a method for this purpose. Something like:

class NamedCycler:
    def __init__(self, cycler, names=None):
        self.cycler = cycler
        if names:
            self._cache = dict(zip(names, cycler))
        else:
            self._cache = {}

    def associated_name(self, key):
        """
        Return a cycler item for the given key. If the key hasn't already been seen, the
        item will come from the next iteration of the cycler.

        """
        if key not in self._cache:
            self._cache[key] = next(self.cycler)
        return self._cache[key]

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with @pelson. Invoking y = x[key] where this is the first time key has been seen looks like a mistake; it requires the reader to know too much about x.
Further, is the use case for this common enough to require a class? How is it superior from the user's standpoint to assigning aardvark = next(my_cycler) as needed? Or using the one line from the initializer, styles = dict(zip(names, my_cycler))?
If you adopt Phil's version, the method could be called named():

styles = NamedCycler(my_cycler)
plot(x, y, **styles.named('aardvark'))

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I really like the getitem interface as it makes use of the natural value by
key lookup that python provides.

I don't think it is any weirder than a defaultdict. It also allows this
object to be a drop in replacement for a dict anyplace that the user is
currently generating a dict of styles and once used can be converted to a
normal dict with dd = dict(x).

I will add an alias named for getitem.

On Wed, Aug 26, 2015, 11:07 AM Phil Elson notifications@github.com wrote:

In cycler.py
#16 (comment):

@@ -38,6 +38,30 @@
{'color': 'b', 'linestyle': '-'}
{'color': 'b', 'linestyle': '--'}
{'color': 'b', 'linestyle': '-.'}
+
+RememberTheStyle
+================
+
+Helper class for mapping keys -> styles::
+

  • from cycler import RememberTheStyle, cycler
  • rts = RememberTheStyle((cycler('c', 'rgb') +
  •                       cycler('ls', ['-', '--', ':']))
    
  • rts['cat']

I find the overridden getitem really non-intuitive and would highly
recommend using a method for this purpose. Something like:

class NamedCycler:
def init(self, cycler, names=None):
self.cycler = cycler
if names:
self._cache = dict(zip(names, cycler))
else:
self._cache = {}

def associated_name(self, key):
    """
    Return a cycler item for the given key. If the key hasn't already been seen, the
    item will come from the next iteration of the cycler.

    """
    if key not in self._cache:
        self._cache[key] = next(self.cycler)
    return self._cache[key]


Reply to this email directly or view it on GitHub
https://github.com/matplotlib/cycler/pull/16/files#r37993202.

rts['aardvark']
rts['aardvark']
rts['dog']

Results in ::

{'c': 'r', 'ls': '-'}
{'c': 'g', 'ls': '--'}
{'c': 'g', 'ls': '--'}
{'c': 'b', 'ls': ':'}


"""

from __future__ import (absolute_import, division, print_function,
Expand All @@ -47,6 +71,7 @@
from itertools import product, cycle
from six.moves import zip, reduce
from operator import mul, add
from collections import Mapping
import copy

__version__ = '0.9.0.post1'
Expand Down Expand Up @@ -94,6 +119,8 @@ class Cycler(object):

and supports basic slicing via ``[]``

Calling a `Cycler` instance returns an infinite repeating cycle.

Parameters
----------
left : Cycler or None
Expand All @@ -107,6 +134,8 @@ class Cycler(object):

"""
def __call__(self):
"""Infinitely loop through contents of Cycler
"""
return cycle(self)

def __init__(self, left, right=None, op=None):
Expand Down Expand Up @@ -352,7 +381,8 @@ def simplify(self):


def cycler(*args, **kwargs):
"""
"""Public constructor function for Cycler objects

Create a new `Cycler` object from a single positional argument,
a pair of positional arguments, or the combination of keyword arguments.

Expand Down Expand Up @@ -442,3 +472,79 @@ def _cycler(label, itr):
itr = (v[lab] for v in itr)

return Cycler._from_iter(label, itr)


class RememberTheStyle(Mapping):
"""Persistent style by key

This is a class for easily managing consistent style across for
classes of data across many plots.

The first time you use `rts[key]` it gets the next style
dictionary from it's internal `Cycler`. Subsequent calls to
`rts[key]` will return an identical dictionary.

Parameters
----------
sty_cylr : Cycler
Anything that yields dictionaries when iterated over

loop : bool, optional
Defaults to `False`. If True, loop over the input `sty_cylr`
when more keys are requested that the length of `sty_cyclr`.
If `False` will raise `RuntimeError` if available styles are
exhausted.


cache : dict-like or None, optional
MuttableMapping instance to use as the internal key -> style cache.

The purpose of this is provide a pre-populated cache or
to share a cache between multiple `RememberTheStyle` instances.

Use this at your own risk.
"""
def __init__(self, sty_cylr, loop=False, cache=None):
if cache is None:
cache = {}

self._cache = cache
# make a shallow copy of the style cycle just to be safe
sty_cylr = Cycler(sty_cylr)
if loop:
self._style_cycler = sty_cylr()
else:
self._style_cycler = iter(sty_cylr)

@property
def style_cache(self):
"""Access to style cache instance

This is a reference to the underlying object, mutate
this with caution.
"""
return self._cache

def __getitem__(self, key):
try:
return dict(self.style_cache[key])
except KeyError:
# pass instead of handling in here to make stack trace
# nicer
pass
try:
new_style = next(self._style_cycler)
except StopIteration:
raise RuntimeError("Asked for more style than we have")

self.style_cache[key] = dict(new_style)
return dict(new_style)

def __iter__(self):
return iter(self.style_cache)

def __len__(self):
return len(self.style_cache)

def named(self, val):
return self[val]
148 changes: 140 additions & 8 deletions doc/source/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,20 @@

cycler
Cycler

RememberTheStyle

The public API of :py:mod:`cycler` consists of a class `Cycler` and a
factory function :func:`cycler`. The function provides a simple interface for
creating 'base' `Cycler` objects while the class takes care of the composition
and iteration logic.
factory function :func:`cycler` pair a helper class
`RememberTheStyle`. The function :func:`cycler` provides a simple
interface for creating 'base' `Cycler` objects while the `Cycler`
class takes care of the composition and iteration logic.

The `RememberTheStyle` class is a helper class to map keys to a style
dictionary. It wraps a `Cycler` instance under a
`~collections.abc.Mapping` (read-only dict-like) interface. Using
`[]` with a new key will yield the next dict in the `Cycler` and using
`[]` with an existing key will yield the same result as the first
time.


`Cycler` Usage
Expand All @@ -39,7 +47,7 @@ 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
from cycler import cycler, RememberTheStyle


color_cycle = cycler(color=['r', 'g', 'b'])
Expand Down Expand Up @@ -204,7 +212,6 @@ We can use `Cycler` instances to cycle over one or more ``kwarg`` to
:include-source:

from cycler import cycler
from itertools import cycle

fig, (ax1, ax2) = plt.subplots(1, 2, tight_layout=True,
figsize=(8, 4))
Expand All @@ -216,15 +223,14 @@ We can use `Cycler` instances to cycle over one or more ``kwarg`` to
ax1.plot(x, x*(i+1), **sty)


for i, sty in zip(range(1, 5), cycle(color_cycle)):
for i, sty in zip(range(1, 5), 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))
Expand Down Expand Up @@ -266,6 +272,132 @@ or if two cycles which have overlapping keys are composed
color_cycle + color_cycle


`RememberTheStyle` Usage
========================

Base
----
Basic usage is very simple:

.. ipython:: python


cy = cycler('c', 'rgb') + cycler('ls', ['-', '--', ':'])

rts = RememberTheStyle(cy)

rts['cat']

rts['aardvark']

rts['aardvark']

rts['dog']

If you ask for more unique keys than the underlying `Cycler` has
you will get a `RuntimeError`.

.. ipython:: python
:okexcept:

rts['hippo']

The `loop` kwarg will repeat the underlying `Cycler` when it is exhausted


.. ipython:: python


cy = cycler('c', 'rgb') + cycler('ls', ['-', '--', ':'])

rts_loop = RememberTheStyle(cy, loop=True)

rts_loop['cat']

rts_loop['aardvark']

rts_loop['dog']

rts_loop['mouse']

rts_loop['duck']

rts_loop['hippo']

The `RememberTheStyle` implements the `~collections.abc.Mapping` ABC
so the expected dict-like interfaces work

.. ipython:: python

list(rts)

len(rts)

list(rts.items())


Example
-------

.. plot::
:include-source:

import matplotlib.pyplot as plt
import matplotlib.lines as mlines
import numpy as np

from cycler import cycler, RememberTheStyle

# set up data -> class mapping
a_mapping = {'aardvark': 'mammal',
'mouse': 'mammal',
'python': 'reptile',
'newt': 'amphibian',
'African swallow': 'bird',
'European swallow': 'bird',
'African swallow (unladdened)': 'bird'}

# set up the cycler and RememberTheStyle
cy = cycler('c', 'rgbkm') * cycler('lw', [3])
rts = RememberTheStyle(cy)

# x data we will use
th = np.linspace(0, 2*np.pi, 256)
# house keeping
arts = {}
fig, ax = plt.subplots()

# loop over the animals and draw lines
for k in sorted(a_mapping):
# get the style based on the class
sty = rts[a_mapping[k]]
# some synthetic data
y = len(k) * np.sin(th + (len(k) / 5))
# plot the line
ln, = ax.plot(th, y, label=k, **sty)
arts[k] = ln

# create proxy artists for the legend
handles = [mlines.Line2D([], [], label=k, **sty)
for k, sty in sorted(rts.items(), key=lambda x: x[0])]
ax.legend(handles=handles, ncol=2)

# set the x limit
ax.set_xlim(0, 2*np.pi)
# and axis labels
ax.set_xlabel(r'$\theta$')
ax.set_ylabel('arb')

Advanced
--------

The `cache` kwarg allows you to pass in a
`~collections.abc.MuttableMapping` instance to use as the style cache.
This is useful if you want to use a non-`dict` instance to back the
`RememberTheStyle` instance or to share a single cache between multiple
`RememberTheStyle` instances. This involves sharing mutable state, use
at your own risk.

Motivation
==========

Expand Down
Loading