├── .gitignore ├── .npmignore ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── example.html ├── example.scss ├── example.ts ├── icons ├── icons8-chevron-down-48.png ├── icons8-chevron-right-48.png ├── icons8-download-48.png ├── icons8-help-48.png └── icons8-level-up-48.png ├── index.ts ├── package-lock.json ├── package.json ├── src ├── calculate.ts ├── commons.ts ├── custom-d3.ts ├── feature-viewer.ts ├── fillsvg.ts ├── helper.ts ├── interfaces.ts ├── styles │ └── styles.scss ├── tooltip.ts └── transition.ts ├── tsconfig.json └── webpack.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Exclude WebStorm project files 2 | .idea 3 | # Exclude node modules 4 | node_modules 5 | # Exclude built project 6 | dist 7 | # Ignore types 8 | **/*.d.ts 9 | # Ignore .js files 10 | **/*.js.map 11 | src/*.js 12 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ## Getting started 4 | 5 | Before you begin: 6 | - Check out the [existing issues](https://github.com/BioComputingUP/feature-viewer-typescript/issues). 7 | 8 | ### Don't see your issue? Open one 9 | 10 | If you spot something new, open an issue. We'll use the issue to have a conversation about the problem you want to fix. 11 | 12 | ### Ready to make a change? Fork the repo 13 | 14 | Fork using GitHub Desktop: 15 | 16 | - [Getting started with GitHub Desktop](https://docs.github.com/en/desktop/installing-and-configuring-github-desktop/getting-started-with-github-desktop) will guide you through setting up Desktop. 17 | - Once Desktop is set up, you can use it to [fork the repo](https://docs.github.com/en/desktop/contributing-and-collaborating-using-github-desktop/cloning-and-forking-repositories-from-github-desktop)! 18 | 19 | Fork using the command line: 20 | 21 | - [Fork the repo](https://docs.github.com/en/github/getting-started-with-github/fork-a-repo#fork-an-example-repository) so that you can make your changes without affecting the original project until you're ready to merge them. 22 | 23 | Fork with [GitHub Codespaces](https://github.com/features/codespaces): 24 | 25 | - [Fork, edit, and preview](https://docs.github.com/en/free-pro-team@latest/github/developing-online-with-codespaces/creating-a-codespace) using [GitHub Codespaces](https://github.com/features/codespaces) without having to install and run the project locally. 26 | 27 | ### Make your update: 28 | Make your changes to the file(s) you'd like to update. Here are some tips and tricks for [using the docs codebase](#working-in-the-githubdocs-repository). 29 | - Are you making changes to the application code? You'll need **Node.js lts/dubnium** to run the library locally. 30 | 31 | ### Open a pull request 32 | When you're done making changes and you'd like to propose them for review, open your pull request (PR). 33 | 34 | ### Submit your PR & get it reviewed 35 | - Once you submit your PR, we will review it with you. The first thing you're going to want to do is a [self review](#self-review). 36 | - After that, we may have questions, check back on your PR to keep up with the conversation. 37 | 38 | ### Your PR is merged! 39 | Congratulations! The whole GitHub community thanks you. :sparkles: 40 | Once your PR is merged, you will be proudly listed as a contributor in the [contributor chart](https://github.com/BioComputingUP/feature-viewer-typescript/graphs/contributors). 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015, SIB Swiss Institute of Bioinformatics 2 | 3 | This program is free software; you can redistribute it and/or modify it under 4 | the terms of the GNU General Public License as published by the Free Software Foundation; 5 | either version 2 of the License, or (at your option) any later version. 6 | 7 | 8 | GNU GENERAL PUBLIC LICENSE 9 | Version 2, June 1991 10 | 11 | 12 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 13 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 14 | Everyone is permitted to copy and distribute verbatim copies 15 | of this license document, but changing it is not allowed. 16 | 17 | Preamble 18 | 19 | The licenses for most software are designed to take away your 20 | freedom to share and change it. By contrast, the GNU General Public 21 | License is intended to guarantee your freedom to share and change free 22 | software--to make sure the software is free for all its users. This 23 | General Public License applies to most of the Free Software 24 | Foundation's software and to any other program whose authors commit to 25 | using it. (Some other Free Software Foundation software is covered by 26 | the GNU Lesser General Public License instead.) You can apply it to 27 | your programs, too. 28 | 29 | When we speak of free software, we are referring to freedom, not 30 | price. Our General Public Licenses are designed to make sure that you 31 | have the freedom to distribute copies of free software (and charge for 32 | this service if you wish), that you receive source code or can get it 33 | if you want it, that you can change the software or use pieces of it 34 | in new free programs; and that you know you can do these things. 35 | 36 | To protect your rights, we need to make restrictions that forbid 37 | anyone to deny you these rights or to ask you to surrender the rights. 38 | These restrictions translate to certain responsibilities for you if you 39 | distribute copies of the software, or if you modify it. 40 | 41 | For example, if you distribute copies of such a program, whether 42 | gratis or for a fee, you must give the recipients all the rights that 43 | you have. You must make sure that they, too, receive or can get the 44 | source code. And you must show them these terms so they know their 45 | rights. 46 | 47 | We protect your rights with two steps: (1) copyright the software, and 48 | (2) offer you this license which gives you legal permission to copy, 49 | distribute and/or modify the software. 50 | 51 | Also, for each author's protection and ours, we want to make certain 52 | that everyone understands that there is no warranty for this free 53 | software. If the software is modified by someone else and passed on, we 54 | want its recipients to know that what they have is not the original, so 55 | that any problems introduced by others will not reflect on the original 56 | authors' reputations. 57 | 58 | Finally, any free program is threatened constantly by software 59 | patents. We wish to avoid the danger that redistributors of a free 60 | program will individually obtain patent licenses, in effect making the 61 | program proprietary. To prevent this, we have made it clear that any 62 | patent must be licensed for everyone's free use or not licensed at all. 63 | 64 | The precise terms and conditions for copying, distribution and 65 | modification follow. 66 | 67 | GNU GENERAL PUBLIC LICENSE 68 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 69 | 70 | 0. This License applies to any program or other work which contains 71 | a notice placed by the copyright holder saying it may be distributed 72 | under the terms of this General Public License. The "Program", below, 73 | refers to any such program or work, and a "work based on the Program" 74 | means either the Program or any derivative work under copyright law: 75 | that is to say, a work containing the Program or a portion of it, 76 | either verbatim or with modifications and/or translated into another 77 | language. (Hereinafter, translation is included without limitation in 78 | the term "modification".) Each licensee is addressed as "you". 79 | 80 | Activities other than copying, distribution and modification are not 81 | covered by this License; they are outside its scope. The act of 82 | running the Program is not restricted, and the output from the Program 83 | is covered only if its contents constitute a work based on the 84 | Program (independent of having been made by running the Program). 85 | Whether that is true depends on what the Program does. 86 | 87 | 1. You may copy and distribute verbatim copies of the Program's 88 | source code as you receive it, in any medium, provided that you 89 | conspicuously and appropriately publish on each copy an appropriate 90 | copyright notice and disclaimer of warranty; keep intact all the 91 | notices that refer to this License and to the absence of any warranty; 92 | and give any other recipients of the Program a copy of this License 93 | along with the Program. 94 | 95 | You may charge a fee for the physical act of transferring a copy, and 96 | you may at your option offer warranty protection in exchange for a fee. 97 | 98 | 2. You may modify your copy or copies of the Program or any portion 99 | of it, thus forming a work based on the Program, and copy and 100 | distribute such modifications or work under the terms of Section 1 101 | above, provided that you also meet all of these conditions: 102 | 103 | a) You must cause the modified files to carry prominent notices 104 | stating that you changed the files and the date of any change. 105 | 106 | b) You must cause any work that you distribute or publish, that in 107 | whole or in part contains or is derived from the Program or any 108 | part thereof, to be licensed as a whole at no charge to all third 109 | parties under the terms of this License. 110 | 111 | c) If the modified program normally reads commands interactively 112 | when run, you must cause it, when started running for such 113 | interactive use in the most ordinary way, to print or display an 114 | announcement including an appropriate copyright notice and a 115 | notice that there is no warranty (or else, saying that you provide 116 | a warranty) and that users may redistribute the program under 117 | these conditions, and telling the user how to view a copy of this 118 | License. (Exception: if the Program itself is interactive but 119 | does not normally print such an announcement, your work based on 120 | the Program is not required to print an announcement.) 121 | 122 | These requirements apply to the modified work as a whole. If 123 | identifiable sections of that work are not derived from the Program, 124 | and can be reasonably considered independent and separate works in 125 | themselves, then this License, and its terms, do not apply to those 126 | sections when you distribute them as separate works. But when you 127 | distribute the same sections as part of a whole which is a work based 128 | on the Program, the distribution of the whole must be on the terms of 129 | this License, whose permissions for other licensees extend to the 130 | entire whole, and thus to each and every part regardless of who wrote it. 131 | 132 | Thus, it is not the intent of this section to claim rights or contest 133 | your rights to work written entirely by you; rather, the intent is to 134 | exercise the right to control the distribution of derivative or 135 | collective works based on the Program. 136 | 137 | In addition, mere aggregation of another work not based on the Program 138 | with the Program (or with a work based on the Program) on a volume of 139 | a storage or distribution medium does not bring the other work under 140 | the scope of this License. 141 | 142 | 3. You may copy and distribute the Program (or a work based on it, 143 | under Section 2) in object code or executable form under the terms of 144 | Sections 1 and 2 above provided that you also do one of the following: 145 | 146 | a) Accompany it with the complete corresponding machine-readable 147 | source code, which must be distributed under the terms of Sections 148 | 1 and 2 above on a medium customarily used for software interchange; or, 149 | 150 | b) Accompany it with a written offer, valid for at least three 151 | years, to give any third party, for a charge no more than your 152 | cost of physically performing source distribution, a complete 153 | machine-readable copy of the corresponding source code, to be 154 | distributed under the terms of Sections 1 and 2 above on a medium 155 | customarily used for software interchange; or, 156 | 157 | c) Accompany it with the information you received as to the offer 158 | to distribute corresponding source code. (This alternative is 159 | allowed only for noncommercial distribution and only if you 160 | received the program in object code or executable form with such 161 | an offer, in accord with Subsection b above.) 162 | 163 | The source code for a work means the preferred form of the work for 164 | making modifications to it. For an executable work, complete source 165 | code means all the source code for all modules it contains, plus any 166 | associated interface definition files, plus the scripts used to 167 | control compilation and installation of the executable. However, as a 168 | special exception, the source code distributed need not include 169 | anything that is normally distributed (in either source or binary 170 | form) with the major components (compiler, kernel, and so on) of the 171 | operating system on which the executable runs, unless that component 172 | itself accompanies the executable. 173 | 174 | If distribution of executable or object code is made by offering 175 | access to copy from a designated place, then offering equivalent 176 | access to copy the source code from the same place counts as 177 | distribution of the source code, even though third parties are not 178 | compelled to copy the source along with the object code. 179 | 180 | 4. You may not copy, modify, sublicense, or distribute the Program 181 | except as expressly provided under this License. Any attempt 182 | otherwise to copy, modify, sublicense or distribute the Program is 183 | void, and will automatically terminate your rights under this License. 184 | However, parties who have received copies, or rights, from you under 185 | this License will not have their licenses terminated so long as such 186 | parties remain in full compliance. 187 | 188 | 5. You are not required to accept this License, since you have not 189 | signed it. However, nothing else grants you permission to modify or 190 | distribute the Program or its derivative works. These actions are 191 | prohibited by law if you do not accept this License. Therefore, by 192 | modifying or distributing the Program (or any work based on the 193 | Program), you indicate your acceptance of this License to do so, and 194 | all its terms and conditions for copying, distributing or modifying 195 | the Program or works based on it. 196 | 197 | 6. Each time you redistribute the Program (or any work based on the 198 | Program), the recipient automatically receives a license from the 199 | original licensor to copy, distribute or modify the Program subject to 200 | these terms and conditions. You may not impose any further 201 | restrictions on the recipients' exercise of the rights granted herein. 202 | You are not responsible for enforcing compliance by third parties to 203 | this License. 204 | 205 | 7. If, as a consequence of a court judgment or allegation of patent 206 | infringement or for any other reason (not limited to patent issues), 207 | conditions are imposed on you (whether by court order, agreement or 208 | otherwise) that contradict the conditions of this License, they do not 209 | excuse you from the conditions of this License. If you cannot 210 | distribute so as to satisfy simultaneously your obligations under this 211 | License and any other pertinent obligations, then as a consequence you 212 | may not distribute the Program at all. For example, if a patent 213 | license would not permit royalty-free redistribution of the Program by 214 | all those who receive copies directly or indirectly through you, then 215 | the only way you could satisfy both it and this License would be to 216 | refrain entirely from distribution of the Program. 217 | 218 | If any portion of this section is held invalid or unenforceable under 219 | any particular circumstance, the balance of the section is intended to 220 | apply and the section as a whole is intended to apply in other 221 | circumstances. 222 | 223 | It is not the purpose of this section to induce you to infringe any 224 | patents or other property right claims or to contest validity of any 225 | such claims; this section has the sole purpose of protecting the 226 | integrity of the free software distribution system, which is 227 | implemented by public license practices. Many people have made 228 | generous contributions to the wide range of software distributed 229 | through that system in reliance on consistent application of that 230 | system; it is up to the author/donor to decide if he or she is willing 231 | to distribute software through any other system and a licensee cannot 232 | impose that choice. 233 | 234 | This section is intended to make thoroughly clear what is believed to 235 | be a consequence of the rest of this License. 236 | 237 | 8. If the distribution and/or use of the Program is restricted in 238 | certain countries either by patents or by copyrighted interfaces, the 239 | original copyright holder who places the Program under this License 240 | may add an explicit geographical distribution limitation excluding 241 | those countries, so that distribution is permitted only in or among 242 | countries not thus excluded. In such case, this License incorporates 243 | the limitation as if written in the body of this License. 244 | 245 | 9. The Free Software Foundation may publish revised and/or new versions 246 | of the General Public License from time to time. Such new versions will 247 | be similar in spirit to the present version, but may differ in detail to 248 | address new problems or concerns. 249 | 250 | Each version is given a distinguishing version number. If the Program 251 | specifies a version number of this License which applies to it and "any 252 | later version", you have the option of following the terms and conditions 253 | either of that version or of any later version published by the Free 254 | Software Foundation. If the Program does not specify a version number of 255 | this License, you may choose any version ever published by the Free Software 256 | Foundation. 257 | 258 | 10. If you wish to incorporate parts of the Program into other free 259 | programs whose distribution conditions are different, write to the author 260 | to ask for permission. For software which is copyrighted by the Free 261 | Software Foundation, write to the Free Software Foundation; we sometimes 262 | make exceptions for this. Our decision will be guided by the two goals 263 | of preserving the free status of all derivatives of our free software and 264 | of promoting the sharing and reuse of software generally. 265 | 266 | NO WARRANTY 267 | 268 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 269 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 270 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 271 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 272 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 273 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 274 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 275 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 276 | REPAIR OR CORRECTION. 277 | 278 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 279 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 280 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 281 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 282 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 283 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 284 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 285 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 286 | POSSIBILITY OF SUCH DAMAGES. 287 | 288 | END OF TERMS AND CONDITIONS 289 | 290 | How to Apply These Terms to Your New Programs 291 | 292 | If you develop a new program, and you want it to be of the greatest 293 | possible use to the public, the best way to achieve this is to make it 294 | free software which everyone can redistribute and change under these terms. 295 | 296 | To do so, attach the following notices to the program. It is safest 297 | to attach them to the start of each source file to most effectively 298 | convey the exclusion of warranty; and each file should have at least 299 | the "copyright" line and a pointer to where the full notice is found. 300 | 301 | {description} 302 | Copyright (C) {year} {fullname} 303 | 304 | This program is free software; you can redistribute it and/or modify 305 | it under the terms of the GNU General Public License as published by 306 | the Free Software Foundation; either version 2 of the License, or 307 | (at your option) any later version. 308 | 309 | This program is distributed in the hope that it will be useful, 310 | but WITHOUT ANY WARRANTY; without even the implied warranty of 311 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 312 | GNU General Public License for more details. 313 | 314 | You should have received a copy of the GNU General Public License along 315 | with this program; if not, write to the Free Software Foundation, Inc., 316 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 317 | 318 | Also add information on how to contact you by electronic and paper mail. 319 | 320 | If the program is interactive, make it output a short notice like this 321 | when it starts in an interactive mode: 322 | 323 | Gnomovision version 69, Copyright (C) year name of author 324 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 325 | This is free software, and you are welcome to redistribute it 326 | under certain conditions; type `show c' for details. 327 | 328 | The hypothetical commands `show w' and `show c' should show the appropriate 329 | parts of the General Public License. Of course, the commands you use may 330 | be called something other than `show w' and `show c'; they could even be 331 | mouse-clicks or menu items--whatever suits your program. 332 | 333 | You should also get your employer (if you work as a programmer) or your 334 | school, if any, to sign a "copyright disclaimer" for the program, if 335 | necessary. Here is a sample; alter the names: 336 | 337 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 338 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 339 | 340 | {signature of Ty Coon}, 1 April 1989 341 | Ty Coon, President of Vice 342 | 343 | This General Public License does not permit incorporating your program into 344 | proprietary programs. If your program is a subroutine library, you may 345 | consider it more useful to permit linking proprietary applications with the 346 | library. If this is what you want to do, use the GNU Lesser General 347 | Public License instead of this License. 348 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🚀 Welcome to your new awesome project! 2 | 3 | This project has been created using **webpack-cli**, you can now run 4 | 5 | ``` 6 | npm run build 7 | ``` 8 | 9 | or 10 | 11 | ``` 12 | yarn build 13 | ``` 14 | 15 | to bundle your application 16 | -------------------------------------------------------------------------------- /example.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Webpack App 6 | 7 | 8 |

Feature viewer example

9 |

Tip: Check your console

10 | 11 |
12 | 13 | 14 | -------------------------------------------------------------------------------- /example.scss: -------------------------------------------------------------------------------- 1 | // Add style to feature viewer container 2 | #feature-viewer { 3 | outline: 1px solid red; 4 | } 5 | -------------------------------------------------------------------------------- /example.ts: -------------------------------------------------------------------------------- 1 | // Import feature viewer 2 | import {FeatureViewer} from "./src/feature-viewer"; 3 | 4 | // Import styles 5 | import './example.scss'; 6 | 7 | // TODO Wait for page to load 8 | window.onload = () => { 9 | // Define sequence 10 | let sequence = "MDPGQQPPPQPAPQGQGQPPSQPPQGQGPPSGPGQPAPAATQAAPQAPPAGHQIVHVRGDSETDLEALFNAVMNPKTANVPQTVPMRLRKLPDSF" + 11 | "FKPPEPKSHSRQASTDAGTAGALTPQHVRAHSSPASLQLGAVSPGTLTPTGVVSGPAATPTAQHLRQSSFEIPDDVPLPAGWEMAKTSSGQRYFL" + 12 | "NHIDQTTTWQDPRKAMLSQMNVTAPTSPPVQQNMMNSASGPLPDGWEQAMTQDGEIYYINHKNKTTSWLDPRLDPRFAMNQRISQSAPVKQPPPL" + 13 | "APQSPQGGVMGGSNSNQQQQMRLQQLQMEKERLRLKQQELLRQAMRNINPSTANSPKCQELALRSQLPTLEQDGGTQNPVSSPGMSQELRTMTTN" + 14 | "SSDPFLNSGTYHSRDESTDSGLSMSSYSVPRTPDDFLNSVDEMDTGDTINQSTLPSQQNRFPDYLEAIPGTNVDLGTLEGDGMNIEGEELMPSLQ" + 15 | "EALSSDILNDMESVLAATKLDKESFLTWL"; 16 | // Instantiate feature viewer 17 | let featureViewer = new FeatureViewer(sequence, '#feature-viewer', 18 | // Define optional settings 19 | { 20 | toolbar: true, 21 | toolbarPosition: 'left', 22 | brushActive: true, 23 | zoomMax: 10, 24 | flagColor: 'white', 25 | flagTrack: 170, 26 | flagTrackMobile: 150 27 | }, 28 | // Define optional features 29 | [ 30 | { 31 | type: 'curve', 32 | id: 'Curve1', 33 | label: 'Random values', 34 | color: 'red', 35 | data: Array.from(new Array(sequence.length), (x, i) => ({x: i + 1, y: Math.random()})), 36 | subfeatures: [ 37 | { 38 | type: 'curve', 39 | id: 'Curve2', 40 | label: 'Random values', 41 | color: 'blue', 42 | height: 1, 43 | data: Array.from(new Array(sequence.length), (x, i) => ({x: i + 1, y: Math.random() /2 })) 44 | } 45 | ] 46 | }, 47 | { 48 | type: 'rect', 49 | id: 'Degrons', 50 | label: 'Degrons', 51 | color:'grey', 52 | data: [{"x": 186, "y": 194, "color": "#006dc4", "tooltip": "186-194 xRxxLxx[LIVM]x", "stroke": "black"}, {"x": 348, "y": 356, "color": "#006dc4", "tooltip": "348-356 xRxxLxx[LIVM]x", "stroke": "black"}, {"x": 1, "y": 2, "color": "#c90076", "tooltip": "1-2 Mx", "stroke": "black"}, {"x": 1, "y": 3, "color": "#c90076", "tooltip": "1-3 M{0,1}([ED])x", "stroke": "black"}, {"x": 399, "y": 405, "color": "#006dc4", "tooltip": "399-405 D(S)Gx{2,3}([ST])", "stroke": "black"}, {"x": 180, "y": 184, "color": "#006dc4", "tooltip": "180-184 [AVP]x[ST][ST][ST]", "stroke": "black"}, {"x": 399, "y": 403, "color": "#006dc4", "tooltip": "399-403 DSGLS", "stroke": "black"}], 53 | }, 54 | { 55 | type: 'rect', 56 | id: 'IDRs', 57 | label: 'IDRs', 58 | data: [{"x": 1, "y": 58, "color": "#9e7398", "stroke": "black"}, {"x": 60, "y": 61, "color": "#9e7398", "stroke": "black"}, {"x": 75, "y": 82, "color": "#9e7398", "stroke": "black"}, {"x": 101, "y": 170, "color": "#9e7398", "stroke": "black"}, {"x": 206, "y": 230, "color": "#9e7398", "stroke": "black"}, {"x": 264, "y": 300, "color": "#9e7398", "stroke": "black"}, {"x": 330, "y": 504, "color": "#9e7398", "stroke": "black"}], 59 | color:'grey' 60 | }, 61 | { 62 | type: 'rect', 63 | id: 'Coils', 64 | label: 'Coils', 65 | data: [{"x": 1, "y": 49, "color": "#e3b9e5", "stroke": "black"}, {"x": 52, "y": 58, "color": "#e3b9e5", "stroke": "black"}, {"x": 74, "y": 74, "color": "#e3b9e5", "stroke": "black"}, {"x": 78, "y": 85, "color": "#e3b9e5", "stroke": "black"}, {"x": 91, "y": 92, "color": "#e3b9e5", "stroke": "black"}, {"x": 98, "y": 108, "color": "#e3b9e5", "stroke": "black"}, {"x": 110, "y": 168, "color": "#e3b9e5", "stroke": "black"}, {"x": 172, "y": 174, "color": "#e3b9e5", "stroke": "black"}, {"x": 182, "y": 182, "color": "#e3b9e5", "stroke": "black"}, {"x": 185, "y": 186, "color": "#e3b9e5", "stroke": "black"}, {"x": 201, "y": 201, "color": "#e3b9e5", "stroke": "black"}, {"x": 212, "y": 213, "color": "#e3b9e5", "stroke": "black"}, {"x": 215, "y": 220, "color": "#e3b9e5", "stroke": "black"}, {"x": 223, "y": 224, "color": "#e3b9e5", "stroke": "black"}, {"x": 226, "y": 228, "color": "#e3b9e5", "stroke": "black"}, {"x": 231, "y": 233, "color": "#e3b9e5", "stroke": "black"}, {"x": 241, "y": 241, "color": "#e3b9e5", "stroke": "black"}, {"x": 245, "y": 245, "color": "#e3b9e5", "stroke": "black"}, {"x": 260, "y": 260, "color": "#e3b9e5", "stroke": "black"}, {"x": 276, "y": 297, "color": "#e3b9e5", "stroke": "black"}, {"x": 334, "y": 334, "color": "#e3b9e5", "stroke": "black"}, {"x": 340, "y": 340, "color": "#e3b9e5", "stroke": "black"}, {"x": 353, "y": 383, "color": "#e3b9e5", "stroke": "black"}, {"x": 386, "y": 415, "color": "#e3b9e5", "stroke": "black"}, {"x": 417, "y": 418, "color": "#e3b9e5", "stroke": "black"}, {"x": 420, "y": 443, "color": "#e3b9e5", "stroke": "black"}, {"x": 447, "y": 474, "color": "#e3b9e5", "stroke": "black"}, {"x": 494, "y": 494, "color": "#e3b9e5", "stroke": "black"}, {"x": 496, "y": 496, "color": "#e3b9e5", "stroke": "black"}, {"x": 503, "y": 504, "color": "#e3b9e5", "stroke": "black"}], 66 | color:'grey' 67 | }, 68 | { 69 | type: 'rect', 70 | id: 'Buried', 71 | label: 'Buried residues', 72 | data: [{"x": 166, "y": 166, "color": "#D3D3D3", "stroke": "black"}, {"x": 179, "y": 179, "color": "#D3D3D3", "stroke": "black"}, {"x": 188, "y": 188, "color": "#D3D3D3", "stroke": "black"}, {"x": 190, "y": 190, "color": "#D3D3D3", "stroke": "black"}, {"x": 200, "y": 202, "color": "#D3D3D3", "stroke": "black"}, {"x": 232, "y": 232, "color": "#D3D3D3", "stroke": "black"}, {"x": 235, "y": 236, "color": "#D3D3D3", "stroke": "black"}, {"x": 238, "y": 238, "color": "#D3D3D3", "stroke": "black"}, {"x": 247, "y": 249, "color": "#D3D3D3", "stroke": "black"}, {"x": 259, "y": 261, "color": "#D3D3D3", "stroke": "black"}], 73 | color:'grey' 74 | }, 75 | { 76 | type: 'lollipop', 77 | id: 'Ubiquitination', 78 | label: 'Ubiquitination', 79 | color:'grey', 80 | data: [{"x": 497, "y": 497, "color": "#ffe800", "tooltip": "497K Ubiquitination
(PhosphoSitePlus)", "stroke": "black"}, {"x": 181, "y": 181, "color": "#ffe800", "tooltip": "181K Ubiquitination
(PhosphoSitePlus)", "stroke": "black"}, {"x": 102, "y": 102, "color": "#ffe800", "tooltip": "102K Ubiquitination
(PhosphoSitePlus)", "stroke": "black"}, {"x": 97, "y": 97, "color": "#ffe800", "tooltip": "97K Ubiquitination
(PhosphoSitePlus)", "stroke": "black"}, {"x": 90, "y": 90, "color": "#ffe800", "tooltip": "90K Ubiquitination
(PhosphoSitePlus)", "stroke": "black"}, {"x": 280, "y": 280, "color": "#ffe800", "tooltip": "280K Ubiquitination
(PhosphoSitePlus)", "stroke": "black"}, {"x": 315, "y": 315, "color": "#ffe800", "tooltip": "315K Ubiquitination
(PhosphoSitePlus)", "stroke": "black"}, {"x": 321, "y": 321, "color": "#ffe800", "tooltip": "321K Ubiquitination
(PhosphoSitePlus)", "stroke": "black"}, {"x": 342, "y": 342, "color": "#ffe800", "tooltip": "342K Ubiquitination
(PhosphoSitePlus)", "stroke": "black"}, {"x": 254, "y": 254, "color": "#ffe800", "tooltip": "254K Ubiquitination
(PhosphoSitePlus)", "stroke": "black"}], 81 | }, 82 | { 83 | type: 'lollipop', 84 | id: 'Phosphorylation', 85 | label: 'Phosphorylation', 86 | color:'grey', 87 | data: [{"x": 473, "y": 473, "color": "#ffba01", "tooltip": "473S Phosphorylation
(iPTMNet)", "stroke": "black"}, {"x": 405, "y": 405, "color": "#ffba01", "tooltip": "405S Phosphorylation
(PhosphoSitePlus)", "stroke": "black"}, {"x": 407, "y": 407, "color": "#ffba01", "tooltip": "407Y Phosphorylation
(PhosphoSitePlus/iPTMNet)", "stroke": "black"}, {"x": 109, "y": 109, "color": "#ffba01", "tooltip": "109S Phosphorylation
(PhosphoSitePlus/iPTMNet)", "stroke": "black"}, {"x": 419, "y": 419, "color": "#ffba01", "tooltip": "419S Phosphorylation
(PhosphoSitePlus)", "stroke": "black"}, {"x": 436, "y": 436, "color": "#ffba01", "tooltip": "436S Phosphorylation
(PhosphoSitePlus)", "stroke": "black"}, {"x": 499, "y": 499, "color": "#ffba01", "tooltip": "499S Phosphorylation
(PhosphoSitePlus)", "stroke": "black"}, {"x": 412, "y": 412, "color": "#ffba01", "tooltip": "412T Phosphorylation
(PhosphoSitePlus/iPTMNet)", "stroke": "black"}, {"x": 403, "y": 403, "color": "#ffba01", "tooltip": "403S Phosphorylation
(PhosphoSitePlus/iPTMNet)", "stroke": "black"}, {"x": 366, "y": 366, "color": "#ffba01", "tooltip": "366S Phosphorylation
(PhosphoSitePlus)", "stroke": "black"}, {"x": 398, "y": 398, "color": "#ffba01", "tooltip": "398T Phosphorylation
(PhosphoSitePlus)", "stroke": "black"}, {"x": 164, "y": 164, "color": "#ffba01", "tooltip": "164S Phosphorylation
(PhosphoSitePlus/iPTMNet)", "stroke": "black"}, {"x": 188, "y": 188, "color": "#ffba01", "tooltip": "188Y Phosphorylation
(PhosphoSitePlus/iPTMNet)", "stroke": "black"}, {"x": 217, "y": 217, "color": "#ffba01", "tooltip": "217S Phosphorylation
(PhosphoSitePlus)", "stroke": "black"}, {"x": 227, "y": 227, "color": "#ffba01", "tooltip": "227S Phosphorylation
(PhosphoSitePlus)", "stroke": "black"}, {"x": 105, "y": 105, "color": "#ffba01", "tooltip": "105S Phosphorylation
(PhosphoSitePlus)", "stroke": "black"}, {"x": 274, "y": 274, "color": "#ffba01", "tooltip": "274S Phosphorylation
(PhosphoSitePlus/iPTMNet)", "stroke": "black"}, {"x": 103, "y": 103, "color": "#ffba01", "tooltip": "103S Phosphorylation
(PhosphoSitePlus)", "stroke": "black"}, {"x": 163, "y": 163, "color": "#ffba01", "tooltip": "163S Phosphorylation
(PhosphoSitePlus/iPTMNet)", "stroke": "black"}, {"x": 276, "y": 276, "color": "#ffba01", "tooltip": "276S Phosphorylation
(PhosphoSitePlus)", "stroke": "black"}, {"x": 300, "y": 300, "color": "#ffba01", "tooltip": "300S Phosphorylation
(PhosphoSitePlus)", "stroke": "black"}, {"x": 340, "y": 340, "color": "#ffba01", "tooltip": "340S Phosphorylation
(PhosphoSitePlus)", "stroke": "black"}, {"x": 400, "y": 400, "color": "#ffba01", "tooltip": "400S Phosphorylation
(PhosphoSitePlus/iPTMNet)", "stroke": "black"}, {"x": 94, "y": 94, "color": "#ffba01", "tooltip": "94S Phosphorylation
(PhosphoSitePlus/iPTMNet)", "stroke": "black"}, {"x": 83, "y": 83, "color": "#ffba01", "tooltip": "83T Phosphorylation
(PhosphoSitePlus)", "stroke": "black"}, {"x": 354, "y": 354, "color": "#ffba01", "tooltip": "354T Phosphorylation
(PhosphoSitePlus)", "stroke": "black"}, {"x": 361, "y": 361, "color": "#ffba01", "tooltip": "361T Phosphorylation
(PhosphoSitePlus)", "stroke": "black"}, {"x": 77, "y": 77, "color": "#ffba01", "tooltip": "77T Phosphorylation
(PhosphoSitePlus)", "stroke": "black"}, {"x": 289, "y": 289, "color": "#ffba01", "tooltip": "289S Phosphorylation
(PhosphoSitePlus/iPTMNet)", "stroke": "black"}, {"x": 63, "y": 63, "color": "#ffba01", "tooltip": "63T Phosphorylation
(PhosphoSitePlus/iPTMNet)", "stroke": "black"}, {"x": 156, "y": 156, "color": "#ffba01", "tooltip": "156T Phosphorylation
(PhosphoSitePlus/iPTMNet)", "stroke": "black"}, {"x": 110, "y": 110, "color": "#ffba01", "tooltip": "110T Phosphorylation
(PhosphoSitePlus)", "stroke": "black"}, {"x": 131, "y": 131, "color": "#ffba01", "tooltip": "131S Phosphorylation
(PhosphoSitePlus/iPTMNet)", "stroke": "black"}, {"x": 141, "y": 141, "color": "#ffba01", "tooltip": "141T Phosphorylation
(PhosphoSitePlus/iPTMNet)", "stroke": "black"}, {"x": 143, "y": 143, "color": "#ffba01", "tooltip": "143T Phosphorylation
(PhosphoSitePlus/iPTMNet)", "stroke": "black"}, {"x": 128, "y": 128, "color": "#ffba01", "tooltip": "128S Phosphorylation
(PhosphoSitePlus/iPTMNet)", "stroke": "black"}, {"x": 127, "y": 127, "color": "#ffba01", "tooltip": "127S Phosphorylation
(PhosphoSitePlus/iPTMNet)", "stroke": "black"}, {"x": 119, "y": 119, "color": "#ffba01", "tooltip": "119T Phosphorylation
(PhosphoSitePlus/iPTMNet)", "stroke": "black"}, {"x": 114, "y": 114, "color": "#ffba01", "tooltip": "114T Phosphorylation
(PhosphoSitePlus)", "stroke": "black"}, {"x": 149, "y": 149, "color": "#ffba01", "tooltip": "149S Phosphorylation
(PhosphoSitePlus/iPTMNet)", "stroke": "black"}, {"x": 154, "y": 154, "color": "#ffba01", "tooltip": "154T Phosphorylation
(PhosphoSitePlus/iPTMNet)", "stroke": "black"}, {"x": 145, "y": 145, "color": "#ffba01", "tooltip": "145T Phosphorylation
(PhosphoSitePlus)", "stroke": "black"}, {"x": 138, "y": 138, "color": "#ffba01", "tooltip": "138S Phosphorylation
(PhosphoSitePlus/iPTMNet)", "stroke": "black"}, {"x": 41, "y": 41, "color": "#ffba01", "tooltip": "41T Phosphorylation
(PhosphoSitePlus)", "stroke": "black"}, {"x": 367, "y": 367, "color": "#ffba01", "tooltip": "367S Phosphorylation
(PhosphoSitePlus/iPTMNet)", "stroke": "black"}, {"x": 381, "y": 381, "color": "#ffba01", "tooltip": "381S Phosphorylation
(PhosphoSitePlus/iPTMNet)", "stroke": "black"}, {"x": 382, "y": 382, "color": "#ffba01", "tooltip": "382S Phosphorylation
(PhosphoSitePlus/iPTMNet)", "stroke": "black"}, {"x": 371, "y": 371, "color": "#ffba01", "tooltip": "371S Phosphorylation
(PhosphoSitePlus/iPTMNet)", "stroke": "black"}, {"x": 61, "y": 61, "color": "#ffba01", "tooltip": "61S Phosphorylation
(PhosphoSitePlus/iPTMNet)", "stroke": "black"}, {"x": 390, "y": 390, "color": "#ffba01", "tooltip": "390T Phosphorylation
(PhosphoSitePlus)", "stroke": "black"}, {"x": 391, "y": 391, "color": "#ffba01", "tooltip": "391Y Phosphorylation
(PhosphoSitePlus)", "stroke": "black"}, {"x": 397, "y": 397, "color": "#ffba01", "tooltip": "397S Phosphorylation
(PhosphoSitePlus/iPTMNet)", "stroke": "black"}, {"x": 388, "y": 388, "color": "#ffba01", "tooltip": "388S Phosphorylation
(PhosphoSitePlus/iPTMNet)", "stroke": "black"}], 88 | }, 89 | { 90 | type: 'lollipop', 91 | id: 'Other', 92 | label: 'Other PTMs', 93 | color:'grey', 94 | data: [{"x": 109, "y": 109, "color": "#F4B5C7", "tooltip": "109S O-glycosylation
(PhosphoSitePlus)", "stroke": "black"}, {"x": 241, "y": 241, "color": "#F4B5C7", "tooltip": "241T O-glycosylation
(PhosphoSitePlus)", "stroke": "black"}, {"x": 497, "y": 497, "color": "#FF6961", "tooltip": "497K Methylation
(PhosphoSitePlus)", "stroke": "black"}, {"x": 494, "y": 494, "color": "#FF6961", "tooltip": "494K Methylation
(PhosphoSitePlus)", "stroke": "black"}, {"x": 97, "y": 97, "color": "#e83484", "tooltip": "97K Sumoylation
(PhosphoSitePlus)", "stroke": "black"}, {"x": 280, "y": 280, "color": "#86CBED", "tooltip": "280K Sumoylation
(PhosphoSitePlus)", "stroke": "black"}, {"x": 315, "y": 315, "color": "#A7E99C", "tooltip": "315K Acetylation
(PhosphoSitePlus)", "stroke": "black"}], 95 | }, 96 | { 97 | type: 'circle', 98 | id: 'Missense', 99 | label: 'Missense mutations', 100 | color:'grey', 101 | data: [{"x": 132, "y": 0.1, "color": "#00008b", "tooltip": "L132R", "stroke": "black"}, {"x": 180, "y": 0.1, "color": "#00008b", "tooltip": "A180T", "stroke": "black"}, {"x": 354, "y": 0.1, "color": "#00008b", "tooltip": "T354A", "stroke": "black"}, {"x": 456, "y": 0.1, "color": "#00008b", "tooltip": "G456E", "stroke": "black"}, {"x": 119, "y": 0.1, "color": "#00008b", "tooltip": "T119S", "stroke": "black"}, {"x": 133, "y": 0.1, "color": "#00008b", "tooltip": "Q133H", "stroke": "black"}, {"x": 501, "y": 0.1, "color": "#00008b", "tooltip": "L501I", "stroke": "black"}, {"x": 209, "y": 0.2, "color": "#00008b", "tooltip": "Q209L Q209H", "stroke": "black"}, {"x": 390, "y": 0.1, "color": "#00008b", "tooltip": "T390I", "stroke": "black"}, {"x": 436, "y": 0.1, "color": "#00008b", "tooltip": "S436T", "stroke": "black"}, {"x": 383, "y": 0.2, "color": "#00008b", "tooltip": "D383H D383G", "stroke": "black"}, {"x": 279, "y": 0.1, "color": "#00008b", "tooltip": "V279L", "stroke": "black"}, {"x": 127, "y": 0.1, "color": "#00008b", "tooltip": "S127F", "stroke": "black"}, {"x": 396, "y": 0.1, "color": "#00008b", "tooltip": "E396K", "stroke": "black"}, {"x": 478, "y": 0.1, "color": "#00008b", "tooltip": "L478F", "stroke": "black"}, {"x": 459, "y": 0.1, "color": "#00008b", "tooltip": "E459K", "stroke": "black"}, {"x": 399, "y": 0.2, "color": "#00008b", "tooltip": "D399G D399H", "stroke": "black"}, {"x": 488, "y": 0.1, "color": "#00008b", "tooltip": "S488Y", "stroke": "black"}, {"x": 124, "y": 0.2, "color": "#00008b", "tooltip": "R124P R124Q", "stroke": "black"}, {"x": 371, "y": 0.1, "color": "#00008b", "tooltip": "S371F", "stroke": "black"}, {"x": 145, "y": 0.1, "color": "#00008b", "tooltip": "T145S", "stroke": "black"}, {"x": 106, "y": 0.1, "color": "#00008b", "tooltip": "R106G", "stroke": "black"}, {"x": 225, "y": 0.1, "color": "#00008b", "tooltip": "M225I", "stroke": "black"}, {"x": 151, "y": 0.1, "color": "#00008b", "tooltip": "P151A", "stroke": "black"}, {"x": 61, "y": 0.1, "color": "#00008b", "tooltip": "S61W", "stroke": "black"}, {"x": 231, "y": 0.1, "color": "#00008b", "tooltip": "P231S", "stroke": "black"}, {"x": 367, "y": 0.1, "color": "#00008b", "tooltip": "S367F", "stroke": "black"}, {"x": 429, "y": 0.1, "color": "#00008b", "tooltip": "I429L", "stroke": "black"}, {"x": 167, "y": 0.1, "color": "#00008b", "tooltip": "I167T", "stroke": "black"}, {"x": 306, "y": 0.1, "color": "#00008b", "tooltip": "M306I", "stroke": "black"}, {"x": 427, "y": 0.1, "color": "#00008b", "tooltip": "D427H", "stroke": "black"}, {"x": 210, "y": 0.1, "color": "#00008b", "tooltip": "M210I", "stroke": "black"}, {"x": 73, "y": 0.1, "color": "#00008b", "tooltip": "M73I", "stroke": "black"}, {"x": 460, "y": 0.1, "color": "#00008b", "tooltip": "G460E", "stroke": "black"}, {"x": 481, "y": 0.1, "color": "#00008b", "tooltip": "D481H", "stroke": "black"}, {"x": 234, "y": 0.1, "color": "#00008b", "tooltip": "D234H", "stroke": "black"}, {"x": 319, "y": 0.1, "color": "#00008b", "tooltip": "R319Q", "stroke": "black"}, {"x": 162, "y": 0.1, "color": "#00008b", "tooltip": "Q162R", "stroke": "black"}, {"x": 125, "y": 0.1, "color": "#00008b", "tooltip": "A125G", "stroke": "black"}, {"x": 266, "y": 0.1, "color": "#00008b", "tooltip": "R266C", "stroke": "black"}, {"x": 166, "y": 0.1, "color": "#00008b", "tooltip": "E166G", "stroke": "black"}, {"x": 246, "y": 0.1, "color": "#00008b", "tooltip": "I246M", "stroke": "black"}], 102 | }, 103 | ]); 104 | }; 105 | -------------------------------------------------------------------------------- /icons/icons8-chevron-down-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BioComputingUP/FeatureViewerTypeScript/46d7be45262f6e8ecad0679ed094130e831f7d1e/icons/icons8-chevron-down-48.png -------------------------------------------------------------------------------- /icons/icons8-chevron-right-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BioComputingUP/FeatureViewerTypeScript/46d7be45262f6e8ecad0679ed094130e831f7d1e/icons/icons8-chevron-right-48.png -------------------------------------------------------------------------------- /icons/icons8-download-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BioComputingUP/FeatureViewerTypeScript/46d7be45262f6e8ecad0679ed094130e831f7d1e/icons/icons8-download-48.png -------------------------------------------------------------------------------- /icons/icons8-help-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BioComputingUP/FeatureViewerTypeScript/46d7be45262f6e8ecad0679ed094130e831f7d1e/icons/icons8-help-48.png -------------------------------------------------------------------------------- /icons/icons8-level-up-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BioComputingUP/FeatureViewerTypeScript/46d7be45262f6e8ecad0679ed094130e831f7d1e/icons/icons8-level-up-48.png -------------------------------------------------------------------------------- /index.ts: -------------------------------------------------------------------------------- 1 | // Just export feature viewer 2 | export * from './src/feature-viewer'; 3 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "feature-viewer-typescript", 3 | "version": "2.3.20", 4 | "description": "Feature Viewer (Typescript)", 5 | "main": "./dist/index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "build": "webpack --mode=production --node-env=production", 9 | "build:dev": "webpack --mode=development", 10 | "build:prod": "webpack --mode=production --node-env=production", 11 | "prepare": "echo \"Building...\" && npm run build", 12 | "watch": "webpack --watch", 13 | "serve": "webpack serve" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://github.com/BioComputingUP/FeatureViewerTypeScript.git" 18 | }, 19 | "keywords": [], 20 | "author": "Martina Bevilacqua, Lisanna Paladin and collaborators", 21 | "license": "ISC", 22 | "bugs": { 23 | "url": "https://github.com/BioComputingUP/FeatureViewerTypeScript/issues" 24 | }, 25 | "homepage": "https://github.com/BioComputingUP/FeatureViewerTypeScript#readme", 26 | "dependencies": { 27 | "@types/d3-axis": "1.0.12", 28 | "@types/d3-brush": "1.0.6", 29 | "@types/d3-scale": "2.1.1", 30 | "@types/d3-selection": "1.4.1", 31 | "@types/d3-shape": "1.3.1", 32 | "@types/d3-transition": "^1.3.2", 33 | "@types/underscore": "1.8.16", 34 | "d3-axis": "^1.0.12", 35 | "d3-brush": "^1.0.6", 36 | "d3-scale": "2.2.2", 37 | "d3-selection": "1.4.0", 38 | "d3-shape": "1.3.5", 39 | "underscore": "^1.9.1" 40 | }, 41 | "devDependencies": { 42 | "@types/node": "^18.7.23", 43 | "@webpack-cli/generators": "^2.5.0", 44 | "css-loader": "^6.7.1", 45 | "html-webpack-plugin": "^5.5.0", 46 | "mini-css-extract-plugin": "^2.6.1", 47 | "prettier": "^2.7.1", 48 | "sass": "^1.55.0", 49 | "sass-loader": "^13.0.2", 50 | "ts-loader": "^9.4.1", 51 | "typescript": "^4.8.4", 52 | "webpack": "^5.74.0", 53 | "webpack-cli": "^4.10.0", 54 | "webpack-dev-server": "^4.9.3" 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/calculate.ts: -------------------------------------------------------------------------------- 1 | import Commons from './commons'; 2 | import {FeaturesList} from "./interfaces"; 3 | import * as _ from "underscore"; 4 | 5 | const commons = new Commons(); 6 | 7 | class Calculate { 8 | 9 | public commons; 10 | 11 | private uniq(a) { 12 | return a.sort().filter(function(item, pos, ary) { 13 | return !pos || item != ary[pos - 1]; 14 | }) 15 | }; 16 | 17 | private orderBy(values: any[], orderType: any) { 18 | return values.sort((a, b) => { 19 | if (a[orderType] < b[orderType]) { 20 | return -1; 21 | } 22 | if (a[orderType] > b[orderType]) { 23 | return 1; 24 | } 25 | return 0 26 | }); 27 | } 28 | 29 | public yxPoints(d) { 30 | this.commons.flagsHeight = 18; 31 | let h = d.y + this.commons.flagsHeight; 32 | let htail = d.y + (this.commons.flagsHeight / 2) 33 | let w = (this.commons.viewerOptions.margin.left - 15) - (20 * (d.flagLevel-1)); 34 | let wtail = (this.commons.viewerOptions.margin.left - 7) - (20 * (d.flagLevel-1)); 35 | let poligon = [5, (d.y - 3), (5), (h), (w), (h), (wtail), (htail), (w), (d.y - 3)].join(',') 36 | return poligon; 37 | //return "5,57,5,78,25,78,33,69,25,57"; 38 | }; 39 | 40 | public getTransf(el) { 41 | return [el.transform.baseVal.getItem(0).matrix.e, el.transform.baseVal.getItem(0).matrix.f] 42 | }; 43 | 44 | public getMarginLeft() { 45 | let flagwidht = this.commons.yAxisSVG.select(".flagBackground").node() 46 | let marginleft = (flagwidht).getBoundingClientRect().width; 47 | return marginleft 48 | } 49 | 50 | public addNLines(array) { 51 | 52 | let dataLevels = []; 53 | 54 | // sort array 55 | array = this.orderBy(array, 'x'); 56 | array.forEach((d) => { 57 | 58 | // init 59 | if (dataLevels.length === 0) { 60 | 61 | dataLevels[0] = [d]; 62 | d.level = 0; 63 | 64 | } else { 65 | 66 | let pulled = false; 67 | for (let lv in dataLevels) { 68 | // check superimposition, compare with last 69 | let last = dataLevels[lv].length - 1; 70 | if (d.x > dataLevels[lv][last].y) { 71 | // same level 72 | dataLevels[lv].push(d); 73 | d.level = lv; 74 | pulled = true; 75 | break; 76 | } 77 | } 78 | if (!pulled) { 79 | let newlv = dataLevels.length 80 | dataLevels[newlv] = [d]; 81 | d.level = newlv; 82 | }; 83 | 84 | } 85 | 86 | }); 87 | return dataLevels.length; 88 | } 89 | 90 | public getHeightRect(level) { 91 | return (level-1) * 20 + 15; 92 | }; 93 | 94 | public searchTree(element, matchingId){ 95 | if (element.id == matchingId) { 96 | return element; 97 | } else if (element.subfeatures) { 98 | var i; 99 | var result = null; 100 | for (i = 0; result == null && i < element.subfeatures.length; i++) { 101 | result = this.searchTree(element.subfeatures[i], matchingId); 102 | } 103 | return result; 104 | } 105 | return null; 106 | } 107 | 108 | public unflatten = function( array: FeaturesList, parent=null, processedIds=null, tree=null ){ 109 | 110 | tree = tree !== null ? tree : []; 111 | parent = parent !== null ? parent : { id: 0 }; 112 | processedIds = processedIds !== null ? processedIds : new Set(); 113 | 114 | var children = _.filter( array, (child) => { 115 | if (child.parentId === undefined) { 116 | child.parentId = 0 117 | } 118 | if (child.parentId === parent.id) { 119 | processedIds.add(child.id) 120 | } 121 | return child.parentId == parent.id; 122 | }); 123 | 124 | if( !_.isEmpty( children ) ){ 125 | if( parent.id == 0 ){ 126 | tree = children; 127 | }else{ 128 | parent['subfeatures'] = children 129 | } 130 | _.each( children, ( child ) => { this.unflatten( array, child, processedIds ) } ); 131 | } 132 | 133 | let res = { 134 | tree:tree, 135 | ids:processedIds 136 | } 137 | 138 | return res; 139 | } 140 | 141 | public flatten(features, flatted=[], parent=null) { 142 | for (let i in features) { 143 | let ft = features[i]; 144 | if (!parent) { ft.parent = []; } else { 145 | if (ft.parent) {ft.parent.concat(parent)} else {ft.parent = parent} 146 | } 147 | flatted.push(ft); 148 | if (ft.subfeatures) { 149 | this.flatten(ft.subfeatures, flatted=flatted, parent=ft.parent.concat(ft.id)) 150 | } 151 | } 152 | return flatted 153 | } 154 | 155 | public displaySequence (seq) { 156 | // checks if dotted sequence or letters 157 | return this.commons.viewerOptions.width / seq > 8; // regulate sequence letter distance 158 | }; 159 | 160 | public updateSVGHeight(position) { 161 | this.commons.svg.attr("height", position + 60 + "px"); 162 | this.commons.svg.select("clipPath rect").attr("height", position + 60 + "px"); 163 | }; 164 | 165 | constructor(commons: {}) { 166 | this.commons = commons; 167 | } 168 | } 169 | 170 | export default Calculate 171 | -------------------------------------------------------------------------------- /src/commons.ts: -------------------------------------------------------------------------------- 1 | import { FeatureObject, ViewerOptions, FeaturesList, FeatureData, FeatureViewerLog } from './interfaces'; 2 | 3 | // let commons: Commons; 4 | 5 | type MyType = {}; 6 | 7 | class Commons { 8 | public fvLength: number; 9 | public events = { 10 | FEATURE_SELECTED_EVENT: "feature-viewer-position-selected", 11 | CLEAR_SELECTION_EVENT: "feature-viewer-clear-selection", 12 | FLAG_SELECTED_EVENT: "feature-viewer-flag-selected", 13 | FLAG_SELECTED_INTERRUPTED_EVENT: "feature-viewer-flag-interrupted-event", 14 | TAG_SELECTED_EVENT: "feature-viewer-button-selected", 15 | ZOOM_EVENT: "feature-viewer-zoom-altered", 16 | ANIMATIONS_FALSE_EVENT: "feature-viewer-animations-off" 17 | }; 18 | public viewerOptions: ViewerOptions = { 19 | showSequence: true, 20 | showSequenceLabel: true, 21 | brushActive: false, 22 | verticalLine: false, 23 | dottedSequence: true, 24 | showHelper: false, 25 | flagColor: "#DFD5D3", 26 | tooltipColor: '#fff', 27 | showSubFeatures: true, 28 | sideBar: true, 29 | labelTrackWidth: 0, 30 | labelTrackWidthMobile: 0, 31 | tagsTrackWidth: 0, 32 | mobileMode: false, 33 | margin: { 34 | top: 10, 35 | bottom: 20, 36 | left: 0, 37 | right: 0 38 | }, 39 | backup: { 40 | labelTrackWidth: 0, 41 | tagsTrackWidth: 0, 42 | features: [] 43 | }, 44 | width: null, 45 | height: null, 46 | zoomMax: 10, 47 | unit: null, 48 | maxDepth: 3, 49 | animation: true, 50 | toolbar: true, 51 | bubbleHelp: true, 52 | showAxis: true, 53 | positionWithoutLetter: null, 54 | drawLadder: true, 55 | ladderWidth: 15, 56 | ladderSpacing: 10, 57 | ladderHeight: 18, 58 | }; 59 | public mobilesize = 951; 60 | public radius = 5; 61 | public flagsHeight = 18; 62 | public maxNumberOfButtons = 0; 63 | public features: FeaturesList; 64 | public yData: any; 65 | public seqShift = 1; 66 | public scalingPosition: any; 67 | public lineYScale: any; 68 | public featureSelected: any; 69 | public flagSelected = []; 70 | public animation = true; 71 | public trigger: any; 72 | public level = 0; 73 | public svgElement: any; 74 | public svg: any; 75 | public svgContainer: any; 76 | public tagsContainer: any; 77 | public tooltipDiv: any; 78 | public customTooltipDiv: any; 79 | public divId: string; 80 | public right_chevron: any; 81 | public down_chevron: any; 82 | public yAxisSVG: any; 83 | public yAxisSVGGroup: any; 84 | public xAxis: any; 85 | public line: any; 86 | public lineBond: any; 87 | public brush: any; 88 | public extent: any; 89 | public pathLevel = 0; 90 | public step: number = 30; // or 20 91 | public elementHeight: number = Math.floor(this.step / 2); // or 1.5 if step is 20 92 | public YPosition: number = this.step; 93 | public scaling: any; 94 | public lineGen: any; 95 | public headMargin: number; 96 | public stringSequence : string; 97 | public calculatedTransitions: any={} 98 | public d3helper: any={}; 99 | public style: any; 100 | public logger: any; 101 | public backgroundcolor: string; 102 | public currentposition: number; 103 | public currentzoom: number; 104 | 105 | private _current_extend = null; 106 | 107 | public get current_extend() { 108 | let extend = this._current_extend; 109 | this._current_extend = null; 110 | return extend || { 111 | length: this.viewerOptions.offset.end - this.viewerOptions.offset.start, 112 | start: this.viewerOptions.offset.start, 113 | end: this.viewerOptions.offset.end 114 | } 115 | }; 116 | 117 | public set current_extend(current_extend) { 118 | this._current_extend = current_extend; 119 | } 120 | 121 | /*constructor() { 122 | if (!commons) { 123 | commons = this; 124 | } 125 | return commons 126 | }*/ 127 | constructor() { 128 | } 129 | } 130 | 131 | export default Commons; 132 | -------------------------------------------------------------------------------- /src/custom-d3.ts: -------------------------------------------------------------------------------- 1 | export * from 'd3-selection'; 2 | export * from 'd3-transition'; 3 | export * from 'd3-scale'; 4 | export * from 'd3-shape'; 5 | export * from 'd3-axis'; 6 | export * from 'd3-brush'; 7 | -------------------------------------------------------------------------------- /src/feature-viewer.ts: -------------------------------------------------------------------------------- 1 | import * as d3 from './custom-d3' 2 | import {event as currentEvent} from 'd3-selection'; 3 | import * as _ from 'underscore'; 4 | import {FeatureObject, FeaturesList, FeatureViewerLog, UserOptions} from './interfaces'; 5 | import Commons from './commons'; 6 | import {SubfeaturesTransition, Transition} from "./transition"; 7 | import FillSVG from "./fillsvg"; 8 | import Calculate from "./calculate"; 9 | import Tool from "./tooltip"; 10 | // import * as htmlToImage from 'html-to-image'; 11 | // import { toPng, toJpeg, toBlob, toPixelData, toSvg } from 'html-to-image'; 12 | 13 | // import * as fvstyles from './../assets/fv.scss'; 14 | 15 | // Import styles 16 | import './styles/styles.scss'; 17 | 18 | class FeatureViewer { 19 | private commons: Commons; 20 | private divId: string; 21 | private sequence: string; 22 | 23 | private transition: Transition; 24 | private subfeaturesTransition: SubfeaturesTransition; 25 | private fillSVG: FillSVG; 26 | private calculate: Calculate; 27 | private tool: Tool; 28 | private lastHighlight: any; 29 | private lastLen: number; 30 | 31 | 32 | private parseUserOptions(options: UserOptions): void { 33 | 34 | const simple_keys = [ 35 | 'showAxis', 36 | 'showSequence', 37 | 'showSequenceLabel', 38 | 'brushActive', 39 | 'toolbar', 40 | 'toolbarPosition', 41 | 'zoomMax', 42 | 'showSubFeatures', 43 | 'flagColor', 44 | 'flagTrack', 45 | 'flagTrackMobile', 46 | 'breakpoint', 47 | 'sideBar', 48 | 'unit', 49 | 'backgroundcolor' 50 | ]; 51 | // and: breakpoint, offset 52 | 53 | for (let key of simple_keys) { 54 | if (options && key in options) { 55 | this.commons.viewerOptions[key] = options[key]; 56 | } 57 | } 58 | 59 | if (options.maxDepth) { 60 | this.commons.viewerOptions.maxDepth = options.maxDepth; 61 | } 62 | this.commons.viewerOptions.maxDepth ++ 63 | 64 | // when to mobile mode 65 | if (options.breakpoint) { 66 | if (typeof options.breakpoint === 'string') { 67 | this.commons.mobilesize = Number(options.breakpoint); 68 | } else if (typeof options.breakpoint === 'number') { 69 | this.commons.mobilesize = options.breakpoint; 70 | } else if (typeof options.breakpoint === 'boolean') { 71 | this.commons.mobilesize = 951; 72 | } 73 | } 74 | 75 | // if zoom specified 76 | this.commons.viewerOptions.offset = {start: 0, end: this.commons.fvLength + 1}; 77 | if (options && options.offset) { 78 | this.commons.viewerOptions.offset = options.offset; 79 | if (options.offset.start < 1) { 80 | this.commons.viewerOptions.offset.start = 1; 81 | this.commons.logger.warn("Offset.start should be > 0. Thus, it has been reset to 1.", {fvId:this.divId}); 82 | } 83 | } 84 | 85 | // brush active 86 | this.commons.viewerOptions.brushActive = options.brushActive? options.brushActive: true; 87 | 88 | // set width of sidebar 89 | this.commons.viewerOptions.tagsTrackWidth = 0; 90 | if (options && options.sideBar) { 91 | if (typeof options.sideBar === 'string') { 92 | this.commons.viewerOptions.tagsTrackWidth = Number(options.sideBar.match(/[0-9]+/g)[0]); 93 | } else if (typeof options.sideBar === 'number') { 94 | this.commons.viewerOptions.tagsTrackWidth = options.sideBar; 95 | } else if (typeof options.sideBar === 'boolean') { 96 | if (options.sideBar) { 97 | this.commons.viewerOptions.tagsTrackWidth = 100; 98 | } else { 99 | this.commons.viewerOptions.tagsTrackWidth = 0; 100 | } 101 | } else { 102 | this.commons.viewerOptions.tagsTrackWidth = 100; 103 | this.commons.logger.warn(`Automatically set tagsTrackWidth to ${this.commons.viewerOptions.tagsTrackWidth}`, {fvId:this.divId}); 104 | } 105 | } 106 | this.commons.viewerOptions.backup.tagsTrackWidth = this.commons.viewerOptions.tagsTrackWidth; 107 | 108 | // set width of flags 109 | this.commons.viewerOptions.labelTrackWidth = 200; 110 | if (options && options.flagTrack) { 111 | if (typeof options.flagTrack === 'string') { 112 | this.commons.viewerOptions.labelTrackWidth = Number(options.flagTrack.match(/[0-9]+/g)[0]); 113 | } else if (typeof options.flagTrack === 'number') { 114 | this.commons.viewerOptions.labelTrackWidth = options.flagTrack; 115 | } else if (typeof options.flagTrack === 'boolean') { 116 | this.commons.viewerOptions.labelTrackWidth = options.flagTrack ? 200 : 0; 117 | } else { 118 | this.commons.viewerOptions.labelTrackWidth = 200; 119 | this.commons.logger.warn(`Automatically set tagsTrackWidth to ${this.commons.viewerOptions.tagsTrackWidth}`, {fvId:this.divId}); 120 | } 121 | } 122 | this.commons.viewerOptions.backup.labelTrackWidth = this.commons.viewerOptions.labelTrackWidth; 123 | 124 | // set width of flags when mobile 125 | this.commons.viewerOptions.labelTrackWidthMobile = 30; 126 | if (options && options.flagTrackMobile) { 127 | if (typeof options.flagTrackMobile === 'string') { 128 | this.commons.viewerOptions.labelTrackWidthMobile = Number(options.flagTrackMobile.match(/[0-9]+/g)[0]); 129 | } else if (typeof options.flagTrackMobile === 'number') { 130 | this.commons.viewerOptions.labelTrackWidthMobile = options.flagTrackMobile; 131 | } else if (typeof options.flagTrackMobile === 'boolean') { 132 | this.commons.viewerOptions.labelTrackWidthMobile = options.flagTrackMobile ? 30 : 0; 133 | } else { 134 | this.commons.viewerOptions.labelTrackWidthMobile = 30; 135 | this.commons.logger.warn(`Automatically set tagsTrackWidth to ${this.commons.viewerOptions.tagsTrackWidth}`, {fvId:this.divId}); 136 | } 137 | } 138 | 139 | // set margins 140 | this.commons.viewerOptions.margin = { 141 | top: 10, 142 | bottom: 20, 143 | left: this.commons.viewerOptions.labelTrackWidth, 144 | right: this.commons.viewerOptions.tagsTrackWidth 145 | }; 146 | 147 | // resize if width < 480, initial view 148 | let myd3node = d3.select(`#${this.divId}`).node(); 149 | if (myd3node) { 150 | let totalwidth = (myd3node).getBoundingClientRect().width; 151 | if (totalwidth < this.commons.mobilesize) { 152 | this.commons.viewerOptions.mobileMode = true; 153 | this.commons.viewerOptions.margin.left = this.commons.viewerOptions.labelTrackWidthMobile; 154 | if (this.commons.viewerOptions.tagsTrackWidth !== 0) { 155 | this.commons.viewerOptions.margin.right = 80 156 | } 157 | } 158 | } 159 | 160 | let myvod3node = d3.select(`#${this.divId}`).node(); 161 | if (myvod3node) { 162 | this.commons.viewerOptions.width = (myvod3node).getBoundingClientRect().width; 163 | this.commons.viewerOptions.height = 600 - this.commons.viewerOptions.margin.top - this.commons.viewerOptions.margin.bottom; 164 | } 165 | // backgroundcolor 166 | this.commons.backgroundcolor = options.backgroundcolor? options.backgroundcolor : "white"; 167 | 168 | }; 169 | 170 | // Y Axis, responsive to click and triggers modifications in feature list 171 | private addYAxis() { 172 | // flags box 173 | this.commons.yAxisSVG = this.commons.svg.append("g") 174 | .attr("class", "pro axis") 175 | .attr("transform", "translate(0," + this.commons.viewerOptions.margin.top + ")"); 176 | this.commons.yAxisSVG.append("rect") 177 | .attr("width", this.commons.viewerOptions.margin.left) 178 | .attr("class", "flagBackground") 179 | .attr("height", "100%") 180 | .attr("fill", this.commons.backgroundcolor) 181 | .attr("fill-opacity", 1); 182 | this.updateYAxis(); 183 | 184 | }; 185 | 186 | private updateYAxis() { 187 | // create g 188 | this.commons.yAxisSVGGroup = this.commons.yAxisSVG 189 | .selectAll(".yAxis") 190 | .data(this.commons.yData) 191 | .enter() 192 | .append("g") 193 | .attr("id", function (d) { 194 | // return divId + '_' + d.title.split(" ").join("_") + '_g' 195 | if (d.title === "Sequence") { 196 | return 'sequence' 197 | } else { 198 | return d.id 199 | } 200 | }) 201 | .attr("class", (d) => { 202 | if (this.commons.viewerOptions.showSubFeatures && d.hasSubFeatures) { 203 | return "flag withsubfeatures" 204 | } else { 205 | return "flag" 206 | } 207 | }) 208 | .on('click', (d) => { 209 | // if (this.commons.viewerOptions.showSubFeatures && d.hasSubFeatures) { 210 | // this.clickFlag(d) 211 | // } 212 | this.clickFlag(d) 213 | }) // change color feature when mouseover 214 | .on('mouseover', (d) => { 215 | if (this.commons.viewerOptions.showSubFeatures && d.hasSubFeatures) { 216 | d3.select(`#${this.divId}`).select(`#${d.id}`).selectAll(".Arrow").style("fill-opacity", 0.9); 217 | } 218 | }) 219 | .on('mouseout', (d) => { 220 | if (this.commons.viewerOptions.showSubFeatures && d.hasSubFeatures) { 221 | d3.select(`#${this.divId}`).select(`#${d.id}`).selectAll(".Arrow").style("fill-opacity", 0.6); 222 | } 223 | }) 224 | .call(this.commons.d3helper.flagTooltip()); 225 | 226 | // create polygon 227 | this.commons.yAxisSVGGroup 228 | .append("polygon") // attach a polygon 229 | .attr("class", (d) => { 230 | if (this.commons.viewerOptions.showSubFeatures && d.hasSubFeatures) { 231 | return "boxshadow Arrow withsubfeatures" 232 | } else { 233 | return "boxshadow Arrow" 234 | } 235 | }) 236 | .style("stroke", (d) => { 237 | return d.flagColor ? d.flagColor : this.commons.viewerOptions.flagColor; 238 | }) // colour the border if selected 239 | .attr("points", (d) => { 240 | if (d.ladderLabel == null) { 241 | // match points with subFeature level 242 | return this.calculate.yxPoints(d) 243 | } 244 | }) 245 | .attr("transform", (d) => { 246 | let y = 0 247 | if (d.id == 'mycurve' || d.id == 'useUniqueId') { y = 5 } 248 | return "translate(" + (20 * (d.flagLevel - 1)) + ", "+ y + ")" 249 | }) 250 | .style("fill", (d) => { 251 | return d.flagColor ? d.flagColor : this.commons.viewerOptions.flagColor; 252 | }); 253 | 254 | // foreingObject for chevron 255 | 256 | this.commons.yAxisSVGGroup 257 | .append("g") // position 258 | .attr("transform", (d) => { 259 | let x = 0; 260 | // horizontal flag placement 261 | this.commons.headMargin = 0;//20 262 | if (d.flagLevel) { 263 | this.commons.headMargin = 0 * (d.flagLevel - 1);//20* 264 | // x = this.commons.headMargin + 5;// uncommented 265 | } 266 | // vertical flag placement 267 | let y = d.y; 268 | return "translate(" + x + "," + y + ")" 269 | 270 | }) 271 | .append("path") 272 | .attr("id", "chevron") 273 | .attr("class", (d) => { 274 | if (this.commons.viewerOptions.showSubFeatures && d.hasSubFeatures) { 275 | return "chevron withsubfeatures" 276 | } else { 277 | return "chevron" 278 | } 279 | }) 280 | .attr("fill", "rgba(39, 37, 37, 0.71)") 281 | .attr("d", (d) => { 282 | if (this.commons.viewerOptions.showSubFeatures && d.hasSubFeatures) { 283 | if (d.isOpen) {return this.commons.down_chevron} else {return this.commons.right_chevron} 284 | } else { 285 | return '' 286 | } 287 | }); 288 | 289 | 290 | 291 | // text 292 | this.commons.yAxisSVGGroup 293 | .append("foreignObject") 294 | // text 295 | .attr("class", (d) => { 296 | if (this.commons.viewerOptions.showSubFeatures && d.hasSubFeatures) { 297 | return "yAxis withsubfeatures" 298 | } else { return "yAxis" } 299 | }) 300 | .attr("x", (d) => { 301 | let cvm = 0; 302 | if (this.commons.viewerOptions.showSubFeatures && d.hasSubFeatures) { 303 | // chevron margin, placed rightly 304 | cvm = 17 305 | } 306 | // horizontal flag placement 307 | this.commons.headMargin = 20; 308 | if (d.ladderLabel == null) { 309 | if (d.flagLevel) { 310 | this.commons.headMargin = 20 * (d.flagLevel - 1); 311 | return cvm + this.commons.headMargin + 8; 312 | } else { 313 | return cvm + 8 314 | } 315 | } 316 | }) 317 | .attr("y", d => { 318 | // vertical flag placement 319 | let y = d.y + this.commons.step/6 320 | if (d.id == 'mycircle') { y = y - 5} 321 | return y 322 | }) 323 | .attr('font-size', '.8125rem') 324 | .attr("width", (d) => { 325 | // text only if space is enough 326 | if (this.commons.viewerOptions.mobileMode) { 327 | return this.calcFlagWidth(d); 328 | } else { 329 | let margin = 20 + this.commons.viewerOptions.ladderSpacing * this.commons.viewerOptions.maxDepth // 20 + (20 * d.flagLevel) --> 0 330 | return this.commons.viewerOptions.margin.left - margin; // chevron margin and text indent 331 | } 332 | }) 333 | .attr("height", this.commons.step) 334 | .html((d) => { 335 | return d.label; 336 | }); 337 | 338 | 339 | const ladderGroup = this.commons.yAxisSVGGroup 340 | .selectAll('.ladder') 341 | .data((e) => { 342 | return [...Array(e.flagLevel)].map((obj, x) => [x, e]) 343 | }) 344 | .enter() 345 | .append('g').attr('id', 'ladder-group') 346 | 347 | // ladderGroup.append('rect') 348 | // ladderGroup.append('path').attr('d', ([i, d]) => { 349 | // return this.roundedRect(0, 0, 16, 20, 5 , false, true, true, false); 350 | // }) 351 | 352 | // badges code, removing this will remove badges 353 | ladderGroup.append('foreignObject') 354 | .attr("width", "30px") 355 | .attr("height", "30px") 356 | .html(([i, d]) => { 357 | if (d.ladderLabel) { 358 | return (d.id !== 'fv_sequence' && i===d.flagLevel-1) ? 359 | `
${d.ladderLabel}
`: ''} 361 | } 362 | ) 363 | 364 | ladderGroup.attr('transform', ([i, d]) => { 365 | if (d.ladderLabel) { 366 | const margin = (this.commons.viewerOptions.margin.left + (i - 1) * this.commons.viewerOptions.ladderSpacing); 367 | const ladderWidth = this.commons.viewerOptions.ladderSpacing * this.commons.viewerOptions.maxDepth; 368 | const x = margin - ladderWidth; 369 | const y = (d.y + this.commons.step / 6); 370 | return "translate(" + x + "," + y + ")" 371 | } 372 | }) 373 | 374 | 375 | .attr('width', this.commons.viewerOptions.ladderWidth).attr('height', this.commons.viewerOptions.ladderHeight) 376 | .attr('fill', ([i, d]) => { 377 | return (d.id !== 'fv_sequence' && i===d.flagLevel-1) ? d.ladderColor : 'rgba(255, 255, 255, 0)' 378 | }) 379 | .attr('class', d => `ladder`) 380 | .attr('rx', 0) 381 | .attr('ry', 2) 382 | 383 | ladderGroup.append('text') 384 | .attr('transform', ([i, d]) => { 385 | if (d.ladderLabel) { 386 | const margin = (this.commons.viewerOptions.margin.left + (i - 1) * this.commons.viewerOptions.ladderSpacing); 387 | const ladderWidth = this.commons.viewerOptions.ladderSpacing * this.commons.viewerOptions.maxDepth; 388 | const x = margin - ladderWidth + this.commons.viewerOptions.ladderWidth / 2; 389 | const y = (d.y + this.commons.step); 390 | return "translate(" + x + "," + y + ")" 391 | } 392 | }) 393 | // .text(([i, d]) => i this.commons.viewerOptions.zoomMax) { 456 | this.commons.current_extend = { 457 | length: extentLength, 458 | start: start, 459 | end: end 460 | }; 461 | 462 | // variables for logger 463 | zoomScale = (this.commons.fvLength / extentLength).toFixed(1); 464 | d3.select(`#${this.divId}`).select(".zoomUnit") 465 | .text(zoomScale.toString()); 466 | 467 | //modify scale 468 | this.commons.scaling.domain([start, end]); 469 | this.commons.scalingPosition.range([start, end]); 470 | 471 | let currentShift = start ? start : this.commons.viewerOptions.offset.start; 472 | 473 | // apply transitions 474 | this.transition_data(this.commons.features, currentShift); 475 | this.fillSVG.reset_axis(); 476 | 477 | // remove sequence 478 | 479 | this.commons.svgContainer.select(".mySequence").remove(); 480 | // draw sequence 481 | if (this.commons.viewerOptions.showSequence) { 482 | 483 | if (seqCheck === false) { 484 | this.fillSVG.sequenceLine(); 485 | } 486 | else if (seqCheck === true) { 487 | this.commons.seqShift = start; 488 | this.fillSVG.sequence(this.sequence.substring(start, end), this.commons.seqShift); 489 | } 490 | } 491 | 492 | } else { 493 | 494 | zoomScale = "Prevented"; 495 | this.commons.logger.warn("Zoom greater than " + this.commons.viewerOptions.zoomMax + " is prevented", {fvId:this.divId}); 496 | } 497 | 498 | this.commons.currentzoom = zoomScale; 499 | // if (CustomEvent) { 500 | // // zooming in 501 | // this.commons.svgElement.dispatchEvent(new CustomEvent(this.commons.events.ZOOM_EVENT, { 502 | // detail: { 503 | // start: start, 504 | // end: end, 505 | // zoom: zoomScale 506 | // } 507 | // })); 508 | // } 509 | // if (this.commons.trigger) this.commons.trigger(this.commons.events.ZOOM_EVENT, { 510 | // start: start, 511 | // end: end, 512 | // zoom: zoomScale 513 | // }); 514 | } 515 | 516 | // remove brush now that transition is complete 517 | d3.select(`#${this.divId}`).select(".brush").call(this.commons.brush.move, null); 518 | 519 | } else { 520 | return 521 | } 522 | 523 | 524 | } 525 | 526 | // Mobile responsiveness 527 | private resizeForMobile() { 528 | // change flags 529 | let flags = d3.select(`#${this.divId}`).selectAll(".Arrow") 530 | .attr("points", (d) => { 531 | // match points with subFeature level 532 | return this.calculate.yxPoints(d) 533 | }); 534 | this.commons.yAxisSVG.select(".flagBackground").attr("width", this.commons.viewerOptions.margin.left); 535 | let flags_text = d3.select(`#${this.divId}`).selectAll(".yAxis") 536 | .attr("width", (d) => { 537 | // text only if space is enough 538 | 539 | if (this.commons.viewerOptions.mobileMode) { 540 | // text width depends on mobile width, flaglevel and presence of subfeatures icon 541 | return this.calcFlagWidth(d); 542 | } else { 543 | let margin = 20 + this.commons.viewerOptions.ladderSpacing * this.commons.viewerOptions.maxDepth // 20 + (20 * d['flagLevel']) --> 0 544 | return this.commons.viewerOptions.margin.left - margin; // chevron margin and text indent 545 | } 546 | }); 547 | 548 | if (this.commons.viewerOptions.mobileMode) { 549 | d3.select(`#${this.divId}`).selectAll("#ladder-group").attr("transform", ([i, d]) => { 550 | return "translate(" + ((d.flagLevel - 1) * this.commons.viewerOptions.ladderSpacing ) + "," + (d['y'] + 3.14) + ")" 551 | }); 552 | } else { 553 | d3.select(`#${this.divId}`).selectAll("#ladder-group").attr("transform", ([i, d]) => { 554 | const margin = 20 + this.commons.viewerOptions.ladderSpacing * this.commons.viewerOptions.maxDepth 555 | return "translate(" + (this.commons.viewerOptions.labelTrackWidth - margin + (d.flagLevel * this.commons.viewerOptions.ladderSpacing)) + "," + (d['y'] + 3.14) + ")" 556 | }); 557 | } 558 | 559 | // background containers, update width 560 | this.commons.svgContainer.attr("transform", "translate(" + (this.commons.viewerOptions.margin.left).toString() + ",12)"); 561 | // this.commons.tagsContainer.attr("transform","translate(" + (this.commons.viewerOptions.width + this.commons.viewerOptions.margin.left) + "," + this.commons.viewerOptions.margin.top + ")") 562 | } 563 | 564 | private calcFlagWidth(d) { 565 | this.commons.headMargin = 20; // 20 * (d.flagLevel - 1) 566 | let totalspace = 0 567 | if ('hasSubFeatures' in d && d.hasSubFeatures) {totalspace} // {totalspace += 20} 568 | 569 | if (this.commons.headMargin) {totalspace += this.commons.headMargin + 8} 570 | let space = this.commons.viewerOptions.labelTrackWidthMobile - 15 - totalspace 571 | if (space < 20) { 572 | return '0px'; 573 | } else { 574 | return space +'px'; 575 | } 576 | } 577 | 578 | private updateWindow() { 579 | // change width now 580 | if (d3.select(`#${this.divId}`).node() !== null) { 581 | 582 | let d3node = d3.select(`#${this.commons.divId}`).node(); 583 | let totalwidth = (d3node).getBoundingClientRect().width; 584 | 585 | // resize for mobile 586 | if (totalwidth < this.commons.mobilesize) { 587 | if (!this.commons.viewerOptions.mobileMode) { 588 | this.commons.viewerOptions.mobileMode = true; 589 | // update margins according to flagBackground width 590 | if (this.commons.viewerOptions.tagsTrackWidth !== 0) { 591 | this.commons.viewerOptions.margin.right = 80 592 | } 593 | this.commons.viewerOptions.margin.left = this.calculate.getMarginLeft() - (this.commons.viewerOptions.labelTrackWidth - this.commons.viewerOptions.labelTrackWidthMobile) 594 | } else { 595 | this.commons.viewerOptions.margin.left = this.calculate.getMarginLeft(); 596 | } 597 | } else { 598 | let margins = this.calculate.getMarginLeft(); 599 | if (this.commons.viewerOptions.mobileMode) { 600 | // no mobile size, resize flags 601 | this.commons.viewerOptions.mobileMode = false; 602 | margins += (this.commons.viewerOptions.labelTrackWidth - this.commons.viewerOptions.labelTrackWidthMobile) 603 | } 604 | this.commons.viewerOptions.margin.left = margins; 605 | } 606 | 607 | // update margins according to flagBackground width 608 | // resize for mobile 609 | this.resizeForMobile() 610 | this.commons.viewerOptions.width = totalwidth - this.commons.viewerOptions.margin.left - this.commons.viewerOptions.margin.right - 17; 611 | 612 | // resize containers 613 | this.commons.svg 614 | .attr("width", totalwidth); 615 | this.commons.svg.select("clipPath rect") 616 | .attr("width", this.commons.viewerOptions.width); 617 | if (this.commons.viewerOptions.brushActive) { 618 | d3.select(`${this.commons.divId} .background`).attr("width", this.commons.viewerOptions.width - 10); 619 | } 620 | 621 | // resize brush 622 | d3.select(`#${this.commons.divId}`).select(".fvbrush").call(this.commons.brush.move, null); 623 | 624 | // new scaling 625 | this.commons.scaling.range([2, this.commons.viewerOptions.width - 2]); 626 | this.commons.scalingPosition.domain([0, this.commons.viewerOptions.width]); 627 | 628 | // update seq visualization 629 | let seq = this.calculate.displaySequence(this.lastLen); 630 | // let seq = this.calculate.displaySequence(this.commons.viewerOptions.offset.end - this.commons.viewerOptions.offset.start); 631 | this.commons.svgContainer.select(".mySequence").remove(); 632 | if (this.commons.viewerOptions.showSequence) { 633 | if (this.getCurrentZoom() == undefined || seq === false) { 634 | this.fillSVG.sequenceLine(); 635 | } else if (this.getCurrentZoom() && seq) { 636 | this.fillSVG.sequence(this.sequence.substring(this.commons.viewerOptions.offset.start, this.commons.viewerOptions.offset.end), this.commons.viewerOptions.offset.start); 637 | } 638 | // if (seq === false) { 639 | // this.fillSVG.sequenceLine(); 640 | // } else if (seq === true) { 641 | // this.fillSVG.sequence(this.sequence.substring(this.commons.viewerOptions.offset.start, this.commons.viewerOptions.offset.end), this.commons.viewerOptions.offset.start); 642 | // } 643 | } 644 | if (this.commons.animation) { 645 | // @ts-ignore 646 | d3.select(`#${this.commons.divId}`).select('#tags_container').transition().duration(500) 647 | } 648 | d3.select(`#${this.commons.divId}`).select('#tags_container') 649 | .attr("transform", "translate(" + (this.commons.viewerOptions.margin.left + this.commons.viewerOptions.width + 10).toString() + ",10)"); 650 | 651 | this.transition_data(this.commons.features, this.commons.current_extend.start); 652 | this.fillSVG.reset_axis(); 653 | this.fillSVG.resizeBrush() 654 | 655 | } 656 | } 657 | 658 | private transition_data(features, start) { 659 | // no usage of start 660 | // apply transition data recursively? 661 | for (const o of features) { 662 | if (o.type === "rect") { 663 | this.transition.rectangle(o); 664 | } else if (o.type === "multipleRect") { 665 | this.transition.multiRec(o); 666 | } else if (o.type === "unique") { 667 | this.transition.unique(o); 668 | } else if (o.type === "circle") { 669 | this.transition.circle(o); 670 | } else if (o.type === "path") { 671 | this.transition.path(o); 672 | } else if (o.type === "lollipop") { 673 | this.transition.lollipop(o); 674 | } else if (o.type === "curve") { 675 | this.transition.lineTransition(o); 676 | } 677 | // resize basal line too 678 | this.transition.basalLine(o); 679 | // apply recursively to subfeatures (if shown) 680 | if (o.subfeatures && o.isOpen) { 681 | this.transition_data(o.subfeatures, start) 682 | } 683 | } 684 | } 685 | // init viewer 686 | private init(div) { 687 | 688 | // first element is 0 689 | this.sequence = " " + this.sequence; 690 | this.commons.stringSequence = this.sequence; 691 | 692 | d3.select(div) 693 | .style("position", "relative") 694 | .style("padding", "0px") 695 | .style("z-index", "2"); 696 | 697 | this.commons.scaling = d3.scaleLinear() 698 | .domain([this.commons.viewerOptions.offset.start, this.commons.viewerOptions.offset.end]) 699 | .range([0, this.commons.viewerOptions.width]); // borders 700 | 701 | this.commons.scalingPosition = d3.scaleLinear() 702 | .domain([0, this.commons.viewerOptions.width]) 703 | .range([this.commons.viewerOptions.offset.start, this.commons.viewerOptions.offset.end]); 704 | 705 | // init objects 706 | 707 | this.tool.initTooltip(div, this.divId); 708 | 709 | // init overlay div 710 | let overlayhtml = '
' 711 | d3.select(`#${this.divId}`) 712 | .append("foreignObject") 713 | .html(overlayhtml) 714 | 715 | this.commons.lineBond = d3.line() 716 | .curve(d3.curveStepBefore) 717 | .x((d) => { 718 | return this.commons.scaling(d['x']); 719 | // return this.commons.scaling(d[0]); 720 | // return this.commons.scaling(d[0].x); 721 | }) 722 | .y((d) => { 723 | //return -d[1] * 10 + this.commons.pathLevel; 724 | return -d['y'] * 10 + this.commons.pathLevel; 725 | // return -(d[0] as any).y * 10 + this.commons.pathLevel; 726 | }); 727 | 728 | this.commons.lineGen = d3.line() 729 | .x((d) => { 730 | // return this.commons.scaling(d[0]); 731 | return this.commons.scaling(d['x']); 732 | // return this.commons.scaling((d[0] as any).x); 733 | }) 734 | .curve(d3.curveBasis); 735 | 736 | this.commons.lineYScale = d3.scaleLinear() 737 | .domain([0, -30]) 738 | .range([0, -20]); 739 | 740 | this.commons.line = d3.line() 741 | .curve(d3.curveLinear) 742 | .x((d) => { 743 | // return this.commons.scaling(d[0]); 744 | return this.commons.scaling(d['x']); 745 | // return this.commons.scaling((d[0] as any).x); 746 | }) 747 | .y((d) => { 748 | // return d[1] + 6; 749 | return d['y'] + 6; 750 | //return (d[0] as any).y + 6; 751 | }); 752 | 753 | let rtickStep = Math.round(this.commons.fvLength/10); // fraction of a tenth 754 | let tickStep = Math.round(rtickStep/10)*10; // nearest 10th multiple 755 | 756 | let tickArray = Array.from(Array(this.commons.fvLength).keys()) 757 | .filter(function (value, index, ar) { 758 | return (index % tickStep == 0 && index !== 0); 759 | }); 760 | 761 | //Create Axis 762 | this.commons.xAxis = d3.axisBottom(this.commons.scaling) 763 | .tickValues(tickArray) 764 | //.scale(this.commons.scaling) // TODO 765 | //.tickFormat(d3.format("d")); 766 | 767 | let yAxisScale = d3.scaleBand() 768 | //.domain([0, this.commons.yData.length]) 769 | .rangeRound([0, 500]); 770 | 771 | // Y Axis 772 | d3.axisLeft(yAxisScale) 773 | //.scale(yAxisScale) // TODO 774 | //.tickValues(this.commons.yData) 775 | .tickFormat(function (d) { 776 | return d 777 | }); 778 | 779 | this.commons.brush = d3.brushX() 780 | //.extent([[this.commons.scaling.range()[0], 0], [this.commons.scaling.range()[1], 1]]) 781 | .on("end", () => { 782 | this.brushend() 783 | }); 784 | 785 | this.commons.right_chevron = "M12.95 10.707l0.707-0.707-5.657-5.657-1.414 1.414 4.242 4.243-4.242 4.243 1.414 1.414 4.95-4.95z" 786 | this.commons.down_chevron = "M9.293 12.95l0.707 0.707 5.657-5.657-1.414-1.414-4.243 4.242-4.243-4.242-1.414 1.414z" 787 | 788 | // Define the divs for the tooltip 789 | this.commons.tooltipDiv = d3.select(`#${this.divId}`) 790 | .append("div") 791 | .attr("class", "fvtooltip") 792 | .attr("id", "fvtooltip") 793 | .style("opacity", 0) 794 | .style("z-index", 1070); 795 | this.commons.customTooltipDiv = d3.select(`#${this.divId}`) 796 | .append("div") 797 | .attr("class", "fvcustomtooltip") 798 | .attr("id", "fvcustomtooltip") 799 | .style("opacity", 0) 800 | .style("z-index", 1070); 801 | 802 | this.commons.style = d3.select(`#${this.divId}`) 803 | // .append("style") 804 | // .html(`${fvstyles}`) 805 | 806 | 807 | // Create SVG 808 | if (this.commons.viewerOptions.toolbar) { 809 | let headerOptions = document.querySelector(div + " .svgHeader") ? d3.select(div + " .svgHeader") : d3.select(div).append("div").attr("class", "svgHeader"); 810 | 811 | if (this.commons.viewerOptions.toolbarPosition) { 812 | // flex-end 813 | if (this.commons.viewerOptions.toolbarPosition === "right") {this.commons.viewerOptions.toolbarPosition = "flex-end"} 814 | else if (this.commons.viewerOptions.toolbarPosition === "left") {this.commons.viewerOptions.toolbarPosition = "flex-start"} 815 | headerOptions.attr("style", "color: rgba(39, 37, 37, 0.71); display: flex; justify-content: " + this.commons.viewerOptions.toolbarPosition) 816 | } 817 | else { 818 | headerOptions.attr("style", "color: rgba(39, 37, 37, 0.71);"); 819 | } 820 | 821 | // position 822 | if (!document.querySelector(div + ' .header-position')) { 823 | let headerPosition = headerOptions 824 | .append("div") 825 | .attr("class", "header-position") 826 | .style("display", "inline-block") 827 | .style("padding-top", "5px") 828 | let button = headerPosition 829 | .append("div") 830 | .attr("class", "position-label") 831 | .style("display", "inline-block") 832 | button 833 | // draw icon 834 | .append("svg") 835 | .attr("class", "helperButton") 836 | .append("path") 837 | .attr("d", "M10 20s-7-9.13-7-13c0-3.866 3.134-7 7-7s7 3.134 7 7v0c0 3.87-7 13-7 13zM10 9c1.105 0 2-0.895 2-2s-0.895-2-2-2v0c-1.105 0-2 0.895-2 2s0.895 2 2 2v0z"); 838 | button 839 | .append("text") 840 | .text("Position:") 841 | headerPosition 842 | .append("div") 843 | .style("display", "inline-block") 844 | .style("padding-left", "5px") 845 | .append("div") 846 | .style("padding-right", "15px") 847 | .style("width", "80px") // fix width otherwise responsive to length number; 848 | .attr("id", "zoomPosition") 849 | .text("0") 850 | } 851 | 852 | //zoom 853 | let headerZoom; 854 | if (!document.querySelector(div + ' .header-zoom')) { 855 | 856 | headerZoom = headerOptions 857 | .append("div") 858 | .attr("class", "header-zoom") 859 | .style("display", "inline-block") 860 | .style("padding-top", "5px"); 861 | let button = headerZoom 862 | .append("div") 863 | .attr("class", "zoom-label") 864 | .style("display", "inline-block") 865 | button 866 | // draw icon 867 | .append("svg") 868 | .attr("class", "helperButton") 869 | .append("path") 870 | .attr("d", "M12.9 14.32c-1.34 1.049-3.050 1.682-4.908 1.682-4.418 0-8-3.582-8-8s3.582-8 8-8c4.418 0 8 3.582 8 8 0 1.858-0.633 3.567-1.695 4.925l0.013-0.018 5.35 5.33-1.42 1.42-5.33-5.34zM8 14c3.314 0 6-2.686 6-6s-2.686-6-6-6v0c-3.314 0-6 2.686-6 6s2.686 6 6 6v0z"); 871 | button 872 | .append("text") 873 | .text("Zoom:") 874 | 875 | headerZoom 876 | .append("div") 877 | .style("display", "inline-block") 878 | .append("div") 879 | .style("padding-left", "5px") 880 | .style("width", "80px") // fix width otherwise responsive to length number; 881 | .append("span") 882 | .text("x ") 883 | .append("span") 884 | .style("padding-right", "15px") 885 | .attr("class", "zoomUnit") 886 | .text("1"); 887 | } 888 | 889 | // help 890 | if (!document.querySelector(div + ' .header-help')) { 891 | 892 | let headerHelp = headerOptions 893 | .append("div") 894 | .attr("class", "header-help") 895 | .style("display", "inline-block") 896 | 897 | headerHelp 898 | .append("button") 899 | .attr("class", "mybuttoncircle") 900 | .attr("id", "downloadButton") 901 | .on("click", () => { 902 | this.downloadSvg() 903 | }) 904 | // draw icon 905 | .append("svg") 906 | .attr("class", "helperButton") 907 | .append("path") 908 | .attr("d", "M13 8v-6h-6v6h-5l8 8 8-8h-5zM0 18h20v2h-20v-2z") 909 | 910 | 911 | headerHelp 912 | .append("button") 913 | .attr("id", "helpButton") 914 | .attr("class", "mybuttoncircle") 915 | .on("click", () => { 916 | this.fillSVG.showHelp() 917 | }) 918 | // draw icon 919 | .append("svg") 920 | .attr("class", "helperButton") 921 | .append("path") 922 | .attr("d", "M2.93 17.070c-1.884-1.821-3.053-4.37-3.053-7.193 0-5.523 4.477-10 10-10 2.823 0 5.372 1.169 7.19 3.050l0.003 0.003c1.737 1.796 2.807 4.247 2.807 6.947 0 5.523-4.477 10-10 10-2.7 0-5.151-1.070-6.95-2.81l0.003 0.003zM9 11v4h2v-6h-2v2zM9 5v2h2v-2h-2z") 923 | 924 | } 925 | 926 | } 927 | 928 | this.commons.svg = d3.select(div).append("svg") 929 | .attr("width", this.commons.viewerOptions.width + this.commons.viewerOptions.margin.left + this.commons.viewerOptions.margin.right) 930 | .attr("height", this.commons.viewerOptions.height + this.commons.viewerOptions.margin.top + this.commons.viewerOptions.margin.bottom) 931 | .style("z-index", "2") 932 | .attr("id", "svgContent") 933 | .on("dblclick", (d,i)=>{ 934 | // react to double click 935 | this.resetZoom(); 936 | // if (CustomEvent) { 937 | // let event = new CustomEvent(this.commons.events.CLEAR_SELECTION_EVENT, {detail: {}}); 938 | // this.commons.svgElement.dispatchEvent(event); 939 | // } else { 940 | // this.commons.logger.warn("CustomEvent is not defined", {fvId:this.divId}); 941 | // } 942 | // if (this.commons.trigger) this.commons.trigger(this.commons.events.CLEAR_SELECTION_EVENT); 943 | }) 944 | .on("contextmenu", (d, i) => { 945 | // react on right click 946 | this.resetZoom(); 947 | if (CustomEvent) { 948 | let event = new CustomEvent(this.commons.events.CLEAR_SELECTION_EVENT, {detail: {}}); 949 | this.commons.svgElement.dispatchEvent(event); 950 | } else { 951 | this.commons.logger.warn("CustomEvent is not defined", {fvId:this.divId}); 952 | } 953 | if (this.commons.trigger) this.commons.trigger(this.commons.events.CLEAR_SELECTION_EVENT); 954 | }); 955 | 956 | this.commons.svgElement = d3.select(`#${this.divId}`).select('svg').node(); 957 | 958 | 959 | // features track box 960 | this.commons.svgContainer = this.commons.svg 961 | .append("g") 962 | .attr("transform", "translate(" + this.commons.viewerOptions.margin.left + "," + this.commons.viewerOptions.margin.top + ")") 963 | .attr("id", "tracks_container") 964 | // prevent right-click 965 | .on("contextmenu", function (d, i) { 966 | currentEvent.preventDefault(); 967 | // react on right-clicking 968 | }); 969 | 970 | 971 | // background 972 | this.commons.svgContainer.append("rect") 973 | .attr("width", "100%") 974 | .attr("height", "100%") 975 | .attr("fill", this.commons.backgroundcolor); 976 | 977 | // tags space 978 | this.commons.tagsContainer = this.commons.svg.append("g") 979 | .attr("transform", "translate(" + (this.commons.viewerOptions.width + this.commons.viewerOptions.margin.left) + "," + this.commons.viewerOptions.margin.top + ")") 980 | .attr("id", "tags_container"); 981 | 982 | if (this.commons.viewerOptions.sideBar) { 983 | // add white rect to hide feature zoom exceeding the viewer length 984 | this.commons.tagsContainer.append("rect") 985 | .attr("x", -6) 986 | .attr("width", "100%") 987 | .attr("height", "100%") 988 | .attr("fill", this.commons.backgroundcolor); 989 | } 990 | 991 | this.commons.svgContainer.on('mousemove', () => { 992 | let absoluteMousePos = d3.mouse(this.commons.svgContainer.node()); 993 | 994 | let posN = Math.round(this.commons.scalingPosition(absoluteMousePos[0])); 995 | let pos; 996 | if (!this.commons.viewerOptions.positionWithoutLetter) { 997 | pos = `${posN}${this.sequence[posN] || ""}`; 998 | } else { 999 | pos = posN.toString(); 1000 | } 1001 | this.commons.currentposition = pos; 1002 | if (this.commons.viewerOptions.toolbar) { 1003 | // d3.select(`${this.divId} #zoomPosition`).text(pos); 1004 | document.querySelector(`#${this.divId} #zoomPosition`).innerHTML = pos; 1005 | } 1006 | }); 1007 | 1008 | if (this.commons.viewerOptions.showSequence) { 1009 | if (this.calculate.displaySequence(this.commons.viewerOptions.offset.end - this.commons.viewerOptions.offset.start)) { 1010 | this.fillSVG.sequence(this.sequence.substring(this.commons.viewerOptions.offset.start, this.commons.viewerOptions.offset.end), this.commons.viewerOptions.offset.start); 1011 | } 1012 | else { 1013 | this.fillSVG.sequenceLine(); 1014 | } 1015 | // check if sequence already initialized, alse add it to yData 1016 | // if (this.commons.yData.filter((e) => {e.id === 'fv_sequence'}).length === 0) { 1017 | // features 1018 | // this.commons.features.push({ 1019 | // data: this.sequence, 1020 | // label: "Sequence", 1021 | // className: "AA", 1022 | // color: "black", 1023 | // type: "sequence", 1024 | // id: "fv_sequence" 1025 | // }); 1026 | // yData 1027 | if (this.commons.viewerOptions.showSequenceLabel) { 1028 | this.commons.yData.push({ 1029 | id: "fv_sequence", 1030 | label: "Sequence", 1031 | y: this.commons.YPosition - 10, 1032 | flagLevel: 1 1033 | }); 1034 | } 1035 | // } 1036 | // this.commons.yData.push({ 1037 | // id: "fv_sequence", 1038 | // label: "", 1039 | // y: this.commons.YPosition - 8, 1040 | // flagLevel: 1 1041 | // }); 1042 | } 1043 | 1044 | this.fillSVG.addXAxis(this.commons.YPosition); 1045 | this.addYAxis(); 1046 | 1047 | if (this.commons.viewerOptions.brushActive) { 1048 | // this.commons.viewerOptions.brushActive = true; 1049 | this.fillSVG.addBrush(); 1050 | } 1051 | /* feature removed 1052 | if (this.commons.viewerOptions.verticalLine) { 1053 | // this.commons.viewerOptions.verticalLine = true; 1054 | this.addVerticalLine(); 1055 | } 1056 | */ 1057 | 1058 | this.calculate.updateSVGHeight(this.commons.YPosition); 1059 | 1060 | // listen to resizing 1061 | window.addEventListener("resize", () => { 1062 | this.updateWindow() 1063 | }); // window.addEventListener works, but iupdateWindow needs to access to internal commons 1064 | } 1065 | 1066 | // interact with features 1067 | private addFeatureCore(object, flagLevel = 1, position = null) { 1068 | this.commons.YPosition += this.commons.step; 1069 | // if no label is given, id on flag 1070 | if (!object.label) {object.label = object.id} 1071 | // deselect it if no isOpen input 1072 | if (!object.isOpen) { 1073 | object.isOpen = false; 1074 | } 1075 | if (this.commons.animation) { 1076 | if (CustomEvent) { 1077 | let event = new CustomEvent(this.commons.events.ANIMATIONS_FALSE_EVENT, {detail: {}}); 1078 | if (this.commons.svgElement) { 1079 | this.commons.svgElement.dispatchEvent(event); 1080 | } 1081 | } else { 1082 | this.commons.logger.warn("CustomEvent is not defined", {fvId:this.divId}); 1083 | } 1084 | if (this.commons.trigger) this.commons.trigger(this.commons.events.ANIMATIONS_FALSE_EVENT); 1085 | } 1086 | 1087 | if (!object.className) { 1088 | object.className = object.type + "fv"; 1089 | } 1090 | else { 1091 | // initialized by user or by viewer? 1092 | if (object.className !== object.type + "fv") { 1093 | object.className = object.className + " " + object.type + "fv"; 1094 | } 1095 | } 1096 | 1097 | if (!object.color) { 1098 | object.color = "#DFD5F5"; 1099 | } 1100 | 1101 | //object.height = this.commons.elementHeight; 1102 | object.flagLevel = flagLevel; 1103 | 1104 | this.fillSVG.typeIdentifier(object); 1105 | // flags 1106 | this.updateYAxis(); 1107 | if (object.type === "curve" || object.type === "path") { 1108 | // this.updateWindow(); 1109 | } 1110 | } 1111 | 1112 | private drawFeatures() { 1113 | // turn off features if more than 100 1114 | if (this.commons.features.length > 100) { 1115 | this.commons.animation = false; 1116 | this.commons.logger.warn("Animation is turned off with more than 100 features", {method:"addFeatureCore", fvId:this.divId, featuresNumber:this.commons.features.length}) 1117 | } 1118 | for (const ft of this.commons.features) { 1119 | this.addFeature(ft) 1120 | } 1121 | 1122 | this.fillSVG.updateXAxis(this.commons.YPosition); 1123 | this.calculate.updateSVGHeight(this.commons.YPosition + 5); 1124 | 1125 | // update brush 1126 | if (this.commons.viewerOptions.brushActive) { 1127 | this.fillSVG.resizeBrush() 1128 | } 1129 | 1130 | } 1131 | 1132 | private recursivelyRemove(ft) { 1133 | // remove subfeatures 1134 | if (ft.subfeatures) { 1135 | for (const sft of ft.subfeatures) { 1136 | this.recursivelyRemove(sft) 1137 | } 1138 | } 1139 | // remove from feature array and from html 1140 | d3.select(`#t${ft.id}_tagarea`).remove(); 1141 | d3.select(`#c${ft.id}_container`).remove(); 1142 | d3.select(`#${ft.id}`).remove(); 1143 | } 1144 | 1145 | private recursiveClose (array) { 1146 | for (const sbt of array) { 1147 | sbt.isOpen = false 1148 | if (sbt.subfeatures) { 1149 | this.recursiveClose(sbt.subfeatures) 1150 | } 1151 | } 1152 | } 1153 | 1154 | private changeFeature(feature, bool) { 1155 | 1156 | // freeze viewer 1157 | this.flagLoading(feature.id); 1158 | // close or open it 1159 | feature.isOpen = bool; 1160 | // if close, reset children status 1161 | if (!feature.isOpen) { 1162 | if (feature.subfeatures) { 1163 | this.recursiveClose(feature.subfeatures) 1164 | } 1165 | } 1166 | 1167 | // overlay if opening many subfeatures 1168 | if (feature.isOpen) { 1169 | if (feature.subfeatures.length > 200) { 1170 | setTimeout(()=>{ 1171 | // empty features 1172 | this.commons.features = this.emptyFeatures() 1173 | // redraw features 1174 | this.drawFeatures() 1175 | // defreeze viewer 1176 | this.stopFlagLoading(feature.id) 1177 | }, 1) 1178 | return 1179 | } 1180 | } 1181 | 1182 | // empty features 1183 | this.commons.features = this.emptyFeatures() 1184 | // redraw features 1185 | this.drawFeatures() 1186 | // defreeze viewer 1187 | this.stopFlagLoading(feature.id) 1188 | 1189 | 1190 | } 1191 | 1192 | private resetTooltip(tooltipdiv) { 1193 | // empty custom tooltip in reset 1194 | tooltipdiv.transition() 1195 | .duration(500) 1196 | .style("opacity", 0); 1197 | tooltipdiv.html(""); 1198 | tooltipdiv.status = 'closed'; 1199 | } 1200 | 1201 | 1202 | /*** PUBLIC FUNCTIONS ***/ 1203 | 1204 | public getCurrentPosition() { 1205 | return this.commons.currentposition; 1206 | } 1207 | 1208 | public getCurrentZoom() { 1209 | return this.commons.currentzoom; 1210 | } 1211 | 1212 | public showHelp() { 1213 | this.fillSVG.showHelp() 1214 | } 1215 | 1216 | public resetHighlight(resetLastHighlight=true) { 1217 | // empty custom tooltip in reset 1218 | this.resetTooltip(this.commons.customTooltipDiv); 1219 | this.resetTooltip(this.commons.tooltipDiv); 1220 | 1221 | if (CustomEvent) { 1222 | let event = new CustomEvent(this.commons.events.CLEAR_SELECTION_EVENT, {detail: {}}); 1223 | this.commons.svgElement.dispatchEvent(event); 1224 | } else { 1225 | this.commons.logger.warn("CustomEvent is not defined", {fvId:this.divId}); 1226 | } 1227 | if (this.commons.trigger) this.commons.trigger(this.commons.events.CLEAR_SELECTION_EVENT); 1228 | 1229 | // remove selected features 1230 | if (this.commons.featureSelected) { 1231 | d3.select(`#${this.divId}`).select(`#${this.commons.featureSelected}`).style("fill-opacity", "0.6"); 1232 | this.commons.featureSelected = null; 1233 | } 1234 | // remove selection rectangle 1235 | d3.select(`#${this.divId}`).selectAll(".selectionRect").remove(); 1236 | 1237 | if (resetLastHighlight === true) { 1238 | this.lastHighlight = null; 1239 | } 1240 | } 1241 | 1242 | public resetZoom() { 1243 | this.resetHighlight() 1244 | //reset scale 1245 | d3.select(`#${this.divId}`).select(".zoomUnit").text("1"); 1246 | this.commons.scaling.domain([this.commons.viewerOptions.offset.start, this.commons.viewerOptions.offset.end]); 1247 | this.commons.scalingPosition.range([this.commons.viewerOptions.offset.start, this.commons.viewerOptions.offset.end]); 1248 | let seq = this.calculate.displaySequence(this.commons.viewerOptions.offset.end - this.commons.viewerOptions.offset.start); 1249 | 1250 | d3.select(`#${this.divId}`).select(".fvbrush").call(this.commons.brush.move, null); 1251 | 1252 | // remove sequence 1253 | this.commons.svgContainer.select(".mySequence").remove(); 1254 | // draw sequence 1255 | if (this.commons.viewerOptions.showSequence) { 1256 | if (seq === false) { 1257 | this.fillSVG.sequenceLine(); 1258 | } 1259 | else if (seq === true) { 1260 | this.fillSVG.sequence(this.sequence.substring(this.commons.viewerOptions.offset.start, this.commons.viewerOptions.offset.end), this.commons.viewerOptions.offset.start); 1261 | } 1262 | } 1263 | this.commons.current_extend = { 1264 | length: this.commons.viewerOptions.offset.end - this.commons.viewerOptions.offset.start, 1265 | start: this.commons.viewerOptions.offset.start, 1266 | end: this.commons.viewerOptions.offset.end 1267 | }; 1268 | this.commons.seqShift = 0; 1269 | 1270 | this.transition_data(this.commons.features, this.commons.viewerOptions.offset.start); 1271 | this.fillSVG.reset_axis(); 1272 | 1273 | // Fire Event 1274 | if (CustomEvent) { 1275 | this.commons.svgElement.dispatchEvent(new CustomEvent(this.commons.events.ZOOM_EVENT, { 1276 | detail: { 1277 | start: 1, 1278 | end: this.sequence.length-1, 1279 | zoom: 1 1280 | } 1281 | })); 1282 | } 1283 | if (this.commons.trigger) this.commons.trigger(this.commons.events.ZOOM_EVENT, { 1284 | start: 1, 1285 | end: this.sequence.length-1, 1286 | zoom: 1 1287 | }); 1288 | this.commons.currentzoom = 1; 1289 | } 1290 | 1291 | public resetAll() { 1292 | 1293 | this.resetHighlight() 1294 | this.resetZoom() 1295 | 1296 | this.emptyFeatures() 1297 | // empty features 1298 | this.commons.features = this.commons.viewerOptions.backup.features; 1299 | // redraw features 1300 | this.drawFeatures() 1301 | } 1302 | 1303 | public downloadSvg() { 1304 | 1305 | let svg_el = document.getElementById('svgContent') 1306 | let filename = "feature_viewer.svg"; 1307 | 1308 | // htmlToImage.toJpeg(document.getElementById(this.divId), { quality: 0.95 }) 1309 | // .then(function (dataUrl) { 1310 | // var link = document.createElement('a'); 1311 | // link.download = filename; 1312 | // link.href = dataUrl; 1313 | // link.click(); 1314 | // }); 1315 | 1316 | 1317 | svg_el.setAttribute("xmlns", "http://www.w3.org/2000/svg"); 1318 | let svgData = svg_el.outerHTML; 1319 | let preface = '\r\n'; 1320 | let svgBlob = new Blob([preface, svgData], {type: "image/svg+xml;charset=utf-8"}); 1321 | let svgUrl = URL.createObjectURL(svgBlob); 1322 | let downloadLink = document.createElement("a"); 1323 | downloadLink.href = svgUrl; 1324 | downloadLink.download = filename; 1325 | document.body.appendChild(downloadLink); 1326 | downloadLink.click(); 1327 | document.body.removeChild(downloadLink); 1328 | 1329 | } 1330 | 1331 | public clickFlag(d) { 1332 | if (d) { 1333 | // remove selected features 1334 | if (this.commons.featureSelected) { 1335 | d3.select(`#${this.commons.divId}`).select(`#${this.commons.featureSelected}`).style("fill-opacity", "0.6"); 1336 | this.commons.featureSelected = null; 1337 | } 1338 | // remove selection rectangle 1339 | d3.select(`#${this.commons.divId}`).select(".selectionRect").remove(); 1340 | 1341 | // empty custom tooltip 1342 | this.commons.customTooltipDiv.transition() 1343 | .duration(500) 1344 | .style("opacity", 0); 1345 | this.commons.customTooltipDiv.html(""); 1346 | 1347 | // dispatches selected flag event 1348 | let id = d.id; 1349 | let flag_detail_object = { 1350 | points: this.calculate.yxPoints(d), 1351 | y: d.y, 1352 | id: d.id, 1353 | label: d.label, 1354 | flagLevel: d.flagLevel 1355 | }; 1356 | // trigger flag_selected event 1357 | if (CustomEvent) { 1358 | 1359 | let eventDetail = {detail: flag_detail_object}, 1360 | event = new CustomEvent(this.commons.events.FLAG_SELECTED_EVENT, eventDetail); 1361 | this.commons.svgElement.dispatchEvent(event); 1362 | 1363 | if (this.commons.viewerOptions.showSubFeatures && d.hasSubFeatures) { 1364 | this.commons.flagSelected.push(flag_detail_object.id); 1365 | 1366 | // transition to open if closed and viceversa 1367 | // add offset to margin 1368 | // update flagBackground width 1369 | // let width = d.isOpen? this.commons.viewerOptions.labelTrackWidth + (20 * d.flagLevel) - 20 : this.commons.viewerOptions.labelTrackWidth + (20 * d.flagLevel); 1370 | let width = d.isOpen? this.commons.viewerOptions.labelTrackWidth : this.commons.viewerOptions.labelTrackWidth; 1371 | if (this.commons.viewerOptions.mobileMode) { 1372 | // width = d.isOpen? this.commons.viewerOptions.labelTrackWidthMobile + (20 * d.flagLevel) - 20 : this.commons.viewerOptions.labelTrackWidthMobile + (20 * d.flagLevel); 1373 | width = d.isOpen? this.commons.viewerOptions.labelTrackWidthMobile : this.commons.viewerOptions.labelTrackWidthMobile; 1374 | } 1375 | this.commons.yAxisSVG.select(".flagBackground").attr("width", width); 1376 | this.updateWindow() 1377 | 1378 | var i; 1379 | var result = null; 1380 | for (i = 0; result == null && i < this.commons.features.length; i++) { 1381 | result = this.calculate.searchTree(this.commons.features[i], flag_detail_object.id); 1382 | } 1383 | let featureToChange = result; 1384 | if (featureToChange) { 1385 | this.changeFeature(featureToChange, !featureToChange.isOpen); 1386 | } else { 1387 | this.commons.logger.warn("Feature not found in feature array", {fvId:this.commons.divId, featureId:flag_detail_object.id}) 1388 | } 1389 | } 1390 | 1391 | 1392 | } else { 1393 | this.commons.logger.warn("CustomEvent is not defined", {fvId:this.commons.divId}); 1394 | } 1395 | 1396 | if (this.commons.trigger) this.commons.trigger(this.commons.events.FLAG_SELECTED_EVENT, flag_detail_object); 1397 | } 1398 | 1399 | }; 1400 | 1401 | public emptyFeatures() { 1402 | 1403 | // clean feature object 1404 | let deepCopy = JSON.parse(JSON.stringify(this.commons.features)) 1405 | for (const ft of this.commons.features) { 1406 | this.recursivelyRemove(ft) 1407 | } 1408 | 1409 | function checkSequence(ft) { 1410 | return ft.id === 'fv_sequence'; 1411 | } 1412 | 1413 | // re-init features and yData 1414 | // this.commons.features = []; 1415 | // this.commons.yData = []; 1416 | this.commons.features = this.commons.features.filter(checkSequence) 1417 | this.commons.yData = this.commons.yData.filter(checkSequence); 1418 | 1419 | // fix axis 1420 | this.fillSVG.updateXAxis(this.commons.step) 1421 | 1422 | // transit svgContent 1423 | let container = d3.select(`#${this.divId} #svgContent`); 1424 | let newContainerH = this.commons.step * 3; 1425 | this.subfeaturesTransition.containerH(container, newContainerH); // header, sequence, axis 1426 | 1427 | // final updates based on svg heigth 1428 | if (this.commons.viewerOptions.brushActive) { 1429 | this.commons.svgContainer.selectAll(".brush rect") 1430 | .attr('height', newContainerH); 1431 | } 1432 | // re-init YPosition 1433 | this.commons.YPosition = this.commons.step; 1434 | 1435 | return deepCopy 1436 | 1437 | } 1438 | 1439 | public flagLoading(id) { 1440 | d3.select(`#${this.commons.divId}`).select("#fvoverlay").attr("class", "pageoverlay") 1441 | }; 1442 | 1443 | public highlightRegion(region, featureid) { 1444 | this.resetHighlight(); 1445 | let flatted = this.calculate.flatten(this.commons.features) 1446 | // features in viewer? 1447 | let feature = flatted.find(i => i.id === featureid); 1448 | if (feature) { 1449 | // find feature in the tree and all its parents; 1450 | if (feature.parent) { 1451 | // let parents = feature.parent.replace("null_", "").split("_") 1452 | for (const i of Object.keys(feature.parent)) { 1453 | let ptftid = feature.parent[i] 1454 | // let parentft = flatted.find(i => i.id === ptftid) 1455 | let parentft = this.commons.yData.find(i => i.id === ptftid); 1456 | if (!parentft.isOpen) {this.clickFlag(parentft)} 1457 | } 1458 | } 1459 | // let newid = region.id? region.id : featureid; 1460 | let regionid = "f_" + featureid + '_' + region.x + '-' + region.y; 1461 | this.tool.colorSelectedFeat(regionid, feature, this.commons.divId); 1462 | } else { this.commons.logger.warn("Selected feature id does not exist!") } 1463 | 1464 | }; 1465 | 1466 | public highlightPosition(region, reset=true) { 1467 | if (reset === true) { 1468 | this.resetHighlight(); 1469 | } 1470 | let start = this.commons.scaling(region.start - .5); 1471 | let end = this.commons.scaling(region.end + .5); 1472 | // remove selection rectangle if already there 1473 | let selectRect; 1474 | // color the background 1475 | if (this.commons.svgContainer.node()) { 1476 | let currentContainer = this.commons.svgContainer.node().getBoundingClientRect(); 1477 | // create 1478 | selectRect = this.commons.svgContainer 1479 | .select(".brush") 1480 | .append("rect") 1481 | .attr("class", "selectionRect box-shadow") 1482 | // add shadow? 1483 | .attr("height", currentContainer.height) 1484 | // place 1485 | selectRect 1486 | .style("display", "block") // remove display none 1487 | .attr("width", end - start) // - shift from the beginning 1488 | .attr("transform", () => { 1489 | return "translate(" + start + ",0)" 1490 | }) 1491 | } 1492 | this.lastHighlight = {type: 'single', selection: region} 1493 | } 1494 | 1495 | public highlightPositions(regions) { 1496 | this.resetHighlight(); 1497 | 1498 | for (const region of regions) { 1499 | this.highlightPosition(region, false); 1500 | } 1501 | this.lastHighlight = {type: 'multi', selection: regions} 1502 | } 1503 | 1504 | private recursiveClick(f, condition) { 1505 | if (f.isOpen === condition) { 1506 | this.clickFlag(this.commons.yData.find(i => i.id === f.id)) 1507 | if (f.subfeatures) { 1508 | for (const i of Object.keys(f.subfeatures)) { 1509 | let sf = f.subfeatures[i]; 1510 | if (sf.isOpen === condition) { 1511 | // this.clickFlag(this.commons.yData.find(i => i.id === sf.id)) 1512 | this.recursiveClick(sf, condition) 1513 | } 1514 | } 1515 | } 1516 | } 1517 | } 1518 | 1519 | 1520 | public collapseAll() { 1521 | for (const i of Object.keys(this.commons.features)) { 1522 | this.recursiveClick(this.commons.features[i], true) 1523 | } 1524 | } 1525 | 1526 | public expandAll() { 1527 | for (const i of Object.keys(this.commons.features)) { 1528 | this.recursiveClick(this.commons.features[i], false) 1529 | } 1530 | } 1531 | 1532 | /** 1533 | * @function 1534 | * @methodOf FeatureViewer 1535 | * @name onRegionSelected 1536 | * @return {object} Object describing the selected feature */ 1537 | public onRegionSelected(listener) { 1538 | this.commons.svgElement.addEventListener(this.commons.events.FEATURE_SELECTED_EVENT, listener); 1539 | }; 1540 | 1541 | public removeResizeListener() { 1542 | window.removeEventListener("resize", this.updateWindow); 1543 | }; 1544 | 1545 | // edit: listener of selected flag 1546 | /** 1547 | * @function 1548 | * @methodOf FeatureViewer 1549 | * @name onFeatureSelected 1550 | * @description Expected usage: once flag is selected, addSubFeature() 1551 | * @return {object} Object describing the selected flag */ 1552 | public onFeatureSelected(listener) { 1553 | this.commons.svgElement.addEventListener(this.commons.events.FLAG_SELECTED_EVENT, listener); 1554 | }; 1555 | 1556 | /** 1557 | * @function 1558 | * @methodOf FeatureViewer 1559 | * @name onButtonSelected 1560 | * @return {object} Object describing the selected 3D button */ 1561 | public onButtonSelected(listener) { 1562 | this.commons.svgElement.addEventListener(this.commons.events.TAG_SELECTED_EVENT, listener); 1563 | }; 1564 | 1565 | /** 1566 | * @function 1567 | * @methodOf FeatureViewer 1568 | * @name onZoom 1569 | * @return {object} Object describing the zoom event */ 1570 | public onZoom(listener) { 1571 | this.commons.svgElement.addEventListener(this.commons.events.ZOOM_EVENT, listener); 1572 | }; 1573 | 1574 | // edit: listener of clear selection 1575 | /** 1576 | * @function 1577 | * @methodOf FeatureViewer 1578 | * @name onClearSelection 1579 | * @return {object} Object describing the zoom out/clear selection event */ 1580 | public onClearSelection(listener) { 1581 | this.commons.svgElement.addEventListener(this.commons.events.CLEAR_SELECTION_EVENT, listener); 1582 | }; 1583 | 1584 | // edit: listener of animation off 1585 | /** 1586 | * @function 1587 | * @methodOf FeatureViewer 1588 | * @name onAnimationOff 1589 | * @return {object} Object describing the zoom out/clear selection event */ 1590 | public onAnimationOff(listener) { 1591 | this.commons.svgElement.addEventListener(this.commons.events.ANIMATIONS_FALSE_EVENT, listener); 1592 | }; 1593 | 1594 | public stopFlagLoading = function (id) { 1595 | d3.select(`#${this.divId}`).select("#fvoverlay").attr("class", null) 1596 | }; 1597 | 1598 | // function to call resize from external 1599 | /** 1600 | * @function 1601 | * @methodOf FeatureViewer 1602 | * @name resizeViewer 1603 | * @description resizes viewer if element dimensions are changed. Please note: resizing is automatic when window changes, call this function when other elements change 1604 | */ 1605 | public resizeViewer = function () { 1606 | this.updateWindow() 1607 | }; 1608 | 1609 | /** 1610 | * @function 1611 | * @methodOf FeatureViewer 1612 | * @name addFeature 1613 | * @param {object} object - The input feature 1614 | * @param {number} flagLevel - The indent level for rendering flag 1615 | * @property {Array} feature.data 1616 | * @property {int} feature.data..x - first position 1617 | * @property {int} feature.data..y - last position (or a value for features of type "curve") 1618 | * @property {string} [feature.data..id] - id 1619 | * @property {string} [feature.data..description] - description 1620 | * @property {string} [feature.data..color] - color 1621 | * @property {string} [feature.data..tooltip] - message for the region tooltip 1622 | * @property {string} feature.type - ("rect","curve","unique","circle") : The type of feature, for a specific rendering 1623 | * @property {string} [feature.name] - The name of theses features, which will be display as a label on the Y-axis 1624 | * @property {string} [feature.className] - a class name, for further personal computing 1625 | * @property {int} [feature.height] - height of the feature 1626 | * @property {string} [feature.color] - The color of the features 1627 | * @property {boolean} [feature.hasSubFeatures] - determines if object is clickable and expands for subFeature visualization 1628 | * @property {string} [feature.filter] - a class filter, for further personal computing 1629 | * @property {number} [feature.disorderContent] - content of disorder content tag (right side of viewer) 1630 | * @property {number} [feature.tooltip] - message for the flag tooltip 1631 | * @property {Array} [feature.links] 1632 | * @property {string} [feature.links..name] - The button name, used to identify click event 1633 | * @property {string} [feature.links..icon] - Glyphicon code or text, specify glyphicon in unicode format, ex. \ue030 1634 | * @property {string} [feature.links..message] - The message for tooltip 1635 | * @property {string} [feature.links..color] - Optional color for the visualized glyphicon 1636 | */ 1637 | private addFeature(object: FeatureObject, flagLevel=1) { 1638 | this.addFeatureCore(object, flagLevel); 1639 | if (object.subfeatures && object.isOpen) { 1640 | flagLevel+=1 1641 | for (const sft of object.subfeatures) { 1642 | this.addFeature(sft, flagLevel) 1643 | } 1644 | } return object.id 1645 | } 1646 | 1647 | public addFeatures(featureList: FeaturesList) { 1648 | this.commons.viewerOptions.backup.features = featureList; 1649 | let featureids = featureList.map(item => item.id).filter(e => {return e}); 1650 | 1651 | // check ids are present 1652 | if (featureids.length !== featureList.length) { 1653 | this.commons.logger.error("Feature ids are not present on all features, creating random ids", 1654 | {method:'addFeatures',fvId:this.divId}) 1655 | for (let f of featureList) { 1656 | if (!f.id) {f.id = 'customid' + Math.random().toString(36).substring(7);} 1657 | } 1658 | featureids = featureList.map(item => item.id).filter(e => {return e}); 1659 | } 1660 | 1661 | let regexIds = new RegExp('^[a-zA-Z]') 1662 | 1663 | // check ids are valid 1664 | for (let f of featureList) { 1665 | if (!regexIds.test(f.id)) { 1666 | this.commons.logger.error("Id " + f.id + "is not a valid html id, substituting it randomly", 1667 | {method:'addFeatures',fvId:this.divId}) 1668 | f.id = 'customid' + Math.random().toString(36).substring(7); 1669 | } 1670 | } 1671 | featureids = featureList.map(item => item.id).filter(e => {return e}); 1672 | 1673 | // check ids are unique 1674 | const uniqueIds = [...new Set(featureids)]; 1675 | if (uniqueIds.length !== featureList.length) { 1676 | this.commons.logger.error("Feature ids are not unique, substituting the non unique ones randomly", 1677 | {method:'addFeatures',fvId:this.divId}) 1678 | let idsencountered = []; 1679 | for (let f of featureList) { 1680 | if (idsencountered.includes(f.id)) {f.id = 'customid' + Math.random().toString(36).substring(7);} 1681 | idsencountered.push(f.id); 1682 | } 1683 | featureids = featureList.map(item => item.id).filter(e => {return e}); 1684 | } 1685 | // add to viewer 1686 | 1687 | // features already in viewer? 1688 | let unflatted = this.calculate.unflatten( 1689 | featureList, 1690 | null, 1691 | null, 1692 | this.commons.features.length !== 0 ? this.commons.features : null 1693 | ); 1694 | 1695 | // add new structured features to the old ones (if any, else sequence) 1696 | this.commons.features = this.commons.features.concat(unflatted.tree); 1697 | let ftsIds = unflatted.ids; 1698 | 1699 | if (!this.commons.viewerOptions.maxDepth) { 1700 | this.commons.viewerOptions.maxDepth = Math.max(...this.commons.features.map(f => this.getLevel(f, 1))); 1701 | } 1702 | 1703 | 1704 | // check if features are missing from the tree 1705 | let unprocessedIds = uniqueIds.filter((x)=>{ 1706 | return !(ftsIds.has(x)) 1707 | }); 1708 | if (unprocessedIds.length !== 0) { 1709 | this.commons.logger.error("Subfeatures with no known parentId", {method:'addFeatures', fvId: this.divId, features:unprocessedIds}) 1710 | } 1711 | 1712 | // features already in viewer? empty it before drawing 1713 | this.commons.features = this.emptyFeatures() 1714 | 1715 | // draw the viewer 1716 | this.drawFeatures() 1717 | 1718 | } 1719 | 1720 | private getLevel(f, l) { 1721 | l++ 1722 | if (f.hasOwnProperty('subfeatures')) { 1723 | l = this.getLevel(f.subfeatures, l) 1724 | } 1725 | return l 1726 | } 1727 | 1728 | constructor(sequence: string, div: string, options?: UserOptions, features?: FeaturesList) { 1729 | 1730 | this.commons = new Commons(); 1731 | 1732 | // init commons 1733 | this.commons.yData = []; 1734 | this.commons.features = []; 1735 | this.commons.YPosition = this.commons.step; 1736 | 1737 | // read divId 1738 | this.commons.divId = this.divId = div.slice(1).toString(); 1739 | this.commons.logger = new FeatureViewerLog(); // TODO this.divId as input 1740 | 1741 | // sequence and seq length 1742 | this.sequence = sequence; 1743 | this.commons.fvLength = sequence.length; 1744 | 1745 | // parse user options 1746 | if (options) { 1747 | this.parseUserOptions(options); 1748 | // sets width too, new re-set it again but in case of window resize 1749 | } else { 1750 | this.parseUserOptions({}); 1751 | } 1752 | 1753 | this.fillSVG = new FillSVG(this.commons); 1754 | this.subfeaturesTransition = new SubfeaturesTransition(this.commons); 1755 | this.transition = new Transition(this.commons); // extends computingFunctions 1756 | this.calculate = new Calculate(this.commons); 1757 | this.tool = new Tool(this.commons); 1758 | 1759 | this.init(div); 1760 | 1761 | // features? 1762 | if (features) { 1763 | this.addFeatures(features) 1764 | } 1765 | this.resizeViewer(); 1766 | } 1767 | } 1768 | 1769 | export {FeatureViewer}; 1770 | -------------------------------------------------------------------------------- /src/fillsvg.ts: -------------------------------------------------------------------------------- 1 | import ComputingFunctions from "./helper"; 2 | import Calculate from "./calculate"; 3 | import * as d3 from './custom-d3'; 4 | 5 | class PreComputing { 6 | 7 | private commons; 8 | private calculate: Calculate; 9 | 10 | public path(object) { 11 | 12 | let height; 13 | if (object.height) { height = object.height } else { height = 3 } 14 | object.data.sort((a, b) => { 15 | return a.x - b.x; 16 | }); 17 | this.commons.level = this.calculate.addNLines(object.data); 18 | object.data = object.data.map((d) => { 19 | return [{ 20 | x: d.x, 21 | y: 0, 22 | id: d.id, 23 | description: d.label || '', 24 | tooltip: d.tooltip || '', 25 | color: d.color, 26 | stroke: d.stroke, 27 | opacity: d.opacity 28 | }, { 29 | x: d.y, 30 | y: d.level + 1, 31 | id: d.id 32 | }, { 33 | x: d.y, 34 | y: 0, 35 | id: d.id 36 | }] 37 | }); 38 | // object.pathLevel = this.commons.level * height + 5; // changed this according to Necci's last commit 39 | object.pathLevel = (this.commons.level * height) / 2 + 5; 40 | this.commons.pathLevel = this.commons.level * height + 5; 41 | object.height = this.commons.level * height + 5; 42 | 43 | }; 44 | 45 | public preComputingLine(object) { 46 | const yScores = object.data[0].map(o => o.y); 47 | const maxScore = Math.max(...yScores); 48 | const minScore = Math.min(...yScores); 49 | 50 | if (!object.height) { object.height = this.commons.step / 2 } 51 | let shift = parseInt(object.height); 52 | let level = 0; 53 | 54 | for (const i of object.data.keys()) { 55 | object.data[i].sort((a, b) => { 56 | return a.x - b.x; 57 | }); 58 | if (object.data[i][0].y !== 0) { 59 | object.data[i].unshift({ 60 | x: object.data[i][0].x - 1, 61 | y: 0 62 | }) 63 | } 64 | if (object.data[i][object.data[i].length - 1].y !== 0) { 65 | object.data[i].push({ 66 | x: object.data[i][object.data[i].length - 1].x + 1, 67 | y: 0 68 | }) 69 | } 70 | let maxValue = Math.max.apply(Math, object.data[i].map((o) => { 71 | //return Math.abs(o.y); 72 | //return Math.round(Math.abs(o.y))+1; 73 | return 1; 74 | })); 75 | // overwrite this value if given option ymax 76 | if ('yLim' in object) { 77 | // maxValue = maxScore 78 | maxValue = object['yLim']; 79 | } 80 | level = maxValue > level ? maxValue : level; 81 | 82 | object.data[i] = [object.data[i].map((d) => { 83 | let yValue = d.y; 84 | if (d.y > maxValue) { 85 | yValue = maxValue; 86 | } 87 | return { 88 | x: d.x , 89 | y: yValue, 90 | id: d.id, 91 | description: d.label || '', 92 | tooltip: d.tooltip || '' 93 | } 94 | }) 95 | ] 96 | } 97 | 98 | // this.commons.lineYScale.range([0, -(shift)]).domain([0, -(level)]); 99 | this.commons.lineYScale.domain([minScore, maxScore]).range([0, this.commons.step/11]); 100 | 101 | object.pathLevel = shift * 10 + 5; 102 | object.level = level; 103 | object.shift = shift * 10 + 5; 104 | 105 | }; 106 | 107 | public multipleRect(object) { 108 | object.data.sort((a, b) => { 109 | return a.x - b.x; 110 | }); 111 | object.level = this.calculate.addNLines(object.data); 112 | object.pathLevel = this.commons.level * 10 + 5; 113 | 114 | this.commons.level = object.level; 115 | this.commons.pathLevel = object.pathLevel; 116 | 117 | }; 118 | 119 | constructor(commons) { 120 | this.commons = commons; 121 | this.calculate = new Calculate(commons); 122 | } 123 | } 124 | 125 | class FillSVG extends ComputingFunctions { 126 | 127 | private preComputing: PreComputing; 128 | private calculate: Calculate; 129 | private storeData; 130 | 131 | private hexToRgb(hex) { 132 | // Expand shorthand form (e.g. "03F") to full form (e.g. "0033FF") 133 | var shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i; 134 | hex = hex.replace(shorthandRegex, function(m, r, g, b) { 135 | return r + r + g + g + b + b; 136 | }); 137 | 138 | var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); 139 | return result ? { 140 | r: parseInt(result[1], 16), 141 | g: parseInt(result[2], 16), 142 | b: parseInt(result[3], 16) 143 | } : null; 144 | }; 145 | 146 | private isLight(mycolor) { 147 | let color = mycolor.match(/^rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*(\d+(?:\.\d+)?))?\)$/); 148 | let r = color[1]; 149 | let g = color[2]; 150 | let b = color[3]; 151 | // HSP (Highly Sensitive Poo) equation from http://alienryderflex.com/hsp.html 152 | let hsp = Math.sqrt( 153 | 0.299 * (r * r) + 154 | 0.587 * (g * g) + 155 | 0.114 * (b * b) 156 | ); 157 | // Using the HSP value, determine whether the color is light or dark 158 | if (hsp>127.5) { 159 | return true; 160 | } 161 | else { 162 | return false; 163 | } 164 | } 165 | 166 | private sbcRip(d, i, r) { 167 | let l = d.length, RGB = {}; 168 | if (l > 9) { 169 | d = d.split(","); 170 | if (d.length < 3 || d.length > 4) return null;//ErrorCheck 171 | RGB[0] = i(d[0].slice(4)); 172 | RGB[1] = i(d[1]); 173 | RGB[2] = i(d[2]); 174 | RGB[3] = d[3] ? parseFloat(d[3]) : -1; 175 | } else { 176 | if (l === 8 || l === 6 || l < 4) return null; //ErrorCheck 177 | if (l < 6) d = "#" + d[1] + d[1] + d[2] + d[2] + d[3] + d[3] + (l > 4 ? d[4] + "" + d[4] : ""); //3 digit 178 | d = i(d.slice(1), 16); 179 | RGB[0] = d >> 16 & 255; 180 | RGB[1] = d >> 8 & 255; 181 | RGB[2] = d & 255; 182 | RGB[3] = l === 9 || l === 5 ? r(((d >> 24 & 255) / 255) * 10000) / 10000 : -1; 183 | } 184 | return RGB; 185 | }; 186 | 187 | private shadeBlendConvert(p, from="#000", to=null) { 188 | if (typeof(p) !== "number" || 189 | p < -1 || 190 | p > 1 || 191 | typeof(from) !== "string" || 192 | (from[0] !== 'r' && from[0] !== '#') || 193 | (typeof(to) !== "string" && typeof(to) !== "undefined")) return null; //ErrorCheck 194 | let i = parseInt; 195 | let r = Math.round; 196 | let h = from.length > 9; 197 | h = typeof(to) === "string" ? to.length > 9 ? true : to === "c" ? !h : false : h; 198 | let b = p < 0; 199 | p = b ? p * -1 : p; 200 | to = to && to !== "c" ? to : b ? "#000000" : "#FFFFFF"; 201 | let f = this.sbcRip(from, i, r); 202 | let t = this.sbcRip(to, i, r); 203 | if (!f || !t) return null; //ErrorCheck 204 | if (h) return "rgb(" + r((t[0] - f[0]) * p + f[0]) + "," + r((t[1] - f[1]) * p + f[1]) + "," + r((t[2] - f[2]) * p + f[2]) + (f[3] < 0 && t[3] < 0 ? ")" : "," + (f[3] > -1 && t[3] > -1 ? r(((t[3] - f[3]) * p + f[3]) * 10000) / 10000 : t[3] < 0 ? f[3] : t[3]) + ")"); 205 | else return "#" + (0x100000000 + (f[3] > -1 && t[3] > -1 ? r(((t[3] - f[3]) * p + f[3]) * 255) : t[3] > -1 ? r(t[3] * 255) : f[3] > -1 ? r(f[3] * 255) : 255) * 0x1000000 + r((t[0] - f[0]) * p + f[0]) * 0x10000 + r((t[1] - f[1]) * p + f[1]) * 0x100 + r((t[2] - f[2]) * p + f[2])).toString(16).slice(f[3] > -1 || t[3] > -1 ? 1 : 3); 206 | } 207 | 208 | public typeIdentifier(feature) { 209 | let thisYPosition; 210 | if (feature.type === "curve") { 211 | if (!feature.height) { feature.height = 10 } 212 | let shift = parseInt(feature.height); 213 | 214 | thisYPosition = this.commons.YPosition //+ shift * 10 ; 215 | } else { 216 | thisYPosition = this.commons.YPosition; 217 | } 218 | 219 | this.tagArea(feature, thisYPosition); 220 | // yData is data for flags, this.rectangle etc. draw the actual objects 221 | 222 | this.commons.yData.push({ 223 | hasSubFeatures: feature.subfeatures ? true: false, 224 | tooltip: feature.tooltip, 225 | label: feature.label, 226 | id: feature.id, 227 | y: thisYPosition, 228 | flagColor: feature.flagColor, 229 | flagLevel: feature.flagLevel, 230 | isOpen: feature.isOpen, 231 | ladderColor: feature.ladderColor? feature.ladderColor : null, 232 | ladderLabel: feature.ladderLabel? feature.ladderLabel : null, 233 | ladderBgColor: feature.ladderBgColor? feature.ladderBgColor : null, 234 | ladderBorderColor: feature.ladderBorderColor? feature.ladderBorderColor : null, 235 | yMin: feature.yMin? feature.yMin : null, 236 | yMax: feature.yMax? feature.yMax : null 237 | }); 238 | 239 | if (feature.type === "rect") { 240 | 241 | this.preComputing.multipleRect(feature); 242 | this.rectangle(feature, this.commons.YPosition); 243 | 244 | } 245 | else if (feature.type === "text") { 246 | 247 | this.commons.scaling.range([5, this.commons.viewerOptions.width - 5]); 248 | let seq = this.displaySequence(this.commons.current_extend.length); 249 | if (seq === false) { 250 | this.sequenceLine(); 251 | } 252 | else if (seq === true) { 253 | this.sequence(feature.data, this.commons.YPosition); 254 | } 255 | //fillSVG.sequence(object.data, YPosition); 256 | 257 | } 258 | else if (feature.type === "unique") { 259 | 260 | this.unique(feature, this.commons.YPosition); 261 | // this.commons.YPosition += 5; 262 | 263 | } 264 | else if (feature.type === "circle") { 265 | 266 | this.circle(feature, this.commons.YPosition); 267 | // this.commons.YPosition += 5; 268 | 269 | } 270 | else if (feature.type === "multipleRect") { 271 | 272 | this.preComputing.multipleRect(feature); 273 | this.multipleRect(feature, this.commons.YPosition, this.commons.level); 274 | this.commons.YPosition += (this.commons.level - 1) * 10; 275 | 276 | } 277 | else if (feature.type === "path") { 278 | 279 | // this type of object overwrites object data, after fillSVG go back to original 280 | this.storeData = feature.data; 281 | this.preComputing.path(feature); 282 | this.path(feature, this.commons.YPosition - 8); 283 | feature.data = this.storeData; 284 | // this.commons.YPosition += this.commons.pathLevel; 285 | 286 | } 287 | else if (feature.type === "curve") { 288 | 289 | // this type of object overwrites object data, after fillSVG go back to original 290 | this.storeData = feature.data; 291 | if (!(Array.isArray(feature.data[0]))) feature.data = [feature.data]; 292 | if (!(Array.isArray(feature.color))) feature.color = [feature.color]; 293 | let negativeNumbers = false; 294 | feature.data.forEach((d) => { 295 | if (d.filter((l) => { 296 | return l.y < 0 297 | }).length) negativeNumbers = true; 298 | }); 299 | this.preComputing.preComputingLine(feature); 300 | 301 | this.fillSVGLine(feature, this.commons.YPosition); 302 | feature.data = this.storeData; 303 | this.commons.YPosition += this.commons.step // feature.pathLevel; // 7 304 | // this.commons.YPosition += negativeNumbers ? feature.pathLevel - 5 : 0; 305 | 306 | } 307 | else if (feature.type === "lollipop") { 308 | this.commons.YPosition += 7; 309 | this.lollipop(feature, this.commons.YPosition); 310 | 311 | } 312 | } 313 | 314 | public tagArea(object, thisYPosition) { 315 | 316 | // var threeArray = [showDisorderContentTag, showViewerTag, showLinkTag]; 317 | 318 | // adjust height not triangle 319 | if (object.type !== 'rect') {thisYPosition -= 5} 320 | 321 | let id = 't' + object.id + "_tagarea"; 322 | let featureArea = this.commons.tagsContainer.append("g") 323 | .attr("class", "tagGroup") 324 | .attr("id", id) 325 | .attr("transform", "translate(0," + thisYPosition + ")"); 326 | 327 | // ad areas in any case 328 | if (object.sidebar) { 329 | 330 | let objectPos = 0; 331 | // check type and add html elements accordingly 332 | for (const bt of object.sidebar) { 333 | if (bt.type) { 334 | if (bt.type !== "button" && bt.type !== "percentage" && bt.type !== "link" && bt.type !== "icon") { 335 | this.commons.logger.error("Unknown type of button", {method:'addFeatures',fvId:this.commons.divId,featureId:object.id,buttonId:bt.buttonId}) 336 | } else { 337 | 338 | let gButton = featureArea 339 | .append('g') 340 | .attr("id", id + '_button_' + bt.id) 341 | .attr("transform", "translate(" + objectPos + ",0)") 342 | .data([{ 343 | label: object.label, 344 | featureId: object.id, 345 | data: object, 346 | type: "button", 347 | id: bt.id, 348 | tooltip: bt.tooltip 349 | }]); 350 | 351 | let content; 352 | if (bt.type == "button") { 353 | let cl = this.hexToRgb(this.commons.viewerOptions.flagColor); 354 | let col = 'rgba(' + [cl.b, cl.g, cl.r].join(',') + ')' 355 | let colalph = 'rgba(' + [cl.b, cl.g, cl.r].join(',') + ',0.8)' 356 | let coltext = this.isLight(col) ? 'black' : 'white'; 357 | content = `` 358 | } 359 | else if (bt.type == "percentage") { 360 | let disordersString = bt.label.toString() + '%'; 361 | let colorrgb = this.hexToRgb(this.gradientColor(bt.label)); 362 | let color = 'rgba(' + [colorrgb.r, colorrgb.g, colorrgb.b].join(',') + ')'; 363 | let textColor = this.isLight(color) ? "black" : "white"; 364 | content = `` 365 | } 366 | else if (bt.type == "link") { 367 | let linkicon = "M9.26 13c-0.167-0.286-0.266-0.63-0.266-0.996 0-0.374 0.103-0.724 0.281-1.023l-0.005 0.009c1.549-0.13 2.757-1.419 2.757-2.99 0-1.657-1.343-3-3-3-0.009 0-0.019 0-0.028 0l0.001-0h-4c-1.657 0-3 1.343-3 3s1.343 3 3 3v0h0.080c-0.053 0.301-0.083 0.647-0.083 1s0.030 0.699 0.088 1.036l-0.005-0.036h-0.080c-2.761 0-5-2.239-5-5s2.239-5 5-5v0h4c0.039-0.001 0.084-0.002 0.13-0.002 2.762 0 5.002 2.239 5.002 5.002 0 2.717-2.166 4.927-4.865 5l-0.007 0zM10.74 7c0.167 0.286 0.266 0.63 0.266 0.996 0 0.374-0.103 0.724-0.281 1.023l0.005-0.009c-1.549 0.13-2.757 1.419-2.757 2.99 0 1.657 1.343 3 3 3 0.009 0 0.019-0 0.028-0l-0.001 0h4c1.657 0 3-1.343 3-3s-1.343-3-3-3v0h-0.080c0.053-0.301 0.083-0.647 0.083-1s-0.030-0.699-0.088-1.036l0.005 0.036h0.080c2.761 0 5 2.239 5 5s-2.239 5-5 5v0h-4c-0.039 0.001-0.084 0.002-0.13 0.002-2.762 0-5.002-2.239-5.002-5.002 0-2.717 2.166-4.927 4.865-5l0.007-0z" 368 | content = `` 369 | } 370 | else if (bt.type == "icon") { 371 | content = `` 372 | } 373 | 374 | gButton 375 | .append('foreignObject') // foreignObject can be styled with no limitation by user 376 | .attr("width", "100%") 377 | .attr("height", "100%") 378 | .attr("y",-6) 379 | .html(content) 380 | 381 | if (bt.type !== "percentage") { 382 | gButton.call(this.commons.d3helper.genericTooltip(bt)); 383 | } 384 | 385 | // update object position 386 | objectPos += (d3.select('#'+bt.id).node()).getBoundingClientRect().width + 3; 387 | 388 | } 389 | } 390 | else if (bt.content) { 391 | 392 | let gHtml = featureArea 393 | .append('g') 394 | .attr("id", id + '_button_' + bt.id) 395 | .attr("transform", "translate(" + (objectPos + 3) + ",0)") 396 | .data([{ 397 | label: object.label, 398 | featureId: object.id, 399 | data: object, 400 | type: "button", 401 | id: bt.id 402 | }]); 403 | 404 | gHtml 405 | .append('foreignObject') 406 | .attr("y", -6) 407 | .attr("width", "100%") 408 | .attr("height", "100%") 409 | .attr("height", "100%") 410 | .append('xhtml:body') 411 | .style("margin", "0") 412 | .attr("id", bt.id) 413 | .html(bt.content) 414 | .call(this.commons.d3helper.genericTooltip(bt)); 415 | 416 | // objectPos += 50; 417 | // get width of the drawn object 418 | try { 419 | let contentwidth = 0; 420 | if (bt.width) 421 | contentwidth = bt.width; 422 | else 423 | contentwidth = (d3.select(`#${bt.id}`).select('*').node()).getBoundingClientRect().width 424 | objectPos += contentwidth + 5; 425 | } catch (e) { 426 | objectPos += 100; 427 | } 428 | 429 | } else { 430 | this.commons.logger.error("Neither html content nor type of button is specified", {method:'addFeatures',fvId:this.commons.divId,featureId:object.id,buttonId:bt.buttonId}) 431 | } 432 | } 433 | 434 | } 435 | } 436 | 437 | public sequence(seq, start = 0) { 438 | // remove eventual sequence still there (in transitions) 439 | this.commons.svgContainer.selectAll(".mySequence").remove(); 440 | //Create group of sequence 441 | let sequenceAAs = this.commons.svgContainer.append("g").attr("class", "mySequence sequenceGroup"); 442 | sequenceAAs 443 | .selectAll(".AA") 444 | .data(seq) 445 | .enter() 446 | .append("text") 447 | //.attr("clip-path", "url(#clip)") // firefox compatibility 448 | .attr("class", "AA") 449 | .attr("text-anchor", "middle") 450 | .attr("x", (d, i) => { 451 | // index starts from 0 452 | return this.commons.scaling.range([2, this.commons.viewerOptions.width - 2])(i + start) 453 | }) 454 | .attr("y", this.commons.step) 455 | .attr("font-size", "12px") 456 | .attr("font-family", "monospace") 457 | .text((d) => { 458 | return d 459 | 460 | }) 461 | 462 | } 463 | 464 | public sequenceLine() { 465 | // remove eventual sequence already there (in transitions) 466 | this.commons.svgContainer.selectAll(".mySequence").remove(); 467 | //Create line to represent the sequence 468 | if (this.commons.viewerOptions.dottedSequence) { 469 | let dottedSeqLine = this.commons.svgContainer.selectAll(".sequenceLine") 470 | .data([[{x: 1, y: this.commons.step - this.commons.elementHeight / 2}, { 471 | x: this.commons.fvLength, 472 | y: this.commons.step - this.commons.elementHeight / 2 473 | }]]) 474 | //.scale(scaling) 475 | .enter() 476 | .append("path") 477 | // .attr("clip-path", "url(#clip)") // firefox compatibility 478 | .attr("d", this.commons.line) 479 | .attr("class", "mySequence sequenceLine") 480 | .style("z-index", "0") 481 | .style("stroke", "grey") 482 | .style("stroke-dasharray", "1,3") 483 | .style("stroke-width", "1px") 484 | .style("stroke-opacity", 0); 485 | 486 | dottedSeqLine 487 | .transition() 488 | .duration(500) 489 | .style("stroke-opacity", 1); 490 | } 491 | } 492 | 493 | public rectangle(object, position) { 494 | 495 | //var rectShift = 20; 496 | if (!object.height) object.height = this.commons.elementHeight; 497 | let rectHeight = this.commons.elementHeight; 498 | 499 | let rectShift = rectHeight + rectHeight / 3; 500 | let lineShift = rectHeight / 2 - 6; 501 | position = Number(position) + 3; // center line 502 | 503 | let rectsPro = this.commons.svgContainer.append("g") 504 | .attr("class", "rectangle featureLine") 505 | //.attr("clip-path", "url(#clip)") // firefox compatibility 506 | .attr("transform", "translate(0," + position + ")") 507 | .attr("id", () => { 508 | // random string 509 | // return divId + '_' + d.title.split(" ").join("_") + '_g' 510 | return 'c' + object.id + '_container' 511 | }); 512 | // commenting to dist 513 | 514 | let dataLine = []; 515 | // case with empty data 516 | if (!this.commons.level) { 517 | this.commons.level = 1 518 | } 519 | for (let i = 0; i < this.commons.level; i++) { 520 | dataLine.push([{ 521 | x: 1, 522 | y: (i * rectShift + lineShift), 523 | }, { 524 | x: this.commons.fvLength, 525 | y: (i * rectShift + lineShift) 526 | }]); 527 | } 528 | 529 | rectsPro.selectAll(".line " + object.className) 530 | .data(dataLine) 531 | .enter() 532 | .append("path") 533 | .attr("d", this.commons.line) 534 | .attr("class", () => { 535 | return "line " + object.className 536 | }) 537 | .style("z-index", "0") 538 | .style("stroke", object.color) 539 | .style("stroke-width", "1px"); 540 | 541 | 542 | let rectsProGroup = rectsPro.selectAll("." + object.className + "Group") 543 | .data(object.data) 544 | .enter() 545 | .append("g") 546 | .attr("class", object.className + "Group") 547 | .attr("transform", (d) => { 548 | return "translate(" + this.rectX(d) + ",0)" 549 | }); 550 | 551 | rectsProGroup 552 | .append("rect") 553 | .attr("class", "element " + object.className) 554 | .attr("id", (d) => { 555 | // add id to object 556 | let id = "f_" + object.id + '_' + d.x + '-' + d.y; 557 | d.id = id; 558 | return id; 559 | }) 560 | .attr("y", (d) => { 561 | return d.level * rectShift 562 | }) 563 | .attr("ry", (d) => { 564 | return this.commons.radius; 565 | }) 566 | .attr("rx", (d) => { 567 | return this.commons.radius; 568 | }) 569 | .attr("width", (d) => { 570 | return this.rectWidth2(d) 571 | }) 572 | .attr("height", this.commons.elementHeight) 573 | .style("fill", (d) => { 574 | return d.color || object.color 575 | }) 576 | .style("fill-opacity", (d) => { 577 | if (d.opacity) { 578 | return d.opacity 579 | } else if (object.opacity) { 580 | return object.opacity 581 | } else { 582 | return "0.6" 583 | } 584 | }) 585 | .style("stroke", (d) => { 586 | if ("stroke" in d) { 587 | return d.stroke 588 | } else if ("stroke" in object) { 589 | return object.stroke 590 | } else { 591 | return d.color 592 | } 593 | }) 594 | .style("z-index", "13") 595 | .call(this.commons.d3helper.tooltip(object)); 596 | 597 | 598 | rectsProGroup 599 | .append("text") 600 | .attr("class", "element " + object.className + "Text") 601 | .attr("y", (d) => { 602 | return d.level * rectShift + rectHeight / 2 603 | }) 604 | .attr("dy", "0.35em") 605 | .style("font-size", "10px") 606 | .text((d) => { 607 | return d.label 608 | }) 609 | // .style("fill", "rgba(39, 37, 37, 0.9)") 610 | // .style("z-index", "15") 611 | .style("visibility", (d) => { 612 | if (d.label) { 613 | return (this.commons.scaling(d.y) - this.commons.scaling(d.x)) > d.label.length * 8 && object.height > 11 ? "visible" : "hidden"; 614 | } else return "hidden"; 615 | }) 616 | .call(this.commons.d3helper.tooltip(object)); 617 | 618 | this.forcePropagation(rectsProGroup); 619 | let uniqueShift = rectHeight > 12 ? rectHeight - 6 : 0; 620 | this.commons.YPosition += this.commons.level < 2 ? uniqueShift : (this.commons.level - 1) * rectShift + uniqueShift; 621 | 622 | } 623 | 624 | public unique(object, position) { 625 | 626 | let rectsPro = this.commons.svgContainer.append("g") 627 | .attr("class", "uniquePosition featureLine") 628 | .attr("transform", "translate(0," + position + ")") 629 | .attr("id", () => { 630 | // random string' 631 | return 'c' + object.id + '_container' 632 | }); 633 | 634 | let dataLine = []; 635 | // case with empty data 636 | if (!this.commons.level) { 637 | this.commons.level = 1 638 | } 639 | for (let i = 0; i < this.commons.level; i++) { 640 | dataLine.push([{ 641 | x: 1, 642 | y: 0, 643 | }, { 644 | x: this.commons.fvLength, 645 | y: 0 646 | }]); 647 | } 648 | 649 | rectsPro.selectAll(".line " + object.className) 650 | .data(dataLine) 651 | .enter() 652 | .append("path") 653 | .attr("d", this.commons.line) 654 | .attr("class", () => { 655 | return "line " + object.className 656 | }) 657 | .style("z-index", "0") 658 | .style("stroke", object.color) 659 | .style("stroke-width", "1px"); 660 | 661 | let readyData = [...object.data]; 662 | 663 | rectsPro.selectAll("." + object.className + 'Unique') 664 | .data(readyData) 665 | .enter() 666 | .append("rect") 667 | // .attr("clip-path", "url(#clip)") // firefox compatibility 668 | .attr("class", "element " + object.className) 669 | .attr("id", (d) => { 670 | let id = "f_" + object.id + '_' + d.x + '-' + d.y; 671 | d.id = id; 672 | return id; 673 | }) 674 | .attr("x", (d) => { 675 | return this.commons.scaling(d.x - 0.4) 676 | }) 677 | .attr("width", (d) => { 678 | if (this.commons.scaling(d.x + 0.4) - this.commons.scaling(d.x - 0.4) < 2) return 2; 679 | else return this.commons.scaling(d.x + 0.4) - this.commons.scaling(d.x - 0.4); 680 | }) 681 | .attr("height", this.commons.elementHeight) 682 | .style("fill", (d) => { 683 | return d.color || object.color 684 | }) 685 | .style("z-index", "3") 686 | .call(this.commons.d3helper.tooltip(object)); 687 | 688 | this.forcePropagation(rectsPro); 689 | } 690 | 691 | public lollipop(object, position) { 692 | let circlesPro = this.commons.svgContainer.append("g") 693 | .attr("class", "pointPosition featureLine") 694 | .attr("transform", "translate(0," + position + ")") 695 | .attr("id", () => { 696 | // random string 697 | // return divId + '_' + d.title.split(" ").join("_") + '_g' 698 | return 'c' + object.id + '_container' 699 | }); 700 | 701 | let dataLine = []; 702 | dataLine.push([{ 703 | x: 1, 704 | y: 0 705 | }, { 706 | x: this.commons.fvLength, 707 | y: 0 708 | }]); 709 | 710 | // basal line 711 | circlesPro.selectAll(".line " + object.className) 712 | .data(dataLine) 713 | .enter() 714 | .append("path") 715 | .attr("d", this.commons.line) 716 | .attr("class", () => { 717 | return "line " + object.className 718 | }) 719 | .style("z-index", "0") 720 | .style("stroke", 'gray') 721 | .style("stroke-width", "0.5px"); 722 | 723 | let readyData = [...object.data]; 724 | 725 | // lollipop base 726 | circlesPro.selectAll("." + object.className + 'Lollipop') 727 | .data(readyData) 728 | .enter() 729 | .append("line") 730 | .attr("x1", (d) => {return this.commons.scaling(d.x)}) 731 | .attr("x2", (d) => {return this.commons.scaling(d.x)}) 732 | .attr("y2", (d) => { 733 | let w = this.commons.scaling(d.x + 0.4) - this.commons.scaling(d.x - 0.4); 734 | if (this.commons.scaling(d.x + 0.4) - this.commons.scaling(d.x - 0.4) < 2) w = 2; 735 | return w + 2; 736 | }) 737 | .attr("y1", -8) 738 | .attr("class", "lineElement " + object.className) 739 | .style("stroke", (d) => { 740 | return d.color || object.color 741 | }) 742 | .style("stroke-width", 1) 743 | 744 | // lollipop head 745 | circlesPro.selectAll("." + object.className + 'Lollipop') 746 | .data(readyData) 747 | .enter() 748 | .append("circle") 749 | //.attr("clip-path", "url(#clip)") 750 | .attr("class", "element " + object.className) 751 | .attr("id", (d) => { 752 | return "f_" + object.id; 753 | }) 754 | // circle dimensions 755 | .attr("cx", (d) => { 756 | return this.commons.scaling(d.x) 757 | }) 758 | .attr("cy", "-8") // same as height 759 | // circle radius 760 | .attr("r", (d) => { 761 | if (d.y<=1) { 762 | return d.y*this.commons.elementHeight*0.5 763 | } else { 764 | this.commons.logger.warn("Maximum circle radius is 1", {method:'addFeatures',fvId:this.commons.divId,featureId:object.id}) 765 | return this.commons.elementHeight*0.5 766 | } 767 | }) 768 | .attr("width", (d) => { 769 | let w = this.commons.scaling(d.x + 0.4) - this.commons.scaling(d.x - 0.4); 770 | if (this.commons.scaling(d.x + 0.4) - this.commons.scaling(d.x - 0.4) < 2) w = 2; 771 | return w; 772 | }) 773 | .style("fill", (d) => { 774 | return d.color || object.color 775 | }) 776 | .style("fill-opacity", (d) => { 777 | if (d.opacity) { 778 | return d.opacity 779 | } else if (object.opacity) { 780 | return object.opacity 781 | } else { 782 | return "1" 783 | } 784 | }) 785 | .style("stroke", (d) => { 786 | if ("stroke" in d) { 787 | return d.stroke 788 | } else if ("stroke" in object) { 789 | return object.stroke 790 | } else { 791 | return d.color 792 | } 793 | }) 794 | .call(this.commons.d3helper.tooltip(object)); 795 | 796 | this.forcePropagation(circlesPro); 797 | } 798 | 799 | public circle(object, position) { 800 | let circlesPro = this.commons.svgContainer.append("g") 801 | .attr("class", "pointPosition featureLine") 802 | .attr("transform", "translate(0," + position + ")") 803 | .attr("id", () => { 804 | // random string 805 | // return divId + '_' + d.title.split(" ").join("_") + '_g' 806 | return 'c' + object.id + '_container' 807 | }); 808 | 809 | let dataLine = []; 810 | dataLine.push([{ 811 | x: 1, 812 | y: 0 813 | }, { 814 | x: this.commons.fvLength, 815 | y: 0 816 | }]); 817 | 818 | // basal line 819 | circlesPro.selectAll(".line " + object.className) 820 | .data(dataLine) 821 | .enter() 822 | .append("path") 823 | .attr("d", this.commons.line) 824 | .attr("class", "line " + object.className) 825 | .style("z-index", "0") 826 | .style("stroke", 'grey') 827 | .style("stroke-width", "0.5px"); 828 | 829 | let readyData = [...object.data]; 830 | 831 | circlesPro.selectAll("." + object.className + 'Circle') 832 | .data(readyData) 833 | .enter() 834 | .append("circle") 835 | //.attr("clip-path", "url(#clip)") 836 | .attr("class", "element " + object.className) 837 | .attr("id", (d) => { 838 | return "f_" + object.id; 839 | }) 840 | // circle dimensions 841 | .attr("cx", (d) => { 842 | return this.commons.scaling(d.x) 843 | }) 844 | .attr("cy", "5") // same as height 845 | // circle radius 846 | .attr("r", (d) => { 847 | if (d.y<=1) { 848 | return d.y*this.commons.elementHeight 849 | } else { 850 | this.commons.logger.warn("Maximum circle radius is 1", {method:'addFeatures',fvId:this.commons.divId,featureId:object.id}) 851 | return this.commons.elementHeight 852 | } 853 | }) 854 | .attr("width", (d) => { 855 | if (this.commons.scaling(d.x + 0.4) - this.commons.scaling(d.x - 0.4) < 2) return 2; 856 | else return this.commons.scaling(d.x + 0.4) - this.commons.scaling(d.x - 0.4); 857 | }) 858 | .style("fill", (d) => { 859 | return d.color || object.color 860 | }) 861 | .style("fill-opacity", (d) => { 862 | if (d.opacity) { 863 | return d.opacity 864 | } else if (object.opacity) { 865 | return object.opacity 866 | } else { 867 | return "1" 868 | } 869 | }) 870 | .style("stroke", (d) => { 871 | if ("stroke" in d) { 872 | return d.stroke 873 | } else if ("stroke" in object) { 874 | return object.stroke 875 | } else { 876 | return d.color 877 | } 878 | }) 879 | .call(this.commons.d3helper.tooltip(object)); 880 | 881 | this.forcePropagation(circlesPro); 882 | } 883 | 884 | public path(object, position) { 885 | 886 | if (!object.height) object.height = this.commons.elementHeight; 887 | let pathHeight = this.commons.elementHeight; 888 | 889 | let pathsDB = this.commons.svgContainer.append("g") 890 | .attr("class", "pathing featureLine") 891 | //.attr("clip-path", "url(#clip)") // firefox compatibility 892 | .attr("transform", "translate(0," + position + ")") 893 | .attr("id", () => { 894 | // random string 895 | // return divId + '_' + d.title.split(" ").join("_") + '_g' 896 | return 'c' + object.id + '_container' 897 | }); 898 | 899 | let dataLine = []; 900 | dataLine.push([{ 901 | x: 1, 902 | y: 0 903 | }, { 904 | x: this.commons.fvLength, 905 | y: 0 906 | }]); 907 | 908 | pathsDB.selectAll(".line " + object.className) 909 | .data(dataLine) 910 | .enter() 911 | .append("path") 912 | .attr("d", this.commons.line) 913 | .attr("class", "line " + object.className) 914 | .style("z-index", "0") 915 | .style("stroke", 'grey') 916 | .style("stroke-width", "0.5px"); 917 | 918 | pathsDB.selectAll(".path" + object.className) 919 | .data(object.data) 920 | .enter() 921 | .append("path") 922 | //.attr("clip-path", "url(#clip)") // firefox compatibility 923 | .attr("class", "element " + object.className) 924 | .attr("id", (d) => { 925 | return "f_" + d[0].id + Math.random().toString(36).substring(7); 926 | }) 927 | .attr("d", this.commons.lineBond) 928 | .style("fill", "none") 929 | .style("stroke", (d) => { 930 | return d[0].color || object.color 931 | }) 932 | .style("z-index", "3") 933 | .style("stroke-width", (d) => { 934 | return d[0].opacity || object.opacity 935 | }) 936 | .call(this.commons.d3helper.tooltip(object)); 937 | 938 | this.forcePropagation(pathsDB); 939 | // re-init object.heigth 940 | object.height = this.commons.step / 2; 941 | 942 | } 943 | 944 | public fillSVGLine(object, position = 0) { 945 | // if (!object.interpolation) object.interpolation = "curveBasis"; // TODO: not sensitive to interpolation now 946 | if (object.fill === undefined) object.fill = true; 947 | let histoG = this.commons.svgContainer.append("g") 948 | // necessary id to get height when placing tags 949 | .attr("id", () => {return 'c' + object.id + '_container'}) 950 | .attr("class", "lining featureLine") 951 | .attr("transform", "translate(0," + position + ")") 952 | .attr("heigth", object.curveHeight); 953 | 954 | let dataLine = []; 955 | dataLine.push([{ 956 | x: 1, 957 | y: 0 958 | }, { 959 | x: this.commons.fvLength, 960 | y: 0 961 | }]); 962 | 963 | /*histoG.selectAll(".line " + object.className) 964 | .data(dataLine) 965 | .enter() 966 | .append("path") 967 | .attr("clip-path", "url(#clip)") 968 | .attr("d", this.commons.lineBond) 969 | .attr("class", "line " + object.className) 970 | .style("z-index", "0") 971 | .style("stroke", "black") 972 | .style("stroke-width", "1px");*/ 973 | // interpolate 974 | //this.commons.lineGen().curve(object.interpolation) 975 | 976 | object.data.forEach((dd, i) => { 977 | 978 | histoG.selectAll("." + object.className + i) 979 | .data(dd) 980 | .enter() 981 | .append("path") 982 | //.attr("clip-path", "url(#clip)") // firefox compatibility 983 | .attr("class", "element " + object.className + " " + object.className + i) 984 | // d3 v4 985 | .attr("d", this.commons.lineGen.y((d) => { 986 | return this.commons.lineYScale(-d.y) * 10 + object.shift; 987 | }) 988 | ) 989 | //.style("fill", object.fill ? this.shadeBlendConvert(0.6, object.color[i]) || this.shadeBlendConvert(0.6, "#000") : "none") 990 | .style("fill", object.color) 991 | .style("fill-opacity", "0.8") 992 | .style("stroke", object.color[i] || "#000") 993 | .style("z-index", "3") 994 | .style("stroke-width", "2px") 995 | .call(this.commons.d3helper.tooltip(object)); 996 | 997 | }); 998 | 999 | this.forcePropagation(histoG); 1000 | } 1001 | 1002 | public multipleRect(object, position = 0, level = this.commons.level) { 1003 | let rectHeight = 8; 1004 | let rectShift = 10; 1005 | 1006 | let rects = this.commons.svgContainer.append("g") 1007 | .attr("class", "multipleRects featureLine") 1008 | .attr("transform", "translate(0," + position + ")"); 1009 | 1010 | for (let i = 0; i < level; i++) { 1011 | rects.append("path") 1012 | .attr("d", this.fillSVGLine([{ 1013 | x: 1, 1014 | y: (i * rectShift - 2) 1015 | }, { 1016 | x: this.commons.fvLength, 1017 | y: (i * rectShift - 2) 1018 | }])) 1019 | .attr("class", () => { 1020 | return "line " + object.className 1021 | }) 1022 | .style("z-index", "0") 1023 | .style("stroke", object.color) 1024 | .style("stroke-width", "1px"); 1025 | } 1026 | 1027 | rects.selectAll("." + object.className) 1028 | .data(object.data) 1029 | .enter() 1030 | .append("rect") 1031 | //.attr("clip-path", "url(#clip)") // firefox compatibility 1032 | .attr("class", "element " + object.className) 1033 | .attr("id", (d) => { 1034 | return "f_" + object.id + Math.random().toString(36).substring(7); 1035 | }) 1036 | .attr("x", (d) => { 1037 | return this.commons.scaling(d.x); 1038 | }) 1039 | .attr("y", (d) => { 1040 | return d.level * rectShift; 1041 | }) 1042 | .attr("ry", (d) => { 1043 | return this.commons.radius; 1044 | }) 1045 | .attr("rx", (d) => { 1046 | return this.commons.radius; 1047 | }) 1048 | .attr("width", (d) => { 1049 | return (this.commons.scaling(d.y) - this.commons.scaling(d.x)); 1050 | }) 1051 | .attr("height", rectHeight) 1052 | .style("fill", (d) => { 1053 | return d.color || object.color 1054 | }) 1055 | .style("z-index", "13") 1056 | .call(this.commons.d3helper.tooltip(object)); 1057 | 1058 | this.forcePropagation(rects); 1059 | } 1060 | 1061 | // AXIS FUNCTIONS 1062 | public reset_axis() { 1063 | if (this.commons.animation) { 1064 | this.commons.svgContainer.transition().duration(500); 1065 | } 1066 | this.commons.svgContainer 1067 | .select(".x.axis") 1068 | .call(this.commons.xAxis); 1069 | } 1070 | 1071 | public addXAxis(position) { 1072 | this.commons.svgContainer.append("g") 1073 | .attr("class", "x axis XAxis") 1074 | .attr("transform", "translate(0," + (position + 20) + ")") 1075 | .call(this.commons.xAxis); 1076 | if (!this.commons.viewerOptions.showAxis) { 1077 | d3.select(`#${this.commons.divId}`).selectAll(".tick") 1078 | .attr("display", "none") 1079 | } 1080 | }; 1081 | 1082 | public updateXAxis(position) { 1083 | this.commons.svgContainer.selectAll(".XAxis") 1084 | .attr("transform", "translate(0," + (position + this.commons.step) + ")"); 1085 | }; 1086 | 1087 | // BRUSH FUNCTION 1088 | 1089 | public resizeBrush() { 1090 | 1091 | if (this.commons.svgContainer) { 1092 | if (this.commons.svgContainer.node() !== null) { 1093 | let rectArea = this.commons.svgContainer.node().getBoundingClientRect(); 1094 | let thisbrush = this.commons.svgContainer.select(".brush"); 1095 | thisbrush.select("rect") 1096 | .attr('height', rectArea.height) 1097 | .attr('width', rectArea.width); 1098 | } 1099 | } 1100 | }; 1101 | 1102 | public addBrush() { 1103 | 1104 | this.commons.svgContainer 1105 | .append("g") 1106 | .attr("class", "brush") 1107 | .attr("id", "fvbrush") 1108 | .call(this.commons.brush) 1109 | //.call(this.commons.brush.move, this.commons.scaling()); 1110 | this.resizeBrush() 1111 | 1112 | }; 1113 | 1114 | // TOOLBAR ON TOP 1115 | 1116 | public showHelp() { 1117 | 1118 | /*let helpContent = "
To zoom in : Left click to select area of interest
" + 1119 | "
To zoom out : Right click to reset the scale
" + 1120 | "
Zoom max : Limited to " + this.commons.zoomMax.toString() + " " + this.commons.viewerOptions.unit + "
";*/ 1121 | let helpContent = "To zoom in : Left click to select area of interest\n To zoom out : Right click to reset the scale\n Zoom max : Limited to " + 1122 | this.commons.viewerOptions.zoomMax.toString(); 1123 | 1124 | alert(helpContent) 1125 | } 1126 | 1127 | 1128 | constructor(commons: {}) { 1129 | super(commons); 1130 | this.preComputing = new PreComputing(commons); 1131 | } 1132 | } 1133 | 1134 | export default FillSVG; 1135 | -------------------------------------------------------------------------------- /src/helper.ts: -------------------------------------------------------------------------------- 1 | 2 | import {event as currentEvent} from 'd3-selection'; 3 | 4 | class ComputingFunctions { 5 | 6 | public commons; 7 | 8 | public rectWidth2 = (d) => { 9 | /*if (d.x === d.y) { 10 | if (this.commons.scaling([d.x + 0.4]) - this.commons.scaling([d.x - 0.4]) < 2) return 2; 11 | else return this.commons.scaling([d.x + 0.4]) - this.commons.scaling([d.x - 0.4]); 12 | }*/ 13 | return (this.commons.scaling([d.y + 0.4])- this.commons.scaling([d.x - 0.4])); 14 | }; 15 | 16 | public rectX = (object) => { 17 | /*if (object.x === object.y) { 18 | return this.commons.scaling([object.x - 0.4]); 19 | }*/ 20 | let scale = this.commons.scaling([object.x - 0.4]); 21 | //if (scale<0) {scale = 0}; 22 | return scale 23 | }; 24 | 25 | protected displaySequence(seq) { 26 | return this.commons.viewerOptions.width / seq > 5; 27 | } 28 | 29 | protected gradientColor(stringContent) { 30 | let percent = Number(stringContent); 31 | //value from 0 to 100 32 | //let hue = ((1 - percent) * 120).toString(10); 33 | let hue; 34 | if (percent === 0) { 35 | hue = '#ffffff' 36 | } else if (percent > 0 && percent <= 15) { 37 | hue = '#d2d2f8' 38 | } else if (percent > 15 && percent < 50) { 39 | hue = '#a6a6f1' 40 | } else if (percent >= 50 && percent < 100) { 41 | hue = '#7a7aeb' 42 | } else if (percent === 100) { 43 | hue = '#4e4ee4' 44 | } 45 | return hue; 46 | //return ["hsl(", hue, ",100%,50%)"].join(""); 47 | }; 48 | 49 | protected forcePropagation(item) { 50 | item.on('mousedown', () => { 51 | let new_click_event = new Event('mousedown'); 52 | new_click_event['pageX'] = currentEvent.layerX; 53 | new_click_event['clientX'] = currentEvent.clientX; 54 | new_click_event['pageY'] = currentEvent.layerY; 55 | new_click_event['clientY'] = currentEvent.clientY; 56 | }); 57 | } 58 | 59 | constructor(commons: {}) { 60 | this.commons = commons; 61 | } 62 | } 63 | 64 | export default ComputingFunctions 65 | -------------------------------------------------------------------------------- /src/interfaces.ts: -------------------------------------------------------------------------------- 1 | 2 | export interface UserOptions { 3 | offset?: { 4 | start: number, 5 | end: number 6 | }, 7 | breakpoint?: number, 8 | showAxis?: boolean, 9 | showSequence?: boolean, 10 | showSequenceLabel?: boolean, 11 | brushActive?: boolean, 12 | toolbar?: boolean, 13 | toolbarPosition?: string, 14 | zoomMax?: number, 15 | showSubFeatures?: boolean, 16 | flagColor?: string, 17 | flagTrack?: number | string | boolean, 18 | flagTrackMobile?: number | string | boolean, 19 | sideBar?: number | string | boolean, 20 | animation?: boolean, 21 | unit?: string, 22 | backgroundcolor?: string, 23 | maxDepth?: number 24 | } 25 | 26 | export interface ViewerOptions { 27 | showSequence: boolean, 28 | showSequenceLabel?: boolean, 29 | brushActive: boolean, 30 | verticalLine: boolean, 31 | dottedSequence: boolean, 32 | offset?: { 33 | start: number, 34 | end: number 35 | }, 36 | tooltipColor: string, 37 | showHelper: boolean, 38 | flagColor: string, 39 | showSubFeatures: boolean, 40 | sideBar: boolean, 41 | labelTrackWidth: number, 42 | labelTrackWidthMobile: number, 43 | tagsTrackWidth: number, 44 | maxDepth?: number, 45 | margin: { 46 | top: number, 47 | bottom: number, 48 | left: number, 49 | right: number 50 | }, 51 | backup: { 52 | labelTrackWidth: number, 53 | tagsTrackWidth: number, 54 | features: any 55 | }, 56 | width: number, 57 | height: number, 58 | zoomMax: number, 59 | mobileMode: boolean, 60 | unit: string, 61 | animation: boolean, 62 | toolbar: boolean, 63 | toolbarPosition?: string, 64 | bubbleHelp: boolean, 65 | showAxis: boolean, 66 | positionWithoutLetter: number, 67 | drawLadder: boolean, 68 | ladderWidth: number, 69 | ladderHeight: number, 70 | ladderSpacing: number, 71 | labelSidebarWidth?: number 72 | } 73 | 74 | export interface FeaturesList extends Array{} 75 | 76 | export interface FeatureObject { 77 | id: string, 78 | type: string // "rect" | "path" | "curve" | "unique" | "circle" | "sequence" | "lollipop", 79 | data: Array | string, 80 | parentId?: any, 81 | label?: string, 82 | className?: string, 83 | height?: number, 84 | yLim?: number, 85 | color?: string, 86 | stroke?: string, 87 | opacity?: number, 88 | tooltip?: string, 89 | sidebar?: Array, 90 | isOpen?: boolean, 91 | flagLevel?: number, 92 | subfeatures?: Array 93 | } 94 | 95 | export interface FeatureData { 96 | x: number, 97 | y?: any, 98 | label?: string, 99 | className?: string, 100 | color?: string, 101 | stroke?: string, 102 | opacity?: number, 103 | tooltip?: string 104 | } 105 | 106 | export interface SideBarObject { 107 | id: string, 108 | tooltip?: string, // or html 109 | content?: string, 110 | type?: string, 111 | width?: number, 112 | label?: string | number 113 | } 114 | 115 | export interface FeatureViewerLogger { 116 | debug(primaryMessage: string, ...supportingData: any[]): void; 117 | warn(primaryMessage: string, ...supportingData: any[]): void; 118 | error(primaryMessage: string, ...supportingData: any[]): void; 119 | info(primaryMessage: string, ...supportingData: any[]): void; 120 | } 121 | 122 | export class FeatureViewerLog implements FeatureViewerLogger { 123 | 124 | public debug(msg: string, ...supportingDetails): void { 125 | this.emitLogMessage("debug", msg, supportingDetails) 126 | } 127 | 128 | public info(msg: string, ...supportingDetails): void { 129 | this.emitLogMessage("info", msg, supportingDetails) 130 | } 131 | 132 | public warn(msg: string, ...supportingDetails): void { 133 | this.emitLogMessage("warn", msg, supportingDetails) 134 | } 135 | 136 | public error(msg: string, ...supportingDetails): void { 137 | this.emitLogMessage("error", msg, supportingDetails) 138 | } 139 | 140 | private emitLogMessage(msgType:"debug"|"info"|"warn"|"error", msg:string, supportingDetails:any[]){ 141 | if(supportingDetails.length > 0) { 142 | console[msgType](msg, supportingDetails); 143 | } else { 144 | console[msgType](msg); 145 | } 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /src/styles/styles.scss: -------------------------------------------------------------------------------- 1 | 2 | @keyframes loadingcolorflag { 3 | 50% {fill: grey } 4 | } 5 | 6 | // non-svg objects in svg 7 | foreignObject > body { 8 | overflow: visible; 9 | } 10 | 11 | // header 12 | .helperButton { 13 | width: 20px; 14 | height: 20px; 15 | } 16 | .helperButton path { 17 | fill: rgba(39, 37, 37, 0.71); 18 | } 19 | 20 | // brush selection 21 | .selectionRect { 22 | fill: rgba(0,0,0,0.15); 23 | fill-opacity: .5; 24 | } 25 | .brush .selection{ 26 | stroke: #fff; 27 | fill: #4682b4; 28 | fill-opacity: .125; 29 | shape-rendering: crispEdges; 30 | display: block; 31 | height: 100%; 32 | } 33 | 34 | // fv buttons 35 | .mybutton { 36 | -moz-border-radius: 10px; 37 | -webkit-border-radius: 10px; 38 | border-radius: 10px; /* future proofing */ 39 | -khtml-border-radius: 10px; /* for old Konqueror browsers */ 40 | background: darkgrey; 41 | border: none; 42 | color: rgb(0,0,0); 43 | height: 30px; 44 | min-width: 36px; 45 | outline: none; 46 | text-decoration: none; 47 | text-align: center; 48 | line-height: 24px; 49 | vertical-align: middle; 50 | overflow: visible !important; 51 | } 52 | .mybuttonsquare { 53 | border-radius: 0px; 54 | font-size: 12px; 55 | height: 30px; 56 | cursor: default; 57 | margin: auto; 58 | min-width: 30px; 59 | width:70px; 60 | font-family:'Roboto'; 61 | font-size:10px; 62 | background: white; 63 | box-shadow: 0 2px 3px 0 rgba(0,0,0,0.12), 0 2px 2px 0 rgba(0,0,0,0.24); 64 | line-height: normal 65 | } 66 | .mybuttoncircle { 67 | border-radius: 50%; 68 | font-size: 12px; 69 | height: 36px; 70 | width: 36px; 71 | cursor: pointer; 72 | background: white; 73 | line-height: normal 74 | } 75 | // fv axis 76 | .axis { 77 | font: 14px sans-serif; 78 | } 79 | // fv flags 80 | .Arrow { 81 | fill-opacity: 0.6; 82 | } 83 | // fv tooltips 84 | .fvtooltip { 85 | position: absolute; 86 | text-align: center; 87 | color: white; 88 | width: auto; 89 | height: auto; 90 | padding: 2px; 91 | font-family: "Arial", sans-serif; 92 | font-size: 10px; 93 | font-weight: normal; 94 | background-color:rgba(0, 0, 0, 0.7); 95 | border: 0px; 96 | border-radius: 4px; 97 | pointer-events: none; 98 | 99 | } 100 | 101 | // fv custom tooltips 102 | .fvcustomtooltip { 103 | position: absolute; 104 | text-align: center; 105 | color: white; 106 | height: auto; 107 | background-color:rgba(0, 0, 0, 0.7); 108 | pointer-events: none; 109 | 110 | } 111 | 112 | 113 | // loading 114 | .pageoverlay { 115 | position: fixed; /* Sit on top of the page content */ 116 | width: 100%; /* Full width (cover the whole page) */ 117 | height: 100%; /* Full height (cover the whole page) */ 118 | top: 0; 119 | left: 0; 120 | right: 0; 121 | bottom: 0; 122 | background-color: rgba(0,0,0,0.3); /* Black background with opacity */ 123 | z-index: 2; /* Specify a stack order in case you're using a different order for other elements */ 124 | cursor: pointer; /* Add a pointer on hover */ 125 | } 126 | -------------------------------------------------------------------------------- /src/tooltip.ts: -------------------------------------------------------------------------------- 1 | 2 | import Calculate from "./calculate"; 3 | import * as d3 from './custom-d3'; 4 | 5 | class Tool extends Calculate { 6 | 7 | private calculate: Calculate; 8 | 9 | public colorSelectedFeat(feat, object, divId) { 10 | // remove previous selected features 11 | if (this.commons.featureSelected) { 12 | d3.select(`#${divId}`).select(`#${this.commons.featureSelected}`).style("fill-opacity", "0.6"); 13 | } 14 | // color selected rectangle 15 | if (object.type !== "path" && object.type !== "curve" && feat) { 16 | 17 | this.commons.featureSelected = feat; 18 | let thisfeat = d3.select(`#${divId}`).select(`#${feat}`); 19 | thisfeat.style("fill-opacity", "1"); 20 | 21 | if (object.type !== 'unique') { 22 | // color the background 23 | let currentContainer = this.commons.svgContainer.node().getBoundingClientRect(); 24 | 25 | let selectRect; 26 | d3.select(`#${divId}`).selectAll(".selectionRect").remove(); 27 | selectRect = this.commons.svgContainer 28 | .select(".brush") 29 | .append("rect") 30 | .attr("class", "selectionRect box-shadow") 31 | // add shadow? 32 | .attr("height", currentContainer.height) 33 | let thisy = this.getTransf((thisfeat.node()).parentElement)[0]; 34 | let myd3node = thisfeat.node(); 35 | let bcr = (myd3node).getBoundingClientRect().width; 36 | selectRect 37 | .style("display", "block") // remove display none 38 | .attr("width", bcr) // - shift from the beginning 39 | .attr("transform", () => { 40 | return "translate(" + thisy + ",0)" 41 | }) 42 | } 43 | } 44 | }; 45 | 46 | public colorSelectedCoordinates(start, end, divId) { 47 | 48 | } 49 | 50 | private updateLineTooltip(mouse, pD, scalingFunction, labelTrackWidth) { 51 | let xP = mouse - labelTrackWidth; 52 | let elemHover = ""; 53 | for (let l = 1; l < pD.length; l++) { 54 | let scalingFirst = scalingFunction(pD[l - 1].x); 55 | let scalingSecond = scalingFunction(pD[l].x); 56 | let halfway = (scalingSecond-scalingFirst)/2; 57 | if (scalingFirst+halfway < xP && scalingSecond+halfway > xP) { 58 | elemHover = pD[l]; 59 | break; 60 | } 61 | } 62 | return elemHover; 63 | }; 64 | 65 | private clickTagFunction(d) { 66 | // trigger tag_selected event: buttons clicked 67 | if (CustomEvent) { 68 | let event = new CustomEvent(this.commons.events.TAG_SELECTED_EVENT, { 69 | detail: d 70 | }); 71 | this.commons.svgElement.dispatchEvent(event); 72 | } else { 73 | console.warn("CustomEvent is not defined...."); 74 | } 75 | if (this.commons.trigger) this.commons.trigger(this.commons.events.TAG_SELECTED_EVENT, event); 76 | }; 77 | 78 | public initTooltip(div, divId) { 79 | 80 | let getMessage = (thing, type='default') => { 81 | 82 | // first line 83 | let tooltip_message = ''; 84 | 85 | // case of flags 86 | if (thing.hasOwnProperty('title')) { 87 | tooltip_message += '

'; 88 | tooltip_message += thing.title; 89 | tooltip_message += '

'; 90 | 91 | } else { 92 | 93 | if (thing.hasOwnProperty('x') || thing.hasOwnProperty('y')) { 94 | tooltip_message += '

'; 95 | tooltip_message += (+thing.x).toString(); 96 | if (+thing.y !== +thing.x) { 97 | if (type =='curve') { 98 | tooltip_message += ' - ' + (+thing.y).toFixed(2).toString() 99 | } else if (type == 'circle') { 100 | // pass 101 | } else { 102 | tooltip_message += ' - ' + (+thing.y).toString() 103 | } 104 | } 105 | tooltip_message += '

'; 106 | } 107 | // case of feature 108 | // if (thing.hasOwnProperty('tooltip')) { 109 | // tooltip_message += '

'; 110 | // tooltip_message += thing.tooltip; 111 | // tooltip_message += '

'; 112 | // } 113 | 114 | } 115 | 116 | return tooltip_message 117 | }; 118 | 119 | let drawTooltip = (tooltipDiv, absoluteMousePos) => { 120 | // angular material tooltip 121 | tooltipDiv 122 | .style('top', (absoluteMousePos[1] - 55) + 'px') 123 | .style("display", "block") 124 | .style('background-color', 'grey') 125 | .style('color', "#fff") 126 | .style('width', 'auto') 127 | .style('max-width', '170px') 128 | .style("height", "auto") 129 | .style('cursor', 'help') 130 | .style('pointer-events', 'none') 131 | .style('borderRadius', '2px') 132 | .style('overflow', 'hidden') 133 | .style('whiteSpace', 'nowrap') 134 | .style('textOverflow', 'ellipsis') 135 | .style('padding', '8px') 136 | .style('font', '10px sans-serif') 137 | .style('text-align', 'center') 138 | .style('position', 'absolute') // don't change this for compatibility to angular2 139 | .style('z-index', 45) 140 | .style('box-shadow', '0 1px 2px 0 #656565') 141 | .style('fontWeight', '500'); 142 | }; 143 | 144 | let scalingFunction = this.commons.scaling; 145 | let labelTrackWidth = this.commons.viewerOptions.labelTrackWidth; 146 | let updateLineTooltipFunction = this.updateLineTooltip; 147 | 148 | this.commons.d3helper = {}; 149 | // d3['helper'] = {}; 150 | 151 | this.commons.d3helper.flagTooltip = () => { 152 | 153 | let tooltipDiv = this.commons.tooltipDiv; 154 | let bodyNode = d3.select(div).node(); 155 | 156 | let tooltip = (selection) => { 157 | 158 | let absoluteMousePos; 159 | let drawMyTooltip = (pD) => { 160 | 161 | absoluteMousePos = d3.mouse(bodyNode); 162 | 163 | let left, top; 164 | left = absoluteMousePos[0].toString(); 165 | top = absoluteMousePos[1].toString(); 166 | 167 | // mobilemode labels overwrite tooltips 168 | if (this.commons.viewerOptions.mobileMode) { 169 | tooltipDiv.transition() 170 | .duration(200) 171 | .style("opacity", 1); 172 | tooltipDiv 173 | .html(pD['label'] || pD['id']) 174 | .style("left", left+'px') 175 | .style("top", top+'px'); 176 | } else if (pD['tooltip']) { 177 | tooltipDiv.transition() 178 | .duration(200) 179 | .style("opacity", 1); 180 | tooltipDiv 181 | .html(pD['tooltip']) 182 | .style("left", left+'px') 183 | .style("top", top+'px'); 184 | } 185 | }; 186 | 187 | selection 188 | // tooltip 189 | .on('mouseover.tooltip', (pD) => { 190 | // if (this.commons.viewerOptions.mobileMode) { 191 | drawMyTooltip(pD); 192 | // } 193 | }) 194 | .on('mousemove.tooltip', (pD) => { 195 | // if (this.commons.viewerOptions.mobileMode) { 196 | drawMyTooltip(pD); 197 | // } 198 | }) 199 | .on('mouseout.tooltip', () => { 200 | // Remove tooltip 201 | tooltipDiv.transition() 202 | .duration(500) 203 | .style("opacity", 0); 204 | }) 205 | }; 206 | 207 | return tooltip; 208 | 209 | }; 210 | 211 | this.commons.d3helper.genericTooltip = (object) => { 212 | 213 | let tooltipDiv = this.commons.tooltipDiv; 214 | let bodyNode = d3.select(div).node(); 215 | let message = object.tooltip; 216 | 217 | let tooltip = (selection) => { 218 | 219 | let absoluteMousePos; 220 | let drawMyTooltip = (pD) => { 221 | 222 | absoluteMousePos = d3.mouse(bodyNode); 223 | 224 | let left, top; 225 | left = absoluteMousePos[0].toString(); 226 | top = absoluteMousePos[1].toString(); 227 | 228 | if (message) { 229 | tooltipDiv.transition() 230 | .duration(200) 231 | .style("opacity", 1); 232 | tooltipDiv 233 | .html(message) 234 | .style("left", left+'px') 235 | .style("top", top+'px'); 236 | } 237 | }; 238 | 239 | selection 240 | // tooltip 241 | .on('mouseover', (pD) => { 242 | drawMyTooltip(pD); 243 | }) 244 | .on('mousemove', (pD) => { 245 | drawMyTooltip(pD); 246 | }) 247 | .on('mouseout', () => { 248 | // Remove tooltip 249 | tooltipDiv.transition() 250 | .duration(500) 251 | .style("opacity", 0); 252 | }) 253 | .on('click', (pD) => { 254 | // TODO 255 | // from message to object with button id too 256 | this.clickTagFunction(object) 257 | }) 258 | }; 259 | 260 | return tooltip; 261 | 262 | }; 263 | 264 | this.commons.d3helper.tooltip = (object) => { 265 | 266 | let tooltipDiv = this.commons.tooltipDiv; 267 | let customTooltipDiv = this.commons.customTooltipDiv; 268 | let viewerWidth = this.commons.viewerOptions.width; 269 | 270 | let bodyNode = d3.select(div).node(); 271 | // let tooltipColor = this.commons.viewerOptions.tooltipColor ? this.commons.viewerOptions.tooltipColor : "#fff"; 272 | 273 | let tooltip = (selection) => { 274 | 275 | let absoluteMousePos; 276 | 277 | let getPositions = (absoluteMousePos) => { 278 | let rightSide = (absoluteMousePos[0] > viewerWidth); 279 | let topshift = 25; 280 | let left = 0, 281 | top = 0; 282 | if (rightSide) { 283 | left = absoluteMousePos[0] + 10 - (tooltipDiv.node().getBoundingClientRect().width); 284 | top = absoluteMousePos[1] - topshift; 285 | } else { 286 | left = absoluteMousePos[0] - 15; 287 | top = absoluteMousePos[1] - topshift; 288 | } 289 | let positions = { 290 | top: top, 291 | left: left 292 | }; 293 | return positions 294 | }; 295 | 296 | let getMyMessage = (pD) => { 297 | let tooltip_message = ''; 298 | if (object.type === "path") { 299 | let reformat = { 300 | x: pD[0].x, 301 | y: pD[1].x 302 | } 303 | tooltip_message = getMessage(reformat); 304 | } 305 | else if (object.type === "curve") { 306 | let elemHover = updateLineTooltipFunction(absoluteMousePos[0], pD, scalingFunction, labelTrackWidth); 307 | tooltip_message = getMessage(elemHover, 'curve'); 308 | } 309 | else if (object.type === "circle") { 310 | tooltip_message = getMessage(pD, 'circle'); 311 | } 312 | else if (object.type === "button") { 313 | tooltip_message = getMessage(object); 314 | } else { 315 | // e.g. rect 316 | tooltip_message = getMessage(pD); 317 | } 318 | return tooltip_message 319 | }; 320 | 321 | let drawMyTooltip = (pD) => { 322 | if (pD.tooltip || object.tooltip) { 323 | customTooltipDiv.html(""); 324 | let html = ''; 325 | if (pD.tooltip) { html = pD.tooltip } else { html = object.tooltip; } 326 | drawCustomTooltip(html); 327 | } else { 328 | absoluteMousePos = d3.mouse(bodyNode); 329 | let positions = getPositions(absoluteMousePos); 330 | let tooltip_message = getMyMessage(pD); 331 | 332 | tooltipDiv.transition() 333 | .duration(200) 334 | .style("opacity", 1); 335 | tooltipDiv 336 | .html(tooltip_message) 337 | .style("left", positions['left']+'px') 338 | .style("top", positions['top']+'px'); 339 | } 340 | 341 | }; 342 | 343 | let drawCustomTooltip = (tooltiphtml) => { 344 | 345 | // remove tooltip div 346 | tooltipDiv.transition() 347 | .duration(500) 348 | .style("opacity", 0); 349 | 350 | // open tooltip if no source or if source is click and status is open 351 | absoluteMousePos = d3.mouse(bodyNode); 352 | let clickposition = (this.commons.viewerOptions.width/2) - absoluteMousePos[0] > 0 ? -1 : 1; 353 | let positions = getPositions(absoluteMousePos); 354 | 355 | // console.log('Selection:', d3.select(`#customTooltipDivContent`)) 356 | // d3.select(`#customTooltipDivContent`).html(tooltiphtml) 357 | 358 | // now fill it 359 | customTooltipDiv.transition() 360 | .duration(200) 361 | .style("opacity", 1); 362 | customTooltipDiv 363 | .style("top", positions['top']+'px') 364 | .style("left", positions['left']+'px') 365 | .append('foreignObject') // foreignObject can be styled with no limitation by user 366 | .attr("width", "100%") 367 | .attr("height", "100%") 368 | .html(tooltiphtml) 369 | // transition if clickposition 370 | if (clickposition) { 371 | customTooltipDiv.style("top", (positions['top']+35)+'px') 372 | } 373 | if (clickposition == 1) { 374 | let transitiondata = customTooltipDiv.node().getBoundingClientRect() 375 | if (transitiondata) { 376 | customTooltipDiv.style("left", (positions['left']-transitiondata.width+55)+'px') 377 | // customTooltipDiv.transition() 378 | // .duration(500) 379 | // .style("left", transitiondata.width); 380 | } 381 | } 382 | 383 | 384 | 385 | } 386 | 387 | selection 388 | .on("mouseover", (pD) => { 389 | drawMyTooltip(pD); 390 | }) 391 | .on('mousemove', (pD) => { 392 | drawMyTooltip(pD); 393 | }) 394 | .on("mouseout", function(pD) { 395 | if (pD.tooltip || object.tooltip) { 396 | // remove custom tooltip 397 | customTooltipDiv.html(""); 398 | customTooltipDiv.transition() 399 | .duration(500) 400 | .style("opacity", 0); 401 | } else { 402 | // remove normal tooltip 403 | tooltipDiv.transition() 404 | .duration(500) 405 | .style("opacity", 0); 406 | } 407 | }) 408 | .on('click', (pD) => { 409 | // not button: feature 410 | if (object.type !== "button") { // feature 411 | 412 | // TODO: define data for event exporting when clicking rects 413 | // TODO: fix exports.event.target.__data__ is undefined 414 | let elemHover; 415 | 416 | let forSelection = pD; 417 | // curve specifics 418 | if (object.type === "curve") { 419 | elemHover = updateLineTooltipFunction(absoluteMousePos[0], pD, scalingFunction, labelTrackWidth); 420 | forSelection = elemHover; 421 | } 422 | 423 | // path is array of pD, line is elemHover, all the rest is a pD object 424 | object['selectedRegion'] = forSelection; 425 | let feature_detail_object = object; 426 | 427 | this.colorSelectedFeat(pD.id, object, divId); 428 | 429 | 430 | // trigger feature_selected event 431 | if (CustomEvent) { 432 | let event = new CustomEvent(this.commons.events.FEATURE_SELECTED_EVENT, { 433 | detail: feature_detail_object 434 | }); 435 | this.commons.svgElement.dispatchEvent(event); 436 | } else { 437 | console.warn("CustomEvent is not defined...."); 438 | } 439 | if (this.commons.trigger) this.commons.trigger(this.commons.events.FEATURE_SELECTED_EVENT, feature_detail_object); 440 | 441 | } else { 442 | 443 | // button 444 | this.clickTagFunction(object) 445 | } 446 | }); 447 | }; 448 | 449 | return tooltip; 450 | }; 451 | 452 | } 453 | 454 | constructor(commons: {}) { 455 | super(commons); 456 | } 457 | } 458 | 459 | export default Tool 460 | -------------------------------------------------------------------------------- /src/transition.ts: -------------------------------------------------------------------------------- 1 | import ComputingFunctions from './helper'; 2 | 3 | export class SubfeaturesTransition extends ComputingFunctions { 4 | 5 | public area(gElement, newY) { 6 | gElement 7 | .attr("transform", "translate(0," + newY +")") 8 | .transition().duration(500); 9 | } 10 | public position(gElement, parentElementRow) { 11 | gElement 12 | .attr("position", "element(#"+ parentElementRow +")"); 13 | } 14 | public Xaxis(axis, newY) { 15 | axis 16 | .attr("transform", "translate(0," + newY +")") 17 | .transition().duration(500); 18 | } 19 | public containerH(container, newH) { 20 | container 21 | .attr("height", newH) 22 | } 23 | 24 | constructor(commons: {}) { 25 | super(commons); 26 | } 27 | } 28 | 29 | export class Transition extends ComputingFunctions { 30 | 31 | public basalLine(object) { 32 | 33 | let container = this.commons.svgContainer.select(`#c${object.id}_container`); 34 | container.selectAll(".line") 35 | .attr("d", this.commons.line); 36 | 37 | } 38 | 39 | public rectangle(object) { 40 | 41 | let container = this.commons.svgContainer.select(`#c${object.id}_container`); 42 | // line does not require transition 43 | 44 | let transit1, transit2; 45 | // group selection 46 | transit1 = container.selectAll("." + "rectfv" + "Group") 47 | transit2 = container.selectAll("." + "rectfv") 48 | // transition 49 | if (this.commons.animation) { 50 | transit1 51 | .transition() 52 | .duration(500); 53 | transit2 54 | .transition() 55 | .duration(500); 56 | } 57 | // transition 58 | transit1.attr("transform", (d) => { 59 | return "translate(" + this.rectX(d) + ",0)" 60 | }); 61 | transit2 62 | .attr("width", this.rectWidth2); 63 | 64 | // transition to text 65 | container.selectAll("." + object.className + "Text") 66 | .attr("transform", (d) => { 67 | if (d.label && this.commons.scaling(d['x'])<0) { 68 | return "translate(" + -this.rectX(d) + ",0)" 69 | } 70 | }) 71 | .style("visibility", (d) => { 72 | if (d.label && this.commons.scaling(d['x'])>0) { 73 | return (this.commons.scaling(d.y) - this.commons.scaling(d['x'])) > d.label.length * 8 && object.height > 11 ? "visible" : "hidden"; 74 | } else return "hidden"; 75 | }); 76 | } 77 | 78 | public multiRec(object) { 79 | 80 | let container = this.commons.svgContainer.select(`#c${object.id}_container`); 81 | container.selectAll("." + "rectfv") 82 | .attr("x", (d) => { 83 | return this.commons.scaling(d['x']) 84 | }) 85 | .attr("width", (d) => { 86 | return this.commons.scaling(d.y) - this.commons.scaling(d['x']) 87 | }); 88 | 89 | } 90 | 91 | public unique(object) { 92 | 93 | let container = this.commons.svgContainer.select(`#c${object.id}_container`); 94 | // line does not require transition 95 | 96 | let transit; 97 | if (this.commons.animation) { 98 | transit = container.selectAll(".element") 99 | .transition() 100 | .duration(500); 101 | } 102 | else { 103 | transit = container.selectAll(".element"); 104 | } 105 | transit 106 | .attr("x", (d) => { 107 | return this.commons.scaling(d['x'] - 0.4) 108 | }) 109 | .attr("width", (d) => { 110 | if (this.commons.scaling(d['x'] + 0.4) - this.commons.scaling(d['x'] - 0.4) < 2) return 2; 111 | else return this.commons.scaling(d['x'] + 0.4) - this.commons.scaling(d['x'] - 0.4); 112 | }); 113 | 114 | } 115 | 116 | public lollipop(object) { 117 | 118 | let container = this.commons.svgContainer.select(`#c${object.id}_container`); 119 | // line does not require transition 120 | 121 | let transit1, transit2; 122 | if (this.commons.animation) { 123 | transit1 = container.selectAll(".element") 124 | .transition() 125 | .duration(500); 126 | transit2 = container.selectAll(".lineElement") 127 | .transition() 128 | .duration(500); 129 | } 130 | else { 131 | transit1 = container.selectAll(".element"); 132 | transit2 = container.selectAll(".lineElement"); 133 | } 134 | transit1 135 | .attr("cx", (d) => { 136 | return this.commons.scaling(d.x) 137 | }); 138 | transit2 139 | .attr("x1", (d) => { 140 | return this.commons.scaling(d.x) 141 | }) 142 | .attr("x2", (d) => { 143 | return this.commons.scaling(d.x) 144 | }) 145 | // .attr("y2", (d) => { 146 | // let w = this.commons.scaling(d.x + 0.4) - this.commons.scaling(d.x - 0.4); 147 | // if (this.commons.scaling(d.x + 0.4) - this.commons.scaling(d.x - 0.4) < 2) w = 2; 148 | // return w + 4; 149 | // }); 150 | 151 | } 152 | 153 | public circle(object) { 154 | 155 | let container = this.commons.svgContainer.select(`#c${object.id}_container`); 156 | // line does not require transition 157 | 158 | let transit; 159 | if (this.commons.animation) { 160 | transit = container.selectAll(".element") 161 | .transition() 162 | .duration(500); 163 | } 164 | else { 165 | transit = container.selectAll(".element"); 166 | } 167 | transit 168 | .attr("cx", (d) => { 169 | return this.commons.scaling(d['x']) 170 | }) 171 | .attr("width", (d) => { 172 | if (this.commons.scaling(d['x'] + 0.4) - this.commons.scaling(d['x'] - 0.4) < 2) return 2; 173 | else return this.commons.scaling(d['x'] + 0.4) - this.commons.scaling(d['x'] - 0.4); 174 | }); 175 | 176 | } 177 | 178 | public path(object) { 179 | 180 | let container = this.commons.svgContainer.select(`#c${object.id}_container`); 181 | container.selectAll(".line") 182 | .attr("d", this.commons.lineBond.x((d) => { 183 | return this.commons.scaling(d['x']); 184 | }) 185 | .y( (d) => { 186 | return -d['y'] * 10 + object.height; 187 | }) 188 | ); 189 | let transit; 190 | if (this.commons.animation) { 191 | transit = container.selectAll("." + "pathfv") 192 | .transition() 193 | .duration(0); 194 | } 195 | else { 196 | transit = container.selectAll("." + "pathfv"); 197 | } 198 | transit 199 | .attr("d", this.commons.lineBond.y((d) => { 200 | return -1 * d['y'] * 10 + object.height; 201 | })); 202 | } 203 | 204 | public lineTransition(object) { 205 | let container = this.commons.svgContainer.select(`#c${object.id}_container`); 206 | const yScores = object.data.map(o => o.y); 207 | const maxScore = Math.max(...yScores); 208 | const minScore = Math.min(...yScores); 209 | // keep height 210 | this.commons.lineYScale.domain([minScore, maxScore]).range([0, this.commons.step/11]); 211 | container.selectAll(".line " + object.className) 212 | .attr("d", (d) => { 213 | return this.commons.lineYScale(-d.y) * 10 + object.shift 214 | }); 215 | 216 | // transit line 217 | let transit; 218 | if (this.commons.animation) { 219 | transit = container.selectAll("." + object.className) 220 | .transition() 221 | .duration(0); 222 | } 223 | else { 224 | transit = container.selectAll("." + object.className); 225 | } 226 | 227 | transit 228 | .attr("d", this.commons.lineGen.y((d) => { 229 | return this.commons.lineYScale(-d.y) * 10 + object.shift; 230 | }) 231 | ); 232 | } 233 | 234 | public text(object, start) { 235 | 236 | let container = this.commons.svgContainer.select(`#c${object.id}_container`); 237 | let transit; 238 | if (this.commons.animation) { 239 | transit = container.selectAll("." + object.className) 240 | .transition() 241 | .duration(500); 242 | } 243 | else { 244 | transit = container.selectAll("." + object.className); 245 | } 246 | transit 247 | .attr("x", (d, i) => { 248 | return this.commons.scaling(i + start) 249 | }); 250 | } 251 | 252 | constructor(commons: {}) { 253 | super(commons); 254 | } 255 | }; 256 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": false, 4 | "declaration": true, 5 | "allowSyntheticDefaultImports": true, 6 | "noImplicitAny": false, 7 | "allowJs": true, 8 | // Module defines the output module resolution system 9 | "module": "es6", 10 | // Target defines the compiled code targets 11 | "target": "es6", 12 | // Skip type checking all .d.ts files. 13 | "skipLibCheck": true 14 | }, 15 | // Define files to be included 16 | "files": ["./index.ts", "./example.ts"], 17 | // Include sources 18 | "include": ["src"], 19 | // Exclude node modules 20 | "exclude": ["node_modules"] 21 | } 22 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | // Generated using webpack-cli https://github.com/webpack/webpack-cli 2 | // Dependencies 3 | const path = require("path"); 4 | // Plugins 5 | const HtmlWebpackPlugin = require("html-webpack-plugin"); 6 | const MiniCssExtractPlugin = require('mini-css-extract-plugin'); 7 | // Define whether current environment is production or not 8 | const isProduction = process.env.NODE_ENV === "production"; 9 | // Main configuration options 10 | const config = { 11 | // Entry point for the app 12 | entry: { 13 | 'index': { import: './index.ts' }, 14 | 'example': { import: './example.ts', dependOn: 'index' }, 15 | }, 16 | // Define output file and directory 17 | output: { 18 | // Define output file name 19 | filename: "[name].js", 20 | // Define output directory 21 | path: path.resolve(__dirname, "dist"), 22 | // Define a library (Universal Module Definition) 23 | library: "feature-viewer-typescript", 24 | libraryTarget: "umd", 25 | umdNamedDefine: true, 26 | // Clean the output directory before emit. 27 | clean: true, 28 | }, 29 | // Live development server 30 | devServer: { 31 | open: true, 32 | host: "localhost", 33 | }, 34 | plugins: [ 35 | new HtmlWebpackPlugin({ 36 | template: "example.html", 37 | }), 38 | new MiniCssExtractPlugin(), 39 | // Add your plugins here 40 | // Learn more about plugins from https://webpack.js.org/configuration/plugins/ 41 | ], 42 | module: { 43 | rules: [ 44 | { 45 | test: /\.(ts|tsx)$/i, 46 | loader: "ts-loader", 47 | exclude: /node_modules/, 48 | }, 49 | { 50 | test: /\.s[ac]ss$/i, 51 | use: [MiniCssExtractPlugin.loader, "css-loader", "sass-loader"], 52 | // use: ["css-loader", "sass-loader"], 53 | }, 54 | { 55 | test: /\.(eot|svg|ttf|woff|woff2|png|jpg|gif)$/i, 56 | type: "asset", 57 | }, 58 | // Add your rules for custom modules here 59 | // Learn more about loaders from https://webpack.js.org/loaders/ 60 | ], 61 | }, 62 | resolve: { 63 | extensions: [".tsx", ".ts", ".jsx", ".js", "..."], 64 | }, 65 | }; 66 | // Export actual module 67 | module.exports = () => { 68 | if (isProduction) { 69 | config.mode = "production"; 70 | } else { 71 | config.mode = "development"; 72 | } 73 | return config; 74 | }; 75 | --------------------------------------------------------------------------------