diff --git a/app/main.py b/app/main.py index 253007a..82e2f99 100644 --- a/app/main.py +++ b/app/main.py @@ -4,90 +4,142 @@ import base64 import json import panel as pn +from openvisuspy import SetupLogger, Slice, ProbeTool, cbool +import time + +class SliceSynchronizer: + def __init__(self, slice1, slice2, throttle_delay=0.05): + self.slice1 = slice1 + self.slice2 = slice2 + self.sync_in_progress = False + self.last_sync_time = 0 + self.throttle_delay = throttle_delay + + def throttle(self, func, *args, **kwargs): + """Throttle function calls to limit frequency.""" + current_time = time.time() + if current_time - self.last_sync_time >= self.throttle_delay: + self.last_sync_time = current_time + func(*args, **kwargs) + + def sync_slices(self, box1=None, box2=None, is_reverse=False): + """Synchronize viewports between two slices, with an option to reverse the direction.""" + if self.sync_in_progress: + return + self.sync_in_progress = True + try: + # Set default boxes if not provided + if box1 is None: + box1 = self.slice1.db.getPhysicBox() + if box2 is None: + box2 = self.slice2.db.getPhysicBox() + + # Determine source and target based on is_reverse flag + if is_reverse: + (src_box, tgt_box) = (box2, box1) + (src_slice, tgt_slice) = (self.slice2, self.slice1) + else: + (src_box, tgt_box) = (box1, box2) + (src_slice, tgt_slice) = (self.slice1, self.slice2) + + # Unpack source and target boxes + (a, b), (c, d) = src_box + (A, B), (C, D) = tgt_box + + # Get viewport from the source slice + x, y, w, h = src_slice.canvas.getViewport() + + # Map coordinates from source to target + x1, x2 = [A + ((value - a) / (b - a)) * (B - A) for value in [x, x + w]] + y1, y2 = [C + ((value - c) / (d - c)) * (D - C) for value in [y, y + h]] + + # Set the target slice's viewport and refresh + tgt_slice.canvas.setViewport([x1, y1, x2 - x1, y2 - y1]) + tgt_slice.refresh("SyncSlices") + finally: + self.sync_in_progress = False + + def update_slice2(self, attr, old, new): + """Throttle and synchronize slice1 -> slice2.""" + self.throttle(self.sync_slices, is_reverse=False) + + def update_slice1(self, attr, old, new): + """Throttle and synchronize slice2 -> slice1 (reverse direction).""" + self.throttle(self.sync_slices, is_reverse=True) -from openvisuspy import SetupLogger, Slice, ProbeTool,cbool - -# //////////////////////////////////////////////////////////////////////////// -def SyncSlices(slice1, slice2, box1=None, box2=None): - - if box1 is None: - box1=slice1.db.getPhysicBox() - - if box2 is None: - box2=slice2.db.getPhysicBox() - - # map box1 to box2 - (a, b), (c, d)=box1 - (A, B), (C, D)=box2 - - x, y, w, h = slice1.canvas.getViewport() - x1, x2 = [A + ((value - a) / (b - a)) * (B - A) for value in [x, x + w]] - y1, y2 = [C + ((value - c) / (d - c)) * (D - C) for value in [y, y + h]] - slice2.canvas.setViewport([x1, y1, x2 - x1, y2 - y1]) - slice2.refresh("SyncSlices") # //////////////////////////////////////////////////////////////////////////// if __name__.startswith('bokeh'): - pn.extension( - "ipywidgets", - "floatpanel", - "codeeditor", - log_level="DEBUG", - notifications=True, - sizing_mode="stretch_width" - ) - - query_params = {k: v for k,v in pn.state.location.query_params.items()} - - log_filename = os.environ.get("OPENVISUSPY_DASHBOARDS_LOG_FILENAME", "/tmp/openvisuspy-dashboards.log") - logger = SetupLogger(log_filename=log_filename, logging_level=logging.DEBUG) - - # sync view - if len(sys.argv[1:])==2: - - from openvisuspy.utils import SafeCallback - from panel import Column, Row - - slice1 = Slice();slice1.load(sys.argv[1]) - slice2 = Slice();slice2.load(sys.argv[2]) - - show_options = { - "top": [ - [ - "palette", - "color_mapper_type", - "resolution", - "num_refinements", - "field", - "range_mode", - "range_min", - "range_max" - ], - ], - # "bottom": [["request", "response"]], - } - slice1.setShowOptions(show_options) - slice2.setShowOptions(show_options) - slice1.scene_body.param.watch( SafeCallback(lambda evt: SyncSlices(slice1,slice2)), "value", onlychanged=True, queued=True) - main_layout = pn.Row(slice1.getMainLayout(), slice2.getMainLayout()) - main_layout.servable() - - else: - - slice = Slice() - slice.load(sys.argv[1]) - - # load a whole scene - if "load" in query_params: - body = json.loads(base64.b64decode(query_params['load']).decode("utf-8")) - slice.setBody(body) - - # select from list of choices - elif "dataset" in query_params: - scene_name = query_params["dataset"] - slice.scene.value = scene_name - - main_layout = slice.getMainLayout() - main_layout.servable() - + pn.extension( + "ipywidgets", + "floatpanel", + "codeeditor", + log_level="DEBUG", + notifications=True, + ) + + query_params = {k: v for k, v in pn.state.location.query_params.items()} + + log_filename = os.environ.get("OPENVISUSPY_DASHBOARDS_LOG_FILENAME", "/tmp/openvisuspy-dashboards.log") + logger = SetupLogger(log_filename=log_filename, logging_level=logging.DEBUG) + + # Sync view + if len(sys.argv[1:]) == 2: + + slice1 = Slice() + slice1.load(sys.argv[1]) + slice2 = Slice() + slice2.load(sys.argv[2]) + + show_options = { + "top": [ + [ + "palette", + "color_mapper_type", + "resolution", + "num_refinements", + "field", + "range_mode", + "range_min", + "range_max" + ], + ], + } + slice1.setShowOptions(show_options) + slice2.setShowOptions(show_options) + + # Initialize the synchronizer + synchronizer = SliceSynchronizer(slice1, slice2, throttle_delay = 0.05) + + # Watch for viewport changes in slice1 and slice2 using on_change + slice1.canvas.fig.x_range.on_change('start', synchronizer.update_slice2) + slice1.canvas.fig.x_range.on_change('end', synchronizer.update_slice2) + slice1.canvas.fig.y_range.on_change('start', synchronizer.update_slice2) + slice1.canvas.fig.y_range.on_change('end', synchronizer.update_slice2) + + slice2.canvas.fig.x_range.on_change('start', synchronizer.update_slice1) + slice2.canvas.fig.x_range.on_change('end', synchronizer.update_slice1) + slice2.canvas.fig.y_range.on_change('start', synchronizer.update_slice1) + slice2.canvas.fig.y_range.on_change('end', synchronizer.update_slice1) + + # Layout for both slices + main_layout = pn.Row(slice1.getMainLayout(), slice2.getMainLayout()) + main_layout.servable() + + else: + slice = Slice() + slice.load(sys.argv[1]) + + # Load a whole scene + if "load" in query_params: + body = json.loads(base64.b64decode(query_params['load']).decode("utf-8")) + slice.setBody(body) + + # Select from list of choices + elif "dataset" in query_params: + scene_name = query_params["dataset"] + slice.scene.value = scene_name + + main_layout = slice.getMainLayout() + main_layout.servable() diff --git a/src/openvisuspy/show_details.py b/src/openvisuspy/show_details.py index 8dc001e..71ed090 100644 --- a/src/openvisuspy/show_details.py +++ b/src/openvisuspy/show_details.py @@ -5,6 +5,10 @@ from bokeh.models import LinearColorMapper import bokeh.models import logging +from mpl_toolkits.axes_grid1 import make_axes_locatable +import matplotlib.pyplot as plt +from bokeh.models import ColumnDataSource, ColorBar, LinearColorMapper +from .utils import * logger = logging.getLogger(__name__) @@ -106,7 +110,7 @@ def ShowDetails(self,x,y,w,h): pdim=self.getPointDim() # todo for 2D dataset - assert(pdim==3) + # assert(pdim==3) z=int(self.offset.value) logic_box=self.toLogic([x,y,w,h]) @@ -148,29 +152,49 @@ def ShowDetails(self,x,y,w,h): self.range_min.value = min(self.range_min.value, self.vmin) self.range_max.value = max(self.range_max.value, self.vmax) logger.info(f"Updating range with selected area vmin={self.vmin} vmax={self.vmax}") - - p = figure(x_range=(self.selected_physic_box[0][0], self.selected_physic_box[0][1]), y_range=(self.selected_physic_box[1][0], self.selected_physic_box[1][1])) + fig, ax = plt.subplots() + + p1 = figure(x_range=(0,100), y_range=(0,100)) palette_name = self.palette.value_name if self.palette.value_name.endswith("256") else "Turbo256" mapper = LinearColorMapper(palette=palette_name, low=np.min(self.detailed_data), high=np.max(self.detailed_data)) # Flip data to match imshow orientation - data_flipped = data - source = bokeh.models.ColumnDataSource(data=dict(image=[data_flipped])) + data_flipped = data + + print(type(self.selected_physic_box[0][1])) dw = abs(self.selected_physic_box[0][1] -self.selected_physic_box[0][0]) + dh = abs(self.selected_physic_box[1][1] - self.selected_physic_box[1][0]) - p.image(image='image', x=self.selected_physic_box[0][0], y=self.selected_physic_box[1][0], dw=dw, dh=dh, color_mapper=mapper, source=source) - color_bar = bokeh.models.ColorBar(color_mapper=mapper, label_standoff=12, location=(0,0)) - p.add_layout(color_bar, 'right') - p.xaxis.axis_label = "X" - p.yaxis.axis_label = "Y" - - self.showDialog( - pn.Column( - self.file_name_input, - pn.Row(save_numpy_button,download_script_button), - pn.Row(apply_avg_min_colormap_button,apply_avg_max_colormap_button,add_range_button,apply_colormap_button), - pn.Row(pn.pane.Bokeh(p,sizing_mode="stretch_both")), - sizing_mode="stretch_both" - ) - , width=900, height=800, name=f"Palette: {palette_name} Min: {self.vmin}, Max: {self.vmax}") + x_min, x_max = int(self.selected_physic_box[0][0]), int(self.selected_physic_box[0][1]) + y_min, y_max = int(self.selected_physic_box[1][0]), int(self.selected_physic_box[1][1]) + + #fig, ax = plt.subplots(figsize=(14, 20)) + fig, ax = plt.subplots(figsize=(4, 4)) + im = ax.imshow(data_flipped, cmap='turbo',extent=[x_min, x_max, y_min, y_max], aspect='auto') + divider = make_axes_locatable(ax) + cax = divider.append_axes("right", size="5%", pad=0.1) + cbar = plt.colorbar(im, cax=cax) + + ax.set_xlim(self.selected_physic_box[0][0], self.selected_physic_box[0][1]) + ax.set_ylim(self.selected_physic_box[1][1], self.selected_physic_box[1][0]) + + ax.set_xlabel("X") + ax.set_ylabel("Y") + plt.tight_layout() + + dialog_layout = pn.Column( + self.file_name_input, + pn.Row(save_numpy_button, download_script_button), + pn.Row(pn.pane.Matplotlib(fig), pn.Column( + pn.pane.Markdown(f"#### Palette Used: {palette_name}"), + pn.pane.Markdown(f"#### New Min/Max Found.."), + pn.pane.Markdown(f"#### Min: {self.vmin}, Max: {self.vmax}"), + pn.Row(apply_avg_min_colormap_button, apply_avg_max_colormap_button), + add_range_button, + apply_colormap_button + )), + sizing_mode="stretch_both" + ) + + self.showDialog(dialog_layout, width=500, height=600, name="Details") \ No newline at end of file