Added in charts for BC control concentrations.

This commit is contained in:
lwark
2025-03-24 08:58:26 -05:00
parent ea24a8ffd4
commit d796dc4b8d
5 changed files with 169 additions and 15 deletions

View File

@@ -725,3 +725,7 @@ class IridaControl(Control):
""" """
from backend.validators import PydIridaControl from backend.validators import PydIridaControl
return PydIridaControl(**self.__dict__) return PydIridaControl(**self.__dict__)
@property
def is_positive_control(self):
return not self.subtype.lower().startswith("en")

View File

@@ -1,6 +1,7 @@
''' '''
Contains functions for generating summary reports Contains functions for generating summary reports
''' '''
import itertools
import sys import sys
from pprint import pformat from pprint import pformat
from pandas import DataFrame, ExcelWriter from pandas import DataFrame, ExcelWriter
@@ -8,7 +9,7 @@ import logging
from pathlib import Path from pathlib import Path
from datetime import date from datetime import date
from typing import Tuple from typing import Tuple
from backend.db.models import BasicSubmission from backend.db.models import BasicSubmission, IridaControl
from tools import jinja_template_loading, get_first_blank_df_row, row_map, ctx from tools import jinja_template_loading, get_first_blank_df_row, row_map, ctx
from PyQt6.QtWidgets import QWidget from PyQt6.QtWidgets import QWidget
from openpyxl.worksheet.worksheet import Worksheet from openpyxl.worksheet.worksheet import Worksheet
@@ -156,7 +157,8 @@ class TurnaroundMaker(ReportArchetype):
self.start_date = start_date self.start_date = start_date
self.end_date = end_date self.end_date = end_date
# NOTE: Set page size to zero to override limiting query size. # NOTE: Set page size to zero to override limiting query size.
self.subs = BasicSubmission.query(start_date=start_date, end_date=end_date, submission_type_name=submission_type, page_size=0) self.subs = BasicSubmission.query(start_date=start_date, end_date=end_date,
submission_type_name=submission_type, page_size=0)
records = [self.build_record(sub) for sub in self.subs] records = [self.build_record(sub) for sub in self.subs]
self.df = DataFrame.from_records(records) self.df = DataFrame.from_records(records)
self.sheet_name = "Turnaround" self.sheet_name = "Turnaround"
@@ -191,9 +193,32 @@ class TurnaroundMaker(ReportArchetype):
completed_date=sub.completed_date, acceptable=tat_ok) completed_date=sub.completed_date, acceptable=tat_ok)
class ConcentrationMaker(ReportArchetype):
def __init__(self, start_date: date, end_date: date, submission_type: str = "Bacterial Culture"):
self.start_date = start_date
self.end_date = end_date
# NOTE: Set page size to zero to override limiting query size.
self.subs = BasicSubmission.query(start_date=start_date, end_date=end_date,
submission_type_name=submission_type, page_size=0)
self.controls = list(itertools.chain.from_iterable([sub.controls for sub in self.subs]))
self.records = [self.build_record(control) for control in self.controls]
self.df = DataFrame.from_records(self.records)
self.sheet_name = "Concentration"
@classmethod
def build_record(cls, control: IridaControl) -> dict:
positive = control.is_positive_control
concentration = control.sample.concentration
if not concentration:
concentration = 0
return dict(name=control.name,
submission=str(control.submission.rsl_plate_num), concentration=concentration,
submitted_date=control.submitted_date, positive=positive)
class ChartReportMaker(ReportArchetype): class ChartReportMaker(ReportArchetype):
def __init__(self, df: DataFrame, sheet_name): def __init__(self, df: DataFrame, sheet_name):
self.df = df self.df = df
self.sheet_name = sheet_name self.sheet_name = sheet_name

View File

@@ -0,0 +1,65 @@
"""
Construct turnaround time charts
"""
from pprint import pformat
from . import CustomFigure
import plotly.express as px
import pandas as pd
from PyQt6.QtWidgets import QWidget
import logging
from operator import itemgetter
logger = logging.getLogger(f"submissions.{__name__}")
class ConcentrationsChart(CustomFigure):
def __init__(self, df: pd.DataFrame, modes: list, settings: dict,
ytitle: str | None = None,
parent: QWidget | None = None,
months: int = 6):
super().__init__(df=df, modes=modes, settings=settings)
self.df = df
self.construct_chart()
# if threshold:
# self.add_hline(y=threshold)
self.update_layout(showlegend=False)
def construct_chart(self, df: pd.DataFrame | None = None):
if df:
self.df = df
# logger.debug(f"Constructing concentration chart with df:\n{self.df}")
try:
self.df = self.df[self.df.concentration.notnull()]
self.df = self.df.sort_values(['submitted_date', 'submission'], ascending=[True, True]).reset_index(drop=True)
self.df = self.df.reset_index().rename(columns={"index": "idx"})
# logger.debug(f"DF after changes:\n{self.df}")
scatter = px.scatter(data_frame=self.df, x='submission', y="concentration",
hover_data=["name", "submission", "submitted_date", "concentration"],
color="positive", color_discrete_map={True: "red", False: "green"}
)
except (ValueError, AttributeError) as e:
logger.error(f"Error constructing chart: {e}")
scatter = px.scatter()
# logger.debug(f"Scatter data: {scatter.data}")
# self.add_traces(scatter.data)
# NOTE: For some reason if data is allowed to sort itself it leads to wrong ordering of x axis.
traces = sorted(scatter.data, key=itemgetter("name"))
for trace in traces:
self.add_trace(trace)
try:
tickvals = self.df['submission'].tolist()
except KeyError:
tickvals = []
try:
ticklabels = self.df['submission'].tolist()
except KeyError:
ticklabels = []
self.update_layout(
xaxis=dict(
tickmode='array',
tickvals=tickvals,
ticktext=ticklabels,
)
)
self.update_traces(marker={'size': 15})

View File

@@ -1,7 +1,7 @@
""" """
Constructs main application. Constructs main application.
""" """
import getpass import getpass, logging, webbrowser, sys, shutil
from pprint import pformat from pprint import pformat
from PyQt6.QtCore import qInstallMessageHandler from PyQt6.QtCore import qInstallMessageHandler
from PyQt6.QtWidgets import ( from PyQt6.QtWidgets import (
@@ -9,7 +9,7 @@ from PyQt6.QtWidgets import (
QHBoxLayout, QScrollArea, QMainWindow, QHBoxLayout, QScrollArea, QMainWindow,
QToolBar QToolBar
) )
import pickle # import pickle
from PyQt6.QtGui import QAction from PyQt6.QtGui import QAction
from pathlib import Path from pathlib import Path
from markdown import markdown from markdown import markdown
@@ -21,12 +21,12 @@ from tools import (
from .functions import select_save_file, select_open_file from .functions import select_save_file, select_open_file
from .pop_ups import HTMLPop, AlertPop from .pop_ups import HTMLPop, AlertPop
from .misc import Pagifier from .misc import Pagifier
import logging, webbrowser, sys, shutil
from .submission_table import SubmissionsSheet from .submission_table import SubmissionsSheet
from .submission_widget import SubmissionFormContainer from .submission_widget import SubmissionFormContainer
from .controls_chart import ControlsViewer from .controls_chart import ControlsViewer
from .summary import Summary from .summary import Summary
from .turnaround import TurnaroundTime from .turnaround import TurnaroundTime
from .concentrations import Concentrations
from .omni_search import SearchBox from .omni_search import SearchBox
from .omni_manager import ManagerWindow from .omni_manager import ManagerWindow
@@ -244,16 +244,14 @@ class App(QMainWindow):
if dlg.exec(): if dlg.exec():
logger.debug("\n\nBeginning parsing\n\n") logger.debug("\n\nBeginning parsing\n\n")
output = dlg.parse_form() output = dlg.parse_form()
# assert isinstance(output, KitType)
# output.save()
logger.debug(f"Kit output: {pformat(output.__dict__)}") logger.debug(f"Kit output: {pformat(output.__dict__)}")
# output.to_sql() # with open(f"{output.name}.obj", "wb") as f:
with open(f"{output.name}.obj", "wb") as f: # pickle.dump(output, f)
pickle.dump(output, f)
logger.debug("\n\nBeginning transformation\n\n") logger.debug("\n\nBeginning transformation\n\n")
sql = output.to_sql() sql = output.to_sql()
with open(f"{output.name}.sql", "wb") as f: assert isinstance(sql, KitType)
pickle.dump(sql, f) # with open(f"{output.name}.sql", "wb") as f:
# pickle.dump(sql, f)
sql.save() sql.save()
@@ -269,10 +267,12 @@ class AddSubForm(QWidget):
self.tab3 = QWidget() self.tab3 = QWidget()
self.tab4 = QWidget() self.tab4 = QWidget()
self.tab5 = QWidget() self.tab5 = QWidget()
self.tab6 = QWidget()
self.tabs.resize(300, 200) self.tabs.resize(300, 200)
# NOTE: Add tabs # NOTE: Add tabs
self.tabs.addTab(self.tab1, "Submissions") self.tabs.addTab(self.tab1, "Submissions")
self.tabs.addTab(self.tab2, "Irida Controls") self.tabs.addTab(self.tab2, "Irida Controls")
self.tabs.addTab(self.tab6, "Concentrations")
self.tabs.addTab(self.tab3, "PCR Controls") self.tabs.addTab(self.tab3, "PCR Controls")
self.tabs.addTab(self.tab4, "Cost Report") self.tabs.addTab(self.tab4, "Cost Report")
self.tabs.addTab(self.tab5, "Turnaround Times") self.tabs.addTab(self.tab5, "Turnaround Times")
@@ -315,6 +315,10 @@ class AddSubForm(QWidget):
self.tab5.layout = QVBoxLayout(self) self.tab5.layout = QVBoxLayout(self)
self.tab5.layout.addWidget(turnaround) self.tab5.layout.addWidget(turnaround)
self.tab5.setLayout(self.tab5.layout) self.tab5.setLayout(self.tab5.layout)
concentration = Concentrations(self)
self.tab6.layout = QVBoxLayout(self)
self.tab6.layout.addWidget(concentration)
self.tab6.setLayout(self.tab6.layout)
# NOTE: add tabs to main widget # NOTE: add tabs to main widget
self.layout.addWidget(self.tabs) self.layout.addWidget(self.tabs)
self.setLayout(self.layout) self.setLayout(self.layout)

View File

@@ -0,0 +1,56 @@
"""
Pane showing turnaround time summary.
"""
from PyQt6.QtWidgets import QWidget, QPushButton, QComboBox, QLabel
from .info_tab import InfoPane
from backend.excel.reports import ConcentrationMaker
from frontend.visualizations.concentrations_chart import ConcentrationsChart
import logging
logger = logging.getLogger(f"submissions.{__name__}")
class Concentrations(InfoPane):
def __init__(self, parent: QWidget):
super().__init__(parent)
self.save_button = QPushButton("Save Chart", parent=self)
self.save_button.pressed.connect(self.save_png)
self.layout.addWidget(self.save_button, 0, 2, 1, 1)
self.export_button = QPushButton("Save Data", parent=self)
self.export_button.pressed.connect(self.save_excel)
self.layout.addWidget(self.export_button, 0, 3, 1, 1)
self.fig = None
self.report_object = None
# self.submission_typer = QComboBox(self)
# subs = ["All"] + [item.name for item in SubmissionType.query()]
# self.submission_typer.addItems(subs)
# self.layout.addWidget(QLabel("Submission Type"), 1, 0, 1, 1)
# self.layout.addWidget(self.submission_typer, 1, 1, 1, 3)
# self.submission_typer.currentTextChanged.connect(self.update_data)
self.update_data()
def update_data(self) -> None:
"""
Sets data in the info pane
Returns:
None
"""
super().update_data()
months = self.diff_month(self.start_date, self.end_date)
chart_settings = dict(start_date=self.start_date, end_date=self.end_date)
# if self.submission_typer.currentText() == "All":
# submission_type = None
# subtype_obj = None
# else:
# submission_type = self.submission_typer.currentText()
# subtype_obj = SubmissionType.query(name = submission_type)
# self.report_obj = ConcentrationMaker(start_date=self.start_date, end_date=self.end_date)#, submission_type=submission_type)
self.report_obj = ConcentrationMaker(**chart_settings)
# if subtype_obj:
# threshold = subtype_obj.defaults['turnaround_time'] + 0.5
# else:
# threshold = None
self.fig = ConcentrationsChart(df=self.report_obj.df, settings=chart_settings, modes=[], months=months)
self.webview.setHtml(self.fig.html)