diff --git a/src/submissions/backend/db/models/controls.py b/src/submissions/backend/db/models/controls.py index 3eff08a..037335a 100644 --- a/src/submissions/backend/db/models/controls.py +++ b/src/submissions/backend/db/models/controls.py @@ -725,3 +725,7 @@ class IridaControl(Control): """ from backend.validators import PydIridaControl return PydIridaControl(**self.__dict__) + + @property + def is_positive_control(self): + return not self.subtype.lower().startswith("en") diff --git a/src/submissions/backend/excel/reports.py b/src/submissions/backend/excel/reports.py index 797d72f..30c44ba 100644 --- a/src/submissions/backend/excel/reports.py +++ b/src/submissions/backend/excel/reports.py @@ -1,6 +1,7 @@ ''' Contains functions for generating summary reports ''' +import itertools import sys from pprint import pformat from pandas import DataFrame, ExcelWriter @@ -8,7 +9,7 @@ import logging from pathlib import Path from datetime import date 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 PyQt6.QtWidgets import QWidget from openpyxl.worksheet.worksheet import Worksheet @@ -152,11 +153,12 @@ class ReportMaker(object): class TurnaroundMaker(ReportArchetype): - def __init__(self, start_date: date, end_date: date, submission_type:str): + def __init__(self, start_date: date, end_date: date, submission_type: str): 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.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] self.df = DataFrame.from_records(records) self.sheet_name = "Turnaround" @@ -188,7 +190,31 @@ class TurnaroundMaker(ReportArchetype): except TypeError: return {} return dict(name=str(sub.rsl_plate_num), days=days, submitted_date=sub.submitted_date, - 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): @@ -196,4 +222,3 @@ class ChartReportMaker(ReportArchetype): def __init__(self, df: DataFrame, sheet_name): self.df = df self.sheet_name = sheet_name - diff --git a/src/submissions/frontend/visualizations/concentrations_chart.py b/src/submissions/frontend/visualizations/concentrations_chart.py new file mode 100644 index 0000000..ede8e3a --- /dev/null +++ b/src/submissions/frontend/visualizations/concentrations_chart.py @@ -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}) diff --git a/src/submissions/frontend/widgets/app.py b/src/submissions/frontend/widgets/app.py index d5aaead..2cfd156 100644 --- a/src/submissions/frontend/widgets/app.py +++ b/src/submissions/frontend/widgets/app.py @@ -1,7 +1,7 @@ """ Constructs main application. """ -import getpass +import getpass, logging, webbrowser, sys, shutil from pprint import pformat from PyQt6.QtCore import qInstallMessageHandler from PyQt6.QtWidgets import ( @@ -9,7 +9,7 @@ from PyQt6.QtWidgets import ( QHBoxLayout, QScrollArea, QMainWindow, QToolBar ) -import pickle +# import pickle from PyQt6.QtGui import QAction from pathlib import Path from markdown import markdown @@ -21,12 +21,12 @@ from tools import ( from .functions import select_save_file, select_open_file from .pop_ups import HTMLPop, AlertPop from .misc import Pagifier -import logging, webbrowser, sys, shutil from .submission_table import SubmissionsSheet from .submission_widget import SubmissionFormContainer from .controls_chart import ControlsViewer from .summary import Summary from .turnaround import TurnaroundTime +from .concentrations import Concentrations from .omni_search import SearchBox from .omni_manager import ManagerWindow @@ -244,16 +244,14 @@ class App(QMainWindow): if dlg.exec(): logger.debug("\n\nBeginning parsing\n\n") output = dlg.parse_form() - # assert isinstance(output, KitType) - # output.save() logger.debug(f"Kit output: {pformat(output.__dict__)}") - # output.to_sql() - with open(f"{output.name}.obj", "wb") as f: - pickle.dump(output, f) + # with open(f"{output.name}.obj", "wb") as f: + # pickle.dump(output, f) logger.debug("\n\nBeginning transformation\n\n") sql = output.to_sql() - with open(f"{output.name}.sql", "wb") as f: - pickle.dump(sql, f) + assert isinstance(sql, KitType) + # with open(f"{output.name}.sql", "wb") as f: + # pickle.dump(sql, f) sql.save() @@ -269,10 +267,12 @@ class AddSubForm(QWidget): self.tab3 = QWidget() self.tab4 = QWidget() self.tab5 = QWidget() + self.tab6 = QWidget() self.tabs.resize(300, 200) # NOTE: Add tabs self.tabs.addTab(self.tab1, "Submissions") self.tabs.addTab(self.tab2, "Irida Controls") + self.tabs.addTab(self.tab6, "Concentrations") self.tabs.addTab(self.tab3, "PCR Controls") self.tabs.addTab(self.tab4, "Cost Report") self.tabs.addTab(self.tab5, "Turnaround Times") @@ -315,6 +315,10 @@ class AddSubForm(QWidget): self.tab5.layout = QVBoxLayout(self) self.tab5.layout.addWidget(turnaround) 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 self.layout.addWidget(self.tabs) self.setLayout(self.layout) diff --git a/src/submissions/frontend/widgets/concentrations.py b/src/submissions/frontend/widgets/concentrations.py new file mode 100644 index 0000000..e4ace71 --- /dev/null +++ b/src/submissions/frontend/widgets/concentrations.py @@ -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)