1+ from dataclasses import dataclass
2+ import os
3+ from time import perf_counter
4+ from typing import Any , Sequence
5+
6+ from imgui_bundle import imgui , icons_fontawesome_6 as fa
7+ import numpy as np
8+
9+ from ...layouts import ImguiFigure , Subplot
10+ from ...graphics import ScatterCollection , LineCollection , LineStack , ImageGraphic
11+ from ...ui import EdgeWindow
12+ from .base import NDGraphic , NDProcessor
13+ from ._nd_image import NDImage , NDImageProcessor
14+ from ._nd_positions import NDPositions , NDPositionsProcessor
15+
16+
17+ @dataclass
18+ class ReferenceRangeContinuous :
19+ start : int | float
20+ stop : int | float
21+ step : int | float
22+ unit : str
23+
24+ def __getitem__ (self , index : int ):
25+ """return the value at the index w.r.t. the step size"""
26+ # if index is negative, turn to positive index
27+ if index < 0 :
28+ raise ValueError ("negative indexing not supported" )
29+
30+ val = self .start + (self .step * index )
31+ if not self .start <= val <= self .stop :
32+ raise IndexError (f"index: { index } value: { val } out of bounds: [{ self .start } , { self .stop } ]" )
33+
34+ return val
35+
36+
37+ @dataclass
38+ class ReferenceRangeDiscrete :
39+ options : Sequence [Any ]
40+ unit : str
41+
42+ def __getitem__ (self , index : int ):
43+ if index > len (self .options ):
44+ raise IndexError
45+
46+ return self .options [index ]
47+
48+ def __len__ (self ):
49+ return len (self .options )
50+
51+
52+ class NDWSubplot :
53+ def __init__ (self , ndw , subplot : Subplot ):
54+ self .ndw = ndw
55+ self ._subplot = subplot
56+
57+ self ._nd_graphics = list ()
58+
59+ @property
60+ def nd_graphics (self ) -> list [NDGraphic ]:
61+ return self ._nd_graphics
62+
63+ def __getitem__ (self , key ):
64+ if isinstance (key , (int , np .integer )):
65+ return self .nd_graphics [key ]
66+
67+ for g in self .nd_graphics :
68+ if g .name == key :
69+ return g
70+
71+ else :
72+ raise KeyError (f"NDGraphc with given key not found: { key } " )
73+
74+ def add_nd_image (self , * args , ** kwargs ):
75+ nd = NDImage (* args , ** kwargs )
76+ self ._nd_graphics .append (nd )
77+ self ._subplot .add_graphic (nd .graphic )
78+ return nd
79+
80+ def add_nd_scatter (self , * args , ** kwargs ):
81+ nd = NDPositions (* args , graphic = ScatterCollection , multi = True , ** kwargs )
82+ self ._nd_graphics .append (nd )
83+ self ._subplot .add_graphic (nd .graphic )
84+
85+ return nd
86+
87+ def add_nd_timeseries (self , * args , graphic : type [LineCollection | LineStack | ImageGraphic ] = LineStack , ** kwargs ):
88+ nd = NDPositions (* args , graphic = LineStack , multi = True , ** kwargs )
89+ self ._nd_graphics .append (nd )
90+ self ._subplot .add_graphic (nd .graphic )
91+ # TODO: think about auto-xrange for subplot camera
92+ return nd
93+
94+ def add_nd_lines (self , * args , ** kwargs ):
95+ nd = NDPositions (* args , graphic = LineCollection , multi = True , ** kwargs )
96+ self ._nd_graphics .append (nd )
97+ self ._subplot .add_graphic (nd .graphic )
98+ return nd
99+
100+ # def __repr__(self):
101+ # return "NDWidget Subplot"
102+ #
103+ # def __str__(self):
104+ # return "NDWidget Subplot"
105+
106+
107+ class NDWSliders (EdgeWindow ):
108+ def __init__ (self , figure , size , ndwidget ):
109+ super ().__init__ (figure = figure , size = size , title = "NDWidget controls" , location = "bottom" )
110+ self ._ndwidget = ndwidget
111+
112+ # n_sliders = self._image_widget.n_sliders
113+ #
114+ # # whether or not a dimension is in play mode
115+ # self._playing: list[bool] = [False] * n_sliders
116+ #
117+ # # approximate framerate for playing
118+ # self._fps: list[int] = [20] * n_sliders
119+ #
120+ # # framerate converted to frame time
121+ # self._frame_time: list[float] = [1 / 20] * n_sliders
122+ #
123+ # # last timepoint that a frame was displayed from a given dimension
124+ # self._last_frame_time: list[float] = [perf_counter()] * n_sliders
125+ #
126+ # # loop playback
127+ # self._loop = False
128+ #
129+ # # auto-plays the ImageWidget's left-most dimension in docs galleries
130+ # if "DOCS_BUILD" in os.environ.keys():
131+ # if os.environ["DOCS_BUILD"] == "1":
132+ # self._playing[0] = True
133+ # self._loop = True
134+ #
135+ # self.pause = False
136+
137+ def update (self ):
138+ indices_changed = False
139+
140+ for dim_index , (current_index , refr ) in enumerate (zip (self ._ndwidget .indices , self ._ndwidget .ref_ranges )):
141+ if isinstance (refr , ReferenceRangeContinuous ):
142+ changed , val = imgui .slider_float (
143+ v = current_index ,
144+ v_min = refr .start ,
145+ v_max = refr .stop ,
146+ label = refr .unit
147+ )
148+
149+ if changed :
150+ new_indices = list (self ._ndwidget .indices )
151+ new_indices [dim_index ] = val
152+
153+ indices_changed = True
154+
155+ if indices_changed :
156+ self ._ndwidget .indices = tuple (new_indices )
157+
158+
159+ class NDWidget :
160+ def __init__ (self , ref_ranges : list [tuple ], ** kwargs ):
161+ self ._ref_ranges = list ()
162+
163+ for r in ref_ranges :
164+ if len (r ) == 4 :
165+ # assume start, stop, step, unit
166+ refr = ReferenceRangeContinuous (* r )
167+ elif len (r ) == 2 :
168+ refr = ReferenceRangeDiscrete (* r )
169+ else :
170+ raise ValueError
171+
172+ self ._ref_ranges .append (refr )
173+
174+ self ._figure = ImguiFigure (** kwargs )
175+
176+ self ._subplots : dict [Subplot , NDWSubplot ] = dict ()
177+ for subplot in self .figure :
178+ self ._subplots [subplot ] = NDWSubplot (self , subplot )
179+
180+ # starting index for all dims
181+ self ._indices = tuple (refr [0 ] for refr in self .ref_ranges )
182+
183+ # hard code the expected height so that the first render looks right in tests, docs etc.
184+ ui_size = 57 + (50 * len (self .indices ))
185+
186+ self ._sliders_ui = NDWSliders (self .figure , ui_size , self )
187+ self .figure .add_gui (self ._sliders_ui )
188+
189+ @property
190+ def figure (self ) -> ImguiFigure :
191+ return self ._figure
192+
193+ @property
194+ def ref_ranges (self ) -> tuple [ReferenceRangeContinuous | ReferenceRangeDiscrete ]:
195+ return tuple (self ._ref_ranges )
196+
197+ @property
198+ def indices (self ) -> tuple :
199+ return self ._indices
200+
201+ @indices .setter
202+ def indices (self , new_indices : tuple [Any ]):
203+ for subplot in self ._subplots .values ():
204+ for ndg in subplot .nd_graphics :
205+ ndg .indices = new_indices
206+
207+ self ._indices = new_indices
208+
209+ def __getitem__ (self , key ):
210+ subplot = self .figure [key ]
211+ return self ._subplots [subplot ]
212+
213+ def show (self , ** kwargs ):
214+ return self .figure .show (** kwargs )
0 commit comments