Skip to content

Repeat render if changes are made in invoked events#3952

Merged
swharden merged 9 commits intoScottPlot:mainfrom
BrianAtZetica:NewRenderAfterEvents
Jun 24, 2024
Merged

Repeat render if changes are made in invoked events#3952
swharden merged 9 commits intoScottPlot:mainfrom
BrianAtZetica:NewRenderAfterEvents

Conversation

@BrianAtZetica
Copy link
Contributor

I had a requirement to synchronise two vertical axes (representing depth and elevation), but:

  • Their values would be offset by a user specificed value (i.e. the ground surface elevation)
  • The depth axis (only) is snapping to tick intervals (using the SnapToTicksY axis rule).

I was finding that if the user changed either vertical axis the other axis would update using the invoked AxisLimitsChanged event, but the render was already in progress, so the change was not seen until the next render occurred. This meant the two axes did not have the correct synchronisation.

The solution implemented in this PR is to test for changes made to axis limits by invoked events, and set a flag in the RenderManager to indicate that a repeat render is required.

However, my scenario was even more complex due to the depth axis snapping to tick values. When the user changed (zoomed/panned) the elevation axis, my AxisLimitsChanged event would change the depth axis. But on the repeat render, the depth axis then snaps to tick intervals and needs to invoke the AxisLimtisChanged event a second time. With the Depth axis now changed, I update the elevation axis, and I need to render a 3rd time to see this change.

I've set the rendering to repeat as long as it detects an axis change during invoked events, but have put a maximum limit of 5 repeat renders (to avoid an endless loop) - this could be made into a parameter in the future perhaps.

…s (events are not invoked on second render).
…AxisRules make a change that the user wants to take action on)
E.g. something changes during an invoked event, then an axis rule makes a change, and then soemthing changes again during another invoked event.
Have put a counter to prevent endless loop of renders.
@bforl
Copy link
Contributor

bforl commented Jun 17, 2024

It seems rather wasteful to render things 3 times to get the required result.

I wonder if there is a better way of achieving the desired results by having the axes communicate without each other?

Maybe you could upload a github code example to demonstrate the issue?

(short-circuit logical operators and making a private set)
@BrianAtZetica
Copy link
Contributor Author

It seems rather wasteful to render things 3 times to get the required result. I wonder if there is a better way of achieving the desired results by having the axes communicate without each other?

I agree! I wasn't able to work out anything more sophistocated. This solved a problem for me, but if a better solution can be found that would be great.

Maybe you could upload a github code example to demonstrate the issue?

Sure. Code smaple below. I demonstrated the original problem in Discord, but this more complete sample code captures the full behaviour described in the pull request:

Demonstration of the code (with the fix in place) follows:

If you change the left axis

  • fist render (render 7 in screenshot) occurrs because left axis changed.
  • left axis snaps to tick intervals during render (render 7)
  • right axis is set in an event because the left axis changed (still render 7)
  • left axis is set in an event because the right axis changed (render 8) - but new range is same as previous range so no more events fire.

image

If you change the right axis:

  • first render (render 49 in screenshot) occurs because right axis changed.
  • left is set in an event because right axis changed (render 49)
  • left axis snaps to tick intervals during second render (render 50)
  • right axis is set in an event because the left axis changed (still render 50)
  • left axis is set in an event because the right axis changed (render 51) - but new range is same as previous range so no more events fire.

image


using ScottPlot;
using System.Diagnostics;
using System.Windows;

namespace Sandbox.WPF;

public partial class MainWindow : Window
{
    private int limit = 1;
    private double rightAxisOffset = 0.5;
    public MainWindow()
    {
        InitializeComponent();

        // plot sine wave on left axis with YAxis snapping to tick intervals
        var sig1 = WpfPlot1.Plot.Add.Signal(Generate.Sin());
        var snapLeftTicks = new ScottPlot.AxisRules.SnapToTicksY(WpfPlot1.Plot.Axes.Left);
        WpfPlot1.Plot.Axes.Rules.Add(snapLeftTicks);

        // Plot cosine on right axis without snapping to tick intervals
        var sig2 = WpfPlot1.Plot.Add.Signal(Generate.Cos());
        sig2.Axes.YAxis = WpfPlot1.Plot.Axes.Right;

        // use an axis changed event to ensure axes are correctly offset
        WpfPlot1.Plot.RenderManager.AxisLimitsChanged += AxesChanged;

        WpfPlot1.Plot.YLabel("YAxis Left (snaps to ticks)");
        WpfPlot1.Plot.Axes.Right.Label.Text = $"YAxis Right ({rightAxisOffset} units offset from Left Axis)";
        
    }

    private void AxesChanged(object? sender, RenderDetails e)
    {
        bool rightRangeChanged = false;
        bool leftRangeChanged = false;

        if (e.Count > 1)
        {
            var newRightRange = e.AxisLimitsByAxis[WpfPlot1.Plot.Axes.Right];
            var oldRightRange = e.PreviousAxisLimitsByAxis[WpfPlot1.Plot.Axes.Right];
            var newLeftRange = e.AxisLimitsByAxis[WpfPlot1.Plot.Axes.Left];
            var oldLeftRange = e.PreviousAxisLimitsByAxis[WpfPlot1.Plot.Axes.Left];

            rightRangeChanged = oldRightRange != newRightRange;
            leftRangeChanged = oldLeftRange != newLeftRange;

            if (rightRangeChanged)
            {
                AxisLimits leftAxisLimits = new AxisLimits(left: WpfPlot1.Plot.Axes.Bottom.Min, right: WpfPlot1.Plot.Axes.Bottom.Max, top: newRightRange.Max - rightAxisOffset, bottom: newRightRange.Min - rightAxisOffset);
                WpfPlot1.Plot.Axes.SetLimitsY(leftAxisLimits, WpfPlot1.Plot.Axes.Left);
                Debug.WriteLine($"Left Axis set to: {(newRightRange.Min - rightAxisOffset).ToString()} -  {(newRightRange.Max - rightAxisOffset).ToString()} during render count {e.Count.ToString()}");
            }
            else if (leftRangeChanged)
            {
                AxisLimits rightAxisLimits = new AxisLimits(left: WpfPlot1.Plot.Axes.Bottom.Min, right: WpfPlot1.Plot.Axes.Bottom.Max, top: newLeftRange.Max + rightAxisOffset, bottom: newLeftRange.Min + rightAxisOffset);
                WpfPlot1.Plot.Axes.SetLimitsY(rightAxisLimits, WpfPlot1.Plot.Axes.Right);
                Debug.WriteLine($"Right Axis set to: {(newLeftRange.Min + rightAxisOffset).ToString()} -  {(newLeftRange.Max + rightAxisOffset).ToString()} during render count {e.Count.ToString()}");
            }

        }
    }

}

@BrianAtZetica
Copy link
Contributor Author

For completeness, below demonstrates the behaviour without this PR:

If you change the left axis

  • fist render (render 50 in screenshot) occurrs because left axis changed.
  • Left axis snaps to tick intervals during render (render 50)
  • Right axis is set in an event because the left axis changed (still render 50)
  • UI is not updated with newly set Right Axis range.

image

If you change the right axis

  • fist render (render 51 in screenshot) occurrs because right axis changed.
  • Left axis is set in an event because the right axis changed (still render 51)
  • Left axis snaps to tick intervals during render (render 50)
  • Right axis is no longer offset by 0.5 units as user intended.

image

@bforl
Copy link
Contributor

bforl commented Jun 19, 2024

Hmm, I see. Are you getting the artifacts of it jumping around a lot as you pan?

I'm not sure of a good solution, maybe someone else will come along with an idea.

@BrianAtZetica
Copy link
Contributor Author

Hmm, I see. Are you getting the artifacts of it jumping around a lot as you pan?

I'm not sure of a good solution, maybe someone else will come along with an idea.

No, panning is nice and smooth. I used to get jumping with SnapToTicks rules, but I resolved that in a recent PR.

Thanks for review and consideration. Happy to let this sit wiating further comments or thoughts from others. I can use in my fork in the meantime.

@swharden
Copy link
Member

Hi @BrianAtZetica, thanks for this PR and @bforlgreen for your input along the way!

The WPF demo you shared was really useful for testing this out. I consolidated all of the proposed logic in the render manager class and did it in such a way that does not add any properties. What do you think about this implementation? If you think it looks good I'll merge it and it will be in the next release! 🚀

@swharden
Copy link
Member

What do you think about this implementation? If you think it looks good I'll merge it

Actually, I'll merge this now and any suggestions can be made in new PRs. Thanks again!

@swharden swharden merged commit f2e370c into ScottPlot:main Jun 24, 2024
@BrianAtZetica
Copy link
Contributor Author

What do you think about this implementation? If you think it looks good I'll merge it

Actually, I'll merge this now and any suggestions can be made in new PRs. Thanks again!

Looks great to me! Thanks so much.

@BrianAtZetica BrianAtZetica deleted the NewRenderAfterEvents branch June 25, 2024 00:52
KroMignon added a commit to KroMignon/ScottPlot that referenced this pull request Jun 26, 2024
* upstream/main:
  Fix interaction of axis panels when scale factor is more than 1 (ScottPlot#3994)
  Added ResetMinAndMaxValues() to DataLoggerSource.cs (ScottPlot#3993)
  CoordinateLine: add constructor overloads (ScottPlot#3987)
  Colormap.GetColors() (ScottPlot#3983)
  Added a constructor overload that accepts List<Coordinates> (ScottPlot#3982)
  Signal: improve support for IReadOnlyList<T> (ScottPlot#3978)
  Axes: improve sharpness of axis lines, tick marks, and grid lines (ScottPlot#3976)
  adding console write file name function (ScottPlot#3965)
  Color.ToColor()
  Sandbox: extend minimal API
  Sandbox: Create .NET API project
  SVG XML Updates (ScottPlot#3957)
  Repeat render if changes are made in invoked events (ScottPlot#3952)
  CI: autoformat
  Experimental DataLogger2 using a `CircularBuffer<T>` (ScottPlot#3946)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants