Skip to content

Commit 4065a65

Browse files
Merge pull request #2737 from plotly/full_fig
full_figure_for_development
2 parents 9fab582 + 293d873 commit 4065a65

File tree

16 files changed

+283
-14
lines changed

16 files changed

+283
-14
lines changed

Diff for: .circleci/config.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ jobs:
99
- checkout
1010
- run:
1111
name: Install black
12-
command: "sudo pip install black"
12+
command: "sudo pip install black==19.10b0"
1313
- run:
1414
name: Check formatting with black
1515
command: "black --check ."

Diff for: CHANGELOG.md

+2-1
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@ This project adheres to [Semantic Versioning](http://semver.org/).
44

55
## [4.10.0] - unreleased
66

7+
### Added
78

8-
## [4.9.1] - unreleased
9+
- Added `plotly.io.full_figure_for_development()` and `plotly.graph_objects.Figure.full_figure_for_development()` ([#2737](https://github.com/plotly/plotly.py/pull/2737))
910

1011

1112
## [4.9.0] - 2020-07-16

Diff for: doc/python/figure-introspection.md

+175
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
---
2+
jupyter:
3+
jupytext:
4+
notebook_metadata_filter: all
5+
text_representation:
6+
extension: .md
7+
format_name: markdown
8+
format_version: '1.2'
9+
jupytext_version: 1.4.2
10+
kernelspec:
11+
display_name: Python 3
12+
language: python
13+
name: python3
14+
language_info:
15+
codemirror_mode:
16+
name: ipython
17+
version: 3
18+
file_extension: .py
19+
mimetype: text/x-python
20+
name: python
21+
nbconvert_exporter: python
22+
pygments_lexer: ipython3
23+
version: 3.7.7
24+
plotly:
25+
description: How to dig into and learn more about the figure data structure.
26+
display_as: file_settings
27+
language: python
28+
layout: base
29+
name: Introspecting Figures
30+
order: 35
31+
page_type: u-guide
32+
permalink: python/figure-introspection/
33+
thumbnail: thumbnail/violin.jpg
34+
---
35+
36+
### The Figure Lifecycle
37+
38+
As explained in the [Figure Data Structure documentation](/python/figure-structure/), when building a figure object with Plotly.py, it is not necessary to populate every possible attribute. At render-time, figure objects (whether generated via [Plotly Express](/python/plotly-express/) or [Graph Objects](/python/graph-objects/) are passed from Plotly.py to [Plotly.js](/javascript/), which is the Javascript library responsible for turning JSON descriptions of figures into graphical representations.
39+
40+
As part of this rendering process, Plotly.js will determine, based on the attributes that have been set, which other attributes require values in order to draw the figure. Plotly.js will then apply either static or dynamic defaults to all of the remaining required attributes and render the figure. A good example of a static default would be the text font size: if unspecified, the default value is always the same. A good example of a dynamic default would be the range of an axis: if unspecified, the default will be computed based on the range of the data in traces associated with that axis.
41+
42+
43+
### Introspecting Plotly Express Figures
44+
45+
Figure objects created by [Plotly Express](/python/plotly-express/) have a number of attributes automatically set, and these can be introspected using the Python `print()` function, or in JupyterLab, the special `fig.show("json")` renderer, which gives an interactive drilldown interface with search:
46+
47+
```python
48+
import plotly.express as px
49+
50+
fig = px.scatter(x=[10, 20], y=[20, 10], height=400, width=400)
51+
fig.show()
52+
print(fig)
53+
```
54+
55+
We can learn more about the attributes Plotly Express has set for us with the Python `help()` function:
56+
57+
```python
58+
help(fig.data[0].__class__.mode)
59+
```
60+
61+
### Accessing Javascript-Computed Defaults
62+
63+
_new in 4.10_
64+
65+
The `.full_figure_for_development()` method provides Python-level access to the default values computed by Plotly.js. This method requires [the Kaleido package](/python/static-image-export/), which is easy to install and also used for [static image export](/python/static-image-export/).
66+
67+
By way of example, here is an extremely simple figure created with [Graph Objects](/python/graph-objects/) (although it could have been made with [Plotly Express](/python/plotly-express/) as well just like above) where we have disabled the default template for maximum readability. Note how in this figure the text labels on the markers are clipped, and sit on top of the markers.
68+
69+
```python
70+
import plotly.graph_objects as go
71+
72+
fig = go.Figure(
73+
data=[go.Scatter(
74+
mode="markers+text",
75+
x=[10,20],
76+
y=[20, 10],
77+
text=["Point A", "Point B"]
78+
)],
79+
layout=dict(height=400, width=400, template="none")
80+
)
81+
fig.show()
82+
```
83+
84+
Let's print this figure to see the very small JSON object that is passed to Plotly.js as input:
85+
86+
```python
87+
print(fig)
88+
```
89+
90+
Now let's look at the "full" figure after Plotly.js has computed the default values for every necessary attribute.
91+
92+
> Heads-up: the full figure is quite long and intimidating, and this page is meant to help demystify things so **please read on**!
93+
94+
Please also note that the `.full_figure_for_development()` function is really meant for interactive learning and debugging, rather than production use, hence its name and the warning it produces by default, which you can see below, and which can be supressed with `warn=False`.
95+
96+
```python
97+
full_fig = fig.full_figure_for_development()
98+
print(full_fig)
99+
```
100+
101+
As you can see, Plotly.js does a lot of work filling things in for us! Let's look at the examples described at the top of the page of static and dynamic defaults. If we look just at `layout.font` and `layout.xaxis.range` we can see that the static default font size is 12 and that the dynamic default range is computed to be a bit beyond the data range which was 10-20:
102+
103+
```python
104+
print("full_fig.layout.font.size: ", full_fig.layout.font.size)
105+
print("full_fig.layout.xaxis.range: ", full_fig.layout.xaxis.range)
106+
```
107+
108+
### Learning About Attributes
109+
110+
111+
What else can we use this `full_fig` for? Let's start by looking at the first entry of the `data`
112+
113+
```python
114+
print(full_fig.data[0])
115+
```
116+
117+
We see that this is an instance of `go.Scatter` (as expected, given the input) and that it has an attribute we've maybe never heard of called `cliponaxis` which by default seems to be set to `True` in this case. Let's find out more about this attribute using the built-in Python `help()` function
118+
119+
```python
120+
help(go.Scatter.cliponaxis)
121+
```
122+
123+
Aha! This explains why in our original figure above, the text was cut off by the edge of the plotting area! Let's try forcing that to `False`, and let's also use the attribute `textposition` which we see in the full figure is by default set to `"middle center"` to get our text off of our markers:
124+
125+
```python
126+
fig.update_traces(cliponaxis=False, textposition="top right")
127+
fig.show()
128+
```
129+
130+
We can use this technique (of making a figure, and querying Plotly.js for the "full" version of that figure, and then exploring the attributes that are automatically set for us) to learn more about the range of possibilities that the figure schema makes available. We can drill down into `layout` attributes also:
131+
132+
```python
133+
help(go.layout.XAxis.autorange)
134+
```
135+
136+
### More about Layout
137+
138+
In the figure we introspected above, we had added [a `scatter` trace](/python/line-and-scatter/), and Plotly.js automatically filled in for us the `xaxis` and `yaxis` values of that trace object to be `x` and `y`, and then also filled out the corresponding `layout.xaxis` and `layout.yaxis` objects for us, complete with their [extensive set of defaults for gridlines, tick labels and so on](/python/axes/).
139+
140+
If we create a figure with [a `scattergeo` trace](/python/scatter-plots-on-maps/) instead, however, Plotly.js will fill in a totally different set of objects in `layout`, corresponding to [a `geo` subplot, with all of its defaults for whether or not to show rivers, lakes, country borders, coastlines etc](https://plotly.com/python/map-configuration/).
141+
142+
```python
143+
import plotly.graph_objects as go
144+
145+
fig = go.Figure(
146+
data=[go.Scattergeo(
147+
mode="markers+text",
148+
lat=[10, 20],
149+
lon=[20, 10],
150+
text=["Point A", "Point B"]
151+
)],
152+
layout=dict(height=400, width=400,
153+
margin=dict(l=0,r=0,b=0,t=0),
154+
template="none")
155+
)
156+
fig.show()
157+
full_fig = fig.full_figure_for_development()
158+
print(full_fig)
159+
```
160+
161+
If I then set `showrivers=True` and re-query the full figure, I see that new keys have appeared in the `layout.geo` object for `rivercolor` and `riverwidth`, showing the dynamic nature of these defaults.
162+
163+
```python
164+
fig.update_geos(showrivers=True)
165+
full_fig = fig.full_figure_for_development()
166+
print(full_fig.layout.geo)
167+
```
168+
169+
### Reference
170+
171+
You can learn more about [all the available attributes in the plotly figure schema](/python/reference/) (and read about its [high-level structure](/python/figure-structure/)) or about [all the classes and functions in the `plotly` module](/python-api-reference/).
172+
173+
```python
174+
175+
```

Diff for: doc/python/figure-structure.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ Attributes are referred to in text and in the [Figure Reference](/python/referen
5757

5858
The [`plotly.graph_objects` module contains an automatically-generated hierarchy of Python classes](/python/graph-objects/) which represent non-leaf attributes in the figure schema and provide a Pythonic API for them. When [manipulating a `plotly.graph_objects.Figure` object](/python/creating-and-updating-figures/), attributes can be set either directly using Python object attributes e.g. `fig.layout.title.font.family="Open Sans"` or using [update methods and "magic underscores"](/python/creating-and-updating-figures/#magic-underscore-notation) e.g. `fig.update_layout(title_font_family="Open Sans")`
5959

60-
When building a figure, it is *not necessary to populate every attribute* of every object. At render-time, the JavaScript layer will compute default values for each required unspecified attribute, depending upon the ones that are specified, as documented in the [Figure Reference](/python/reference/). An example of this would be `layout.xaxis.range`, which may be specified explicitly, but if not will be computed based on the range of `x` values for every trace linked to that axis. The JavaScript layer will ignore unknown attributes or malformed values, although the `plotly.graph_objects` module provides Python-side validation for attribute values. Note also that if [the `layout.template` key is present (as it is by default)](/python/templates/) then default values will be drawn first from the contents of the template and only if missing from there will the JavaScript layer infer further defaults. The built-in template can be disabled by setting `layout.template="none"`.
60+
When building a figure, it is *not necessary to populate every attribute* of every object. At render-time, [the JavaScript layer will compute default values](/python/figure-introspection/) for each required unspecified attribute, depending upon the ones that are specified, as documented in the [Figure Reference](/python/reference/). An example of this would be `layout.xaxis.range`, which may be specified explicitly, but if not will be computed based on the range of `x` values for every trace linked to that axis. The JavaScript layer will ignore unknown attributes or malformed values, although the `plotly.graph_objects` module provides Python-side validation for attribute values. Note also that if [the `layout.template` key is present (as it is by default)](/python/templates/) then default values will be drawn first from the contents of the template and only if missing from there will the JavaScript layer infer further defaults. The built-in template can be disabled by setting `layout.template="none"`.
6161

6262
### The Top-Level `data` Attribute
6363

Diff for: doc/requirements.txt

+1
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,4 @@ python-frontmatter
2525
datashader
2626
pyarrow
2727
cufflinks==0.17.3
28+
kaleido

Diff for: packages/python/plotly/plotly/basedatatypes.py

+30-1
Original file line numberDiff line numberDiff line change
@@ -1104,7 +1104,7 @@ def _add_annotation_like(
11041104
if refs[0].subplot_type != "xy":
11051105
raise ValueError(
11061106
"""
1107-
Cannot add {prop_singular} to subplot at position ({r}, {c}) because subplot
1107+
Cannot add {prop_singular} to subplot at position ({r}, {c}) because subplot
11081108
is of type {subplot_type}.""".format(
11091109
prop_singular=prop_singular,
11101110
r=row,
@@ -2896,6 +2896,35 @@ def to_json(self, *args, **kwargs):
28962896

28972897
return pio.to_json(self, *args, **kwargs)
28982898

2899+
def full_figure_for_development(self, warn=True, as_dict=False):
2900+
"""
2901+
Compute default values for all attributes not specified in the input figure and
2902+
returns the output as a "full" figure. This function calls Plotly.js via Kaleido
2903+
to populate unspecified attributes. This function is intended for interactive use
2904+
during development to learn more about how Plotly.js computes default values and is
2905+
not generally necessary or recommended for production use.
2906+
2907+
Parameters
2908+
----------
2909+
fig:
2910+
Figure object or dict representing a figure
2911+
2912+
warn: bool
2913+
If False, suppress warnings about not using this in production.
2914+
2915+
as_dict: bool
2916+
If True, output is a dict with some keys that go.Figure can't parse.
2917+
If False, output is a go.Figure with unparseable keys skipped.
2918+
2919+
Returns
2920+
-------
2921+
plotly.graph_objects.Figure or dict
2922+
The full figure
2923+
"""
2924+
import plotly.io as pio
2925+
2926+
return pio.full_figure_for_development(self, warn, as_dict)
2927+
28992928
def write_json(self, *args, **kwargs):
29002929
"""
29012930
Convert a figure to JSON and write it to a file or writeable

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

+3-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import sys
33

44
if sys.version_info < (3, 7):
5-
from ._kaleido import to_image, write_image
5+
from ._kaleido import to_image, write_image, full_figure_for_development
66
from . import orca, kaleido
77
from ._json import to_json, from_json, read_json, write_json
88
from ._templates import templates, to_templated
@@ -25,6 +25,7 @@
2525
"renderers",
2626
"show",
2727
"base_renderers",
28+
"full_figure_for_development",
2829
]
2930
else:
3031
__all__, __getattr__, __dir__ = relative_import(
@@ -33,6 +34,7 @@
3334
[
3435
"._kaleido.to_image",
3536
"._kaleido.write_image",
37+
"._kaleido.full_figure_for_development",
3638
"._json.to_json",
3739
"._json.from_json",
3840
"._json.read_json",

Diff for: packages/python/plotly/plotly/io/_kaleido.py

+57-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from __future__ import absolute_import
22
from six import string_types
33
import os
4+
import json
45
import plotly
56
from plotly.io._utils import validate_coerce_fig_to_dict
67

@@ -120,7 +121,7 @@ def to_image(
120121
"""
121122
Image export using the "kaleido" engine requires the kaleido package,
122123
which can be installed using pip:
123-
$ pip install -U kaleido
124+
$ pip install -U kaleido
124125
"""
125126
)
126127

@@ -260,4 +261,58 @@ def write_image(
260261
file.write(img_data)
261262

262263

263-
__all__ = ["to_image", "write_image", "scope"]
264+
def full_figure_for_development(fig, warn=True, as_dict=False):
265+
"""
266+
Compute default values for all attributes not specified in the input figure and
267+
returns the output as a "full" figure. This function calls Plotly.js via Kaleido
268+
to populate unspecified attributes. This function is intended for interactive use
269+
during development to learn more about how Plotly.js computes default values and is
270+
not generally necessary or recommended for production use.
271+
272+
Parameters
273+
----------
274+
fig:
275+
Figure object or dict representing a figure
276+
277+
warn: bool
278+
If False, suppress warnings about not using this in production.
279+
280+
as_dict: bool
281+
If True, output is a dict with some keys that go.Figure can't parse.
282+
If False, output is a go.Figure with unparseable keys skipped.
283+
284+
Returns
285+
-------
286+
plotly.graph_objects.Figure or dict
287+
The full figure
288+
"""
289+
290+
# Raise informative error message if Kaleido is not installed
291+
if scope is None:
292+
raise ValueError(
293+
"""
294+
Full figure generation requires the kaleido package,
295+
which can be installed using pip:
296+
$ pip install -U kaleido
297+
"""
298+
)
299+
300+
if warn:
301+
import warnings
302+
303+
warnings.warn(
304+
"full_figure_for_development is not recommended or necessary for "
305+
"production use in most circumstances. \n"
306+
"To suppress this warning, set warn=False"
307+
)
308+
309+
fig = json.loads(scope.transform(fig, format="json").decode("utf-8"))
310+
if as_dict:
311+
return fig
312+
else:
313+
import plotly.graph_objects as go
314+
315+
return go.Figure(fig, skip_invalid=True)
316+
317+
318+
__all__ = ["to_image", "write_image", "scope", "full_figure_for_development"]

Diff for: packages/python/plotly/plotly/tests/test_optional/test_kaleido/test_kaleido.py

+6
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,12 @@ def test_kaleido_engine_to_image_returns_bytes():
2828
assert result.startswith(b"<svg")
2929

3030

31+
def test_kaleido_fulljson():
32+
empty_fig = dict(data=[], layout={})
33+
result = pio.full_figure_for_development(empty_fig, warn=False, as_dict=True)
34+
assert result["layout"]["calendar"] == "gregorian"
35+
36+
3137
def test_kaleido_engine_to_image():
3238
with mocked_scope() as scope:
3339
pio.to_image(fig, engine="kaleido", validate=False)

Diff for: packages/python/plotly/plotly/tests/test_orca/images/darwin/fig1.eps

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
%!PS-Adobe-3.0 EPSF-3.0
2-
%Produced by poppler pdftops version: 0.65.0 (http://poppler.freedesktop.org)
2+
%Produced by poppler pdftops version: 0.81.0 (http://poppler.freedesktop.org)
33
%%Creator: Chromium
44
%%LanguageLevel: 2
55
%%DocumentSuppliedResources: (atend)

Diff for: packages/python/plotly/plotly/tests/test_orca/images/darwin/latexfig.eps

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
%!PS-Adobe-3.0 EPSF-3.0
2-
%Produced by poppler pdftops version: 0.65.0 (http://poppler.freedesktop.org)
2+
%Produced by poppler pdftops version: 0.81.0 (http://poppler.freedesktop.org)
33
%%Creator: Chromium
44
%%LanguageLevel: 2
55
%%DocumentSuppliedResources: (atend)

Diff for: packages/python/plotly/plotly/tests/test_orca/images/darwin/topofig.eps

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
%!PS-Adobe-3.0 EPSF-3.0
2-
%Produced by poppler pdftops version: 0.65.0 (http://poppler.freedesktop.org)
2+
%Produced by poppler pdftops version: 0.81.0 (http://poppler.freedesktop.org)
33
%%Creator: Chromium
44
%%LanguageLevel: 2
55
%%DocumentSuppliedResources: (atend)

Diff for: packages/python/plotly/plotly/tests/test_orca/images/linux/failed/fig1.eps

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)