Skip to content

Instantly share code, notes, and snippets.

@DanielKoohmarey
Last active May 31, 2021 17:07
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
Star You must be signed in to star a gist
Save DanielKoohmarey/201c89f0e270f080dedb5e5798f2f0a3 to your computer and use it in GitHub Desktop.
Single page offline web app for DNA phenotyping using multinomial logistic models
<!DOCTYPE html>
<html lang=en>
<head>
<title>284 And Me</title>
<style>
p {
max-width: 800px;
margin: auto;
}
body {
margin: 0px;
text-align: center;
font-family: system-ui;
}
table {
margin: auto;
}
#drop_zone {
border: 5px solid grey;
width: 400px;
height: 200px;
border-radius: 5px;
margin: auto;
border-style: dashed;
margin-bottom: 20px;
font-style: italic;
color: grey;
margin-top: 20px;
}
#filename, #filetype {
font-style: italic;
}
#error {
color: red;
display: none;
}
h1 {
margin-top: 0px;
box-shadow: #3773cd 0px 5px 15px 0px;
background-color: skyblue;
color: white;
}
#iris-prediction, #hair-prediction {
font-weight: bold;
}
/* https://loading.io/css/ */
#page-cover {
z-index: 99;
position: absolute;
top: 0;
width: 100%;
height: 100%;
overflow: hidden;
background-color: skyblue;
opacity: .5;
display: none;
}
.lds-grid {
display: none;
position: absolute;
top: 250px;
left: 0;
right: 0;
margin: auto;
width: 80px;
height: 80px;
z-index: 999;
}
.lds-grid div {
position: absolute;
width: 16px;
height: 16px;
border-radius: 50%;
background: white;
animation: lds-grid 1.2s linear infinite;
}
.lds-grid div:nth-child(1) {
top: 8px;
left: 8px;
animation-delay: 0s;
}
.lds-grid div:nth-child(2) {
top: 8px;
left: 32px;
animation-delay: -0.4s;
}
.lds-grid div:nth-child(3) {
top: 8px;
left: 56px;
animation-delay: -0.8s;
}
.lds-grid div:nth-child(4) {
top: 32px;
left: 8px;
animation-delay: -0.4s;
}
.lds-grid div:nth-child(5) {
top: 32px;
left: 32px;
animation-delay: -0.8s;
}
.lds-grid div:nth-child(6) {
top: 32px;
left: 56px;
animation-delay: -1.2s;
}
.lds-grid div:nth-child(7) {
top: 56px;
left: 8px;
animation-delay: -0.8s;
}
.lds-grid div:nth-child(8) {
top: 56px;
left: 32px;
animation-delay: -1.2s;
}
.lds-grid div:nth-child(9) {
top: 56px;
left: 56px;
animation-delay: -1.6s;
}
@keyframes lds-grid {
0%, 100% {
opacity: 1;
}
50% {
opacity: 0.5;
}
}
/* https://codepen.io/dlouise/pen/gLYaMg */
.eye-ball {
margin-top: 20px;
margin-bottom: 20px;
display: inline-block;
height: 175px;
width: 175px;
border-radius: 50%;
background: #feffff;
position: relative;
background: radial-gradient(ellipse at center, #feffff 50%, #aaa 100%);
}
.iris {
height: 50px;
width: 50px;
border-radius: 50%;
width: 50px;
height: 50px;
padding: 20px;
position: absolute;
top: 50%;
left: 50%;
margin: -48px 0 0 -45px;
}
.iris.saddlebrown {
background: radial-gradient(ellipse at center, saddlebrown 48%, #002B04 100%);
}
.iris.deepskyblue {
background: radial-gradient(ellipse at center, deepskyblue 48%, #002B04 100%);
}
.iris.grey {
background: radial-gradient(ellipse at center, white 48%, #002B04 100%);
}
.pupil {
background-color: #000;
border-radius: 50%;
width: 39px;
height: 39px;
position: absolute;
top: 50%;
left: 50%;
margin: -21px 0 0 -20px;
}
.reflection {
position: relative;
height: 12px;
width: 12px;
background: #fff;
border-radius: 50%;
z-index: 1;
top: 60%;
left: 50%;
margin: -28px 0 0 5px;
opacity: 0.9;
}
/* https://codepen.io/DanielaValero/pen/QWbbvEo?editors=1100 */
.skinColor {
background-color: skyblue;
}
.head {
display: flex;
flex-direction: column;
align-items: center;
justify-content: middle;
margin-top: 20px;
}
.neck {
width: 40px;
height: 25px;
margin-top: -25px;
z-index: 1000000;
position: relative;
}
.face {
width: 160px;
height: 180px;
border-radius: 50%;
margin-bottom: 15px;
box-shadow: inset 2px 30px #3773cd, inset -1px 30px #3773cd;
position: relative;
display: flex;
align-items: center;
justify-content: center;
}
</style>
</head>
<body>
<h1>284 And Me</h1>
<p> This client only application uses the <a href="https://pubmed.ncbi.nlm.nih.gov/20457092/">IrisPlex (Walsh et al 2010)</a> and <a href="https://www.ncbi.nlm.nih.gov/pmc/articles/PMC3057002/">Model-based prediction of human hair color (Branicki et al 2011)</a> papers to determine which iris and hair colors have the highest probability of occuring given the input SNPs. At a high level, this is done by looking at select SNPs (relative to reference human assembly build 37) associated with certain colors and modeling the color using a multinomial logistic regression model to identify all possible color probabilities. Your data is secure & private as it never leaves your browser.</p>
<div id="drop_zone" ondrop="dropHandler(event);" ondragover="dragOverHandler(event);">
<p>Drag & drop exported 23andMe or AncestryDNA data to predict eye & hair color!</p>
</div>
<div id="error"></div>
<div>File loaded: <span id="filename"></span> File type: <span id="filetype"></span></div>
<br />
<table>
<tr>
<td>
<div>Iris color prediction: <span id="iris-prediction"></span> (Probability: <span id="iris-probability"></span>%, missing SNPs: <span id="iris-missing"></span>)</div>
<div class="eye-ball">
<div class="iris">
<div class="pupil"></div>
<div class="reflection"></div>
</div>
</div>
</td>
<td>
<div style="margin-left:20px;">Hair color prediction: <span id="hair-prediction"></span> (Probability: <span id="hair-probability"></span>%, missing SNPs: <span id="hair-missing"></span>)</div>
<div class="head">
<div class="face skinColor">
</div>
<div class="neck skinColor"></div>
</div>
</td>
</tr>
</table>
<div id="page-cover" style="z-index:99"></div>
<div class="lds-grid"><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div><div></div></div>
<script type="text/javascript">
function enableLoading() {
document.getElementById('page-cover').style.display = "block";
document.getElementsByClassName('lds-grid')[0].style.display = "block";
}
function disableLoading() {
document.getElementById('page-cover').style.display = "none";
document.getElementsByClassName('lds-grid')[0].style.display = "none";
}
// map between rsid (i.e rs548049170 & genotype (i.e TT)
var snpData = {};
const RSID_COL = 0;
const CHROM_COL = 1;
const POS_COL = 2;
const GENO_COL = 3;
// https://www.ncbi.nlm.nih.gov/pubmed/20457092
// PS3 Part 2
irisModelTable = {
'rsids':
{
'rs12913832':
{
'minor_allele': 'A',
'beta1': -4.81,
'beta2': -1.79
},
'rs1800407':
{
'minor_allele': 'T',
'beta1': 1.40,
'beta2': 0.87
},
'rs12896399':
{
'minor_allele': 'G',
'beta1': -0.58,
'beta2': -0.03
},
'rs16891982':
{
'minor_allele': 'C',
'beta1': -1.30,
'beta2': -0.50
},
'rs1393350':
{
'minor_allele': 'A',
'beta1': 0.47,
'beta2': 0.27
},
'rs12203592':
{
'minor_allele': 'T',
'beta1': 0.70,
'beta2': 0.73
},
},
'alpha1': 3.94,
'alpha2': .65
}
function predictIrisColor(snpMap) {
sum_beta1x = 0;
sum_beta2x = 0;
missing = 0;
// iterate over snps and compute beta sum (allele count * beta)
for (const [rsid, params] of Object.entries(irisModelTable['rsids'])) {
if (!(rsid in snpMap)) {
console.log("Warning rsid not available: " + rsid);
missing++;
continue;
}
genotype = snpMap[rsid];
// count the occurences of the minor allele in the genotype
for (var i = minor_allele_count = 0;
i < genotype.length;
minor_allele_count += +(params['minor_allele'] === genotype[i++]));
sum_beta1x += minor_allele_count * params['beta1'];
sum_beta2x += minor_allele_count * params['beta2'];
}
exp_alpha1 = Math.exp(irisModelTable['alpha1'] + sum_beta1x);
exp_alpha2 = Math.exp(irisModelTable['alpha2'] + sum_beta2x);
color_prob = {}
color_prob['blue'] = exp_alpha1 / (1 + exp_alpha1 + exp_alpha2);
color_prob['other'] = exp_alpha2 / (1 + exp_alpha1 + exp_alpha2);
color_prob['brown'] = 1 - (color_prob['blue'] + color_prob['other']);
color_prob['missing'] = missing;
return color_prob;
}
function testIrisModel() {
// preliminary model validation
testSnpDataBlue = {
'rs1393350': 'AA',
'rs12896399': 'TT',
'rs1800407': 'CC',
'rs12913832': 'GG',
'rs16891982': 'GG',
'rs12203592': 'CC'
};
predicted = predictIrisColor(testSnpDataBlue);
delete predicted['missing'];
predicted_color = Object.keys(predicted).reduce((a, b) => predicted[a] > predicted[b] ? a : b);
console.log("Predicted color: " + predicted_color);
if ((predicted['blue'] == 0.968458267134707) &&
(predicted['other'] == 0.02418434182474713) &&
(predicted['brown'] == 0.007357391040545891)) {
console.log("Model passed blue validation.");
}
else {
console.log(predicted);
console.log("Model error! Check code.");
}
profSnpData = {
'rs1393350': 'GA',
'rs12896399': 'GG',
'rs1800407': 'CC',
'rs12913832': 'GG',
'rs16891982': 'GG',
'rs12203592': 'CC'
};
predicted = predictIrisColor(profSnpData);
delete predicted['missing'];
predicted_color = Object.keys(predicted).reduce((a, b) => predicted[a] > predicted[b] ? a : b);
console.log("Predicted color: " + predicted_color);
if ((predicted['blue'] == 0.8846395587757137)) {
console.log("Model passed prof validation.");
}
else {
console.log(predicted);
console.log("Model error! Check code.");
}
}
//testIrisModel();
// https://www.ncbi.nlm.nih.gov/pmc/articles/PMC3057002/
// Table 2
hairModelTable = {
'rsids':
{
'rs12913832':
{
'effect_allele': 'T',
'beta1': -1.75,
'beta2': 0.10,
'beta3': -2.49
},
'rs12203592':
{
'effect_allele': 'T',
'beta1': -1.29,
'beta2': -1.15,
'beta3': -1.13
},
'rs1042602':
{
'effect_allele': 'A',
'beta1': 0.39,
'beta2': 0.30,
'beta3': 1.20
},
'rs4959270':
{
'effect_allele': 'A',
'beta1': 0.77,
'beta2': 0.85,
'beta3': 1.15
},
'rs28777':
{
'effect_allele': 'C',
'beta1': -1.69,
'beta2': -12.89,
'beta3': 0.10
},
'rs683':
{
'effect_allele': 'C',
'beta1': 0.10,
'beta2': 0.58,
'beta3': -0.02
},
'rs1800407':
{
'effect_allele': 'T',
'beta1': 0.49,
'beta2': -1.14,
'beta3': 0.19
},
'rs2402130':
{
'effect_allele': 'G',
'beta1': -0.48,
'beta2': -0.09,
'beta3': -0.54
},
'rs12821256':
{
'effect_allele': 'C',
'beta1': 0.69,
'beta2': 0.01,
'beta3': 0.87
},
'rs16891982':
{
'effect_allele': 'C',
'beta1': -0.82,
'beta2': -11.78,
'beta3': -3.48
},
'rs2378249':
{
'effect_allele': 'G',
'beta1': -0.18,
'beta2': -0.16,
'beta3': 0.40
},
},
// https://www.quora.com/How-many-people-have-blond-brown-black-and-red-hair-in-the-United-States
// alpha1 = ln(pi_1/pi_4) = ln(probability blond / probability black) in target demographic
// Assume all US : red 0.3%, blond 22%, brown 33%, black 20%
'alpha1': 1.5,//Math.log(.541/.117), //1.5,//Math.log(.2/.2),//1.5, // blond
'alpha2': 1, //Math.log(.084/.117),//1,//Math.log(.11/.2),//1, //1, // brown
'alpha3': .25 //Math.log(.249/.117),//.25//Math.log(.03/.2)//.25, //.25 // red
}
// 4 hair color category prediction
// https://www.ncbi.nlm.nih.gov/pmc/articles/PMC3057002/
// Note we are missing 4 of 13 SNPs so will compare accuracy to online Hirisplex
function predictHairColor(snpMap) {
sum_beta1x = 0; // blond
sum_beta2x = 0; // brown
sum_beta3x = 0; // red
missing = 0;
// iterate over snps and compute beta sum (allele count * beta)
for (const [rsid, params] of Object.entries(hairModelTable['rsids'])) {
if (!(rsid in snpMap)) {
console.log("Warning rsid not available: " + rsid);
missing++;
continue;
}
genotype = snpMap[rsid];
// count the occurences of the minor allele in the genotype
for (var i = minor_allele_count = 0;
i < genotype.length;
minor_allele_count += +(params['effect_allele'] === genotype[i++]));
sum_beta1x += minor_allele_count * params['beta1'];
sum_beta2x += minor_allele_count * params['beta2'];
sum_beta3x += minor_allele_count * params['beta3'];
}
exp_alpha1 = Math.exp(hairModelTable['alpha1'] + sum_beta1x);
exp_alpha2 = Math.exp(hairModelTable['alpha2'] + sum_beta2x);
exp_alpha3 = Math.exp(hairModelTable['alpha3'] + sum_beta3x);
color_prob = {};
color_prob['blond'] = exp_alpha1 / (1 + exp_alpha1 + exp_alpha2 + exp_alpha3);
color_prob['brown'] = exp_alpha2 / (1 + exp_alpha1 + exp_alpha2 + exp_alpha3);
color_prob['red'] = exp_alpha3 / (1 + exp_alpha1 + exp_alpha2 + exp_alpha3);
color_prob['black'] = 1 - (color_prob['blond'] + color_prob['brown'] + color_prob['red']);
color_prob['missing'] = missing;
return color_prob;
}
function testHairModel() {
// feed SNPs manually into https://hirisplex.erasmusmc.nl/ from test 23Me file
// to compute expected color probabilities
/* blond hair 0.101
brown hair 0.621
red hair 0.005
black hair 0.272
light hair 0.271
dark hair 0.729 */
testSnpData23Me = {
'rs12913832': 'AG',
'rs12203592': 'CT',
'rs1042602': 'AC',
'rs4959270': 'AC',
'rs28777': 'AC',
'rs683': 'AC',
'rs1800407': 'CC',
//'rs2402130':'GG',
'rs12821256': 'TT',
'rs16891982': 'CG',
//'rs2378249':'CC',
};
predicted = predictHairColor(testSnpData23Me);
delete predicted['missing'];
predicted_color = Object.keys(predicted).reduce((a, b) => predicted[a] > predicted[b] ? a : b);
console.log("Predicted color: " + predicted_color + ", expected black");
console.log(predicted);
/*if((predicted['brown'] > .6) &&
(predicted['black'] > .2) &&
(predicted['red'] > .0001) &&
(predicted['blond'] > 0.05))
{
console.log("Model passed brown hair validation.");
}
else
{
console.log(predicted) ;
console.log("Model error! Check code.");
}
if((predicted['brown'] > predicted['black']) &&
(predicted['black'] > predicted['blond']) &&
(predicted['blond'] > predicted['red']))
{
console.log("Model passed brown hair validation.");
}
else
{
console.log(predicted) ;
console.log("Model error! Check code.");
}*/
// https://my.pgp-hms.org/public_genetic_data?data_type=23andMe
// https://bc638d37d91e9bb38cd39616ecd16963-89.collections.su92l.arvadosapi.com/_/genome_v5_Full_20200711220308.txt
// https://hirisplex.erasmusmc.nl/ fed data in from genome_Sharla_Kinman_v4_Full_20170627133322.txt
/*
blond hair 0.643
brown hair 0.316
red hair 0.011
black hair 0.031
light hair 0.962
dark hair 0.038 */
// https://6250c48ff92bfeede85509aefe8f83d0-103.collections.su92l.arvadosapi.com/_/genome_Sharla_Kinman_v4_Full_20170627133322.txt
//genome_Sharla_Kinman_v4_Full_20170627133322.txt
testSnpData23Me2 = {
'rs12913832': 'GG',
'rs12203592': 'CC',
'rs1042602': 'AC',
'rs4959270': 'CC',
'rs28777': 'AA',
'rs683': 'AA',
'rs1800407': 'CC',
'rs2402130': 'AA',
'rs12821256': 'CC',
'rs16891982': 'GG',
'rs2378249': 'AA',
};
predicted = predictHairColor(testSnpData23Me2);
delete predicted['missing'];
predicted_color = Object.keys(predicted).reduce((a, b) => predicted[a] > predicted[b] ? a : b);
console.log("Predicted color: " + predicted_color + ", expected blond");
console.log(predicted);
/*if((predicted['blond'] > .5) &&
(predicted['brown'] > .2) &&
(predicted['red'] < .2) &&
(predicted['black'] < .2))
{
console.log("Model passed blond hair validation.");
}
else
{
console.log(predicted) ;
console.log("Model error! Check code.");
}*/
/*
blond hair 0.072
brown hair 0.048
red hair 0.879
black hair 0.001
light hair 0.989
dark hair 0.011
*/
// https://54137a447f1c7a4a1f75594fda5d3d7b-102.collections.su92l.arvadosapi.com/_/genome_Jodi_Riggins_v5_Full_20180217093249.txt
// genome_Jodi_Riggins_v5_Full_20180217093249.txt
testSnpData23Me3 = {
'rs12913832': 'GG',
'rs12203592': 'CT',
'rs1042602': 'AC',
'rs4959270': 'AA',
'rs28777': 'AA',
'rs683': 'AC',
'rs1800407': 'CC',
//'rs2402130':'AA',
'rs12821256': 'TT',
'rs16891982': 'GG',
//'rs2378249':'AA',
};
predicted = predictHairColor(testSnpData23Me3);
delete predicted['missing'];
predicted_color = Object.keys(predicted).reduce((a, b) => predicted[a] > predicted[b] ? a : b);
console.log("Predicted color: " + predicted_color + ", expected red");
console.log(predicted);
/*if((predicted['red'] > .7) &&
(predicted['blond'] < .2) &&
(predicted['brown'] < .2) &&
(predicted['black'] < .2))
{
console.log("Model passed blond hair validation.");
}
else
{
console.log(predicted) ;
console.log("Model error! Check code.");
}*/
}
//testHairModel();
function processFile(file) {
error = document.getElementById('error').style.display = 'none';
enableLoading();
console.log("processing file: " + file.name);
document.getElementById("filename").innerHTML = file.name;
const reader = new FileReader();
reader.onload = (event) => {
const file = event.target.result;
const allLines = file.split(/\r\n|\n/);
// validate file
valid = false;
if (allLines[0].indexOf("# This data file generated by 23andMe") == 0) {
// Reading line by line
allLines.forEach((line) => {
// skip comments
if (line[0] == "#")
return;
cols = line.split("\t");
snpData[cols[RSID_COL]] = cols[GENO_COL];
});
valid = true;
document.getElementById("filetype").innerHTML = "23andMe";
}
if (allLines[0].indexOf("#AncestryDNA raw data download") == 0) {
// Reading line by line
allLines.forEach((line) => {
// skip comments
if (line[0] == "#")
return;
cols = line.split("\t");
snpData[cols[RSID_COL]] = cols[GENO_COL] + cols[GENO_COL + 1];
});
valid = true;
document.getElementById("filetype").innerHTML = "AncestryDNA";
}
if (valid) {
// compute iris color prediction
color_probabilities = predictIrisColor(snpData);
console.log(color_probabilities);
missing = color_probabilities['missing'];
delete color_probabilities['missing'];
// find the color with highest probability
predicted_color = Object.keys(color_probabilities).reduce((a, b) =>
color_probabilities[a] > color_probabilities[b] ? a : b);
document.getElementById('iris-prediction').innerHTML = predicted_color;
document.getElementById('iris-probability').innerHTML = Math.round(color_probabilities[predicted_color] * 100);
css_iris_color = { "blue": "deepskyblue", "brown": "saddlebrown", "other": "grey" };
document.getElementsByClassName("iris")[0].classList.add(css_iris_color[predicted_color]);
document.getElementById('iris-prediction').style.color = css_iris_color[predicted_color];
document.getElementById('iris-missing').innerHTML = missing;
document.getElementById('iris-missing').style.color = missing ? 'red' : 'green';
// compute hair color prediction
color_probabilities = predictHairColor(snpData);
console.log(color_probabilities);
missing = color_probabilities['missing'];
delete color_probabilities['missing'];
// find the color with highest probability
predicted_color = Object.keys(color_probabilities).reduce((a, b) =>
color_probabilities[a] > color_probabilities[b] ? a : b);
document.getElementById('hair-prediction').innerHTML = predicted_color;
document.getElementById('hair-probability').innerHTML = Math.round(color_probabilities[predicted_color] * 100);
css_iris_color = { "blond": "#efe07b", "brown": "saddlebrown", "red": "#d44848", "black": "black" };
document.getElementById('hair-prediction').style.color = css_iris_color[predicted_color];
document.getElementsByClassName('face')[0].style['box-shadow'] = "inset 2px 30px " + css_iris_color[predicted_color] +
", inset -1px 30px " + css_iris_color[predicted_color];
document.getElementById('hair-missing').innerHTML = missing;
document.getElementById('hair-missing').style.color = missing ? 'red' : 'green';
}
else {
error = document.getElementById('error');
error.style.display = 'block';
error.innerHTML = "Unexpected file format! Only 23AndMe & AncestryDNA files are supported.";
document.getElementById("filetype").innerHTML = "Unsupported";
}
disableLoading();
};
reader.onerror = (event) => {
alert(event.target.error.name);
disableLoading();
};
reader.readAsText(file);
}
// Drag & drop functionality from https://developer.mozilla.org/en-US/docs/Web/API/HTML_Drag_and_Drop_API/File_drag_and_drop
function dragOverHandler(ev) {
// Prevent default behavior (Prevent file from being opened)
ev.preventDefault();
}
function dropHandler(ev) {
// Prevent default behavior (Prevent file from being opened)
ev.preventDefault();
if (ev.dataTransfer.items) {
// Use DataTransferItemList interface to access the file(s)
if (ev.dataTransfer.items[0].kind === 'file') {
var file = ev.dataTransfer.items[0].getAsFile();
snpData = file;
processFile(file);
}
} else {
// Use DataTransfer interface to access the file(s)
if (ev.dataTransfer.files.length > 0) {
processFile(ev.dataTransfer.files[0]);
}
}
// Pass event to removeDragData for cleanup
removeDragData(ev)
}
function removeDragData(ev) {
if (ev.dataTransfer.items) {
// Use DataTransferItemList interface to remove the drag data
ev.dataTransfer.items.clear();
} else {
// Use DataTransfer interface to remove the drag data
ev.dataTransfer.clearData();
}
}</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment