├── .gitignore ├── .npmignore ├── .nvmrc ├── LICENSE ├── README.md ├── changelog ├── dist ├── assets │ ├── .gitkeep │ └── proseqviewer.css ├── index.d.ts ├── index.js └── sqv-bundle.js ├── figure.png ├── package.json ├── src ├── assets │ ├── .gitkeep │ └── proseqviewer.css ├── index.ts └── lib │ ├── colors.model.ts │ ├── consensus.model.ts │ ├── events.model.ts │ ├── icons.model.ts │ ├── icons.ts │ ├── interface.ts │ ├── options.model.ts │ ├── palettes.ts │ ├── patterns.model.ts │ ├── proseqviewer.ts │ ├── rows.model.ts │ ├── selection.model.ts │ └── sequenceInfoModel.ts ├── tsconfig.json └── webpack.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | node_modules/ 3 | /dist/lib 4 | package-lock.json 5 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src 2 | tsconfig.json 3 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | lts/erbium 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | 5 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 6 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 7 | Everyone is permitted to copy and distribute verbatim copies 8 | of this license document, but changing it is not allowed. 9 | 10 | Preamble 11 | 12 | The licenses for most software are designed to take away your 13 | freedom to share and change it. By contrast, the GNU General Public 14 | License is intended to guarantee your freedom to share and change free 15 | software--to make sure the software is free for all its users. This 16 | General Public License applies to most of the Free Software 17 | Foundation's software and to any other program whose authors commit to 18 | using it. (Some other Free Software Foundation software is covered by 19 | the GNU Lesser General Public License instead.) You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | this service if you wish), that you receive source code or can get it 26 | if you want it, that you can change the software or use pieces of it 27 | in new free programs; and that you know you can do these things. 28 | 29 | To protect your rights, we need to make restrictions that forbid 30 | anyone to deny you these rights or to ask you to surrender the rights. 31 | These restrictions translate to certain responsibilities for you if you 32 | distribute copies of the software, or if you modify it. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must give the recipients all the rights that 36 | you have. You must make sure that they, too, receive or can get the 37 | source code. And you must show them these terms so they know their 38 | rights. 39 | 40 | We protect your rights with two steps: (1) copyright the software, and 41 | (2) offer you this license which gives you legal permission to copy, 42 | distribute and/or modify the software. 43 | 44 | Also, for each author's protection and ours, we want to make certain 45 | that everyone understands that there is no warranty for this free 46 | software. If the software is modified by someone else and passed on, we 47 | want its recipients to know that what they have is not the original, so 48 | that any problems introduced by others will not reflect on the original 49 | authors' reputations. 50 | 51 | Finally, any free program is threatened constantly by software 52 | patents. We wish to avoid the danger that redistributors of a free 53 | program will individually obtain patent licenses, in effect making the 54 | program proprietary. To prevent this, we have made it clear that any 55 | patent must be licensed for everyone's free use or not licensed at all. 56 | 57 | The precise terms and conditions for copying, distribution and 58 | modification follow. 59 | 60 | GNU GENERAL PUBLIC LICENSE 61 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 62 | 63 | 0. This License applies to any program or other work which contains 64 | a notice placed by the copyright holder saying it may be distributed 65 | under the terms of this General Public License. The "Program", below, 66 | refers to any such program or work, and a "work based on the Program" 67 | means either the Program or any derivative work under copyright law: 68 | that is to say, a work containing the Program or a portion of it, 69 | either verbatim or with modifications and/or translated into another 70 | language. (Hereinafter, translation is included without limitation in 71 | the term "modification".) Each licensee is addressed as "you". 72 | 73 | Activities other than copying, distribution and modification are not 74 | covered by this License; they are outside its scope. The act of 75 | running the Program is not restricted, and the output from the Program 76 | is covered only if its contents constitute a work based on the 77 | Program (independent of having been made by running the Program). 78 | Whether that is true depends on what the Program does. 79 | 80 | 1. You may copy and distribute verbatim copies of the Program's 81 | source code as you receive it, in any medium, provided that you 82 | conspicuously and appropriately publish on each copy an appropriate 83 | copyright notice and disclaimer of warranty; keep intact all the 84 | notices that refer to this License and to the absence of any warranty; 85 | and give any other recipients of the Program a copy of this License 86 | along with the Program. 87 | 88 | You may charge a fee for the physical act of transferring a copy, and 89 | you may at your option offer warranty protection in exchange for a fee. 90 | 91 | 2. You may modify your copy or copies of the Program or any portion 92 | of it, thus forming a work based on the Program, and copy and 93 | distribute such modifications or work under the terms of Section 1 94 | above, provided that you also meet all of these conditions: 95 | 96 | a) You must cause the modified files to carry prominent notices 97 | stating that you changed the files and the date of any change. 98 | 99 | b) You must cause any work that you distribute or publish, that in 100 | whole or in part contains or is derived from the Program or any 101 | part thereof, to be licensed as a whole at no charge to all third 102 | parties under the terms of this License. 103 | 104 | c) If the modified program normally reads commands interactively 105 | when run, you must cause it, when started running for such 106 | interactive use in the most ordinary way, to print or display an 107 | announcement including an appropriate copyright notice and a 108 | notice that there is no warranty (or else, saying that you provide 109 | a warranty) and that users may redistribute the program under 110 | these conditions, and telling the user how to view a copy of this 111 | License. (Exception: if the Program itself is interactive but 112 | does not normally print such an announcement, your work based on 113 | the Program is not required to print an announcement.) 114 | 115 | These requirements apply to the modified work as a whole. If 116 | identifiable sections of that work are not derived from the Program, 117 | and can be reasonably considered independent and separate works in 118 | themselves, then this License, and its terms, do not apply to those 119 | sections when you distribute them as separate works. But when you 120 | distribute the same sections as part of a whole which is a work based 121 | on the Program, the distribution of the whole must be on the terms of 122 | this License, whose permissions for other licensees extend to the 123 | entire whole, and thus to each and every part regardless of who wrote it. 124 | 125 | Thus, it is not the intent of this section to claim rights or contest 126 | your rights to work written entirely by you; rather, the intent is to 127 | exercise the right to control the distribution of derivative or 128 | collective works based on the Program. 129 | 130 | In addition, mere aggregation of another work not based on the Program 131 | with the Program (or with a work based on the Program) on a volume of 132 | a storage or distribution medium does not bring the other work under 133 | the scope of this License. 134 | 135 | 3. You may copy and distribute the Program (or a work based on it, 136 | under Section 2) in object code or executable form under the terms of 137 | Sections 1 and 2 above provided that you also do one of the following: 138 | 139 | a) Accompany it with the complete corresponding machine-readable 140 | source code, which must be distributed under the terms of Sections 141 | 1 and 2 above on a medium customarily used for software interchange; or, 142 | 143 | b) Accompany it with a written offer, valid for at least three 144 | years, to give any third party, for a charge no more than your 145 | cost of physically performing source distribution, a complete 146 | machine-readable copy of the corresponding source code, to be 147 | distributed under the terms of Sections 1 and 2 above on a medium 148 | customarily used for software interchange; or, 149 | 150 | c) Accompany it with the information you received as to the offer 151 | to distribute corresponding source code. (This alternative is 152 | allowed only for noncommercial distribution and only if you 153 | received the program in object code or executable form with such 154 | an offer, in accord with Subsection b above.) 155 | 156 | The source code for a work means the preferred form of the work for 157 | making modifications to it. For an executable work, complete source 158 | code means all the source code for all modules it contains, plus any 159 | associated interface definition files, plus the scripts used to 160 | control compilation and installation of the executable. However, as a 161 | special exception, the source code distributed need not include 162 | anything that is normally distributed (in either source or binary 163 | form) with the major components (compiler, kernel, and so on) of the 164 | operating system on which the executable runs, unless that component 165 | itself accompanies the executable. 166 | 167 | If distribution of executable or object code is made by offering 168 | access to copy from a designated place, then offering equivalent 169 | access to copy the source code from the same place counts as 170 | distribution of the source code, even though third parties are not 171 | compelled to copy the source along with the object code. 172 | 173 | 4. You may not copy, modify, sublicense, or distribute the Program 174 | except as expressly provided under this License. Any attempt 175 | otherwise to copy, modify, sublicense or distribute the Program is 176 | void, and will automatically terminate your rights under this License. 177 | However, parties who have received copies, or rights, from you under 178 | this License will not have their licenses terminated so long as such 179 | parties remain in full compliance. 180 | 181 | 5. You are not required to accept this License, since you have not 182 | signed it. However, nothing else grants you permission to modify or 183 | distribute the Program or its derivative works. These actions are 184 | prohibited by law if you do not accept this License. Therefore, by 185 | modifying or distributing the Program (or any work based on the 186 | Program), you indicate your acceptance of this License to do so, and 187 | all its terms and conditions for copying, distributing or modifying 188 | the Program or works based on it. 189 | 190 | 6. Each time you redistribute the Program (or any work based on the 191 | Program), the recipient automatically receives a license from the 192 | original licensor to copy, distribute or modify the Program subject to 193 | these terms and conditions. You may not impose any further 194 | restrictions on the recipients' exercise of the rights granted herein. 195 | You are not responsible for enforcing compliance by third parties to 196 | this License. 197 | 198 | 7. If, as a consequence of a court judgment or allegation of patent 199 | infringement or for any other reason (not limited to patent issues), 200 | conditions are imposed on you (whether by court order, agreement or 201 | otherwise) that contradict the conditions of this License, they do not 202 | excuse you from the conditions of this License. If you cannot 203 | distribute so as to satisfy simultaneously your obligations under this 204 | License and any other pertinent obligations, then as a consequence you 205 | may not distribute the Program at all. For example, if a patent 206 | license would not permit royalty-free redistribution of the Program by 207 | all those who receive copies directly or indirectly through you, then 208 | the only way you could satisfy both it and this License would be to 209 | refrain entirely from distribution of the Program. 210 | 211 | If any portion of this section is held invalid or unenforceable under 212 | any particular circumstance, the balance of the section is intended to 213 | apply and the section as a whole is intended to apply in other 214 | circumstances. 215 | 216 | It is not the purpose of this section to induce you to infringe any 217 | patents or other property right claims or to contest validity of any 218 | such claims; this section has the sole purpose of protecting the 219 | integrity of the free software distribution system, which is 220 | implemented by public license practices. Many people have made 221 | generous contributions to the wide range of software distributed 222 | through that system in reliance on consistent application of that 223 | system; it is up to the author/donor to decide if he or she is willing 224 | to distribute software through any other system and a licensee cannot 225 | impose that choice. 226 | 227 | This section is intended to make thoroughly clear what is believed to 228 | be a consequence of the rest of this License. 229 | 230 | 8. If the distribution and/or use of the Program is restricted in 231 | certain countries either by patents or by copyrighted interfaces, the 232 | original copyright holder who places the Program under this License 233 | may add an explicit geographical distribution limitation excluding 234 | those countries, so that distribution is permitted only in or among 235 | countries not thus excluded. In such case, this License incorporates 236 | the limitation as if written in the body of this License. 237 | 238 | 9. The Free Software Foundation may publish revised and/or new versions 239 | of the General Public License from time to time. Such new versions will 240 | be similar in spirit to the present version, but may differ in detail to 241 | address new problems or concerns. 242 | 243 | Each version is given a distinguishing version number. If the Program 244 | specifies a version number of this License which applies to it and "any 245 | later version", you have the option of following the terms and conditions 246 | either of that version or of any later version published by the Free 247 | Software Foundation. If the Program does not specify a version number of 248 | this License, you may choose any version ever published by the Free Software 249 | Foundation. 250 | 251 | 10. If you wish to incorporate parts of the Program into other free 252 | programs whose distribution conditions are different, write to the author 253 | to ask for permission. For software which is copyrighted by the Free 254 | Software Foundation, write to the Free Software Foundation; we sometimes 255 | make exceptions for this. Our decision will be guided by the two goals 256 | of preserving the free status of all derivatives of our free software and 257 | of promoting the sharing and reuse of software generally. 258 | 259 | NO WARRANTY 260 | 261 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 262 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 263 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 264 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 265 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 266 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 267 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 268 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 269 | REPAIR OR CORRECTION. 270 | 271 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 272 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 273 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 274 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 275 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 276 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 277 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 278 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 279 | POSSIBILITY OF SUCH DAMAGES. 280 | 281 | END OF TERMS AND CONDITIONS 282 | 283 | How to Apply These Terms to Your New Programs 284 | 285 | If you develop a new program, and you want it to be of the greatest 286 | possible use to the public, the best way to achieve this is to make it 287 | free software which everyone can redistribute and change under these terms. 288 | 289 | To do so, attach the following notices to the program. It is safest 290 | to attach them to the start of each source file to most effectively 291 | convey the exclusion of warranty; and each file should have at least 292 | the "copyright" line and a pointer to where the full notice is found. 293 | 294 | {description} 295 | Copyright (C) {year} {fullname} 296 | 297 | This program is free software; you can redistribute it and/or modify 298 | it under the terms of the GNU General Public License as published by 299 | the Free Software Foundation; either version 2 of the License, or 300 | (at your option) any later version. 301 | 302 | This program is distributed in the hope that it will be useful, 303 | but WITHOUT ANY WARRANTY; without even the implied warranty of 304 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 305 | GNU General Public License for more details. 306 | 307 | You should have received a copy of the GNU General Public License along 308 | with this program; if not, write to the Free Software Foundation, Inc., 309 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 310 | 311 | Also add information on how to contact you by electronic and paper mail. 312 | 313 | If the program is interactive, make it output a short notice like this 314 | when it starts in an interactive mode: 315 | 316 | Gnomovision version 69, Copyright (C) year name of author 317 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 318 | This is free software, and you are welcome to redistribute it 319 | under certain conditions; type `show c' for details. 320 | 321 | The hypothetical commands `show w' and `show c' should show the appropriate 322 | parts of the General Public License. Of course, the commands you use may 323 | be called something other than `show w' and `show c'; they could even be 324 | mouse-clicks or menu items--whatever suits your program. 325 | 326 | You should also get your employer (if you work as a programmer) or your 327 | school, if any, to sign a "copyright disclaimer" for the program, if 328 | necessary. Here is a sample; alter the names: 329 | 330 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 331 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 332 | 333 | {signature of Ty Coon}, 1 April 1989 334 | Ty Coon, President of Vice 335 | 336 | This General Public License does not permit incorporating your program into 337 | proprietary programs. If your program is a subroutine library, you may 338 | consider it more useful to permit linking proprietary applications with the 339 | library. If this is what you want to do, use the GNU Lesser General 340 | Public License instead of this License. 341 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # ProSeqViewer 3 | 4 | ProSeqViewer is a [TypeScript](https://www.typescriptlang.org/) library to visualize annotation 5 | on single sequences and multiple sequence alignments. 6 | 7 | ![ProSeqViewer](figure.png?raw=true "ProSeqViewer") 8 | 9 | 10 | ProSeqViewer can be integrated in both modern and dynamic frameworks like [Angular](https://angular.io/) 11 | as well as in static HTML websites. It is used by [MobiDB](http://mobidb.bio.unipd.it/), 12 | [DisProt](http://www.disprot.org/), [RepeatsDB](http://repeatsdb.bio.unipd.it/) 13 | 14 | ### ProSeqViewer features 15 | 16 | * Generates pure HTML, compatible with any browser and operating system 17 | * Easy to install 18 | * Lightweight 19 | * Zero dependencies 20 | * Fast, able to render large alignments 21 | * Interactive, capture mouse selections and clicks 22 | * Responsive, dynamically adapt to window changes 23 | 24 | ## Links 25 | * [ProSeqViewer library GitHub repository](https://github.com/BioComputingUP/ProSeqViewer) 26 | * [Documentation website](https://biocomputingup.github.io/ProSeqViewer-documentation/) 27 | * [NPM package](https://www.npmjs.com/package/proseqviewer) 28 | 29 | ## Getting started 30 | 31 | ### JavaScript installation 32 | 33 | Import the JavaScript bundle and CSS from local files 34 | ```html 35 | 36 | 37 | 38 | 39 | ``` 40 | 41 | Alternatively, import from GitHub 42 | ```html 43 | 44 | 45 | 46 | 47 | ``` 48 | 49 | Add component 50 | ```html 51 | 52 |
53 | 54 | ``` 55 | 56 | Create an instance 57 | ```html 58 | 59 | 60 | 86 | 87 | ``` 88 | 89 | ### Angular installation 90 | 91 | Install ProSeqViewer from npm 92 | ``` 93 | npm install proseqviewer 94 | ``` 95 | 96 | Add ProSeqViewer CSS to your angular.json file 97 | ```json 98 | { 99 | styles: ["./node_modules/proseqviewer/dist/assets/proseqviewer.css"] 100 | } 101 | ``` 102 | 103 | Import in your component 104 | ```typescript 105 | import {ProSeqViewer} from 'proseqviewer/dist'; 106 | ``` 107 | 108 | Add component to your page 109 | ```html 110 |
111 | ``` 112 | 113 | Create an instance in your component 114 | ```typescript 115 | // Input sequences 116 | const sequences = [ 117 | {sequence: 'TLRAIENFYISNNKISDIPEFVR', id: 1, label: 'ASPA_ECOLI/13-156'}, 118 | {sequence: 'TLRASENFPITGYKIHEE..MIN', id: 2, label: 'ASPA_BACSU/16-156'}, 119 | {sequence: 'GTKFPRRIIWS............', id: 3, label: 'FUMC_SACS2/1-124'} 120 | ] 121 | 122 | // Input icons 123 | const icons = [ 124 | {sequenceId: 1, start: 2, end: 2, icon: 'lollipop'}, 125 | {sequenceId: 1, start: 13, end: 13, icon: 'lollipop'} 126 | ] 127 | 128 | // Options and configuration 129 | const options = { 130 | chunkSize: 0, 131 | sequenceColor: 'clustal', 132 | lateralIndexes: false 133 | }; 134 | 135 | // Initialize the viewer 136 | const psv = new ProSeqViewer('psv'); 137 | 138 | // Generate the HTML 139 | psv.draw({sequences, options, icons}); 140 | 141 | ``` 142 | 143 | ## Developers 144 | If you are a developer you can update the GitHub and NPM repo with these commands 145 | ```bash 146 | nvm use 147 | npm install 148 | npm run buildall 149 | npm publish 150 | ``` 151 | 152 | 153 | ## License 154 | 155 | This program is free software; you can redistribute it and/or modify it under the terms of the CC-BY 156 | License as published by the Creative Commons. 157 | -------------------------------------------------------------------------------- /changelog: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | 1.1.0 | 15/10/2021 5 | Update input parameters: consensus and sequenceColor. 6 | 7 | 8 | 1.1.7 | 3/11/2021 9 | Fix lateralIndexes bug. 10 | -------------------------------------------------------------------------------- /dist/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BioComputingUP/ProSeqViewer/3bcadd92532207260ff30bc992c4b36676d53910/dist/assets/.gitkeep -------------------------------------------------------------------------------- /dist/assets/proseqviewer.css: -------------------------------------------------------------------------------- 1 | .root{ 2 | font-family: monospace, monospace; 3 | } 4 | 5 | .container { 6 | margin-left: 0; 7 | } 8 | 9 | .cnk { 10 | position: relative; 11 | white-space: nowrap; 12 | display: inline-block; 13 | vertical-align: top; 14 | } 15 | 16 | .idx { 17 | position: relative; 18 | display: inline-block; 19 | vertical-align: top; 20 | -moz-user-select: none; 21 | -ms-user-select: none; 22 | -webkit-user-select: none; 23 | -webkit-touch-callout: none; 24 | } 25 | 26 | .hidden { 27 | display: none; 28 | } 29 | 30 | .lblContainer { 31 | direction: rtl; 32 | } 33 | 34 | .lbl-hidden { 35 | padding-left: 2px; 36 | padding-right: 5px; 37 | -webkit-user-select: none; 38 | display:block; 39 | height:1em; 40 | line-height:1em; 41 | margin-bottom: 1.5em; 42 | font-style: italic; 43 | justify-content: flex-end; 44 | flex-direction: column; 45 | } 46 | 47 | .lbl { 48 | font-size: 1em; 49 | } 50 | 51 | .cell{ 52 | margin-bottom: 1.5em; 53 | height: 1em; 54 | display: inline-block; 55 | } 56 | 57 | .highlight { 58 | background-color: #EBD270!important;; 59 | } 60 | 61 | /* 'card' name is not available because used by bootstrap */ 62 | .crd { 63 | position: relative; 64 | display: inline-block; 65 | } 66 | 67 | .crds { 68 | display: inline-block; 69 | -webkit-user-select: none; 70 | } 71 | -------------------------------------------------------------------------------- /dist/index.d.ts: -------------------------------------------------------------------------------- 1 | export { ProSeqViewer } from './lib/proseqviewer'; 2 | -------------------------------------------------------------------------------- /dist/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | exports.ProSeqViewer = void 0; 4 | var proseqviewer_1 = require("./lib/proseqviewer"); 5 | Object.defineProperty(exports, "ProSeqViewer", { enumerable: true, get: function () { return proseqviewer_1.ProSeqViewer; } }); 6 | -------------------------------------------------------------------------------- /dist/sqv-bundle.js: -------------------------------------------------------------------------------- 1 | /******/ (() => { // webpackBootstrap 2 | /******/ "use strict"; 3 | /******/ var __webpack_modules__ = ({ 4 | 5 | /***/ 333: 6 | /***/ ((__unused_webpack_module, exports) => { 7 | 8 | 9 | Object.defineProperty(exports, "__esModule", ({ value: true })); 10 | exports.ColorsModel = void 0; 11 | class ColorsModel { 12 | static getRowsList(coloring) { 13 | const outCol = this.palette[coloring]; 14 | if (!outCol) { 15 | return []; 16 | } 17 | return Object.keys(outCol); 18 | } 19 | static getPositions(coloring, rowNum) { 20 | let outCol; 21 | outCol = this.palette[coloring]; 22 | if (!outCol) { 23 | return []; 24 | } 25 | outCol = outCol[rowNum]; 26 | if (!outCol) { 27 | return []; 28 | } 29 | outCol = outCol.positions; 30 | if (!outCol) { 31 | return []; 32 | } 33 | return outCol; 34 | } 35 | process(allInputs) { 36 | if (!allInputs.regions) { 37 | allInputs.regions = []; 38 | } 39 | if (allInputs.options && !allInputs.options.sequenceColor) { 40 | const sequenceColorRegions = []; 41 | for (const sequence of allInputs.sequences) { 42 | if (sequence.sequenceColor) { 43 | // @ts-ignore 44 | sequenceColorRegions.push({ sequenceId: sequence.id, start: 1, end: sequence.sequence.length, sequenceColor: sequence.sequenceColor }); 45 | } 46 | } 47 | for (const reg of allInputs.regions) { 48 | if (!reg.backgroundColor && reg.sequenceId !== -99999999999998) { 49 | sequenceColorRegions.push(reg); 50 | } 51 | } 52 | if (sequenceColorRegions.length > 0) { 53 | allInputs.regions = sequenceColorRegions; 54 | } 55 | } 56 | const allRegions = Array.prototype.concat(allInputs.icons, allInputs.regions, allInputs.patterns); // ordering 57 | let newRegions = this.fixMissingIds(allRegions, allInputs.sequences); 58 | newRegions = this.transformInput(allRegions, newRegions, allInputs.sequences, allInputs.options); 59 | this.transformColors(allInputs.options); 60 | return newRegions; 61 | } 62 | // transform input structure 63 | transformInput(regions, newRegions, sequences, globalColor) { 64 | // if don't receive new colors, keep old colors 65 | if (!regions) { 66 | return; 67 | } 68 | // if receive new colors, change them 69 | ColorsModel.palette = {}; 70 | let info; 71 | if (!globalColor) { 72 | for (const seq of sequences) { 73 | let reg = { sequenceId: seq.id, backgroundColor: '', start: 1, end: seq.sequence.length, sequenceColor: '' }; 74 | if (seq.sequenceColor) { 75 | reg.backgroundColor = seq.sequenceColor; 76 | reg.sequenceColor = seq.sequenceColor; 77 | info = this.setSequenceColor(reg, seq); 78 | } 79 | } 80 | } 81 | // overwrite region color if sequenceColor is set 82 | // @ts-ignore 83 | for (const reg of newRegions) { 84 | let sequenceColor; 85 | if (reg.icon) { 86 | continue; 87 | } 88 | if (sequences.find(x => x.id === reg.sequenceId)) { 89 | sequenceColor = sequences.find(x => x.id === reg.sequenceId).sequenceColor; 90 | if (sequenceColor && !globalColor) { 91 | // sequenceColor is set. Cannot set backgroundColor 92 | reg.sequenceColor = sequenceColor; 93 | } 94 | } 95 | info = this.processColor(reg); 96 | if (info === -1) { 97 | continue; 98 | } 99 | ColorsModel.palette[info.type][info.sequenceId].positions 100 | .push({ start: reg.start, end: reg.end, target: info.letterStyle }); 101 | if (sequenceColor && sequenceColor.includes('binary')) { 102 | // @ts-ignore 103 | ColorsModel.palette[info.type].binaryColors = this.getBinaryColors(sequenceColor); 104 | } 105 | } 106 | return newRegions; 107 | } 108 | setSequenceColor(reg, seq) { 109 | let info; 110 | info = this.processColor(reg); 111 | ColorsModel.palette[info.type][info.sequenceId].positions 112 | .push({ start: reg.start, end: reg.end, target: info.letterStyle }); 113 | if (seq.sequenceColor.includes('binary')) { 114 | // @ts-ignore 115 | ColorsModel.palette[info.type].binaryColors = this.getBinaryColors(seq.sequenceColor); 116 | } 117 | return info; 118 | } 119 | fixMissingIds(regions, sequences) { 120 | const newRegions = []; 121 | for (const reg of regions) { 122 | if (!reg) { 123 | continue; 124 | } 125 | if (sequences.find(x => x.id === reg.sequenceId)) { 126 | newRegions.push(reg); 127 | } 128 | else { 129 | for (const seq of sequences) { 130 | const newReg = {}; 131 | // tslint:disable-next-line:forin 132 | for (const key in reg) { 133 | if (reg[key] !== 'sequenceId') { 134 | newReg[key] = reg[key]; 135 | } 136 | newReg['sequenceId'] = seq.id; 137 | } 138 | newRegions.push(newReg); 139 | } 140 | } 141 | } 142 | return newRegions; 143 | } 144 | transformColors(opt) { 145 | const sequenceColor = opt.sequenceColor; 146 | let arrColors; 147 | let n; 148 | let c; 149 | for (const type in ColorsModel.palette) { 150 | switch (type) { 151 | case 'gradient': { 152 | // tslint:disable-next-line:forin 153 | for (const row in ColorsModel.palette[type]) { 154 | c = ColorsModel.palette[type][row]; 155 | n = c.positions.length + c.chars.length; 156 | arrColors = this.gradient(n); 157 | c.positions.sort((a, b) => (a.start > b.start) ? 1 : -1); 158 | for (const e of c.positions) { 159 | e.backgroundColor = arrColors.pop(); 160 | } 161 | } 162 | break; 163 | } 164 | case 'binary': { 165 | // tslint:disable-next-line:forin 166 | for (const row in ColorsModel.palette[type]) { 167 | if (row === 'binaryColors') { 168 | continue; 169 | } 170 | c = ColorsModel.palette[type][row]; 171 | n = c.positions.length + c.chars.length; 172 | arrColors = this.binary(n, ColorsModel.palette[type].binaryColors); 173 | c.positions.sort((a, b) => (a.start > b.start) ? 1 : -1); 174 | for (const e of c.positions) { 175 | e.backgroundColor = arrColors.pop(); 176 | } 177 | } 178 | break; 179 | } 180 | case sequenceColor: { 181 | // tslint:disable-next-line:forin 182 | // ColorsModel.palette[type]: an obj with regions and color associated es. positions: 1-200, zappo 183 | for (const row in ColorsModel.palette[type]) { 184 | c = ColorsModel.palette[type][row]; 185 | if (c.positions.length > 0) { 186 | for (const pos of c.positions) { 187 | pos.backgroundColor = sequenceColor; 188 | } 189 | } 190 | } 191 | break; 192 | } 193 | } 194 | } 195 | } 196 | processColor(e) { 197 | const result = { type: 'custom', sequenceId: -1, letterStyle: '' }; 198 | // check if row key is a number 199 | if (e.sequenceId === undefined || isNaN(+e.sequenceId)) { 200 | // wrong entity row key 201 | return -1; 202 | } 203 | result.sequenceId = +e.sequenceId; 204 | // transform target in CSS property 205 | if (e.color) { 206 | result.letterStyle = `color:${e.color};`; 207 | } 208 | if (e.backgroundColor) { 209 | result.letterStyle += `background-color:${e.backgroundColor};`; 210 | } 211 | if (e.backgroundImage) { 212 | result.letterStyle += `background-image: ${e.backgroundImage};`; 213 | } 214 | // define color or palette 215 | if (e.sequenceColor) { 216 | result.type = e.sequenceColor; 217 | } 218 | if (result.type.includes('binary')) { 219 | result.type = 'binary'; 220 | } 221 | // reserving space for the transformed object (this.palette) 222 | // if color type not inserted yet 223 | if (!(result.type in ColorsModel.palette)) { 224 | ColorsModel.palette[result.type] = {}; 225 | } 226 | // if row not inserted yet 227 | if (!(result.sequenceId in ColorsModel.palette[result.type])) { 228 | ColorsModel.palette[result.type][result.sequenceId] = { positions: [], chars: [] }; 229 | } 230 | return result; 231 | } 232 | gradient(n) { 233 | return this.evenlySpacedColors(n); 234 | } 235 | getBinaryColors() { 236 | const color1 = '#93E1D8'; 237 | const color2 = '#FFA69E'; 238 | return [color1, color2]; 239 | } 240 | binary(n, binaryColors) { 241 | let reg = 0; 242 | let flag; 243 | const arrColors = []; 244 | while (reg < n) { 245 | if (flag) { 246 | arrColors.push(binaryColors[0]); 247 | flag = !flag; 248 | } 249 | else { 250 | arrColors.push(binaryColors[1]); 251 | flag = !flag; 252 | } 253 | reg += 1; 254 | } 255 | return arrColors; 256 | } 257 | evenlySpacedColors(n) { 258 | /** how to go around the rgb wheel */ 259 | /** add to next rgb component, subtract to previous */ 260 | /** ex.: 255,0,0 -(add)-> 255,255,0 -(subtract)-> 0,255,0 */ 261 | // starting color: red 262 | const rgb = [255, 0, 0]; 263 | // 1536 colors in the rgb wheel 264 | const delta = Math.floor(1536 / n); 265 | let remainder; 266 | let add = true; 267 | let value = 0; 268 | let tmp; 269 | const colors = []; 270 | for (let i = 0; i < n; i++) { 271 | remainder = delta; 272 | while (remainder > 0) { 273 | if (add) { 274 | tmp = (((value + 1) % 3) + 3) % 3; 275 | if (rgb[tmp] + remainder > 255) { 276 | remainder -= (255 - rgb[tmp]); 277 | rgb[tmp] = 255; 278 | add = false; 279 | value = tmp; 280 | } 281 | else { 282 | rgb[tmp] += remainder; 283 | remainder = 0; 284 | } 285 | } 286 | else { 287 | tmp = (((value - 1) % 3) + 3) % 3; 288 | if (rgb[tmp] - remainder < 0) { 289 | remainder -= rgb[tmp]; 290 | rgb[tmp] = 0; 291 | add = true; 292 | } 293 | else { 294 | rgb[tmp] -= remainder; 295 | remainder = 0; 296 | } 297 | } 298 | } 299 | colors.push('rgba(' + rgb[0] + ',' + rgb[1] + ',' + rgb[2] + ', 0.4)'); 300 | } 301 | return colors; 302 | } 303 | } 304 | exports.ColorsModel = ColorsModel; 305 | 306 | 307 | /***/ }), 308 | 309 | /***/ 588: 310 | /***/ ((__unused_webpack_module, exports, __webpack_require__) => { 311 | 312 | 313 | Object.defineProperty(exports, "__esModule", ({ value: true })); 314 | exports.ConsensusModel = void 0; 315 | const palettes_1 = __webpack_require__(548); 316 | class ConsensusModel { 317 | static setConsensusInfo(type, sequences) { 318 | const idIdentity = -99999999999999; 319 | const idPhysical = -99999999999998; 320 | const consensusInfo = []; 321 | for (let i = 0; i < sequences[0].sequence.length; i++) { 322 | const consensusColumn = {}; 323 | for (const sequence of sequences) { 324 | let letter = sequence.sequence[i]; 325 | if (type === 'physical') { 326 | if (sequence.id === idIdentity) { 327 | continue; 328 | } 329 | if (letter in palettes_1.Palettes.consensusAaLesk) { 330 | letter = palettes_1.Palettes.consensusAaLesk[letter][0]; 331 | } 332 | } 333 | else { 334 | if (sequence.id === idPhysical) { 335 | continue; 336 | } 337 | } 338 | if (letter === '-' || !letter) { 339 | continue; 340 | } 341 | if (consensusColumn[letter]) { 342 | consensusColumn[letter] += 1; 343 | } 344 | else { 345 | consensusColumn[letter] = 1; 346 | } 347 | } 348 | consensusInfo.push(consensusColumn); 349 | } 350 | return consensusInfo; 351 | } 352 | static createConsensus(type, consensus, consensus2, sequences, regions, threshold, palette) { 353 | if (threshold < 50) { 354 | threshold = 100 - threshold; 355 | } 356 | let id = -99999999999999; 357 | let label; 358 | if (type === 'physical') { 359 | label = 'Consensus physical ' + threshold + '%'; 360 | id = -99999999999998; 361 | } 362 | else { 363 | label = 'Consensus identity ' + threshold + '%'; 364 | } 365 | let consensusSequence = ''; 366 | // tslint:disable-next-line:forin 367 | for (const column in consensus) { 368 | let maxLetter; 369 | let maxIndex; 370 | if (Object.keys(consensus[column]).length === 0) { 371 | maxLetter = '.'; 372 | } 373 | else { 374 | maxLetter = Object.keys(consensus[column]).reduce((a, b) => consensus[column][a] > consensus[column][b] ? a : b); 375 | maxIndex = consensus[column][maxLetter]; 376 | } 377 | let backgroundColor; 378 | let color; 379 | const frequency = (maxIndex / sequences.length) * 100; 380 | if (type === 'physical') { 381 | // consensus id to see if I have all letters equals 382 | // equals letters have precedence over properties 383 | let maxLetterId; 384 | let maxIndexId; 385 | if (Object.keys(consensus[column]).length === 0) { 386 | maxLetterId = '.'; 387 | } 388 | else { 389 | maxLetterId = Object.keys(consensus2[column]).reduce((a, b) => consensus2[column][a] > consensus2[column][b] ? a : b); 390 | maxIndexId = consensus2[column][maxLetterId]; 391 | } 392 | const frequencyId = (maxIndexId / sequences.length) * 100; 393 | if (frequencyId >= threshold) { 394 | maxLetter = maxLetterId; 395 | [backgroundColor, color] = ConsensusModel.setColorsIdentity(frequencyId, palette, 'physical'); 396 | } 397 | else { 398 | if (frequency >= threshold) { 399 | [backgroundColor, color] = ConsensusModel.setColorsPhysical(maxLetter, palette); 400 | } 401 | } 402 | } 403 | else { 404 | [backgroundColor, color] = ConsensusModel.setColorsIdentity(frequency, palette, 'identity'); 405 | } 406 | if (frequency < threshold) { 407 | maxLetter = '.'; 408 | } 409 | // + 1 because residues start from 1 and not 0 410 | regions.push({ start: +column + 1, end: +column + 1, sequenceId: id, backgroundColor, color }); 411 | consensusSequence += maxLetter; 412 | } 413 | sequences.push({ id, sequence: consensusSequence, label }); 414 | return [sequences, regions]; 415 | } 416 | static setColorsIdentity(frequency, palette, flag) { 417 | let backgroundColor; 418 | let color; 419 | let finalPalette; 420 | if (palette && typeof palette !== 'string' && flag == 'identity') { 421 | finalPalette = palette; 422 | } 423 | else { 424 | finalPalette = palettes_1.Palettes.consensusLevelsIdentity; 425 | } 426 | let steps = []; 427 | for (let key in finalPalette) { 428 | steps.push(+key); // 42 429 | } 430 | steps = steps.sort((a, b) => a < b ? 1 : a > b ? -1 : 0); 431 | for (const step of steps) { 432 | if (frequency >= step) { 433 | backgroundColor = finalPalette[step][0]; 434 | color = finalPalette[step][1]; 435 | break; 436 | } 437 | } 438 | return [backgroundColor, color]; 439 | } 440 | static setColorsPhysical(letter, palette) { 441 | let finalPalette; 442 | let backgroundColor; 443 | let color; 444 | if (palette && typeof palette !== 'string') { 445 | finalPalette = palette; 446 | } 447 | else { 448 | finalPalette = palettes_1.Palettes.consensusAaLesk; 449 | } 450 | for (const el in finalPalette) { 451 | if (finalPalette[el][0] == letter) { 452 | backgroundColor = finalPalette[el][1]; 453 | color = finalPalette[el][2]; 454 | break; 455 | } 456 | } 457 | return [backgroundColor, color]; 458 | } 459 | process(sequences, regions, options) { 460 | if (!regions) { 461 | regions = []; 462 | } 463 | let maxIdx = 0; 464 | for (const row of sequences) { 465 | if (maxIdx < row.sequence.length) { 466 | maxIdx = row.sequence.length; 467 | } 468 | } 469 | for (const row of sequences) { 470 | const diff = maxIdx - row.sequence.length; 471 | if (diff > 0 && row.id !== -99999999999999 && row.id !== -99999999999998) { 472 | for (let i = 0; i < diff; i++) { 473 | row.sequence += '-'; 474 | } 475 | } 476 | } 477 | if (options.sequenceColorMatrix) { 478 | regions = []; 479 | sequences.sort((a, b) => a.id - b.id); 480 | const min = sequences[0]; 481 | let palette = palettes_1.Palettes.substitutionMatrixBlosum; 482 | // console.log(palette) 483 | if (options.sequenceColorMatrixPalette) { 484 | palette = options.sequenceColorMatrixPalette; 485 | } 486 | let key; 487 | // tslint:disable-next-line:prefer-for-of 488 | for (let i = 0; i < min.sequence.length; i++) { 489 | for (const sequence of sequences) { 490 | if (sequence.id === min.id) { 491 | key = sequence.sequence[i] + sequence.sequence[i]; 492 | if (key in palette) { 493 | regions.push({ sequenceId: sequence.id, start: i + 1, end: i + 1, 494 | backgroundColor: palette[key][0], color: palette[key][1] }); 495 | } 496 | } 497 | else { 498 | // score with first sequence 499 | key = sequence.sequence[i] + min.sequence[i]; 500 | if (key in palette) { 501 | regions.push({ sequenceId: sequence.id, start: i + 1, end: i + 1, 502 | backgroundColor: palette[key][0] }); 503 | } 504 | else if (palette[min.sequence[i] + sequence.sequence[i]]) { 505 | key = min.sequence[i] + sequence.sequence[i]; 506 | regions.push({ sequenceId: sequence.id, start: i + 1, end: i + 1, 507 | backgroundColor: palette[key][0], color: palette[key][1] }); 508 | } 509 | } 510 | } 511 | } 512 | } 513 | else if (options.sequenceColor) { 514 | regions = []; 515 | for (const sequence of sequences) { 516 | sequence.sequenceColor = options.sequenceColor; 517 | regions.push({ sequenceId: sequence.id, start: 1, end: sequence.sequence.length, sequenceColor: options.sequenceColor }); 518 | } 519 | } 520 | let consensusInfoIdentity; 521 | let consensusInfoPhysical; 522 | if (options.consensusColorIdentity) { 523 | consensusInfoIdentity = ConsensusModel.setConsensusInfo('identity', sequences); 524 | [sequences, regions] = ConsensusModel.createConsensus('identity', consensusInfoIdentity, false, sequences, regions, options.dotThreshold, options.consensusColorIdentity); 525 | } 526 | else if (options.consensusColorMapping) { 527 | consensusInfoPhysical = ConsensusModel.setConsensusInfo('physical', sequences); 528 | if (!consensusInfoIdentity) { 529 | consensusInfoIdentity = ConsensusModel.setConsensusInfo('identity', sequences); 530 | } 531 | [sequences, regions] = ConsensusModel.createConsensus('physical', consensusInfoPhysical, consensusInfoIdentity, sequences, regions, options.dotThreshold, options.consensusColorMapping); 532 | } 533 | return [sequences, regions]; 534 | } 535 | } 536 | exports.ConsensusModel = ConsensusModel; 537 | 538 | 539 | /***/ }), 540 | 541 | /***/ 252: 542 | /***/ ((__unused_webpack_module, exports) => { 543 | 544 | 545 | Object.defineProperty(exports, "__esModule", ({ value: true })); 546 | exports.EventsModel = void 0; 547 | class EventsModel { 548 | onRegionSelected() { 549 | const sequenceViewers = document.getElementsByClassName('cell'); 550 | // @ts-ignore 551 | for (const sqv of sequenceViewers) { 552 | sqv.addEventListener('dblclick', r => { 553 | const evt = new CustomEvent('onRegionSelected', { detail: { char: r.srcElement.innerHTML, x: r.srcElement.dataset.resX, y: r.srcElement.dataset.resY } }); 554 | window.dispatchEvent(evt); 555 | }); 556 | } 557 | } 558 | } 559 | exports.EventsModel = EventsModel; 560 | 561 | 562 | /***/ }), 563 | 564 | /***/ 312: 565 | /***/ ((__unused_webpack_module, exports) => { 566 | 567 | 568 | Object.defineProperty(exports, "__esModule", ({ value: true })); 569 | exports.Icons = void 0; 570 | class Icons { 571 | } 572 | exports.Icons = Icons; 573 | Icons.lollipop = ''; 574 | Icons.arrowLeft = ' '; 575 | Icons.arrowRight = ' '; 576 | Icons.strand = ''; 577 | Icons.noSecondary = ''; 578 | Icons.helix = ''; 579 | Icons.turn = ''; 580 | 581 | 582 | /***/ }), 583 | 584 | /***/ 473: 585 | /***/ ((__unused_webpack_module, exports, __webpack_require__) => { 586 | 587 | 588 | Object.defineProperty(exports, "__esModule", ({ value: true })); 589 | exports.IconsModel = void 0; 590 | const icons_1 = __webpack_require__(312); 591 | class IconsModel { 592 | process(regions, sequences, iconsPaths) { 593 | const rows = {}; 594 | if (regions && sequences) { 595 | for (const seq of sequences) { 596 | for (const reg of regions) { 597 | if (+seq.id === reg.sequenceId) { 598 | if (!rows[seq.id]) { 599 | rows[seq.id] = {}; 600 | } 601 | // tslint:disable-next-line:forin 602 | for (let key in sequences.find(x => x.id === seq.id).sequence) { 603 | key = (+key + 1).toString(); 604 | // chars with icon 605 | if (+key >= reg.start && +key <= reg.end && reg.icon) { 606 | if (reg.icon) { 607 | const region = reg.end - (reg.start - 1); 608 | const center = Math.floor(region / 2); 609 | let icon; 610 | if (reg.color && reg.color[0] === '(') { 611 | reg.color = 'rgb' + reg.color; 612 | } 613 | // default icons 614 | switch (reg.icon) { 615 | case 'lollipop': { 616 | icon = icons_1.Icons.lollipop; 617 | break; 618 | } 619 | case 'arrowRight': { 620 | icon = icons_1.Icons.arrowRight; 621 | break; 622 | } 623 | case 'arrowLeft': { 624 | icon = icons_1.Icons.arrowLeft; 625 | break; 626 | } 627 | case 'strand': { 628 | icon = icons_1.Icons.strand; 629 | break; 630 | } 631 | case 'noSecondary': { 632 | icon = icons_1.Icons.noSecondary; 633 | break; 634 | } 635 | case 'helix': { 636 | icon = icons_1.Icons.helix; 637 | break; 638 | } 639 | case 'turn': { 640 | icon = icons_1.Icons.turn; 641 | break; 642 | } 643 | default: { 644 | // customizable icons (svg) 645 | icon = reg.icon; 646 | break; 647 | } 648 | } 649 | if (reg.display === 'center' && +key === reg.start + center) { 650 | rows[seq.id][key] = { char: icon }; 651 | } 652 | else if (!reg.display) { 653 | rows[seq.id][key] = { char: icon }; 654 | } 655 | } 656 | } 657 | // chars without icon 658 | if (!rows[seq.id][key]) { 659 | rows[seq.id][key] = { char: '' }; 660 | } 661 | } 662 | } 663 | } 664 | } 665 | } 666 | const filteredRows = {}; 667 | // tslint:disable-next-line:forin 668 | for (const row in rows) { 669 | let flag; 670 | const chars = rows[row]; 671 | for (const char in rows[row]) { 672 | if (rows[row][char].char !== '') { 673 | flag = true; 674 | } 675 | } 676 | if (flag) { 677 | filteredRows[row] = chars; 678 | } 679 | } 680 | return filteredRows; 681 | } 682 | } 683 | exports.IconsModel = IconsModel; 684 | 685 | 686 | /***/ }), 687 | 688 | /***/ 175: 689 | /***/ ((__unused_webpack_module, exports) => { 690 | 691 | 692 | Object.defineProperty(exports, "__esModule", ({ value: true })); 693 | exports.OptionsModel = void 0; 694 | class OptionsModel { 695 | constructor() { 696 | this.options = { 697 | fontSize: '14px', 698 | chunkSize: 10, 699 | chunkSeparation: 1, 700 | emptyFiller: ' ', 701 | indexesLocation: null, 702 | wrapLine: true, 703 | viewerWidth: '', 704 | dotThreshold: 90, 705 | lineSeparation: '5px', 706 | sequenceColor: undefined, 707 | customPalette: undefined, 708 | sequenceColorMatrix: undefined, 709 | sequenceColorMatrixPalette: undefined, 710 | consensusColorIdentity: undefined, 711 | consensusColorMapping: undefined, 712 | selection: undefined 713 | }; 714 | } 715 | process(opt, consensus) { 716 | /** check input fontSize */ 717 | if (opt && opt.fontSize) { 718 | const fSize = opt.fontSize; 719 | const fNum = +fSize.substr(0, fSize.length - 2); 720 | const fUnit = fSize.substr(fSize.length - 2, 2); 721 | if (isNaN(fNum) || (fUnit !== 'px' && fUnit !== 'vw' && fUnit !== 'em')) { 722 | // wrong fontSize format 723 | } 724 | else { 725 | this.options.fontSize = fSize; 726 | } 727 | } 728 | else { 729 | // fontSize not set 730 | this.options.fontSize = '14px'; // default reset 731 | } 732 | /** check input chunkSize */ 733 | if (opt && opt.chunkSize) { 734 | const cSize = +opt.chunkSize; 735 | if (isNaN(cSize) || cSize < 0) { 736 | // wrong chunkSize format 737 | } 738 | else { 739 | this.options.chunkSize = cSize; 740 | } 741 | } 742 | /** check input spaceSize */ 743 | if (opt && opt.chunkSeparation) { 744 | const chunkSeparation = +opt.chunkSeparation; 745 | if (chunkSeparation >= 0) { 746 | this.options.chunkSeparation = chunkSeparation; 747 | } 748 | } 749 | if (opt && opt.chunkSize == 0) { 750 | this.options.chunkSize = 1; 751 | this.options.chunkSeparation = 0; 752 | } 753 | /** check indexesLocation value */ 754 | if (opt && opt.indexesLocation) { 755 | if (opt.indexesLocation == "top" || opt.indexesLocation == "lateral") { 756 | this.options.indexesLocation = opt.indexesLocation; 757 | } 758 | } 759 | /** check selection value */ 760 | if (opt && opt.selection) { 761 | if (opt.selection == "columnselection" || opt.selection == "areaselection") { 762 | this.options.selection = opt.selection; 763 | } 764 | } 765 | /** check sequenceColor value */ 766 | if (opt && opt.sequenceColor) { 767 | if (typeof opt.sequenceColor !== 'string') { 768 | const keys = Object.keys(opt.sequenceColor); 769 | if (keys[0].length === 1) { 770 | this.options.sequenceColor = 'custom'; 771 | this.options.customPalette = opt.sequenceColor; 772 | } 773 | else { 774 | this.options.sequenceColorMatrix = 'custom'; 775 | this.options.sequenceColorMatrixPalette = opt.sequenceColor; 776 | } 777 | } 778 | else { 779 | if (opt.sequenceColor === "blosum62") { 780 | this.options.sequenceColorMatrix = opt.sequenceColor; 781 | } 782 | else if (opt.sequenceColor === "clustal") { 783 | this.options.sequenceColor = opt.sequenceColor; 784 | } 785 | } 786 | } 787 | /** check consensusType value */ 788 | if (consensus && consensus.color) { 789 | if (typeof consensus.color !== 'string') { 790 | const keys = Object.keys(consensus.color); 791 | if (typeof (keys[0]) === 'string') { 792 | this.options.consensusColorIdentity = consensus.color; 793 | } 794 | else { 795 | this.options.consensusColorMapping = consensus.color; 796 | } 797 | } 798 | else { 799 | if (consensus.color === "identity") { 800 | this.options.consensusColorIdentity = consensus.color; 801 | } 802 | else if (consensus.color === "physical") { 803 | this.options.consensusColorMapping = consensus.color; 804 | } 805 | } 806 | } 807 | /** check consensusThreshold value */ 808 | if (consensus && consensus.dotThreshold) { 809 | if (typeof consensus.dotThreshold == 'number') { 810 | this.options.dotThreshold = consensus.dotThreshold; 811 | } 812 | } 813 | /** check rowMarginBottom value */ 814 | if (opt && opt.lineSeparation !== undefined) { 815 | const rSize = opt.lineSeparation; 816 | const rNum = +rSize.substr(0, rSize.length - 2); 817 | const rUnit = rSize.substr(rSize.length - 2, 2); 818 | if (isNaN(rNum) || (rUnit !== 'px' && rUnit !== 'vw' && rUnit !== 'em')) { 819 | // wrong lineSeparation format 820 | } 821 | else { 822 | this.options.lineSeparation = rSize; 823 | } 824 | } 825 | else { 826 | // lineSeparation not set 827 | this.options.lineSeparation = '5px'; // default reset 828 | } 829 | /** check wrapline value */ 830 | if (opt && typeof opt.wrapLine == 'boolean') { 831 | this.options.wrapLine = opt.wrapLine; 832 | } 833 | /** check oneLineWidth */ 834 | if (opt && opt.viewerWidth) { 835 | const viewerWidth = opt.viewerWidth; 836 | const olNum = +viewerWidth.substr(0, viewerWidth.length - 2); 837 | const olUnit = viewerWidth.substr(viewerWidth.length - 2, 2); 838 | if (isNaN(olNum) || (olUnit !== 'px' && olUnit !== 'vw' && olUnit !== 'em')) { 839 | // wrong oneLineWidth format 840 | } 841 | else { 842 | this.options.viewerWidth = viewerWidth; 843 | } 844 | } 845 | return this.options; 846 | } 847 | } 848 | exports.OptionsModel = OptionsModel; 849 | 850 | 851 | /***/ }), 852 | 853 | /***/ 548: 854 | /***/ ((__unused_webpack_module, exports) => { 855 | 856 | 857 | Object.defineProperty(exports, "__esModule", ({ value: true })); 858 | exports.Palettes = void 0; 859 | class Palettes { 860 | } 861 | exports.Palettes = Palettes; 862 | // AA propensities 863 | Palettes.clustal = { 864 | A: '#80a0f0', I: '#80a0f0', L: '#80a0f0', M: '#80a0f0', F: '#80a0f0', W: '#80a0f0', V: '#80a0f0', 865 | K: '#f01505', R: '#f01505', E: '#c048c0', D: '#c048c0', C: '#f08080', G: '#f09048', 866 | N: '#15c015', Q: '#15c015', S: '#15c015', T: '#15c015', P: '#c0c000', H: '#15a4a4', Y: '#15a4a4' 867 | }; 868 | Palettes.zappo = { 869 | A: '#ffafaf', R: '#6464ff', N: '#00ff00', D: '#ff0000', C: '#ffff00', Q: '#00ff00', E: '#ff0000', 870 | G: '#ff00ff', H: '#6464ff', I: '#ffafaf', L: '#ffafaf', K: '#ffafaf', M: '#ffc800', F: '#ff00ff', 871 | P: '#00ff00', S: '#00ff00', T: '#15c015', W: '#ffc800', V: '#ffc800', Y: '#ffafaf' 872 | }; 873 | Palettes.taylor = { 874 | A: '#ccff00', R: '#0000ff', N: '#cc00ff', D: '#ff0000', C: '#ffff00', Q: '#ff00cc', E: '#ff0066', 875 | G: '#ff9900', H: '#0066ff', I: '#66ff00', L: '#33ff00', K: '#6600ff', M: '#00ff00', F: '#00ff66', 876 | P: '#ffcc00', S: '#ff3300', T: '#ff6600', W: '#00ccff', V: '#00ffcc', Y: '#99ff00' 877 | }; 878 | Palettes.hydrophobicity = { 879 | A: '#ad0052', R: '#0000ff', N: '#0c00f3', D: '#0c00f3', C: '#c2003d', Q: '#0c00f3', E: '#0c00f3', 880 | G: '#6a0095', H: '#1500ea', I: '#ff0000', L: '#ea0015', K: '#0000ff', M: '#b0004f', F: '#cb0034', 881 | P: '#4600b9', S: '#5e00a1', T: '#61009e', W: '#5b00a4', V: '#4f00b0', Y: '#f60009', 882 | B: '#0c00f3', X: '#680097', Z: '#0c00f3' 883 | }; 884 | Palettes.helixpropensity = { 885 | A: '#e718e7', R: '#6f906f', N: '#1be41b', D: '#778877', C: '#23dc23', Q: '#926d92', E: '#ff00ff', 886 | G: '#00ff00', H: '#758a75', I: '#8a758a', L: '#ae51ae', K: '#a05fa0', M: '#ef10ef', F: '#986798', 887 | P: '#00ff00', S: '#36c936', T: '#47b847', W: '#8a758a', V: '#21de21', Y: '#857a85', 888 | B: '#49b649', X: '#758a75', Z: '#c936c9' 889 | }; 890 | Palettes.strandpropensity = { 891 | A: '#5858a7', R: '#6b6b94', N: '#64649b', D: '#2121de', C: '#9d9d62', Q: '#8c8c73', E: '#0000ff', 892 | G: '#4949b6', H: '#60609f', I: '#ecec13', L: '#b2b24d', K: '#4747b8', M: '#82827d', F: '#c2c23d', 893 | P: '#2323dc', S: '#4949b6', T: '#9d9d62', W: '#c0c03f', V: '#d3d32c', Y: '#ffff00', 894 | B: '#4343bc', X: '#797986', Z: '#4747b8' 895 | }; 896 | Palettes.turnpropensity = { 897 | A: '#2cd3d3', R: '#708f8f', N: '#ff0000', D: '#e81717', C: '#a85757', Q: '#3fc0c0', E: '#778888', 898 | G: '#ff0000', H: '#708f8f', I: '#00ffff', L: '#1ce3e3', K: '#7e8181', M: '#1ee1e1', F: '#1ee1e1', 899 | P: '#f60909', S: '#e11e1e', T: '#738c8c', W: '#738c8c', V: '#9d6262', Y: '#07f8f8', 900 | B: '#f30c0c', X: '#7c8383', Z: '#5ba4a4' 901 | }; 902 | Palettes.buriedindex = { 903 | A: '#00a35c', R: '#00fc03', N: '#00eb14', D: '#00eb14', C: '#0000ff', Q: '#00f10e', E: '#00f10e', 904 | G: '#009d62', H: '#00d52a', I: '#0054ab', L: '#007b84', K: '#00ff00', M: '#009768', F: '#008778', 905 | P: '#00e01f', S: '#00d52a', T: '#00db24', W: '#00a857', V: '#00e619', Y: '#005fa0', 906 | B: '#00eb14', X: '#00b649', Z: '#00f10e' 907 | }; 908 | Palettes.nucleotide = { 909 | A: '#64F73F', C: '#FFB340', G: '#EB413C', T: '#3C88EE', U: '#3C88EE' 910 | }; 911 | Palettes.purinepyrimidine = { 912 | A: '#FF83FA', C: '#40E0D0', G: '#FF83FA', T: '#40E0D0', U: '#40E0D0', R: '#FF83FA', Y: '#40E0D0' 913 | }; 914 | Palettes.consensusLevelsIdentity = { 915 | 100: ['#0A0A0A', '#FFFFFF'], 916 | 70: ['#333333', '#FFFFFF'], 917 | 40: ['#707070', '#FFFFFF'], 918 | 10: ['#A3A3A3', '#FFFFFF'], 919 | 0: ['#FFFFFF', '#FFFFFF'] 920 | }; 921 | // colour scheme in Lesk, Introduction to Bioinformatics 922 | Palettes.consensusAaLesk = { 923 | A: ['n', '#f09048', '#FFFFFF'], 924 | G: ['n', '#f09048', '#FFFFFF'], 925 | S: ['n', '#f09048', '#FFFFFF'], 926 | T: ['n', '#f09048', '#FFFFFF'], 927 | C: ['h', '#558B6E', '#FFFFFF'], 928 | V: ['h', '#558B6E', '#FFFFFF'], 929 | I: ['h', '#558B6E', '#FFFFFF'], 930 | L: ['h', '#558B6E', '#FFFFFF'], 931 | P: ['h', '#558B6E', '#FFFFFF'], 932 | F: ['h', '#558B6E', '#FFFFFF'], 933 | Y: ['h', '#558B6E', '#FFFFFF'], 934 | M: ['h', '#558B6E', '#FFFFFF'], 935 | W: ['h', '#558B6E', '#FFFFFF'], 936 | N: ['p', '#D4358D', '#FFFFFF'], 937 | Q: ['p', '#D4358D', '#FFFFFF'], 938 | H: ['p', '#D4358D', '#FFFFFF'], 939 | D: ['~', '#A10702', '#FFFFFF'], 940 | E: ['~', '#A10702', '#FFFFFF'], 941 | K: ['+', '#3626A7', '#FFFFFF'], 942 | R: ['+', '#3626A7', '#FFFFFF'] // +: positively charged 943 | }; 944 | Palettes.substitutionMatrixBlosum = { WF: ['#CFDBF2', '#000000'], QQ: ['#A1B8E3', '#000000'], 945 | HH: ['#7294D5', '#000000'], YY: ['#81A0D9', '#000000'], ZZ: ['#A1B8E3', '#000000'], 946 | CC: ['#6288D0', '#000000'], PP: ['#81A0D9', '#000000'], VI: ['#B0C4E8', '#000000'], 947 | VM: ['#CFDBF2', '#000000'], KK: ['#A1B8E3', '#000000'], ZK: ['#CFDBF2', '#000000'], 948 | DN: ['#CFDBF2', '#000000'], SS: ['#A1B8E3', '#000000'], QR: ['#CFDBF2', '#000000'], 949 | NN: ['#91ACDE', '#000000'], YF: ['#B0C4E8', '#000000'], VL: ['#CFDBF2', '#000000'], 950 | KR: ['#C0CFED', '#000000'], ED: ['#C0CFED', '#000000'], VV: ['#A1B8E3', '#000000'], 951 | MI: ['#CFDBF2', '#000000'], MM: ['#A1B8E3', '#000000'], ZD: ['#CFDBF2', '#000000'], 952 | FF: ['#91ACDE', '#000000'], BD: ['#A1B8E3', '#000000'], HN: ['#CFDBF2', '#000000'], 953 | TT: ['#A1B8E3', '#000000'], SN: ['#CFDBF2', '#000000'], LL: ['#A1B8E3', '#000000'], 954 | EQ: ['#C0CFED', '#000000'], YW: ['#C0CFED', '#000000'], EE: ['#A1B8E3', '#000000'], 955 | KQ: ['#CFDBF2', '#000000'], WW: ['#3867BC', '#000000'], ML: ['#C0CFED', '#000000'], 956 | KE: ['#CFDBF2', '#000000'], ZE: ['#A1B8E3', '#000000'], ZQ: ['#B0C4E8', '#000000'], 957 | BE: ['#CFDBF2', '#000000'], DD: ['#91ACDE', '#000000'], SA: ['#CFDBF2', '#000000'], 958 | YH: ['#C0CFED', '#000000'], GG: ['#91ACDE', '#000000'], AA: ['#A1B8E3', '#000000'], 959 | II: ['#A1B8E3', '#000000'], TS: ['#CFDBF2', '#000000'], RR: ['#A1B8E3', '#000000'], 960 | LI: ['#C0CFED', '#000000'], ZB: ['#CFDBF2', '#000000'], BN: ['#B0C4E8', '#000000'], 961 | BB: ['#A1B8E3', '#000000'] 962 | }; 963 | 964 | 965 | /***/ }), 966 | 967 | /***/ 227: 968 | /***/ ((__unused_webpack_module, exports) => { 969 | 970 | 971 | Object.defineProperty(exports, "__esModule", ({ value: true })); 972 | exports.PatternsModel = void 0; 973 | class PatternsModel { 974 | // find index of matched regex positions and create array of regions with color 975 | process(patterns, sequences) { 976 | if (!patterns) { 977 | return; 978 | } 979 | const regions = []; // OutPatterns 980 | // @ts-ignore 981 | for (const element of patterns) { 982 | // tslint:disable-next-line:no-conditional-assignment 983 | const pattern = element.pattern; 984 | let str; 985 | if (sequences.find(x => x.id === element.sequenceId)) { 986 | str = sequences.find(x => x.id === element.sequenceId).sequence; 987 | if (element.start && element.end) { 988 | str = str.substr(element.start - 1, element.end - (element.start - 1)); 989 | } 990 | this.regexMatch(str, pattern, regions, element); 991 | } 992 | else { 993 | for (const seq of sequences) { 994 | // regex 995 | if (element.start && element.end) { 996 | str = seq.sequence.substr(element.start - 1, element.end - (element.start - 1)); 997 | } 998 | this.regexMatch(str, pattern, regions, element); 999 | } 1000 | } 1001 | } 1002 | return regions; 1003 | } 1004 | regexMatch(str, pattern, regions, element) { 1005 | const re = new RegExp(pattern, "g"); 1006 | let match; 1007 | // tslint:disable-next-line:no-conditional-assignment 1008 | while ((match = re.exec(str)) != null) { 1009 | regions.push({ start: +match.index + 1, end: +match.index + +match[0].length, 1010 | backgroundColor: element.backgroundColor, color: element.color, backgroundImage: element.backgroundImage, 1011 | borderColor: element.borderColor, borderStyle: element.borderStyle, sequenceId: element.sequenceId }); 1012 | } 1013 | } 1014 | } 1015 | exports.PatternsModel = PatternsModel; 1016 | 1017 | 1018 | /***/ }), 1019 | 1020 | /***/ 505: 1021 | /***/ ((__unused_webpack_module, exports, __webpack_require__) => { 1022 | 1023 | 1024 | Object.defineProperty(exports, "__esModule", ({ value: true })); 1025 | exports.ProSeqViewer = void 0; 1026 | const options_model_1 = __webpack_require__(175); 1027 | const rows_model_1 = __webpack_require__(912); 1028 | const colors_model_1 = __webpack_require__(333); 1029 | const selection_model_1 = __webpack_require__(788); 1030 | const icons_model_1 = __webpack_require__(473); 1031 | const sequenceInfoModel_1 = __webpack_require__(695); 1032 | const events_model_1 = __webpack_require__(252); 1033 | const patterns_model_1 = __webpack_require__(227); 1034 | const consensus_model_1 = __webpack_require__(588); 1035 | class ProSeqViewer { 1036 | constructor(divId) { 1037 | this.divId = divId; 1038 | this.init = false; 1039 | this.params = new options_model_1.OptionsModel(); 1040 | this.rows = new rows_model_1.RowsModel(); 1041 | this.consensus = new consensus_model_1.ConsensusModel(); 1042 | this.regions = new colors_model_1.ColorsModel(); 1043 | this.patterns = new patterns_model_1.PatternsModel(); 1044 | this.icons = new icons_model_1.IconsModel(); 1045 | this.labels = new sequenceInfoModel_1.SequenceInfoModel(); 1046 | this.selection = new selection_model_1.SelectionModel(); 1047 | this.events = new events_model_1.EventsModel(); 1048 | window.onresize = () => { 1049 | this.calculateIdxs(false); 1050 | }; 1051 | window.onclick = () => { 1052 | this.calculateIdxs(true); 1053 | }; // had to add this to cover mobidb toggle event 1054 | } 1055 | calculateIdxs(flag) { 1056 | for (const id of ProSeqViewer.sqvList) { 1057 | if (document.getElementById(id) != null) { 1058 | const sqvBody = document.getElementById(id); 1059 | const chunks = sqvBody.getElementsByClassName('cnk'); 1060 | let oldTop = 0; 1061 | let newTop = 1; 1062 | for (let i = 0; i < chunks.length; i++) { 1063 | // erase old indexes before recalculating them 1064 | chunks[i].firstElementChild.className = 'idx hidden'; 1065 | if (flag) { 1066 | // avoid calculating if idx already set 1067 | if (chunks[i].firstElementChild.className === 'idx') { 1068 | return; 1069 | } 1070 | } 1071 | newTop = chunks[i].getBoundingClientRect().top + window.scrollY; 1072 | if (newTop > oldTop) { 1073 | chunks[i].firstElementChild.className = 'idx'; 1074 | oldTop = newTop; 1075 | } 1076 | } 1077 | } 1078 | } 1079 | } 1080 | draw(inputs) { 1081 | const sqvBody = document.getElementById(this.divId); 1082 | if (sqvBody) { 1083 | sqvBody.innerHTML = `
input error
`; 1084 | } 1085 | ProSeqViewer.sqvList.push(this.divId); 1086 | let labels; 1087 | let labelsFlag; 1088 | let startIndexes; 1089 | let tooltips; 1090 | let data; 1091 | /** check and process parameters input */ 1092 | inputs.options = this.params.process(inputs.options, inputs.consensus); 1093 | /** check and consensus input and global colorScheme */ 1094 | if (inputs.options) { 1095 | [inputs.sequences, inputs.regions] = this.consensus.process(inputs.sequences, inputs.regions, inputs.options); 1096 | } 1097 | /** check and process patterns input */ 1098 | inputs.patterns = this.patterns.process(inputs.patterns, inputs.sequences); 1099 | /** check and process colors input */ 1100 | inputs.regions = this.regions.process(inputs); 1101 | /** check and process icons input */ 1102 | let icons = this.icons.process(inputs.regions, inputs.sequences, inputs.icons); 1103 | /** check and process sequences input */ 1104 | data = this.rows.process(inputs.sequences, icons, inputs.regions, inputs.options); 1105 | /** check and process labels input */ 1106 | [labels, startIndexes, tooltips, labelsFlag] = this.labels.process(inputs.regions, inputs.sequences); 1107 | /** create/update sqv-body html */ 1108 | this.createGUI(data, labels, startIndexes, tooltips, inputs.options, labelsFlag); 1109 | /** listen copy paste events */ 1110 | this.selection.process(); 1111 | /** listen selection events */ 1112 | this.events.onRegionSelected(); 1113 | } 1114 | generateLabels(idx, labels, startIndexes, indexesLocation, chunkSize, fontSize, tooltips, data, lineSeparation) { 1115 | let labelshtml = ''; 1116 | let labelsContainer = ''; 1117 | const noGapsLabels = []; 1118 | if (labels.length > 0) { 1119 | if (indexesLocation != 'lateral') { 1120 | labelshtml += ``; 1121 | } 1122 | let flag; 1123 | let count = -1; 1124 | let seqN = -1; 1125 | for (const seqNum of data) { 1126 | if (noGapsLabels.length < data.length) { 1127 | noGapsLabels.push(0); 1128 | } 1129 | seqN += 1; 1130 | for (const res in seqNum) { 1131 | if (seqNum[res].char && seqNum[res].char.includes('svg')) { 1132 | flag = true; 1133 | break; 1134 | } 1135 | } 1136 | if (flag) { 1137 | noGapsLabels[seqN] = ''; 1138 | if (idx) { 1139 | // line with only icons, no need for index 1140 | labelshtml += ` ${noGapsLabels[seqN]}`; 1141 | } 1142 | else { 1143 | labelshtml += ``; 1144 | } 1145 | } 1146 | else { 1147 | count += 1; 1148 | if (idx) { 1149 | if (!chunkSize) { 1150 | // lateral index regular 1151 | labelshtml += ` 1152 | ${(startIndexes[count] - 1) + idx}`; 1153 | } 1154 | else { 1155 | let noGaps = 0; 1156 | for (const res in seqNum) { 1157 | if (+res <= (idx) && seqNum[res].char !== '-') { 1158 | noGaps += 1; 1159 | } 1160 | } 1161 | // lateral index gap 1162 | noGapsLabels[seqN] = noGaps; 1163 | labelshtml += ` 1164 | ${(startIndexes[count] - 1) + noGapsLabels[seqN]}`; 1165 | } 1166 | } 1167 | else { 1168 | labelshtml += `${labels[count]}${tooltips[count]}`; 1169 | } 1170 | } 1171 | flag = false; 1172 | } 1173 | if (indexesLocation == 'lateral' || 'both') { 1174 | labelsContainer = `${labelshtml}`; 1175 | } 1176 | else { 1177 | // add margin in case we only have labels and no indexes 1178 | labelsContainer = `${labelshtml}`; 1179 | } 1180 | } 1181 | return labelsContainer; 1182 | } 1183 | addTopIndexes(chunkSize, x, maxTop, rowMarginBottom) { 1184 | let cells = ''; 1185 | // adding top indexes 1186 | let chunkTopIndex; 1187 | if (x % chunkSize === 0 && x <= maxTop) { 1188 | chunkTopIndex = `${x}`; 1189 | } 1190 | else { 1191 | chunkTopIndex = ``; 1192 | } 1193 | cells += chunkTopIndex; 1194 | return cells; 1195 | } 1196 | createGUI(data, labels, startIndexes, tooltips, options, labelsFlag) { 1197 | const sqvBody = document.getElementById(this.divId); 1198 | // convert to nodes to improve rendering (idea to try): 1199 | // Create new element 1200 | // const root = document.createElement('div'); 1201 | // // Add class to element 1202 | // root.className = 'my-new-element'; 1203 | // // Add color 1204 | // root.style.color = 'red'; 1205 | // // Fill element with html 1206 | // root.innerHTML = ``; 1207 | // // Add element node to DOM graph 1208 | // sqvBody.appendChild(root); 1209 | // // Exit 1210 | // return; 1211 | if (!sqvBody) { 1212 | // Cannot find sqv-body element 1213 | return; 1214 | } 1215 | const chunkSize = options.chunkSize; 1216 | const fontSize = options.fontSize; 1217 | const chunkSeparation = options.chunkSeparation; 1218 | const indexesLocation = options.indexesLocation; 1219 | const wrapLine = options.wrapLine; 1220 | const viewerWidth = options.viewerWidth; 1221 | const lineSeparation = options.lineSeparation + ';'; 1222 | const fNum = +fontSize.substr(0, fontSize.length - 2); 1223 | const fUnit = fontSize.substr(fontSize.length - 2, 2); 1224 | // maxIdx = length of the longest sequence 1225 | let maxIdx = 0; 1226 | let maxTop = 0; 1227 | for (const row of data) { 1228 | if (maxIdx < Object.keys(row).length) { 1229 | maxIdx = Object.keys(row).length; 1230 | } 1231 | if (maxTop < Object.keys(row).length) { 1232 | maxTop = Object.keys(row).length; 1233 | } 1234 | } 1235 | const lenghtIndex = maxIdx.toString().length; 1236 | const indexWidth = (fNum * lenghtIndex).toString() + fUnit; 1237 | // consider the last chunk even if is not long enough 1238 | if (chunkSize > 0) { 1239 | maxIdx += (chunkSize - (maxIdx % chunkSize)) % chunkSize; 1240 | } 1241 | // generate labels 1242 | const labelsContainer = this.generateLabels(false, labels, startIndexes, indexesLocation, false, indexWidth, tooltips, data, lineSeparation); 1243 | let index = ''; 1244 | let cards = ''; 1245 | let cell; 1246 | let entity; 1247 | let style; 1248 | let html = ''; 1249 | let idxNum = 0; 1250 | let idx; 1251 | let cells = ''; 1252 | for (let x = 1; x <= maxIdx; x++) { 1253 | if (indexesLocation != 'lateral') { 1254 | cells = this.addTopIndexes(chunkSize, x, maxTop, lineSeparation); 1255 | } 1256 | ; 1257 | for (let y = 0; y < data.length; y++) { 1258 | entity = data[y][x]; 1259 | style = 'font-size: 1em;display:block;height:1em;line-height:1em;margin-bottom:' + lineSeparation; 1260 | if (y === data.length - 1) { 1261 | style = 'font-size: 1em;display:block;line-height:1em;margin-bottom:' + lineSeparation; 1262 | } 1263 | if (!entity) { 1264 | // emptyfiller 1265 | style = 'font-size: 1em;display:block;color: rgba(0, 0, 0, 0);height:1em;line-height:1em;margin-bottom:' + lineSeparation; 1266 | cell = `A`; // mock char, this has to be done to have chunks all of the same length (last chunk can't be shorter) 1267 | } 1268 | else { 1269 | if (entity.target) { 1270 | style += `${entity.target}`; 1271 | } 1272 | if (entity.char && !entity.char.includes('svg')) { 1273 | // y is the row, x is the column 1274 | cell = `${entity.char}`; 1276 | } 1277 | else { 1278 | style += '-webkit-user-select: none;'; 1279 | cell = `${entity.char}`; 1280 | } 1281 | } 1282 | cells += cell; 1283 | } 1284 | cards += `
${cells}
`; // width 3/5em to reduce white space around letters 1285 | cells = ''; 1286 | if (chunkSize > 0 && x % chunkSize === 0) { 1287 | // considering the row of top indexes 1288 | if (indexesLocation != 'top') { 1289 | idxNum += chunkSize; // lateral index (set only if top indexes missing) 1290 | idx = idxNum - (chunkSize - 1); 1291 | // adding labels 1292 | const gapsContainer = this.generateLabels(idx, labels, startIndexes, indexesLocation, chunkSize, indexWidth, false, data, lineSeparation); 1293 | if (labels[0] === '') { 1294 | index = gapsContainer; // lateral number indexes 1295 | } 1296 | else { 1297 | index = labelsContainer + gapsContainer; // lateral number indexes + labels 1298 | } 1299 | if (!labelsFlag) { 1300 | index = gapsContainer; // lateral number indexes 1301 | } 1302 | else { 1303 | // if(indexesLocation == 'both'){ 1304 | index = labelsContainer + gapsContainer; // lateral number indexes + labels 1305 | // } 1306 | } 1307 | } 1308 | else { 1309 | index = labelsContainer; // top 1310 | } 1311 | index = ``; 1312 | style = `font-size: ${fontSize};`; 1313 | if (x !== maxIdx) { 1314 | style += 'padding-right: ' + chunkSeparation + 'em;'; 1315 | } 1316 | else { 1317 | style += 'margin-right: ' + chunkSeparation + 'em;'; 1318 | } 1319 | let chunk = ''; 1320 | if (labelsFlag || options.consensusType || indexesLocation == 'both' || indexesLocation == 'lateral') { // both 1321 | chunk = `
${index}
${cards}
`; 1322 | } 1323 | else { 1324 | chunk = `
${cards}
`; // top 1325 | } 1326 | cards = ''; 1327 | index = ''; 1328 | html += chunk; 1329 | } 1330 | } 1331 | let innerHTML; 1332 | if (wrapLine) { 1333 | innerHTML = `
${html}
`; 1334 | } 1335 | else { 1336 | innerHTML = `
1337 |
${html}
1338 |
`; 1339 | } 1340 | sqvBody.innerHTML = innerHTML; 1341 | window.dispatchEvent(new Event('resize')); 1342 | } 1343 | } 1344 | window.ProSeqViewer = ProSeqViewer; 1345 | ProSeqViewer.sqvList = []; 1346 | // VERY IMPORTANT AND USEFUL TO BE ABLE TO HAVE A WORKING BUNDLE.JS!! NEVER DELETE THIS LINE 1347 | window.ProSeqViewer = ProSeqViewer; 1348 | 1349 | 1350 | /***/ }), 1351 | 1352 | /***/ 912: 1353 | /***/ ((__unused_webpack_module, exports, __webpack_require__) => { 1354 | 1355 | 1356 | Object.defineProperty(exports, "__esModule", ({ value: true })); 1357 | exports.RowsModel = void 0; 1358 | const palettes_1 = __webpack_require__(548); 1359 | const colors_model_1 = __webpack_require__(333); 1360 | class RowsModel { 1361 | constructor() { 1362 | this.substitutiveId = 99999999999999; 1363 | } 1364 | processRows(rows, icons, regions, opt) { 1365 | const allData = []; 1366 | // decide which color is more important in case of overwriting 1367 | const coloringOrder = ['custom', 'clustal', 'zappo', 'gradient', 'binary']; 1368 | // order row Numbers 1369 | const rowNumsOrdered = Object.keys(rows).map(Number).sort((n1, n2) => n1 - n2); 1370 | // order keys of Row object 1371 | const ordered = {}; 1372 | for (const rowNum of rowNumsOrdered) { 1373 | ordered[rowNum] = Object.keys(rows[+rowNum]).map(Number).sort((n1, n2) => n1 - n2); 1374 | } 1375 | let data; 1376 | let coloringRowNums; 1377 | let tmp; 1378 | // loop through data rows 1379 | for (const rowNum of rowNumsOrdered) { 1380 | tmp = ordered[rowNum]; 1381 | // data key: indexes, value: chars 1382 | data = rows[rowNum]; 1383 | // data[rowNum].label = this.rows.getLabel(rowNum, this.sequences); 1384 | // console.log(data) 1385 | if (regions) { 1386 | for (const coloring of coloringOrder.reverse()) { 1387 | coloringRowNums = colors_model_1.ColorsModel.getRowsList(coloring).map(Number); 1388 | // if there is coloring for the data row 1389 | if (coloringRowNums.indexOf(rowNum) < 0) { 1390 | // go to next coloring 1391 | continue; 1392 | } 1393 | const positions = colors_model_1.ColorsModel.getPositions(coloring, rowNum); 1394 | // positions = start, end, target (bgcolor || fgcolor) 1395 | if (positions.length > 0) { 1396 | for (const e of positions) { 1397 | for (let i = e.start; i <= e.end; i++) { 1398 | if (!data[i]) { 1399 | continue; 1400 | } 1401 | if (e.backgroundColor && !e.backgroundColor.startsWith('#')) { // is a palette 1402 | if (e.backgroundColor == 'custom') { 1403 | data[i].backgroundColor = opt.customPalette[data[i].char]; 1404 | } 1405 | else { 1406 | data[i].backgroundColor = palettes_1.Palettes[e.backgroundColor][data[i].char]; // e.backgroundcolor = zappo, clustal.. 1407 | } 1408 | } 1409 | else { 1410 | data[i].backgroundColor = e.backgroundColor; // is a region or pattern 1411 | } 1412 | data[i].target = e.target + 'background-color:' + data[i].backgroundColor; 1413 | } 1414 | } 1415 | } 1416 | } 1417 | if (icons !== {}) { 1418 | const iconsData = icons[rowNum]; 1419 | if (iconsData) { 1420 | allData.push(iconsData); 1421 | } 1422 | } 1423 | } 1424 | allData.push(data); 1425 | } 1426 | return allData; 1427 | } 1428 | process(sequences, icons, regions, opt) { 1429 | // check and set global sequenceColor 1430 | if (opt && opt.sequenceColor) { 1431 | // @ts-ignore 1432 | for (const sequence of sequences) { 1433 | if (!sequence.sequenceColor) { 1434 | sequence.sequenceColor = opt.sequenceColor; 1435 | } 1436 | } 1437 | } 1438 | // keep previous data 1439 | if (!sequences) { 1440 | return; 1441 | } 1442 | // reset data 1443 | const rows = {}; 1444 | // check if there are undefined or duplicate ids and prepare to reset them 1445 | const values = []; 1446 | let undefinedValues = 0; 1447 | for (const r of Object.keys(sequences)) { 1448 | if (isNaN(+sequences[r].id)) { 1449 | // missing id 1450 | undefinedValues += 1; 1451 | sequences[r].id = this.substitutiveId; 1452 | this.substitutiveId -= 1; 1453 | // otherwise just reset missing ids and log the resetted id 1454 | } 1455 | else { 1456 | if (values.includes(+sequences[r].id)) { 1457 | // Duplicate sequence id 1458 | delete sequences[r]; 1459 | } 1460 | else { 1461 | values.push(+sequences[r].id); 1462 | } 1463 | } 1464 | } 1465 | for (const row of Object.keys(sequences)) { 1466 | /** check sequences id type */ 1467 | let id; 1468 | if (isNaN(+sequences[row].id)) { 1469 | id = values.sort()[values.length - 1] + 1; 1470 | } 1471 | else { 1472 | id = sequences[row].id; 1473 | } 1474 | /** set row chars */ 1475 | rows[id] = {}; 1476 | for (const idx of Object.keys(sequences[row].sequence)) { 1477 | const idxKey = (+idx + 1).toString(); 1478 | const char = sequences[row].sequence[idx]; 1479 | rows[id][idxKey] = { char }; 1480 | } 1481 | } 1482 | return this.processRows(rows, icons, regions, opt); 1483 | } 1484 | } 1485 | exports.RowsModel = RowsModel; 1486 | 1487 | 1488 | /***/ }), 1489 | 1490 | /***/ 788: 1491 | /***/ ((__unused_webpack_module, exports) => { 1492 | 1493 | 1494 | Object.defineProperty(exports, "__esModule", ({ value: true })); 1495 | exports.SelectionModel = void 0; 1496 | class SelectionModel { 1497 | constructor() { 1498 | this.event_sequence = []; 1499 | } 1500 | set_start(e) { 1501 | let id; 1502 | let element; 1503 | if (e.path) { 1504 | // chrome support 1505 | element = e.path[0]; 1506 | id = document.getElementById(element.dataset.resId); 1507 | } 1508 | else { 1509 | // firefox support 1510 | element = e.originalTarget; 1511 | id = document.getElementById(element.dataset.resId); 1512 | } 1513 | this.lastId = element.dataset.resId; 1514 | this.lastSqv = id; 1515 | this.start = { y: element.dataset.resY, x: element.dataset.resX, sqvId: element.dataset.resId }; 1516 | this.lastOver = { y: element.dataset.resY, x: element.dataset.resX, sqvId: element.dataset.resId }; 1517 | const elements = document.querySelectorAll('[data-res-id=' + element.dataset.resId + ']'); 1518 | this.selectionhighlight(elements); 1519 | this.firstOver = false; 1520 | } 1521 | selectionhighlight(elements) { 1522 | for (const selection of elements) { 1523 | const x = +selection.getAttribute('data-res-x'); 1524 | const y = +selection.getAttribute('data-res-y'); 1525 | let firstX = Math.min(+this.start.x, +this.lastOver.x); 1526 | let lastX = Math.max(+this.start.x, +this.lastOver.x); 1527 | let firstY = Math.min(+this.start.y, +this.lastOver.y); 1528 | let lastY = Math.max(+this.start.y, +this.lastOver.y); 1529 | // on every drag reselect the whole area ... 1530 | if (x >= +firstX && x <= +lastX && 1531 | y >= +firstY && y <= +lastY && 1532 | selection.getAttribute('data-res-id') === this.lastOver.sqvId) { 1533 | selection.classList.add('highlight'); 1534 | } 1535 | else { 1536 | selection.classList.remove('highlight'); 1537 | } 1538 | } 1539 | } 1540 | process() { 1541 | const sequenceViewers = document.getElementsByClassName('cell'); 1542 | // remove selection on new click 1543 | window.onmousedown = (event) => { 1544 | this.event_sequence.push(0); 1545 | // @ts-ignore 1546 | for (const sqv of sequenceViewers) { 1547 | sqv.onmousedown = (e) => { 1548 | this.set_start(e); 1549 | }; 1550 | } 1551 | if (this.event_sequence[0] == 0 && this.event_sequence[1] == 1 && this.event_sequence[2] == 2 && this.event_sequence[0] == 0) { 1552 | // left click 1553 | const elements = document.querySelectorAll('[data-res-id=' + this.lastId + ']'); 1554 | // @ts-ignore 1555 | for (const selection of elements) { 1556 | selection.classList.remove('highlight'); 1557 | } 1558 | } 1559 | // if first click outside sqvDiv (first if is valid in Chrome, second in firefox) 1560 | if (!event.target.dataset.resX) { 1561 | this.firstOver = true; 1562 | } 1563 | if (event.explicitOriginalTarget && event.explicitOriginalTarget.dataset) { 1564 | this.firstOver = true; 1565 | } 1566 | this.event_sequence = [0]; 1567 | }; 1568 | // @ts-ignore 1569 | for (const sqv of sequenceViewers) { 1570 | sqv.onmouseover = (e) => { 1571 | if (!(1 in this.event_sequence)) { 1572 | this.event_sequence.push(1); 1573 | } 1574 | if (this.firstOver) { 1575 | this.set_start(e); 1576 | } 1577 | let element; 1578 | if (e.path) { 1579 | element = e.path[0]; 1580 | } 1581 | else { 1582 | element = e.originalTarget; 1583 | } 1584 | if (this.start) { 1585 | this.lastOver = { y: element.dataset.resY, x: element.dataset.resX, sqvId: element.dataset.resId }; 1586 | const elements = document.querySelectorAll('[data-res-id=' + element.dataset.resId + ']'); 1587 | if (this.lastId == element.dataset.resId) { 1588 | this.selectionhighlight(elements); 1589 | } 1590 | } 1591 | }; 1592 | } 1593 | document.body.onmouseup = () => { 1594 | this.event_sequence.push(2); 1595 | this.firstOver = false; 1596 | if (this.start) { 1597 | this.start = undefined; 1598 | } 1599 | if (this.event_sequence[0] == 0 && this.event_sequence[1] == 2) { 1600 | const elements = document.querySelectorAll('[data-res-id=' + this.lastId + ']'); 1601 | // @ts-ignore 1602 | for (const selection of elements) { 1603 | selection.classList.remove('highlight'); 1604 | } 1605 | } 1606 | }; 1607 | document.body.addEventListener('keydown', (e) => { 1608 | const elements = document.querySelectorAll('[data-res-id=' + this.lastId + ']'); 1609 | // @ts-ignore 1610 | e = e || window.event; 1611 | const key = e.which || e.keyCode; // keyCode detection 1612 | const ctrl = e.ctrlKey ? e.ctrlKey : ((key === 17)); // ctrl detection 1613 | if (key === 67 && ctrl) { 1614 | let textToPaste = ''; 1615 | const textDict = {}; 1616 | let row = ''; 1617 | // tslint:disable-next-line:forin 1618 | // @ts-ignore 1619 | for (const selection of elements) { 1620 | if (selection.classList.contains('highlight')) { 1621 | if (!textDict[selection.getAttribute('data-res-y')]) { 1622 | textDict[selection.getAttribute('data-res-y')] = ''; 1623 | } 1624 | // new line when new row 1625 | if (selection.getAttribute('data-res-y') !== row && row !== '') { 1626 | textDict[selection.getAttribute('data-res-y')] += selection.innerText; 1627 | } 1628 | else { 1629 | textDict[selection.getAttribute('data-res-y')] += selection.innerText; 1630 | } 1631 | row = selection.getAttribute('data-res-y'); 1632 | } 1633 | } 1634 | let flag; 1635 | for (const textRow in textDict) { 1636 | if (flag) { 1637 | textToPaste += '\n' + textDict[textRow]; 1638 | } 1639 | else { 1640 | textToPaste += textDict[textRow]; 1641 | flag = true; 1642 | } 1643 | } 1644 | if (textToPaste !== '') { 1645 | // copy to clipboard for the paste event 1646 | const dummy = document.createElement('textarea'); 1647 | document.body.appendChild(dummy); 1648 | dummy.value = textToPaste; 1649 | dummy.select(); 1650 | document.execCommand('copy'); 1651 | document.body.removeChild(dummy); 1652 | const evt = new CustomEvent('onHighlightSelection', { detail: { text: textToPaste, eventType: 'highlight selection' } }); 1653 | window.dispatchEvent(evt); 1654 | } 1655 | } 1656 | }, false); 1657 | } 1658 | } 1659 | exports.SelectionModel = SelectionModel; 1660 | 1661 | 1662 | /***/ }), 1663 | 1664 | /***/ 695: 1665 | /***/ ((__unused_webpack_module, exports) => { 1666 | 1667 | 1668 | Object.defineProperty(exports, "__esModule", ({ value: true })); 1669 | exports.SequenceInfoModel = void 0; 1670 | class SequenceInfoModel { 1671 | constructor() { 1672 | this.isHTML = (str) => { 1673 | const fragment = document.createRange().createContextualFragment(str); 1674 | // remove all non text nodes from fragment 1675 | fragment.querySelectorAll('*').forEach(el => el.parentNode.removeChild(el)); 1676 | // if there is textContent, then not a pure HTML 1677 | return !(fragment.textContent || '').trim(); 1678 | }; 1679 | } 1680 | process(regions, sequences) { 1681 | const labels = []; 1682 | const startIndexes = []; 1683 | const tooltips = []; 1684 | let flag; 1685 | sequences.sort((a, b) => a.id - b.id); 1686 | for (const seq of sequences) { 1687 | if (!seq) { 1688 | continue; 1689 | } 1690 | if (seq.startIndex) { 1691 | startIndexes.push(seq.startIndex); 1692 | } 1693 | else { 1694 | startIndexes.push(1); 1695 | } 1696 | if (seq.labelTooltip) { 1697 | tooltips.push(seq.labelTooltip); 1698 | } 1699 | else { 1700 | tooltips.push(''); 1701 | } 1702 | if (seq.label && !this.isHTML(seq.label)) { 1703 | labels.push(seq.label); 1704 | flag = true; // to check if I have at least one label 1705 | } 1706 | else { 1707 | labels.push(''); 1708 | } 1709 | } 1710 | return [labels, startIndexes, tooltips, flag]; 1711 | } 1712 | } 1713 | exports.SequenceInfoModel = SequenceInfoModel; 1714 | 1715 | 1716 | /***/ }) 1717 | 1718 | /******/ }); 1719 | /************************************************************************/ 1720 | /******/ // The module cache 1721 | /******/ var __webpack_module_cache__ = {}; 1722 | /******/ 1723 | /******/ // The require function 1724 | /******/ function __webpack_require__(moduleId) { 1725 | /******/ // Check if module is in cache 1726 | /******/ var cachedModule = __webpack_module_cache__[moduleId]; 1727 | /******/ if (cachedModule !== undefined) { 1728 | /******/ return cachedModule.exports; 1729 | /******/ } 1730 | /******/ // Create a new module (and put it into the cache) 1731 | /******/ var module = __webpack_module_cache__[moduleId] = { 1732 | /******/ // no module.id needed 1733 | /******/ // no module.loaded needed 1734 | /******/ exports: {} 1735 | /******/ }; 1736 | /******/ 1737 | /******/ // Execute the module function 1738 | /******/ __webpack_modules__[moduleId](module, module.exports, __webpack_require__); 1739 | /******/ 1740 | /******/ // Return the exports of the module 1741 | /******/ return module.exports; 1742 | /******/ } 1743 | /******/ 1744 | /************************************************************************/ 1745 | var __webpack_exports__ = {}; 1746 | // This entry need to be wrapped in an IIFE because it need to be isolated against other modules in the chunk. 1747 | (() => { 1748 | var exports = __webpack_exports__; 1749 | var __webpack_unused_export__; 1750 | 1751 | __webpack_unused_export__ = ({ value: true }); 1752 | __webpack_unused_export__ = void 0; 1753 | var proseqviewer_1 = __webpack_require__(505); 1754 | __webpack_unused_export__ = ({ enumerable: true, get: function () { return proseqviewer_1.ProSeqViewer; } }); 1755 | 1756 | })(); 1757 | 1758 | /******/ })() 1759 | ; -------------------------------------------------------------------------------- /figure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BioComputingUP/ProSeqViewer/3bcadd92532207260ff30bc992c4b36676d53910/figure.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "proseqviewer", 3 | "version": "1.1.9", 4 | "main": "index.js", 5 | "types": "index.d.ts", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/BioComputingUP/ProSeqViewer" 9 | }, 10 | "homepage": "https://biocomputingup.github.io/ProSeqViewer-documentation/", 11 | "scripts": { 12 | "test": "echo \"Error: no test specified\" && exit 1", 13 | "build": "rm -rfv dist && tsc", 14 | "postbuild": "cp -r src/assets dist/", 15 | "wp": "npx webpack", 16 | "postwp": "sed -i 's/^exports\\.\\(ProSeqViewer = ProSeqViewer;\\)$/window\\.\\1/' dist/sqv-bundle.js", 17 | "buildall": "rm -rfv dist && npm run build && npm run postbuild && npm run wp && npm run postwp", 18 | "start": "npm run build -- -w" 19 | }, 20 | "files": [ 21 | "dist/**" 22 | ], 23 | "author": "Martina Bevilacqua", 24 | "license": "ISC", 25 | "description": "TypeScript library to visualize annotation on single sequences and multiple sequence alignments", 26 | "devDependencies": { 27 | "@types/node": "14.14.13", 28 | "typescript": "4.2.4", 29 | "webpack": "5.32.0", 30 | "webpack-cli": "4.6.0" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BioComputingUP/ProSeqViewer/3bcadd92532207260ff30bc992c4b36676d53910/src/assets/.gitkeep -------------------------------------------------------------------------------- /src/assets/proseqviewer.css: -------------------------------------------------------------------------------- 1 | .root{ 2 | font-family: monospace, monospace; 3 | } 4 | 5 | .container { 6 | margin-left: 0; 7 | } 8 | 9 | .cnk { 10 | position: relative; 11 | white-space: nowrap; 12 | display: inline-block; 13 | vertical-align: top; 14 | } 15 | 16 | .idx { 17 | position: relative; 18 | display: inline-block; 19 | vertical-align: top; 20 | -moz-user-select: none; 21 | -ms-user-select: none; 22 | -webkit-user-select: none; 23 | -webkit-touch-callout: none; 24 | } 25 | 26 | .hidden { 27 | display: none; 28 | } 29 | 30 | .lblContainer { 31 | direction: rtl; 32 | } 33 | 34 | .lbl-hidden { 35 | padding-left: 2px; 36 | padding-right: 5px; 37 | -webkit-user-select: none; 38 | display:block; 39 | height:1em; 40 | line-height:1em; 41 | margin-bottom: 1.5em; 42 | font-style: italic; 43 | justify-content: flex-end; 44 | flex-direction: column; 45 | } 46 | 47 | .lbl { 48 | font-size: 1em; 49 | } 50 | 51 | .cell{ 52 | margin-bottom: 1.5em; 53 | height: 1em; 54 | display: inline-block; 55 | } 56 | 57 | .highlight { 58 | background-color: #EBD270!important;; 59 | } 60 | 61 | /* 'card' name is not available because used by bootstrap */ 62 | .crd { 63 | position: relative; 64 | display: inline-block; 65 | } 66 | 67 | .crds { 68 | display: inline-block; 69 | -webkit-user-select: none; 70 | } 71 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export {ProSeqViewer} from './lib/proseqviewer' 2 | -------------------------------------------------------------------------------- /src/lib/colors.model.ts: -------------------------------------------------------------------------------- 1 | interface InpColor { 2 | backgroundColor: string; 3 | backgroundImage?: string; 4 | sequenceId: string; 5 | color?: string; 6 | start: number; 7 | end: number; 8 | sequenceColor?: string; 9 | } 10 | 11 | /** Output Colors */ 12 | // E.g.: 'coloring': {rowNum: { positions:[{start, end, color, target}] , chars: [{entity, color, target}]}} 13 | interface OutColors { 14 | [coloring: string]: OutColor; 15 | } 16 | 17 | interface OutColor { 18 | [rowNum: string]: {positions: Array, chars: Array}; 19 | } 20 | 21 | interface PositionColor { 22 | start: number; 23 | end: number; 24 | backgroundColor?: string; 25 | target: string; 26 | } 27 | 28 | interface CharColor { 29 | entity: string; 30 | backgroundColor?: string; 31 | target: string; 32 | } 33 | 34 | export class ColorsModel { 35 | 36 | static palette: OutColors; 37 | 38 | 39 | static getRowsList(coloring: string) { 40 | const outCol = this.palette[coloring]; 41 | if (!outCol) { 42 | return []; 43 | } 44 | return Object.keys(outCol); 45 | } 46 | 47 | static getPositions(coloring: string, rowNum: number) { 48 | let outCol: any; 49 | outCol = this.palette[coloring]; 50 | if (!outCol) { 51 | return []; 52 | } 53 | outCol = outCol[rowNum]; 54 | if (!outCol) { 55 | return []; 56 | } 57 | outCol = outCol.positions; 58 | if (!outCol) { 59 | return []; 60 | } 61 | return outCol; 62 | } 63 | 64 | process(allInputs) { 65 | if (!allInputs.regions) { 66 | allInputs.regions = []; 67 | } 68 | 69 | if (allInputs.options && !allInputs.options.sequenceColor) { 70 | const sequenceColorRegions = []; 71 | for (const sequence of allInputs.sequences) { 72 | if (sequence.sequenceColor) { 73 | // @ts-ignore 74 | sequenceColorRegions.push({sequenceId: sequence.id, start: 1, end: sequence.sequence.length, sequenceColor: sequence.sequenceColor}); 75 | } 76 | } 77 | for (const reg of allInputs.regions) { 78 | if (!reg.backgroundColor && reg.sequenceId !== -99999999999998 ) { 79 | sequenceColorRegions.push(reg); 80 | } 81 | } 82 | 83 | if (sequenceColorRegions.length > 0) { 84 | allInputs.regions = sequenceColorRegions; 85 | } 86 | } 87 | 88 | const allRegions = Array.prototype.concat(allInputs.icons, allInputs.regions, allInputs.patterns); // ordering 89 | let newRegions = this.fixMissingIds(allRegions, allInputs.sequences); 90 | newRegions = this.transformInput(allRegions, newRegions, allInputs.sequences, allInputs.options); 91 | this.transformColors(allInputs.options); 92 | 93 | return newRegions; 94 | } 95 | 96 | // transform input structure 97 | private transformInput(regions, newRegions, sequences, globalColor) { 98 | 99 | // if don't receive new colors, keep old colors 100 | if (!regions) { return; } 101 | 102 | 103 | // if receive new colors, change them 104 | ColorsModel.palette = {}; 105 | let info; 106 | if (!globalColor) { 107 | for (const seq of sequences) { 108 | 109 | let reg = {sequenceId: seq.id, backgroundColor: '', start: 1, end: seq.sequence.length, sequenceColor: ''}; 110 | if (seq.sequenceColor) { 111 | 112 | reg.backgroundColor = seq.sequenceColor; 113 | reg.sequenceColor = seq.sequenceColor; 114 | info = this.setSequenceColor(reg, seq); 115 | } 116 | } 117 | } 118 | 119 | // overwrite region color if sequenceColor is set 120 | // @ts-ignore 121 | for (const reg of newRegions) { 122 | 123 | let sequenceColor; 124 | if (reg.icon) { continue; } 125 | if (sequences.find(x => x.id === reg.sequenceId)) { 126 | 127 | sequenceColor = sequences.find(x => x.id === reg.sequenceId).sequenceColor; 128 | if (sequenceColor && !globalColor) { 129 | // sequenceColor is set. Cannot set backgroundColor 130 | reg.sequenceColor = sequenceColor; } 131 | } 132 | 133 | 134 | 135 | info = this.processColor(reg); 136 | if (info === -1) { continue; } 137 | 138 | 139 | ColorsModel.palette[info.type][info.sequenceId].positions 140 | .push({start: reg.start, end: reg.end, target: info.letterStyle}); 141 | if (sequenceColor && sequenceColor.includes('binary')) { 142 | 143 | // @ts-ignore 144 | ColorsModel.palette[info.type].binaryColors = this.getBinaryColors(sequenceColor); 145 | } 146 | } 147 | return newRegions; 148 | } 149 | 150 | private setSequenceColor(reg, seq) { 151 | let info; 152 | 153 | info = this.processColor(reg); 154 | 155 | ColorsModel.palette[info.type][info.sequenceId].positions 156 | .push({start: reg.start, end: reg.end, target: info.letterStyle}); 157 | 158 | if (seq.sequenceColor.includes('binary')) { 159 | // @ts-ignore 160 | ColorsModel.palette[info.type].binaryColors = this.getBinaryColors(seq.sequenceColor); 161 | } 162 | return info; 163 | } 164 | 165 | private fixMissingIds(regions, sequences) { 166 | const newRegions = []; 167 | for (const reg of regions) { 168 | if (!reg) { continue; } 169 | if (sequences.find(x => x.id === reg.sequenceId)) { 170 | newRegions.push(reg); 171 | } else { 172 | for (const seq of sequences) { 173 | const newReg = {}; 174 | // tslint:disable-next-line:forin 175 | for (const key in reg) { 176 | if (reg[key] !== 'sequenceId') { 177 | newReg[key] = reg[key]; 178 | } 179 | newReg['sequenceId'] = seq.id; 180 | } 181 | newRegions.push(newReg); 182 | } 183 | } 184 | } 185 | return newRegions; 186 | } 187 | 188 | private transformColors(opt) { 189 | const sequenceColor = opt.sequenceColor 190 | let arrColors; 191 | let n; 192 | let c; 193 | 194 | 195 | for (const type in ColorsModel.palette) { 196 | switch (type) { 197 | case 'gradient': { 198 | // tslint:disable-next-line:forin 199 | for (const row in ColorsModel.palette[type]) { 200 | c = ColorsModel.palette[type][row]; 201 | n = c.positions.length + c.chars.length; 202 | arrColors = this.gradient(n); 203 | c.positions.sort((a, b) => (a.start > b.start) ? 1 : -1); 204 | for (const e of c.positions) { 205 | e.backgroundColor = arrColors.pop(); 206 | } 207 | } 208 | break; 209 | } 210 | case 'binary': { 211 | // tslint:disable-next-line:forin 212 | for (const row in ColorsModel.palette[type]) { 213 | if (row === 'binaryColors') { 214 | continue; 215 | } 216 | c = ColorsModel.palette[type][row]; 217 | n = c.positions.length + c.chars.length; 218 | arrColors = this.binary(n, ColorsModel.palette[type].binaryColors); 219 | c.positions.sort((a, b) => (a.start > b.start) ? 1 : -1); 220 | for (const e of c.positions) { 221 | e.backgroundColor = arrColors.pop(); 222 | } 223 | } 224 | break; 225 | } 226 | case sequenceColor: { 227 | // tslint:disable-next-line:forin 228 | // ColorsModel.palette[type]: an obj with regions and color associated es. positions: 1-200, zappo 229 | for (const row in ColorsModel.palette[type]) { 230 | 231 | c = ColorsModel.palette[type][row]; 232 | if (c.positions.length > 0) { 233 | 234 | for (const pos of c.positions) { 235 | pos.backgroundColor = sequenceColor; 236 | } 237 | } 238 | } 239 | break; 240 | } 241 | } 242 | } 243 | } 244 | 245 | private processColor(e: InpColor) { 246 | 247 | 248 | const result = {type: 'custom', sequenceId: -1, letterStyle: ''}; 249 | 250 | // check if row key is a number 251 | if (e.sequenceId === undefined || isNaN(+e.sequenceId)) { 252 | // wrong entity row key 253 | return -1; 254 | } 255 | result.sequenceId = +e.sequenceId; 256 | 257 | // transform target in CSS property 258 | if (e.color) {result.letterStyle = `color:${e.color};`; 259 | } 260 | if (e.backgroundColor) { 261 | result.letterStyle += `background-color:${e.backgroundColor};`; } 262 | if (e.backgroundImage) { 263 | result.letterStyle += `background-image: ${e.backgroundImage};`; 264 | } 265 | 266 | // define color or palette 267 | if (e.sequenceColor) { result.type = e.sequenceColor; } 268 | 269 | 270 | if (result.type.includes('binary')) { result.type = 'binary'; } 271 | 272 | // reserving space for the transformed object (this.palette) 273 | // if color type not inserted yet 274 | if (!(result.type in ColorsModel.palette)) { 275 | ColorsModel.palette[result.type] = {}; 276 | } 277 | // if row not inserted yet 278 | if (!(result.sequenceId in ColorsModel.palette[result.type])) { 279 | ColorsModel.palette[result.type][result.sequenceId] = {positions: [], chars: []}; 280 | } 281 | return result; 282 | } 283 | 284 | 285 | private gradient(n: number) { 286 | return this.evenlySpacedColors(n); 287 | } 288 | 289 | private getBinaryColors() { 290 | const color1 = '#93E1D8'; 291 | const color2 = '#FFA69E'; 292 | return [color1, color2]; 293 | } 294 | 295 | private binary(n: number, binaryColors) { 296 | let reg = 0; 297 | let flag; 298 | 299 | const arrColors = []; 300 | while (reg < n) { 301 | if (flag) { 302 | arrColors.push(binaryColors[0]); 303 | flag = !flag; 304 | } else { 305 | arrColors.push(binaryColors[1]); 306 | flag = !flag; 307 | } 308 | reg += 1; 309 | } 310 | return arrColors; 311 | } 312 | 313 | private evenlySpacedColors( n: number ) { 314 | /** how to go around the rgb wheel */ 315 | /** add to next rgb component, subtract to previous */ 316 | /** ex.: 255,0,0 -(add)-> 255,255,0 -(subtract)-> 0,255,0 */ 317 | 318 | // starting color: red 319 | const rgb = [255, 0, 0]; 320 | // 1536 colors in the rgb wheel 321 | const delta = Math.floor(1536 / n); 322 | 323 | let remainder; 324 | let add = true; 325 | let value = 0; 326 | let tmp; 327 | 328 | const colors = []; 329 | for (let i = 0; i < n; i++) { 330 | remainder = delta; 331 | while (remainder > 0) { 332 | if (add) { 333 | tmp = (((value + 1) % 3) + 3) % 3; 334 | if (rgb[tmp] + remainder > 255) { 335 | remainder -= (255 - rgb[tmp]); 336 | rgb[tmp] = 255; 337 | add = false; 338 | value = tmp; 339 | } else { 340 | rgb[tmp] += remainder; 341 | remainder = 0; 342 | } 343 | } else { 344 | tmp = (((value - 1) % 3) + 3) % 3; 345 | if (rgb[tmp] - remainder < 0) { 346 | remainder -= rgb[tmp]; 347 | rgb[tmp] = 0; 348 | add = true; 349 | } else { 350 | rgb[tmp] -= remainder; 351 | remainder = 0; 352 | } 353 | } 354 | } 355 | colors.push('rgba(' + rgb[0] + ',' + rgb[1] + ',' + rgb[2] + ', 0.4)'); 356 | } 357 | return colors; 358 | } 359 | } 360 | -------------------------------------------------------------------------------- /src/lib/consensus.model.ts: -------------------------------------------------------------------------------- 1 | import {Palettes} from './palettes'; 2 | 3 | export class ConsensusModel { 4 | 5 | 6 | 7 | static setConsensusInfo(type, sequences) { 8 | const idIdentity = -99999999999999; 9 | const idPhysical = -99999999999998; 10 | const consensusInfo = []; 11 | 12 | for (let i = 0; i < sequences[0].sequence.length; i++) { 13 | const consensusColumn = {}; 14 | for (const sequence of sequences) { 15 | let letter = sequence.sequence[i]; 16 | if (type === 'physical') { 17 | if (sequence.id === idIdentity) { continue; } 18 | if (letter in Palettes.consensusAaLesk) { 19 | letter = Palettes.consensusAaLesk[letter][0]; 20 | } 21 | } else { 22 | if (sequence.id === idPhysical) { continue; } 23 | } 24 | if (letter === '-' || !letter) { continue; } 25 | if (consensusColumn[letter]) { 26 | consensusColumn[letter] += 1; 27 | } else { 28 | consensusColumn[letter] = 1; 29 | } 30 | } 31 | consensusInfo.push(consensusColumn); 32 | } 33 | return consensusInfo; 34 | } 35 | 36 | static createConsensus(type, consensus, consensus2, sequences, regions, threshold, palette) { 37 | 38 | 39 | if (threshold < 50) { 40 | threshold = 100 - threshold; 41 | } 42 | 43 | let id = -99999999999999; 44 | let label; 45 | if (type === 'physical') { 46 | label = 'Consensus physical ' + threshold + '%'; 47 | id = -99999999999998; 48 | } else { 49 | label = 'Consensus identity ' + threshold + '%'; 50 | } 51 | 52 | let consensusSequence = ''; 53 | 54 | // tslint:disable-next-line:forin 55 | for (const column in consensus) { 56 | 57 | let maxLetter; 58 | let maxIndex; 59 | if (Object.keys(consensus[column]).length === 0) { 60 | maxLetter = '.'; 61 | } else { 62 | maxLetter = Object.keys(consensus[column]).reduce((a, b) => 63 | consensus[column][a] > consensus[column][b] ? a : b); 64 | maxIndex = consensus[column][maxLetter]; 65 | } 66 | 67 | let backgroundColor; 68 | let color; 69 | const frequency = (maxIndex / sequences.length) * 100; 70 | if (type === 'physical') { 71 | // consensus id to see if I have all letters equals 72 | // equals letters have precedence over properties 73 | let maxLetterId; 74 | let maxIndexId; 75 | if (Object.keys(consensus[column]).length === 0) { 76 | maxLetterId = '.'; 77 | } else { 78 | maxLetterId = Object.keys(consensus2[column]).reduce((a, b) => 79 | consensus2[column][a] > consensus2[column][b] ? a : b); 80 | maxIndexId = consensus2[column][maxLetterId]; 81 | } 82 | const frequencyId = (maxIndexId / sequences.length) * 100; 83 | if (frequencyId >= threshold) { 84 | maxLetter = maxLetterId; 85 | [backgroundColor, color] = ConsensusModel.setColorsIdentity(frequencyId, palette, 'physical'); 86 | } else { 87 | 88 | if (frequency >= threshold) { 89 | [backgroundColor, color] = ConsensusModel.setColorsPhysical(maxLetter, palette); 90 | } 91 | } 92 | 93 | } else { 94 | [backgroundColor, color] = ConsensusModel.setColorsIdentity(frequency, palette, 'identity'); 95 | } 96 | if (frequency < threshold) { 97 | maxLetter = '.'; 98 | } 99 | // + 1 because residues start from 1 and not 0 100 | regions.push({start: +column + 1, end: +column + 1, sequenceId: id, backgroundColor, color }); 101 | consensusSequence += maxLetter; 102 | } 103 | 104 | sequences.push({id, sequence: consensusSequence, label}); 105 | 106 | return [sequences, regions]; 107 | } 108 | 109 | static setColorsIdentity(frequency, palette, flag) { 110 | let backgroundColor; 111 | let color; 112 | let finalPalette; 113 | 114 | if (palette && typeof palette !== 'string' && flag == 'identity') { 115 | finalPalette = palette; 116 | } else { 117 | finalPalette = Palettes.consensusLevelsIdentity; 118 | } 119 | let steps = []; 120 | for (let key in finalPalette) { 121 | steps.push(+key); // 42 122 | } 123 | steps = steps.sort((a,b) => a < b ? 1 : a > b ? -1 : 0) 124 | 125 | for (const step of steps) { 126 | 127 | if (frequency >= step) { 128 | backgroundColor = finalPalette[step][0]; 129 | color = finalPalette[step][1]; 130 | break; 131 | } 132 | } 133 | return [backgroundColor, color]; 134 | } 135 | 136 | static setColorsPhysical(letter, palette) { 137 | let finalPalette; 138 | let backgroundColor; 139 | let color; 140 | 141 | if (palette && typeof palette !== 'string') { 142 | finalPalette = palette; 143 | } else { 144 | finalPalette = Palettes.consensusAaLesk; 145 | } 146 | 147 | for (const el in finalPalette) { 148 | 149 | if (finalPalette[el][0] == letter) { 150 | backgroundColor = finalPalette[el][1]; 151 | color = finalPalette[el][2]; 152 | break; 153 | } 154 | } 155 | return [backgroundColor, color]; 156 | } 157 | 158 | process(sequences, regions, options) { 159 | if (!regions) { regions = []} 160 | let maxIdx = 0; 161 | 162 | for (const row of sequences) { 163 | if (maxIdx < row.sequence.length) { maxIdx = row.sequence.length; } 164 | } 165 | 166 | for (const row of sequences) { 167 | const diff = maxIdx - row.sequence.length; 168 | if (diff > 0 && row.id !== -99999999999999 && row.id !== -99999999999998) { 169 | for (let i = 0; i < diff; i++) { 170 | row.sequence += '-'; 171 | } 172 | } 173 | } 174 | 175 | if (options.sequenceColorMatrix) { 176 | regions = []; 177 | sequences.sort( (a, b) => a.id - b.id); 178 | const min = sequences[0]; 179 | 180 | let palette = Palettes.substitutionMatrixBlosum; 181 | // console.log(palette) 182 | if (options.sequenceColorMatrixPalette) { 183 | palette = options.sequenceColorMatrixPalette 184 | } 185 | 186 | let key; 187 | // tslint:disable-next-line:prefer-for-of 188 | for (let i = 0; i < min.sequence.length; i++) { 189 | for (const sequence of sequences) { 190 | 191 | if (sequence.id === min.id) { 192 | key = sequence.sequence[i] + sequence.sequence[i] 193 | if (key in palette) { 194 | 195 | regions.push({sequenceId: sequence.id, start: i + 1, end: i + 1, 196 | backgroundColor: palette[key][0], color: palette[key][1]}); 197 | } 198 | 199 | } else { 200 | 201 | // score with first sequence 202 | key = sequence.sequence[i] + min.sequence[i] 203 | if (key in palette) { 204 | regions.push({sequenceId: sequence.id, start: i + 1, end: i + 1, 205 | backgroundColor: palette[key][0]}); 206 | } else if (palette[min.sequence[i] + sequence.sequence[i]]) { 207 | key = min.sequence[i] + sequence.sequence[i] 208 | regions.push({sequenceId: sequence.id, start: i + 1, end: i + 1, 209 | backgroundColor: palette[key][0], color: palette[key][1]}); 210 | } 211 | 212 | } 213 | } 214 | 215 | } 216 | 217 | } else if (options.sequenceColor) { 218 | regions = []; 219 | for (const sequence of sequences) { 220 | sequence.sequenceColor = options.sequenceColor; 221 | regions.push({sequenceId: sequence.id, start: 1, end: sequence.sequence.length, sequenceColor: options.sequenceColor}); 222 | } 223 | } 224 | 225 | let consensusInfoIdentity; 226 | let consensusInfoPhysical; 227 | 228 | if (options.consensusColorIdentity) { 229 | consensusInfoIdentity = ConsensusModel.setConsensusInfo('identity', sequences); 230 | [sequences, regions] = ConsensusModel.createConsensus('identity', consensusInfoIdentity, false, sequences, regions, options.dotThreshold, options.consensusColorIdentity); 231 | } else if (options.consensusColorMapping) { 232 | consensusInfoPhysical = ConsensusModel.setConsensusInfo('physical', sequences); 233 | if (!consensusInfoIdentity) { consensusInfoIdentity = ConsensusModel.setConsensusInfo('identity', sequences); } 234 | [sequences, regions] = ConsensusModel.createConsensus('physical', consensusInfoPhysical, consensusInfoIdentity, sequences, regions, options.dotThreshold, options.consensusColorMapping); 235 | } 236 | 237 | return [sequences, regions]; 238 | } 239 | 240 | } 241 | -------------------------------------------------------------------------------- /src/lib/events.model.ts: -------------------------------------------------------------------------------- 1 | export class EventsModel { 2 | 3 | public onRegionSelected() { 4 | 5 | const sequenceViewers = document.getElementsByClassName('cell'); 6 | 7 | // @ts-ignore 8 | for (const sqv of sequenceViewers) { 9 | 10 | sqv.addEventListener('dblclick', r => { 11 | 12 | const evt = new CustomEvent('onRegionSelected', {detail: {char: r.srcElement.innerHTML, x: r.srcElement.dataset.resX, y: r.srcElement.dataset.resY}} ); 13 | window.dispatchEvent(evt); 14 | 15 | }); 16 | } 17 | 18 | } 19 | 20 | } 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/lib/icons.model.ts: -------------------------------------------------------------------------------- 1 | import {Icons} from './icons'; 2 | 3 | export class IconsModel { 4 | 5 | process(regions, sequences, iconsPaths) { 6 | const rows = {}; 7 | if (regions && sequences) { 8 | for (const seq of sequences) { 9 | for (const reg of regions) { 10 | if (+seq.id === reg.sequenceId) { 11 | if (!rows[seq.id]) { 12 | rows[seq.id] = {}; 13 | } 14 | // tslint:disable-next-line:forin 15 | for (let key in sequences.find(x => x.id === seq.id).sequence) { 16 | key = (+key + 1).toString(); 17 | // chars with icon 18 | if (+key >= reg.start && +key <= reg.end && reg.icon) { 19 | if (reg.icon) { 20 | const region = reg.end - (reg.start - 1); 21 | const center = Math.floor(region / 2); 22 | let icon; 23 | if (reg.color && reg.color[0] === '(') { 24 | reg.color = 'rgb' + reg.color; 25 | } 26 | 27 | // default icons 28 | 29 | switch (reg.icon) { 30 | case 'lollipop': { 31 | icon = Icons.lollipop; 32 | break; 33 | } 34 | case 'arrowRight': { 35 | icon = Icons.arrowRight; 36 | break; 37 | } 38 | case 'arrowLeft': { 39 | icon = Icons.arrowLeft; 40 | break; 41 | } 42 | case 'strand': { 43 | icon = Icons.strand; 44 | break; 45 | } 46 | case 'noSecondary': { 47 | icon = Icons.noSecondary; 48 | break; 49 | } 50 | case 'helix': { 51 | icon = Icons.helix; 52 | break; 53 | } 54 | case 'turn': { 55 | icon = Icons.turn; 56 | break; 57 | } 58 | default: { 59 | // customizable icons (svg) 60 | icon = reg.icon; 61 | break; 62 | } 63 | } 64 | 65 | if (reg.display === 'center' && +key === reg.start + center) { 66 | rows[seq.id][key] = {char: icon}; 67 | } else if (!reg.display) { 68 | rows[seq.id][key] = {char: icon}; 69 | } 70 | 71 | } 72 | } 73 | // chars without icon 74 | if (!rows[seq.id][key]) { 75 | rows[seq.id][key] = {char: ''}; 76 | } 77 | } 78 | } 79 | } 80 | } 81 | 82 | } 83 | const filteredRows = {}; 84 | // tslint:disable-next-line:forin 85 | for (const row in rows) { 86 | let flag; 87 | const chars = rows[row]; 88 | for (const char in rows[row]) { 89 | if (rows[row][char].char !== '') { 90 | flag = true; 91 | } 92 | } 93 | 94 | if (flag) { 95 | filteredRows[row] = chars; 96 | } 97 | } 98 | return filteredRows; 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/lib/icons.ts: -------------------------------------------------------------------------------- 1 | export class Icons { 2 | static lollipop = ''; 3 | static arrowLeft = ' '; 4 | static arrowRight = ' '; 5 | static strand = ''; 6 | static noSecondary = ''; 7 | static helix = ''; 8 | static turn = ''; 9 | } 10 | -------------------------------------------------------------------------------- /src/lib/interface.ts: -------------------------------------------------------------------------------- 1 | export interface Sequences { 2 | sequence: string, 3 | id?: number, 4 | label?: string, 5 | sequenceColor?: string, 6 | labelTooltip?: string, 7 | startIndex?: number 8 | } 9 | 10 | export interface Regions { 11 | sequenceId: number, 12 | start: number, 13 | end: number, 14 | backgroundColor?: string, 15 | color?: string, 16 | backgroundImage?: string 17 | } 18 | 19 | 20 | export interface Patterns { 21 | sequenceId: number, 22 | pattern: string, 23 | start?: number, 24 | end?: number, 25 | backgroundColor?: string, 26 | color?: string, 27 | backgroundImage?: string 28 | } 29 | 30 | export interface Icons { 31 | sequenceId: number, 32 | start: number, 33 | end: number, 34 | icon: string, // TODO check if placeholder or block (maybe to be implemented) 35 | display?: string, // center 36 | } 37 | 38 | export interface Options { 39 | fontSize?: string, 40 | chunkSize?: number, // number of chunk letters 41 | chunkSeparation?: number, // space between chunks 42 | viewerWidth?: string, 43 | indexesLocation?: string, // "top" / "lateral" 44 | wrapLine?: boolean, 45 | lineSeparation?: string, // margin bottom of horizontal rows 46 | 47 | sequenceColor?: string | {}, // colorscheme, introduce option for custom input 48 | selection?: string 49 | } 50 | 51 | export interface Consensus { 52 | color: string | {}, 53 | dotThreshold?: number 54 | } 55 | 56 | 57 | export interface Input { 58 | consensus?: Consensus; 59 | sequences: Array, 60 | regions?: Array, 61 | patterns?: Array, 62 | icons?: Array, 63 | options?: Options, 64 | } 65 | -------------------------------------------------------------------------------- /src/lib/options.model.ts: -------------------------------------------------------------------------------- 1 | export class OptionsModel { 2 | 3 | options = { 4 | fontSize: '14px', 5 | chunkSize: 10, 6 | chunkSeparation: 1, // relative to fontSize 7 | emptyFiller: ' ', // fills gap at the end of the MSA sequences 8 | indexesLocation: null, 9 | wrapLine: true, 10 | viewerWidth: '', 11 | dotThreshold: 90, 12 | lineSeparation: '5px', 13 | sequenceColor: undefined, 14 | customPalette: undefined, 15 | sequenceColorMatrix: undefined, // blosum 16 | sequenceColorMatrixPalette: undefined, 17 | consensusColorIdentity: undefined, 18 | consensusColorMapping: undefined, 19 | selection: undefined 20 | }; 21 | 22 | process(opt, consensus) { 23 | 24 | /** check input fontSize */ 25 | if (opt && opt.fontSize) { 26 | const fSize = opt.fontSize; 27 | const fNum = +fSize.substr(0, fSize.length - 2); 28 | const fUnit = fSize.substr(fSize.length - 2, 2); 29 | 30 | if (isNaN(fNum) || (fUnit !== 'px' && fUnit !== 'vw' && fUnit !== 'em')) { 31 | // wrong fontSize format 32 | } else { 33 | this.options.fontSize = fSize; 34 | } 35 | } else { 36 | // fontSize not set 37 | this.options.fontSize = '14px'; // default reset 38 | } 39 | 40 | /** check input chunkSize */ 41 | if (opt && opt.chunkSize) { 42 | 43 | const cSize = +opt.chunkSize; 44 | if (isNaN(cSize) || cSize < 0) { 45 | // wrong chunkSize format 46 | } else { 47 | this.options.chunkSize = cSize; 48 | } 49 | } 50 | 51 | /** check input spaceSize */ 52 | if (opt && opt.chunkSeparation) { 53 | 54 | const chunkSeparation = +opt.chunkSeparation; 55 | if (chunkSeparation >= 0) { 56 | this.options.chunkSeparation = chunkSeparation; 57 | } 58 | } 59 | 60 | if (opt && opt.chunkSize == 0) { 61 | this.options.chunkSize = 1; 62 | this.options.chunkSeparation = 0; 63 | } 64 | 65 | /** check indexesLocation value */ 66 | if (opt && opt.indexesLocation) { 67 | if (opt.indexesLocation == "top" || opt.indexesLocation == "lateral") { 68 | this.options.indexesLocation = opt.indexesLocation; 69 | } 70 | } 71 | 72 | /** check selection value */ 73 | 74 | if (opt && opt.selection) { 75 | if (opt.selection == "columnselection" || opt.selection == "areaselection") { 76 | this.options.selection = opt.selection; 77 | } 78 | } 79 | 80 | /** check sequenceColor value */ 81 | if (opt && opt.sequenceColor) { 82 | if (typeof opt.sequenceColor !== 'string' ) { 83 | const keys = Object.keys(opt.sequenceColor); 84 | 85 | if(keys[0].length === 1) { 86 | this.options.sequenceColor = 'custom'; 87 | this.options.customPalette = opt.sequenceColor; 88 | } else { 89 | this.options.sequenceColorMatrix = 'custom'; 90 | this.options.sequenceColorMatrixPalette = opt.sequenceColor; 91 | } 92 | } else { 93 | if(opt.sequenceColor === "blosum62") { 94 | this.options.sequenceColorMatrix = opt.sequenceColor; 95 | } else if (opt.sequenceColor === "clustal") { 96 | this.options.sequenceColor = opt.sequenceColor; 97 | } 98 | } 99 | } 100 | 101 | 102 | /** check consensusType value */ 103 | if (consensus && consensus.color) { 104 | 105 | if (typeof consensus.color !== 'string' ) { 106 | const keys = Object.keys(consensus.color); 107 | if(typeof (keys[0]) === 'string') { 108 | this.options.consensusColorIdentity = consensus.color 109 | } else { 110 | this.options.consensusColorMapping = consensus.color 111 | } 112 | } else { 113 | if(consensus.color === "identity") { 114 | this.options.consensusColorIdentity = consensus.color 115 | } else if (consensus.color === "physical") { 116 | this.options.consensusColorMapping = consensus.color 117 | } 118 | } 119 | } 120 | 121 | 122 | /** check consensusThreshold value */ 123 | if (consensus && consensus.dotThreshold) { 124 | if (typeof consensus.dotThreshold == 'number') { 125 | this.options.dotThreshold = consensus.dotThreshold; 126 | } 127 | } 128 | 129 | /** check rowMarginBottom value */ 130 | if (opt && opt.lineSeparation !== undefined) { 131 | const rSize = opt.lineSeparation; 132 | const rNum = +rSize.substr(0, rSize.length - 2); 133 | const rUnit = rSize.substr(rSize.length - 2, 2); 134 | 135 | if (isNaN(rNum) || (rUnit !== 'px' && rUnit !== 'vw' && rUnit !== 'em')) { 136 | // wrong lineSeparation format 137 | } else { 138 | this.options.lineSeparation = rSize; 139 | } 140 | } else { 141 | // lineSeparation not set 142 | this.options.lineSeparation = '5px'; // default reset 143 | } 144 | 145 | /** check wrapline value */ 146 | 147 | if (opt && typeof opt.wrapLine == 'boolean') { 148 | this.options.wrapLine = opt.wrapLine; 149 | } 150 | 151 | /** check oneLineWidth */ 152 | if (opt && opt.viewerWidth) { 153 | const viewerWidth = opt.viewerWidth; 154 | const olNum = +viewerWidth.substr(0, viewerWidth.length - 2); 155 | const olUnit = viewerWidth.substr(viewerWidth.length - 2, 2); 156 | if (isNaN(olNum) || (olUnit !== 'px' && olUnit !== 'vw' && olUnit !== 'em')) { 157 | // wrong oneLineWidth format 158 | } else { 159 | this.options.viewerWidth = viewerWidth; 160 | } 161 | } 162 | 163 | return this.options; 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /src/lib/palettes.ts: -------------------------------------------------------------------------------- 1 | export class Palettes { 2 | 3 | 4 | // AA propensities 5 | static clustal = { 6 | A: '#80a0f0', I: '#80a0f0', L: '#80a0f0', M: '#80a0f0', F: '#80a0f0', W: '#80a0f0', V: '#80a0f0', 7 | K: '#f01505', R: '#f01505', E: '#c048c0', D: '#c048c0', C: '#f08080', G: '#f09048', 8 | N: '#15c015', Q: '#15c015', S: '#15c015', T: '#15c015', P: '#c0c000', H: '#15a4a4', Y: '#15a4a4' 9 | }; 10 | 11 | static zappo = { 12 | A: '#ffafaf', R: '#6464ff', N: '#00ff00', D: '#ff0000', C: '#ffff00', Q: '#00ff00',E: '#ff0000', 13 | G: '#ff00ff', H: '#6464ff', I: '#ffafaf', L: '#ffafaf',K: '#ffafaf', M: '#ffc800', F: '#ff00ff', 14 | P: '#00ff00', S: '#00ff00', T: '#15c015', W: '#ffc800', V: '#ffc800', Y: '#ffafaf' 15 | }; 16 | 17 | static taylor = { 18 | A: '#ccff00', R: '#0000ff', N: '#cc00ff', D: '#ff0000', C: '#ffff00', Q: '#ff00cc',E: '#ff0066', 19 | G: '#ff9900', H: '#0066ff', I: '#66ff00', L: '#33ff00',K: '#6600ff', M: '#00ff00', F: '#00ff66', 20 | P: '#ffcc00', S: '#ff3300', T: '#ff6600', W: '#00ccff', V: '#00ffcc', Y: '#99ff00' 21 | }; 22 | 23 | static hydrophobicity = { 24 | A: '#ad0052', R: '#0000ff', N: '#0c00f3', D: '#0c00f3', C: '#c2003d', Q: '#0c00f3',E: '#0c00f3', 25 | G: '#6a0095', H: '#1500ea', I: '#ff0000', L: '#ea0015',K: '#0000ff', M: '#b0004f', F: '#cb0034', 26 | P: '#4600b9', S: '#5e00a1', T: '#61009e', W: '#5b00a4', V: '#4f00b0', Y: '#f60009', 27 | B: '#0c00f3', X: '#680097', Z: '#0c00f3' 28 | }; 29 | static helixpropensity = { 30 | A: '#e718e7', R: '#6f906f', N: '#1be41b', D: '#778877', C: '#23dc23', Q: '#926d92',E: '#ff00ff', 31 | G: '#00ff00', H: '#758a75', I: '#8a758a', L: '#ae51ae',K: '#a05fa0', M: '#ef10ef', F: '#986798', 32 | P: '#00ff00', S: '#36c936', T: '#47b847', W: '#8a758a', V: '#21de21', Y: '#857a85', 33 | B: '#49b649', X: '#758a75', Z: '#c936c9' 34 | }; 35 | static strandpropensity = { 36 | A: '#5858a7', R: '#6b6b94', N: '#64649b', D: '#2121de', C: '#9d9d62', Q: '#8c8c73',E: '#0000ff', 37 | G: '#4949b6', H: '#60609f', I: '#ecec13', L: '#b2b24d',K: '#4747b8', M: '#82827d', F: '#c2c23d', 38 | P: '#2323dc', S: '#4949b6', T: '#9d9d62', W: '#c0c03f', V: '#d3d32c', Y: '#ffff00', 39 | B: '#4343bc', X: '#797986', Z: '#4747b8' 40 | }; 41 | static turnpropensity = { 42 | A: '#2cd3d3', R: '#708f8f', N: '#ff0000', D: '#e81717', C: '#a85757', Q: '#3fc0c0',E: '#778888', 43 | G: '#ff0000', H: '#708f8f', I: '#00ffff', L: '#1ce3e3', K: '#7e8181', M: '#1ee1e1', F: '#1ee1e1', 44 | P: '#f60909', S: '#e11e1e', T: '#738c8c', W: '#738c8c', V: '#9d6262', Y: '#07f8f8', 45 | B: '#f30c0c', X: '#7c8383', Z: '#5ba4a4' 46 | }; 47 | 48 | static buriedindex = { 49 | A: '#00a35c', R: '#00fc03', N: '#00eb14', D: '#00eb14', C: '#0000ff', Q: '#00f10e',E: '#00f10e', 50 | G: '#009d62', H: '#00d52a', I: '#0054ab', L: '#007b84', K: '#00ff00', M: '#009768', F: '#008778', 51 | P: '#00e01f', S: '#00d52a', T: '#00db24', W: '#00a857', V: '#00e619', Y: '#005fa0', 52 | B: '#00eb14', X: '#00b649', Z: '#00f10e' 53 | }; 54 | 55 | static nucleotide = { 56 | A: '#64F73F', C: '#FFB340', G: '#EB413C', T: '#3C88EE', U: '#3C88EE' 57 | }; 58 | static purinepyrimidine = { 59 | A: '#FF83FA', C: '#40E0D0', G: '#FF83FA', T: '#40E0D0', U: '#40E0D0', R: '#FF83FA', Y: '#40E0D0' 60 | }; 61 | 62 | 63 | static consensusLevelsIdentity = { 64 | 100: ['#0A0A0A', '#FFFFFF'], 65 | 70: ['#333333', '#FFFFFF'], 66 | 40: ['#707070', '#FFFFFF'], 67 | 10: ['#A3A3A3', '#FFFFFF'], 68 | 0: ['#FFFFFF', '#FFFFFF'] 69 | }; 70 | 71 | // colour scheme in Lesk, Introduction to Bioinformatics 72 | static consensusAaLesk = { 73 | A: ['n', '#f09048', '#FFFFFF'], // n: small nonpolar 74 | G: ['n', '#f09048', '#FFFFFF'], 75 | S: ['n', '#f09048', '#FFFFFF'], 76 | T: ['n', '#f09048', '#FFFFFF'], 77 | C: ['h', '#558B6E', '#FFFFFF'], // h, hydrophobic 78 | V: ['h', '#558B6E', '#FFFFFF'], 79 | I: ['h', '#558B6E', '#FFFFFF'], 80 | L: ['h', '#558B6E', '#FFFFFF'], 81 | P: ['h', '#558B6E', '#FFFFFF'], 82 | F: ['h', '#558B6E', '#FFFFFF'], 83 | Y: ['h', '#558B6E', '#FFFFFF'], 84 | M: ['h', '#558B6E', '#FFFFFF'], 85 | W: ['h', '#558B6E', '#FFFFFF'], 86 | N: ['p', '#D4358D', '#FFFFFF'], // p: polar 87 | Q: ['p', '#D4358D', '#FFFFFF'], 88 | H: ['p', '#D4358D', '#FFFFFF'], 89 | D: ['~', '#A10702', '#FFFFFF'], // ~: negatively charged 90 | E: ['~', '#A10702', '#FFFFFF'], 91 | K: ['+', '#3626A7', '#FFFFFF'], 92 | R: ['+', '#3626A7', '#FFFFFF'] // +: positively charged 93 | } 94 | 95 | static substitutionMatrixBlosum = { WF: [ '#CFDBF2', '#000000'], QQ: [ '#A1B8E3', '#000000'], 96 | HH: [ '#7294D5', '#000000'], YY: [ '#81A0D9', '#000000'], ZZ: [ '#A1B8E3', '#000000'], 97 | CC: [ '#6288D0', '#000000'], PP: [ '#81A0D9', '#000000'], VI: [ '#B0C4E8', '#000000'], 98 | VM: [ '#CFDBF2', '#000000'], KK: [ '#A1B8E3', '#000000'], ZK: [ '#CFDBF2', '#000000'], 99 | DN: [ '#CFDBF2', '#000000'], SS: [ '#A1B8E3', '#000000'], QR: [ '#CFDBF2', '#000000'], 100 | NN: [ '#91ACDE', '#000000'], YF: [ '#B0C4E8', '#000000'], VL: [ '#CFDBF2', '#000000'], 101 | KR: [ '#C0CFED', '#000000'], ED: [ '#C0CFED', '#000000'], VV: [ '#A1B8E3', '#000000'], 102 | MI: [ '#CFDBF2', '#000000'], MM: [ '#A1B8E3', '#000000'], ZD: [ '#CFDBF2', '#000000'], 103 | FF: [ '#91ACDE', '#000000'], BD: [ '#A1B8E3', '#000000'], HN: [ '#CFDBF2', '#000000'], 104 | TT: [ '#A1B8E3', '#000000'], SN: [ '#CFDBF2', '#000000'], LL: [ '#A1B8E3', '#000000'], 105 | EQ: [ '#C0CFED', '#000000'], YW: [ '#C0CFED', '#000000'], EE: [ '#A1B8E3', '#000000'], 106 | KQ: [ '#CFDBF2', '#000000'], WW: [ '#3867BC', '#000000'], ML: [ '#C0CFED', '#000000'], 107 | KE: [ '#CFDBF2', '#000000'], ZE: [ '#A1B8E3', '#000000'], ZQ: [ '#B0C4E8', '#000000'], 108 | BE: [ '#CFDBF2', '#000000'], DD: [ '#91ACDE', '#000000'], SA: [ '#CFDBF2', '#000000'], 109 | YH: [ '#C0CFED', '#000000'], GG: [ '#91ACDE', '#000000'], AA: [ '#A1B8E3', '#000000'], 110 | II: [ '#A1B8E3', '#000000'], TS: [ '#CFDBF2', '#000000'], RR: [ '#A1B8E3', '#000000'], 111 | LI: [ '#C0CFED', '#000000'], ZB: [ '#CFDBF2', '#000000'], BN: [ '#B0C4E8', '#000000'], 112 | BB: [ '#A1B8E3', '#000000'] 113 | }; 114 | } 115 | -------------------------------------------------------------------------------- /src/lib/patterns.model.ts: -------------------------------------------------------------------------------- 1 | 2 | export class PatternsModel { 3 | 4 | // find index of matched regex positions and create array of regions with color 5 | process(patterns, sequences) { 6 | if (!patterns) { return } 7 | const regions = []; // OutPatterns 8 | // @ts-ignore 9 | for (const element of patterns) { 10 | // tslint:disable-next-line:no-conditional-assignment 11 | const pattern = element.pattern; 12 | let str; 13 | if (sequences.find(x => x.id === element.sequenceId)) { 14 | str = sequences.find(x => x.id === element.sequenceId).sequence; 15 | if (element.start && element.end) { 16 | str = str.substr(element.start - 1, element.end - (element.start - 1)); 17 | } 18 | this.regexMatch(str, pattern, regions, element); 19 | } else { 20 | for (const seq of sequences) { 21 | // regex 22 | if (element.start && element.end) { 23 | str = seq.sequence.substr(element.start - 1, element.end - (element.start - 1)); 24 | } 25 | this.regexMatch(str, pattern, regions, element); 26 | } 27 | } 28 | } 29 | return regions; 30 | 31 | } 32 | 33 | regexMatch(str, pattern, regions, element) { 34 | const re = new RegExp(pattern, "g"); 35 | let match; 36 | // tslint:disable-next-line:no-conditional-assignment 37 | while ((match = re.exec(str)) != null) { 38 | regions.push({start: +match.index + 1, end: +match.index + +match[0].length, 39 | backgroundColor: element.backgroundColor, color: element.color, backgroundImage: element.backgroundImage, 40 | borderColor: element.borderColor, borderStyle: element.borderStyle, sequenceId: element.sequenceId}); 41 | } 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /src/lib/proseqviewer.ts: -------------------------------------------------------------------------------- 1 | import {OptionsModel} from './options.model'; 2 | import {RowsModel} from './rows.model'; 3 | import {ColorsModel} from './colors.model'; 4 | import {SelectionModel} from './selection.model'; 5 | import {IconsModel} from './icons.model'; 6 | import {SequenceInfoModel} from './sequenceInfoModel'; 7 | import {EventsModel} from './events.model'; 8 | import {PatternsModel} from './patterns.model'; 9 | import {ConsensusModel} from './consensus.model'; 10 | import {Input} from './interface'; 11 | 12 | export class ProSeqViewer { 13 | static sqvList = []; 14 | divId: string; 15 | init: boolean; 16 | params: OptionsModel; 17 | rows: RowsModel; 18 | consensus: ConsensusModel; 19 | regions: ColorsModel; 20 | patterns: PatternsModel; 21 | icons: IconsModel; 22 | labels: SequenceInfoModel; 23 | selection: SelectionModel; 24 | events: EventsModel; 25 | 26 | constructor(divId?: string ) { 27 | this.divId = divId; 28 | this.init = false; 29 | this.params = new OptionsModel(); 30 | this.rows = new RowsModel(); 31 | this.consensus = new ConsensusModel(); 32 | this.regions = new ColorsModel(); 33 | this.patterns = new PatternsModel(); 34 | this.icons = new IconsModel(); 35 | this.labels = new SequenceInfoModel(); 36 | this.selection = new SelectionModel(); 37 | this.events = new EventsModel(); 38 | 39 | 40 | 41 | window.onresize = () => { 42 | this.calculateIdxs(false); 43 | }; 44 | 45 | 46 | 47 | window.onclick = () => { 48 | this.calculateIdxs(true); 49 | }; // had to add this to cover mobidb toggle event 50 | } 51 | 52 | 53 | private calculateIdxs(flag) { 54 | 55 | 56 | 57 | for (const id of ProSeqViewer.sqvList) { 58 | 59 | if (document.getElementById(id) != null) { 60 | const sqvBody = document.getElementById(id); 61 | const chunks = sqvBody.getElementsByClassName('cnk'); 62 | 63 | let oldTop = 0; 64 | let newTop = 1; 65 | 66 | for (let i = 0; i < chunks.length; i++) { 67 | // erase old indexes before recalculating them 68 | chunks[i].firstElementChild.className = 'idx hidden'; 69 | 70 | if (flag) { 71 | // avoid calculating if idx already set 72 | if (chunks[i].firstElementChild.className === 'idx') { 73 | return; 74 | } 75 | } 76 | 77 | newTop = chunks[i].getBoundingClientRect().top + window.scrollY; 78 | 79 | if (newTop > oldTop) { 80 | chunks[i].firstElementChild.className = 'idx'; 81 | oldTop = newTop; 82 | } 83 | 84 | } 85 | } 86 | } 87 | 88 | } 89 | 90 | 91 | public draw(inputs: Input) { 92 | const sqvBody = document.getElementById(this.divId); 93 | if (sqvBody) { 94 | sqvBody.innerHTML = `
input error
`; 95 | } 96 | ProSeqViewer.sqvList.push(this.divId); 97 | 98 | let labels; 99 | let labelsFlag; 100 | let startIndexes; 101 | let tooltips; 102 | let data; 103 | 104 | /** check and process parameters input */ 105 | inputs.options = this.params.process(inputs.options, inputs.consensus); 106 | 107 | /** check and consensus input and global colorScheme */ 108 | if (inputs.options){ [inputs.sequences, inputs.regions ] = this.consensus.process(inputs.sequences, inputs.regions, inputs.options); } 109 | 110 | 111 | 112 | /** check and process patterns input */ 113 | inputs.patterns = this.patterns.process(inputs.patterns, inputs.sequences); 114 | 115 | 116 | /** check and process colors input */ 117 | inputs.regions = this.regions.process(inputs); 118 | 119 | 120 | /** check and process icons input */ 121 | let icons = this.icons.process(inputs.regions, inputs.sequences, inputs.icons); 122 | 123 | /** check and process sequences input */ 124 | data = this.rows.process(inputs.sequences, icons, inputs.regions, inputs.options); 125 | 126 | /** check and process labels input */ 127 | [ labels, startIndexes, tooltips, labelsFlag ] = this.labels.process(inputs.regions, inputs.sequences); 128 | 129 | /** create/update sqv-body html */ 130 | this.createGUI(data, labels, startIndexes, tooltips, inputs.options, labelsFlag); 131 | 132 | /** listen copy paste events */ 133 | this.selection.process(); 134 | 135 | /** listen selection events */ 136 | this.events.onRegionSelected(); 137 | } 138 | 139 | private generateLabels(idx, labels, startIndexes, indexesLocation, chunkSize, fontSize, tooltips, data, lineSeparation) { 140 | let labelshtml = ''; 141 | let labelsContainer = ''; 142 | const noGapsLabels = []; 143 | 144 | if (labels.length > 0) { 145 | if (indexesLocation != 'lateral') { 146 | labelshtml += ``; 147 | } 148 | let flag; 149 | let count = -1; 150 | let seqN = -1; 151 | for (const seqNum of data) { 152 | if (noGapsLabels.length < data.length) { 153 | noGapsLabels.push(0); 154 | } 155 | seqN += 1; 156 | for (const res in seqNum) { 157 | if (seqNum[res].char && seqNum[res].char.includes('svg')) { 158 | flag = true; 159 | break; 160 | } 161 | } 162 | 163 | if (flag) { 164 | noGapsLabels[seqN] = ''; 165 | if (idx) { 166 | // line with only icons, no need for index 167 | labelshtml += ` ${noGapsLabels[seqN]}`; 168 | } else { 169 | labelshtml += ``; 170 | } 171 | 172 | } else { 173 | count += 1; 174 | if (idx) { 175 | if (!chunkSize) { 176 | // lateral index regular 177 | labelshtml += ` 178 | ${(startIndexes[count] - 1) + idx}`; 179 | } else { 180 | let noGaps = 0; 181 | for (const res in seqNum) { 182 | if (+res <= (idx) && seqNum[res].char !== '-') { 183 | noGaps += 1; 184 | } 185 | } 186 | // lateral index gap 187 | noGapsLabels[seqN] = noGaps; 188 | labelshtml += ` 189 | ${(startIndexes[count] - 1) + noGapsLabels[seqN]}`; 190 | } 191 | 192 | } else { 193 | labelshtml += `${labels[count]}${tooltips[count]}`; 194 | } 195 | } 196 | flag = false; 197 | } 198 | 199 | if (indexesLocation == 'lateral' || 'both') { 200 | labelsContainer = `${labelshtml}`; 201 | 202 | } else { 203 | // add margin in case we only have labels and no indexes 204 | labelsContainer = `${labelshtml}`; 205 | 206 | } 207 | 208 | } 209 | return labelsContainer; 210 | } 211 | 212 | private addTopIndexes(chunkSize, x, maxTop, rowMarginBottom) { 213 | let cells = ''; 214 | // adding top indexes 215 | 216 | let chunkTopIndex; 217 | if (x % chunkSize === 0 && x <= maxTop) { 218 | chunkTopIndex = `${x}`; 219 | 220 | } else { 221 | chunkTopIndex = ``; 222 | } 223 | cells += chunkTopIndex; 224 | return cells; 225 | } 226 | 227 | private createGUI(data, labels, startIndexes, tooltips, options, labelsFlag) { 228 | 229 | const sqvBody = document.getElementById(this.divId); 230 | 231 | // convert to nodes to improve rendering (idea to try): 232 | // Create new element 233 | // const root = document.createElement('div'); 234 | // // Add class to element 235 | // root.className = 'my-new-element'; 236 | // // Add color 237 | // root.style.color = 'red'; 238 | // // Fill element with html 239 | // root.innerHTML = ``; 240 | // // Add element node to DOM graph 241 | // sqvBody.appendChild(root); 242 | // // Exit 243 | // return; 244 | 245 | if (!sqvBody) { 246 | // Cannot find sqv-body element 247 | return; 248 | } 249 | 250 | const chunkSize = options.chunkSize; 251 | const fontSize = options.fontSize; 252 | const chunkSeparation = options.chunkSeparation; 253 | const indexesLocation = options.indexesLocation; 254 | const wrapLine = options.wrapLine; 255 | const viewerWidth = options.viewerWidth; 256 | const lineSeparation = options.lineSeparation + ';'; 257 | const fNum = +fontSize.substr(0, fontSize.length - 2); 258 | const fUnit = fontSize.substr(fontSize.length - 2, 2); 259 | 260 | 261 | // maxIdx = length of the longest sequence 262 | let maxIdx = 0; 263 | let maxTop = 0; 264 | for (const row of data) { 265 | if (maxIdx < Object.keys(row).length) { maxIdx = Object.keys(row).length; } 266 | if (maxTop < Object.keys(row).length) { maxTop = Object.keys(row).length; } 267 | } 268 | 269 | const lenghtIndex = maxIdx.toString().length; 270 | const indexWidth = (fNum * lenghtIndex).toString() + fUnit; 271 | 272 | // consider the last chunk even if is not long enough 273 | if (chunkSize > 0) { maxIdx += (chunkSize - (maxIdx % chunkSize)) % chunkSize; } 274 | 275 | // generate labels 276 | const labelsContainer = this.generateLabels(false, labels, startIndexes, indexesLocation, false, indexWidth, tooltips, data, lineSeparation); 277 | 278 | let index = ''; 279 | let cards = ''; 280 | let cell; 281 | let entity; 282 | let style; 283 | let html = ''; 284 | let idxNum = 0; 285 | let idx; 286 | let cells = ''; 287 | for (let x = 1; x <= maxIdx; x++) { 288 | if (indexesLocation != 'lateral') {cells = this.addTopIndexes(chunkSize, x, maxTop, lineSeparation)}; 289 | 290 | for (let y = 0; y < data.length; y++) { 291 | entity = data[y][x]; 292 | style = 'font-size: 1em;display:block;height:1em;line-height:1em;margin-bottom:' + lineSeparation; 293 | if (y === data.length - 1) { style = 'font-size: 1em;display:block;line-height:1em;margin-bottom:' + lineSeparation; } 294 | if (!entity) { 295 | // emptyfiller 296 | style = 'font-size: 1em;display:block;color: rgba(0, 0, 0, 0);height:1em;line-height:1em;margin-bottom:' + lineSeparation; 297 | cell = `A`; // mock char, this has to be done to have chunks all of the same length (last chunk can't be shorter) 298 | } else { 299 | 300 | if (entity.target) { style += `${entity.target}`; } 301 | if (entity.char && !entity.char.includes('svg')) { 302 | // y is the row, x is the column 303 | cell = `${entity.char}`; 305 | } else { 306 | style += '-webkit-user-select: none;'; 307 | cell = `${entity.char}`; 308 | } 309 | } 310 | cells += cell; 311 | } 312 | 313 | cards += `
${cells}
`; // width 3/5em to reduce white space around letters 314 | cells = ''; 315 | 316 | 317 | if (chunkSize > 0 && x % chunkSize === 0) { 318 | // considering the row of top indexes 319 | if (indexesLocation != 'top') { 320 | idxNum += chunkSize; // lateral index (set only if top indexes missing) 321 | idx = idxNum - (chunkSize - 1); 322 | // adding labels 323 | const gapsContainer = this.generateLabels(idx, labels, startIndexes, indexesLocation, chunkSize, indexWidth, false, data, lineSeparation); 324 | 325 | if (labels[0] === '') { 326 | index = gapsContainer; // lateral number indexes 327 | } else { 328 | index = labelsContainer + gapsContainer; // lateral number indexes + labels 329 | } 330 | 331 | 332 | if (!labelsFlag) { 333 | index = gapsContainer; // lateral number indexes 334 | } else { 335 | index = labelsContainer + gapsContainer; // lateral number indexes + labels 336 | } 337 | } else { 338 | index = labelsContainer; // top 339 | } 340 | 341 | index = ``; 342 | style = `font-size: ${fontSize};`; 343 | 344 | if (x !== maxIdx) { style += 'padding-right: ' + chunkSeparation + 'em;'; } else { style += 'margin-right: ' + chunkSeparation + 'em;'; } 345 | 346 | let chunk = ''; 347 | 348 | if (labelsFlag || options.consensusType || indexesLocation == 'both' || indexesLocation == 'lateral') { // both 349 | chunk = `
${index}
${cards}
`; 350 | } else { 351 | chunk = `
${cards}
`; // top 352 | } 353 | cards = ''; 354 | index = ''; 355 | html += chunk; 356 | } 357 | 358 | } 359 | let innerHTML; 360 | 361 | 362 | if (wrapLine) { 363 | innerHTML = `
${html}
`; 364 | 365 | } else { 366 | 367 | innerHTML = `
368 |
${html}
369 |
`; 370 | 371 | } 372 | 373 | sqvBody.innerHTML = innerHTML; 374 | window.dispatchEvent(new Event('resize')); 375 | } 376 | 377 | 378 | } 379 | // VERY IMPORTANT AND USEFUL TO BE ABLE TO HAVE A WORKING BUNDLE.JS!! NEVER DELETE THIS LINE 380 | (window as any).ProSeqViewer = ProSeqViewer; 381 | -------------------------------------------------------------------------------- /src/lib/rows.model.ts: -------------------------------------------------------------------------------- 1 | import {Palettes} from './palettes'; 2 | import {ColorsModel} from './colors.model'; 3 | 4 | export class RowsModel { 5 | substitutiveId = 99999999999999; 6 | 7 | private processRows(rows, icons, regions, opt) { 8 | 9 | const allData = []; 10 | 11 | // decide which color is more important in case of overwriting 12 | const coloringOrder = ['custom', 'clustal', 'zappo', 'gradient', 'binary']; 13 | 14 | // order row Numbers 15 | const rowNumsOrdered = Object.keys(rows).map(Number).sort((n1, n2) => n1 - n2); 16 | 17 | // order keys of Row object 18 | const ordered = {}; 19 | for (const rowNum of rowNumsOrdered) { 20 | ordered[rowNum] = Object.keys(rows[+rowNum]).map(Number).sort((n1, n2) => n1 - n2); 21 | } 22 | 23 | let data; 24 | let coloringRowNums; 25 | let tmp; 26 | // loop through data rows 27 | for (const rowNum of rowNumsOrdered) { 28 | tmp = ordered[rowNum]; 29 | // data key: indexes, value: chars 30 | data = rows[rowNum]; 31 | // data[rowNum].label = this.rows.getLabel(rowNum, this.sequences); 32 | // console.log(data) 33 | if (regions) { 34 | for (const coloring of coloringOrder.reverse()) { 35 | coloringRowNums = ColorsModel.getRowsList(coloring).map(Number); 36 | // if there is coloring for the data row 37 | if (coloringRowNums.indexOf(rowNum) < 0) { 38 | // go to next coloring 39 | continue; 40 | } 41 | 42 | const positions = ColorsModel.getPositions(coloring, rowNum); 43 | // positions = start, end, target (bgcolor || fgcolor) 44 | if (positions.length > 0) { 45 | for (const e of positions) { 46 | for (let i = e.start; i <= e.end; i++) { 47 | if (!data[i]) { 48 | continue; 49 | } 50 | 51 | if (e.backgroundColor && !e.backgroundColor.startsWith('#')) { // is a palette 52 | if (e.backgroundColor == 'custom') { 53 | data[i].backgroundColor = opt.customPalette[data[i].char]; 54 | } else { 55 | data[i].backgroundColor = Palettes[e.backgroundColor][data[i].char]; // e.backgroundcolor = zappo, clustal.. 56 | } 57 | } else { 58 | data[i].backgroundColor = e.backgroundColor; // is a region or pattern 59 | } 60 | data[i].target = e.target + 'background-color:' + data[i].backgroundColor; 61 | } 62 | } 63 | } 64 | } 65 | if (icons !== {}) { 66 | const iconsData = icons[rowNum]; 67 | if (iconsData) { allData.push(iconsData); } 68 | } 69 | } 70 | allData.push(data); 71 | } 72 | return allData; 73 | } 74 | 75 | process(sequences, icons, regions, opt) { 76 | 77 | // check and set global sequenceColor 78 | if (opt && opt.sequenceColor) { 79 | // @ts-ignore 80 | for (const sequence of sequences) { 81 | if (!sequence.sequenceColor) { 82 | sequence.sequenceColor = opt.sequenceColor; 83 | } 84 | } 85 | } 86 | 87 | // keep previous data 88 | if (!sequences) { return; } 89 | 90 | // reset data 91 | const rows = {}; 92 | 93 | // check if there are undefined or duplicate ids and prepare to reset them 94 | const values = []; 95 | let undefinedValues = 0; 96 | 97 | for (const r of Object.keys(sequences)) { 98 | 99 | if (isNaN(+sequences[r].id)) { 100 | // missing id 101 | undefinedValues += 1; 102 | sequences[r].id = this.substitutiveId; 103 | this.substitutiveId -= 1; 104 | // otherwise just reset missing ids and log the resetted id 105 | } else { 106 | if (values.includes(+sequences[r].id)) { 107 | // Duplicate sequence id 108 | delete sequences[r]; 109 | } else { 110 | values.push(+sequences[r].id); 111 | } 112 | } 113 | } 114 | 115 | for (const row of Object.keys(sequences)) { 116 | 117 | /** check sequences id type */ 118 | let id; 119 | if (isNaN(+sequences[row].id)) { 120 | id = values.sort()[values.length - 1] + 1; 121 | } else { 122 | id = sequences[row].id; 123 | } 124 | 125 | /** set row chars */ 126 | rows[id] = {}; 127 | for (const idx of Object.keys(sequences[row].sequence)) { 128 | const idxKey = (+idx + 1).toString(); 129 | const char = sequences[row].sequence[idx]; 130 | rows[id][idxKey] = {char}; 131 | } 132 | } 133 | return this.processRows(rows, icons, regions, opt); 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /src/lib/selection.model.ts: -------------------------------------------------------------------------------- 1 | import {start} from "repl"; 2 | 3 | interface Cell { 4 | x: string; 5 | y: string; 6 | char?: string; 7 | sequenceId?: number; 8 | sqvId: string; 9 | } 10 | 11 | export class SelectionModel { 12 | 13 | start: Cell; 14 | lastOver: Cell; 15 | lastSqv; 16 | lastId; 17 | firstOver; 18 | event_sequence = []; 19 | 20 | 21 | private set_start(e) { 22 | let id; 23 | let element; 24 | if (e.path) { 25 | // chrome support 26 | element = e.path[0]; 27 | id = document.getElementById(element.dataset.resId); 28 | } else { 29 | // firefox support 30 | element = e.originalTarget; 31 | id = document.getElementById(element.dataset.resId); 32 | } 33 | this.lastId = element.dataset.resId; 34 | this.lastSqv = id; 35 | 36 | 37 | this.start = {y: element.dataset.resY, x: element.dataset.resX, sqvId: element.dataset.resId}; 38 | this.lastOver = {y: element.dataset.resY, x: element.dataset.resX, sqvId: element.dataset.resId}; 39 | 40 | const elements = document.querySelectorAll('[data-res-id=' + element.dataset.resId + ']'); 41 | this.selectionhighlight(elements); 42 | this.firstOver = false; 43 | 44 | } 45 | 46 | private selectionhighlight(elements) { 47 | 48 | for (const selection of elements) { 49 | const x = +selection.getAttribute('data-res-x'); 50 | const y = +selection.getAttribute('data-res-y'); 51 | 52 | 53 | let firstX = Math.min(+this.start.x, +this.lastOver.x); 54 | let lastX = Math.max(+this.start.x, +this.lastOver.x); 55 | let firstY = Math.min(+this.start.y, +this.lastOver.y); 56 | let lastY = Math.max(+this.start.y, +this.lastOver.y); 57 | 58 | 59 | // on every drag reselect the whole area ... 60 | if (x >= +firstX && x <= +lastX && 61 | y >= +firstY && y <= +lastY && 62 | selection.getAttribute('data-res-id') === this.lastOver.sqvId ) { 63 | selection.classList.add('highlight'); 64 | } else { 65 | selection.classList.remove('highlight'); 66 | } 67 | } 68 | } 69 | 70 | public process() { 71 | const sequenceViewers = document.getElementsByClassName('cell'); 72 | 73 | 74 | 75 | 76 | // remove selection on new click 77 | window.onmousedown = (event) => { 78 | 79 | this.event_sequence.push(0); 80 | 81 | 82 | // @ts-ignore 83 | for (const sqv of sequenceViewers) { 84 | sqv.onmousedown = (e) => { 85 | this.set_start(e); 86 | } 87 | } 88 | 89 | if (this.event_sequence[0] == 0 && this.event_sequence[1] == 1 && this.event_sequence[2] == 2 && this.event_sequence[0]== 0) { 90 | // left click 91 | 92 | const elements = document.querySelectorAll('[data-res-id=' + this.lastId + ']'); 93 | // @ts-ignore 94 | for (const selection of elements) { 95 | selection.classList.remove('highlight'); 96 | } 97 | 98 | } 99 | 100 | // if first click outside sqvDiv (first if is valid in Chrome, second in firefox) 101 | if (!event.target.dataset.resX) { 102 | this.firstOver = true; 103 | } 104 | if (event.explicitOriginalTarget && event.explicitOriginalTarget.dataset) { 105 | this.firstOver = true; 106 | } 107 | 108 | this.event_sequence = [0]; 109 | 110 | }; 111 | 112 | 113 | 114 | // @ts-ignore 115 | for (const sqv of sequenceViewers) { 116 | 117 | sqv.onmouseover = (e: any) => { 118 | 119 | if (!(1 in this.event_sequence)){ 120 | this.event_sequence.push(1); 121 | } 122 | 123 | if(this.firstOver) { 124 | 125 | this.set_start(e); 126 | } 127 | 128 | let element; 129 | if (e.path) { element = e.path[0]; } else { element = e.originalTarget; } 130 | 131 | if (this.start) { 132 | 133 | this.lastOver = {y: element.dataset.resY, x: element.dataset.resX, sqvId: element.dataset.resId}; 134 | 135 | const elements = document.querySelectorAll('[data-res-id=' + element.dataset.resId + ']'); 136 | if (this.lastId == element.dataset.resId) { 137 | this.selectionhighlight(elements); 138 | } 139 | 140 | } 141 | 142 | }; 143 | } 144 | 145 | 146 | document.body.onmouseup = () => { 147 | this.event_sequence.push(2); 148 | this.firstOver = false; 149 | if (this.start) { 150 | this.start = undefined; 151 | } 152 | if (this.event_sequence[0] == 0 && this.event_sequence[1] == 2) { 153 | const elements = document.querySelectorAll('[data-res-id=' + this.lastId + ']'); 154 | // @ts-ignore 155 | for (const selection of elements) { 156 | selection.classList.remove('highlight'); 157 | } 158 | } 159 | 160 | }; 161 | 162 | document.body.addEventListener('keydown', (e: any) => { 163 | const elements = document.querySelectorAll('[data-res-id=' + this.lastId + ']'); 164 | // @ts-ignore 165 | e = e || window.event; 166 | const key = e.which || e.keyCode; // keyCode detection 167 | const ctrl = e.ctrlKey ? e.ctrlKey : ((key === 17)); // ctrl detection 168 | if ( key === 67 && ctrl ) { 169 | let textToPaste = ''; 170 | const textDict = {}; 171 | let row = ''; 172 | // tslint:disable-next-line:forin 173 | // @ts-ignore 174 | for ( const selection of elements) { 175 | if (selection.classList.contains('highlight')) { 176 | if (!textDict[selection.getAttribute('data-res-y')]) { 177 | textDict[selection.getAttribute('data-res-y')] = ''; 178 | } 179 | // new line when new row 180 | if (selection.getAttribute('data-res-y') !== row && row !== '') { 181 | textDict[selection.getAttribute('data-res-y')] += selection.innerText; 182 | } else { 183 | textDict[selection.getAttribute('data-res-y')] += selection.innerText; 184 | } 185 | row = selection.getAttribute('data-res-y'); 186 | } 187 | } 188 | 189 | let flag; 190 | for (const textRow in textDict) { 191 | if (flag) { 192 | textToPaste += '\n' + textDict[textRow]; 193 | } else { 194 | textToPaste += textDict[textRow]; 195 | flag = true; 196 | } 197 | } 198 | if (textToPaste !== '') { 199 | // copy to clipboard for the paste event 200 | const dummy = document.createElement('textarea'); 201 | document.body.appendChild(dummy); 202 | dummy.value = textToPaste; 203 | 204 | 205 | dummy.select(); 206 | document.execCommand('copy'); 207 | document.body.removeChild(dummy); 208 | 209 | 210 | const evt = new CustomEvent('onHighlightSelection', {detail: {text: textToPaste, eventType: 'highlight selection'}} ); 211 | window.dispatchEvent(evt); 212 | } 213 | } 214 | }, false); 215 | 216 | 217 | }} 218 | -------------------------------------------------------------------------------- /src/lib/sequenceInfoModel.ts: -------------------------------------------------------------------------------- 1 | export class SequenceInfoModel { 2 | 3 | process(regions, sequences) { 4 | const labels = []; 5 | const startIndexes = []; 6 | const tooltips = []; 7 | let flag; 8 | sequences.sort((a, b) => a.id - b.id); 9 | for (const seq of sequences) { 10 | if (!seq) { continue; } 11 | if (seq.startIndex) { 12 | startIndexes.push(seq.startIndex); 13 | } else { 14 | startIndexes.push(1); 15 | } 16 | if (seq.labelTooltip) { 17 | tooltips.push(seq.labelTooltip); 18 | } else { 19 | tooltips.push(''); 20 | } 21 | if (seq.label && !this.isHTML(seq.label) ) { 22 | labels.push(seq.label); 23 | flag = true // to check if I have at least one label 24 | } else { 25 | labels.push(''); 26 | } 27 | } 28 | 29 | return [labels, startIndexes, tooltips, flag]; 30 | } 31 | 32 | isHTML = (str) => { 33 | const fragment = document.createRange().createContextualFragment(str); 34 | 35 | // remove all non text nodes from fragment 36 | fragment.querySelectorAll('*').forEach(el => el.parentNode.removeChild(el)); 37 | 38 | // if there is textContent, then not a pure HTML 39 | return !(fragment.textContent || '').trim(); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es2015", 5 | "declaration": true, 6 | "outDir": "./dist", 7 | "lib": [ 8 | "es6", 9 | "dom", 10 | "es2017" 11 | ], 12 | "baseUrl": ".", 13 | "paths": { 14 | "assets/*": [ 15 | "assets/*" 16 | ] 17 | } 18 | }, 19 | "include": [ 20 | "src/index.ts", 21 | "src/lib/*" 22 | ], 23 | "exclude": [ 24 | "node_modules" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | entry: './dist/index.js', 3 | output: { 4 | filename: 'sqv-bundle.js' 5 | }, 6 | optimization: { 7 | minimize: false 8 | }, 9 | mode: "production" 10 | }; 11 | --------------------------------------------------------------------------------