Source code for bokeh.themes.theme
''' Provide a ``Theme`` class for specify overrides for default values
for Bokeh :class:`~bokeh.model.Model` properties.
'''
from __future__ import absolute_import, print_function
import yaml
from ..core.has_props import HasProps
# whenever we cache that there's nothing themed for a class, we
# use this same dict instance, so we don't have a zillion empty
# dicts in our caches.
_empty_dict = dict()
# Note: in DirectoryHandler and in general we assume this is an
# immutable object, because we share it among sessions and we
# don't monitor it for changes. If you make this mutable by adding
# any kind of setter, you could have to refactor some other code.
[docs]class Theme(object):
'''
Args:
filename (str, optional) : path to a YAML theme file
json (str, optional) : a JSON dictionary specifying theme values
Raises:
ValueError
If neither ``filename`` or ``json`` is supplied.
'''
def __init__(self, filename=None, json=None):
if (filename is not None) and (json is not None):
raise ValueError("Theme should be constructed from a file or from json not both")
if filename is not None:
f = open(filename)
try:
json = yaml.load(f)
# empty docs result in None rather than {}, fix it.
if json is None:
json = {}
finally:
f.close()
if json is None:
raise ValueError("Theme requires json or a filename to construct")
self._json = json
if 'attrs' not in self._json:
self._json['attrs'] = {}
if not isinstance(self._json['attrs'], dict):
raise ValueError("theme problem: attrs field should be a dictionary of class names, not %r" % (self._json['attrs']))
for key, value in self._json['attrs'].items():
if not isinstance(value, dict):
raise ValueError("theme problem: attrs.%s should be a dictionary of properties, not %r" % (key, value))
self._line_defaults = self._json.get('line_defaults', _empty_dict)
self._fill_defaults = self._json.get('fill_defaults', _empty_dict)
self._text_defaults = self._json.get('text_defaults', _empty_dict)
# mapping from class name to the full set of properties
# (including those merged in from base classes) for that
# class.
self._by_class_cache = {}
def _add_glyph_defaults(self, cls, props):
from ..models.glyphs import Glyph
if issubclass(cls, Glyph):
if hasattr(cls, "line_alpha"):
props.update(self._line_defaults)
if hasattr(cls, "fill_alpha"):
props.update(self._fill_defaults)
if hasattr(cls, "text_alpha"):
props.update(self._text_defaults)
def _for_class(self, cls):
if cls.__name__ not in self._by_class_cache:
attrs = self._json['attrs']
combined = {}
# we go in reverse order so that subclass props override base class
for base in cls.__mro__[-2::-1]:
if not issubclass(base, HasProps):
continue
self._add_glyph_defaults(base, combined)
combined.update(attrs.get(base.__name__, _empty_dict))
if len(combined) == 0:
combined = _empty_dict
self._by_class_cache[cls.__name__] = combined
return self._by_class_cache[cls.__name__]
[docs] def apply_to_model(self, model):
''' Apply this theme to a model.
.. warning::
Typically, don't call this method directly. Instead, set the theme
on the :class:`~bokeh.document.Document` the model is a part of.
'''
model.apply_theme(self._for_class(model.__class__))
# a little paranoia because it would be Bad(tm) to mess
# this up... would be nicer if python had a way to freeze
# the dict.
if len(_empty_dict) > 0:
raise RuntimeError("Somebody put stuff in _empty_dict")