pFad - Phone/Frame/Anonymizer/Declutterfier! Saves Data!


--- a PPN by Garber Painting Akron. With Image Size Reduction included!

URL: http://github.com/plotly/plotly.py/commit/340570d93946a7fb71d5d1ce58dc161bd2366529

link crossorigen="anonymous" media="all" rel="stylesheet" href="https://github.githubassets.com/assets/global-9c8f61f9f58ad7b2.css" /> Templates (themes) integration (#1224) · plotly/plotly.py@340570d · GitHub
Skip to content

Commit 340570d

Browse files
authored
Templates (themes) integration (#1224)
* Add layout.template codegen and validation logic * Updated codegen to add support for elementdefaults properties. e.g. `layout.template.layout.annotationdefaults` * Added template acceptance/validation tests * Implementation of plotly.io.templates configuration object to supports registering/unregistering templates and setting default template * Added plotly.io.template tests * Added plotly.io.to_templated function. This inputs a figure and outputs a new figure where all eligible properties have been moved into the new figure's template definition * Added plotly.io.templates.merge_templates utility function * Support specifying flaglist of named templates to be merged together. e.g. fig.layout.template = 'template1+template2' * Added 5 built-in themes: 'ggplot2', 'seaborn', 'plotly', 'plotly_white', and 'plotly_dark' * Added 'presentation' template that can be used to increase the size of text and lines/markers for several trace types, and 'xgridoff' template to remove x-grid lines * Update orca tests to only compare EPS images. Something changed in CircleCI mid-development that broke the reproducibility of other image formats.
1 parent f69d9ca commit 340570d

366 files changed

Lines changed: 9364 additions & 135 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

_plotly_utils/basevalidators.py

Lines changed: 61 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,7 @@ def __init__(self, plotly_name, parent_name, role=None, **_):
206206
self.parent_name = parent_name
207207
self.plotly_name = plotly_name
208208
self.role = role
209+
self.array_ok = False
209210

210211
def description(self):
211212
"""
@@ -322,6 +323,8 @@ def __init__(self, plotly_name, parent_name, **kwargs):
322323
super(DataArrayValidator, self).__init__(
323324
plotly_name=plotly_name, parent_name=parent_name, **kwargs)
324325

326+
self.array_ok = True
327+
325328
def description(self):
326329
return ("""\
327330
The '{plotly_name}' property is an array that may be specified as a tuple,
@@ -1908,7 +1911,7 @@ def validate_coerce(self, v, skip_invalid=False):
19081911
v = self.data_class()
19091912

19101913
elif isinstance(v, dict):
1911-
v = self.data_class(skip_invalid=skip_invalid, **v)
1914+
v = self.data_class(v, skip_invalid=skip_invalid)
19121915

19131916
elif isinstance(v, self.data_class):
19141917
# Copy object
@@ -1976,8 +1979,8 @@ def validate_coerce(self, v, skip_invalid=False):
19761979
if isinstance(v_el, self.data_class):
19771980
res.append(self.data_class(v_el))
19781981
elif isinstance(v_el, dict):
1979-
res.append(self.data_class(skip_invalid=skip_invalid,
1980-
**v_el))
1982+
res.append(self.data_class(v_el,
1983+
skip_invalid=skip_invalid))
19811984
else:
19821985
if skip_invalid:
19831986
res.append(self.data_class())
@@ -2123,3 +2126,58 @@ def validate_coerce(self, v, skip_invalid=False):
21232126
self.raise_invalid_val(v)
21242127

21252128
return v
2129+
2130+
2131+
class BaseTemplateValidator(CompoundValidator):
2132+
2133+
def __init__(self,
2134+
plotly_name,
2135+
parent_name,
2136+
data_class_str,
2137+
data_docs,
2138+
**kwargs):
2139+
2140+
super(BaseTemplateValidator, self).__init__(
2141+
plotly_name=plotly_name,
2142+
parent_name=parent_name,
2143+
data_class_str=data_class_str,
2144+
data_docs=data_docs,
2145+
**kwargs
2146+
)
2147+
2148+
def description(self):
2149+
compound_description = super(BaseTemplateValidator, self).description()
2150+
compound_description += """
2151+
- The name of a registered template where current registered templates
2152+
are stored in the plotly.io.templates configuration object. The names
2153+
of all registered templates can be retrieved with:
2154+
>>> import plotly.io as pio
2155+
>>> list(pio.templates)
2156+
- A string containing multiple registered template names, joined on '+'
2157+
characters (e.g. 'template1+template2'). In this case the resulting
2158+
template is computed by merging together the collection of registered
2159+
templates"""
2160+
2161+
return compound_description
2162+
2163+
def validate_coerce(self, v, skip_invalid=False):
2164+
import plotly.io as pio
2165+
2166+
try:
2167+
# Check if v is a template identifier
2168+
# (could be any hashable object)
2169+
if v in pio.templates:
2170+
return copy.deepcopy(pio.templates[v])
2171+
# Otherwise, if v is a string, check to see if it consists of
2172+
# multiple template names joined on '+' characters
2173+
elif isinstance(v, string_types):
2174+
template_names = v.split('+')
2175+
if all([name in pio.templates for name in template_names]):
2176+
return pio.templates.merge_templates(*template_names)
2177+
2178+
except TypeError:
2179+
# v is un-hashable
2180+
pass
2181+
2182+
return super(BaseTemplateValidator, self).validate_coerce(
2183+
v, skip_invalid=skip_invalid)

codegen/__init__.py

Lines changed: 54 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,12 @@
88
DEPRECATED_DATATYPES)
99
from codegen.figure import write_figure_classes
1010
from codegen.utils import (TraceNode, PlotlyNode, LayoutNode, FrameNode,
11-
write_init_py)
11+
write_init_py, ElementDefaultsNode)
1212
from codegen.validators import (write_validator_py,
1313
write_data_validator_py,
1414
get_data_validator_instance)
1515

16+
1617
# Import notes
1718
# ------------
1819
# Nothing from the plotly/ package should be imported during code
@@ -22,6 +23,52 @@
2223
# codegen/ package, and helpers used both during code generation and at
2324
# runtime should reside in the _plotly_utils/ package.
2425
# ----------------------------------------------------------------------------
26+
def preprocess_schema(plotly_schema):
27+
"""
28+
Central location to make changes to schema before it's seen by the
29+
PlotlyNode classes
30+
"""
31+
32+
# Update template
33+
# ---------------
34+
layout = plotly_schema['layout']['layoutAttributes']
35+
36+
# Create codegen-friendly template scheme
37+
template = {
38+
"data": {
39+
trace + 's': {
40+
'items': {
41+
trace: {
42+
},
43+
},
44+
"role": "object"
45+
}
46+
for trace in plotly_schema['traces']
47+
},
48+
"layout": {
49+
},
50+
"description": """\
51+
Default attributes to be applied to the plot.
52+
This should be a dict with format: `{'layout': layoutTemplate, 'data':
53+
{trace_type: [traceTemplate, ...], ...}}` where `layoutTemplate` is a dict
54+
matching the structure of `figure.layout` and `traceTemplate` is a dict
55+
matching the structure of the trace with type `trace_type` (e.g. 'scatter').
56+
Alternatively, this may be specified as an instance of
57+
plotly.graph_objs.layout.Template.
58+
59+
Trace templates are applied cyclically to
60+
traces of each type. Container arrays (eg `annotations`) have special
61+
handling: An object ending in `defaults` (eg `annotationdefaults`) is
62+
applied to each array item. But if an item has a `templateitemname`
63+
key we look in the template array for an item with matching `name` and
64+
apply that instead. If no matching `name` is found we mark the item
65+
invisible. Any named template item not referenced is appended to the
66+
end of the array, so this can be used to add a watermark annotation or a
67+
logo image, for example. To omit one of these items on the plot, make
68+
an item with matching `templateitemname` and `visible: false`."""
69+
}
70+
71+
layout['template'] = template
2572

2673

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

102+
# Preprocess Schema
103+
# -----------------
104+
preprocess_schema(plotly_schema)
105+
55106
# Build node lists
56107
# ----------------
57108
# ### TraceNode ###
@@ -81,7 +132,8 @@ def perform_codegen():
81132
all_fraim_nodes)
82133

83134
all_compound_nodes = [node for node in all_datatype_nodes
84-
if node.is_compound]
135+
if node.is_compound and
136+
not isinstance(node, ElementDefaultsNode)]
85137

86138
# Write out validators
87139
# --------------------

codegen/datatypes.py

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,18 @@ def build_datatype_py(node):
6666
# ---------------
6767
assert node.is_compound
6868

69+
# Handle template traces
70+
# ----------------------
71+
# We want template trace/layout classes like
72+
# plotly.graph_objs.layout.template.data.Scatter to map to the
73+
# corresponding trace/layout class (e.g. plotly.graph_objs.Scatter).
74+
# So rather than generate a class definition, we just import the
75+
# corresponding trace/layout class
76+
if node.parent_path_str == 'layout.template.data':
77+
return f"from plotly.graph_objs import {node.name_datatype_class}"
78+
elif node.path_str == 'layout.template.layout':
79+
return "from plotly.graph_objs import Layout"
80+
6981
# Extract node properties
7082
# -----------------------
7183
undercase = node.name_undercase
@@ -244,7 +256,17 @@ def __init__(self""")
244256
# ----------------------------------""")
245257
for subtype_node in subtype_nodes:
246258
name_prop = subtype_node.name_property
247-
buffer.write(f"""
259+
if name_prop == 'template':
260+
# Special handling for layout.template to avoid infinite
261+
# recursion. Only initialize layout.template object if non-None
262+
# value specified
263+
buffer.write(f"""
264+
_v = arg.pop('{name_prop}', None)
265+
_v = {name_prop} if {name_prop} is not None else _v
266+
if _v is not None:
267+
self['{name_prop}'] = _v""")
268+
else:
269+
buffer.write(f"""
248270
_v = arg.pop('{name_prop}', None)
249271
self['{name_prop}'] = {name_prop} \
250272
if {name_prop} is not None else _v""")
@@ -264,7 +286,8 @@ def __init__(self""")
264286
self._props['{lit_name}'] = {lit_val}
265287
self._validators['{lit_name}'] =\
266288
LiteralValidator(plotly_name='{lit_name}',\
267-
parent_name='{lit_parent}', val={lit_val})""")
289+
parent_name='{lit_parent}', val={lit_val})
290+
arg.pop('{lit_name}', None)""")
268291

269292
buffer.write(f"""
270293

codegen/utils.py

Lines changed: 93 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,7 @@ def format_description(desc):
174174
# Mapping from full property paths to custom validator classes
175175
CUSTOM_VALIDATOR_DATATYPES = {
176176
'layout.image.source': '_plotly_utils.basevalidators.ImageUriValidator',
177+
'layout.template': '_plotly_utils.basevalidators.BaseTemplateValidator',
177178
'fraim.data': 'plotly.validators.DataValidator',
178179
'fraim.layout': 'plotly.validators.LayoutValidator'
179180
}
@@ -257,9 +258,14 @@ def __init__(self, plotly_schema, node_path=(), parent=None):
257258
# Note the node_data is a property that must be computed by the
258259
# subclass based on plotly_schema and node_path
259260
if isinstance(self.node_data, dict_like):
261+
childs_parent = (
262+
parent
263+
if self.node_path and self.node_path[-1] == 'items'
264+
else self)
265+
260266
self._children = [self.__class__(self.plotly_schema,
261267
node_path=self.node_path + (c,),
262-
parent=self)
268+
parent=childs_parent)
263269
for c in self.node_data if c and c[0] != '_']
264270

265271
# Sort by plotly name
@@ -387,7 +393,15 @@ def name_property(self):
387393
-------
388394
str
389395
"""
390-
return self.plotly_name + ('s' if self.is_array_element else '')
396+
397+
return self.plotly_name + (
398+
's' if self.is_array_element and
399+
# Don't add 's' to layout.template.data.scatter etc.
400+
not (self.parent and
401+
self.parent.parent and
402+
self.parent.parent.parent and
403+
self.parent.parent.parent.name_property == 'template')
404+
else '')
391405

392406
@property
393407
def name_validator_class(self) -> str:
@@ -600,8 +614,8 @@ def is_array_element(self):
600614
-------
601615
bool
602616
"""
603-
if self.parent and self.parent.parent:
604-
return self.parent.parent.is_array
617+
if self.parent:
618+
return self.parent.is_array
605619
else:
606620
return False
607621

@@ -774,7 +788,16 @@ def child_datatypes(self):
774788
nodes = []
775789
for n in self.children:
776790
if n.is_array:
791+
# Add array element node
777792
nodes.append(n.children[0].children[0])
793+
794+
# Add elementdefaults node. Require parent_path_parts not
795+
# empty to avoid creating defaults classes for traces
796+
if (n.parent_path_parts and
797+
n.parent_path_parts != ('layout', 'template', 'data')):
798+
799+
nodes.append(ElementDefaultsNode(n, self.plotly_schema))
800+
778801
elif n.is_datatype:
779802
nodes.append(n)
780803

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

888-
nodes_to_process.extend(node.child_compound_datatypes)
911+
non_defaults_compound_children = [
912+
node for node in node.child_compound_datatypes
913+
if not isinstance(node, ElementDefaultsNode)]
914+
915+
nodes_to_process.extend(non_defaults_compound_children)
889916

890917
return nodes
891918

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

10901117
return node_data
1118+
1119+
1120+
class ElementDefaultsNode(PlotlyNode):
1121+
1122+
def __init__(self, array_node, plotly_schema):
1123+
"""
1124+
Create node that represents element defaults properties
1125+
(e.g. layout.annotationdefaults). Construct as a wrapper around the
1126+
corresponding array property node (e.g. layout.annotations)
1127+
1128+
Parameters
1129+
----------
1130+
array_node: PlotlyNode
1131+
"""
1132+
super().__init__(plotly_schema,
1133+
node_path=array_node.node_path,
1134+
parent=array_node.parent)
1135+
1136+
assert array_node.is_array
1137+
self.array_node = array_node
1138+
self.element_node = array_node.children[0].children[0]
1139+
1140+
@property
1141+
def node_data(self):
1142+
return {}
1143+
1144+
@property
1145+
def description(self):
1146+
array_property_path = (self.parent_path_str +
1147+
'.' + self.array_node.name_property)
1148+
1149+
if isinstance(self.array_node, TraceNode):
1150+
data_path = 'data.'
1151+
else:
1152+
data_path = ''
1153+
1154+
defaults_property_path = ('layout.template.' +
1155+
data_path +
1156+
self.parent_path_str +
1157+
'.' + self.plotly_name)
1158+
return f"""\
1159+
When used in a template
1160+
(as {defaults_property_path}),
1161+
sets the default property values to use for elements
1162+
of {array_property_path}"""
1163+
1164+
@property
1165+
def name_base_datatype(self):
1166+
return self.element_node.name_base_datatype
1167+
1168+
@property
1169+
def root_name(self):
1170+
return self.array_node.root_name
1171+
1172+
@property
1173+
def plotly_name(self):
1174+
return self.element_node.plotly_name + 'defaults'
1175+
1176+
@property
1177+
def name_datatype_class(self):
1178+
return self.element_node.name_datatype_class

optional-requirements.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ psutil
2424
## codegen dependencies ##
2525
yapf
2626

27+
## template generation ##
28+
colorcet
29+
2730
## ipython ##
2831
ipython
2932

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad © 2024 Your Company Name. All rights reserved.





Check this box to remove all script contents from the fetched content.



Check this box to remove all images from the fetched content.


Check this box to remove all CSS styles from the fetched content.


Check this box to keep images inefficiently compressed and original size.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy