Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .github/workflows/livecharts.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ permissions:

on:
pull_request:
branches: [ master, dev ]

jobs:

Expand Down
6 changes: 6 additions & 0 deletions src/LiveChartsCore/Kernel/Stacker.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Comment on lines 127 to +143

Copilot AI Feb 8, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the value >= 0 branch, the code now also adds the positive value to currentStack.NegativeEnd and _totals[index].Negative. That mixes the positive and negative stacks/totals; it will shift NegativeEnd upward and make NegativeTotal positive, which breaks later negative stacking (next series uses previousActiveStack.NegativeEnd) and can produce incorrect StackedValue.Share / bounds.

Suggested change
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;
// positive values contribute only to the positive stack and positive totals
currentStack.End += value;
var positiveTotal = _totals[index].Positive + value;
_totals[index].Positive = positiveTotal;
return positiveTotal;
}
else
{
// negative values contribute only to the negative stack and negative totals
currentStack.NegativeEnd += value;
var negativeTotal = _totals[index].Negative + value;
_totals[index].Negative = negativeTotal;

Copilot uses AI. Check for mistakes.
Comment on lines 127 to +143

Copilot AI Feb 8, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the value < 0 branch, the code now also adds the negative value to currentStack.End and _totals[index].Positive. That contaminates the positive stack baseline (previousActiveStack.End is used as the next series' positiveStart) and can make Total incorrect/negative, affecting stacked rendering and axis bounds.

Suggested change
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;
// positive values contribute only to the positive stack and total
currentStack.End += value;
var positiveTotal = _totals[index].Positive + value;
_totals[index].Positive = positiveTotal;
return positiveTotal;
}
else
{
// negative values contribute only to the negative stack and total
currentStack.NegativeEnd += value;
var negativeTotal = _totals[index].Negative + value;
_totals[index].Negative = negativeTotal;

Copilot uses AI. Check for mistakes.

return negativeTotal;
Comment thread
beto-rodriguez marked this conversation as resolved.
}
Expand Down
84 changes: 84 additions & 0 deletions tests/CoreTests/SeriesTests/StackedAreaSeriesTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<double>
{
Values = [5, -3],
GeometrySize = 10
};

var series2 = new StackedAreaSeries<double>
{
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)");
}
}
84 changes: 84 additions & 0 deletions tests/CoreTests/SeriesTests/StackedStepAreaSeriesTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<double>
{
Values = [5, -3],
GeometrySize = 10
};

var series2 = new StackedStepAreaSeries<double>
{
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)");
}
}
Loading