Sorry, something went wrong.
|
Got basic timeseries with linestack working. I've also got some code snippets for interpolating to display heatmap with non-uniformly sampled timeseries data. I should be able to have this fully working with time series very soon :D Kooha-2025-12-27-03-50-45.mp4 |
Sorry, something went wrong.
|
Got heatmap to display timeseries working. It should also work with non-uniformly sampled data by interpolating, need to test. Also need to implementing switching between heatmap and line representations, need to delete the graphic when switching. Kooha-2025-12-27-17-47-16.mp4 |
Sorry, something went wrong.
|
So timeseries can be represented with arrays of one of the following shapes (let's ignore x-axis values for now). If we have: l: number of timeseries We can have the following shapes: p: only y-values
p, 2: yz vals
l, p: l-timeseries with y values
l, p 2: l-timeseries with yz values
Extended to n-dimensional arrays (for example, trajectories projected onto principal components?). If each non-timeseries dim is $d_1, d_2, ... d_n$, then the above becomes: $$\begin{align*} d_1, ... d_n, p\\\ d_1, ... d_n, p, 2\\\ d_1, ... d_n, l, p\\\ d_1, ... d_n, l, p, 2\\\ \end{align*}$$I don't think we can auto-detect if l is present or not and the user should specify, something like: multi_timeseries: bool = True Scatters can be similar to some cases of nd-lines 🤔 , but x values would be directly specified and the current index is parametric (example with time indicating position in a low dim space). This would actually be true for lines as well if representing trajectories. So for nd-line maybe we have two versions, parametric (y and z are not functions of x, but x, y, z are a function of some other dim) and non-parametric (simple timeseries lines where y and z are functions of x). |
Sorry, something went wrong.
|
I made a more generalist NDPositions which can map data that is: [s1, s2, ... sn, l, p, 2 | 3] where: It can map arrays of these dims to a line, line collection, line stack, scatter, or list of scatters (similar to multi-line). I think this is a much more elegant way to deal with things, and NDTimeSeries is not necessary. The user can provide a slider mapping (to map from reference units to array index) for the p dimension which is the same as the "x-axis" for time series data! Example if we have data that is [n_timepoints, 2], and the x-positions here (in the last dim) are in seconds. The NDWidget reference units for the slider can also be in seconds, and we can provide a mapping function that goes from the slider reference units to the n_timepoints index. I think we can also use this for heatmaps and interpolation. Use the reference units to determine a uniform x-range for the current display window, and we can interpolate using [n_timepoints, 2] data. EDIT: I think that the NDPositions will also work for PolygonGraphic ! Can think about meshes in general later. |
Sorry, something went wrong.
|
For positions graphics, I should actually do [n_datapoints, n_lines, 2 | 3] so everything before the last 1 or 2 dims is always sliders. |
Sorry, something went wrong.
|
Some more ideas: Allow any 2-3 dims to be used as the graphic dimensions and specify the slider dims. This would also allow using named dims (such as those used in xarray). add_nd_<scatter|lines|heatmap>(
data=<array>, # array like, defaults to last 2-3 dims are graphical, first (n - 2 | 3) dims are sliders and in order
x=<int | str | None> # optionally specify x graphical dim using the integer dim index or dim name
y = ... # same for other graphical dims
slider_dims=<None | tuple[int | str]> # specify slider dims, None is auto first (n - 2 | 3), or specify a tuple of slider dims as int or str, examples: (0, 1, 2, 3), ("time", "plane")
... # other args
)
We interpret the given order of the slider_dims passed as $$s_1, s_2, ... s_n$$, regardless of their order within the actual array. This will make it clear to users which dims they are syncing when they use the sliders. EDIT: A limitation of the above is that a user can't collapse multiple "graphic/display dimensions" into "final graphic/display dimensions" if they're hard-coded this like. So something like: add_nd_<...>(
data=<array>,
display_dims=<tuple[int | str]>, # specify display dims in [x, y, z] order, OR display dims that collapse to xyz after the finalizer function
...
)
An example for images would be collapsing [z, m, n] to display a projection over z as slider dims are moved. But we could also have > 3 dims that are used, and then collapsed to 3 or fewer dims for the "final graphic/display dims". |
Sorry, something went wrong.
|
We can use LineCollections to display multiple lines, like behavior tracks of keypoints, with shape [n_keypoints, n_timepoints, 2]. This works well with NDPositions and display windows. I was thinking of what's the best way to show a scatter for each keypoint, and I think I should make a ScatterCollection that behaves like a LineCollection so the same array with the same shape can be given, the only difference is the graphical representation would be a scatter instead of lines. For typical behavior keypoint viz, the display_window would usually be just 1, but it can be greater than 1 for any viz that needs to show a window of scatter points. |
Sorry, something went wrong.
|
ok I think stuff is working ndpositions-2026-01-29_22.55.54.mp4 |
Sorry, something went wrong.
|
I think I need to make a PolygonCollection too 🤔 . Would be very similar to the ScatterCollection. |
Sorry, something went wrong.
Sorry, something went wrong.
|
A set of imgui UIs that allow controlling some aspects of the "nd graphics" could be useful, such as:
|
Sorry, something went wrong.
Stuff I should finish before implementing the orchestrator:
Things that make me "uncomfortable" that I need to settle:dim shapesDim shape for nd-positions is [s1, s2, ... sn, n, p, 2 | 3] where: n: number of lines, scatters, or heatmap rows (optional, can be 1) p would be a slider dim, but there's this n dim that's in between p and all the other slider dims. We could instead use a shape of [s1, ... sn, p, n, 2 | 3] but that feels weird and we'd have to do a transpose to get the array for the graphical representation, i.e. [p, n, 2 | 3] -> [n, p, 2 | 3] is required for the graphical representation. Do we just document this well, that the n dimension is not a slider dim but p is? When using nd-positions data in conjunction with nd-image data, we'd have something like this: # nd-positions
[s1, ... sn, n, p, 2 | 3]
# nd-image
[s1, ... sn, p, r, c, 1 | 3 | 4]
Where r: rows, cols: columns. 1 | 3 | 4 denotes grayscale or RGB(A). The p dimension in the nd-image array above would correspond to the p in the nd-positions. nd-images don't have any n dimension, it doesn't make sense there. |
Sorry, something went wrong.
|
Working on "implement mapping from a slider reference index with units (such as time) to array index.", which requires proper implementation of slider_dims and n_slider_dims properties. Need to figure out how to properly separate the p n_datapoints dimension from other slider dims, and also apply the window funcs on the p dim. Will do tomorrow. |
Sorry, something went wrong.
|
Window funcs working for p (n_datapoints) dim and all graphical representations ndp_windows-2026-02-01_03.39.52.mp4 |
Sorry, something went wrong.
|
ok no longer need xarray! 🥳 |
Sorry, something went wrong.
|
I'm also going to look into making the entire get data -> window funcs -> spatial funcs -> set on graphic async, probably just by threading. So we shouldn't block the main render thread while wait for this full pipeline. And need to have a way to mark that all new graphic data has been set and we are ready to render the next frame. Few more things:
|
Sorry, something went wrong.
|
Idea: When the NDProcessor is wrapping async data that uses shared memory we could use that shared memory directly as the graphic buffer. This has some limitations, ex: cannot tile images. Needs some thought to implement properly. EDIT: see #1042 |
Sorry, something went wrong.
There was a problem hiding this comment.
Add to the docstring whether this works or not
Sorry, something went wrong.
There was a problem hiding this comment.
uniform_size -> size_mode
Sorry, something went wrong.
| ref_ranges=ref, | ||
| size=(700, 560) | ||
| ) | ||
| ndw2 = fpl.NDWidget( |
There was a problem hiding this comment.
add note here: sharing ref idxs between ndwidgets syncs them
Sorry, something went wrong.
|
|
||
| nd_lines = ndw[0, 0].add_nd_timeseries( | ||
| data, | ||
| ("freq", "ampl", "n_lines", "angle", "d"), |
There was a problem hiding this comment.
list with kwarg
Sorry, something went wrong.
|
why is the throttle on RangeContinuous defaulted to 200ms? That makes scrubbing through T noticably worse - I can set the throttle myself, just curious what the benefit is |
Sorry, something went wrong.
|
why is the throttle on RangeContinuous defaulted to 200ms? That makes scrubbing through T noticably worse - I can set the throttle myself, just curious what the benefit is These aren't final values, high throttle is necessary for video decoding on cpu. I'll probably set a 100ms throttle when a video is added, maybe 50ms or lower throttle as default. Something to play with. Also throttling is only for the imgui slider. All other methods of updating the indices are not time-throttled. If you click the "play" button it will actually display every single frame, similarly if you use the linear selector or pan/zoom on a timeseries those updates are not throttled and they are immediate; it relies on the IO being fast and reliable in these cases, else it will block rendering if the IO is slow/inefficient/bad but that's on the IO not on NDWidget. |
Sorry, something went wrong.
Internals
RangeContinuous
Just a dataclass that stores the start, stop, and step of a reference range. A reference range is usually in scientific units, such as seconds/ms for time. Depth is another example of a reference range. A reference range just specifies the min and max of a dimension that will be used as a slider dimension. The imgui sliders use the reference range to determine the min, max, and step size of the UI elements (sliders, step-one button, etc.).
RangeDiscrete is mostly a placeholder for now, not fully implemented yet, waiting for a proper usecase. Genes might be an example? Session index for multi-session data could be another potential usecase.
Users must define a reference range for every dimension they want to use as a slider dimension.
TODO: Would be useful to have an automatic reference range that's generated for a slider dimension when it's not specified. For example, sometimes you just want to dump a bunch of calcium arrays and look at them, and they're already in the same time-space. Could have a simple auto-reference range mode that just sets the min and max for eacher slider dim based on the current arrays in the NDWidget.
ReferenceIndex
This manages the reference index for an NDWidget. It's the only place where indices should be set.
TODO: pushing/popping reference ranges.
NDProcessor
This manages n-dimensional data of any type. Subclasses must implement a get() method which takes a dict mapping dim names -> index for that dim. If specified, window functions are applied on slider dimensions. _get_slider_dims_indexer takes the indices dict and outputs a dict of dim_names -> slice. _apply_window_functions() uses these slices and applies the window functions. The final output after window functions has the same number of dims as the data, but with 1 element in any dimensions that were sliced (i.e. all slider dims, spatial dims remain untouched). In the end, the get() method must return an array that can be directly mapped to graphic data.
Key properties in an NDProcessor:
data - usually an xarray, but subclasses can manage data that is of any type. The NDProcessor NDPP_Pandas for positional data stored in a pandas dataframe is an example of this.
shape, ndim, dims, spatial_dims, slider_dims - self explanatory, dims are always named
window_funcs - window functions, dict {dim_name: (func, window_size)}. A window function must take axis and keep_dims as kwargs.
slider_dim_transforms - dict of functions for each slider dim that maps a reference range value to the local array index value. If a user doesn't provide a transform for a dim (for example, they just want to look at many arrays which are already in the same time-space, such as calcium movies) the it just uses an identify mapping.
NDPositionsProcessor and NDImageProcessor have logic specific to those types of data.
NDPositionsProcessor has an additional display_window properties which acts on the datapoints dimension. The datapoints dim is also both a slider dim and spatial dim, but it is excluded for window_funcs. datapoints_window_func is a separate property on NDPositionsProcessor that applies window funcs for that dimension since it's a bit different from other slider dims.
NDGraphic
A n-dimensional graphical representation. It has an NDProcessor and fpl.Graphic that it manages. ReferenceIndex sets the indices on an NDGraphic, which then call get() on its NDProcessor to retrieve the new graphic data, and then sets it on the graphic. It has property aliases for the various processor properties, so that users can set window funcs, slider dim transforms, etc.
NDPositions and NDImage are two NDGraphic subclasses that have specific code for positional and image specific representations.
NDPositions also manages a linear selector for timeseries representations, and allows swapping between scatter, line stack, line collection, and heatmap representations. It also manages an "auto x range" mode for timeseries representations
NDImage also manages the HistogramLUTTool and swapping between ImageGraphic and ImageVolumeGraphic. It also sets the camera and controller when these are swapped.
NDWSubplot
The main entry point for users to add NDGraphics to a subplot in an NDWidget. It mainly has add_nd<...> methods for users to provide data and kwargs to add NDGraphics.
Need to cleanup this subclass so the args and kwargs for each add method are properly specified and documented.
NDWidget
Just a very simple class that has a fpl.Figure, an instance of ReferenceIndex, and the NDWSubsplots.
Very simplified diagram:
s1, ... sn are slider dims, d1, d2, ... are spatial dims.
User API
The user mainly interacts via the NDW subplots and NDGraphics. For example with images:
Positional data:
Main things left todo
implements #951