Skip to content

Instantly share code, notes, and snippets.

@DanielKoohmarey
Last active May 31, 2021 17:07
Show Gist options
  • Select an option

  • Save DanielKoohmarey/201c89f0e270f080dedb5e5798f2f0a3 to your computer and use it in GitHub Desktop.

Select an option

Save DanielKoohmarey/201c89f0e270f080dedb5e5798f2f0a3 to your computer and use it in GitHub Desktop.

Revisions

  1. DanielKoohmarey revised this gist May 31, 2021. No changes.
  2. DanielKoohmarey revised this gist May 31, 2021. 1 changed file with 0 additions and 5 deletions.
    5 changes: 0 additions & 5 deletions phenotyping.html
    Original file line number Diff line number Diff line change
    @@ -523,11 +523,6 @@ <h1>284 And Me</h1>
    // 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%

    // Training set
    // Hair color was classified into 7 categories: blond (16.4%), dark-blond (37.7%), brown (9.4%), auburn (3.1%), blond-red (11.2%), red (10.6%), and black (11.7%).
    // For some analyses, we grouped blond and dark-blond into one blond group (54.1%) and auburn, blond-red, and red into one red group (24.9%) resulting into 4 categories.


    '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
  3. DanielKoohmarey revised this gist May 31, 2021. 1 changed file with 867 additions and 869 deletions.
    1,736 changes: 867 additions & 869 deletions phenotyping.html
    Original file line number Diff line number Diff line change
    @@ -1,876 +1,874 @@
    <!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>

    <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%

    // Training set
    // Hair color was classified into 7 categories: blond (16.4%), dark-blond (37.7%), brown (9.4%), auburn (3.1%), blond-red (11.2%), red (10.6%), and black (11.7%).
    // For some analyses, we grouped blond and dark-blond into one blond group (54.1%) and auburn, blond-red, and red into one red group (24.9%) resulting into 4 categories.


    '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>
    <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%

    // Training set
    // Hair color was classified into 7 categories: blond (16.4%), dark-blond (37.7%), brown (9.4%), auburn (3.1%), blond-red (11.2%), red (10.6%), and black (11.7%).
    // For some analyses, we grouped blond and dark-blond into one blond group (54.1%) and auburn, blond-red, and red into one red group (24.9%) resulting into 4 categories.


    '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>
  4. DanielKoohmarey created this gist May 31, 2021.
    876 changes: 876 additions & 0 deletions phenotyping.html
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,876 @@
    <!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%

    // Training set
    // Hair color was classified into 7 categories: blond (16.4%), dark-blond (37.7%), brown (9.4%), auburn (3.1%), blond-red (11.2%), red (10.6%), and black (11.7%).
    // For some analyses, we grouped blond and dark-blond into one blond group (54.1%) and auburn, blond-red, and red into one red group (24.9%) resulting into 4 categories.


    '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>