Skip to content

Commit e604d07

Browse files
authored
Merge branch 'master' into doc-typo_fix
2 parents 1c1d63b + 216fca2 commit e604d07

File tree

15 files changed

+207
-31
lines changed

15 files changed

+207
-31
lines changed

.circleci/config.yml

Lines changed: 1 addition & 1 deletion
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

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,13 @@ 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+
- Empty pandas dataframe with facet row/column set no longer fails [[#4038](https://github.com/plotly/plotly.py/pull/4038)]
910
- 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)]
1011
- 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)]
15+
- Fixed issue with `colors.n_colors` where generated RGB color values were not being constrained to stay between 0 and 255 [[#4110](https://github.com/plotly/plotly.py/pull/4110)]
1116

1217
## [5.14.1] - 2023-04-05
1318

packages/javascript/jupyterlab-plotly/src/Figure.ts

Lines changed: 3 additions & 0 deletions
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

packages/javascript/jupyterlab-plotly/src/plotly-renderer.ts

Lines changed: 5 additions & 1 deletion
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

packages/javascript/jupyterlab-plotly/style/index.css

Lines changed: 1 addition & 6 deletions
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+
}

packages/python/plotly/_plotly_utils/colors/__init__.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -694,11 +694,19 @@ def n_colors(lowcolor, highcolor, n_colors, colortype="tuple"):
694694
incr_2 = diff_2 / (n_colors - 1)
695695
list_of_colors = []
696696

697+
def _constrain_color(c):
698+
if c > 255.0:
699+
return 255.0
700+
elif c < 0.0:
701+
return 0.0
702+
else:
703+
return c
704+
697705
for index in range(n_colors):
698706
new_tuple = (
699-
lowcolor[0] + (index * incr_0),
700-
lowcolor[1] + (index * incr_1),
701-
lowcolor[2] + (index * incr_2),
707+
_constrain_color(lowcolor[0] + (index * incr_0)),
708+
_constrain_color(lowcolor[1] + (index * incr_1)),
709+
_constrain_color(lowcolor[2] + (index * incr_2)),
702710
)
703711
list_of_colors.append(new_tuple)
704712

packages/python/plotly/_plotly_utils/tests/validators/test_integer_validator.py

Lines changed: 1 addition & 1 deletion
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)

packages/python/plotly/plotly/basedatatypes.py

Lines changed: 37 additions & 14 deletions
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,

packages/python/plotly/plotly/express/_core.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1880,6 +1880,10 @@ def infer_config(args, constructor, trace_patch, layout_patch):
18801880
args[position] = args["marginal"]
18811881
args[other_position] = None
18821882

1883+
# Ignore facet rows and columns when data frame is empty so as to prevent nrows/ncols equaling 0
1884+
if len(args["data_frame"]) == 0:
1885+
args["facet_row"] = args["facet_col"] = None
1886+
18831887
# If both marginals and faceting are specified, faceting wins
18841888
if args.get("facet_col") is not None and args.get("marginal_y") is not None:
18851889
args["marginal_y"] = None

packages/python/plotly/plotly/io/_renderers.py

Lines changed: 6 additions & 4 deletions
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

packages/python/plotly/plotly/tests/test_core/test_colors/test_colors.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,3 +210,30 @@ def test_sample_colorscale(self):
210210
colors.sample_colorscale("TuRbId_r", 12),
211211
colors.sequential.turbid_r,
212212
)
213+
214+
def test_n_colors(self):
215+
# test that n_colors constrains values to between 0 and 255
216+
generated_colorscale = colors.n_colors(
217+
lowcolor="rgb(255,0,0)",
218+
highcolor="rgb(0,255,0)",
219+
n_colors=14,
220+
colortype="rgb",
221+
)
222+
expected_colorscale = [
223+
"rgb(255.0, 0.0, 0.0)",
224+
"rgb(235.3846153846154, 19.615384615384617, 0.0)",
225+
"rgb(215.76923076923077, 39.23076923076923, 0.0)",
226+
"rgb(196.15384615384613, 58.846153846153854, 0.0)",
227+
"rgb(176.53846153846155, 78.46153846153847, 0.0)",
228+
"rgb(156.9230769230769, 98.07692307692308, 0.0)",
229+
"rgb(137.3076923076923, 117.69230769230771, 0.0)",
230+
"rgb(117.69230769230768, 137.30769230769232, 0.0)",
231+
"rgb(98.07692307692307, 156.92307692307693, 0.0)",
232+
"rgb(78.46153846153845, 176.53846153846155, 0.0)",
233+
"rgb(58.84615384615384, 196.15384615384616, 0.0)",
234+
"rgb(39.230769230769226, 215.76923076923077, 0.0)",
235+
"rgb(19.615384615384585, 235.38461538461542, 0.0)",
236+
"rgb(0.0, 255.0, 0.0)",
237+
]
238+
239+
self.assertEqual(generated_colorscale, expected_colorscale)

packages/python/plotly/plotly/tests/test_core/test_update_objects/test_update_annotations.py

Lines changed: 59 additions & 0 deletions
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)

packages/python/plotly/plotly/tests/test_io/test_renderers.py

Lines changed: 43 additions & 0 deletions
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

packages/python/plotly/setup.cfg

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[metadata]
22
description_file = README.md
3-
license_file = LICENSE.txt
3+
license_files = LICENSE.txt
44

55
[bdist_wheel]
66
universal=1

test/percy/plotly-express.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -543,3 +543,6 @@
543543
)
544544
fig = px.timeline(df, x_start="Start", x_end="Finish", y="Task", color="Task")
545545
fig.write_html(os.path.join(dir_name, "timeline.html"), auto_play=False)
546+
547+
548+
px.bar(pd.DataFrame(columns=["A", "B", "X"]), x="A", y="X", facet_col="B")

0 commit comments

Comments
 (0)