Skip to content

Commit 649d275

Browse files
authored
Merge branch 'master' into 3984-facets-empty-dataset
2 parents 04029f6 + e6e70fd commit 649d275

File tree

16 files changed

+221
-33
lines changed

16 files changed

+221
-33
lines changed

Diff for: .circleci/config.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -371,7 +371,7 @@ jobs:
371371
name: Create conda environment
372372
command: |
373373
conda create -n env --yes python=3.9 conda-build conda-verify
374-
conda install -n env -c conda-forge jupyterlab nodejs=16
374+
conda install -n env -c conda-forge jupyterlab=3 nodejs=16
375375
conda init bash
376376
mkdir output
377377

Diff for: CHANGELOG.md

+5-1
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,12 @@ This project adheres to [Semantic Versioning](http://semver.org/).
66

77
### Fixed
88
- Fixed another compatibility issue with Pandas 2.0, just affecting `px.*(line_close=True)` [[#4190](https://github.com/plotly/plotly.py/pull/4190)]
9-
- Added some rounding to the `make_subplots` function to handle situations where the user-input specs cause the domain to exceed 1 by small amounts https://github.com/plotly/plotly.py/pull/4153
109
- Empty pandas dataframe with facet row/column set no longer fails [[#4038](https://github.com/plotly/plotly.py/pull/4038)]
10+
- Added some rounding to the `make_subplots` function to handle situations where the user-input specs cause the domain to exceed 1 by small amounts [[#4153](https://github.com/plotly/plotly.py/pull/4153)]
11+
- Sanitize JSON output to prevent an XSS vector when graphs are inserted directly into HTML [[#4196](https://github.com/plotly/plotly.py/pull/4196)]
12+
- Fixed issue with shapes and annotations plotting on the wrong y axis when supplied with a specific axis in the `yref` parameter [[#4177](https://github.com/plotly/plotly.py/pull/4177)]
13+
- Remove `use_2to3` setuptools arg, which is invalid in the latest Python and setuptools versions [[#4206](https://github.com/plotly/plotly.py/pull/4206)]
14+
- Fix [#4066](https://github.com/plotly/plotly.py/issues/4066) JupyterLab v4 giving tiny default graph height [[#4227](https://github.com/plotly/plotly.py/pull/4227)]
1115

1216
## [5.14.1] - 2023-04-05
1317

Diff for: packages/javascript/jupyterlab-plotly/src/Figure.ts

+3
Original file line numberDiff line numberDiff line change
@@ -849,6 +849,9 @@ export class FigureView extends DOMWidgetView {
849849
// the model is not directly mutated by the Plotly.js library.
850850
var initialTraces = _.cloneDeep(this.model.get("_data"));
851851
var initialLayout = _.cloneDeep(this.model.get("_layout"));
852+
if (!initialLayout.height) {
853+
initialLayout.height = 360;
854+
}
852855
var config = this.model.get("_config");
853856
config.editSelection = false;
854857

Diff for: packages/javascript/jupyterlab-plotly/src/plotly-renderer.ts

+5-1
Original file line numberDiff line numberDiff line change
@@ -126,12 +126,16 @@ export class RenderedPlotly extends Widget implements IRenderMime.IRenderer {
126126
| any
127127
| IPlotlySpec;
128128

129+
if (!layout.height) {
130+
layout.height = 360;
131+
}
132+
129133
// Load plotly asynchronously
130134
const loadPlotly = async (): Promise<void> => {
131135
if (RenderedPlotly.Plotly === null) {
132136
RenderedPlotly.Plotly = await import("plotly.js/dist/plotly");
133137
RenderedPlotly._resolveLoadingPlotly();
134-
}
138+
}
135139
return RenderedPlotly.loadingPlotly;
136140
};
137141

Diff for: packages/javascript/jupyterlab-plotly/style/index.css

+1-6
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,7 @@
2121
overflow: hidden;
2222
}
2323

24-
/* Output styles */
25-
.jp-OutputArea .jp-RenderedPlotly {
26-
min-height: 360px;
27-
}
28-
2924
/* Document icon */
3025
.jp-PlotlyIcon {
3126
background-image: var(--jp-icon-plotly);
32-
}
27+
}

Diff for: packages/python/plotly/_plotly_utils/tests/validators/test_integer_validator.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ def test_acceptance_min(val, validator_min):
7474
assert validator_min.validate_coerce(val) == approx(val)
7575

7676

77-
@pytest.mark.parametrize("val", [-2, -123, np.iinfo(np.int).min])
77+
@pytest.mark.parametrize("val", [-2, -123, np.iinfo(int).min])
7878
def test_rejection_min(val, validator_min):
7979
with pytest.raises(ValueError) as validation_failure:
8080
validator_min.validate_coerce(val)

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

+37-14
Original file line numberDiff line numberDiff line change
@@ -1559,17 +1559,31 @@ def _add_annotation_like(
15591559
subplot_type=refs[0].subplot_type,
15601560
)
15611561
)
1562-
if len(refs) == 1 and secondary_y:
1563-
raise ValueError(
1564-
"""
1565-
Cannot add {prop_singular} to secondary y-axis of subplot at position ({r}, {c})
1566-
because subplot does not have a secondary y-axis"""
1567-
)
1568-
if secondary_y:
1569-
xaxis, yaxis = refs[1].layout_keys
1562+
1563+
# If the new_object was created with a yref specified that did not include paper or domain, the specified yref should be used otherwise assign the xref and yref from the layout_keys
1564+
if (
1565+
new_obj.yref is None
1566+
or new_obj.yref == "y"
1567+
or "paper" in new_obj.yref
1568+
or "domain" in new_obj.yref
1569+
):
1570+
if len(refs) == 1 and secondary_y:
1571+
raise ValueError(
1572+
"""
1573+
Cannot add {prop_singular} to secondary y-axis of subplot at position ({r}, {c})
1574+
because subplot does not have a secondary y-axis""".format(
1575+
prop_singular=prop_singular, r=row, c=col
1576+
)
1577+
)
1578+
if secondary_y:
1579+
xaxis, yaxis = refs[1].layout_keys
1580+
else:
1581+
xaxis, yaxis = refs[0].layout_keys
1582+
xref, yref = xaxis.replace("axis", ""), yaxis.replace("axis", "")
15701583
else:
1571-
xaxis, yaxis = refs[0].layout_keys
1572-
xref, yref = xaxis.replace("axis", ""), yaxis.replace("axis", "")
1584+
yref = new_obj.yref
1585+
xaxis = refs[0].layout_keys[0]
1586+
xref = xaxis.replace("axis", "")
15731587
# if exclude_empty_subplots is True, check to see if subplot is
15741588
# empty and return if it is
15751589
if exclude_empty_subplots and (
@@ -1591,6 +1605,11 @@ def _add_domain(ax_letter, new_axref):
15911605
new_obj.update(xref=xref, yref=yref)
15921606

15931607
self.layout[prop_plural] += (new_obj,)
1608+
# The 'new_obj.xref' and 'new_obj.yref' parameters need to be reset otherwise it
1609+
# will appear as if user supplied yref params when looping through subplots and
1610+
# will force annotation to be on the axis of the last drawn annotation
1611+
# i.e. they all end up on the same axis.
1612+
new_obj.update(xref=None, yref=None)
15941613

15951614
return self
15961615

@@ -4034,6 +4053,7 @@ def _process_multiple_axis_spanning_shapes(
40344053
row=row,
40354054
col=col,
40364055
exclude_empty_subplots=exclude_empty_subplots,
4056+
yref=shape_kwargs.get("yref", "y"),
40374057
)
40384058
# update xref and yref for the new shapes and annotations
40394059
for layout_obj, n_layout_objs_before in zip(
@@ -4045,10 +4065,13 @@ def _process_multiple_axis_spanning_shapes(
40454065
):
40464066
# this was called intending to add to a single plot (and
40474067
# self.add_{layout_obj} succeeded)
4048-
# however, in the case of a single plot, xref and yref are not
4049-
# specified, so we specify them here so the following routines can work
4050-
# (they need to append " domain" to xref or yref)
4051-
self.layout[layout_obj][-1].update(xref="x", yref="y")
4068+
# however, in the case of a single plot, xref and yref MAY not be
4069+
# specified, IF they are not specified we specify them here so the following routines can work
4070+
# (they need to append " domain" to xref or yref). If they are specified, we leave them alone.
4071+
if self.layout[layout_obj][-1].xref is None:
4072+
self.layout[layout_obj][-1].update(xref="x")
4073+
if self.layout[layout_obj][-1].yref is None:
4074+
self.layout[layout_obj][-1].update(yref="y")
40524075
new_layout_objs = tuple(
40534076
filter(
40544077
lambda x: x is not None,

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

+26-3
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,25 @@ def coerce_to_strict(const):
5757
return const
5858

5959

60+
_swap_json = (
61+
("<", "\\u003c"),
62+
(">", "\\u003e"),
63+
("/", "\\u002f"),
64+
)
65+
_swap_orjson = _swap_json + (
66+
("\u2028", "\\u2028"),
67+
("\u2029", "\\u2029"),
68+
)
69+
70+
71+
def _safe(json_str, _swap):
72+
out = json_str
73+
for unsafe_char, safe_char in _swap:
74+
if unsafe_char in out:
75+
out = out.replace(unsafe_char, safe_char)
76+
return out
77+
78+
6079
def to_json_plotly(plotly_object, pretty=False, engine=None):
6180
"""
6281
Convert a plotly/Dash object to a JSON string representation
@@ -120,7 +139,9 @@ def to_json_plotly(plotly_object, pretty=False, engine=None):
120139

121140
from _plotly_utils.utils import PlotlyJSONEncoder
122141

123-
return json.dumps(plotly_object, cls=PlotlyJSONEncoder, **opts)
142+
return _safe(
143+
json.dumps(plotly_object, cls=PlotlyJSONEncoder, **opts), _swap_json
144+
)
124145
elif engine == "orjson":
125146
JsonConfig.validate_orjson()
126147
opts = orjson.OPT_NON_STR_KEYS | orjson.OPT_SERIALIZE_NUMPY
@@ -136,7 +157,9 @@ def to_json_plotly(plotly_object, pretty=False, engine=None):
136157

137158
# Try without cleaning
138159
try:
139-
return orjson.dumps(plotly_object, option=opts).decode("utf8")
160+
return _safe(
161+
orjson.dumps(plotly_object, option=opts).decode("utf8"), _swap_orjson
162+
)
140163
except TypeError:
141164
pass
142165

@@ -146,7 +169,7 @@ def to_json_plotly(plotly_object, pretty=False, engine=None):
146169
datetime_allowed=True,
147170
modules=modules,
148171
)
149-
return orjson.dumps(cleaned, option=opts).decode("utf8")
172+
return _safe(orjson.dumps(cleaned, option=opts).decode("utf8"), _swap_orjson)
150173

151174

152175
def to_json(fig, validate=True, pretty=False, remove_uids=True, engine=None):

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

+6-4
Original file line numberDiff line numberDiff line change
@@ -525,13 +525,15 @@ def show(fig, renderer=None, validate=True, **kwargs):
525525
else:
526526
# If ipython isn't available, try to display figures in the default
527527
# browser
528-
import webbrowser
529-
530528
try:
529+
import webbrowser
530+
531531
webbrowser.get()
532532
default_renderer = "browser"
533-
except webbrowser.Error:
534-
# Default browser could not be loaded
533+
except Exception:
534+
# Many things could have gone wrong
535+
# There could not be a webbrowser Python module,
536+
# or the module may be a dumb placeholder
535537
pass
536538

537539
renderers.render_on_display = True

Diff for: packages/python/plotly/plotly/tests/test_core/test_update_objects/test_update_annotations.py

+59
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
import plotly.graph_objs as go
77
from plotly.subplots import make_subplots
8+
89
import pytest
910

1011

@@ -351,6 +352,64 @@ def test_no_exclude_empty_subplots():
351352
assert fig.layout[k][3]["xref"] == "x4" and fig.layout[k][3]["yref"] == "y4"
352353

353354

355+
def test_supplied_yref_on_single_plot_subplot():
356+
### test a (1,1) subplot figure object
357+
fig = make_subplots(1, 1)
358+
fig.add_trace(go.Scatter(x=[1, 2, 3, 4], y=[1, 2, 2, 1]))
359+
fig.add_trace(go.Scatter(x=[1, 2, 3, 4], y=[4, 3, 2, 1], yaxis="y2"))
360+
fig.update_layout(
361+
yaxis=dict(title="yaxis1 title"),
362+
yaxis2=dict(title="yaxis2 title", overlaying="y", side="right"),
363+
)
364+
# add horizontal line on y2. Secondary_y can be True or False when yref is supplied
365+
fig.add_hline(y=3, yref="y2", secondary_y=True)
366+
assert fig.layout["shapes"][0]["yref"] == "y2"
367+
368+
369+
def test_supplied_yref_on_non_subplot_figure_object():
370+
### test a non-subplot figure object from go.Figure
371+
trace1 = go.Scatter(x=[1, 2, 3, 4], y=[1, 2, 2, 1])
372+
trace2 = go.Scatter(x=[1, 2, 3, 4], y=[4, 3, 2, 1], yaxis="y2")
373+
data = [trace1, trace2]
374+
layout = go.Layout(
375+
yaxis=dict(title="yaxis1 title"),
376+
yaxis2=dict(title="yaxis2 title", overlaying="y", side="right"),
377+
)
378+
fig = go.Figure(data=data, layout=layout)
379+
# add horizontal line on y2. Secondary_y can be True or False when yref is supplied
380+
fig.add_hline(y=3, yref="y2", secondary_y=False)
381+
assert fig.layout["shapes"][0]["yref"] == "y2"
382+
383+
384+
def test_supplied_yref_on_multi_plot_subplot():
385+
### test multiple subploted figure object with subplots.make_subplots
386+
fig = make_subplots(
387+
rows=1,
388+
cols=2,
389+
shared_yaxes=False,
390+
specs=[[{"secondary_y": True}, {"secondary_y": True}]],
391+
)
392+
### Add traces to the first subplot
393+
fig.add_trace(go.Scatter(x=[1, 2, 3], y=[1, 2, 3]), row=1, col=1)
394+
fig.add_trace(
395+
go.Scatter(x=[1, 2, 3], y=[3, 2, 1], yaxis="y2"), row=1, col=1, secondary_y=True
396+
)
397+
### Add traces to the second subplot
398+
fig.add_trace(go.Scatter(x=[1, 2, 3], y=[1, 2, 3], yaxis="y"), row=1, col=2)
399+
fig.add_trace(
400+
go.Scatter(x=[1, 2, 3], y=[1, 1, 2], yaxis="y2"), row=1, col=2, secondary_y=True
401+
)
402+
# add a horizontal line on both subplots on their respective secondary y.
403+
# When using the subplots.make_subplots() method yref parameter should NOT be supplied per docstring instructions.
404+
# Instead secondary_y specs and secondary_y parameter MUST be True to plot on secondary y
405+
fig.add_hline(y=2, row=1, col=1, secondary_y=True)
406+
fig.add_hline(y=1, row=1, col=2, secondary_y=True)
407+
assert fig.layout["shapes"][0]["yref"] == "y2"
408+
assert fig.layout["shapes"][0]["xref"] == "x domain"
409+
assert fig.layout["shapes"][1]["yref"] == "y4"
410+
assert fig.layout["shapes"][1]["xref"] == "x2 domain"
411+
412+
354413
@pytest.fixture
355414
def select_annotations_integer():
356415
fig = make_subplots(2, 3)

Diff for: packages/python/plotly/plotly/tests/test_io/test_renderers.py

+43
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,7 @@ def test_plotly_mimetype_renderer_show(fig1, renderer):
126126
# ------------
127127
# See plotly/tests/test_orca/test_image_renderers.py
128128

129+
129130
# HTML
130131
# ----
131132
def assert_full_html(html):
@@ -381,3 +382,45 @@ def test_repr_mimebundle_mixed_renderer(fig1):
381382
assert set(fig1._repr_mimebundle_().keys()) == set(
382383
{"application/vnd.plotly.v1+json", "text/html"}
383384
)
385+
386+
387+
def test_missing_webbrowser_module(fig1):
388+
"""
389+
Assert that no errors occur if the webbrowser module is absent
390+
"""
391+
try:
392+
import builtins
393+
except ImportError:
394+
import __builtin__ as builtins
395+
realimport = builtins.__import__
396+
397+
def webbrowser_absent_import(name, globals, locals, fromlist, level):
398+
"""
399+
Mimick an absent webbrowser module
400+
"""
401+
if name == "webbrowser":
402+
raise ImportError
403+
return realimport(name, globals, locals, fromlist, level)
404+
405+
with mock.patch("builtins.__import__", webbrowser_absent_import):
406+
# 1: check whether importing webbrowser actually results in an ImportError
407+
with pytest.raises(ImportError):
408+
import webbrowser
409+
410+
# 2: check whether the _repr_html_ can handle it regardless
411+
fig1._repr_html_()
412+
413+
414+
def test_missing_webbrowser_methods(fig1):
415+
"""
416+
Assert that no errors occur if the webbrowser module does not contain some methods
417+
"""
418+
import webbrowser
419+
420+
removed_webbrowser_get_method = webbrowser.get
421+
try:
422+
del webbrowser.get
423+
fig1._repr_html_()
424+
finally:
425+
# restore everything after this test
426+
webbrowser.get = removed_webbrowser_get_method

0 commit comments

Comments
 (0)