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.23k stars 546 forks source link

Visual issues in PieChart and BarChart #1090

Closed vadimffe closed 11 months ago

vadimffe commented 1 year ago

Describe the bug After updating to version 800 and later 802 from 701, CustomLegend is not displayed properly on NET MAUI, NET 7. I have to resize window to full screen and back, only after this I can see Legend items. Seems like some issue with Measurement? Also setting

VerticalAlignment = Align.Center, HorizontalAlignment = Align.Start,

does not align legend items to the left. See screenshot below. Also can not get text and icon align in the middle as it seems that Horizontally text is always aligned at the bottom and icon is in the centre. See screenshot.

image

I am using default template provided here: https://lvcharts.com/docs/Maui/2.0.0-beta.800/CartesianChart.Legends

Desktop (please complete the following information):

vadimffe commented 1 year ago

Pie Chart:

In addition to above when you drag your mouse out of Pie Chart it does not return to it's default state, but stays showing previous element.

https://github.com/beto-rodriguez/LiveCharts2/assets/72302395/5af6eabc-f157-496b-bfec-c0132baf186a

Another problem is already mentioned above.

When I first start my application:

image

Resize to full screen:

image

Resize back to start-up size:

image

As you can see only now, after multiple window resizing, circles are visible. Also they are not aligned to the middle and centre.

Cartesian Chart:

In previous versions I have added custom properties for it to look nicer. I have added corner radius property of each bar and minimum width. As currently bars are too thin especially if application window is resized to smaller size.

image

These has been added (can be done probably in a better way).

For MinBarWidth:

[LiveChartsCore/BarSeries.cs]

{
    private double _pading = 5;
    private double _maxBarWidth = 50;
    private double _minBarWidth = 50;
    private bool _ignoresBarPosition = false;
    private double _rx;
    private double _ry;

@@ -69,6 +70,9 @@ protected BarSeries(SeriesProperties properties)
    /// <inheritdoc cref="IBarSeries{TDrawingContext}.MaxBarWidth"/>
    public double MaxBarWidth { get => _maxBarWidth; set => SetProperty(ref _maxBarWidth, value); }

    /// <inheritdoc cref="IBarSeries{TDrawingContext}.MinBarWidth"/>
    public double MinBarWidth { get => _minBarWidth; set => SetProperty(ref _minBarWidth, value); }

    /// <inheritdoc cref="IBarSeries{TDrawingContext}.IgnoresBarPosition"/>
    public bool IgnoresBarPosition { get => _ignoresBarPosition; set => SetProperty(ref _ignoresBarPosition, value); }

@@ -154,10 +158,12 @@ protected class MeasureHelper
            uw /= count;
            var mw = (float)barSeries.MaxBarWidth;
            if (uw > mw) uw = mw;

            if (uw < (float)barSeries.MinBarWidth) uw = (float)barSeries.MinBarWidth;
            uwm = 0.5f * uw;
            cp = barSeries.IgnoresBarPosition ? 0 : (pos - count / 2f) * uw + uwm;

            // apply the pading
            // apply the padding
            uw -= padding;
            cp += padding * 0.5f;

[Sketches/IBarSeries.cs]

    /// </value>
    double MaxBarWidth { get; set; }

    /// <summary>
    /// Gets or sets the minimum width of the bar.
    /// </summary>
    /// <value>
    /// The minimum width of the bar.
    /// </value>
    double MinBarWidth { get; set; }

    /// <summary>
    /// Gets or sets a value indicating whether the bar position respects the other bars that share 
    /// the same <see cref="ChartPoint.SecondaryValue"/>.

For rounded corners. [LiveChartsCore/ColumnSeries.cs]

{
    private readonly bool _isRounded = false;

    public double Rx { get; set; } = 1;
    public double Ry { get; set; } = 1;

    /// <summary>
    /// Initializes a new instance of the <see cref="ColumnSeries{TModel, TVisual, TLabel, TDrawingContext}"/> class.
    /// </summary>

@@ -155,7 +158,7 @@ public override void Invalidate(Chart<TDrawingContext> chart)
                    Height = hi
                };

                if (_isRounded)
                if (_isRounded && Rx > 0 && Ry > 0)
                {
                    var rounded = (IRoundedRectangleChartPoint<TDrawingContext>)r;
                    rounded.Rx = rx;

@@ -202,7 +205,7 @@ public override void Invalidate(Chart<TDrawingContext> chart)
            visual.Width = helper.uw;
            visual.Height = b;

            if (_isRounded)
            if (_isRounded && Rx > 0 && Ry > 0)
            {
                var rounded = (IRoundedRectangleChartPoint<TDrawingContext>)visual;
                rounded.Rx = rx;
vadimffe commented 1 year ago

With recent update 2.0.0-beta.900, the only issue that is left is bars width described in the last post, starting from Cartesian Chart section. Also issue with moving mouse elsewhere and section staying selected is still there.

vadimffe commented 1 year ago

Here are some further observations. When I expand my application to take the whole screen size (chart area is bigger), it seems like this issue is not there anymore:

https://user-images.githubusercontent.com/72302395/254680407-5af6eabc-f157-496b-bfec-c0132baf186a.mp4

So my Assumption is that on smaller sizes area is too small to recognize the mouse has been moved away. Issue with measurement?

Also I have now merged with recent version (2.0.0-beta.911) in my branch and you can check how bar chart bars thickness has been handled (don't know if you have better solution, so I will not create a Pull request). For me default was too thin and I want it to be thicker. Now bar thickness can be controlled from its settings in ViewModel:

      IEnumerable<ISeries> data = new List<ISeries>
      {
        new ColumnSeries<DateTimePoint>
        {
          Values = myData.Select(x => new DateTimePoint(x.Start, x.Duration)),
          Fill = new LiveChartsCore.SkiaSharpView.Painting.SolidColorPaint(SKColor.Parse("#ffffff")),
          Stroke = new LiveChartsCore.SkiaSharpView.Painting.SolidColorPaint(SKColor.Parse("#ffffff")),
          MinBarWidth = 15,
        }
      };

LiveCharts2/src/LiveChartsCore/BarSeries.cs

// The MIT License(MIT)
//
// Copyright(c) 2021 Alberto Rodriguez Orozco & LiveCharts Contributors
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.

using System;
using System.Collections.Generic;
using LiveChartsCore.Drawing;
using LiveChartsCore.Kernel;
using LiveChartsCore.Kernel.Drawing;
using LiveChartsCore.Kernel.Sketches;
using LiveChartsCore.Measure;

namespace LiveChartsCore;

/// <summary>
/// Defines a bar series point.
/// </summary>
/// <typeparam name="TModel">The type of the model.</typeparam>
/// <typeparam name="TVisual">The type of the visual.</typeparam>
/// <typeparam name="TLabel">The type of the label.</typeparam>
/// <typeparam name="TDrawingContext">The type of the drawing context.</typeparam>
/// <seealso cref="CartesianSeries{TModel, TVisual, TLabel, TDrawingContext}" />
/// <seealso cref="IBarSeries{TDrawingContext}" />
public abstract class BarSeries<TModel, TVisual, TLabel, TDrawingContext>
    : StrokeAndFillCartesianSeries<TModel, TVisual, TLabel, TDrawingContext>, IBarSeries<TDrawingContext>
        where TVisual : class, ISizedGeometry<TDrawingContext>, new()
        where TDrawingContext : DrawingContext
        where TLabel : class, ILabelGeometry<TDrawingContext>, new()
{
    private double _pading = 5;
    private double _maxBarWidth = 50;
    private double _minBarWidth = 50;
    private bool _ignoresBarPosition = false;
    private double _rx;
    private double _ry;
    private IPaint<TDrawingContext>? _errorPaint;

    /// <summary>
    /// Initializes a new instance of the <see cref="BarSeries{TModel, TVisual, TLabel, TDrawingContext}"/> class.
    /// </summary>
    /// <param name="properties">The properties.</param>
    protected BarSeries(SeriesProperties properties)
        : base(properties)
    { }

    /// <inheritdoc cref="IBarSeries{TDrawingContext}.GroupPadding"/>
    [Obsolete($"Replace by {nameof(Padding)} property.")]
    public double GroupPadding { get => _pading; set => SetProperty(ref _pading, value); }

    /// <inheritdoc cref="IBarSeries{TDrawingContext}.Padding"/>
    public double Padding { get => _pading; set => SetProperty(ref _pading, value); }

    /// <inheritdoc cref="IBarSeries{TDrawingContext}.MaxBarWidth"/>
    public double MaxBarWidth { get => _maxBarWidth; set => SetProperty(ref _maxBarWidth, value); }

    /// <inheritdoc cref="IBarSeries{TDrawingContext}.MinBarWidth"/>
    public double MinBarWidth { get => _minBarWidth; set => SetProperty(ref _minBarWidth, value); }

    /// <inheritdoc cref="IBarSeries{TDrawingContext}.IgnoresBarPosition"/>
    public bool IgnoresBarPosition { get => _ignoresBarPosition; set => SetProperty(ref _ignoresBarPosition, value); }

    /// <inheritdoc cref="IBarSeries{TDrawingContext}.Rx"/>
    public double Rx { get => _rx; set => SetProperty(ref _rx, value); }

    /// <inheritdoc cref="IBarSeries{TDrawingContext}.Ry"/>
    public double Ry { get => _ry; set => SetProperty(ref _ry, value); }

    /// <inheritdoc cref="IErrorSeries{TDrawingContext}.ErrorPaint"/>
    public IPaint<TDrawingContext>? ErrorPaint
    {
        get => _errorPaint;
        set => SetPaintProperty(ref _errorPaint, value, true);
    }

    /// <inheritdoc cref="Series{TModel, TVisual, TLabel, TDrawingContext}.GetMiniaturesSketch"/>
    public override Sketch<TDrawingContext> GetMiniaturesSketch()
    {
        var schedules = new List<PaintSchedule<TDrawingContext>>();

        if (Fill is not null) schedules.Add(BuildMiniatureSchedule(Fill, new TVisual()));
        if (Stroke is not null) schedules.Add(BuildMiniatureSchedule(Stroke, new TVisual()));

        return new Sketch<TDrawingContext>(MiniatureShapeSize, MiniatureShapeSize, GeometrySvg)
        {
            PaintSchedules = schedules
        };
    }

    /// <summary>
    /// A mesure helper class.
    /// </summary>
    protected class MeasureHelper
    {
        /// <summary>
        /// Initializes a new instance of the measue helper class.
        /// </summary>
        /// <param name="scaler">The scaler.</param>
        /// <param name="cartesianChart">The chart.</param>
        /// <param name="barSeries">The series.</param>
        /// <param name="axis">The axis.</param>
        /// <param name="p">The pivot.</param>
        /// <param name="minP">The min pivot allowed.</param>
        /// <param name="maxP">The max pivot allowed.</param>
        /// <param name="isStacked">Indicates whether the series is stacked or not.</param>
        /// <param name="isRow">Indicates whether the serie is row or not.</param>
        public MeasureHelper(
            Scaler scaler,
            CartesianChart<TDrawingContext> cartesianChart,
            IBarSeries<TDrawingContext> barSeries,
            ICartesianAxis axis,
            float p,
            float minP,
            float maxP,
            bool isStacked,
            bool isRow)
        {
            this.p = p;
            if (p < minP) this.p = minP;
            if (p > maxP) this.p = maxP;

            uw = scaler.MeasureInPixels(axis.UnitWidth);
            actualUw = uw;

            var gp = (float)barSeries.Padding;

            if (uw - gp < 1) gp -= uw - gp;

            uw -= gp;
            uwm = 0.5f * uw;

            int pos, count;

            if (isStacked)
            {
                pos = isRow
                    ? cartesianChart.SeriesContext.GetStackedRowPostion(barSeries)
                    : cartesianChart.SeriesContext.GetStackedColumnPostion(barSeries);
                count = isRow
                    ? cartesianChart.SeriesContext.GetStackedRowSeriesCount()
                    : cartesianChart.SeriesContext.GetStackedColumnSeriesCount();
            }
            else
            {
                pos = isRow
                    ? cartesianChart.SeriesContext.GetRowPostion(barSeries)
                    : cartesianChart.SeriesContext.GetColumnPostion(barSeries);
                count = isRow
                    ? cartesianChart.SeriesContext.GetRowSeriesCount()
                    : cartesianChart.SeriesContext.GetColumnSeriesCount();
            }

            cp = 0f;

            var padding = (float)barSeries.Padding;
            if (barSeries.IgnoresBarPosition) count = 1;

            uw /= count;
            var mw = (float)barSeries.MaxBarWidth;
            if (uw > mw) uw = mw;

            if (uw < (float)barSeries.MinBarWidth) uw = (float)barSeries.MinBarWidth;
            uwm = 0.5f * uw;
            cp = barSeries.IgnoresBarPosition
                ? 0
                : (pos - count / 2f) * uw + uwm;

            // apply the padding
            uw -= padding;
            cp += padding * 0.5f;

            if (uw < 1)
            {
                uw = 1;
                uwm = 0.5f;
            }
        }

        /// <summary>
        /// helper units.
        /// </summary>
        public float uw, uwm, cp, p, actualUw;
    }

    /// <inheritdoc cref="Series{TModel, TVisual, TLabel, TDrawingContext}.OnPointerEnter(ChartPoint)"/>
    protected override void OnPointerEnter(ChartPoint point)
    {
        var visual = (TVisual?)point.Context.Visual;
        if (visual is null) return;
        visual.Opacity = 0.8f;

        base.OnPointerEnter(point);
    }

    /// <inheritdoc cref="Series{TModel, TVisual, TLabel, TDrawingContext}.OnPointerLeft(ChartPoint)"/>
    protected override void OnPointerLeft(ChartPoint point)
    {
        var visual = (TVisual?)point.Context.Visual;
        if (visual is null) return;
        visual.Opacity = 1;

        base.OnPointerLeft(point);
    }

    internal override IPaint<TDrawingContext>?[] GetPaintTasks()
    {
        return new[] { Stroke, Fill, DataLabelsPaint, _errorPaint };
    }
}

LiveCharts2/src/LiveChartsCore/Kernel/Sketches/IBarSeries.cs

// The MIT License(MIT)
//
// Copyright(c) 2021 Alberto Rodriguez Orozco & LiveCharts Contributors
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.

using System;
using LiveChartsCore.Drawing;

namespace LiveChartsCore.Kernel.Sketches;

/// <summary>
/// Defines a bar series point.
/// </summary>
/// <typeparam name="TDrawingContext">The type of the drawing context.</typeparam>
/// <seealso cref="IChartSeries{TDrawingContext}" />
public interface IBarSeries<TDrawingContext> : IChartSeries<TDrawingContext>, IStrokedAndFilled<TDrawingContext>, ICartesianSeries<TDrawingContext>
    where TDrawingContext : DrawingContext
{
    /// <summary>
    /// Gets or sets the rx, the radius used in the x axis to round the corners of each column in pixels.
    /// </summary>
    /// <value>
    /// The rx.
    /// </value>
    double Rx { get; set; }

    /// <summary>
    /// Gets or sets the ry, the radius used in the y axis to round the corners of each column in pixels.
    /// </summary>
    /// <value>
    /// The ry.
    /// </value>
    double Ry { get; set; }

    /// <summary>
    /// Gets or sets the padding for each group of bars that share the same secondary coordinate.
    /// </summary>
    /// <value>
    /// The bar group padding.
    /// </value>
    [Obsolete($"Replace by {nameof(Padding)} property.")]
    double GroupPadding { get; set; }

    /// <summary>
    /// Gets or sets the padding for each bar in the series.
    /// </summary>
    /// <value>
    /// The bar group padding.
    /// </value>
    double Padding { get; set; }

    /// <summary>
    /// Gets or sets the maximum width of the bar.
    /// </summary>
    /// <value>
    /// The maximum width of the bar.
    /// </value>
    double MaxBarWidth { get; set; }

    /// <summary>
    /// Gets or sets the minimum width of the bar.
    /// </summary>
    /// <value>
    /// The minimum width of the bar.
    /// </value>
    double MinBarWidth { get; set; }

    /// <summary>
    /// Gets or sets a value indicating whether the bar position respects the other bars that share 
    /// the same <see cref="ChartPoint.SecondaryValue"/>.
    /// </summary>
    bool IgnoresBarPosition { get; set; }
}

For corner rounding radius:

LiveCharts2/src/LiveChartsCore/ColumnSeries.cs

using System;
using LiveChartsCore.Drawing;
using LiveChartsCore.Kernel;
using LiveChartsCore.Kernel.Drawing;
using LiveChartsCore.Kernel.Sketches;
using LiveChartsCore.Measure;

namespace LiveChartsCore;

/// <summary>
/// Defines a column series.
/// </summary>
/// <typeparam name="TModel">The type of the model.</typeparam>
/// <typeparam name="TVisual">The type of the visual.</typeparam>
/// <typeparam name="TLabel">the type of the label.</typeparam>
/// <typeparam name="TDrawingContext">The type of the drawing context.</typeparam>
public abstract class ColumnSeries<TModel, TVisual, TLabel, TDrawingContext> : BarSeries<TModel, TVisual, TLabel, TDrawingContext>
    where TVisual : class, ISizedGeometry<TDrawingContext>, new()
    where TDrawingContext : DrawingContext
    where TLabel : class, ILabelGeometry<TDrawingContext>, new()
{
    private readonly bool _isRounded = false;

    public double Rx { get; set; } = 1;
    public double Ry { get; set; } = 1;

    /// <summary>
    /// Initializes a new instance of the <see cref="ColumnSeries{TModel, TVisual, TLabel, TDrawingContext}"/> class.
    /// </summary>
    protected ColumnSeries(bool isStacked = false)
        : base(
              SeriesProperties.Bar | SeriesProperties.PrimaryAxisVerticalOrientation |
              SeriesProperties.Solid | SeriesProperties.PrefersXStrategyTooltips | (isStacked ? SeriesProperties.Stacked : 0))
    {
        DataPadding = new LvcPoint(0, 1);
        _isRounded = typeof(IRoundedGeometry<TDrawingContext>).IsAssignableFrom(typeof(TVisual));
    }

    /// <inheritdoc cref="ChartElement{TDrawingContext}.Invalidate(Chart{TDrawingContext})"/>
    public override void Invalidate(Chart<TDrawingContext> chart)
    {
        var cartesianChart = (CartesianChart<TDrawingContext>)chart;
        var primaryAxis = cartesianChart.YAxes[ScalesYAt];
        var secondaryAxis = cartesianChart.XAxes[ScalesXAt];

        var drawLocation = cartesianChart.DrawMarginLocation;
        var drawMarginSize = cartesianChart.DrawMarginSize;
        var secondaryScale = secondaryAxis.GetNextScaler(cartesianChart);
        var primaryScale = primaryAxis.GetNextScaler(cartesianChart);
        var previousPrimaryScale = primaryAxis.GetActualScaler(cartesianChart);
        var previousSecondaryScale = secondaryAxis.GetActualScaler(cartesianChart);

        var isStacked = (SeriesProperties & SeriesProperties.Stacked) == SeriesProperties.Stacked;

        var helper = new MeasureHelper(secondaryScale, cartesianChart, this, secondaryAxis, primaryScale.ToPixels(pivot),
            cartesianChart.DrawMarginLocation.Y, cartesianChart.DrawMarginLocation.Y + cartesianChart.DrawMarginSize.Height, isStacked);
        var pHelper = previousSecondaryScale == null || previousPrimaryScale == null
            ? null
            : new MeasureHelper(
                previousSecondaryScale, cartesianChart, this, secondaryAxis, previousPrimaryScale.ToPixels(pivot),
                cartesianChart.DrawMarginLocation.Y, cartesianChart.DrawMarginLocation.Y + cartesianChart.DrawMarginSize.Height, isStacked);

        var actualZIndex = ZIndex == 0 ? ((ISeries)this).SeriesId : ZIndex;
        if (Fill is not null)
        {
            Fill.ZIndex = actualZIndex + 0.1;
            Fill.SetClipRectangle(cartesianChart.Canvas, new LvcRectangle(drawLocation, drawMarginSize));
            cartesianChart.Canvas.AddDrawableTask(Fill);
        }
        if (Stroke is not null)
        {
            Stroke.ZIndex = actualZIndex + 0.2;
            Stroke.SetClipRectangle(cartesianChart.Canvas, new LvcRectangle(drawLocation, drawMarginSize));
            cartesianChart.Canvas.AddDrawableTask(Stroke);
        }
        if (DataLabelsPaint is not null)
        {
            DataLabelsPaint.ZIndex = actualZIndex + 0.3;
            DataLabelsPaint.SetClipRectangle(cartesianChart.Canvas, new LvcRectangle(drawLocation, drawMarginSize));
            cartesianChart.Canvas.AddDrawableTask(DataLabelsPaint);
        }

        var dls = (float)DataLabelsSize;
        var pointsCleanup = ChartPointCleanupContext.For(everFetched);

        var rx = (float)Rx;
        var ry = (float)Ry;

        var stacker = isStacked ? cartesianChart.SeriesContext.GetStackPosition(this, GetStackGroup()) : null;
        var hasSvg = this.HasSvgGeometry();

        foreach (var point in Fetch(cartesianChart))
        {
            var visual = point.Context.Visual as TVisual;
            var coordinate = point.Coordinate;
            var primary = primaryScale.ToPixels(coordinate.PrimaryValue);
            var secondary = secondaryScale.ToPixels(coordinate.SecondaryValue);
            var b = Math.Abs(primary - helper.p);

            if (point.IsEmpty)
            {
                if (visual is not null)
                {
                    visual.X = secondary - helper.uwm + helper.cp;
                    visual.Y = helper.p;
                    visual.Width = helper.uw;
                    visual.Height = 0;
                    visual.RemoveOnCompleted = true;
                    point.Context.Visual = null;
                }
                continue;
            }

            if (visual is null)
            {
                var xi = secondary - helper.uwm + helper.cp;
                var pi = helper.p;
                var uwi = helper.uw;
                var hi = 0f;

                if (previousSecondaryScale is not null && previousPrimaryScale is not null && pHelper is not null)
                {
                    var previousPrimary = previousPrimaryScale.ToPixels(coordinate.PrimaryValue);
                    var bp = Math.Abs(previousPrimary - pHelper.p);
                    var cyp = coordinate.PrimaryValue > pivot ? previousPrimary : previousPrimary - bp;

                    xi = previousSecondaryScale.ToPixels(coordinate.SecondaryValue) - pHelper.uwm + pHelper.cp;
                    pi = cartesianChart.IsZoomingOrPanning ? cyp : pHelper.p;
                    uwi = pHelper.uw;
                    hi = cartesianChart.IsZoomingOrPanning ? bp : 0;
                }

                var r = new TVisual
                {
                    X = xi,
                    Y = pi,
                    Width = uwi,
                    Height = hi
                };

                if (_isRounded && Rx > 0 && Ry > 0)
                {
                    var rounded = (IRoundedGeometry<TDrawingContext>)r;
                    rounded.BorderRadius = new LvcPoint(rx, ry);
                }

                visual = r;
                point.Context.Visual = visual;
                OnPointCreated(point);

                _ = everFetched.Add(point);
            }

            if (hasSvg)
            {
                var svgVisual = (ISvgPath<TDrawingContext>)visual;
                if (_geometrySvgChanged || svgVisual.SVGPath is null)
                    svgVisual.SVGPath = GeometrySvg ?? throw new Exception("svg path is not defined");
            }

            Fill?.AddGeometryToPaintTask(cartesianChart.Canvas, visual);
            Stroke?.AddGeometryToPaintTask(cartesianChart.Canvas, visual);

            var cy = primaryAxis.IsInverted
                ? (coordinate.PrimaryValue > pivot ? primary - b : primary)
                : (coordinate.PrimaryValue > pivot ? primary : primary - b);
            var x = secondary - helper.uwm + helper.cp;

            if (stacker is not null)
            {
                var sy = stacker.GetStack(point);

                float primaryI, primaryJ;
                if (coordinate.PrimaryValue >= 0)
                {
                    primaryI = primaryScale.ToPixels(sy.Start);
                    primaryJ = primaryScale.ToPixels(sy.End);
                }
                else
                {
                    primaryI = primaryScale.ToPixels(sy.NegativeStart);
                    primaryJ = primaryScale.ToPixels(sy.NegativeEnd);
                }

                cy = primaryJ;
                b = primaryI - primaryJ;
            }

            visual.X = x;
            visual.Y = cy;
            visual.Width = helper.uw;
            visual.Height = b;

            if (_isRounded && Rx > 0 && Ry > 0)
            {
                var rounded = (IRoundedGeometry<TDrawingContext>)visual;
                rounded.BorderRadius = new LvcPoint(rx, ry);
            }
            visual.RemoveOnCompleted = false;

            if (point.Context.HoverArea is not RectangleHoverArea ha)
                point.Context.HoverArea = ha = new RectangleHoverArea();

            _ = ha
                .SetDimensions(secondary - helper.actualUw * 0.5f, cy, helper.actualUw, b)
                .CenterXToolTip();

            _ = coordinate.PrimaryValue >= pivot ? ha.StartYToolTip() : ha.EndYToolTip().IsLessThanPivot();

            pointsCleanup.Clean(point);

            if (DataLabelsPaint is not null)
            {
                var label = (TLabel?)point.Context.Label;

                if (label is null)
                {
                    var l = new TLabel { X = secondary - helper.uwm + helper.cp, Y = helper.p, RotateTransform = (float)DataLabelsRotation };
                    l.Animate(EasingFunction ?? cartesianChart.EasingFunction, AnimationsSpeed ?? cartesianChart.AnimationsSpeed);
                    label = l;
                    point.Context.Label = l;
                }

                DataLabelsPaint.AddGeometryToPaintTask(cartesianChart.Canvas, label);

                label.Text = DataLabelsFormatter(new ChartPoint<TModel, TVisual, TLabel>(point));
                label.TextSize = dls;
                label.Padding = DataLabelsPadding;
                var m = label.Measure(DataLabelsPaint);
                var labelPosition = GetLabelPosition(
                    x, cy, helper.uw, b, m,
                    DataLabelsPosition, SeriesProperties, coordinate.PrimaryValue > Pivot, drawLocation, drawMarginSize);
                if (DataLabelsTranslate is not null) label.TranslateTransform =
                        new LvcPoint(m.Width * DataLabelsTranslate.Value.X, m.Height * DataLabelsTranslate.Value.Y);

                label.X = labelPosition.X;
                label.Y = labelPosition.Y;
            }
            else
            {
                // ....
            }

            OnPointMeasured(point);
        }

        pointsCleanup.CollectPoints(
            everFetched, cartesianChart.View, primaryScale, secondaryScale, SoftDeleteOrDisposePoint);
        _geometrySvgChanged = false;
    }

    /// <inheritdoc cref="CartesianSeries{TModel, TVisual, TLabel, TDrawingContext}.GetRequestedSecondaryOffset"/>
    protected override double GetRequestedSecondaryOffset()
    {
        return 0.5f;
    }

    /// <inheritdoc cref="Series{TModel, TVisual, TLabel, TDrawingContext}.SetDefaultPointTransitions(ChartPoint)"/>
    protected override void SetDefaultPointTransitions(ChartPoint chartPoint)
    {
        var chart = chartPoint.Context.Chart;
        if (chartPoint.Context.Visual is not TVisual visual) throw new Exception("Unable to initialize the point instance.");
        visual.Animate(EasingFunction ?? chart.EasingFunction, AnimationsSpeed ?? chart.AnimationsSpeed);
    }

    /// <inheritdoc cref="CartesianSeries{TModel, TVisual, TLabel, TDrawingContext}.SoftDeleteOrDisposePoint(ChartPoint, Scaler, Scaler)"/>
    protected internal override void SoftDeleteOrDisposePoint(ChartPoint point, Scaler primaryScale, Scaler secondaryScale)
    {
        var visual = (TVisual?)point.Context.Visual;
        if (visual is null) return;
        if (DataFactory is null) throw new Exception("Data provider not found");

        var chartView = (ICartesianChartView<TDrawingContext>)point.Context.Chart;
        if (chartView.Core.IsZoomingOrPanning)
        {
            visual.CompleteTransition(null);
            visual.RemoveOnCompleted = true;
            DataFactory.DisposePoint(point);
            return;
        }

        var p = primaryScale.ToPixels(pivot);
        var secondary = secondaryScale.ToPixels(point.Coordinate.SecondaryValue);

        visual.X = secondary - visual.Width * 0.5f;
        visual.Y = p;
        visual.Height = 0;
        visual.RemoveOnCompleted = true;

        DataFactory.DisposePoint(point);

        var label = (TLabel?)point.Context.Label;
        if (label is null) return;

        label.TextSize = 1;
        label.RemoveOnCompleted = true;
    }
}
vadimffe commented 1 year ago

After updating to 2.0.0-beta.920 the only issue/improvement that I am missing is setting MinBarWidth. It looks like pie chart sections are getting returned on mouse movement away from the chart. Also bar roundness looks to have a possibility to be set with Rx and Ry parameters:

      IEnumerable<ISeries> data = new List<ISeries>
      {
        new ColumnSeries<DateTimePoint>
        {
          Values = myData.Select(x => new DateTimePoint(x.Start, x.Duration)),
          Fill = new LiveChartsCore.SkiaSharpView.Painting.SolidColorPaint(SKColor.Parse("#ffffff")),
          Stroke = new LiveChartsCore.SkiaSharpView.Painting.SolidColorPaint(SKColor.Parse("#ffffff")),
          Rx = 1,
          Ry = 1,
        }
      };
vadimffe commented 11 months ago

With latest rc release I can confirm that all above issues seems to be solved. I have not checked all the changes, but it would be great to have a control on bar width. To be able to make it thinner or thicker

beto-rodriguez commented 11 months ago

Thanks for the feedback! I am glad to read that most of the issues seem fixed.

About the bar width, there are a couple of properties that you can change: https://livecharts.dev/docs/Maui/2.0.0-rc1/samples.bars.spacing

Specially the Padding and MaxBarWidth, are those properties enough for you?