From 2927ff6ddcf3eb7709cdb4d96a22b3522a1dcf57 Mon Sep 17 00:00:00 2001 From: Alexander Serdyukov Date: Mon, 18 May 2026 20:39:03 +0400 Subject: [PATCH 01/46] O/E per scaffold testing --- .../upper_ribbon/FileWizardModal.vue | 22 ++++++++++++++----- .../components/upper_ribbon/HeaderRibbon.vue | 2 +- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/src/app/ui/components/upper_ribbon/FileWizardModal.vue b/src/app/ui/components/upper_ribbon/FileWizardModal.vue index 01c7cc1..5a12dd0 100644 --- a/src/app/ui/components/upper_ribbon/FileWizardModal.vue +++ b/src/app/ui/components/upper_ribbon/FileWizardModal.vue @@ -203,6 +203,10 @@ +
+ Expected and O/E are computed independently inside each scaffold. If the opened assembly has no + scaffolds yet, each contig is treated as its own scaffold for expected-value estimation. +
@@ -732,6 +736,13 @@ const findPresetIdByName = (name: string): string => availablePresets.value[0]?.id ?? ""; +const usesExpectedPreset = computed( + () => + primaryPreset.value?.preset.options.signalDisplayMode !== "OBSERVED" || + (requiresSecondarySource.value && + secondaryPreset.value?.preset.options.signalDisplayMode !== "OBSERVED") +); + const canRunWizard = computed(() => wizardBlockingIssues.value.length === 0); const wizardBlockingIssues = computed(() => { const issues: string[] = []; @@ -762,11 +773,7 @@ const wizardBlockingIssues = computed(() => { "Bundled/external hictk toolchain is required for .hic conversion but is currently unavailable." ); } - const usesExpectedPreset = - primaryPreset.value?.preset.options.signalDisplayMode !== "OBSERVED" || - (requiresSecondarySource.value && - secondaryPreset.value?.preset.options.signalDisplayMode !== "OBSERVED"); - if (viewMode.value !== "single" && usesExpectedPreset) { + if (viewMode.value !== "single" && usesExpectedPreset.value) { issues.push( "Expected and O/E presets are currently supported only in single-map mode. Use Observed presets for overlay and upper/lower rendering." ); @@ -810,6 +817,11 @@ const wizardNotes = computed(() => { "Both AGPs are selected. This is supported for comparison, but the resulting two-source view may become intentionally unaligned after assembly edits." ); } + if (usesExpectedPreset.value) { + notes.push( + "Expected and O/E are computed inside each scaffold. If the assembly has no scaffolds yet, each contig is treated as its own scaffold." + ); + } return Array.from(new Set(notes)); }); diff --git a/src/app/ui/components/upper_ribbon/HeaderRibbon.vue b/src/app/ui/components/upper_ribbon/HeaderRibbon.vue index ca8abb6..42fbc76 100644 --- a/src/app/ui/components/upper_ribbon/HeaderRibbon.vue +++ b/src/app/ui/components/upper_ribbon/HeaderRibbon.vue @@ -110,7 +110,7 @@ - - - +
+
diff --git a/src/app/ui/components/upper_ribbon/CoolerConverter.vue b/src/app/ui/components/upper_ribbon/CoolerConverter.vue index 8f041d1..21e8504 100644 --- a/src/app/ui/components/upper_ribbon/CoolerConverter.vue +++ b/src/app/ui/components/upper_ribbon/CoolerConverter.vue @@ -121,6 +121,16 @@

Output: {{ deriveOutputFilename(selectedCoolerFilename) }}

+
+ .hic assembly layout: + .hic files do not reliably encode the contig/scaffold layout used by + Juicebox Assembly Tool. Keep the matching .assembly + or AGP file next to the dataset; load an AGP after conversion + when scaffolding coordinates are required. +

{{ singleBlockedMessage }}

diff --git a/src/app/ui/components/upper_ribbon/FileWizardModal.vue b/src/app/ui/components/upper_ribbon/FileWizardModal.vue index 5a12dd0..12c94ee 100644 --- a/src/app/ui/components/upper_ribbon/FileWizardModal.vue +++ b/src/app/ui/components/upper_ribbon/FileWizardModal.vue @@ -812,6 +812,14 @@ const wizardNotes = computed(() => { "Secondary AGP input is best treated as comparative metadata. Use the primary AGP as the authoritative assembly when scaffolding operations are expected." ); } + const selectedHicSources = [primarySource, secondarySource] + .filter((source, index) => index === 0 || requiresSecondarySource.value) + .filter((source) => source.filename.toLowerCase().endsWith(".hic")); + if (selectedHicSources.length > 0) { + notes.push( + ".hic sources do not reliably carry Juicebox/JBAT scaffold layout. Select the matching AGP after conversion; if the project only has a .assembly file, convert it to AGP before scaffolding-sensitive work." + ); + } if (primaryAgp.value && secondaryAgp.value) { notes.push( "Both AGPs are selected. This is supported for comparison, but the resulting two-source view may become intentionally unaligned after assembly edits." diff --git a/src/app/ui/components/upper_ribbon/NavigationBar.vue b/src/app/ui/components/upper_ribbon/NavigationBar.vue index c05d9fc..f8f00a5 100644 --- a/src/app/ui/components/upper_ribbon/NavigationBar.vue +++ b/src/app/ui/components/upper_ribbon/NavigationBar.vue @@ -36,6 +36,11 @@ >File
@@ -78,6 +90,11 @@ const featureTooltipVisible = ref(false); const featureTooltipTitle = ref(""); const featureTooltipRange = ref(""); const featureTooltipSecondary = ref(""); +const featureTooltipAttributes = ref>([]); const featureTooltipStyle = ref>({ left: "0px", top: "0px", @@ -161,7 +178,7 @@ const onMouseMoveFeature = (event: MouseEvent): void => { hit.kind === "feature" ? hit.label ?? hit.featureType ?? hit.trackName : hit.trackName; - featureTooltipRange.value = `${hit.startBp.toLocaleString()}-${hit.endBp.toLocaleString()} bp`; + featureTooltipRange.value = formatHoverRange(hit); const secondaryParts: string[] = []; if (hit.kind === "feature") { if (hit.featureType) { @@ -171,12 +188,14 @@ const onMouseMoveFeature = (event: MouseEvent): void => { secondaryParts.push(`strand ${hit.strand}`); } secondaryParts.push(hit.trackName); + featureTooltipAttributes.value = formatFeatureAttributes(hit.attributes); } else { secondaryParts.push(`value ${formatTrackValue(hit.value)}`); if (hit.count > 1) { secondaryParts.push(`${hit.count.toLocaleString()} bins/items`); } secondaryParts.push(hit.trackType); + featureTooltipAttributes.value = []; } featureTooltipSecondary.value = secondaryParts.join(" | "); featureTooltipVisible.value = true; @@ -192,6 +211,40 @@ const formatTrackValue = (value: number): string => { return value.toLocaleString(undefined, { maximumFractionDigits: 4 }); }; +const formatFeatureAttributes = ( + attributes: Record +): Array<{ key: string; value: string; swatch?: string }> => + Object.entries(attributes) + .filter(([key, value]) => key.trim().length > 0 && value.trim().length > 0) + .slice(0, 12) + .map(([key, value]) => ({ + key, + value, + swatch: key.toLowerCase() === "itemrgb" ? bedRgbToCss(value) : undefined, + })); + +const bedRgbToCss = (value: string): string | undefined => { + const channels = value.split(",").map((part) => Number(part.trim())); + if ( + channels.length !== 3 || + channels.some((channel) => !Number.isInteger(channel) || channel < 0 || channel > 255) + ) { + return undefined; + } + return `rgb(${channels[0]}, ${channels[1]}, ${channels[2]})`; +}; + +const formatHoverRange = (hit: { + startBp: number; + endBp: number; + startPx: number; + endPx: number; +}): string => { + const bpRange = `${hit.startBp.toLocaleString()}-${hit.endBp.toLocaleString()} bp`; + const binRange = `bins ${hit.startPx.toLocaleString()}-${hit.endPx.toLocaleString()}`; + return `${bpRange} | ${binRange}`; +}; + const onMouseLeaveFeature = (): void => { hideFeatureTooltip(); }; @@ -390,4 +443,13 @@ onBeforeUnmount(() => { .feature-meta { opacity: 0.95; } + +.feature-color-swatch { + display: inline-block; + width: 0.75em; + height: 0.75em; + margin-right: 0.35em; + border: 1px solid rgba(255, 255, 255, 0.65); + vertical-align: -0.05em; +} diff --git a/src/app/ui/components/upper_ribbon/FileWizardModal.vue b/src/app/ui/components/upper_ribbon/FileWizardModal.vue index ad256a6..9f439bf 100644 --- a/src/app/ui/components/upper_ribbon/FileWizardModal.vue +++ b/src/app/ui/components/upper_ribbon/FileWizardModal.vue @@ -1157,6 +1157,7 @@ const waitForTrackPrecompute = async (): Promise const applyWizardPresentationState = (): void => { matrixViewStore.setPresentationMode(viewMode.value); + matrixViewStore.setLayersSwapped(false); if (viewMode.value === "split") { matrixViewStore.setSelectionFastaSources("PRIMARY", "SECONDARY"); } else { diff --git a/src/app/ui/components/upper_ribbon/NormalizationSelector.vue b/src/app/ui/components/upper_ribbon/NormalizationSelector.vue index 38ef26f..cc87c34 100644 --- a/src/app/ui/components/upper_ribbon/NormalizationSelector.vue +++ b/src/app/ui/components/upper_ribbon/NormalizationSelector.vue @@ -35,33 +35,38 @@
- -
-
- - - -
- - Rectangles use current selection. Markers and rectangles stay aligned after scaffolding operations. -