beto-rodriguez / LiveCharts2

Simple, flexible, interactive & powerful charts, maps and gauges for .Net, LiveCharts2 can now practically run everywhere Maui, Uno Platform, Blazor-wasm, WPF, WinForms, Xamarin, Avalonia, WinUI, UWP.
https://livecharts.dev
MIT License
4.35k stars 569 forks source link

Make Legend Positioning more flexible #1588

Closed MajesticBevans closed 1 month ago

MajesticBevans commented 2 months ago

Saw old issue was closed by OP but not sure why, just reposting as I would also like to be able to move legends to within the bounds of the chart itself, especially for cartesians...

Is your feature request related to a problem? Please describe. Yes, the issue relates to the positioning of the legend in LiveCharts2. I find that the current setup, which prevents the legend from overlapping the main chart area, sometimes limits flexibility in design. This can be particularly challenging in scenarios where space is limited or a specific aesthetic is desired.

Describe the solution you'd like I would like the ability to control whether the legend overlaps the CartesianChart area. This could be implemented via a property or a method that allows developers to specify how the legend should interact with the chart area—whether it should be exclusive or overlapping. Ideally, there would be more options in the LegendPosition enum, such as TopOfChart, BottomOfChart, TopRightOfChart etc. This could also just be a simple boolean flag.

Describe alternatives you've considered An alternative could be manually adjusting the chart and legend margins through custom code, but this approach requires hacking around the intended functionality of the library, which is not ideal or sustainable with updates. Another option could be using external legend controls that are not part of the charting library itself, but this defeats the purpose of having an integrated solution and complicates the layout management.

Additional context Enabling the legend to overlap the chart area could enhance the flexibility of LiveCharts2, especially in designs where space is at a premium or a specific overlapping aesthetic is preferred. Flexibility in positioning legends is a feature seen in many contemporary data visualization tools. Introducing this capability could help ensure that LiveCharts2 remains adaptable and useful for a wide range of design preferences.

ts-research7 commented 1 month ago

I’m not sure if this is the best approach, but I created a custom legend class that is a slight modification of the SKDefaultLegend class.

Please review the following resources: https://livecharts.dev/docs/WPF/2.0.0-rc2/samples.general.customLegends

https://github.com/beto-rodriguez/LiveCharts2/blob/master/src/skiasharp/LiveChartsCore.SkiaSharp/SKCharts/SKDefaultLegend.cs

Below is my code. Ensure that LegendPosition is set to Left. You may also want to adjust LiveCharts.DefaultSettings.MaxTooltipsAndLegendsLabelsWidth as necessary.

public class CustomLegend : IChartLegend<SkiaSharpDrawingContext>
{
    private static readonly int s_zIndex = 10050;
    private IPaint<SkiaSharpDrawingContext>? _backgroundPaint = null;

    // Marked as internal only for testing purposes
    internal readonly StackPanel<RoundedRectangleGeometry, SkiaSharpDrawingContext> _stackPanel = new()
    {
        Padding = new Padding(82, 15, 0, 0),
        HorizontalAlignment = Align.Start,
        VerticalAlignment = Align.Middle
    };

    // Constructor for the CustomLegend class
    public CustomLegend()
    {
        FontPaint = new SolidColorPaint(new SKColor(30, 30, 30, 255));
    }

    // Property for font paint
    public IPaint<SkiaSharpDrawingContext>? FontPaint { get; set; }

    // Property for background paint
    public IPaint<SkiaSharpDrawingContext>? BackgroundPaint
    {
        get => _backgroundPaint;
        set
        {
            _backgroundPaint = value;
            if (value is not null) value.IsFill = true;
        }
    }

    // Property for font size
    public double TextSize { get; set; } = 11;

    // Method to draw the legend
    public void Draw(Chart<SkiaSharpDrawingContext> chart)
    {
        _stackPanel.X = 0;
        _stackPanel.Y = 0;

        chart.AddVisual(_stackPanel);
        if (chart.LegendPosition == LegendPosition.Hidden)
        {
            chart.RemoveVisual(_stackPanel);
        }
    }

    // Method to measure the legend size
    public LvcSize Measure(Chart<SkiaSharpDrawingContext> chart)
    {
        BuildLayout(chart);
        return new LvcSize(0, 0);
    }

    // Helper method to build the legend layout
    private void BuildLayout(Chart<SkiaSharpDrawingContext> chart)
    {
        if (chart.View.LegendTextPaint is not null) FontPaint = chart.View.LegendTextPaint;
        if (chart.View.LegendBackgroundPaint is not null) BackgroundPaint = chart.View.LegendBackgroundPaint;
        if (chart.View.LegendTextSize is not null) TextSize = chart.View.LegendTextSize.Value;

        if (FontPaint is not null) FontPaint.ZIndex = s_zIndex + 1;

        _stackPanel.Orientation = chart.LegendPosition is LegendPosition.Left or LegendPosition.Right
            ? ContainerOrientation.Vertical
            : ContainerOrientation.Horizontal;

        if (_stackPanel.Orientation == ContainerOrientation.Horizontal)
        {
            _stackPanel.MaxWidth = chart.ControlSize.Width;
            _stackPanel.MaxHeight = double.MaxValue;
        }
        else
        {
            _stackPanel.MaxWidth = double.MaxValue;
            _stackPanel.MaxHeight = chart.ControlSize.Height;
        }

        if (BackgroundPaint is not null) BackgroundPaint.ZIndex = s_zIndex;
        _stackPanel.BackgroundPaint = BackgroundPaint;

        // Remove existing visuals
        foreach (var visual in _stackPanel.Children.ToArray())
        {
            _ = _stackPanel.Children.Remove(visual);
            chart.RemoveVisual(visual);
        }

        // Add series to the legend
        foreach (var series in chart.Series.Where(x => x.IsVisibleAtLegend))
        {
            _stackPanel.Children.Add(new StackPanel<RectangleGeometry, SkiaSharpDrawingContext>
            {
                Padding = new Padding(0, 2),
                VerticalAlignment = Align.Middle,
                HorizontalAlignment = Align.Middle,
                Children =
                {
                    series.GetMiniaturesSketch().AsDrawnControl(s_zIndex),
                    new LabelVisual
                    {
                        Text = series.Name ?? string.Empty,
                        Paint = FontPaint,
                        TextSize = TextSize,
                        Padding = new Padding(8, 0, 0, 0),
                        MaxWidth = (float)LiveCharts.DefaultSettings.MaxTooltipsAndLegendsLabelsWidth,
                        VerticalAlignment = Align.Start,
                        HorizontalAlignment = Align.Start,
                        ClippingMode = ClipMode.None
                    }
                }
            });
        }
    }
}
beto-rodriguez commented 1 month ago

@ts-research7 answer should work, just return an empty size (width 0 and height 0) in the Measure method.

beto-rodriguez commented 1 month ago

I will close this for now, default legends are maybe not super flexible, but they fit the needs of most of the users in the library.

But you can user anything as legend as soon as it implements IChartLegend<T>, here is an example where I explain more about this (for WPF and tooltips, but the logic is the same):

https://github.com/beto-rodriguez/LiveCharts2/issues/912#issuecomment-1719959013

I will close this for now, but feel free to reply or open a new issue if required.