From 4c9044a5f1ef36d70148dace139c253fdf5f47fe Mon Sep 17 00:00:00 2001 From: alwu <121817690+wuall826@users.noreply.github.com> Date: Tue, 17 Sep 2024 10:30:30 -0400 Subject: [PATCH] [GLT-4245] TAT trend report legend for Color by Gate (#228) --- changes/add_GLT-4245 | 1 + changes/fix_GLT-4245 | 1 + src/main/resources/static/css/output.css | 12 ++++ src/main/resources/templates/tat-trend.html | 68 +++++++++---------- ts/component/legend.ts | 41 ++++++++++-- ts/component/table-builder.ts | 2 +- ts/tat-trend.ts | 74 ++++++++++++--------- ts/util/color-mapping.ts | 15 +++++ 8 files changed, 141 insertions(+), 73 deletions(-) create mode 100644 changes/add_GLT-4245 create mode 100644 changes/fix_GLT-4245 create mode 100644 ts/util/color-mapping.ts diff --git a/changes/add_GLT-4245 b/changes/add_GLT-4245 new file mode 100644 index 00000000..d766cde4 --- /dev/null +++ b/changes/add_GLT-4245 @@ -0,0 +1 @@ +TAT Trend report legend for colour by gate \ No newline at end of file diff --git a/changes/fix_GLT-4245 b/changes/fix_GLT-4245 new file mode 100644 index 00000000..e725ba85 --- /dev/null +++ b/changes/fix_GLT-4245 @@ -0,0 +1 @@ +TAT Trend Report color by gate feature to work correctly with time range grouping \ No newline at end of file diff --git a/src/main/resources/static/css/output.css b/src/main/resources/static/css/output.css index 1114a6c4..bd862dbf 100644 --- a/src/main/resources/static/css/output.css +++ b/src/main/resources/static/css/output.css @@ -658,6 +658,10 @@ Ensure the default browser behavior of the `hidden` attribute. display: none; } +.h-3{ + height: 0.75rem; +} + .h-full{ height: 100%; } @@ -666,6 +670,10 @@ Ensure the default browser behavior of the `hidden` attribute. min-height: 100vh; } +.w-3{ + width: 0.75rem; +} + .w-6{ width: 1.5rem; } @@ -800,6 +808,10 @@ Ensure the default browser behavior of the `hidden` attribute. justify-content: space-between; } +.gap-1{ + gap: 0.25rem; +} + .gap-2{ gap: 0.5rem; } diff --git a/src/main/resources/templates/tat-trend.html b/src/main/resources/templates/tat-trend.html index 232f08f0..8e1a8dd8 100644 --- a/src/main/resources/templates/tat-trend.html +++ b/src/main/resources/templates/tat-trend.html @@ -11,49 +11,49 @@

TAT Trend Report

-
-
+
+
+ class="font-inter font-medium text-12 text-black bg-grey-100 px-2 py-1 rounded-md hover:ring-2 ring-green-200 ring-offset-1">Week + class="font-inter font-medium text-12 text-black bg-grey-100 px-2 py-1 rounded-md hover:ring-2 ring-green-200 ring-offset-1">Month -
- - -
-
- - - - - - - - - +
+ + +
+
+ + + + + + + + + +
+
diff --git a/ts/component/legend.ts b/ts/component/legend.ts index ff3d79ea..2775588a 100644 --- a/ts/component/legend.ts +++ b/ts/component/legend.ts @@ -1,10 +1,12 @@ import { qcStatuses } from "../data/qc-status"; import { makeIcon } from "../util/html-utils"; +import { GATE_COLOR_MAPPING, getColorForGate } from "../util/color-mapping"; const legendId = "legend-container"; + class Legend { private container: HTMLElement; - constructor() { + constructor(type: "qc" | "gate" = "qc") { // outer container this.container = document.createElement("div"); this.container.className = @@ -38,10 +40,20 @@ class Legend { // grid of legend labels const body = document.createElement("div"); body.className = "m-4 grid grid-rows-5 grid-flow-col gap-2"; - for (const qcStatus of Object.values(qcStatuses)) { - body.appendChild(makeLegendEntry(qcStatus.icon, qcStatus.label)); + // populate legend based on type + if (type === "qc") { + for (const qcStatus of Object.values(qcStatuses)) { + body.appendChild(makeLegendEntry(qcStatus.icon, qcStatus.label)); + } + body.appendChild(makeLegendEntry("pen-ruler", "Preliminary value")); + } else if (type === "gate") { + for (const gate of Object.keys(GATE_COLOR_MAPPING)) { + const color = getColorForGate(gate); + body.appendChild(makeLegendEntryColor(color, gate)); + } + } else { + throw new Error(`Unsupported legend type: ${type}`); } - body.appendChild(makeLegendEntry("pen-ruler", "Preliminary value")); this.container.append(header); this.container.append(body); @@ -152,10 +164,10 @@ class Legend { } } -export function toggleLegend() { +export function toggleLegend(type: "qc" | "gate" = "qc") { const legendWindow = document.getElementById(legendId); if (!legendWindow) { - const legendContainer = new Legend(); + const legendContainer = new Legend(type); document.body.appendChild(legendContainer.getTag()); } else { legendWindow.remove(); @@ -174,3 +186,20 @@ function makeLegendEntry(iconName: string, text: string) { labelContainer.appendChild(label); return labelContainer; } + +function makeLegendEntryColor(color: string, text: string) { + const labelContainer = document.createElement("div"); + labelContainer.className = + "flex items-center space-x-2 bg-grey-100 rounded-md font-inter font-medium p-2 text-12"; + // create the color box + const colorBox = document.createElement("div"); + colorBox.className = "w-3 h-3 inline-block border-2"; + colorBox.style.backgroundColor = `${color}B3`; + colorBox.style.borderColor = color; + const label = document.createElement("span"); + label.innerHTML = text; + // append the color box and label to the container + labelContainer.appendChild(colorBox); + labelContainer.appendChild(label); + return labelContainer; +} diff --git a/ts/component/table-builder.ts b/ts/component/table-builder.ts index 09f23af1..2c41a50c 100644 --- a/ts/component/table-builder.ts +++ b/ts/component/table-builder.ts @@ -1056,5 +1056,5 @@ function getElement(element?: Type) { export const legendAction: StaticAction = { title: "Legend", - handler: toggleLegend, + handler: () => toggleLegend("qc"), }; diff --git a/ts/tat-trend.ts b/ts/tat-trend.ts index a2390ae6..63499e21 100644 --- a/ts/tat-trend.ts +++ b/ts/tat-trend.ts @@ -1,6 +1,8 @@ import Plotly from "plotly.js-dist-min"; import { post } from "./util/requests"; import { getRequiredElementById } from "./util/html-utils"; +import { toggleLegend } from "./component/legend"; +import { getColorForGate } from "./util/color-mapping"; let jsonData: any[] = []; const uirevision = "true"; @@ -259,7 +261,7 @@ function plotData( pointpos: 0, marker: { size: 6, - color: colorByGate ? gateColors[gate] : assayColors[assay], + color: colorByGate ? getColorForGate(gate) : assayColors[assay], }, boxmean: true, legendgroup: assay, @@ -344,6 +346,31 @@ function updatePlot( Plotly.react("plotContainer", newPlot, layout); } +function updatePlotWithLegend( + selectedGrouping: string, + jsonData: any[], + selectedGates: string[], + selectedDataType: string +) { + updatePlot(selectedGrouping, jsonData, selectedGates, selectedDataType); + const legendButton = document.getElementById("legendButton"); + if (getColorByGate()) { + // show the Legend button + if (legendButton) { + legendButton.classList.remove("hidden"); + } + } else { + // hide the Legend button and close the legend if it's open + if (legendButton) { + legendButton.classList.add("hidden"); + } + const legendElement = document.getElementById("legend-container"); + if (legendElement) { + legendElement.remove(); // hide the legend if 'color by gate' is deselected + } + } +} + function parseUrlParams(): { key: string; value: string }[] { const params: { key: string; value: string }[] = []; const searchParams = new URLSearchParams(window.location.search); @@ -364,9 +391,7 @@ function getSelectedGrouping(): string { const selectedButton = document.querySelector( "#groupingButtons button.active" ); - return selectedButton - ? selectedButton.id.replace("Button", "") - : "fiscalQuarter"; + return selectedButton ? selectedButton.dataset.grouping! : "fiscalQuarter"; } function getSelectedDataType(): string { @@ -376,7 +401,7 @@ function getSelectedDataType(): string { return dataSelection.value; } -document.addEventListener("DOMContentLoaded", () => { +window.addEventListener("load", () => { const params = parseUrlParams(); const requestData = { filters: params.length > 0 ? params : [] }; @@ -395,10 +420,11 @@ document.addEventListener("DOMContentLoaded", () => { }); const handlePlotUpdate = () => { + const selectedGrouping = getSelectedGrouping(); const selectedGates = getSelectedGates(); const selectedDataType = getSelectedDataType(); - updatePlot( - getSelectedGrouping(), + updatePlotWithLegend( + selectedGrouping, jsonData, selectedGates, selectedDataType @@ -410,33 +436,17 @@ document.addEventListener("DOMContentLoaded", () => { buttons.forEach((button) => button.classList.remove("active")); (event.currentTarget as HTMLButtonElement).classList.add("active"); - const selectedGrouping = (event.currentTarget as HTMLButtonElement).dataset - .grouping; - const selectedGates = getSelectedGates(); - const selectedDataType = getSelectedDataType(); - newPlot(selectedGrouping!, jsonData, selectedGates, selectedDataType); + handlePlotUpdate(); }; - const weekButton = getRequiredElementById("weekButton"); - const monthButton = getRequiredElementById("monthButton"); - const quarterButton = getRequiredElementById("quarterButton"); - const yearButton = getRequiredElementById("yearButton"); + ["weekButton", "monthButton", "quarterButton", "yearButton"].forEach((id) => { + getRequiredElementById(id).addEventListener("click", handleNewPlot); + }); - weekButton.addEventListener("click", handleNewPlot); - monthButton.addEventListener("click", handleNewPlot); - quarterButton.addEventListener("click", handleNewPlot); - yearButton.addEventListener("click", handleNewPlot); + ["dataSelection", "gatesCheckboxes", "toggleColors"].forEach((id) => { + getRequiredElementById(id).addEventListener("change", handlePlotUpdate); + }); - getRequiredElementById("dataSelection").addEventListener( - "change", - handlePlotUpdate - ); - getRequiredElementById("gatesCheckboxes").addEventListener( - "change", - handlePlotUpdate - ); - getRequiredElementById("toggleColors").addEventListener( - "change", - handlePlotUpdate - ); + const legendButton = getRequiredElementById("legendButton"); + legendButton.addEventListener("click", () => toggleLegend("gate")); }); diff --git a/ts/util/color-mapping.ts b/ts/util/color-mapping.ts new file mode 100644 index 00000000..fc1535dc --- /dev/null +++ b/ts/util/color-mapping.ts @@ -0,0 +1,15 @@ +export const GATE_COLOR_MAPPING: { [gate: string]: string } = { + "Receipt": "#4477AA", + "Extraction": "#66CCEE", + "Library Prep": "#228833", + "Library Qual": "#CCBB44", + "Full-Depth": "#EE6677", + "Analysis Review": "#44AA99", + "Release Approval": "#AA3377", + "Release": "#BBBBBB", + "Full Case": "#000000", +}; + +export function getColorForGate(gate: string): string { + return GATE_COLOR_MAPPING[gate] || "#DDDDDD"; +}