Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 56 additions & 7 deletions pydatalab/src/pydatalab/apps/nmr/blocks.py
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,37 @@ def generate_nmr_plot(self, parse: bool = True):

"""
if parse:
if not self.data.get("file_id"):
file_ids = self.data.get("file_ids")
if file_ids and len(file_ids) > 0:
all_dfs = []

for file_id in self.data["file_ids"]:
try:
file_info = get_file_info_by_id(file_id, update_if_live=True)
df = self.load_nmr_data(file_info)

if df:
df_converted = pd.DataFrame(df)
df_converted["normalized intensity"] = (
df_converted.intensity / df_converted.intensity.max()
)
df_converted.index.name = file_info.get("name", "unknown")
all_dfs.append(df_converted)
except Exception as exc:
warnings.warn(f"Could not load NMR file {file_id}: {exc}")
continue

if not all_dfs:
warnings.warn("No compatible NMR data could be loaded from file_ids")
return

metadata = self.data.get("metadata", {})
self.data["bokeh_plot_data"] = self.make_nmr_plot(
all_dfs[0], metadata, extra_dfs=all_dfs[1:]
)
return

elif not self.data.get("file_id"):
return None

file_info = get_file_info_by_id(self.data["file_id"], update_if_live=True)
Expand Down Expand Up @@ -256,11 +286,13 @@ def generate_nmr_plot(self, parse: bool = True):
self.data["bokeh_plot_data"] = self.make_nmr_plot(df, self.data["metadata"])

@classmethod
def make_nmr_plot(cls, df: pd.DataFrame, metadata: dict[str, Any]) -> str:
def make_nmr_plot(
cls, df: pd.DataFrame, metadata: dict[str, Any], extra_dfs: list[pd.DataFrame] | None = None
) -> str:
"""Create a Bokeh plot for the NMR data stored in the dataframe and metadata."""
nucleus_label = metadata.get("nucleus") or ""
# replace numbers with superscripts
nucleus_label = nucleus_label.translate(str.maketrans("0123456789", "⁰¹²³⁴⁵⁶⁷⁸⁹"))

df.rename(
{
"ppm": f"{nucleus_label} chemical shift (ppm)",
Expand All @@ -273,8 +305,24 @@ def make_nmr_plot(cls, df: pd.DataFrame, metadata: dict[str, Any]) -> str:
inplace=True,
)

dfs_to_plot = [df]
if extra_dfs:
for extra_df in extra_dfs:
extra_df.rename(
{
"ppm": f"{nucleus_label} chemical shift (ppm)",
"hz": f"{nucleus_label} chemical shift (Hz)",
"intensity": "Intensity",
"intensity_per_scan": "Intensity per scan",
"normalized intensity": "Normalized intensity",
},
axis=1,
inplace=True,
)
dfs_to_plot.append(extra_df)

bokeh_layout = selectable_axes_plot(
df,
dfs_to_plot,
x_options=[
f"{nucleus_label} chemical shift (ppm)",
f"{nucleus_label} chemical shift (Hz)",
Expand All @@ -287,8 +335,9 @@ def make_nmr_plot(cls, df: pd.DataFrame, metadata: dict[str, Any]) -> str:
plot_line=True,
point_size=3,
)
# flip x axis, per NMR convention. Note that the figure is the second element
# of the layout in the current implementation, but this could be fragile.
bokeh_layout.children[1].x_range.flipped = True
for child in bokeh_layout.children:
if hasattr(child, "x_range"):
child.x_range.flipped = True
break

return bokeh.embed.json_item(bokeh_layout, theme=DATALAB_BOKEH_THEME)
42 changes: 36 additions & 6 deletions pydatalab/src/pydatalab/apps/xrd/blocks.py
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,10 @@ def _calc_baselines_and_normalize(
def generate_xrd_plot(self, filenames: list[str | Path] | None = None) -> None:
"""Generate a Bokeh plot potentially containing multiple XRD patterns.

This function will first check whether a `file_id` is set in the block data.
This function will first check whether `file_ids` is set in the block data.
If so, it will load those specific files.

Otherwise, it will check whether a `file_id` is set in the block data.
If not, it will interpret this as the "all compatible files" option, and will
look into the item data to find all attached files, and attempt to read them as
XRD patterns.
Expand All @@ -220,9 +223,38 @@ def generate_xrd_plot(self, filenames: list[str | Path] | None = None) -> None:
file_info = None
all_files = None
pattern_dfs = None
y_options: list[str] = []
peak_data: dict = {}

file_ids = self.data.get("file_ids")
if file_ids and len(file_ids) > 0 and filenames is None:
pattern_dfs = []
peak_information = {}

for ind, file_id in enumerate(self.data["file_ids"]):
try:
file_info = get_file_info_by_id(file_id, update_if_live=True)
peak_data = {}
pattern_df, y_options, peak_data = self.load_pattern(
file_info["location"],
wavelength=float(self.data.get("wavelength", self.defaults["wavelength"])),
)
pattern_df.attrs["item_id"] = self.data.get("item_id", "unknown")
pattern_df.attrs["original_filename"] = file_info.get("name", "unknown")
pattern_df.attrs["wavelength"] = (
f"{self.data.get('wavelength', self.defaults['wavelength'])} Å"
)
peak_information[str(file_id)] = PeakInformation(**peak_data).dict()
pattern_df["normalized intensity (staggered)"] += ind
pattern_dfs.append(pattern_df)
except Exception as exc:
warnings.warn(f"Could not parse file {file_id} as XRD data. Error: {exc}")
continue

self.data["computed"] = {}
self.data["computed"]["peak_data"] = peak_information

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

pattern_dfs = []
peak_information = {}
y_options: list[str] = []
for ind, f in enumerate(all_files):
try:
peak_data: dict = {}
peak_data = {}
pattern_df, y_options, peak_data = self.load_pattern(
f["location"],
wavelength=float(self.data.get("wavelength", self.defaults["wavelength"])),
Expand Down Expand Up @@ -312,7 +343,6 @@ def generate_xrd_plot(self, filenames: list[str | Path] | None = None) -> None:
if self.data.get("computed") is None:
self.data["computed"] = {"peak_data": {}}
self.data["computed"]["peak_data"][f] = peak_model.dict()
pattern_dfs = [pattern_df]

if pattern_dfs:
p = self._make_plots(pattern_dfs, y_options)
Expand Down
89 changes: 89 additions & 0 deletions webapp/src/components/MetadataViewer.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
<template>
<div v-if="displayedMetadata && Object.keys(displayedMetadata).length > 0">
<table class="table table-sm">
<tbody>
<tr v-for="(value, key) in displayedMetadata" :key="key">
<th scope="row">{{ formatLabel(key) }}</th>
<td>{{ formatValue(key, value) }}</td>
</tr>
</tbody>
</table>
</div>
</template>

<script>
export default {
props: {
metadata: {
type: Object,
default: () => ({}),
},
labels: {
type: Object,
default: () => ({}),
},
excludeKeys: {
type: Array,
default: () => [],
},
},
computed: {
displayedMetadata() {
if (!this.metadata) return {};

const filtered = {};
for (const [key, value] of Object.entries(this.metadata)) {
if (!this.excludeKeys.includes(key) && value !== null && value !== undefined) {
filtered[key] = value;
}
}
return filtered;
},
},
methods: {
formatLabel(key) {
if (this.labels[key]) {
return this.labels[key];
}

return key
.replace(/_/g, " ")
.replace(/([A-Z])/g, " $1")
.trim()
.replace(/^\w/, (c) => c.toUpperCase());
},
formatValue(key, value) {
if (value === null || value === undefined) {
return "";
}

if (typeof value === "object") {
if (Array.isArray(value)) {
return value.join(", ");
}
return JSON.stringify(value);
}

const keyLower = key.toLowerCase();
if (keyLower.includes("_hz") && !isNaN(value)) {
return `${value} Hz`;
}
if (keyLower.includes("_mhz") && !isNaN(value)) {
return `${value} MHz`;
}
if (keyLower.includes("_s") && !isNaN(value)) {
return `${value} s`;
}

return value;
},
},
};
</script>

<style scoped>
th {
color: #454545;
font-weight: 500;
}
</style>
20 changes: 11 additions & 9 deletions webapp/src/components/datablocks/BokehBlock.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,19 @@
<!-- think about elegant two-way binding to DataBlockBase... or, just pass all the block data into
DataBlockBase as a prop, and save from within DataBlockBase -->
<DataBlockBase :item_id="item_id" :block_id="block_id">
<FileSelectDropdown
v-model="file_id"
:item_id="item_id"
:block_id="block_id"
:extensions="blockInfo?.attributes?.accepted_file_extensions"
update-block-on-change
/>
<template #controls>
<FileSelectDropdown
v-model="file_id"
:item_id="item_id"
:block_id="block_id"
:extensions="blockInfo?.attributes?.accepted_file_extensions"
update-block-on-change
/>
</template>

<div id="bokehPlotContainer" class="limited-width">
<template #plot>
<BokehPlot :bokeh-plot-data="bokehPlotData" />
</div>
</template>
</DataBlockBase>
</template>

Expand Down
Loading