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

Templates (themes) integration #1224

Merged
merged 34 commits into from
Oct 23, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
106426e
Add initial template validation logic
jonmmease Oct 6, 2018
afaaec8
Updated codegen to add support for elementdefaults properties
jonmmease Oct 6, 2018
e426808
Added codegen docstrings for layout.template and the elementdefaults …
jonmmease Oct 7, 2018
08e095b
Added template acceptance/validation tests
jonmmease Oct 7, 2018
f84c8a5
Add test to make sure various template assignments trigger relayout m…
jonmmease Oct 7, 2018
01d5fa7
Implementation of plotly.io.templates configuration object
jonmmease Oct 7, 2018
db58685
Added plotly.io.template tests
jonmmease Oct 8, 2018
ae51ae1
plotly.io.to_templated function
jonmmease Oct 9, 2018
f94b4a7
Added plotly.io.templates.merge_templates utility function
jonmmease Oct 9, 2018
7fffd41
Support specifying flaglist of named templates to be merged together
jonmmease Oct 10, 2018
9955bb4
Customize the description of the TemplateValidator
jonmmease Oct 10, 2018
3dc8cde
Cleanup docstring documentation of template functions
jonmmease Oct 10, 2018
8ddc56f
Added initial ggplot2 theme
jonmmease Oct 11, 2018
a0a73c6
Refactor template generation to DRY it and add seaborn template
jonmmease Oct 12, 2018
27690b9
Added initial prototype plotly dark / light themes
jonmmease Oct 13, 2018
adda2b9
Remove dark 1, rename dark 2 just dark.
jonmmease Oct 13, 2018
d13032b
Elide template in figure representation since these are typically
jonmmease Oct 13, 2018
09972fc
Added colorcet to optional dependencies
jonmmease Oct 13, 2018
ceb0e6c
Rename plotly_light -> plotly, plotly_light2 -> plotly_white
jonmmease Oct 13, 2018
e6fabe9
Set autocolorscale to false in templates to make sure template
jonmmease Oct 13, 2018
a025e79
Switch to 5-color ggplot2 colorway
jonmmease Oct 13, 2018
4103057
Added 'presentation' template that can be used to increase the size
jonmmease Oct 13, 2018
b7bfcb7
Added template to package_data in setup.py so that they are included …
jonmmease Oct 13, 2018
0d9ec88
Test fixes
jonmmease Oct 14, 2018
798168f
Add __future__ absolute import statements needed for Python 2.7
jonmmease Oct 14, 2018
aaa66b8
Only compare EPS images
jonmmease Oct 14, 2018
76a69cb
Added xgrid template to re-enable to xgrid lines that are off by default
jonmmease Oct 15, 2018
58143da
Leave x-grid on by default in all templates and add xgridoff template
jonmmease Oct 15, 2018
3222b67
Template refinements with interpolated colors
jonmmease Oct 22, 2018
ee318c6
Initialize fig.layout.template in BaseFigure constructor
jonmmease Oct 22, 2018
bc0ca2a
Cleanup templategen colors module
jonmmease Oct 22, 2018
13f623c
Code review updates
jonmmease Oct 22, 2018
5b0e461
Initialize template object when layout is assigned to a figure
jonmmease Oct 23, 2018
5c34bb5
Make zerolines same color as grid lines with heavier weight in plotly…
jonmmease Oct 23, 2018
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
64 changes: 61 additions & 3 deletions _plotly_utils/basevalidators.py
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,7 @@ def __init__(self, plotly_name, parent_name, role=None, **_):
self.parent_name = parent_name
self.plotly_name = plotly_name
self.role = role
self.array_ok = False

def description(self):
"""
Expand Down Expand Up @@ -322,6 +323,8 @@ def __init__(self, plotly_name, parent_name, **kwargs):
super(DataArrayValidator, self).__init__(
plotly_name=plotly_name, parent_name=parent_name, **kwargs)

self.array_ok = True

def description(self):
return ("""\
The '{plotly_name}' property is an array that may be specified as a tuple,
Expand Down Expand Up @@ -1908,7 +1911,7 @@ def validate_coerce(self, v, skip_invalid=False):
v = self.data_class()

elif isinstance(v, dict):
v = self.data_class(skip_invalid=skip_invalid, **v)
v = self.data_class(v, skip_invalid=skip_invalid)

elif isinstance(v, self.data_class):
# Copy object
Expand Down Expand Up @@ -1976,8 +1979,8 @@ def validate_coerce(self, v, skip_invalid=False):
if isinstance(v_el, self.data_class):
res.append(self.data_class(v_el))
elif isinstance(v_el, dict):
res.append(self.data_class(skip_invalid=skip_invalid,
**v_el))
res.append(self.data_class(v_el,
skip_invalid=skip_invalid))
else:
if skip_invalid:
res.append(self.data_class())
Expand Down Expand Up @@ -2123,3 +2126,58 @@ def validate_coerce(self, v, skip_invalid=False):
self.raise_invalid_val(v)

return v


class BaseTemplateValidator(CompoundValidator):

def __init__(self,
plotly_name,
parent_name,
data_class_str,
data_docs,
**kwargs):

super(BaseTemplateValidator, self).__init__(
plotly_name=plotly_name,
parent_name=parent_name,
data_class_str=data_class_str,
data_docs=data_docs,
**kwargs
)

def description(self):
compound_description = super(BaseTemplateValidator, self).description()
compound_description += """
- The name of a registered template where current registered templates
are stored in the plotly.io.templates configuration object. The names
of all registered templates can be retrieved with:
>>> import plotly.io as pio
>>> list(pio.templates)
- A string containing multiple registered template names, joined on '+'
characters (e.g. 'template1+template2'). In this case the resulting
template is computed by merging together the collection of registered
templates"""

return compound_description

def validate_coerce(self, v, skip_invalid=False):
import plotly.io as pio

try:
# Check if v is a template identifier
# (could be any hashable object)
if v in pio.templates:
return copy.deepcopy(pio.templates[v])
# Otherwise, if v is a string, check to see if it consists of
# multiple template names joined on '+' characters
elif isinstance(v, string_types):
template_names = v.split('+')
if all([name in pio.templates for name in template_names]):
return pio.templates.merge_templates(*template_names)

except TypeError:
# v is un-hashable
pass

return super(BaseTemplateValidator, self).validate_coerce(
v, skip_invalid=skip_invalid)
56 changes: 54 additions & 2 deletions codegen/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,12 @@
DEPRECATED_DATATYPES)
from codegen.figure import write_figure_classes
from codegen.utils import (TraceNode, PlotlyNode, LayoutNode, FrameNode,
write_init_py)
write_init_py, ElementDefaultsNode)
from codegen.validators import (write_validator_py,
write_data_validator_py,
get_data_validator_instance)


# Import notes
# ------------
# Nothing from the plotly/ package should be imported during code
Expand All @@ -22,6 +23,52 @@
# codegen/ package, and helpers used both during code generation and at
# runtime should reside in the _plotly_utils/ package.
# ----------------------------------------------------------------------------
def preprocess_schema(plotly_schema):
"""
Central location to make changes to schema before it's seen by the
PlotlyNode classes
"""

# Update template
# ---------------
layout = plotly_schema['layout']['layoutAttributes']

# Create codegen-friendly template scheme
template = {
"data": {
trace + 's': {
'items': {
trace: {
},
},
"role": "object"
}
for trace in plotly_schema['traces']
},
"layout": {
},
"description": """\
Default attributes to be applied to the plot.
This should be a dict with format: `{'layout': layoutTemplate, 'data':
{trace_type: [traceTemplate, ...], ...}}` where `layoutTemplate` is a dict
matching the structure of `figure.layout` and `traceTemplate` is a dict
matching the structure of the trace with type `trace_type` (e.g. 'scatter').
Alternatively, this may be specified as an instance of
plotly.graph_objs.layout.Template.

Trace templates are applied cyclically to
traces of each type. Container arrays (eg `annotations`) have special
handling: An object ending in `defaults` (eg `annotationdefaults`) is
applied to each array item. But if an item has a `templateitemname`
key we look in the template array for an item with matching `name` and
apply that instead. If no matching `name` is found we mark the item
invisible. Any named template item not referenced is appended to the
end of the array, so this can be used to add a watermark annotation or a
logo image, for example. To omit one of these items on the plot, make
an item with matching `templateitemname` and `visible: false`."""
}

layout['template'] = template


def perform_codegen():
Expand Down Expand Up @@ -52,6 +99,10 @@ def perform_codegen():
with open('plotly/package_data/plot-schema.json', 'r') as f:
plotly_schema = json.load(f)

# Preprocess Schema
# -----------------
preprocess_schema(plotly_schema)

# Build node lists
# ----------------
# ### TraceNode ###
Expand Down Expand Up @@ -81,7 +132,8 @@ def perform_codegen():
all_frame_nodes)

all_compound_nodes = [node for node in all_datatype_nodes
if node.is_compound]
if node.is_compound and
not isinstance(node, ElementDefaultsNode)]

# Write out validators
# --------------------
Expand Down
27 changes: 25 additions & 2 deletions codegen/datatypes.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,18 @@ def build_datatype_py(node):
# ---------------
assert node.is_compound

# Handle template traces
# ----------------------
# We want template trace/layout classes like
# plotly.graph_objs.layout.template.data.Scatter to map to the
# corresponding trace/layout class (e.g. plotly.graph_objs.Scatter).
# So rather than generate a class definition, we just import the
# corresponding trace/layout class
if node.parent_path_str == 'layout.template.data':
return f"from plotly.graph_objs import {node.name_datatype_class}"
elif node.path_str == 'layout.template.layout':
return "from plotly.graph_objs import Layout"

# Extract node properties
# -----------------------
undercase = node.name_undercase
Expand Down Expand Up @@ -244,7 +256,17 @@ def __init__(self""")
# ----------------------------------""")
for subtype_node in subtype_nodes:
name_prop = subtype_node.name_property
buffer.write(f"""
if name_prop == 'template':
# Special handling for layout.template to avoid infinite
# recursion. Only initialize layout.template object if non-None
# value specified
buffer.write(f"""
_v = arg.pop('{name_prop}', None)
_v = {name_prop} if {name_prop} is not None else _v
if _v is not None:
self['{name_prop}'] = _v""")
else:
buffer.write(f"""
_v = arg.pop('{name_prop}', None)
self['{name_prop}'] = {name_prop} \
if {name_prop} is not None else _v""")
Expand All @@ -264,7 +286,8 @@ def __init__(self""")
self._props['{lit_name}'] = {lit_val}
self._validators['{lit_name}'] =\
LiteralValidator(plotly_name='{lit_name}',\
parent_name='{lit_parent}', val={lit_val})""")
parent_name='{lit_parent}', val={lit_val})
arg.pop('{lit_name}', None)""")

buffer.write(f"""

Expand Down
98 changes: 93 additions & 5 deletions codegen/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,7 @@ def format_description(desc):
# Mapping from full property paths to custom validator classes
CUSTOM_VALIDATOR_DATATYPES = {
'layout.image.source': '_plotly_utils.basevalidators.ImageUriValidator',
'layout.template': '_plotly_utils.basevalidators.BaseTemplateValidator',
'frame.data': 'plotly.validators.DataValidator',
'frame.layout': 'plotly.validators.LayoutValidator'
}
Expand Down Expand Up @@ -257,9 +258,14 @@ def __init__(self, plotly_schema, node_path=(), parent=None):
# Note the node_data is a property that must be computed by the
# subclass based on plotly_schema and node_path
if isinstance(self.node_data, dict_like):
childs_parent = (
parent
if self.node_path and self.node_path[-1] == 'items'
else self)

self._children = [self.__class__(self.plotly_schema,
node_path=self.node_path + (c,),
parent=self)
parent=childs_parent)
for c in self.node_data if c and c[0] != '_']

# Sort by plotly name
Expand Down Expand Up @@ -387,7 +393,15 @@ def name_property(self):
-------
str
"""
return self.plotly_name + ('s' if self.is_array_element else '')

return self.plotly_name + (
's' if self.is_array_element and
# Don't add 's' to layout.template.data.scatter etc.
not (self.parent and
self.parent.parent and
self.parent.parent.parent and
self.parent.parent.parent.name_property == 'template')
else '')

@property
def name_validator_class(self) -> str:
Expand Down Expand Up @@ -600,8 +614,8 @@ def is_array_element(self):
-------
bool
"""
if self.parent and self.parent.parent:
return self.parent.parent.is_array
if self.parent:
return self.parent.is_array
else:
return False

Expand Down Expand Up @@ -774,7 +788,16 @@ def child_datatypes(self):
nodes = []
for n in self.children:
if n.is_array:
# Add array element node
nodes.append(n.children[0].children[0])

# Add elementdefaults node. Require parent_path_parts not
# empty to avoid creating defaults classes for traces
if (n.parent_path_parts and
n.parent_path_parts != ('layout', 'template', 'data')):

nodes.append(ElementDefaultsNode(n, self.plotly_schema))

elif n.is_datatype:
nodes.append(n)

Expand Down Expand Up @@ -885,7 +908,11 @@ def get_all_compound_datatype_nodes(plotly_schema, node_class):
if node.plotly_name and not node.is_array:
nodes.append(node)

nodes_to_process.extend(node.child_compound_datatypes)
non_defaults_compound_children = [
node for node in node.child_compound_datatypes
if not isinstance(node, ElementDefaultsNode)]

nodes_to_process.extend(non_defaults_compound_children)

return nodes

Expand Down Expand Up @@ -1088,3 +1115,64 @@ def node_data(self) -> dict:
node_data = node_data[prop_name]

return node_data


class ElementDefaultsNode(PlotlyNode):

def __init__(self, array_node, plotly_schema):
"""
Create node that represents element defaults properties
(e.g. layout.annotationdefaults). Construct as a wrapper around the
corresponding array property node (e.g. layout.annotations)

Parameters
----------
array_node: PlotlyNode
"""
super().__init__(plotly_schema,
node_path=array_node.node_path,
parent=array_node.parent)

assert array_node.is_array
self.array_node = array_node
self.element_node = array_node.children[0].children[0]

@property
def node_data(self):
return {}

@property
def description(self):
array_property_path = (self.parent_path_str +
'.' + self.array_node.name_property)

if isinstance(self.array_node, TraceNode):
data_path = 'data.'
else:
data_path = ''

defaults_property_path = ('layout.template.' +
data_path +
self.parent_path_str +
'.' + self.plotly_name)
return f"""\
When used in a template
(as {defaults_property_path}),
sets the default property values to use for elements
of {array_property_path}"""

@property
def name_base_datatype(self):
return self.element_node.name_base_datatype

@property
def root_name(self):
return self.array_node.root_name

@property
def plotly_name(self):
return self.element_node.plotly_name + 'defaults'

@property
def name_datatype_class(self):
return self.element_node.name_datatype_class
3 changes: 3 additions & 0 deletions optional-requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ psutil
## codegen dependencies ##
yapf

## template generation ##
colorcet

## ipython ##
ipython

Expand Down
Loading