Skip to content

Commit 71cd819

Browse files
committed
Fixed layout break issue when all the sequences are masked out
1 parent 1a60a6f commit 71cd819

2 files changed

Lines changed: 101 additions & 41 deletions

File tree

frontend/MSA.vue

Lines changed: 38 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@
7272
</v-col>
7373
</v-row>
7474
<v-card class="minimap fill-height">
75-
<v-row dense v-if="cssGradients" style="align-items: center;">
75+
<v-row dense style="align-items: center;">
7676
<v-col align="center" no-gutters style="max-width: fit-content; margin-right: 4px; position: relative;">
7777
<div style="display: flex; flex-direction: row;">
7878
<div class="input-div-wrapper expansion-panel" :class="{ 'is-expanded': settingsPanelOpen }">
@@ -129,22 +129,24 @@
129129
</div>
130130
</v-col>
131131
<v-col class="minimap-col">
132-
<div
133-
v-for="(block, i) in cssGradients"
134-
:key="'col-' + i"
135-
class="gradient-block-col"
136-
:style="minimapBlock(i)"
137-
@click="handleMapBlockClick(i)"
138-
>
139-
<div class="gradient-block">
140-
<div
132+
<template v-if="cssGradients">
133+
<div
134+
v-for="(block, i) in cssGradients"
135+
:key="'col-' + i"
136+
class="gradient-block-col"
137+
:style="minimapBlock(i)"
138+
@click="handleMapBlockClick(i)"
139+
>
140+
<div class="gradient-block">
141+
<div
141142
v-for="(gradient, j) in block"
142143
:key="'gradient-' + j"
143144
class="gradient-row"
144145
:style="{ 'background-image': gradient }"
145-
/>
146+
/>
147+
</div>
146148
</div>
147-
</div>
149+
</template>
148150
</v-col>
149151
<div class="pl-2">
150152

@@ -289,6 +291,7 @@ function clamp(value, min, max) {
289291
}
290292
291293
function makeMatchRatioMask(entries, ratio) {
294+
if (!entries.length || !entries[0]?.aa) return [];
292295
const columnLength = entries[0].aa.length;
293296
const mask = new Array(columnLength).fill(0);
294297
for (let i = 0; i < columnLength; i++) {
@@ -562,6 +565,9 @@ export default {
562565
this.settingsPanelOpen = !this.settingsPanelOpen;
563566
},
564567
handleUpdateMatchRatio: function() {
568+
if (!this.entries.length
569+
|| !this.entries[0]?.aa
570+
|| this.entries[0].aa.length == 0) return;
565571
if (this.matchRatio === 0.0) {
566572
this.mask = new Array(this.entries[0].aa.length).fill(1);
567573
} else {
@@ -716,16 +722,20 @@ export default {
716722
},
717723
handleCSSGradient(gradients) {
718724
const numBlocks = Math.ceil(this.alnLen / this.lineLen);
719-
const blockSize = gradients.length / numBlocks;
725+
if (!numBlocks || !isFinite(numBlocks) || !gradients.length) {
726+
this.cssGradients = null;
727+
this.gradientRatio = null;
728+
return;
729+
}
720730
721731
// Organise into blocks. Subsetted to numMinimapGradients for large MSAs
722732
// Use a step to ensure coverage over entire MSA.
733+
const blockSize = gradients.length / numBlocks;
723734
this.cssGradients = Array.from({ length: numBlocks }, () => []);
724735
if (blockSize < this.numMinimapGradients) {
725736
this.cssGradients.forEach((arr, i) => {
726-
let block = i * blockSize;
727-
let slice = gradients.slice(block, block + blockSize);
728-
arr.push(...slice);
737+
const block = i * blockSize;
738+
arr.push(...gradients.slice(block, block + blockSize));
729739
});
730740
} else {
731741
const step = (blockSize - 1) / (this.numMinimapGradients - 1);
@@ -738,8 +748,19 @@ export default {
738748
739749
// Calculate length of last block (all others will be lineLen)
740750
// Get array of %s that sum to 100%
741-
const lastBlockLen = this.cssGradients[numBlocks - 1][0].split('%,').length / 2;
751+
const lastBlock = this.cssGradients[numBlocks - 1];
752+
if (!lastBlock?.length || !lastBlock[0]) {
753+
this.cssGradients = null;
754+
this.gradientRatio = null;
755+
return;
756+
}
757+
const lastBlockLen = lastBlock[0].split('%,').length / 2;
742758
const total = (numBlocks - 1) * this.lineLen + lastBlockLen;
759+
if (!total) {
760+
this.cssGradients = null;
761+
this.gradientRatio = null;
762+
return;
763+
}
743764
this.gradientRatio = new Array(numBlocks - 1).fill(this.lineLen / total * 100);
744765
this.gradientRatio.push(lastBlockLen / total * 100);
745766
},

frontend/MSAView.vue

Lines changed: 63 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -34,13 +34,14 @@
3434
:alphabet="alphabet"
3535
:lineLen="lineLen"
3636
/> -->
37+
<div v-if="alnLen === 0" class="all-masked-overlay" :style="{ gridColumn: '2', gridRow: `2 / ${entries.length + 2}` }">All sequences are masked</div>
3738
<template v-for="({ name, aa, ss, seqStart, css }, j) in cachedEntryRanges[i]">
3839
<span class="header" :title="name" :style="headerStyle(j)" @click="handleClickHeader($event, j)">{{
3940
name }}</span>
4041
<div class="sequence-wrapper" :style="sequenceStyle(j)">
4142
<span class="sequence"
42-
:style="[css, { 'color': sequenceColor, 'font-weight': fontWeight }]">{{
43-
alphabet == 'aa' ? aa : ss }}</span>
43+
:style="alnLen > 0 ? [css, { 'color': sequenceColor, 'font-weight': fontWeight }] : {}">{{
44+
alnLen > 0 ? (alphabet == 'aa' ? aa : ss) : '' }}</span>
4445
</div>
4546
<div class="column-wrapper" v-if="j == 0" :style="overlayStyle(aa.length)">
4647
<div
@@ -65,7 +66,7 @@
6566
:style="residueMarkerStyle(hoverInfo, end - start)"
6667
></div>
6768
</div>
68-
<span class="count" :style="countStyle(j)">{{ countSequence(aa, seqStart).toString() }}</span>
69+
<span class="count" :style="countStyle(j)">{{ alnLen > 0 ? countSequence(aa, seqStart) : '' }}</span>
6970
</template>
7071
</div>
7172
</div>
@@ -221,6 +222,7 @@ export default {
221222
data() {
222223
return {
223224
lineLen: 80,
225+
cachedCharWidth: 0,
224226
resizeObserver: null,
225227
hoverTimer: null,
226228
activeColumn: "",
@@ -319,10 +321,17 @@ export default {
319321
return sequence.scrollWidth;
320322
},
321323
blockRanges() {
322-
const maxVal = Math.max(1, Math.ceil(this.alnLen / this.lineLen));
324+
const len = this.lineLen;
325+
if (!len || !isFinite(len) || len <= 0) {
326+
// lineLen is invalid (NaN, 0, Infinity) — return a single block
327+
// covering whatever alnLen we have so the DOM block always exists
328+
// and handleResize can recover a valid lineLen from it.
329+
return [[0, this.alnLen]];
330+
}
331+
const maxVal = Math.max(1, Math.ceil(this.alnLen / len));
323332
return Array.from({ length: maxVal }, (_, i) => {
324-
let start = i * this.lineLen;
325-
let end = Math.min(this.alnLen, i * this.lineLen + this.lineLen);
333+
let start = i * len;
334+
let end = Math.min(this.alnLen, i * len + len);
326335
return [start, end];
327336
});
328337
},
@@ -500,26 +509,40 @@ export default {
500509
})
501510
},
502511
handleResize() {
503-
// Resize based on first row
504512
const container = document.querySelector(".msa-block");
505-
if (!container) {
506-
return
507-
}
508-
const header = container.querySelector(".header");
509-
const count = container.querySelector(".count");
510-
const sequence = container.querySelector(".sequence");
511-
if (sequence && sequence.textContent.length > 0) {
512-
this.conservationScaleX = Math.max(1, sequence.scrollWidth / sequence.textContent.length);
513+
if (!container) return;
514+
515+
const header = container.querySelector(".header");
516+
const count = container.querySelector(".count");
517+
const sequence = container.querySelector(".sequence");
518+
if (!sequence || !header || !count) return;
519+
520+
const seqLen = sequence.textContent.length;
521+
const seqWidth = sequence.scrollWidth;
522+
const sideWidth = header.offsetWidth + count.offsetWidth + 32;
523+
const availWidth = container.offsetWidth - sideWidth;
524+
525+
if (seqLen === 0 || seqWidth === 0) {
526+
// All columns masked — sequence is empty and unmeasurable.
527+
// Use the last known char width to keep lineLen valid on resize.
528+
// Do NOT cap against entries[0].aa.length here: when all columns
529+
// are masked that length is 0, which would set lineLen=0 and break
530+
// blockRanges (0/0 = NaN → Infinity → empty array).
531+
if (this.cachedCharWidth > 0 && availWidth > 0) {
532+
this.lineLen = Math.floor(availWidth / this.cachedCharWidth);
533+
}
534+
return;
513535
}
514-
const containerWidth = container.offsetWidth - header.offsetWidth - count.offsetWidth - 32;
515-
516-
// calculate #chars difference
517-
const content = sequence.textContent;
518-
const charDiff = Math.abs(Math.ceil(content.length * (sequence.scrollWidth - containerWidth) / sequence.scrollWidth));
519536
520-
if (sequence.scrollWidth > containerWidth) {
521-
this.lineLen = Math.min(this.lineLen - charDiff, this.entries[0].aa.length);
522-
} else if (sequence.scrollWidth < containerWidth) {
537+
const charWidth = seqWidth / seqLen;
538+
this.cachedCharWidth = charWidth;
539+
this.conservationScaleX = Math.max(1, charWidth);
540+
541+
const charDiff = Math.round(Math.abs(seqWidth - availWidth) / charWidth);
542+
543+
if (seqWidth > availWidth) {
544+
this.lineLen = Math.max(1, this.lineLen - charDiff);
545+
} else if (seqWidth < availWidth) {
523546
this.lineLen = Math.min(this.lineLen + charDiff, this.entries[0].aa.length);
524547
}
525548
},
@@ -559,8 +582,9 @@ export default {
559582
return Array.from(this.entries, entry => this.getEntryRange(entry, start, end, makeGradients));
560583
},
561584
countSequence(sequence, start) {
585+
if (!sequence.length) return 0;
562586
let gaps = sequence.split('-').length - 1;
563-
return start + this.lineLen - gaps;
587+
return start + sequence.length - gaps;
564588
},
565589
generateCSSGradient(start, end, aaSequence, ssSequence) {
566590
if (!this.scores) {
@@ -1023,5 +1047,20 @@ export default {
10231047
.theme--dark .column-active::before {
10241048
border-top-color: rgba(255, 255, 255, 0.7);
10251049
}
1050+
.all-masked-overlay {
1051+
display: flex;
1052+
align-items: center;
1053+
justify-content: center;
1054+
font-family: monospace;
1055+
font-style: italic;
1056+
font-size: 13px;
1057+
letter-spacing: normal;
1058+
color: rgba(120, 120, 120, 0.9);
1059+
pointer-events: none;
1060+
z-index: 4;
1061+
}
1062+
.theme--dark .all-masked-overlay {
1063+
color: rgba(180, 180, 180, 0.7);
1064+
}
10261065
10271066
</style>

0 commit comments

Comments
 (0)