From 5960241136ab60cfcfba683b59ee81cdb881c8e9 Mon Sep 17 00:00:00 2001 From: Nicolas Kruchten Date: Wed, 8 Jul 2020 08:37:40 -0400 Subject: [PATCH] initial impl of px.timeline --- .../python/plotly/plotly/express/__init__.py | 2 + .../plotly/plotly/express/_chart_types.py | 47 +++++++++++++++++++ .../python/plotly/plotly/express/_core.py | 36 +++++++++++++- packages/python/plotly/plotly/express/_doc.py | 10 ++++ 4 files changed, 93 insertions(+), 2 deletions(-) diff --git a/packages/python/plotly/plotly/express/__init__.py b/packages/python/plotly/plotly/express/__init__.py index bec7e915cc9..4bffa25d784 100644 --- a/packages/python/plotly/plotly/express/__init__.py +++ b/packages/python/plotly/plotly/express/__init__.py @@ -28,6 +28,7 @@ line_geo, area, bar, + timeline, bar_polar, violin, box, @@ -81,6 +82,7 @@ "parallel_categories", "area", "bar", + "timeline", "bar_polar", "violin", "box", diff --git a/packages/python/plotly/plotly/express/_chart_types.py b/packages/python/plotly/plotly/express/_chart_types.py index 65ae21c2774..3a6d6d5b1aa 100644 --- a/packages/python/plotly/plotly/express/_chart_types.py +++ b/packages/python/plotly/plotly/express/_chart_types.py @@ -358,6 +358,53 @@ def bar( bar.__doc__ = make_docstring(bar, append_dict=_cartesian_append_dict) +def timeline( + data_frame=None, + x_start=None, + x_end=None, + y=None, + color=None, + 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, + text=None, + animation_frame=None, + animation_group=None, + category_orders={}, + labels={}, + color_discrete_sequence=None, + color_discrete_map={}, + color_continuous_scale=None, + range_color=None, + color_continuous_midpoint=None, + opacity=None, + range_x=None, + range_y=None, + title=None, + template=None, + width=None, + height=None, +): + """ + In a timeline plot, each row of `data_frame` is represented as a rectangular + mark on an x axis of type `date`, spanning from `x_start` to `x_end`. + """ + return make_figure( + args=locals(), + constructor="timeline", + trace_patch=dict(textposition="auto", orientation="h"), + layout_patch=dict(barmode="overlay"), + ) + + +timeline.__doc__ = make_docstring(timeline) + + def histogram( data_frame=None, x=None, diff --git a/packages/python/plotly/plotly/express/_core.py b/packages/python/plotly/plotly/express/_core.py index 9ae672e2328..719cbf98c0d 100644 --- a/packages/python/plotly/plotly/express/_core.py +++ b/packages/python/plotly/plotly/express/_core.py @@ -19,7 +19,7 @@ # Declare all supported attributes, across all plot types direct_attrables = ( - ["x", "y", "z", "a", "b", "c", "r", "theta", "size", "base"] + ["base", "x", "y", "z", "a", "b", "c", "r", "theta", "size", "x_start", "x_end"] + ["hover_name", "text", "names", "values", "parents", "wide_cross"] + ["ids", "error_x", "error_x_minus", "error_y", "error_y_minus", "error_z"] + ["error_z_minus", "lat", "lon", "locations", "animation_group"] @@ -610,7 +610,8 @@ def configure_cartesian_axes(args, fig, orders): # Set x-axis titles and axis options in the bottom-most row x_title = get_decorated_label(args, args["x"], "x") for xaxis in fig.select_xaxes(row=1): - xaxis.update(title_text=x_title) + if "is_timeline" not in args: + xaxis.update(title_text=x_title) set_cartesian_axis_opts(args, xaxis, "x", orders) # Configure axis type across all x-axes @@ -621,6 +622,9 @@ def configure_cartesian_axes(args, fig, orders): if "log_y" in args and args["log_y"]: fig.update_yaxes(type="log") + if "is_timeline" in args: + fig.update_xaxes(type="date") + return fig.layout @@ -1354,6 +1358,7 @@ def build_dataframe(args, constructor): 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( @@ -1599,6 +1604,30 @@ def aggfunc_continuous(x): return args +def process_dataframe_timeline(args): + """ + Massage input for bar traces for px.timeline() + """ + args["is_timeline"] = True + if args["x_start"] is None or args["x_end"] is None: + raise ValueError("Both x_start and x_end are required") + + try: + x_start = pd.to_datetime(args["data_frame"][args["x_start"]]) + x_end = pd.to_datetime(args["data_frame"][args["x_end"]]) + except TypeError: + raise TypeError( + "Both x_start and x_end must refer to data convertible to datetimes." + ) + + # note that we are not adding any columns to the data frame here, so no risk of overwrite + args["data_frame"][args["x_end"]] = (x_end - x_start).astype("timedelta64[ms]") + args["x"] = args["x_end"] + del args["x_end"] + args["base"] = args["x_start"] + del args["x_start"] + + def infer_config(args, constructor, trace_patch, layout_patch): attrs = [k for k in direct_attrables + array_attrables if k in args] grouped_attrs = [] @@ -1801,6 +1830,9 @@ def make_figure(args, constructor, trace_patch=None, layout_patch=None): args = build_dataframe(args, constructor) if constructor in [go.Treemap, go.Sunburst] and args["path"] is not None: args = process_dataframe_hierarchy(args) + if constructor == "timeline": + constructor = go.Bar + args = process_dataframe_timeline(args) trace_specs, grouped_mappings, sizeref, show_colorbar = infer_config( args, constructor, trace_patch, layout_patch diff --git a/packages/python/plotly/plotly/express/_doc.py b/packages/python/plotly/plotly/express/_doc.py index 9873bbd0171..8ef960037de 100644 --- a/packages/python/plotly/plotly/express/_doc.py +++ b/packages/python/plotly/plotly/express/_doc.py @@ -36,6 +36,16 @@ colref_desc, "Values from this column or array_like are used to position marks along the z axis in cartesian coordinates.", ], + x_start=[ + colref_type, + colref_desc, + "Values from this column or array_like are used to position marks along the x axis in cartesian coordinates.", + ], + x_end=[ + colref_type, + colref_desc, + "Values from this column or array_like are used to position marks along the x axis in cartesian coordinates.", + ], a=[ colref_type, colref_desc,