Repository: holoviz/panel

Issue:
Tabulator streaming sets nan index for inital empty Dataframes
Hello,

i have copied this from my post at [https://discourse.holoviz.org/t/how-to-stream-to-initially-empty-tabulator/2167](https://discourse.holoviz.org/t/how-to-stream-to-initially-empty-tabulator/2167)

im using the new Tabulator widget and trying the streaming tutorially from this page [https://panel.holoviz.org/reference/widgets/Tabulator.html#streaming](https://panel.holoviz.org/reference/widgets/Tabulator.html#streaming) and the example works.
But if i start from an initially empty dataframe for the tabulator, like this
```python
different_sized_stream_df = pd.DataFrame(np.random.randn(0, 5), columns=list('ABCDE'))
different_sizedstream_table = pn.widgets.Tabulator(different_sized_stream_df, layout='fit_columns', width=450, height=400)

def different_sized_stream_data(follow=True):
    different_sized_stream_df = pd.DataFrame(np.random.randn(10, 5), columns=list('ABCDE'))
    different_sizedstream_table.stream(different_sized_stream_df, follow=follow)

pn.state.add_periodic_callback(different_sized_stream_data, period=1000)

pn.Column(
pn.Row(different_sizedstream_table)
).servable()
```

then no values are shown and i get this error:
```pytb
2021-04-19 20:31:01,858 Exception in callback <bound method PeriodicCallback._periodic_callback of PeriodicCallback(callback=<function different_sized_stream_data at 0x0000018F0516F8B8>, count=None, name='PeriodicCallback00101', per
iod=1000, running=True, timeout=None)>
Traceback (most recent call last):
  File "PATH\.venv\lib\site-packages\tornado\ioloop.py", line 905, in _run
    return self.callback()
  File "PATH\.venv\lib\site-packages\panel\io\callbacks.py", line 70, in _periodic_callback
    self.callback()
  File "PATH\panel_dev.py", line 21, in different_sized_stream_data
    different_sizedstream_table.stream(different_sized_stream_df, follow=follow)
  File "PATH\.venv\lib\site-packages\panel\widgets\tables.py", line 851, in stream
    super().stream(stream_value, rollover, reset_index)
  File "PATH\.venv\lib\site-packages\panel\widgets\tables.py", line 394, in stream
    self.param.trigger('value')
  File "PATH\.venv\lib\site-packages\param\parameterized.py", line 1541, in trigger
    self_.set_param(**dict(params, **triggers))
  File "PATH\.venv\lib\site-packages\param\parameterized.py", line 1472, in set_param
    self_._batch_call_watchers()
  File "PATH\.venv\lib\site-packages\param\parameterized.py", line 1611, in _batch_call_watchers
    self_._execute_watcher(watcher, events)
  File "PATH\.venv\lib\site-packages\param\parameterized.py", line 1573, in _execute_watcher
    watcher.fn(*args, **kwargs)
  File "PATH\.venv\lib\site-packages\panel\widgets\tables.py", line 748, in _validate
    self.style = self.value.style
  File "PATH\.venv\lib\site-packages\pandas\core\frame.py", line 901, in style
    return Styler(self)
  File "PATH\.venv\lib\site-packages\pandas\io\formats\style.py", line 156, in __init__
    raise ValueError("style is not supported for non-unique indices.")
ValueError: style is not supported for non-unique indices.
```

The culprit seems to be this line at [https://github.com/holoviz/panel/blob/44be3cfbc069ee2579b780aadc1f4076a70473f7/panel/widgets/tables.py#L382](https://github.com/holoviz/panel/blob/44be3cfbc069ee2579b780aadc1f4076a70473f7/panel/widgets/tables.py#L382)
```python
 value_index_start = self.value.index.max() + 1
        if isinstance(stream_value, pd.DataFrame):
            if reset_index:
                stream_value = stream_value.reset_index(drop=True)
                stream_value.index += value_index_start
```

self.value.index.max() gives np.nan for an empty index, therefore all resulting  index values will be np.nan.
and consequently non-unique when the styling is set here [https://github.com/holoviz/panel/blob/44be3cfbc069ee2579b780aadc1f4076a70473f7/panel/widgets/tables.py#L742](https://github.com/holoviz/panel/blob/44be3cfbc069ee2579b780aadc1f4076a70473f7/panel/widgets/tables.py#L742).
```python
    def _validate(self, event):
        super()._validate(event)
        if self.value is not None:
            todo = []
            if self.style is not None:
                todo = self.style._todo
            self.style = self.value.style
            self.style._todo = todo
```

A kinda hacky workaround is settin an index on the empty initial dataframe like this
```
pd.DataFrame([], columns=list('ABCDE'), index=[0,1,2])
```

then the updating works, but it shows the fake index rows as all nan-cells which looks ugly.

Best option would be to probably safeguard the starting index with something like this i guess:
```
        if self.value.index.max() is np.nan: # or self.value.size
            value_index_start = 1
        else:
            value_index_start = self.value.index.max() + 1
```


Discussion:

Resolving patch:
diff --git a/panel/widgets/tables.py b/panel/widgets/tables.py
--- a/panel/widgets/tables.py
+++ b/panel/widgets/tables.py
@@ -388,7 +388,12 @@ def stream(self, stream_value, rollover=None, reset_index=True):
         {'x': [1, 2, 3, 4], 'y': ['a', 'b', 'c', 'd']}
         """
         import pandas as pd
-        value_index_start = self.value.index.max() + 1
+
+        if not np.isfinite(self.value.index.max()):
+            value_index_start = 1
+        else:
+            value_index_start = self.value.index.max() + 1
+
         if isinstance(stream_value, pd.DataFrame):
             if reset_index:
                 stream_value = stream_value.reset_index(drop=True)


Test patch:
diff --git a/panel/tests/widgets/test_tables.py b/panel/tests/widgets/test_tables.py
--- a/panel/tests/widgets/test_tables.py
+++ b/panel/tests/widgets/test_tables.py
@@ -194,6 +194,8 @@ def test_hierarchical_index(document, comm):
     assert isinstance(agg2, MinAggregator)
 
 
+
+
 def test_none_table(document, comm):
     table = DataFrame(value=None)
     assert table.indexes == []
@@ -393,6 +395,19 @@ def high_red(value):
         4: {1: ['color: red']}
     }
 
+def test_tabulator_empty_table(document, comm):
+    value_df = makeMixedDataFrame()
+    empty_df = pd.DataFrame([], columns=value_df.columns)
+    table = Tabulator(empty_df)
+
+    table.get_root(document, comm)
+
+    assert table.value.shape == empty_df.shape
+
+    table.stream(value_df, follow=True)
+
+    assert table.value.shape == value_df.shape
+
 def test_tabulator_stream_series(document, comm):
     df = makeMixedDataFrame()
     table = Tabulator(df)