diff options
Diffstat (limited to 'venv/lib/python3.8/site-packages/plotly/io/_templates.py')
-rw-r--r-- | venv/lib/python3.8/site-packages/plotly/io/_templates.py | 492 |
1 files changed, 492 insertions, 0 deletions
diff --git a/venv/lib/python3.8/site-packages/plotly/io/_templates.py b/venv/lib/python3.8/site-packages/plotly/io/_templates.py new file mode 100644 index 0000000..160ee7c --- /dev/null +++ b/venv/lib/python3.8/site-packages/plotly/io/_templates.py @@ -0,0 +1,492 @@ +import textwrap +import pkgutil + +import copy +import os +import json +from functools import reduce + +try: + from math import gcd +except ImportError: + # Python 2 + from fractions import gcd + +# Create Lazy sentinal object to indicate that a template should be loaded +# on-demand from package_data +Lazy = object() + + +# Templates configuration class +# ----------------------------- +class TemplatesConfig(object): + """ + Singleton object containing the current figure templates (aka themes) + """ + + def __init__(self): + # Initialize properties dict + self._templates = {} + + # Initialize built-in templates + default_templates = [ + "ggplot2", + "seaborn", + "simple_white", + "plotly", + "plotly_white", + "plotly_dark", + "presentation", + "xgridoff", + "ygridoff", + "gridon", + "none", + ] + + for template_name in default_templates: + self._templates[template_name] = Lazy + + self._validator = None + self._default = None + + # ### Magic methods ### + # Make this act as a dict of templates + def __len__(self): + return len(self._templates) + + def __contains__(self, item): + return item in self._templates + + def __iter__(self): + return iter(self._templates) + + def __getitem__(self, item): + if isinstance(item, str): + template_names = item.split("+") + else: + template_names = [item] + + templates = [] + for template_name in template_names: + template = self._templates[template_name] + if template is Lazy: + from plotly.graph_objs.layout import Template + + if template_name == "none": + # "none" is a special built-in named template that applied no defaults + template = Template(data_scatter=[{}]) + self._templates[template_name] = template + else: + # Load template from package data + path = os.path.join( + "package_data", "templates", template_name + ".json" + ) + template_str = pkgutil.get_data("plotly", path).decode("utf-8") + template_dict = json.loads(template_str) + template = Template(template_dict, _validate=False) + + self._templates[template_name] = template + templates.append(self._templates[template_name]) + + return self.merge_templates(*templates) + + def __setitem__(self, key, value): + self._templates[key] = self._validate(value) + + def __delitem__(self, key): + # Remove template + del self._templates[key] + + # Check if we need to remove it as the default + if self._default == key: + self._default = None + + def _validate(self, value): + if not self._validator: + from plotly.validator_cache import ValidatorCache + + self._validator = ValidatorCache.get_validator("layout", "template") + + return self._validator.validate_coerce(value) + + def keys(self): + return self._templates.keys() + + def items(self): + return self._templates.items() + + def update(self, d={}, **kwargs): + """ + Update one or more templates from a dict or from input keyword + arguments. + + Parameters + ---------- + d: dict + Dictionary from template names to new template values. + + kwargs + Named argument value pairs where the name is a template name + and the value is a new template value. + """ + for k, v in dict(d, **kwargs).items(): + self[k] = v + + # ### Properties ### + @property + def default(self): + """ + The name of the default template, or None if no there is no default + + If not None, the default template is automatically applied to all + figures during figure construction if no explicit template is + specified. + + The names of available templates may be retrieved with: + + >>> import plotly.io as pio + >>> list(pio.templates) + + Returns + ------- + str + """ + return self._default + + @default.setter + def default(self, value): + # Validate value + # Could be a Template object, the key of a registered template, + # Or a string containing the names of multiple templates joined on + # '+' characters + self._validate(value) + self._default = value + + def __repr__(self): + return """\ +Templates configuration +----------------------- + Default template: {default} + Available templates: +{available} +""".format(default=repr(self.default), available=self._available_templates_str()) + + def _available_templates_str(self): + """ + Return nicely wrapped string representation of all + available template names + """ + available = "\n".join( + textwrap.wrap( + repr(list(self)), + width=79 - 8, + initial_indent=" " * 8, + subsequent_indent=" " * 9, + ) + ) + return available + + def merge_templates(self, *args): + """ + Merge a collection of templates into a single combined template. + Templates are process from left to right so if multiple templates + specify the same propery, the right-most template will take + precedence. + + Parameters + ---------- + args: list of Template + Zero or more template objects (or dicts with compatible properties) + + Returns + ------- + template: + A combined template object + + Examples + -------- + + >>> pio.templates.merge_templates( + ... go.layout.Template(layout={'font': {'size': 20}}), + ... go.layout.Template(data={'scatter': [{'mode': 'markers'}]}), + ... go.layout.Template(layout={'font': {'family': 'Courier'}})) + layout.Template({ + 'data': {'scatter': [{'mode': 'markers', 'type': 'scatter'}]}, + 'layout': {'font': {'family': 'Courier', 'size': 20}} + }) + """ + if args: + return reduce(self._merge_2_templates, args) + else: + from plotly.graph_objs.layout import Template + + return Template() + + def _merge_2_templates(self, template1, template2): + """ + Helper function for merge_templates that merges exactly two templates + + Parameters + ---------- + template1: Template + template2: Template + + Returns + ------- + Template: + merged template + """ + # Validate/copy input templates + result = self._validate(template1) + other = self._validate(template2) + + # Cycle traces + for trace_type in result.data: + result_traces = result.data[trace_type] + other_traces = other.data[trace_type] + + if result_traces and other_traces: + lcm = ( + len(result_traces) + * len(other_traces) + // gcd(len(result_traces), len(other_traces)) + ) + + # Cycle result traces + result.data[trace_type] = result_traces * (lcm // len(result_traces)) + + # Cycle other traces + other.data[trace_type] = other_traces * (lcm // len(other_traces)) + + # Perform update + result.update(other) + + return result + + +# Make config a singleton object +# ------------------------------ +templates = TemplatesConfig() +del TemplatesConfig + + +# Template utilities +# ------------------ +def walk_push_to_template(fig_obj, template_obj, skip): + """ + Move style properties from fig_obj to template_obj. + + Parameters + ---------- + fig_obj: plotly.basedatatypes.BasePlotlyType + template_obj: plotly.basedatatypes.BasePlotlyType + skip: set of str + Set of names of properties to skip + """ + from _plotly_utils.basevalidators import ( + CompoundValidator, + CompoundArrayValidator, + is_array, + ) + + for prop in list(fig_obj._props): + if prop == "template" or prop in skip: + # Avoid infinite recursion + continue + + fig_val = fig_obj[prop] + template_val = template_obj[prop] + + validator = fig_obj._get_validator(prop) + + if isinstance(validator, CompoundValidator): + walk_push_to_template(fig_val, template_val, skip) + if not fig_val._props: + # Check if we can remove prop itself + fig_obj[prop] = None + elif isinstance(validator, CompoundArrayValidator) and fig_val: + template_elements = list(template_val) + template_element_names = [el.name for el in template_elements] + template_propdefaults = template_obj[prop[:-1] + "defaults"] + + for fig_el in fig_val: + element_name = fig_el.name + if element_name: + # No properties are skipped inside a named array element + skip = set() + if fig_el.name in template_element_names: + item_index = template_element_names.index(fig_el.name) + template_el = template_elements[item_index] + walk_push_to_template(fig_el, template_el, skip) + else: + template_el = fig_el.__class__() + walk_push_to_template(fig_el, template_el, skip) + template_elements.append(template_el) + template_element_names.append(fig_el.name) + + # Restore element name + # since it was pushed to template above + fig_el.name = element_name + else: + walk_push_to_template(fig_el, template_propdefaults, skip) + + template_obj[prop] = template_elements + + elif not validator.array_ok or not is_array(fig_val): + # Move property value from figure to template + template_obj[prop] = fig_val + try: + fig_obj[prop] = None + except ValueError: + # Property cannot be set to None, move on. + pass + + +def to_templated(fig, skip=("title", "text")): + """ + Return a copy of a figure where all styling properties have been moved + into the figure's template. The template property of the resulting figure + may then be used to set the default styling of other figures. + + Parameters + ---------- + fig + Figure object or dict representing a figure + skip + A collection of names of properties to skip when moving properties to + the template. Defaults to ('title', 'text') so that the text + of figure titles, axis titles, and annotations does not become part of + the template + + Examples + -------- + Imports + + >>> import plotly.graph_objs as go + >>> import plotly.io as pio + + Construct a figure with large courier text + + >>> fig = go.Figure(layout={'title': 'Figure Title', + ... 'font': {'size': 20, 'family': 'Courier'}, + ... 'template':"none"}) + >>> fig # doctest: +NORMALIZE_WHITESPACE + Figure({ + 'data': [], + 'layout': {'font': {'family': 'Courier', 'size': 20}, + 'template': '...', 'title': {'text': 'Figure Title'}} + }) + + Convert to a figure with a template. Note how the 'font' properties have + been moved into the template property. + + >>> templated_fig = pio.to_templated(fig) + >>> templated_fig.layout.template + layout.Template({ + 'layout': {'font': {'family': 'Courier', 'size': 20}} + }) + >>> templated_fig + Figure({ + 'data': [], 'layout': {'template': '...', 'title': {'text': 'Figure Title'}} + }) + + + Next create a new figure with this template + + >>> fig2 = go.Figure(layout={ + ... 'title': 'Figure 2 Title', + ... 'template': templated_fig.layout.template}) + >>> fig2.layout.template + layout.Template({ + 'layout': {'font': {'family': 'Courier', 'size': 20}} + }) + + The default font in fig2 will now be size 20 Courier. + + Next, register as a named template... + + >>> pio.templates['large_courier'] = templated_fig.layout.template + + and specify this template by name when constructing a figure. + + >>> go.Figure(layout={ + ... 'title': 'Figure 3 Title', + ... 'template': 'large_courier'}) # doctest: +ELLIPSIS + Figure(...) + + Finally, set this as the default template to be applied to all new figures + + >>> pio.templates.default = 'large_courier' + >>> fig = go.Figure(layout={'title': 'Figure 4 Title'}) + >>> fig.layout.template + layout.Template({ + 'layout': {'font': {'family': 'Courier', 'size': 20}} + }) + + Returns + ------- + go.Figure + """ + + # process fig + from plotly.basedatatypes import BaseFigure + from plotly.graph_objs import Figure + + if not isinstance(fig, BaseFigure): + fig = Figure(fig) + + # Process skip + if not skip: + skip = set() + else: + skip = set(skip) + + # Always skip uids + skip.add("uid") + + # Initialize templated figure with deep copy of input figure + templated_fig = copy.deepcopy(fig) + + # Handle layout + walk_push_to_template( + templated_fig.layout, templated_fig.layout.template.layout, skip=skip + ) + + # Handle traces + trace_type_indexes = {} + for trace in list(templated_fig.data): + template_index = trace_type_indexes.get(trace.type, 0) + + # Extend template traces if necessary + template_traces = list(templated_fig.layout.template.data[trace.type]) + while len(template_traces) <= template_index: + # Append empty trace + template_traces.append(trace.__class__()) + + # Get corresponding template trace + template_trace = template_traces[template_index] + + # Perform push properties to template + walk_push_to_template(trace, template_trace, skip=skip) + + # Update template traces in templated_fig + templated_fig.layout.template.data[trace.type] = template_traces + + # Update trace_type_indexes + trace_type_indexes[trace.type] = template_index + 1 + + # Remove useless trace arrays + any_non_empty = False + for trace_type in templated_fig.layout.template.data: + traces = templated_fig.layout.template.data[trace_type] + is_empty = [trace.to_plotly_json() == {"type": trace_type} for trace in traces] + if all(is_empty): + templated_fig.layout.template.data[trace_type] = None + else: + any_non_empty = True + + # Check if we can remove the data altogether key + if not any_non_empty: + templated_fig.layout.template.data = None + + return templated_fig |