Repository: holoviz/holoviews

Issue:
Area stack requires individual elements to share the same vdim
HoloViews 1.15.4
Bokeh 2.4.3
Python 3.10
In notebooks and server

####

I need to stack several columns of a dataframe. However `hv.Area.stack` yields an empty plot.

```python
import pandas as pd
import holoviews as hv
hv.extension('bokeh')

df = pd.DataFrame({'x': [1, 2, 3], 'y_1': [1, 2, 3], 'y_2': [6, 4, 2], 'y_3': [8, 1, 2]})
overlay = hv.Overlay([hv.Area(df, kdims='x', vdims=col, label=col) for col in ['y_1', 'y_2', 'y_3']])

hv.Area.stack(overlay)
```

This is because it is expected that the different elements will share the same vdim. [In the docs](https://holoviews.org/reference/elements/bokeh/Area.html) a numpy array is used and the elements share the same `value` vdim. But I think the need to display stacked columns of a dataframe is quite common. I suppose you could circumvent the issue by feeding `Areas` columns instead of the dataframe, but in my situation I need to use the API like `Element(df, kdims=kdims, vdims=vdims)`.

In the [source code](https://github.com/holoviz/holoviews/blob/38a4350606e7154f9858d432ca23c84d0321e4b8/holoviews/element/chart.py#L238), the Overlay is cast to a NdOverlay as follows:

```
:Overlay
   .Area.Y_1 :Area   [x]   (y_1)
   .Area.Y_2 :Area   [x]   (y_2)
   .Area.Y_3 :Area   [x]   (y_3)
```
becomes
```
:NdOverlay   [Element]
   :Area   [x]   (y_3)
```

 and the resulting dframe that is used to build the stacked areas is:
```
           y_1  y_2  y_3
Element x               
0       1  1.0  NaN  NaN
        2  2.0  NaN  NaN
        3  3.0  NaN  NaN
1       1  NaN  6.0  NaN
        2  NaN  4.0  NaN
        3  NaN  2.0  NaN
2       1  NaN  NaN  8.0
        2  NaN  NaN  1.0
        3  NaN  NaN  2.0
```

Then the vdim is determined to be `y_3` throughout the for loop that follows and the corresponding dataframe column is used for each element of the stack, meaning you end up with NaNs everywhere.

Here is a modified version to make it work without a complete overhaul:
```python
    @classmethod
    def stack(cls, areas, baseline_name='Baseline'):
        """
        Stacks an (Nd)Overlay of Area or Curve Elements by offsetting
        their baselines. To stack a HoloMap or DynamicMap use the map
        method.
        """
        if not len(areas):
            return areas
        is_overlay = isinstance(areas, Overlay)
        if is_overlay:
            element_vdims = [[element.vdims[0], baseline_name] for element in areas]
            areas = NdOverlay({i: el for i, el in enumerate(areas)})
        df = areas.dframe(multi_index=True)
        levels = list(range(areas.ndims))
        # vdim = areas.last.vdims[0]
        # vdims = [vdim, baseline_name]
        baseline = None
        stacked = areas.clone(shared_data=False)
        for (key, sdf), vdims in zip(df.groupby(level=levels, sort=False), element_vdims):
            vdim = vdims[0]
            sdf = sdf.droplevel(levels).reindex(index=df.index.unique(-1), fill_value=0)
            if baseline is None:
                sdf[baseline_name] = 0
            else:
                sdf[vdim.name] = sdf[vdim.name] + baseline
                sdf[baseline_name] = baseline
            baseline = sdf[vdim.name]
            stacked[key] = areas[key].clone(sdf, vdims=vdims)
        return Overlay(stacked.values()) if is_overlay else stacked
```


Discussion:
This is a reasonable request. 

Can you submit a PR with your suggested changes, and then we can take it from there? 
Sure!

Resolving patch:
diff --git a/holoviews/element/chart.py b/holoviews/element/chart.py
--- a/holoviews/element/chart.py
+++ b/holoviews/element/chart.py
@@ -248,11 +248,11 @@ def stack(cls, areas, baseline_name='Baseline'):
             areas = NdOverlay({i: el for i, el in enumerate(areas)})
         df = areas.dframe(multi_index=True)
         levels = list(range(areas.ndims))
-        vdim = areas.last.vdims[0]
-        vdims = [vdim, baseline_name]
+        vdims = [[el.vdims[0], baseline_name] for el in areas]
         baseline = None
         stacked = areas.clone(shared_data=False)
-        for key, sdf in df.groupby(level=levels, sort=False):
+        for (key, sdf), element_vdims in zip(df.groupby(level=levels, sort=False), vdims):
+            vdim = element_vdims[0]
             sdf = sdf.droplevel(levels).reindex(index=df.index.unique(-1), fill_value=0)
             if baseline is None:
                 sdf[baseline_name] = 0
@@ -260,5 +260,5 @@ def stack(cls, areas, baseline_name='Baseline'):
                 sdf[vdim.name] = sdf[vdim.name] + baseline
                 sdf[baseline_name] = baseline
             baseline = sdf[vdim.name]
-            stacked[key] = areas[key].clone(sdf, vdims=vdims)
+            stacked[key] = areas[key].clone(sdf, vdims=element_vdims)
         return Overlay(stacked.values()) if is_overlay else stacked


Test patch:
diff --git a/holoviews/tests/plotting/bokeh/test_areaplot.py b/holoviews/tests/plotting/bokeh/test_areaplot.py
--- a/holoviews/tests/plotting/bokeh/test_areaplot.py
+++ b/holoviews/tests/plotting/bokeh/test_areaplot.py
@@ -1,8 +1,8 @@
 import datetime as dt
-
+import pandas as pd
 import numpy as np
 
-from holoviews.element import Area
+from holoviews.element import Area, Overlay
 
 from ...utils import LoggingComparisonTestCase
 from .test_plot import TestBokehPlot, bokeh_renderer
@@ -152,3 +152,11 @@ def test_area_legend(self):
         overlay = Area.stack(python * pypy * jython)
         labels = [n[1] for n in overlay.data]
         self.assertEqual(labels, ['Python', 'Pypy', 'Jython'])
+
+    def test_area_stack_vdims(self):
+        df = pd.DataFrame({'x': [1, 2, 3], 'y_1': [1, 2, 3], 'y_2': [6, 4, 2], 'y_3': [8, 1, 2]})
+        overlay = Overlay([Area(df, kdims='x', vdims=col, label=col) for col in ['y_1', 'y_2', 'y_3']])
+        plot = Area.stack(overlay)
+        baselines = [np.array([0, 0, 0]), np.array([1., 2., 3.]), np.array([7., 6., 5.])]
+        for n, baseline in zip(plot.data, baselines):
+            self.assertEqual(plot.data[n].data.Baseline.to_numpy(), baseline)
diff --git a/holoviews/tests/plotting/matplotlib/test_areaplot.py b/holoviews/tests/plotting/matplotlib/test_areaplot.py
--- a/holoviews/tests/plotting/matplotlib/test_areaplot.py
+++ b/holoviews/tests/plotting/matplotlib/test_areaplot.py
@@ -1,4 +1,6 @@
-from holoviews.element import Area
+from holoviews.element import Area, Overlay
+import pandas as pd
+import numpy as np
 
 from ...utils import LoggingComparisonTestCase
 from .test_plot import TestMPLPlot, mpl_renderer
@@ -96,3 +98,11 @@ def test_area_padding_logy(self):
         self.assertEqual(y_range[0], 0.03348369522101712)
         self.assertEqual(y_range[1], 3.3483695221017129)
         self.log_handler.assertContains('WARNING', 'Logarithmic axis range encountered value less than')
+
+    def test_area_stack_vdims(self):
+        df = pd.DataFrame({'x': [1, 2, 3], 'y_1': [1, 2, 3], 'y_2': [6, 4, 2], 'y_3': [8, 1, 2]})
+        overlay = Overlay([Area(df, kdims='x', vdims=col, label=col) for col in ['y_1', 'y_2', 'y_3']])
+        plot = Area.stack(overlay)
+        baselines = [np.array([0, 0, 0]), np.array([1., 2., 3.]), np.array([7., 6., 5.])]
+        for n, baseline in zip(plot.data, baselines):
+            self.assertEqual(plot.data[n].data.Baseline.to_numpy(), baseline)
diff --git a/holoviews/tests/plotting/plotly/test_areaplot.py b/holoviews/tests/plotting/plotly/test_areaplot.py
--- a/holoviews/tests/plotting/plotly/test_areaplot.py
+++ b/holoviews/tests/plotting/plotly/test_areaplot.py
@@ -1,6 +1,7 @@
 import numpy as np
+import pandas as pd
 
-from holoviews.element import Area
+from holoviews.element import Area, Overlay
 
 from .test_plot import TestPlotlyPlot
 
@@ -55,3 +56,11 @@ def test_area_visible(self):
         curve = Area([1, 2, 3]).opts(visible=False)
         state = self._get_plot_state(curve)
         self.assertEqual(state['data'][0]['visible'], False)
+
+    def test_area_stack_vdims(self):
+        df = pd.DataFrame({'x': [1, 2, 3], 'y_1': [1, 2, 3], 'y_2': [6, 4, 2], 'y_3': [8, 1, 2]})
+        overlay = Overlay([Area(df, kdims='x', vdims=col, label=col) for col in ['y_1', 'y_2', 'y_3']])
+        plot = Area.stack(overlay)
+        baselines = [np.array([0, 0, 0]), np.array([1., 2., 3.]), np.array([7., 6., 5.])]
+        for n, baseline in zip(plot.data, baselines):
+            self.assertEqual(plot.data[n].data.Baseline.to_numpy(), baseline)