Source code for qimchi.components.metadata
import json
from datetime import datetime
from pathlib import Path
from dash import Input, Output, State, callback, html, dcc
# Local imports
from qimchi.components.utils import read_data
from qimchi.state import load_state_from_disk
from qimchi.logger import logger
[docs]
def metadata_viewer() -> html.Div:
"""
Metadata viewer
Returns:
dash.html.Div: Div element, displaying all the metadata relevant to the data
"""
return html.Div(
[
html.Div(
[
html.Div(id="display-session-id2"),
html.H5(
html.B("Metadata"), className="is-size-5 is-4 is-pulled-left"
),
# Toggle button to show/hide metadata
html.Button(
html.I(className="fa-solid fa-eye-slash"),
id="hide-metadata",
className="button is-info is-pulled-right",
),
],
className="column is-full mt-0",
style={
"minHeight": "4rem",
},
),
# Metadata viewer
html.P(
"",
className="column is-full mt-0",
id="metadata-viewer",
),
],
className="has-background-light",
)
def _render_tree(data: dict) -> html.Ul | html.Span:
"""
Recursive function to render collapsible items
"""
if isinstance(data, dict):
return html.Ul(
[
html.Li(
# If the value is an empty list, empty string, empty dict, or None, render as "key: value" without a collapsible arrow
(
html.Span(
[
html.Strong(f"{key}", style={"color": "black"}),
f": {value}",
]
)
if value in [None, [], "", {}]
else (
html.Span(
[
html.Strong(f"{key}", style={"color": "black"}),
f": {value}",
]
)
if not isinstance(value, dict)
# Otherwise, make it collapsible, if it's a dictionary
else html.Details(
[
html.Summary(
html.Strong(key, style={"color": "black"}),
style={"cursor": "pointer"},
),
_render_tree(
value
), # NOTE: Recursively render nested dicts
]
)
)
),
style={
"listStyleType": "none",
"marginLeft": "20px",
}, # No bullets, indented
)
for key, value in data.items()
],
style={"paddingLeft": "0px"},
)
else:
# Render non-dict values directly as strings in a span
return html.Span(str(data))
[docs]
@callback(
Output("metadata-viewer", "children"),
State("session-id", "data"),
Input("load-signal", "data"),
)
def update_metadata_viewer(sess_id: str, sig: int) -> list:
"""
Callback to update metadata viewer
Args:
sess_id (str): Session ID to load the state for
sig (int): Signal to indicate that data has been updated
Returns:
list: Metadata list to be displayed in the metadata viewer
"""
_state = load_state_from_disk(sess_id)
if sig in [None, 0]:
logger.warning("update_metadata_viewer | No data selected")
return f"Session ID: {sess_id} | No Data Selected"
data = read_data(sess_id, src="update_metadata_viewer")
metadata = data.attrs
try:
dt = datetime.fromisoformat(metadata["Timestamp"])
meta = []
meta.append(html.B(f"Session ID: {sess_id}"))
meta.append(html.Br())
meta_expandable = []
keys_order = [
"Timestamp",
"Cryostat",
"Wafer ID",
"Device Type",
"Sample Name",
"Experiment Name",
"Measurement ID",
"Sweeps",
"Extra Metadata",
"Instruments Snapshot",
"Parameters Snapshot",
]
# Sort keys in the order defined above
metadata = {k: metadata[k] for k in keys_order if k in metadata}
json_keys = [
"Sweeps",
"Extra Metadata",
"Instruments Snapshot",
"Parameters Snapshot",
]
# Store "Parameters Snapshot" in _state
if "Parameters Snapshot" in metadata:
_state.parameters_snapshot = metadata["Parameters Snapshot"]
_state.save_state()
for key in metadata:
if key == "Timestamp":
meta.append(html.B(f"{key}: "))
meta.append(dt.strftime("%Y-%m-%d %H:%M:%S"))
meta.append(html.Br())
elif key == "Cryostat":
meta.append(html.B(f"{key}: "))
data = metadata[key].capitalize()
meta.append(data)
meta.append(html.Br())
elif key in json_keys:
try:
data: str = metadata[key]
data: dict = json.loads(data)
except Exception as err:
logger.error(
f"update_metadata_viewer | Error: {err}", exc_info=True
)
raise err
# If dict is empty, skip the key
if not data:
logger.debug(
f"update_metadata_viewer | Key: '{key}' is empty; skipping rendering."
)
continue
meta_expandable.append(html.B(f"{key}: "))
for k, v in data.items(): # key: str | val: dict
meta_expandable.append(
html.Details(
[
html.Summary(
html.Strong(
f"{k.upper()}", style={"color": "black"}
),
style={"cursor": "pointer"},
),
# BUG: Instruments Snapshot rendering too slow because it's too big
# FIXME: This is a workaround to render the Instruments Snapshot as a string
_render_tree(
str(v) if key == "Instruments Snapshot" else v
),
],
className="ml-4",
),
)
else:
meta.append(html.B(f"{key}: "))
meta.append(metadata[key])
meta.append(html.Br())
# Full path to the current dataset with a download button
meta.extend(
[
dcc.Clipboard(
content=str(Path(_state.measurement_path).resolve()),
title="Copy Path",
className="button is-info is-small",
style={
"height": "2rem",
"width": "2.5rem",
},
),
dcc.Download(id="download-dataset"),
html.Button(
html.I(className="fa-solid fa-download"),
id="download-dataset-btn",
title="Download Dataset",
className="button is-info is-small",
style={
"height": "2rem",
"width": "2.5rem",
},
),
html.Br(),
]
)
# Non-collapsible stuff to the left & collapsible stuff to the right
meta = html.Div(
[
html.Div(meta, style={"float": "left", "width": "50%"}),
html.Div(meta_expandable, style={"float": "right", "width": "50%"}),
]
)
return meta
except Exception as err:
logger.error(f"update_metadata_viewer | Error: {err}", exc_info=True)
return f"Session ID: {sess_id} | No readable metadata found!"
[docs]
@callback(
Output("metadata-viewer", "className"),
Output("hide-metadata", "children"),
Input("hide-metadata", "n_clicks"),
)
def show_hide_metadata(n_clicks: int) -> tuple:
"""
Callback to show or hide the metadata viewer
Args:
n_clicks (int): Number of times the button has been clicked
Returns:
tuple: className to display the metadata viewer and the updated button icon
"""
if n_clicks is None or n_clicks % 2 == 0:
return "column is-full mt-0", html.I(className="fa-solid fa-eye-slash")
else:
return "column is-hidden mt-0", html.I(className="fa-solid fa-eye")