Polar plot: add option to allow clockwise orientation
Polar plots are typically counter-clockwise, but not always.
I have found that this can be accomplished simply by reversing the Y-axis.
Test based on https://scottplot.net/cookbook/5.0/PolarAxis/PolarQuickStart/
After adding formsPlot1.Plot.Axes.InvertY();, it looks like the following:
Inverting the Y-axis only works when when you leave the rotation at zero. Once you rotate the axis coordinates get placed opposite where they should be.
To my knowledge the only real difference between a clockwise or counter-clockwise polar chart is the direction of the angle. So I just modified the classes to reverse the angle if the axis is clockwise. This works great for me.
Updated PolarAxis.cs by adding an IsClockwise property and changing the base SetSpokes and GetCoordinates methods.
public bool IsClockwise { get; set; } = false;
public void SetSpokes(Angle[] angles, double length, string[] labels)
{
if (angles.Length != labels.Length)
throw new ArgumentException($"{nameof(angles)} and {nameof(labels)} must have equal length");
Spokes.Clear();
for (int i = 0; i < angles.Length; i++)
{
Angle angle = angles[i];
if (IsClockwise)
{
// reverse the angle when clockwise.
angle = -angle;
}
PolarAxisSpoke spoke = new(angle, length) { LabelText = labels[i] };
Spokes.Add(spoke);
}
}
public Coordinates GetCoordinates(PolarCoordinates point)
{
if (IsClockwise)
{
// subtract the rotation and reverse the angle when clockwise.
return point.WithAngle(-(point.Angle - Rotation)).ToCartesian();
}
return point.WithAngle(point.Angle + Rotation).ToCartesian();
}
Updated Ellipse.cs to have an IsClockwise property and changed the RenderAnnulus and RenderEllipse methods. I don't know for certain if this works in every situation. I have not used an annulus, but the ellipse sector is working.
public bool IsClockwise { get; set; } = false;
protected virtual void RenderAnnulus(RenderPack rp, SKPaint paint, PixelRect rect)
{
float innerRx = Axes.GetPixelX(InnerRadiusX) - Axes.GetPixelX(0);
float innerRy = Axes.GetPixelY(InnerRadiusY) - Axes.GetPixelY(0);
PixelRect innerRect = new(-innerRx, innerRx, innerRy, -innerRy);
if (SweepAngle.Normalized.Degrees == 0 ||
Math.Abs(SweepAngle.Degrees) >= 360)
{
Drawing.FillEllipticalAnnulus(rp.Canvas, paint, FillStyle, rect, innerRect);
Drawing.DrawEllipticalAnnulus(rp.Canvas, paint, LineStyle, rect, innerRect);
}
else
{
double startAngle = StartAngle.Normalized.Degrees;
double sweepAngle = SweepAngle.Degrees;
if (IsClockwise)
{
// reveres angles when clockwise.
startAngle = -startAngle;
sweepAngle = -sweepAngle;
}
Drawing.FillAnnularSector(rp.Canvas, paint, FillStyle, rect, innerRect,
(float)-startAngle, (float)-sweepAngle);
Drawing.DrawAnnularSector(rp.Canvas, paint, LineStyle, rect, innerRect,
(float)-startAngle, (float)-sweepAngle);
}
}
protected virtual void RenderEllipse(RenderPack rp, SKPaint paint, PixelRect rect)
{
if (SweepAngle.Normalized.Degrees == 0 ||
Math.Abs(SweepAngle.Degrees) >= 360)
{
Drawing.FillOval(rp.Canvas, paint, FillStyle, rect);
Drawing.DrawOval(rp.Canvas, paint, LineStyle, rect);
}
else
{
double startAngle = StartAngle.Normalized.Degrees;
double sweepAngle = SweepAngle.Degrees;
if (IsClockwise)
{
// reveres angles when clockwise.
startAngle = -startAngle;
sweepAngle = -sweepAngle;
}
if (IsSector)
{
Drawing.FillSector(rp.Canvas, paint, FillStyle, rect,
(float)-startAngle, (float)-sweepAngle);
Drawing.DrawSector(rp.Canvas, paint, LineStyle, rect,
(float)-startAngle, (float)-sweepAngle);
}
else
{
Drawing.DrawEllipticalArc(rp.Canvas, paint, LineStyle, rect,
(float)-startAngle, (float)-sweepAngle);
}
}
}
Hi @CoderPM2011, thanks for your feedback and PR #5028! Although inverting axes is one strategy that could be used, I'm interested in a simpler solution that doesn't require manipulating state outside the polar axis itself.
@mattwelch2000, your proposed solution is very interesting! Adding an IsClockwise property is more consistent with my preferred implementation strategy.
I'll close #5028 and create a new PR integrating ideas from both of your suggestions, then merge it tagging both of you co-authors. I think I have everything I need here to get this done quickly, but I'll follow-up here if I hit any snags along the way...
By the way, I literally hit this exact problem myself yesterday (needing to show a clockwise polar plot) and this is the silly way I did it: custom spoke labels and negative angles
var polar = formsPlot2.Plot.Add.PolarAxis();
polar.Rotation = Angle.FromDegrees(90); // so North (degrees) is up
string[] spokeLabels = Enumerable.Range(0, 12)
.Select(x => x * 30) // 30 degree ticks
.Select(x => x.ToString())
.ToArray();
polar.SetSpokes(spokeLabels, maxRadius * 1.1, clockwise: true); // interestingly this already supported clockwise
Coordinates[] polarPoints = Enumerable.Range(0, angles.Count)
.Select(x => polar.GetCoordinates(strains[x], -angles[x])) // negative angle is how I simulated clockwise
.ToArray();
... an interesting situation is that Plot.Add.Polar() calls SetSpokes() which lays them out counter-clockwise, so setting IsClockwise later has a mismatch between spokes and data points. I'll probably modify that property to automatically reverse angle of existing spokes if it's changed after spokes are added 🤔
...interestingly, the same issue is present with setting Rotation after the fact. I'll try to fix that at the same time...
so setting
IsClockwiselater has a mismatch between spokes and data points
My bad, this is not true. CW/CCW and rotational corrections of spokes ~~are~~ can be applied at render time.
I just merged #5046 which I think solves this issue... but I'd appreciate any feedback you may have. Thanks again!
| Default | Rotation=-90 and Clockwise=true |
|---|---|