LiveCharts2: Rotate Tooltip on CartesianChart (Box Series)

57 views Asked by At

I was looking for a way to display a box and whiskers chart with horizontal series instead of the default vertical options using LiveCharts2. I couldn't find anything and opted to rotate the grid containing the charts as shown below:


    <Grid HorizontalAlignment="Center"
          VerticalAlignment="Center"
          Height="1230">
    
        <Grid.LayoutTransform>
            <RotateTransform Angle="90"/>
        </Grid.LayoutTransform>
    
        <Grid.RowDefinitions>
            <RowDefinition Height="*"/>
            <RowDefinition Height="*"/>
        </Grid.RowDefinitions>
    
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*"/>
            <ColumnDefinition Width="*"/>
        </Grid.ColumnDefinitions>
    
        <Border Grid.Row="0"
                Grid.Column="0"
                Background="{DynamicResource chartTileColour}"
                Margin="10,5,5,5"
                CornerRadius="5"
                VerticalAlignment="Center"
                HorizontalAlignment="Center">
            <lvc:CartesianChart Series="{Binding SerieFrictionAngle}"
                            VerticalAlignment="Center"
                            VerticalContentAlignment="Center"
                            XAxes="{Binding XAxes}"
                            YAxes="{Binding YAxes}"
                            Height="550"
                            Width="300"
                            Margin="20"/>
        </Border>
    
        <Border Grid.Row="1"
                Grid.Column="0"
                Background="{DynamicResource chartTileColour}"
                Margin="10,5,5,5"
                CornerRadius="5"
                VerticalAlignment="Center"
                HorizontalAlignment="Center">
            <lvc:CartesianChart Series="{Binding SerieCohesion}"
                            VerticalAlignment="Center"
                            VerticalContentAlignment="Center"
                            XAxes="{Binding XAxes}"
                            YAxes="{Binding YAxes}"
                            Height="550"
                            Width="300"
                            Margin="20"/>
        </Border>
    
        <Border Grid.Row="0"
                Grid.Column="1"
                Background="{DynamicResource chartTileColour}"
                Margin="10,5,5,5"
                Width="auto"
                CornerRadius="5"                                
                VerticalAlignment="Center"
                HorizontalAlignment="Center">
            <lvc:CartesianChart Series="{Binding SeriePermeability}"
                            VerticalAlignment="Center"
                            VerticalContentAlignment="Center"
                            XAxes="{Binding XAxes}"
                            YAxes="{Binding YAxes}"
                            Height="550"
                            Width="300"
                            Margin="20"/>
        </Border>
    
        <Border Grid.Row="1"
                Grid.Column="1"
                Background="{DynamicResource chartTileColour}"
                Margin="10,5,5,5"
                CornerRadius="5"
                VerticalAlignment="Center"
                HorizontalAlignment="Center">
            <lvc:CartesianChart Series="{Binding SerieDryDensity}"
                            VerticalAlignment="Center"
                            VerticalContentAlignment="Center"
                            XAxes="{Binding XAxes}"
                            YAxes="{Binding YAxes}"
                            Height="550"
                            Width="300"
                            Margin="20"/>
        </Border>
    
    </Grid>

Each chart will contain 5 series (Tailings, Grave, Sand, Silt, Clay). The series are populated as follows:


    public partial class ViewModelStatistics : ObservableObject
    {
        SqlConnection sqlConnection;
    
        public ObservableValue ObservableValue1 { get; set; }
        public ObservableValue ObservableValue2 { get; set; }
    
        int cusPushout = 0;
        int cusMaxRad = 20;
        int cusInnerRad = 10;
    
        public ISeries[] SerieFrictionAngle { get; set; }
        public ISeries[] SerieCohesion { get; set; }
        public ISeries[] SeriePermeability { get; set; }
        public ISeries[] SerieDryDensity { get; set; }
    
    
        public ViewModelStatistics()
        {
            DetermineParameters();
    
            List<double> frictionAngle = DetermineStatistics("Friction Angle");
            List<double> cohesion = DetermineStatistics("Cohesion");
            List<double> permeability = DetermineStatistics("Permeability");
            List<double> dryDensity = DetermineStatistics("Dry Density");
    
            // max, upper quartile, lower quartile, min, median
            SerieFrictionAngle = new List<ISeries>
            {
                new BoxSeries<BoxValue>{Name = "Friction Angle" ,Values = new BoxValue[]{
                    new(frictionAngle[0], frictionAngle[1], frictionAngle[3], frictionAngle[4], frictionAngle[2]),
                    new(41.00, 35.00, 30.00, 22.89, 32.67),
                    new(39.00, 34.00, 30.00, 24.50, 32.14),
                    new(39.00, 35.00, 30.00, 20.00, 32.89),
                    new(40.00, 34.70, 28.00, 15.10, 30.76),
    
                }},
    
            }.ToArray();
    
            SerieCohesion = new List<ISeries>
            {
                new BoxSeries<BoxValue>{Name = "Cohesion" ,Values = new BoxValue[]{
                    new(cohesion[0], cohesion[1], cohesion[3], cohesion[4], cohesion[2]),
                    new(),
                    new(26.90, 3.00, 0.00, 0.00, 0.88),
                    new(21.00, 9.00, 2.00, 0.00, 5.33),
                    new(48.90, 10.30, 0.00, 0.00, 5.30),
    
                }},
            }.ToArray();
    
            SeriePermeability = new List<ISeries>
            {
                new BoxSeries<BoxValue>{Name = "Permeability" ,Values = new BoxValue[]{
                    new(3.50E+06, 3.50E+06, 4.02E+01, 1.68E+04, 1.68E+04),
                    new(1.12E+07, 2.53E+06, 6.37E+04, 1.60E+03, 1.77E+06),
                    new(2.40E+08, 2.25E+06, 1.74E+04, 1.49E+03, 5.03E+05),
                    new(8.89E+06, 3.50E+05, 4.20E+04, 3.90E+04, 7.38E+04),
                    new(1.29E+07, 1.33E+06, 2.68E+04, 6.80E+03, 4.51E+05),
    
                }},
            }.ToArray();
    
            SerieDryDensity = new List<ISeries>
            {
                new BoxSeries<BoxValue>{Name = "Dry Density" ,Values = new BoxValue[]{
                    new(dryDensity[0], dryDensity[1], dryDensity[3], dryDensity[4], dryDensity[2]),
                    new(2125.00, 2006.50, 1772.75, 1578.00, 1897.40),
                    new(2100.00, 1635.00, 1422.50, 1350.00, 1571.87),
                    new(2190.00, 1921.00, 1639.00, 1226.00, 1748.97),
                    new(2190.00, 1744.00, 1370.00, 1216.00, 1531.15),
                    new(1928.00, 1596.00, 1367.00, 1058.00, 1477.32),
    
                }},
            }.ToArray();
    
        }
    
    
        public Axis[] YAxes { get; set; } =
    {
            new Axis()
            {
                Name = "Axis Name",         
                NameTextSize = 10,
                TextSize = 10,
                LabelsRotation = -90,    
                MinLimit = 0,
                IsInverted = false,
                Position = LiveChartsCore.Measure.AxisPosition.End,
            }
        };
    
    
        public Axis[] YAxesPermeability { get; set; } =
        {
    
            new LogaritmicAxis(10)
            {
                Name = "Permeability",
                NameTextSize = 10,
                TextSize = 10,
                LabelsRotation = -90,
                //MinLimit = 0,
                IsInverted = false,
                Position = LiveChartsCore.Measure.AxisPosition.End,
    
                SeparatorsPaint = new SolidColorPaint
                {
                    Color = SKColors.Black.WithAlpha(100),
                    StrokeThickness = 1,
                },
                SubseparatorsPaint = new SolidColorPaint
                {
                    Color = SKColors.Black.WithAlpha(50),
                    StrokeThickness = 0.5f
                },
    
                UnitWidth = 0.00001,
                SubseparatorsCount = 9,
            }
    
    
        };
    
        public Axis[] XAxes { get; set; } =
        {
            new Axis()
            {
                NameTextSize = 10,
                TextSize = 10,
                Labels = new string[] { "Tailings", "Gravel", "Sand", "Silt", "Clay" },
                LabelsRotation = -90,
            }
        };
    
    
        public List<double> DetermineStatistics(string columnName)
        {
            List<double> statisticsList = new List<double>();
    
            // SQL Connection Method 1
            sqlConnection = new SqlConnection(App.SQLConnection);
    
            string query = "select * from SoilParametersTable";
            SqlDataAdapter sqlDataAdapter = new SqlDataAdapter(query, sqlConnection);
    
            using (sqlDataAdapter)
            {
                DataTable soilTable = new DataTable();
                sqlDataAdapter.Fill(soilTable);
    
                // Filter out non-numeric values and extract numeric values 
                // from the specified column
                var columnValues = soilTable.AsEnumerable()
                    .Where(row => double.TryParse(row.Field<string>(columnName), out _))
                    .Select(row => double.Parse(row.Field<string>(columnName)))
                    .ToList();
    
                // Calculate statistics
                double max = columnValues.Max();
                double q3 = columnValues.OrderBy(x => x).ElementAt((int)(columnValues.Count() * 0.75));
                double mean = columnValues.Average();
                double q1 = columnValues.OrderBy(x => x).ElementAt((int)(columnValues.Count() * 0.25));
                double min = columnValues.Min();
    
                // Construct list of strings containing the calculated statistics
                statisticsList.Add(max);
                statisticsList.Add(q3);
                statisticsList.Add(mean);
                statisticsList.Add(q1);
                statisticsList.Add(min);
    
            }
    
    
            return statisticsList;
        }
    
    }

I still need to do some basic formatting. The problem I'm having is that the tooltip is still in the original orientation of the chart (as shown below).

Code Output

I have read through the documentation but did not see any tool tip rotation properties. I tried to place the tooltip in a container and rotate that (as I did with the graphs) but that also won't compile.


    <Border Grid.Row="1"
            Grid.Column="1"
            Background="{DynamicResource chartTileColour}"
            Margin="10,5,5,5"
            CornerRadius="5"
            VerticalAlignment="Center"
            HorizontalAlignment="Center">
        <lvc:CartesianChart Series="{Binding SerieDryDensity}"
                        VerticalAlignment="Center"
                        VerticalContentAlignment="Center"
                        XAxes="{Binding XAxes}"
                        YAxes="{Binding YAxes}"
                        Height="550"
                        Width="300"
                        Margin="20">
            <lvc:CartesianChart.Tooltip>
                <DataTemplate>
                    <Border Background="White" BorderBrush="Black" BorderThickness="1" CornerRadius="5">
                        <TextBlock Text="{Binding FormattedValues[0]}" Margin="5" TextWrapping="Wrap" RenderTransformOrigin="0.5,0.5">
                            <TextBlock.RenderTransform>
                                <RotateTransform Angle="-45"/>
                            </TextBlock.RenderTransform>
                        </TextBlock>
                    </Border>
                </DataTemplate>
            </lvc:CartesianChart.Tooltip>
        </lvc:CartesianChart>
    </Border>

2

There are 2 answers

0
Stephan On BEST ANSWER

I wasn't able to rotate the default Tooltip so I created a custom tooltip and rotated the label instead.

Xaml:

<lvc:CartesianChart Series="{Binding SerieFrictionAngle}"
    VerticalAlignment="Stretch"
    VerticalContentAlignment="Stretch"
    XAxes="{Binding XAxesFrictionAngle}"
    YAxes="{Binding YAxesFrictionAngle}"
    MinHeight="400"
    MinWidth="320"
    Margin="20">
    <lvc:CartesianChart.Tooltip>
        <local:CustomTooltip></local:CustomTooltip>
    </lvc:CartesianChart.Tooltip>
</lvc:CartesianChart>

Code behind:

public class CustomTooltip : IChartTooltip<SkiaSharpDrawingContext>
   {
       private StackPanel<RoundedRectangleGeometry, SkiaSharpDrawingContext>? _stackPanel;
       private static readonly int s_zIndex = 10100;
       private readonly SolidColorPaint _backgroundPaint = new(new SKColor(240, 240, 240).WithAlpha(220)) { ZIndex = s_zIndex };
       private readonly SolidColorPaint _fontPaint = new(new SKColor(0, 0, 0)) { ZIndex = s_zIndex + 1 };

       public void Show(IEnumerable<ChartPoint> foundPoints, Chart<SkiaSharpDrawingContext> chart)
       {
           if (_stackPanel is null)
           {
               _stackPanel = new StackPanel<RoundedRectangleGeometry, SkiaSharpDrawingContext>
               {
                   Padding = new Padding(25),
                   Orientation = ContainerOrientation.Vertical,
                   HorizontalAlignment = Align.Middle,
                   VerticalAlignment = Align.Middle,
                   BackgroundPaint = _backgroundPaint,
                   Rotation = 0,
               };
           }

           // clear the previous elements.
           foreach (var child in _stackPanel.Children.ToArray())
           {
               _ = _stackPanel.Children.Remove(child);
               chart.RemoveVisual(child);
           }

           foreach (var point in foundPoints)
           {
               var sketch = ((IChartSeries<SkiaSharpDrawingContext>)point.Context.Series).GetMiniaturesSketch();

               var relativePanel = sketch.AsDrawnControl(s_zIndex);

               // Additional labels can be created if individual 
               // properties of the series should be stacked.
               // Rotate this label for the desired effect.
               var label = new LabelVisual
               {
                   Text = $"{point.AsDataLabel}",
                   Paint = _fontPaint,
                   Rotation = -90,
                   TextSize = 15,
                   Padding = new Padding(20,20,10,-30),
                   ClippingMode = ClipMode.None, // required on tooltips 
                   VerticalAlignment = Align.End,
                   HorizontalAlignment = Align.End
               };
           
               // Stack Panel Properties
               var sp = new StackPanel<RoundedRectangleGeometry, SkiaSharpDrawingContext>
               {
                   Padding = new Padding(0,20,0,20),
                   VerticalAlignment = Align.End,
                   HorizontalAlignment = Align.End,
                   Rotation = 0,
                   // Add additional elements to the stack panel if required
                   Children =
                   {               
                       relativePanel,
                       label,
                   } 
               };

               _stackPanel?.Children.Add(sp);
           }

           var size = _stackPanel.Measure(chart);

           var location = foundPoints.GetTooltipLocation(size, chart);
           // +60 required to drop the tooltip below the series 
           // to prevent blocking the series on mouse hover
           _stackPanel.X = location.X+60;
           _stackPanel.Y = location.Y;
           chart.AddVisual(_stackPanel);
       }

       public void Hide(Chart<SkiaSharpDrawingContext> chart)
       {
           if (chart is null || _stackPanel is null) return;
           chart.RemoveVisual(_stackPanel);
       }
   }

The code above outputs a Tooltip as shown below. The mouse is hovered over the Gravel series on the Friction Angle chart.

image description

6
egeer On

EDIT - This will not work for LiveCharts2

This approach will work for LiveCharts tooltips, but not LiveCharts2. The reason for that is LiveCharts2 renders the tooltips using SkiaSharp and not standard WPF FrameWorkElement objects. Thus there is no way to add a Style to that UI component.


You should be able to accomplish rotating the tooltip by using the control's Resources. Below I am doing this with the Border control, which should inherit in the chart. You should be able to this in the chart too, if that does not work. The nice thing about using the control's Resources is that you don't need to bother with rebuilding the template.

<Border.Resources>
    <Style TargetType="ToolTip">
        <Setter Property="LayoutTransform">
            <Setter.Value>
                <RotateTransform Angle="-45"/>
            </Setter.Value>
        </Setter>
    </Style>
</Border.Resources>

enter image description here

Below is the whole control for context. As you can see, the TextBlock'a ToolTip is inheriting from the Grid's resources.

<Border>
    <Border.Style>
        <Style TargetType="Border">
            <Setter Property="LayoutTransform">
                <Setter.Value>
                    <RotateTransform Angle="-90" />
                </Setter.Value>
            </Setter>
        </Style>
    </Border.Style>
    <Border.Resources>
        <Style TargetType="ToolTip">
            <Setter Property="LayoutTransform">
                <Setter.Value>
                    <RotateTransform Angle="-45" />
                </Setter.Value>
            </Setter>
        </Style>
    </Border.Resources>
    <TextBlock Text="ROTATED"
                FontSize="18"
                FontWeight="Bold"
                HorizontalAlignment="Center"
                VerticalAlignment="Center"
                ToolTip="Also Rotated?" />
</Border>