Repository: plotly/plotly.py
Pull request: more flexible type of input arguments for px functions
This is a first a draft for accepting more flexible types of input arguments for `px` functions, eg numpy arrays, dataframe columns, mixed type etc. At the moment the transformation done by `build_or_augment_dataframe` is done argument by argument, so having different arguments with different types is ok. 

Todo:
- write more tests
- decide what to do with arguments which are lists, such as `hover_data`, `custom_data`, `dimensions`. At the moment they are just skipped so they should be column names. I did this because I wanted to discuss the names they should have, should it be `customdata_0`, `customdata_1` etc. ?

For the cases which are taken into account, please see the test file.

Closes #1767 , follow-up on https://github.com/plotly/plotly_express/pull/87 (thank you @malmaud !)

Problem statement:
Plotly Express column input formats
Right now for all `px` methods, `data_frame` is a required argument and the values of many kwargs are column names as strings.

We should be much more flexible in what we accept, for example the following should work:

```python
px.scatter(x=[1,2,3], y=[1,2,3])
```

In which case we would internally build a data frame with column names "x" and "y".

In addition, the following should work:

```python
px.scatter(df, x=df.index, y=df.col)
```

In which case we can read the column names (and optionally index name?) from the data frame.

Finally, we should allow for mixing and matching:

```python
px.scatter(df, x=df.index, y=df.col, color=[1,2,3])
```

Hints from issue discussion:
A couple of additional thoughts... The following should work, and result in a data frame internally with column names "x" and "y" because the parent data frame was not provided to `px`:

```python
px.scatter(x=df.x_col, y=df.y_col)
```

Finally, just to be explicit, we should accept the same formats wherever we accept lists of column names, like in `dimensions`, `hover_data` and `custom_data`.
This was a PR which implemented part of these requirements earlier this summer, but was held up by the then-impending repo shuffle: https://github.com/plotly/plotly_express/pull/87
Also take a look at `xarray`
Re `xarray` I seem to recall reading that 1-dimensional arrays could have an "onboard" name, unlike `pandas.Series` objects, whose names are only available in a containing `pandas.DataFrame`... if this name is easy to read, it might be nice to use it instead of "x" in the case of `px.scatter(x=<xarray vector with onboard name>)` :)
We could also accept `px.scatter(x={'time':[1,2,3]}, y={'revenue':[1,2,3]})`. What do you think?

It would be a way to give names to columns easily. We can also use the `labels` argument for this.
Yes, I think that `px.scatter(x=[1,2,3], y=[1,2,3], labels={'x': 'time', 'y': 'money'})` would be the way here.
depending on how it's implemented, the above should "just work" actually :)
and what do you think about `px.scatter(x={'time':[1,2,3]}, y={'money':[1,2,3]})`? Should we allow it as well?
I'm less of a fan of this, partly because it's not a previously-existing convention, as far as I know, and because it shuts the door to a future type-based dispatching on `x`, `y` etc (i.e. we might find some other future use for this pattern other than names...). Also, by the time you're creating dicts, you may as well create a data frame with `pd.DataFrame({'time':[1,2,3], 'money':[1,2,3]})`...

Maybe we should accept `{'time':[1,2,3], 'money':[1,2,3]}` as the `data_frame` argument, by just passing whatever we get in there into the `pd.DataFrame` constructor, so that you could do `px.scatter({'time':[1,2,3], 'money': [1,2,3]}, x='time', y='money')` ?

Commit message: more flexible type of input arguments for px functions

Patch to review:
diff --git a/packages/python/plotly/plotly/express/_chart_types.py b/packages/python/plotly/plotly/express/_chart_types.py
index 7a08d2e6ac4..c17c5a657e5 100644
--- a/packages/python/plotly/plotly/express/_chart_types.py
+++ b/packages/python/plotly/plotly/express/_chart_types.py
@@ -4,7 +4,7 @@
 
 
 def scatter(
-    data_frame,
+    data_frame=None,
     x=None,
     y=None,
     color=None,
@@ -57,7 +57,7 @@ def scatter(
 
 
 def density_contour(
-    data_frame,
+    data_frame=None,
     x=None,
     y=None,
     z=None,
@@ -113,7 +113,7 @@ def density_contour(
 
 
 def density_heatmap(
-    data_frame,
+    data_frame=None,
     x=None,
     y=None,
     z=None,
@@ -167,7 +167,7 @@ def density_heatmap(
 
 
 def line(
-    data_frame,
+    data_frame=None,
     x=None,
     y=None,
     line_group=None,
@@ -212,7 +212,7 @@ def line(
 
 
 def area(
-    data_frame,
+    data_frame=None,
     x=None,
     y=None,
     line_group=None,
@@ -257,7 +257,7 @@ def area(
 
 
 def bar(
-    data_frame,
+    data_frame=None,
     x=None,
     y=None,
     color=None,
@@ -307,7 +307,7 @@ def bar(
 
 
 def histogram(
-    data_frame,
+    data_frame=None,
     x=None,
     y=None,
     color=None,
@@ -364,7 +364,7 @@ def histogram(
 
 
 def violin(
-    data_frame,
+    data_frame=None,
     x=None,
     y=None,
     color=None,
@@ -415,7 +415,7 @@ def violin(
 
 
 def box(
-    data_frame,
+    data_frame=None,
     x=None,
     y=None,
     color=None,
@@ -461,7 +461,7 @@ def box(
 
 
 def strip(
-    data_frame,
+    data_frame=None,
     x=None,
     y=None,
     color=None,
@@ -511,7 +511,7 @@ def strip(
 
 
 def scatter_3d(
-    data_frame,
+    data_frame=None,
     x=None,
     y=None,
     z=None,
@@ -562,7 +562,7 @@ def scatter_3d(
 
 
 def line_3d(
-    data_frame,
+    data_frame=None,
     x=None,
     y=None,
     z=None,
@@ -608,7 +608,7 @@ def line_3d(
 
 
 def scatter_ternary(
-    data_frame,
+    data_frame=None,
     a=None,
     b=None,
     c=None,
@@ -647,7 +647,7 @@ def scatter_ternary(
 
 
 def line_ternary(
-    data_frame,
+    data_frame=None,
     a=None,
     b=None,
     c=None,
@@ -682,7 +682,7 @@ def line_ternary(
 
 
 def scatter_polar(
-    data_frame,
+    data_frame=None,
     r=None,
     theta=None,
     color=None,
@@ -726,7 +726,7 @@ def scatter_polar(
 
 
 def line_polar(
-    data_frame,
+    data_frame=None,
     r=None,
     theta=None,
     color=None,
@@ -766,7 +766,7 @@ def line_polar(
 
 
 def bar_polar(
-    data_frame,
+    data_frame=None,
     r=None,
     theta=None,
     color=None,
@@ -804,7 +804,7 @@ def bar_polar(
 
 
 def choropleth(
-    data_frame,
+    data_frame=None,
     lat=None,
     lon=None,
     locations=None,
@@ -844,7 +844,7 @@ def choropleth(
 
 
 def scatter_geo(
-    data_frame,
+    data_frame=None,
     lat=None,
     lon=None,
     locations=None,
@@ -888,7 +888,7 @@ def scatter_geo(
 
 
 def line_geo(
-    data_frame,
+    data_frame=None,
     lat=None,
     lon=None,
     locations=None,
@@ -930,7 +930,7 @@ def line_geo(
 
 
 def scatter_mapbox(
-    data_frame,
+    data_frame=None,
     lat=None,
     lon=None,
     color=None,
@@ -966,7 +966,7 @@ def scatter_mapbox(
 
 
 def line_mapbox(
-    data_frame,
+    data_frame=None,
     lat=None,
     lon=None,
     color=None,
@@ -997,7 +997,7 @@ def line_mapbox(
 
 
 def scatter_matrix(
-    data_frame,
+    data_frame=None,
     dimensions=None,
     color=None,
     symbol=None,
@@ -1035,7 +1035,7 @@ def scatter_matrix(
 
 
 def parallel_coordinates(
-    data_frame,
+    data_frame=None,
     dimensions=None,
     color=None,
     labels={},
@@ -1059,7 +1059,7 @@ def parallel_coordinates(
 
 
 def parallel_categories(
-    data_frame,
+    data_frame=None,
     dimensions=None,
     color=None,
     labels={},
diff --git a/packages/python/plotly/plotly/express/_core.py b/packages/python/plotly/plotly/express/_core.py
index 3d55d3004da..ba28ad71ea8 100644
--- a/packages/python/plotly/plotly/express/_core.py
+++ b/packages/python/plotly/plotly/express/_core.py
@@ -5,7 +5,7 @@
 from _plotly_utils.basevalidators import ColorscaleValidator
 from .colors import qualitative, sequential
 import math
-import pandas
+import pandas as pd
 import numpy as np
 
 from plotly.subplots import (
@@ -754,6 +754,42 @@ def apply_default_cascade(args):
         args["marginal_x"] = None
 
 
+def build_or_augment_dataframe(args, attrables, array_attrables):
+    """
+    Constructs an implicit dataframe and modifies `args` in-place.
+    `attrables` is a list of keys into `args`, all of whose corresponding
+    values are converted into columns of a dataframe.
+    Used to be support calls to plotting function that elide a dataframe
+    argument; for example `scatter(x=[1,2], y=[3,4])`.
+    """
+    if args.get("data_frame") is None:
+        df = pd.DataFrame()
+    else:
+        df = args["data_frame"]
+        df = df.reset_index()
+    data_frame_columns = {}
+    for field in attrables:
+        if field in array_attrables:
+            continue
+        argument = args.get(field)
+        if argument is None:
+            continue
+        elif isinstance(argument, str) and argument in df.columns:
+            continue
+        else:  # args[field] should be an array or df or index now
+            try:
+                col_name = argument.name  # pandas df
+            except AttributeError:
+                labels = args.get("labels")
+                col_name = labels[field] if labels and labels.get(field) else field
+            df[col_name] = argument
+            # This sets the label of an attribute to be
+            # the name of the attribute.
+            args[field] = col_name
+    args["data_frame"] = df
+    return args
+
+
 def infer_config(args, constructor, trace_patch):
     # Declare all supported attributes, across all plot types
     attrables = (
@@ -766,6 +802,9 @@ def infer_config(args, constructor, trace_patch):
     array_attrables = ["dimensions", "custom_data", "hover_data"]
     group_attrables = ["animation_frame", "facet_row", "facet_col", "line_group"]
 
+    all_attrables = attrables + group_attrables + ["color"]
+    build_or_augment_dataframe(args, all_attrables, array_attrables)
+
     # Validate that the strings provided as attribute values reference columns
     # in the provided data_frame
     df_columns = args["data_frame"].columns
@@ -1095,7 +1134,7 @@ def make_figure(args, constructor, trace_patch={}, layout_patch={}):
     fig.layout.update(layout_patch)
     fig.frames = frame_list if len(frames) > 1 else []
 
-    fig._px_trendlines = pandas.DataFrame(trendline_rows)
+    fig._px_trendlines = pd.DataFrame(trendline_rows)
 
     configure_axes(args, constructor, fig, orders)
     configure_animation_controls(args, constructor, fig)
diff --git a/packages/python/plotly/plotly/tests/test_core/test_px/test_px_input.py b/packages/python/plotly/plotly/tests/test_core/test_px/test_px_input.py
new file mode 100644
index 00000000000..d488af08a2f
--- /dev/null
+++ b/packages/python/plotly/plotly/tests/test_core/test_px/test_px_input.py
@@ -0,0 +1,29 @@
+import plotly.express as px
+import numpy as np
+import pandas as pd
+
+
+def test_numpy():
+    fig = px.scatter(x=[1, 2, 3], y=[2, 3, 4], color=[1, 3, 9])
+
+
+def test_numpy_labels():
+    fig = px.scatter(
+        x=[1, 2, 3], y=[2, 3, 4], labels={"x": "time"}
+    )  # other labels will be kw arguments
+    assert fig.data[0]["hovertemplate"] == "time=%{x}<br>y=%{y}"
+
+
+def test_with_index():
+    tips = px.data.tips()
+    fig = px.scatter(tips, x="index", y="total_bill")
+    fig = px.scatter(tips, x="index", y=tips.total_bill)
+    assert fig.data[0]["hovertemplate"] == "index=%{x}<br>total_bill=%{y}"
+    # I was not expecting this to work but it does...
+    fig = px.scatter(tips, x="index", y=10 * tips.total_bill)
+    assert fig.data[0]["hovertemplate"] == "index=%{x}<br>total_bill=%{y}"
+
+
+def test_mixed_case():
+    df = pd.DataFrame(dict(time=[1, 2, 3], temperature=[20, 30, 25]))
+    fig = px.scatter(df, x="time", y="temperature", color=[1, 3, 9])


Code review comments:
diff_hunk: @@ -754,6 +754,42 @@ def apply_default_cascade(args):
         args["marginal_x"] = None
 
 
+def build_or_augment_dataframe(args, attrables, array_attrables):
+    """
+    Constructs an implicit dataframe and modifies `args` in-place.
+    `attrables` is a list of keys into `args`, all of whose corresponding
+    values are converted into columns of a dataframe.
+    Used to be support calls to plotting function that elide a dataframe
+    argument; for example `scatter(x=[1,2], y=[3,4])`.
+    """
+    if args.get("data_frame") is None:
+        df = pd.DataFrame()
path: packages/python/plotly/plotly/express/_core.py
text: @user1:
arguably we could just always start with an empty DF and progressively add columns to it.

@author:
done