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 6 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/change_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
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, false);
},
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
148 changes: 109 additions & 39 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, addTooltip, 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,47 @@ export function addMetricValueContents(
}
}

function createCollapseButton(contentWrapper: HTMLElement): HTMLButtonElement {
const toggleButton = document.createElement("button");
toggleButton.classList.add("fa-solid", "fa-caret-down", "text-sm");
toggleButton.classList.add("active:text-green-200");

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

return toggleButton;
}

function handleCollapse(
metricDisplay: HTMLElement,
contentWrapper: HTMLElement,
fragment: DocumentFragment,
shouldCollapse: boolean
) {
const metricWrapper = document.createElement("div");
metricWrapper.className = "flex space-x-1";

metricWrapper.appendChild(metricDisplay);

if (shouldCollapse) {
const toggleButton = createCollapseButton(contentWrapper);
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,33 +705,34 @@ function addQ30Contents(
}
return;
}
fragment.appendChild(
makeMetricDisplay(sample.run.percentOverQ30, metrics, addTooltip)

const metricDisplay = 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(metricDisplay, contentWrapper, 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
Expand All @@ -700,17 +745,20 @@ 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) => {
Expand All @@ -720,16 +768,17 @@ function addClustersPfContents(
};
tooltip.addTarget(runDiv, addContents);
}
fragment.appendChild(runDiv);

if (sample.run.lanes.length > 1) {
const contentWrapper = document.createElement("div");
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) => {

sample.run!.lanes.forEach((lane) => {
if (lane.clustersPf) {
const laneDiv = document.createElement("div");
laneDiv.classList.add("whitespace-nowrap", "print-hanging");
Expand All @@ -741,9 +790,13 @@ function addClustersPfContents(
if (addTooltip && perLaneMetrics.length) {
tooltip.addTarget(laneDiv, addContents);
}
fragment.appendChild(laneDiv);
contentWrapper.appendChild(laneDiv);
}
});

handleCollapse(runDiv, contentWrapper, fragment, shouldCollapse);
} else {
fragment.appendChild(runDiv);
}
}

Expand Down Expand Up @@ -836,7 +889,8 @@ function addPhixContents(
sample: Sample,
metrics: Metric[],
fragment: DocumentFragment,
addTooltip: boolean
djcooke marked this conversation as resolved.
Show resolved Hide resolved
addTooltip: boolean,
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 (
Expand All @@ -858,31 +912,47 @@ function addPhixContents(
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}` : "";

let text = multipleLanes ? `L${lane.laneNumber}: ` : "";
if (nullOrUndefined(lane.percentPfixRead1)) {
const span = document.createElement("span");
span.innerText = text + ": ";
laneDiv.appendChild(span);
laneDiv.appendChild(makeNotFoundIcon());
text += `<span>${makeNotFoundIcon()}</span>`;
} else {
if (multipleLanes) {
text += " ";
}
text += `R1: ${lane.percentPfixRead1}`;
if (!nullOrUndefined(lane.percentPfixRead2)) {
text += `; R2: ${lane.percentPfixRead2}`;
}
laneDiv.innerText = text;
if (addTooltip) {
tooltip.addTarget(laneDiv, addContents);
}
}
fragment.appendChild(laneDiv);

laneDiv.innerHTML = text;
wuall826 marked this conversation as resolved.
Show resolved Hide resolved

if (addTooltip && !nullOrUndefined(lane.percentPfixRead1)) {
tooltip.addTarget(laneDiv, addContents);
}

return laneDiv;
};

const minPhixValue = Math.min(
...sample.run.lanes
.flatMap((lane) => [lane.percentPfixRead1, lane.percentPfixRead2])
.filter((value) => typeof value === "number")
);

const contentWrapper = document.createElement("div");
const multipleLanes = sample.run.lanes.length > 1;

sample.run.lanes.forEach((lane) => {
const laneDiv = processLane(lane, multipleLanes);
contentWrapper.appendChild(laneDiv);
});

const minPhixDiv = document.createElement("div");
minPhixDiv.innerText = `${minPhixValue.toFixed(2)}+/R`;

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

function getPhixHighlight(
Expand Down