Conversation
|
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 |
|
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 |
|
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: Extended to n-dimensional arrays (for example, trajectories projected onto principal components?). If each non-timeseries dim is I don't think we can auto-detect if
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). |
|
I made a more generalist
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 Example if we have data that is 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 EDIT: I think that the |
|
For positions graphics, I should actually do |
|
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). We interpret the given order of the 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: An example for images would be collapsing |
|
We can use I was thinking of what's the best way to show a scatter for each keypoint, and I think I should make a |
|
ok I think stuff is working ndpositions-2026-01-29_22.55.54.mp4 |
|
I think I need to make a |
|
A set of imgui UIs that allow controlling some aspects of the "nd graphics" could be useful, such as:
|
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
Do we just document this well, that the When using nd-positions data in conjunction with nd-image data, we'd have something like this: Where The |
|
Working on "implement mapping from a slider reference index with units (such as time) to array index.", which requires proper implementation of |
|
Window funcs working for ndp_windows-2026-02-01_03.39.52.mp4 |
|
ok got the behavior viz working again, now with named dims. @apasarkar, @FlynnOConnell you will have to adjust your lazy array base classes slightly to work with xarray: You need to add these methods, they don't actually have to be implemented def __array__(self, dtype=None, copy=None):
if copy:
return copy(self)
return self
def __array_ufunc__(self, ufunc, method, *inputs, **kwargs):
raise NotImplementedError
def __array_function__(self, func, types, *args, **kwargs):
raise NotImplementedErrorYou will need to account for an def __getitem__(self, indices: tuple[slice, ...]) -> np.ndarray:
# indices can be a tuple of slice | Ellipsis
# need to accoutn for Ellipsis as the last object in the tuple
passexample implementation with the decord lazy video reader wrapper: class LazyVideo(LazyArray):
def __init__(
self,
path: Union[Path, str],
min_max: Tuple[int, int] = None,
**kwargs,
):
self._video_reader = VideoReader(str(path), **kwargs)
try:
frame0 = self._video_reader[10].asnumpy()
self._video_reader.seek(0)
except IndexError:
frame0 = self._video_reader[0].asnumpy()
self._video_reader.seek(0)
self._shape = (self._video_reader._num_frame, *frame0.shape)
self._dtype = frame0.dtype
if min_max is not None:
self._min, self._max = min_max
else:
self._min = frame0.min()
self._max = frame0.max()
@property
def dtype(self) -> str:
return self._dtype
@property
def shape(self) -> Tuple[int, int, int]:
"""[n_frames, m, n, 3 | 4]"""
return self._shape
@property
def min(self) -> float:
warn("min not implemented for LazyTiff, returning min of 0th index")
return self._min
@property
def max(self) -> float:
warn("max not implemented for LazyTiff, returning min of 0th index")
return self._max
@lru_cache(maxsize=32)
def __getitem__(self, indices: tuple[slice, ...]) -> np.ndarray:
# indices can be a tuple of slice | Ellipsis
# apply to frame index, then remaining dims
a = self._video_reader[indices[0]].asnumpy()[indices[1:]]
self._video_reader.seek(0)
return a |
|
ideas: If a ref range is not specified for a slider dim, it auto makes one just using the identity transform using the max of that array for that slider dim. |
|
ok update imgui sliders UI, need to do right click menus for NDGraphic properties, as well as some image properties like brightness, contrast, gamma for any ImageGraphic in general. |
| def _draw_resize_handle(self): | ||
| if self._location == "bottom": | ||
| imgui.set_cursor_pos((0, 0)) | ||
| imgui.invisible_button("##resize_handle", imgui.ImVec2(imgui.get_window_width(), self._separator_thickness)) | ||
|
|
||
| return x_pos, y_pos, width, height | ||
| hovered = imgui.is_item_hovered() | ||
| active = imgui.is_item_active() | ||
|
|
||
| # Get the actual screen rect of the button after it's been laid out | ||
| rect_min = imgui.get_item_rect_min() | ||
| rect_max = imgui.get_item_rect_max() | ||
|
|
||
| elif self._location == "right": | ||
| imgui.set_cursor_pos((0, 0)) | ||
| screen_pos = imgui.get_cursor_screen_pos() | ||
| win_height = imgui.get_window_height() | ||
| mouse_pos = imgui.get_mouse_pos() | ||
|
|
||
| rect_min = imgui.ImVec2(screen_pos.x, screen_pos.y) | ||
| rect_max = imgui.ImVec2(screen_pos.x + self._separator_thickness, screen_pos.y + win_height) | ||
|
|
||
| hovered = ( | ||
| rect_min.x <= mouse_pos.x <= rect_max.x | ||
| and rect_min.y <= mouse_pos.y <= rect_max.y | ||
| ) | ||
|
|
||
| if hovered and imgui.is_mouse_clicked(0): | ||
| self._right_gui_resizing = True | ||
|
|
||
| if not imgui.is_mouse_down(0): | ||
| self._right_gui_resizing = False | ||
|
|
||
| active = self._right_gui_resizing | ||
|
|
||
| imgui.set_cursor_pos((self._separator_thickness, 0)) | ||
|
|
||
| if hovered and imgui.is_mouse_double_clicked(0): | ||
| if not self._collapsed: | ||
| self._old_size = self.size | ||
| if self._location == "bottom": | ||
| self.size = int(self._separator_thickness) | ||
| elif self._location == "right": | ||
| self.size = int(self._separator_thickness) | ||
| self._collapsed = True | ||
| else: | ||
| self.size = self._old_size | ||
| self._collapsed = False | ||
|
|
||
| if hovered or active: | ||
| if not self._resize_cursor_set: | ||
| if self._location == "bottom": | ||
| self._figure.canvas.set_cursor("ns_resize") | ||
|
|
||
| elif self._location == "right": | ||
| self._figure.canvas.set_cursor("ew_resize") | ||
|
|
||
| self._resize_cursor_set = True | ||
| imgui.set_tooltip("Drag to resize, double click to expand/collapse") | ||
|
|
||
| elif self._resize_cursor_set: | ||
| self._figure.canvas.set_cursor("default") | ||
| self._resize_cursor_set = False | ||
|
|
||
| if active and imgui.is_mouse_dragging(0): | ||
| if self._location == "bottom": | ||
| delta = imgui.get_mouse_drag_delta(0).y | ||
|
|
||
| elif self._location == "right": | ||
| delta = imgui.get_mouse_drag_delta(0).x | ||
|
|
||
| imgui.reset_mouse_drag_delta(0) | ||
| px, py, pw, ph = self._figure.get_pygfx_render_area() | ||
|
|
||
| if self._location == "bottom": | ||
| new_render_size = ph + delta | ||
| elif self._location == "right": | ||
| new_render_size = pw + delta | ||
|
|
||
| # check if the new size would make the pygfx render area too small | ||
| if (delta < 0) and (new_render_size < 150): | ||
| print("not enough render area") | ||
| self._resize_blocked = True | ||
|
|
||
| if self._resize_blocked: | ||
| # check if cursor has returned | ||
| if self._location == "bottom": | ||
| _min, pos, _max = rect_min.y, imgui.get_mouse_pos().y, rect_max.y | ||
|
|
||
| elif self._location == "right": | ||
| _min, pos, _max = rect_min.x, imgui.get_mouse_pos().x, rect_max.x | ||
|
|
||
| if ((_min - 5) <= pos <= (_max + 5)) and delta > 0: | ||
| # if the mouse cursor is back on the bar and the delta > 0, i.e. render area increasing | ||
| self._resize_blocked = False | ||
|
|
||
| if not self._resize_blocked: | ||
| self.size = max(30, round(self.size - delta)) | ||
| self._collapsed = False | ||
|
|
||
| draw_list = imgui.get_window_draw_list() | ||
|
|
||
| line_color = ( | ||
| imgui.get_color_u32(imgui.ImVec4(0.9, 0.9, 0.9, 1.0)) | ||
| if (hovered or active) | ||
| else imgui.get_color_u32(imgui.ImVec4(0.5, 0.5, 0.5, 0.8)) | ||
| ) | ||
| bg_color = ( | ||
| imgui.get_color_u32(imgui.ImVec4(0.2, 0.2, 0.2, 0.8)) | ||
| if (hovered or active) | ||
| else imgui.get_color_u32(imgui.ImVec4(0.15, 0.15, 0.15, 0.6)) | ||
| ) | ||
|
|
||
| # Background bar | ||
| draw_list.add_rect_filled( | ||
| imgui.ImVec2(rect_min.x, rect_min.y), | ||
| imgui.ImVec2(rect_max.x, rect_max.y), | ||
| bg_color, | ||
| ) | ||
|
|
||
| # Three grip dots centered on the line | ||
| dot_spacing = 7.0 | ||
| dot_radius = 2 | ||
| if self._location == "bottom": | ||
| mid_y = (rect_min.y + rect_max.y) * 0.5 | ||
| center_x = (rect_min.x + rect_max.x) * 0.5 | ||
| for i in (-1, 0, 1): | ||
| cx = center_x + i * dot_spacing | ||
| draw_list.add_circle_filled(imgui.ImVec2(cx, mid_y), dot_radius, line_color) | ||
|
|
||
| imgui.set_cursor_pos((0, imgui.get_cursor_pos_y() - imgui.get_style().item_spacing.y)) | ||
|
|
||
| elif self._location == "right": | ||
| mid_x = (rect_min.x + rect_max.x) * 0.5 | ||
| center_y = (rect_min.y + rect_max.y) * 0.5 | ||
| for i in (-1, 0, 1): | ||
| cy = center_y + i * dot_spacing | ||
| draw_list.add_circle_filled( | ||
| imgui.ImVec2(mid_x, cy), dot_radius, line_color | ||
| ) | ||
|
|
||
| def _draw_title(self, title: str): | ||
| padding = imgui.ImVec2(10, 4) | ||
| text_size = imgui.calc_text_size(title) | ||
| win_width = imgui.get_window_width() | ||
| box_size = imgui.ImVec2(win_width, text_size.y + padding.y * 2) | ||
|
|
||
| box_screen_pos = imgui.get_cursor_screen_pos() | ||
|
|
||
| draw_list = imgui.get_window_draw_list() | ||
|
|
||
| # Background — use imgui's default title bar color | ||
| draw_list.add_rect_filled( | ||
| imgui.ImVec2(box_screen_pos.x, box_screen_pos.y), | ||
| imgui.ImVec2(box_screen_pos.x + box_size.x, box_screen_pos.y + box_size.y), | ||
| imgui.get_color_u32(imgui.Col_.title_bg_active), | ||
| ) | ||
|
|
||
| # Centered text | ||
| text_pos = imgui.ImVec2( | ||
| box_screen_pos.x + (win_width - text_size.x) * 0.5, | ||
| box_screen_pos.y + padding.y, | ||
| ) | ||
| draw_list.add_text( | ||
| text_pos, imgui.get_color_u32(imgui.ImVec4(1, 1, 1, 1)), title | ||
| ) | ||
|
|
||
| imgui.dummy(imgui.ImVec2(win_width, box_size.y)) |
There was a problem hiding this comment.
As per our AI use disclosure rules, I used the claude LLM to get a starting point for the imgui resize splitter code, but as usual with LLMs the output was mostly hot garbage and I rewrote most of it to make it work. Since it's not any core backend logic and just imgui UI I figured use of an LLM as a starting point for code gen was fine.
|
nd graphic options are now accessible through a right click menu popup:
Which then opens a window to change stuff until it's closed explicitly: I think these kinds of floating windows can be pretty useful for many things 🤔 @FlynnOConnell Checkboxes in the right click window to select the spatial dims. |
|
Another thing I should look into, when the |



it begins 😄
implements #951