Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[GLT-3764] Reduce run metric clutter #152

Merged
merged 9 commits into from
Sep 29, 2023
Merged
Show file tree
Hide file tree
Changes from 3 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
1 change: 1 addition & 0 deletions changes/add_GLT-3764.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Collapse lane-level values to reduce run metric visual clutter
wuall826 marked this conversation as resolved.
Show resolved Hide resolved
10 changes: 8 additions & 2 deletions ts/case-report.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { TableBuilder, TableDefinition } from "./component/table-builder";
import { Metric, MetricCategory, MetricSubcategory } from "./data/assay";
import { Case, Qcable } from "./data/case";
import { qcStatuses } from "./data/qc-status";
import { makeTextDiv } from "./util/html-utils";
import {
getMetricValue,
getRequisitionMetricCellHighlight,
Expand All @@ -24,7 +25,7 @@ import {
Sample,
subcategoryApplies as sampleSubcategoryApplies,
} from "./data/sample";
import { addTextDiv, makeNameDiv, makeTextDiv } from "./util/html-utils";
import { addTextDiv, makeNameDiv } from "./util/html-utils";
import { getMetricRequirementText } from "./util/metrics";
import { get } from "./util/requests";
import { siteConfig } from "./util/site-config";
Expand Down Expand Up @@ -202,7 +203,7 @@ const sampleGateMetricsDefinition: TableDefinition<ReportSample, Metric> = {
headingClass: "print-width-20",
child: true,
addChildContents(object, parent, fragment) {
addMetricValueContents(parent.sample, [object], fragment, false);
addMetricValueContents(parent.sample, [object], fragment, false, !true);
wuall826 marked this conversation as resolved.
Show resolved Hide resolved
},
getCellHighlight(reportSample, metric) {
if (metric == null) {
Expand Down Expand Up @@ -488,6 +489,7 @@ async function loadCase(caseId: string) {
new TableBuilder(sampleGateMetricsDefinition, "receiptTableContainer").build(
receipts
);

const extractions = getReportSamples(
data,
data.tests.flatMap((test) => test.extractions),
Expand All @@ -497,6 +499,7 @@ async function loadCase(caseId: string) {
sampleGateMetricsDefinition,
"extractionTableContainer"
).build(extractions);

const libraryPreps = getReportSamples(
data,
data.tests.flatMap((test) => test.libraryPreparations),
Expand All @@ -506,6 +509,7 @@ async function loadCase(caseId: string) {
sampleGateMetricsDefinition,
"libraryPreparationTableContainer"
).build(libraryPreps);

const libraryQualifications = getReportSamples(
data,
data.tests.flatMap((test) => test.libraryQualifications),
Expand All @@ -515,6 +519,7 @@ async function loadCase(caseId: string) {
sampleGateMetricsDefinition,
"libraryQualificationTableContainer"
).build(libraryQualifications);

const fullDepths = getReportSamples(
data,
data.tests.flatMap((test) => test.fullDepthSequencings),
Expand All @@ -524,6 +529,7 @@ async function loadCase(caseId: string) {
sampleGateMetricsDefinition,
"fullDepthSequencingTableContainer"
).build(fullDepths);

const informatics = getReportInformatics(data);
new TableBuilder(
requisitionGateMetricsDefinition,
Expand Down
2 changes: 1 addition & 1 deletion ts/component/table-builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -880,4 +880,4 @@ function getElement<Type>(element?: Type) {
export const legendAction: StaticAction = {
title: "Legend",
handler: toggleLegend,
};
};
2 changes: 1 addition & 1 deletion ts/data/case.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,9 @@ export interface Donor {
export interface Qcable {
qcPassed?: boolean;
qcReason?: string;
qcNote?: string;
wuall826 marked this conversation as resolved.
Show resolved Hide resolved
qcUser?: string;
qcDate?: string;
qcNote?: string;
dataReviewPassed?: boolean;
dataReviewUser?: string;
dataReviewDate?: string;
Expand Down
198 changes: 141 additions & 57 deletions ts/data/sample.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {
import { Tooltip } from "../component/tooltip";
import { urls } from "../util/urls";
import { Metric, MetricCategory, MetricSubcategory } from "./assay";
import { Donor, Qcable, Run } from "./case";
import { Donor, Lane, Qcable, Run } from "./case";
import { QcStatus, qcStatuses } from "./qc-status";
import {
anyFail,
Expand Down Expand Up @@ -574,7 +574,8 @@ export function addMetricValueContents(
sample: Sample,
metrics: Metric[],
fragment: DocumentFragment,
addTooltip: boolean
addTooltip: boolean,
shouldCollapse: boolean = true
) {
const metricNames = metrics
.map((metric) => metric.name)
Expand All @@ -586,16 +587,23 @@ export function addMetricValueContents(
// handle metrics that have multiple values
switch (metricName) {
case METRIC_LABEL_Q30:
addQ30Contents(sample, metrics, fragment, addTooltip);
addQ30Contents(sample, metrics, fragment, addTooltip, shouldCollapse);
return;
case METRIC_LABEL_CLUSTERS_PF_1:
case METRIC_LABEL_CLUSTERS_PF_2:
addClustersPfContents(sample, metrics, fragment, addTooltip);
addClustersPfContents(
sample,
metrics,
fragment,
addTooltip,
shouldCollapse
);
return;
case METRIC_LABEL_PHIX:
addPhixContents(sample, metrics, fragment, addTooltip);
addPhixContents(sample, metrics, fragment, shouldCollapse);
return;
}

if (metrics.every((metric) => metric.thresholdType === "BOOLEAN")) {
// pass/fail based on QC status
if (sample.qcPassed) {
Expand Down Expand Up @@ -646,11 +654,57 @@ export function addMetricValueContents(
}
}

function createCollapseButton(
contentWrapper: HTMLElement,
toggleText: string,
wuall826 marked this conversation as resolved.
Show resolved Hide resolved
shouldCollapse: boolean
wuall826 marked this conversation as resolved.
Show resolved Hide resolved
): HTMLButtonElement {
const toggleButton = document.createElement("button");
toggleButton.classList.add("fa", "fa-caret-down", "text-sm", "icon-button");
wuall826 marked this conversation as resolved.
Show resolved Hide resolved

toggleButton.textContent = toggleText;

if (shouldCollapse) {
toggleButton.addEventListener("click", () => {
const isExpanded = contentWrapper.classList.toggle("hidden");
toggleButton.classList.toggle("fa-caret-down", isExpanded);
toggleButton.classList.toggle("fa-caret-up", !isExpanded);
});

toggleButton.addEventListener("mouseover", () => {
toggleButton.classList.add("text-green-200");
});

toggleButton.addEventListener("mouseout", () => {
toggleButton.classList.remove("text-green-200");
});
}

return toggleButton;
}

function handleCollapse(
contentWrapper: HTMLElement,
metricWrapper: HTMLElement,
fragment: DocumentFragment,
shouldCollapse: boolean
) {
if (shouldCollapse) {
const toggleButton = createCollapseButton(contentWrapper, "", true);
metricWrapper.appendChild(toggleButton);
contentWrapper.classList.add("hidden");
}

fragment.appendChild(metricWrapper);
fragment.appendChild(contentWrapper);
}

function addQ30Contents(
sample: Sample,
metrics: Metric[],
fragment: DocumentFragment,
addTooltip: boolean
addTooltip: boolean,
shouldCollapse: boolean = true
) {
// run-level value is checked, but run and lane-level are both displayed
if (!sample.run || !sample.run.percentOverQ30) {
Expand All @@ -661,37 +715,37 @@ function addQ30Contents(
}
return;
}
fragment.appendChild(

// Create a wrapper for the metric and the button
const metricWrapper = document.createElement("div");
metricWrapper.style.display = "flex";
wuall826 marked this conversation as resolved.
Show resolved Hide resolved
metricWrapper.appendChild(
makeMetricDisplay(sample.run.percentOverQ30, metrics, addTooltip)
);
sample.run.lanes.forEach((lane) => {
if (!lane.percentOverQ30Read1) {
return;
}

const contentWrapper = document.createElement("div");

sample.run!.lanes.forEach((lane) => {
if (!lane.percentOverQ30Read1) return;
let text = sample.run?.lanes.length === 1 ? "" : `L${lane.laneNumber} `;
text += `R1: ${lane.percentOverQ30Read1}`;
if (lane.percentOverQ30Read2) {
text += ";";
}
if (lane.percentOverQ30Read2) {
text += ` R2: ${lane.percentOverQ30Read2}`;
}
if (lane.percentOverQ30Read2) text += `; R2: ${lane.percentOverQ30Read2}`;
const div = document.createElement("div");
div.classList.add("whitespace-nowrap", "print-hanging");
div.appendChild(document.createTextNode(text));
fragment.appendChild(div);
contentWrapper.appendChild(div);
});

handleCollapse(contentWrapper, metricWrapper, fragment, shouldCollapse);
}

function addClustersPfContents(
sample: Sample,
metrics: Metric[],
fragment: DocumentFragment,
addTooltip: boolean
addTooltip: boolean,
shouldCollapse: boolean = true
) {
// For joined flowcells, run-level is checked
// For non-joined, each lane is checked
// Metric is sometimes specified "/lane", sometimes per run
wuall826 marked this conversation as resolved.
Show resolved Hide resolved
if (!sample.run || !sample.run.clustersPf) {
if (sample.run && !sample.run.completionDate) {
fragment.appendChild(makeSequencingIcon());
Expand All @@ -700,50 +754,65 @@ function addClustersPfContents(
}
return;
}

const separatedMetrics = separateRunVsLaneMetrics(metrics, sample.run);
const perRunMetrics = separatedMetrics[0];
const perLaneMetrics = separatedMetrics[1];
const tooltip = Tooltip.getInstance();
const runDiv = document.createElement("div");
const divisorUnit = getDivisorUnit(metrics);

runDiv.innerText = formatMetricValue(
sample.run.clustersPf,
metrics,
divisorUnit
);

if (addTooltip && perRunMetrics.length) {
// whether originally or not, these metrics are per run
wuall826 marked this conversation as resolved.
Show resolved Hide resolved
const addContents = (fragment: DocumentFragment) => {
perRunMetrics.forEach((metric) =>
addMetricRequirementText(metric, fragment)
);
};
tooltip.addTarget(runDiv, addContents);
}
fragment.appendChild(runDiv);

// Create a wrapper for the metric and the button
const metricWrapper = document.createElement("div");
metricWrapper.style.display = "flex";
metricWrapper.style.alignItems = "center";

metricWrapper.appendChild(runDiv);

if (sample.run.lanes.length > 1) {
const addContents = (fragment: DocumentFragment) => {
// these metrics are per lane
wuall826 marked this conversation as resolved.
Show resolved Hide resolved
perLaneMetrics.forEach((metric) =>
addMetricRequirementText(metric, fragment)
);
};
sample.run.lanes.forEach((lane) => {
if (lane.clustersPf) {
const laneDiv = document.createElement("div");
laneDiv.classList.add("whitespace-nowrap", "print-hanging");
laneDiv.innerText = `L${lane.laneNumber}: ${formatMetricValue(
lane.clustersPf,
metrics,
divisorUnit
)}`;
if (addTooltip && perLaneMetrics.length) {
tooltip.addTarget(laneDiv, addContents);
const processLaneContents = () => {
wuall826 marked this conversation as resolved.
Show resolved Hide resolved
const contentWrapper = document.createElement("div");
const addContents = (fragment: DocumentFragment) => {
perLaneMetrics.forEach((metric) =>
addMetricRequirementText(metric, fragment)
);
};

sample.run!.lanes.forEach((lane) => {
if (lane.clustersPf) {
const laneDiv = document.createElement("div");
laneDiv.classList.add("whitespace-nowrap", "print-hanging");
laneDiv.innerText = `L${lane.laneNumber}: ${formatMetricValue(
lane.clustersPf,
metrics,
divisorUnit
)}`;
if (perLaneMetrics.length) {
tooltip.addTarget(laneDiv, addContents);
}
contentWrapper.appendChild(laneDiv);
}
fragment.appendChild(laneDiv);
}
});
});

handleCollapse(contentWrapper, metricWrapper, fragment, shouldCollapse);
};

processLaneContents();
}
}

Expand Down Expand Up @@ -836,9 +905,8 @@ function addPhixContents(
sample: Sample,
metrics: Metric[],
fragment: DocumentFragment,
addTooltip: boolean
djcooke marked this conversation as resolved.
Show resolved Hide resolved
shouldCollapse: boolean = true
) {
// There is no run-level metric, so we check each read of each lane
wuall826 marked this conversation as resolved.
Show resolved Hide resolved
if (
!sample.run ||
!sample.run.lanes ||
Expand All @@ -853,13 +921,7 @@ function addPhixContents(
return;
}

const tooltip = Tooltip.getInstance();
const addContents = (fragment: DocumentFragment) => {
metrics.forEach((metric) => addMetricRequirementText(metric, fragment));
};

const multipleLanes = sample.run.lanes.length > 1;
sample.run.lanes.forEach((lane) => {
const processLane = (lane: Lane, multipleLanes: boolean) => {
wuall826 marked this conversation as resolved.
Show resolved Hide resolved
const laneDiv = document.createElement("div");
laneDiv.classList.add("whitespace-nowrap", "print-hanging");
let text = multipleLanes ? `L${lane.laneNumber}` : "";
Expand All @@ -877,12 +939,34 @@ function addPhixContents(
text += `; R2: ${lane.percentPfixRead2}`;
}
laneDiv.innerText = text;
if (addTooltip) {
tooltip.addTarget(laneDiv, addContents);
}
const tooltip = Tooltip.getInstance();
const addContents = (fragment: DocumentFragment) => {
metrics.forEach((metric) => addMetricRequirementText(metric, fragment));
};
tooltip.addTarget(laneDiv, addContents);
}
fragment.appendChild(laneDiv);
return laneDiv;
};

const minPhixValue = Math.min(
...sample.run.lanes.map((lane) => lane.percentPfixRead1)
);
wuall826 marked this conversation as resolved.
Show resolved Hide resolved
const metricWrapper = document.createElement("div");
metricWrapper.style.display = "flex"; // Set metricWrapper to be a flex container
metricWrapper.style.alignItems = "center"; // Vertically center contents
wuall826 marked this conversation as resolved.
Show resolved Hide resolved

const minPhixDiv = document.createElement("div");
minPhixDiv.classList.add("whitespace-nowrap", "print-hanging");
wuall826 marked this conversation as resolved.
Show resolved Hide resolved
minPhixDiv.innerText = `${minPhixValue.toFixed(2)}+/R`;
metricWrapper.appendChild(minPhixDiv);

const contentWrapper = document.createElement("div");
const multipleLanes = sample.run.lanes.length > 1;
sample.run.lanes.forEach((lane) => {
contentWrapper.appendChild(processLane(lane, multipleLanes));
});

handleCollapse(contentWrapper, metricWrapper, fragment, shouldCollapse);
}

function getPhixHighlight(
Expand Down