Skip to content

Davereinhart/protein visualization #417

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 46 commits into
base: release-2025.3.0
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
6b519a9
Install PDBe-molstar and configure to replace NGL viewer in ProteinSt…
davereinhart May 19, 2025
35a8310
Protein visualizer layout and styling, add toggle button to hide visu…
davereinhart May 20, 2025
14e42fc
Protein structure view changes
davereinhart May 20, 2025
5d40c9d
Make ScoreSetHeatmap x and y axis element unselectable, add mode prop
davereinhart May 23, 2025
6ac3ca6
cleanup
davereinhart May 23, 2025
d258be2
Fix errors
davereinhart May 23, 2025
07ace1a
cleanup
davereinhart May 23, 2025
8b6a651
Add heatmap selection rectangle
davereinhart May 23, 2025
a218375
Enable row and column calculations for heatmap selection box
davereinhart May 23, 2025
8cf06d6
Updated heatmap range selection to work outside of datum context and …
davereinhart May 25, 2025
2e5b2de
Updates to enable selection of range of columns on ScoreSetHeatmap to…
davereinhart May 25, 2025
9086942
Replace ScoreSetView dialog with SideBar component to utilize full sc…
davereinhart May 27, 2025
d003337
Styling protein visualization, disabling hover tooltip during scorema…
davereinhart May 27, 2025
fafc3f9
Hide tooltips during ScoreSetHeatmap range selection while mouse down
davereinhart May 28, 2025
e79cc64
ScoreSetVisualizer size adjustments
davereinhart May 30, 2025
5b7de06
Update heatmap to handle single node click as single column selection…
davereinhart May 30, 2025
9558756
Refactoring heatmap module range selection to eliminate redundant calcs
davereinhart Jun 17, 2025
d376f55
Calculate mean score and color at each position for protein visualiza…
davereinhart Jun 17, 2025
f898e83
Color protein visualization by mean score at each residue position
davereinhart Jun 18, 2025
43a5147
ScoreSet/Protein visualizer: Use highlight instead of color on select…
davereinhart Jun 18, 2025
32024ff
Rework tooltips for protein visualization
davereinhart Jun 20, 2025
3147744
Update heatmap to allow range selection by start/end coordinates and …
davereinhart Jun 20, 2025
2290862
Add function to ScoreSetHeatmap component to set left scroll position…
davereinhart Jun 20, 2025
425fcfe
Emit residue click and hover events from protein visualization compon…
davereinhart Jun 20, 2025
0d20157
Add max missense score to protein visualization tooltips
davereinhart Jun 20, 2025
9385742
Move pbde-molstar import from index.hmtl to ProteinStructureView comp…
davereinhart Jun 27, 2025
8086aa6
Allow user to switch protein visualization color scheme between mean,…
davereinhart Jul 1, 2025
7d64554
Disable default behavior to display side chains when highlighting res…
davereinhart Jul 1, 2025
525aacd
Update d3 selection to ensure heatmap legend shows up on ScoreSetVisu…
davereinhart Jul 1, 2025
7280cbd
Make y-axis of heatmap including legend position static so that tick …
davereinhart Jul 2, 2025
b6bf582
Add stacked heatmap before main heatmap to ensure correct padding cal…
davereinhart Jul 2, 2025
265ad83
Update heatmap to calculate padding and width of newly added y-axis S…
davereinhart Jul 2, 2025
c602c73
Update saveChartAsFile function to allow elements to be excluded base…
davereinhart Jul 2, 2025
9c5604d
Merge branch 'main' into davereinhart/protein-visualization
davereinhart Jul 3, 2025
83a0dd9
Merge branch 'davereinhart/heatmap-fixed-axis' into davereinhart/prot…
davereinhart Jul 3, 2025
9ea23c9
Fix y-axis SVG padding and width for cross-browser compatibility
davereinhart Jul 3, 2025
41e5215
Change z-index of yAxisSvg so that hover tooltips are below it
davereinhart Jul 3, 2025
697900e
Remove unused imports from heatmap
davereinhart Jul 3, 2025
36acbeb
Prevent vertical scrolling on heatmap container
davereinhart Jul 3, 2025
e9df67c
Remove CSS class for heatmap large y-axis tick labels, replace with f…
davereinhart Jul 3, 2025
98a0fa1
Set yAxisSvg background color on render instead of refresh
davereinhart Jul 3, 2025
9994ee7
Merge branch 'davereinhart/heatmap-fixed-axis' into davereinhart/prot…
davereinhart Jul 3, 2025
814e17e
Prevent vertical scroll on heatmap container
davereinhart Jul 3, 2025
f2af473
Don't show heatmap on ScoreSetView if ScoreSetVisualizer is being sho…
davereinhart Jul 3, 2025
d1a64ff
Make heatmap's fixed y-axis tick labels clickable, and emit value of …
davereinhart Jul 3, 2025
b9f1bb2
Handle ScoreSetHeatmap row selection in ScoreSetVisualizer component
davereinhart Jul 7, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10,495 changes: 7,002 additions & 3,493 deletions package-lock.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
"moment": "^2.29.4",
"native-file-system-adapter": "^3.0.1",
"papaparse": "^5.4.1",
"pdbe-molstar": "^3.4.0",
"pdfmake": "^0.2.8",
"pinia": "^2.2.1",
"pluralize": "^8.0.0",
Expand Down
245 changes: 149 additions & 96 deletions src/components/ProteinStructureView.vue
Original file line number Diff line number Diff line change
@@ -1,42 +1,69 @@
<template>
<div class="mavedb-protein-structure-viewer-container" ref="container"></div>
<Dropdown v-model="selectedPdb" :options="pdbs" optionLabel="id" />
<Dropdown v-model="colorScheme" :options="colorSchemeOptions" />
</template>

<div style="display:flex; flex-flow: column; height: 100%;">
<span v-if="alphaFoldData?.length > 1" class="p-float-label" style="margin-top: 10px; margin-bottom:4px">
<Dropdown :id="$scopedId('alphafold-id')" style="height:3em" v-model="selectedAlphaFold" :options="alphaFoldData" optionLabel="id" />
<label :for="$scopedId('alphafold-id')">AlphaFold ID</label>
</span>
<div class="flex">
<span class="ml-2">Color by:</span>
<SelectButton class="protein-viz-colorby-button ml-2" v-model="colorBy" optionLabel="name" optionValue="value" :options="colorByOptions" />
</div>
<div id="pdbe-molstar-viewer-container" style="flex: 1; position: relative"></div>
</div>
</template>
<script>

import axios from 'axios'
import $ from 'jquery'
import * as NGL from 'ngl'
import Dropdown from 'primevue/dropdown'
import SelectButton from 'primevue/selectbutton'
import { PDBeMolstarPlugin } from 'pdbe-molstar/lib/viewer'
import 'pdbe-molstar/build/pdbe-molstar-light.css'
import _ from 'lodash'
import { watch, ref } from 'vue'

export default {
name: 'ProteinStructureView',
components: {Dropdown},
components: {Dropdown, SelectButton},
emits: ['hoveredOverResidue', 'clickedResidue'],

props: {
uniprotId: {
type: String,
required: true,
default: 'P02829'
},
selectedResidueRange: {
selectedResidueRanges: {
type: Array,
default: null
},
highlightedResidueRange: {
type: Array,
default: null
}
},
selectionData: {
type: Array,
default: () => []
},
rowSelected: {
type: Boolean,
default: false
},
residueTooltips: {
type: Array,
default: () => []
},
},

data: () => ({
uniprotData: null,
selectedPdb: null,
viewerInstance: null,
selectedAlphaFold: null,
stage: null,
mainComponent: null,
colorByOptions: [
{name: 'Mean Score', value: 'mean.color'},
{name: 'Min Missense Score', value: 'minMissense.color'},
{name: 'Max Missense Score', value: 'maxMissense.color'},
],
colorScheme: 'bfactor',
colorSchemeOptions: [
'atomindex',
Expand All @@ -62,39 +89,81 @@ export default {
'value',
'volume'
],
selectionRepresentations: []
}),

computed: {
pdbs: function() {
if (!this.uniprotData) {
return []
}
return $('entry dbReference[type="PDB"]', this.uniprotData).map((i, element) => {
const $element = $(element)
return {
id: $element.attr('id'),
method: $element.find('property[type="method"]').first().attr('value'),
resolution: $element.find('property[type="resolution"]').first().attr('value'),
chains: $element.find('property[type="chains"]').first().attr('value')
selectionDataWithSelectedColorBy: function() {
return _.map(this.selectionData, (x) => ({
start_residue_number: x.start_residue_number,
end_residue_number: x.end_residue_number,
color: _.get(x, this.colorBy, '#000')
}))
},
alphaFoldData: function() {
if (!this.uniprotData) {
return []
}
}).get().filter((pdb) => pdb.id != null)
}
return $('entry dbReference[type="AlphaFoldDB"]', this.uniprotData).map((i, element) => {
const $element = $(element)
return {
id: $element.attr('id'),
method: $element.find('property[type="method"]').first().attr('value'),
resolution: $element.find('property[type="resolution"]').first().attr('value'),
chains: $element.find('property[type="chains"]').first().attr('value')
}
}).get().filter((x) => x.id != null)
},
},

mounted: function() {
this.render()
},

setup(props) {
const colorBy = ref('mean.color')

watch(() => props.rowSelected, (newValue) => {
if (_.isNumber(newValue)) {
colorBy.value = [newValue, 'color']
} else {
colorBy.value = 'mean.color'
}
})

return {
colorBy,
}
},

watch: {
pdbs: {
colorBy: {
handler: function() {
this.viewerInstance.visual.select({data: this.selectionDataWithSelectedColorBy})
},
},
selectedResidueRanges: {
handler: function(newValue) {
const selectedRanges = newValue.map((x) => ({
start_residue_number: x.start,
end_residue_number: x.end,
color: null,
focus: true
}))
this.viewerInstance.visual.select({data:[...this.selectionDataWithSelectedColorBy, ...selectedRanges]})
this.viewerInstance.visual.highlight({
data: selectedRanges,
})
},
deep: true,
},
alphaFoldData: {
handler: function() {
let newSelectedPdb = null
if (this.selectedPdb) {
newSelectedPdb = this.pdbs.find((pdb) => pdb.id == newSelectedPdb.id)
let newSelectedAlphaFold = null
if (this.selectedAlphaFold) {
newSelectedAlphaFold = this.alphaFoldData.find((x) => x.id == newSelectedAlphaFold.id)
}
if (!this.selectedPdb && this.pdbs.length > 0) {
this.selectedPdb = this.pdbs[0]
if (!this.selectedAlphaFold && this.alphaFoldData.length > 0) {
this.selectedAlphaFold = this.alphaFoldData[0]
}
}
},
Expand All @@ -103,7 +172,7 @@ export default {
this.refreshSelection()
}
},
selectedPdb: {
selectedAlphaFold: {
handler: function() {
this.render()
}
Expand All @@ -122,33 +191,8 @@ export default {
},

methods: {
refreshSelection: function() {
if (this.stage && this.mainComponent) {
for (const representation of this.selectionRepresentations) {
console.log(representation)
//representation.setVisibility(false)
this.mainComponent.removeAllRepresentations()
this.mainComponent.removeRepresentation(representation)
}
this.selectionRepresentations = []
if (this.selectedResidueRange) {
// Get all atoms within 5 Angstroms.
var selection = new NGL.Selection(`${this.selectedResidueRange[0]}-${this.selectedResidueRange[1]}`);
var radius = 5
var atomSet = this.mainComponent.structure.getAtomSetWithinSelection( selection, radius );
// Expand selection to complete groups
var atomSet2 = this.mainComponent.structure.getAtomSetWithinGroup(atomSet)
this.selectionRepresentations.push(
this.mainComponent.addRepresentation('cartoon', {sele: atomSet2.toSeleString(), colorScheme: 'resname'})
)
console.log(this.selectionRepresentations)
//this.mainComponent.autoView()
}
}
},

fetchUniprotData: async function() {
const response = await axios.get(`https://www.uniprot.org/uniprot/${this.uniprotId}.xml`)
const response = await axios.get(`https://rest.uniprot.org/uniprotkb/${this.uniprotId}.xml`)
if (response.data) {
const parser = new DOMParser()
this.uniprotData = parser.parseFromString(response.data, 'text/xml')
Expand All @@ -158,50 +202,59 @@ export default {
},

render: function() {
const self = this
if (this.selectedPdb) {
if (!this.stage) {
this.stage = new NGL.Stage(this.$refs.container)
this.stage.signals.clicked.add((pickingProxy) => {
if (pickingProxy) {
const atom = pickingProxy.atom || pickingProxy.closestBondAtom
if (atom?.residueIndex != null) {
this.$emit('clickedResidue', {residueNumber: atom.residueIndex + 1})
}
// console.log(atom.qualifiedName())
}
})
this.stage.signals.hovered.add((pickingProxy) => {
if (pickingProxy) {
const atom = pickingProxy.atom || pickingProxy.closestBondAtom
if (atom?.residueIndex != null) {
this.$emit('hoveredOverResidue', {residueNumber: atom.residueIndex + 1})
}
// console.log(atom.qualifiedName())
}
})
}
// rcsb://1crn
this.stage.removeAllComponents()
this.stage.loadFile(`rcsb://${this.selectedPdb.id}`, /*{defaultRepresentation: true}*/).then((component) => {
this.mainComponent = component
component.addRepresentation('cartoon', {colorScheme: self.colorScheme})
//this.stage.autoView()
if (this.selectedAlphaFold) {
const viewerInstance = new PDBeMolstarPlugin()
const options = {
customData: {
url: `https://alphafold.ebi.ac.uk/files/AF-${this.selectedAlphaFold.id}-F1-model_v4.cif`,
format: 'cif',
},
/** This applies AlphaFold confidence score colouring theme for AlphaFold model */
// alphafoldView: true,
hideControls: true,
bgColor: { r: 255, g: 255, b: 255 },
// hideCanvasControls: [
// 'selection',
// 'animation',
// 'controlToggle',
// 'controlInfo',
// ],
// sequencePanel: true,
landscape: true,
highlightColor: '#ffffff',
selection: {
data: this.selectionDataWithSelectedColorBy,
},
selectInteraction: false,
};
const viewerContainer = document.getElementById('pdbe-molstar-viewer-container')
viewerInstance.render(viewerContainer, options)
viewerInstance.events.loadComplete.subscribe(() => {
viewerInstance.plugin.layout.context.canvas3d.camera.state.fog = 0
viewerInstance.plugin.layout.context.canvas3d.camera.state.clipFar = false
viewerInstance.visual.tooltips({data:this.residueTooltips.value})
})
} else {
//

document.addEventListener('PDB.molstar.click', (e) => {
this.$emit('clickedResidue', e.eventData)
})
document.addEventListener('PDB.molstar.mouseover', (e) => {
this.$emit('hoveredOverResidue', e.eventData)
})
this.viewerInstance = viewerInstance
}
}
}
}

</script>

<style scoped>

.mavedb-protein-structure-viewer-container {
height: 600px;
width: 600px;
<style>
.msp-plugin .msp-layout-standard {
border: 0;
}
.protein-viz-colorby-button .p-button {
padding: 2px !important;
font-size: 0.8em;
}

</style>
Loading