view scripts/alphafold.html @ 22:3f188450ca4f draft default tip

planemo upload for repository https://github.com/usegalaxy-au/tools-au commit d626bb28203543a70d3fc60d662cb054bc3cef7c
author galaxy-australia
date Wed, 30 Oct 2024 21:46:34 +0000
parents 6ab1a261520a
children
line wrap: on
line source

<!DOCTYPE html>
<html lang="en" dir="ltr">

  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">

    <title> Alphafold structure prediction </title>

    <link rel="preconnect" href="https://fonts.googleapis.com">
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
    <link href="https://fonts.googleapis.com/css2?family=Ubuntu:wght@300;400;500;700&display=swap" rel="stylesheet">
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
    <script src="https://cdnjs.cloudflare.com/ajax/libs/chroma-js/2.1.0/chroma.min.js" integrity="sha512-yocoLferfPbcwpCMr8v/B0AB4SWpJlouBwgE0D3ZHaiP1nuu5djZclFEIj9znuqghaZ3tdCMRrreLoM8km+jIQ==" crossorigin="anonymous"></script>

    <style type="text/css">
      * {
        margin: 0;
        padding: 0;
      }
      html, body {
        width: 100%;
        font-size: 1rem;
      }
      body {
        font-family: 'Ubuntu', sans-serif;
      }
      canvas {
        background-color: white;
      }
      h1, h2, h3, h4, h5, h6 {
        color: dodgerblue;
        text-align: center;
        font-weight: lighter;
      }
      h1 {
        margin: 2rem;
        font-size: 3rem;
      }
      h2 {
        font-size: 2rem;
        margin-top: 1rem;
        margin-bottom: .5rem;
      }
      button.btn {
        color: #ccc;
        margin: 1rem;
        padding: .5rem;
        font-size: 1rem;
        min-width: 4rem;
        border: none;
        border-radius: .5rem;
        background-color: grey;
        transition-duration: 0.25s;
        cursor: pointer;
      }
      button.btn.selected {
        color: #eee;
        background-color: dodgerblue;
      }
      button.btn.green {
        color: #eee;
        background-color: #10941f;
      }
      button.btn:focus {
        outline: none;
        color: inherit;
      }
      button.btn:hover {
        color: white;
        box-shadow: 0 0 10px dodgerblue;
      }
      button.btn.green:hover {
        color: white;
        box-shadow: 0 0 10px limegreen;
      }
      .main {
        min-height: 90vh;
        position: relative;
      }
      .flex {
        display: flex;
        justify-content: center;
        align-items: center;
        padding: 1rem;
      }
      .col {
        flex-direction: column;
        flex-grow: 0;
      }
      .controls {
        padding-bottom: 10vh;
      }
      .box {
        padding: .5rem 1rem;
        margin: .5rem auto;
        width: fit-content;
        border-radius: 1rem;
      }
      .mono {
        font-family: monospace;
        color: #555;
        background-color: #ddd;
        padding: .25rem;
        border-radius: .25rem;
      }
      .space-1 {
        line-height: 1.2;
      }
      .space-2 {
        line-height: 1.5;
      }
      .relative {
        position: relative;
      }
      .legend {
        max-width: 350px;
      }
      .legend .scale {
        display: flex;
        flex-direction: column;
        align-items: center;
      }
      .legend .color {
        width: 150px;
        height: 30px;
        justify-content: space-between;
        background: linear-gradient(
          90deg,
          rgba(255,55,0,1)   0%,
          rgba(216,224,6,1)  33%,
          rgba(34,213,238,1) 66%,
          rgba(3,30,148,1)   100%
          );
      }
      .legend .ticks {
        margin-top: -1rem;
        width: 180px;
        justify-content: space-between;
      }
      #ngl-root-parent {
        width: 40vw;
        height: 30vw;
        margin: auto;
        position: relative;
      }
      #ngl-root {
        width: 40vw;
        height: 30vw;
        border-radius: 15px;
        border: 1px solid grey;
      }
      #ngl-nothing {
        position: absolute;
        top: 0;
        left: 0;
        display: none;
        text-align: center;
        width: 40vw;
        height: 30vw;
        padding-top: 12vw;
      }
      #ngl-loading {
        position: absolute;
        top: 0;
        left: 0;
        display: flex;
        justify-content: center;
        align-items: center;
        width: 800px;
        height: 600px;
        width: 40vw;
        height: 30vw;
      }
      #ngl-loading svg {
        width: 30%;
        height: 30%;
        width: 10vw;
        height: 10vw;
      }

      /* Responsive */
      @media (max-width: 1400px) {
        :root {
          font-size: 10pt;
        }
        button.btn {
          margin: .5rem;
          padding: .25rem;
        }
        .box {
          padding: .5rem;
          margin: .5rem auto;
        }
        .legend {
          max-width: 200px;
        }
        .help-text {
          font-size: 0.8rem;
        }
        .mono {
          padding: .25rem .5rem;
        }
      }
      @media (max-width: 1000px) {
        :root {
          font-size: 8pt;
        }
      }
      @media (max-width: 800px) {
        :root {
          font-size: 6pt;
        }
      }
    </style>

    <script src="https://cdn.rawgit.com/arose/ngl/v2.0.0-dev.37/dist/ngl.js"></script>
  </head>


  <body>
    <h1> Alphafold structure prediction </h1>

    <div class="main flex">
      <div class="col relative">
        <div id="ngl-root-parent">

          <div id="ngl-root"></div>

          <div id="ngl-nothing">
            Select a representation to display
          </div>

          <div id="ngl-loading">
            <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="margin: auto; background: none; display: block; shape-rendering: auto;" width="200px" height="200px" viewBox="0 0 100 100" preserveAspectRatio="xMidYMid">
              <g transform="rotate(0 50 50)">
                <rect x="47" y="24" rx="3" ry="6" width="6" height="12" fill="#88879e">
                  <animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.9166666666666666s" repeatCount="indefinite"></animate>
                </rect>
              </g><g transform="rotate(30 50 50)">
                <rect x="47" y="24" rx="3" ry="6" width="6" height="12" fill="#88879e">
                  <animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.8333333333333334s" repeatCount="indefinite"></animate>
                </rect>
              </g><g transform="rotate(60 50 50)">
                <rect x="47" y="24" rx="3" ry="6" width="6" height="12" fill="#88879e">
                  <animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.75s" repeatCount="indefinite"></animate>
                </rect>
              </g><g transform="rotate(90 50 50)">
                <rect x="47" y="24" rx="3" ry="6" width="6" height="12" fill="#88879e">
                  <animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.6666666666666666s" repeatCount="indefinite"></animate>
                </rect>
              </g><g transform="rotate(120 50 50)">
                <rect x="47" y="24" rx="3" ry="6" width="6" height="12" fill="#88879e">
                  <animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.5833333333333334s" repeatCount="indefinite"></animate>
                </rect>
              </g><g transform="rotate(150 50 50)">
                <rect x="47" y="24" rx="3" ry="6" width="6" height="12" fill="#88879e">
                  <animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.5s" repeatCount="indefinite"></animate>
                </rect>
              </g><g transform="rotate(180 50 50)">
                <rect x="47" y="24" rx="3" ry="6" width="6" height="12" fill="#88879e">
                  <animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.4166666666666667s" repeatCount="indefinite"></animate>
                </rect>
              </g><g transform="rotate(210 50 50)">
                <rect x="47" y="24" rx="3" ry="6" width="6" height="12" fill="#88879e">
                  <animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.3333333333333333s" repeatCount="indefinite"></animate>
                </rect>
              </g><g transform="rotate(240 50 50)">
                <rect x="47" y="24" rx="3" ry="6" width="6" height="12" fill="#88879e">
                  <animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.25s" repeatCount="indefinite"></animate>
                </rect>
              </g><g transform="rotate(270 50 50)">
                <rect x="47" y="24" rx="3" ry="6" width="6" height="12" fill="#88879e">
                  <animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.16666666666666666s" repeatCount="indefinite"></animate>
                </rect>
              </g><g transform="rotate(300 50 50)">
                <rect x="47" y="24" rx="3" ry="6" width="6" height="12" fill="#88879e">
                  <animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.08333333333333333s" repeatCount="indefinite"></animate>
                </rect>
              </g><g transform="rotate(330 50 50)">
                <rect x="47" y="24" rx="3" ry="6" width="6" height="12" fill="#88879e">
                  <animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="0s" repeatCount="indefinite"></animate>
                </rect>
              </g>
            </svg>
          </div>
        </div>

        <div class="flex">
          <div class="box space-1">
            <p>
              <span class="mono">Scroll up/down</span>
              to zoom in and out
            </p>
            <p>
              <span class="mono">Click + drag</span>
              to rotate the structure
            </p>
            <p>
              <span class="mono">CTRL + click + drag</span>
              to move the structure
            </p>
            <p>
              <span class="mono">Click</span>
              an atom to bring it into focus
            </p>
          </div>

          <div class="box legend">
            <div class="scale">
              <div class="color"></div>
              <div class="flex ticks">
                <div>&lt;50</div>
                <div>70</div>
                <div>90+</div>
              </div>
            </div>

            <div>
              <p class="text-center">
                <small>
                Alphafold produces a
                <a href="https://alphafold.ebi.ac.uk/faq#faq-5" target="_blank">
                  per-residue confidence score (pLDDT)
                </a>
                between 0 and 100. Some regions below 50 pLDDT may be
                unstructured in isolation.
              </small>
              </p>
            </div>
          </div>
        </div>
      </div>

      <div class="flex col controls">
        <div class="box text-center">
          <h3> Select model </h3>
          <p>The top-ranked structures predicted by Alphafold</p>
          <div>
            <button class="btn selected" id="btn-ranked_0" onclick="setModel(0);">
              Ranked 0
            </button>

            <button class="btn" id="btn-ranked_1" onclick="setModel(1);">
              Ranked 1
            </button>

            <button class="btn" id="btn-ranked_2" onclick="setModel(2);">
              Ranked 2
            </button>

            <button class="btn" id="btn-ranked_3" onclick="setModel(3);">
              Ranked 3
            </button>

            <button class="btn" id="btn-ranked_4" onclick="setModel(4);">
              Ranked 4
            </button>
          </div>
        </div>

        <div class="box text-center">
          <h3> Toggle representations </h3>
          <div>
            <button class="btn selected" id="btn-cartoon" onclick="toggleModelRepresentation('cartoon');">
              Cartoon
            </button>

            <button class="btn" id="btn-ball-stick" onclick="toggleModelRepresentation('ball+stick');">
              Ball + stick
            </button>

            <button class="btn" id="btn-surface" onclick="toggleModelRepresentation('surface');">
              Surface
            </button>

            <button class="btn" id="btn-backbone" onclick="toggleModelRepresentation('backbone');">
              Backbone
            </button>
          </div>
        </div>

        <div class="box text-center">
          <h3> Actions </h3>
          <div>
            <button class="btn selected" id="btn-toggle-spin" onclick="toggleSpin();">
              Toggle spin
            </button>

            <button class="btn" id="btn-toggle-dark" onclick="toggleDark();">
              Dark mode
            </button>
          </div>
        </div>

        <div class="box text-center">
          <h3> Download </h3>
          <div>
            <button class="btn green" onclick="downloadPng();">
              Snapshot
            </button>

            <button class="btn green" onclick="downloadPdb();">
              PDB
            </button>
          </div>
        </div>
      </div>
    </div>
  </body>


  <script type="text/javascript">

    // Render NGLviewer for PDB files

    // State management has been implemented with vanilla Js but could have used
    // Vue - it's a fairly simple use case so a global 'state' object works fine
    // without complicating things too much.


    // Define a custom color scheme to represent model confidence consistently
    // across different representations
    // ------------------------------------------------------------------------
    const colorScale = chroma.scale([
      'red', 'yellow', 'green', 'cyan', 'blue'
    ]).mode('lab').domain([0, 0.9]);

    const confidenceScheme = NGL.ColormakerRegistry.addScheme(function (params) {
      this.atomColor = function (atom) {
        // Actually model confidence (pLDDT)
        const c = atom.bfactor;
        const BREAK_RED = 40;   // Below this is just plain red
        let range, r, g, b;

        if (c < BREAK_RED) {
          return 0xFF0000;
        }
        const p = (c - BREAK_RED) / (100 - BREAK_RED)
        return eval(colorScale(p).hex().replace('#', '0x'));
      };
    });

    // NGL color schemes https://nglviewer.org/ngl/api/manual/usage/coloring.html
    const COLORSCHEME = confidenceScheme;  //'bfactor'

    const MODELS = [
      'ranked_0.pdb',
      'ranked_1.pdb',
      'ranked_2.pdb',
      'ranked_3.pdb',
      'ranked_4.pdb',
    ]

    const REPRESENTATIONS = [
      'cartoon',
      'ball+stick',
      'surface',
      'backbone',
    ]

    const DEFAULT_REPRESENTATION = REPRESENTATIONS[0];
    const MAX_CLICK_INTERVAL_MS = 500;  // For debouncing model clicks

    let stage;
    let nonceSetModel;

    let state = {
      model: 0,
      modelObject: null,
      representations: {},
      colorScheme: 'bfactor',
      darkMode: false,
      loading: 1,
      spin: true,
    }

    const uri = (i) => MODELS[i];
    // Switch to this function to return sample model URI (local dev)
    // const uri = (i) => `https://raw.githubusercontent.com/neoformit/alphafold-galaxy/main/data/${MODELS[i]}`;

    document.addEventListener("DOMContentLoaded", async function () {
      // Can set debug for development if NGL is being... funny
      // NGL.setDebug(true)

      // Create NGL Stage object
      stage = new NGL.Stage("ngl-root", { backgroundColor: 'white' });

      // Handle window resizing
      window.addEventListener("resize",  () => stage.handleResize());

      loadModel();
      while (true) {
        if (!state.loading) {
          // Reload page if NGL failed to display. Weird occassional bug.
          const canvas = document.querySelector('#ngl-root canvas');
          canvas.height < 50 && window.reload();
          break
        }
        await new Promise(resolve => setTimeout(resolve, 500));
      }
    });

    // Models ------------------------------------------------------------------

    const setModel = (ix) => {
      state.model = ix;
      stage.removeComponent(state.modelObject);
      setLoading(1);

      // Debounce rapid model clicking with a nonce
      nonceSetModel = new Object();
      const localNonce = nonceSetModel;
      setTimeout( () => {
        if (localNonce === nonceSetModel) {
          // The user has stopped clicking, hurray...
          loadModel().then(updateButtons);
        }
      }, MAX_CLICK_INTERVAL_MS);
    }

    const loadModel = () => {
      reps = Object.keys(state.representations);
      if (reps.length) {
        state.representations = {};
      } else {
        reps = [DEFAULT_REPRESENTATION];
      }

      // Load PDB entry
      return stage.loadFile(uri(state.model)).then( (o) => {
        state.modelObject = o;
        reps.forEach( (r) => addModelRepresentation(r) );
        stage.setSpin(state.spin);
        o.autoView();
        setLoading(0);
      })
    }

    // Representations ---------------------------------------------------------

    const toggleModelRepresentation = (rep) => {
      rep in state.representations ?
        removeModelRepresentation(rep)
        : addModelRepresentation(rep)
    }

    const addModelRepresentation = (rep) => {
      state.representations[rep] =
        state.modelObject.addRepresentation(rep, {colorScheme: COLORSCHEME});
      updateButtons();
    }

    const removeModelRepresentation = (rep) => {
      o = state.representations[rep];
      state.modelObject.removeRepresentation(o);
      delete state.representations[rep];
      updateButtons();
    }

    const clearModelRepresentations = () => {
      state.modelObject && state.modelObject.removeAllRepresentations();
      state.representations = {};
    }

    // Actions -----------------------------------------------------------------

    const toggleDark = () => {
      state.darkMode = !state.darkMode;
      stage.setParameters({
        backgroundColor: state.darkMode ? 'black' : 'white',
      });
      const btn = document.querySelector('#btn-toggle-dark');
      btn && btn.classList.toggle('selected');
    }

    const setLoading = (state) => {
      document.getElementById('ngl-loading')
        .style.display = state ? 'flex' : 'none';
      state.loading = state;
    }

    const toggleSpin = () => {
      stage.toggleSpin();
      const btn = document.querySelector('#btn-toggle-spin');
      btn && btn.classList.toggle('selected');
      state.spin = !state.spin;
    }

    const downloadPng = () => {
      const params = {
        factor: 3,
        antialias: true,
      }
      stage.makeImage(params).then( (blob) => {
        const name = MODELS[state.model].replace('.pdb', '.png');
        const url = URL.createObjectURL(blob);
        makeDownload(url, name);
      })
    }

    const downloadPdb = () => {
      const url = uri(state.model);
      const name = `alphafold_${MODELS[state.model]}`;
      makeDownload(url, name);
    }

    const makeDownload = (url, name) => {
      // Will not work with cross-origin urls (i.e. during development)
      console.log(`Creating file download for ${name}, href ${url}`);
      const saveLink = document.createElement('a');
      saveLink.href = url;
      saveLink.download = name;
      document.body.appendChild(saveLink);
      saveLink.dispatchEvent(
        new MouseEvent('click', {
          bubbles: true,
          cancelable: true,
          view: window
        })
      );
      document.body.removeChild(saveLink);
    }

    const updateButtons = () => {
      MODELS.forEach( (name, i) => {
        const id = `#btn-${name.replace('.pdb', '')}`;
        const btn = document.querySelector(id);
        if (!btn) return
        i == state.model ?
          btn.classList.add('selected')
          : btn.classList.remove('selected');
      })

      REPRESENTATIONS.forEach( (name) => {
        const id = `#btn-${name}`.replace('+', '-');
        const btn = document.querySelector(id);
        if (!btn) return
        if (name in state.representations) {
          btn.classList.add('selected')
        } else {
          btn.classList.remove('selected');
        }
      });

      // Show "Nothing to display" if no representations are selected
      document.querySelector('#ngl-nothing').style.display =
        Object.keys(state.representations).length ?
        'none'
        : 'block';
    }

  </script>

</html>