From 9bfd172271ac4862d0b518596640496ad96f1e92 Mon Sep 17 00:00:00 2001 From: Nicolas Kruchten Date: Mon, 30 Mar 2020 21:36:55 -0400 Subject: [PATCH 1/5] initial pass at PX auto-orientation --- .../plotly/plotly/express/_chart_types.py | 40 +++++------------ .../python/plotly/plotly/express/_core.py | 43 ++++++++++++++++--- 2 files changed, 48 insertions(+), 35 deletions(-) diff --git a/packages/python/plotly/plotly/express/_chart_types.py b/packages/python/plotly/plotly/express/_chart_types.py index f7e7da8cbfd..789f02af829 100644 --- a/packages/python/plotly/plotly/express/_chart_types.py +++ b/packages/python/plotly/plotly/express/_chart_types.py @@ -236,7 +236,7 @@ def area( labels={}, color_discrete_sequence=None, color_discrete_map={}, - orientation="v", + orientation=None, groupnorm=None, log_x=False, log_y=False, @@ -256,9 +256,7 @@ def area( return make_figure( args=locals(), constructor=go.Scatter, - trace_patch=dict( - stackgroup=1, mode="lines", orientation=orientation, groupnorm=groupnorm - ), + trace_patch=dict(stackgroup=1, mode="lines", groupnorm=groupnorm), ) @@ -291,7 +289,7 @@ def bar( range_color=None, color_continuous_midpoint=None, opacity=None, - orientation="v", + orientation=None, barmode="relative", log_x=False, log_y=False, @@ -335,7 +333,7 @@ def histogram( color_discrete_map={}, marginal=None, opacity=None, - orientation="v", + orientation=None, barmode="relative", barnorm=None, histnorm=None, @@ -361,13 +359,7 @@ def histogram( args=locals(), constructor=go.Histogram, trace_patch=dict( - orientation=orientation, - histnorm=histnorm, - histfunc=histfunc, - nbinsx=nbins if orientation == "v" else None, - nbinsy=None if orientation == "v" else nbins, - cumulative=dict(enabled=cumulative), - bingroup="x" if orientation == "v" else "y", + histnorm=histnorm, histfunc=histfunc, cumulative=dict(enabled=cumulative), ), layout_patch=dict(barmode=barmode, barnorm=barnorm), ) @@ -393,7 +385,7 @@ def violin( labels={}, color_discrete_sequence=None, color_discrete_map={}, - orientation="v", + orientation=None, violinmode="group", log_x=False, log_y=False, @@ -414,12 +406,7 @@ def violin( args=locals(), constructor=go.Violin, trace_patch=dict( - orientation=orientation, - points=points, - box=dict(visible=box), - scalegroup=True, - x0=" ", - y0=" ", + points=points, box=dict(visible=box), scalegroup=True, x0=" ", y0=" ", ), layout_patch=dict(violinmode=violinmode), ) @@ -445,7 +432,7 @@ def box( labels={}, color_discrete_sequence=None, color_discrete_map={}, - orientation="v", + orientation=None, boxmode="group", log_x=False, log_y=False, @@ -470,9 +457,7 @@ def box( return make_figure( args=locals(), constructor=go.Box, - trace_patch=dict( - orientation=orientation, boxpoints=points, notched=notched, x0=" ", y0=" " - ), + trace_patch=dict(boxpoints=points, notched=notched, x0=" ", y0=" "), layout_patch=dict(boxmode=boxmode), ) @@ -497,7 +482,7 @@ def strip( labels={}, color_discrete_sequence=None, color_discrete_map={}, - orientation="v", + orientation=None, stripmode="group", log_x=False, log_y=False, @@ -516,7 +501,6 @@ def strip( args=locals(), constructor=go.Box, trace_patch=dict( - orientation=orientation, boxpoints="all", pointpos=0, hoveron="points", @@ -1398,9 +1382,7 @@ def funnel( In a funnel plot, each row of `data_frame` is represented as a rectangular sector of a funnel. """ - return make_figure( - args=locals(), constructor=go.Funnel, trace_patch=dict(orientation=orientation), - ) + return make_figure(args=locals(), constructor=go.Funnel,) funnel.__doc__ = make_docstring(funnel) diff --git a/packages/python/plotly/plotly/express/_core.py b/packages/python/plotly/plotly/express/_core.py index 613920d05fa..1400f6a4f83 100644 --- a/packages/python/plotly/plotly/express/_core.py +++ b/packages/python/plotly/plotly/express/_core.py @@ -92,6 +92,10 @@ def get_label(args, column): return column +def _is_continuous(df, col_name): + return df[col_name].dtype.kind in "ifc" + + def get_decorated_label(args, column, role): label = get_label(args, column) if "histfunc" in args and ( @@ -188,7 +192,7 @@ def make_trace_kwargs(args, trace_spec, trace_data, mapping_labels, sizeref): if ((not attr_value) or (name in attr_value)) and ( trace_spec.constructor != go.Parcoords - or args["data_frame"][name].dtype.kind in "ifc" + or _is_continuous(args["data_frame"], name) ) and ( trace_spec.constructor != go.Parcats @@ -1124,7 +1128,7 @@ def aggfunc_discrete(x): agg_f[count_colname] = "sum" if args["color"]: - if df[args["color"]].dtype.kind not in "ifc": + if not _is_continuous(df, args["color"]): aggfunc_color = aggfunc_discrete discrete_color = True elif not aggfunc_color: @@ -1212,6 +1216,36 @@ def infer_config(args, constructor, trace_patch): if constructor in [go.Treemap, go.Sunburst] and args["path"] is not None: args = process_dataframe_hierarchy(args) + if "orientation" in args: + has_x = args["x"] is not None + has_y = args["y"] is not None + if args["orientation"] is None: + if constructor in [go.Histogram, go.Scatter]: + if has_y and not has_x: + args["orientation"] = "h" + elif constructor in [go.Violin, go.Box, go.Bar, go.Funnel]: + if has_x and not has_y: + args["orientation"] = "h" + + if args["orientation"] is None and has_x and has_y: + x_is_continuous = _is_continuous(args["data_frame"], args["x"]) + y_is_continuous = _is_continuous(args["data_frame"], args["y"]) + if x_is_continuous and not y_is_continuous: + args["orientation"] = "h" + if y_is_continuous and not x_is_continuous: + args["orientation"] = "v" + + if args["orientation"] is None: + args["orientation"] = "v" + + if constructor == go.Histogram: + orientation = args["orientation"] + nbins = args["nbins"] + trace_patch["nbinsx"] = nbins if orientation == "v" else None + trace_patch["nbinsy"] = None if orientation == "v" else nbins + trace_patch["bingroup"] = "x" if orientation == "v" else "y" + trace_patch["orientation"] = args["orientation"] + attrs = [k for k in attrables if k in args] grouped_attrs = [] @@ -1226,10 +1260,7 @@ def infer_config(args, constructor, trace_patch): if "color_discrete_sequence" not in args: attrs.append("color") else: - if ( - args["color"] - and args["data_frame"][args["color"]].dtype.kind in "ifc" - ): + if args["color"] and _is_continuous(args["data_frame"], args["color"]): attrs.append("color") args["color_is_continuous"] = True elif constructor in [go.Sunburst, go.Treemap]: From 87d99846cf5a395457a825d0434aa9d77282a2b0 Mon Sep 17 00:00:00 2001 From: Nicolas Kruchten Date: Tue, 31 Mar 2020 17:25:48 -0400 Subject: [PATCH 2/5] cleanup --- packages/python/plotly/plotly/express/_chart_types.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/python/plotly/plotly/express/_chart_types.py b/packages/python/plotly/plotly/express/_chart_types.py index 789f02af829..223377b2ac8 100644 --- a/packages/python/plotly/plotly/express/_chart_types.py +++ b/packages/python/plotly/plotly/express/_chart_types.py @@ -307,7 +307,7 @@ def bar( return make_figure( args=locals(), constructor=go.Bar, - trace_patch=dict(orientation=orientation, textposition="auto"), + trace_patch=dict(textposition="auto"), layout_patch=dict(barmode=barmode), ) @@ -1368,7 +1368,7 @@ def funnel( color_discrete_sequence=None, color_discrete_map={}, opacity=None, - orientation="h", + orientation=None, log_x=False, log_y=False, range_x=None, @@ -1382,7 +1382,7 @@ def funnel( In a funnel plot, each row of `data_frame` is represented as a rectangular sector of a funnel. """ - return make_figure(args=locals(), constructor=go.Funnel,) + return make_figure(args=locals(), constructor=go.Funnel) funnel.__doc__ = make_docstring(funnel) From c74e7236259ca539c7a04510590735ed5f56848f Mon Sep 17 00:00:00 2001 From: Nicolas Kruchten Date: Tue, 31 Mar 2020 17:38:45 -0400 Subject: [PATCH 3/5] fix for odd box/violin spacing when axis matches color --- .../python/plotly/plotly/express/_chart_types.py | 6 +++--- packages/python/plotly/plotly/express/_core.py | 14 ++++++++++++-- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/packages/python/plotly/plotly/express/_chart_types.py b/packages/python/plotly/plotly/express/_chart_types.py index 223377b2ac8..bff581e6ff8 100644 --- a/packages/python/plotly/plotly/express/_chart_types.py +++ b/packages/python/plotly/plotly/express/_chart_types.py @@ -386,7 +386,7 @@ def violin( color_discrete_sequence=None, color_discrete_map={}, orientation=None, - violinmode="group", + violinmode=None, log_x=False, log_y=False, range_x=None, @@ -433,7 +433,7 @@ def box( color_discrete_sequence=None, color_discrete_map={}, orientation=None, - boxmode="group", + boxmode=None, log_x=False, log_y=False, range_x=None, @@ -483,7 +483,7 @@ def strip( color_discrete_sequence=None, color_discrete_map={}, orientation=None, - stripmode="group", + stripmode=None, log_x=False, log_y=False, range_x=None, diff --git a/packages/python/plotly/plotly/express/_core.py b/packages/python/plotly/plotly/express/_core.py index 1400f6a4f83..7b20111584d 100644 --- a/packages/python/plotly/plotly/express/_core.py +++ b/packages/python/plotly/plotly/express/_core.py @@ -1194,7 +1194,7 @@ def aggfunc_continuous(x): return args -def infer_config(args, constructor, trace_patch): +def infer_config(args, constructor, trace_patch, layout_patch): # Declare all supported attributes, across all plot types attrables = ( ["x", "y", "z", "a", "b", "c", "r", "theta", "size", "dimensions"] @@ -1246,6 +1246,16 @@ def infer_config(args, constructor, trace_patch): trace_patch["bingroup"] = "x" if orientation == "v" else "y" trace_patch["orientation"] = args["orientation"] + if constructor in [go.Violin, go.Box]: + mode = "boxmode" if constructor == go.Box else "violinmode" + if layout_patch[mode] is None and args["color"] is not None: + if args["y"] == args["color"] and args["orientation"] == "h": + layout_patch[mode] = "overlay" + elif args["x"] == args["color"] and args["orientation"] == "v": + layout_patch[mode] = "overlay" + if layout_patch[mode] is None: + layout_patch[mode] = "group" + attrs = [k for k in attrables if k in args] grouped_attrs = [] @@ -1396,7 +1406,7 @@ def make_figure(args, constructor, trace_patch={}, layout_patch={}): apply_default_cascade(args) args, trace_specs, grouped_mappings, sizeref, show_colorbar = infer_config( - args, constructor, trace_patch + args, constructor, trace_patch, layout_patch ) grouper = [x.grouper or one_group for x in grouped_mappings] or [one_group] grouped = args["data_frame"].groupby(grouper, sort=False) From 6eb708f9de9043ae3307cd204b4eda18fd684796 Mon Sep 17 00:00:00 2001 From: Nicolas Kruchten Date: Tue, 31 Mar 2020 21:14:58 -0400 Subject: [PATCH 4/5] clean up default {} --- .../python/plotly/plotly/express/_core.py | 89 +++++++++---------- 1 file changed, 44 insertions(+), 45 deletions(-) diff --git a/packages/python/plotly/plotly/express/_core.py b/packages/python/plotly/plotly/express/_core.py index 7b20111584d..a858bd0b36e 100644 --- a/packages/python/plotly/plotly/express/_core.py +++ b/packages/python/plotly/plotly/express/_core.py @@ -1216,46 +1216,6 @@ def infer_config(args, constructor, trace_patch, layout_patch): if constructor in [go.Treemap, go.Sunburst] and args["path"] is not None: args = process_dataframe_hierarchy(args) - if "orientation" in args: - has_x = args["x"] is not None - has_y = args["y"] is not None - if args["orientation"] is None: - if constructor in [go.Histogram, go.Scatter]: - if has_y and not has_x: - args["orientation"] = "h" - elif constructor in [go.Violin, go.Box, go.Bar, go.Funnel]: - if has_x and not has_y: - args["orientation"] = "h" - - if args["orientation"] is None and has_x and has_y: - x_is_continuous = _is_continuous(args["data_frame"], args["x"]) - y_is_continuous = _is_continuous(args["data_frame"], args["y"]) - if x_is_continuous and not y_is_continuous: - args["orientation"] = "h" - if y_is_continuous and not x_is_continuous: - args["orientation"] = "v" - - if args["orientation"] is None: - args["orientation"] = "v" - - if constructor == go.Histogram: - orientation = args["orientation"] - nbins = args["nbins"] - trace_patch["nbinsx"] = nbins if orientation == "v" else None - trace_patch["nbinsy"] = None if orientation == "v" else nbins - trace_patch["bingroup"] = "x" if orientation == "v" else "y" - trace_patch["orientation"] = args["orientation"] - - if constructor in [go.Violin, go.Box]: - mode = "boxmode" if constructor == go.Box else "violinmode" - if layout_patch[mode] is None and args["color"] is not None: - if args["y"] == args["color"] and args["orientation"] == "h": - layout_patch[mode] = "overlay" - elif args["x"] == args["color"] and args["orientation"] == "v": - layout_patch[mode] = "overlay" - if layout_patch[mode] is None: - layout_patch[mode] = "group" - attrs = [k for k in attrables if k in args] grouped_attrs = [] @@ -1309,8 +1269,45 @@ def infer_config(args, constructor, trace_patch, layout_patch): if "symbol" in args: grouped_attrs.append("marker.symbol") - # Compute final trace patch - trace_patch = trace_patch.copy() + if "orientation" in args: + has_x = args["x"] is not None + has_y = args["y"] is not None + if args["orientation"] is None: + if constructor in [go.Histogram, go.Scatter]: + if has_y and not has_x: + args["orientation"] = "h" + elif constructor in [go.Violin, go.Box, go.Bar, go.Funnel]: + if has_x and not has_y: + args["orientation"] = "h" + + if args["orientation"] is None and has_x and has_y: + x_is_continuous = _is_continuous(args["data_frame"], args["x"]) + y_is_continuous = _is_continuous(args["data_frame"], args["y"]) + if x_is_continuous and not y_is_continuous: + args["orientation"] = "h" + if y_is_continuous and not x_is_continuous: + args["orientation"] = "v" + + if args["orientation"] is None: + args["orientation"] = "v" + + if constructor == go.Histogram: + orientation = args["orientation"] + nbins = args["nbins"] + trace_patch["nbinsx"] = nbins if orientation == "v" else None + trace_patch["nbinsy"] = None if orientation == "v" else nbins + trace_patch["bingroup"] = "x" if orientation == "v" else "y" + trace_patch["orientation"] = args["orientation"] + + if constructor in [go.Violin, go.Box]: + mode = "boxmode" if constructor == go.Box else "violinmode" + if layout_patch[mode] is None and args["color"] is not None: + if args["y"] == args["color"] and args["orientation"] == "h": + layout_patch[mode] = "overlay" + elif args["x"] == args["color"] and args["orientation"] == "v": + layout_patch[mode] = "overlay" + if layout_patch[mode] is None: + layout_patch[mode] = "group" if constructor in [go.Histogram2d, go.Densitymapbox]: show_colorbar = True @@ -1358,7 +1355,7 @@ def infer_config(args, constructor, trace_patch, layout_patch): # Create trace specs trace_specs = make_trace_spec(args, constructor, attrs, trace_patch) - return args, trace_specs, grouped_mappings, sizeref, show_colorbar + return trace_specs, grouped_mappings, sizeref, show_colorbar def get_orderings(args, grouper, grouped): @@ -1402,10 +1399,12 @@ def get_orderings(args, grouper, grouped): return orders, group_names, group_values -def make_figure(args, constructor, trace_patch={}, layout_patch={}): +def make_figure(args, constructor, trace_patch=None, layout_patch=None): + trace_patch = trace_patch or {} + layout_patch = layout_patch or {} apply_default_cascade(args) - args, trace_specs, grouped_mappings, sizeref, show_colorbar = infer_config( + trace_specs, grouped_mappings, sizeref, show_colorbar = infer_config( args, constructor, trace_patch, layout_patch ) grouper = [x.grouper or one_group for x in grouped_mappings] or [one_group] From fbfc0820f46ec81d61a8f165399674b5c39b5851 Mon Sep 17 00:00:00 2001 From: Nicolas Kruchten Date: Tue, 31 Mar 2020 22:21:11 -0400 Subject: [PATCH 5/5] smarter histfunc defaults --- packages/python/plotly/plotly/express/_core.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/packages/python/plotly/plotly/express/_core.py b/packages/python/plotly/plotly/express/_core.py index a858bd0b36e..d293c9bf8fa 100644 --- a/packages/python/plotly/plotly/express/_core.py +++ b/packages/python/plotly/plotly/express/_core.py @@ -1292,6 +1292,9 @@ def infer_config(args, constructor, trace_patch, layout_patch): args["orientation"] = "v" if constructor == go.Histogram: + if has_x and has_y and args["histfunc"] is None: + args["histfunc"] = trace_patch["histfunc"] = "sum" + orientation = args["orientation"] nbins = args["nbins"] trace_patch["nbinsx"] = nbins if orientation == "v" else None @@ -1309,6 +1312,13 @@ def infer_config(args, constructor, trace_patch, layout_patch): if layout_patch[mode] is None: layout_patch[mode] = "group" + if ( + constructor == go.Histogram2d + and args["z"] is not None + and args["histfunc"] is None + ): + args["histfunc"] = trace_patch["histfunc"] = "sum" + if constructor in [go.Histogram2d, go.Densitymapbox]: show_colorbar = True trace_patch["coloraxis"] = "coloraxis1"