Skip to content

Commit 240d0b4

Browse files
authored
Add convenience methods for annotations, shapes, and images (#1817)
* Add _select_annotations_like helper method * select/for_each/update annotations methods * code generation for select/for_each/update annotations/shapes/images * Add add_* methods for annotations/shapes/images * add add/select/update tests for annotations * Add add/select tests without row/col on figure not created with make_subplots
1 parent 573f953 commit 240d0b4

File tree

7 files changed

+2867
-12
lines changed

7 files changed

+2867
-12
lines changed

Diff for: packages/python/plotly/codegen/__init__.py

+7
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,12 @@ def perform_codegen():
143143
if node.node_data.get("_isSubplotObj", False)
144144
]
145145

146+
layout_array_nodes = [
147+
node
148+
for node in layout_node.child_compound_datatypes
149+
if node.is_array_element and node.has_child("xref") and node.has_child("yref")
150+
]
151+
146152
# ### FrameNode ###
147153
compound_frame_nodes = PlotlyNode.get_all_compound_datatype_nodes(
148154
plotly_schema, FrameNode
@@ -210,6 +216,7 @@ def perform_codegen():
210216
layout_validator,
211217
frame_validator,
212218
subplot_nodes,
219+
layout_array_nodes,
213220
)
214221

215222
# Write datatype __init__.py files

Diff for: packages/python/plotly/codegen/figure.py

+251-5
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ def build_figure_py(
2525
layout_validator,
2626
frame_validator,
2727
subplot_nodes,
28+
layout_array_nodes,
2829
):
2930
"""
3031
@@ -47,6 +48,8 @@ def build_figure_py(
4748
FrameValidator instance
4849
subplot_nodes: list of str
4950
List of names of all of the layout subplot properties
51+
layout_array_nodes: list of PlotlyNode
52+
List of array nodes under layout that can be positioned using xref/yref
5053
Returns
5154
-------
5255
str
@@ -66,8 +69,10 @@ def build_figure_py(
6669
# ### Import base class ###
6770
buffer.write(f"from plotly.{base_package} import {base_classname}\n")
6871

69-
# ### Import trace graph_obj classes ###
70-
trace_types_csv = ", ".join([n.name_datatype_class for n in trace_nodes])
72+
# ### Import trace graph_obj classes / layout ###
73+
trace_types_csv = ", ".join(
74+
[n.name_datatype_class for n in trace_nodes] + ["layout as _layout"]
75+
)
7176
buffer.write(f"from plotly.graph_objs import ({trace_types_csv})\n")
7277

7378
# Write class definition
@@ -358,14 +363,253 @@ def update_{plural_name}(
358363
return self"""
359364
)
360365

366+
# update annotations/shapes/images
367+
# --------------------------------
368+
for node in layout_array_nodes:
369+
singular_name = node.plotly_name
370+
plural_name = node.name_property
371+
372+
if singular_name == "image":
373+
# Rename image to layout_image to avoid conflict with an image trace
374+
method_prefix = "layout_"
375+
else:
376+
method_prefix = ""
377+
378+
buffer.write(
379+
f"""
380+
def select_{method_prefix}{plural_name}(
381+
self, selector=None, row=None, col=None, secondary_y=None
382+
):
383+
\"\"\"
384+
Select {plural_name} from a particular subplot cell and/or {plural_name}
385+
that satisfy custom selection criteria.
386+
387+
Parameters
388+
----------
389+
selector: dict or None (default None)
390+
Dict to use as selection criteria.
391+
Annotations will be selected if they contain properties corresponding
392+
to all of the dictionary's keys, with values that exactly match
393+
the supplied values. If None (the default), all {plural_name} are
394+
selected.
395+
row, col: int or None (default None)
396+
Subplot row and column index of {plural_name} to select.
397+
To select {plural_name} by row and column, the Figure must have been
398+
created using plotly.subplots.make_subplots. To select only those
399+
{singular_name} that are in paper coordinates, set row and col to the
400+
string 'paper'. If None (the default), all {plural_name} are selected.
401+
secondary_y: boolean or None (default None)
402+
* If True, only select {plural_name} associated with the secondary
403+
y-axis of the subplot.
404+
* If False, only select {plural_name} associated with the primary
405+
y-axis of the subplot.
406+
* If None (the default), do not filter {plural_name} based on secondary
407+
y-axis.
408+
409+
To select {plural_name} by secondary y-axis, the Figure must have been
410+
created using plotly.subplots.make_subplots. See the docstring
411+
for the specs argument to make_subplots for more info on
412+
creating subplots with secondary y-axes.
413+
Returns
414+
-------
415+
generator
416+
Generator that iterates through all of the {plural_name} that satisfy
417+
all of the specified selection criteria
418+
\"\"\"
419+
return self._select_annotations_like(
420+
"{plural_name}", selector=selector, row=row, col=col, secondary_y=secondary_y
421+
)
422+
423+
def for_each_{method_prefix}{singular_name}(
424+
self, fn, selector=None, row=None, col=None, secondary_y=None
425+
):
426+
\"\"\"
427+
Apply a function to all {plural_name} that satisfy the specified selection
428+
criteria
429+
430+
Parameters
431+
----------
432+
fn:
433+
Function that inputs a single {singular_name} object.
434+
selector: dict or None (default None)
435+
Dict to use as selection criteria.
436+
Traces will be selected if they contain properties corresponding
437+
to all of the dictionary's keys, with values that exactly match
438+
the supplied values. If None (the default), all {plural_name} are
439+
selected.
440+
row, col: int or None (default None)
441+
Subplot row and column index of {plural_name} to select.
442+
To select {plural_name} by row and column, the Figure must have been
443+
created using plotly.subplots.make_subplots. To select only those
444+
{plural_name} that are in paper coordinates, set row and col to the
445+
string 'paper'. If None (the default), all {plural_name} are selected.
446+
secondary_y: boolean or None (default None)
447+
* If True, only select {plural_name} associated with the secondary
448+
y-axis of the subplot.
449+
* If False, only select {plural_name} associated with the primary
450+
y-axis of the subplot.
451+
* If None (the default), do not filter {plural_name} based on secondary
452+
y-axis.
453+
454+
To select {plural_name} by secondary y-axis, the Figure must have been
455+
created using plotly.subplots.make_subplots. See the docstring
456+
for the specs argument to make_subplots for more info on
457+
creating subplots with secondary y-axes.
458+
Returns
459+
-------
460+
self
461+
Returns the Figure object that the method was called on
462+
\"\"\"
463+
for obj in self._select_annotations_like(
464+
prop='{plural_name}',
465+
selector=selector,
466+
row=row,
467+
col=col,
468+
secondary_y=secondary_y,
469+
):
470+
fn(obj)
471+
472+
return self
473+
474+
def update_{method_prefix}{plural_name}(
475+
self,
476+
patch,
477+
selector=None,
478+
row=None,
479+
col=None,
480+
secondary_y=None,
481+
**kwargs
482+
):
483+
\"\"\"
484+
Perform a property update operation on all {plural_name} that satisfy the
485+
specified selection criteria
486+
487+
Parameters
488+
----------
489+
patch: dict or None (default None)
490+
Dictionary of property updates to be applied to all {plural_name} that
491+
satisfy the selection criteria.
492+
selector: dict or None (default None)
493+
Dict to use as selection criteria.
494+
Traces will be selected if they contain properties corresponding
495+
to all of the dictionary's keys, with values that exactly match
496+
the supplied values. If None (the default), all {plural_name} are
497+
selected.
498+
row, col: int or None (default None)
499+
Subplot row and column index of {plural_name} to select.
500+
To select {plural_name} by row and column, the Figure must have been
501+
created using plotly.subplots.make_subplots. To select only those
502+
{singular_name} that are in paper coordinates, set row and col to the
503+
string 'paper'. If None (the default), all {plural_name} are selected.
504+
secondary_y: boolean or None (default None)
505+
* If True, only select {plural_name} associated with the secondary
506+
y-axis of the subplot.
507+
* If False, only select {plural_name} associated with the primary
508+
y-axis of the subplot.
509+
* If None (the default), do not filter {plural_name} based on secondary
510+
y-axis.
511+
512+
To select {plural_name} by secondary y-axis, the Figure must have been
513+
created using plotly.subplots.make_subplots. See the docstring
514+
for the specs argument to make_subplots for more info on
515+
creating subplots with secondary y-axes.
516+
**kwargs
517+
Additional property updates to apply to each selected {singular_name}. If
518+
a property is specified in both patch and in **kwargs then the
519+
one in **kwargs takes precedence.
520+
521+
Returns
522+
-------
523+
self
524+
Returns the Figure object that the method was called on
525+
\"\"\"
526+
for obj in self._select_annotations_like(
527+
prop='{plural_name}',
528+
selector=selector,
529+
row=row,
530+
col=col,
531+
secondary_y=secondary_y,
532+
):
533+
obj.update(patch, **kwargs)
534+
535+
return self
536+
"""
537+
)
538+
# Add layout array items
539+
buffer.write(
540+
f"""
541+
def add_{method_prefix}{singular_name}(self"""
542+
)
543+
add_constructor_params(
544+
buffer,
545+
node.child_datatypes,
546+
prepend_extras=["arg"],
547+
append_extras=["row", "col", "secondary_y"],
548+
)
549+
550+
prepend_extras = [
551+
(
552+
"arg",
553+
f"instance of {node.name_datatype_class} or dict with "
554+
"compatible properties",
555+
)
556+
]
557+
append_extras = [
558+
("row", f"Subplot row for {singular_name}"),
559+
("col", f"Subplot column for {singular_name}"),
560+
("secondary_y", f"Whether to add {singular_name} to secondary y-axis"),
561+
]
562+
add_docstring(
563+
buffer,
564+
node,
565+
header=f"Create and add a new {singular_name} to the figure's layout",
566+
prepend_extras=prepend_extras,
567+
append_extras=append_extras,
568+
return_type=fig_classname,
569+
)
570+
571+
# #### Function body ####
572+
buffer.write(
573+
f"""
574+
new_obj = _layout.{node.name_datatype_class}(arg,
575+
"""
576+
)
577+
578+
for i, subtype_node in enumerate(node.child_datatypes):
579+
subtype_prop_name = subtype_node.name_property
580+
buffer.write(
581+
f"""
582+
{subtype_prop_name}={subtype_prop_name},"""
583+
)
584+
585+
buffer.write("""**kwargs)""")
586+
587+
buffer.write(
588+
f"""
589+
return self._add_annotation_like(
590+
'{singular_name}',
591+
'{plural_name}',
592+
new_obj,
593+
row=row,
594+
col=col,
595+
secondary_y=secondary_y,
596+
)"""
597+
)
598+
361599
# Return source string
362600
# --------------------
363601
buffer.write("\n")
364602
return buffer.getvalue()
365603

366604

367605
def write_figure_classes(
368-
outdir, trace_node, data_validator, layout_validator, frame_validator, subplot_nodes
606+
outdir,
607+
trace_node,
608+
data_validator,
609+
layout_validator,
610+
frame_validator,
611+
subplot_nodes,
612+
layout_array_nodes,
369613
):
370614
"""
371615
Construct source code for the Figure and FigureWidget classes and
@@ -385,9 +629,10 @@ def write_figure_classes(
385629
LayoutValidator instance
386630
frame_validator : CompoundArrayValidator
387631
FrameValidator instance
388-
subplot_nodes: list of str
632+
subplot_nodes: list of PlotlyNode
389633
List of names of all of the layout subplot properties
390-
634+
layout_array_nodes: list of PlotlyNode
635+
List of array nodes under layout that can be positioned using xref/yref
391636
Returns
392637
-------
393638
None
@@ -420,6 +665,7 @@ def write_figure_classes(
420665
layout_validator,
421666
frame_validator,
422667
subplot_nodes,
668+
layout_array_nodes,
423669
)
424670

425671
# ### Format and write to file###

Diff for: packages/python/plotly/codegen/utils.py

+6
Original file line numberDiff line numberDiff line change
@@ -893,6 +893,12 @@ def child_literals(self) -> List["PlotlyNode"]:
893893
"""
894894
return [n for n in self.children if n.is_literal]
895895

896+
def has_child(self, name) -> bool:
897+
"""
898+
Check whether node has child of the specified name
899+
"""
900+
return bool([n for n in self.children if n.plotly_name == name])
901+
896902
def get_constructor_params_docstring(self, indent=12):
897903
"""
898904
Return a docstring-style string containing the names and

0 commit comments

Comments
 (0)