Repository: matplotlib/matplotlib
Pull request: Feature: Support passing DataFrames to table.table
## PR summary
Resolves #28726 : feature request: support passing DataFrames to table.table
[dawnwangcx](https://github.com/dawnwangcx) wanted a feature to enable building tables using Pandas's DataFrame object. I implemented his solution and made a Unit test in test_table.py to confirm the DataFrame  was correctly converted into a table.



## PR checklist
<!-- Please mark any checkboxes that do not apply to this PR as [N/A].-->

- [x] "closes #0000" is in the body of the PR description to [link the related issue](https://github.com/matplotlib/matplotlib/issues/28726)
- [x] new and changed code is [tested](https://matplotlib.org/devdocs/devel/testing.html)
- [x] [N/A] *Plotting related* features are demonstrated in an [example](https://matplotlib.org/devdocs/devel/document.html#write-examples-and-tutorials)
- [x] *New Features* and *API Changes* are noted with a [directive and release note](https://matplotlib.org/devdocs/devel/api_changes.html#announce-changes-deprecations-and-new-features)
- [x] Documentation complies with [general](https://matplotlib.org/devdocs/devel/document.html#write-rest-pages) and [docstring](https://matplotlib.org/devdocs/devel/document.html#write-docstrings) guidelines

<!--We understand that PRs can sometimes be overwhelming, especially as the
reviews start coming in.  Please let us know if the reviews are unclear or
the recommended next step seems overly demanding, if you would like help in
addressing a reviewer's comments, or if you have been waiting too long to hear
back on your PR.-->


Problem statement:
feature request: support passing DataFrames to table.table
### Bug summary

When trying to input a dataframe to a table, it will raise keyerror. Since cellText [0] will not give the first row of the dataframe and thus not returning the correct column length. So, I made a modification to recognize cellText as dataframe.
contact: chenxuw@alumni.cmu.edu

### Code for reproduction

```Python
df = pd.DataFrame() #any dataframe
table = table.table(ax,df,loc='center')
```


### Actual outcome

Traceback (most recent call last):
  File "C:\Users\053232\AppData\Local\Programs\Python\Python38\lib\site-packages\pandas\core\indexes\base.py", line 3653, in get_loc
    return self._engine.get_loc(casted_key)
  File "pandas\_libs\index.pyx", line 147, in pandas._libs.index.IndexEngine.get_loc
  File "pandas\_libs\index.pyx", line 176, in pandas._libs.index.IndexEngine.get_loc
  File "pandas\_libs\hashtable_class_helper.pxi", line 7080, in pandas._libs.hashtable.PyObjectHashTable.get_item
  File "pandas\_libs\hashtable_class_helper.pxi", line 7088, in pandas._libs.hashtable.PyObjectHashTable.get_item
KeyError: 0


The above exception was the direct cause of the following exception:


Traceback (most recent call last):
  File "c:/Users/Downloads/suspension_check.py", line 147, in <module>
    generate_image(suspended_etfs,'suspended_etfs')
  File "c:/Users/Downloads/suspension_check.py", line 106, in generate_image
    tbl = table.table(ax, df, loc='center')
  File "C:\Users\AppData\Local\Programs\Python\Python38\lib\site-packages\matplotlib\table.py", line 754, in table
    cols = len(cellText[0])
  File "C:\Users\AppData\Local\Programs\Python\Python38\lib\site-packages\pandas\core\frame.py", line 3761, in __getitem__
    indexer = self.columns.get_loc(key)
  File "C:\Users\AppData\Local\Programs\Python\Python38\lib\site-packages\pandas\core\indexes\base.py", line 3655, in get_loc
    raise KeyError(key) from err
KeyError: 0

### Expected outcome

 return a table contains a dataframe 

### Additional information

# Check we have some cellText
    if cellText is None:
        # assume just colours are needed
        rows = len(cellColours)
        cols = len(cellColours[0])
        cellText = [[''] * cols] * rows
    elif isinstance(cellText, pd.DataFrame):
        # 获取DataFrame的列名
        column_names = cellText.columns.values
        cellText = np.vstack([column_names, cellText.values])

### Operating system

_No response_

### Matplotlib Version

3.7.5

### Matplotlib Backend

_No response_

### Python version

_No response_

### Jupyter version

_No response_

### Installation

pip

Hints from issue discussion:
I would not consider this a bug a the input is documented as a 2D list of strings (https://matplotlib.org/stable/api/table_api.html#matplotlib.table.table) .

That said, this may be a reasonable feature request as I don't think it would be too hard to detect if we were passed a `pd.DataFrame` and then convert it to a list-of-list-of-str at the top of `table.table`.
@dawnwangcx You are welcome to submit you proposal as a pull request.

Commit message: Added check for Pandas Dataframe in table.table

Patch to review:
diff --git a/lib/matplotlib/table.py b/lib/matplotlib/table.py
index 0f75021926fd..adfcbae4b0c9 100644
--- a/lib/matplotlib/table.py
+++ b/lib/matplotlib/table.py
@@ -25,6 +25,7 @@
 """
 
 import numpy as np
+import pandas as pd
 
 from . import _api, _docstring
 from .artist import Artist, allow_rasterization
@@ -655,7 +656,7 @@ def table(ax,
           cellLoc='right', colWidths=None,
           rowLabels=None, rowColours=None, rowLoc='left',
           colLabels=None, colColours=None, colLoc='center',
-          loc='bottom', bbox=None, edges='closed',
+          loc='bottom', bbox=None, edges='closed', df=None,
           **kwargs):
     """
     Add a table to an `~.axes.Axes`.
@@ -674,7 +675,7 @@ def table(ax,
 
     Parameters
     ----------
-    cellText : 2D list of str, optional
+    cellText : 2D list of str, Pandas.DataFrame, optional
         The texts to place into the table cells.
 
         *Note*: Line breaks in the strings are currently not accounted for and
@@ -743,6 +744,11 @@ def table(ax,
         rows = len(cellColours)
         cols = len(cellColours[0])
         cellText = [[''] * cols] * rows
+    # Convert the Pandas dataframe to a 2d string array
+    elif isinstance(cellText, pd.DataFrame):
+        headers = cellText.columns.tolist()
+        data = cellText.map(str).values
+        cellText = np.vstack([headers, data])
 
     rows = len(cellText)
     cols = len(cellText[0])


Code review comments:
diff_hunk: @@ -25,6 +25,7 @@
 """
 
 import numpy as np
+import pandas as pd
path: lib/matplotlib/table.py
text: @user1:
pandas is an optional dependency for matplotlib. We cannot rely that pandas is available. (It is in the test environment so that we can test against it).

@author:
Gotcha. Ill remove it and find another way

diff_hunk: @@ -655,7 +656,7 @@ def table(ax,
           cellLoc='right', colWidths=None,
           rowLabels=None, rowColours=None, rowLoc='left',
           colLabels=None, colColours=None, colLoc='center',
-          loc='bottom', bbox=None, edges='closed',
+          loc='bottom', bbox=None, edges='closed', df=None,
path: lib/matplotlib/table.py
text: @user1:
```suggestion
          loc='bottom', bbox=None, edges='closed',
```
This isn't used. Looks like this came in accidentally.

@author:
You are right. I fixed this in the next commit.

diff_hunk: @@ -743,6 +744,11 @@ def table(ax,
         rows = len(cellColours)
         cols = len(cellColours[0])
         cellText = [[''] * cols] * rows
+    # Convert the Pandas dataframe to a 2d string array
+    elif isinstance(cellText, pd.DataFrame):
path: lib/matplotlib/table.py
text: @user1:
Because pandas is an optional dependency, we cannot `isinstance` it here. The typical approach would be duck-typing, see 

https://github.com/matplotlib/matplotlib/blob/64d45cb8f45a38109b729cd3e1835e2dd36e65fc/lib/matplotlib/cbook.py#L2395

but since you need the column headers, that may not be sufficient here. It think we have to go though someting analogous to 

https://github.com/matplotlib/matplotlib/blob/64d45cb8f45a38109b729cd3e1835e2dd36e65fc/lib/matplotlib/cbook.py#L2356





@author:
Following your idea, I implemented a _is_pandas_dataframe(x) method and modified unpack_to_numpy to include it. I used these two methods inside the table.py instead and validated the change with a unit test.