Skip to content

Commit

Permalink
Added .options method for simplified option setting (#2306)
Browse files Browse the repository at this point in the history
  • Loading branch information
philippjfr committed Mar 1, 2018
1 parent 846aeec commit 36c8bc8
Show file tree
Hide file tree
Showing 11 changed files with 552 additions and 136 deletions.
46 changes: 44 additions & 2 deletions examples/getting_started/2-Customization.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@
"%%opts Spikes [height=100 width=600 yaxis=None] (color='grey' line_width=0.25)\n",
"curve = hv.Curve( spike_train, 'milliseconds', 'Hertz')\n",
"spikes = hv.Spikes(spike_train, 'milliseconds', [])\n",
"(curve+spikes).cols(1)"
"(curve + spikes).cols(1)"
]
},
{
Expand All @@ -161,7 +161,49 @@
"\n",
"The corresponding [User Guide](../user_guide/03-Customizing_Plots.ipynb) entry explains the keywords used in detail, but a quick summary is that we have elongated the ``Curve`` and ``Scatter`` elements and toggled various axes with the ***plot options***. We have also specified the color and line widths of the [Bokeh glyphs](http://bokeh.pydata.org/en/latest/docs/user_guide/plotting.html) with the ***style options***.\n",
"\n",
"As you can see, these tools allow significant customization of how our elements appear. HoloViews offers many other tools for setting options either locally or globally, including the ``%output`` and ``%opts`` *line magics*, the ``.opts`` method on all HoloViews objects and the ``hv.output`` and ``hv.opts`` utilities. All these tools, how they work and details of the opts syntax can be found in the [User Guide](../user_guide/03-Customizing_Plots.ipynb)."
"As you can see, these tools allow significant customization of how our elements appear. HoloViews offers many other tools for setting options either locally or globally, including the ``%output`` and ``%opts`` *line magics*, the ``.opts`` and ``.options`` methods on all HoloViews objects and the ``hv.output`` and ``hv.opts`` utilities. We will briefly consider the ``.options`` based approach, which makes it possible to work outside the notebook. All these tools, how they work and details of the opts syntax can be found in the [User Guide](../user_guide/03-Customizing_Plots.ipynb)."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Outside the notebook\n",
"\n",
"When working inside the notebook environment using the ``%%opts`` magic is a very convenient and powerful way of working because it allows tab-completion of the options however outside the notebook and when you're are customizing a specific object it is often useful to set the options directly on an object. The simplest way of doing so is using the ``.options`` method, which will automatically deduce whether an option is a ``style``, ``plot`` or ``norm`` option.\n",
"\n",
"To demonstrate this we can reproduce the plot above by combining both the plot and style options above into a flat set of options:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"curve_opts = dict(height=100, width=600, xaxis=None, tools=['hover'], color='red', line_width=1.5)\n",
"spike_opts = dict(height=100, width=600, yaxis=None, color='grey', line_width=0.25)\n",
"\n",
"curve = hv.Curve(spike_train, 'milliseconds', 'Hertz')\n",
"spikes = hv.Spikes(spike_train, 'milliseconds', [])\n",
"\n",
"(curve.options(**curve_opts) + spikes.options(**spike_opts)).cols(1)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"When using ``.options`` to apply options directly to an individual object we do not have to explicitly declare which object the options apply to, however often it is useful to set options on a composite object. In these cases the options can be declared as a dictionary of the type name and the options. The code below is therefore equivalent to the syntax we used above:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"layout = (curve + spikes).options({'Curve': curve_opts, 'Spikes': spike_opts}).cols(1)"
]
},
{
Expand Down
66 changes: 53 additions & 13 deletions examples/user_guide/03-Customizing_Plots.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -181,21 +181,51 @@
"cell_type": "markdown",
"metadata": {},
"source": [
"The straight line has no ``group`` and ``label`` so it gets 'blue' from the ``Curve`` level of specificity. The two sine curves are red as they both have the ``group`` specification of 'Sinusoid'. Lastly we has a sine squared curve with the same ``group`` label of 'Sinusoid' but it also has the ``label`` 'Squared' which is why it is green."
"The straight line has no ``group`` and ``label`` so it gets 'blue' from the ``Curve`` level of specificity. The two sine curves are red as they both have the ``group`` specification of 'Sinusoid'. Lastly we has a sine squared curve with the same ``group`` label of 'Sinusoid' but it also has the ``label`` 'Squared' which is why it is green.\n",
"\n",
"#### Dictionary format\n",
"\n",
"HoloViews avoids string parsing and special syntax (other than the basic operators described in [Composing Elements](./02-Composing_Elements.ipynb)) where possible. For this reason, all options are fundamentally reduced to a simple dictionary format. For example, here is the pure Python equivalent of the options shown above, using the ``opts`` method that will be described shortly:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"dict_spec = {'Curve': {'style':dict(color='blue')}, \n",
" 'Curve.Sinusoid': {'style':dict(color='red')}, \n",
" 'Curve.Sinusoid.Squared ': {'style':dict(color='green'),\n",
" 'plot':dict(interpolation='steps-mid')}}\n",
"\n",
"dcurve = hv.Curve((xs, xs/3))\n",
"dgroup_curve1 = hv.Curve((xs, np.sin(xs)), group='Sinusoid')\n",
"dgroup_curve2 = hv.Curve((xs, np.sin(xs+np.pi/4)), group='Sinusoid')\n",
"dlabel_curve = hv.Curve((xs, np.sin(xs)**2), group='Sinusoid', label='Squared')\n",
"dlayout = dcurve * dgroup_curve1 * dgroup_curve2 * dlabel_curve\n",
"dlayout.opts(dict_spec)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"#### Dictionary format"
"Although it is as simple as possible, this format is tedious and verbose to use: HoloViews allows you to specify *all* your options separate from your elements in one specifiation which means there is a minimum possible complexity. For this reason, the most commonly used format is the succinct string format describe below, which is parsed into the dictionary format behind the scenes."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"HoloViews avoids string parsing and special syntax (other than the basic operators described in [Composing Elements](./02-Composing_Elements.ipynb)) where possible. For this reason, all options are fundamentally reduced to a simple dictionary format. For example, here is the pure Python equivalent of the options shown above, using the ``opts`` method that will be described shortly:"
"#### Simplified format"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"The dictionary format above can be quite cumbersome to work with, therefore HoloViews provides a simpler ``.options`` method, which automatically distinguishes between ``plot``, ``style`` and ``norm`` options. We can take advantage of this to easily apply a mixture of options:"
]
},
{
Expand All @@ -204,22 +234,32 @@
"metadata": {},
"outputs": [],
"source": [
"dict_spec = {'Curve':{'style':dict(color='blue')}, \n",
" 'Curve.Sinusoid':{'style':dict(color='red')}, \n",
" 'Curve.Sinusoid.Squared ': {'style':dict(color='green'), 'plot':dict(interpolation='steps-mid')}}\n",
"dcurve = hv.Curve((xs, xs/3))\n",
"dgroup_curve1 = hv.Curve((xs, np.sin(xs)), group='Sinusoid')\n",
"dgroup_curve2 = hv.Curve((xs, np.sin(xs+np.pi/4)), group='Sinusoid')\n",
"dlabel_curve = hv.Curve((xs, np.sin(xs)**2), group='Sinusoid', label='Squared')\n",
"dlayout = dcurve * dgroup_curve1 * dgroup_curve2 * dlabel_curve\n",
"dlayout.opts(dict_spec)"
"hv.Curve((xs, np.sin(xs))).options(width=500, color='red')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Although it is as simple as possible, this format is tedious and verbose to use: HoloViews allows you to specify *all* your options separate from your elements in one specifiation which means there is a minimum possible complexity. For this reason, the most commonly used format is the succinct string format describe below, which is parsed into the dictionary format behind the scenes."
"In a simple case like above where we are setting options that apply to the ``Curve`` element directly on a ``Curve`` we do not need to qualify further. However, when we are a composite object like an ``Overlay`` or ``Layout``, we have to be explicit about the object we are customizing, again using the ``type[[.group].label]`` specification."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"curve = hv.Curve((xs, np.sin(xs+np.pi/4)))\n",
"stepped_curve = hv.Curve((xs, np.sin(xs)**2), group='Stepped')\n",
"area = hv.Area((xs, np.sin(xs)**2))\n",
"negative_area = hv.Area((xs, -(np.sin(xs)**2)), group='Negative')\n",
"\n",
"options = {'Curve': dict(width=500, color='red'),\n",
" 'Curve.Stepped': dict(color='green', interpolation='steps-mid'),\n",
" 'Area.Negative': dict(color='red')}\n",
"\n",
"(curve * stepped_curve + area * negative_area).options(options)"
]
},
{
Expand Down
82 changes: 69 additions & 13 deletions holoviews/core/dimension.py
Original file line number Diff line number Diff line change
Expand Up @@ -1086,29 +1086,43 @@ def __call__(self, options=None, **kwargs):
'in future. Use the equivalent opts method instead.')
return self.opts(options, **kwargs)

def opts(self, options=None, **kwargs):
def opts(self, options=None, backend=None, clone=True, **kwargs):
"""
Apply the supplied options to a clone of the object which is
then returned. Note that if no options are supplied at all,
all ids are reset.
Applies options on an object or nested group of objects in a
by options group returning a new object with the options
applied. If the options are to be set directly on the object a
simple format may be used, e.g.:
obj.opts(style={'cmap': 'viridis'}, plot={'show_title': False})
If the object is nested the options must be qualified using
a type[.group][.label] specification, e.g.:
obj.opts({'Image': {'plot': {'show_title': False},
'style': {'cmap': 'viridis}}})
If no opts are supplied all options on the object will be reset.
Disabling clone will modify the object inplace.
"""
from ..util.parser import OptsSpec
backend = backend or Store.current_backend
if isinstance(options, basestring):
from ..util.parser import OptsSpec
try:
options = OptsSpec.parse(options)
except SyntaxError:
options = OptsSpec.parse(
'{clsname} {options}'.format(clsname=self.__class__.__name__,
options=options))

groups = set(Store.options().groups.keys())
backend_options = Store.options(backend=backend)
groups = set(backend_options.groups.keys())
if kwargs and set(kwargs) <= groups:
if not all(isinstance(v, dict) for v in kwargs.values()):
raise Exception("The %s options must be specified using dictionary groups" %
','.join(repr(k) for k in kwargs.keys()))

# Check whether the user is specifying targets (such as 'Image.Foo')
entries = Store.options().children
entries = backend_options.children
targets = [k.split('.')[0] in entries for grp in kwargs.values() for k in grp]
if any(targets) and not all(targets):
raise Exception("Cannot mix target specification keys such as 'Image' with non-target keywords.")
Expand All @@ -1126,12 +1140,54 @@ def opts(self, options=None, **kwargs):

kwargs = {k:{identifier:v} for k,v in kwargs.items()}

if options is None and kwargs=={}:
deep_clone = self.map(lambda x: x.clone(id=None))
else:
deep_clone = self.map(lambda x: x.clone(id=x.id))
StoreOptions.set_options(deep_clone, options, **kwargs)
return deep_clone
obj = self
if options is None and kwargs == {}:
if clone:
obj = self.map(lambda x: x.clone(id=None))
else:
self.map(lambda x: setattr(x, 'id', None))
elif clone:
obj = self.map(lambda x: x.clone(id=x.id))
StoreOptions.set_options(obj, options, backend=backend, **kwargs)
return obj


def options(self, options=None, backend=None, clone=True, **kwargs):
"""
Applies options on an object or nested group of objects in a
flat format returning a new object with the options
applied. If the options are to be set directly on the object a
simple format may be used, e.g.:
obj.options(cmap='viridis', show_title=False)
If the object is nested the options must be qualified using
a type[.group][.label] specification, e.g.:
obj.options('Image', cmap='viridis', show_title=False)
or using:
obj.options({'Image': dict(cmap='viridis', show_title=False)})
If no options are supplied all options on the object will be reset.
Disabling clone will modify the object inplace.
"""
if isinstance(options, basestring):
options = {options: kwargs}
elif options and kwargs:
raise ValueError("Options must be defined in one of two formats."
"Either supply keywords defining the options for "
"the current object, e.g. obj.options(cmap='viridis'), "
"or explicitly define the type, e.g."
"obj.options({'Image': {'cmap': 'viridis'}})."
"Supplying both formats is not supported.")
elif kwargs:
options = {type(self).__name__: kwargs}

from ..util import opts
expanded = opts.expand_options(options, backend)
return self.opts(expanded, backend, clone)



Expand Down
11 changes: 6 additions & 5 deletions holoviews/core/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -1326,13 +1326,13 @@ def tree_to_dict(cls, tree):
return specs

@classmethod
def propagate_ids(cls, obj, match_id, new_id, applied_keys):
def propagate_ids(cls, obj, match_id, new_id, applied_keys, backend=None):
"""
Recursively propagate an id through an object for components
matching the applied_keys. This method can only be called if
there is a tree with a matching id in Store.custom_options
"""
if not new_id in Store.custom_options():
if not new_id in Store.custom_options(backend=backend):
raise AssertionError("The set_ids method requires "
"Store.custom_options to contain"
" a tree with id %d" % new_id)
Expand Down Expand Up @@ -1389,7 +1389,7 @@ def validate_spec(cls, spec, backends=None):
Only useful when invalid keywords generate exceptions instead of
skipping i.e Options.skip_invalid is False.
"""
loaded_backends = Store.loaded_backends()if backends is None else backends
loaded_backends = Store.loaded_backends() if backends is None else backends

error_info = {}
backend_errors = defaultdict(set)
Expand All @@ -1407,6 +1407,7 @@ def validate_spec(cls, spec, backends=None):
error_info[error_key+(backend,)] = error.allowed_keywords
backend_errors[error_key].add(backend)


for ((keyword, target, group_name), backends) in backend_errors.items():
# If the keyword failed for the target across all loaded backends...
if set(backends) == set(loaded_backends):
Expand Down Expand Up @@ -1649,7 +1650,7 @@ def set_options(cls, obj, options=None, backend=None, **kwargs):
options = cls.merge_options(Store.options(backend=backend).groups.keys(), options, **kwargs)
spec, compositor_applied = cls.expand_compositor_keys(options)
custom_trees, id_mapping = cls.create_custom_trees(obj, spec)
cls.update_backends(id_mapping, custom_trees)
cls.update_backends(id_mapping, custom_trees, backend=backend)
for (match_id, new_id) in id_mapping:
cls.propagate_ids(obj, match_id, new_id, compositor_applied+list(spec.keys()))
cls.propagate_ids(obj, match_id, new_id, compositor_applied+list(spec.keys()), backend=backend)
return obj
53 changes: 48 additions & 5 deletions holoviews/core/spaces.py
Original file line number Diff line number Diff line change
Expand Up @@ -857,20 +857,63 @@ def _execute_callback(self, *args):
return self._style(retval)


def opts(self, options=None, **kwargs):
def opts(self, options=None, backend=None, clone=True, **kwargs):
"""
Apply the supplied options to a clone of the DynamicMap which is
then returned. Note that if no options are supplied at all,
all ids are reset.
Applies options on an object or nested group of objects in a
by options group returning a new object with the options
applied. If the options are to be set directly on the object a
simple format may be used, e.g.:
obj.opts(style={'cmap': 'viridis'}, plot={'show_title': False})
If the object is nested the options must be qualified using
a type[.group][.label] specification, e.g.:
obj.opts({'Image': {'plot': {'show_title': False},
'style': {'cmap': 'viridis}}})
If no opts are supplied all options on the object will be reset.
Disabling clone will modify the object inplace.
"""
from ..util import Dynamic
dmap = Dynamic(self, operation=lambda obj, **dynkwargs: obj.opts(options, **kwargs),
dmap = Dynamic(self, operation=lambda obj, **dynkwargs: obj.opts(options, backend,
clone, **kwargs),
streams=self.streams, link_inputs=True)
dmap.data = OrderedDict([(k, v.opts(options, **kwargs))
for k, v in self.data.items()])
return dmap


def options(self, options=None, backend=None, clone=True, **kwargs):
"""
Applies options on an object or nested group of objects in a
flat format returning a new object with the options
applied. If the options are to be set directly on the object a
simple format may be used, e.g.:
obj.options(cmap='viridis', show_title=False)
If the object is nested the options must be qualified using
a type[.group][.label] specification, e.g.:
obj.options('Image', cmap='viridis', show_title=False)
or using:
obj.options({'Image': dict(cmap='viridis', show_title=False)})
If no options are supplied all options on the object will be reset.
Disabling clone will modify the object inplace.
"""
from ..util import Dynamic
dmap = Dynamic(self, operation=lambda obj, **dynkwargs: obj.options(options, backend,
clone, **kwargs),
streams=self.streams, link_inputs=True)
dmap.data = OrderedDict([(k, v.options(options, backend, **kwargs))
for k, v in self.data.items()])
return dmap


def clone(self, data=None, shared_data=True, new_type=None, link_inputs=True,
*args, **overrides):
"""
Expand Down
Loading

0 comments on commit 36c8bc8

Please sign in to comment.