diff --git a/CHANGELOG.md b/CHANGELOG.md index e4d4c23860..fdd156f882 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,12 +5,22 @@ This project adheres to [Semantic Versioning](http://semver.org/). ## [4.9.0] - unreleased +### Added + +- `px.NO_COLOR` constant to override wide-form color assignment in Plotly Express ([#2614](https://github.com/plotly/plotly.py/pull/2614)) +- `facet_row_spacing` and `facet_col_spacing` added to Plotly Express cartesian 2d functions ([#2614](https://github.com/plotly/plotly.py/pull/2614)) + +### Fixed + +- trendline traces are now of type `scattergl` when `render_mode="webgl"` in Plotly Express ([#2614](https://github.com/plotly/plotly.py/pull/2614)) + ### Updated - Added all cartesian-2d Plotly Express functions, plus `imshow` to Pandas backend with `kind` option - `plotly.express.imshow` now uses data frame index and columns names and values to populate axis parameters by default ([#2539](https://github.com/plotly/plotly.py/pull/2539)) + ## [4.8.2] - 2020-06-26 ### Updated diff --git a/doc/python/facet-plots.md b/doc/python/facet-plots.md index 2178eb7c33..2de009c0ef 100644 --- a/doc/python/facet-plots.md +++ b/doc/python/facet-plots.md @@ -6,7 +6,7 @@ jupyter: extension: .md format_name: markdown format_version: '1.2' - jupytext_version: 1.3.4 + jupytext_version: 1.4.2 kernelspec: display_name: Python 3 language: python @@ -20,7 +20,7 @@ jupyter: name: python nbconvert_exporter: python pygments_lexer: ipython3 - version: 3.7.0 + version: 3.7.7 plotly: description: How to make Facet and Trellis Plots in Python with Plotly. display_as: statistical @@ -103,7 +103,7 @@ fig.show() ### Customize Subplot Figure Titles -Since subplot figure titles are [annotations](https://plotly.com/python/text-and-annotations/#simple-annotation), you can use the `for_each_annotation` function to customize them. +Since subplot figure titles are [annotations](https://plotly.com/python/text-and-annotations/#simple-annotation), you can use the `for_each_annotation` function to customize them, for example to remove the equal-sign (`=`). In the following example, we pass a lambda function to `for_each_annotation` in order to change the figure subplot titles from `smoker=No` and `smoker=Yes` to just `No` and `Yes`. @@ -115,8 +115,25 @@ fig.for_each_annotation(lambda a: a.update(text=a.text.split("=")[-1])) fig.show() ``` +### Controlling Facet Spacing + +The `facet_row_spacing` and `facet_col_spacing` arguments can be used to control the spacing between rows and columns. These values are specified in fractions of the plotting area in paper coordinates and not in pixels, so they will grow or shrink with the `width` and `height` of the figure. + +The defaults work well with 1-4 rows or columns at the default figure size with the default font size, but need to be reduced to around 0.01 for very large figures or figures with many rows or columns. Conversely, if activating tick labels on all facets, the spacing will need to be increased. + ```python +import plotly.express as px +df = px.data.gapminder().query("continent == 'Africa'") + +fig = px.line(df, x="year", y="lifeExp", facet_col="country", facet_col_wrap=7, + facet_row_spacing=0.04, # default is 0.07 when facet_col_wrap is used + facet_col_spacing=0.04, # default is 0.03 + height=600, width=800, + title="Life Expectancy in Africa") +fig.for_each_annotation(lambda a: a.update(text=a.text.split("=")[-1])) +fig.update_yaxes(showticklabels=True) +fig.show() ``` ### Synchronizing axes in subplots with `matches` @@ -138,4 +155,4 @@ for i in range(1, 4): fig.add_trace(go.Scatter(x=x, y=np.random.random(N)), 1, i) fig.update_xaxes(matches='x') fig.show() -``` \ No newline at end of file +``` diff --git a/doc/python/wide-form.md b/doc/python/wide-form.md index 26bbe9527c..196243bdd3 100644 --- a/doc/python/wide-form.md +++ b/doc/python/wide-form.md @@ -158,6 +158,16 @@ fig = px.bar(wide_df, x="nation", y=["gold", "silver", "bronze"], facet_col="var fig.show() ``` +You can also prevent `color` from getting assigned if you're mapping `variable` to some other argument: + +```python +import plotly.express as px +wide_df = px.data.medals_wide(indexed=False) + +fig = px.bar(wide_df, x="nation", y=["gold", "silver", "bronze"], facet_col="variable", color=px.NO_COLOR) +fig.show() +``` + If using a data frame's named indexes, either explicitly or relying on the defaults, the row-index references (i.e. `df.index`) or column-index names (i.e. the value of `df.columns.name`) must be used: ```python diff --git a/packages/python/plotly/plotly/express/__init__.py b/packages/python/plotly/plotly/express/__init__.py index 72d0b44554..bec7e915cc 100644 --- a/packages/python/plotly/plotly/express/__init__.py +++ b/packages/python/plotly/plotly/express/__init__.py @@ -53,6 +53,7 @@ set_mapbox_access_token, defaults, get_trendline_results, + NO_COLOR, ) from ._special_inputs import IdentityMap, Constant, Range # noqa: F401 @@ -100,4 +101,5 @@ "IdentityMap", "Constant", "Range", + "NO_COLOR", ] diff --git a/packages/python/plotly/plotly/express/_chart_types.py b/packages/python/plotly/plotly/express/_chart_types.py index 2d41c40590..cb56f127d0 100644 --- a/packages/python/plotly/plotly/express/_chart_types.py +++ b/packages/python/plotly/plotly/express/_chart_types.py @@ -23,6 +23,8 @@ def scatter( facet_row=None, facet_col=None, facet_col_wrap=0, + facet_row_spacing=None, + facet_col_spacing=None, error_x=None, error_x_minus=None, error_y=None, @@ -74,6 +76,8 @@ def density_contour( facet_row=None, facet_col=None, facet_col_wrap=0, + facet_row_spacing=None, + facet_col_spacing=None, hover_name=None, hover_data=None, animation_frame=None, @@ -141,6 +145,8 @@ def density_heatmap( facet_row=None, facet_col=None, facet_col_wrap=0, + facet_row_spacing=None, + facet_col_spacing=None, hover_name=None, hover_data=None, animation_frame=None, @@ -213,6 +219,8 @@ def line( facet_row=None, facet_col=None, facet_col_wrap=0, + facet_row_spacing=None, + facet_col_spacing=None, error_x=None, error_x_minus=None, error_y=None, @@ -260,6 +268,8 @@ def area( facet_row=None, facet_col=None, facet_col_wrap=0, + facet_row_spacing=None, + facet_col_spacing=None, animation_frame=None, animation_group=None, category_orders={}, @@ -301,6 +311,8 @@ def bar( facet_row=None, facet_col=None, facet_col_wrap=0, + facet_row_spacing=None, + facet_col_spacing=None, hover_name=None, hover_data=None, custom_data=None, @@ -353,6 +365,8 @@ def histogram( facet_row=None, facet_col=None, facet_col_wrap=0, + facet_row_spacing=None, + facet_col_spacing=None, hover_name=None, hover_data=None, animation_frame=None, @@ -417,6 +431,8 @@ def violin( facet_row=None, facet_col=None, facet_col_wrap=0, + facet_row_spacing=None, + facet_col_spacing=None, hover_name=None, hover_data=None, custom_data=None, @@ -464,6 +480,8 @@ def box( facet_row=None, facet_col=None, facet_col_wrap=0, + facet_row_spacing=None, + facet_col_spacing=None, hover_name=None, hover_data=None, custom_data=None, @@ -514,6 +532,8 @@ def strip( facet_row=None, facet_col=None, facet_col_wrap=0, + facet_row_spacing=None, + facet_col_spacing=None, hover_name=None, hover_data=None, custom_data=None, @@ -1398,6 +1418,8 @@ def funnel( facet_row=None, facet_col=None, facet_col_wrap=0, + facet_row_spacing=None, + facet_col_spacing=None, hover_name=None, hover_data=None, custom_data=None, diff --git a/packages/python/plotly/plotly/express/_core.py b/packages/python/plotly/plotly/express/_core.py index 82da1ea7be..fcbac2202d 100644 --- a/packages/python/plotly/plotly/express/_core.py +++ b/packages/python/plotly/plotly/express/_core.py @@ -15,6 +15,7 @@ _subplot_type_for_trace_type, ) +NO_COLOR = "px_no_color_constant" # Declare all supported attributes, across all plot types direct_attrables = ( @@ -842,7 +843,7 @@ def make_trace_spec(args, constructor, attrs, trace_patch): # Add trendline trace specifications if "trendline" in args and args["trendline"]: trace_spec = TraceSpec( - constructor=go.Scatter, + constructor=go.Scattergl if constructor == go.Scattergl else go.Scatter, attrs=["trendline"], trace_patch=dict(mode="lines"), marginal=None, @@ -1349,6 +1350,10 @@ def build_dataframe(args, constructor): label=_escape_col_name(df_input, "index", [var_name, value_name]) ) + no_color = False + if type(args.get("color", None)) == str and args["color"] == NO_COLOR: + no_color = True + args["color"] = None # now that things have been prepped, we do the systematic rewriting of `args` df_output, wide_id_vars = process_args_into_dataframe( @@ -1440,7 +1445,8 @@ def build_dataframe(args, constructor): args["x" if orient_v else "y"] = value_name args["y" if orient_v else "x"] = wide_cross_name args["color"] = args["color"] or var_name - + if no_color: + args["color"] = None args["data_frame"] = df_output return args @@ -2054,9 +2060,9 @@ def init_figure(args, subplot_type, frame_list, nrows, ncols, col_labels, row_la row_heights = [main_size] * (nrows - 1) + [1 - main_size] vertical_spacing = 0.01 elif args.get("facet_col_wrap", 0): - vertical_spacing = 0.07 + vertical_spacing = args.get("facet_row_spacing", None) or 0.07 else: - vertical_spacing = 0.03 + vertical_spacing = args.get("facet_row_spacing", None) or 0.03 if bool(args.get("marginal_y", False)): if args["marginal_y"] == "histogram" or ("color" in args and args["color"]): @@ -2067,7 +2073,7 @@ def init_figure(args, subplot_type, frame_list, nrows, ncols, col_labels, row_la column_widths = [main_size] * (ncols - 1) + [1 - main_size] horizontal_spacing = 0.005 else: - horizontal_spacing = 0.02 + horizontal_spacing = args.get("facet_col_spacing", None) or 0.02 else: # Other subplot types: # 'scene', 'geo', 'polar', 'ternary', 'mapbox', 'domain', None diff --git a/packages/python/plotly/plotly/express/_doc.py b/packages/python/plotly/plotly/express/_doc.py index 4c7b591f78..f1b892695d 100644 --- a/packages/python/plotly/plotly/express/_doc.py +++ b/packages/python/plotly/plotly/express/_doc.py @@ -224,6 +224,14 @@ "Wraps the column variable at this width, so that the column facets span multiple rows.", "Ignored if 0, and forced to 0 if `facet_row` or a `marginal` is set.", ], + facet_row_spacing=[ + "float between 0 and 1", + "Spacing between facet rows, in paper units. Default is 0.03 or 0.0.7 when facet_col_wrap is used.", + ], + facet_col_spacing=[ + "float between 0 and 1", + "Spacing between facet columns, in paper units Default is 0.02.", + ], animation_frame=[ colref_type, colref_desc, diff --git a/packages/python/plotly/plotly/tests/test_core/test_px/test_colors.py b/packages/python/plotly/plotly/tests/test_core/test_px/test_colors.py index 5ca41f9311..36bde27d1f 100644 --- a/packages/python/plotly/plotly/tests/test_core/test_px/test_colors.py +++ b/packages/python/plotly/plotly/tests/test_core/test_px/test_colors.py @@ -1,5 +1,4 @@ import plotly.express as px -import numpy as np def test_reversed_colorscale(): diff --git a/packages/python/plotly/plotly/tests/test_core/test_px/test_facets.py b/packages/python/plotly/plotly/tests/test_core/test_px/test_facets.py new file mode 100644 index 0000000000..eeac32853f --- /dev/null +++ b/packages/python/plotly/plotly/tests/test_core/test_px/test_facets.py @@ -0,0 +1,43 @@ +import plotly.express as px +from pytest import approx + + +def test_facets(): + df = px.data.tips() + fig = px.scatter(df, x="total_bill", y="tip") + assert "xaxis2" not in fig.layout + assert "yaxis2" not in fig.layout + assert fig.layout.xaxis.domain == (0.0, 1.0) + assert fig.layout.yaxis.domain == (0.0, 1.0) + + fig = px.scatter(df, x="total_bill", y="tip", facet_row="sex", facet_col="smoker") + assert fig.layout.xaxis4.domain[0] - fig.layout.xaxis.domain[1] == approx(0.02) + assert fig.layout.yaxis4.domain[0] - fig.layout.yaxis.domain[1] == approx(0.03) + + fig = px.scatter(df, x="total_bill", y="tip", facet_col="day", facet_col_wrap=2) + assert fig.layout.xaxis4.domain[0] - fig.layout.xaxis.domain[1] == approx(0.02) + assert fig.layout.yaxis4.domain[0] - fig.layout.yaxis.domain[1] == approx(0.07) + + fig = px.scatter( + df, + x="total_bill", + y="tip", + facet_row="sex", + facet_col="smoker", + facet_col_spacing=0.09, + facet_row_spacing=0.08, + ) + assert fig.layout.xaxis4.domain[0] - fig.layout.xaxis.domain[1] == approx(0.09) + assert fig.layout.yaxis4.domain[0] - fig.layout.yaxis.domain[1] == approx(0.08) + + fig = px.scatter( + df, + x="total_bill", + y="tip", + facet_col="day", + facet_col_wrap=2, + facet_col_spacing=0.09, + facet_row_spacing=0.08, + ) + assert fig.layout.xaxis4.domain[0] - fig.layout.xaxis.domain[1] == approx(0.09) + assert fig.layout.yaxis4.domain[0] - fig.layout.yaxis.domain[1] == approx(0.08) diff --git a/packages/python/plotly/plotly/tests/test_core/test_px/test_px.py b/packages/python/plotly/plotly/tests/test_core/test_px/test_px.py index d037bc10b5..1a298eb484 100644 --- a/packages/python/plotly/plotly/tests/test_core/test_px/test_px.py +++ b/packages/python/plotly/plotly/tests/test_core/test_px/test_px.py @@ -253,3 +253,25 @@ def test_marginal_ranges(): ) assert fig.layout.xaxis2.range is None assert fig.layout.yaxis3.range is None + + +def test_render_mode(): + df = px.data.gapminder() + df2007 = df.query("year == 2007") + fig = px.scatter(df2007, x="gdpPercap", y="lifeExp", trendline="ols") + assert fig.data[0].type == "scatter" + assert fig.data[1].type == "scatter" + fig = px.scatter( + df2007, x="gdpPercap", y="lifeExp", trendline="ols", render_mode="webgl" + ) + assert fig.data[0].type == "scattergl" + assert fig.data[1].type == "scattergl" + fig = px.scatter(df, x="gdpPercap", y="lifeExp", trendline="ols") + assert fig.data[0].type == "scattergl" + assert fig.data[1].type == "scattergl" + fig = px.scatter(df, x="gdpPercap", y="lifeExp", trendline="ols", render_mode="svg") + assert fig.data[0].type == "scatter" + assert fig.data[1].type == "scatter" + fig = px.density_contour(df, x="gdpPercap", y="lifeExp", trendline="ols") + assert fig.data[0].type == "histogram2dcontour" + assert fig.data[1].type == "scatter" diff --git a/packages/python/plotly/plotly/tests/test_core/test_px/test_px_wide.py b/packages/python/plotly/plotly/tests/test_core/test_px/test_px_wide.py index ebd650371f..e5ac5640ec 100644 --- a/packages/python/plotly/plotly/tests/test_core/test_px/test_px_wide.py +++ b/packages/python/plotly/plotly/tests/test_core/test_px/test_px_wide.py @@ -708,6 +708,17 @@ def append_special_case(df_in, args_in, args_expect, df_expect): ), ) +# NO_COLOR +df = pd.DataFrame(dict(a=[1, 2], b=[3, 4])) +append_special_case( + df_in=df, + args_in=dict(x=None, y=None, color=px.NO_COLOR), + args_expect=dict(x="index", y="value", color=None, orientation="v",), + df_expect=pd.DataFrame( + dict(variable=["a", "a", "b", "b"], index=[0, 1, 0, 1], value=[1, 2, 3, 4]) + ), +) + @pytest.mark.parametrize("df_in, args_in, args_expect, df_expect", special_cases) def test_wide_mode_internal_special_cases(df_in, args_in, args_expect, df_expect):