Skip to content

[WIP] Property Cycling #4686

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

Merged
merged 31 commits into from
Aug 29, 2015
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
d08c627
Add Cycler as a dependency
WeatherGod Jul 11, 2015
e3c0951
Implemented style cycling using cycler.
WeatherGod Jul 12, 2015
160401b
Fix a typo for cycler dependency
WeatherGod Jul 14, 2015
6b2223d
Initial implementation of property cycling
WeatherGod Jul 14, 2015
0c1e609
Fix some typos and handle some other codes using color cycling
WeatherGod Jul 14, 2015
61d10f9
Make dict access safer
WeatherGod Jul 15, 2015
e356972
Added some tests for baseline features
WeatherGod Jul 15, 2015
68f0afb
Updated the fill test to get the hatching.
WeatherGod Jul 16, 2015
c806726
fix typo
WeatherGod Jul 28, 2015
9965f5f
should have linted
WeatherGod Jul 28, 2015
5fa07f2
Getting cycling to work!
WeatherGod Aug 14, 2015
98fb775
Add deprecation notice for axes.color_cycle
WeatherGod Aug 15, 2015
f443629
Some axes.prop_cycler param improvements.
WeatherGod Aug 15, 2015
648d3ea
Made the linewidth thicker for a test
WeatherGod Aug 15, 2015
2e7b6a5
Updated styles and templates for prop_cycle
WeatherGod Aug 15, 2015
a1b822f
Some documentation and updating an old example
WeatherGod Aug 16, 2015
6df7829
PEP8 fixes
WeatherGod Aug 16, 2015
871d616
Simplify set/getdefaults logic for property cycler
WeatherGod Aug 17, 2015
e7786d0
Added basic validation/normalization for cycler
WeatherGod Aug 17, 2015
153ef67
* aliases support
WeatherGod Aug 17, 2015
8ce4797
Support and test for marker cycling
WeatherGod Aug 17, 2015
f3e8394
* Fix cycler advance bug (and added test)
WeatherGod Aug 17, 2015
6b5a6a5
Added the deprecation notice to api_changes/
WeatherGod Aug 17, 2015
c72a74f
PEP8 fixes
WeatherGod Aug 17, 2015
53f1ff2
Fix test now that we normalize property names
WeatherGod Aug 17, 2015
9a31950
some simplification and doc-fixes
WeatherGod Aug 19, 2015
55bf053
Fix a typo
WeatherGod Aug 20, 2015
44f74e3
Finalized the validating cycler design.
WeatherGod Aug 22, 2015
5a3ce17
reduce() isn't built-in in py3k. go figure...
WeatherGod Aug 23, 2015
9848661
Improve set_prop_cycle(). Docstring fixes.
WeatherGod Aug 24, 2015
63d6321
Realized that I didn't have a hatch validator
WeatherGod Aug 24, 2015
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
Prev Previous commit
Next Next commit
Implemented style cycling using cycler.
* New rcparam 'axes.style_cycle'
* Deprecate rcparam 'axes.color_cycle'
* 'axes.style_cycle' can be a cycler object or string repr
* Enables setting of just about any property
  • Loading branch information
WeatherGod committed Aug 15, 2015
commit e3c09518bca3e1e4f941c8f54c788e970f0876e1
55 changes: 37 additions & 18 deletions lib/matplotlib/axes/_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import math
from operator import itemgetter

from cycler import cycler
import numpy as np
from numpy import ma

Expand Down Expand Up @@ -138,20 +139,25 @@ class _process_plot_var_args(object):
def __init__(self, axes, command='plot'):
self.axes = axes
self.command = command
self.set_color_cycle()
self.set_style_cycle()

def __getstate__(self):
# note: it is not possible to pickle a itertools.cycle instance
return {'axes': self.axes, 'command': self.command}

def __setstate__(self, state):
self.__dict__ = state.copy()
self.set_color_cycle()

def set_color_cycle(self, clist=None):
if clist is None:
clist = rcParams['axes.color_cycle']
self.color_cycle = itertools.cycle(clist)
self.set_style_cycle()

def set_style_cycle(self, style_cycler=None):
if style_cycler is None:
style_cycler = rcParams['axes.style_cycle']
if style_cycler is None and 'axes.color_cycle' in rcParams:
clist = rcParams['axes.color_cycle']
style_cycler = cycler('color', clist)
self.style_cycler = itertools.cycle(style_cycler)
# Make a copy
self._style_keys = list(style_cycler.keys)

def __call__(self, *args, **kwargs):

Expand Down Expand Up @@ -229,24 +235,37 @@ def _xy_from_xy(self, x, y):
y = y[:, np.newaxis]
return x, y

def _setdefaults(self, kw, kwargs):
# Only advance the cycler if the cycler
# has information that is not specified
# in the supplied kw and kwargs dicts
if any([kw.get(k, None) is None and kwargs.get(k, None) is None
for k in self._style_keys]):
default_dict = six.next(self.style_cycler)
else:
default_dict = None
Copy link
Member

Choose a reason for hiding this comment

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

Should probably return an empty dict?


for k in self._style_keys:
if (default_dict is not None and
kw.get(k, None) is None and
kwargs.get(k, None) is None):
kwargs[k] = kw[k] = default_dict[k]

def _makeline(self, x, y, kw, kwargs):
kw = kw.copy() # Don't modify the original kw.
kwargs = kwargs.copy()
if kw.get('color', None) is None and kwargs.get('color', None) is None:
kwargs['color'] = kw['color'] = six.next(self.color_cycle)
# (can't use setdefault because it always evaluates
# its second argument)
seg = mlines.Line2D(x, y,
**kw
)
self._setdefaults(kw, kwargs)
seg = mlines.Line2D(x, y, **kw)
self.set_lineprops(seg, **kwargs)
return seg

def _makefill(self, x, y, kw, kwargs):
try:
facecolor = kw['color']
except KeyError:
facecolor = six.next(self.color_cycle)
kw = kw.copy() # Don't modify the original kw.
kwargs = kwargs.copy()
# Might be problematic with fallback names such as
# 'facecolor' and such. Possibly fixed by traitlets?
self._setdefaults(kw, kwargs)
facecolor = kw['color']
seg = mpatches.Polygon(np.hstack((x[:, np.newaxis],
y[:, np.newaxis])),
facecolor=facecolor,
Expand Down
32 changes: 32 additions & 0 deletions lib/matplotlib/rcsetup.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@
from matplotlib.fontconfig_pattern import parse_fontconfig_pattern
from matplotlib.colors import is_color_like

from cycler import cycler, Cycler
Copy link
Member

Choose a reason for hiding this comment

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

Do I have my paths and/or directory structure set up wrongly, or should this read

from cycler.cycler import cycler, Cycler

?

Copy link
Member Author

Choose a reason for hiding this comment

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

The Cycler class and the (recommended to use) cycler() function are available directly from the top level module.

Copy link
Member

Choose a reason for hiding this comment

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

Ahh nevermind my path problem, I had the path pointing to the directory containing the repo rather than the repo itself...
cycler -> dir
cycler.cycler -> cycler.py
cycler.cycler.cycler -> func


#interactive_bk = ['gtk', 'gtkagg', 'gtkcairo', 'qt4agg',
# 'tkagg', 'wx', 'wxagg', 'cocoaagg', 'webagg']
# The capitalized forms are needed for ipython at present; this may
Expand Down Expand Up @@ -299,6 +301,12 @@ def validate_color(s):

raise ValueError('%s does not look like a color arg%s' % (s, msg))

def deprecate_axes_colorcycle(value):
warnings.warn("axes.color_cycle is deprecated. Use axes.style_cycle "
"instead. Will be removed in 2.1.0")
return validate_colorlist(value)
Copy link
Member

Choose a reason for hiding this comment

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

Where does this get called from?

Copy link
Member Author

Choose a reason for hiding this comment

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

Good catch. I forgot to add that in as the validator.

Copy link
Member

Choose a reason for hiding this comment

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

Ahh, okay, this just wraps around the validator... hmm, why do it this way then and not as a decorator for validate_colorlist?

Also what do you think about setting axes.prop_cycle here? That should make the logic easier elsewhere as we don't have to care about color_cycle elsewhere in the code...

Copy link
Member Author

Choose a reason for hiding this comment

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

@OceanWolf, We don't decorate the validate_colorlist() function because it is a perfectly valid function on its own for validating a list of colors, and so it could be used elsewhere in contexts that have nothing to do with color_cycle. You see this design elsewhere for other deprecated params.

As for setting the prop_cycle in validate_colorlist(), this is just a validator function. These validators are used by the RcSetup class to validate inputs for every entry. The validators cannot, and should not, be setting any params.

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 the first point, but on the second you misunderstand me, I didn't mention setting prop_cycle in validate_colorlist, but in this deprecate_axes_colorcycle function. As by definition this function will get removed in 2.1.0, it makes sense to me to that here.

I find it good practice to work around the deprecated stuff as early, aka as high up as possible in the codebase, rather then let it filter through to the deeper level code, as I see happens just above @tacaswell's comment "Should probably tag next version of cycler".

Copy link
Member

Choose a reason for hiding this comment

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

This is going to have a very long deprecation cycle, at least a couple
years, given how long this has been in the api.

I could also be convinced that it should never be removed and keeping an
alias that sets the color cycle is a good idea.

On Sat, Aug 15, 2015, 3:01 PM OceanWolf notifications@github.com wrote:

In lib/matplotlib/rcsetup.py
#4686 (comment):

@@ -299,6 +301,12 @@ def validate_color(s):

 raise ValueError('%s does not look like a color arg%s' % (s, msg))

+def deprecate_axes_colorcycle(value):

  • warnings.warn("axes.color_cycle is deprecated. Use axes.prop_cycle "
  •              "instead. Will be removed in 2.1.0")
    
  • return validate_colorlist(value)

I agree with the first point, but on the second you misunderstand me, I
didn't mention setting prop_cycle in validate_colorlist, but in this
deprecate_axes_colorcycle function. As by definition this function will
get removed in 2.1.0, it makes sense to me to that here.

I find it good practice to work around the deprecated stuff as early, aka
as high up as possible in the codebase, rather then let it filter through
to the deeper level code, as I see happens just above @tacaswell
https://github.com/tacaswell's comment "Should probably tag next
version of cycler".


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




def validate_colorlist(s):
'return a list of colorspecs'
Expand All @@ -310,6 +318,25 @@ def validate_colorlist(s):
msg = "'s' must be of type [ string | list | tuple ]"
raise ValueError(msg)

def validate_cycler(s):
'return a cycler object from a string repr or the object itself'
if isinstance(s, six.string_types):
try:
# TODO: We might want to rethink this...
cycler_inst = eval(s, {'cycler': cycler, 'Cycler': Cycler})
Copy link
Member

Choose a reason for hiding this comment

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

This is exciting!

Copy link
Member

Choose a reason for hiding this comment

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

Can you give an example of passing a string to this function?

Copy link
Member Author

Choose a reason for hiding this comment

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

It would look exactly like how you would create cycler objects in code,
just with quotes around it. It is so we can specify them in the rc file.
I'll be adding unit tests for this later today as well as documentation.
On Aug 15, 2015 12:51 AM, "OceanWolf" notifications@github.com wrote:

In lib/matplotlib/rcsetup.py
#4686 (comment):

@@ -310,6 +318,25 @@ def validate_colorlist(s):
msg = "'s' must be of type [ string | list | tuple ]"
raise ValueError(msg)

+def validate_cycler(s):

  • 'return a cycler object from a string repr or the object itself'
  • if isinstance(s, six.string_types):
  •    try:
    
  •        # TODO: We might want to rethink this...
    
  •        cycler_inst = eval(s, {'cycler': cycler, 'Cycler': Cycler})
    

Can you give an example of passing a string to this function?


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

Copy link
Member

Choose a reason for hiding this comment

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

Ahh, of course, because the matplotlibrc passes in a string...

Could we find a simpler way of passing in a cycle as a string? Asking the user to pass in code as a param feels bad. How about:

(prop, list) op (prop, list) ...
we could use a recursive function to deal with brackets, e.g. ((prop, list) + (prop, list)) * (prop, list)

Copy link
Member

Choose a reason for hiding this comment

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

But we still would have to parse it. By having users pass in the python
code we get to side step that.

This is also the repr of a Cycler so it is super easy for users to generate
this string.

On Sat, Aug 15, 2015 at 1:40 AM OceanWolf notifications@github.com wrote:

In lib/matplotlib/rcsetup.py
#4686 (comment):

@@ -310,6 +318,25 @@ def validate_colorlist(s):
msg = "'s' must be of type [ string | list | tuple ]"
raise ValueError(msg)

+def validate_cycler(s):

  • 'return a cycler object from a string repr or the object itself'
  • if isinstance(s, six.string_types):
  •    try:
    
  •        # TODO: We might want to rethink this...
    
  •        cycler_inst = eval(s, {'cycler': cycler, 'Cycler': Cycler})
    

Ahh, of course, because the matplotlibrc passes in a string...

Could we find a simpler way of passing in a cycle as a string? Asking the
user to pass in code as a param feels bad. How about:

(prop, list) op (prop, list) ...

we could use a recursive function to deal with brackets, e.g. ((prop,
list) + (prop, list)) * (prop, list)


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

Copy link
Member

Choose a reason for hiding this comment

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

Sure, I see that, it just feels messy, a config file shouldn't contain code...

Copy link
Member

Choose a reason for hiding this comment

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

It already does contain code, val = ['a', 'b', 'c'] is code. Also see
lisp rabbit hole ;)

On Sat, Aug 15, 2015 at 2:07 AM OceanWolf notifications@github.com wrote:

In lib/matplotlib/rcsetup.py
#4686 (comment):

@@ -310,6 +318,25 @@ def validate_colorlist(s):
msg = "'s' must be of type [ string | list | tuple ]"
raise ValueError(msg)

+def validate_cycler(s):

  • 'return a cycler object from a string repr or the object itself'
  • if isinstance(s, six.string_types):
  •    try:
    
  •        # TODO: We might want to rethink this...
    
  •        cycler_inst = eval(s, {'cycler': cycler, 'Cycler': Cycler})
    

Sure, I see that, it just feels messy, a config file shouldn't contain
code...


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

except BaseException as e:
raise ValueError("'%s' is not a valid cycler construction: %s" %
(s, e))
elif isinstance(s, Cycler):
cycler_inst = s
else:
raise ValueError("object was not a string or Cycler instance: %s" % s)

# TODO: apply validation to the cycled elements
# probably doable via traitlets

return cycler_inst

def validate_stringlist(s):
'return a list'
if isinstance(s, six.string_types):
Expand Down Expand Up @@ -750,6 +777,11 @@ def __call__(self, s):
'axes.color_cycle': [['b', 'g', 'r', 'c', 'm', 'y', 'k'],
validate_colorlist], # cycle of plot
# line colors
# This entry can be either a cycler object or a
# string repr of a cycler-object, which gets eval()'ed
# to create the object.
'axes.style_cycle': [cycler('color', 'bgrcmyk'),
validate_cycler],
'axes.xmargin': [0, ValidateInterval(0, 1,
closedmin=True,
closedmax=True)], # margin added to xaxis
Expand Down