Mercurial > repos > iuc > recentrifuge
diff test-data/kraken_test/test1_csv.rcf.html @ 0:09b7b0b2e2c2 draft
planemo upload for repository https://github.com/mesocentre-clermont-auvergne/galaxy-tools/tree/master/tools/recentrifuge commit fdcec50b71967011e4351eb347a9df2840be6bee
author | iuc |
---|---|
date | Mon, 27 Jun 2022 11:03:22 +0000 |
parents | |
children | fe733f05c2f8 |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/test-data/kraken_test/test1_csv.rcf.html Mon Jun 27 11:03:22 2022 +0000 @@ -0,0 +1,6581 @@ +<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> +<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"><head><meta charset="utf-8"><link rel="shortcut icon" href="data:image/x-icon;base64,AAABAAMAEBAAAAEAIABoBAAANgAAABgYAAABACAAiAkAAJ4EAAAgIAAAAQAgAKgQAAAmDgAAKAAAABAAAAAgAAAAAQAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP///wCAgIAC////AP///wC0tKJHlZWSqI6OmuRxcXn9koaK9J2Ym8uNm5V73PPoFv///wAAAAAD////AP///wCAgIAC////AP//7xCHh4OodHSi/2xqw/9nZ9H/XVuU/9eFqv/njb7/yYiq/5GAiOifqKJV////AAAAAAL///8A////AO3t2w56eoHPdHLH/3Vz3vx4dtz7dXTb/mVimv/XhKn/8ZPG/fSVx/vvlcT/kG+E/32Himb///8AAAAAA////wB0dHageHbO/3585fl5d9r/dnTX/3V02/5mYpz80X2i/eaIuv/ukcD/wXak/I6Maf+OkXD9iIKjL////wCEhFo+c3Kv/4F+6P18etz/e3nd/3l33P57eeT/aWag/9F5oP/sib3+tmuZ/3uEXf/P1YX7zdKG/3x9dbX///8Ac3J9nYKA4f+CgOb7fnzf/3174P58et//bm2v8Vtgc7SdbIHNqWmM/4aLZP/L0YT/0dWH/tfdiP+go3P6YlyWJ3BvmduGg+n/g4Hk/oF+4/6AfuX/bm2k7pCUUUX///8Ajv/jCVBmXqPBxoH/1NmI/c7Uhf/b4Yn9wMWB/2BdcGBraqX6iYbs/4SB5P+Eguj7hYPm/2Vlcov///8AACRtB////wAAAIAWrLB29uHojv/X3on+3eSK+9PYh/9XWVN7bWum+ouI7/+Gg+f/hoTq+4iF6f9lZXKL////ACQAbQf///8AAABoFoF+Xva0snL/y818/uDmiPvZ34n/WVlTe3Jxm9uMiu//iofr/oiG6f6Jhu3/cnGo7oyQQ0X///8A/znjCXF8dKNotpL/Za2O/WuVe/+FkGv9mJ5w/2pqaGBzc3+djYrr/42K8fuKh+v/i4js/oqH6/93dbfxYmJztGaTec1yxJr/gOGu/4nptf+K6Lf+i+q8/2eXhfqDNG8ngIBKPnt6uP+Rjvf9jInr/4yJ7f+Miu3+jor0/25xoP920pz/gOGw/oHerv+H4rL/kPC9+5Ttvf98ioC1////AP///wB0dHSgioff/5SR+/mOi+7/jYvt/4+L8P5scJ78etKe/YXis/+H47P/kO+8/JLxv/92m4f9nXKNL////wD///8A7e3bDnt7hM+Ihdv/ko/4/JSS+PuUkPb+bnKg/4Lapv+Q7739kfO/+43puf9/qJH/mYeUZv///wAAAAADgICAAv///wD//+8QhoaBqIB/sf+Iht7/jYjw/2tvnf+D3Kf/ieS1/4bIpP+AkYfoqJOfVf///wAAAAAC////AP///wCAgIAC////AP///wCwsJdHlZWQqJORn+RzdHv9hZOK9Jecmcuej5l7/+jzFv///wAAAAAD////AP///wD4PwAA4A8AAMAHAACAAwAAgAEAAAABAAADgQAAA8EAAAPBAAADgQAAAAEAAIABAACAAwAAwAcAAOAPAAD4PwAAKAAAABgAAAAwAAAAAQAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP///wD///8A////AP///wEAAAAB////AP///wi6urdDubmxj6ennsuQkIzvc3Nz/YSIh/WWoZzWrrezoLe6t1X///8T////AP///wCAgIAC////AP///wD///8A////AP///wD///8A////Af///wD///8Ax8fDQJeYkLV4eH39WlmM/19dpP9dX6b/WFRr/611j/+/fJ//om2I/4N3ff+Jko7Ntri2Xv///wD///8AgICAAv///wD///8A////AP///wD///8B////AP///wOfn5t9c3N29GRjn/9oZ8P/c3LU/XVy3P9rbdH/YVt8/9eIrv/5mcz/75bE/t+Mt/67epz/fnN5/4uTkKP///8W////AAAAAAL///8A////AP///wH///8A////A5iYkZJoaH3/bWvB/3h23Px4dt3+dHPX/3Rx1v9sbcv/YFp6/82ApP/rj8D/65DA//KVxv/1mcn83424/5t1iP98gX7A////F////wCAgIAC////AAAAAAL///8AkJCHdWZlf/9zcs7/fHrj/Hh32/92ddn/dXTX/3Zz2P9vcM//YFt7/8t9of/pi73/54u7/+eNvf/vk8P/7ZbE/pxsh/xLVU7/iomJqP///wD///8A////Af///wCfn5Y4ZmV07XZ0zv9+fOX8enjc/3p43P94dtv/dnXZ/3d12f9xcdD/YFp6/8h5nf/mh7r/44e3/+qMvf/ljrv/jV59/3F+V/68wH3/gYNp/4uIlWf///8AAAAAAv///wJ3d22pc3G6/4KA5/t9e9//fHre/3t53f96eNz/eHbZ/3l23P50dNb9Ylx+/ch2m/3jg7b+44W2/+CItv+NXXz/bnpW/8XIgv/Y3Yv8ur57/3Fxbtni4uIa////AHl5XTdoZ4b4f3zb/4F+5P5+fN//fXvf/3x63v96edz/ennf/nt54P9xccb/Xlxy/7Rvj//gf7P/3IKw/olZeP9te1b/xsqD/9DVh//P1IX/09iG/pWYcf9nZ3Nt////AISEb35ycLb/hYPr/YB+4f9/feH/fnzh/3173/99e+L+enjX/2dmmfhlZXiwYGBiknJkaqGVZ37pg11x/3eBXP7GyoP/0NWH/87Uhf/R1ob/2eCK+8HFff9tbXWv////BG9vdb16eMn/hYPq/4F/4/+BfuP/gH7i/4B94v59e93/Y2KO+ISEbX7y8uQT////AP///weAjo5aT1tP5L7Cf//T2Ij+ztOF/9HWhv/S14b/2d+K/s3Sgv+BgnLcg4OSI11deeaBf9r/hYPo/4OB5P+DgeX/gX/i/4OB6v10c77/cXFjmv///wL///8A////AP///wD///8AeHN9apuecP/U2Yf/0teG/tPZhv/V24f/2uCJ/tTahv+KjGj6AABINV9egfuGg+P/hoTp/4WC5v+EgeX/goDk/4WD6/1wbq//WFgAQ////wAzMzMFAAAAAVVVVQP///8AAABjEoiLZ/Df5Y7/2d+J/dfch//W3If/2d+H/9vhif6ZnW3/AAA5P19egvuHheX/iIXq/4eE6P+Gg+f/hILl/4eE7P1xb7D/WFgAQ////wAzMzMFAAAAAVVVVQP///8AAABVEnd1WvDEx37/y9GD/dnfif/g54z/4OeL/97kiv6anWz/AAA1P15eeuaFg93/ioft/4iF6f+Hhen/hoPn/4mG7/14d8L/cXFimv///wP///8A////AP///wD///8AenV3a1dyZf9QY1//bGVW/pORY/+trnD/xch7/tTZhP+Pkmn6AABINXBwdb1/fc7/jYry/4mH6v+Jhur/iIbp/4iG6/6Gg+X/Z2aT+IKEaX7y8uQT////AP///wiXgY9bXndp5HXLnv95z6P+dsif/2Wrjv9RcWv/Z3Rk/nyBYv9xcWbbfHyDI4SEa354d73/kI31/YuI6/+LiOz/iofr/4mG6v+Lh+3+hoPi/21soPhnZ3qwYGBikmNza6JhinXpcsWa/3vdq/6C4a//iOi1/4zpt/+O6Lf/kOu8+3O1l/9jVmKv////BHl5WDdraor4iofn/4+M8v6Miez/i4nt/4uI7P+LiOv/i4jt/ouJ8P9+e83/XGBt/223jv952aj/e9uq/n3aqv+D36//huKy/4rntv+Q7r3/lvTC/nupj/9zYmxt////AP///wJ3d2qpfXvF/5KP9vyNiu7/jYru/4yJ7f+Miu7/i4jr/46L8P6FgN79XmZ5/XTMm/1+3q7+f9ys/4Lgr/+G5LP/iue2/4zot/+V9cL8iNWs/3BxcNni4uIa////AP///wCfn5I4aGd17YeE3v+Ukfj8jovu/46L7/+Oi+//jYru/46M8f+FgNv/XmV3/3jMnf+D4rH/g+Cw/4fks/+K57b/jem4/5TywP6Q57n/bYh5/5KIjWf///8AAAAAAgAAAAL///8AkJCFdWtqhf+IheL/lZL5/JCN8P+PjO//j4zv/5GO8/+Hgt3/X2Z4/33Rof+I5rb/h+Sz/4nmtf+N6rn/lPPB/pHruvxxm4T/h3yDqP///wD///8A////Af///wH///8A////A5aWj5JtbIP/g4DW/5SR9vyVkvj+kY7y/5KP9P+Ig97/X2d4/4DUpf+N6rn/jeq4/5Lwvv+U88D8h9qu/3KVgv+Lgoe/////F////wCAgIAC////AP///wD///8B////AP///wOfn5l9dHR39HVzsf+Fg97/k4/x/ZiW/v+Nh+f/YWl7/4ngrv+W+MX/kuy8/onesP52uZb/dH54/5aOk6P///8W////AAAAAAL///8A////AP///wD///8A////Af///wD///8Ax8fDQJWVjbV5eX79amic/3h2vv93crf/WV9r/3W1kP95vZn/aZ2C/3eCfP+Ui5DNu7i7Xv///wD///8AgICAAv///wD///8A////AP///wD///8A////AP///wEAAAAB////AP///wi6urdDuLitj6WmmcuPj4rvc3Nz/YiEh/Whlp3Wt66zoLq3ulX///8T////AP///wD///8C////AP///wD///8A////AP8B/wD8AH8A+AAfAOAADwDgAAcAwAAHAIAAAwCAAAMAgAABAAB8AQAAfgEAAP4BAAD+AQAAfgEAAHwBAIAAAQCAAAMAgAADAMAABwDgAAcA4AAPAPgAHwD8AH8A/wH/ACgAAAAgAAAAQAAAAAEAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////KO3t7XXFxcWwqqqq25eXl/RwcHD9mJiY9Kurq9vHx8ew8PDwdf///yj///8A////AP///wD///8A////AP///wD///8A////AP///wD///8AAAAAAP///wD///8A////AP///wD///8A////AP///wD///8A9/f3TLS0tLpvb3H/UVFR/1VVWf9bW27/WVlz/1FRUf96ZG//c2Nr/1lWV/9RUVH/d3V2/7m5ubr5+flM////AP///wD///8A////AP///wD///8A////AP///wAAAAAA////AP///wD///8A////AP///wD///8A/f39HLW1tatbW13/V1de/2FgkP9rabv/cG7S/29u0/9paL7/UVFR/9GIrv/wlcX/75bE/8+Irf+ZcYb/XVhb/2FfYP+8vLyr/v7+HP///wD///8A////AP///wD///8A////AAAAAAD///8A////AP///wD///8A////AOvr60l2dnnpVVVZ/2ZlnP9ycdL/c3LW/3Jx1f9xcNT/cG/T/2ppvv9RUVH/zoSs/+yRwf/uk8L/7pPC/++UxP/pk8D/pXaP/1hVV/9/fn/p7+/vSf///wD///8A////AP///wD///8AAAAAAP///wD///8A////AP///wDg4OBWX19h/11dcv9zccr/d3XZ/3Z02P91c9f/dHLX/3Nx1v9xcNT/a2q//1FRUf/Ngqn/6o6+/+uQwP/tksH/7pPD/++Vxf/xl8b/3I64/3Jiav9lY2T/2dnZVv///wD///8A////AP///wAAAAAA////AP///wD///8A5OTkQ1tbXf9hYIH/eHfY/3l32/94dtr/d3ba/3Z12f90c9f/c3LW/3Nx1f9tbMH/UVFR/8l/pf/mirr/6Iy8/+mOvv/qj7//7JHB/++VxP/xl8b/04qx/1RUUv9ZWVj/6+vrQ////wD///8A////AAAAAAD///8A////APX19Q9mZmjfYF98/3t53P97ed3/enjc/3l32/93dtr/dnXZ/3Z02P91c9f/dXTY/25tw/9RUVH/yHyj/+WIuf/libn/54q7/+mNvf/rj7//7JHB/9WJsf9XVlX/Z2hb/3BxX/9vb23f+fn5D////wD///8AAAAAAP///wD///8AkZCSnFpaZ/97edn/fHre/3t53v97ed3/enjc/3l33P94dtv/dnXZ/3V02P92dNj/cG7D/1FRUf/FeaH/4oW2/+OGt//liLj/54q6/+mOvv/Sha3/V1VV/2doW//FyYL/xcqC/19fWf+fn5+c////AP///wAAAAAA////ANjY2DhRUVL/dXPA/3994f9+fOD/fXvf/3x63v97ed3/enjc/3l32/94dtr/eHba/3d12f9xb8T/UVFR/8J1nf/fgLH/4IOz/+KEtf/liLn/zoCq/1dVVf9naFr/w8eA/83Shf/P1Yb/q652/1JSUf/h4eE4////AAAAAAD///8Afn6ApWNigv+BfuL/gH3h/3584P99e9//fHre/3x63v97ed3/eXjc/3l32/95d9v/d3ba/3Fwxf9RUVH/wXSc/9x9r//fgbL/4YO0/8t9pv9XVVT/Z2ha/8LHgf/N0oX/ztOF/8/Uhv/S14b/c3Vh/42Ni6X///8AAAAAAOfn5xJRUVH/eXbG/4F/4/+AfuL/f33h/3584P99e+D/fHrf/3x63v97ed3/e3ne/3p43f9ycMH/Y2OR/1FRUf+NY3j/w3Sd/9x9rv/IeaL/V1VU/2doWv/DyIH/zNGF/87Thf/Q1Yb/0daG/9TZh/+ytnj/UlJS/+/v7xIAAAAAq6urYF1cav+DgeX/goDk/4F/4/+AfuL/f33h/3994f9+fOD/fXvf/3x63v95d9X/YF99/1FRUb9sbG+AZmVmgG1qa4BRUVG/fWBv/1dVVP9naFr/xMiB/83Shv/O04b/z9WG/9HWhv/S14b/1NqH/9TZhv9fYFn/t7e3YAAAAAB6enyeaGeV/4SC5v+DgeX/goDk/4F+4/+BfuP/f33h/3584P9+fOD/fHrb/1xcbv9sbG6g39/fQP///wD///8A////ANra2kBbWlugVVVT/8LHgf/O04X/ztOF/8/Uhf/Q1Yb/0teG/9PZh//V24f/2N6I/4aIZv+IiIeeAAAAAFpaXMx0crX/hYLm/4SC5v+DgeX/goDk/4KA5P+Bf+P/gH7i/3994f9oZ5v/X19hv/Hx8SD///8A////AP///wD///8A////AOfn5yBcXFq/m55w/9DVhv/R1ob/0teG/9LXhv/T2Yb/1dqH/9jdiP/Z34j/o6Zy/2VlY8wAAAAAUVFR7Hx7zP+GhOj/hYLm/4SB5f+Egub/g4Hl/4KA5P+Bf+P/gX/j/1paZP+xsbFA////AP///wD///8A////AP///wD///8A////AKampkBqa17/0teG/9LXhv/T2Yf/1dqH/9bciP/Y3oj/2N6H/9rgiP+5vXr/UlJS7AAAAABRUVH7gX7W/4eE6P+GhOj/hYPn/4WC5v+EgeX/g4Hl/4OB5f9/fNv/UVFR/9zc3AD///8A////AP///wD///8A////AP///wD///8A0NDQAFRUU//R14b/1dqH/9Xbh//W3If/192H/9jeh//a4Ij/3OKI/8PJfv9RUVH7AAAAAFFRUfuCgNj/iIbq/4eE6P+HhOj/hoPn/4WC5v+Egub/hILm/4B+3f9RUVH/3NzcAP///wD///8A////AP///wD///8A////AP///wDLy8sAUlJR/87ThP/W3Ij/192H/9nfiP/Z34j/2uCI/9vhiP/d44j/xMh+/1FRUfsAAAAAUVFR7H99zv+Jh+v/iIXq/4iF6v+Hhen/hoTo/4aD5/+Fgub/hYLm/1taZP+xsbFA////AP///wD///8A////AP///wD///8A////AJWVlUBSUlH/UlJR/25vXf+Ul2z/ur97/9jeh//c44j/3uWJ/97lif+8wXr/UVFR7AAAAABaWlzMd3W4/4qH6/+Kh+v/iYbq/4iF6f+HhOj/iIXp/4aE6P+GhOj/a2qe/19fYr/x8fEg////AP///wD///8A////AP///wDp6ekgW1xcv2SWe/9nm4D/Wm9k/1daVv9RUVH/VlZT/3p8Yf+ipnH/ys+A/6ircv9lZmPMAAAAAHp6fJ5rapf/i4js/4uJ7f+KiOz/iYbq/4mG6v+Ihur/iIbq/4eE6P+FguP/Xl5w/2xsb6Df399A////AP///wD///8A2traQGVnZqBccGX/d9Sk/3vZqf+B367/g+Gw/33No/9qk33/W2lh/1ZXVP9RUVH/U1NR/3d3dZ4AAAAAq6urYF5ea/+Ni+//jInt/4uI7P+LiOz/iofr/4mG6v+Jhur/iIXp/4iF6f+EguD/ZGSB/1FRUb9ra2+AZWdmgGpubIBRUVG/Xn1u/3TOn/9516f/ftyr/4Herf+G47L/iOa1/4vot/+Q7bz/k+++/4PHo/9SVFP/oaGhYAAAAADn5+cSUVFR/4F/z/+Niu7/jInt/4yJ7f+LiOz/i4nt/4qI7P+LiOz/iofr/4mG6v+Jhur/fnzN/2ZljP9RUVH/YpB5/2++lf921KT/edem/3vZqf+A3q3/hOGw/4bjs/+J5rX/juu6/5Dsu/+U8cD/g8Wi/1JTUv/t7e0SAAAAAP///wB+foClZ2aG/46L8P+Oi/D/jYru/4yJ7f+Mie3/i4js/4uI7P+LiOz/iofr/4qH6/+KiOz/eXjA/1FRUf9zyJz/eden/3zaqf982qn/ftys/4Hfrv+G47L/iOW0/4rntv+N6rn/ku++/5XxwP9le2//iYqKpf///wAAAAAA////ANjY2DhRUVL/gH7L/4+M8P+Oi+//jovv/42K7v+Niu7/jIru/4yK7v+Mie3/jInt/4uI7P96eMD/UVFR/3bLoP972an/ftyr/4Herv+D4K//heOy/4jmtf+L6Lf/juu5/5DtvP+V8cD/gcGf/1NUU//l5eU4////AAAAAAD///8A////AJGRkpxdXGn/jYrp/5CN8f+PjPD/j4zw/46L7/+Oi+//jYru/42K7v+Niu7/jYru/3x5wv9RUVH/ec6i/4Herv+B367/hOGw/4bjsv+I5bT/i+i3/43quf+Q7bz/k+++/4/jtv9bY17/nZ6enP///wD///8AAAAAAP///wD///8A9vb2D2Zmad9mZYL/kI3w/5CN8f+QjfH/j4zw/4+M8P+PjPD/j4zw/46L7/+Oi+//fHrD/1FRUf9/0qb/hOGw/4Xjsv+F47L/iOa1/4vot/+O67r/kO28/5Lvvf+S7b3/ZHht/3N1dN/6+voP////AP///wAAAAAA////AP///wD///8A5OTkQ1xcXv9oZ4j/kI3v/5GN8f+RjfH/kI3x/5CN8f+QjfH/kI3x/5CN8f99e8P/UVFR/3/UqP+I5bT/iOW0/4vot/+L6Lf/juu5/4/su/+S773/kem6/2Z9cf9gYmH/6+vrQ////wD///8A////AAAAAAD///8A////AP///wD///8A4ODgVl9fYf9iYnf/iojh/5KP8/+Sj/P/kY7y/5GO8v+RjvL/kY7y/358xf9RUVH/hdmt/4vot/+O67n/juu5/5Dtu/+Q7bv/ku++/4rXrv9hcGj/aGpp/+np6Vb///8A////AP///wD///8AAAAAAP///wD///8A////AP///wD///8A7OzsSXd3eelWVlr/dXOr/5CO7v+TkPT/k5D0/5KP8/+Sj/P/f33F/1FRUf+I267/kO28/5Dsu/+Q7Lv/ku+9/5Douv9zoon/U1VU/4GDgunx8fFJ////AP///wD///8A////AP///wAAAAAA////AP///wD///8A////AP///wD///8A/f39HLW1tatbW13/WVlg/3Bunv+Gg9T/k4/y/5SR9f+Bfsf/UVFR/4vfsv+S773/kOu7/4TKpf9ul4H/VlpY/19hYP/AwMCr////HP///wD///8A////AP///wD///8A////AAAAAAD///8A////AP///wD///8A////AP///wD///8A////APf390y0tLS6cG9y/1FRUf9WVlr/Y2J2/2Fgdv9RUVH/ZYBx/2N1a/9VV1b/UVFR/3R2df+/v7+6+vr6TP///wD///8A////AP///wD///8A////AP///wD///8AAAAAAP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8o7e3tdcXFxbCqqqrbl5eX9HBwcP2YmJj0q6ur28fHx7Dv7+91////KP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wAAAAAA//Af//+AA//+AAD//AAAf/gAAD/wAAAf4AAAD8AAAAfAAAAHgAAAA4AAAAOAAAADAAfAAQAP4AEAH/ABAB/wAQAf8AEAH/ABAA/gAQAHwAGAAAADgAAAA4AAAAPAAAAHwAAAB+AAAA/wAAAf+AAAP/wAAH/+AAD//4AD///wH/8="><link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Ubuntu"><script id="notfound">window.onload=function(){document.body.innerHTML=""}</script><script language="javascript" type="text/javascript">{//---------------------------------------------------------------------------- +// +// PURPOSE +// +// Krona is a flexible tool for exploring the relative proportions of +// hierarchical data, such as metagenomic classifications, using a +// radial, space-filling display. It is implemented using HTML5 and +// JavaScript, allowing charts to be explored locally or served over the +// Internet, requiring only a current version of any major web +// browser. Krona charts can be created using an Excel template or from +// common bioinformatic formats using the provided conversion scripts. +// +// +// COPYRIGHT LICENSE +// +// Copyright (c) 2011, Battelle National Biodefense Institute (BNBI); +// all rights reserved. Authored by: Brian Ondov, Nicholas Bergman, and +// Adam Phillippy +// +// This Software was prepared for the Department of Homeland Security +// (DHS) by the Battelle National Biodefense Institute, LLC (BNBI) as +// part of contract HSHQDC-07-C-00020 to manage and operate the National +// Biodefense Analysis and Countermeasures Center (NBACC), a Federally +// Funded Research and Development Center. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// +// * Neither the name of the Battelle National Biodefense Institute nor +// the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written +// permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +// +// +// TRADEMARK LICENSE +// +// KRONA(TM) is a trademark of the Department of Homeland Security, and use +// of the trademark is subject to the following conditions: +// +// * Distribution of the unchanged, official code/software using the +// KRONA(TM) mark is hereby permitted by the Department of Homeland +// Security, provided that the software is distributed without charge +// and modification. +// +// * Distribution of altered source code/software using the KRONA(TM) mark +// is not permitted unless written permission has been granted by the +// Department of Homeland Security. +// +// +// FOR MORE INFORMATION VISIT +// +// https://github.com/marbl/Krona/wiki/ +// +//---------------------------------------------------------------------------- +// +// Copyright (C) 2017-2022 Jose Manuel Martí Martínez, for the changes in +// this file from the Krona Javascript 2.0 release. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the above copyright notice is +// reproduced and all the above conditions are met. +// +// The KRONA(TM) mark has been substituted in the generated charts by +// another logo in compliance with the above-stated conditions. +// +// FOR MORE INFORMATION VISIT +// +// https://github.com/khyox/recentrifuge/wiki/ +// +//---------------------------------------------------------------------------- +} + +/////////////// +// Variables // +/////////////// + +var canvas; +var canvasButtons = []; // Keep trace of CanvasButton objects +var ChartEnum = Object.freeze({ + TAXOMIC: 'taxonomic', + GENOMIC: 'genomic' +}) +var chart = ChartEnum.TAXOMIC +var context; +var svg; // for snapshot mode +var collapse = true; +var collapseCheckBox; +var collapseLast; +var compress; +var compressCheckBox; +var maxAbsoluteDepthText; +var maxAbsoluteDepthButtonDecrease; +var maxAbsoluteDepthButtonIncrease; +var fontSize = 12; +var fontSizeText; +var fontSizeButtonDecrease; +var fontSizeButtonIncrease; +var fontSizeLast; +var bkgBright = "eeeeee"; +var bkgBrightButtonDecrease; +var bkgBrightButtonIncrease; +var radiusButtonDecrease; +var radiusButtonIncrease; +var shorten; +var shortenCheckBox; +var maxAbsoluteDepth; +var backButton; +var upButton; +var forwardButton; +var snapshotButton; +var snapshotMode = false; +var details; +var detailsName; +var search; +var searchResults; +var nSearchResults; +var useHueCheckBox; +var useHueDiv; +var sortByScoreCheckBox; +var datasetDropDown; +var datasetButtonLast; +var datasetButtonPrev; +var datasetButtonNext; +var rankDropDown; +var keyControl; +var showKeys = true; +var linkButton; +var linkText; +var frame; + +// Node references. Note that the meanings of 'selected' and 'focused' are +// swapped in the docs. +// +var head; // the root of the entire tree +var selectedNode = 0; // the root of the current view +var focusNode = 0; // a node chosen for more info (single-click) +var highlightedNode = 0; // mouse hover node +var highlightingHidden = false; +var nodes = new Array(); // Array with all the nodes +var nodesIndex; // Index of nodes, points last using hue(score) buttons +var currentNodeID = 0; // to iterate while loading + +var nodeHistory = new Array(); +var nodeHistoryPosition = 0; + +var dataEnabled = false; // true when supplemental files are present + +// store non-Krona GET variables so they can be passed on to links +// +var getVariables = new Array(); + +// selectedNodeLast is separate from the history, since we need to check +// properties of the last node viewed when browsing through the history +// +var selectedNodeLast = 0; +var zoomOut = false; + +// temporary zoom-in while holding the mouse button on a wedge +// +var quickLook = false; // true when in quick look state +var mouseDown = false; +var mouseDownTime; // to detect mouse button hold +var quickLookHoldLength = 200; + +var imageWidth; +var imageHeight; +var centerX; +var centerY; +var gRadius; +var updateViewNeeded = false; + +// Determines the angle that the pie chart starts at. 90 degrees makes the +// center label consistent with the children. +// +var rotationOffset = Math.PI / 2; + +var buffer; +var bufferFactor = .1; + +// The maps are the small pie charts showing the current slice being viewed. +// +var mapBuffer = 10; +var mapRadius = 0; +var maxMapRadius = 25; +var mapWidth = 150; +var maxLabelOverhang = Math.PI * 4.18; + +// Keys are the labeled boxes for slices in the highest level that are too thin +// to label. +// +var maxKeySizeFactor = 2; // will be multiplied by font size +var keySize; +var keys; +var keyBuffer = 10; +var currentKey; +var keyMinTextLeft; +var keyMinAngle; + +var minRingWidthFactor = 5; // will be multiplied by font size +var maxPossibleDepth; // the theoretical max that can be displayed +var maxDisplayDepth; // the actual depth that will be displayed +var headerHeight = 0;//document.getElementById('options').clientHeight; +var historySpacingFactor = 1.6; // will be multiplied by font size +var historyAlphaDelta = .25; + +// appearance +// +var lineOpacity = 0.3; +var saturation = 0.5; +var lightnessBase = 0.6; +var lightnessMax = .8; +var thinLineWidth = .3; +var highlightLineWidth = 1.5; +var labelBoxBuffer = 6; +var labelBoxRounding = 15; +var labelWidthFudge = 1.05; // The width of unshortened labels are set slightly + // longer than the name width so the animation + // finishes faster. +var fontNormal; +var fontBold; +var fontFamily = 'sans-serif'; +//var fontFaceBold = 'bold Arial'; +var nodeRadius; +var angleFactor; +var tickLength; +var compressedRadii; + +// colors +// +var highlightFill = 'rgba(255, 255, 255, .3)'; +var colorUnclassified = 'rgb(220,220,220)'; + +// label staggering +// +var labelOffsets; // will store the current offset at each depth +// +// This will store pointers to the last node that had a label in each offset +// (or "track") of each depth. These will be used to shorten neighboring +// labels that would overlap. The [nLabelNodes] index will store the last node +// with a radial label. labelFirstNodes is the same, but to check for going all +// the way around and overlapping the first labels. +// +var labelLastNodes; +var labelFirstNodes; +// +var nLabelOffsets = 3; // the number of offsets to use + +var mouseX = -1; +var mouseY = -1; +var mouseXRel = -1; +var mouseYRel = -1; + +// tweening +// +var progress = 0; // for tweening; goes from 0 to 1. +var progressLast = 0; +var tweenFactor = 0; // progress converted by a curve for a smoother effect. +var tweenLength = 850; // in ms +var tweenCurvature = 13; +// +// tweenMax is used to scale the sigmoid function so its range is [0,1] for the +// domain [0,1] +// +var tweenMax = 1 / (1 + Math.exp(-tweenCurvature / 2)); +// +var tweenStartTime; + +// for framerate debug +// +var tweenFrames = 0; +var fpsDisplay = document.getElementById('frameRate'); + +// Arrays to translate xml attribute names into displayable attribute names +// +var attributes = []; +// +var magnitudeIndex; // the index of attribute arrays used for magnitude +var membersAssignedIndex; +var membersSummaryIndex; + +// For defining gradients +// +var hueDisplayName; +var hueStopPositions; +var hueStopHues; +var hueStopText; + +// multiple datasets +// +const DEFAULT_RANK = 'SUMMARY'; +const NO_RANK = 'NONE'; +var currentRank = DEFAULT_RANK; +var currentDataset = 0; +var lastDataset = 0; +var datasets = 1; +var datasetNames; +const DATASET_MAX_SIZE = 20; // Max size in rows of the dataset selection list +var datasetsVisible = 1; // Number of datasets not hidden +var datasetAlpha = new Tween(0, 0); +var datasetWidths = []; +var datasetChanged; +var datasetSelectWidth = 50; +var numRawSamples; +var stats; + +window.onload = load; + +var image; +var hiddenPattern; +var loadingImage; +var logoImage; + +// Setup CSS-like style of tooltips for attributes +// +var csstring = '.CellWithTooltip{ position:relative; }\n' + + '.Tooltip{ display:none;position:absolute;z-index:100;border:2px;' + + 'background-color:white;border-style:solid;border-width:2px;' + + 'border-color:red;padding:3px;color:red;top:20px;left:0px; }' + + '.CellWithTooltip:hover span.Tooltip{ display:block; }'; +var style = document.createElement('style'); +if (style.styleSheet) { + style.styleSheet.cssText = csstring; +} else { + style.appendChild(document.createTextNode(csstring)); +} +document.getElementsByTagName('head')[0].appendChild(style); + +/////////////// +// Functions // +/////////////// + +function backingScale() { + if ('devicePixelRatio' in window) { + if (window.devicePixelRatio > 1) { + return window.devicePixelRatio; + } + } + + return 1; +} + +function resize() { + imageWidth = window.innerWidth; + imageHeight = window.innerHeight; + + if (!snapshotMode) { + context.canvas.width = imageWidth * backingScale(); + context.canvas.height = imageHeight * backingScale(); + context.canvas.style.width = imageWidth + "px" + context.canvas.style.height = imageHeight + "px" + context.scale(backingScale(), backingScale()); + } + + if (datasetDropDown) { + var ratio = + (datasetDropDown.offsetTop + datasetDropDown.clientHeight) * 2 / + imageHeight; + + if (ratio > 1) { + ratio = 1; + } + + ratio = Math.sqrt(ratio); + + datasetSelectWidth = + (datasetDropDown.offsetLeft + datasetDropDown.clientWidth) * ratio; + } + var leftMargin = datasets > 1 ? datasetSelectWidth + 30 : 0; + var minDimension = imageWidth - mapWidth - leftMargin > imageHeight ? + imageHeight : + imageWidth - mapWidth - leftMargin; + + maxMapRadius = minDimension * .03; + buffer = minDimension * bufferFactor; + margin = minDimension * .015; + centerX = (imageWidth - mapWidth - leftMargin) / 2 + leftMargin; + centerY = imageHeight / 2; + gRadius = minDimension / 2 - buffer; + //context.font = '11px sans-serif'; +} + +function handleResize() { + updateViewNeeded = true; +} + +function Attribute() { +} + +function SampleStats(sample, ictrl, sread, sclas, sfilt, scmin, scavg, scmax, + lnmin, lnavg, lnmax, tclas, tfilt, tfold) { + // Class to store the statistics of a sample + this.sample = sample; + this.is_ctrl = (ictrl === 'True'); + this.sread = sread; + this.sclas = sclas; + this.sfilt = sfilt; + this.scmin = scmin; + this.scavg = scavg; + this.scmax = scmax; + this.lnmin = lnmin; + this.lnavg = lnavg; + this.lnmax = lnmax; + this.tclas = tclas; + this.tfilt = tfilt; + this.tfold = tfold; +} + +function CanvasButton(name, x, y, w, h, fill) { + // Constructor for a button in the canvas + this.name = name; + this.x = x || 0; + this.y = y || 0; + this.w = w || 1; + this.h = h || 1; + this.fill = fill || '#000000'; + + // Draws the button to a given context + this.draw = function (ctx) { + var oldAlpha = ctx.globalAlpha + ctx.globalAlpha = 1; + ctx.strokeStyle = '#' + bkgBright; + ctx.lineWidth = 3; + ctx.strokeRect(this.x, this.y, this.w, this.h); + ctx.fillStyle = this.fill; + ctx.fillRect(this.x, this.y, this.w, this.h); + ctx.strokeStyle = '#000000'; + ctx.lineWidth = 0.5; + ctx.strokeRect(this.x, this.y, this.w, this.h); + // Draws symbols in buttons + ctx.fillStyle = '#000000'; + ctx.globalAlpha = 0.7; + switch (this.name) { + case 'mostScore': + ctx.beginPath(); + ctx.moveTo(this.x + 1 * this.w / 2, this.y + this.h / 8); + ctx.lineTo(this.x + 1 * this.w / 6, this.y + this.h / 2); + ctx.lineTo(this.x + 5 * this.w / 6, this.y + this.h / 2); + ctx.fill(); + case 'moreScore': + ctx.beginPath(); + ctx.moveTo(this.x + 1 * this.w / 2, this.y + 1 * this.h / 4); + ctx.lineTo(this.x + 1 * this.w / 6, this.y + 3 * this.h / 4); + ctx.lineTo(this.x + 5 * this.w / 6, this.y + 3 * this.h / 4); + ctx.fill(); + break; + case 'lestScore': + ctx.beginPath(); + ctx.moveTo(this.x + 1 * this.w / 2, this.y + 7 * this.h / 8); + ctx.lineTo(this.x + 1 * this.w / 6, this.y + 1 * this.h / 2); + ctx.lineTo(this.x + 5 * this.w / 6, this.y + 1 * this.h / 2); + ctx.fill(); + case 'lessScore': + ctx.beginPath(); + ctx.moveTo(this.x + 1 * this.w / 2, this.y + 3 * this.h / 4); + ctx.lineTo(this.x + 1 * this.w / 6, this.y + 1 * this.h / 4); + ctx.lineTo(this.x + 5 * this.w / 6, this.y + 1 * this.h / 4); + ctx.fill(); + break; + } + ctx.globalAlpha = oldAlpha + }; + + // Determine if a point is inside the button's bounds + this.is_inside = function (mx, my) { + // Check the Mouse X,Y fall in the button's area + return (this.x <= mx) && (this.x + this.w >= mx) && + (this.y <= my) && (this.y + this.h >= my); + } +} + +function Tween(start, end) { + this.start = start; + this.end = end; + this.current = this.start; + + this.current = function () { + if (progress == 1 || this.start == this.end) { + return this.end; + } + else { + return this.start + tweenFactor * (this.end - this.start); + } + }; + + this.setTarget = function (target) { + this.start = this.current(); + this.end = target; + } +} + +function Node() { + this.id = currentNodeID; + currentNodeID++; + nodes[this.id] = this; + + this.angleStart = new Tween(Math.PI, 0); + this.angleEnd = new Tween(Math.PI, 0); + this.radiusInner = new Tween(1, 1); + this.labelRadius = new Tween(1, 1); + this.labelWidth = new Tween(0, 0); + this.scale = new Tween(1, 1); // TEMP + this.radiusOuter = new Tween(1, 1); + + this.r = new Tween(255, 255); + this.g = new Tween(255, 255); + this.b = new Tween(255, 255); + + this.alphaLabel = new Tween(0, 1); + this.alphaLine = new Tween(0, 1); + this.alphaArc = new Tween(0, 0); + this.alphaWedge = new Tween(0, 1); + this.alphaOther = new Tween(0, 1); + this.alphaPattern = new Tween(0, 0); + this.children = Array(); + this.parent = 0; + + this.attributes = new Array(attributes.length); + + this.addChild = function (child) { + this.children.push(child); + }; + + this.addLabelNode = function (depth, labelOffset) { + if (labelHeadNodes[depth][labelOffset] == 0) { + // this will become the head node for this list + + labelHeadNodes[depth][labelOffset] = this; + this.labelPrev = this; + } + + var head = labelHeadNodes[depth][labelOffset]; + + this.labelNext = head; + this.labelPrev = head.labelPrev; + head.labelPrev.labelNext = this; + head.labelPrev = this; + } + + this.canDisplayDepth = function () { + // whether this node is at a depth that can be displayed, according + // to the max absolute depth + + return this.depth <= maxAbsoluteDepth; + } + + this.canDisplayHistory = function () { + var radiusInner; + + if (compress) { + radiusInner = compressedRadii[0]; + } + else { + radiusInner = nodeRadius; + } + + return ( + -this.labelRadius.end * gRadius + + historySpacingFactor * fontSize / 2 < + radiusInner * gRadius + ); + } + + this.canDisplayLabelCurrent = function () { + return ( + (this.angleEnd.current() - this.angleStart.current()) * + (this.radiusInner.current() * gRadius + gRadius) >= + minWidth()); + } + + this.checkHighlight = function () { + if (this.children.length == 0 && this == focusNode) { + //return false; + } + + if (this.hide) { + return false; + } + + if (this.radiusInner.end == 1) { + // compressed to the outside; don't check + + return false; + } + + var highlighted = false; + + var angleStartCurrent = this.angleStart.current() + rotationOffset; + var angleEndCurrent = this.angleEnd.current() + rotationOffset; + var radiusInner = this.radiusInner.current() * gRadius; + + for (var i = 0; i < this.children.length; i++) { + highlighted = this.children[i].checkHighlight(); + + if (highlighted) { + return true; + } + } + + if (this.radial) { + var angleText = (angleStartCurrent + angleEndCurrent) / 2; + var radiusText = (gRadius + radiusInner) / 2; + + context.rotate(angleText); + context.beginPath(); + context.moveTo(radiusText, -fontSize); + context.lineTo(radiusText, fontSize); + context.lineTo(radiusText + centerX, fontSize); + context.lineTo(radiusText + centerX, -fontSize); + context.closePath(); + context.rotate(-angleText); + + if (context.isPointInPath(mouseXRel, mouseYRel)) { + var label = String(this.getPercentage()) + '%' + ' ' + + this.name; + + if (this.searchResultChildren()) { + label += searchResultString(this.searchResultChildren()); + } + + if + ( + Math.sqrt((mouseXRel) * (mouseXRel) + + (mouseYRel) * (mouseYRel)) / backingScale() < + radiusText + measureText(label) + ) { + highlighted = true; + } + } + } + else { + for (var i = 0; i < this.hiddenLabels.length; i++) { + var hiddenLabel = this.hiddenLabels[i]; + + context.rotate(hiddenLabel.angle); + context.beginPath(); + context.moveTo(gRadius, -fontSize); + context.lineTo(gRadius, fontSize); + context.lineTo(gRadius + centerX, fontSize); + context.lineTo(gRadius + centerX, -fontSize); + context.closePath(); + context.rotate(-hiddenLabel.angle); + + if (context.isPointInPath(mouseXRel, mouseYRel)) { + var label = String(hiddenLabel.value) + ' more'; + + if (hiddenLabel.search) { + label += searchResultString(hiddenLabel.search); + } + + if + ( + Math.sqrt((mouseXRel) * (mouseXRel) + + (mouseYRel) * (mouseYRel)) / backingScale() < + gRadius + fontSize + measureText(label) + ) { + highlighted = true; + break; + } + } + } + } + + if (!highlighted && this != selectedNode && !this.getCollapse()) { + context.beginPath(); + context.arc(0, 0, radiusInner, angleStartCurrent, angleEndCurrent, + false); + context.arc(0, 0, gRadius, angleEndCurrent, angleStartCurrent, + true); + context.closePath(); + + if (context.isPointInPath(mouseXRel, mouseYRel)) { + highlighted = true; + } + + if + ( + !highlighted && + (angleEndCurrent - angleStartCurrent) * + (radiusInner + gRadius) < + minWidth() && + this.getDepth() == selectedNode.getDepth() + 1 + ) { + if (showKeys && this.checkHighlightKey()) { + highlighted = true; + } + } + } + + if (highlighted) { + if (this != highlightedNode) { + // document.body.style.cursor='pointer'; + } + + highlightedNode = this; + } + + return highlighted; + } + + this.checkHighlightCenter = function () { + if (!this.canDisplayHistory()) { + return; + } + + var cx = centerX; + var cy = centerY - this.labelRadius.end * gRadius; + //var dim = context.measureText(this.name); + + var width = this.nameWidth; + + if (this.searchResultChildren()) { + var results = searchResultString(this.searchResultChildren()); + var dim = context.measureText(results); + width += dim.width; + } + + if + ( + mouseX > cx - width / 2 && + mouseX < cx + width / 2 && + mouseY > cy - historySpacingFactor * fontSize / 2 && + mouseY < cy + historySpacingFactor * fontSize / 2 + ) { + highlightedNode = this; + return; + } + + if (this.getParent()) { + this.getParent().checkHighlightCenter(); + } + } + + this.checkHighlightKey = function () { + var offset = keyOffset(); + + var xMin = imageWidth - keySize - margin - this.keyNameWidth + - keyBuffer; + var xMax = imageWidth - margin; + var yMin = offset; + var yMax = offset + keySize; + + currentKey++; + + return ( + mouseX > xMin && + mouseX < xMax && + mouseY > yMin && + mouseY < yMax); + } + + this.checkHighlightMap = function () { + if (this.parent) { + this.parent.checkHighlightMap(); + } + + if (this.getCollapse() || this == focusNode) { + return; + } + + var box = this.getMapPosition(); + + if + ( + mouseX > box.x - mapRadius && + mouseX < box.x + mapRadius && + mouseY > box.y - mapRadius && + mouseY < box.y + mapRadius + ) { + highlightedNode = this; + } + } + + /* this.collapse = function() + { + for (var i = 0; i < this.children.length; i++ ) + { + this.children[i] = this.children[i].collapse(); + } + + if + ( + this.children.length == 1 && + this.children[0].magnitude == this.magnitude + ) + { + this.children[0].parent = this.parent; + this.children[0].getDepth() = this.parent.getDepth() + 1; + return this.children[0]; + } + else + { + return this; + } + } +*/ + this.draw = function (labelMode, selected, searchHighlighted) { + var depth = this.getDepth() - selectedNode.getDepth() + 1; +// var hidden = false; + + if (selectedNode == this) { + selected = true; + } + + var angleStartCurrent = this.angleStart.current() + rotationOffset; + var angleEndCurrent = this.angleEnd.current() + rotationOffset; + var radiusInner = this.radiusInner.current() * gRadius; + var canDisplayLabelCurrent = this.canDisplayLabelCurrent(); + var hiddenSearchResults = false; + + /* if ( ! this.hide ) + { + for ( var i = 0; i < this.children.length; i++ ) + { + if ( this.children[i].hide && this.children[i].searchResults ) + { + hiddenSearchResults = true; + } + } + } +*/ + var drawChildren = + (!this.hide || !this.hidePrev && progress < 1) && + (!this.hideAlone || !this.hideAlonePrev && progress < 1); + +// if ( this.alphaWedge.current() > 0 || this.alphaLabel.current() > 0 ) + { + var lastChildAngleEnd = angleStartCurrent; + + if (this.hasChildren())//canDisplayChildren ) + { + lastChildAngleEnd = + this.children[this.children.length - 1].angleEnd.current() + + rotationOffset; + } + + if (labelMode) { + var drawRadial = + !( + this.parent && + this.parent != selectedNode && + angleEndCurrent == this.parent.angleEnd.current() + + rotationOffset + ); + + //if ( angleStartCurrent != angleEndCurrent ) + { + this.drawLines(angleStartCurrent, angleEndCurrent, + radiusInner, drawRadial, selected); + } + + var alphaOtherCurrent = this.alphaOther.current(); + var childRadiusInner; + + if (this == selectedNode || alphaOtherCurrent) { + childRadiusInner = + this.children.length ? + this.children[this.children.length + - 1].radiusInner.current() * gRadius + : radiusInner + } + + if (this == selectedNode) { + this.drawReferenceRings(childRadiusInner); + } + + if + ( + selected && + !searchHighlighted && + this != selectedNode && + ( + this.isSearchResult || + this.hideAlone && this.searchResultChildren() || + false +// this.hide && +// this.containsSearchResult + ) + ) { + context.globalAlpha = this.alphaWedge.current(); + + drawWedge + ( + angleStartCurrent, + angleEndCurrent, + radiusInner, + gRadius, + highlightFill, + 0, + true + ); + + if + ( + this.keyed && + !showKeys && + this.searchResults && + !searchHighlighted && + this != highlightedNode && + this != focusNode + ) { + var angle = (angleEndCurrent + angleStartCurrent) / 2; + this.drawLabel(angle, true, false, true, true); + } + + //this.drawHighlight(false); + searchHighlighted = true; + } + + if + ( + this == selectedNode || + // true + //(canDisplayLabelCurrent) && + this != highlightedNode && + this != focusNode + ) { + if (this.radial != this.radialPrev + && this.alphaLabel.end == 1) { + context.globalAlpha = tweenFactor; + } + else { + context.globalAlpha = this.alphaLabel.current(); + } + + this.drawLabel + ( + (angleStartCurrent + angleEndCurrent) / 2, + this.hideAlone && this.searchResultChildren() || + (this.isSearchResult || hiddenSearchResults) && selected, + this == selectedNode && !this.radial, + selected, + this.radial + ); + + if (this.radial != this.radialPrev + && this.alphaLabel.start == 1 && progress < 1) { + context.globalAlpha = 1 - tweenFactor; + + this.drawLabel + ( + (angleStartCurrent + angleEndCurrent) / 2, + (this.isSearchResult || hiddenSearchResults) + && selected, + this == selectedNodeLast && !this.radialPrev, + selected, + this.radialPrev + ); + } + } + + if + ( + alphaOtherCurrent && + lastChildAngleEnd != null + ) { + if + ( + (angleEndCurrent - lastChildAngleEnd) * + (childRadiusInner + gRadius) >= + minWidth() + ) { + //context.font = fontNormal; + context.globalAlpha = this.alphaOther.current(); + + drawTextPolar + ( + this.getUnclassifiedText(), + this.getUnclassifiedPercentage(), + (lastChildAngleEnd + angleEndCurrent) / 2, + (childRadiusInner + gRadius) / 2, + true, + false, + false, + 0, + 0 + ); + } + } + + if (this == selectedNode && this.keyUnclassified && showKeys) { + this.drawKey + ( + (lastChildAngleEnd + angleEndCurrent) / 2, + false, + false + ); + } + } + else { + var alphaWedgeCurrent = this.alphaWedge.current(); + + if (alphaWedgeCurrent || this.alphaOther.current()) { + var currentR = this.r.current(); + var currentG = this.g.current(); + var currentB = this.b.current(); + + var fill = rgbText(currentR, currentG, currentB); + + var radiusOuter; + var lastChildAngle; + var truncateWedge = + ( + (this.hasChildren() || this == selectedNode) && + !this.keyed && + (compress || depth < maxDisplayDepth) && + drawChildren + ); + + if (truncateWedge) { + radiusOuter = this.children.length + ? this.children[0].radiusInner.current() + * gRadius : radiusInner; + } + else { + radiusOuter = gRadius; + } + /* + if ( this.hasChildren() ) + { + radiusOuter = this.children[0].getUncollapsed().radiusInner.current() * gRadius + 1; + } + else + { // TEMP + radiusOuter = radiusInner + nodeRadius * gRadius; + + if ( radiusOuter > gRadius ) + { + radiusOuter = gRadius; + } + } + */ + context.globalAlpha = alphaWedgeCurrent; + + if (radiusInner != radiusOuter || truncateWedge) { + drawWedge + ( + angleStartCurrent, + angleEndCurrent, + radiusInner, + radiusOuter,//this.radiusOuter.current() * gRadius, + //'rgba(0, 200, 0, .1)', + fill, + this.alphaPattern.current() + ); + + if (truncateWedge) { + // fill in the extra space if the sum of our + // childrens' magnitudes is less than ours + + if (lastChildAngleEnd < angleEndCurrent) + //&& false) // TEMP + { + if (radiusOuter > 1) { + // overlap slightly to hide the seam + + // radiusOuter -= 1; + } + + if (alphaWedgeCurrent < 1) { + context.globalAlpha + = this.alphaOther.current(); + drawWedge + ( + lastChildAngleEnd, + angleEndCurrent, + radiusOuter, + gRadius, + colorUnclassified, + 0 + ); + context.globalAlpha = alphaWedgeCurrent; + } + + drawWedge + ( + lastChildAngleEnd, + angleEndCurrent, + radiusOuter, + gRadius, + //this.radiusOuter.current() * gRadius, + //'rgba(200, 0, 0, .1)', + fill, + this.alphaPattern.current() + ); + } + } + + if (radiusOuter < gRadius) { + // patch up the seam + // + context.beginPath(); + context.arc(0, 0, radiusOuter, + angleStartCurrent/*lastChildAngleEnd*/, + angleEndCurrent, false); + context.strokeStyle = fill; + context.lineWidth = 1; + context.stroke(); + } + } + + if (this.keyed && selected && showKeys) + //&& progress == 1 ) + { + this.drawKey + ( + (angleStartCurrent + angleEndCurrent) / 2, + ( + this == highlightedNode || + this == focusNode || + this.searchResults + ), + this == highlightedNode || this == focusNode + ); + } + } + } + } + + this.hiddenLabels = Array(); + + if (drawChildren) { + // draw children + // + for (var i = 0; i < this.children.length; i++) { + if (this.drawHiddenChildren(i, selected, labelMode, + searchHighlighted)) { + var childHiddenEnd = this.children[i].hiddenEnd; + if (childHiddenEnd > i) { // Avoid infinite loop + i = childHiddenEnd; + } + } + else { + this.children[i].draw(labelMode, selected, + searchHighlighted); + } + } + } + }; + + this.drawHiddenChildren = function + (firstHiddenChild, + selected, + labelMode, + searchHighlighted) { + var firstChild = this.children[firstHiddenChild]; + + if (firstChild.hiddenEnd == null + || firstChild.radiusInner.current() == 1) { + return false; + } + + for (var i = firstHiddenChild; i < firstChild.hiddenEnd; i++) { + if (!this.children[i].hide + || !this.children[i].hidePrev && progress < 1) { + return false; + } + } + + var angleStart = firstChild.angleStart.current() + rotationOffset; + var lastChild = this.children[firstChild.hiddenEnd]; + var angleEnd = lastChild.angleEnd.current() + rotationOffset; + var radiusInner = gRadius * firstChild.radiusInner.current(); + var hiddenChildren = firstChild.hiddenEnd - firstHiddenChild + 1; + + if (labelMode) { + var hiddenSearchResults = 0; + + for (var i = firstHiddenChild; i <= firstChild.hiddenEnd; i++) { + hiddenSearchResults += this.children[i].searchResults; + + if (this.children[i].magnitude == 0) { + hiddenChildren--; + } + } + + if + ( + selected && + (angleEnd - angleStart) * + (gRadius + gRadius) >= + minWidth() || + this == highlightedNode && + hiddenChildren || + hiddenSearchResults + ) { + context.globalAlpha = this.alphaWedge.current(); + + this.drawHiddenLabel + ( + angleStart, + angleEnd, + hiddenChildren, + hiddenSearchResults + ); + } + } + + var drawWedges = true; + + for (var i = firstHiddenChild; i <= firstChild.hiddenEnd; i++) { + // all hidden children must be completely hidden to draw together + + if (this.children[i].alphaPattern.current() + != this.children[i].alphaWedge.current()) { + drawWedges = false; + break; + } + } + + if (labelMode) { + if (drawWedges) { + var drawRadial = (angleEnd + < this.angleEnd.current() + rotationOffset); + this.drawLines(angleStart, angleEnd, radiusInner, drawRadial); + } + + if (hiddenSearchResults && !searchHighlighted) { + drawWedge + ( + angleStart, + angleEnd, + radiusInner, + gRadius,//this.radiusOuter.current() * gRadius, + highlightFill, + 0, + true + ); + } + } + else if (drawWedges) { + context.globalAlpha = this.alphaWedge.current(); + + var fill = rgbText + ( + firstChild.r.current(), + firstChild.g.current(), + firstChild.b.current() + ); + + drawWedge + ( + angleStart, + angleEnd, + radiusInner, + gRadius,//this.radiusOuter.current() * gRadius, + fill, + context.globalAlpha, + false + ); + } + + return drawWedges; + } + + this.drawHiddenLabel = function (angleStart, angleEnd, value, + hiddenSearchResults) { + var textAngle = (angleStart + angleEnd) / 2; + var labelRadius = gRadius + fontSize;//(radiusInner + radius) / 2; + + var hiddenLabel = Array(); + + hiddenLabel.value = value; + hiddenLabel.angle = textAngle; + hiddenLabel.search = hiddenSearchResults; + + this.hiddenLabels.push(hiddenLabel); + + drawTick(gRadius - fontSize * .75, fontSize * 1.5, textAngle); + drawTextPolar + ( + value.toString() + ' more', + 0, // inner text + textAngle, + labelRadius, + true, // radial + hiddenSearchResults, // bubble + this == highlightedNode || this == focusNode, // bold + false, + hiddenSearchResults + ); + } + + this.drawHighlight = function (bold) { + var angleStartCurrent = this.angleStart.current() + rotationOffset; + var angleEndCurrent = this.angleEnd.current() + rotationOffset; + var radiusInner = this.radiusInner.current() * gRadius; + + //this.setHighlightStyle(); + + if (this == focusNode && this + == highlightedNode && this.hasChildren()) { +// context.fillStyle = "rgba(255, 255, 255, .3)"; + arrow + ( + angleStartCurrent, + angleEndCurrent, + radiusInner + ); + } + else { + drawWedge + ( + angleStartCurrent, + angleEndCurrent, + radiusInner, + gRadius, + highlightFill, + 0, + true + ); + } + + // check if hidden children should be highlighted + // + for (var i = 0; i < this.children.length; i++) { + if + ( + this.children[i].getDepth() - selectedNode.getDepth() + 1 <= + maxDisplayDepth && + this.children[i].hiddenEnd != null + ) { + var firstChild = this.children[i]; + var lastChild = this.children[firstChild.hiddenEnd]; + var hiddenAngleStart = firstChild.angleStart.current() + + rotationOffset; + var hiddenAngleEnd = lastChild.angleEnd.current() + + rotationOffset; + var hiddenRadiusInner = gRadius + * firstChild.radiusInner.current(); + + drawWedge + ( + hiddenAngleStart, + hiddenAngleEnd, + hiddenRadiusInner, + gRadius, + 'rgba(255, 255, 255, .3)', + 0, + true + ); + + if (false && !this.searchResults) { + this.drawHiddenLabel + ( + hiddenAngleStart, + hiddenAngleEnd, + firstChild.hiddenEnd - i + 1 + ); + } + + i = firstChild.hiddenEnd; + } + } + +// context.strokeStyle = 'black'; + context.fillStyle = 'black'; + + var highlight = !(progress < 1 && zoomOut + && this == selectedNodeLast); + + var angle = (angleEndCurrent + angleStartCurrent) / 2; + + if (!(this.keyed && showKeys)) { + this.drawLabel(angle, true, bold, true, this.radial); + } + } + + this.drawHighlightCenter = function () { + if (!this.canDisplayHistory()) { + return; + } + + context.lineWidth = highlightLineWidth; + context.strokeStyle = 'black'; + context.fillStyle = "rgba(255, 255, 255, .6)"; + + context.fillStyle = 'black'; + this.drawLabel(3 * Math.PI / 2, true, true, false); + context.font = fontNormal; + } + + this.drawKey = function (angle, highlight, bold) { + var offset = keyOffset(); + var color; + var colorText = this.magnitude == 0 ? 'gray' : 'black'; + var patternAlpha = this.alphaPattern.end; + var boxLeft = imageWidth - keySize - margin; + var textY = offset + keySize / 2; + + var label; + var keyNameWidth; + + if (this == selectedNode) { + color = colorUnclassified; + label = + this.getUnclassifiedText() + + ' ' + + this.getUnclassifiedPercentage(); + keyNameWidth = measureText(label, false); + } + else { + label = this.keyLabel; + color = rgbText(this.r.end, this.g.end, this.b.end); + + if (highlight) { + if (this.searchResultChildren()) { + label = label + + searchResultString(this.searchResultChildren()); + } + + keyNameWidth = measureText(label, bold); + } + else { + keyNameWidth = this.keyNameWidth; + } + } + + var textLeft = boxLeft - keyBuffer - keyNameWidth - fontSize / 2; + var labelLeft = textLeft; + + if (labelLeft > keyMinTextLeft - fontSize / 2) { + keyMinTextLeft -= fontSize / 2; + + if (keyMinTextLeft < centerX - gRadius + fontSize / 2) { + keyMinTextLeft = centerX - gRadius + fontSize / 2; + } + + labelLeft = keyMinTextLeft; + } + + var lineX = new Array(); + var lineY = new Array(); + + var bendRadius; + var keyAngle = Math.atan((textY - centerY) / (labelLeft - centerX)); + var arcAngle; + + if (keyAngle < 0) { + keyAngle += Math.PI; + } + + if (keyMinAngle == 0 || angle < keyMinAngle) { + keyMinAngle = angle; + } + + if (angle > Math.PI && keyMinAngle > Math.PI) { + // allow lines to come underneath the chart + + angle -= Math.PI * 2; + } + + lineX.push(Math.cos(angle) * gRadius); + lineY.push(Math.sin(angle) * gRadius); + + if (angle < keyAngle + && textY > centerY + + Math.sin(angle) * (gRadius + buffer * (currentKey - 1) + / (keys + 1) / 2 + buffer / 2)) { + bendRadius = gRadius + buffer - buffer * currentKey + / (keys + 1) / 2; + } + else { + bendRadius = gRadius + buffer * currentKey + / (keys + 1) / 2 + buffer / 2; + } + + var outside = + Math.sqrt + ( + Math.pow(labelLeft - centerX, 2) + + Math.pow(textY - centerY, 2) + ) > bendRadius; + + if (!outside) { + arcAngle = Math.asin((textY - centerY) / bendRadius); + + keyMinTextLeft = min(keyMinTextLeft, centerX + + bendRadius * Math.cos(arcAngle) - fontSize / 2); + + if (labelLeft < textLeft && textLeft > centerX + + bendRadius * Math.cos(arcAngle)) { + lineX.push(textLeft - centerX); + lineY.push(textY - centerY); + } + } + else { + keyMinTextLeft = min(keyMinTextLeft, labelLeft - fontSize / 2); + + if (angle < keyAngle) { + // flip everything over y = x + // + arcAngle = Math.PI / 2 - keyLineAngle + ( + Math.PI / 2 - angle, + Math.PI / 2 - keyAngle, + bendRadius, + textY - centerY, + labelLeft - centerX, + lineY, + lineX + ); + + } + else { + arcAngle = keyLineAngle + ( + angle, + keyAngle, + bendRadius, + labelLeft - centerX, + textY - centerY, + lineX, + lineY + ); + } + } + + if (labelLeft > centerX + bendRadius * Math.cos(arcAngle) || + textY > centerY + bendRadius * Math.sin(arcAngle) + .01) +// if ( outside || ) + { + lineX.push(labelLeft - centerX); + lineY.push(textY - centerY); + + if (textLeft != labelLeft) { + lineX.push(textLeft - centerX); + lineY.push(textY - centerY); + } + } + + context.globalAlpha = this.alphaWedge.current(); + + if (snapshotMode) { + var labelSVG; + + if (this == selectedNode) { + labelSVG = + this.getUnclassifiedText() + + spacer() + + this.getUnclassifiedPercentage(); + } + else { + labelSVG = this.name + spacer() + this.getPercentage() + '%'; + } + + svg += + '<rect fill="' + color + '" ' + + 'x="' + boxLeft + '" y="' + offset + + '" width="' + keySize + '" height="' + keySize + '"/>'; + + if (patternAlpha) { + svg += + '<rect fill="url(#hiddenPattern)" style="stroke:none" ' + + 'x="' + boxLeft + '" y="' + offset + + '" width="' + keySize + '" height="' + keySize + '"/>'; + } + + svg += + '<path class="line' + + (highlight ? ' highlight' : '') + + '" d="M ' + (lineX[0] + centerX) + ',' + + (lineY[0] + centerY); + + if (angle != arcAngle) { + svg += + ' L ' + (centerX + bendRadius * Math.cos(angle)) + ',' + + (centerY + bendRadius * Math.sin(angle)) + + ' A ' + bendRadius + ',' + bendRadius + ' 0 ' + + '0,' + (angle > arcAngle ? '0' : '1') + ' ' + + (centerX + bendRadius * Math.cos(arcAngle)) + ',' + + (centerY + bendRadius * Math.sin(arcAngle)); + } + + for (var i = 1; i < lineX.length; i++) { + svg += + ' L ' + (centerX + lineX[i]) + ',' + + (centerY + lineY[i]); + } + + svg += '"/>'; + + if (highlight) { + if (this.searchResultChildren()) { + labelSVG = labelSVG + + searchResultString(this.searchResultChildren()); + } + + drawBubbleSVG + ( + boxLeft - keyBuffer - keyNameWidth - fontSize / 2, + textY - fontSize, + keyNameWidth + fontSize, + fontSize * 2, + fontSize, + 0 + ); + + if (this.isSearchResult) { + drawSearchHighlights + ( + label, + boxLeft - keyBuffer - keyNameWidth, + textY, + 0 + ) + } + } + + svg += svgText(labelSVG, boxLeft - keyBuffer, textY, 'end', bold, + colorText); + } + else { + context.fillStyle = color; + context.translate(-centerX, -centerY); + context.strokeStyle = 'black'; + context.globalAlpha = 1;//this.alphaWedge.current(); + + context.fillRect(boxLeft, offset, keySize, keySize); + + if (patternAlpha) { + context.globalAlpha = patternAlpha; + context.fillStyle = hiddenPattern; + + // make clipping box for Firefox performance + context.beginPath(); + context.moveTo(boxLeft, offset); + context.lineTo(boxLeft + keySize, offset); + context.lineTo(boxLeft + keySize, offset + keySize); + context.lineTo(boxLeft, offset + keySize); + context.closePath(); + context.save(); + context.clip(); + + context.fillRect(boxLeft, offset, keySize, keySize); + context.fillRect(boxLeft, offset, keySize, keySize); + + context.restore(); // remove clipping region + } + + if (highlight) { + this.setHighlightStyle(); + context.fillRect(boxLeft, offset, keySize, keySize); + } + else { + context.lineWidth = thinLineWidth; + } + + context.strokeRect(boxLeft, offset, keySize, keySize); + + if (lineX.length) { + context.beginPath(); + context.moveTo(lineX[0] + centerX, lineY[0] + centerY); + + context.arc(centerX, centerY, bendRadius, angle, arcAngle, + angle > arcAngle); + + for (var i = 1; i < lineX.length; i++) { + context.lineTo(lineX[i] + centerX, lineY[i] + centerY); + } + + context.globalAlpha = this == selectedNode ? + this.children[0].alphaWedge.current() : + this.alphaWedge.current(); + context.lineWidth = highlight + ? highlightLineWidth : thinLineWidth; + context.stroke(); + context.globalAlpha = 1; + } + + if (highlight) { + drawBubbleCanvas + ( + boxLeft - keyBuffer - keyNameWidth - fontSize / 2, + textY - fontSize, + keyNameWidth + fontSize, + fontSize * 2, + fontSize, + 0 + ); + + if (this.isSearchResult) { + drawSearchHighlights + ( + label, + boxLeft - keyBuffer - keyNameWidth, + textY, + 0 + ) + } + } + + drawText(label, boxLeft - keyBuffer, offset + keySize / 2, 0, + 'end', bold, colorText); + + context.translate(centerX, centerY); + } + + currentKey++; + } + + this.drawLabel = function (angle, bubble, bold, selected, radial) { + if (context.globalAlpha == 0) { + return; + } + + var innerText; + var label; + var radius; + + if (radial) { + radius = (this.radiusInner.current() + 1) * gRadius / 2; + } + else { + radius = this.labelRadius.current() * gRadius; + } + + if (radial && (selected || bubble)) { + var percentage = this.getPercentage(); + innerText = percentage + '%'; + } + + if + ( + !radial && + this != selectedNode && + !bubble && + (!zoomOut || this != selectedNodeLast) + ) { + label = this.shortenLabel(); + } + else { + label = this.name; + } + + var flipped = drawTextPolar + ( + label, + innerText, + angle, + radius, + radial, + bubble, + bold, +// this.isSearchResult && this.shouldAddSearchResultsString() && (!selected || this == selectedNode || highlight), + this.isSearchResult + && (!selected || this == selectedNode || bubble), + (this.hideAlone || !selected || this == selectedNode) + ? this.searchResultChildren() : 0 + ); + + var depth = this.getDepth() - selectedNode.getDepth() + 1; + + if + ( + !radial && + !bubble && + this != selectedNode && + this.angleEnd.end != this.angleStart.end && + nLabelOffsets[depth - 2] > 2 && + this.labelWidth.current() + > (this.angleEnd.end - this.angleStart.end) * Math.abs(radius) && + !(zoomOut && this == selectedNodeLast) && + this.labelRadius.end > 0 + ) { + // name extends beyond wedge; draw tick mark towards the central + // radius for easier identification + + var radiusCenter = compress ? + (compressedRadii[depth - 1] + compressedRadii[depth - 2]) / 2 : + (depth - .5) * nodeRadius; + + if (this.labelRadius.end > radiusCenter) { + if (flipped) { + drawTick(radius - tickLength * 1.4, tickLength, angle); + } + else { + drawTick(radius - tickLength * 1.7, tickLength, angle); + } + } + else { + if (flipped) { + drawTick(radius + tickLength * .7, tickLength, angle); + } + else { + drawTick(radius + tickLength * .4, tickLength, angle); + } + } + } + } + + this.drawLines = function (angleStart, angleEnd, radiusInner, drawRadial, + selected) { + if (snapshotMode) { + if (this != selectedNode) { + if (angleEnd == angleStart + Math.PI * 2) { + // fudge to prevent overlap, which causes arc ambiguity + // + angleEnd -= .1 / gRadius; + } + + var longArc = angleEnd - angleStart > Math.PI ? 1 : 0; + + var x1 = centerX + radiusInner * Math.cos(angleStart); + var y1 = centerY + radiusInner * Math.sin(angleStart); + + var x2 = centerX + gRadius * Math.cos(angleStart); + var y2 = centerY + gRadius * Math.sin(angleStart); + + var x3 = centerX + gRadius * Math.cos(angleEnd); + var y3 = centerY + gRadius * Math.sin(angleEnd); + + var x4 = centerX + radiusInner * Math.cos(angleEnd); + var y4 = centerY + radiusInner * Math.sin(angleEnd); + + if (this.alphaArc.end) { + var dArray = + [ + " M ", x4, ",", y4, + " A ", radiusInner, ",", radiusInner, " 0 ", + longArc, + " 0 ", x1, ",", y1 + ]; + + svg += '<path class="line" d="' + dArray.join('') + '"/>'; + } + + if (drawRadial && this.alphaLine.end) { + svg += '<line x1="' + x3 + '" y1="' + y3 + '" x2="' + x4 + + '" y2="' + y4 + '"/>'; + } + } + } + else { + context.lineWidth = thinLineWidth; + context.strokeStyle = 'black'; + context.beginPath(); + context.arc(0, 0, radiusInner, angleStart, angleEnd, false); + context.globalAlpha = this.alphaArc.current(); + context.stroke(); + + if (drawRadial) { + var x1 = radiusInner * Math.cos(angleEnd); + var y1 = radiusInner * Math.sin(angleEnd); + var x2 = gRadius * Math.cos(angleEnd); + var y2 = gRadius * Math.sin(angleEnd); + + context.beginPath(); + context.moveTo(x1, y1); + context.lineTo(x2, y2); + +// if ( this.getCollapse() )//( selected && this != selectedNode ) + { + context.globalAlpha = this.alphaLine.current(); + } + + context.stroke(); + } + } + } + + this.drawMap = function (child) { + if (this.parent) { + this.parent.drawMap(child); + } + + if (this.getCollapse() && this != child || this == focusNode) { + return; + } + + var angleStart = + (child.baseMagnitude - this.baseMagnitude) / this.magnitude + * Math.PI * 2 + rotationOffset; + var angleEnd = + (child.baseMagnitude - this.baseMagnitude + child.magnitude) / + this.magnitude * Math.PI * 2 + + rotationOffset; + + var box = this.getMapPosition(); + + context.save(); + context.fillStyle = 'black'; + context.textAlign = 'end'; + context.textBaseline = 'middle'; + + var textX = box.x - mapRadius - mapBuffer; + var percentage = getPercentage(child.magnitude / this.magnitude); + + var highlight = this == selectedNode || this == highlightedNode; + + if (highlight) { + context.font = fontBold; + } + else { + context.font = fontNormal; + } + + context.fillText(percentage + '% of', textX, box.y - mapRadius / 3); + context.fillText(this.name, textX, box.y + mapRadius / 3); + + if (highlight) { + context.font = fontNormal; + } + + if (this == highlightedNode && this != selectedNode) { + context.fillStyle = 'rgb(245, 245, 245)'; +// context.fillStyle = 'rgb(200, 200, 200)'; + } + else { + context.fillStyle = 'rgb(255, 255, 255)'; + } + + context.beginPath(); + context.arc(box.x, box.y, mapRadius, 0, Math.PI * 2, true); + context.closePath(); + context.fill(); + + if (this == selectedNode) { + context.lineWidth = 1; + context.fillStyle = 'rgb(100, 100, 100)'; + } + else { + if (this == highlightedNode) { + context.lineWidth = .2; + context.fillStyle = 'rgb(190, 190, 190)'; + } + else { + context.lineWidth = .2; + context.fillStyle = 'rgb(200, 200, 200)'; + } + } + + var maxDepth = this.getMaxDepth(); + + if (!compress && maxDepth > maxPossibleDepth + this.getDepth() - 1) { + maxDepth = maxPossibleDepth + this.getDepth() - 1; + } + + if (this.getDepth() < selectedNode.getDepth()) { + if (child.getDepth() - 1 >= maxDepth) { + maxDepth = child.getDepth(); + } + } + + var radiusInner; + + if (compress) { + radiusInner = 0; +// Math.atan(child.getDepth() - this.getDepth()) / +// Math.PI * 2 * .9; + } + else { + radiusInner = + (child.getDepth() - this.getDepth()) / + (maxDepth - this.getDepth() + 1); + } + + context.stroke(); + context.beginPath(); + + if (radiusInner == 0) { + context.moveTo(box.x, box.y); + } + else { + context.arc(box.x, box.y, mapRadius * radiusInner, angleEnd, + angleStart, true); + } + + context.arc(box.x, box.y, mapRadius, angleStart, angleEnd, false); + context.closePath(); + context.fill(); + + if (this == highlightedNode && this != selectedNode) { + context.lineWidth = 1; + context.stroke(); + } + + context.restore(); + } + + this.drawReferenceRings = function (childRadiusInner) { + if (snapshotMode) { + svg += + '<circle cx="' + centerX + '" cy="' + centerY + + '" r="' + childRadiusInner + '"/>'; + svg += + '<circle cx="' + centerX + '" cy="' + centerY + + '" r="' + gRadius + '"/>'; + } + else { + context.globalAlpha = 1 - this.alphaLine.current();//this.getUncollapsed().alphaLine.current(); + context.beginPath(); + context.arc(0, 0, childRadiusInner, 0, Math.PI * 2, false); + context.stroke(); + context.beginPath(); + context.arc(0, 0, gRadius, 0, Math.PI * 2, false); + context.stroke(); + } + } + + this.getCollapse = function () { + return ( + collapse && + this.collapse && + this.depth != maxAbsoluteDepth + ); + } + + this.getDepth = function () { + if (collapse) { + return this.depthCollapsed; + } + else { + return this.depth; + } + }; + + this.getHue = function () { + return this.hues[currentDataset]; + }; + + this.getMagnitude = function () { + return this.attributes[magnitudeIndex][currentDataset]; + }; + + this.getMapPosition = function () { + return { + x: (details.offsetLeft + details.clientWidth - mapRadius), + y: ((focusNode.getDepth() - this.getDepth()) * + (mapBuffer + mapRadius * 2) - mapRadius) + + details.clientHeight + details.offsetTop + }; + } + + this.getMaxDepth = function (limit) { + var max; + + if (collapse) { + return this.maxDepthCollapsed; + } + else { + if (this.maxDepth > maxAbsoluteDepth) { + return maxAbsoluteDepth; + } + else { + return this.maxDepth; + } + } + } + + this.getData = function (index, summary) { + var files = new Array(); + + if + ( + this.attributes[index] != null && + this.attributes[index][currentDataset] != null && + this.attributes[index][currentDataset] != '' + ) { + files.push + ( + document.location + + '.files/' + + this.attributes[index][currentDataset] + ); + } + + if (summary) { + for (var i = 0; i < this.children.length; i++) { + files = files.concat(this.children[i].getData(index, true)); + } + } + + return files; + } + + this.getList = function (index, summary) { + var list; + + if + ( + this.attributes[index] != null && + this.attributes[index][currentDataset] != null + ) { + list = this.attributes[index][currentDataset]; + } + else { + list = new Array(); + } + + if (summary) { + for (var i = 0; i < this.children.length; i++) { + list = list.concat(this.children[i].getList(index, true)); + } + } + + return list; + } + + this.getParent = function () { + // returns parent, accounting for collapsing or 0 if doesn't exist + + var parent = this.parent; + + while (parent != 0 && parent.getCollapse()) { + parent = parent.parent; + } + + return parent; + } + + this.getPercentage = function () { + return getPercentage(this.magnitude / selectedNode.magnitude); + } + + this.getUnclassifiedPercentage = function () { + if (this.children.length) { + var lastChild = this.children[this.children.length - 1]; + + return getPercentage + ( + ( + this.baseMagnitude + + this.magnitude - + lastChild.magnitude - + lastChild.baseMagnitude + ) / this.magnitude + ) + '%'; + } + else { + return '100%'; + } + } + + this.getUnclassifiedText = function () { + return '[other ' + this.name + ']'; + } + + this.getUncollapsed = function () { + // recurse through collapsed children until uncollapsed node is found + + if (this.getCollapse()) { + return this.children[0].getUncollapsed(); + } + else { + return this; + } + }; + + this.hasChildren = function () { + return this.depth < maxAbsoluteDepth && this.magnitude + && this.children.length; + }; + + this.hasParent = function (parent) { + if (this.parent) { + if (this.parent === parent) { + return true; + } + else { + return this.parent.hasParent(parent); + } + } + else { + return false; + } + }; + + this.isLeaf = function (_recursing) { + // Returns true/1 for a real leave, false/0 otherwise, counting the + // non-empty leaves downstream and checking for positive counts. + // Param _recursing is an internal auxiliar variable not to be used + var leaves = 0; + if (this.children.length) { // Node has children -> recurse + for (var i = 0; i < this.children.length; i++) { + leaves += this.children[i].isLeaf(true); + } + if (_recursing) { + return leaves ? leaves : +!!this.magnitude; + // If this has no leaves but has magnitude, this is a leaf. + // NOTE: +!!num is 0 for num=0 and is 1 otherwise + } else { + return !!this.magnitude && !leaves; + } + } else { // Node has not children + if (!this.magnitude) { + return 0; // Fake leaf (empty) + } else { + return 1; // This is true leaf + } + } + }; + + this.maxVisibleDepth = function (maxDepth) { + var childInnerRadius; + var depth = this.getDepth() - selectedNode.getDepth() + 1; + var currentMaxDepth = depth; + + if (this.hasChildren() && depth < maxDepth) { + var lastChild = this.children[this.children.length - 1]; + + if (lastChild.baseMagnitude + lastChild.magnitude < + this.baseMagnitude + this.magnitude) { + currentMaxDepth++; + } + + if (compress) { + childInnerRadius = compressedRadii[depth - 1]; + } + else { + childInnerRadius = (depth) / maxDepth; + } + + for (var i = 0; i < this.children.length; i++) { + if + (//true || + this.children[i].magnitude * + angleFactor * + (childInnerRadius + 1) * + gRadius >= + minWidth() + ) { + var childMaxDepth + = this.children[i].maxVisibleDepth(maxDepth); + + if (childMaxDepth > currentMaxDepth) { + currentMaxDepth = childMaxDepth; + } + } + } + } + + return currentMaxDepth; + } + + this.resetLabelWidth = function () { + var nameWidthOld = this.nameWidth; + + if (true || !this.radial)//&& fontSize != fontSizeLast ) + { + var dim = context.measureText(this.name); + this.nameWidth = dim.width; + } + + if (fontSize != fontSizeLast + && this.labelWidth.end == nameWidthOld * labelWidthFudge) { + // font size changed; adjust start of tween to match + + this.labelWidth.start = this.nameWidth * labelWidthFudge; + } + else { + this.labelWidth.start = this.labelWidth.current(); + } + + this.labelWidth.end = this.nameWidth * labelWidthFudge; + } + + this.restrictLabelWidth = function (width) { + if (width < this.labelWidth.end) { + this.labelWidth.end = width; + } + } + + this.search = function () { + this.isSearchResult = false; + this.searchResults = 0; + + if + ( + !this.getCollapse() && + search.value !== '' && + this.name.toLowerCase().indexOf(search.value.toLowerCase()) !== -1 + ) { + this.isSearchResult = true; + this.searchResults = 1; + nSearchResults++; + } + + for (var i = 0; i < this.children.length; i++) { + this.searchResults += this.children[i].search(); + } + + return this.searchResults; + } + + this.searchResultChildren = function () { + if (this.isSearchResult) { + return this.searchResults - 1; + } + else { + return this.searchResults; + } + } + + this.setDepth = function (depth, depthCollapsed) { + this.depth = depth; + this.depthCollapsed = depthCollapsed; + + if + ( + this.children.length === 1 && + // this.magnitude > 0 && + this.children[0].magnitude === this.magnitude && + (head.children.length > 1 || this.children[0].children.length) + ) { + this.collapse = true; + } + else { + this.collapse = false; + depthCollapsed++; + } + + for (var i = 0; i < this.children.length; i++) { + this.children[i].setDepth(depth + 1, depthCollapsed); + } + } + + this.setHighlightStyle = function () { + context.lineWidth = highlightLineWidth; + + if (this.hasChildren() || this !== focusNode + || this !== highlightedNode) { + context.strokeStyle = 'black'; + context.fillStyle = "rgba(255, 255, 255, .3)"; + } + else { + context.strokeStyle = 'rgb(90,90,90)'; + context.fillStyle = "rgba(155, 155, 155, .3)"; + } + } + + this.setLabelWidth = function (node) { + if (!shorten || this.radial) { + return; // don't need to set width + } + + if (node.hide) { + alert('wtf'); + return; + } + + var angle = (this.angleStart.end + this.angleEnd.end) / 2; + var a; // angle difference + + if (node == selectedNode) { + a = Math.abs(angle - node.angleOther); + } + else { + a = Math.abs(angle + - (node.angleStart.end + node.angleEnd.end) / 2); + } + + if (a == 0) { + return; + } + + if (a > Math.PI) { + a = 2 * Math.PI - a; + } + + if (node.radial || node == selectedNode) { + var nodeLabelRadius; + + if (node == selectedNode) { + // radial 'other' label + + nodeLabelRadius = (node.children[0].radiusInner.end + 1) / 2; + } + else { + nodeLabelRadius = (node.radiusInner.end + 1) / 2; + } + + if (a < Math.PI / 2) { + var r = this.labelRadius.end * gRadius + .5 * fontSize + var hypotenuse = r / Math.cos(a); + var opposite = r * Math.tan(a); + var fontRadius = .8 * fontSize; + + if + ( + nodeLabelRadius * gRadius < hypotenuse && + this.labelWidth.end / 2 + fontRadius > opposite + ) { + this.labelWidth.end = 2 * (opposite - fontRadius); + } + } + } + else if + ( + this.labelRadius.end == node.labelRadius.end && + a < Math.PI / 4 + ) { + // same radius with small angle; use circumferential approximation + + var dist = a * this.labelRadius.end * gRadius - fontSize + * (1 - a * 4 / Math.PI) * 1.3; + + if (this.labelWidth.end < dist) { + node.restrictLabelWidth((dist - this.labelWidth.end / 2) * 2); + } + else if (node.labelWidth.end < dist) { + this.restrictLabelWidth((dist - node.labelWidth.end / 2) * 2); + } + else { + // both labels reach halfway point; restrict both + + this.labelWidth.end = dist; + node.labelWidth.end = dist + } + } + else { + var r1 = this.labelRadius.end * gRadius; + var r2 = node.labelRadius.end * gRadius; + + // first adjust the radii to account for the height of the font + // by shifting them toward each other + // + var fontFudge = .35 * fontSize; + // + if (this.labelRadius.end < node.labelRadius.end) { + r1 += fontFudge; + r2 -= fontFudge; + } + else if (this.labelRadius.end > node.labelRadius.end) { + r1 -= fontFudge; + r2 += fontFudge; + } + else { + r1 -= fontFudge; + r2 -= fontFudge; + } + + var r1s = r1 * r1; + var r2s = r2 * r2; + + // distance between the centers of the two labels + // + var dist = Math.sqrt(r1s + r2s - 2 * r1 * r2 * Math.cos(a)); + + // angle at our label center between our radius and the line to the + // other label center + // + var b = Math.acos((r1s + dist * dist - r2s) / (2 * r1 * dist)); + + // distance from our label center to the intersection of the + // two tangents + // + var l1 = Math.sin(a + b - Math.PI / 2) * dist / Math.sin(Math.PI - a); + + // distance from other label center the the intersection of the + // two tangents + // + var l2 = Math.sin(Math.PI / 2 - b) * dist / Math.sin(Math.PI - a); + + l1 = Math.abs(l1) - .4 * fontSize; + l2 = Math.abs(l2) - .4 * fontSize; + /* + // amount to shorten the distances because of height of the font + // + var l3 = 0; + var fontRadius = fontSize * .55; + // + if ( l1 < 0 || l2 < 0 ) + { + var l4 = fontRadius / Math.tan(a); + l1 = Math.abs(l1); + l2 = Math.abs(l2); + + l1 -= l4; + l2 -= l4; + } + else + { + var c = Math.PI - a; + + l3 = fontRadius * Math.tan(c / 2); + } +*/ + if (this.labelWidth.end / 2 > l1 && node.labelWidth.end / 2 > l2) { + // shorten the farthest one from the intersection + + if (l1 > l2) { + this.restrictLabelWidth(2 * (l1));// - l3 - fontRadius)); + } + else { + node.restrictLabelWidth(2 * (l2));// - l3 - fontRadius)); + } + } + /* + else if ( this.labelWidth.end / 2 > l1 + l3 && node.labelWidth.end + / 2 > l2 - l3 ) + { + node.restrictLabelWidth(2 * (l2 - l3)); + } + else if ( this.labelWidth.end / 2 > l1 - l3 && node.labelWidth.end + / 2 > l2 + l3 ) + { + this.restrictLabelWidth(2 * (l1 - l3)); + }*/ + } + } + + this.setMagnitudes = function (baseMagnitude) { + this.magnitude = this.getMagnitude(); + this.baseMagnitude = baseMagnitude; + + for (var i = 0; i < this.children.length; i++) { + this.children[i].setMagnitudes(baseMagnitude); + baseMagnitude += this.children[i].magnitude; + } + + this.maxChildMagnitude = baseMagnitude; + } + + this.setMaxDepths = function () { + this.maxDepth = this.depth; + this.maxDepthCollapsed = this.depthCollapsed; + + for (i in this.children) { + var child = this.children[i]; + + child.setMaxDepths(); + + if (child.maxDepth > this.maxDepth) { + this.maxDepth = child.maxDepth; + } + + if + ( + child.maxDepthCollapsed > this.maxDepthCollapsed && + (child.depth <= maxAbsoluteDepth || maxAbsoluteDepth == 0) + ) { + this.maxDepthCollapsed = child.maxDepthCollapsed; + } + } + } + + this.setTargetLabelRadius = function () { + var depth = this.getDepth() - selectedNode.getDepth() + 1; + var index = depth - 2; + var labelOffset = labelOffsets[index]; + + if (this.radial) { + //this.labelRadius.setTarget((this.radiusInner.end + 1) / 2); + var max = + depth == maxDisplayDepth ? + 1 : + compressedRadii[index + 1]; + + this.labelRadius.setTarget((compressedRadii[index] + max) / 2); + } + else { + var radiusCenter; + var width; + + if (compress) { + if (nLabelOffsets[index] > 1) { + this.labelRadius.setTarget + ( + lerp + ( + labelOffset + .75, + 0, + nLabelOffsets[index] + .5, + compressedRadii[index], + compressedRadii[index + 1] + ) + ); + } + else { + this.labelRadius.setTarget((compressedRadii[index] + + compressedRadii[index + 1]) / 2); + } + } + else { + radiusCenter = + nodeRadius * (depth - 1) + + nodeRadius / 2; + width = nodeRadius; + + this.labelRadius.setTarget + ( + radiusCenter + width + * ((labelOffset + 1) / (nLabelOffsets[index] + 1) - .5) + ); + } + } + + if (!this.hide && !this.keyed && nLabelOffsets[index]) { + // check last and first labels in each track for overlap + + for (var i = 0; i < maxDisplayDepth - 1; i++) { + for (var j = 0; j <= nLabelOffsets[i]; j++) { + var last = labelLastNodes[i][j]; + var first = labelFirstNodes[i][j]; + + if (last) { + if (j == nLabelOffsets[i]) { + // last is radial + this.setLabelWidth(last); + } + else { + last.setLabelWidth(this); + } + } + + if (first) { + if (j == nLabelOffsets[i]) { + this.setLabelWidth(first); + } + else { + first.setLabelWidth(this); + } + } + } + } + + if (selectedNode.canDisplayLabelOther) { + // in case there is an 'other' label + this.setLabelWidth(selectedNode); + } + + if (this.radial) { + // use the last 'track' of this depth for radial + + labelLastNodes[index][nLabelOffsets[index]] = this; + + if (labelFirstNodes[index][nLabelOffsets[index]] == 0) { + labelFirstNodes[index][nLabelOffsets[index]] = this; + } + } + else { + labelLastNodes[index][labelOffset] = this; + + // update offset + + labelOffsets[index] += 1; + + if (labelOffsets[index] > nLabelOffsets[index]) { + labelOffsets[index] -= nLabelOffsets[index]; + + if (!(nLabelOffsets[index] & 1)) { + labelOffsets[index]--; + } + } + else if (labelOffsets[index] == nLabelOffsets[index]) { + labelOffsets[index] -= nLabelOffsets[index]; + + if (false && !(nLabelOffsets[index] & 1)) { + labelOffsets[index]++; + } + } + + if (labelFirstNodes[index][labelOffset] == 0) { + labelFirstNodes[index][labelOffset] = this; + } + } + } + else if (this.hide) { + this.labelWidth.end = 0; + } + } + + this.setTargets = function () { + if (this == selectedNode) { + this.setTargetsSelected + ( + 0, + 1, + lightnessBase, + false, + false + ); + return; + } + + var depthRelative = this.getDepth() - selectedNode.getDepth(); + + var parentOfSelected = selectedNode.hasParent(this); + /* ( +// ! this.getCollapse() && + this.baseMagnitude <= selectedNode.baseMagnitude && + this.baseMagnitude + this.magnitude >= + selectedNode.baseMagnitude + selectedNode.magnitude + ); +*/ + if (parentOfSelected) { + this.resetLabelWidth(); + } + else { + //context.font = fontNormal; + var dim = context.measureText(this.name); + this.nameWidth = dim.width; + //this.labelWidth.setTarget(this.labelWidth.end); + this.labelWidth.setTarget(0); + } + + // set angles + // + if (this.baseMagnitude <= selectedNode.baseMagnitude) { + this.angleStart.setTarget(0); + } + else { + this.angleStart.setTarget(Math.PI * 2); + } + // + if + ( + parentOfSelected || + this.baseMagnitude + this.magnitude >= + selectedNode.baseMagnitude + selectedNode.magnitude + ) { + this.angleEnd.setTarget(Math.PI * 2); + } + else { + this.angleEnd.setTarget(0); + } + + // children + // + for (var i = 0; i < this.children.length; i++) { + this.children[i].setTargets(); + } + + if (this.getDepth() <= selectedNode.getDepth()) { + // collapse in + + this.radiusInner.setTarget(0); + + if (parentOfSelected) { + this.labelRadius.setTarget + ( + (depthRelative) * + historySpacingFactor * fontSize / gRadius + ); + //this.scale.setTarget(1 - (selectedNode.getDepth() + // - this.getDepth()) / 18); // TEMP + } + else { + this.labelRadius.setTarget(0); + //this.scale.setTarget(1); // TEMP + } + } + else if (depthRelative + 1 > maxDisplayDepth) { + // collapse out + + this.radiusInner.setTarget(1); + this.labelRadius.setTarget(1); + //this.scale.setTarget(1); // TEMP + } + else { + // don't collapse + + if (compress) { + this.radiusInner.setTarget(compressedRadii[depthRelative - 1]); + } + else { + this.radiusInner.setTarget(nodeRadius * (depthRelative)); + } + + //this.scale.setTarget(1); // TEMP + + if (this == selectedNode) { + this.labelRadius.setTarget(0); + } + else { + if (compress) { + this.labelRadius.setTarget + ( + (compressedRadii[depthRelative - 1] + + compressedRadii[depthRelative]) / 2 + ); + } + else { + this.labelRadius.setTarget(nodeRadius * (depthRelative) + + nodeRadius / 2); + } + } + } + +// this.r.start = this.r.end; +// this.g.start = this.g.end; +// this.b.start = this.b.end; + + this.r.setTarget(255); + this.g.setTarget(255); + this.b.setTarget(255); + + this.alphaLine.setTarget(0); + this.alphaArc.setTarget(0); + this.alphaWedge.setTarget(0); + this.alphaPattern.setTarget(0); + this.alphaOther.setTarget(0); + + if (parentOfSelected && !this.getCollapse()) { + var alpha = + ( + 1 - + (selectedNode.getDepth() - this.getDepth()) / + (Math.floor((compress ? compressedRadii[0] : nodeRadius) + * gRadius / (historySpacingFactor * fontSize) - .5) + 1) + ); + + if (alpha < 0) { + alpha = 0; + } + + this.alphaLabel.setTarget(alpha); + this.radial = false; + } + else { + this.alphaLabel.setTarget(0); + } + + this.hideAlonePrev = this.hideAlone; + this.hidePrev = this.hide; + + if (parentOfSelected) { + this.hideAlone = false; + this.hide = false; + } + + if (this.getParent() == selectedNode.getParent()) { + this.hiddenEnd = null; + } + + this.radialPrev = this.radial; + } + + this.setTargetsSelected = function (hueMin, hueMax, lightness, hide, + nextSiblingHidden) { + var collapse = this.getCollapse(); + var depth = this.getDepth() - selectedNode.getDepth() + 1; + var canDisplayChildLabels = false; + var lastChild; + + if (this.hasChildren())//&& ! hide ) + { + lastChild = this.children[this.children.length - 1]; + this.hideAlone = true; + } + else { + this.hideAlone = false; + } + + // set child wedges + // + for (var i = 0; i < this.children.length; i++) { + this.children[i].setTargetWedge(); + + if + ( + !this.children[i].hide && + (collapse || depth < maxDisplayDepth) && + this.depth < maxAbsoluteDepth + ) { + canDisplayChildLabels = true; + this.hideAlone = false; + } + } + + if (this == selectedNode || lastChild && lastChild.angleEnd.end + < this.angleEnd.end - .01) { + this.hideAlone = false; + } + + if (this.hideAlonePrev == undefined) { + this.hideAlonePrev = this.hideAlone; + } + + if (this == selectedNode) { + var otherArc = + this.children.length ? + angleFactor * + ( + this.baseMagnitude + this.magnitude - + lastChild.baseMagnitude - lastChild.magnitude + ) + : this.baseMagnitude + this.magnitude; + this.canDisplayLabelOther = + this.children.length ? + otherArc * + (this.children[0].radiusInner.end + 1) * gRadius >= + minWidth() + : true; + + this.keyUnclassified = false; + + if (this.canDisplayLabelOther) { + this.angleOther = Math.PI * 2 - otherArc / 2; + } + else if (otherArc > 0.0000000001) { + this.keyUnclassified = true; + keys++; + } + + this.angleStart.setTarget(0); + this.angleEnd.setTarget(Math.PI * 2); + + if (this.children.length) { + this.radiusInner.setTarget(0); + } + else { + this.radiusInner.setTarget(compressedRadii[0]); + } + + this.hidePrev = this.hide; + this.hide = false; + this.hideAlonePrev = this.hideAlone; + this.hideAlone = false; + this.keyed = false; + } + + if (hueMax - hueMin > 1 / 12) { + hueMax = hueMin + 1 / 12; + } + + // set lightness + // + if (!(hide || this.hideAlone)) { + if (useHue()) { + lightness = (lightnessBase + lightnessMax) / 2; + } + else { + lightness = lightnessBase + (depth - 1) * lightnessFactor; + + if (lightness > lightnessMax) { + lightness = lightnessMax; + } + } + } + + if (hide) { + this.hide = true; + } + + if (this.hidePrev == undefined) { + this.hidePrev = this.hide; + } + + var hiddenStart = -1; + var hiddenHueNumer = 0; + var hiddenHueDenom = 0; + + + if (!this.hide) { + this.hiddenEnd = null; + } + + for (var i = 0; true; i++) { + if (!this.hideAlone && !hide && (i == this.children.length + || !this.children[i].hide)) { + // reached a non-hidden child or the end; set targets for + // previous group of hidden children (if any) using their + // average hue + + if (hiddenStart != -1) { + var hiddenHue = hiddenHueDenom ? hiddenHueNumer + / hiddenHueDenom : hueMin; + + for (var j = hiddenStart; j < i; j++) { + this.children[j].setTargetsSelected + ( + hiddenHue, + null, + lightness, + false, + j < i - 1 + ); + + this.children[j].hiddenEnd = null; + } + + this.children[hiddenStart].hiddenEnd = i - 1; + } + } + + if (i == this.children.length) { + break; + } + + var child = this.children[i]; + var childHueMin; + var childHueMax; + + if (this.magnitude > 0 && !this.hide && !this.hideAlone) { + if (useHue()) { + childHueMin = child.hues[currentDataset]; + } + else if (this == selectedNode) { + var min = 0.0; + var max = 1.0; + + if (this.children.length > 6) { + childHueMin = lerp((1 - Math.pow( + 1 - i / this.children.length, 1.4)) * .95, + 0, 1, min, max); + childHueMax = lerp((1 - Math.pow( + 1 - (i + .55) / this.children.length, 1.4)) * .95, + 0, 1, min, max); + } + else { + childHueMin = lerp(i / this.children.length, 0, 1, + min, max); + childHueMax = lerp((i + .55) / this.children.length, + 0, 1, min, max); + } + } + else { + childHueMin = lerp + ( + child.baseMagnitude, + this.baseMagnitude, + this.baseMagnitude + this.magnitude, + hueMin, + hueMax + ); + childHueMax = lerp + ( + child.baseMagnitude + child.magnitude * .99, + this.baseMagnitude, + this.baseMagnitude + this.magnitude, + hueMin, + hueMax + ); + } + } + else { + childHueMin = hueMin; + childHueMax = hueMax; + } + + if (!this.hideAlone && !hide && !this.hide && child.hide) { + if (hiddenStart == -1) { + hiddenStart = i; + } + + if (useHue()) { + hiddenHueNumer += childHueMin * child.magnitude; + hiddenHueDenom += child.magnitude; + } + else { + hiddenHueNumer += childHueMin; + hiddenHueDenom++; + } + } + else { + hiddenStart = -1; + + this.children[i].setTargetsSelected + ( + childHueMin, + childHueMax, + lightness, + hide || this.keyed || this.hideAlone + || this.hide && !collapse, + false + ); + } + } + + if (this.hue && this.magnitude) { + this.hue.setTarget(this.hues[currentDataset]); + + if (this.attributes[magnitudeIndex][lastDataset] == 0) { + this.hue.start = this.hue.end; + } + } + + this.radialPrev = this.radial; + + if (this == selectedNode) { + this.resetLabelWidth(); + this.labelWidth.setTarget(this.nameWidth * labelWidthFudge); + this.alphaWedge.setTarget(0); + this.alphaLabel.setTarget(1); + this.alphaOther.setTarget(1); + this.alphaArc.setTarget(0); + this.alphaLine.setTarget(0); + this.alphaPattern.setTarget(0); + this.r.setTarget(255); + this.g.setTarget(255); + this.b.setTarget(255); + this.radial = false; + this.labelRadius.setTarget(0); + } + else { + var rgb = hslToRgb + ( + hueMin, + saturation, + lightness + ); + + this.r.setTarget(rgb.r); + this.g.setTarget(rgb.g); + this.b.setTarget(rgb.b); + this.alphaOther.setTarget(0); + + this.alphaWedge.setTarget(1); + + if (this.hide || this.hideAlone) { + this.alphaPattern.setTarget(1); + } + else { + this.alphaPattern.setTarget(0); + } + + // set radial + // + if (!(hide || this.hide))//&& ! this.keyed ) + { + if (this.hideAlone) { + this.radial = true; + } + else if (false && canDisplayChildLabels) { + this.radial = false; + } + else { + this.radial = true; + + if (this.hasChildren() && depth < maxDisplayDepth) { + var lastChild = this.children[this.children.length - 1]; + + if + ( + lastChild.angleEnd.end == this.angleEnd.end || + ( + (this.angleStart.end + this.angleEnd.end) / 2 - + lastChild.angleEnd.end + ) * (this.radiusInner.end + 1) * gRadius * 2 < + minWidth() + ) { + this.radial = false; + } + } + } + } + + // set alphaLabel + // + if + ( + collapse || + hide || + this.hide || + this.keyed || + depth > maxDisplayDepth || + !this.canDisplayDepth() + ) { + this.alphaLabel.setTarget(0); + } + else { + if + ( + (this.radial || nLabelOffsets[depth - 2]) + ) { + this.alphaLabel.setTarget(1); + } + else { + this.alphaLabel.setTarget(0); + + if (this.radialPrev) { + this.alphaLabel.start = 0; + } + } + } + + // set alphaArc + // + if + ( + collapse || + hide || + depth > maxDisplayDepth || + !this.canDisplayDepth() + ) { + this.alphaArc.setTarget(0); + } + else { + this.alphaArc.setTarget(1); + } + + // set alphaLine + // + if + ( + hide || + this.hide && nextSiblingHidden || + depth > maxDisplayDepth || + !this.canDisplayDepth() + ) { + this.alphaLine.setTarget(0); + } + else { + this.alphaLine.setTarget(1); + } + + //if ( ! this.radial ) + { + this.resetLabelWidth(); + } + + // set labelRadius target + // + if (collapse) { + this.labelRadius.setTarget(this.radiusInner.end); + } + else { + if (depth > maxDisplayDepth || !this.canDisplayDepth()) { + this.labelRadius.setTarget(1); + } + else { + this.setTargetLabelRadius(); + } + } + } + } + + this.setTargetWedge = function () { + var depth = this.getDepth() - selectedNode.getDepth() + 1; + + // set angles + // + var baseMagnitudeRelative = this.baseMagnitude + - selectedNode.baseMagnitude; + // + this.angleStart.setTarget(baseMagnitudeRelative * angleFactor); + this.angleEnd.setTarget((baseMagnitudeRelative + this.magnitude) + * angleFactor); + + // set radiusInner + // + if (depth > maxDisplayDepth || !this.canDisplayDepth()) { + this.radiusInner.setTarget(1); + } + else { + if (compress) { + this.radiusInner.setTarget(compressedRadii[depth - 2]); + } + else { + this.radiusInner.setTarget(nodeRadius * (depth - 1)); + } + } + + if (this.hide != undefined) { + this.hidePrev = this.hide; + } + + if (this.hideAlone != undefined) { + this.hideAlonePrev = this.hideAlone; + } + + // set hide + // + if + ( + (this.angleEnd.end - this.angleStart.end) * + (this.radiusInner.end * gRadius + gRadius) < + minWidth() + ) { + if (depth == 2 && !this.getCollapse() && this.depth + <= maxAbsoluteDepth) { + this.keyed = true; + keys++; + this.hide = false; + + var percentage = this.getPercentage(); + this.keyLabel = this.name + ' ' + percentage + '%'; + var dim = context.measureText(this.keyLabel); + this.keyNameWidth = dim.width; + } + else { + this.keyed = false; + this.hide = depth > 2; + } + } + else { + this.keyed = false; + this.hide = false; + } + } + + this.shortenLabel = function () { + var label = this.name; + + var labelWidth = this.nameWidth; + var maxWidth = this.labelWidth.current(); + var minEndLength = 0; + + if (labelWidth > maxWidth && label.length > minEndLength * 2) { + var endLength = + Math.floor((label.length - 1) * maxWidth / labelWidth / 2); + + if (endLength < minEndLength) { + endLength = minEndLength; + } + + return ( + label.substring(0, endLength) + + '...' + + label.substring(label.length - endLength)); + } + else { + return label; + } + } + + /* this.shouldAddSearchResultsString = function() + { + if ( this.isSearchResult ) + { + return this.searchResults > 1; + } + else + { + return this.searchResults > 0; + } + } +*/ + this.sort = function () { + this.children.sort(function (a, b) { + if (sortByScoreCheckBox.checked) { + return b.getHue() - a.getHue() + } else { + return b.getMagnitude() - a.getMagnitude() + } + }); + + for (var i = 0; i < this.children.length; i++) { + this.children[i].sort(); + } + } +} + +var options; + +function addOptionElement(position, innerHTML, title, padding) { + var div = document.createElement("div"); +// div.style.position = 'absolute'; +// div.style.top = position + 'px'; + div.innerHTML = innerHTML; +// div.style.display = 'block'; + div.style.padding = padding || '2px'; + + if (title) { + div.title = title; + } + + options.appendChild(div); + var height = 0;//div.clientHeight; + return position + height; +} + +function addOptionElements(hueName, hueDefault) { + options = document.createElement('div'); + options.style.position = 'absolute'; + options.style.top = '0px'; + options.addEventListener('mousedown', function (e) { + mouseClick(e) + }, false); +// options.onmouseup = function(e) {mouseUp(e)} + document.body.appendChild(options); + + if (chart === ChartEnum.TAXOMIC) { + document.body.style.font = '11px Ubuntu'; + } else { + document.body.style.font = '12px Saira Semi Condensed'; + } + var position = 5; + + function logLoaded(fontFace) { + console.log(fontFace.family, 'loaded successfully.'); + } + +// Loading FontFaces via JavaScript is alternative to using CSS's @font-face rule. +// var ubuntuMonoFontFace = new FontFace('Ubuntu Mono', 'url(https://fonts.gstatic.com/s/ubuntumono/v7/KFOjCneDtsqEr0keqCMhbCc6CsTYl4BO.woff2)'); +// document.fonts.add(ubuntuMonoFontFace); +// ubuntuMonoFontFace.loaded.then(logLoaded); +// var oxygenFontFace = new FontFace('Oxygen', 'url(https://fonts.gstatic.com/s/oxygen/v5/qBSyz106i5ud7wkBU-FrPevvDin1pK8aKteLpeZ5c0A.woff2)'); +// document.fonts.add(oxygenFontFace); +// oxygenFontFace.loaded.then(logLoaded); + var oxygenMonoFontFace = new FontFace('Oxygen Mono', 'url(https://fonts.gstatic.com/s/oxygenmono/v5/h0GsssGg9FxgDgCjLeAd7hjYx-6tPUUv.woff2)'); + document.fonts.add(oxygenMonoFontFace); + oxygenMonoFontFace.loaded.then(logLoaded); + var sairaCondensedFontFace = new FontFace('Saira Condensed', 'url(https://fonts.gstatic.com/s/sairacondensed/v3/EJROQgErUN8XuHNEtX81i9TmEkrvoutF2o-Srg.woff2)'); + document.fonts.add(sairaCondensedFontFace); + sairaCondensedFontFace.loaded.then(logLoaded); + var sairaSemiCondensedFontFace = new FontFace('Saira Semi Condensed', 'url(https://fonts.gstatic.com/s/sairasemicondensed/v3/U9MD6c-2-nnJkHxyCjRcnMHcWVWV1cWRRX8MaOY8q3T_.woff2)'); + document.fonts.add(sairaSemiCondensedFontFace); + sairaSemiCondensedFontFace.loaded.then(logLoaded); + +// The .ready promise resolves when all fonts that have been previously requested +// are loaded and layout operations are complete. + document.fonts.ready.then(function () { + console.log('There are', document.fonts.size, 'FontFaces loaded.\n'); + + // document.fonts has a Set-like interface. Here, we're iterating over its values. + for (var fontFace of document.fonts.values()) { + console.log('FontFace:'); + for (var property in fontFace) { + console.log(' ' + property + ': ' + fontFace[property]); + } + console.log('\n'); + } + }); + + details = document.createElement('div'); + details.style.position = 'absolute'; + details.style.top = '1%'; + details.style.right = '2%'; + details.style.textAlign = 'right'; + document.body.insertBefore(details, canvas); +//<div id="details" style="position:absolute;top:1%;right:2%;text-align:right;"> + + details.innerHTML = '\ +<span id="detailsName" style="font-weight:bold"></span> \ +<input type="button" id="detailsExpand" onclick="expand(focusNode);"\ +value="↔" title="Expand this wedge to become the new focus of the chart"/><br/>\ +<div id="detailsInfo" style="float:right"></div>'; + + keyControl = document.createElement('input'); + keyControl.type = 'button'; + keyControl.value = showKeys ? 'x' : '…'; + keyControl.style.position = ''; + keyControl.style.position = 'fixed'; + keyControl.style.visibility = 'hidden'; + + document.body.insertBefore(keyControl, canvas); + + var logoElement = document.getElementById('logo'); + + if (logoElement) { + logoImage = logoElement.src; + } + else { + logoImage = 'https://raw.githubusercontent.com/khyox/recentrifuge/master/recentrifuge/img/logo-rcf-mini.uri'; + } + var placeholderTit; + if (chart === ChartEnum.GENOMIC) { + placeholderTit = "Complete or partial function, process, component..."; + } else { + placeholderTit = "Taxon scientific name, complete or partial name..."; + } + position = addOptionElement + ( + position, + '<a style="margin:2px" target="_blank" href="http://www.recentrifuge.org"><img style="vertical-align:middle;width:136px;height:32px;padding:8px 10px 6px 10px" src="' + logoImage + '"/></a><input type="button" id="back" value="←" title="Go back (Shortcut: ←)"/>\ +<input type="button" id="forward" value="→" title="Go forward (Shortcut: →)"/> \ + Search: <input type="text" placeholder="' + placeholderTit + '" size="45" id="search"/>\ +<input id="searchClear" type="button" value="x" onclick="clearSearch()"/> \ +<span id="searchResults"></span>' + ); + + if (datasets > 1) { + var size = datasets < DATASET_MAX_SIZE ? datasets : DATASET_MAX_SIZE; + + var select = + '<table style="border-collapse:collapse;margin-left:10px"><tr><td style="padding:0px">' + + '<select id="datasets" style="min-width:100px" size="' + size + '" onchange="onDatasetChange()">'; + + for (var i = 0; i < datasetNames.length; i++) { + select += '<option>' + datasetNames[i] + '</option>'; + } + select += + '</select></td><td style="vertical-align:top;padding:2px;">' + + '<input style="display:block" title="Previous dataset ' + + '(Shortcut: ↑)" id="prevDataset" type="button"' + + ' value="↑" onclick="prevDataset()" disabled="true"/>' + + '<input title="Next dataset (Shortcut: ↓)" ' + + 'id="nextDataset" type="button" value="↓" ' + + 'onclick="nextDataset()"/><br/></td>' + + '<td style="vertical-align:top;padding:2px;">' + + '<input style="display:block" ' + + 'title="Switch to the prior dataset that was viewed ' + + '(Shortcut: TAB)" id="lastDataset" type="button" ' + + 'style="font:11px Ubuntu" value="prior" ' + + 'onclick="selectLastDataset()"/>' + + '<select id="ranks" onchange="onRankChange()" ' + + 'title="Filter samples by taxonomic rank">' + + '<option value="SUMMARY">SUMMARY</option>' + + '<option value="strain">strain</option>' + + '<option value="species">species</option>' + + '<option value="genus">genus</option>' + + '<option value="family">family</option>' + + '<option value="order">order</option>' + + '<option value="class">class</option>' + + '<option value="phylum">phylum</option>' + + '<option value="kingdom">kingdom</option>' + + '<option value="domain">domain</option>' + + '<option value="ALL">ALL</option>' + + '<option value="NONE">NONE</option>' + + '</select></td></tr></table>'; + + position = addOptionElement(position + 5, select); + + datasetDropDown = document.getElementById('datasets'); + datasetButtonLast = document.getElementById('lastDataset'); + datasetButtonPrev = document.getElementById('prevDataset'); + datasetButtonNext = document.getElementById('nextDataset'); + rankDropDown = document.getElementById('ranks'); + if (chart === ChartEnum.GENOMIC) { + for (i = 1; i < 10; i++) { + rankDropDown.remove(1); // Remove taxonomic ranks from options + } + datasetDropDown.style.color='#FFFFFF' + datasetDropDown.style.backgroundColor='#555555' // #B20DFF22' + } + position += datasetDropDown.clientHeight; + } + + position = addOptionElement + ( + position + 5, + '<input type="button" id="maxAbsoluteDepthDecrease" style="margin:1px 4px 0 10px" value="-"/>\ +<span id="maxAbsoluteDepth"></span>\ + <input type="button" id="maxAbsoluteDepthIncrease" style="margin:2px 1px 0 2px" value="+"/> Max depth', + 'Maximum depth to display, counted from the top level \ +and including collapsed wedges.' + ); + + position = addOptionElement + ( + position, + '<input type="button" id="fontSizeDecrease" style="margin:0 4px 0 10px" value="-"/>\ +<span id="fontSize"></span>\ + <input type="button" id="fontSizeIncrease" style="margin:0 2px 0 2px" value="+"/> Font size' + ); + + position = addOptionElement + ( + position, + '<input type="button" id="radiusDecrease" style="margin:0 4px 0 10px" value="-"/>\ +<input type="button" id="radiusIncrease" style="margin:0 2px 0 1px" value="+"/> Chart size' + ); + + position = addOptionElement + ( + position, + '<input type="button" id="bkgBrightDecrease" style="margin:0 4px 5px 10px" value="-"/>\ +<input type="button" id="bkgBrightIncrease" style="margin:0 2px 5px 1px" value="+"/> Bkg bright' + ); + + if (hueName) { + hueDisplayName = attributes[attributeIndex(hueName)].displayName; + + position = addOptionElement + ( + position + 5, + '<input type="checkbox" id="useHue" style="float:left; ' + + 'margin:1px 4px 0 12px"/><div>Color by ' + hueDisplayName + + '</div>' + ); + + useHueCheckBox = document.getElementById('useHue'); + useHueCheckBox.checked = hueDefault; + useHueCheckBox.onclick = handleResize; + useHueCheckBox.onmousedown = suppressEvent; + + position = addOptionElement + ( + position, + '<input type="checkbox" id="sortByScore"/> Use to sort', + 'Activates sorting the taxa by this magnitude', + '0px 2px 2px 25px' + ); + + sortByScoreCheckBox = document.getElementById('sortByScore'); + sortByScoreCheckBox.onclick = onSortChange; + sortByScoreCheckBox.onmousedown = suppressEvent; + } + + position = addOptionElement + ( + position, + '<input type="checkbox" id="collapse" style="margin:4px 4px 0 12px" ' + + 'checked="checked"/>Collapse', + 'Collapse wedges that are redundant (entirely composed of another ' + + 'wedge). Also affects score navigation, restricting to lowest level.' + ); + + /* + position = addOptionElement + ( + position, + ' <input type="checkbox" id="shorten" checked="checked" />Shorten labels</div>', + 'Prevent labels from overlapping by shortening them' + ); + + position = addOptionElement + ( + position, + ' <input type="checkbox" id="compress" checked="checked" />Compress', + 'Compress wedges if needed to show the entire depth' + ); + */ + + position = addOptionElement + ( + position, + '<input type="button" id="snapshot" style="margin:5px 2px 0 10px"\ + value="Snapshot" title="Render the current view as SVG (Scalable \ +Vector Graphics), a vectorial publication-quality format that can be saved or \ +printed as PDF"/> <input type="button" id="help" value="?"\ + onclick="window.open(\'https://github.com/khyox/recentrifuge/wiki\',\ + \'help\')" title="Help"/>'); + + position = addOptionElement + ( + position + 5, + '<input type="button" id="linkButton" style="margin:5px 2px 0 10px" value="Link"/>\ +<input type="text" size="30" id="linkText"/>', + 'Show a link to this view that can be copied for bookmarking or sharing' + ); +} + +function arrow(angleStart, angleEnd, radiusInner) { + if (context.globalAlpha == 0) { + return; + } + + var angleCenter = (angleStart + angleEnd) / 2; + var radiusArrowInner = radiusInner - gRadius / 10;//nodeRadius * gRadius; + var radiusArrowOuter = gRadius * 1.1;//(1 + nodeRadius); + var radiusArrowCenter = (radiusArrowInner + radiusArrowOuter) / 2; + var pointLength = (radiusArrowOuter - radiusArrowInner) / 5; + + context.fillStyle = highlightFill; + context.lineWidth = highlightLineWidth; + + // First, mask out the first half of the arrow. This will prevent the tips + // from superimposing if the arrow goes most of the way around the circle. + // Masking is done by setting the clipping region to the inverse of the + // half-arrow, which is defined by cutting the half-arrow out of a large + // rectangle + // + context.beginPath(); + context.arc(0, 0, radiusInner, angleCenter, angleEnd, false); + context.lineTo + ( + radiusArrowInner * Math.cos(angleEnd), + radiusArrowInner * Math.sin(angleEnd) + ); + context.lineTo + ( + radiusArrowCenter * Math.cos(angleEnd) + - pointLength * Math.sin(angleEnd), + radiusArrowCenter * Math.sin(angleEnd) + + pointLength * Math.cos(angleEnd) + ); + context.lineTo + ( + radiusArrowOuter * Math.cos(angleEnd), + radiusArrowOuter * Math.sin(angleEnd) + ); + context.arc(0, 0, gRadius, angleEnd, angleCenter, true); + context.closePath(); + context.moveTo(-imageWidth, -imageHeight); + context.lineTo(imageWidth, -imageHeight); + context.lineTo(imageWidth, imageHeight); + context.lineTo(-imageWidth, imageHeight); + context.closePath(); + context.save(); + context.clip(); + + // Next, draw the other half-arrow with the first half masked out + // + context.beginPath(); + context.arc(0, 0, radiusInner, angleCenter, angleStart, true); + context.lineTo + ( + radiusArrowInner * Math.cos(angleStart), + radiusArrowInner * Math.sin(angleStart) + ); + context.lineTo + ( + radiusArrowCenter * Math.cos(angleStart) + + pointLength * Math.sin(angleStart), + radiusArrowCenter * Math.sin(angleStart) + - pointLength * Math.cos(angleStart) + ); + context.lineTo + ( + radiusArrowOuter * Math.cos(angleStart), + radiusArrowOuter * Math.sin(angleStart) + ); + context.arc(0, 0, gRadius, angleStart, angleCenter, false); + context.fill(); + context.stroke(); + + // Finally, remove the clipping region and draw the first half-arrow. This + // half is extended slightly to fill the seam. + // + context.restore(); + context.beginPath(); + context.arc(0, 0, radiusInner, angleCenter + - 2 / (2 * Math.PI * radiusInner), angleEnd, false); + context.lineTo + ( + radiusArrowInner * Math.cos(angleEnd), + radiusArrowInner * Math.sin(angleEnd) + ); + context.lineTo + ( + radiusArrowCenter * Math.cos(angleEnd) + - pointLength * Math.sin(angleEnd), + radiusArrowCenter * Math.sin(angleEnd) + + pointLength * Math.cos(angleEnd) + ); + context.lineTo + ( + radiusArrowOuter * Math.cos(angleEnd), + radiusArrowOuter * Math.sin(angleEnd) + ); + context.arc(0, 0, gRadius, angleEnd, angleCenter - 2 + / (2 * Math.PI * gRadius), true); + context.fill(); + context.stroke(); +} + +function attributeIndex(aname) { + for (var i = 0; i < attributes.length; i++) { + if (aname == attributes[i].name) { + return i; + } + } + + return null; +} + +function bkgBrightDecrease() { + var bkgBrightInt = parseInt(bkgBright, 16) + if (bkgBrightInt > parseInt('555555', 16)) { + bkgBright = (bkgBrightInt - 0x111111).toString(16) + document.body.style.backgroundColor = '#' + bkgBright + updateViewNeeded = true; + } +} + +function bkgBrightIncrease() { + var bkgBrightInt = parseInt(bkgBright, 16) + if (bkgBrightInt < parseInt('ffffff', 16)) { + bkgBright = (bkgBrightInt + 0x111111).toString(16) + document.body.style.backgroundColor = '#' + bkgBright + updateViewNeeded = true; + } +} + +function checkHighlight() { + var lastHighlightedNode = highlightedNode; + var lastHighlightingHidden = highlightingHidden; + + highlightedNode = selectedNode; + resetKeyOffset(); + + if (progress == 1) { + selectedNode.checkHighlight(); + if (selectedNode.getParent()) { + selectedNode.getParent().checkHighlightCenter(); + } + + focusNode.checkHighlightMap(); + } + + if (highlightedNode != selectedNode) { + if (highlightedNode == focusNode) { +// canvas.style.display='none'; +// window.resizeBy(1,0); +// canvas.style.cursor='ew-resize'; +// window.resizeBy(-1,0); +// canvas.style.display='inline'; + } + else { +// canvas.style.cursor='pointer'; + } + } + else { +// canvas.style.cursor='auto'; + } + + if + ( + ( + true || + highlightedNode != lastHighlightedNode || + highlightingHidden != highlightingHiddenLast + ) && + progress == 1 + ) { + draw(); // TODO: handle in update() + } +} + +function checkSelectedCollapse() { + var newNode = selectedNode; + + while (newNode.getCollapse()) { + newNode = newNode.children[0]; + } + + if (newNode.children.length == 0 && newNode.getParent()) { + newNode = newNode.getParent(); + } + + if (newNode != selectedNode) { + selectNode(newNode); + } +} + +function clearSearch() { + if (search.value != '') { + search.value = ''; + nodesIndex = undefined; + onSearchChange(); + } +} + +function createSVG() { + svgNS = "http://www.w3.org/2000/svg"; + var SVG = {}; + SVG.xlinkns = "http://www.w3.org/1999/xlink"; + + var newSVG = document.createElementNS(svgNS, "svg:svg"); + + newSVG.setAttribute("id", "canvas"); + // How big is the canvas in pixels + newSVG.setAttribute("width", '100%'); + newSVG.setAttribute("height", '100%'); + // Set the coordinates used by drawings in the canvas +// newSVG.setAttribute("viewBox", "0 0 " + imageWidth + " " + imageHeight); + // Define the XLink namespace that SVG uses + newSVG.setAttributeNS + ( + "http://www.w3.org/2000/xmlns/", + "xmlns:xlink", + SVG.xlinkns + ); + + return newSVG; +} + +function degrees(radians) { + return radians * 180 / Math.PI; +} + +function draw() { + tweenFrames++; + //resize(); +// context.fillRect(0, 0, imageWidth, imageHeight); + context.clearRect(0, 0, imageWidth, imageHeight); + + context.font = fontNormal; + context.textBaseline = 'middle'; + + //context.strokeStyle = 'rgba(0, 0, 0, 0.3)'; + context.translate(centerX, centerY); + + resetKeyOffset(); + + head.draw(false, false); // draw pie slices + head.draw(true, false); // draw labels + + var pathRoot = selectedNode; + + if (focusNode != 0 && focusNode != selectedNode) { + context.globalAlpha = 1; + focusNode.drawHighlight(true); + pathRoot = focusNode; + } + + if + ( + highlightedNode && + highlightedNode.getDepth() >= selectedNode.getDepth() && + highlightedNode != focusNode + ) { + if + ( + progress == 1 && + highlightedNode != selectedNode && + ( + highlightedNode != focusNode || + focusNode.children.length > 0 + ) + ) { + context.globalAlpha = 1; + highlightedNode.drawHighlight(true); + } + + //pathRoot = highlightedNode; + } + else if + ( + progress == 1 && + highlightedNode.getDepth() < selectedNode.getDepth() + ) { + context.globalAlpha = 1; + highlightedNode.drawHighlightCenter(); + } + + if (quickLook && false) // TEMP + { + context.globalAlpha = 1 - progress / 2; + selectedNode.drawHighlight(true); + } + else if (progress < 1)//&& zoomOut() ) + { + if (!zoomOut)//() ) + { + context.globalAlpha = selectedNode.alphaLine.current(); + selectedNode.drawHighlight(true); + } + else if (selectedNodeLast) { + context.globalAlpha = 1 - 4 * Math.pow(progress - .5, 2); + selectedNodeLast.drawHighlight(false); + } + } + + drawDatasetName(); + + //drawHistory(); + + context.translate(-centerX, -centerY); + context.globalAlpha = 1; + + mapRadius = + (imageHeight / 2 - details.clientHeight - details.offsetTop) / + (pathRoot.getDepth() - 1) * 3 / 4 / 2; + + if (mapRadius > maxMapRadius) { + mapRadius = maxMapRadius; + } + + mapBuffer = mapRadius / 2; + + //context.font = fontNormal; + pathRoot.drawMap(pathRoot); + + if (hueDisplayName && useHue()) { + drawLegend(); + } +} + +function drawBubble(angle, radius, width, radial, flip) { + var height = fontSize * 2; + var x; + var y; + + width = width + fontSize; + + if (radial) { + y = -fontSize; + + if (flip) { + x = radius - width + fontSize / 2; + } + else { + x = radius - fontSize / 2; + } + } + else { + x = -width / 2; + y = -radius - fontSize; + } + + if (snapshotMode) { + drawBubbleSVG(x + centerX, y + centerY, width, height, fontSize, angle); + } + else { + drawBubbleCanvas(x, y, width, height, fontSize, angle); + } +} + +function drawBubbleCanvas(x, y, width, height, radius, rotation) { + context.strokeStyle = 'black'; + context.lineWidth = highlightLineWidth; + context.fillStyle = 'rgba(255, 255, 255, .75)'; + context.rotate(rotation); + roundedRectangle(x, y, width, fontSize * 2, fontSize); + context.fill(); + context.stroke(); + context.rotate(-rotation); +} + +function drawBubbleSVG(x, y, width, height, radius, rotation) { + svg += + '<rect x="' + x + '" y="' + y + + '" width="' + width + + '" height="' + height + + '" rx="' + radius + + '" ry="' + radius + + '" fill="rgba(255, 255, 255, .75)' + + '" class="highlight" ' + + 'transform="rotate(' + + degrees(rotation) + ',' + centerX + ',' + centerY + + ')"/>'; +} + +function drawDatasetName() { + var alpha = datasetAlpha.current(); + + if (alpha > 0) { + var radius = gRadius * compressedRadii[0] / -2; + + if (alpha > 1) { + alpha = 1; + } + + context.globalAlpha = alpha; + + drawBubble(0, -radius, datasetWidths[currentDataset], false, false); + drawText(datasetNames[currentDataset], 0, radius, 0, 'center', true); + } +} + +function drawHistory() { + var alpha = 1; + context.textAlign = 'center'; + + for (var i = 0; i < nodeHistoryPosition && alpha > 0; i++) { + + context.globalAlpha = alpha - historyAlphaDelta * tweenFactor; + context.fillText + ( + nodeHistory[nodeHistoryPosition - i - 1].name, + 0, + (i + tweenFactor) * historySpacingFactor * fontSize - 1 + ); + + if (alpha > 0) { + alpha -= historyAlphaDelta; + } + } + + context.globalAlpha = 1; +} + +function drawLegend() { + var width = imageHeight * .0265; + var side = width * 0.9 + var left_buttons = imageWidth * .008; + var left = left_buttons + side + fontSize; + var height = imageHeight * .15; + var top = imageHeight - fontSize * 3.5 - height; + var textLeft = left + width + fontSize / 2; + var delta = (height - side) / 3; + + canvasButtons = [] // Delete previous buttons + var buttonMost = new CanvasButton('mostScore', left_buttons, + top, side, side, '#c87cca'); + var buttonLest = new CanvasButton('lestScore', left_buttons, + top + 3 * delta, side, side, '#d38381'); + canvasButtons.push(buttonMost, buttonLest); + if (nodesIndex !== undefined) { + var buttonMore = new CanvasButton('moreScore', left_buttons, + top + delta, side, side, '#81c8d3'); + var buttonLess = new CanvasButton('lessScore', left_buttons, + top + 2 * delta, side, side, '#96d281'); + canvasButtons.push(buttonMore, buttonLess) + } + canvasButtons.forEach(function (element) { + element.draw(context); + }); + context.fillStyle = 'black'; + context.textAlign = 'start'; + context.font = fontNormal; + context.fillText(hueDisplayName, left_buttons, imageHeight - fontSize * 1.5); + + var gradient = context.createLinearGradient(0, top + height, 0, top); + + for (var i = 0; i < hueStopPositions.length; i++) { + gradient.addColorStop(hueStopPositions[i], hueStopHsl[i]); + + var textY = top + (1 - hueStopPositions[i]) * height; + + if + ( + i === 0 || + i === hueStopPositions.length - 1 || + textY > top + fontSize && textY < top + height - fontSize + ) { + context.fillText(hueStopText[i], textLeft, textY); + } + } + + context.fillStyle = gradient; + context.fillRect(left, top, width, height); + context.lineWidth = thinLineWidth; + context.strokeRect(left, top, width, height); + + // Sample statistics + if (currentDataset < numRawSamples) { + var stat = stats[currentDataset]; + // Define aux position variables + var statsX = textLeft + 2 * width; + var statsY = top; + var rad = width; + context.font = "Bold 11px Ubuntu"; + var statLabelText; + if (chart === ChartEnum.GENOMIC) { + context.fillStyle = 'rgba(170, 20, 255, 1)'; + statLabelText = 'Functional sample statistics'; + } else if (stat.is_ctrl) { + context.fillStyle = 'rgba(50, 50, 200, 1)'; + statLabelText = 'Control statistics'; + } else { + context.fillStyle = 'rgba(200, 50, 50, 1)'; + statLabelText = 'Sample statistics'; + } + context.fillText(statLabelText, statsX + width, + imageHeight - fontSize * 1.5); + // Get the set of strings + var oldFont = context.font; + context.font = "10.5px monospace"; // In case the next line fails + context.font = "10.5px Oxygen Mono"; + var readTit; + var nodeTit; + if (chart === ChartEnum.GENOMIC) { + readTit = 'Annotations read: ' + nodeTit = 'GOs' + } else { + readTit = 'Sequences read: ' + nodeTit = 'TaxIDs' + } + var statsStrs = [ + readTit + stat.sread, + ' those classified: ' + ( + stat.sclas / stat.sread * 100).toPrecision(3) + '%', + ' those accepted: ' + + (stat.sfilt / stat.sclas * 100).toPrecision(3) + '%', + 'Score average: ' + parseFloat(stat.scavg).toFixed(1), + ' min: ' + parseFloat(stat.scmin).toFixed(1) + + ' max: ' + parseFloat(stat.scmax).toFixed(1), + 'Length average: ' + stat.lnavg, + ' min: ' + stat.lnmin + ' max: ' + stat.lnmax, + nodeTit + ' by classifier: ' + stat.tclas, + ' those accepted: ' + + (stat.tfilt / stat.tclas * 100).toPrecision(3) + '%', + ' final: ' + + (stat.tfold / stat.tfilt * 100).toPrecision(3) + '% [' + + stat.tfold + ']' + ]; + var maxTextWidth = Math.max.apply(null, statsStrs.map(function (text) { + return context.measureText(text).width + })); + // Draw the rounded rectangle + context.lineWidth = 3; + if (chart === ChartEnum.GENOMIC) { + context.strokeStyle = '#B20DFF'; + context.fillStyle = 'rgba(180, 100, 255, 0.2)'; + } else if (stat.is_ctrl) { + context.strokeStyle = '#3333CC'; + context.fillStyle = 'rgba(0, 255, 255, 0.2)'; + } else { + context.strokeStyle = '#CC3333'; + context.fillStyle = 'rgba(255, 255, 0, 0.2)'; + } + var box = new roundedRectangle( + statsX, statsY, 1.2 * maxTextWidth, height, {tr: rad, bl: rad}); + context.stroke(); + context.fill(); + context.fillStyle = context.strokeStyle = '#222222'; + // Write the stats inside + var statsNum = statsStrs.length; + var statsLeft = statsX + maxTextWidth * 0.1; + var statsDelta = height / (statsNum + 1); + for (i = 0; i < statsNum; i++) { + context.fillText(statsStrs[i], + statsLeft, top + i * statsDelta + fontSize); + } + // Restore font + context.font = oldFont; + } +} + +function drawLegendSVG() { + var left = imageWidth * .01; + var width = imageHeight * .0265; + var height = imageHeight * .15; + var top = imageHeight - fontSize * 3.5 - height; + var textLeft = left + width + fontSize / 2; + + var text = ''; + + text += svgText(hueDisplayName, left, imageHeight - fontSize * 1.5); + + var svgtest = + '<linearGradient id="gradient" x1="0%" y1="100%" x2="0%" y2="0%">'; + + for (var i = 0; i < hueStopPositions.length; i++) { + svgtest += + '<stop offset="' + round(hueStopPositions[i] * 100) + + '%" style="stop-color:' + hueStopHsl[i] + '"/>'; + + var textY = top + (1 - hueStopPositions[i]) * height; + + if + ( + i == 0 || + i == hueStopPositions.length - 1 || + textY > top + fontSize && textY < top + height - fontSize + ) { + text += svgText(hueStopText[i], textLeft, textY); + } + } + + svgtest += '</linearGradient>'; + //alert(svgtest); + svg += svgtest; + svg += + '<rect style="fill:url(#gradient)" x="' + left + '" y="' + top + + '" width="' + width + '" height="' + height + '"/>'; + + svg += text; +} + +function drawSearchHighlights(label, bubbleX, bubbleY, rotation, center) { + var index = -1; + var labelLength = label.length; + + bubbleX -= fontSize / 4; + + do { + index = label.toLowerCase().indexOf(search.value.toLowerCase(), + index + 1); + + if (index != -1 && index < labelLength) { + var dim = context.measureText(label.substr(0, index)); + var x = bubbleX + dim.width; + + dim = context.measureText(label.substr(index, search.value.length)); + + var y = bubbleY - fontSize * 3 / 4; + var width = dim.width + fontSize / 2; + var height = fontSize * 3 / 2; + var radius = fontSize / 2; + + if (snapshotMode) { + if (center) { + x += centerX; + y += centerY; + } + + svg += + '<rect x="' + x + '" y="' + y + + '" width="' + width + + '" height="' + height + + '" rx="' + radius + + '" ry="' + radius + + '" class="searchHighlight' + + '" transform="rotate(' + + degrees(rotation) + ',' + centerX + ',' + centerY + + ')"/>'; + } + else { + context.fillStyle = 'rgb(255, 255, 100)'; + context.rotate(rotation); + roundedRectangle(x, y, width, height, radius); + context.fill(); + context.rotate(-rotation); + } + } + } + while (index != -1 && index < labelLength); +} + +function drawText(text, x, y, angle, anchor, bold, color) { + if (color == undefined) { + color = 'black'; + } + + if (snapshotMode) { + svg += + '<text x="' + (centerX + x) + '" y="' + (centerY + y) + + '" text-anchor="' + anchor + '" style="font-color:' + color + + ';font-weight:' + (bold ? 'bold' : 'normal') + + '" transform="rotate(' + degrees(angle) + ',' + centerX + + ',' + centerY + ')">' + + text + '</text>'; + } + else { + context.fillStyle = color; + context.textAlign = anchor; + context.font = bold ? fontBold : fontNormal; + context.rotate(angle); + context.fillText(text, x, y); + context.rotate(-angle); + } +} + +function drawTextPolar +(text, + innerText, + angle, + radius, + radial, + bubble, + bold, + searchResult, + searchResults) { + var anchor; + var textX; + var textY; + var spacer; + var totalText = text; + var flip; + + if (snapshotMode) { + spacer = '   '; + } + else { + spacer = ' '; + } + + if (radial) { + flip = angle < 3 * Math.PI / 2; + + if (flip) { + angle -= Math.PI; + radius = -radius; + anchor = 'end'; + + if (innerText) { + totalText = text + spacer + innerText; + } + } + else { + anchor = 'start'; + + if (innerText) { + totalText = innerText + spacer + text; + } + } + + textX = radius; + textY = 0; + } + else { + flip = angle < Math.PI || angle > 2 * Math.PI; + var label; + + anchor = snapshotMode ? 'middle' : 'center'; + + if (flip) { + angle -= Math.PI; + radius = -radius; + } + + angle += Math.PI / 2; + textX = 0; + textY = -radius; + } + + if (bubble) { + var textActual = totalText; + + if (innerText && snapshotMode) { + if (flip) { + textActual = text + ' ' + innerText; + } + else { + textActual = innerText + ' ' + text; + } + } + + if (searchResults) { + textActual = textActual + searchResultString(searchResults); + } + + var textWidth = measureText(textActual, bold); + + var x = textX; + + if (anchor == 'end') { + x -= textWidth; + } + else if (anchor != 'start') { + // centered + x -= textWidth / 2; + } + + drawBubble(angle, radius, textWidth, radial, flip); + + if (searchResult) { + drawSearchHighlights + ( + textActual, + x, + textY, + angle, + true + ) + } + } + + if (searchResults) { + totalText = totalText + searchResultString(searchResults); + } + + drawText(totalText, textX, textY, angle, anchor, bold); + + return flip; +} + +function drawTick(start, length, angle) { + if (snapshotMode) { + svg += + '<line x1="' + (centerX + start) + + '" y1="' + centerY + + '" x2="' + (centerX + start + length) + + '" y2="' + centerY + + '" class="tick" transform="rotate(' + + degrees(angle) + ',' + centerX + ',' + centerY + + ')"/>'; + } + else { + context.rotate(angle); + context.beginPath(); + context.moveTo(start, 0); + context.lineTo(start + length, 0); + context.lineWidth = thinLineWidth * 2; + context.stroke(); + context.rotate(-angle); + } +} + +function drawWedge +(angleStart, + angleEnd, + radiusInner, + radiusOuter, + color, + patternAlpha, + highlight) { + if (context.globalAlpha == 0) { + return; + } + + if (snapshotMode) { + if (angleEnd == angleStart + Math.PI * 2) { + // fudge to prevent overlap, which causes arc ambiguity + // + angleEnd -= .1 / gRadius; + } + + var longArc = angleEnd - angleStart > Math.PI ? 1 : 0; + + var x1 = centerX + radiusInner * Math.cos(angleStart); + var y1 = centerY + radiusInner * Math.sin(angleStart); + + var x2 = centerX + gRadius * Math.cos(angleStart); + var y2 = centerY + gRadius * Math.sin(angleStart); + + var x3 = centerX + gRadius * Math.cos(angleEnd); + var y3 = centerY + gRadius * Math.sin(angleEnd); + + var x4 = centerX + radiusInner * Math.cos(angleEnd); + var y4 = centerY + radiusInner * Math.sin(angleEnd); + + var dArray = + [ + " M ", x1, ",", y1, + " L ", x2, ",", y2, + " A ", gRadius, ",", gRadius, " 0 ", longArc, ",1 ", x3 + , ",", y3, + " L ", x4, ",", y4, + " A ", radiusInner, ",", radiusInner, " 0 ", longArc, + " 0 ", x1, ",", y1, + " Z " + ]; + + svg += + '<path class="' + (highlight ? 'highlight' : 'wedge') + + '" fill="' + color + + '" d="' + dArray.join('') + '"/>'; + + if (patternAlpha > 0) { + svg += + '<path class="wedge" fill="url(#hiddenPattern)" d="' + + dArray.join('') + '"/>'; + } + } + else { + // fudge to prevent seams during animation + // + angleEnd += 1 / gRadius; + + context.fillStyle = color; + context.beginPath(); + context.arc(0, 0, radiusInner, angleStart, angleEnd, false); + context.arc(0, 0, radiusOuter, angleEnd, angleStart, true); + context.closePath(); + context.fill(); + + if (patternAlpha > 0) { + context.save(); + context.clip(); + context.globalAlpha = patternAlpha; + context.fillStyle = hiddenPattern; + context.fill(); + context.restore(); + } + + if (highlight) { + context.lineWidth = highlight ? highlightLineWidth : thinLineWidth; + context.strokeStyle = 'black'; + context.stroke(); + } + } +} + +function expand(node) { + selectNode(node); + updateView(); +} + +function focusLost() { + mouseX = -1; + mouseY = -1; + checkHighlight(); + document.body.style.cursor = 'auto'; +} + +function fontSizeDecrease() { + if (fontSize > 1) { + fontSize--; + updateViewNeeded = true; + } +} + +function fontSizeIncrease() { + fontSize++; + updateViewNeeded = true; +} + +function getGetString(name, value, bool) { + return name + '=' + (bool ? value ? 'true' : 'false' : value); +} + +function hideLink() { + hide(linkText); + show(linkButton); +} + +function show(object) { + object.style.display = 'inline'; +} + +function hide(object) { + object.style.display = 'none'; +} + +function showLink() { + var urlHalves = String(document.location).split('?'); + var newGetVariables = new Array(); + + newGetVariables.push + ( + getGetString('dataset', currentDataset, false), + getGetString('node', selectedNode.id, false), + getGetString('collapse', collapse, true), + getGetString('color', useHue(), true), + getGetString('depth', maxAbsoluteDepth - 1, false), + getGetString('font', fontSize, false), + getGetString('key', showKeys, true) + ); + + hide(linkButton); + show(linkText); + linkText.value = urlHalves[0] + '?' + + getVariables.concat(newGetVariables).join('&'); + //linkText.disabled = false; + linkText.focus(); + linkText.select(); + //linkText.disabled = true; +// document.location = urlHalves[0] + '?' + getVariables.join('&'); +} + +function getFirstChild(element) { + element = element.firstChild; + + if (element && element.nodeType != 1) { + element = getNextSibling(element); + } + + return element; +} + +function getNextSibling(element) { + do { + element = element.nextSibling; + } + while (element && element.nodeType != 1); + + return element; +} + +function getPercentage(fraction) { + return round(fraction * 100); +} + +function hslText(hue) { + if (1 || snapshotMode) { + // Safari doesn't seem to allow hsl() in SVG + + var rgb = hslToRgb(hue, saturation, (lightnessBase + lightnessMax) / 2); + + return rgbText(rgb.r, rgb.g, rgb.b); + } + else { + var hslArray = + [ + 'hsl(', + Math.floor(hue * 360), + ',', + Math.floor(saturation * 100), + '%,', + Math.floor((lightnessBase + lightnessMax) * 50), + '%)' + ]; + + return hslArray.join(''); + } +} + +function hslToRgb(h, s, l) { + var m1, m2; + var r, g, b; + + if (s == 0) { + r = g = b = Math.floor((l * 255)); + } + else { + if (l <= 0.5) { + m2 = l * (s + 1); + } + else { + m2 = l + s - l * s; + } + + m1 = l * 2 - m2; + + r = Math.floor(hueToRgb(m1, m2, h + 1 / 3)); + g = Math.floor(hueToRgb(m1, m2, h)); + b = Math.floor(hueToRgb(m1, m2, h - 1 / 3)); + } + + return {r: r, g: g, b: b}; +} + +function hueToRgb(m1, m2, hue) { + var v; + + while (hue < 0) { + hue += 1; + } + + while (hue > 1) { + hue -= 1; + } + + if (6 * hue < 1) + v = m1 + (m2 - m1) * hue * 6; + else if (2 * hue < 1) + v = m2; + else if (3 * hue < 2) + v = m1 + (m2 - m1) * (2 / 3 - hue) * 6; + else + v = m1; + + return 255 * v; +} + +function interpolateHue(hueStart, hueEnd, valueStart, valueEnd) { + // since the gradient will be RGB based, we need to add stops to hit all the + // colors in the hue spectrum + + function selective_round(value){ + // Selective round depending on the hue scale width + if(valueEnd - valueStart < 10){ + return(value.toFixed(1)) + } else { + return(round(value)) + } + } + + hueStopPositions = new Array(); + hueStopHsl = new Array(); + hueStopText = new Array(); + + hueStopPositions.push(0); + hueStopHsl.push(hslText(hueStart)); + hueStopText.push(selective_round(valueStart)); + + for + ( + var i = (hueStart > hueEnd ? 5 / 6 : 1 / 6); + (hueStart > hueEnd ? i > 0 : i < 1); + i += (hueStart > hueEnd ? -1 : 1) / 6 + ) { + if + ( + hueStart > hueEnd ? + i > hueEnd && i < hueStart : + i > hueStart && i < hueEnd + ) { + hueStopPositions.push(lerp(i, hueStart, hueEnd, 0, 1)); + hueStopHsl.push(hslText(i)); + hueStopText.push(selective_round(lerp( + i, hueStart, hueEnd, valueStart, valueEnd))); + } + } + + hueStopPositions.push(1); + hueStopHsl.push(hslText(hueEnd)); + hueStopText.push(selective_round(valueEnd)); +} + +function keyLineAngle(angle, keyAngle, bendRadius, keyX, keyY, pointsX, + pointsY) { + if (angle < Math.PI / 2 && keyY < bendRadius * Math.sin(angle) + || angle > Math.PI / 2 && keyY < bendRadius) { + return Math.asin(keyY / bendRadius); + } + else { + // find the angle of the normal to a tangent line that goes to + // the label + + var textDist = Math.sqrt + ( + Math.pow(keyX, 2) + + Math.pow(keyY, 2) + ); + + var tanAngle = Math.acos(bendRadius / textDist) + keyAngle; + + if (angle < tanAngle || angle < Math.PI / 2)//|| labelLeft < centerX ) + { + // angle doesn't reach far enough for tangent; collapse and + // connect directly to label + + if (keyY / Math.tan(angle) > 0) { + pointsX.push(keyY / Math.tan(angle)); + pointsY.push(keyY); + } + else { + pointsX.push(bendRadius * Math.cos(angle)); + pointsY.push(bendRadius * Math.sin(angle)); + } + + return angle; + } + else { + return tanAngle; + } + } +} + +function keyOffset() { + return imageHeight - (keys - currentKey + 1) * (keySize + keyBuffer) + + keyBuffer - margin; +} + +function lerp(value, fromStart, fromEnd, toStart, toEnd) { + // Rescale value from source scale [fromStart, fromEnd] + // to target scale [toStart, toEnd] + return (value - fromStart) * + (toEnd - toStart) / + (fromEnd - fromStart) + + toStart; +} + +function createCanvas() { + canvas = document.createElement('canvas'); + document.body.appendChild(canvas); + context = canvas.getContext('2d'); +} + +function load() { + document.body.style.overflow = "hidden"; + document.body.style.margin = 0; + document.body.style.backgroundColor = '#' + bkgBright; + createCanvas(); + + if (context == undefined) { + document.body.innerHTML = '\ +<br/>Recentrifuge: Sorry, this browser does not support HTML5 (please see \ +<a href="https://github.com/khyox/recentrifuge/wiki/Browser-support">Browser support</a>).\ + '; + return; + } + + if (typeof context.fillText != 'function') { + document.body.innerHTML = '\ +<br/>Recentrifuge: Sorry, this browser does not support HTML5 canvas text (please see \ +<a href="https://github.com/khyox/recentrifuge/wiki/Browser-support">Browser support</a>).\ + '; + return; + } + + resize(); + + var kronaElement = document.getElementsByTagName('krona')[0]; + + var magnitudeName; + var hueName; + var hueDefault; + var hueStart; + var hueEnd; + var valueStart; + var valueEnd; + + if (kronaElement.getAttribute('collapse') !== undefined) { + collapse = kronaElement.getAttribute('collapse') === 'true'; + } + + if (kronaElement.getAttribute('key') !== undefined) { + showKeys = kronaElement.getAttribute('key') === 'true'; + } + + if (kronaElement.getAttribute('chart') !== undefined) { + switch (kronaElement.getAttribute('chart')) { + case 'TAXOMIC': + chart = ChartEnum.TAXOMIC; + fontFamily = 'Ubuntu' + fontSize = 11 + break; + case 'GENOMIC': + chart = ChartEnum.GENOMIC; + fontFamily = 'Saira Condensed' + fontSize = 12 + break; + } + } + + for + ( + var element = getFirstChild(kronaElement); + element; + element = getNextSibling(element) + ) { + switch (element.tagName.toLowerCase()) { + case 'attributes': + magnitudeName = element.getAttribute('magnitude'); + // + for + ( + var attributeElement = getFirstChild(element); + attributeElement; + attributeElement = getNextSibling(attributeElement) + ) { + var tag = attributeElement.tagName.toLowerCase(); + + if (tag == 'attribute') { + var attribute = new Attribute(); + attribute.name = + attributeElement.firstChild.nodeValue.toLowerCase(); + attribute.displayName = + attributeElement.getAttribute('display'); + + if (attributeElement.getAttribute('tip')) { + attribute.tip = + attributeElement.getAttribute('tip'); + } + + if (attributeElement.getAttribute('hrefBase')) { + attribute.hrefBase = + attributeElement.getAttribute('hrefBase'); + } + + if (attributeElement.getAttribute('target')) { + attribute.target = + attributeElement.getAttribute('target'); + } + + if (attribute.name === magnitudeName) { + magnitudeIndex = attributes.length; + } + + if (attributeElement.getAttribute('listAll')) { + attribute.listAll = + attributeElement.getAttribute('listAll').toLowerCase(); + } + else if (attributeElement.getAttribute('listNode')) { + attribute.listNode = + attributeElement.getAttribute('listNode').toLowerCase(); + } + else if (attributeElement.getAttribute('dataAll')) { + attribute.dataAll = + attributeElement.getAttribute('dataAll').toLowerCase(); + } + else if (attributeElement.getAttribute('dataNode')) { + attribute.dataNode = + attributeElement.getAttribute('dataNode').toLowerCase(); + } + + if (attributeElement.getAttribute('postUrl')) { + attribute.postUrl = + attributeElement.getAttribute('postUrl'); + } + + if (attributeElement.getAttribute('postVar')) { + attribute.postVar = + attributeElement.getAttribute('postVar'); + } + + if (attributeElement.getAttribute('mono')) { + attribute.mono = true; + } + + attributes.push(attribute); + } + else if (tag == 'list') { + var attribute = new Attribute(); + + attribute.name = attributeElement.firstChild.nodeValue; + attribute.list = true; + attributes.push(attribute); + } + else if (tag == 'data') { + var attribute = new Attribute(); + + attribute.name = attributeElement.firstChild.nodeValue; + attribute.data = true; + attributes.push(attribute); + + var enableScript = document.createElement('script'); + var date = new Date(); + enableScript.src = + attributeElement.getAttribute('enable') + '?' + + date.getTime(); + document.body.appendChild(enableScript); + } + } + break; + + case 'color': + hueName = element.getAttribute('attribute'); + hueStart = Number(element.getAttribute('hueStart')) / 360; + hueEnd = Number(element.getAttribute('hueEnd')) / 360; + valueStart = Number(element.getAttribute('valueStart')); + valueEnd = Number(element.getAttribute('valueEnd')); + // + interpolateHue(hueStart, hueEnd, valueStart, valueEnd); + // + if (element.getAttribute('default') == 'true') { + hueDefault = true; + } + break; + + case 'datasets': + datasetNames = []; + stats = []; + numRawSamples = element.getAttribute('rawSamples'); + var i = 0; + for (var j = getFirstChild(element); j; j = getNextSibling(j)) { + var datasetName = j.firstChild.nodeValue; + datasetNames.push(datasetName); + if (i < numRawSamples) { // Get stats of raw samples + var stat = new SampleStats( + datasetName, + j.getAttribute('isctr'), + j.getAttribute('sread'), + j.getAttribute('sclas'), + j.getAttribute('sfilt'), + j.getAttribute('scmin'), + j.getAttribute('scavg'), + j.getAttribute('scmax'), + j.getAttribute('lnmin'), + j.getAttribute('lnavg'), + j.getAttribute('lnmax'), + j.getAttribute('tclas'), + j.getAttribute('tfilt'), + j.getAttribute('tfold') + ); + stats.push(stat) + } + } + datasets = datasetNames.length; + break; + + case 'node': + head = loadTreeDOM + ( + element, + magnitudeName, + hueName, + hueStart, + hueEnd, + valueStart, + valueEnd + ); + break; + } + } + + // get GET options + // + var urlHalves = String(document.location).split('?'); + var datasetDefault = 0; + var maxDepthDefault; + var nodeDefault = 0; + // + if (urlHalves[1]) { + var vars = urlHalves[1].split('&'); + + for (i = 0; i < vars.length; i++) { + var pair = vars[i].split('='); + + switch (pair[0]) { + case 'collapse': + collapse = pair[1] == 'true'; + break; + + case 'color': + hueDefault = pair[1] == 'true'; + break; + + case 'dataset': + datasetDefault = Number(pair[1]); + break; + + case 'depth': + maxDepthDefault = Number(pair[1]) + 1; + break; + + case 'key': + showKeys = pair[1] == 'true'; + break; + + case 'font': + fontSize = Number(pair[1]); + break; + + case 'node': + nodeDefault = Number(pair[1]); + break; + + default: + getVariables.push(pair[0] + '=' + pair[1]); + break; + } + } + } + + addOptionElements(hueName, hueDefault); + if (datasets > 1) { + if (datasets > numRawSamples) { // Check for cross-analysis samples + selectRank(DEFAULT_RANK); + } else { + selectRank(NO_RANK); + } + } + setCallBacks(); + + head.sort(); + maxAbsoluteDepth = 0; + selectDataset(datasetDefault); + + if (maxDepthDefault && maxDepthDefault < head.maxDepth) { + maxAbsoluteDepth = maxDepthDefault; + } + else { + maxAbsoluteDepth = head.maxDepth; + } + + selectNode(nodes[nodeDefault]); + + setInterval(update, 20); + + window.onresize = handleResize; + updateMaxAbsoluteDepth(); + updateViewNeeded = true; +} + +function loadTreeDOM +(domNode, + magnitudeName, + hueName, + hueStart, + hueEnd, + valueStart, + valueEnd) { + var newNode = new Node(); + + newNode.name = domNode.getAttribute('name'); + + if (domNode.getAttribute('href')) { + newNode.href = domNode.getAttribute('href'); + } + + if (hueName) { + newNode.hues = new Array(); + } + + for (var i = getFirstChild(domNode); i; i = getNextSibling(i)) { + switch (i.tagName.toLowerCase()) { + case 'node': + var newChild = loadTreeDOM + ( + i, + magnitudeName, + hueName, + hueStart, + hueEnd, + valueStart, + valueEnd + ); + newChild.parent = newNode; + newNode.children.push(newChild); + break; + + default: + var attributeName = i.tagName.toLowerCase(); + var index = attributeIndex(attributeName); + // + newNode.attributes[index] = new Array(); + // + for (var j = getFirstChild(i); j; j = getNextSibling(j)) { + if (attributes[index] == undefined) { + var x = 5; + } + if (attributes[index].list) { + newNode.attributes[index].push(new Array()); + + for (var k = getFirstChild(j); k; k = getNextSibling(k)) { + newNode.attributes[index][ + newNode.attributes[ + index].length - 1].push( + k.firstChild.nodeValue); + } + } + else { + var value = j.firstChild ? j.firstChild.nodeValue : ''; + + if (j.getAttribute('href')) { + var target; + + if (attributes[index].target) { + target = ' target="' + + attributes[index].target + '"'; + } + + value = '<a href="' + attributes[index].hrefBase + + j.getAttribute('href') + '"' + + target + '>' + value + '</a>'; + } + + newNode.attributes[index].push(value); + } + } + // + if (attributeName == magnitudeName + || attributeName == hueName) { + for (j = 0; j < datasets; j++) { + // j is the dataset index (goes from 0 to datasets-1) + var value = newNode.attributes[index][j] + == undefined ? 0 : Number(newNode.attributes[index][j]); + + newNode.attributes[index][j] = value; + + if (attributeName == hueName) { + var hue = lerp + ( + value, + valueStart, + valueEnd, + hueStart, + hueEnd + ); + + if (hue < hueStart == hueStart < hueEnd) { + hue = hueStart; + } + else if (hue > hueEnd == hueStart < hueEnd) { + hue = hueEnd; + } + + newNode.hues[j] = hue; + } + } + + if (attributeName == hueName) { + newNode.hue = new Tween(newNode.hues[0], + newNode.hues[0]); + } + } + break; + } + } + + return newNode; +} + +function maxAbsoluteDepthDecrease() { + if (maxAbsoluteDepth > 2) { + maxAbsoluteDepth--; + head.setMaxDepths(); + handleResize(); + } +} + +function maxAbsoluteDepthIncrease() { + if (maxAbsoluteDepth < head.maxDepth) { + maxAbsoluteDepth++; + head.setMaxDepths(); + handleResize(); + } +} + +function measureText(text, bold) { + context.font = bold ? fontBold : fontNormal; + var dim = context.measureText(text); + return dim.width; +} + +function min(a, b) { + return a < b ? a : b; +} + +function minWidth() { + // Min wedge width (at center) for displaying a node (or for displaying a + // label if it's at the highest level being viewed, multiplied by 2 to make + // further calculations simpler + + return (fontSize * 2.3); +} + +function mouseMove(e) { + mouseX = e.pageX; + mouseY = e.pageY - headerHeight; + mouseXRel = (mouseX - centerX) * backingScale() + mouseYRel = (mouseY - centerY) * backingScale() + + if (head && !quickLook) { + checkHighlight(); + } +} + +function mouseClick(e) { + // Event listener function for mouse click on CANVAS + if (highlightedNode == focusNode && focusNode != selectedNode + || selectedNode.hasParent(highlightedNode)) { + if (highlightedNode.hasChildren()) { + expand(highlightedNode); + } + } + else if (progress == 1)//( highlightedNode != selectedNode ) + { + setFocus(highlightedNode); +// document.body.style.cursor='ew-resize'; + draw(); + checkHighlight(); + var date = new Date(); + mouseDownTime = date.getTime(); + mouseDown = true; + var button = undefined; + for (var i = 0; i < canvasButtons.length; i++) { + if (canvasButtons[i].is_inside(e.pageX, e.pageY)) { + context.strokeStyle = '#CC0000'; + context.lineWidth = 2; + button = canvasButtons[i]; + context.strokeRect(button.x, button.y, button.w, button.h); + } + } + if (button) { + // Reorder the array of nodes only when needed + if (nodesIndex === undefined || !nodes.reduce( + function (acc, current, index) { + // Calculate deviation from id == index for every node + return acc + Math.abs(current.id - index) + }, 0)) { + nodes.sort(function (a, b) { + return b.getHue() - a.getHue() + }); + } + + function lookForLeaf(testIndex, reverse) { + // Look for nodes without children but with counts + for (; testIndex >= 0 && testIndex <= nodes.length - 1 + && !nodes[testIndex].isLeaf(); + reverse ? testIndex-- : testIndex++) { + } + if (testIndex >= 0 && testIndex <= nodes.length - 1 + && nodes[testIndex].isLeaf()) nodesIndex = testIndex; + } + + function lookForNode(testIndex, reverse) { + // Look for nodes with counts + for (; testIndex >= 0 && testIndex <= nodes.length - 1 + && nodes[testIndex].getHue() <= 0; + reverse ? testIndex-- : testIndex++) { + } + if (testIndex >= 0 && testIndex <= nodes.length - 1 + && nodes[testIndex].getHue() > 0) + nodesIndex = testIndex; + } + + switch (button.name) { + case 'mostScore': + nodesIndex = 0; + if (collapseCheckBox.checked) { + lookForLeaf(nodesIndex, false); + } else { + lookForNode(nodesIndex, false); + } + break; + case 'moreScore': + if (collapseCheckBox.checked) { + lookForLeaf(nodesIndex - 1, true); + } else { + lookForNode(nodesIndex - 1, true); + } + break; + case 'lessScore': + if (collapseCheckBox.checked) { + lookForLeaf(nodesIndex + 1, false); + } else { + lookForNode(nodesIndex + 1, false); + } + break; + case 'lestScore': + nodesIndex = nodes.length - 1; + if (collapseCheckBox.checked) { + lookForLeaf(nodesIndex, true); + } else { + lookForNode(nodesIndex, true); + } + break; + default: + alert('ERROR! Unknown button in canvas. Ignoring!') + } + search.value = nodes[nodesIndex].name; + onSearchChange(); + context.strokeStyle = '#CC0000'; + context.lineWidth = 2; + context.strokeRect(button.x, button.y, button.w, button.h); + setTimeout(function () { + drawLegend() + }, 700) + } + } +} + +function mouseUp(e) { + if (quickLook) { + navigateBack(); + quickLook = false; + } + + mouseDown = false; +} + +function navigateBack() { + if (nodeHistoryPosition > 0) { + nodeHistory[nodeHistoryPosition] = selectedNode; + nodeHistoryPosition--; + + if (nodeHistory[nodeHistoryPosition].collapse) { + collapseCheckBox.checked = collapse = false; + } + + setSelectedNode(nodeHistory[nodeHistoryPosition]); + updateDatasetButtons(); + updateView(); + } +} + +function navigateUp() { + if (selectedNode.getParent()) { + selectNode(selectedNode.getParent()); + updateView(); + } +} + +function navigateForward() { + if (nodeHistoryPosition < nodeHistory.length - 1) { + nodeHistoryPosition++; + var newNode = nodeHistory[nodeHistoryPosition]; + + if (newNode.collapse) { + collapseCheckBox.checked = collapse = false; + } + + if (nodeHistoryPosition == nodeHistory.length - 1) { + // this will ensure the forward button is disabled + + nodeHistory.length = nodeHistoryPosition; + } + + setSelectedNode(newNode); + updateDatasetButtons(); + updateView(); + } +} + +function nextDataset() { + var newDataset = currentDataset; + + do { + if (newDataset === datasets - 1) { + newDataset = 0; + } + else { + newDataset++; + } + } + while (datasetDropDown.options[newDataset].disabled + || datasetDropDown.options[newDataset].hidden) + + selectDataset(newDataset); +} + +function onDatasetChange() { + selectDataset(datasetDropDown.selectedIndex); + nodesIndex = undefined; +} + +function onKeyDown(event) { + if + ( + event.keyCode == 37 && + document.activeElement.id != 'search' && + document.activeElement.id != 'linkText' + ) { + navigateBack(); + event.preventDefault(); + } + else if + ( + event.keyCode == 39 && + document.activeElement.id != 'search' && + document.activeElement.id != 'linkText' + ) { + navigateForward(); + event.preventDefault(); + } + else if (event.keyCode == 38 && datasets > 1) { + prevDataset(); + + //if ( document.activeElement.id == 'datasets' ) + { + event.preventDefault(); + } + } + else if (event.keyCode == 40 && datasets > 1) { + nextDataset(); + + //if ( document.activeElement.id == 'datasets' ) + { + event.preventDefault(); + } + } + else if (event.keyCode == 9 && datasets > 1) { + selectLastDataset(); + event.preventDefault(); + } + else if (event.keyCode == 83) { + progress += .2; + } + else if (event.keyCode == 66) { + progress -= .2; + } + else if (event.keyCode == 70) { + progress = 1; + } +} + +function onKeyPress(event) { + if (event.keyCode == 38 && datasets > 1) { +// prevDataset(); + + //if ( document.activeElement.id == 'datasets' ) + { + event.preventDefault(); + } + } + else if (event.keyCode == 40 && datasets > 1) { +// nextDataset(); + + //if ( document.activeElement.id == 'datasets' ) + { + event.preventDefault(); + } + } +} + +function onKeyUp(event) { + if (event.keyCode == 27 && document.activeElement.id == 'search') { + search.value = ''; + onSearchChange(); + } + else if (event.keyCode == 38 && datasets > 1) { +// prevDataset(); + + //if ( document.activeElement.id == 'datasets' ) + { + event.preventDefault(); + } + } + else if (event.keyCode == 40 && datasets > 1) { +// nextDataset(); + + //if ( document.activeElement.id == 'datasets' ) + { + event.preventDefault(); + } + } +} + +function onRankChange() { + selectRank(rankDropDown.value); +} + +function onSearchChange() { + nSearchResults = 0; + head.search(); + + if (search.value == '') { + searchResults.innerHTML = ''; + } + else { + searchResults.innerHTML = nSearchResults + ' results'; + } + + setFocus(selectedNode); + draw(); +} + +function onSortChange() { + head.sort(); + head.setMagnitudes(0); + handleResize(); +} + +function post(url, variable, value, postWindow) { + var form = document.createElement('form'); + var input = document.createElement('input'); + var inputDataset = document.createElement('input'); + + form.appendChild(input); + form.appendChild(inputDataset); + + form.method = "POST"; + form.action = url; + + if (postWindow == undefined) { + form.target = '_blank'; + postWindow = window; + } + + input.type = 'hidden'; + input.name = variable; + input.value = value; + + inputDataset.type = 'hidden'; + inputDataset.name = 'dataset'; + inputDataset.value = currentDataset; + + postWindow.document.body.appendChild(form); + form.submit(); +} + +function prevDataset() { + var newDataset = currentDataset; + + do { + if (newDataset == 0) { + newDataset = datasets - 1; + } + else { + newDataset--; + } + } + while (datasetDropDown.options[newDataset].disabled + || datasetDropDown.options[newDataset].hidden); + + selectDataset(newDataset); +} + +function radiusDecrease() { + if (bufferFactor < .309) { + bufferFactor += .03; + updateViewNeeded = true; + } +} + +function radiusIncrease() { + if (bufferFactor > .041) { + bufferFactor -= .03; + updateViewNeeded = true; + } +} + +function resetKeyOffset() { + currentKey = 1; + keyMinTextLeft = centerX + gRadius + buffer - buffer / (keys + 1) / + 2 + fontSize / 2; + keyMinAngle = 0; +} + +function rgbText(r, g, b) { + var rgbArray = + [ + "rgb(", + Math.floor(r), + ",", + Math.floor(g), + ",", + Math.floor(b), + ")" + ]; + + return rgbArray.join(''); +} + +function round(number) { + if (number >= 1 || number <= -1) { + return number.toFixed(0); + } + else { + return number.toPrecision(1); + } +} + +function roundedRectangle(x, y, width, height, radius, fill, stroke) { + // Optionals: radius, stroke, fill + if (typeof stroke === 'undefined') { + stroke = true; + } + if (typeof radius === 'undefined') { + radius = 5; + } else if (typeof radius === 'number') { + if (radius * 2 > width) { + radius = width / 2; + } + if (radius * 2 > height) { + radius = height / 2; + } + radius = {tl: radius, tr: radius, br: radius, bl: radius}; + } else { + var defaultRadius = {tl: 0, tr: 0, br: 0, bl: 0}; + for (var side in defaultRadius) { + radius[side] = radius[side] || defaultRadius[side]; + } + } + + context.beginPath(); + context.arc(x + radius.tl, y + radius.tl, radius.tl, + Math.PI, Math.PI * 3 / 2, false); + context.lineTo(x + width - radius.tr, y); + context.arc(x + width - radius.tr, y + radius.tr, radius.tr, + Math.PI * 3 / 2, Math.PI * 2, false); + context.lineTo(x + width, y + height - radius.br); + context.arc(x + width - radius.br, y + height - radius.br, radius.br, + 0, Math.PI / 2, false); + context.lineTo(x + radius.bl, y + height); + context.arc(x + radius.bl, y + height - radius.bl, radius.bl, + Math.PI / 2, Math.PI, false); + context.lineTo(x, y + radius.tl); + + if (fill) { + context.fill(); + } + if (stroke) { + context.stroke(); + } +} + +function passClick(e) { + mouseClick(e); +} + +function searchResultString(results) { + var searchResults = this.searchResults; + + if (this.isSearchResult) { + // don't count ourselves + searchResults--; + } + + return ' - ' + results + (results > 1 ? ' results' : ' result'); +} + +function setCallBacks() { + canvas.onselectstart = function () { + return false; + } // prevent unwanted highlighting + options.onselectstart = function () { + return false; + } // prevent unwanted highlighting + document.onmousemove = mouseMove; + window.onblur = focusLost; + window.onmouseout = focusLost; + document.onkeyup = onKeyUp; + document.onkeydown = onKeyDown; + canvas.onmousedown = mouseClick; + document.onmouseup = mouseUp; + keyControl.onclick = toggleKeys; + collapseCheckBox = document.getElementById('collapse'); + collapseCheckBox.checked = collapse; + collapseCheckBox.onclick = handleResize; + collapseCheckBox.onmousedown = suppressEvent; + maxAbsoluteDepthText = document.getElementById('maxAbsoluteDepth'); + maxAbsoluteDepthButtonDecrease = + document.getElementById('maxAbsoluteDepthDecrease'); + maxAbsoluteDepthButtonIncrease = + document.getElementById('maxAbsoluteDepthIncrease'); + maxAbsoluteDepthButtonDecrease.onclick = maxAbsoluteDepthDecrease; + maxAbsoluteDepthButtonIncrease.onclick = maxAbsoluteDepthIncrease; + maxAbsoluteDepthButtonDecrease.onmousedown = suppressEvent; + maxAbsoluteDepthButtonIncrease.onmousedown = suppressEvent; + fontSizeText = document.getElementById('fontSize'); + fontSizeButtonDecrease = document.getElementById('fontSizeDecrease'); + fontSizeButtonIncrease = document.getElementById('fontSizeIncrease'); + fontSizeButtonDecrease.onclick = fontSizeDecrease; + fontSizeButtonIncrease.onclick = fontSizeIncrease; + fontSizeButtonDecrease.onmousedown = suppressEvent; + fontSizeButtonIncrease.onmousedown = suppressEvent; + bkgBrightButtonDecrease = document.getElementById('bkgBrightDecrease'); + bkgBrightButtonIncrease = document.getElementById('bkgBrightIncrease'); + bkgBrightButtonDecrease.onclick = bkgBrightDecrease; + bkgBrightButtonIncrease.onclick = bkgBrightIncrease; + bkgBrightButtonDecrease.onmousedown = suppressEvent; + bkgBrightButtonIncrease.onmousedown = suppressEvent; + radiusButtonDecrease = document.getElementById('radiusDecrease'); + radiusButtonIncrease = document.getElementById('radiusIncrease'); + radiusButtonDecrease.onclick = radiusDecrease; + radiusButtonIncrease.onclick = radiusIncrease; + radiusButtonDecrease.onmousedown = suppressEvent; + radiusButtonIncrease.onmousedown = suppressEvent; + maxAbsoluteDepth = 0; + backButton = document.getElementById('back'); + backButton.onclick = navigateBack; + backButton.onmousedown = suppressEvent; + forwardButton = document.getElementById('forward'); + forwardButton.onclick = navigateForward; + forwardButton.onmousedown = suppressEvent; + snapshotButton = document.getElementById('snapshot'); + snapshotButton.onclick = snapshot; + snapshotButton.onmousedown = suppressEvent; + detailsName = document.getElementById('detailsName'); + detailsExpand = document.getElementById('detailsExpand'); + detailsInfo = document.getElementById('detailsInfo'); + search = document.getElementById('search'); + search.onkeyup = onSearchChange; + search.onmousedown = suppressEvent; + searchResults = document.getElementById('searchResults'); + useHueDiv = document.getElementById('useHueDiv'); + linkButton = document.getElementById('linkButton'); + linkButton.onclick = showLink; + linkButton.onmousedown = suppressEvent; + linkText = document.getElementById('linkText'); + linkText.onblur = hideLink; + linkText.onmousedown = suppressEvent; + hide(linkText); + var helpButton = document.getElementById('help'); + helpButton.onmousedown = suppressEvent; + var searchClear = document.getElementById('searchClear'); + searchClear.onmousedown = suppressEvent; + if (datasets > 1) { + datasetDropDown.onmousedown = suppressEvent; + var prevDatasetButton = document.getElementById('prevDataset'); + prevDatasetButton.onmousedown = suppressEvent; + var nextDatasetButton = document.getElementById('nextDataset'); + nextDatasetButton.onmousedown = suppressEvent; + var lastDatasetButton = document.getElementById('lastDataset'); + lastDatasetButton.onmousedown = suppressEvent; + } + + image = document.getElementById('hiddenImage'); + + if (image.complete) { + hiddenPattern = context.createPattern(image, 'repeat'); + } + else { + image.onload = function () { + hiddenPattern = context.createPattern(image, 'repeat'); + } + } + + var loadingImageElement = document.getElementById('loadingImage'); + + if (loadingImageElement) { + loadingImage = loadingImageElement.src; + } +} + +function selectDataset(newDataset) { + lastDataset = currentDataset; + currentDataset = newDataset + if (datasets > 1) { + datasetDropDown.selectedIndex = currentDataset; + updateDatasetButtons(); + datasetAlpha.start = 1.5; + datasetChanged = true; + } + head.setMagnitudes(0); + head.setDepth(1, 1); + head.setMaxDepths(); + handleResize(); +} + +function selectLastDataset() { + selectDataset(lastDataset); +} + +function selectNode(newNode) { + if (selectedNode != newNode) { + // truncate history at current location to create a new branch + // + nodeHistory.length = nodeHistoryPosition; + + if (selectedNode != 0) { + nodeHistory.push(selectedNode); + nodeHistoryPosition++; + } + + setSelectedNode(newNode); + //updateView(); + } + + updateDatasetButtons(); +} + +function selectRank(rank) { + rankDropDown.value = rank; + currentRank = rank; + datasetsVisible = 0; + for (var i = 0; i < datasets; i++) { + if (currentRank === 'ALL' + || i < numRawSamples + || (currentRank !== NO_RANK && ( + datasetNames[i].endsWith('EXCLUSIVE_' + currentRank) || + datasetNames[i].endsWith('SHARED_' + currentRank) || + datasetNames[i].endsWith('CONTROL_SHARED' + currentRank) || + datasetNames[i].endsWith('CTRL_' + currentRank)))) { + datasetDropDown.options[i].hidden = false; + datasetsVisible++; + } else { + datasetDropDown.options[i].hidden = true; + } + } + if (datasetDropDown.options[currentDataset].hidden === true) { + selectDataset(0); + } else { + selectDataset(currentDataset); + } + datasetDropDown.size = (datasetsVisible < DATASET_MAX_SIZE ? + datasetsVisible : DATASET_MAX_SIZE); +} + +function setFocus(node) { + if (node == focusNode) { +// return; + } + + focusNode = node; + + if (node.href) { + detailsName.innerHTML = + '<a target="_blank" href="' + node.href + '">' + node.name + '</a>'; + } + else { + detailsName.innerHTML = node.name; + } + + var table = '<table>'; + + table += '<tr><td></td></tr>'; + + for (var i = 0; i < node.attributes.length; i++) { + if (attributes[i].displayName && node.attributes[i] != undefined) { + var index = node.attributes[i].length == 1 + && attributes[i].mono ? 0 : currentDataset; + + if (typeof node.attributes[i][currentDataset] == 'number' + || node.attributes[i][index] != undefined + && node.attributes[i][currentDataset] != '') { + var value = node.attributes[i][index]; + + if (attributes[i].listNode != undefined) { + value = + '<a href="" onclick="showList(' + + attributeIndex(attributes[i].listNode) + ',' + i + + ',false);return false;" title="Show list">' + + value + '</a>'; + } + else if (attributes[i].listAll != undefined) { + value = + '<a href="" onclick="showList(' + + attributeIndex(attributes[i].listAll) + ',' + i + + ',true);return false;" title="Show list">' + + value + '</a>'; + } + else if (attributes[i].dataNode != undefined && dataEnabled) { + value = + '<a href="" onclick="showData(' + + attributeIndex(attributes[i].dataNode) + ',' + i + + ',false);return false;" title="Show data">' + + value + '</a>'; + } + else if (attributes[i].dataAll != undefined && dataEnabled) { + value = + '<a href="" onclick="showData(' + + attributeIndex(attributes[i].dataAll) + ',' + i + + ',true);return false;" title="Show data">' + + value + '</a>'; + } + + table += + '<tr><td class="CellWithTooltip">' + + '<strong>' + attributes[i].displayName + ':</strong>' + + '<span class="Tooltip">' + + attributes[i].tip + '</span>' + + '</td><td>' + value + '</td></tr>'; + } + } + } + + table += '</table>'; + detailsInfo.innerHTML = table; + + detailsExpand.disabled = !focusNode.hasChildren() + || focusNode == selectedNode; +} + +function setSelectedNode(newNode) { + if (selectedNode && selectedNode.hasParent(newNode)) { + zoomOut = true; + } + else { + zoomOut = false; + } + + selectedNodeLast = selectedNode; + selectedNode = newNode; + + //if ( focusNode != selectedNode ) + { + setFocus(selectedNode); + } +} + +function waitForData(dataWindow, target, title, time, postUrl, postVar) { + if (nodeData.length == target) { + if (postUrl != undefined) { + for (var i = 0; i < nodeData.length; i++) { + nodeData[i] = nodeData[i].replace(/\n/g, ','); + } + + var postString = nodeData.join(''); + postString = postString.slice(0, -1); + + dataWindow.document.body.removeChild(dataWindow.document.getElementById('loading')); + document.body.removeChild(document.getElementById('data')); + + post(postUrl, postVar, postString, dataWindow); + } + else { + //dataWindow.document.body.removeChild(dataWindow.document.getElementById('loading')); + //document.body.removeChild(document.getElementById('data')); + + dataWindow.document.open(); + dataWindow.document.write('<pre>' + nodeData.join('') + '</pre>'); + dataWindow.document.close(); + } + + dataWindow.document.title = title; // replace after document.write() + } + else { + var date = new Date(); + + if (date.getTime() - time > 10000) { + dataWindow.document.body.removeChild(dataWindow.document.getElementById('loading')); + document.body.removeChild(document.getElementById('data')); + dataWindow.document.body.innerHTML = + 'Timed out loading supplemental files for:<br/>' + document.location; + } + else { + setTimeout(function () { + waitForData(dataWindow, target, title, time, postUrl, postVar); + }, 100); + } + } +} + +function data(newData) { + nodeData.push(newData); +} + +function enableData() { + dataEnabled = true; +} + +function showData(indexData, indexAttribute, summary) { + var dataWindow = window.open('', '_blank'); + var title = 'Re@ - ' + attributes[indexAttribute].displayName + + ' - ' + focusNode.name; + dataWindow.document.title = title; + + nodeData = new Array(); + + if (dataWindow && dataWindow.document && dataWindow.document.body != null) { + //var loadImage = document.createElement('img'); + //loadImage.src = "file://localhost/Users/ondovb/Krona/KronaTools/img/loading.gif"; + //loadImage.id = "loading"; + //loadImage.alt = "Loading..."; + //dataWindow.document.body.appendChild(loadImage); + dataWindow.document.body.innerHTML = + '<img id="loading" src="' + loadingImage + '" alt="Loading..."></img>'; + } + + var scripts = document.createElement('div'); + scripts.id = 'data'; + document.body.appendChild(scripts); + + var files = focusNode.getData(indexData, summary); + + var date = new Date(); + var time = date.getTime(); + + for (var i = 0; i < files.length; i++) { + var script = document.createElement('script'); + script.src = files[i] + '?' + time; + scripts.appendChild(script); + } + + waitForData(dataWindow, files.length, title, time, + attributes[indexAttribute].postUrl, attributes[indexAttribute].postVar); + + return false; +} + +function showList(indexList, indexAttribute, summary) { + var list = focusNode.getList(indexList, summary); + + if (attributes[indexAttribute].postUrl != undefined) { + post(attributes[indexAttribute].postUrl, + attributes[indexAttribute].postVar, list.join(',')); + } + else { + var dataWindow = window.open('', '_blank'); + + if (true || navigator.appName == 'Microsoft Internet Explorer') // :( + { + dataWindow.document.open(); + dataWindow.document.write('<pre>' + list.join('\n') + '</pre>'); + dataWindow.document.close(); + } + else { + var pre = document.createElement('pre'); + dataWindow.document.body.appendChild(pre); + pre.innerHTML = list; + } + + dataWindow.document.title = 'Re@ - ' + + attributes[indexAttribute].displayName + ' - ' + focusNode.name; + } +} + +function snapshot() { + svg = svgHeader(); + + resetKeyOffset(); + + snapshotMode = true; + + selectedNode.draw(false, true); + selectedNode.draw(true, true); + + if (focusNode != 0 && focusNode != selectedNode) { + context.globalAlpha = 1; + focusNode.drawHighlight(true); + } + + if (hueDisplayName && useHue()) { + drawLegendSVG(); + } + + snapshotMode = false; + + svg += svgFooter(); + + var snapshotWindow = window.open('', '_blank', '', 'replace=false'); + snapshotWindow.document.write('<html><body>' + + '<button title="Download Rec@ntrifuge snapshot as SVG file" ' + + 'onclick="document.getElementById(\'link\').click()">' + + 'Download</button><a id="link" href="data:image/svg+xml,' + + encodeURIComponent(svg) + '" download="Recfg_snapshot.svg" hidden>' + + 'Download</a><br></html></body>'); + snapshotWindow.document.title = 'Re@ [snapshot] ' + + location.href.split("/").slice(-1)[0].split(".html")[0]; + snapshotWindow.document.write(svg); +} + +function save() { + alert(document.body.innerHTML); +} + +function spacer() { + if (snapshotMode) { + return '   '; + } + else { + return ' '; + } +} + +function suppressEvent(e) { + e.cancelBubble = true; + if (e.stopPropagation) e.stopPropagation(); +} + +function svgFooter() { + return '</svg>'; +} + +function svgHeader() { + var patternWidth = fontSize * .6;//radius / 50; + + return '\ +<?xml version="1.0" standalone="no"?>\ +<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" \ + "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">\ +<svg width="' + imageWidth + '" height="' + imageHeight + '" version="1.1"\ + xmlns="http://www.w3.org/2000/svg">\ +<title>Rec@ntrifuge (snapshot) - ' + + (datasets > 1 ? datasetNames[currentDataset] + ' - ' : '') + + selectedNode.name + + '</title>\ +<defs>\ + <style type="text/css">\ + @import url("https://fonts.googleapis.com/css?family=' + fontFamily + '");\ + text {font-size: ' + fontSize + 'px; font-family: ' + fontFamily + + '; dominant-baseline:central}\ + path {stroke-width:' + thinLineWidth * fontSize / 12 + ';}\ + path.wedge {stroke:none}\ + path.line {fill:none;stroke:black;}\ + line {stroke:black;stroke-width:' + thinLineWidth * fontSize / 12 + ';}\ + line.tick {stroke-width:' + thinLineWidth * fontSize / 6 + ';}\ + line.pattern {stroke-width:' + thinLineWidth * fontSize / 18 + ';}\ + circle {fill:none;stroke:black;stroke-width:' + thinLineWidth + * fontSize / 12 + ';}\ + rect {stroke:black;stroke-width:' + thinLineWidth * fontSize / 12 + ';}\ + .highlight {stroke:black;stroke-width:' + highlightLineWidth + * fontSize / 12 + ';}\ + .searchHighlight {fill:rgb(255, 255, 100);stroke:none;}\ + </style>\ +<pattern id="hiddenPattern" patternUnits="userSpaceOnUse" \ +x="0" y="0" width="' + patternWidth + '" height="' + patternWidth + '">\ +<line class="pattern" x1="0" y1="0" x2="' + patternWidth / 2 + '" y2="' + + patternWidth / 2 + '"/>\ +<line class="pattern" x1="' + patternWidth / 2 + '" y1="' + patternWidth + + '" x2="' + patternWidth + '" y2="' + patternWidth / 2 + '"/>\ +</pattern>\ +</defs>\ +'; +} + +function svgText(text, x, y, anchor, bold, color) { + if (typeof(anchor) == 'undefined') { + anchor = 'start'; + } + + if (color == undefined) { + color = 'black'; + } + + return '<text x="' + x + '" y="' + y + + '" style="font-color:' + color + ';font-weight:' + + (bold ? 'bold' : 'normal') + + '" text-anchor="' + anchor + '">' + text + '</text>'; +} + +function toggleKeys() { + if (showKeys) { + keyControl.value = '…'; + showKeys = false; + } + else { + keyControl.value = 'x'; + showKeys = true; + } + + updateKeyControl(); + + if (progress == 1) { + draw(); + } +} + +function update() { + if (!head) { + return; + } + + if (mouseDown && focusNode != selectedNode) { + var date = new Date(); + + if (date.getTime() - mouseDownTime > quickLookHoldLength) { + if (focusNode.hasChildren()) { + expand(focusNode); + quickLook = true; + } + } + } + + if (updateViewNeeded) { + resize(); + mouseX = -1; + mouseY = -1; + + collapse = collapseCheckBox.checked; + compress = true;//compressCheckBox.checked; + shorten = true;//shortenCheckBox.checked; + + checkSelectedCollapse(); + updateMaxAbsoluteDepth(); + + if (focusNode.getCollapse() || focusNode.depth > maxAbsoluteDepth) { + setFocus(selectedNode); + } + else { + setFocus(focusNode); + } + + updateView(); + + updateViewNeeded = false; + } + + var date = new Date(); + progress = (date.getTime() - tweenStartTime) / tweenLength; +// progress += .01; + + if (progress >= 1) { + progress = 1; + } + + if (progress != progressLast) { + tweenFactor =// progress; + (1 / (1 + Math.exp(-tweenCurvature * (progress - .5))) - .5) / + (tweenMax - .5) / 2 + .5; + + if (progress == 1) { + snapshotButton.disabled = false; + zoomOut = false; + + //updateKeyControl(); + + if (!quickLook) { + //checkHighlight(); + } + + + if (fpsDisplay) { + fpsDisplay.innerHTML = 'fps: ' + + Math.round(tweenFrames * 1000 / tweenLength); + } + } + + draw(); + } + + progressLast = progress; +} + +function updateDatasetButtons() { + if (datasets == 1) { + return; + } + + var node = selectedNode ? selectedNode : head; + + datasetButtonLast.disabled = + node.attributes[magnitudeIndex][lastDataset] == 0; + + datasetButtonPrev.disabled = true; + datasetButtonNext.disabled = true; + + for (var i = 0; i < datasets; i++) { + var disable = node.attributes[magnitudeIndex][i] == 0; + + datasetDropDown.options[i].disabled = disable; + + if (!disable) { + if (i != currentDataset) { + datasetButtonPrev.disabled = false; + datasetButtonNext.disabled = false; + } + } + } +} + +function updateDatasetWidths() { + if (datasets > 1) { + for (var i = 0; i < datasets; i++) { + context.font = fontBold; + var dim = context.measureText(datasetNames[i]); + datasetWidths[i] = dim.width; + } + } +} + +function updateKeyControl() { + if (keys == 0)//|| progress != 1 ) + { + keyControl.style.visibility = 'hidden'; + } + else { + keyControl.style.visibility = 'visible'; + keyControl.style.right = margin + 'px'; + + if (showKeys) { + keyControl.style.top = + imageHeight - + ( + keys * (keySize + keyBuffer) - + keyBuffer + + margin + + keyControl.clientHeight * 1.5 + ) + 'px'; + } + else { + keyControl.style.top = + (imageHeight - margin - keyControl.clientHeight) + 'px'; + } + } +} + +function updateView() { + if (selectedNode.depth > maxAbsoluteDepth - 1) { + maxAbsoluteDepth = selectedNode.depth + 1; + } + + highlightedNode = selectedNode; + + angleFactor = 2 * Math.PI / (selectedNode.magnitude); + + maxPossibleDepth = Math.floor(gRadius / (fontSize * minRingWidthFactor)); + + if (maxPossibleDepth < 4) { + maxPossibleDepth = 4; + } + + var minRadiusInner = fontSize * 8 / gRadius; + var minRadiusFirst = fontSize * 6 / gRadius; + var minRadiusOuter = fontSize * 5 / gRadius; + + if (.25 < minRadiusInner) { + minRadiusInner = .25; + } + + if (.15 < minRadiusFirst) { + minRadiusFirst = .15; + } + + if (.15 < minRadiusOuter) { + minRadiusOuter = .15; + } + + // visibility of nodes depends on the depth they are displayed at, + // so we need to set the max depth assuming they can all be displayed + // and iterate it down based on the deepest child node we can display + // + var maxDepth; + var newMaxDepth = selectedNode.getMaxDepth() - selectedNode.getDepth() + 1; + // + do { + maxDepth = newMaxDepth; + + if (!compress && maxDepth > maxPossibleDepth) { + maxDepth = maxPossibleDepth; + } + + if (compress) { + compressedRadii = new Array(maxDepth); + + compressedRadii[0] = minRadiusInner; + + var offset = 0; + + while + ( + lerp + ( + Math.atan(offset + 2), + Math.atan(offset + 1), + Math.atan(maxDepth + offset - 1), + minRadiusInner, + 1 - minRadiusOuter + ) - minRadiusInner > minRadiusFirst && + offset < 10 + ) { + offset++; + } + + offset--; + + for (var i = 1; i < maxDepth; i++) { + compressedRadii[i] = lerp + ( + Math.atan(i + offset), + Math.atan(offset), + Math.atan(maxDepth + offset - 1), + minRadiusInner, + 1 - minRadiusOuter + ) + } + } + else { + nodeRadius = 1 / maxDepth; + } + + newMaxDepth = selectedNode.maxVisibleDepth(maxDepth); + + if (compress) { + if (newMaxDepth <= maxPossibleDepth) { +// compress + } + } + else { + if (newMaxDepth > maxPossibleDepth) { + newMaxDepth = maxPossibleDepth; + } + } + } + while (newMaxDepth < maxDepth); + + maxDisplayDepth = maxDepth; + + lightnessFactor = (lightnessMax - lightnessBase) + / (maxDepth > 8 ? 8 : maxDepth); + keys = 0; + + nLabelOffsets = new Array(maxDisplayDepth - 1); + labelOffsets = new Array(maxDisplayDepth - 1); + labelLastNodes = new Array(maxDisplayDepth - 1); + labelFirstNodes = new Array(maxDisplayDepth - 1); + + for (var i = 0; i < maxDisplayDepth - 1; i++) { + if (compress) { + if (i == maxDisplayDepth - 1) { + nLabelOffsets[i] = 0; + } + else { + var width = + (compressedRadii[i + 1] - compressedRadii[i]) * + gRadius; + + nLabelOffsets[i] = Math.floor(width / fontSize / 1.2); + + if (nLabelOffsets[i] > 2) { + nLabelOffsets[i] = min + ( + Math.floor(width / fontSize / 1.75), + 5 + ); + } + } + } + else { + nLabelOffsets[i] = Math.max + ( + Math.floor(Math.sqrt((nodeRadius * gRadius / fontSize)) * 1.5), + 3 + ); + } + + labelOffsets[i] = Math.floor((nLabelOffsets[i] - 1) / 2); + labelLastNodes[i] = new Array(nLabelOffsets[i] + 1); + labelFirstNodes[i] = new Array(nLabelOffsets[i] + 1); + + for (var j = 0; j <= nLabelOffsets[i]; j++) { + // these arrays will allow nodes with neighboring labels to link to + // each other to determine max label length + + labelLastNodes[i][j] = 0; + labelFirstNodes[i][j] = 0; + } + } + + fontSizeText.innerHTML = fontSize; + fontNormal = fontSize + 'px ' + fontFamily; + context.font = fontNormal; + fontBold = 'bold ' + fontSize + 'px ' + fontFamily; + tickLength = fontSize * .7; + + head.setTargets(0); + + keySize = ((imageHeight - margin * 3) * 1 / 2) / keys * 3 / 4; + + if (keySize > fontSize * maxKeySizeFactor) { + keySize = fontSize * maxKeySizeFactor; + } + + keyBuffer = keySize / 3; + + fontSizeLast = fontSize; + + if (datasetChanged) { + datasetChanged = false; + } + else { + datasetAlpha.start = 0; + } + + var date = new Date(); + tweenStartTime = date.getTime(); + progress = 0; + tweenFrames = 0; + + updateKeyControl(); + updateDatasetWidths(); + + document.title = ('Re@ - ' + + location.href.split("/").slice(-1)[0].split(".html")[0]); + updateNavigationButtons(); + snapshotButton.disabled = true; + + maxAbsoluteDepthText.innerHTML = maxAbsoluteDepth - 1; + + maxAbsoluteDepthButtonDecrease.disabled = (maxAbsoluteDepth == 2); + maxAbsoluteDepthButtonIncrease.disabled = + (maxAbsoluteDepth == head.maxDepth); + + bkgBrightButtonDecrease.disabled = (bkgBright == '555555'); + bkgBrightButtonIncrease.disabled = (bkgBright == 'ffffff'); + + if (collapse != collapseLast && search.value != '') { + onSearchChange(); + collapseLast = collapse; + } +} + +function updateMaxAbsoluteDepth() { + while (maxAbsoluteDepth > 1 && selectedNode.depth > maxAbsoluteDepth - 1) { + selectedNode = selectedNode.getParent(); + } +} + +function updateNavigationButtons() { + backButton.disabled = (nodeHistoryPosition == 0); +// upButton.disabled = (selectedNode.getParent() == 0); + forwardButton.disabled = (nodeHistoryPosition == nodeHistory.length); +} + +function useHue() { + return useHueCheckBox && useHueCheckBox.checked; +} + +/* +function zoomOut() +{ + return ( + selectedNodeLast != 0 && + selectedNodeLast.getDepth() < selectedNode.getDepth()); +} +*/</script></head><body><img id="hiddenImage" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAICAYAAADED76LAAAAAXNSR0IArs4c6QAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9oLCBQhNQwWVnsAAAAidEVYdENvbW1lbnQAQ3JlYXRlZCB3aXRoIEdJTVAgb24gYSBNYWOHqHdDAAABE0lEQVQYGQEIAff+AwAAABkAAAAAAAAA+gAAAAAAAAAAAAAAAAAAAAAAAAAMAwAAAAAAAAANAAAAAAAAAPoAAAAAAAAADAAAAAYAAAD0AwAAAPoAAAAAAAAAAAAAAPoAAAAMAAAADQAAAPoAAAD6AAAAAAAAAAAAAAAAAAAAAAwAAAAZAAAADAAAAAAAAAAAAAAAAAAAAAAAAAAADAAAABkAAAAMAAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAAGQAAAAwAAAAAAAAADAAAAAwAAAAABAAAAAAAAAAAAAAA8wAAAPQAAAAAAAAAAAAAAA0AAAAAAAAAAAwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwAAAAZRssKC5OpXwYAAAAASUVORK5CYII=" style="display:none"><img id="loadingImage" src="data:image/gif;base64,R0lGODlhEAAQAPIAAP///wAAAMLCwkJCQgAAAGJiYoKCgpKSkiH/C05FVFNDQVBFMi4wAwEAAAAh/hpDcmVhdGVkIHdpdGggYWpheGxvYWQuaW5mbwAh+QQJCgAAACwAAAAAEAAQAAADMwi63P4wyklrE2MIOggZnAdOmGYJRbExwroUmcG2LmDEwnHQLVsYOd2mBzkYDAdKa+dIAAAh+QQJCgAAACwAAAAAEAAQAAADNAi63P5OjCEgG4QMu7DmikRxQlFUYDEZIGBMRVsaqHwctXXf7WEYB4Ag1xjihkMZsiUkKhIAIfkECQoAAAAsAAAAABAAEAAAAzYIujIjK8pByJDMlFYvBoVjHA70GU7xSUJhmKtwHPAKzLO9HMaoKwJZ7Rf8AYPDDzKpZBqfvwQAIfkECQoAAAAsAAAAABAAEAAAAzMIumIlK8oyhpHsnFZfhYumCYUhDAQxRIdhHBGqRoKw0R8DYlJd8z0fMDgsGo/IpHI5TAAAIfkECQoAAAAsAAAAABAAEAAAAzIIunInK0rnZBTwGPNMgQwmdsNgXGJUlIWEuR5oWUIpz8pAEAMe6TwfwyYsGo/IpFKSAAAh+QQJCgAAACwAAAAAEAAQAAADMwi6IMKQORfjdOe82p4wGccc4CEuQradylesojEMBgsUc2G7sDX3lQGBMLAJibufbSlKAAAh+QQJCgAAACwAAAAAEAAQAAADMgi63P7wCRHZnFVdmgHu2nFwlWCI3WGc3TSWhUFGxTAUkGCbtgENBMJAEJsxgMLWzpEAACH5BAkKAAAALAAAAAAQABAAAAMyCLrc/jDKSatlQtScKdceCAjDII7HcQ4EMTCpyrCuUBjCYRgHVtqlAiB1YhiCnlsRkAAAOwAAAAAAAAAAAA==" style="display:none"><img id="logo" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAakAAABkCAYAAAA8Lc+FAAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAC4jAAAuIwF4pT92AAAAB3RJTUUH4gYZFgwotKLFqAAAIABJREFUeNrtnXmYVMXVh9/q7umZnhl2RBbZQUEFUVkVcQFxBTUK4kJiEgU1UWQgavw0kmiCIouKEbckYhAV3EBUNIgbKG4RIyC7CAqyD9t0z0x31/dHXXToube7bs/tmWGm3ufpR+ypvl1dt2796lSdOgcMBoPBYDAYDAaDwWAwGAwGg8FgMBgMBoPBYDAYDAaDwWAwGAwGg8Fg0EIA7Ty4jgR2AXtMkxoMBoPBS5GSHl6vCFgHfAEsAuYC200zGww1nvrWc6/DyUBhBb/PB5wInA70AZoCjYFgQrnpwF/M7Tm8kRl8RYE5wGmmmQ2GGk1DF+NCowp8Tx7we2CV5nc9ZG6NESnd14tAc9PcBoMRqTS/43jga5fjjhEpI1KuXluAU02TGwxGpFxyKRBJY8wxInWY46vk72sKvA2caZreYDBo0gt4Bsg2TVH7CFTBd+YCrwDdgbXmFhgMP/Ey0Eqj3C0ox6TaQB3Uvnau6R5GpCqTesDz1gwpZm7D4Ym8lTrEcjsh48cgfB2Jx5siRB0kdQEQFIE4AHIHiPUIsR5/fLmYEP7etJ4txwFHaz4/tYWxwJGmaxiRqgpOBkYCj5rbcJiI0hD8HJVzJkIMAM4kyskg/SBAShACS5zKfurn/0oJUZAFoU3KEhDvIvyviEn7dpjWNdjQECgwzWBEqiq5G/gnakPUUF3FaWz+8cjYNUiuBJp5cMmWwBUgr0BGp8kxue8i4/+mNPKCmEqxaXGDxVAg3zSDESld7gK2JbznAxpYrxNQ56FCLq7ZBBgGPG1uRTUUp4JQbyR3Eo+dn2gfeYgfKQeAGEBW6H45Wk6D4CNiyt5d5g7Ues52UXYfsBT4MeH9paYZD2/cRJw4GliTokwOcLMlaLozoHeAAeZWVCfLKa8rUk5S4lEl7AbupW74ETGOklrU9KvQ25O6EHi9mtU9CxX9QYf3gVKNchstqzsVXwHnoY64GIxIadEHWGiJVipiKNd0sy9R1eI0glzyQnchGGMNOq7YWwKFxZK9xZK4hPygIMcPzfJFumbYOqT8rZgSed+IVLUXKa/JRoVZ0zkmcz7wpnmCayaZ2pP6GLgNvYN0fqAfyv3WUFUCNSa3B1I+j2bA4a1Fknc3xXl/U5z/7YizapdkZ8R+vpPth7b1BMc28nFqcx99W/jofqQPX2rlao8QC2VBaDKl4TvNflWtogH65zi/MM1lLCm3lhSoQI8rgbYaZScDY8ztqCKBKsi9DuRUUhyWLCyWzFoV45kVMT7aHK9QZOJmeYJLO/oZfqyfnk21xqLPiIuLxINFNXlJx1hSP9MG+NbFZNscZamhZDLiRIkL6+h4cyuqQJyG4Jejcx4H+UQygdpaJLntw1JaPhFh5IJSFldQoAC2HJA8sjRKr5nF9HmumNmrY6mu2QOf/ESOzetq7pwZmwymI3jFB5rlOptbUckCNY4ArUIzEGKEU5lwFO5cXEq7pyJM+CzK/tLM1GXJljhD55XQa2YxH3wfT1a0JXH5oRyT28PcQYPBiJQX6C4PNjG3olIFKsie0AtIhjmVWbAxTtdnIvz1kyhF0cqp12c/xjljVjEjF5QmEURZFynnG4vKYDAi5QU/aJbLRs8T0OAFe3MfR/ALuz/FJdy1uJSBLxaztlBWetUk8MT/onT7d4SvtjtaVQ2Jx9+WY3Jam5tpMBiRqtBwCMQ1y9Yxt6MSRKAgdBvIa+z+tqdYcv4rJdz7SdT1npNAOUP0buZjQCv1Ov0oH10a+8jLcl/PdYWSvs8XM2+94374kUjxohxnJjcGQ02mMsIiFaMXhaI+3qSar4Pa46qPCsQpUWewNgLfUXVeQEGrXkdYdRPAHqte6yHzh1bl6NxBIP9m97ddEck5L5Xw+da49vXa11ceev1b+enZVFA/WzhaR9/ukXzwfZy3NsR4bX2MAxr7W/tL4eI5JTxzXpArO/ntinRnb87DEBlRhc9PCBXtoLZR15qEGiqXeqiM5weqeT1zrGdjjwtDxXECnCkX9IMUohe1+WTgv2n+jl7A5SjX3A44h/ApRDlzPI/yPMz0uZuOwBXARUDXJJOCCLAEeAmYCXgeEkiOyjsSf/xrSyQPYWdEMmB2CUu3p+5LPgGD2/sZdWKAM1qmZ4jvL4XnV0Z54PMoq3en7n4BH8y6MMglHfxOlbpQTCzSdcnui17izeXAPJsB4mpgMCrVTMMy928dKlfaC8AnGte/2WbyNhZorPHZp1HHOxL5DHWI/iCD0XNKWgQsTiLE/YFzgR5AJ1TSwu0cupecA4zSvAcPA+GEieWNCWUaArdqXu+OJAPhF8AC699tUfEAU7EbeEKj3MFU9jpMSWMiGkSFjbsIOAloXWZsi1p9dCEwHRV1oyw9gLM0vmOtNe5UhLbAL6znqisq3UzZ9ZMdqOMVH1nP1IcudKdSRKpI05LqAixLQ5zGk14SxS3WZx/NgHXVBrjX6mB+t2M4MA24x8sZuiwIvWp19kMoicHAl4p5//vUAnX6UT4ePDOLbkd4s0ock/CvZVH+uCjKjnDybpgTgA8vz6b7kbbfvYH94ePEExRpfO04VGDjVMwAhpf5/6uABzVF5APgBmBFkjLbNa/lhikcGjV8pjVJSsXdwF8S3vNbv2GsNTja1b9Jgqjs1Kxn44SyLYBMpW95uIx4noteZIq11gQzFU3RD8VUx3q2delhTXh0zplKYJYlmAcj94xGnT9NxWvWZCYdTgX+BAx0+bm1wAOo4OIp3bIyvScl0HeIcGO+ZgETrNlfull+m1kd+GOHhzBdrgX+Zw1q/jQ+nw/8wZolneKJQI0OXWknUADXLyhJKVC5AXjy7CzeG5rtmUAB+AVc2yXAN9dkc2G75E0VicJlr5U4RbVoQ17orgz1YR9wvyVauqLSz7KMBx2mS0onWdbgVI+fDYMeV1gTnbaa5YW1kvSFprhWlHrAPywLfGAan+8APA58blleVSpS9dCPnq07y8hHZer8Q5oiYDdj+ZSKHygWwCTgSbxxAmkJvAtcXCGBGkcOgvF2f5u+Isa/lic3IjvUF3x+dQ7Xdsnc9mXjkGDuxUH+ckpyD4vv9kpuXFDq1Po3y7H5mTjK8JSLZafEmfMLwImH0eDot1YXPkUtvxsqn36o5bt0HIJaWWNGuwzWr63VP37jwbVOQC0BDqpKkdLNPVSEXoDZAPAiKuKxlzRBrVu3qcA1xuN9gragNdCdlfYV9oZGYZOSfNM+yah3k3svnNTEx6Jh2XRumHKeEQE5HynvRnIFUp6NlGeDuADECDUjl8tSKfxdvQNMG5CVdFYza3WM+RtshTWXWHyUx+0/Cvh1BT4fAmaTItxUNaEBKtTS7R5N/gzuaWT1l6wKXKOFNZa1yED9WlvW09EeXjPPGtPPSTboZxJd0/Nb9PbGxif7MRXkSGCuZVm5dai4HBVQNxMErZt4ArDJlRV1W4N6lEZut/vbDe+UsqfYucmPa+RjwWVBGuQkkwzxDfAQgaKZYkLq/TM5JtiJuH8EgutwSOVyfdcAO8LqrJYTNy0s5Ztr/AQSp1hC/k7exP1iqideZ6eC82FnF7QHfoXeRnxVcQxqea8jhqrkerwJbNA2AxPmPJTTQ/MMjXGzgN7AN5VtSfXVLLdSo8zJqM3ATNIFlQvLDQ1Re1uZnuU+6vpTpZFfo9zdD+GdjXFedz5/RPN8wZu/SCJQgp0IMZJNRV3E5KLHdQQKQEwqWSmmhAuI+Tqg9nhsubNXgMuOdp7Mry2UPLfStv71yMq5zMNlDa8mcbeSuaSRXnClEahqwXUeXsvr/jaOzMZYrQs8Y/fMZVKkAoDugLFIs5EqYxmiwOVsYRSVE9bpQlTmYz0rahw+bNxj4xL+8IGzleITMOO8IC3rOPbxD/HTTUwqekLMTs8rUjx0YKuYHB4OcriT1fr4gCya5zs/Z/d9Vupgeosrq+Hg0x441ozBhiQ0pfo6qbQBbqmE7+lurTqUE5JM8Wv0vVNSBaLtiEpsVhmEgJHouSlnW2Uri1tQZwxSszf3XJDtE9+evyHGl9ucvfnGnBzgTOfzTy9RGr5KTPbmfJmYHJkhx+ZsJi7mkXBMoWGO4G99s7hmvv3RkhU7JYt+iHNai3J1PVOOyjtSPHRgazV70M9CeWwe5B4yc07qcGQvai8scYXCq3NShwPV2cHm91RO4AesfvBPymz/ZOqLu6HObOiwAfgyRZmhaVh9e1BnL3KtGYqbzw/XFKkzUXtZbgijIl8IS8SDLj47COUxuSe1KSWH2Rn8D3/pbPy0yBf8qU+W0/Xe5EDkCvEEnsZCFxMjC2VB6FeoNelDb0JnPxM/97Fsh/348+w3MTuR8uGPn4FyOKlOnJR4K2zK/EZTpF6kZuWT2ody8y9LOxciNYHDP59U82paLx8Vcx5ySwfgdOC9shXwklzgd9ZsP0/zMzNI7TRxqos6rLMG80aoNdR2wFHAYy6u0Ra9NXo39dpjWV2NUVEAOln/vhX9k+hZwBkp9WkcQUR5t841uyVvb3B+lu89NYt8O40SrKI4MtSNQMmx2vcfMTk8G+ST5Z4OATd1c17hnbPOIQeVkGd42Kd3WTP17qhjAZ1RZ+FWu7xOo8Nw4CxCRWcZgXIoamX123NIZ4/UkIwGaVif/2dZYK1QgQ0m4n14teP5ObKKDkuBX6KccVqi/BKmoXFotwyHeG+7saRGUN5N3Gf9gIaWtXIq7vz7S1CHwlLRS/N6G1EHYLclvL8FdXp+O/qOEQNJHWFDt14Ra7nnvzYzyAf4OTSJzmZnf9Q5sSTdN3cAyHIOEy8kSSzYqo7gqs62ghAFcZV4NPk5NnlrbnNi8WuQ4jygD3H8soAw8CWSV4mGH0/qdSey7kBGh5IQQuuqzgFuea+UsE0X//GAZNUuSadEF3kp+nj0gO4AelI+Q+xKa/Cej75z0OEkUmFUBIrHUKHEElmFCgFl8A43QrAH6MOhnnCbUOeX5lr9MtejerkJKDAbFcSg7GT2e1TQhfnWGKejOQMp4y3txpIaC9yX8Pqb9f5vrMHT7QG0R1HLfcmo6+IG/tFGoMoyzsUMuKemxaX7O5PFJXwF/SzGGtab7G/37kurna2o358YIMu+NzwmJhU5ruur7L6hPxCVK5Hir9agfVDtQsApCCaQFVonR+de5KhRk/btQJR3087Lgl7NnLvpR5ttlwI7Sm+8m+7FOYX5AVQcP92Za93DZLBca83O73MQKENmcCMqd2Pjqm3xIeX39yqC7pmorahlQafVlrmoYAc6nFDWsqzKFM3fW4NAKlpqXi+a0sJQm6u6exWp9poE+gfmdAI4Pqd5rdQHpAXlLIlN+6RjAFmfwCnKeISYz/Ee/ZzdlwmkjrLRGCFflgU5v3S+O/7pdm/3a+HcTVftjts/8AWhoyrYP+Oo2HfJ+I6atTe02ZrFrjKaUenormpFUI4FyXg8xWTdDbrP0QukDm33d81rCcrs0VWVSEWAS9ELSKnr3v0devH/vta8XqrvrYNe4FxIHmj0IF9pXqsxSawEeRPZyHKb9Cze7OzRd1oLHy1s3b3Fs0m95PbmPp4su6+dHoKYJguy7WdnU/avwCZdS98Wyc9MOUhMqwr20TXopY5ZXIMGymuTWI6G6iFSn5A68HQJP0d+ryi6Dh2fa5TZhH4i3CZuG8ZrgboKtX6qg64Q6EYM113CaORRvUAv784O9N1l83CKdejPPQ5kuTA8HyURqbNbO4iAjE93FMOxuRcQl+nE78oF3w3YHMwWIKV6CC88xHRM4oLhGD1d+CoaP3G9Zrkt1AzmoRch3FC1IrVSs9xqj+ql6wClGwF/G3orUEdUlUjtQgVM/dDFZ3T3uWIelxMe1UvN6/XEs3uFW9gv29l5RyQ7G3X6UbYG9XamRGwPWUsQxOVEu7+VxuGlNTGW7YjTu5mPC9r57RpyEI7RQ8SuRGfP/KDzrdjntCMk4hUVKd1JT7iGDJL3G52oUnQDFRR63H+9Gn91PX/deDJXukjNRSU2+8Hl54LVtFNVz3rFaWunCt/ucfby72qffuMD4XQ0oCDnLJQr8qGqFlbZfcsK4lmtfORnCW48wc85bX56DpM4wshyS7b5ScJtxp1+lqyw40QJtYet1Kxly8MR4XG/lB7Vq8qDI1fWntR0VD6jH0xfzHhXLxdaJRKFLQfs+2zzfEFdO7mVcmmSL7FNH3LjO6XlLLaFG+Ns3CuJy0POSexM8miVyxycLNW8o4AJsd90Bm3+6+GgZqid4pkxKsuS6m3udSUhqZvYrXaEpaPF0TzPqQ+KZGva3e0stUQX9xOO8PGXUwKxQe39fnFIX5MzkjwS5UI5rSt0Hj/rOwbBjRuR0udH0wSG6oobkZqIfc6na7BZ+kngGFSE8a9Nk2d83lNuDTmc5Kx3PUdjXu5K8i3lzoct+iF+yFR8+rlBhh/rR5Rfa9+KFE/ZfuPt9RpQUnJc4vvr9zjvp7Wt6yBSPrnFdAZttpkmMNQEkXoC+wgMEr1N18szLFLtsIn/ZkOTGn1HBTmJCzeRmLMlku13GuRFJMm3lJO23Qm5qeauizH82HJ7wbvBd46YcsB+2be4eDBClFt8/HKbc/071Letf5Q9xRvM461NkWkCQwU5H73gBq7HXy+W+17RFKlhwJ0ZbKQGwJBa31Vk+b0FX5JVZUcBkzI7xaB2SNilxHNWL62Jcfz0CNd3DdA8XyCl+PCyTlmXiYn7t9lXGwHihsT34xJeWevskNnTPhrFRq8D4RoMhqRkLJWHFyK1BhWio3OKcu1R4Va+NPczo6ZUYeIeeP1sZ5Xa7+grJJJF4/6GhEN+A1r5CAUOXVpcsVNy888p6nszr7g5TktLY0LDkOVjIS7eHHd0+ggF4OQmPjtr8lPTDwyGmoFX3n2vapYbapo846ZUuTQe9ZKI1MZ9jj7c7ZII4Ud23zGia9I5TxbwL8qcf/jpm8bmtEXyoN2Hnl7uvKE2oJWfoN3pEikWmX5gMBiRKssczXKXU73TaNcAQ0rsTnwrPwvHVPBbD0h7F28pnA8Wy/jT2Lgs/61vFt2OSNqlupEQ/FKOqdOYuO91bNaqv94RZ/py56W+IU4p5n2+901HMBiMSJXlU1TA2FS0xYuoCoYkxNfavdu5ob1ISeDzrTbec4K+coj9KXgxJbIeWJj4fm4A3h0aZGDrpN3qTpSnJ/KWUB9k9BOQtkvFY98vxWnLrF624OIOfrt6rxIT9y8z/cBgMCKVONa9plnWLPllVKN8tiH8OzV0NmAd4vo1oXXOaUksttuwCYVSP1vw1qXZvDgoyFmtfOWcNvyC4MDWvlmx0aHn8LEI5ZVZjqeXx3j7O2fX8+u6+KljG/NDzDadwGAwImWHWfKrDmQVrcJmKa53kpxMb3wbcxI8x7TRKseUvMfp75d29PPOZdkU/i7Ep1dm85/Lslk8LJvC34d469LsTj7BMKf+9+6mOCMXOEd/yQ3ALSfZ7n/FkbF/m05gMNQcvIw48S4qY2S9FOVaoiJQfOzxb1mPt8m+DsuzI2IC+2SBWJm4hHZWSz9OMSA/2hznh/3SJl2HHCZHh+4QU8L255rqRsazN9SFJK7/dYLQo6n+XGjZjjiXvlZCSZIwwGO7BxxSizBPTC5ebR5rg6HSeR1vw96tyYRIlaBC/evkFxqaAZHajUpfbEC+Q8KRgPb1BW3qCjbsLb/JE5cwfXmMO3qV6w5BhLwbGGEriOOIyiHhK2iZswvEyIrWev6GGMNeL2VPsfPh3fb1Bbf2cAjYJ+Qkc+8NhirhIeA/mbiw1wFm3Sz5+cx9zZRGlXdqABh6jHM2gEeWRim2tV7Eb+XoUE9Hy202MTE5cj3I4aSZW2lPseSmhaWc/3JJUoEK+ODf5wXJs9MoKd8UkyIfmJtvMNQsvBaK14FijXLNgFNM82eI7OB72IT0/9WxzobzlgOSZ1ZEnfrIDHkTdZN9pZgcmUFpuBPIcehnd91SEhfjO/6zePUjS6Mpw3CP75tFH/u9tSgE/mBuvMFgRCoV+1B7U7rWlCEDiPv27LYmDIdwbCNBryQOFHctjtpbMoKOZIWednJJ/6nYVPaKyZE/Mzncnjh9EdwG4mkEHwMfIMQCBM8DY5CyH3XDrbIfLLpje1j+lhSJIUd0DTC2u4PICh4SU/YvN3feYKh5ZCJVxxzgXI1yl6HiPaXKlFtdk89V76R4UkxHyEsS3769R4BL5tpXfWuR5E8fRXnoTNs9n0tomTNNEhkpUuQeEiB5MLwY/UR6i4BHgJvt/vjb4/082t8x8+HXlIT/zzzKhhowya+OxGpiI7+CXrr0psBpGuUi1fTmRap11zpQ9AY2cfIu6uDnhCRRIR5ZGmXBRqfbJ66jIDRD3pSRbJ13AOsSxI7bewZ4cmAQh2DtB5C+q8RUrSVmg8HrcTFUQwyLZIRrokhtBe0AnzoHe3VdwYMuvnOXxusTD2+eTt2O1qzXLiA/1cVUFHD5sI2Vw919nPt5XMLVb5Q4BnUFriQYeleOzWnrcb85AFyLZaXVzxa8PDjI+L5ZTofq4khxlZhyoDbmKDPnDDNLXc1yR1aBxdJMs1xeJU/GdbXkIVRKpVSv3plW5TnoZeO9FLXEkyQtn3ZCtubWw5tq//0sVFqPVKSane+1BDRX41otSO1M0E+zXkWAXtbZ0shUskJjEq97SQc/57X186bDId6tRZILXylh4ZCgfXBaSR+kb6kcE7qTfeHHPEyL8R4w7YpO/hsnnZ5FszyRbJguEJOL5lA7ycWQSRqjYkmmGnuO8/A7dbcPWmuW6+RRvXZplmukUaY+8HtNQZuWSUsK9KOiNwHOSFFmk4tGSiWMddE7x3XQIkyF7uG1CzTKjNC8lnYWVTGVvUhpG118Wv8s8rOcP/vfbXEGzymhyHH6IOsieZj83K/kmJzhcgRZFekwEoQcnTuodHSo+8zzg8kESgKjxKTwQ7V4ED3K6EjGuUijzHkefp/ukvWppE4cWBeVhNALvtcsp7N1M8SF5mzNtEittF46pFry2wfs1LzWZCifPr0MY0kdEaPsb0iFrqv1HSTkX7J5IHp4WK8yQ3rOw3aC27quYEK/5Lrywfdx+s8uZns4mXEqOyPFM+SHNsiC0CR5S6iPHKdvocvR+cfJgpy7KAitQMi5AUHP5LNN+RsxOfxwDR0YdWfT52NIBzcW//8BdVJYKoOqQKSCqGXxZNyvuSqjg66RcHUKKy8L0D0mEisrjpnchJujaXL+Avhdig60RNMa6Y3yKLsd5Qp/0A44ArjBEgtd3tIoswQYqFGuGWqf7jaUY8nBfbY8S6Qnu6jXfDc3QTxYWChHh25B8Fzi3244IcCSLXGeWeG8HL5kS5xTnivm5cFBujROOqdpDhTgo4C9of1yjFiClKsQrEHKfSD2IWUuiDoIWqP24HpDrInmFssm4lwuHox8TM1Fd2mlv9XHJ5J8qdyQXvtiDbhvAlcB3yX8rSsquo2X4+d2F2XvtsQjMU5lPjAeuN7Den2hWS4fFXFiOOX383OBR4GOmtf6DLWdknGRetUalFPRyHrokg2+izRFCuAk4G1LCH60LKumLq3GQkuAUrHYxTVbADMsMf4B8KM2XoMu23W+2xshpoSflwU5w0GUm4E/NiDIsh3F/Hebs0Pm2kJJz5nF3Nc3i5tPCuhISj5SDgAGqB1C6xMizf1+yctE/deLqfu3U7PZ6KLseGCUNYjssGbibwEvGy3yRKRALa2tssaf1dZY0hnomYFVqM0uygaBZ4C/AB+hYqY2QX+/3Q0foZbYdR7ejta4+aX1iliT19Nd1uuQMS6TIvUpKkyOjjfK0BSD7yzgXmtg1yUXhzQQGryoOUN9z8VvLGv2tkmzXl9aD417BDci+YqE5c5QAOZdEuSMWcWs3u28rBeJwi3vlfLCqhgPnplFz6aZPyKyeb8sal7Hd5mYXPRmLRlEP7OWTXRpmjB5O2BEKinbrEmim/3TbGsS3T/DdduUxmfaVGAs0WWnJVSnuvjMidarIgbOT2RypIkDczXLXpzColgPzKukjhxHuUnqUEIZL5RKYEq6HxSTIt+B+CU23o/N8gQLh2TTvn7qydLHW+L0nlnM0Hkl9skSPWD1bsm1b5fQ9h+RXDGp6EhqD+8YHckoYeDzalq3pVSDg7MOTK3E71oMfFVZIgX6AWcbAGenKHM3lbP+/jSwzOUN3FIJ9foSeLYiFxCTi+YihG0OqBb5gneHZCc96HsQCcxeHaPHs8X0e6GYJ/4XZWdEVujHFUVh1uoYF80pofPTEf6xLHYwXceD1B5vtuXohxUzpMfCalqvfUB1PfP3Mumu4LinXPQYXyV0iL2aZVPF8vsK5bWSSTYAY1x+phDl+CEzWK8w8Ev0Inkkp07Rn50s3JZ1BIuGZXNRe/1V1Q9/iDNyQSnNHovQ9/li7lhUyrz1MdYWSqJxZ5HbtE+ycGOcv30S5fxXSmgyLczl80qYuy5G/NCWrAc8VosG0Xsy3JdqO89WY4uluqYaKgVGVkK/fBJ432nM0Hl1TPOLn9e8fiGkDLfjtwZYmYHXLip2OG9chuoVBS7xsifIceTIgty3ZEFI2r1io0PynlOzZJavYnXP8iGb5gnZsYGQJx/pk8c0ELJ1XSFzAmld7yqPfr7ufdLN8HuJ5vXczJL/nmabJ3qJztT83J88aNeGLuqpc/CznYvr+V3WdaZHz+Y9Lsrma9SrKcrZoKL1iluDvU7ZuS7a7fYMjXESlV/Q9pB6ZQRI1F3yq0fqwLQx1IGwWR7X8Vugr7Xcki5/tl5esh+1X/eKlxcV44ggiy52yjvlE3BnrwBfXJ1NtyPS7yKlcfjxgGTNbskXW+Os2i35bq8kkt6i7UPWQ1wbGIveEQhDetyOO5dvOx6zJhNe8iMwwYPrjEc/oIIb7gMykVj0fZQDUFFVidQb6B9S1InlVwxcaQmCF+F4FgK9gBWbkJGGAAAFR0lEQVQVNVCsWfrVllVYUVajcm5lxGFETCGMPzwY5BtOZbo09rHkymwe6JdFg5yqDReX5aPRmO6BuW4OCh/GhFEHvJ8zepIRNqIiz6SbyeAlHCL2e8A9VGzf7DHgzgxPoG7GuwDb01FnTR2PB1SGSO1BuWrrMAi9yMIxSxB6WiKYDquspZr+HsyqyvIscDzwjzRFdIfVCY4nwxupYiIH2BQZTJLDxNl+GNs9wNrfZDO2e4A6wcodTQI++NWxflb+OoeJ/bJ6zF0afKqWDKQHJ2MXAGuNrnjOQuvZ3+ryc/dZk+nSDNWrFBichiVUYgnIDWR+72gq0B2H/SNNvrF+5zWpJgsBYIHmRYsqUKGnXAjicei7iS61HuIu1szoQtRhO6dzEFtR3lMvoJICZqqj/YAKXfJnlEPIxahDxk4CvA/40Jqhzbb+v1IQs4lBeIwsyFkO4u84hJVqmCN4oF8Wd/XO4l/Lovz9qyhrdmfuWWgcElzZyc/vugU4usHPVtzpLXy/nDUo589DX4t8S+3gDdQZwv6oWHHdUIfAG6MiqZiI6OmzCDgBFXRgJMkD934G3EXlLMMesCbQl6OWJrulELVXrbGmMhN/LkfFXT0dFXf0AlKHnIujXMz/iQpsoLXwXxM7eBAVcqcRKtBisXXT16HWfKsKP2ozuIlVL2HV6zvUQb4q9ziStwQ74/NPRzOO4Bdb47y8Nsara2Os2FlxwWqaJxjQyselHf2c39ZP0HE7XD4lJkeuM2OswUPyUMv+J6L2PnNQnsmrUYdZ11Rh3Tqilv47WEIQQx2yXY5apdpt85nR6IVbe82yaLwYd7ugQka1tMa4XKsNf0StXC1xqGutEylDRYRqHAH25vwRxJ24CNm0rUiyZEucJVvirNipHCQ27JUU2qSjzw1Aw5DgmAaCzg19HNtIcFoLH8c31l59jiNFLzGl6HNzxwwGWypbpDJGwNxLwyGzlnFEIXKPHJ3zLD7xVySX60xmmuQKBrf3M9jmjNVu66BvVELdoCDbX9FK8glS+szdMhhqPuZBN9jrwJTIejEpfAVC9AL5FhXYjG2QI2iQIzgiVGGB+gLEBWJS+BQxJfypuUsGQ83HWFKG5GI1qegz4Fw5JtgJ6b8ZFYo/vxKrEEPKtxG+x5hc9Jow0RgMBiNSBkN5sSpZCdwob2vwR0rDg0D8AjiHzKQyl8D/gOcIiH+LCeHN5g4YajhHkDzJ4kEK0Us54q8pDWNEyuBOrO7fvQflPjpDjiCX/NyzgN4gT0F5BaZjZUWBtQjxMcTfwed/Rzxw4EfT2oZaxFjgVo1yT6JcvlPRSPN7i6t7wxiRMqQvWE9QBEXzsKJiyCH4aZHTkgBtiNMGfG1A5iBkPeL4ED4/yH1IuRshChFsJu7/hnr714hxaZ/+NxhqArou7gOtcTvVGaPumtfbZpreYDAYDKk4Gf1grAUprtUWdchX51p3maY3GAwGQyoCqIDSOsKyD+jncJ1sVFQdXcE71zS9wWAwGHR42YW4lAL/QgUi7oqKY3odKt6n7jWKUZE2DAaDwWBIyRVkLl+T3esd0+QGg8Fg0CUIbK5Ekfq1aXKDwWAwuGFEJQnUJlJnQjcYDAaD4RAEKjVLpkVqiGlqg8FgMKRDG1RKi0wJ1D9NExsMBoOhIvRCZTX3WqDmY5b5DAaDweABfVDJDb0SqJk4ZN42GAwGgyEdmgFzKihOu4FrMUluDQaDwZAhBgFvoFLH64rTNuCvQMPD+YcbZTUYDIbDh9aoILP9gBNRKT4ao0Iq7UMFqv0KWAD8h8Mgynkq/h885rfKXRQafwAAAABJRU5ErkJggg== +" style="display:none"><noscript>Javascript must be enabled to view this page.</noscript><div style="display:none"><krona collapse="true" key="true" chart="TAXOMIC"><attributes magnitude="count"><attribute display="Count" dataAll="members" tip="Number of reads assigned to this and child taxa">count</attribute><attribute display="Unassigned" dataNode="members" tip="Number of reads assigned specifically to this taxon">unassigned</attribute><attribute display="TaxID" mono="true" hrefBase="https://www.ncbi.nlm.nih.gov/Taxonomy/Browser/wwwtax.cgi?mode=Info&id=" tip="Taxonomic identifier">tid</attribute><attribute display="Rank" mono="true" tip="Taxonomic rank/level">rank</attribute><attribute display="Kmer coverage (%)" tip="Averaged score of reads assigned to this and child taxa">score</attribute></attributes><datasets rawSamples="1"><dataset isctr="False" sread="99" sclas="99" sfilt="99" scmin="36" scavg="99.94949494949495" scmax="347" lnmin="198 nt" lnavg="533 nt" lnmax="602 nt" tclas="13" tfilt="13" tfold="1" sclim="None" totnt="52.81 knt">/tmp/tmpz5ieggzi/files/d/5/5/dataset_d55a31f8-0f74-4938-9a4f-dce196b7467b</dataset></datasets><color attribute="score" hueStart="0" hueEnd="300" valueStart="13.6" valueEnd="13.6" default="true"> </color><node name="root" href="https://www.google.com/search?q=root"><count><val>9</val></count><unassigned><val></val></unassigned><tid><val href="1">1</val></tid><rank><val>no_rank</val></rank><score><val>13.6</val></score><node name="Bacteria" href="https://www.google.com/search?q=Bacteria"><count><val>9</val></count><unassigned><val></val></unassigned><tid><val href="2">2</val></tid><rank><val>superkingdom</val></rank><score><val>13.6</val></score><node name="Proteobacteria" href="https://www.google.com/search?q=Proteobacteria"><count><val>9</val></count><unassigned><val></val></unassigned><tid><val href="1224">1224</val></tid><rank><val>phylum</val></rank><score><val>13.6</val></score><node name="Gammaproteobacteria" href="https://www.google.com/search?q=Gammaproteobacteria"><count><val>9</val></count><unassigned><val></val></unassigned><tid><val href="1236">1236</val></tid><rank><val>class</val></rank><score><val>13.6</val></score><node name="Enterobacteriales" href="https://www.google.com/search?q=Enterobacteriales"><count><val>9</val></count><unassigned><val></val></unassigned><tid><val href="91347">91347</val></tid><rank><val>order</val></rank><score><val>13.6</val></score><node name="Enterobacteriaceae" href="https://www.google.com/search?q=Enterobacteriaceae"><count><val>9</val></count><unassigned><val>9</val></unassigned><tid><val href="543">543</val></tid><rank><val>family</val></rank><score><val>13.6</val></score></node></node></node></node></node></node></krona></div></body></html> \ No newline at end of file