|
34 | 34 | :alphabet="alphabet" |
35 | 35 | :lineLen="lineLen" |
36 | 36 | /> --> |
| 37 | + <div v-if="alnLen === 0" class="all-masked-overlay" :style="{ gridColumn: '2', gridRow: `2 / ${entries.length + 2}` }">All sequences are masked</div> |
37 | 38 | <template v-for="({ name, aa, ss, seqStart, css }, j) in cachedEntryRanges[i]"> |
38 | 39 | <span class="header" :title="name" :style="headerStyle(j)" @click="handleClickHeader($event, j)">{{ |
39 | 40 | name }}</span> |
40 | 41 | <div class="sequence-wrapper" :style="sequenceStyle(j)"> |
41 | 42 | <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> |
44 | 45 | </div> |
45 | 46 | <div class="column-wrapper" v-if="j == 0" :style="overlayStyle(aa.length)"> |
46 | 47 | <div |
|
65 | 66 | :style="residueMarkerStyle(hoverInfo, end - start)" |
66 | 67 | ></div> |
67 | 68 | </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> |
69 | 70 | </template> |
70 | 71 | </div> |
71 | 72 | </div> |
@@ -221,6 +222,7 @@ export default { |
221 | 222 | data() { |
222 | 223 | return { |
223 | 224 | lineLen: 80, |
| 225 | + cachedCharWidth: 0, |
224 | 226 | resizeObserver: null, |
225 | 227 | hoverTimer: null, |
226 | 228 | activeColumn: "", |
@@ -319,10 +321,17 @@ export default { |
319 | 321 | return sequence.scrollWidth; |
320 | 322 | }, |
321 | 323 | 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)); |
323 | 332 | 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); |
326 | 335 | return [start, end]; |
327 | 336 | }); |
328 | 337 | }, |
@@ -500,26 +509,40 @@ export default { |
500 | 509 | }) |
501 | 510 | }, |
502 | 511 | handleResize() { |
503 | | - // Resize based on first row |
504 | 512 | 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; |
513 | 535 | } |
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)); |
519 | 536 |
|
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) { |
523 | 546 | this.lineLen = Math.min(this.lineLen + charDiff, this.entries[0].aa.length); |
524 | 547 | } |
525 | 548 | }, |
@@ -559,8 +582,9 @@ export default { |
559 | 582 | return Array.from(this.entries, entry => this.getEntryRange(entry, start, end, makeGradients)); |
560 | 583 | }, |
561 | 584 | countSequence(sequence, start) { |
| 585 | + if (!sequence.length) return 0; |
562 | 586 | let gaps = sequence.split('-').length - 1; |
563 | | - return start + this.lineLen - gaps; |
| 587 | + return start + sequence.length - gaps; |
564 | 588 | }, |
565 | 589 | generateCSSGradient(start, end, aaSequence, ssSequence) { |
566 | 590 | if (!this.scores) { |
@@ -1023,5 +1047,20 @@ export default { |
1023 | 1047 | .theme--dark .column-active::before { |
1024 | 1048 | border-top-color: rgba(255, 255, 255, 0.7); |
1025 | 1049 | } |
| 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 | +} |
1026 | 1065 |
|
1027 | 1066 | </style> |
0 commit comments