From the Windows 8 Camps: GridViews with Inlined Group Headers

One of the questions that got asked at a recent Windows 8 developer camp was around how the XAML GridView control can be used to set up a view that looks something like the one used in the People app which is a bit like this;

image 

That is – a GridView that is grouping its data but is then displaying the group headers inline with the rest of the data. This is different from the way you’d see a GridView in the Grid application templates that ship in Visual Studio.

The GridView is a complex control with quite a lot of templates associated with it and, in some ways, working with it reminds me of working with the DataGrid control in WPF/Silverlight in that quite often I’m looking at a piece of UI and wondering which template is involved in putting that piece of UI onto the screen.

I’m not sure what the ‘best’ way is of figuring out a control like this one but one approach I sometimes take is to wander into Visual Studio and take an instance of the control and edit a copy of its template using the graphical editor. With a GridView that gives me;

 <Style
      x:Key="GridViewStyle1"
      TargetType="GridView">
      <Setter
        Property="Padding"
        Value="0,0,0,10" />
      <Setter
        Property="IsTabStop"
        Value="False" />
      <Setter
        Property="TabNavigation"
        Value="Once" />
      <Setter
        Property="ScrollViewer.HorizontalScrollBarVisibility"
        Value="Auto" />
      <Setter
        Property="ScrollViewer.VerticalScrollBarVisibility"
        Value="Disabled" />
      <Setter
        Property="ScrollViewer.HorizontalScrollMode"
        Value="Enabled" />
      <Setter
        Property="ScrollViewer.IsHorizontalRailEnabled"
        Value="False" />
      <Setter
        Property="ScrollViewer.VerticalScrollMode"
        Value="Disabled" />
      <Setter
        Property="ScrollViewer.IsVerticalRailEnabled"
        Value="False" />
      <Setter
        Property="ScrollViewer.ZoomMode"
        Value="Disabled" />
      <Setter
        Property="IsSwipeEnabled"
        Value="True" />
      <Setter
        Property="ItemContainerTransitions">
        <Setter.Value>
          <TransitionCollection>
            <AddDeleteThemeTransition />
            <ContentThemeTransition />
            <ReorderThemeTransition />
            <EntranceThemeTransition
              IsStaggeringEnabled="False" />
          </TransitionCollection>
        </Setter.Value>
      </Setter>
      <Setter
        Property="ItemsPanel">
        <Setter.Value>
          <ItemsPanelTemplate>
            <WrapGrid
              Orientation="Vertical" />
          </ItemsPanelTemplate>
        </Setter.Value>
      </Setter>
      <Setter
        Property="Template">
        <Setter.Value>
          <ControlTemplate
            TargetType="GridView">
            <Border
              BorderBrush="{TemplateBinding BorderBrush}"
              BorderThickness="{TemplateBinding BorderThickness}"
              Background="{TemplateBinding Background}">
              <ScrollViewer
                x:Name="ScrollViewer"
                HorizontalScrollMode="{TemplateBinding ScrollViewer.HorizontalScrollMode}"
                HorizontalScrollBarVisibility="{TemplateBinding ScrollViewer.HorizontalScrollBarVisibility}"
                IsHorizontalRailEnabled="{TemplateBinding ScrollViewer.IsHorizontalRailEnabled}"
                IsHorizontalScrollChainingEnabled="{TemplateBinding ScrollViewer.IsHorizontalScrollChainingEnabled}"
                IsVerticalScrollChainingEnabled="{TemplateBinding ScrollViewer.IsVerticalScrollChainingEnabled}"
                IsVerticalRailEnabled="{TemplateBinding ScrollViewer.IsVerticalRailEnabled}"
                TabNavigation="{TemplateBinding TabNavigation}"
                VerticalScrollBarVisibility="{TemplateBinding ScrollViewer.VerticalScrollBarVisibility}"
                VerticalScrollMode="{TemplateBinding ScrollViewer.VerticalScrollMode}"
                ZoomMode="{TemplateBinding ScrollViewer.ZoomMode}">
                <ItemsPresenter
                  HeaderTemplate="{TemplateBinding HeaderTemplate}"
                  Header="{TemplateBinding Header}"
                  HeaderTransitions="{TemplateBinding HeaderTransitions}"
                  Padding="{TemplateBinding Padding}" />
              </ScrollViewer>
            </Border>
          </ControlTemplate>
        </Setter.Value>
      </Setter>
    </Style>

Apologies for pasting in such a large lump of XAML but the style for this control is a large lump of XAML 🙂

What this says to me is that;

  1. A GridView is a ScrollViewer wrapped around an ItemsPresenter.
  2. The items panel that is used is a WrapGrid.
  3. There are some intriguing Header and HeaderTemplate properties that can easily distract you from your purpose if you are thinking about displaying headers on grouped data.

If I take a simple use of a GridView;

    <GridView
      x:Name="myGridView">
      <GridView.Items>
        <x:String>One</x:String>
        <x:String>Two</x:String>
        <x:String>Three</x:String>
        <x:String>Four</x:String>
        <x:String>Five</x:String>
        <x:String>Six</x:String>
        <x:String>Seven</x:String>
        <x:String>Eight</x:String>
        <x:String>Nine</x:String>
        <x:String>Ten</x:String>
      </GridView.Items>
    </GridView>

Then that’s going to create and display my items for me and, in doing so, it’s going to do a few things;

  1. create a panel to put the set of items into.
  2. create an instance of the item template (if any) for each data item.
  3. (if necessary) create a GridViewItem to wrap around each instantiated item template and style that GridViewItem in accordance with the ItemContainerStyle.

I once drew a PowerPoint slide about this which I’ll repeat here – it’s probably not 100% perfect and it predates GridView but I think it’s fairly close to what goes on;

image

By default what I get displayed is;

image

but if I change the ItemTemplate;

 <GridView
      x:Name="myGridView">
      <GridView.ItemTemplate>
        <DataTemplate>
          <Grid Background="Blue">
            <TextBlock
              Text="{Binding}" />            
          </Grid>
        </DataTemplate>
      </GridView.ItemTemplate>
      <GridView.Items>
        <x:String>One</x:String>
        <x:String>Two</x:String>
        <x:String>Three</x:String>
        <x:String>Four</x:String>
        <x:String>Five</x:String>
        <x:String>Six</x:String>
        <x:String>Seven</x:String>
        <x:String>Eight</x:String>
        <x:String>Nine</x:String>
        <x:String>Ten</x:String>
      </GridView.Items>
    </GridView>

then it does the right thing but looks terrible;

image

and you can spend many a long, happy hour trying to figure out where that alignment is coming from until you realise that each item is being wrapped in a GridViewItem in accordance with the ItemContainerStyle as in;

  <GridView
      x:Name="myGridView">
      <GridView.ItemTemplate>
        <DataTemplate>
          <Grid Background="Blue">
            <TextBlock
              Text="{Binding}"
              TextAlignment="Center"
              VerticalAlignment="Center" />
          </Grid>
        </DataTemplate>
      </GridView.ItemTemplate>
      <GridView.ItemContainerStyle>
        <Style TargetType="GridViewItem">
          <Setter Property="HorizontalContentAlignment" Value="Stretch">            
          </Setter>
          <Setter
            Property="VerticalContentAlignment"
            Value="Stretch">
          </Setter>
        </Style>
      </GridView.ItemContainerStyle>
      <GridView.Items>
        <x:String>One</x:String>
        <x:String>Two</x:String>
        <x:String>Three</x:String>
        <x:String>Four</x:String>
        <x:String>Five</x:String>
        <x:String>Six</x:String>
        <x:String>Seven</x:String>
        <x:String>Eight</x:String>
        <x:String>Nine</x:String>
        <x:String>Ten</x:String>
      </GridView.Items>
    </GridView>

and that ItemContainerSyle is contributing things like hover and selection states as illustrated below;

image

and if you go so far as to change the control template for the GridViewItem within that style;

 <GridView
      x:Name="myGridView">
      <GridView.ItemTemplate>
        <DataTemplate>
          <Grid Background="Blue">
            <TextBlock
              Text="{Binding}"
              TextAlignment="Center"
              VerticalAlignment="Center" />
          </Grid>
        </DataTemplate>
      </GridView.ItemTemplate>
      <GridView.ItemContainerStyle>
        <Style TargetType="GridViewItem">
          <Setter Property="HorizontalContentAlignment" Value="Stretch">            
          </Setter>
          <Setter
            Property="VerticalContentAlignment"
            Value="Stretch">
          </Setter>
          <Setter Property="Template">
            <Setter.Value>
              <ControlTemplate>
                <ContentPresenter />
              </ControlTemplate>
            </Setter.Value>
          </Setter>
        </Style>
      </GridView.ItemContainerStyle>
      <GridView.Items>
        <x:String>One</x:String>
        <x:String>Two</x:String>
        <x:String>Three</x:String>
        <x:String>Four</x:String>
        <x:String>Five</x:String>
        <x:String>Six</x:String>
        <x:String>Seven</x:String>
        <x:String>Eight</x:String>
        <x:String>Nine</x:String>
        <x:String>Ten</x:String>
      </GridView.Items>
    </GridView>

Then you lose quite a lot, including things like the hover behaviour and the selection behaviour and so on (unless you go to pains to put them back with your own variants). This is shown below where Item 10 is selected, but you wouldn’t know it any more.

image

Taking that modification away, if I want to change the panel that’s being used to display these items then I can do and it’s relatively easy to do and I can either stick with the WrapGrid that’s being used and tweak it so that it displays itself in different ways;

    <GridView
      x:Name="myGridView">
      <GridView.ItemTemplate>
        <DataTemplate>
          <Grid Background="Blue" Width="192" Height="192">
            <TextBlock
              Text="{Binding}"
              TextAlignment="Center"
              VerticalAlignment="Center" />
          </Grid>
        </DataTemplate>
      </GridView.ItemTemplate>
      <GridView.ItemsPanel>
        <ItemsPanelTemplate>
          <WrapGrid
            Orientation="Horizontal"
            MaximumRowsOrColumns="3" /> 
        </ItemsPanelTemplate>
      </GridView.ItemsPanel>
      <GridView.Items>
        <x:String>One</x:String>
        <x:String>Two</x:String>
        <x:String>Three</x:String>
        <x:String>Four</x:String>
        <x:String>Five</x:String>
        <x:String>Six</x:String>
        <x:String>Seven</x:String>
        <x:String>Eight</x:String>
        <x:String>Nine</x:String>
        <x:String>Ten</x:String>
      </GridView.Items>
    </GridView>

and that line 17 above will give me;

image

or I can change it again;

 <GridView.ItemsPanel>
        <ItemsPanelTemplate>
          <WrapGrid
            Orientation="Vertical"
            MaximumRowsOrColumns="2" /> 
        </ItemsPanelTemplate>
      </GridView.ItemsPanel>

and that line 5 will give me;

image

or maybe change the panel type altogether;

 <GridView.ItemsPanel>
        <ItemsPanelTemplate>
          <StackPanel
            VerticalAlignment="Top"
            Orientation="Horizontal" />
        </ItemsPanelTemplate>
      </GridView.ItemsPanel>

and that line 3-5 will give me;

image

Going back to that Header and HeaderTemplate property – what do they give me? They’re nothing to do with grouping of data and everything to do with just displaying a header on the top of the control so if I simply change my XAML to be;

<Grid
    Background="{StaticResource ApplicationPageBackgroundThemeBrush}">
    <GridView
      x:Name="myGridView">
      <GridView.Header>
        <x:String>My GridView</x:String>
      </GridView.Header>
      <GridView.HeaderTemplate>
        <DataTemplate>
          <Border BorderThickness="1" BorderBrush="Silver">
            <TextBlock
              Text="{Binding}" Style="{StaticResource HeaderTextStyle}" />
          </Border>
        </DataTemplate>
      </GridView.HeaderTemplate>
      <GridView.ItemTemplate>
        <DataTemplate>
          <Grid Background="Blue" Width="192" Height="192">
            <TextBlock
              Text="{Binding}"
              TextAlignment="Center"
              VerticalAlignment="Center" />
          </Grid>
        </DataTemplate>
      </GridView.ItemTemplate>
      <GridView.Items>
        <x:String>One</x:String>
        <x:String>Two</x:String>
        <x:String>Three</x:String>
        <x:String>Four</x:String>
        <x:String>Five</x:String>
        <x:String>Six</x:String>
        <x:String>Seven</x:String>
        <x:String>Eight</x:String>
        <x:String>Nine</x:String>
        <x:String>Ten</x:String>
      </GridView.Items>
    </GridView>

Then that simply adds a header to the GridView with (in this case) a silver border around it;

image

It’s worth saying that ItemTemplates can be controlled from code dynamically to vary on a per-item basis by using an ItemTemplateSelector as can the ItemContainerStyle (using a StyleSelector).

With all of that said, how does grouping creep into this control? First thing – as stated up in the docs – you have to use a CollectionViewSource in order to bind to a collection that is grouped and that brings in the properties IsSourceGrouped and ItemsPath.

If I take a blank or ‘vanilla’ GridView;

    <GridView
      x:Name="myGridView">
    </GridView>

and just ‘manually’ feed it some data rather than binding it;

      Random rand = new Random();

      CollectionViewSource collectionViewSource = new CollectionViewSource();
      collectionViewSource.IsSourceGrouped = true;
      collectionViewSource.ItemsPath = new PropertyPath("Items");

      var groups =
        Enumerable.Range((int)'A', 26).Select(
          letter => 
            {
              var gp = new Group()
              {
                GroupName = string.Format("{0}", (char)letter)
              };

              gp.Items = new List<Item>(
                Enumerable.Range(1, rand.Next(5, 15)).Select(
                  index => new Item()
                  {
                    ItemName = string.Format("{0}{1}", (char)letter, index),
                    Group = gp
                  }
                )
              );              
              return (gp);
            }
        );

      collectionViewSource.Source = groups;

      this.myGridView.ItemsSource = collectionViewSource.View;

then it displays;

image

Then the control is definitely looking inside of my groups to find all of the items but the UI doesn’t really say anything about the groups themselves. Changing the item template still has an effect;

image

and, very clearly, the ItemContainerStyle is still doing its work and GridViewItems are being created. So, where are my groups? It comes into play (as the docs say) when the GroupStyle is changed. For example, even setting a blank looking GroupStyle has an impact;

    <GridView
      x:Name="myGridView">
      <GridView.ItemTemplate>
        <DataTemplate>
          <Grid Background="Blue">
            <TextBlock
              Text="{Binding ItemName}" />
          </Grid>
        </DataTemplate>
      </GridView.ItemTemplate>
      <GridView.GroupStyle>
        <GroupStyle />
      </GridView.GroupStyle>
    </GridView>

in that I now have;

image

which is “interesting” in itself. The GroupStyle has a ContainerStyle (plus a ContainerStyleSelector such that it can be changed dynamically on a per-item basis from code) and then a HeaderTemplate (plus a HeaderTemplateSelector such that it can be changed dynamically on a per-item basis from code) and then a Panel.

In meeting this control for the first time, this was the bit that was most different to me and these items show up as editable templates in Visual Studio or Blend (as do all the other templates I’ve been talking about  :-));

image

but only the item container offers a chance to create a copy of the default style.

If I manipulate the panel that’s being used for my items to be a StackPanel that’s orientated horizontally;

 <GridView
      x:Name="myGridView">
      <GridView.GroupStyle>
        <GroupStyle>
          <GroupStyle.Panel>
            <ItemsPanelTemplate>
              <StackPanel
                Margin="2"
                Orientation="Horizontal"
                Background="Green"/>
            </ItemsPanelTemplate>
          </GroupStyle.Panel>
        </GroupStyle>
      </GridView.GroupStyle>
      <GridView.ItemTemplate>
        <DataTemplate>
          <Grid
            Background="Blue">
            <TextBlock
              Text="{Binding ItemName}" />
          </Grid>
        </DataTemplate>
      </GridView.ItemTemplate>
    </GridView>

then that gives me a layout;

image

or I could use a WrapGrid to give me something like;

    <GridView
      x:Name="myGridView">
      <GridView.GroupStyle>
        <GroupStyle>
          <GroupStyle.Panel>
            <ItemsPanelTemplate>
              <VariableSizedWrapGrid
                Background="Green"
                Margin="2"
                Orientation="Horizontal"
                MaximumRowsOrColumns="3" />
            </ItemsPanelTemplate>
          </GroupStyle.Panel>
        </GroupStyle>
      </GridView.GroupStyle>
      <GridView.ItemTemplate>
        <DataTemplate>
          <Grid Background="Blue">
            <TextBlock
              Text="{Binding ItemName}" />
          </Grid>
        </DataTemplate>
      </GridView.ItemTemplate>
    </GridView>

giving me a layout;

image

And I can manipulate the way in which my headers are being displayed via the HeaderTemplate;

<GridView
      x:Name="myGridView">
      <GridView.GroupStyle>
        <GroupStyle>
          <GroupStyle.Panel>
            <ItemsPanelTemplate>
              <VariableSizedWrapGrid
                Background="Green"
                Margin="2"
                Orientation="Horizontal"
                MaximumRowsOrColumns="3" />
            </ItemsPanelTemplate>
          </GroupStyle.Panel>
          <GroupStyle.HeaderTemplate>
            <DataTemplate>
              <Grid>
                <Ellipse
                  Width="48"
                  Height="48"
                  Fill="White"
                  Margin="1" />
                <TextBlock
                  VerticalAlignment="Center"
                  TextAlignment="Center"
                  Foreground="Black"
                  Style="{StaticResource SubheaderTextStyle}"
                  Text="{Binding GroupName}" />
              </Grid>
            </DataTemplate>
          </GroupStyle.HeaderTemplate>
        </GroupStyle>
      </GridView.GroupStyle>
      <GridView.ItemTemplate>
        <DataTemplate>
          <Grid Background="Blue">
            <TextBlock
              Text="{Binding ItemName}" />
          </Grid>
        </DataTemplate>
      </GridView.ItemTemplate>
    </GridView>

giving me (ok, I know the text isn’t aligned properly);

image

but what if I want to control the relationship between the header and the panel? That’s in the ContainerStyle which defines a Border wrapped around a Grid wrapper around a ContentControl and an ItemsControl. So, if I wanted my group header to be centred to the left of my panel;

 <GridView
      x:Name="myGridView">
      <GridView.GroupStyle>
        <GroupStyle>
          <GroupStyle.Panel>
            <ItemsPanelTemplate>
              <VariableSizedWrapGrid
                Background="Green"
                Margin="2"
                Orientation="Horizontal"
                MaximumRowsOrColumns="3" />
            </ItemsPanelTemplate>
          </GroupStyle.Panel>
          <GroupStyle.HeaderTemplate>
            <DataTemplate>
              <Grid>
                <Ellipse
                  Width="48"
                  Height="48"
                  Fill="White"
                  Margin="1" />
                <TextBlock
                  VerticalAlignment="Center"
                  TextAlignment="Center"
                  Foreground="Black"
                  Style="{StaticResource SubheaderTextStyle}"
                  Text="{Binding GroupName}" />
              </Grid>
            </DataTemplate>
          </GroupStyle.HeaderTemplate>
          <GroupStyle.ContainerStyle>
            <Style
              TargetType="GroupItem">
              <Setter
                Property="IsTabStop"
                Value="False" />
              <Setter
                Property="Template">
                <Setter.Value>
                  <ControlTemplate
                    TargetType="GroupItem">
                    <Border
                      BorderBrush="{TemplateBinding BorderBrush}"
                      BorderThickness="{TemplateBinding BorderThickness}"
                      Background="{TemplateBinding Background}">
                      <Grid>
                        <Grid.ColumnDefinitions>
                          <ColumnDefinition
                            Width="Auto" />
                          <ColumnDefinition
                            Width="*" />
                        </Grid.ColumnDefinitions>
                        <ContentControl
                          x:Name="HeaderContent"
                          VerticalContentAlignment="Center"
                          ContentTemplate="{TemplateBinding ContentTemplate}"
                          ContentTransitions="{TemplateBinding ContentTransitions}"
                          Content="{TemplateBinding Content}"
                          IsTabStop="False"
                          Margin="{TemplateBinding Padding}"
                          TabIndex="0" />
                        <ItemsControl
                          x:Name="ItemsControl"
                          IsTabStop="False"
                          ItemsSource="{Binding GroupItems}"
                          Grid.Column="1"
                          TabIndex="1"
                          TabNavigation="Once">
                          <ItemsControl.ItemContainerTransitions>
                            <TransitionCollection>
                              <AddDeleteThemeTransition />
                              <ContentThemeTransition />
                              <ReorderThemeTransition />
                              <EntranceThemeTransition
                                IsStaggeringEnabled="False" />
                            </TransitionCollection>
                          </ItemsControl.ItemContainerTransitions>
                        </ItemsControl>
                      </Grid>
                    </Border>
                  </ControlTemplate>
                </Setter.Value>
              </Setter>
            </Style>
            
          </GroupStyle.ContainerStyle>
        </GroupStyle>
      </GridView.GroupStyle>
      <GridView.ItemTemplate>
        <DataTemplate>
          <Grid Background="Blue">
            <TextBlock
              Text="{Binding ItemName}" />
          </Grid>
        </DataTemplate>
      </GridView.ItemTemplate>
    </GridView>

then that gives me;

image

and one thing that style seems to tell me (via its TargetType of GroupItem) is that when the control is displaying grouped data it does not display a GridViewItem for each group but, instead, a GroupItem as the docs state;

ContainerStyle – Gets or sets the style that is applied to the GroupItem generated for each item”

Returning to “The Point”

With all of that said, can the control achieve that layout that I wanted of inlining the header content with the item detail for each item in the group? It’s hard to see how it’s going to do it. In the diagram;

image

the panel in red would presumably need to be some kind of wrap panel oriented vertically in order to wrap groups into the next column when they overflow the current one but then a group brings its own panel with it and that panel would seem to need to span two columns of the parent wrap panel in order to work and it’s hard to see how that would happen.

As far as I can figure out, a way to do this would be to flatten the data and use some kind of template selector to display groups differently from items. For example – keeping the original construction of the data but trying to flatten it out;

      Random rand = new Random();

      // Set up the grouped data exactly as before.
      var groups =
        Enumerable.Range((int)'A', 26).Select(
          letter => 
            {
              var gp = new Group()
              {
                GroupName = string.Format("{0}", (char)letter)
              };

              gp.Items = new List<Item>(
                Enumerable.Range(1, rand.Next(5, 15)).Select(
                  index => new Item()
                  {
                    ItemName = string.Format("{0}{1}", (char)letter, index),
                    Group = gp
                  }
                )
              );              
              return (gp);
            }
        );

      // and flatten it into a list of anonymous types - probably a much smarter way
      // of doing this.
      var g = groups.Select(gp => new { IsGroup = true, Name = gp.GroupName });
      var i = groups.SelectMany(gp => gp.Items).Select(item => new { IsGroup = false, Name = item.ItemName });
      var u = g.Union(i).OrderBy(item => item.Name);

      this.myGridView.ItemsSource = u;

where I’m relying on the names of my data items and groups when I use that OrderBy and then if I have a little template selector;

 public class MyTemplateSelector : DataTemplateSelector
  {
    public DataTemplate Group { get; set; }
    public DataTemplate Item { get; set; }

    protected override Windows.UI.Xaml.DataTemplate SelectTemplateCore(object item, 
      Windows.UI.Xaml.DependencyObject container)
    {
      // unfortunately, our item is an anonymous type because I was lazy and now
      // I'm being more lazy.
      dynamic d = (dynamic)item;

      return (d.IsGroup ? this.Group : this.Item);
    }
  }

I can bring that into my XAML and define a couple of templates (which really need some constants/styles defined for them to tidy them up);

<Page.Resources>
    <DataTemplate
      x:Key="groupTemplate">
      <Grid
        Width="192"
        Margin="0,0,6,0"
        Background="LightBlue">
        <TextBlock
          Margin="3"
          Foreground="White"
          Style="{StaticResource HeaderTextStyle}"
          Text="{Binding Name}"
          TextAlignment="Left"/>
      </Grid>
    </DataTemplate>
    <DataTemplate
      x:Key="itemTemplate">
      <Grid
        Background="LightGreen"
        Width="192">
        <TextBlock
          Foreground="White"
          Style="{StaticResource SubheaderTextStyle}"
          Text="{Binding Name}" 
          TextAlignment="Left"
          Margin="3"/>
      </Grid>
    </DataTemplate>
    <local:MyTemplateSelector
      x:Key="mySelector"
      Item="{StaticResource itemTemplate}"
      Group="{StaticResource groupTemplate}">
    </local:MyTemplateSelector>
  </Page.Resources>
  <Grid
    Background="{StaticResource ApplicationPageBackgroundThemeBrush}">
    <GridView
      x:Name="myGridView"
      ItemTemplateSelector="{Binding Source={StaticResource mySelector}}">

    </GridView>
  </Grid>

giving me a layout of;

image

which is about as close as I’m going to get for this post – I haven’t managed to do this while keeping the grouped nature of the data so if there’s a way to do that, let me know.