Repository: bokeh/bokeh (version 3.4)

Issue:
[FEATURE] DataTable from_dataframe() 
### Problem description

Lack of easy way to create simple tables from pandas DataFrames, with some basic colouring or formatting options

### Feature description

Add a from_dataframe() method to the DataTable class

### Potential alternatives

A create_datatable_from_dataframe() standalone function

### Additional information

II can work on this myself if others are interested in the feature


Discussion:
Similarly, a from_dict() method could be implemented.
Both should be static methods in order to work like this:

`table = DataTable.from_dataframe(df, formatters={'col1': my_formatter})`

I made some tests on a fork


 `@staticmethod`

    def from_dataframe(dataframe, formatters={}, editors={}, **kwargs) -> DataTable:

        """Creates a DataTable from the columns and values of a pandas DataFrame.
        formatters: Dictionnary of instantiated formatters. Keys must be in the DataFrame's columns.
        editors: Dictionnary of instantiated editors. Keys must be in the DataFrame's columns.
        """

        columns = [TableColumn(field=col, 
                               title=col, 
                               formatter=formatters[col] if col in formatters else CellFormatter(),
                               editor=editors[col] if col in editors else CellEditor()) for col in dataframe.columns]
        data_table = DataTable(columns=columns, source=ColumnDataSource(dataframe), **kwargs)
        return data_table

This worked on a few exemples I tried, any insights ?
cc @bokeh/dev for any thoughts
I think it's a good idea to have something like this. There is high level api to plot data on figures without explicitly knowing anything about ColumnDataSources e.g. p.line(x=[1,2,3],y=[1,2,3]) , why not something similar for a Table?

My only input/thoughts --> Ideally I'd like to be able to instantiate a Table model 2 different ways:

 1. The current way --> via a collection of TableColumns and pointing to a CDS
 2. Convenience/"high level" method to build one straight from from either a dataframe, a dictionary or a pre-existing CDS. With this method an optional arg specifying a subset of columns to use would be nice to have as well.
 
I agree, manually creating columns is almost always useless when using DataFrames as a data source since the columns already have names. They can still be accessed by `table.columns` afterwards, and so are formatters with `table.columns[i].formatter`.
The high-level method should be able to take in either a formatter to be applied to all columns or a list/dictionnary of formatters
@Azaya89 is this something you might be interested to look into?
> @Azaya89 is this something you might be interested to look into?

Sorry for the late reply. This seems similar to the discussion we had a while ago. I could take a look at it in the next few days if that's OK?
@Azaya89 any time that works for you!

Patch:
diff --git a/src/bokeh/models/widgets/tables.py b/src/bokeh/models/widgets/tables.py
--- a/src/bokeh/models/widgets/tables.py
+++ b/src/bokeh/models/widgets/tables.py
@@ -45,6 +45,7 @@
     Required,
     String,
 )
+from ...core.property.singletons import Intrinsic
 from ...model import Model
 from ..sources import CDSView, ColumnDataSource, DataSource
 from .widget import Widget
@@ -858,6 +859,54 @@ def __init__(self, *args, **kwargs) -> None:
     The height of each row in pixels.
     """)
 
+    @staticmethod
+    def from_data(data, columns=None, formatters={}, **kwargs) -> DataTable:
+        """ Create a simple table from a pandas dataframe, dictionary or ColumnDataSource.
+
+        Args:
+            data (DataFrame or dict or ColumnDataSource) :
+                The data to create the table from. If the data is a dataframe
+                or dictionary, a ColumnDataSource will be created from it.
+
+            columns (list, optional) :
+                A list of column names to use from the input data.
+                If None, use all columns. (default: None)
+
+            formatters (dict, optional) :
+                A mapping of column names and corresponding Formatters to
+                apply to each column. (default: None)
+
+        Keyword arguments:
+            Any additional keyword arguments will be passed to DataTable.
+
+        Returns:
+            DataTable
+
+        Raises:
+            ValueError
+                If the provided data is not a ColumnDataSource
+                or a data source that a ColumnDataSource can be created from.
+
+        """
+
+        if isinstance(data, ColumnDataSource):
+            source = data.clone()
+        else:
+            try:
+                source = ColumnDataSource(data)
+            except ValueError as e:
+                raise ValueError("Expected a ColumnDataSource or something a ColumnDataSource can be created from like a dict or a DataFrame") from e
+
+        if columns is not None:
+            source.data = {col: source.data[col] for col in columns}
+
+        table_columns = []
+        for c in source.data.keys():
+            formatter = formatters.get(c, Intrinsic)
+            table_columns.append(TableColumn(field=c, title=c, formatter=formatter))
+
+        return DataTable(source=source, columns=table_columns, index_position=None, **kwargs)
+
 class GroupingInfo(Model):
     '''Describes how to calculate totals and sub-totals
 


Test patch:
diff --git a/tests/unit/bokeh/models/test_sources.py b/tests/unit/bokeh/models/test_sources.py
--- a/tests/unit/bokeh/models/test_sources.py
+++ b/tests/unit/bokeh/models/test_sources.py
@@ -25,7 +25,7 @@
 import pandas as pd
 
 # Bokeh imports
-from bokeh.models import Selection
+from bokeh.models import ColumnDataSource, DataTable, Selection
 from bokeh.util.serialization import convert_datetime_array
 from bokeh.util.warnings import BokehDeprecationWarning
 
@@ -785,6 +785,37 @@ def test_data_column_lengths(self) -> None:
         assert len(warns) == 1
         assert str(warns[0].message) == "ColumnDataSource's columns must be of the same length. Current lengths: ('a', 3), ('b', 2)"
 
+class TestDataTable:
+    def test_from_data_with_dataframe(self):
+        df = pd.DataFrame({'A': [1, 2, 3], 'B': [4, 5, 6]})
+        table = DataTable.from_data(df)
+        assert isinstance(table, DataTable)
+        assert set(table.source.data) - {"index"} == {'A', 'B'}
+
+    def test_from_data_with_dict(self):
+        data = {'A': [1, 2, 3], 'B': [4, 5, 6]}
+        table = DataTable.from_data(data)
+        assert isinstance(table, DataTable)
+        assert set(table.source.data) == {'A', 'B'}
+
+    def test_from_data_with_columndatasource(self):
+        source = ColumnDataSource({'A': [1, 2, 3], 'B': [4, 5, 6]})
+        table = DataTable.from_data(source)
+        assert isinstance(table, DataTable)
+        assert set(table.source.data) == {'A', 'B'}
+        assert source is not table.source
+
+    @pytest.mark.parametrize("typ", (dict, pd.DataFrame, ColumnDataSource))
+    def test_from_data_with_columns(self, typ):
+        data = typ({'A': [1, 2, 3], 'B': [4, 5, 6], 'C': [7, 8, 9]})
+        table = DataTable.from_data(data, columns=['A', 'B'])
+        assert isinstance(table, DataTable)
+        assert set(table.source.data) == {'A', 'B'}
+
+    def test_from_data_with_invalid_data(self):
+        with pytest.raises(ValueError, match="Expected a ColumnDataSource or something a ColumnDataSource can be created from like a dict or a DataFrame"):
+            DataTable.from_data("invalid data")
+
 #-----------------------------------------------------------------------------
 # Dev API
 #-----------------------------------------------------------------------------