Skip to content

Commit 67b59c1

Browse files
Use MultiFileSelectDropdown on XRD and NMR Block
1 parent 5920ede commit 67b59c1

File tree

12 files changed

+790
-529
lines changed

12 files changed

+790
-529
lines changed

pydatalab/src/pydatalab/apps/nmr/blocks.py

Lines changed: 55 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -228,7 +228,36 @@ def generate_nmr_plot(self, parse: bool = True):
228228
229229
"""
230230
if parse:
231-
if not self.data.get("file_id"):
231+
if self.data.get("file_ids") and len(self.data.get("file_ids")) > 0:
232+
all_dfs = []
233+
234+
for file_id in self.data["file_ids"]:
235+
try:
236+
file_info = get_file_info_by_id(file_id, update_if_live=True)
237+
df = self.load_nmr_data(file_info)
238+
239+
if df:
240+
df_converted = pd.DataFrame(df)
241+
df_converted["normalized intensity"] = (
242+
df_converted.intensity / df_converted.intensity.max()
243+
)
244+
df_converted.index.name = file_info.get("name", "unknown")
245+
all_dfs.append(df_converted)
246+
except Exception as exc:
247+
warnings.warn(f"Could not load NMR file {file_id}: {exc}")
248+
continue
249+
250+
if not all_dfs:
251+
warnings.warn("No compatible NMR data could be loaded from file_ids")
252+
return
253+
254+
metadata = self.data.get("metadata", {})
255+
self.data["bokeh_plot_data"] = self.make_nmr_plot(
256+
all_dfs[0], metadata, extra_dfs=all_dfs[1:]
257+
)
258+
return
259+
260+
elif not self.data.get("file_id"):
232261
return None
233262

234263
file_info = get_file_info_by_id(self.data["file_id"], update_if_live=True)
@@ -256,11 +285,13 @@ def generate_nmr_plot(self, parse: bool = True):
256285
self.data["bokeh_plot_data"] = self.make_nmr_plot(df, self.data["metadata"])
257286

258287
@classmethod
259-
def make_nmr_plot(cls, df: pd.DataFrame, metadata: dict[str, Any]) -> str:
288+
def make_nmr_plot(
289+
cls, df: pd.DataFrame, metadata: dict[str, Any], extra_dfs: list[pd.DataFrame] | None = None
290+
) -> str:
260291
"""Create a Bokeh plot for the NMR data stored in the dataframe and metadata."""
261292
nucleus_label = metadata.get("nucleus") or ""
262-
# replace numbers with superscripts
263293
nucleus_label = nucleus_label.translate(str.maketrans("0123456789", "⁰¹²³⁴⁵⁶⁷⁸⁹"))
294+
264295
df.rename(
265296
{
266297
"ppm": f"{nucleus_label} chemical shift (ppm)",
@@ -273,8 +304,24 @@ def make_nmr_plot(cls, df: pd.DataFrame, metadata: dict[str, Any]) -> str:
273304
inplace=True,
274305
)
275306

307+
dfs_to_plot = [df]
308+
if extra_dfs:
309+
for extra_df in extra_dfs:
310+
extra_df.rename(
311+
{
312+
"ppm": f"{nucleus_label} chemical shift (ppm)",
313+
"hz": f"{nucleus_label} chemical shift (Hz)",
314+
"intensity": "Intensity",
315+
"intensity_per_scan": "Intensity per scan",
316+
"normalized intensity": "Normalized intensity",
317+
},
318+
axis=1,
319+
inplace=True,
320+
)
321+
dfs_to_plot.append(extra_df)
322+
276323
bokeh_layout = selectable_axes_plot(
277-
df,
324+
dfs_to_plot,
278325
x_options=[
279326
f"{nucleus_label} chemical shift (ppm)",
280327
f"{nucleus_label} chemical shift (Hz)",
@@ -287,8 +334,9 @@ def make_nmr_plot(cls, df: pd.DataFrame, metadata: dict[str, Any]) -> str:
287334
plot_line=True,
288335
point_size=3,
289336
)
290-
# flip x axis, per NMR convention. Note that the figure is the second element
291-
# of the layout in the current implementation, but this could be fragile.
292-
bokeh_layout.children[1].x_range.flipped = True
337+
for child in bokeh_layout.children:
338+
if hasattr(child, "x_range"):
339+
child.x_range.flipped = True
340+
break
293341

294342
return bokeh.embed.json_item(bokeh_layout, theme=DATALAB_BOKEH_THEME)

pydatalab/src/pydatalab/apps/xrd/blocks.py

Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -209,7 +209,10 @@ def _calc_baselines_and_normalize(
209209
def generate_xrd_plot(self, filenames: list[str | Path] | None = None) -> None:
210210
"""Generate a Bokeh plot potentially containing multiple XRD patterns.
211211
212-
This function will first check whether a `file_id` is set in the block data.
212+
This function will first check whether `file_ids` is set in the block data.
213+
If so, it will load those specific files.
214+
215+
Otherwise, it will check whether a `file_id` is set in the block data.
213216
If not, it will interpret this as the "all compatible files" option, and will
214217
look into the item data to find all attached files, and attempt to read them as
215218
XRD patterns.
@@ -220,9 +223,37 @@ def generate_xrd_plot(self, filenames: list[str | Path] | None = None) -> None:
220223
file_info = None
221224
all_files = None
222225
pattern_dfs = None
226+
y_options: list[str] = []
227+
peak_data: dict = {}
228+
229+
if self.data.get("file_ids") and len(self.data.get("file_ids")) > 0 and filenames is None:
230+
pattern_dfs = []
231+
peak_information = {}
232+
233+
for ind, file_id in enumerate(self.data["file_ids"]):
234+
try:
235+
file_info = get_file_info_by_id(file_id, update_if_live=True)
236+
peak_data = {}
237+
pattern_df, y_options, peak_data = self.load_pattern(
238+
file_info["location"],
239+
wavelength=float(self.data.get("wavelength", self.defaults["wavelength"])),
240+
)
241+
pattern_df.attrs["item_id"] = self.data.get("item_id", "unknown")
242+
pattern_df.attrs["original_filename"] = file_info.get("name", "unknown")
243+
pattern_df.attrs["wavelength"] = (
244+
f"{self.data.get('wavelength', self.defaults['wavelength'])} Å"
245+
)
246+
peak_information[str(file_id)] = PeakInformation(**peak_data).dict()
247+
pattern_df["normalized intensity (staggered)"] += ind
248+
pattern_dfs.append(pattern_df)
249+
except Exception as exc:
250+
warnings.warn(f"Could not parse file {file_id} as XRD data. Error: {exc}")
251+
continue
252+
253+
self.data["computed"] = {}
254+
self.data["computed"]["peak_data"] = peak_information
223255

224-
if self.data.get("file_id") is None and filenames is None:
225-
# If no file set, try to plot them all
256+
elif self.data.get("file_id") is None and filenames is None:
226257
item_info = flask_mongo.db.items.find_one(
227258
{"item_id": self.data["item_id"]},
228259
projection={"file_ObjectIds": 1},
@@ -247,10 +278,9 @@ def generate_xrd_plot(self, filenames: list[str | Path] | None = None) -> None:
247278

248279
pattern_dfs = []
249280
peak_information = {}
250-
y_options: list[str] = []
251281
for ind, f in enumerate(all_files):
252282
try:
253-
peak_data: dict = {}
283+
peak_data = {}
254284
pattern_df, y_options, peak_data = self.load_pattern(
255285
f["location"],
256286
wavelength=float(self.data.get("wavelength", self.defaults["wavelength"])),
@@ -312,7 +342,6 @@ def generate_xrd_plot(self, filenames: list[str | Path] | None = None) -> None:
312342
if "computed" not in self.data:
313343
self.data["computed"] = {"peak_data": {}}
314344
self.data["computed"]["peak_data"][f] = peak_model.dict()
315-
pattern_dfs = [pattern_df]
316345

317346
if pattern_dfs:
318347
p = self._make_plots(pattern_dfs, y_options)
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
<template>
2+
<div v-if="displayedMetadata && Object.keys(displayedMetadata).length > 0">
3+
<table class="table table-sm">
4+
<tbody>
5+
<tr v-for="(value, key) in displayedMetadata" :key="key">
6+
<th scope="row">{{ formatLabel(key) }}</th>
7+
<td>{{ formatValue(key, value) }}</td>
8+
</tr>
9+
</tbody>
10+
</table>
11+
</div>
12+
</template>
13+
14+
<script>
15+
export default {
16+
props: {
17+
metadata: {
18+
type: Object,
19+
default: () => ({}),
20+
},
21+
labels: {
22+
type: Object,
23+
default: () => ({}),
24+
},
25+
excludeKeys: {
26+
type: Array,
27+
default: () => [],
28+
},
29+
},
30+
computed: {
31+
displayedMetadata() {
32+
if (!this.metadata) return {};
33+
34+
const filtered = {};
35+
for (const [key, value] of Object.entries(this.metadata)) {
36+
if (!this.excludeKeys.includes(key) && value !== null && value !== undefined) {
37+
filtered[key] = value;
38+
}
39+
}
40+
return filtered;
41+
},
42+
},
43+
methods: {
44+
formatLabel(key) {
45+
if (this.labels[key]) {
46+
return this.labels[key];
47+
}
48+
49+
return key
50+
.replace(/_/g, " ")
51+
.replace(/([A-Z])/g, " $1")
52+
.trim()
53+
.replace(/^\w/, (c) => c.toUpperCase());
54+
},
55+
formatValue(key, value) {
56+
if (value === null || value === undefined) {
57+
return "";
58+
}
59+
60+
if (typeof value === "object") {
61+
if (Array.isArray(value)) {
62+
return value.join(", ");
63+
}
64+
return JSON.stringify(value);
65+
}
66+
67+
const keyLower = key.toLowerCase();
68+
if (keyLower.includes("_hz") && !isNaN(value)) {
69+
return `${value} Hz`;
70+
}
71+
if (keyLower.includes("_mhz") && !isNaN(value)) {
72+
return `${value} MHz`;
73+
}
74+
if (keyLower.includes("_s") && !isNaN(value)) {
75+
return `${value} s`;
76+
}
77+
78+
return value;
79+
},
80+
},
81+
};
82+
</script>
83+
84+
<style scoped>
85+
th {
86+
color: #454545;
87+
font-weight: 500;
88+
}
89+
</style>

webapp/src/components/datablocks/BokehBlock.vue

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,19 @@
22
<!-- think about elegant two-way binding to DataBlockBase... or, just pass all the block data into
33
DataBlockBase as a prop, and save from within DataBlockBase -->
44
<DataBlockBase :item_id="item_id" :block_id="block_id">
5-
<FileSelectDropdown
6-
v-model="file_id"
7-
:item_id="item_id"
8-
:block_id="block_id"
9-
:extensions="blockInfo?.attributes?.accepted_file_extensions"
10-
update-block-on-change
11-
/>
5+
<template #controls>
6+
<FileSelectDropdown
7+
v-model="file_id"
8+
:item_id="item_id"
9+
:block_id="block_id"
10+
:extensions="blockInfo?.attributes?.accepted_file_extensions"
11+
update-block-on-change
12+
/>
13+
</template>
1214

13-
<div class="row">
14-
<div id="bokehPlotContainer" class="col-xl-9 col-lg-10 col-md-11 mx-auto">
15-
<BokehPlot :bokeh-plot-data="bokehPlotData" />
16-
</div>
17-
</div>
15+
<template #plot>
16+
<BokehPlot :bokeh-plot-data="bokehPlotData" />
17+
</template>
1818
</DataBlockBase>
1919
</template>
2020

0 commit comments

Comments
 (0)