Mercurial > repos > galaxy-australia > alphafold2
comparison scripts/alphafold.html @ 20:6ab1a261520a draft
planemo upload for repository https://github.com/usegalaxy-au/tools-au commit c3a90eb12ada44d477541baa4dd6182be29cd554-dirty
author | galaxy-australia |
---|---|
date | Sun, 28 Jul 2024 20:09:55 +0000 |
parents | |
children |
comparison
equal
deleted
inserted
replaced
19:2f7702fd0a4c | 20:6ab1a261520a |
---|---|
1 <!DOCTYPE html> | |
2 <html lang="en" dir="ltr"> | |
3 | |
4 <head> | |
5 <meta charset="utf-8"> | |
6 <meta http-equiv="X-UA-Compatible" content="IE=edge"> | |
7 <meta name="viewport" content="width=device-width, initial-scale=1"> | |
8 | |
9 <title> Alphafold structure prediction </title> | |
10 | |
11 <link rel="preconnect" href="https://fonts.googleapis.com"> | |
12 <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> | |
13 <link href="https://fonts.googleapis.com/css2?family=Ubuntu:wght@300;400;500;700&display=swap" rel="stylesheet"> | |
14 <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous"> | |
15 <script src="https://cdnjs.cloudflare.com/ajax/libs/chroma-js/2.1.0/chroma.min.js" integrity="sha512-yocoLferfPbcwpCMr8v/B0AB4SWpJlouBwgE0D3ZHaiP1nuu5djZclFEIj9znuqghaZ3tdCMRrreLoM8km+jIQ==" crossorigin="anonymous"></script> | |
16 | |
17 <style type="text/css"> | |
18 * { | |
19 margin: 0; | |
20 padding: 0; | |
21 } | |
22 html, body { | |
23 width: 100%; | |
24 font-size: 1rem; | |
25 } | |
26 body { | |
27 font-family: 'Ubuntu', sans-serif; | |
28 } | |
29 canvas { | |
30 background-color: white; | |
31 } | |
32 h1, h2, h3, h4, h5, h6 { | |
33 color: dodgerblue; | |
34 text-align: center; | |
35 font-weight: lighter; | |
36 } | |
37 h1 { | |
38 margin: 2rem; | |
39 font-size: 3rem; | |
40 } | |
41 h2 { | |
42 font-size: 2rem; | |
43 margin-top: 1rem; | |
44 margin-bottom: .5rem; | |
45 } | |
46 button.btn { | |
47 color: #ccc; | |
48 margin: 1rem; | |
49 padding: .5rem; | |
50 font-size: 1rem; | |
51 min-width: 4rem; | |
52 border: none; | |
53 border-radius: .5rem; | |
54 background-color: grey; | |
55 transition-duration: 0.25s; | |
56 cursor: pointer; | |
57 } | |
58 button.btn.selected { | |
59 color: #eee; | |
60 background-color: dodgerblue; | |
61 } | |
62 button.btn.green { | |
63 color: #eee; | |
64 background-color: #10941f; | |
65 } | |
66 button.btn:focus { | |
67 outline: none; | |
68 color: inherit; | |
69 } | |
70 button.btn:hover { | |
71 color: white; | |
72 box-shadow: 0 0 10px dodgerblue; | |
73 } | |
74 button.btn.green:hover { | |
75 color: white; | |
76 box-shadow: 0 0 10px limegreen; | |
77 } | |
78 .main { | |
79 min-height: 90vh; | |
80 position: relative; | |
81 } | |
82 .flex { | |
83 display: flex; | |
84 justify-content: center; | |
85 align-items: center; | |
86 padding: 1rem; | |
87 } | |
88 .col { | |
89 flex-direction: column; | |
90 flex-grow: 0; | |
91 } | |
92 .controls { | |
93 padding-bottom: 10vh; | |
94 } | |
95 .box { | |
96 padding: .5rem 1rem; | |
97 margin: .5rem auto; | |
98 width: fit-content; | |
99 border-radius: 1rem; | |
100 } | |
101 .mono { | |
102 font-family: monospace; | |
103 color: #555; | |
104 background-color: #ddd; | |
105 padding: .25rem; | |
106 border-radius: .25rem; | |
107 } | |
108 .space-1 { | |
109 line-height: 1.2; | |
110 } | |
111 .space-2 { | |
112 line-height: 1.5; | |
113 } | |
114 .relative { | |
115 position: relative; | |
116 } | |
117 .legend { | |
118 max-width: 350px; | |
119 } | |
120 .legend .scale { | |
121 display: flex; | |
122 flex-direction: column; | |
123 align-items: center; | |
124 } | |
125 .legend .color { | |
126 width: 150px; | |
127 height: 30px; | |
128 justify-content: space-between; | |
129 background: linear-gradient( | |
130 90deg, | |
131 rgba(255,55,0,1) 0%, | |
132 rgba(216,224,6,1) 33%, | |
133 rgba(34,213,238,1) 66%, | |
134 rgba(3,30,148,1) 100% | |
135 ); | |
136 } | |
137 .legend .ticks { | |
138 margin-top: -1rem; | |
139 width: 180px; | |
140 justify-content: space-between; | |
141 } | |
142 #ngl-root-parent { | |
143 width: 40vw; | |
144 height: 30vw; | |
145 margin: auto; | |
146 position: relative; | |
147 } | |
148 #ngl-root { | |
149 width: 40vw; | |
150 height: 30vw; | |
151 border-radius: 15px; | |
152 border: 1px solid grey; | |
153 } | |
154 #ngl-nothing { | |
155 position: absolute; | |
156 top: 0; | |
157 left: 0; | |
158 display: none; | |
159 text-align: center; | |
160 width: 40vw; | |
161 height: 30vw; | |
162 padding-top: 12vw; | |
163 } | |
164 #ngl-loading { | |
165 position: absolute; | |
166 top: 0; | |
167 left: 0; | |
168 display: flex; | |
169 justify-content: center; | |
170 align-items: center; | |
171 width: 800px; | |
172 height: 600px; | |
173 width: 40vw; | |
174 height: 30vw; | |
175 } | |
176 #ngl-loading svg { | |
177 width: 30%; | |
178 height: 30%; | |
179 width: 10vw; | |
180 height: 10vw; | |
181 } | |
182 | |
183 /* Responsive */ | |
184 @media (max-width: 1400px) { | |
185 :root { | |
186 font-size: 10pt; | |
187 } | |
188 button.btn { | |
189 margin: .5rem; | |
190 padding: .25rem; | |
191 } | |
192 .box { | |
193 padding: .5rem; | |
194 margin: .5rem auto; | |
195 } | |
196 .legend { | |
197 max-width: 200px; | |
198 } | |
199 .help-text { | |
200 font-size: 0.8rem; | |
201 } | |
202 .mono { | |
203 padding: .25rem .5rem; | |
204 } | |
205 } | |
206 @media (max-width: 1000px) { | |
207 :root { | |
208 font-size: 8pt; | |
209 } | |
210 } | |
211 @media (max-width: 800px) { | |
212 :root { | |
213 font-size: 6pt; | |
214 } | |
215 } | |
216 </style> | |
217 | |
218 <script src="https://cdn.rawgit.com/arose/ngl/v2.0.0-dev.37/dist/ngl.js"></script> | |
219 </head> | |
220 | |
221 | |
222 <body> | |
223 <h1> Alphafold structure prediction </h1> | |
224 | |
225 <div class="main flex"> | |
226 <div class="col relative"> | |
227 <div id="ngl-root-parent"> | |
228 | |
229 <div id="ngl-root"></div> | |
230 | |
231 <div id="ngl-nothing"> | |
232 Select a representation to display | |
233 </div> | |
234 | |
235 <div id="ngl-loading"> | |
236 <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"> | |
237 <g transform="rotate(0 50 50)"> | |
238 <rect x="47" y="24" rx="3" ry="6" width="6" height="12" fill="#88879e"> | |
239 <animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.9166666666666666s" repeatCount="indefinite"></animate> | |
240 </rect> | |
241 </g><g transform="rotate(30 50 50)"> | |
242 <rect x="47" y="24" rx="3" ry="6" width="6" height="12" fill="#88879e"> | |
243 <animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.8333333333333334s" repeatCount="indefinite"></animate> | |
244 </rect> | |
245 </g><g transform="rotate(60 50 50)"> | |
246 <rect x="47" y="24" rx="3" ry="6" width="6" height="12" fill="#88879e"> | |
247 <animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.75s" repeatCount="indefinite"></animate> | |
248 </rect> | |
249 </g><g transform="rotate(90 50 50)"> | |
250 <rect x="47" y="24" rx="3" ry="6" width="6" height="12" fill="#88879e"> | |
251 <animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.6666666666666666s" repeatCount="indefinite"></animate> | |
252 </rect> | |
253 </g><g transform="rotate(120 50 50)"> | |
254 <rect x="47" y="24" rx="3" ry="6" width="6" height="12" fill="#88879e"> | |
255 <animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.5833333333333334s" repeatCount="indefinite"></animate> | |
256 </rect> | |
257 </g><g transform="rotate(150 50 50)"> | |
258 <rect x="47" y="24" rx="3" ry="6" width="6" height="12" fill="#88879e"> | |
259 <animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.5s" repeatCount="indefinite"></animate> | |
260 </rect> | |
261 </g><g transform="rotate(180 50 50)"> | |
262 <rect x="47" y="24" rx="3" ry="6" width="6" height="12" fill="#88879e"> | |
263 <animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.4166666666666667s" repeatCount="indefinite"></animate> | |
264 </rect> | |
265 </g><g transform="rotate(210 50 50)"> | |
266 <rect x="47" y="24" rx="3" ry="6" width="6" height="12" fill="#88879e"> | |
267 <animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.3333333333333333s" repeatCount="indefinite"></animate> | |
268 </rect> | |
269 </g><g transform="rotate(240 50 50)"> | |
270 <rect x="47" y="24" rx="3" ry="6" width="6" height="12" fill="#88879e"> | |
271 <animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.25s" repeatCount="indefinite"></animate> | |
272 </rect> | |
273 </g><g transform="rotate(270 50 50)"> | |
274 <rect x="47" y="24" rx="3" ry="6" width="6" height="12" fill="#88879e"> | |
275 <animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.16666666666666666s" repeatCount="indefinite"></animate> | |
276 </rect> | |
277 </g><g transform="rotate(300 50 50)"> | |
278 <rect x="47" y="24" rx="3" ry="6" width="6" height="12" fill="#88879e"> | |
279 <animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="-0.08333333333333333s" repeatCount="indefinite"></animate> | |
280 </rect> | |
281 </g><g transform="rotate(330 50 50)"> | |
282 <rect x="47" y="24" rx="3" ry="6" width="6" height="12" fill="#88879e"> | |
283 <animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="0s" repeatCount="indefinite"></animate> | |
284 </rect> | |
285 </g> | |
286 </svg> | |
287 </div> | |
288 </div> | |
289 | |
290 <div class="flex"> | |
291 <div class="box space-1"> | |
292 <p> | |
293 <span class="mono">Scroll up/down</span> | |
294 to zoom in and out | |
295 </p> | |
296 <p> | |
297 <span class="mono">Click + drag</span> | |
298 to rotate the structure | |
299 </p> | |
300 <p> | |
301 <span class="mono">CTRL + click + drag</span> | |
302 to move the structure | |
303 </p> | |
304 <p> | |
305 <span class="mono">Click</span> | |
306 an atom to bring it into focus | |
307 </p> | |
308 </div> | |
309 | |
310 <div class="box legend"> | |
311 <div class="scale"> | |
312 <div class="color"></div> | |
313 <div class="flex ticks"> | |
314 <div><50</div> | |
315 <div>70</div> | |
316 <div>90+</div> | |
317 </div> | |
318 </div> | |
319 | |
320 <div> | |
321 <p class="text-center"> | |
322 <small> | |
323 Alphafold produces a | |
324 <a href="https://alphafold.ebi.ac.uk/faq#faq-5" target="_blank"> | |
325 per-residue confidence score (pLDDT) | |
326 </a> | |
327 between 0 and 100. Some regions below 50 pLDDT may be | |
328 unstructured in isolation. | |
329 </small> | |
330 </p> | |
331 </div> | |
332 </div> | |
333 </div> | |
334 </div> | |
335 | |
336 <div class="flex col controls"> | |
337 <div class="box text-center"> | |
338 <h3> Select model </h3> | |
339 <p>The top-ranked structures predicted by Alphafold</p> | |
340 <div> | |
341 <button class="btn selected" id="btn-ranked_0" onclick="setModel(0);"> | |
342 Ranked 0 | |
343 </button> | |
344 | |
345 <button class="btn" id="btn-ranked_1" onclick="setModel(1);"> | |
346 Ranked 1 | |
347 </button> | |
348 | |
349 <button class="btn" id="btn-ranked_2" onclick="setModel(2);"> | |
350 Ranked 2 | |
351 </button> | |
352 | |
353 <button class="btn" id="btn-ranked_3" onclick="setModel(3);"> | |
354 Ranked 3 | |
355 </button> | |
356 | |
357 <button class="btn" id="btn-ranked_4" onclick="setModel(4);"> | |
358 Ranked 4 | |
359 </button> | |
360 </div> | |
361 </div> | |
362 | |
363 <div class="box text-center"> | |
364 <h3> Toggle representations </h3> | |
365 <div> | |
366 <button class="btn selected" id="btn-cartoon" onclick="toggleModelRepresentation('cartoon');"> | |
367 Cartoon | |
368 </button> | |
369 | |
370 <button class="btn" id="btn-ball-stick" onclick="toggleModelRepresentation('ball+stick');"> | |
371 Ball + stick | |
372 </button> | |
373 | |
374 <button class="btn" id="btn-surface" onclick="toggleModelRepresentation('surface');"> | |
375 Surface | |
376 </button> | |
377 | |
378 <button class="btn" id="btn-backbone" onclick="toggleModelRepresentation('backbone');"> | |
379 Backbone | |
380 </button> | |
381 </div> | |
382 </div> | |
383 | |
384 <div class="box text-center"> | |
385 <h3> Actions </h3> | |
386 <div> | |
387 <button class="btn selected" id="btn-toggle-spin" onclick="toggleSpin();"> | |
388 Toggle spin | |
389 </button> | |
390 | |
391 <button class="btn" id="btn-toggle-dark" onclick="toggleDark();"> | |
392 Dark mode | |
393 </button> | |
394 </div> | |
395 </div> | |
396 | |
397 <div class="box text-center"> | |
398 <h3> Download </h3> | |
399 <div> | |
400 <button class="btn green" onclick="downloadPng();"> | |
401 Snapshot | |
402 </button> | |
403 | |
404 <button class="btn green" onclick="downloadPdb();"> | |
405 PDB | |
406 </button> | |
407 </div> | |
408 </div> | |
409 </div> | |
410 </div> | |
411 </body> | |
412 | |
413 | |
414 <script type="text/javascript"> | |
415 | |
416 // Render NGLviewer for PDB files | |
417 | |
418 // State management has been implemented with vanilla Js but could have used | |
419 // Vue - it's a fairly simple use case so a global 'state' object works fine | |
420 // without complicating things too much. | |
421 | |
422 | |
423 // Define a custom color scheme to represent model confidence consistently | |
424 // across different representations | |
425 // ------------------------------------------------------------------------ | |
426 const colorScale = chroma.scale([ | |
427 'red', 'yellow', 'green', 'cyan', 'blue' | |
428 ]).mode('lab').domain([0, 0.9]); | |
429 | |
430 const confidenceScheme = NGL.ColormakerRegistry.addScheme(function (params) { | |
431 this.atomColor = function (atom) { | |
432 // Actually model confidence (pLDDT) | |
433 const c = atom.bfactor; | |
434 const BREAK_RED = 40; // Below this is just plain red | |
435 let range, r, g, b; | |
436 | |
437 if (c < BREAK_RED) { | |
438 return 0xFF0000; | |
439 } | |
440 const p = (c - BREAK_RED) / (100 - BREAK_RED) | |
441 return eval(colorScale(p).hex().replace('#', '0x')); | |
442 }; | |
443 }); | |
444 | |
445 // NGL color schemes https://nglviewer.org/ngl/api/manual/usage/coloring.html | |
446 const COLORSCHEME = confidenceScheme; //'bfactor' | |
447 | |
448 const MODELS = [ | |
449 'ranked_0.pdb', | |
450 'ranked_1.pdb', | |
451 'ranked_2.pdb', | |
452 'ranked_3.pdb', | |
453 'ranked_4.pdb', | |
454 ] | |
455 | |
456 const REPRESENTATIONS = [ | |
457 'cartoon', | |
458 'ball+stick', | |
459 'surface', | |
460 'backbone', | |
461 ] | |
462 | |
463 const DEFAULT_REPRESENTATION = REPRESENTATIONS[0]; | |
464 const MAX_CLICK_INTERVAL_MS = 500; // For debouncing model clicks | |
465 | |
466 let stage; | |
467 let nonceSetModel; | |
468 | |
469 let state = { | |
470 model: 0, | |
471 modelObject: null, | |
472 representations: {}, | |
473 colorScheme: 'bfactor', | |
474 darkMode: false, | |
475 loading: 1, | |
476 spin: true, | |
477 } | |
478 | |
479 const uri = (i) => MODELS[i]; | |
480 // Switch to this function to return sample model URI (local dev) | |
481 // const uri = (i) => `https://raw.githubusercontent.com/neoformit/alphafold-galaxy/main/data/${MODELS[i]}`; | |
482 | |
483 document.addEventListener("DOMContentLoaded", async function () { | |
484 // Can set debug for development if NGL is being... funny | |
485 // NGL.setDebug(true) | |
486 | |
487 // Create NGL Stage object | |
488 stage = new NGL.Stage("ngl-root", { backgroundColor: 'white' }); | |
489 | |
490 // Handle window resizing | |
491 window.addEventListener("resize", () => stage.handleResize()); | |
492 | |
493 loadModel(); | |
494 while (true) { | |
495 if (!state.loading) { | |
496 // Reload page if NGL failed to display. Weird occassional bug. | |
497 const canvas = document.querySelector('#ngl-root canvas'); | |
498 canvas.height < 50 && window.reload(); | |
499 break | |
500 } | |
501 await new Promise(resolve => setTimeout(resolve, 500)); | |
502 } | |
503 }); | |
504 | |
505 // Models ------------------------------------------------------------------ | |
506 | |
507 const setModel = (ix) => { | |
508 state.model = ix; | |
509 stage.removeComponent(state.modelObject); | |
510 setLoading(1); | |
511 | |
512 // Debounce rapid model clicking with a nonce | |
513 nonceSetModel = new Object(); | |
514 const localNonce = nonceSetModel; | |
515 setTimeout( () => { | |
516 if (localNonce === nonceSetModel) { | |
517 // The user has stopped clicking, hurray... | |
518 loadModel().then(updateButtons); | |
519 } | |
520 }, MAX_CLICK_INTERVAL_MS); | |
521 } | |
522 | |
523 const loadModel = () => { | |
524 reps = Object.keys(state.representations); | |
525 if (reps.length) { | |
526 state.representations = {}; | |
527 } else { | |
528 reps = [DEFAULT_REPRESENTATION]; | |
529 } | |
530 | |
531 // Load PDB entry | |
532 return stage.loadFile(uri(state.model)).then( (o) => { | |
533 state.modelObject = o; | |
534 reps.forEach( (r) => addModelRepresentation(r) ); | |
535 stage.setSpin(state.spin); | |
536 o.autoView(); | |
537 setLoading(0); | |
538 }) | |
539 } | |
540 | |
541 // Representations --------------------------------------------------------- | |
542 | |
543 const toggleModelRepresentation = (rep) => { | |
544 rep in state.representations ? | |
545 removeModelRepresentation(rep) | |
546 : addModelRepresentation(rep) | |
547 } | |
548 | |
549 const addModelRepresentation = (rep) => { | |
550 state.representations[rep] = | |
551 state.modelObject.addRepresentation(rep, {colorScheme: COLORSCHEME}); | |
552 updateButtons(); | |
553 } | |
554 | |
555 const removeModelRepresentation = (rep) => { | |
556 o = state.representations[rep]; | |
557 state.modelObject.removeRepresentation(o); | |
558 delete state.representations[rep]; | |
559 updateButtons(); | |
560 } | |
561 | |
562 const clearModelRepresentations = () => { | |
563 state.modelObject && state.modelObject.removeAllRepresentations(); | |
564 state.representations = {}; | |
565 } | |
566 | |
567 // Actions ----------------------------------------------------------------- | |
568 | |
569 const toggleDark = () => { | |
570 state.darkMode = !state.darkMode; | |
571 stage.setParameters({ | |
572 backgroundColor: state.darkMode ? 'black' : 'white', | |
573 }); | |
574 const btn = document.querySelector('#btn-toggle-dark'); | |
575 btn && btn.classList.toggle('selected'); | |
576 } | |
577 | |
578 const setLoading = (state) => { | |
579 document.getElementById('ngl-loading') | |
580 .style.display = state ? 'flex' : 'none'; | |
581 state.loading = state; | |
582 } | |
583 | |
584 const toggleSpin = () => { | |
585 stage.toggleSpin(); | |
586 const btn = document.querySelector('#btn-toggle-spin'); | |
587 btn && btn.classList.toggle('selected'); | |
588 state.spin = !state.spin; | |
589 } | |
590 | |
591 const downloadPng = () => { | |
592 const params = { | |
593 factor: 3, | |
594 antialias: true, | |
595 } | |
596 stage.makeImage(params).then( (blob) => { | |
597 const name = MODELS[state.model].replace('.pdb', '.png'); | |
598 const url = URL.createObjectURL(blob); | |
599 makeDownload(url, name); | |
600 }) | |
601 } | |
602 | |
603 const downloadPdb = () => { | |
604 const url = uri(state.model); | |
605 const name = `alphafold_${MODELS[state.model]}`; | |
606 makeDownload(url, name); | |
607 } | |
608 | |
609 const makeDownload = (url, name) => { | |
610 // Will not work with cross-origin urls (i.e. during development) | |
611 console.log(`Creating file download for ${name}, href ${url}`); | |
612 const saveLink = document.createElement('a'); | |
613 saveLink.href = url; | |
614 saveLink.download = name; | |
615 document.body.appendChild(saveLink); | |
616 saveLink.dispatchEvent( | |
617 new MouseEvent('click', { | |
618 bubbles: true, | |
619 cancelable: true, | |
620 view: window | |
621 }) | |
622 ); | |
623 document.body.removeChild(saveLink); | |
624 } | |
625 | |
626 const updateButtons = () => { | |
627 MODELS.forEach( (name, i) => { | |
628 const id = `#btn-${name.replace('.pdb', '')}`; | |
629 const btn = document.querySelector(id); | |
630 if (!btn) return | |
631 i == state.model ? | |
632 btn.classList.add('selected') | |
633 : btn.classList.remove('selected'); | |
634 }) | |
635 | |
636 REPRESENTATIONS.forEach( (name) => { | |
637 const id = `#btn-${name}`.replace('+', '-'); | |
638 const btn = document.querySelector(id); | |
639 if (!btn) return | |
640 if (name in state.representations) { | |
641 btn.classList.add('selected') | |
642 } else { | |
643 btn.classList.remove('selected'); | |
644 } | |
645 }); | |
646 | |
647 // Show "Nothing to display" if no representations are selected | |
648 document.querySelector('#ngl-nothing').style.display = | |
649 Object.keys(state.representations).length ? | |
650 'none' | |
651 : 'block'; | |
652 } | |
653 | |
654 </script> | |
655 | |
656 </html> |