From 084192bbad5dad1b61c485cf5a30edf45f4d7123 Mon Sep 17 00:00:00 2001 From: Benjamin CHARMES Date: Tue, 1 Jul 2025 14:49:08 +0200 Subject: [PATCH 01/13] ability to hide points and only show line --- pydatalab/src/pydatalab/bokeh_plots.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pydatalab/src/pydatalab/bokeh_plots.py b/pydatalab/src/pydatalab/bokeh_plots.py index 906693141..cfab66a9b 100644 --- a/pydatalab/src/pydatalab/bokeh_plots.py +++ b/pydatalab/src/pydatalab/bokeh_plots.py @@ -234,6 +234,11 @@ def selectable_axes_plot( title=plot_title, **kwargs, ) + + if tools is None: + coordinate_hover = HoverTool(tooltips=[("X", "$x{0.00}"), ("Y", "$y{0.00}")], mode="mouse") + p.add_tools(coordinate_hover) + p.toolbar.logo = "grey" if tools: From 2753bfa124a03007a1547b56ca3cf35caa97c21c Mon Sep 17 00:00:00 2001 From: Benjamin CHARMES Date: Tue, 1 Jul 2025 15:53:52 +0200 Subject: [PATCH 02/13] ability to hide points and only show line --- pydatalab/src/pydatalab/bokeh_plots.py | 37 +++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/pydatalab/src/pydatalab/bokeh_plots.py b/pydatalab/src/pydatalab/bokeh_plots.py index cfab66a9b..0b9d26557 100644 --- a/pydatalab/src/pydatalab/bokeh_plots.py +++ b/pydatalab/src/pydatalab/bokeh_plots.py @@ -18,7 +18,7 @@ LinearColorMapper, TableColumn, ) -from bokeh.models.widgets import Select +from bokeh.models.widgets import CheckboxGroup, Select from bokeh.palettes import Accent, Dark2 from bokeh.plotting import ColumnDataSource, figure from bokeh.themes import Theme @@ -399,6 +399,41 @@ def selectable_axes_plot( ) plot_columns = [table] + plot_columns + if plot_points and plot_line: + plot_visibility_controls = CheckboxGroup( + labels=["Show lines", "Show points"], + active=[0, 1], + margin=(5, 5, 5, 5), + inline=True, + ) + + line_renderers = [r for r in p.renderers if hasattr(r.glyph, "line_color")] + circle_renderers = [r for r in p.renderers if hasattr(r.glyph, "size")] + + visibility_callback = CustomJS( + args=dict( + checkboxes=plot_visibility_controls, + line_renderers=line_renderers, + circle_renderers=circle_renderers, + ), + code=""" + var active = checkboxes.active; + var show_lines = active.includes(0); + var show_points = active.includes(1); + + for (var i = 0; i < line_renderers.length; i++) { + line_renderers[i].visible = show_lines; + } + + for (var i = 0; i < circle_renderers.length; i++) { + circle_renderers[i].visible = show_points; + } + """, + ) + + plot_visibility_controls.js_on_change("active", visibility_callback) + plot_columns.append(plot_visibility_controls) + layout = column(*plot_columns) p.js_on_event(DoubleTap, CustomJS(args=dict(p=p), code="p.reset.emit()")) From bc682ebf06fede9f589f6386f5dbdf9b8bb8abc2 Mon Sep 17 00:00:00 2001 From: Benjamin CHARMES Date: Tue, 1 Jul 2025 16:51:41 +0200 Subject: [PATCH 03/13] make plot generally bigger --- pydatalab/src/pydatalab/bokeh_plots.py | 2 +- webapp/src/components/datablocks/BokehBlock.vue | 6 ++---- webapp/src/components/datablocks/CycleBlock.vue | 2 +- webapp/src/components/datablocks/NMRBlock.vue | 2 +- webapp/src/components/datablocks/UVVisBlock.vue | 6 ++---- webapp/src/components/datablocks/XRDBlock.vue | 9 +++------ 6 files changed, 10 insertions(+), 17 deletions(-) diff --git a/pydatalab/src/pydatalab/bokeh_plots.py b/pydatalab/src/pydatalab/bokeh_plots.py index 0b9d26557..aa395b79d 100644 --- a/pydatalab/src/pydatalab/bokeh_plots.py +++ b/pydatalab/src/pydatalab/bokeh_plots.py @@ -434,7 +434,7 @@ def selectable_axes_plot( plot_visibility_controls.js_on_change("active", visibility_callback) plot_columns.append(plot_visibility_controls) - layout = column(*plot_columns) + layout = column(*plot_columns, sizing_mode="scale_width") p.js_on_event(DoubleTap, CustomJS(args=dict(p=p), code="p.reset.emit()")) return layout diff --git a/webapp/src/components/datablocks/BokehBlock.vue b/webapp/src/components/datablocks/BokehBlock.vue index c086fa6f2..7b31822a6 100644 --- a/webapp/src/components/datablocks/BokehBlock.vue +++ b/webapp/src/components/datablocks/BokehBlock.vue @@ -10,10 +10,8 @@ DataBlockBase as a prop, and save from within DataBlockBase --> update-block-on-change /> -
-
- -
+
+
diff --git a/webapp/src/components/datablocks/CycleBlock.vue b/webapp/src/components/datablocks/CycleBlock.vue index 3de96c2e3..b5cd80add 100644 --- a/webapp/src/components/datablocks/CycleBlock.vue +++ b/webapp/src/components/datablocks/CycleBlock.vue @@ -278,7 +278,7 @@ export default { } .limited-width { - max-width: 650px; + max-width: 100%; } .slider { diff --git a/webapp/src/components/datablocks/NMRBlock.vue b/webapp/src/components/datablocks/NMRBlock.vue index f5349020a..bae8f7b30 100644 --- a/webapp/src/components/datablocks/NMRBlock.vue +++ b/webapp/src/components/datablocks/NMRBlock.vue @@ -41,7 +41,7 @@ DataBlockBase as a prop, and save from within DataBlockBase -->
-
+
diff --git a/webapp/src/components/datablocks/UVVisBlock.vue b/webapp/src/components/datablocks/UVVisBlock.vue index 75e7a5519..2eac032a8 100644 --- a/webapp/src/components/datablocks/UVVisBlock.vue +++ b/webapp/src/components/datablocks/UVVisBlock.vue @@ -10,10 +10,8 @@ :update-block-on-change="true" />
-
-
- -
+
+
diff --git a/webapp/src/components/datablocks/XRDBlock.vue b/webapp/src/components/datablocks/XRDBlock.vue index 58e167423..c2946d10e 100644 --- a/webapp/src/components/datablocks/XRDBlock.vue +++ b/webapp/src/components/datablocks/XRDBlock.vue @@ -34,12 +34,9 @@ DataBlockBase as a prop, and save from within DataBlockBase --> {{ wavelengthParseError }}
- -
-
- -
-
+
+
+
From 6fff76119043ad6ebadc67014d53ad2734219784 Mon Sep 17 00:00:00 2001 From: Benjamin CHARMES Date: Tue, 1 Jul 2025 17:10:01 +0200 Subject: [PATCH 04/13] ability to stagger rather than overlap patterns --- pydatalab/src/pydatalab/apps/xrd/blocks.py | 40 +++++++++++++++++-- webapp/src/components/datablocks/XRDBlock.vue | 31 ++++++++++++++ 2 files changed, 68 insertions(+), 3 deletions(-) diff --git a/pydatalab/src/pydatalab/apps/xrd/blocks.py b/pydatalab/src/pydatalab/apps/xrd/blocks.py index 5fb501997..646aaa7ac 100644 --- a/pydatalab/src/pydatalab/apps/xrd/blocks.py +++ b/pydatalab/src/pydatalab/apps/xrd/blocks.py @@ -23,7 +23,7 @@ class XRDBlock(DataBlock): description = "Visualize XRD patterns and perform simple baseline corrections." accepted_file_extensions = (".xrdml", ".xy", ".dat", ".xye", ".rasx", ".cif") - defaults = {"wavelength": 1.54060} + defaults = {"wavelength": 1.54060, "stagger_enabled": False, "stagger_offset": 1.0} @property def plot_functions(self): @@ -59,7 +59,8 @@ def load_pattern( df, peak_data = compute_cif_pxrd( location, wavelength=wavelength or cls.defaults["wavelength"] ) - theoretical = True # Track whether this is a computed PXRD that does not need background subtraction + # Track whether this is a computed PXRD that does not need background subtraction + theoretical = True else: columns = ["twotheta", "intensity", "error"] @@ -235,7 +236,25 @@ def generate_xrd_plot(self) -> None: warnings.warn(f"Could not parse file {f['location']} as XRD data. Error: {exc}") continue peak_information[str(f["immutable_id"])] = PeakInformation(**peak_data).dict() - pattern_df["normalized intensity (staggered)"] += ind + stagger_enabled = self.data.get("stagger_enabled", self.defaults["stagger_enabled"]) + stagger_offset = float( + self.data.get("stagger_offset", self.defaults["stagger_offset"]) + ) + + if stagger_enabled: + for col in pattern_df.columns: + if "intensity" in col.lower(): + if "(staggered)" not in col: + staggered_col = f"{col} (staggered)" + pattern_df[staggered_col] = pattern_df[col] + (ind * stagger_offset) + else: + pattern_df[col] = pattern_df[col] + (ind * stagger_offset) + else: + for col in pattern_df.columns: + if "intensity" in col.lower() and "(staggered)" not in col: + staggered_col = f"{col} (staggered)" + pattern_df[staggered_col] = pattern_df[col] + pattern_dfs.append(pattern_df) self.data["peak_data"] = peak_information @@ -261,6 +280,21 @@ def generate_xrd_plot(self) -> None: pattern_dfs = [pattern_dfs] if pattern_dfs: + stagger_enabled = self.data.get("stagger_enabled", self.defaults["stagger_enabled"]) + stagger_offset = float(self.data.get("stagger_offset", self.defaults["stagger_offset"])) + + if stagger_enabled and len(pattern_dfs) > 1: + for ind, df in enumerate(pattern_dfs): + offset = ind * stagger_offset + for col in df.columns: + if "intensity" in col.lower() and df[col].dtype in [ + "float64", + "float32", + "int64", + "int32", + ]: + df[col] = df[col] + offset + p = selectable_axes_plot( pattern_dfs, x_options=["2θ (°)", "Q (Å⁻¹)", "d (Å)"], diff --git a/webapp/src/components/datablocks/XRDBlock.vue b/webapp/src/components/datablocks/XRDBlock.vue index c2946d10e..3c43e611e 100644 --- a/webapp/src/components/datablocks/XRDBlock.vue +++ b/webapp/src/components/datablocks/XRDBlock.vue @@ -35,6 +35,35 @@ DataBlockBase as a prop, and save from within DataBlockBase --> +
+
+
+ + +
+ + + +
+
@@ -81,6 +110,8 @@ export default { return this.$store.state.blocksInfos["xrd"]; }, wavelength: createComputedSetterForBlockField("wavelength"), + stagger_enabled: createComputedSetterForBlockField("stagger_enabled"), + stagger_offset: createComputedSetterForBlockField("stagger_offset"), file_id: createComputedSetterForBlockField("file_id"), }, methods: { From c91e9c9369fdcf1b4c5be8ecaff3ee2dfd2205a2 Mon Sep 17 00:00:00 2001 From: Benjamin CHARMES Date: Tue, 1 Jul 2025 17:30:09 +0200 Subject: [PATCH 05/13] move legend outside plot by default and add a maximum label length --- pydatalab/src/pydatalab/bokeh_plots.py | 32 ++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/pydatalab/src/pydatalab/bokeh_plots.py b/pydatalab/src/pydatalab/bokeh_plots.py index aa395b79d..1a11880d3 100644 --- a/pydatalab/src/pydatalab/bokeh_plots.py +++ b/pydatalab/src/pydatalab/bokeh_plots.py @@ -279,6 +279,23 @@ def selectable_axes_plot( else: label = df_.index.name if len(df) > 1 else "" + if label is None: + label = "" + + if label and len(label) > 15: + if "." in label: + name, ext = label.rsplit(".", 1) + if len(ext) < 6: + available = 15 - len(ext) - 4 + if available > 3: + label = f"{name[:available]}...{ext}" + else: + label = f"{label[:12]}..." + else: + label = f"{label[:12]}..." + else: + label = f"{label[:12]}..." + source = ColumnDataSource(df_) if color_options: @@ -370,6 +387,21 @@ def selectable_axes_plot( p.legend.click_policy = "hide" if len(df) <= 1: p.legend.visible = False + else: + legend_items = p.legend.items + p.legend.visible = False + + from bokeh.models import Legend + + external_legend = Legend( + items=legend_items, + click_policy="hide", + background_fill_alpha=0.8, + label_text_font_size="9pt", + spacing=1, + margin=2, + ) + p.add_layout(external_legend, "right") if not skip_plot: plot_columns.append(p) From 7a81d95339f3592a6769de0371276e60d6ec397f Mon Sep 17 00:00:00 2001 From: Benjamin CHARMES Date: Tue, 1 Jul 2025 17:59:55 +0200 Subject: [PATCH 06/13] Small improvement for 'Show lines' & 'Show points' --- pydatalab/src/pydatalab/bokeh_plots.py | 74 ++++++++++++++++++-------- 1 file changed, 51 insertions(+), 23 deletions(-) diff --git a/pydatalab/src/pydatalab/bokeh_plots.py b/pydatalab/src/pydatalab/bokeh_plots.py index 1a11880d3..148870230 100644 --- a/pydatalab/src/pydatalab/bokeh_plots.py +++ b/pydatalab/src/pydatalab/bokeh_plots.py @@ -18,7 +18,7 @@ LinearColorMapper, TableColumn, ) -from bokeh.models.widgets import CheckboxGroup, Select +from bokeh.models.widgets import Select from bokeh.palettes import Accent, Dark2 from bokeh.plotting import ColumnDataSource, figure from bokeh.themes import Theme @@ -432,39 +432,67 @@ def selectable_axes_plot( plot_columns = [table] + plot_columns if plot_points and plot_line: - plot_visibility_controls = CheckboxGroup( - labels=["Show lines", "Show points"], - active=[0, 1], - margin=(5, 5, 5, 5), - inline=True, + from bokeh.layouts import row + + show_lines_btn = Button( + label="✓ Show lines", button_type="primary", width_policy="min", margin=(2, 5, 2, 5) + ) + show_points_btn = Button( + label="✓ Show points", button_type="primary", width_policy="min", margin=(2, 5, 2, 5) ) - line_renderers = [r for r in p.renderers if hasattr(r.glyph, "line_color")] + line_renderers = [ + r + for r in p.renderers + if hasattr(r.glyph, "line_color") and not hasattr(r.glyph, "size") + ] circle_renderers = [r for r in p.renderers if hasattr(r.glyph, "size")] - visibility_callback = CustomJS( - args=dict( - checkboxes=plot_visibility_controls, - line_renderers=line_renderers, - circle_renderers=circle_renderers, - ), + lines_callback = CustomJS( + args=dict(btn=show_lines_btn, renderers=line_renderers), code=""" - var active = checkboxes.active; - var show_lines = active.includes(0); - var show_points = active.includes(1); - - for (var i = 0; i < line_renderers.length; i++) { - line_renderers[i].visible = show_lines; + if (btn.label.includes('✓')) { + btn.label = '✗ Show lines'; + btn.button_type = 'default'; + for (var i = 0; i < renderers.length; i++) { + renderers[i].visible = false; + } + } else { + btn.label = '✓ Show lines'; + btn.button_type = 'primary'; + for (var i = 0; i < renderers.length; i++) { + renderers[i].visible = true; + } } + """, + ) - for (var i = 0; i < circle_renderers.length; i++) { - circle_renderers[i].visible = show_points; + points_callback = CustomJS( + args=dict(btn=show_points_btn, renderers=circle_renderers), + code=""" + if (btn.label.includes('✓')) { + btn.label = '✗ Show points'; + btn.button_type = 'default'; + for (var i = 0; i < renderers.length; i++) { + renderers[i].visible = false; + } + } else { + btn.label = '✓ Show points'; + btn.button_type = 'primary'; + for (var i = 0; i < renderers.length; i++) { + renderers[i].visible = true; + } } """, ) - plot_visibility_controls.js_on_change("active", visibility_callback) - plot_columns.append(plot_visibility_controls) + show_lines_btn.js_on_click(lines_callback) + show_points_btn.js_on_click(points_callback) + + controls_layout = row( + show_lines_btn, show_points_btn, sizing_mode="scale_width", margin=(10, 0, 10, 0) + ) + plot_columns.append(controls_layout) layout = column(*plot_columns, sizing_mode="scale_width") From e720ff3656fecd4ceca275447592f3c7df48c08c Mon Sep 17 00:00:00 2001 From: Ben Charmes <107116804+BenjaminCharmes@users.noreply.github.com> Date: Thu, 10 Jul 2025 14:47:13 +0200 Subject: [PATCH 07/13] stagger enabled by default Co-authored-by: Matthew Evans --- pydatalab/src/pydatalab/apps/xrd/blocks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pydatalab/src/pydatalab/apps/xrd/blocks.py b/pydatalab/src/pydatalab/apps/xrd/blocks.py index 646aaa7ac..b68f7a810 100644 --- a/pydatalab/src/pydatalab/apps/xrd/blocks.py +++ b/pydatalab/src/pydatalab/apps/xrd/blocks.py @@ -23,7 +23,7 @@ class XRDBlock(DataBlock): description = "Visualize XRD patterns and perform simple baseline corrections." accepted_file_extensions = (".xrdml", ".xy", ".dat", ".xye", ".rasx", ".cif") - defaults = {"wavelength": 1.54060, "stagger_enabled": False, "stagger_offset": 1.0} + defaults = {"wavelength": 1.54060, "stagger_enabled": True, "stagger_offset": 1.0} @property def plot_functions(self): From e45112daf7e89436c2be0288e9560c4733d60542 Mon Sep 17 00:00:00 2001 From: Benjamin CHARMES Date: Thu, 10 Jul 2025 15:27:33 +0200 Subject: [PATCH 08/13] Add review suggestions --- pydatalab/src/pydatalab/apps/xrd/blocks.py | 8 +-- pydatalab/src/pydatalab/bokeh_plots.py | 61 ++++--------------- pydatalab/src/pydatalab/utils.py | 19 ++++++ .../src/components/datablocks/BokehBlock.vue | 8 ++- 4 files changed, 40 insertions(+), 56 deletions(-) diff --git a/pydatalab/src/pydatalab/apps/xrd/blocks.py b/pydatalab/src/pydatalab/apps/xrd/blocks.py index b68f7a810..1f11b401d 100644 --- a/pydatalab/src/pydatalab/apps/xrd/blocks.py +++ b/pydatalab/src/pydatalab/apps/xrd/blocks.py @@ -287,12 +287,8 @@ def generate_xrd_plot(self) -> None: for ind, df in enumerate(pattern_dfs): offset = ind * stagger_offset for col in df.columns: - if "intensity" in col.lower() and df[col].dtype in [ - "float64", - "float32", - "int64", - "int32", - ]: + if "intensity" in col.lower(): + df[col] = df[col].astype("float64") df[col] = df[col] + offset p = selectable_axes_plot( diff --git a/pydatalab/src/pydatalab/bokeh_plots.py b/pydatalab/src/pydatalab/bokeh_plots.py index 148870230..00f0aa605 100644 --- a/pydatalab/src/pydatalab/bokeh_plots.py +++ b/pydatalab/src/pydatalab/bokeh_plots.py @@ -24,6 +24,8 @@ from bokeh.themes import Theme from scipy.signal import find_peaks +from .utils import shrink_label + FONTSIZE = "12pt" TYPEFACE = "Helvetica, sans-serif" COLORS = Dark2[8] @@ -279,22 +281,7 @@ def selectable_axes_plot( else: label = df_.index.name if len(df) > 1 else "" - if label is None: - label = "" - - if label and len(label) > 15: - if "." in label: - name, ext = label.rsplit(".", 1) - if len(ext) < 6: - available = 15 - len(ext) - 4 - if available > 3: - label = f"{name[:available]}...{ext}" - else: - label = f"{label[:12]}..." - else: - label = f"{label[:12]}..." - else: - label = f"{label[:12]}..." + label = shrink_label(label) source = ColumnDataSource(df_) @@ -337,7 +324,14 @@ def selectable_axes_plot( ) lines = ( - p.line(x=x_default, y=y_default, source=source, color=line_color, legend_label=label) + p.line( + x=x_default, + y=y_default, + source=source, + color=line_color, + legend_label=label, + line_width=2, + ) if plot_line else None ) @@ -434,39 +428,12 @@ def selectable_axes_plot( if plot_points and plot_line: from bokeh.layouts import row - show_lines_btn = Button( - label="✓ Show lines", button_type="primary", width_policy="min", margin=(2, 5, 2, 5) - ) show_points_btn = Button( label="✓ Show points", button_type="primary", width_policy="min", margin=(2, 5, 2, 5) ) - line_renderers = [ - r - for r in p.renderers - if hasattr(r.glyph, "line_color") and not hasattr(r.glyph, "size") - ] circle_renderers = [r for r in p.renderers if hasattr(r.glyph, "size")] - lines_callback = CustomJS( - args=dict(btn=show_lines_btn, renderers=line_renderers), - code=""" - if (btn.label.includes('✓')) { - btn.label = '✗ Show lines'; - btn.button_type = 'default'; - for (var i = 0; i < renderers.length; i++) { - renderers[i].visible = false; - } - } else { - btn.label = '✓ Show lines'; - btn.button_type = 'primary'; - for (var i = 0; i < renderers.length; i++) { - renderers[i].visible = true; - } - } - """, - ) - points_callback = CustomJS( args=dict(btn=show_points_btn, renderers=circle_renderers), code=""" @@ -486,12 +453,10 @@ def selectable_axes_plot( """, ) - show_lines_btn.js_on_click(lines_callback) show_points_btn.js_on_click(points_callback) - controls_layout = row( - show_lines_btn, show_points_btn, sizing_mode="scale_width", margin=(10, 0, 10, 0) - ) + controls_layout = row(show_points_btn, sizing_mode="scale_width", margin=(10, 0, 10, 0)) + plot_columns.append(controls_layout) layout = column(*plot_columns, sizing_mode="scale_width") diff --git a/pydatalab/src/pydatalab/utils.py b/pydatalab/src/pydatalab/utils.py index e99975400..d90c9d254 100644 --- a/pydatalab/src/pydatalab/utils.py +++ b/pydatalab/src/pydatalab/utils.py @@ -53,3 +53,22 @@ class BSONProvider(DefaultJSONProvider): @staticmethod def default(o): return CustomJSONEncoder.default(o) + + +def shrink_label(label: str | None, max_length: int = 15) -> str: + """Shrink label to fit within max_length, preserving file extension when possible.""" + if not label or len(label) <= max_length: + return label or "" + + if "." in label: + name, ext = label.rsplit(".", 1) + if len(ext) < 6: + available = max_length - len(ext) - 4 + if available > 3: + return f"{name[:available]}...{ext}" + else: + return f"{label[:12]}..." + else: + return f"{label[:12]}..." + else: + return f"{label[:12]}..." diff --git a/webapp/src/components/datablocks/BokehBlock.vue b/webapp/src/components/datablocks/BokehBlock.vue index 7b31822a6..0b882b35b 100644 --- a/webapp/src/components/datablocks/BokehBlock.vue +++ b/webapp/src/components/datablocks/BokehBlock.vue @@ -10,7 +10,7 @@ DataBlockBase as a prop, and save from within DataBlockBase --> update-block-on-change /> -
+
@@ -70,4 +70,8 @@ export default { }; - + From 1947112ae371ea670291f8570ab77fd093d8b189 Mon Sep 17 00:00:00 2001 From: Benjamin CHARMES Date: Thu, 10 Jul 2025 16:04:37 +0200 Subject: [PATCH 09/13] Better display legend filenames --- pydatalab/src/pydatalab/bokeh_plots.py | 44 ++++++++++++++++++++------ pydatalab/src/pydatalab/utils.py | 32 ++++++++++++------- 2 files changed, 54 insertions(+), 22 deletions(-) diff --git a/pydatalab/src/pydatalab/bokeh_plots.py b/pydatalab/src/pydatalab/bokeh_plots.py index 00f0aa605..9df6594b1 100644 --- a/pydatalab/src/pydatalab/bokeh_plots.py +++ b/pydatalab/src/pydatalab/bokeh_plots.py @@ -37,6 +37,10 @@ if (line1) {line1.glyph.x.field = column;} source.change.emit(); xaxis.axis_label = column; + + if (hover_tool) { + hover_tool.tooltips = [[column, "$x{0.00}"], [hover_tool.tooltips[1][0], "$y{0.00}"]]; + } """ SELECTABLE_CALLBACK_y = """ var column = cb_obj.value; @@ -44,6 +48,10 @@ if (line1) {line1.glyph.y.field = column;} source.change.emit(); yaxis.axis_label = column; + + if (hover_tool) { + hover_tool.tooltips = [[hover_tool.tooltips[0][0], "$x{0.00}"], [column, "$y{0.00}"]]; + } """ GENERATE_CSV_CALLBACK = """ let columns = Object.keys(source.data); @@ -238,7 +246,9 @@ def selectable_axes_plot( ) if tools is None: - coordinate_hover = HoverTool(tooltips=[("X", "$x{0.00}"), ("Y", "$y{0.00}")], mode="mouse") + coordinate_hover = HoverTool( + tooltips=[(x_axis_label, "$x{0.00}"), (y_axis_label, "$y{0.00}")], mode="mouse" + ) p.add_tools(coordinate_hover) p.toolbar.logo = "grey" @@ -265,7 +275,13 @@ def selectable_axes_plot( labels = [] if isinstance(df, dict): - labels = list(df.keys()) + original_labels = list(df.keys()) + else: + original_labels = [ + df_.index.name if df_.index.name else f"Dataset {i}" for i, df_ in enumerate(df) + ] + + labels = [shrink_label(label) for label in original_labels] plot_columns = [] @@ -276,12 +292,7 @@ def selectable_axes_plot( if isinstance(df, dict): df_ = df[df_] - if labels: - label = labels[ind] - else: - label = df_.index.name if len(df) > 1 else "" - - label = shrink_label(label) + label = labels[ind] if ind < len(labels) else "" source = ColumnDataSource(df_) @@ -353,13 +364,26 @@ def selectable_axes_plot( callbacks_x.append( CustomJS( - args=dict(circle1=circles, line1=lines, source=source, xaxis=p.xaxis[0]), + args=dict( + circle1=circles, + line1=lines, + source=source, + xaxis=p.xaxis[0], + hover_tool=coordinate_hover, + ), code=SELECTABLE_CALLBACK_x, ) ) + callbacks_y.append( CustomJS( - args=dict(circle1=circles, line1=lines, source=source, yaxis=p.yaxis[0]), + args=dict( + circle1=circles, + line1=lines, + source=source, + yaxis=p.yaxis[0], + hover_tool=coordinate_hover, + ), code=SELECTABLE_CALLBACK_y, ) ) diff --git a/pydatalab/src/pydatalab/utils.py b/pydatalab/src/pydatalab/utils.py index d90c9d254..78055869d 100644 --- a/pydatalab/src/pydatalab/utils.py +++ b/pydatalab/src/pydatalab/utils.py @@ -55,20 +55,28 @@ def default(o): return CustomJSONEncoder.default(o) -def shrink_label(label: str | None, max_length: int = 15) -> str: - """Shrink label to fit within max_length, preserving file extension when possible.""" - if not label or len(label) <= max_length: - return label or "" +def shrink_label(label: str | None, max_length: int = 10) -> str: + """Shrink label to exactly max_length chars with format: start...end.ext""" + if not label: + return "" + + if len(label) <= max_length: + return label if "." in label: name, ext = label.rsplit(".", 1) - if len(ext) < 6: - available = max_length - len(ext) - 4 - if available > 3: - return f"{name[:available]}...{ext}" - else: - return f"{label[:12]}..." + + extension_length = len(ext) + 1 + + available_for_start = max_length - extension_length - 4 + + if available_for_start >= 1: + name_start = name[:available_for_start] + last_char = name[-1] + return f"{name_start}...{last_char}.{ext}" else: - return f"{label[:12]}..." + name_start = name[0] + last_char = name[-1] + return f"{name_start}...{last_char}.{ext}" else: - return f"{label[:12]}..." + return label[: max_length - 3] + "..." From 932db8d569dd3183ecc072d11be5f3205eb5ecf0 Mon Sep 17 00:00:00 2001 From: Benjamin CHARMES Date: Thu, 10 Jul 2025 16:22:28 +0200 Subject: [PATCH 10/13] Add filenames in the hovertool --- pydatalab/src/pydatalab/bokeh_plots.py | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/pydatalab/src/pydatalab/bokeh_plots.py b/pydatalab/src/pydatalab/bokeh_plots.py index 9df6594b1..eb2aa379f 100644 --- a/pydatalab/src/pydatalab/bokeh_plots.py +++ b/pydatalab/src/pydatalab/bokeh_plots.py @@ -39,7 +39,7 @@ xaxis.axis_label = column; if (hover_tool) { - hover_tool.tooltips = [[column, "$x{0.00}"], [hover_tool.tooltips[1][0], "$y{0.00}"]]; + hover_tool.tooltips = [["File", "@filename"], [column, "$x{0.00}"], [hover_tool.tooltips[2][0], "$y{0.00}"]]; } """ SELECTABLE_CALLBACK_y = """ @@ -50,9 +50,10 @@ yaxis.axis_label = column; if (hover_tool) { - hover_tool.tooltips = [[hover_tool.tooltips[0][0], "$x{0.00}"], [column, "$y{0.00}"]]; + hover_tool.tooltips = [["File", "@filename"], [hover_tool.tooltips[1][0], "$x{0.00}"], [column, "$y{0.00}"]]; } """ + GENERATE_CSV_CALLBACK = """ let columns = Object.keys(source.data); console.log(columns); @@ -247,9 +248,14 @@ def selectable_axes_plot( if tools is None: coordinate_hover = HoverTool( - tooltips=[(x_axis_label, "$x{0.00}"), (y_axis_label, "$y{0.00}")], mode="mouse" + tooltips=[ + ("File", "@filename"), + (x_axis_label, "$x{0.00}"), + (y_axis_label, "$y{0.00}"), + ], + mode="mouse", ) - p.add_tools(coordinate_hover) + p.add_tools(coordinate_hover) p.toolbar.logo = "grey" @@ -291,10 +297,16 @@ def selectable_axes_plot( if isinstance(df, dict): df_ = df[df_] + filename = list(df.keys())[ind] + else: + filename = original_labels[ind] label = labels[ind] if ind < len(labels) else "" - source = ColumnDataSource(df_) + df_with_filename = df_.copy() + df_with_filename["filename"] = filename + + source = ColumnDataSource(df_with_filename) if color_options: color = {"field": color_options[0], "transform": color_mapper} From 5bde79724477c90c95107573d4ec67dbd262d2c2 Mon Sep 17 00:00:00 2001 From: Benjamin CHARMES Date: Thu, 10 Jul 2025 16:25:50 +0200 Subject: [PATCH 11/13] Hovertool show original_y value and not stagger value --- pydatalab/src/pydatalab/apps/xrd/blocks.py | 9 +++++ pydatalab/src/pydatalab/bokeh_plots.py | 40 +++++++++++++++++----- 2 files changed, 40 insertions(+), 9 deletions(-) diff --git a/pydatalab/src/pydatalab/apps/xrd/blocks.py b/pydatalab/src/pydatalab/apps/xrd/blocks.py index 1f11b401d..046ddca15 100644 --- a/pydatalab/src/pydatalab/apps/xrd/blocks.py +++ b/pydatalab/src/pydatalab/apps/xrd/blocks.py @@ -288,8 +288,17 @@ def generate_xrd_plot(self) -> None: offset = ind * stagger_offset for col in df.columns: if "intensity" in col.lower(): + original_col = f"{col}_original" + df[original_col] = df[col].copy() + df[col] = df[col].astype("float64") df[col] = df[col] + offset + else: + for df in pattern_dfs: + for col in df.columns: + if "intensity" in col.lower(): + original_col = f"{col}_original" + df[original_col] = df[col].copy() p = selectable_axes_plot( pattern_dfs, diff --git a/pydatalab/src/pydatalab/bokeh_plots.py b/pydatalab/src/pydatalab/bokeh_plots.py index eb2aa379f..075ad4340 100644 --- a/pydatalab/src/pydatalab/bokeh_plots.py +++ b/pydatalab/src/pydatalab/bokeh_plots.py @@ -50,7 +50,28 @@ yaxis.axis_label = column; if (hover_tool) { - hover_tool.tooltips = [["File", "@filename"], [hover_tool.tooltips[1][0], "$x{0.00}"], [column, "$y{0.00}"]]; + var tooltips = [["File", "@filename"], [hover_tool.tooltips[1][0], "$x{0.00}"]]; + + if (column.toLowerCase().includes('intensity')) { + var original_column = column + "_original"; + var column_exists = false; + for (var key in source.data) { + if (key === original_column) { + column_exists = true; + break; + } + } + + if (column_exists) { + tooltips.push([column, "@{" + original_column + "}{0.00}"]); + } else { + tooltips.push([column, "$y{0.00}"]); + } + } else { + tooltips.push([column, "$y{0.00}"]); + } + + hover_tool.tooltips = tooltips; } """ @@ -247,14 +268,15 @@ def selectable_axes_plot( ) if tools is None: - coordinate_hover = HoverTool( - tooltips=[ - ("File", "@filename"), - (x_axis_label, "$x{0.00}"), - (y_axis_label, "$y{0.00}"), - ], - mode="mouse", - ) + base_tooltips = [("File:", "@filename"), (x_axis_label, "$x{0.00}")] + + if "intensity" in y_label.lower(): + original_col = f"{y_label}_original" + base_tooltips.append((y_axis_label, f"@{{{original_col}}}{{0.00}}")) + else: + base_tooltips.append((y_axis_label, "$y{0.00}")) + + coordinate_hover = HoverTool(tooltips=base_tooltips, mode="mouse") p.add_tools(coordinate_hover) p.toolbar.logo = "grey" From 1a883ff8b54dd97137f53fbec217de16bb4626be Mon Sep 17 00:00:00 2001 From: Benjamin CHARMES Date: Thu, 10 Jul 2025 16:28:16 +0200 Subject: [PATCH 12/13] Remove click_policy on plot legend --- pydatalab/src/pydatalab/bokeh_plots.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pydatalab/src/pydatalab/bokeh_plots.py b/pydatalab/src/pydatalab/bokeh_plots.py index 075ad4340..1fa5eebf8 100644 --- a/pydatalab/src/pydatalab/bokeh_plots.py +++ b/pydatalab/src/pydatalab/bokeh_plots.py @@ -436,7 +436,7 @@ def selectable_axes_plot( yaxis_select.js_on_change("value", *callbacks_y) if p.legend: - p.legend.click_policy = "hide" + p.legend.click_policy = "none" if len(df) <= 1: p.legend.visible = False else: @@ -447,7 +447,7 @@ def selectable_axes_plot( external_legend = Legend( items=legend_items, - click_policy="hide", + click_policy="none", background_fill_alpha=0.8, label_text_font_size="9pt", spacing=1, From c2ac3fa52e145d18ae64918bacd0ff0b4ab5eec6 Mon Sep 17 00:00:00 2001 From: Benjamin CHARMES Date: Fri, 11 Jul 2025 10:08:31 +0200 Subject: [PATCH 13/13] Fix python test and improve hover --- pydatalab/src/pydatalab/bokeh_plots.py | 36 ++++++++++++++++---------- 1 file changed, 22 insertions(+), 14 deletions(-) diff --git a/pydatalab/src/pydatalab/bokeh_plots.py b/pydatalab/src/pydatalab/bokeh_plots.py index 1fa5eebf8..ea24e5eb8 100644 --- a/pydatalab/src/pydatalab/bokeh_plots.py +++ b/pydatalab/src/pydatalab/bokeh_plots.py @@ -267,18 +267,6 @@ def selectable_axes_plot( **kwargs, ) - if tools is None: - base_tooltips = [("File:", "@filename"), (x_axis_label, "$x{0.00}")] - - if "intensity" in y_label.lower(): - original_col = f"{y_label}_original" - base_tooltips.append((y_axis_label, f"@{{{original_col}}}{{0.00}}")) - else: - base_tooltips.append((y_axis_label, "$y{0.00}")) - - coordinate_hover = HoverTool(tooltips=base_tooltips, mode="mouse") - p.add_tools(coordinate_hover) - p.toolbar.logo = "grey" if tools: @@ -312,6 +300,7 @@ def selectable_axes_plot( labels = [shrink_label(label) for label in original_labels] plot_columns = [] + hover_tools = [] for ind, df_ in enumerate(df): if skip_plot: @@ -330,6 +319,17 @@ def selectable_axes_plot( source = ColumnDataSource(df_with_filename) + current_tooltips = [("File:", "@filename"), (x_axis_label, "$x{0.00}")] + + if "intensity" in y_label.lower(): + original_col = f"{y_label}_original" + if original_col in source.data: + current_tooltips.append((y_axis_label, f"@{{{original_col}}}{{0.00}}")) + else: + current_tooltips.append((y_axis_label, f"@{{{y_default}}}")) + else: + current_tooltips.append((y_axis_label, f"@{{{y_default}}}{{0.00}}")) + if color_options: color = {"field": color_options[0], "transform": color_mapper} line_color = "black" @@ -381,6 +381,14 @@ def selectable_axes_plot( else None ) + line_hover = None + if lines: + line_hover = HoverTool( + tooltips=current_tooltips, mode="mouse", renderers=[lines], line_policy="nearest" + ) + p.add_tools(line_hover) + hover_tools.append(line_hover) + if y_aux: for y in y_aux: aux_lines = ( # noqa @@ -403,7 +411,7 @@ def selectable_axes_plot( line1=lines, source=source, xaxis=p.xaxis[0], - hover_tool=coordinate_hover, + hover_tool=line_hover, ), code=SELECTABLE_CALLBACK_x, ) @@ -416,7 +424,7 @@ def selectable_axes_plot( line1=lines, source=source, yaxis=p.yaxis[0], - hover_tool=coordinate_hover, + hover_tool=line_hover, ), code=SELECTABLE_CALLBACK_y, )