Skip to content

Commit 2d45376

Browse files
Fix for logarithmic axis for LinearBarSeries (#740) (#1942)
* Add demo for LinearBarSeries with logarithmic axis. * Fix for logarithmic LinearBarSeries. * Fix for padding calculation for logarithmic axis. * Updated Changelog. * Use BaseLine and LowestValidRoundTripValue on LogarithmicAxis to render LineBarSeries correctly. * Better separated box generation from zoom. --------- Co-authored-by: VisualMelon <VisualMelon@users.noreply.github.com>
1 parent a32be6e commit 2d45376

5 files changed

Lines changed: 151 additions & 5 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ All notable changes to this project will be documented in this file.
4747
- CategoryAxis.ItemsSource without BarSeries (#1847)
4848
- Histogram now rendering properly when using logarithmic Y axis (#740)
4949
- Fix ExampleLibrary build errors in certain code pages (#1890)
50+
- LineBarSeries now rendering properly when using logarithmic scale (#740)
5051
- Fix for double.Epsilon zero check that fails on some architectures (#1924)
5152

5253
## [2.1.0] - 2021-10-02

Source/Examples/ExampleLibrary/Series/LinearBarSeriesExamples.cs

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,20 @@ public static PlotModel WithStroke()
4444
[Example("With negative colors")]
4545
public static PlotModel WithNegativeColors()
4646
{
47-
var model = new PlotModel { Title = "LinearBarSeries with stroke" };
47+
return CreateWithNegativeColors();
48+
}
49+
50+
[Example("With negative colors (logaritmic)")]
51+
public static PlotModel WithNegativeColorsLogarithmic()
52+
{
53+
return CreateWithNegativeColors(true);
54+
}
55+
56+
public static PlotModel CreateWithNegativeColors(bool logarithmic = false)
57+
{
58+
var model = new PlotModel { Title = logarithmic ? "LinearBarSeries with stroke (logarithmic)" : "LinearBarSeries with stroke" };
4859
model.Axes.Add(new LinearAxis { Position = AxisPosition.Bottom });
49-
model.Axes.Add(new LinearAxis { Position = AxisPosition.Left });
60+
model.Axes.Add(logarithmic ? (Axis)new LogarithmicAxis { Position = AxisPosition.Left } : new LinearAxis { Position = AxisPosition.Left });
5061
var linearBarSeries = CreateExampleLinearBarSeriesWithNegativeValues();
5162
linearBarSeries.Title = "LinearBarSeries";
5263
linearBarSeries.FillColor = OxyColor.Parse("#454CAF50");

Source/OxyPlot/Axes/LogarithmicAxis.cs

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,12 @@ namespace OxyPlot.Axes
2020
/// <see href="http://en.wikipedia.org/wiki/Logarithmic_scale"/>
2121
public class LogarithmicAxis : Axis
2222
{
23+
/// <summary>
24+
/// The lowest value that can be transformed to logarithmic and
25+
/// back yielding the same non-infinite number.
26+
/// </summary>
27+
public static readonly double LowestValidRoundtripValue = 2.22507385850726E-308;
28+
2329
/// <summary>
2430
/// Initializes a new instance of the <see cref = "LogarithmicAxis" /> class.
2531
/// </summary>
@@ -633,5 +639,86 @@ protected override void CoerceActualMaxMin()
633639

634640
base.CoerceActualMaxMin();
635641
}
642+
643+
/// <summary>
644+
/// Calculates the actual maximum value of the axis, including the <see cref="Axis.MaximumPadding" />.
645+
/// </summary>
646+
/// <returns>The new actual maximum value of the axis.</returns>
647+
/// <remarks>
648+
/// Must be called before <see cref="CalculateActualMinimum" />
649+
/// </remarks>
650+
protected override double CalculateActualMaximum()
651+
{
652+
var actualMaximum = this.DataMaximum;
653+
double range = this.DataMaximum - this.DataMinimum;
654+
655+
if (range < double.Epsilon)
656+
{
657+
double zeroRange = this.DataMaximum > 0 ? this.DataMaximum : 1;
658+
actualMaximum += zeroRange * 0.5;
659+
}
660+
661+
if (!double.IsNaN(this.DataMinimum) && !double.IsNaN(actualMaximum))
662+
{
663+
// On log axis we actually see log(x). Now if we want to have a padding we need to change actualMaximum
664+
// to an x value that corresponds to the padding expected in log scale
665+
// x_0 x_1 x_2
666+
// |---------------------|--------|
667+
// log(x_0) log(x_1) log(x_2)
668+
// where actualMaximum = x_2
669+
//
670+
// log(x_2) - log(x_1) = padding * [log(x_1) - log(x_0)]
671+
// log(x_2) = padding * log(x_1/x_0) + log(x_1)
672+
// x_2 = (x_1/x_0)^padding * x_1
673+
674+
double x1 = actualMaximum;
675+
double x0 = this.DataMinimum;
676+
return Math.Pow(x1, this.MaximumPadding + 1) * (x0 > double.Epsilon ? Math.Pow(x0, -this.MaximumPadding) : 1);
677+
}
678+
679+
return actualMaximum;
680+
}
681+
682+
/// <summary>
683+
/// Calculates the actual minimum value of the axis, including the <see cref="Axis.MinimumPadding" />.
684+
/// </summary>
685+
/// <returns>The new actual minimum value of the axis.</returns>
686+
/// <remarks>
687+
/// Must be called after <see cref="CalculateActualMaximum" />
688+
/// </remarks>
689+
protected override double CalculateActualMinimum()
690+
{
691+
var actualMinimum = this.DataMinimum;
692+
double range = this.DataMaximum - this.DataMinimum;
693+
694+
if (range < double.Epsilon)
695+
{
696+
double zeroRange = this.DataMaximum > 0 ? this.DataMaximum : 1;
697+
actualMinimum -= zeroRange * 0.5;
698+
}
699+
700+
if (!double.IsNaN(this.ActualMaximum))
701+
{
702+
// For the padding on the min value it is very similar to the calculation mentioned in
703+
// CalculateActualMaximum. However, since this is called after CalculateActualMaximum,
704+
// we no longer know x_1.
705+
// x_3 x_0 x_1 x_2
706+
// |---------|------------|--------|
707+
// log(x_3) log(x_0) log(x_1) log(x_2)
708+
// where actualMiminum = x_3
709+
// log(x_0) - log(x_3) = padding * [log(x_1) - log(x_0)]
710+
// from CalculateActualMaximum we can use
711+
// log(x_1) = [log(x_2) + max_padding * log(x_0)] / (1 + max_padding)
712+
// x_3 = x_0^[1 + padding - padding * max_padding / (1 + max_padding)] * x_2^[-padding / (1 + max_padding)]
713+
714+
double x1 = this.ActualMaximum;
715+
double x0 = actualMinimum;
716+
double existingPadding = this.MaximumPadding;
717+
return Math.Pow(x0, 1 + this.MinimumPadding - this.MinimumPadding * existingPadding / (1 + existingPadding)) * Math.Pow(x1, -this.MinimumPadding / (1 + existingPadding));
718+
}
719+
720+
return actualMinimum;
721+
}
722+
636723
}
637724
}

Source/OxyPlot/Series/BarSeries/BarSeries.cs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
namespace OxyPlot.Series
1111
{
12+
using OxyPlot.Axes;
1213
using System;
1314
using System.Collections.Generic;
1415
using System.Linq;
@@ -46,7 +47,7 @@ public BarSeries()
4647
/// </summary>
4748
/// <value>The actual color.</value>
4849
public OxyColor ActualFillColor => this.FillColor.GetActualColor(this.defaultFillColor);
49-
50+
5051
/// <summary>
5152
/// Gets or sets the base value.
5253
/// </summary>
@@ -387,6 +388,11 @@ public override void Render(IRenderContext rc)
387388

388389
var topValue = this.IsStacked ? baseValue + value : value;
389390

391+
if (this.YAxis.IsLogarithmic() && !this.YAxis.IsValidValue(topValue))
392+
{
393+
continue;
394+
}
395+
390396
// Calculate offset
391397
double categoryValue;
392398
if (this.IsStacked)
@@ -403,6 +409,11 @@ public override void Render(IRenderContext rc)
403409
this.Manager.SetCurrentBaseValue(stackIndex, categoryIndex, value < 0, topValue);
404410
}
405411

412+
if (this.YAxis.IsLogarithmic() && !this.YAxis.IsValidValue(baseValue))
413+
{
414+
baseValue = LogarithmicAxis.LowestValidRoundtripValue;
415+
}
416+
406417
var rect = new OxyRect(this.Transform(baseValue, categoryValue), this.Transform(topValue, categoryValue + actualBarWidth));
407418

408419
this.ActualBarRectangles.Add(rect);

Source/OxyPlot/Series/BarSeries/LinearBarSeries.cs

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,10 @@
99

1010
namespace OxyPlot.Series
1111
{
12+
using OxyPlot.Axes;
13+
using System;
1214
using System.Collections.Generic;
15+
using System.Linq;
1316

1417
/// <summary>
1518
/// Represents a series to display bars in a linear axis
@@ -93,6 +96,12 @@ public OxyColor ActualColor
9396
}
9497
}
9598

99+
/// <summary>
100+
/// Gets or sets the base line. This value is used for plotting to determine
101+
/// the start of the plot. A suitable value is selected automatically if it is NaN.
102+
/// </summary>
103+
public double BaseLine { get; set; } = double.NaN;
104+
96105
/// <summary>
97106
/// Gets the nearest point.
98107
/// </summary>
@@ -195,7 +204,27 @@ protected internal override void UpdateAxisMaxMin()
195204
{
196205
base.UpdateAxisMaxMin();
197206

198-
this.YAxis.Include(0.0);
207+
this.YAxis.Include(this.GetBaseLineOrAutomaticValue(this.YAxis));
208+
}
209+
210+
/// <summary>
211+
/// Gets the base line value. If the value of the property is NaN, a sensible value is returned.
212+
/// </summary>
213+
/// <param name="axis">The axis for which to return a sensible value for.</param>
214+
/// <returns>The base line property or a sensible default.</returns>
215+
protected double GetBaseLineOrAutomaticValue(Axis axis)
216+
{
217+
if (double.IsNaN(this.BaseLine))
218+
{
219+
if (axis.IsLogarithmic())
220+
{
221+
var lowestPostiveY = this.ActualPoints == null ? 1 : this.ActualPoints.Select(p => p.Y).Where(y => y > 0).Min();
222+
return Math.Sqrt(lowestPostiveY);
223+
}
224+
return 0;
225+
}
226+
227+
return this.BaseLine;
199228
}
200229

201230
/// <summary>
@@ -265,7 +294,7 @@ private void RenderBars(IRenderContext rc, List<DataPoint> actualPoints)
265294
}
266295

267296
var screenPoint = this.Transform(actualPoint) - widthVector;
268-
var basePoint = this.Transform(new DataPoint(actualPoint.X, 0)) + widthVector;
297+
var basePoint = this.Transform(new DataPoint(actualPoint.X, this.GetBasePointY())) + widthVector;
269298
var rectangle = new OxyRect(basePoint, screenPoint);
270299
this.rectangles.Add(rectangle);
271300
this.rectanglesPointIndexes.Add(pointIndex);
@@ -281,6 +310,13 @@ private void RenderBars(IRenderContext rc, List<DataPoint> actualPoints)
281310
}
282311
}
283312

313+
private double GetBasePointY()
314+
{
315+
if (this.YAxis.IsLogarithmic())
316+
return LogarithmicAxis.LowestValidRoundtripValue;
317+
return 0;
318+
}
319+
284320
/// <summary>
285321
/// Computes the bars width.
286322
/// </summary>

0 commit comments

Comments
 (0)