Search code examples
c#wpflivecharts

LiveCharts2: Rotate Tooltip on CartesianChart (Box Series)


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>


Solution

  • 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