Skip to content

Commit bcd1ca2

Browse files
authored
Fix bug where FindWindowStartIndex did not correctly handle NaN-values (oxyplot#1512)
* Fix issue oxyplot#1512, Fix bug where FindWindowStartIndex did not correctly handle NaN-values * Set Minimum = 4
1 parent 113cee0 commit bcd1ca2

File tree

5 files changed

+225
-29
lines changed

5 files changed

+225
-29
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ All notable changes to this project will be documented in this file.
7979
- Display of ampersands in OxyPlot.WindowsForms Tracker (#1585)
8080
- Full Plotarea Polar plot rendering with non-zero minimum values (#1586)
8181
- Auto margins are set incorrectly if Axis.TitleFontSize is set to non-default value (related to #1577)
82+
- Incomplete rendering of AreaSeries in some situations (#1512)
8283

8384
## [2.0.0] - 2019-10-19
8485
### Added

CONTRIBUTORS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
# Please keep the list sorted.
1212

1313
Alexei Shcherbakov
14+
Anders Musikka <anders@andersmusikka.se>
1415
Auriou
1516
Bartłomiej Szypelow <bszypelow@users.noreply.github.com>
1617
benjaminrupp

Source/Examples/ExampleLibrary/Issues/Issues.cs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2283,6 +2283,38 @@ public static PlotModel EmojiText()
22832283

22842284
return plot;
22852285
}
2286+
[Example("#1512: FindWindowStartIndex.")]
2287+
public static PlotModel FindWindowsStartIndex()
2288+
{
2289+
var plotModel1 = new PlotModel { Title = "AreaSeries broken in time" };
2290+
var axis = new LinearAxis {Position = AxisPosition.Left, MinimumPadding = 0, MaximumPadding = 0.06, AbsoluteMinimum = 0};
2291+
var xAxis = new LinearAxis() {Position = AxisPosition.Bottom, Minimum = 4};
2292+
2293+
plotModel1.Axes.Add(axis);
2294+
plotModel1.Axes.Add(xAxis);
2295+
2296+
var N = 15;
2297+
var random = new Random(6);
2298+
var currentValue = random.NextDouble() - 0.5;
2299+
var areaSeries = new AreaSeries();
2300+
for (int i = 0; i < N; ++i)
2301+
{
2302+
if (random.Next(4) == 0)
2303+
{
2304+
areaSeries.Points.Add(DataPoint.Undefined);
2305+
areaSeries.Points2.Add(DataPoint.Undefined);
2306+
}
2307+
2308+
currentValue += random.NextDouble();
2309+
areaSeries.Points.Add(new DataPoint(currentValue, currentValue));
2310+
areaSeries.Points2.Add(new DataPoint(currentValue, currentValue));
2311+
}
2312+
2313+
plotModel1.Series.Add(areaSeries);
2314+
2315+
2316+
return plotModel1;
2317+
}
22862318

22872319
private class TimeSpanPoint
22882320
{
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
// --------------------------------------------------------------------------------------------------------------------
2+
// <copyright file="XYAxisSeriesTests.cs" company="OxyPlot">
3+
// Copyright (c) 2020 OxyPlot contributors
4+
// </copyright>
5+
// <summary>
6+
// Provides unit tests for the <see cref="XYAxisSeries" /> class.
7+
// </summary>
8+
// --------------------------------------------------------------------------------------------------------------------
9+
10+
namespace OxyPlot.Tests
11+
{
12+
using System;
13+
14+
using NUnit.Framework;
15+
16+
using OxyPlot.Series;
17+
18+
/// <summary>
19+
/// Provides unit tests for the <see cref="XYAxisSeries" /> class.
20+
/// </summary>
21+
public class XYAxisSeriesTests
22+
{
23+
/// <summary>
24+
/// Test class just to allow methods in XYAxisSeries to be tested.
25+
/// </summary>
26+
private class TestAxisSeries : XYAxisSeries
27+
{
28+
public override void Render(IRenderContext rc)
29+
{
30+
throw new NotImplementedException();
31+
}
32+
}
33+
34+
/// <summary>
35+
/// Generate lots of random data, then checking the FindWindowStartIndex method
36+
/// against this data.
37+
/// </summary>
38+
[Test]
39+
public void RunFuzzTest()
40+
{
41+
for (int i = 0; i < 10000; ++i)
42+
{
43+
FuzzIteration(i);
44+
}
45+
}
46+
47+
/// <summary>
48+
/// Generate lots of random data, then checking the FindWindowStartIndex method
49+
/// against this data. This time with data containing NaN-values.
50+
/// </summary>
51+
[Test]
52+
public void RunFuzzWithNanTest()
53+
{
54+
for (int i = 0; i < 10000; ++i)
55+
{
56+
FuzzIterationWithNan(i);
57+
}
58+
}
59+
60+
private static void FuzzIteration(int seed)
61+
{
62+
var xyAxiseries = new TestAxisSeries();
63+
64+
var N = 15;
65+
var random = new Random(seed);
66+
var testData = new System.Collections.Generic.List<double>();
67+
var currentValue = random.NextDouble() - 0.5;
68+
for (int i = 0; i < N; ++i)
69+
{
70+
testData.Add(currentValue);
71+
currentValue += random.NextDouble();
72+
}
73+
74+
var targetX = random.NextDouble() * (N / 2.0) - 1.0;
75+
var guess = random.Next(0, testData.Count);
76+
77+
int foundIndex = xyAxiseries.FindWindowStartIndex(testData, x => x, targetX, guess);
78+
79+
if (foundIndex > 0)
80+
Assert.LessOrEqual(testData[foundIndex], targetX, "At " + seed);
81+
if (foundIndex < testData.Count - 1)
82+
Assert.GreaterOrEqual(testData[foundIndex + 1], targetX, "At " + seed);
83+
}
84+
85+
private static void FuzzIterationWithNan(int seed)
86+
{
87+
var xyAxiseries = new TestAxisSeries();
88+
89+
var N = 15;
90+
var random = new Random(seed);
91+
var testData = new System.Collections.Generic.List<double>();
92+
var currentValue = random.NextDouble() - 0.5;
93+
for (int i = 0; i < N; ++i)
94+
{
95+
if (random.Next(4) == 0)
96+
testData.Add(double.NaN);
97+
testData.Add(currentValue);
98+
currentValue += random.NextDouble();
99+
}
100+
101+
double? PrevNonNan(int index)
102+
{
103+
while (index >= 0)
104+
{
105+
if (double.IsNaN(testData[index]) == false)
106+
return testData[index];
107+
index -= 1;
108+
}
109+
110+
return null;
111+
}
112+
113+
double? NextNonNan(int index)
114+
{
115+
while (index >= 0)
116+
{
117+
if (double.IsNaN(testData[index]) == false)
118+
return testData[index];
119+
index += 1;
120+
}
121+
122+
return null;
123+
}
124+
125+
var targetX = random.NextDouble() * (N / 2.0) - 1.0;
126+
var guess = random.Next(0, testData.Count);
127+
128+
int foundIndex = xyAxiseries.FindWindowStartIndex(testData, x => x, targetX, guess);
129+
130+
if (foundIndex > 0)
131+
{
132+
if (double.IsNaN(testData[foundIndex]))
133+
{
134+
var prevNonNaN = PrevNonNan(foundIndex-1);
135+
if (prevNonNaN.HasValue)
136+
Assert.LessOrEqual(prevNonNaN, targetX, "At " + seed);
137+
}
138+
else
139+
{
140+
Assert.LessOrEqual(testData[foundIndex], targetX, "At " + seed);
141+
}
142+
}
143+
144+
if (foundIndex < testData.Count - 1)
145+
{
146+
if (double.IsNaN(testData[foundIndex + 1]))
147+
{
148+
var nextNonNaN = NextNonNan(foundIndex+1);
149+
if (nextNonNaN.HasValue)
150+
Assert.GreaterOrEqual(nextNonNaN, targetX, "At " + seed);
151+
}
152+
else
153+
{
154+
Assert.GreaterOrEqual(testData[foundIndex + 1], targetX, "At " + seed);
155+
}
156+
}
157+
}
158+
}
159+
}

Source/OxyPlot/Series/XYAxisSeries.cs

Lines changed: 32 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -750,62 +750,65 @@ protected int UpdateWindowStartIndex<T>(IList<T> items, Func<T, double> xgetter,
750750
/// <param name="targetX">target x.</param>
751751
/// <param name="initialGuess">initial guess index.</param>
752752
/// <returns>
753-
/// index of x with max(x) &lt;= target x or -1 if cannot find
753+
/// index of x with max(x) &lt;= target x or 0 if cannot find
754754
/// </returns>
755-
protected int FindWindowStartIndex<T>(IList<T> items, Func<T, double> xgetter, double targetX, int initialGuess)
755+
public int FindWindowStartIndex<T>(IList<T> items, Func<T, double> xgetter, double targetX, int initialGuess)
756756
{
757-
int lastguess = 0;
758757
int start = 0;
759-
int end = items.Count - 1;
760-
int curGuess = initialGuess;
758+
int nominalEnd = items.Count - 1;
759+
while (nominalEnd > 0 && double.IsNaN(xgetter(items[nominalEnd])))
760+
nominalEnd -= 1;
761+
int end = nominalEnd;
762+
int curGuess = Math.Max(0, Math.Min(end, initialGuess));
761763

762-
while (start <= end)
764+
double GetX(int index)
763765
{
764-
if (curGuess < start)
766+
while (index <= nominalEnd)
765767
{
766-
return lastguess;
767-
}
768-
else if (curGuess > end)
769-
{
770-
return end;
768+
double guessX = xgetter(items[index]);
769+
if (double.IsNaN(guessX))
770+
index += 1;
771+
else
772+
return guessX;
771773
}
774+
return xgetter(items[nominalEnd]);
775+
}
772776

773-
double guessX = xgetter(items[curGuess]);
777+
while (start < end)
778+
{
779+
double guessX = GetX(curGuess);
774780
if (guessX.Equals(targetX))
775781
{
776-
return curGuess;
782+
start = curGuess;
783+
break;
777784
}
778785
else if (guessX > targetX)
779786
{
780787
end = curGuess - 1;
781-
if (end < start)
782-
{
783-
return lastguess;
784-
}
785-
else if (end == start)
786-
{
787-
return end;
788-
}
789788
}
790789
else
791-
{
792-
start = curGuess + 1;
793-
lastguess = curGuess;
790+
{
791+
start = curGuess;
794792
}
795793

796794
if (start >= end)
797795
{
798-
return lastguess;
796+
break;
799797
}
800798

801-
double endX = xgetter(items[end]);
802-
double startX = xgetter(items[start]);
799+
double endX = GetX(end);
800+
double startX = GetX(start);
803801

804802
var m = (end - start + 1) / (endX - startX);
803+
805804
curGuess = start + (int)((targetX - startX) * m);
805+
curGuess = Math.Max(start + 1, Math.Min(curGuess, end));
806806
}
807807

808-
return lastguess;
808+
while (start > 0 && (xgetter(items[start]) > targetX))
809+
start -= 1;
810+
811+
return start;
809812
}
810813
}
811814
}

0 commit comments

Comments
 (0)