Skip to content

Commit 4c372ac

Browse files
authored
Fix #2061 DateTimeAxis.ToDateTime doesn't behave as intended in .NET 8 (#2089)
1 parent ac2296e commit 4c372ac

5 files changed

Lines changed: 104 additions & 18 deletions

File tree

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ All notable changes to this project will be documented in this file.
1515
- Expanded `IntervalBarSeries` and `TornadoBarSeries` to allow for varied label positions and angles (#2027)
1616
- VectorSeries (#107)
1717
- LogarithmicColorAxis (#92)
18+
- DateTimeAxis.DateTimePrecision property (related to #2061)
1819

1920
### Changed
2021
- Make consistent BaseValue and BaseLine across BarSeries, LinearBarSeries, and HistogramSeries
@@ -26,6 +27,7 @@ All notable changes to this project will be documented in this file.
2627
- Add support for .NET 7.0 (#1937)
2728
- Update SkiaSharp to Version 2.88.6
2829
- AxisRendererBase is now generic
30+
- DateTimeAxis.ToDateTime(double value) is now obsolete, replacements are provided (related to #2061)
2931

3032
### Removed
3133
- Support for .NET Framework 4.0 and 4.5 (#1839)
@@ -41,6 +43,7 @@ All notable changes to this project will be documented in this file.
4143
- Font weight not being applied in ImageSharp (#2006)
4244
- SkiaSharp - Fix use of obsolete functions (#1937)
4345
- Dashed lines are solid when exporting via SkiaSharp.SvgExporter (#1674)
46+
- DateTimeAxis.ToDateTime doesn't behave as intended in .NET 8 (#2061)
4447

4548
## [2.1.2] - 2022-12-03
4649

Source/Examples/ExampleLibrary/Axes/DateTimeAxisExamples.cs

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -231,7 +231,11 @@ public static PlotModel SunriseandsunsetinOslo()
231231
public static PlotModel LabelFormatter()
232232
{
233233
var model = new PlotModel { Title = "Using LabelFormatter to format labels by day of week" };
234-
model.Axes.Add(new DateTimeAxis { LabelFormatter = x => DateTimeAxis.ToDateTime(x).DayOfWeek.ToString().Substring(0, 3) });
234+
235+
var dateTimeAxis = new DateTimeAxis();
236+
dateTimeAxis.LabelFormatter = x => dateTimeAxis.ConvertToDateTime(x).DayOfWeek.ToString().Substring(0, 3);
237+
model.Axes.Add(dateTimeAxis);
238+
235239
var series = new LineSeries();
236240
model.Series.Add(series);
237241
for (int i = 0; i < 7; i++)
@@ -244,5 +248,39 @@ public static PlotModel LabelFormatter()
244248

245249
return model;
246250
}
251+
252+
[Example("DateTimePrecision")]
253+
public static PlotModel DateTimePrecision()
254+
{
255+
var model = new PlotModel
256+
{
257+
Title = "This shows the effect of DateTimePrecision",
258+
Subtitle = "(only apparent under .NET 7 and above)"
259+
};
260+
261+
var startDate = new DateTime(2014, 07, 20);
262+
263+
var dateTimeAxis1 = new DateTimeAxis
264+
{
265+
Minimum = DateTimeAxis.ToDouble(startDate),
266+
Maximum = DateTimeAxis.ToDouble(startDate.AddMinutes(50)),
267+
Position = AxisPosition.Bottom,
268+
DateTimePrecision = new TimeSpan(1),
269+
Title = "DateTimePrecision = 1 tick (no rounding)",
270+
};
271+
272+
var dateTimeAxis2 = new DateTimeAxis
273+
{
274+
Minimum = dateTimeAxis1.Minimum,
275+
Maximum = dateTimeAxis1.Maximum,
276+
Position = AxisPosition.Top,
277+
Title = "DateTimePrecision = 1 ms (default value)",
278+
};
279+
280+
model.Axes.Add(dateTimeAxis1);
281+
model.Axes.Add(dateTimeAxis2);
282+
283+
return model;
284+
}
247285
}
248-
}
286+
}

Source/Examples/WPF/WpfExamples/Examples/AnimationsDemo/Models/Pnl.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ public class Pnl : IAnimatablePoint
2222

2323
public DateTime Time
2424
{
25-
get { return DateTimeAxis.ToDateTime(this.X); }
25+
get { return DateTimeAxis.ToDateTime(this.X, DateTimeAxis.DefaultPrecision); }
2626
set
2727
{
2828
var finalX = DateTimeAxis.ToDouble(value);
@@ -47,4 +47,4 @@ public override string ToString()
4747
return String.Format("{0:HH:mm} {1:0.0}", this.Time, this.Value);
4848
}
4949
}
50-
}
50+
}

Source/OxyPlot.Tests/Axes/DateTimeAxisTests.cs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -35,31 +35,31 @@ public void ToDouble_NoDate()
3535
public void ToDateTime_ValidDate()
3636
{
3737
var date = new DateTime(2011, 3, 15);
38-
Assert.AreEqual(date, DateTimeAxis.ToDateTime(date.ToOADate()));
38+
Assert.AreEqual(date, DateTimeAxis.ToDateTime(date.ToOADate(), DateTimeAxis.DefaultPrecision));
3939
}
4040

4141
[Test]
4242
public void ToDateTime_NoDate()
4343
{
44-
Assert.AreEqual(new DateTime(), DateTimeAxis.ToDateTime(-693593));
44+
Assert.AreEqual(new DateTime(), DateTimeAxis.ToDateTime(-693593, DateTimeAxis.DefaultPrecision));
4545
}
4646

4747
[Test]
4848
public void ToDateTime_NaN()
4949
{
50-
Assert.AreEqual(new DateTime(), DateTimeAxis.ToDateTime(double.NaN));
50+
Assert.AreEqual(new DateTime(), DateTimeAxis.ToDateTime(double.NaN, DateTimeAxis.DefaultPrecision));
5151
}
5252

5353
[Test]
5454
public void ToDateTime_VeryBigValue()
5555
{
56-
Assert.AreEqual(new DateTime(), DateTimeAxis.ToDateTime(double.MaxValue));
56+
Assert.AreEqual(new DateTime(), DateTimeAxis.ToDateTime(double.MaxValue, DateTimeAxis.DefaultPrecision));
5757
}
5858

5959
[Test]
6060
public void ToDateTime_VerySmallValue()
6161
{
62-
Assert.AreEqual(new DateTime(), DateTimeAxis.ToDateTime(double.MinValue));
62+
Assert.AreEqual(new DateTime(), DateTimeAxis.ToDateTime(double.MinValue, DateTimeAxis.DefaultPrecision));
6363
}
6464
}
65-
}
65+
}

Source/OxyPlot/Axes/DateTimeAxis.cs

Lines changed: 53 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,11 @@ namespace OxyPlot.Axes
2626
/// <code>"h:mm"</code> shows hours and minutes</remarks>
2727
public class DateTimeAxis : LinearAxis
2828
{
29+
/// <summary>
30+
/// The default precision that is used for rounding DateTime values. 1ms is used to emulate the behavior of .NET 6 and earlier.
31+
/// </summary>
32+
public static readonly TimeSpan DefaultPrecision = TimeSpan.FromMilliseconds(1);
33+
2934
/// <summary>
3035
/// The time origin.
3136
/// </summary>
@@ -61,6 +66,7 @@ public DateTimeAxis()
6166
this.IntervalType = DateTimeIntervalType.Auto;
6267
this.FirstDayOfWeek = DayOfWeek.Monday;
6368
this.CalendarWeekRule = CalendarWeekRule.FirstFourDayWeek;
69+
this.DateTimePrecision = DefaultPrecision;
6470
}
6571

6672
/// <summary>
@@ -83,6 +89,13 @@ public DateTimeAxis()
8389
/// </summary>
8490
public DateTimeIntervalType MinorIntervalType { get; set; }
8591

92+
/// <summary>
93+
/// Gets or sets the precision that is used for DateTime values internally. Limiting the precision avoids 'unexpected' tick labels, e.g.
94+
/// '11:59' for a value of 11:59.99999. The default value is 1 Millisecond.
95+
/// </summary>
96+
/// <remarks>For .NET 6 and below, the minimum precision is 1 ms. Using a DateTimePrecision smaller than 1 ms will not result in increased precision.</remarks>
97+
public TimeSpan DateTimePrecision { get; set; }
98+
8699
/// <summary>
87100
/// Gets or sets the time zone (used when formatting date/time values).
88101
/// </summary>
@@ -124,18 +137,40 @@ public static DataPoint CreateDataPoint(double x, DateTime y)
124137
}
125138

126139
/// <summary>
127-
/// Converts a numeric representation of the date (number of days after the time origin) to a DateTime structure.
140+
/// Converts a numeric representation of the date (number of days after the time origin) to a DateTime structure, using a precision of 1 Millisecond.
128141
/// </summary>
129142
/// <param name="value">The number of days after the time origin.</param>
130143
/// <returns>A <see cref="DateTime" /> structure. Ticks = 0 if the value is invalid.</returns>
144+
[Obsolete("Use ConvertToDateTime(double value) or ToDateTime(double value, TimeSpan precision) instead.")]
131145
public static DateTime ToDateTime(double value)
146+
{
147+
return ToDateTime(value, DefaultPrecision);
148+
}
149+
150+
/// <summary>
151+
/// Converts a numeric representation of the date (number of days after the time origin) to a DateTime structure.
152+
/// </summary>
153+
/// <param name="value">The number of days after the time origin.</param>
154+
/// <param name="precision">The precision that is used for the conversion. The DateTime value is rounded to the next integer multiple of this value.</param>
155+
/// <returns>A <see cref="DateTime" /> structure. Ticks = 0 if the value is invalid.</returns>
156+
public static DateTime ToDateTime(double value, TimeSpan precision)
132157
{
133158
if (double.IsNaN(value) || value < MinDayValue || value > MaxDayValue)
134159
{
135160
return new DateTime();
136161
}
137162

138-
return TimeOrigin.AddDays(value - 1);
163+
var preliminaryDateTime = TimeOrigin.AddDays(value - 1);
164+
165+
var precisionIntervals = preliminaryDateTime.Ticks / precision.Ticks;
166+
var remainderTicks = preliminaryDateTime.Ticks % precision.Ticks;
167+
168+
if (remainderTicks >= precision.Ticks / 2)
169+
{
170+
precisionIntervals += 1;
171+
}
172+
173+
return new DateTime(precisionIntervals * precision.Ticks);
139174
}
140175

141176
/// <summary>
@@ -149,6 +184,16 @@ public static double ToDouble(DateTime value)
149184
return span.TotalDays + 1;
150185
}
151186

187+
/// <summary>
188+
/// Converts a numeric representation of the date (number of days after the time origin) to a DateTime structure, using the precision specified by <see cref="DateTimePrecision" />.
189+
/// </summary>
190+
/// <param name="value">The number of days after the time origin.</param>
191+
/// <returns>A <see cref="DateTime" /> structure. Ticks = 0 if the value is invalid.</returns>
192+
public DateTime ConvertToDateTime(double value)
193+
{
194+
return ToDateTime(value, this.DateTimePrecision);
195+
}
196+
152197
/// <summary>
153198
/// Gets the tick values.
154199
/// </summary>
@@ -175,7 +220,7 @@ public override void GetTickValues(
175220
/// <returns>The value.</returns>
176221
public override object GetValue(double x)
177222
{
178-
var time = ToDateTime(x);
223+
var time = this.ConvertToDateTime(x);
179224

180225
if (this.TimeZone != null)
181226
{
@@ -291,7 +336,7 @@ protected override string GetDefaultStringFormat()
291336
protected override string FormatValueOverride(double x)
292337
{
293338
// convert the double value to a DateTime
294-
var time = ToDateTime(x);
339+
var time = this.ConvertToDateTime(x);
295340

296341
// If a time zone is specified, convert the time
297342
if (this.TimeZone != null)
@@ -451,7 +496,7 @@ private IList<double> CreateDateTickValues(
451496
double min, double max, double step, DateTimeIntervalType intervalType)
452497
{
453498
var values = new Collection<double>();
454-
var start = ToDateTime(min);
499+
var start = this.ConvertToDateTime(min);
455500
if (start.Ticks == 0)
456501
{
457502
// Invalid start time
@@ -478,7 +523,7 @@ private IList<double> CreateDateTickValues(
478523
}
479524

480525
// Adds a tick to the end time to make sure the end DateTime is included.
481-
var end = ToDateTime(max).AddTicks(1);
526+
var end = this.ConvertToDateTime(max).AddTicks(1);
482527
if (end.Ticks == 0)
483528
{
484529
// Invalid end time
@@ -487,8 +532,8 @@ private IList<double> CreateDateTickValues(
487532

488533
var current = start;
489534
double eps = step * 1e-3;
490-
var minDateTime = ToDateTime(min - eps);
491-
var maxDateTime = ToDateTime(max + eps);
535+
var minDateTime = this.ConvertToDateTime(min - eps);
536+
var maxDateTime = this.ConvertToDateTime(max + eps);
492537

493538
if (minDateTime.Ticks == 0 || maxDateTime.Ticks == 0)
494539
{

0 commit comments

Comments
 (0)