ScottPlot icon indicating copy to clipboard operation
ScottPlot copied to clipboard

Polar plot: add option to allow clockwise orientation

Open swharden opened this issue 9 months ago • 2 comments

Polar plots are typically counter-clockwise, but not always.

Image

Image

Image

Image

swharden avatar Mar 30 '25 20:03 swharden

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: Image

CoderPM2011 avatar Apr 03 '25 05:04 CoderPM2011

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);
        }
    }
}

mattwelch2000 avatar May 01 '25 17:05 mattwelch2000

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...

swharden avatar Aug 17 '25 18:08 swharden

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();

swharden avatar Aug 17 '25 18:08 swharden

... 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 🤔

swharden avatar Aug 17 '25 18:08 swharden

...interestingly, the same issue is present with setting Rotation after the fact. I'll try to fix that at the same time...

swharden avatar Aug 17 '25 18:08 swharden

so setting IsClockwise later 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.

swharden avatar Aug 17 '25 19:08 swharden

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
image image

swharden avatar Aug 17 '25 19:08 swharden