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

Allow configuring time series plot time axis from blueprint #8050

Open
Wumpf opened this issue Nov 8, 2024 · 14 comments
Open

Allow configuring time series plot time axis from blueprint #8050

Wumpf opened this issue Nov 8, 2024 · 14 comments
Labels
🟦 blueprint The data that defines our UI 📈 plot Plots, charts, graphs, timeseries, …

Comments

@Wumpf
Copy link
Member

Wumpf commented Nov 8, 2024

Currently the time axis plot is ephemeral state that lives entirely in egui_plot. I.e. users can not set it from blueprint api and any left/right scrolling that has been manually is not stored in the blueprint state, getting lost on restart.

There is an "automatic" state that covers a lot of usecases already though: the time axis will adjust to the visible time range which determines the data that is queried. This should be the fallback provider for this property/component in this context!

Note also, that this auto-behavior doesn't always work as desired today: egui_plot zooms in on the available data rather than a defined range. This means that if your visible time range goes from e.g. 100 to 500 but your data only starts at 200, then the shown range becomes 200 to 500 when it should actually be 100 to 500

table TimeAxis (
    "attr.rerun.scope": "blueprint",
    "attr.rust.derive": "Default"
) {
    /// The range of the axis.
    ///
    /// If unset, the range well be automatically determined by the visible time range of the view.
    range: rerun.components.Range1D ("attr.rerun.component_optional", nullable, order: 2100);

    /// If enabled, the time axis range will remain locked to the specified range when zooming.
    zoom_lock: rerun.blueprint.components.LockRangeDuringZoom ("attr.rerun.component_optional", nullable, order: 2200);
}

Related to:

@Wumpf Wumpf added 🟦 blueprint The data that defines our UI 📈 plot Plots, charts, graphs, timeseries, … labels Nov 8, 2024
@jviereck
Copy link
Contributor

jviereck commented Nov 9, 2024

Are things that are stored on the blueprint timeline indexed? If so, what happens when the user moved the plot at time t=1.0 manually and then had moved the plots manually at t=2.0? As a user, I want to see the plots stay the same as I prepared them at t=1.0 when having the time goes forward (e.g. I zoom onto a special part of a timeseriesplot), but if the setup changes at t=2.0, this is not what I want.

@Wumpf
Copy link
Member Author

Wumpf commented Nov 11, 2024

Blueprint is stored in a separate store from the "regular" data store. We do use an index timeline there but it has no relationship with any of the time axis that are used in the data store.
Today, we aggressively garbage collect all old data from it, but we actually wanted to use this history implement undo/redo (which almost happend for 0.20, but this got delayed now unfortunately)

@jviereck
Copy link
Contributor

The TimeSeriesView can be configured to show the last e.g. 5 seconds of new data. This allows to update the time-axis as new data is arriving.

What is causing this time-axis update today? Would the TimeAxis.range from the proposed fbs hold the actual time-axis and then some other system would update the range on new data?

@Wumpf
Copy link
Member Author

Wumpf commented Nov 11, 2024

Right now egui_plot just auto focuses onto the queried data. It essentially just looks onto what lines/points/markers are in the plot view and snaps to that. That is until one scrolls around at which point it stores the time range in its internal state (note though that the y range == ScalarAxis is already reflected to the blueprint store).

The configuration we already have on visible time range is strictly speaking just a query range to the datastore. Granted, for most usecases this is perfectly sufficient and I even had a prototype at some point where I tied the visible time range directly to what's on screen (I even found it again, maybe some of the patterns can be salvaged!). However, we decided to keep the query separate from the presentation. I.e. a camera movement (== scrolling around in the plot) should not change the query to the store (== "visible time range"), at least not by default.

@jviereck
Copy link
Contributor

Thanks for the input @Wumpf .

then the shown range becomes 200 to 500 when it should actually be 200 to 500

There seems to be a typo somewhere here - it makes it sound the actual and desired shouldn't be the same, but it seems it is?

This should be the base default provider for this property!

What do you mean by default provider (what is a default provider)?

The configuration we already have on visible time range is strictly speaking just a query range to the datastore. [...]

Let me try to summaries things here to check I understand it:

  • There is a presented time-range, which is the range as displayed on the x-axis in the plot (independent from the available data as queried from the store)
  • There is a visible time-range, which is the range data is queries from the store (independent from the presented axis range in the view)

At the moment the presented time-range is in the most cases derived from the visible time-range in the sense that the scaling for the x-axis is set to cover all the queries data in the visible time-range. If the user moves the presented time-range manually, this automatic linkage breaks.

Looking at 1, it seems that each timeline can have a different VisibleTimeRange.

My further understanding is this issue is about adding a way to manually update the presented time-range from the blueprint. Assuming the current behavior should stay supported, this means the following cases need to be covered:

1/ visible time-range of current timeline determines and updates presented time-range
2/ visible time-range and presented time-range are out of sync

In the future there might also be the case that changes to the presented time-range should influence the visible time-range (aka moving the axis around should show all the data points in view by updating the query range):

3/ presented time-range determines and updates the visible time-range of current timeline

At the moment, if the TimeSeriesView is in state 1/ or in state 2/ is only defined implicit. Practically, I think the only two supported cases should be 1/ and 3/, where there should be an explicit button / checkbox to switch between the two of them (if 2/ should be supported, what's the usecase for it?).

Let's assume we add a new property on the TimeSeriesSpaceViewState to determine if the TimeSeriesView is in state 1/, 2/ or 3/.

In terms what needs to be implemented for this issue, here is what I think

a) Add a new table TimeAxis to keep explicitly track of the presented time-range
b) Add new property to keep track if TimeSeriesView is in case 1/, 2/ or 3/.
c) Add a UI bit to track the new property and allow users to switch between the 1/, 2/ and 3/ cases.
e) If in state 1/, add functionality to update the presented time-range in table TimeAxis.
f) Based on user interaction with plot, switch from 1/ to 2/ (similar to what happens in the moment but make the switch explicit)
g) Implement case 3/ (should be close to the implementation @Wumpf did before).

Does this work outline make sense?

@jviereck
Copy link
Contributor

Should the presented time-range be timeline specific? I think it should as otherwise when switching the timeline it will be hard to sync the different x-axis ranges.

What should happen in the cases 1/-3/ if the user zooms on the x-axis? My feeling is that in 1/ the visible time-range should be zoomed while in 2/ and 3/ the presented time-range should be zoomed.

@Wumpf
Copy link
Member Author

Wumpf commented Nov 12, 2024

There seems to be a typo somewhere here - it makes it sound the actual and desired shouldn't be the same, but it seems it is?

Correct, typoed there! Fixed it. Today we show the data range rather than what's in visible time range

What do you mean by default provider (what is a default provider)?

Technically called "fallback provider". Which is an internal mechanism to customize what value component value should be used & shown in the ui in a given context. See https://github.com/rerun-io/rerun/blob/main/crates/viewer/re_viewer_context/src/component_fallbacks.rs
We often use "default" and "fallback" interchangeably. Fixed wording in issue.

At the moment the presented time-range is in the most cases derived from the visible time-range in the sense that the scaling for the x-axis is set to cover all the queries data in the visible time-range.

well.. not quite. The problem is that egui_plot knows nothing about anything and just zooms in on supplied data.

My further understanding is this issue is about adding a way to manually update the presented time-range from the blueprint.

yes and to persist the data in the first place.

3/ presented time-range determines and updates the visible time-range of current timeline

No, the idea is that if "presented time-range" is fully independent. It just happens to source its default from visible time range.
Yes, we may think later about having it tie in the other direction but that adds a lot of complexity.

b) Add new property to keep track if TimeSeriesView is in case 1/, 2/ or 3/.

Leaving out 3/ this doesn't need an extra property. It might be helpful for you to read up first how other properties are implemented and familiarize yourself with the system of how they typically work. I wish there was more good examples to pick from, we never quite got to build this out fully (illustrated by the fact that this ticket is open :/).

c) Add a UI bit to track the new property and allow users to switch between the 1/, 2/ and 3/ cases.

Ui for properties is semi automatic (there's util functions that reduce this quite a bit), so it should be easy... if it wasn't for the multi-timeline issue you identified, details below. Again, there should be no active switching other than a reset-to-default which is already part of the ui via the "..." menu on each property. Granted, this has a stronger effect than on most, so this might be special, but I'd rather have this be consistent and then later adjust how we handle this (this isn't the only case with a more complex fallback).

Should the presented time-range be timeline specific?

This is tricky or at least annoying indeed. The datastructure I sketched is too limited for this and probably this needs to be handled akin to how visible time range is handled. However, I'd leave it out of a first draft and just have this work for any timeline. I.e. if the property is there then its values are what's being used no matter what. Having this enabled on a per timeline basis makes all aspects of this feature a lot more complex and is unlikely to bring much user value I reckon.

What should happen in the cases 1/-3/ if the user zooms on the x-axis? My feeling is that in 1/ the visible time-range should be zoomed while in 2/ and 3/ the presented time-range should be zoomed.

Since I already excluded 3/ 😉 , zooming would just directly edit the TimeAxis property. Exactly like it works with the ScalarAxis today

@jviereck
Copy link
Contributor

I feel the more messages we sent back and forth, the more unsure I get what to do.

If the user defines a TimeSeriesView like this:

rrb.TimeSeriesView(
        origin="/trig",
        # Set a custom Y axis.
        axis_y=rrb.ScalarAxis(range=(-1.0, 1.0), zoom_lock=True),
        # Configure the legend.
        plot_legend=rrb.PlotLegend(visible=False),
        # Set time different time ranges for different timelines.
        time_ranges=[
            # Sliding window depending on the time cursor for the first timeline.
            rrb.VisibleTimeRange(
                "timeline0",
                start=rrb.TimeRangeBoundary.cursor_relative(seq=-100),
                end=rrb.TimeRangeBoundary.cursor_relative(),
            ),
            # Time range from some point to the end of the timeline for the second timeline.
            rrb.VisibleTimeRange(
                "timeline1",
                start=rrb.TimeRangeBoundary.absolute(seconds=300.0),
                end=rrb.TimeRangeBoundary.infinite(),
            ),
        ],
    ),

What determines the value on the TimeAxis property especially when the TimeRangeBound is relative and new data is logged?

@Wumpf
Copy link
Member Author

Wumpf commented Nov 13, 2024

huh yeah this time zone delayed back and forth doesn't feel very effective, we're obviously at least partially talking past each other. Would you be okay with setting up a zoom (or similar) call? You can ping me on Discord at .wumpf (since that's where we have our support forum. If Discord doesn't work for you we can also find something else. Screen share quality is horrifying there anyways)
This might also make it easier for me to document stuff in the future :)


To the code example:

  • status quo today (you can ofc just try that, but as a reference point)
    • timeline0 is selected
      • no left/right panning or zooming has been done in the viewer or the panning/zooming has been reset by double clicking on the view
        • timeaxis start == max(time cursor - 100, min(time of data logged between timecursor - 100 and time cursor) )
        • timeaxis end == min(time cursor, max(time of data logged between timecursor - 100 and time cursor) )
      • left/right panning or zooming has been done in the viewer
        • the result of that user operation, independent of any changes in data
        • can go back to auto-state above by double clicking in the view
    • timeline1 is selected
      • no left/right panning or zooming has been done in the viewer or the panning/zooming has been reset by double clicking on the view
        • timeaxis start == max(t=300s, min(time of data logged between 300s and infinity) )
        • timeaxis end == max(time of data logged between 300s and infinity)
      • left/right panning or zooming has been done in the viewer
        • same behavior as on timeline0
  • where I'd like us to go
    • timeline0 is selected
      • no left/right panning or zooming has been done in the viewer or the panning/zooming has been reset by double clicking on the view
        • timeaxis start == time cursor - 100
        • timeaxis end == time cursor
      • left/right panning or zooming has been done in the viewer
        • the result of that user operation, independent of any changes in data
        • can go back to auto-state above by double clicking in the view
        • can go back to auto-state above by clicking unset in the time property just like it works on Scalar Axis today
          image
    • timeline1 is selected
      • no left/right panning or zooming has been done in the viewer or the panning/zooming has been reset by double clicking on the view
        • timeaxis start == t=300s
        • timeaxis end == max(time of data logged between 300s and infinity)
      • left/right panning or zooming has been done in the viewer
        • same behavior as on timeline0
    • actual time range is visible in the selection panel at all times under Scalar Axis

Hope I didn't mess up any of the details and this makes sense!

@jviereck
Copy link
Contributor

Hi @Wumpf , I will reach out to you via Discord.

Thanks for your outline above. I think what I called case 1/ and case 2/ correspond to the following:

no left/right panning or zooming has been done in the viewer or the panning/zooming has been reset by double clicking on the view

^--- this looks like case 1/ in my definition

left/right panning or zooming has been done in the viewer

^--- this looks like case 2/ in my definition

Is this correct?

I assume we don't want to rely on the state in the egui_plot to determine if we are in 1/ or 2/. Then I don't see how we know during rendering if the view should follow 1/ or 2/.

We could keep track if we are in 1/ or 2/ by changing table TimeAxis.range to an optional type: If the value is not set, we assume and rely on 1/, if it is set, we handle things as if we were in 2/.

@jviereck
Copy link
Contributor

jviereck commented Dec 2, 2024

Quoting what @Wumpf wrote before:

The configuration we already have on visible time range is strictly speaking just a query range to the datastore. Granted, for most usecases this is perfectly sufficient and I even had a prototype at some point where I tied the visible time range directly to what's on screen (I even found it again, maybe some of the patterns can be salvaged!). However, we decided to keep the query separate from the presentation. I.e. a camera movement (== scrolling around in the plot) should not change the query to the store (== "visible time range"), at least not by default

Keeping the viewed data range and queried data range in sync is what I would expect should happen by default. What is a use case where the queried and viewed data range should not match?

@ProfFan
Copy link

ProfFan commented Dec 2, 2024

In my application I have several non-synced local time frames that I need to view, so I need to set a different X (time) for each time series plot. However currently I can only set 1 global time and this corrupts all plots that does not use the global time.

@jviereck
Copy link
Contributor

jviereck commented Dec 2, 2024

However currently I can only set 1 global time and this corrupts all plots that does not use the global time.

What do you mean you can set only 1 global time?

@ProfFan
Copy link

ProfFan commented Dec 3, 2024

@jviereck All time series plots use the "global time" (set in the dropdown on top of the timeline) as the X axis. However I want to use a different time as the X axis

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
🟦 blueprint The data that defines our UI 📈 plot Plots, charts, graphs, timeseries, …
Projects
None yet
Development

No branches or pull requests

3 participants