Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make Legend Positioning more flexible #1588

Closed
MajesticBevans opened this issue Sep 11, 2024 · 3 comments
Closed

Make Legend Positioning more flexible #1588

MajesticBevans opened this issue Sep 11, 2024 · 3 comments

Comments

@MajesticBevans
Copy link

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
Copy link

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
Copy link
Owner

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

@beto-rodriguez
Copy link
Owner

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

#912 (comment)

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants