diff --git a/.github/workflows/livecharts.yml b/.github/workflows/livecharts.yml index 8f3b80a6f..fd0877e8b 100644 --- a/.github/workflows/livecharts.yml +++ b/.github/workflows/livecharts.yml @@ -7,7 +7,6 @@ permissions: on: pull_request: - branches: [ master, dev ] jobs: diff --git a/src/LiveChartsCore/Kernel/Stacker.cs b/src/LiveChartsCore/Kernel/Stacker.cs index 473f2b7c6..1e0341cbb 100644 --- a/src/LiveChartsCore/Kernel/Stacker.cs +++ b/src/LiveChartsCore/Kernel/Stacker.cs @@ -125,16 +125,22 @@ public double StackPoint(ChartPoint point, int seriesStackPosition) if (value >= 0) { currentStack.End += value; + currentStack.NegativeEnd += value; var positiveTotal = _totals[index].Positive + value; _totals[index].Positive = positiveTotal; + var negativeTotal = _totals[index].Negative + value; + _totals[index].Negative = negativeTotal; return positiveTotal; } else { + currentStack.End += value; currentStack.NegativeEnd += value; var negativeTotal = _totals[index].Negative + value; _totals[index].Negative = negativeTotal; + var positiveTotal = _totals[index].Positive + value; + _totals[index].Positive = positiveTotal; return negativeTotal; } diff --git a/tests/CoreTests/SeriesTests/StackedAreaSeriesTest.cs b/tests/CoreTests/SeriesTests/StackedAreaSeriesTest.cs index 5abcfe5cd..6db7b0a70 100644 --- a/tests/CoreTests/SeriesTests/StackedAreaSeriesTest.cs +++ b/tests/CoreTests/SeriesTests/StackedAreaSeriesTest.cs @@ -349,4 +349,88 @@ public void ShouldPlaceDataLabel() Assert.IsTrue(!chart.CoreCanvas.ContainsPaintTask(previousPaint)); } + + [TestMethod] + public void ShouldHandleMixedPositiveNegativeValues() + { + // Regression test for #2086: Verify that stacked values are calculated correctly + // when there are mixed positive and negative values across multiple series at the same index. + // + // The fix ensures that both End and NegativeEnd are kept in sync to maintain proper + // stacking relationships. This is necessary because: + // - When Series2 starts, it needs to know where Series1 ended, regardless of whether + // Series1's last value was positive or negative + // - Start is derived from the previous series' End in the stacker logic + // - NegativeStart is derived from the previous series' NegativeEnd in the stacker logic + // - For proper stacking, End and NegativeEnd must represent the same cumulative position + + // Series 1: positive at index 0, negative at index 1 + // Series 2: negative at index 0, positive at index 1 + var series1 = new StackedAreaSeries + { + Values = [5, -3], + GeometrySize = 10 + }; + + var series2 = new StackedAreaSeries + { + Values = [-2, 4], + GeometrySize = 10 + }; + + var chart = new SKCartesianChart + { + Width = 1000, + Height = 1000, + Series = [series1, series2], + XAxes = [new Axis()], + YAxes = [new Axis()] + }; + + _ = chart.GetImage(); + + var datafactory1 = series1.DataFactory; + var points1 = datafactory1.Fetch(series1, chart.CoreChart).ToArray(); + + var datafactory2 = series2.DataFactory; + var points2 = datafactory2.Fetch(series2, chart.CoreChart).ToArray(); + + // For SecondaryValue = 0: series1 = 5 (positive), series2 = -2 (negative) + var point1_0 = points1.Single(p => p.Coordinate.SecondaryValue == 0); + var point2_0 = points2.Single(p => p.Coordinate.SecondaryValue == 0); + + // For SecondaryValue = 1: series1 = -3 (negative), series2 = 4 (positive) + var point1_1 = points1.Single(p => p.Coordinate.SecondaryValue == 1); + var point2_1 = points2.Single(p => p.Coordinate.SecondaryValue == 1); + + // Verify series1 at index 0 (positive value 5) + // Both End and NegativeEnd are set to 5 to keep them in sync + Assert.AreEqual(0, point1_0.StackedValue.Start, 0.001, "Series1[0] Start should be 0"); + Assert.AreEqual(5, point1_0.StackedValue.End, 0.001, "Series1[0] End should be 5"); + Assert.AreEqual(0, point1_0.StackedValue.NegativeStart, 0.001, "Series1[0] NegativeStart should be 0"); + Assert.AreEqual(5, point1_0.StackedValue.NegativeEnd, 0.001, "Series1[0] NegativeEnd should be 5 (kept in sync with End)"); + + // Verify series2 at index 0 (negative value -2) + // Start and NegativeStart are both derived from series1's End/NegativeEnd (both 5) + // After adding -2, both End and NegativeEnd become 3 + Assert.AreEqual(5, point2_0.StackedValue.Start, 0.001, "Series2[0] Start should be 5 (from Series1 End)"); + Assert.AreEqual(3, point2_0.StackedValue.End, 0.001, "Series2[0] End should be 3 (5 + (-2))"); + Assert.AreEqual(5, point2_0.StackedValue.NegativeStart, 0.001, "Series2[0] NegativeStart should be 5 (from Series1 NegativeEnd)"); + Assert.AreEqual(3, point2_0.StackedValue.NegativeEnd, 0.001, "Series2[0] NegativeEnd should be 3 (kept in sync with End)"); + + // Verify series1 at index 1 (negative value -3) + // Both End and NegativeEnd are set to -3 to keep them in sync + Assert.AreEqual(0, point1_1.StackedValue.Start, 0.001, "Series1[1] Start should be 0"); + Assert.AreEqual(-3, point1_1.StackedValue.End, 0.001, "Series1[1] End should be -3"); + Assert.AreEqual(0, point1_1.StackedValue.NegativeStart, 0.001, "Series1[1] NegativeStart should be 0"); + Assert.AreEqual(-3, point1_1.StackedValue.NegativeEnd, 0.001, "Series1[1] NegativeEnd should be -3 (kept in sync with End)"); + + // Verify series2 at index 1 (positive value 4) + // Start and NegativeStart are both derived from series1's End/NegativeEnd (both -3) + // After adding 4, both End and NegativeEnd become 1 + Assert.AreEqual(-3, point2_1.StackedValue.Start, 0.001, "Series2[1] Start should be -3 (from Series1 End)"); + Assert.AreEqual(1, point2_1.StackedValue.End, 0.001, "Series2[1] End should be 1 (-3 + 4)"); + Assert.AreEqual(-3, point2_1.StackedValue.NegativeStart, 0.001, "Series2[1] NegativeStart should be -3 (from Series1 NegativeEnd)"); + Assert.AreEqual(1, point2_1.StackedValue.NegativeEnd, 0.001, "Series2[1] NegativeEnd should be 1 (kept in sync with End)"); + } } diff --git a/tests/CoreTests/SeriesTests/StackedStepAreaSeriesTest.cs b/tests/CoreTests/SeriesTests/StackedStepAreaSeriesTest.cs index 8acb7d413..9172ee826 100644 --- a/tests/CoreTests/SeriesTests/StackedStepAreaSeriesTest.cs +++ b/tests/CoreTests/SeriesTests/StackedStepAreaSeriesTest.cs @@ -348,4 +348,88 @@ public void ShouldPlaceDataLabel() Assert.IsTrue(!chart.CoreCanvas.ContainsPaintTask(previousPaint)); } + + [TestMethod] + public void ShouldHandleMixedPositiveNegativeValues() + { + // Regression test for #2086: Verify that stacked values are calculated correctly + // when there are mixed positive and negative values across multiple series at the same index. + // + // The fix ensures that both End and NegativeEnd are kept in sync to maintain proper + // stacking relationships. This is necessary because: + // - When Series2 starts, it needs to know where Series1 ended, regardless of whether + // Series1's last value was positive or negative + // - Start is derived from the previous series' End in the stacker logic + // - NegativeStart is derived from the previous series' NegativeEnd in the stacker logic + // - For proper stacking, End and NegativeEnd must represent the same cumulative position + + // Series 1: positive at index 0, negative at index 1 + // Series 2: negative at index 0, positive at index 1 + var series1 = new StackedStepAreaSeries + { + Values = [5, -3], + GeometrySize = 10 + }; + + var series2 = new StackedStepAreaSeries + { + Values = [-2, 4], + GeometrySize = 10 + }; + + var chart = new SKCartesianChart + { + Width = 1000, + Height = 1000, + Series = [series1, series2], + XAxes = [new Axis()], + YAxes = [new Axis()] + }; + + _ = chart.GetImage(); + + var datafactory1 = series1.DataFactory; + var points1 = datafactory1.Fetch(series1, chart.CoreChart).ToArray(); + + var datafactory2 = series2.DataFactory; + var points2 = datafactory2.Fetch(series2, chart.CoreChart).ToArray(); + + // For SecondaryValue = 0: series1 = 5 (positive), series2 = -2 (negative) + var point1_0 = points1.Single(p => p.Coordinate.SecondaryValue == 0); + var point2_0 = points2.Single(p => p.Coordinate.SecondaryValue == 0); + + // For SecondaryValue = 1: series1 = -3 (negative), series2 = 4 (positive) + var point1_1 = points1.Single(p => p.Coordinate.SecondaryValue == 1); + var point2_1 = points2.Single(p => p.Coordinate.SecondaryValue == 1); + + // Verify series1 at index 0 (positive value 5) + // Both End and NegativeEnd are set to 5 to keep them in sync + Assert.AreEqual(0, point1_0.StackedValue.Start, 0.001, "Series1[0] Start should be 0"); + Assert.AreEqual(5, point1_0.StackedValue.End, 0.001, "Series1[0] End should be 5"); + Assert.AreEqual(0, point1_0.StackedValue.NegativeStart, 0.001, "Series1[0] NegativeStart should be 0"); + Assert.AreEqual(5, point1_0.StackedValue.NegativeEnd, 0.001, "Series1[0] NegativeEnd should be 5 (kept in sync with End)"); + + // Verify series2 at index 0 (negative value -2) + // Start and NegativeStart are both derived from series1's End/NegativeEnd (both 5) + // After adding -2, both End and NegativeEnd become 3 + Assert.AreEqual(5, point2_0.StackedValue.Start, 0.001, "Series2[0] Start should be 5 (from Series1 End)"); + Assert.AreEqual(3, point2_0.StackedValue.End, 0.001, "Series2[0] End should be 3 (5 + (-2))"); + Assert.AreEqual(5, point2_0.StackedValue.NegativeStart, 0.001, "Series2[0] NegativeStart should be 5 (from Series1 NegativeEnd)"); + Assert.AreEqual(3, point2_0.StackedValue.NegativeEnd, 0.001, "Series2[0] NegativeEnd should be 3 (kept in sync with End)"); + + // Verify series1 at index 1 (negative value -3) + // Both End and NegativeEnd are set to -3 to keep them in sync + Assert.AreEqual(0, point1_1.StackedValue.Start, 0.001, "Series1[1] Start should be 0"); + Assert.AreEqual(-3, point1_1.StackedValue.End, 0.001, "Series1[1] End should be -3"); + Assert.AreEqual(0, point1_1.StackedValue.NegativeStart, 0.001, "Series1[1] NegativeStart should be 0"); + Assert.AreEqual(-3, point1_1.StackedValue.NegativeEnd, 0.001, "Series1[1] NegativeEnd should be -3 (kept in sync with End)"); + + // Verify series2 at index 1 (positive value 4) + // Start and NegativeStart are both derived from series1's End/NegativeEnd (both -3) + // After adding 4, both End and NegativeEnd become 1 + Assert.AreEqual(-3, point2_1.StackedValue.Start, 0.001, "Series2[1] Start should be -3 (from Series1 End)"); + Assert.AreEqual(1, point2_1.StackedValue.End, 0.001, "Series2[1] End should be 1 (-3 + 4)"); + Assert.AreEqual(-3, point2_1.StackedValue.NegativeStart, 0.001, "Series2[1] NegativeStart should be -3 (from Series1 NegativeEnd)"); + Assert.AreEqual(1, point2_1.StackedValue.NegativeEnd, 0.001, "Series2[1] NegativeEnd should be 1 (kept in sync with End)"); + } }