Last active
February 4, 2020 13:22
-
-
Save oeway/a2ee17ce0c278cd7b8ba9774a7db9c07 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<docs lang="markdown"> | |
This plugin allows to visualize results of a classification of protein | |
localization patterns from microscope images of the entire HPA cell atlas(v18, 63955 images). | |
Classification is performed with the winning approach of a 2018 Kaggle challenge | |
(https://www.kaggle.com/c/human-protein-atlas-image-classification). | |
Plots shows a UMAP projection (Uniform Manifold Approximation and Projection) calculated from the | |
last layer of the network (densenet121, dimensions: 1024). Each dot is an image and when clicking on a | |
dot, the corresponding image is displayed together with information about the cell line, the | |
protein, and a link to the HPA website. The plugin also allows to search for a gene, cell line, or | |
a localization pattern. | |
</docs> | |
<config lang="json"> | |
{ | |
"name": "HPA-UMAP-3D", | |
"type": "window", | |
"tags": [], | |
"ui": "Explore protein localization patterns in HPA images.", | |
"version": "0.1.2", | |
"api_version": "0.1.2", | |
"description": "Explore protein localization patterns in HPA images.", | |
"icon": "photo", | |
"inputs": null, | |
"outputs": null, | |
"requirements": [ | |
"https://cdn.plot.ly/plotly-latest.min.js", | |
"https://unpkg.com/papaparse@latest/papaparse.min.js", | |
"https://cdn.jsdelivr.net/npm/[email protected]/dist/vue.min.js", | |
"https://unpkg.com/spectre.css/dist/spectre.min.css", | |
"https://unpkg.com/spectre.css/dist/spectre-exp.min.css", | |
"https://unpkg.com/spectre.css/dist/spectre-icons.min.css"], | |
"dependencies": [], | |
"defaults": {"w": 50, "h":20, "fullscreen": true}, | |
"cover": "https://dl.dropbox.com/s/6cjd5xg2io2mqg5/HPA-UMAP-3D-v0.1.3.gif" | |
} | |
</config> | |
<script lang="javascript"> | |
function gaussianRand() { | |
var rand = 0; | |
for (var i = 0; i < 6; i += 1) { | |
rand += Math.random(); | |
} | |
return (rand / 6)-0.5; | |
} | |
function transpose(data) { | |
let result = {}; | |
for (let row of data) { | |
for (let [key, value] of Object.entries(row)) { | |
result[key] = result[key] || []; | |
result[key].push(value); | |
} | |
} | |
return result; | |
} | |
const convertBase64ToFile = function (image) { | |
let mime = image.match(/data:([a-zA-Z0-9]+\/[a-zA-Z0-9-.+]+).*,.*/); | |
if (mime && mime.length) { | |
mime = mime[1]; | |
} | |
let ab | |
if(mime === 'image/svg+xml'){ | |
const byteString = image.split(',')[1]; | |
ab = decodeURIComponent(byteString) | |
} | |
else{ | |
const byteString = atob(image.split(',')[1]); | |
ab = new ArrayBuffer(byteString.length); | |
const ia = new Uint8Array(ab); | |
for (let i = 0; i < byteString.length; i += 1) { | |
ia[i] = byteString.charCodeAt(i); | |
} | |
} | |
const newBlob = new Blob([ab], { | |
type: mime, | |
}); | |
return newBlob; | |
}; | |
const COLORS = [ | |
'#f44336', '#e91e63', '#9c27b0', '#673ab7', '#3f51b5', | |
'#2196f3', '#03a9f4', '#00bcd4', '#009688', '#4caf50', | |
'#8bc34a', '#cddc39', '#ffeb3b', '#ffc107', '#ff9800', | |
'#ff5722', '#795548', '#009e9e', '#607d8b', '#d500f9', | |
'#212121', '#ff9e80', '#ff6d00', '#ffff00', '#76ff03', | |
'#00e676', '#64ffda', '#18ffff', | |
] | |
class ImJoyPlugin { | |
async setup() { | |
const self = this | |
this.store = {images: [], current_index: 0, loading: true, thumbnail: null, search_text: ''} | |
this.app = new Vue({ | |
el: '#app', | |
data: this.store, | |
methods: { | |
openImage(url){ | |
api.utils.openUrl(url) | |
}, | |
openHPA(meta){ | |
const url = 'https://www.proteinatlas.org/'+meta.gene_ensembl_id+'-'+meta.gene+'/cell/' | |
api.utils.openUrl(url) | |
}, | |
showImage(obj){ | |
this.images.push(obj) | |
if(this.images.length>5){ | |
this.images.shift() | |
} | |
this.current_index = this.images.length -1 | |
}, | |
showThumbnail(obj){ | |
this.thumbnail = obj | |
}, | |
search(text){ | |
self.updateUmap(self.umap_data, text) | |
}, | |
async export_image(){ | |
await api.showStatus('exporting as image...') | |
let url = await Plotly.toImage(self.umap_chart, {format:'png', height:2048, width:2048}) | |
await api.exportFile(convertBase64ToFile(url), 'HPA_V18_UMAP_densenet121.png') | |
url = await Plotly.toImage(self.umap_chart, {format:'svg', height:1024, width: 1024}) | |
await api.exportFile(convertBase64ToFile(url), 'HPA_V18_UMAP_densenet121.svg') | |
await api.showStatus('done') | |
} | |
} | |
}) | |
} | |
async plot(data){ | |
document.getElementById('chart').innerHTML = '' | |
const updatemenus =[ | |
{ | |
buttons: [ | |
{ | |
args: [{'marker.size': 0.1}], | |
label:'marker size: 0.1', | |
method:'restyle' | |
}, | |
{ | |
args: [{'marker.size': 0.5}], | |
label:'marker size: 0.5', | |
method:'restyle' | |
}, | |
{ | |
args: [{'marker.size': 1}], | |
label:'marker size: 1', | |
method:'restyle' | |
}, | |
{ | |
args: [{'marker.size': 2}], | |
label:'marker size: 2', | |
method:'restyle' | |
}, | |
{ | |
args: [{'marker.size': 4}], | |
label:'marker size: 4', | |
method:'restyle' | |
}, | |
{ | |
args: [{'marker.size': 8}], | |
label:'marker size: 8', | |
method:'restyle' | |
}, | |
{ | |
args: [{'marker.size': 16}], | |
label:'marker size: 16', | |
method:'restyle' | |
} | |
], | |
direction: 'down', | |
pad: {'r': 0, 't': 0}, | |
active: 1, | |
showactive: true, | |
type: 'dropdown', | |
x: 0.8, | |
xanchor: 'left', | |
y: 1.05, | |
yanchor: 'top' | |
} | |
] | |
const layout = { | |
hovermode:'closest', | |
title:'HPAv18 UMAP (model: densenet121, images: '+data[0].x.length+')', | |
xaxis: { | |
showticklabels: false, | |
nticks: 10, | |
domain: [-10, 10], | |
range: [-10, 10], | |
zeroline: false | |
}, | |
yaxis: { | |
showticklabels: false, | |
nticks: 10, | |
domain: [-10, 10], | |
range: [-10, 10], | |
zeroline: false | |
}, | |
updatemenus: updatemenus | |
} | |
this.umap_chart = await Plotly.newPlot('chart', data, layout,{responsive: true}) | |
// Plotly.relayout('chart', { | |
// width: document.getElementById('chart').clientWidth, | |
// height: document.getElementById('chart').clientHeight | |
// }) | |
// let win=null; | |
document.getElementById('chart').on('plotly_click', async (data) => { | |
// Plotly.relayout('chart', { | |
// width: document.getElementById('chart').clientWidth, | |
// height: document.getElementById('chart').clientHeight | |
// }) | |
for(var i=0; i < data.points.length; i++){ | |
console.log('====>', data.points[i].customdata) | |
const id = data.points[i].customdata.id | |
//pts = data.points[i].customdata.gene+'\n'; | |
const image_base_url = 'https://images.proteinatlas.org/' + id.split('_')[0] + '/' + id.split('_')[1] +'_' + id.split('_')[2] + '_'+ id.split('_')[3] | |
this.app.showImage({url: image_base_url+'_blue.jpg', meta: data.points[i].customdata}) | |
this.app.showImage({url: image_base_url+'_red.jpg', meta: data.points[i].customdata}) | |
this.app.showImage({url: image_base_url+'_green.jpg', meta: data.points[i].customdata}) | |
this.app.showImage({url: image_base_url+'_yellow.jpg', meta: data.points[i].customdata}) | |
this.app.showImage({url: image_base_url+'_blue_red_green.jpg', meta: data.points[i].customdata}) | |
const thumbnail_url = 'https://images.proteinatlas.org/' + id.split('_')[0] + '/' + id.split('_')[1] +'_' + id.split('_')[2] + '_'+ id.split('_')[3]+ '_blue_red_green_yellow_thumb.jpg' | |
this.app.showThumbnail({url: thumbnail_url, meta: data.points[i].customdata}) | |
await api.showStatus(data.points[i].customdata.gene+', '+ data.points[i].customdata['cell_line'] + ', ' + data.points[i].customdata.location+ ', '+data.points[i].customdata.id) | |
const screen_width = window.innerWidth | |
|| document.documentElement.clientWidth | |
|| document.body.clientWidth; | |
if(screen_width<1024) window.scrollTo(0,document.body.scrollHeight); | |
} | |
//api.alert('Closest point clicked:\n\n'+pts); | |
}); | |
document.getElementById('chart').on('plotly_hover', async (data) => { | |
for(var i=0; i < data.points.length; i++){ | |
const id = data.points[i].customdata.id | |
const thumbnail_url = 'https://images.proteinatlas.org/' + id.split('_')[0] + '/' + id.split('_')[1] +'_' + id.split('_')[2] + '_'+ id.split('_')[3]+ '_blue_red_green_yellow_thumb.jpg' | |
this.app.showThumbnail({url: thumbnail_url, meta: data.points[i].customdata}) | |
} | |
}); | |
} | |
async generateFakeData(){ | |
const Xs = [], Ys = [], Zs = [], Meta=[], n = 100000; | |
for (let i = 0; i < n; i += 1) { | |
Xs.push(gaussianRand()); | |
Ys.push(gaussianRand()); | |
Zs.push(gaussianRand()); | |
Meta.push('index: '+i) | |
} | |
const data = [{ | |
type: "scatter3d", | |
mode: "markers", | |
marker: { | |
line: { | |
width: 1, | |
color: '#404040'} | |
}, | |
x: Xs, | |
y: Ys, | |
z: Zs, | |
customdata: Meta | |
}] | |
return data | |
} | |
async updateUmap(table, filter){ | |
filter = (filter || '') && filter.trim() | |
const filters = filter.split(' ') | |
table = table.filter((row)=>{ | |
let ret = false | |
for(let filter of filters){ | |
ret = ret || (!filter || row.gene === filter || row['gene_ensembl_id'] === filter || row['cell_line'] === filter || row['id'] === filter || row['location'].includes(filter) ) | |
} | |
return row.gene && ret | |
}) | |
console.log('filtered: ', table) | |
if(table.length<=0){ | |
await api.alert('No image found.') | |
return | |
} | |
await api.showMessage(table.length + ' images found.') | |
const dataObj = transpose(table) | |
const color_map = {} | |
for(let i=0; i<COLORS.length; i++){ | |
color_map[i] = COLORS[i] | |
} | |
const data = [{ | |
type: "scatter3d", | |
mode: "markers", | |
marker: { | |
size: 2, | |
color: dataObj.location_code.map((index)=>{ | |
if(typeof index === 'string'){ | |
// return '#e0e0e0d5' | |
return '#c6c6c6' | |
} | |
else{ | |
return color_map[index] | |
} | |
}) | |
//colorscale: 'Viridis' | |
}, | |
x: dataObj.x, | |
y: dataObj.y, | |
z: dataObj.z, | |
customdata: table | |
}] | |
this.plot(data) | |
} | |
async run(my) { | |
// const url = "https://dl.dropbox.com/s/k9ekd4ff3fyjbfk/umap_results_fit_all_transform_all_sorted_20190422.csv" | |
const url = "https://dl.dropbox.com/s/s4m2iysupy8gwj0/umap_results_fit_all_transform_all.csv" | |
Papa.parse(url, { | |
download: true, | |
header: true, | |
dynamicTyping: true, | |
skipEmptyLines: true, | |
error: (err, file, inputElem, reason)=> { | |
api.alert('Falied to load the table: ' + reason.toString()) | |
this.app.loading = false | |
this.app.$forceUpdate() | |
}, | |
complete: (results) => { | |
this.app.loading = false | |
this.app.$forceUpdate() | |
console.log('===============>', results) | |
this.umap_data = results.data | |
this.updateUmap(this.umap_data) | |
} | |
}) | |
} | |
} | |
api.export(new ImJoyPlugin()) | |
</script> | |
<window lang="html"> | |
<div id="app"> | |
<header class="navbar"> | |
<section class="navbar-section"> | |
<a class="navbar-brand mr-2">HPA UMAP</a> | |
<button @click="export_image()" class="btn btn-link">Export</button> | |
</section> | |
<section class="navbar-section"> | |
<div class="input-group input-inline"> | |
<input class="form-input" type="text" v-model="search_text" @keyup.enter="search(search_text)" placeholder="search"> | |
<button class="btn btn-primary input-group-btn" @click="search(search_text)">Search</button> | |
</div> | |
</section> | |
</header> | |
<div class="container"> | |
<div class="loading loading-lg" v-if="loading">Loading...</div> | |
<div class="columns" v-show="!loading"> | |
<div id="chart" class="column col-2"></div> | |
<div id="image-container" class="column col-2"> | |
<img v-if="thumbnail" class="img-responsive rounded hpa-thumbnail" :src="thumbnail.url" :alt="thumbnail.meta.id"></img> | |
<div v-if="images[current_index]" style="text-align: center;"> | |
<br> | |
<h3>{{images[current_index].meta['gene']}} ({{images[current_index].meta['cell_line']}})</h3> | |
<p>cell line: {{images[current_index].meta['cell_line']}}</p> | |
<p>gene: {{images[current_index].meta['gene']}}</p> | |
<p>location: {{images[current_index].meta['location']}}</p> | |
<p>image id: {{ images[current_index].meta.id}} </p> | |
<button class="btn btn-primary" @click="openHPA(images[current_index].meta)">Open HPA page</button> <button class="btn" @click="openImage(images[current_index].url)"><i class="icon icon-share"></i></button> | |
</div> | |
<h2 v-else> | |
<br> | |
Click on the UMAP to see the sample image. | |
</h2> | |
<div class="carousel"> | |
<!-- carousel locator --> | |
<input class="carousel-locator" v-for="(img, i) in images" :id="'slide-'+i" type="radio" name="carousel-radio" hidden="" v-model="current_index" :value="i" checked=""> | |
<!-- carousel container --> | |
<div class="carousel-container"> | |
<!-- carousel item --> | |
<figure class="carousel-item" v-for="(img, i) in images"> | |
<img class="img-responsive rounded" :src="img.url" :alt="img.meta.id" @click="openImage(images[current_index].url)"> | |
</figure> | |
</div> | |
<!-- carousel navigation --> | |
<div class="carousel-nav"> | |
<label class="nav-item text-hide c-hand" v-for="(img, i) in images" :for="'slide-'+i">{{img.meta.id}}</label> | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
</window> | |
<style lang="css"> | |
#chart{ | |
width: 800px; | |
height: 800px; | |
max-width: calc( 100% + 140px ); | |
margin-left: -70px; | |
} | |
#image-container{ | |
text-align: center; | |
width: 400px; | |
height: 400px; | |
max-width: 100%; | |
} | |
.carousel{ | |
margin: 20px; | |
} | |
.hpa-thumbnail{ | |
position: absolute; | |
left: 30px; | |
top: 100px; | |
} | |
</style> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment