Skip to content

Commit ff94fe6

Browse files
committed
- Optimized result page of Foldseek and Folddisco with dynamic loading
- Added Top hit/100 summary sections
1 parent e655929 commit ff94fe6

15 files changed

+4002
-258
lines changed

frontend/NameField.vue

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -135,14 +135,6 @@ small.ticket {
135135
margin-right: 4px;
136136
}
137137
138-
/* @media print, screen and (max-width: 599px) {
139-
small.ticket {
140-
display: inline-block;
141-
line-height: 0.9;
142-
}
143-
} */
144-
145-
/* --- 설정 변수 --- */
146138
.custom-field-container {
147139
--primary-color: #fff;
148140
--error-color: #ff5252;

frontend/NavigationButton.vue

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,10 +65,15 @@ export default {
6565
tabOffset: {
6666
type: Number,
6767
default: 140
68+
},
69+
hasMoreClusters: {
70+
type: Boolean,
71+
default: false
6872
}
6973
},
7074
emits: [
71-
'needUpdate'
75+
'needUpdate',
76+
'needRenderNext'
7277
],
7378
watch: {
7479
scrollOffsetArr: {
@@ -125,7 +130,12 @@ export default {
125130
126131
// scrollNext behavior
127132
if (currIdx == this.scrollOffsetArr?.length-1 || endOfPage) {
128-
this.enableScrollNext = false
133+
if (this.hasMoreClusters) {
134+
this.enableScrollNext = true
135+
this.scrollNextTarget = Infinity
136+
} else {
137+
this.enableScrollNext = false
138+
}
129139
} else {
130140
this.enableScrollNext = true
131141
this.scrollNextTarget = this.scrollOffsetArr?.[currIdx+1] - this.tabOffset
@@ -144,6 +154,10 @@ export default {
144154
}
145155
},
146156
goTo(target) {
157+
if (target === Infinity) {
158+
this.$emit('needRenderNext')
159+
return
160+
}
147161
window.scrollTo({
148162
top: target,
149163
left: 0,

frontend/ResultFoldDisco.vue

Lines changed: 160 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -84,17 +84,36 @@
8484
:color="selectedDatabases > 0 ? hits.results[selectedDatabases - 1].color : null"
8585
center-active
8686
grow
87-
v-model="tabModel"
87+
v-model="selectedDatabases"
8888
show-arrows
8989
@change="handleChangeDatabase()"
9090
v-if="hits.results.length > 1"
9191
>
92-
<!-- <v-tab>All databases</v-tab> -->
92+
<v-tab>Summary</v-tab>
9393
<v-tab v-for="entry in hits.results" :key="entry.db">{{ entry.db.replaceAll(/_folddisco$/g, '') }} ({{ entry.alignments ? Object.values(entry.alignments).length : 0 }})</v-tab>
9494
</v-tabs>
9595
</v-sheet>
96+
<TopHits
97+
v-if="hits && hits.results && hits.results.length > 1"
98+
v-show="selectedDatabases == 0"
99+
:hits="hits" :mode="2"
100+
@jumpTo="i => selectedDatabases = i+1"
101+
/>
102+
<keep-alive>
103+
<Top100Folddisco
104+
v-if="hits && hits.results && hits.results.length > 1 && selectedDatabases == 0"
105+
ref="top100" :hits="hits" :alignment="alignment" :selectedStates="selectedStates"
106+
:selectedCounts="selectedCounts" :selectUpperbound="selectUpperbound"
107+
@showAlignment="(o, db, e) => showAlignment(o, db, e)"
108+
@toggleSelection="(db, i, v) => handleToggleSelection(db, i, v, true)"
109+
@bulkToggle="(a, v) => handleBulkToggleFromTop100(a, v)"
110+
@forwardDropdown="(e, h) => forwardDropdown(e, h)"
111+
@clearAll="clearAllEntries" @jumpTo="i => selectedDatabases = i+1"
112+
/>
113+
</keep-alive>
96114
<ResultFoldDiscoDB v-for="(entry, entryidx) in hits.results" :key="entry.db"
97-
v-if="selectedDatabases == 0 || (entryidx + 1) == selectedDatabases"
115+
:ref="'dbComponent' + entryidx"
116+
v-if="(entryidx + 1) == selectedDatabases"
98117
:entryidx="entryidx" :entry="entry" :toggleSourceDb="toggleSourceDb"
99118
:selectedStates="selectedStates[entryidx]" :selectedCounts="selectedCountPerDb[entryidx]"
100119
:totalSelectedCounts="selectedCounts" :selectUpperbound="selectUpperbound" :alignment="alignment"
@@ -105,6 +124,7 @@
105124
@toggleSelection="(i, v) => handleToggleSelection(entryidx, i, v)"
106125
@bulkToggle="(a, v) => handleBulkToggle(entryidx, a, v)"
107126
@updateScroll="() => updateScrollOffsetArr()"
127+
@clusterInfo="(info) => handleClusterInfo(entryidx, info)"
108128
></ResultFoldDiscoDB>
109129
</template>
110130
</panel>
@@ -126,7 +146,9 @@
126146
<NavigationButton :selectedDatabases="selectedDatabases"
127147
:scrollOffsetArr="scrollOffsetArr"
128148
:tabOffset="tabOffset"
149+
:hasMoreClusters="hasMoreClusters"
129150
@needUpdate="updateScrollOffsetArr"
151+
@needRenderNext="handleNeedRenderNext"
130152
></NavigationButton>
131153
</v-flex>
132154
</v-layout>
@@ -169,6 +191,8 @@ import NavigationButton from './NavigationButton.vue';
169191
import ResultFoldDiscoDB from './ResultFoldDiscoDB.vue';
170192
import SelectToSendPanel from './SelectToSendPanel.vue';
171193
import NameField from './NameField.vue';
194+
import TopHits from './TopHits.vue';
195+
import Top100Folddisco from './Top100Folddisco.vue';
172196
173197
function getAbsOffsetTop($el) {
174198
var sum = 0;
@@ -183,7 +207,15 @@ function getAbsOffsetTop($el) {
183207
export default {
184208
name: 'ResultFoldDisco',
185209
tool: 'folddisco',
186-
components: { Panel, StructureViewerMotif, NavigationButton, ResultFoldDiscoDB, SelectToSendPanel, NameField },
210+
components: { Panel,
211+
StructureViewerMotif,
212+
NavigationButton,
213+
ResultFoldDiscoDB,
214+
SelectToSendPanel,
215+
NameField,
216+
TopHits,
217+
Top100Folddisco,
218+
},
187219
// components: { ResultView },
188220
mixins: [ ResultMixin, ResultSankeyMixin ],
189221
data() {
@@ -205,11 +237,12 @@ export default {
205237
toggleSourceDb: "",
206238
dbToIdx: null,
207239
selectUpperbound: 100,
208-
selectedStates: {},
240+
selectedStates: null,
209241
selectedCounts: 0,
210-
selectedCountPerDb: {},
242+
selectedCountPerDb: null,
211243
selectedSets: new Set(),
212244
scrollOffsetArr: [],
245+
clusterInfoPerDb: {},
213246
}
214247
},
215248
created() {
@@ -273,19 +306,18 @@ export default {
273306
}
274307
return dbGaps;
275308
},
276-
tabModel: {
277-
get() {
278-
return this.selectedDatabases - 1;
279-
},
280-
set(val) {
281-
this.selectedDatabases = val + 1;
282-
}
283-
},
284309
tabOffset() {
285310
let addend = this.hits?.results?.length == 1 ? 92 : 140
286311
let sheetHeight = this.$vuetify.breakpoint.xsOnly ? 356 : this.$vuetify.breakpoint.mdAndDown ? 304 : 180
287312
let colheadHeight = 32
288313
return addend + sheetHeight + colheadHeight
314+
},
315+
hasMoreClusters() {
316+
for (const key in this.clusterInfoPerDb) {
317+
const info = this.clusterInfoPerDb[key]
318+
if (info.totalClusterCount > info.renderedClusterCount) return true
319+
}
320+
return false
289321
}
290322
},
291323
mounted() {
@@ -358,6 +390,7 @@ export default {
358390
this.updateScrollOffsetArr()
359391
}, 0)
360392
})
393+
this.selectedDatabases = n.results.length == 1 ? 1 : 0
361394
}
362395
},
363396
immediate: false,
@@ -387,7 +420,7 @@ export default {
387420
},
388421
async getTargetPdb(item, db) {
389422
let target = item.dbkey;
390-
if (db.startsWith("pdb_")) {
423+
if (db.startsWith("pdb")) {
391424
target = item.target;
392425
}
393426
const re = "api/result/folddisco/" + this.$route.params.ticket + '?database=' + db +'&id=' + target;
@@ -397,9 +430,9 @@ export default {
397430
try {
398431
const request = await this.$axios.get(re, {
399432
headers: {
400-
'Cache-Control': 'no-cache, no-store, must-revalidate',
401-
'Pragma': 'no-cache',
402-
'Expires': '0',
433+
// 'Cache-Control': 'no-cache, no-store, must-revalidate',
434+
// 'Pragma': 'no-cache',
435+
// 'Expires': '0',
403436
'Accept': 'text/plain',
404437
},
405438
transformResponse: [(d) => d],
@@ -436,7 +469,7 @@ export default {
436469
handleChangeDatabase() {
437470
this.closeAlignment();
438471
},
439-
handleToggleSelection(db, idx, value) {
472+
handleToggleSelection(db, idx, value, fromTop100=false) {
440473
if (!this.selectedStates || !this.selectedStates[db]
441474
|| this.selectedCounts > this.selectUpperbound && value) {
442475
return
@@ -449,21 +482,36 @@ export default {
449482
if (this.selectedStates[db][idx] != value) {
450483
// Does it really reflect changes?
451484
this.selectedStates[db][idx] = value
452-
let el = document.getElementById(id)
453-
if (el) {
454-
el.classList.toggle('selected', value)
485+
486+
if (fromTop100) {
487+
document.getElementById('top.' + id)?.classList.toggle('selected', value)
488+
} else {
489+
document.getElementById(id)?.classList.toggle('selected', value)
455490
}
491+
492+
456493
const newVal = this.selectedCountPerDb[db] + deltaUnit
457494
this.selectedCountPerDb[db] = newVal
458495
this.selectedCounts += deltaUnit
459496
toCall(id)
460497
461498
// update select-all button state
462-
const targetDbLength = this.selectedStates[db].length
463-
el = document.getElementById(db + '#select-all')
464-
if (el) {
465-
el.classList.toggle('any-selected', newVal > 0)
466-
el.classList.toggle('all-selected', newVal == targetDbLength)
499+
if (!fromTop100) {
500+
const targetDbLength = this.selectedStates[db].length
501+
let el = document.getElementById(db + '#select-all')
502+
if (el) {
503+
el.classList.toggle('any-selected', newVal > 0)
504+
el.classList.toggle('all-selected', newVal == targetDbLength)
505+
}
506+
} else {
507+
const length = this.$refs.top100.entryLength
508+
const newValTop = this.$refs.top100.selectedTopEntries + deltaUnit
509+
const selectAllButton = document.getElementById('top#select-all')
510+
if (selectAllButton) {
511+
selectAllButton.classList.toggle('any-selected', newValTop > 0)
512+
selectAllButton.classList.toggle('all-selected', newValTop == length)
513+
}
514+
this.$refs.top100.selectedTopEntries = newValTop
467515
}
468516
}
469517
},
@@ -485,10 +533,8 @@ export default {
485533
}
486534
if (this.selectedStates[db][i] != value) {
487535
let id = db + '#' + i.toString()
488-
let el = document.getElementById(id)
489-
if (el) {
490-
el.classList.toggle('selected', value)
491-
}
536+
document.getElementById(id)?.classList.toggle('selected', value)
537+
492538
this.selectedStates[db][i] = value
493539
toCall(id)
494540
delta += deltaUnit
@@ -507,6 +553,54 @@ export default {
507553
selectAllButton.classList.toggle('all-selected', newVal == dbLength)
508554
}
509555
},
556+
handleBulkToggleFromTop100(indices, value) {
557+
// This method is called by Top100 component only
558+
559+
if (!this.selectedStates
560+
|| this.selectedCounts > this.selectUpperbound && value) {
561+
return
562+
}
563+
564+
let delta = 0
565+
let deltaPerDb = Object.fromEntries(Object.keys(this.selectedStates).map(k => [k, 0]))
566+
const deltaUnit = value ? 1 : -1
567+
const deltaUpperbound = this.selectUpperbound - this.selectedCounts
568+
const toCall = value ? this.selectedSets.add.bind(this.selectedSets) :
569+
this.selectedSets.delete.bind(this.selectedSets)
570+
let dbSet = new Set()
571+
572+
for (const idx of indices) {
573+
if (value && delta >= deltaUpperbound) {
574+
break;
575+
}
576+
let [db, i] = idx.split("#")
577+
dbSet.add(db)
578+
579+
if (this.selectedStates[db][i] != value) {
580+
document.getElementById('top.' + idx)?.classList.toggle('selected', value)
581+
this.selectedStates[db][i] = value
582+
toCall(idx)
583+
delta += deltaUnit
584+
deltaPerDb[db] += deltaUnit
585+
}
586+
}
587+
588+
this.selectedCounts += delta
589+
for (let db of dbSet) {
590+
this.selectedCountPerDb[db] += delta
591+
}
592+
593+
const length = this.$refs.top100.entryLength
594+
const newVal = this.$refs.top100.selectedTopEntries + delta
595+
this.$refs.top100.selectedTopEntries = newVal
596+
597+
// update select-all button state
598+
const selectAllButton = document.getElementById('top#select-all')
599+
if (selectAllButton) {
600+
selectAllButton.classList.toggle('any-selected', newVal > 0)
601+
selectAllButton.classList.toggle('all-selected', newVal == length)
602+
}
603+
},
510604
updateToggleSourceDb(db) {
511605
this.toggleSourceDb = db
512606
},
@@ -532,6 +626,11 @@ export default {
532626
el.classList.toggle('all-selected', false)
533627
}
534628
}
629+
el = document.getElementById('top#select-all')
630+
if (el) {
631+
el.classList.toggle('any-selected', false)
632+
el.classList.toggle('all-selected', false)
633+
}
535634
536635
// update selected states manually
537636
let prevSelected = document.querySelectorAll('.selected')
@@ -661,6 +760,36 @@ export default {
661760
const arr = document.querySelectorAll('[class^="result-entry-"]')
662761
const offsetArr = [...arr].map(n => Math.ceil(n.getBoundingClientRect().top + window.scrollY))
663762
this.scrollOffsetArr = offsetArr
763+
},
764+
handleClusterInfo(entryidx, info) {
765+
this.$set(this.clusterInfoPerDb, entryidx, info)
766+
},
767+
async handleNeedRenderNext() {
768+
// Find the first child component that has unrendered clusters
769+
const results = this.hits?.results
770+
if (!results) return
771+
for (let i = 0; i < results.length; i++) {
772+
if (this.selectedDatabases != 0 && (i + 1) != this.selectedDatabases) continue
773+
const refs = this.$refs['dbComponent' + i]
774+
const comp = Array.isArray(refs) ? refs[0] : refs
775+
if (!comp) continue
776+
const info = this.clusterInfoPerDb[i]
777+
if (!info || info.totalClusterCount <= info.renderedClusterCount) continue
778+
// Find the next unrendered cluster key
779+
const nextKey = comp.clusterKeys[comp.renderedClusterKeys.length]
780+
if (!nextKey) continue
781+
await comp.renderUpToCluster(nextKey)
782+
this.$nextTick(() => {
783+
this.updateScrollOffsetArr()
784+
// Scroll to the newly rendered cluster
785+
const el = document.querySelector('.result-entry-' + i + nextKey)
786+
if (el) {
787+
const top = Math.ceil(el.getBoundingClientRect().top + window.scrollY) - this.tabOffset
788+
window.scrollTo({ top, left: 0, behavior: 'smooth' })
789+
}
790+
})
791+
return
792+
}
664793
}
665794
},
666795
};

0 commit comments

Comments
 (0)