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).
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>
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.