Skip to content

Commit 4bc1cd8

Browse files
Added metadata field test
1 parent a48f5b1 commit 4bc1cd8

10 files changed

Lines changed: 299 additions & 116 deletions

src/libunicorn/graphengine/graphengine_backend.js

Lines changed: 111 additions & 75 deletions
Large diffs are not rendered by default.

src/libunicorn/graphengine/graphengine_dom.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@
6969
metadataSummaryMeta: document.getElementById("metadataSummaryMeta"),
7070
metadataSummaryFields: document.getElementById("metadataSummaryFields"),
7171
metadataFieldSelect: document.getElementById("metadataFieldSelect"),
72+
metadataColorFieldSelect: document.getElementById("metadataColorFieldSelect"),
7273
metadataFieldColorMeta: document.getElementById("metadataFieldColorMeta"),
7374
metadataFieldColorList: document.getElementById("metadataFieldColorList"),
7475
metadataSelectedMeta: document.getElementById("metadataSelectedMeta"),

src/libunicorn/graphengine/graphengine_metadata.js

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,11 @@
8888
function ensureMetadataVisualizationState(metadata = state.remote.metadata) {
8989
const availableFields = getAvailableMetadataFields(metadata);
9090
const storedMode = readStoredMetadataVisualizationMode();
91-
state.remote.metadataVisualizationMode = normalizeMetadataVisualizationMode(storedMode, availableFields);
91+
let normalizedMode = normalizeMetadataVisualizationMode(storedMode, availableFields);
92+
if (normalizedMode === NONE_VISUALIZATION_MODE && availableFields.length) {
93+
normalizedMode = `${FIELD_MODE_PREFIX}${availableFields[0]}`;
94+
}
95+
state.remote.metadataVisualizationMode = normalizedMode;
9296
writeStoredMetadataVisualizationMode(state.remote.metadataVisualizationMode);
9397

9498
const storedColors = readStoredMetadataValueColors();
@@ -162,6 +166,25 @@
162166
return normalizeColor(mapping[normalizedValue], MISSING_METADATA_COLOR);
163167
}
164168

169+
function setMetadataValueColor(field, value, color) {
170+
if (!field || value == null) {
171+
return MISSING_METADATA_COLOR;
172+
}
173+
const normalizedValue = String(value);
174+
const fallback = normalizedValue === MISSING_METADATA_VALUE
175+
? MISSING_METADATA_COLOR
176+
: core.SOURCE_COLORS[0];
177+
const nextColor = normalizeColor(color, fallback);
178+
const mapping = ensureFieldValueColorAssignments(field, [normalizedValue]);
179+
mapping[normalizedValue] = nextColor;
180+
state.remote.metadataValueColors = {
181+
...state.remote.metadataValueColors,
182+
[field]: mapping,
183+
};
184+
writeStoredMetadataValueColors(state.remote.metadataValueColors);
185+
return nextColor;
186+
}
187+
165188
function buildResolvedDatasetLabel(datasetSummary, field, value, missing) {
166189
const filename = String(datasetSummary?.filename || "");
167190
if (!field || value == null) {
@@ -218,7 +241,10 @@
218241
return null;
219242
}
220243
const field = resolved[0].metadata_field;
221-
const values = Array.from(new Set(resolved.map((entry) => entry.metadata_value)))
244+
const values = Array.from(new Set([
245+
...resolved.map((entry) => entry.metadata_value),
246+
MISSING_METADATA_VALUE,
247+
]))
222248
.sort((left, right) => {
223249
if (left === MISSING_METADATA_VALUE) return 1;
224250
if (right === MISSING_METADATA_VALUE) return -1;
@@ -247,6 +273,7 @@
247273
ensureMetadataVisualizationState,
248274
getDatasetMetadataValue,
249275
getMetadataValueColor,
276+
setMetadataValueColor,
250277
resolveDatasetMetadataDisplay,
251278
resolveDatasetMetadataDisplays,
252279
buildMetadataLegendPayload,

src/libunicorn/graphengine/graphengine_state.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@
3030
backendNamesFile: "",
3131
metadata: null,
3232
metadataFieldSelection: [],
33-
metadataFieldColors: {},
3433
metadataVisualizationMode: "none",
3534
metadataValueColors: {},
3635
expandedTaxids: new Set(),

src/libunicorn/graphengine/graphengine_ui.js

Lines changed: 53 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,11 @@
55
const state = namespace.state;
66
const els = namespace.els;
77
const core = namespace.core;
8+
const backend = namespace.backend;
9+
const metadataResolver = namespace.metadata;
810

9-
if (!state || !els || !core) {
10-
throw new Error("Unicorn graphengine UI expected state, DOM, and core modules to load first.");
11+
if (!state || !els || !core || !metadataResolver) {
12+
throw new Error("Unicorn graphengine UI expected state, DOM, core, and metadata modules to load first.");
1113
}
1214

1315
function initRemotePanel() {
@@ -392,14 +394,56 @@
392394
els.sourceLegend.innerHTML = "";
393395
return;
394396
}
397+
const resolvedDisplays = typeof backend?.getSelectedResolvedMetadataDisplays === "function"
398+
? backend.getSelectedResolvedMetadataDisplays()
399+
: [];
400+
const resolvedByFilename = new Map(
401+
resolvedDisplays.map((entry) => [entry.filename, entry]),
402+
);
403+
const metadataLegend = typeof backend?.getSelectedMetadataLegendPayload === "function"
404+
? backend.getSelectedMetadataLegendPayload()
405+
: null;
406+
const metadataLegendHtml = metadataLegend && Array.isArray(metadataLegend.values) && metadataLegend.values.length
407+
? `
408+
<div class="legend-section legend-section-meta">
409+
<div class="legend-section-title">Coloring by ${core.escapeHtml(metadataLegend.field)}</div>
410+
<div class="legend-meta-list">
411+
${metadataLegend.values.map((entry) => `
412+
<div class="legend-meta-item">
413+
<span class="legend-swatch" style="background:${entry.color}"></span>
414+
<span class="legend-label">${core.escapeHtml(entry.value === metadataResolver.MISSING_METADATA_VALUE ? "(missing)" : String(entry.value))}</span>
415+
</div>
416+
`).join("")}
417+
</div>
418+
</div>
419+
`
420+
: "";
395421
els.sourceLegend.hidden = false;
396-
els.sourceLegend.innerHTML = state.series.map((source, index) => `
397-
<label class="legend-item${source.visible ? "" : " is-muted"}" data-series-index="${index}">
398-
<input class="legend-toggle" type="checkbox" ${source.visible ? "checked" : ""} aria-label="Toggle ${core.escapeHtml(source.label)}">
399-
<span class="legend-swatch" style="background:${source.color}"></span>
400-
<span class="legend-label" title="${core.escapeHtml(source.label)}">${core.escapeHtml(source.label)}</span>
401-
</label>
402-
`).join("");
422+
els.sourceLegend.innerHTML = `
423+
${metadataLegendHtml}
424+
<div class="legend-section legend-section-datasets">
425+
<div class="legend-section-title">Datasets</div>
426+
${state.series.map((source, index) => {
427+
const resolved = resolvedByFilename.get(source.label);
428+
const metadataValue = resolved?.metadata_value === metadataResolver.MISSING_METADATA_VALUE
429+
? "(missing)"
430+
: resolved?.metadata_value;
431+
const detail = resolved?.metadata_field
432+
? `<span class="legend-detail">${core.escapeHtml(String(metadataValue))}</span>`
433+
: "";
434+
return `
435+
<label class="legend-item${source.visible ? "" : " is-muted"}" data-series-index="${index}">
436+
<input class="legend-toggle" type="checkbox" ${source.visible ? "checked" : ""} aria-label="Toggle ${core.escapeHtml(source.label)}">
437+
<span class="legend-swatch" style="background:${source.color}"></span>
438+
<span class="legend-copy">
439+
<span class="legend-label" title="${core.escapeHtml(source.label)}">${core.escapeHtml(source.label)}</span>
440+
${detail}
441+
</span>
442+
</label>
443+
`;
444+
}).join("")}
445+
</div>
446+
`;
403447

404448
els.sourceLegend.querySelectorAll(".legend-item").forEach((item) => {
405449
item.addEventListener("change", (event) => {

src/libunicorn/graphengine/index.html

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -127,12 +127,17 @@ <h3>Backend Metadata</h3>
127127
<select id="metadataFieldSelect" multiple size="5" aria-describedby="metadataFieldPickerHint"></select>
128128
</label>
129129
<div id="metadataFieldPickerHint" class="metadata-picker-hint">Select one or more metadata fields to display across the panel.</div>
130-
<section class="metadata-color-panel" aria-label="Metadata field colors">
130+
<section class="metadata-color-panel" aria-label="Metadata value colors">
131131
<div class="metadata-summary-head">
132-
<h3>Field Display Colors</h3>
132+
<h3>Visualization Value Colors</h3>
133133
</div>
134-
<div id="metadataFieldColorMeta" class="metadata-summary-meta">No metadata fields selected</div>
135-
<div id="metadataFieldColorList" class="metadata-field-color-list">Select one or more metadata fields to customize their display colors.</div>
134+
<label class="metadata-field-picker" for="metadataColorFieldSelect">
135+
<span>Active color field</span>
136+
<select id="metadataColorFieldSelect" aria-describedby="metadataColorFieldHint"></select>
137+
</label>
138+
<div id="metadataColorFieldHint" class="metadata-picker-hint">Colors are assigned per value in the active metadata field and reused by tree, barplot, and PCoA visual semantics.</div>
139+
<div id="metadataFieldColorMeta" class="metadata-summary-meta">No active metadata color field</div>
140+
<div id="metadataFieldColorList" class="metadata-field-color-list">Choose a metadata field to edit value colors.</div>
136141
</section>
137142
</section>
138143
<section class="metadata-selected-panel" aria-label="Selected dataset metadata">

src/libunicorn/graphengine/styles.css

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -819,6 +819,34 @@ code {
819819
padding-top: 6px;
820820
}
821821

822+
.legend-section {
823+
display: grid;
824+
gap: 8px;
825+
}
826+
827+
.legend-section-title {
828+
color: var(--muted);
829+
font-size: 11px;
830+
font-weight: 800;
831+
text-transform: uppercase;
832+
letter-spacing: 0.04em;
833+
}
834+
835+
.legend-meta-list {
836+
display: grid;
837+
gap: 8px;
838+
}
839+
840+
.legend-meta-item {
841+
display: flex;
842+
align-items: center;
843+
gap: 8px;
844+
min-width: 0;
845+
padding: 8px 10px;
846+
border: 1px solid #d7ccb7;
847+
background: #fffdf7;
848+
}
849+
822850
.client-log-head {
823851
display: flex;
824852
align-items: center;
@@ -974,6 +1002,21 @@ code {
9741002
font-weight: 700;
9751003
}
9761004

1005+
.legend-copy {
1006+
display: grid;
1007+
min-width: 0;
1008+
}
1009+
1010+
.legend-detail {
1011+
min-width: 0;
1012+
overflow: hidden;
1013+
text-overflow: ellipsis;
1014+
white-space: nowrap;
1015+
color: var(--muted);
1016+
font-size: 11px;
1017+
font-weight: 600;
1018+
}
1019+
9771020
.legend-toggle {
9781021
flex: 0 0 auto;
9791022
width: 16px;
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
from pathlib import Path
2+
3+
from playwright.sync_api import Page, expect
4+
5+
from .helpers import STEP_TIMEOUT_MS, log_step
6+
from .test_metadata_integration import ( # noqa: F401
7+
metadata_free_connected_page,
8+
metadata_free_page,
9+
metadata_free_services,
10+
)
11+
12+
13+
def test_metadata_legend_switches_with_active_field(
14+
metadata_free_connected_page: Page,
15+
tmp_path: Path,
16+
) -> None:
17+
metadata_file = tmp_path / "metadata.txt"
18+
metadata_file.write_text(
19+
"\n".join([
20+
"dataset\tgroup\thost\tsite",
21+
"Lib_sim1_collapsed.bdamage.txt\tlegend-group-alpha\tlegend-host-wheat\te2e-greenhouse-a",
22+
"Lib_sim1_collapsed.alnfilt.bdamage.txt\tlegend-group-beta\tlegend-host-barley\te2e-greenhouse-b",
23+
]),
24+
encoding="utf-8",
25+
)
26+
27+
log_step("Uploading metadata for active-field legend switching")
28+
metadata_free_connected_page.locator("#metadataFile").set_input_files(str(metadata_file))
29+
metadata_free_connected_page.locator("#uploadBtn").click()
30+
expect(metadata_free_connected_page.locator("#metadataSummaryMeta")).to_contain_text("2 matched", timeout=STEP_TIMEOUT_MS)
31+
32+
log_step("Rendering the backend tree so the main source legend becomes available")
33+
metadata_free_connected_page.locator("#renderBtn").click()
34+
metadata_free_connected_page.wait_for_function(
35+
"() => document.querySelectorAll('#treeSvg .node').length > 1",
36+
timeout=180000,
37+
)
38+
39+
source_legend = metadata_free_connected_page.locator("#sourceLegend")
40+
41+
log_step("Confirming the legend defaults to the first metadata field")
42+
expect(source_legend).to_contain_text("Coloring by group", timeout=STEP_TIMEOUT_MS)
43+
expect(source_legend).to_contain_text("legend-group-alpha", timeout=STEP_TIMEOUT_MS)
44+
expect(source_legend).to_contain_text("legend-group-beta", timeout=STEP_TIMEOUT_MS)
45+
46+
log_step("Switching the active metadata color field from group to host")
47+
metadata_free_connected_page.locator("#metadataColorFieldSelect").select_option("host")
48+
49+
expect(source_legend).to_contain_text("Coloring by host", timeout=STEP_TIMEOUT_MS)
50+
expect(source_legend).to_contain_text("legend-host-wheat", timeout=STEP_TIMEOUT_MS)
51+
expect(source_legend).to_contain_text("legend-host-barley", timeout=STEP_TIMEOUT_MS)
52+
expect(source_legend).not_to_contain_text("Coloring by group", timeout=STEP_TIMEOUT_MS)
53+
expect(source_legend).not_to_contain_text("legend-group-alpha", timeout=STEP_TIMEOUT_MS)

src/libunicorn/graphengine/tests/logs/e2e_backend.log

Lines changed: 0 additions & 8 deletions
This file was deleted.

src/libunicorn/graphengine/tests/logs/e2e_frontend.log

Lines changed: 0 additions & 17 deletions
This file was deleted.

0 commit comments

Comments
 (0)