Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SetNextPlotLimitX/Y in case of drag and drop? #165

Closed
mvphilip opened this issue Jan 15, 2021 · 12 comments
Closed

SetNextPlotLimitX/Y in case of drag and drop? #165

mvphilip opened this issue Jan 15, 2021 · 12 comments
Labels
enhancement New feature or request

Comments

@mvphilip
Copy link

Hello, Is there a recommended way to set plot limits if using the drag and drop demo? As the data is not available before a BeginPlot it's not possible for me to programmatically set limits. There is an assert in the code so I suppose this use case is not supported?

Thanks and what a great tool you've made here!

-mp

@epezent
Copy link
Owner

epezent commented Jan 15, 2021

@mvphilip, if I understand correctly, you want the plot to auto-fit (or allow for a manual refit) when new data is dropped onto it, but can't because new drops occur between BeginPlot and EndPlot. Correct?

Because ImPlot doesn't cache plot data and thus renders everything on the fly instead of at the at the end, it's not possible to change a plot's limits mid-plot (i.e. between BeginPlot and EndPlot), hence the assert. The best solution I can offer you right now is to handle this yourself across two frames using a flag:

static bool need_refit = false;
if (need_refit) {
    ImPlot::FitNextPlotAxes();
    need_reft = false;
}
if (ImPlot::BeginPlot("MyPlot")) {
    // render shown data
    for (int i = 0; i < data.size(); ++i) {
        if (data[i].show) 
            ImPlot::PlotLine(data[i].label, data[i].x, data[i].y, data[i].size());        
    }
    // check for incoming drops
    if (ImGui::BeginDragDropTarget()) {
        if (const ImGuiPayload* payload = ImGui::AcceptDragDropPayload("DND_PLOT")) {
            int i = *(int*)payload->Data;
            data[i].show = true;
            need_refit = true;
        }
        ImGui::EndDragDropTarget();
    }
    ImPlot::EndPlot();
}

Adapt this to whatever data structures you are using. Feel free to replace FitNextPlotAxes with SetNextPlotLimits if you need a more specific fit policy.

Hope this helps.

@epezent
Copy link
Owner

epezent commented Jan 15, 2021

As a note to my future self, there might be an easy path forward to allowing fits and setting axes limits mid-plot. Let's keep this issue open until I have more time to investigate.

@mvphilip
Copy link
Author

Thanks a lot for the suggestion!

-mp

@epezent epezent added the enhancement New feature or request label Feb 26, 2021
@yamen
Copy link

yamen commented Mar 11, 2021

Just highlighting that I would greatly value ability to set limits after begin plot - I usually want to respond to a scroll or pan event by setting one of the axes manually. Example lock y axis and manually set its range when zooming or panning.

Right now this is only possible on the next frame which causes some jumpiness, and similar use cases mean a lot of variables just to capture a request that needs to be processed in next frame (eg refit requested).

FWIW, hooking into the zoom/pan behaviour is probably good enough, not sure what the best API for that is.

@epezent
Copy link
Owner

epezent commented Mar 11, 2021

@yamen thanks for the feedback. I will think carefully about how we might achieve this. In the meantime, could you thoroughly explain what it is you are trying to do? As you suggested with input hooks, there might be an easier, more straightforward way to satisfy your needs.

Second, I want to take the opportunity to layout the sequence of input/rendering events in ImPlot. This may help you think about the problem better and maybe suggest where we might inject the functionality you need.

  • ImPlot::SetNextXXX
    • cache user requests to the NextPlotData struct
  • ImPlot::BeginPlot
    • check for flag changes and cache (plot and axes).
    • ImGui::BeginChild to capture scroll events from the parent window.
    • consume NextPlotData, e.g. set axis limits from SetNextPlotLimits
    • apply axis constraints (e.g. limits, equal, etc.)
    • compute padding and bounding boxes:
      • given user defined ImVec2 size of the plot widget frame ...
      • compute top/bottom padding given presence of title and/or x-axis labels
      • calculate plot height given top/bottom padding
      • given plot height and current Y range(s), create y-tick labels and compute left/right padding
      • calculate plot width given left/right padding
      • given plot width and current X range, create x-tick labels
    • determine mouse hover states for plot and axes
    • process drag input and update axis limits accordingly
    • process scroll input and update axis limits accordingly
    • box selection / query rect
    • process double-click events and set FitThisFrame etc. flags
    • given axes limits, update position-to-pixel transformation cache
    • render background elements
      • render grid
      • render title
      • render axis labels
      • render tick labels
  • ImPlot::PlotXXX
    • if FitThisFrame is true, iterate user data to find extents and cache them if larger than the currently cached fit extents
    • given current transformation cache, transform user data to pixel positions
    • generate render triangles and send to ImGui immediately (i.e. we never cache pre-transformed plot data or triangle data)
  • ImPlot::EndPlot
    • render foreground elements
      • render tick marks
      • render annotations
      • render selection/query boxs
      • render crosshairs
      • render mouse position/inlay text
      • render legend
      • render border
    • if FitThisFrame, apply the maximum data extents to the axis limits
    • render any context menus
    • ImGui::EndChild
    • reset global state for next plot

I realize this is a lot to process, and I glossed over a bunch of details. But hopefully this will help everyone understand how ImPlot does immediate mode plotting and why it's challenging for us to make certain state changes between Begin/EndPlot. Not saying it's impossible, just that we'd have to restructure some things.

Furthermore, I wouldn't say this order of execution is perfect either. As I type this out, I see some issues and areas we could improve (e.g. axis grid/ticks and plot data may be desynced one frame after input events).

@yamen
Copy link

yamen commented Mar 11, 2021

@epezent what an amazingly thorough response. Probably worth popping into the Wiki for future reference, as I think it will aid others when making suggestions or feature requests. I will explain my use case below, but from the looks of the above workflow, the "point of no return" seems to be the start of the transformation cache / rendering, which also happens to be the last step of BeginPlot - being able to update the calculated plot data right before this point might be a worthwhile advanced user hook.

The other way to view the root cause is that "fitting" is done after scrolling / panning / zooming - ie, the data is only ever processed after axis ranges are fixed. I can see why this is desirable from a pipeline point of view and as default behaviour, but the user does know about their data before its plotted and can override the defaults a little better perhaps.

As to the use case:

  • Financial time series data, large (10,000+) data set, with broad Y-range
  • Zooming and panning should typically be interpreted as being for the X-axis only
  • Y-axis behaviour is generally one of the following:
    • Auto-fit based on visible data, full plot height (both scale and min-max are automatically calculated)
    • Auto-fit based on visible data, but with manually set scale (scale is fixed and min-max of visible values are centred on Y-axis)
    • Manually managed completely (no fit behaviour)
  • Moving past the lowest X-axis value should be avoided
  • Leaving some padding on the right of the highest X-axis value is OK

What I've tried and the results, example is for a mouse scroll zoom:

  • Triggering a 'fit next plot' every frame, before BeginPlot
    • Default ImPlot scrolling behaviour updates plot limits X and Y in frame N
    • Plot zooms in both X and Y axis, potentially with undesirable limits (eg Y data not shown, X scrolling before leftmost value etc)
    • I call 'fit next plot' on frame N + 1
    • Plot limits are updated on frame N + 1
    • Plot is updated on frame N + 1 with correct limits
    • Functionally works, but a jarring experience as the chart jumps between frames
  • Locking Y-plot axis (and maybe calling 'fit next plot' every frame)
    • Achieves smooth scrolling along x-axis with no jumps, Y-axis is fit to entire dataset range regardless of zoom
    • Y axis fit is now manually triggered by user (eg button or mouse middle click etc) and processed on frame after trigger (setting and clearing boolean flags)
    • X axis can still scroll before the earliest data point and so we still get jumping if we zoom out too much (locking X-axis minimum doesn't help because it's interpret as 'locked value' as opposed to 'minimum value')

This ended up being a bit of an essay. Hopefully it makes sense on parsing...

@epezent
Copy link
Owner

epezent commented Mar 11, 2021

Thank you for the equally thorough response! Let me mull over this for a few days. If your work allows, could you post some GIFs of what you have done so far?

@yamen
Copy link

yamen commented Mar 11, 2021

This is a GIF recorded at 33fps which shows the issue, the jumpiness is worse in reality. Here you can first see how trying to zoom beyond data range causes the X-axis min to first go below 0, and then to clamp back to 0 the frame after. Similarly, as we zoom into data, the Y-axis first does normal zoom behaviour, then flicks into the data range.

GIF 12-03-2021 10-37-07 AM

Worth noting that panning is actually quite smooth, because although there might be jumps they are very minor compared to mouse wheel scrolling which jumps 10%.

@epezent
Copy link
Owner

epezent commented Mar 21, 2021

#192

@epezent
Copy link
Owner

epezent commented Mar 28, 2021

@yamen , see #200. Not sure if this will meet all your needs, but at the least it should enable the behavior you were after in #165 (comment)

@epezent
Copy link
Owner

epezent commented Mar 28, 2021

I update the official candlestick demo with these flags enabled. Seems to work quite well:

rangefit_candle

@yamen
Copy link

yamen commented Mar 28, 2021 via email

@epezent epezent closed this as completed Oct 20, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

3 participants