Skip to content
10 changes: 4 additions & 6 deletions index.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@
EMPTY_BORDER_PX,
make_tab,
get_tab_id,
SAMPLE_TYPE_OPTIONS
)

pd.set_option('future.no_silent_downcasting', True)
Expand Down Expand Up @@ -530,6 +529,8 @@ def plate_mapper(plate_ids, objects):
if str(well.well_position_id) in selected_well_ids:
well.get_sample(app_state).sample_type = sample_type



warnings = collect_missing_sample_warnings(app_state)
show_warning = bool(warnings)
warning_msg = build_warning_msg(warnings)
Expand Down Expand Up @@ -748,9 +749,9 @@ def sync_plate_style(cell_evt, row_data, virtual_row_data, fig):
fills.append(fill_color)
customdata.append([w, st])

if w in error_wells:
if w in error_wells and st in ("Library", "Positive Control"):
line_colors.append(ERROR_BORDER); line_widths.append(3)
elif w in warn_wells:
elif w in warn_wells and st in ("Library", "Positive Control"):
line_colors.append(WARN_BORDER); line_widths.append(3)
else:
line_colors.append(DEFAULT_BORDER); line_widths.append(BORDER_PX)
Expand Down Expand Up @@ -796,8 +797,6 @@ def update_rows_callback(_, app_data_state_json, table_id):

df = plate.dataset(app_state, tidy=False).replace(['None', 'none', 'NaN', 'nan', ''], np.nan).infer_objects(copy=False)

print("Plate dataset: ", df.head(3))

# Keep columns tidy, but do not drop keys AG Grid needs (e.g., well_position_id if you use getRowId)
if is_qpcr:
df = df.dropna(axis=1, how="all")
Expand Down Expand Up @@ -1047,7 +1046,6 @@ def toggle_delete_plate_modal(n_open, n_cancel, n_confirm):
return False
return no_update


# Here we run the app on the specified host and port.
if __name__ == "__main__":
app.run(debug=bfabric_web_apps.DEBUG, port=bfabric_web_apps.PORT, host=bfabric_web_apps.HOST)
Expand Down
4 changes: 2 additions & 2 deletions utils/api_call_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,14 @@ def format_tube_id(val):
return "/".join(parts)
return val

def initialize_sample_attributes(plate: Plate, app_state):
def populate_samples_by_identifier_lookup(plate: Plate, app_state):

B = bfabric_interface.get_wrapper()
formatted_tube_ids = []

for well in plate.plate_wells:
tube_id = str(getattr(well, "file_sample_identifier", "")).strip()
print("tube_id", tube_id)

if tube_id:
formatted = format_tube_id(tube_id)
formatted_tube_ids.append(formatted)
Expand Down
100 changes: 77 additions & 23 deletions utils/plate_render_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,6 @@

pd.set_option('future.no_silent_downcasting', True)

# Mapping of sample types to their display options
SAMPLE_TYPE_OPTIONS = [
{"label": "Negative Control", "value": "Negative Control"},
{"label": "Positive Control", "value": "Positive Control"},
{"label": "Standard", "value": "Standard"},
{"label": "PhiX", "value": "PhiX"},
{"label": "Library", "value": "Library"},
]

SAMPLE_TYPE_COLORS = {
"Negative Control": "#ef5350", # red
"Positive Control": "#2979ff", # blue
Expand Down Expand Up @@ -174,6 +165,23 @@ def build_tab_body(plate, app_state):
import dash_bootstrap_components as dbc
import dash_ag_grid as dag

# Dynamic options for the sample_type dropdown:
if plate.plate_type == "qPCR":
SAMPLE_TYPE_OPTIONS = [
{"label": "Negative Control", "value": "Negative Control"},
{"label": "Positive Control", "value": "Positive Control"},
{"label": "Standard", "value": "Standard"},
{"label": "PhiX", "value": "PhiX"},
{"label": "Library", "value": "Library"},
]
else:
SAMPLE_TYPE_OPTIONS = [
{"label": "Negative Control", "value": "Negative Control"},
{"label": "Positive Control", "value": "Positive Control"},
{"label": "PhiX", "value": "PhiX"},
{"label": "Library", "value": "Library"},
]

pid = str(plate.plate_id) # normalize once; reuse everywhere
is_qpcr = (plate.plate_type == "qPCR")

Expand All @@ -199,8 +207,22 @@ def build_tab_body(plate, app_state):

getRowStyle = {
"styleConditions": [
{"condition": "params.data.__error__ === true", "style": {"backgroundColor": "#ffcccc"}},
{"condition": "params.data.__warning__ === true", "style": {"backgroundColor": "#f9f490"}},
{
"condition": (
"params.data.__error__ === true && " +
"params.data.sample_type !== 'Negative Control' && " +
"params.data.sample_type !== 'PhiX'"
),
"style": {"backgroundColor": "#ffcccc"},
},
{
"condition": (
"params.data.__warning__ === true && " +
"params.data.sample_type !== 'Negative Control' && " +
"params.data.sample_type !== 'PhiX'"
),
"style": {"backgroundColor": "#f9f490"},
},
]
}

Expand Down Expand Up @@ -238,17 +260,29 @@ def _is_num(s):
"cellEditorParams": {"values": [opt["value"] for opt in SAMPLE_TYPE_OPTIONS]},
"singleClickEdit": True,
"cellStyle": {
"function": """(params) => {
const colors = {
'Negative Control': '#ef5350',
'Positive Control': '#2979ff',
'Standard': '#153677',
'PhiX': '#ffd600',
'Library': '#43a047'
};
const c = colors[params.value] || '';
return { backgroundColor: c, color: (params.value==='PhiX'?'black':'white') };
}"""
"styleConditions": [
{
"condition": "params.value === 'Negative Control'",
"style": {"backgroundColor": "rgb(247,169,168)", "color": "black", "borderRadius": "10px"},
},
{
"condition": "params.value === 'Positive Control'",
"style": {"backgroundColor": "rgb(148,188,255)", "color": "black", "borderRadius": "10px"},
},
{
"condition": "params.value === 'Standard'",
"style": {"backgroundColor": "rgb(138,154,187)", "color": "black", "borderRadius": "10px"},
},
{
"condition": "params.value === 'PhiX'",
"style": {"backgroundColor": "rgb(255,235,127)", "color": "black", "borderRadius": "10px"},
},
{
"condition": "params.value === 'Library'",
"style": {"backgroundColor": "rgb(161,207,163)", "color": "black", "borderRadius": "10px"},
},
],
"defaultStyle": {"backgroundColor": "white", "color": "black"},
},
"cellRenderer": {
"function": {"code": """
Expand All @@ -269,7 +303,9 @@ def _is_num(s):
# ensure sample_id is editable and right after sample_type
col_sample_id = {
"field": "sample_id",
"editable": True,
"editable": {
"function": "params.data.sample_type === 'Library' || params.data.sample_type === 'Positive Control'"
},
"filter": "agTextColumnFilter",
"valueFormatter": {"function": "params.value == null ? '' : params.value"},
}
Expand Down Expand Up @@ -422,6 +458,24 @@ def _is_num(s):
)
])


delete_plate_modal = html.Div([
dbc.Modal(
[
dbc.ModalHeader(dbc.ModalTitle("Delete Plate")),
dbc.ModalBody(
"Are you sure you want to delete this plate? This will remove all associated wells and their data. This action cannot be undone."
),
dbc.ModalFooter([
dbc.Button("Delete", id={"type": "confirm-delete-plate", "index": pid}, color="danger"),
dbc.Button("Cancel", id={"type": "cancel-delete-plate", "index": pid}, className="ms-auto"),
]),
],
id={"type": "modal-delete-plate", "index": pid},
is_open=False, backdrop="static",
)
])

dummy = html.Div(id={"type": "dummy-div", "index": pid}, style={"display": "none"})

return [table_wrap, plate_wrap, buttons_row, delete_plate_modal, flagging_modal, delete_rows_modal, dummy]
64 changes: 57 additions & 7 deletions utils/plates_callback_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from dash import html
from models import Plate, PlateWell, Measurement, AppSpecificFile, Sample
from app_state import AppState
from .api_call_utils import initialize_sample_attributes, safe_str
from .api_call_utils import populate_samples_by_identifier_lookup, safe_str
from bfabric_web_apps.objects.BfabricInterface import bfabric_interface

def find_and_populate_samples_by_sample_id(
Expand Down Expand Up @@ -45,12 +45,14 @@ def find_and_populate_samples_by_sample_id(
sample_name = safe_str(bfabric_sample.get("name"))
container_id = safe_str(bfabric_sample.get("container", {}).get("id"))
well.well_note = "Sample found"
sample_type = well.get_sample(app_state).sample_type

new_sample = Sample(
sample_id=sample_id,
sample_name=sample_name,
container=container_id,
file_sample_identifier=tube_id
file_sample_identifier=tube_id,
sample_type=sample_type
)
app_state.sample_registry.append(new_sample)
well.file_sample_identifier = tube_id
Expand All @@ -71,11 +73,14 @@ def handle_plate_upload(contents, filenames, app_state):
uploaded_file = AppSpecificFile.create_from_raw_data(name=name, raw_data=decoded_str)
app_state, plate = Plate.from_file(input_file=uploaded_file, app_state=app_state)

# here we make the new functon to automatically populate sample_type
app_state, plate = populate_sample_types(app_state, plate)

if plate.plate_type in ("D1000", "D5000", "HSD1000"):
plate = initialize_sample_attributes(plate, app_state)
plate = populate_samples_by_identifier_lookup(plate, app_state)

#print("intial app state sample_registry:", app_state.sample_registry)
# print("intial app state plate_registry:", app_state.plate_registry)

# print("Added plate:", plate.plate_id)


Expand Down Expand Up @@ -142,14 +147,59 @@ def update_app_state_from_ui(app_state, all_ui_tables):
if sample:
if sample.sample_type != sample_type:
sample.sample_type = sample_type
# print(f"Updated well {well_pos} with sample_type: {sample_type}")

break
if sample_id:
if sample.sample_id != sample_id:
if sample.sample_id != sample_id and sample.sample_type in ("Library", "Positive Control"):
find_and_populate_samples_by_sample_id(app_state, sample_id, well=pw)
break

# print(f"Updated well {well_pos} with sample_id: {sample_id}, sample_type: {sample_type}")
def populate_sample_types(app_state, plate):
if plate.plate_type.lower() == "qpcr":
for well in plate.plate_wells:
position = well.well_position_id
row = position[0]
col = int(position[1:])

# Only consider columns 1, 4, 7, 10
if col not in (1, 4, 7, 10):
continue

# Assign sample types based on postion
if "A" <= row <= "F" and 1 <= col <= 3:
sample_type = "Standard"
elif row in ("G", "H") and 1 <= col <= 3:
sample_type = "Negative Control"
else:
sample_type = "Library"

sample = well.get_sample(app_state)
sample.sample_type = sample_type

return app_state, plate


else:
for well in plate.plate_wells:
desc = well.sample_identifier
desc_lower = desc.lower()

if (
"negative control" in desc_lower
or "neg" in desc_lower
):
sample_type = "Negative Control"
elif "2220" in desc_lower:
sample_type = "Positive Control"
elif "phi" in desc_lower:
sample_type = "PhiX"
else:
sample_type = "Library"

sample = well.get_sample(app_state)
sample.sample_type = sample_type

return app_state, plate



Expand Down
12 changes: 11 additions & 1 deletion utils/warning_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,17 @@ def collect_missing_sample_warnings(app_state):
# Tab label, just like in UI
tab_name = f"Tapestation-{tapestation_index} ({plate.plate_id})"
tapestation_index += 1
missing = [w for w in plate.plate_wells if getattr(w, "well_note", None) in ("No sample found", "Multiple samples found")]

missing = []
for w in plate.plate_wells:
sample = w.get_sample(app_state)
sample_type = getattr(sample, "sample_type", None)
if (
getattr(w, "well_note", None) in ("No sample found", "Multiple samples found")
and sample_type in ("Library", "Positive Control")
):
missing.append(w)

if missing:
for note in set(w.well_note for w in missing):
wells = [w.well_position_id for w in missing if w.well_note == note]
Expand Down
Loading