├── .gitignore ├── .npmignore ├── .travis.yml ├── LICENSE ├── README.md ├── dist └── .gitkeep ├── jest.config.js ├── package.json ├── rules └── geneticrules.ts ├── server.js ├── src ├── img │ ├── menudots.svg │ └── zoomtofit.svg ├── js │ ├── CachedProperty.ts │ ├── binarysearch.ts │ ├── bucketsort.ts │ ├── clustering.ts │ ├── extractrgba.ts │ ├── haselementsininterval.ts │ ├── heatmapcolors.ts │ ├── main.js │ ├── makesvgelement.ts │ ├── minimaputils.ts │ ├── modelutils.ts │ ├── oncoprint.ts │ ├── oncoprintheaderview.ts │ ├── oncoprintlabelview.ts │ ├── oncoprintlegendrenderer.ts │ ├── oncoprintminimapview.ts │ ├── oncoprintmodel.ts │ ├── oncoprintruleset.ts │ ├── oncoprintshape.ts │ ├── oncoprintshapetosvg.ts │ ├── oncoprintshapetovertexes.ts │ ├── oncoprinttooltip.ts │ ├── oncoprinttrackinfoview.ts │ ├── oncoprinttrackoptionsview.ts │ ├── oncoprintwebglcellview.ts │ ├── oncoprintzoomslider.ts │ ├── polyfill.ts │ ├── precomputedcomparator.ts │ ├── shaders.ts │ ├── svgfactory.ts │ ├── utils.ts │ └── workers │ │ └── clustering-worker.ts └── test │ ├── gradientCategoricalRuleset.spec.ts │ ├── mocks │ └── empty-module.js │ └── monolith.spec.ts ├── test ├── generate_data.py ├── glyphmap-data.js ├── heatmap-data.js ├── index.html ├── oncoprint-glyphmap.js └── oncoprint-heatmap.js ├── tsconfig.json ├── typings ├── custom.d.ts └── missing.d.ts ├── webpack.config.js └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.gitignore.io 2 | 3 | dist/* 4 | !dist/.gitkeep 5 | 6 | ### Node ### 7 | # Logs 8 | logs 9 | *.log 10 | 11 | # Runtime data 12 | pids 13 | *.pid 14 | *.seed 15 | 16 | # Directory for instrumented libs generated by jscoverage/JSCover 17 | lib-cov 18 | 19 | # Coverage directory used by tools like istanbul 20 | coverage 21 | 22 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 23 | .grunt 24 | 25 | # node-waf configuration 26 | .lock-wscript 27 | 28 | # Compiled binary addons (http://nodejs.org/api/addons.html) 29 | build/Release 30 | 31 | # Dependency directory 32 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git- 33 | node_modules 34 | 35 | 36 | ### vim ### 37 | [._]*.s[a-w][a-z] 38 | [._]s[a-w][a-z] 39 | *.un~ 40 | Session.vim 41 | .netrwhist 42 | *~ 43 | 44 | ### emacs ### 45 | \#*\# 46 | 47 | # DS_Store 48 | .DS_Store 49 | src/.DS_Store 50 | test/.DS_Store 51 | .idea 52 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.gitignore.io 2 | 3 | ### Repo Files ### 4 | # src/ 5 | test/ 6 | server.js 7 | gulpfile.js 8 | .gitignore 9 | .travis.yml 10 | 11 | ### Node ### 12 | # Logs 13 | logs 14 | *.log 15 | 16 | # Runtime data 17 | pids 18 | *.pid 19 | *.seed 20 | 21 | # Directory for instrumented libs generated by jscoverage/JSCover 22 | lib-cov 23 | 24 | # Coverage directory used by tools like istanbul 25 | coverage 26 | 27 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 28 | .grunt 29 | 30 | # node-waf configuration 31 | .lock-wscript 32 | 33 | # Compiled binary addons (http://nodejs.org/api/addons.html) 34 | build/Release 35 | 36 | # Dependency directory 37 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git- 38 | node_modules 39 | 40 | 41 | ### vim ### 42 | [._]*.s[a-w][a-z] 43 | [._]s[a-w][a-z] 44 | *.un~ 45 | Session.vim 46 | .netrwhist 47 | *~ 48 | 49 | ### emacs ### 50 | \#*\# 51 | 52 | # DS_Store 53 | .DS_Store 54 | src/.DS_Store 55 | test/.DS_Store 56 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '6' 4 | script: 5 | - npm run build 6 | - npm run test 7 | # generate version in package.json using git describe --tags 8 | - sed -i "s|\"version\".*|\"version\":\"$(git describe --tags | sed 's/^v//g')\",|g" package.json 9 | deploy: 10 | provider: npm 11 | email: cbioportal@gmail.com 12 | skip_cleanup: true 13 | api_key: 14 | secure: EVdXGpTxgB6g+8rmWXR7fv3hUgDgbmiUgIYmPAw4In6wOfIymQMwcxQ0rzbZlqPuhMCUKrHjzWo6pcTqoYF862wssfZozy2e8tlIVVbAqJhJ+w9wmFzPuJlCPt4vpXrRe+yBNR5dJqYeX12jZFANWN8Iro6f5wXdq5Jn5H8CTt0= 15 | on: 16 | tags: true 17 | repo: cBioPortal/oncoprintjs 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | 9 | This version of the GNU Lesser General Public License incorporates 10 | the terms and conditions of version 3 of the GNU General Public 11 | License, supplemented by the additional permissions listed below. 12 | 13 | 0. Additional Definitions. 14 | 15 | As used herein, "this License" refers to version 3 of the GNU Lesser 16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU 17 | General Public License. 18 | 19 | "The Library" refers to a covered work governed by this License, 20 | other than an Application or a Combined Work as defined below. 21 | 22 | An "Application" is any work that makes use of an interface provided 23 | by the Library, but which is not otherwise based on the Library. 24 | Defining a subclass of a class defined by the Library is deemed a mode 25 | of using an interface provided by the Library. 26 | 27 | A "Combined Work" is a work produced by combining or linking an 28 | Application with the Library. The particular version of the Library 29 | with which the Combined Work was made is also called the "Linked 30 | Version". 31 | 32 | The "Minimal Corresponding Source" for a Combined Work means the 33 | Corresponding Source for the Combined Work, excluding any source code 34 | for portions of the Combined Work that, considered in isolation, are 35 | based on the Application, and not on the Linked Version. 36 | 37 | The "Corresponding Application Code" for a Combined Work means the 38 | object code and/or source code for the Application, including any data 39 | and utility programs needed for reproducing the Combined Work from the 40 | Application, but excluding the System Libraries of the Combined Work. 41 | 42 | 1. Exception to Section 3 of the GNU GPL. 43 | 44 | You may convey a covered work under sections 3 and 4 of this License 45 | without being bound by section 3 of the GNU GPL. 46 | 47 | 2. Conveying Modified Versions. 48 | 49 | If you modify a copy of the Library, and, in your modifications, a 50 | facility refers to a function or data to be supplied by an Application 51 | that uses the facility (other than as an argument passed when the 52 | facility is invoked), then you may convey a copy of the modified 53 | version: 54 | 55 | a) under this License, provided that you make a good faith effort to 56 | ensure that, in the event an Application does not supply the 57 | function or data, the facility still operates, and performs 58 | whatever part of its purpose remains meaningful, or 59 | 60 | b) under the GNU GPL, with none of the additional permissions of 61 | this License applicable to that copy. 62 | 63 | 3. Object Code Incorporating Material from Library Header Files. 64 | 65 | The object code form of an Application may incorporate material from 66 | a header file that is part of the Library. You may convey such object 67 | code under terms of your choice, provided that, if the incorporated 68 | material is not limited to numerical parameters, data structure 69 | layouts and accessors, or small macros, inline functions and templates 70 | (ten or fewer lines in length), you do both of the following: 71 | 72 | a) Give prominent notice with each copy of the object code that the 73 | Library is used in it and that the Library and its use are 74 | covered by this License. 75 | 76 | b) Accompany the object code with a copy of the GNU GPL and this license 77 | document. 78 | 79 | 4. Combined Works. 80 | 81 | You may convey a Combined Work under terms of your choice that, 82 | taken together, effectively do not restrict modification of the 83 | portions of the Library contained in the Combined Work and reverse 84 | engineering for debugging such modifications, if you also do each of 85 | the following: 86 | 87 | a) Give prominent notice with each copy of the Combined Work that 88 | the Library is used in it and that the Library and its use are 89 | covered by this License. 90 | 91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license 92 | document. 93 | 94 | c) For a Combined Work that displays copyright notices during 95 | execution, include the copyright notice for the Library among 96 | these notices, as well as a reference directing the user to the 97 | copies of the GNU GPL and this license document. 98 | 99 | d) Do one of the following: 100 | 101 | 0) Convey the Minimal Corresponding Source under the terms of this 102 | License, and the Corresponding Application Code in a form 103 | suitable for, and under terms that permit, the user to 104 | recombine or relink the Application with a modified version of 105 | the Linked Version to produce a modified Combined Work, in the 106 | manner specified by section 6 of the GNU GPL for conveying 107 | Corresponding Source. 108 | 109 | 1) Use a suitable shared library mechanism for linking with the 110 | Library. A suitable mechanism is one that (a) uses at run time 111 | a copy of the Library already present on the user's computer 112 | system, and (b) will operate properly with a modified version 113 | of the Library that is interface-compatible with the Linked 114 | Version. 115 | 116 | e) Provide Installation Information, but only if you would otherwise 117 | be required to provide such information under section 6 of the 118 | GNU GPL, and only to the extent that such information is 119 | necessary to install and execute a modified version of the 120 | Combined Work produced by recombining or relinking the 121 | Application with a modified version of the Linked Version. (If 122 | you use option 4d0, the Installation Information must accompany 123 | the Minimal Corresponding Source and Corresponding Application 124 | Code. If you use option 4d1, you must provide the Installation 125 | Information in the manner specified by section 6 of the GNU GPL 126 | for conveying Corresponding Source.) 127 | 128 | 5. Combined Libraries. 129 | 130 | You may place library facilities that are a work based on the 131 | Library side by side in a single library together with other library 132 | facilities that are not Applications and are not covered by this 133 | License, and convey such a combined library under terms of your 134 | choice, if you do both of the following: 135 | 136 | a) Accompany the combined library with a copy of the same work based 137 | on the Library, uncombined with any other library facilities, 138 | conveyed under the terms of this License. 139 | 140 | b) Give prominent notice with the combined library that part of it 141 | is a work based on the Library, and explaining where to find the 142 | accompanying uncombined form of the same work. 143 | 144 | 6. Revised Versions of the GNU Lesser General Public License. 145 | 146 | The Free Software Foundation may publish revised and/or new versions 147 | of the GNU Lesser General Public License from time to time. Such new 148 | versions will be similar in spirit to the present version, but may 149 | differ in detail to address new problems or concerns. 150 | 151 | Each version is given a distinguishing version number. If the 152 | Library as you received it specifies that a certain numbered version 153 | of the GNU Lesser General Public License "or any later version" 154 | applies to it, you have the option of following the terms and 155 | conditions either of that published version or of any later version 156 | published by the Free Software Foundation. If the Library as you 157 | received it does not specify a version number of the GNU Lesser 158 | General Public License, you may choose any version of the GNU Lesser 159 | General Public License ever published by the Free Software Foundation. 160 | 161 | If the Library as you received it specifies that a proxy can decide 162 | whether future versions of the GNU Lesser General Public License shall 163 | apply, that proxy's public statement of acceptance of any version is 164 | permanent authorization for you to choose that version for the 165 | Library. 166 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [DEPRECATED] In favor of monorepo, `oncoprintjs` library has been moved under [cBioPortal/cbioportal-frontend](https://github.com/cBioPortal/cbioportal-frontend/tree/master/packages/oncoprintjs). This repo is no longer maintained. 2 | -------------------------------------------------------------------------------- /dist/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cBioPortal/oncoprintjs/c1816a495ef8ac6e9e060a0c833ed7cb01f65a32/dist/.gitkeep -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | roots: ["./src/test/"], 3 | moduleDirectories: [ 4 | ".", "src", "src/test", "node_modules" 5 | ], 6 | transform: { 7 | "^.+\\.ts$": "ts-jest", 8 | } , 9 | moduleNameMapper: { 10 | "\\.(css|jpg|png|svg)$": "mocks/empty-module.js" 11 | } 12 | }; 13 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "oncoprintjs", 3 | "version": "v5.0.3", 4 | "description": "A data visualization for cancer genomic data.", 5 | "types": "./dist/js/oncoprint.d.ts", 6 | "main": "./dist/oncoprint.bundle.js", 7 | "scripts": { 8 | "build": "rm -rf ./dist/* && webpack", 9 | "build-dev": "yarn build --env.DEV=true", 10 | "test": "jest", 11 | "test:watch": "npm run test -- --watch" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "https://github.com/cBioPortal/oncoprintjs.git" 16 | }, 17 | "keywords": [ 18 | "cancer", 19 | "genomics", 20 | "visualization", 21 | "webgl" 22 | ], 23 | "author": { 24 | "name": "Adam Abeshouse", 25 | "email": "adamabeshouse@gmail.com" 26 | }, 27 | "contributors": [ 28 | { 29 | "name": "JJ Gao", 30 | "email": "jianjiong.gao@gmail.com" 31 | }, 32 | { 33 | "name": "Gideon Dresdner", 34 | "email": "oncoprintjs@gideonite.com" 35 | }, 36 | { 37 | "name": "Fedde Schaeffer", 38 | "email": "fedde@thehyve.nl" 39 | } 40 | ], 41 | "license": "ISC", 42 | "bugs": { 43 | "url": "https://github.com/cBioPortal/oncoprintjs/issues" 44 | }, 45 | "homepage": "https://github.com/cBioPortal/oncoprintjs", 46 | "devDependencies": { 47 | "@types/chai": "^4.2.3", 48 | "@types/jquery": "^3.3.31", 49 | "@types/lodash": "^4.14.139", 50 | "@types/mocha": "^5.2.7", 51 | "chai": "^4.1.2", 52 | "jest": "^24.9.0", 53 | "mocha": "^5.0.5", 54 | "ts-jest": "^24.1.0", 55 | "ts-loader": "5.4.5", 56 | "typescript": "^3.6.3", 57 | "url-loader": "^1.1.1", 58 | "webpack": "^4.0.0", 59 | "webpack-cli": "^3.3.9", 60 | "webpack-shell-plugin": "^0.5.0", 61 | "worker-loader": "^1.1.0" 62 | }, 63 | "dependencies": { 64 | "gl-matrix": "2.3.2", 65 | "jquery": "^3.0.0", 66 | "jstat": "^1.7.1", 67 | "lodash": "^4.17.15", 68 | "tayden-clusterfck": "^0.7.0" 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /rules/geneticrules.ts: -------------------------------------------------------------------------------- 1 | import * as $ from "jquery"; 2 | import { 3 | GeneticAlterationRuleParams, 4 | IGeneticAlterationRuleSetParams, 5 | RuleSetParams, 6 | RuleSetType 7 | } from "../src/js/oncoprintruleset"; 8 | import {shallowExtend} from "../src/js/utils"; 9 | 10 | export const MUT_COLOR_MISSENSE = '#008000'; 11 | export const MUT_COLOR_MISSENSE_PASSENGER = '#53D400'; 12 | export const MUT_COLOR_INFRAME = '#993404'; 13 | export const MUT_COLOR_INFRAME_PASSENGER = '#a68028'; 14 | export const MUT_COLOR_TRUNC = "#000000"; 15 | export const MUT_COLOR_TRUNC_PASSENGER = '#708090'; 16 | export const MUT_COLOR_FUSION = '#8B00C9'; 17 | export const MUT_COLOR_PROMOTER = '#00B7CE'; 18 | export const MUT_COLOR_OTHER = '#cf58bc';//'#cfb537'; 19 | 20 | export const MRNA_COLOR_HIGH = "#ff9999"; 21 | export const MRNA_COLOR_LOW = "#6699cc"; 22 | export const MUT_COLOR_GERMLINE = '#FFFFFF'; 23 | 24 | export const PROT_COLOR_HIGH = "#ff3df8"; 25 | export const PROT_COLOR_LOW = "#00E1FF"; 26 | 27 | export const CNA_COLOR_AMP = "#ff0000"; 28 | export const CNA_COLOR_GAIN = "#ffb6c1"; 29 | export const CNA_COLOR_HETLOSS = "#8fd8d8"; 30 | export const CNA_COLOR_HOMDEL = "#0000ff"; 31 | 32 | export const DEFAULT_GREY = "#BEBEBE"; 33 | 34 | 35 | const MUTATION_LEGEND_ORDER = 0; 36 | const FUSION_LEGEND_ORDER = 1; 37 | const GERMLINE_LEGEND_ORDER = 2; 38 | const AMP_LEGEND_ORDER = 10; 39 | const GAIN_LEGEND_ORDER = 11; 40 | const HOMDEL_LEGEND_ORDER = 12; 41 | const HETLOSS_LEGEND_ORDER = 13; 42 | const MRNA_HIGH_LEGEND_ORDER = 20; 43 | const MRNA_LOW_LEGEND_ORDER = 21; 44 | const PROT_HIGH_LEGEND_ORDER = 31; 45 | const PROT_LOW_LEGEND_ORDER = 32; 46 | 47 | enum ShapeId { 48 | defaultGrayRectangle="defaultGrayRectangle", 49 | 50 | ampRectangle="ampRectangle", 51 | gainRectangle="gainRectangle", 52 | homdelRectangle="homdelRectangle", 53 | hetlossRectangle="hetlossRectangle", 54 | 55 | mrnaHighRectangle="mrnaHighRectangle", 56 | mrnaLowRectangle="mrnaLowRectangle", 57 | 58 | protHighRectangle="protHighRectangle", 59 | protLowRectangle="protLowRectangle", 60 | 61 | fusionRectangle="fusionRectangle", 62 | 63 | germlineRectangle="germlineRectangle", 64 | 65 | missenseMutationDriverRectangle="missenseMutationDriverRectangle", 66 | missenseMutationVUSRectangle="missenseMutationVUSRectangle", 67 | otherMutationRectangle="otherMutationRectangle", 68 | promoterMutationRectangle="promoterMutationRectangle", 69 | truncatingMutationDriverRectangle="truncatingMutationDriverRectangle", 70 | truncatingMutationVUSRectangle="truncatingMutationVUSRectangle", 71 | inframeMutationDriverRectangle="inframeMutationDriverRectangle", 72 | inframeMutationVUSRectangle="inframeMutationVUSRectangle", 73 | } 74 | 75 | const shapeBank = { 76 | [ShapeId.defaultGrayRectangle]: { 77 | 'type': 'rectangle', 78 | 'fill': DEFAULT_GREY, 79 | 'z': 1 80 | }, 81 | [ShapeId.ampRectangle]: { 82 | 'type': 'rectangle', 83 | 'fill': CNA_COLOR_AMP, 84 | 'x': 0, 85 | 'y': 0, 86 | 'width': 100, 87 | 'height': 100, 88 | 'z': 2, 89 | }, 90 | [ShapeId.gainRectangle]: { 91 | 'type': 'rectangle', 92 | 'fill': CNA_COLOR_GAIN, 93 | 'x': 0, 94 | 'y': 0, 95 | 'width': 100, 96 | 'height': 100, 97 | 'z': 2, 98 | }, 99 | [ShapeId.homdelRectangle]:{ 100 | 'type': 'rectangle', 101 | 'fill': CNA_COLOR_HOMDEL, 102 | 'x': 0, 103 | 'y': 0, 104 | 'width': 100, 105 | 'height': 100, 106 | 'z': 2, 107 | }, 108 | [ShapeId.hetlossRectangle]:{ 109 | 'type': 'rectangle', 110 | 'fill': CNA_COLOR_HETLOSS, 111 | 'x': 0, 112 | 'y': 0, 113 | 'width': 100, 114 | 'height': 100, 115 | 'z': 2, 116 | }, 117 | [ShapeId.mrnaHighRectangle]:{ 118 | 'type': 'rectangle', 119 | 'fill': 'rgba(0, 0, 0, 0)', 120 | 'stroke': MRNA_COLOR_HIGH, 121 | 'stroke-width': 2, 122 | 'x': 0, 123 | 'y': 0, 124 | 'width': 100, 125 | 'height': 100, 126 | 'z': 3, 127 | }, 128 | [ShapeId.mrnaLowRectangle]:{ 129 | 'type': 'rectangle', 130 | 'fill': 'rgba(0, 0, 0, 0)', 131 | 'stroke': MRNA_COLOR_LOW, 132 | 'stroke-width': 2, 133 | 'x': 0, 134 | 'y': 0, 135 | 'width': 100, 136 | 'height': 100, 137 | 'z': 3, 138 | }, 139 | [ShapeId.protHighRectangle]:{ 140 | 'type': 'rectangle', 141 | 'fill': PROT_COLOR_HIGH, 142 | 'x':0, 143 | 'y':0, 144 | 'width':100, 145 | 'height':20, 146 | 'z': 4, 147 | }, 148 | [ShapeId.protLowRectangle]:{ 149 | 'type': 'rectangle', 150 | 'fill': PROT_COLOR_LOW, 151 | 'x':0, 152 | 'y':80, 153 | 'width':100, 154 | 'height':20, 155 | 'z': 4, 156 | }, 157 | [ShapeId.fusionRectangle]:{ 158 | 'type': 'rectangle', 159 | 'fill': MUT_COLOR_FUSION, 160 | 'x': 0, 161 | 'y': 20, 162 | 'width': 100, 163 | 'height': 60, 164 | 'z': 5 165 | }, 166 | [ShapeId.germlineRectangle]:{ 167 | 'type': 'rectangle', 168 | 'fill': MUT_COLOR_GERMLINE, 169 | 'x': 0, 170 | 'y': 46, 171 | 'width': 100, 172 | 'height': 8, 173 | 'z': 7 174 | }, 175 | [ShapeId.missenseMutationDriverRectangle]:{ 176 | 'type': 'rectangle', 177 | 'fill': MUT_COLOR_MISSENSE, 178 | 'x': 0, 179 | 'y': 33.33, 180 | 'width': 100, 181 | 'height': 33.33, 182 | 'z': 6 183 | }, 184 | [ShapeId.missenseMutationVUSRectangle]:{ 185 | 'type': 'rectangle', 186 | 'fill': MUT_COLOR_MISSENSE_PASSENGER, 187 | 'x': 0, 188 | 'y': 33.33, 189 | 'width': 100, 190 | 'height': 33.33, 191 | 'z': 6 192 | }, 193 | [ShapeId.otherMutationRectangle]:{ 194 | 'type': 'rectangle', 195 | 'fill': MUT_COLOR_OTHER, 196 | 'x': 0, 197 | 'y': 33.33, 198 | 'width': 100, 199 | 'height': 33.33, 200 | 'z': 6, 201 | }, 202 | [ShapeId.promoterMutationRectangle]:{ 203 | 'type': 'rectangle', 204 | 'fill': MUT_COLOR_PROMOTER, 205 | 'x': 0, 206 | 'y': 33.33, 207 | 'width': 100, 208 | 'height': 33.33, 209 | 'z': 6, 210 | }, 211 | [ShapeId.truncatingMutationDriverRectangle]:{ 212 | 'type': 'rectangle', 213 | 'fill': MUT_COLOR_TRUNC, 214 | 'x': 0, 215 | 'y': 33.33, 216 | 'width': 100, 217 | 'height': 33.33, 218 | 'z': 6, 219 | }, 220 | [ShapeId.truncatingMutationVUSRectangle]:{ 221 | 'type': 'rectangle', 222 | 'fill': MUT_COLOR_TRUNC_PASSENGER, 223 | 'x': 0, 224 | 'y': 33.33, 225 | 'width': 100, 226 | 'height': 33.33, 227 | 'z': 6, 228 | }, 229 | [ShapeId.inframeMutationDriverRectangle]:{ 230 | 'type': 'rectangle', 231 | 'fill': MUT_COLOR_INFRAME, 232 | 'x': 0, 233 | 'y': 33.33, 234 | 'width': 100, 235 | 'height': 33.33, 236 | 'z': 6, 237 | }, 238 | [ShapeId.inframeMutationVUSRectangle]:{ 239 | 'type': 'rectangle', 240 | 'fill': MUT_COLOR_INFRAME_PASSENGER, 241 | 'x': 0, 242 | 'y': 33.33, 243 | 'width': 100, 244 | 'height': 33.33, 245 | 'z': 6, 246 | } 247 | }; 248 | 249 | const non_mutation_rule_params:GeneticAlterationRuleParams = { 250 | // Default: gray rectangle 251 | always: { 252 | shapes: [shapeBank[ShapeId.defaultGrayRectangle]], 253 | legend_label: "No alterations", 254 | legend_order: Number.POSITIVE_INFINITY // put at the end always 255 | }, 256 | conditional:{ 257 | // Copy number alteration 258 | 'disp_cna': { 259 | // Red rectangle for amplification 260 | 'amp': { 261 | shapes: [shapeBank[ShapeId.ampRectangle]], 262 | legend_label: 'Amplification', 263 | legend_order: AMP_LEGEND_ORDER 264 | }, 265 | // Light red rectangle for gain 266 | 'gain': { 267 | shapes: [shapeBank[ShapeId.gainRectangle]], 268 | legend_label: 'Gain', 269 | legend_order: GAIN_LEGEND_ORDER 270 | }, 271 | // Blue rectangle for deep deletion 272 | 'homdel': { 273 | shapes: [shapeBank[ShapeId.homdelRectangle]], 274 | legend_label: 'Deep Deletion', 275 | legend_order: HOMDEL_LEGEND_ORDER 276 | }, 277 | // Light blue rectangle for shallow deletion 278 | 'hetloss': { 279 | shapes: [shapeBank[ShapeId.hetlossRectangle]], 280 | legend_label: 'Shallow Deletion', 281 | legend_order: HETLOSS_LEGEND_ORDER 282 | } 283 | }, 284 | // mRNA regulation 285 | 'disp_mrna': { 286 | // Light red outline for High 287 | 'high': { 288 | shapes: [shapeBank[ShapeId.mrnaHighRectangle]], 289 | legend_label: 'mRNA High', 290 | legend_order: MRNA_HIGH_LEGEND_ORDER 291 | }, 292 | // Light blue outline for downregulation 293 | 'low': { 294 | shapes: [shapeBank[ShapeId.mrnaLowRectangle]], 295 | legend_label: 'mRNA Low', 296 | legend_order: MRNA_LOW_LEGEND_ORDER 297 | }, 298 | }, 299 | // protein expression regulation 300 | 'disp_prot': { 301 | // small up arrow for upregulated 302 | 'high': { 303 | shapes: [shapeBank[ShapeId.protHighRectangle]], 304 | legend_label: 'Protein High', 305 | legend_order: PROT_HIGH_LEGEND_ORDER 306 | }, 307 | // small down arrow for upregulated 308 | 'low': { 309 | shapes: [shapeBank[ShapeId.protLowRectangle]], 310 | legend_label: 'Protein Low', 311 | legend_order: PROT_LOW_LEGEND_ORDER 312 | } 313 | }, 314 | // fusion 315 | 'disp_fusion': { 316 | // tall inset purple rectangle for fusion 317 | 'true': { 318 | shapes: [shapeBank[ShapeId.fusionRectangle]], 319 | legend_label: 'Fusion', 320 | legend_order: FUSION_LEGEND_ORDER 321 | } 322 | } 323 | } 324 | }; 325 | 326 | export const germline_rule_params = { 327 | // germline 328 | 'disp_germ': { 329 | // white stripe in the middle 330 | 'true': { 331 | shapes: [{ 332 | 'type': 'rectangle', 333 | 'fill': MUT_COLOR_GERMLINE, 334 | 'x': 0, 335 | 'y': 46, 336 | 'width': 100, 337 | 'height': 8, 338 | 'z': 7 339 | }], 340 | legend_label: 'Germline Mutation', 341 | legend_order: GERMLINE_LEGEND_ORDER 342 | } 343 | } 344 | }; 345 | 346 | const base_genetic_rule_set_params:Partial = { 347 | type: RuleSetType.GENE, 348 | legend_label: 'Genetic Alteration', 349 | na_legend_label: 'Not profiled', 350 | legend_base_color: DEFAULT_GREY 351 | }; 352 | 353 | export const genetic_rule_set_same_color_for_all_no_recurrence:IGeneticAlterationRuleSetParams = 354 | shallowExtend(base_genetic_rule_set_params, { 355 | 'rule_params': { 356 | always:non_mutation_rule_params.always, 357 | conditional: shallowExtend(non_mutation_rule_params.conditional, { 358 | 'disp_mut': { 359 | 'trunc,inframe,missense,promoter,other,trunc_rec,inframe_rec,missense_rec,promoter_rec,other_rec': { 360 | shapes: [shapeBank[ShapeId.missenseMutationDriverRectangle]], 361 | legend_label: 'Mutation', 362 | legend_order: MUTATION_LEGEND_ORDER 363 | } 364 | } 365 | } as GeneticAlterationRuleParams["conditional"]) 366 | } 367 | }) as IGeneticAlterationRuleSetParams; 368 | 369 | export const genetic_rule_set_same_color_for_all_recurrence:IGeneticAlterationRuleSetParams = 370 | shallowExtend(base_genetic_rule_set_params, { 371 | 'rule_params': { 372 | always:non_mutation_rule_params.always, 373 | conditional: shallowExtend(non_mutation_rule_params.conditional, { 374 | 'disp_mut': { 375 | 'missense_rec,inframe_rec,trunc_rec,promoter_rec,other_rec': { 376 | shapes: [shapeBank[ShapeId.missenseMutationDriverRectangle]], 377 | legend_label: 'Mutation (putative driver)', 378 | legend_order: MUTATION_LEGEND_ORDER 379 | }, 380 | 'missense,inframe,trunc,promoter,other': { 381 | shapes: [shapeBank[ShapeId.missenseMutationVUSRectangle]], 382 | legend_label: 'Mutation (unknown significance)', 383 | legend_order: MUTATION_LEGEND_ORDER 384 | }, 385 | }, 386 | } as GeneticAlterationRuleParams["conditional"]) 387 | } 388 | }) as IGeneticAlterationRuleSetParams; 389 | 390 | export const genetic_rule_set_different_colors_no_recurrence:IGeneticAlterationRuleSetParams = 391 | shallowExtend(base_genetic_rule_set_params, { 392 | 'rule_params': { 393 | always:non_mutation_rule_params.always, 394 | conditional:shallowExtend(non_mutation_rule_params.conditional, { 395 | 'disp_mut': { 396 | 'other,other_rec':{ 397 | shapes: [shapeBank[ShapeId.otherMutationRectangle]], 398 | legend_label: 'Other Mutation', 399 | legend_order: MUTATION_LEGEND_ORDER 400 | }, 401 | 'promoter,promoter_rec': { 402 | shapes: [shapeBank[ShapeId.promoterMutationRectangle]], 403 | legend_label: 'Promoter Mutation', 404 | legend_order: MUTATION_LEGEND_ORDER 405 | }, 406 | 'trunc,trunc_rec': { 407 | shapes: [shapeBank[ShapeId.truncatingMutationDriverRectangle]], 408 | legend_label: 'Truncating Mutation', 409 | legend_order: MUTATION_LEGEND_ORDER 410 | }, 411 | 'inframe,inframe_rec': { 412 | shapes: [shapeBank[ShapeId.inframeMutationDriverRectangle]], 413 | legend_label: 'Inframe Mutation', 414 | legend_order: MUTATION_LEGEND_ORDER 415 | }, 416 | 'missense,missense_rec': { 417 | shapes: [shapeBank[ShapeId.missenseMutationDriverRectangle]], 418 | legend_label: 'Missense Mutation', 419 | legend_order: MUTATION_LEGEND_ORDER 420 | }, 421 | } 422 | } as GeneticAlterationRuleParams["conditional"]) 423 | } 424 | }) as IGeneticAlterationRuleSetParams; 425 | 426 | export const genetic_rule_set_different_colors_recurrence:IGeneticAlterationRuleSetParams = 427 | shallowExtend(base_genetic_rule_set_params, { 428 | 'rule_params': { 429 | always:non_mutation_rule_params.always, 430 | conditional: shallowExtend(non_mutation_rule_params.conditional, { 431 | 'disp_mut': { 432 | 'other,other_rec':{ 433 | shapes: [shapeBank[ShapeId.otherMutationRectangle]], 434 | legend_label: 'Other Mutation', 435 | legend_order: MUTATION_LEGEND_ORDER 436 | }, 437 | 'promoter,promoter_rec': { 438 | shapes: [shapeBank[ShapeId.promoterMutationRectangle]], 439 | legend_label: 'Promoter Mutation', 440 | legend_order: MUTATION_LEGEND_ORDER 441 | }, 442 | 'trunc_rec': { 443 | shapes: [shapeBank[ShapeId.truncatingMutationDriverRectangle]], 444 | legend_label: 'Truncating Mutation (putative driver)', 445 | legend_order: MUTATION_LEGEND_ORDER 446 | }, 447 | 'trunc': { 448 | shapes: [shapeBank[ShapeId.truncatingMutationVUSRectangle]], 449 | legend_label: 'Truncating Mutation (unknown significance)', 450 | legend_order: MUTATION_LEGEND_ORDER 451 | }, 452 | 'inframe_rec': { 453 | shapes: [shapeBank[ShapeId.inframeMutationDriverRectangle]], 454 | legend_label: 'Inframe Mutation (putative driver)', 455 | legend_order: MUTATION_LEGEND_ORDER 456 | }, 457 | 'inframe': { 458 | shapes: [shapeBank[ShapeId.inframeMutationVUSRectangle]], 459 | legend_label: 'Inframe Mutation (unknown significance)', 460 | legend_order: MUTATION_LEGEND_ORDER 461 | }, 462 | 'missense_rec': { 463 | shapes: [shapeBank[ShapeId.missenseMutationDriverRectangle]], 464 | legend_label: 'Missense Mutation (putative driver)', 465 | legend_order: MUTATION_LEGEND_ORDER 466 | }, 467 | 'missense': { 468 | shapes: [shapeBank[ShapeId.missenseMutationVUSRectangle]], 469 | legend_label: 'Missense Mutation (unknown significance)', 470 | legend_order: MUTATION_LEGEND_ORDER 471 | }, 472 | } 473 | } as GeneticAlterationRuleParams["conditional"]) 474 | } 475 | }) as IGeneticAlterationRuleSetParams; -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | 3 | var server = express(); 4 | server.use(express.static(__dirname + '/dist')); 5 | server.use(express.static(__dirname + '/rules')); 6 | server.use(express.static(__dirname + '/test')); 7 | 8 | var port = 3000; 9 | server.listen(port, function() { 10 | console.log('View Oncoprint at http://localhost:' + port + '/index.html'); 11 | }); 12 | -------------------------------------------------------------------------------- /src/img/menudots.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/img/zoomtofit.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/js/CachedProperty.ts: -------------------------------------------------------------------------------- 1 | export default class CachedProperty { 2 | private bound_properties:CachedProperty[] = []; 3 | 4 | constructor(private value:T, private updateFn:(...args:any[])=>T) { 5 | } 6 | 7 | public update(...args:any[]) { 8 | this.value = this.updateFn.apply(null, args); 9 | for (let i=0; i) { 23 | this.bound_properties.push(cached_property); 24 | } 25 | } -------------------------------------------------------------------------------- /src/js/binarysearch.ts: -------------------------------------------------------------------------------- 1 | export default function binarysearch(array:T[], target_key:number, keyFn:(t:T)=>number, return_closest_lower_if_not_found?:boolean) { 2 | if (!array.length) { 3 | return -1; // return -1 for an empty array 4 | } 5 | var upper_excl = array.length; 6 | var lower_incl = 0; 7 | var middle; 8 | while (lower_incl < upper_excl) { 9 | middle = Math.floor((upper_excl + lower_incl) / 2); 10 | var middle_key = keyFn(array[middle]); 11 | if (middle_key === target_key) { 12 | return middle; 13 | } else if (target_key > middle_key) { 14 | lower_incl = middle + 1; 15 | } else if (target_key < middle_key) { 16 | upper_excl = middle; 17 | } else { 18 | // make sure we don't infinite loop in case anything's wrong 19 | // so that those three cases don't cover everything 20 | return -1; 21 | } 22 | } 23 | if (return_closest_lower_if_not_found) { 24 | return Math.max(0, lower_incl-1); 25 | } else { 26 | return -1; 27 | } 28 | } -------------------------------------------------------------------------------- /src/js/bucketsort.ts: -------------------------------------------------------------------------------- 1 | import {extendArray, fastParseInt10, sgndiff} from "./utils"; 2 | 3 | const string_type = typeof ""; 4 | 5 | export type SortingVector = (number|string)[]; 6 | 7 | type BucketRange = { 8 | lower_index_incl:number; 9 | upper_index_excl:number; 10 | } 11 | 12 | type CompareEquals = (a:T, b:T)=>number; 13 | 14 | type GetVector = (t:T)=>SortingVector; 15 | 16 | export function bucketSort(array:T[], getVector?:(t:T)=>SortingVector, compareEquals?:CompareEquals) { 17 | // array: an array of data 18 | // getVector: a function that takes an element of array and returns an int vector. defaults to identity 19 | // compareEquals: an optional standard sort comparator - if specified it is run on the 20 | // results of the final buckets before returning 21 | getVector = getVector || function(d:SortingVector) { return d; } as any; 22 | 23 | var current_sorted_array = array; 24 | var current_bucket_ranges = [{lower_index_incl: 0, upper_index_excl: array.length}]; 25 | 26 | var new_sorted_array:T[], new_bucket_ranges:BucketRange[], bucket_range, sorted_result; 27 | 28 | // find max length vector, to use as template for vector component types, and whose length will be the sort depth 29 | var max_length_vector:SortingVector = []; 30 | var proposed_vector; 31 | for (var i=0; i max_length_vector.length) { 34 | max_length_vector = proposed_vector; 35 | } 36 | } 37 | var vector_length = max_length_vector.length; 38 | for (var vector_index=0; vector_index(array:T[], getString?:(t:T)=>string) { 74 | // array: an array of data 75 | // getString: a function that takes an element of `array` and returns a string. defaults to identity 76 | 77 | // returns strings sorted in "natural order" (i.e. numbers sorted correctly - P2 comes before P10) 78 | getString = getString || function(d:string) { return d; } as any; 79 | // compute string vectors we'll sort with 80 | var data = array.map(function(d) { 81 | return { 82 | d: d, 83 | vector: stringToVector(getString(d)) 84 | }; 85 | }); 86 | // sort 87 | var sorted = bucketSort(data, function(d) { return d.vector; }); 88 | // return original passed-in data 89 | return sorted.map(function(datum) { return datum.d; }); 90 | } 91 | 92 | export function stringToVector(string:string) { 93 | var vector = []; 94 | var len = string.length; 95 | var numberStartIncl = -1; 96 | var charCode; 97 | for (var i=0; i=48 && charCode <= 57) { 100 | // if character is numeric digit 0-9 101 | if (numberStartIncl === -1) { 102 | // if we're not in a number yet, start number 103 | numberStartIncl = i; 104 | } 105 | // otherwise, nothing to do 106 | } else { 107 | // character is not numeric 108 | if (numberStartIncl > -1) { 109 | // if we're in a number, then we need to add the number to the vector 110 | vector.push(fastParseInt10(string, numberStartIncl, i)); 111 | // and record no longer in a number 112 | numberStartIncl = -1; 113 | } 114 | // add character code to vector 115 | vector.push(charCode); 116 | } 117 | } 118 | if (numberStartIncl > -1) { 119 | // if we're in a number at the end of the string, add it to vector 120 | vector.push(fastParseInt10(string, numberStartIncl)); 121 | // no need to reset numberStartIncl because the algorithm is done 122 | } 123 | return vector; 124 | } 125 | 126 | export function compareFull(d1:T, d2:T, getVector:GetVector, compareEquals?:CompareEquals) { 127 | // utility function - comparator that describes sort order given by bucketSort 128 | var ret = compare(getVector(d1), getVector(d2)); 129 | if (ret === 0 && compareEquals) { 130 | ret = compareEquals(d1, d2); 131 | } 132 | return ret; 133 | } 134 | 135 | function compareVectorElements(elt1:number|string, elt2:number|string) { 136 | if (typeof elt1 === string_type) { 137 | return compare(stringToVector(elt1 as string), stringToVector(elt2 as string)); 138 | } else { 139 | return sgndiff(elt1 as number, elt2 as number); 140 | } 141 | } 142 | 143 | export function compare(vector1:SortingVector, vector2:SortingVector) { 144 | // utility function - comparator that describes vector sort order given by bucketSort 145 | 146 | var ret = 0; 147 | // go left to right, return result of first difference 148 | // if one vector is shorter, that one comes first 149 | var cmp; 150 | for (var i=0; i= vector2.length) { 152 | // if we've gotten here, that means no change up til i, and vector2 is shorter 153 | ret = 1; 154 | break; 155 | } 156 | cmp = compareVectorElements(vector1[i], vector2[i]); 157 | if (cmp !== 0) { 158 | ret = cmp; 159 | break; 160 | } 161 | } 162 | if (ret === 0) { 163 | if (vector1.length < vector2.length) { 164 | // we iterated through vector1, so if we get here and no difference, then if 165 | // vector1 is shorter, then it comes first 166 | ret = -1; 167 | } 168 | // theres no way to get here and no difference if vector2 is shorter 169 | } 170 | return ret; 171 | } 172 | 173 | export function bucketSortHelper( 174 | array:T[], 175 | getVector:GetVector, 176 | sort_range_lower_index_incl:number, 177 | sort_range_upper_index_excl:number, 178 | vector_index:number, 179 | isStringElt:boolean 180 | ) { 181 | // returns { sorted_array: d[], bucket_ranges:{lower_index_incl, upper_index_excl}[]}} }, 182 | // where sorted_array only contains elements from the specified range of 183 | // array[sort_range_lower_index_incl:sort_range_upper_index_excl] 184 | 185 | // stop if empty sort range, or end of vector 186 | if (!array.length || sort_range_lower_index_incl >= sort_range_upper_index_excl) { 187 | return { 188 | sorted_array: [], 189 | bucket_ranges: [] 190 | } 191 | } 192 | 193 | // bucket sort the specified range 194 | // gather elements into buckets 195 | var buckets:{[vectorElt:string]:T[]} = {}; 196 | var keys = []; 197 | var vector, key; 198 | var sortFirst = []; 199 | for (var i=sort_range_lower_index_incl; i vector_index) { 202 | key = vector[vector_index]; 203 | if (!(key in buckets)) { 204 | keys.push(key); 205 | buckets[key] = []; 206 | } 207 | buckets[key].push(array[i]); 208 | } else { 209 | // if the vector has no entry at this index, sort earlier, in line w string sorting convention of shorter strings first 210 | sortFirst.push(array[i]); 211 | } 212 | } 213 | // reduce in sorted order 214 | if (!isStringElt) { 215 | // sort numbers 216 | keys.sort(sgndiff); 217 | } else { 218 | // sort strings 219 | keys = stringSort(keys); 220 | } 221 | 222 | var sorted_array:T[] = []; 223 | var bucket_ranges = []; 224 | var lower_index_incl, upper_index_excl; 225 | // add sortFirst 226 | if (sortFirst.length) { 227 | lower_index_incl = sort_range_lower_index_incl + sorted_array.length; 228 | bucket_ranges.push({ 229 | lower_index_incl: lower_index_incl, 230 | upper_index_excl: lower_index_incl + sortFirst.length 231 | }); 232 | extendArray(sorted_array, sortFirst); 233 | } 234 | for (var i=0; i { 45 | return _hcluster(casesAndEntitites, "CASES"); 46 | } 47 | 48 | /** 49 | * Use: hclusterGeneticEntities(a); 50 | * 51 | * @return a deferred which gets resolved with the clustering result 52 | * when the clustering is done. 53 | */ 54 | export function hclusterTracks(casesAndEntitites:CasesAndEntities):Promise { 55 | return _hcluster(casesAndEntitites, "ENTITIES"); 56 | } -------------------------------------------------------------------------------- /src/js/extractrgba.ts: -------------------------------------------------------------------------------- 1 | import { fastParseInt16 } from "./utils"; 2 | import {RGBAColor} from "./oncoprintruleset"; 3 | 4 | export default function extractrgba(str:string):RGBAColor { 5 | if (str[0] === "#") { 6 | // hex, convert to rgba 7 | return hexToRGBA(str); 8 | } 9 | const match = str.match(/^[\s]*rgba\([\s]*([0-9.]+)[\s]*,[\s]*([0-9.]+)[\s]*,[\s]*([0-9.]+)[\s]*,[\s]*([0-9.]+)[\s]*\)[\s]*$/); 10 | if (match && match.length === 5) { 11 | return [parseFloat(match[1]) / 255, 12 | parseFloat(match[2]) / 255, 13 | parseFloat(match[3]) / 255, 14 | parseFloat(match[4])]; 15 | } 16 | throw `could not extract rgba from ${str}`; 17 | }; 18 | 19 | export function hexToRGBA(str:string):RGBAColor { 20 | const r = fastParseInt16(str[1] + str[2]); 21 | const g = fastParseInt16(str[3] + str[4]); 22 | const b = fastParseInt16(str[5] + str[6]); 23 | return [r,g,b,1]; 24 | } 25 | 26 | export function rgbaToHex(rgba:RGBAColor):string { 27 | let hexR = rgba[0].toString(16); 28 | let hexG = rgba[1].toString(16); 29 | let hexB = rgba[2].toString(16); 30 | if (hexR.length === 1) { 31 | hexR = '0' + hexR; 32 | } 33 | if (hexG.length === 1) { 34 | hexG = '0' + hexG; 35 | } 36 | if (hexB.length === 1) { 37 | hexB = '0' + hexB; 38 | } 39 | return `#${hexR}${hexG}${hexB}`; 40 | } -------------------------------------------------------------------------------- /src/js/haselementsininterval.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2016 Memorial Sloan-Kettering Cancer Center. 3 | * 4 | * This library is distributed in the hope that it will be useful, but WITHOUT 5 | * ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS 6 | * FOR A PARTICULAR PURPOSE. The software and documentation provided hereunder 7 | * is on an "as is" basis, and Memorial Sloan-Kettering Cancer Center has no 8 | * obligations to provide maintenance, support, updates, enhancements or 9 | * modifications. In no event shall Memorial Sloan-Kettering Cancer Center be 10 | * liable to any party for direct, indirect, special, incidental or 11 | * consequential damages, including lost profits, arising out of the use of this 12 | * software and its documentation, even if Memorial Sloan-Kettering Cancer 13 | * Center has been advised of the possibility of such damage. 14 | */ 15 | 16 | /* 17 | * This file is part of cBioPortal. 18 | * 19 | * cBioPortal is free software: you can redistribute it and/or modify 20 | * it under the terms of the GNU Affero General Public License as 21 | * published by the Free Software Foundation, either version 3 of the 22 | * License. 23 | * 24 | * This program is distributed in the hope that it will be useful, 25 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 26 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 27 | * GNU Affero General Public License for more details. 28 | * 29 | * You should have received a copy of the GNU Affero General Public License 30 | * along with this program. If not, see . 31 | */ 32 | 33 | 34 | export default function haselementsininterval(sorted_list:T[], valueFn:(t:T)=>number, lower_inc_val:number, upper_exc_val:number):boolean { 35 | // in: sorted_list, a list sorted in increasing order of valueFn 36 | // valueFn, a function that takes an element of sorted_list and returns a number 37 | // lower_inc and upper_ex: define a half-open interval [lower_inc, upper_exc) 38 | // out: boolean, true iff there are any elements whose image under valueFn is in [lower_inc, upper_exc) 39 | 40 | let test_lower_inc = 0; 41 | let test_upper_exc = sorted_list.length; 42 | let middle, middle_val; 43 | let ret = false; 44 | while (true) { 45 | if (test_lower_inc >= test_upper_exc) { 46 | break; 47 | } 48 | middle = Math.floor((test_lower_inc + test_upper_exc) / 2) 49 | middle_val = valueFn(sorted_list[middle]); 50 | if (middle_val >= upper_exc_val) { 51 | test_upper_exc = middle; 52 | } else if (middle_val < lower_inc_val) { 53 | test_lower_inc = middle + 1; 54 | } else { 55 | // otherwise, the middle value is inside the interval, 56 | // so there's at least one value inside the interval 57 | ret = true; 58 | break; 59 | } 60 | } 61 | return ret; 62 | }; -------------------------------------------------------------------------------- /src/js/main.js: -------------------------------------------------------------------------------- 1 | import OncoprintJS from './oncoprint'; 2 | 3 | if (typeof window !== "undefined") { 4 | window.Oncoprint = OncoprintJS; 5 | } 6 | 7 | module.exports = OncoprintJS; 8 | -------------------------------------------------------------------------------- /src/js/makesvgelement.ts: -------------------------------------------------------------------------------- 1 | export default function makesvgelement(tag:string, attrs:any) { 2 | const el = document.createElementNS('http://www.w3.org/2000/svg', tag); 3 | for (const k in attrs) { 4 | if (k in attrs) { 5 | el.setAttribute(k, attrs[k]); 6 | } 7 | } 8 | return el; 9 | } -------------------------------------------------------------------------------- /src/js/minimaputils.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cBioPortal/oncoprintjs/c1816a495ef8ac6e9e060a0c833ed7cb01f65a32/src/js/minimaputils.ts -------------------------------------------------------------------------------- /src/js/modelutils.ts: -------------------------------------------------------------------------------- 1 | import OncoprintModel, {TrackGroupProp, TrackProp} from "./oncoprintmodel"; 2 | 3 | export function calculateTrackTops(model:OncoprintModel, zoomed:boolean) { 4 | return calculateTrackAndHeaderTops(model, zoomed).trackTops; 5 | } 6 | 7 | export function calculateHeaderTops(model:OncoprintModel, zoomed:boolean) { 8 | return calculateTrackAndHeaderTops(model, zoomed).headerTops; 9 | } 10 | 11 | export function calculateTrackAndHeaderTops(model:OncoprintModel, zoomed:boolean) { 12 | const trackTops:TrackProp = {}; 13 | const headerTops:TrackGroupProp = {}; 14 | const groups = model.getTrackGroups(); 15 | let y = 0; 16 | for (let i = 0; i < groups.length; i++) { 17 | const group = groups[i]; 18 | if (group.header) { 19 | headerTops[i] = y; 20 | } 21 | if (group.tracks.length > 0) { 22 | // space at top for header 23 | y += model.getTrackGroupHeaderHeight(group); 24 | } 25 | for (let j = 0; j < group.tracks.length; j++) { 26 | const track_id = group.tracks[j]; 27 | trackTops[track_id] = y; 28 | y += model.getTrackHeight(track_id, !zoomed); 29 | } 30 | if (group.tracks.length > 0) { 31 | // space at bottom for padding 32 | y += model.getTrackGroupPadding(!zoomed); 33 | } 34 | } 35 | return {trackTops, headerTops}; 36 | } -------------------------------------------------------------------------------- /src/js/oncoprintheaderview.ts: -------------------------------------------------------------------------------- 1 | import OncoprintModel from "./oncoprintmodel"; 2 | import menuDotsIcon from "../img/menudots.svg"; 3 | import svgfactory from "./svgfactory"; 4 | import $ from "jquery"; 5 | import ClickEvent = JQuery.ClickEvent; 6 | import {CLOSE_MENUS_EVENT as TRACK_OPTIONS_VIEW_CLOSE_MENUS_EVENT} from "./oncoprinttrackoptionsview"; 7 | 8 | const MENU_DOTS_SIZE = 20; 9 | const LABEL_CLASS = "oncoprintjs__header__label"; 10 | const TOGGLE_BTN_CLASS = "oncoprintjs__header__toggle_btn_img"; 11 | const TOGGLE_BTN_OPEN_CLASS = "oncoprintjs__header__open"; 12 | const DROPDOWN_CLASS = "oncoprintjs__header__dropdown"; 13 | const SEPARATOR_CLASS = "oncoprintjs__header__separator"; 14 | const NTH_CLASS_PREFIX = "track-group-"; 15 | 16 | const FADE_MS = 100; 17 | 18 | const HEADER_FONT_SIZE = 16; 19 | 20 | export const CLOSE_MENUS_EVENT = "oncoprint-header-view.do-close-menus"; 21 | 22 | export default class OncoprintHeaderView { 23 | private rendering_suppressed = false; 24 | private $occluded_ctr:JQuery; // holds labels and menu buttons, which can be occluded by scrolling 25 | private $dropdowns_ctr:JQuery; 26 | private clickHandler:()=>void; 27 | private $dropdowns:JQuery[] = []; 28 | 29 | constructor($div:JQuery) { 30 | $div.css({ 31 | position:'relative', 32 | 'pointer-events':'none' 33 | }); 34 | 35 | const $occluding_superctr = $("
").appendTo($div).css({ 36 | position:'relative', 37 | 'overflow-y':'hidden', 38 | 'overflow-x':'hidden', 39 | width:"100%", 40 | height:"100%" 41 | }); 42 | this.$occluded_ctr = $("
").appendTo($occluding_superctr).css({ 43 | position:'absolute', 44 | width: "100%", 45 | height: "100%" 46 | }); 47 | 48 | this.$dropdowns_ctr = $("
").appendTo($div).css({ 49 | position:'absolute', 50 | width: "100%", 51 | height: "100%" 52 | }); 53 | 54 | 55 | this.clickHandler = ()=>{ 56 | $(document).trigger(CLOSE_MENUS_EVENT); 57 | }; 58 | $(document).on("click", this.clickHandler); 59 | 60 | $(document).on(CLOSE_MENUS_EVENT, ()=>{ 61 | this.closeAllDropdowns(); 62 | }); 63 | } 64 | 65 | public destroy() { 66 | $(document).off("click", this.clickHandler); 67 | $(document).off(CLOSE_MENUS_EVENT); 68 | } 69 | 70 | private closeAllDropdowns() { 71 | for (const $dropdown of this.$dropdowns) { 72 | $dropdown.fadeOut(FADE_MS); 73 | } 74 | } 75 | 76 | private closeDropdownsExcept($keep_open_dropdown:JQuery) { 77 | for (const $dropdown of this.$dropdowns) { 78 | if ($dropdown !== $keep_open_dropdown) { 79 | $dropdown.fadeOut(FADE_MS); 80 | } 81 | } 82 | $(document).trigger(TRACK_OPTIONS_VIEW_CLOSE_MENUS_EVENT); 83 | } 84 | 85 | private static $makeDropdownOption(text:string, weight:string, isDisabled?:()=>boolean, callback?:(evt:ClickEvent)=>void) { 86 | const li = $('
  • ').text(text).css({'font-weight': weight, 'font-size': 12, 'border-bottom': '1px solid rgba(0,0,0,0.3)'}); 87 | const disabled = isDisabled && isDisabled(); 88 | if (!disabled) { 89 | if (callback) { 90 | li.addClass("clickable"); 91 | li.css({'cursor': 'pointer'}); 92 | li.click(callback) 93 | .hover(function () { 94 | $(this).css({'background-color': 'rgb(200,200,200)'}); 95 | }, function () { 96 | $(this).css({'background-color': 'rgba(255,255,255,0)'}); 97 | }); 98 | } else { 99 | li.click(function(evt) { evt.stopPropagation(); }); 100 | } 101 | } else { 102 | li.addClass("disabled"); 103 | li.css({'color': 'rgb(200, 200, 200)', 'cursor': 'default'}); 104 | } 105 | return li; 106 | } 107 | 108 | private static $makeDropdownSeparator() { 109 | return $('
  • ').css({'border-top': '1px solid black'}).addClass(SEPARATOR_CLASS); 110 | } 111 | 112 | 113 | public render(model:OncoprintModel) { 114 | // clear existing elements 115 | this.$occluded_ctr.empty(); 116 | this.$occluded_ctr.css({ 117 | top:-model.getVertScroll() 118 | }); 119 | this.$dropdowns_ctr.empty(); 120 | this.$dropdowns_ctr.css({ 121 | top:-model.getVertScroll() 122 | }); 123 | this.$dropdowns = []; 124 | 125 | // add headers 126 | const trackGroups = model.getTrackGroups(); 127 | const headerTops = model.getZoomedHeaderTops(); 128 | 129 | trackGroups.forEach((group, trackGroupIndex)=>{ 130 | if (group.header) { 131 | const $headerDiv = $("
    ").css({ 132 | 'pointer-events':'auto' 133 | }); 134 | 135 | // add label 136 | $(`${group.header.label.text}`) 137 | .appendTo($headerDiv) 138 | .css({ 139 | "margin-right":10, 140 | // TODO - custom styling 141 | "font-weight":"bold", 142 | "text-decoration":"underline", 143 | "font-size":HEADER_FONT_SIZE, 144 | "font-family":"Arial" 145 | }).addClass(LABEL_CLASS); 146 | 147 | if (group.header.options.length > 0) { 148 | // add dropdown menu 149 | const $dropdown = $('
      ') 150 | .appendTo(this.$dropdowns_ctr) 151 | .css({ 152 | 'position':'absolute', 153 | 'width': 120, 154 | 'display': 'none', 155 | 'list-style-type': 'none', 156 | 'padding-left': '6', 157 | 'padding-right': '6', 158 | 'float': 'right', 159 | 'background-color': 'rgb(255,255,255)', 160 | 'left':'0px', 161 | 'top': headerTops[trackGroupIndex] + MENU_DOTS_SIZE, 162 | 'pointer-events':'auto' 163 | }) 164 | .addClass(DROPDOWN_CLASS).addClass(NTH_CLASS_PREFIX+(trackGroupIndex)); 165 | 166 | this.$dropdowns.push($dropdown); 167 | 168 | const populateDropdownOptions = ()=>{ 169 | // repopulate dropdown every time it opens, and every time an option is clicked, 170 | // in order to update dynamic disabled status and weight 171 | $dropdown.empty(); 172 | // add dropdown options 173 | group.header.options.forEach((option)=>{ 174 | if (option.separator) { 175 | $dropdown.append(OncoprintHeaderView.$makeDropdownSeparator()); 176 | } else { 177 | $dropdown.append(OncoprintHeaderView.$makeDropdownOption( 178 | option.label || "", 179 | option.weight ? option.weight() : "normal", 180 | option.disabled, 181 | function(evt) { 182 | evt.stopPropagation(); 183 | option.onClick && option.onClick(trackGroupIndex); 184 | populateDropdownOptions(); 185 | } 186 | )); 187 | } 188 | }); 189 | }; 190 | 191 | // add dropdown button 192 | const $img = $("") 193 | .appendTo($headerDiv) 194 | .attr({ 195 | src: menuDotsIcon, 196 | width:MENU_DOTS_SIZE, 197 | height:MENU_DOTS_SIZE 198 | }) 199 | .css({ 200 | cursor:"pointer", 201 | border:"1px solid rgba(125,125,125,0)", 202 | display:"inline-block" 203 | }) 204 | .addClass(TOGGLE_BTN_CLASS).addClass(NTH_CLASS_PREFIX+(trackGroupIndex)) 205 | .on("click", (evt)=>{ 206 | evt.stopPropagation(); 207 | if ($dropdown.is(":visible")) { 208 | $img.removeClass(TOGGLE_BTN_OPEN_CLASS); 209 | $dropdown.fadeOut(FADE_MS); 210 | } else { 211 | populateDropdownOptions(); 212 | $dropdown.css('left', $img.offset().left); 213 | $img.addClass(TOGGLE_BTN_OPEN_CLASS); 214 | $dropdown.fadeIn(FADE_MS); 215 | this.closeDropdownsExcept($dropdown); 216 | } 217 | }); 218 | } 219 | 220 | $headerDiv.css({ 221 | position:"absolute", 222 | top:headerTops[trackGroupIndex], 223 | left:0, 224 | width:"100%" 225 | }); 226 | this.$occluded_ctr.append($headerDiv); 227 | } 228 | }); 229 | } 230 | 231 | public setScroll(model:OncoprintModel) { 232 | this.$occluded_ctr.css({ 233 | top:-model.getVertScroll() 234 | }); 235 | this.$dropdowns_ctr.css({ 236 | top:-model.getVertScroll() 237 | }); 238 | 239 | this.closeAllDropdowns(); 240 | } 241 | 242 | public setVertScroll(model:OncoprintModel) { 243 | this.setScroll(model); 244 | } 245 | 246 | public suppressRendering() { 247 | this.rendering_suppressed = true; 248 | } 249 | public releaseRendering(model:OncoprintModel) { 250 | this.rendering_suppressed = false; 251 | this.render(model); 252 | } 253 | public toSVGGroup(model:OncoprintModel, offset_x:number, offset_y:number) { 254 | const group = svgfactory.group((offset_x || 0), (offset_y || 0)); 255 | const trackGroups = model.getTrackGroups(); 256 | const headerTops = model.getZoomedHeaderTops(); 257 | 258 | trackGroups.forEach((trackGroup, index)=>{ 259 | const header = trackGroup.header 260 | if (header) { 261 | const y = headerTops[index]; 262 | group.appendChild(svgfactory.text( 263 | header.label.text, 264 | 0, y, 265 | HEADER_FONT_SIZE, 266 | "Arial", 267 | "bold", 268 | undefined, 269 | undefined, 270 | "underline" 271 | )) 272 | } 273 | }); 274 | 275 | return group; 276 | } 277 | } -------------------------------------------------------------------------------- /src/js/oncoprintlegendrenderer.ts: -------------------------------------------------------------------------------- 1 | import svgfactory from './svgfactory'; 2 | import $ from 'jquery'; 3 | import OncoprintModel from "./oncoprintmodel"; 4 | import {Rule, RuleWithId} from "./oncoprintruleset"; 5 | 6 | function nodeIsVisible(node:HTMLElement) { 7 | let ret = true; 8 | while (node && node.tagName.toLowerCase() !== "html") { 9 | if ($(node).css('display') === 'none') { 10 | ret = false; 11 | break; 12 | } 13 | node = node.parentNode as HTMLElement; 14 | } 15 | return ret; 16 | } 17 | 18 | export default class OncoprintLegendView { 19 | 20 | private $svg:JQuery; 21 | private rendering_suppressed = false; 22 | private width:number; 23 | 24 | private rule_set_label_config = { 25 | weight: 'bold', 26 | size: 12, 27 | font: 'Arial' 28 | }; 29 | private rule_label_config = { 30 | weight: 'normal', 31 | size: 12, 32 | font: 'Arial' 33 | }; 34 | 35 | private padding_after_rule_set_label = 10; 36 | private padding_between_rules = 20; 37 | private padding_between_rule_set_rows = 10; 38 | 39 | constructor (private $div:JQuery, private base_width:number, private base_height:number) { 40 | this.$svg = $(svgfactory.svg(200,200)).appendTo(this.$div); 41 | this.width = $div.width(); 42 | } 43 | 44 | private renderLegend(model:OncoprintModel, target_svg?:SVGElement, show_all?:boolean) { 45 | if (this.rendering_suppressed) { 46 | return; 47 | } 48 | if (typeof target_svg === 'undefined') { 49 | target_svg = this.$svg[0]; 50 | } 51 | if (!nodeIsVisible(target_svg as any as HTMLElement)) { 52 | return; 53 | } 54 | $(target_svg).empty(); 55 | const defs = svgfactory.defs(); 56 | target_svg.appendChild(defs); 57 | 58 | const everything_group = svgfactory.group(0,0); 59 | target_svg.appendChild(everything_group); 60 | 61 | const rule_sets = model.getRuleSets(); 62 | let y = 0; 63 | const rule_start_x = 200; 64 | for (let i=0; i 0) { 77 | const label = svgfactory.text(rule_sets[i].legend_label, 0, 0, 12, 'Arial', 'bold'); 78 | rule_set_group.appendChild(label); 79 | svgfactory.wrapText(label, rule_start_x); 80 | } 81 | })(); 82 | 83 | let x = rule_start_x + this.padding_after_rule_set_label; 84 | let in_group_y_offset = 0; 85 | 86 | const labelSort = function(ruleA:RuleWithId, ruleB:RuleWithId) { 87 | const labelA = ruleA.rule.legend_label; 88 | const labelB = ruleB.rule.legend_label; 89 | if (labelA && labelB) { 90 | return labelA.localeCompare(labelB); 91 | } else if (!labelA && !labelB) { 92 | return 0; 93 | } else if (!labelA) { 94 | return -1; 95 | } else if (!labelB) { 96 | return 1; 97 | } 98 | }; 99 | 100 | rules.sort(function(ruleA, ruleB) { 101 | // sort, by legend_order, then alphabetically 102 | const orderA = ruleA.rule.legend_order; 103 | const orderB = ruleB.rule.legend_order; 104 | 105 | if (typeof orderA === "undefined" && typeof orderB === "undefined") { 106 | // if neither have defined order, then sort alphabetically 107 | return labelSort(ruleA, ruleB); 108 | } else if (typeof orderA !== "undefined" && typeof orderB !== "undefined") { 109 | // if both have defined order, sort by order 110 | if (orderA < orderB) { 111 | return -1; 112 | } else if (orderA > orderB) { 113 | return 1; 114 | } else { 115 | // if order is same, sort alphabetically 116 | return labelSort(ruleA, ruleB); 117 | } 118 | } else if (typeof orderA === "undefined") { 119 | if (orderB === Number.POSITIVE_INFINITY) { 120 | return -1; // A comes before B regardless, if B is forced to end 121 | } else { 122 | //otherwise, A comes after B if B has defined order and A doesnt 123 | return 1; 124 | } 125 | } else if (typeof orderB === "undefined") { 126 | if (orderA === Number.POSITIVE_INFINITY) { 127 | return 1; // A comes after B regardless, if A is forced to end 128 | } else { 129 | // otherwise, A comes before B if A has defined order and B doesnt 130 | return -1; 131 | } 132 | } 133 | }); 134 | for (let j=0; j this.width) { 143 | x = rule_start_x + this.padding_after_rule_set_label; 144 | in_group_y_offset = rule_set_group.getBBox().height + this.padding_between_rule_set_rows; 145 | group.setAttribute('transform', 'translate('+x+','+in_group_y_offset+')'); 146 | } 147 | x += group.getBBox().width; 148 | x += this.padding_between_rules; 149 | } 150 | y += rule_set_group.getBBox().height; 151 | y += 3*this.padding_between_rule_set_rows; 152 | } 153 | const everything_box = everything_group.getBBox(); 154 | this.$svg[0].setAttribute('width', everything_box.width.toString()); 155 | // add 10px to height to give room for rectangle stroke, which doesn't factor in accurately into the bounding box 156 | // so that bounding boxes are too small to show the entire stroke (see https://github.com/cBioPortal/cbioportal/issues/3994) 157 | this.$svg[0].setAttribute('height', (everything_box.height + 10).toString()); 158 | } 159 | 160 | private ruleToSVGGroup(rule:Rule, model:OncoprintModel, target_svg:SVGElement, target_defs:SVGDefsElement) { 161 | const root = svgfactory.group(0,0); 162 | const config = rule.getLegendConfig(); 163 | if (config.type === 'rule') { 164 | const concrete_shapes = rule.apply(config.target, model.getCellWidth(true), this.base_height); 165 | if (rule.legend_base_color) { 166 | // generate backgrounds 167 | const baseRect = svgfactory.rect(0, 0, model.getCellWidth(true), this.base_height, { 168 | type: "rgba", 169 | value: rule.legend_base_color 170 | }); 171 | root.appendChild(baseRect); 172 | } 173 | // generate shapes 174 | for (let i=0; istring; 47 | type NumberParamFunction = (d:Datum)=>number; 48 | type RGBAParamFunction = (d:Datum)=>RGBAColor; 49 | type ParamFunction = StringParamFunction | NumberParamFunction | RGBAParamFunction; 50 | 51 | export type ShapeParams = 52 | {[x in StringParameter]?:string|StringParamFunction} & 53 | {[x in NumberParameter]?:number|NumberParamFunction} & 54 | {[x in RGBAParameter]?:RGBAColor|RGBAParamFunction }; 55 | 56 | type ShapeParamsWithType = { 57 | [x in StringParameter]?:({ type:"function", value:StringParamFunction} | {type:"value", value:string}) 58 | } & { 59 | [x in NumberParameter]?:({ type:"function", value:NumberParamFunction} | {type:"value", value:number}) 60 | } & { 61 | [x in RGBAParameter]?:({ type:"function", value:RGBAParamFunction} | {type:"value", value:RGBAColor}) 62 | }; 63 | 64 | export type ComputedShapeParams = {[x in StringParameter]?:string} & {[x in NumberParameter]?:number} & {[x in RGBAParameter]?:RGBAColor}; 65 | 66 | function isPercentParam(param_name:string):param_name is PercentNumberParameter { 67 | return param_name in percent_parameter_name_to_dimension_index; 68 | } 69 | 70 | export class Shape { 71 | 72 | private static cache:{[hash:string]:ComputedShapeParams} = {}; // shape cache to reuse objects and thus save memory 73 | private params_with_type:ShapeParamsWithType = {}; 74 | private onlyDependsOnWidthAndHeight:boolean; 75 | 76 | private instanceCache = { 77 | lastComputedParams: null as ComputedShapeParams|null, 78 | lastWidth:-1, 79 | lastHeight:-1 80 | }; 81 | 82 | constructor(private params:ShapeParams) { 83 | this.completeWithDefaults(); 84 | this.markParameterTypes(); 85 | } 86 | 87 | public static hashComputedShape(computed_params:ComputedShapeParams, z_index?:number|string) { 88 | return hash_parameter_order.reduce(function (hash:string, param_name:Parameter) { 89 | return hash + "," + computed_params[param_name]; 90 | }, "") + "," + z_index; 91 | } 92 | 93 | private static getCachedShape(computed_params:ComputedShapeParams) { 94 | const hash = Shape.hashComputedShape(computed_params); 95 | Shape.cache[hash] = Shape.cache[hash] || Object.freeze(computed_params); 96 | return Shape.cache[hash]; 97 | } 98 | 99 | public getRequiredParameters():Parameter[] { 100 | throw "Not defined for base class"; 101 | } 102 | 103 | public completeWithDefaults() { 104 | const required_parameters = this.getRequiredParameters(); 105 | for (let i=0; i = {}; 136 | const param_names = Object.keys(this.params_with_type) as Parameter[]; 137 | const dimensions:[number, number] = [base_width, base_height]; 138 | for (let i=0; i = 169 | {[x in ShapeParamType & StringParameter]:string} & 170 | {[x in ShapeParamType & NumberParameter]:number} & 171 | {[x in ShapeParamType & RGBAParameter]:RGBAColor}; 172 | 173 | type RectangleParameter = "width" | "height" | "x" | "y" | "z" | "stroke" | "stroke-width" | "fill"; 174 | export type ComputedRectangleParams = SpecificComputedShapeParams; 175 | export class Rectangle extends Shape { 176 | public getRequiredParameters(): RectangleParameter[] { 177 | return ['width', 'height', 'x', 'y', 'z', 'stroke', 'fill', 'stroke-width']; 178 | } 179 | } 180 | 181 | type TriangleParameter = "x1" | "x2" | "x3" | "y1" | "y2" | "y3" | "z" | "stroke" | "stroke-width" | "fill"; 182 | export type ComputedTriangleParams = SpecificComputedShapeParams; 183 | export class Triangle extends Shape { 184 | public getRequiredParameters(): TriangleParameter[] { 185 | return ['x1', 'x2', 'x3', 'y1', 'y2', 'y3', 'z', 'stroke', 'fill', 'stroke-width']; 186 | } 187 | } 188 | 189 | export type EllipseParameter = "width" | "height" | "x" | "y" | "z" | "stroke" | "stroke-width" | "fill"; 190 | export type ComputedEllipseParams = SpecificComputedShapeParams; 191 | export class Ellipse extends Shape { 192 | public getRequiredParameters(): EllipseParameter[] { 193 | return ['width', 'height', 'x', 'y', 'z', 'stroke', 'fill', 'stroke-width']; 194 | } 195 | } 196 | 197 | export type LineParameter = "x1" | "y1" | "x2" | "y2" | "z" | "stroke" | "stroke-width"; 198 | export type ComputedLineParams = SpecificComputedShapeParams; 199 | export class Line extends Shape { 200 | public getRequiredParameters(): LineParameter[] { 201 | return ['x1', 'x2', 'y1', 'y2', 'z', 'stroke', 'stroke-width']; 202 | } 203 | } -------------------------------------------------------------------------------- /src/js/oncoprintshapetosvg.ts: -------------------------------------------------------------------------------- 1 | import makeSVGElement from './makesvgelement'; 2 | import extractRGBA from './extractrgba'; 3 | import { 4 | ComputedEllipseParams, ComputedLineParams, 5 | ComputedRectangleParams, 6 | ComputedShapeParams, 7 | ComputedTriangleParams 8 | } from "./oncoprintshape"; 9 | import {rgbString} from "./utils"; 10 | 11 | function extractColor(str:string) { 12 | if (str.indexOf("rgb(") > -1) { 13 | return { 14 | 'rgb': str, 15 | 'opacity': 1 16 | }; 17 | } 18 | const rgba_arr = extractRGBA(str); 19 | return { 20 | 'rgb': 'rgb('+rgba_arr[0]*255+','+rgba_arr[1]*255+','+rgba_arr[2]*255+')', 21 | 'opacity': rgba_arr[3] 22 | }; 23 | } 24 | 25 | function rectangleToSVG(params:ComputedRectangleParams, offset_x:number, offset_y:number) { 26 | return makeSVGElement('rect', { 27 | width: params.width, 28 | height: params.height, 29 | x: params.x + offset_x, 30 | y: params.y + offset_y, 31 | stroke: rgbString(params.stroke), 32 | 'stroke-opacity': params.stroke[3], 33 | 'stroke-width': params['stroke-width'], 34 | fill: rgbString(params.fill), 35 | 'fill-opacity': params.fill[3] 36 | }); 37 | } 38 | 39 | function triangleToSVG(params:ComputedTriangleParams, offset_x:number, offset_y:number) { 40 | return makeSVGElement('polygon', { 41 | points: [[params.x1 + offset_x, params.y1 + offset_y], [params.x2 + offset_x, params.y2 + offset_y], [params.x3 + offset_x, params.y3 + offset_y]].map(function (a) { 42 | return a[0] + ',' + a[1]; 43 | }).join(' '), 44 | stroke: rgbString(params.stroke), 45 | 'stroke-opacity': params.stroke[3], 46 | 'stroke-width': params['stroke-width'], 47 | fill: rgbString(params.fill), 48 | 'fill-opacity': params.fill[3] 49 | }); 50 | } 51 | 52 | function ellipseToSVG(params:ComputedEllipseParams, offset_x:number, offset_y:number) { 53 | return makeSVGElement('ellipse', { 54 | rx: params.width / 2, 55 | height: params.height / 2, 56 | cx: params.x + offset_x, 57 | cy: params.y + offset_y, 58 | stroke: rgbString(params.stroke), 59 | 'stroke-opacity': params.stroke[3], 60 | 'stroke-width': params['stroke-width'], 61 | fill: rgbString(params.fill), 62 | 'fill-opacity': params.fill[3] 63 | }); 64 | } 65 | 66 | function lineToSVG(params:ComputedLineParams, offset_x:number, offset_y:number) { 67 | return makeSVGElement('line', { 68 | x1: params.x1 + offset_x, 69 | y1: params.y1 + offset_y, 70 | x2: params.x2 + offset_x, 71 | y2: params.y2 + offset_y, 72 | stroke: rgbString(params.stroke), 73 | 'stroke-opacity': params.stroke[3], 74 | 'stroke-width': params['stroke-width'], 75 | }); 76 | } 77 | 78 | export default function shapeToSVG(oncoprint_shape_computed_params:ComputedShapeParams, offset_x:number, offset_y:number) { 79 | var type = oncoprint_shape_computed_params.type; 80 | if (type === 'rectangle') { 81 | return rectangleToSVG(oncoprint_shape_computed_params as ComputedRectangleParams, offset_x, offset_y); 82 | } else if (type === 'triangle') { 83 | return triangleToSVG(oncoprint_shape_computed_params as ComputedTriangleParams, offset_x, offset_y); 84 | } else if (type === 'ellipse') { 85 | return ellipseToSVG(oncoprint_shape_computed_params as ComputedEllipseParams, offset_x, offset_y); 86 | } else if (type === 'line') { 87 | return lineToSVG(oncoprint_shape_computed_params as ComputedLineParams, offset_x, offset_y); 88 | } 89 | } -------------------------------------------------------------------------------- /src/js/oncoprintshapetovertexes.ts: -------------------------------------------------------------------------------- 1 | import {ComputedShapeParams, Rectangle} from "./oncoprintshape"; 2 | import {RGBAColor} from "./oncoprintruleset"; 3 | 4 | const halfsqrt2 = Math.sqrt(2) / 2; 5 | 6 | function normalizeRGBA(color:RGBAColor):[number, number, number, number] { 7 | return [ 8 | color[0], 9 | color[1], 10 | color[2], 11 | color[3]*255 12 | ] 13 | } 14 | 15 | type AddVertexCallback = (vertex:[number,number,number], color:[number,number,number,number])=>void; 16 | 17 | function rectangleToVertexes(params:ComputedShapeParams, z_index:number, addVertex:AddVertexCallback) { 18 | const x = params.x, y = params.y, height = params.height, width = params.width; 19 | 20 | // Fill 21 | const fill_rgba = normalizeRGBA(params.fill); 22 | addVertex([x,y,z_index], fill_rgba); 23 | addVertex([x+width, y, z_index], fill_rgba); 24 | addVertex([x+width, y+height, z_index], fill_rgba); 25 | 26 | addVertex([x,y,z_index], fill_rgba); 27 | addVertex([x+width, y+height, z_index], fill_rgba); 28 | addVertex([x,y+height,z_index],fill_rgba); 29 | 30 | // Stroke 31 | const stroke_width = params['stroke-width']; 32 | if (stroke_width > 0) { 33 | // left side 34 | const stroke_rgba = normalizeRGBA(params.stroke); 35 | addVertex([x, y, z_index], stroke_rgba); 36 | addVertex([x + stroke_width, y, z_index], stroke_rgba); 37 | addVertex([x + stroke_width, y + height, z_index], stroke_rgba); 38 | 39 | addVertex([x, y, z_index], stroke_rgba); 40 | addVertex([x + stroke_width, y + height, z_index], stroke_rgba); 41 | addVertex([x, y + height, z_index], stroke_rgba); 42 | 43 | // right side 44 | addVertex([x + width, y, z_index], stroke_rgba); 45 | addVertex([x + width - stroke_width, y, z_index], stroke_rgba); 46 | addVertex([x + width - stroke_width, y + height, z_index], stroke_rgba); 47 | 48 | addVertex([x + width, y, z_index], stroke_rgba); 49 | addVertex([x + width - stroke_width, y + height, z_index], stroke_rgba); 50 | addVertex([x + width, y + height, z_index], stroke_rgba); 51 | 52 | // top side 53 | addVertex([x, y, z_index], stroke_rgba); 54 | addVertex([x + width, y, z_index], stroke_rgba); 55 | addVertex([x + width, y + stroke_width, z_index], stroke_rgba); 56 | 57 | addVertex([x, y, z_index], stroke_rgba); 58 | addVertex([x + width, y + stroke_width, z_index], stroke_rgba); 59 | addVertex([x, y + stroke_width, z_index], stroke_rgba); 60 | 61 | // bottom side 62 | addVertex([x, y + height, z_index], stroke_rgba); 63 | addVertex([x + width, y + height, z_index], stroke_rgba); 64 | addVertex([x + width, y + height - stroke_width, z_index], stroke_rgba); 65 | 66 | addVertex([x, y + height, z_index], stroke_rgba); 67 | addVertex([x + width, y + height - stroke_width, z_index], stroke_rgba); 68 | addVertex([x, y + height - stroke_width, z_index], stroke_rgba); 69 | } 70 | } 71 | 72 | function triangleToVertexes(params:ComputedShapeParams, z_index:number, addVertex:AddVertexCallback) { 73 | const fill_rgba = normalizeRGBA(params.fill); 74 | addVertex([params.x1, params.y1, z_index], fill_rgba); 75 | addVertex([params.x2, params.y2, z_index], fill_rgba); 76 | addVertex([params.x3, params.y3, z_index], fill_rgba); 77 | } 78 | 79 | function ellipseToVertexes(params:ComputedShapeParams, z_index:number, addVertex:AddVertexCallback) { 80 | const center = {x:params.x +params.width / 2, y:params.y +params.height / 2}; 81 | const horzrad =params.width / 2; 82 | const vertrad =params.height / 2; 83 | 84 | const fill_rgba = normalizeRGBA(params.fill); 85 | addVertex([center.x, center.y, z_index], fill_rgba); 86 | addVertex([center.x + horzrad, center.y, z_index], fill_rgba); 87 | addVertex([center.x + halfsqrt2 * horzrad, center.y + halfsqrt2 * vertrad, z_index], fill_rgba); 88 | 89 | addVertex([center.x, center.y, z_index], fill_rgba); 90 | addVertex([center.x + halfsqrt2 * horzrad, center.y + halfsqrt2 * vertrad, z_index], fill_rgba); 91 | addVertex([center.x, center.y + vertrad, z_index], fill_rgba); 92 | 93 | addVertex([center.x, center.y, z_index], fill_rgba); 94 | addVertex([center.x, center.y + vertrad, z_index], fill_rgba); 95 | addVertex([center.x - halfsqrt2 * horzrad, center.y + halfsqrt2 * vertrad, z_index], fill_rgba); 96 | 97 | addVertex([center.x, center.y, z_index], fill_rgba); 98 | addVertex([center.x - halfsqrt2 * horzrad, center.y + halfsqrt2 * vertrad, z_index], fill_rgba); 99 | addVertex([center.x - horzrad, center.y, z_index], fill_rgba); 100 | 101 | addVertex([center.x, center.y, z_index], fill_rgba); 102 | addVertex([center.x - horzrad, center.y, z_index], fill_rgba); 103 | addVertex([center.x - halfsqrt2 * horzrad, center.y - halfsqrt2 * vertrad, z_index], fill_rgba); 104 | 105 | addVertex([center.x, center.y, z_index], fill_rgba); 106 | addVertex([center.x - halfsqrt2 * horzrad, center.y - halfsqrt2 * vertrad, z_index], fill_rgba); 107 | addVertex([center.x, center.y - vertrad, z_index], fill_rgba); 108 | 109 | addVertex([center.x, center.y, z_index], fill_rgba); 110 | addVertex([center.x, center.y - vertrad, z_index], fill_rgba); 111 | addVertex([center.x + halfsqrt2 * horzrad, center.y - halfsqrt2 * vertrad, z_index], fill_rgba); 112 | 113 | addVertex([center.x, center.y, z_index], fill_rgba); 114 | addVertex([center.x + halfsqrt2 * horzrad, center.y - halfsqrt2 * vertrad, z_index], fill_rgba); 115 | addVertex([center.x + horzrad, center.y, z_index], fill_rgba); 116 | } 117 | 118 | function lineToVertexes(params:ComputedShapeParams, z_index:number, addVertex:AddVertexCallback) { 119 | // For simplicity of dealing with webGL we'll implement lines as thin triangle pairs 120 | let x1 = params.x1; 121 | let x2 = params.x2; 122 | let y1 = params.y1; 123 | let y2 = params.y2; 124 | 125 | if (x1 !== x2) { 126 | // WLOG make x1,y1 the one on the left 127 | if (Math.min(x1, x2) === x2) { 128 | const tmpx1 = x1; 129 | const tmpy1 = y1; 130 | x1 = x2; 131 | y1 = y2; 132 | x2 = tmpx1; 133 | y2 = tmpy1; 134 | } 135 | } 136 | 137 | const perpendicular_vector = [y2 - y1, x1 - x2]; 138 | const perpendicular_vector_length = Math.sqrt(perpendicular_vector[0] * perpendicular_vector[0] + perpendicular_vector[1] * perpendicular_vector[1]); 139 | const unit_perp_vector = [perpendicular_vector[0] / perpendicular_vector_length, perpendicular_vector[1] / perpendicular_vector_length]; 140 | 141 | const half_stroke_width = params['stroke-width'] / 2; 142 | const direction1 = [unit_perp_vector[0] * half_stroke_width, unit_perp_vector[1] * half_stroke_width]; 143 | const direction2 = [direction1[0] * -1, direction1[1] * -1]; 144 | const A = [x1 + direction1[0], y1 + direction1[1]]; 145 | const B = [x1 + direction2[0], y1 + direction2[1]]; 146 | const C = [x2 + direction1[0], y2 + direction1[1]]; 147 | const D = [x2 + direction2[0], y2 + direction2[1]]; 148 | 149 | const stroke_rgba = normalizeRGBA(params.stroke); 150 | addVertex([A[0], A[1], z_index], stroke_rgba); 151 | addVertex([B[0], B[1], z_index], stroke_rgba); 152 | addVertex([C[0], C[1], z_index], stroke_rgba); 153 | 154 | addVertex([C[0], C[1], z_index], stroke_rgba); 155 | addVertex([D[0], D[1], z_index], stroke_rgba); 156 | addVertex([B[0], B[1], z_index], stroke_rgba); 157 | } 158 | 159 | export default function(oncoprint_shape_computed_params:ComputedShapeParams, z_index:number, addVertex:AddVertexCallback) { 160 | // target_position_array is an array with 3-d float vertexes 161 | // target_color_array is an array with rgba values in [0,1] 162 | // We pass them in to save on concatenation costs 163 | 164 | const type = oncoprint_shape_computed_params.type; 165 | if (type === "rectangle") { 166 | return rectangleToVertexes(oncoprint_shape_computed_params, z_index, addVertex); 167 | } else if (type === "triangle") { 168 | return triangleToVertexes(oncoprint_shape_computed_params, z_index, addVertex); 169 | } else if (type === "ellipse") { 170 | return ellipseToVertexes(oncoprint_shape_computed_params, z_index, addVertex); 171 | } else if (type === "line") { 172 | return lineToVertexes(oncoprint_shape_computed_params, z_index, addVertex); 173 | } 174 | } 175 | 176 | export function getNumWebGLVertexes(shape:ComputedShapeParams) { 177 | let ret:number; 178 | switch (shape.type) { 179 | case 'rectangle': 180 | if (shape['stroke-width'] > 0) { 181 | ret = 30; 182 | } else { 183 | ret = 6; 184 | } 185 | break; 186 | case 'triangle': 187 | ret = 3; 188 | break; 189 | case 'ellipse': 190 | ret = 24; 191 | break; 192 | case 'line': 193 | ret = 6; 194 | break; 195 | } 196 | return ret; 197 | } -------------------------------------------------------------------------------- /src/js/oncoprinttooltip.ts: -------------------------------------------------------------------------------- 1 | import $ from "jquery"; 2 | 3 | const TOOLTIP_CLASS = "oncoprintjs__tooltip"; 4 | 5 | export type OncoprintTooltipParams = { noselect?:boolean }; 6 | 7 | export default class OncoprintToolTip { 8 | private $div:JQuery; 9 | private hide_timeout_id:number|undefined; 10 | private show_timeout_id:number|undefined; 11 | public center:boolean; 12 | private shown:boolean; 13 | 14 | constructor(private $container:JQuery, params?:OncoprintTooltipParams) { 15 | params = params || {}; 16 | 17 | this.$div = $('
      ').addClass(TOOLTIP_CLASS).appendTo($container).css({'background-color':'rgba(255,255,255,1)', 'position':'absolute', 'display':'none', 'border':'1px solid black', 'max-width':300, 'min-width':150}); 18 | if (params.noselect) { 19 | this.$div.addClass("noselect"); 20 | } 21 | this.hide_timeout_id = undefined; 22 | this.show_timeout_id = undefined; 23 | this.center = false; 24 | 25 | this.shown = false; 26 | 27 | const self = this; 28 | this.$div.on("mousemove", function(evt) { 29 | evt.stopPropagation(); 30 | self.cancelScheduledHide(); 31 | }); 32 | this.$div.on("mouseleave", function(evt) { 33 | evt.stopPropagation(); 34 | self.hide(); 35 | }); 36 | } 37 | public show(wait:number|undefined, viewport_x:number, viewport_y:number, $contents:JQuery, fade?:boolean) { 38 | this.cancelScheduledHide(); 39 | 40 | if (typeof wait !== 'undefined' && !this.shown) { 41 | const self = this; 42 | this.cancelScheduledShow(); 43 | this.show_timeout_id = setTimeout(function() { 44 | self.doShow(viewport_x, viewport_y, $contents, fade); 45 | }, wait); 46 | } else { 47 | this.doShow(viewport_x, viewport_y, $contents, fade); 48 | } 49 | } 50 | private doShow(viewport_x:number, viewport_y:number, $contents:JQuery, fade?:boolean) { 51 | this.cancelScheduledShow(); 52 | this.show_timeout_id = undefined; 53 | this.$div.empty(); 54 | this.$div.css({'top':0, 'left':0, 'z-index':9999}); // put up top left so that it doesnt cause any page expansion, before the position calculation which depends on page size 55 | this.$div.append($contents); 56 | if (!fade) { 57 | this.$div.show(); 58 | } else { 59 | this.$div.stop().fadeIn('fast'); 60 | } 61 | // adjust tooltip position based on size of contents 62 | let x = viewport_x - (this.center ? this.$div.width()/2 : 0); 63 | let y = viewport_y - this.$div.height(); 64 | // clamp to visible area 65 | const min_padding = 20; 66 | y = Math.max(y, min_padding); // make sure not too high 67 | y = Math.min(y, $(window).height() - this.$div.height()); // make sure not too low 68 | x = Math.max(x, min_padding); // make sure not too left 69 | x = Math.min(x, $(window).width() - this.$div.width() - min_padding); // make sure not too right 70 | 71 | this.$div.css({'top':y, 'left':x, 'z-index':9999}); 72 | this.shown = true; 73 | } 74 | private doHide(fade?:boolean) { 75 | this.cancelScheduledHide(); 76 | this.hide_timeout_id = undefined; 77 | if (!fade) { 78 | this.$div.hide(); 79 | } else { 80 | this.$div.fadeOut(); 81 | } 82 | this.shown = false; 83 | }; 84 | private cancelScheduledShow() { 85 | clearTimeout(this.show_timeout_id); 86 | this.show_timeout_id = undefined; 87 | } 88 | private cancelScheduledHide() { 89 | clearTimeout(this.hide_timeout_id); 90 | this.hide_timeout_id = undefined; 91 | } 92 | public showIfNotAlreadyGoingTo(wait:number|undefined, viewport_x:number, viewport_y:number, $contents:JQuery) { 93 | if (typeof this.show_timeout_id === 'undefined') { 94 | this.show(wait, viewport_x, viewport_y, $contents); 95 | } 96 | } 97 | public hideIfNotAlreadyGoingTo(wait?:number) { 98 | if (typeof this.hide_timeout_id === 'undefined') { 99 | this.hide(wait); 100 | } 101 | } 102 | public hide(wait?:number) { 103 | this.cancelScheduledShow(); 104 | 105 | if (!this.shown) { 106 | return; 107 | } 108 | 109 | if (typeof wait !== 'undefined') { 110 | const self = this; 111 | this.cancelScheduledHide(); 112 | this.hide_timeout_id = setTimeout(function() { 113 | self.doHide(); 114 | }, wait); 115 | } else { 116 | this.doHide(); 117 | } 118 | } 119 | public fadeIn(wait:number|undefined, viewport_x:number, viewport_y:number, $contents:JQuery) { 120 | this.show(wait, viewport_x, viewport_y, $contents, true); 121 | } 122 | } -------------------------------------------------------------------------------- /src/js/oncoprinttrackinfoview.ts: -------------------------------------------------------------------------------- 1 | import svgfactory from "./svgfactory"; 2 | import $ from "jquery"; 3 | import OncoprintToolTip from "./oncoprinttooltip"; 4 | import OncoprintModel from "./oncoprintmodel"; 5 | 6 | export default class OncoprintTrackInfoView { 7 | 8 | private $ctr:JQuery; 9 | private $text_ctr:JQuery; 10 | private base_font_size = 12; 11 | private font_family = 'Arial'; 12 | private font_weight = 'bold'; 13 | private width = 0; 14 | private $label_elts:JQuery[] = []; 15 | private rendering_suppressed = false; 16 | private minimum_track_height:number; 17 | 18 | constructor(private $div:JQuery, private tooltip:OncoprintToolTip) { 19 | this.tooltip.center = false; 20 | 21 | this.$ctr = $('
      ').css({'position': 'absolute', 'overflow-y':'hidden', 'overflow-x':'hidden'}).appendTo(this.$div); 22 | this.$text_ctr = $('
      ').css({'position':'absolute'}).appendTo(this.$ctr); 23 | } 24 | 25 | private destroyLabelElts() { 26 | for (let i=0; i').css({'position': 'absolute', 56 | 'font-family': self.font_family, 57 | 'font-weight': self.font_weight, 58 | 'font-size': font_size}) 59 | .addClass('noselect'); 60 | const text = model.getTrackInfo(tracks[i]); 61 | if (!text) { 62 | return; 63 | } 64 | $new_label.text(text); 65 | $new_label.appendTo(self.$text_ctr); 66 | self.$label_elts.push($new_label); 67 | setTimeout(function() { 68 | $new_label.on("mousemove", function() { 69 | const $tooltip_elt = model.$getTrackInfoTooltip(tracks[i]); 70 | if ($tooltip_elt) { 71 | const offset = $new_label[0].getBoundingClientRect(); 72 | self.tooltip.fadeIn(200, offset.left, offset.top, $tooltip_elt); 73 | } 74 | }).on("mouseleave", function() { 75 | self.tooltip.hideIfNotAlreadyGoingTo(150); 76 | }); 77 | }, 0); // delay to give time for render before adding events 78 | const top = label_tops[tracks[i]] + (model.getCellHeight(tracks[i]) - $new_label.outerHeight()) / 2; 79 | $new_label.css({'top': top + 'px'}); 80 | self.width = Math.max(32, self.width, $new_label[0].clientWidth); 81 | })(); 82 | } 83 | if (this.width > 0) { 84 | this.width += 10; 85 | } 86 | }; 87 | private scroll(scroll_y:number) { 88 | if (this.rendering_suppressed) { 89 | return; 90 | } 91 | this.$text_ctr.css({'top': -scroll_y}); 92 | }; 93 | 94 | private resize(model:OncoprintModel, getCellViewHeight:()=>number) { 95 | if (this.rendering_suppressed) { 96 | return; 97 | } 98 | this.$div.css({'width': this.getWidth(), 'height': getCellViewHeight()}); 99 | this.$ctr.css({'width': this.getWidth(), 'height': getCellViewHeight()}); 100 | }; 101 | 102 | public getFontSize() { 103 | return Math.max(Math.min(this.base_font_size, this.minimum_track_height), 7); 104 | } 105 | public getWidth() { 106 | return this.width; 107 | } 108 | public addTracks(model:OncoprintModel, getCellViewHeight:()=>number) { 109 | this.renderAllInfo(model); 110 | this.resize(model, getCellViewHeight); 111 | } 112 | public moveTrack(model:OncoprintModel, getCellViewHeight:()=>number) { 113 | this.renderAllInfo(model); 114 | this.resize(model, getCellViewHeight); 115 | } 116 | public setTrackGroupOrder(model:OncoprintModel, getCellViewHeight:()=>number) { 117 | this.renderAllInfo(model); 118 | this.resize(model, getCellViewHeight); 119 | } 120 | public removeTrack(model:OncoprintModel, getCellViewHeight:()=>number) { 121 | this.renderAllInfo(model); 122 | this.resize(model, getCellViewHeight); 123 | } 124 | public setTrackInfo(model:OncoprintModel, getCellViewHeight:()=>number) { 125 | this.renderAllInfo(model); 126 | this.resize(model, getCellViewHeight); 127 | } 128 | public setTrackGroupHeader(model:OncoprintModel, getCellViewHeight:()=>number) { 129 | this.renderAllInfo(model); 130 | this.resize(model, getCellViewHeight); 131 | } 132 | public setScroll(model:OncoprintModel) { 133 | this.setVertScroll(model); 134 | } 135 | public setHorzScroll(model:OncoprintModel) { 136 | } 137 | public setVertScroll(model:OncoprintModel) { 138 | this.scroll(model.getVertScroll()); 139 | } 140 | public setZoom(model:OncoprintModel, getCellViewHeight:()=>number) { 141 | this.setVertZoom(model, getCellViewHeight); 142 | } 143 | 144 | public setViewport(model:OncoprintModel, getCellViewHeight:()=>number) { 145 | this.renderAllInfo(model); 146 | this.resize(model, getCellViewHeight); 147 | this.scroll(model.getVertScroll()); 148 | } 149 | public setVertZoom(model:OncoprintModel, getCellViewHeight:()=>number) { 150 | this.renderAllInfo(model); 151 | this.resize(model, getCellViewHeight); 152 | } 153 | public suppressRendering() { 154 | this.rendering_suppressed = true; 155 | } 156 | public releaseRendering(model:OncoprintModel, getCellViewHeight:()=>number) { 157 | this.rendering_suppressed = false; 158 | this.renderAllInfo(model); 159 | this.resize(model, getCellViewHeight); 160 | this.scroll(model.getVertScroll()); 161 | } 162 | public destroy() { 163 | this.destroyLabelElts(); 164 | } 165 | public toSVGGroup(model:OncoprintModel, offset_x:number, offset_y:number) { 166 | const root = svgfactory.group((offset_x || 0), (offset_y || 0)); 167 | const cell_tops = model.getCellTops(); 168 | const tracks = model.getTracks(); 169 | 170 | const font_size = this.getFontSize(); 171 | 172 | for (let i = 0; i < tracks.length; i++) { 173 | const track_id = tracks[i]; 174 | const y = cell_tops[track_id] + model.getCellHeight(track_id) / 2; 175 | const info = model.getTrackInfo(track_id); 176 | const text_elt = svgfactory.text(info, 0, y, font_size, this.font_family, this.font_weight, "bottom"); 177 | text_elt.setAttribute("dy", "0.35em"); 178 | root.appendChild(text_elt); 179 | } 180 | return root; 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /src/js/oncoprinttrackoptionsview.ts: -------------------------------------------------------------------------------- 1 | import $ from "jquery"; 2 | import menuDotsIcon from '../img/menudots.svg'; 3 | import OncoprintModel, {TrackGroupProp, TrackId, TrackProp, TrackSortDirection} from "./oncoprintmodel"; 4 | import ClickEvent = JQuery.ClickEvent; 5 | import {CLOSE_MENUS_EVENT as HEADER_VIEW_CLOSE_MENUS_EVENT} from "./oncoprintheaderview"; 6 | 7 | const TOGGLE_BTN_CLASS = "oncoprintjs__track_options__toggle_btn_img"; 8 | const TOGGLE_BTN_OPEN_CLASS = "oncoprintjs__track_options__open"; 9 | const DROPDOWN_CLASS = "oncoprintjs__track_options__dropdown"; 10 | const SEPARATOR_CLASS = "oncoprintjs__track_options__separator"; 11 | const NTH_CLASS_PREFIX = "nth-"; 12 | 13 | export const CLOSE_MENUS_EVENT = "oncoprint-track-options-view.do-close-menus"; 14 | 15 | type TrackCallback = (trackId:TrackId)=>void; 16 | export default class OncoprintTrackOptionsView { 17 | 18 | private $ctr:JQuery; 19 | private $buttons_ctr:JQuery; 20 | private $dropdown_ctr:JQuery; 21 | 22 | private img_size:number; 23 | private rendering_suppressed = false; 24 | private track_options_$elts:TrackProp<{ $div:JQuery, $img:JQuery, $dropdown:JQuery}> = {}; 25 | private menu_shown:TrackProp = {}; 26 | private clickHandler:()=>void; 27 | private interaction_disabled = false; 28 | 29 | constructor( 30 | private $div:JQuery, 31 | private moveUpCallback:TrackCallback, 32 | private moveDownCallback:TrackCallback, 33 | private removeCallback:TrackCallback, 34 | private sortChangeCallback:(trackId:TrackId, sortDirection:TrackSortDirection)=>void, 35 | private unexpandCallback:TrackCallback, 36 | private showGapsCallback:(trackId:TrackId, showGaps:boolean)=>void 37 | ) { 38 | const position = $div.css('position'); 39 | if (position !== 'absolute' && position !== 'relative') { 40 | console.log("WARNING: div passed to OncoprintTrackOptionsView must be absolute or relative positioned - layout problems will occur"); 41 | } 42 | 43 | this.$ctr = $('
      ').css({'position': 'absolute', 'overflow-y':'hidden', 'overflow-x':'hidden'}).appendTo(this.$div); 44 | this.$buttons_ctr = $('
      ').css({'position':'absolute'}).appendTo(this.$ctr); 45 | this.$dropdown_ctr = $('
      ').css({'position': 'absolute'}).appendTo(this.$div); 46 | 47 | const self = this; 48 | this.clickHandler = function() { 49 | $(document).trigger(CLOSE_MENUS_EVENT); 50 | }; 51 | $(document).on("click", this.clickHandler); 52 | } 53 | 54 | private renderAllOptions(model:OncoprintModel) { 55 | if (this.rendering_suppressed) { 56 | return; 57 | } 58 | const self = this; 59 | $(document).off(CLOSE_MENUS_EVENT); 60 | $(document).on(CLOSE_MENUS_EVENT, function() { 61 | self.hideAllMenus(); 62 | }); 63 | 64 | this.$buttons_ctr.empty(); 65 | this.$dropdown_ctr.empty(); 66 | this.scroll(model.getVertScroll()); 67 | 68 | var tracks = model.getTracks(); 69 | var minimum_track_height = Number.POSITIVE_INFINITY; 70 | for (let i = 0; i < tracks.length; i++) { 71 | minimum_track_height = Math.min(minimum_track_height, model.getTrackHeight(tracks[i])); 72 | } 73 | this.img_size = Math.floor(minimum_track_height * 0.75); 74 | 75 | for (let i = 0; i < tracks.length; i++) { 76 | this.renderTrackOptions(model, tracks[i], i); 77 | } 78 | } 79 | 80 | private scroll(scroll_y:number) { 81 | if (this.rendering_suppressed) { 82 | return; 83 | } 84 | this.$buttons_ctr.css({'top': -scroll_y}); 85 | this.$dropdown_ctr.css({'top': -scroll_y}); 86 | 87 | this.hideAllMenus(); 88 | } 89 | 90 | private resize(model:OncoprintModel, getCellViewHeight:()=>number) { 91 | if (this.rendering_suppressed) { 92 | return; 93 | } 94 | this.$div.css({'width': this.getWidth(), 'height': getCellViewHeight()}); 95 | this.$ctr.css({'width': this.getWidth(), 'height': getCellViewHeight()}); 96 | } 97 | 98 | private hideTrackMenu(track_id:TrackId) { 99 | this.menu_shown[track_id] = false; 100 | const $elts = this.track_options_$elts[track_id]; 101 | $elts.$dropdown.css({'z-index': 1}); 102 | $elts.$dropdown.css({'border': '1px solid rgba(125,125,125,0)'}); 103 | $elts.$img.css({'border': '1px solid rgba(125,125,125,0)'}); 104 | $elts.$dropdown.fadeOut(100); 105 | } 106 | 107 | private showTrackMenu(track_id:TrackId) { 108 | this.menu_shown[track_id] = true; 109 | const $elts = this.track_options_$elts[track_id]; 110 | $elts.$dropdown.css({'z-index': 10}); 111 | $elts.$dropdown.css({'border': '1px solid rgba(125,125,125,1)'}); 112 | $elts.$img.css({'border': '1px solid rgba(125,125,125,1)'}); 113 | $elts.$dropdown.fadeIn(100); 114 | } 115 | 116 | private hideAllMenus() { 117 | for (const track_id in this.track_options_$elts) { 118 | if (this.track_options_$elts.hasOwnProperty(track_id)) { 119 | this.hideTrackMenu(parseInt(track_id, 10)); 120 | } 121 | } 122 | } 123 | 124 | private hideMenusExcept(track_id:TrackId) { 125 | for (const _other_track_id in this.track_options_$elts) { 126 | if (this.track_options_$elts.hasOwnProperty(_other_track_id)) { 127 | const other_track_id = parseInt(_other_track_id, 10); 128 | if (other_track_id === track_id) { 129 | continue; 130 | } 131 | this.hideTrackMenu(other_track_id); 132 | } 133 | } 134 | 135 | $(document).trigger(HEADER_VIEW_CLOSE_MENUS_EVENT); 136 | } 137 | 138 | private static $makeDropdownOption(text:string, weight:string, disabled?:boolean, callback?:(evt:ClickEvent)=>void) { 139 | const li = $('
    • ').text(text).css({'font-weight': weight, 'font-size': 12, 'border-bottom': '1px solid rgba(0,0,0,0.3)'}); 140 | if (!disabled) { 141 | if (callback) { 142 | li.addClass("clickable"); 143 | li.css({'cursor': 'pointer'}); 144 | li.click(callback) 145 | .hover(function () { 146 | $(this).css({'background-color': 'rgb(200,200,200)'}); 147 | }, function () { 148 | $(this).css({'background-color': 'rgba(255,255,255,0)'}); 149 | }); 150 | } else { 151 | li.click(function(evt) { evt.stopPropagation(); }); 152 | } 153 | } else { 154 | li.addClass("disabled"); 155 | li.css({'color': 'rgb(200, 200, 200)', 'cursor': 'default'}); 156 | } 157 | return li; 158 | } 159 | 160 | private static $makeDropdownSeparator() { 161 | return $('
    • ').css({'border-top': '1px solid black'}).addClass(SEPARATOR_CLASS); 162 | } 163 | 164 | private static renderSortArrow($sortarrow:JQuery, model:OncoprintModel, track_id:TrackId) { 165 | let sortarrow_char = ''; 166 | if (model.isTrackSortDirectionChangeable(track_id)){ 167 | sortarrow_char = { 168 | '1': '', 169 | '-1': '', 170 | '0': ''}[model.getTrackSortDirection(track_id)]; 171 | } 172 | $sortarrow.html(sortarrow_char); 173 | } 174 | 175 | private renderTrackOptions(model:OncoprintModel, track_id:TrackId, index:number) { 176 | let $div:JQuery, $img:JQuery, $sortarrow:JQuery, $dropdown:JQuery; 177 | const top = model.getZoomedTrackTops(track_id); 178 | $div = $('
      ').appendTo(this.$buttons_ctr).css({'position': 'absolute', 'left': '0px', 'top': top + 'px', 'white-space': 'nowrap'}); 179 | $img = $('').appendTo($div) 180 | .attr({ 181 | 'src': menuDotsIcon, 182 | 'width': this.img_size, 183 | 'height': this.img_size 184 | }) 185 | .css({ 186 | 'float': 'left', 187 | 'cursor': 'pointer', 188 | 'border': '1px solid rgba(125,125,125,0)' 189 | }).addClass(TOGGLE_BTN_CLASS).addClass(NTH_CLASS_PREFIX+(index+1)); 190 | $sortarrow = $('').appendTo($div).css({'position': 'absolute', 'top': Math.floor(this.img_size / 4) + 'px'}); 191 | $dropdown = $('
        ').appendTo(this.$dropdown_ctr) 192 | .css({ 193 | 'position':'absolute', 194 | 'width': 120, 195 | 'display': 'none', 196 | 'list-style-type': 'none', 197 | 'padding-left': '6', 198 | 'padding-right': '6', 199 | 'float': 'right', 200 | 'background-color': 'rgb(255,255,255)', 201 | 'left':'0px', 'top': top + this.img_size + 'px' 202 | }).addClass(DROPDOWN_CLASS).addClass(NTH_CLASS_PREFIX+(index+1)); 203 | this.track_options_$elts[track_id] = {'$div': $div, '$img': $img, '$dropdown': $dropdown}; 204 | 205 | OncoprintTrackOptionsView.renderSortArrow($sortarrow, model, track_id); 206 | 207 | const self = this; 208 | $img.hover(function (evt) { 209 | if (!self.menu_shown[track_id]) { 210 | $(this).css({'border': '1px solid rgba(125,125,125,0.3)'}); 211 | } 212 | }, function (evt) { 213 | if (!self.menu_shown[track_id]) { 214 | $(this).css({'border': '1px solid rgba(125,125,125,0)'}); 215 | } 216 | }); 217 | $img.click(function (evt) { 218 | evt.stopPropagation(); 219 | if ($dropdown.is(":visible")) { 220 | $img.addClass(TOGGLE_BTN_OPEN_CLASS); 221 | self.hideTrackMenu(track_id); 222 | } else { 223 | $img.removeClass(TOGGLE_BTN_OPEN_CLASS); 224 | self.showTrackMenu(track_id); 225 | } 226 | self.hideMenusExcept(track_id); 227 | }); 228 | 229 | const movingDisabled = model.getTrackMovable(track_id) && model.isTrackInClusteredGroup(track_id); 230 | 231 | if (model.getTrackMovable(track_id)) { 232 | $dropdown.append(OncoprintTrackOptionsView.$makeDropdownOption('Move up', 'normal', movingDisabled, function (evt) { 233 | evt.stopPropagation(); 234 | self.moveUpCallback(track_id); 235 | })); 236 | $dropdown.append(OncoprintTrackOptionsView.$makeDropdownOption('Move down', 'normal', movingDisabled, function (evt) { 237 | evt.stopPropagation(); 238 | self.moveDownCallback(track_id); 239 | })); 240 | } 241 | if (model.isTrackRemovable(track_id)) { 242 | $dropdown.append(OncoprintTrackOptionsView.$makeDropdownOption('Remove track', 'normal', false, function (evt) { 243 | evt.stopPropagation(); 244 | self.removeCallback(track_id); 245 | })); 246 | } 247 | if (model.isTrackSortDirectionChangeable(track_id)) { 248 | $dropdown.append(OncoprintTrackOptionsView.$makeDropdownSeparator()); 249 | let $sort_inc_li:JQuery; 250 | let $sort_dec_li:JQuery; 251 | let $dont_sort_li:JQuery; 252 | $sort_inc_li = OncoprintTrackOptionsView.$makeDropdownOption('Sort a-Z', (model.getTrackSortDirection(track_id) === 1 ? 'bold' : 'normal'), false, function (evt) { 253 | evt.stopPropagation(); 254 | $sort_inc_li.css('font-weight', 'bold'); 255 | $sort_dec_li.css('font-weight', 'normal'); 256 | $dont_sort_li.css('font-weight', 'normal'); 257 | self.sortChangeCallback(track_id, 1); 258 | OncoprintTrackOptionsView.renderSortArrow($sortarrow, model, track_id); 259 | }); 260 | $sort_dec_li = OncoprintTrackOptionsView.$makeDropdownOption('Sort Z-a', (model.getTrackSortDirection(track_id) === -1 ? 'bold' : 'normal'), false, function (evt) { 261 | evt.stopPropagation(); 262 | $sort_inc_li.css('font-weight', 'normal'); 263 | $sort_dec_li.css('font-weight', 'bold'); 264 | $dont_sort_li.css('font-weight', 'normal'); 265 | self.sortChangeCallback(track_id, -1); 266 | OncoprintTrackOptionsView.renderSortArrow($sortarrow, model, track_id); 267 | }); 268 | $dont_sort_li = OncoprintTrackOptionsView.$makeDropdownOption('Don\'t sort track', (model.getTrackSortDirection(track_id) === 0 ? 'bold' : 'normal'), false, function (evt) { 269 | evt.stopPropagation(); 270 | $sort_inc_li.css('font-weight', 'normal'); 271 | $sort_dec_li.css('font-weight', 'normal'); 272 | $dont_sort_li.css('font-weight', 'bold'); 273 | self.sortChangeCallback(track_id, 0); 274 | OncoprintTrackOptionsView.renderSortArrow($sortarrow, model, track_id); 275 | }); 276 | $dropdown.append($sort_inc_li); 277 | $dropdown.append($sort_dec_li); 278 | $dropdown.append($dont_sort_li); 279 | } 280 | if (model.isTrackExpandable(track_id)) { 281 | $dropdown.append(OncoprintTrackOptionsView.$makeDropdownOption( 282 | model.getExpandButtonText(track_id), 283 | 'normal', 284 | false, 285 | function (evt) { 286 | evt.stopPropagation(); 287 | // close the menu to discourage clicking again, as it 288 | // may take a moment to finish expanding 289 | self.renderAllOptions(model); 290 | model.expandTrack(track_id); 291 | })); 292 | } 293 | if (model.isTrackExpanded(track_id)) { 294 | $dropdown.append(OncoprintTrackOptionsView.$makeDropdownOption( 295 | 'Remove expansion', 296 | 'normal', 297 | false, 298 | function (evt) { 299 | evt.stopPropagation(); 300 | self.unexpandCallback(track_id); 301 | })); 302 | } 303 | if (model.getTrackCanShowGaps(track_id)) { 304 | $dropdown.append(OncoprintTrackOptionsView.$makeDropdownSeparator()); 305 | const $show_gaps_opt = OncoprintTrackOptionsView.$makeDropdownOption( 306 | 'Show gaps', 307 | model.getTrackShowGaps(track_id) ? 'bold' : 'normal', 308 | false, 309 | function(evt) { 310 | evt.stopPropagation(); 311 | $show_gaps_opt.css('font-weight', 'bold'); 312 | $dont_show_gaps_opt.css('font-weight', 'normal'); 313 | self.showGapsCallback(track_id, true); 314 | } 315 | ); 316 | const $dont_show_gaps_opt = OncoprintTrackOptionsView.$makeDropdownOption( 317 | "Don't show gaps", 318 | model.getTrackShowGaps(track_id) ? 'normal' : 'bold', 319 | false, 320 | function(evt) { 321 | evt.stopPropagation(); 322 | 323 | $show_gaps_opt.css('font-weight', 'normal'); 324 | $dont_show_gaps_opt.css('font-weight', 'bold'); 325 | self.showGapsCallback(track_id, false); 326 | } 327 | ); 328 | $dropdown.append($show_gaps_opt); 329 | $dropdown.append($dont_show_gaps_opt); 330 | } 331 | // Add custom options 332 | const custom_options = model.getTrackCustomOptions(track_id); 333 | if (custom_options && custom_options.length > 0) { 334 | for (var i=0; inumber) { 367 | this.rendering_suppressed = false; 368 | this.renderAllOptions(model); 369 | this.resize(model, getCellViewHeight); 370 | this.scroll(model.getVertScroll()); 371 | } 372 | public setScroll(model:OncoprintModel) { 373 | this.setVertScroll(model); 374 | } 375 | public setHorzScroll(model:OncoprintModel) { 376 | } 377 | public setVertScroll(model:OncoprintModel) { 378 | this.scroll(model.getVertScroll()); 379 | } 380 | public setZoom(model:OncoprintModel, getCellViewHeight:()=>number) { 381 | this.setVertZoom(model, getCellViewHeight); 382 | } 383 | public setVertZoom(model:OncoprintModel, getCellViewHeight:()=>number) { 384 | this.renderAllOptions(model); 385 | this.resize(model, getCellViewHeight); 386 | } 387 | public setTrackGroupHeader(model:OncoprintModel, getCellViewHeight:()=>number) { 388 | this.renderAllOptions(model); 389 | this.resize(model, getCellViewHeight); 390 | } 391 | public sort(model:OncoprintModel, getCellViewHeight:()=>number) { 392 | this.renderAllOptions(model); 393 | this.resize(model, getCellViewHeight); 394 | } 395 | public setViewport(model:OncoprintModel, getCellViewHeight:()=>number) { 396 | this.renderAllOptions(model); 397 | this.resize(model, getCellViewHeight); 398 | this.scroll(model.getVertScroll()); 399 | } 400 | public getWidth() { 401 | if (this.$buttons_ctr.is(":empty")) { 402 | return 0; 403 | } else { 404 | return 18 + this.img_size; 405 | } 406 | } 407 | public setTrackShowGaps(model:OncoprintModel, getCellViewHeight:()=>number) { 408 | this.renderAllOptions(model); 409 | this.resize(model, getCellViewHeight); 410 | } 411 | public addTracks(model:OncoprintModel, getCellViewHeight:()=>number) { 412 | this.renderAllOptions(model); 413 | this.resize(model, getCellViewHeight); 414 | } 415 | public moveTrack(model:OncoprintModel, getCellViewHeight:()=>number) { 416 | this.renderAllOptions(model); 417 | this.resize(model, getCellViewHeight); 418 | } 419 | public setTrackGroupOrder(model:OncoprintModel) { 420 | this.renderAllOptions(model); 421 | } 422 | public setSortConfig(model:OncoprintModel) { 423 | this.renderAllOptions(model); 424 | } 425 | public removeTrack(model:OncoprintModel, track_id:TrackId, getCellViewHeight:()=>number) { 426 | delete this.track_options_$elts[track_id]; 427 | this.renderAllOptions(model); 428 | this.resize(model, getCellViewHeight); 429 | } 430 | public destroy() { 431 | $(document).off("click", this.clickHandler); 432 | $(document).off(CLOSE_MENUS_EVENT); 433 | }; 434 | public setTrackCustomOptions(model:OncoprintModel) { 435 | this.renderAllOptions(model); 436 | }; 437 | public setTrackMovable(model:OncoprintModel) { 438 | this.renderAllOptions(model); 439 | } 440 | } 441 | -------------------------------------------------------------------------------- /src/js/oncoprintzoomslider.ts: -------------------------------------------------------------------------------- 1 | import $ from "jquery"; 2 | import MouseMoveEvent = JQuery.MouseMoveEvent; 3 | import MouseDownEvent = JQuery.MouseDownEvent; 4 | 5 | const VERTICAL = "v"; 6 | const HORIZONTAL = "h"; 7 | 8 | function clamp(x:number) { 9 | return Math.max(Math.min(x, 1), 0); 10 | } 11 | 12 | export type OncoprintZoomSliderParams = { 13 | btn_size: number, 14 | horizontal?: boolean, 15 | width?:number, 16 | 17 | vertical?:boolean, // either horizontal and width, or vertical and height, must be set 18 | height?:number, 19 | 20 | init_val: number, 21 | left:number, 22 | top:number, 23 | onChange:(val:number)=>void 24 | }; 25 | 26 | export default class OncoprintZoomSlider { 27 | private $div:JQuery; 28 | private onChange:OncoprintZoomSliderParams["onChange"]; 29 | private value:number; 30 | private slider_bar_size:number; 31 | private orientation:"v"|"h"; 32 | private $slider:JQuery; 33 | private $plus_btn:JQuery; 34 | private $minus_btn:JQuery; 35 | 36 | constructor($container:JQuery, params?:Partial) { 37 | this.$div = $('
        ').css({'position':'absolute', 38 | 'top': params.top || 0, 39 | 'left': params.left || 0}).appendTo($container); 40 | params = params || {}; 41 | params.btn_size = params.btn_size || 13; 42 | this.onChange = params.onChange || function() {}; 43 | 44 | this.initialize(params as OncoprintZoomSliderParams); 45 | 46 | this.value = params.init_val === undefined ? 0.5 : params.init_val; 47 | this.slider_bar_size = (this.orientation === VERTICAL ? params.height : params.width) - 2*params.btn_size; 48 | this.updateSliderPos(); 49 | } 50 | 51 | private initialize(params:OncoprintZoomSliderParams) { 52 | var $ctr = this.$div; 53 | var icon_size = Math.round(params.btn_size * 0.7); 54 | var icon_padding = Math.round((params.btn_size - icon_size)/2); 55 | var $slider_bar = $('
        ').css({'position':'absolute', 56 | 'background-color':'#ffffff', 57 | 'outline': 'solid 1px black'}).appendTo($ctr); 58 | var $slider = $('
        ').css({'position':'absolute', 59 | 'background-color':'#ffffff', 60 | 'border': 'solid 1px black', 61 | 'border-radius': '3px', 62 | 'cursor': 'pointer'}).appendTo($ctr); 63 | 64 | var $plus_btn = $('
        ').css({'position':'absolute', 65 | 'min-height': params.btn_size, 66 | 'min-width': params.btn_size, 67 | 'background-color':'#ffffff', 68 | 'border': 'solid 1px black', 69 | 'border-radius': '3px', 70 | 'cursor': 'pointer'}) 71 | .appendTo($ctr); 72 | $('').addClass("icon fa fa-plus").css({'position':'absolute', 73 | 'top':icon_padding, 74 | 'left':icon_padding, 75 | 'min-width':icon_size, 76 | 'min-height':icon_size}) 77 | .appendTo($plus_btn); 78 | var $minus_btn = $('
        ').css({'position':'absolute', 79 | 'min-height': params.btn_size, 80 | 'min-width': params.btn_size, 81 | 'background-color':'#ffffff', 82 | 'border': 'solid 1px black', 83 | 'border-radius': '3px', 84 | 'cursor': 'pointer'}) 85 | .appendTo($ctr); 86 | $('').addClass("icon fa fa-minus").css({'position':'absolute', 87 | 'top':icon_padding, 88 | 'left':icon_padding, 89 | 'min-width':icon_size, 90 | 'min-height':icon_size}) 91 | .appendTo($minus_btn); 92 | if (params.vertical) { 93 | $slider_bar.css({'min-height': params.height - 2 * params.btn_size, 94 | 'min-width': Math.round(params.btn_size / 5)}); 95 | $slider.css({'min-height': Math.round(params.btn_size / 2), 96 | 'min-width': params.btn_size}); 97 | 98 | $plus_btn.css({'top': 0, 'left': 0}); 99 | $minus_btn.css({'top': params.height - params.btn_size, 'left': 0}); 100 | $slider_bar.css({'top': params.btn_size, 'left': 0.4 * params.btn_size}); 101 | $slider.css({'left': 0}); 102 | this.orientation = VERTICAL; 103 | } else { 104 | $slider_bar.css({'min-height': Math.round(params.btn_size / 5), 105 | 'min-width': params.width - 2 * params.btn_size}); 106 | $slider.css({'min-height': params.btn_size, 107 | 'min-width': Math.round(params.btn_size / 2)}); 108 | 109 | $plus_btn.css({'top': 0, 'left': params.width - params.btn_size}); 110 | $minus_btn.css({'top': 0, 'left': 0}); 111 | $slider_bar.css({'top': 0.4*params.btn_size, 'left': params.btn_size}); 112 | $slider.css({'top': 0}); 113 | this.orientation = HORIZONTAL; 114 | } 115 | 116 | const self = this; 117 | 118 | $plus_btn.click(function() { 119 | self.value /= 0.7; 120 | params.onChange(self.value); 121 | }); 122 | $minus_btn.click(function() { 123 | self.value *= 0.7; 124 | params.onChange(self.value); 125 | }); 126 | 127 | [$slider, $plus_btn, $minus_btn].map(function($btn) { $btn.hover(function() { 128 | $(this).css({'background-color':'#cccccc'}); 129 | }, function() { 130 | $(this).css({'background-color': '#ffffff'}); 131 | }); }); 132 | 133 | 134 | 135 | this.$slider = $slider; 136 | this.$plus_btn = $plus_btn; 137 | this.$minus_btn = $minus_btn; 138 | 139 | (function setUpSliderDrag() { 140 | let start_mouse:number; 141 | let start_val:number; 142 | let dragging:boolean; 143 | function handleSliderDrag(evt:MouseMoveEvent) { 144 | evt.stopPropagation(); 145 | evt.preventDefault(); 146 | let delta_mouse; 147 | if (self.orientation === VERTICAL) { 148 | delta_mouse = start_mouse - evt.pageY; // vertical zoom, positive is up, but CSS positive is down, so we need to invert 149 | } else { 150 | delta_mouse = evt.pageX - start_mouse; 151 | } 152 | const delta_val = delta_mouse / self.slider_bar_size; 153 | self.setSliderValue(start_val + delta_val); 154 | } 155 | function stopSliderDrag() { 156 | if (dragging && start_val !== self.value) { 157 | self.onChange(self.value); 158 | } 159 | dragging = false; 160 | } 161 | self.$slider.on("mousedown", function (evt:MouseDownEvent) { 162 | if (self.orientation === VERTICAL) { 163 | start_mouse = evt.pageY; 164 | } else { 165 | start_mouse = evt.pageX; 166 | } 167 | start_val = self.value; 168 | dragging = true; 169 | $(document).on("mousemove", handleSliderDrag); 170 | }); 171 | $(document).on("mouseup click", function () { 172 | $(document).off("mousemove", handleSliderDrag); 173 | stopSliderDrag(); 174 | }); 175 | })() 176 | }; 177 | 178 | private updateSliderPos() { 179 | const proportion = this.value; 180 | var $slider = this.$slider; 181 | var bounds = this.getSliderBounds(); 182 | if (this.orientation === VERTICAL) { 183 | $slider.css('top', bounds.bottom*(1-proportion) + bounds.top*proportion); 184 | } else if (this.orientation === HORIZONTAL) { 185 | $slider.css('left', bounds.left*(1-proportion) + bounds.right*proportion); 186 | } 187 | }; 188 | 189 | private getSliderBounds() { 190 | if (this.orientation === VERTICAL) { 191 | return {bottom: parseInt(this.$minus_btn.css('top'), 10) - parseInt(this.$slider.css('min-height'), 10), 192 | top: parseInt(this.$plus_btn.css('top'), 10) + parseInt(this.$plus_btn.css('min-height'), 10)}; 193 | } else { 194 | return {left: parseInt(this.$minus_btn.css('left'), 10) + parseInt(this.$minus_btn.css('min-width'), 10), 195 | right: parseInt(this.$plus_btn.css('left'), 10) - parseInt(this.$slider.css('min-width'), 10)}; 196 | } 197 | }; 198 | 199 | 200 | public setSliderValue(proportion:number) { 201 | this.value = clamp(proportion); 202 | this.updateSliderPos(); 203 | } 204 | } 205 | -------------------------------------------------------------------------------- /src/js/polyfill.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2016 Memorial Sloan-Kettering Cancer Center. 3 | * 4 | * This library is distributed in the hope that it will be useful, but WITHOUT 5 | * ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS 6 | * FOR A PARTICULAR PURPOSE. The software and documentation provided hereunder 7 | * is on an "as is" basis, and Memorial Sloan-Kettering Cancer Center has no 8 | * obligations to provide maintenance, support, updates, enhancements or 9 | * modifications. In no event shall Memorial Sloan-Kettering Cancer Center be 10 | * liable to any party for direct, indirect, special, incidental or 11 | * consequential damages, including lost profits, arising out of the use of this 12 | * software and its documentation, even if Memorial Sloan-Kettering Cancer 13 | * Center has been advised of the possibility of such damage. 14 | */ 15 | 16 | /* 17 | * This file is part of cBioPortal. 18 | * 19 | * cBioPortal is free software: you can redistribute it and/or modify 20 | * it under the terms of the GNU Affero General Public License as 21 | * published by the Free Software Foundation, either version 3 of the 22 | * License. 23 | * 24 | * This program is distributed in the hope that it will be useful, 25 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 26 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 27 | * GNU Affero General Public License for more details. 28 | * 29 | * You should have received a copy of the GNU Affero General Public License 30 | * along with this program. If not, see . 31 | */ 32 | 33 | export type OMath = Math & { log2:(x:number)=>number }; 34 | export const OMath:OMath = (Math as any); 35 | 36 | OMath.log2 = OMath.log2 || function(x:number) { return Math.log(x) / Math.LN2; }; -------------------------------------------------------------------------------- /src/js/precomputedcomparator.ts: -------------------------------------------------------------------------------- 1 | import * as BucketSort from "./bucketsort"; 2 | import binarysearch from "./binarysearch"; 3 | import hasElementsInInterval from "./haselementsininterval"; 4 | import { 5 | ColumnId, 6 | TrackSortComparator, 7 | TrackSortDirection, 8 | TrackSortSpecification, 9 | TrackSortSpecificationComparators, TrackSortSpecificationVectors, TrackSortVector 10 | } from "./oncoprintmodel"; 11 | import {SortingVector} from "./bucketsort"; 12 | 13 | type DatumWithVectors = { 14 | d:T; 15 | preferred_vector:SortingVector; 16 | mandatory_vector:SortingVector; 17 | }; 18 | 19 | export default class PrecomputedComparator { 20 | 21 | private preferred_change_points:number[]; 22 | private mandatory_change_points:number[]; 23 | private id_to_index:{[columnId:string]:number}; 24 | 25 | constructor(list:T[], comparator:TrackSortSpecification, sort_direction:TrackSortDirection, element_identifier_key:string&keyof T) { 26 | if (comparator.isVector) { 27 | this.initializeVector(list, comparator, sort_direction, element_identifier_key); 28 | } else { 29 | this.initializeComparator(list, comparator as TrackSortSpecificationComparators, sort_direction, element_identifier_key); 30 | } 31 | } 32 | 33 | private initializeComparator(list:T[], comparator:TrackSortComparator | TrackSortSpecificationComparators, sort_direction:TrackSortDirection, element_identifier_key:keyof T) { 34 | // initializeComparator initializes the PrecomputedComparator in the case that 35 | // the sort order is given using a comparator 36 | let preferred, mandatory; 37 | if (typeof comparator === "function") { 38 | preferred = comparator; 39 | mandatory = comparator; 40 | } else { 41 | preferred = comparator.preferred; 42 | mandatory = comparator.mandatory; 43 | } 44 | function makeDirectedComparator(cmp:TrackSortComparator) { 45 | return function (d1:T, d2:T) { 46 | if (sort_direction === 0) { 47 | return 0; 48 | } 49 | const res = cmp(d1, d2); 50 | if (res === 2) { 51 | return 1; 52 | } else if (res === -2) { 53 | return -1; 54 | } else { 55 | return res * sort_direction; 56 | } 57 | }; 58 | } 59 | const preferredComparator = makeDirectedComparator(preferred); 60 | const mandatoryComparator = makeDirectedComparator(mandatory); 61 | const sorted_list = list.sort(preferredComparator); 62 | 63 | // i is a change point iff comp(elt[i], elt[i+1]) !== 0 64 | this.preferred_change_points = [0]; // i is a preferred change pt iff its a change pt with comp = preferredComparator but not with comp = mandatoryComparator 65 | this.mandatory_change_points = [0]; // i is a mandatory change pt iff its a change pt with comp = mandatoryComparator 66 | 67 | // note that by the following process, preferred_change_points and mandatory_change_points are sorted 68 | for (let i=1; i, sort_direction:TrackSortDirection, element_identifier_key:keyof T) { 82 | // initializeVector initializes the PrecomputedComparator in the case that the sort order is specified by vectors for bucket sort 83 | function makeDirectedVector(vec:TrackSortVector) { 84 | if (sort_direction === 0) { 85 | return function(d:T) { return 0; }; 86 | } else { 87 | return function(d:T) { 88 | return vec(d).map(function(n:number|string) { 89 | if (typeof n === "number") { 90 | return n * sort_direction; 91 | } else { 92 | return n; 93 | } 94 | }); 95 | } 96 | } 97 | } 98 | const preferredVector = makeDirectedVector(getVector.preferred); 99 | const mandatoryVector = makeDirectedVector(getVector.mandatory); 100 | 101 | // associate each data to its vector and sort them together 102 | const list_with_vectors:DatumWithVectors[] = list.map(function(d) { 103 | return { d: d, preferred_vector: preferredVector(d), mandatory_vector: mandatoryVector(d) }; 104 | }) as DatumWithVectors[]; 105 | // sort by preferred vector 106 | const _compareEquals = getVector.compareEquals; 107 | const compareEquals = _compareEquals ? function(d1:DatumWithVectors, d2:DatumWithVectors) { 108 | return _compareEquals(d1.d, d2.d); 109 | } : undefined; 110 | const sorted_list = BucketSort.bucketSort( 111 | list_with_vectors, 112 | function(d) { return d.preferred_vector; }, 113 | compareEquals 114 | ); 115 | 116 | // i is a change point iff comp(elt[i], elt[i+1]) !== 0 117 | this.preferred_change_points = [0]; // i (besides 0) is a preferred change pt iff its a change pt with comp = preferredComparator but not with comp = mandatoryComparator 118 | this.mandatory_change_points = [0]; // i (besides 0) is a mandatory change pt iff its a change pt with comp = mandatoryComparator 119 | 120 | // note that by the following process, preferred_change_points and mandatory_change_points are sorted 121 | const getMandatoryVector = function(d:{ mandatory_vector:(number|string)[]}) { return d.mandatory_vector; }; 122 | const getPreferredVector = function(d:{ preferred_vector:(number|string)[]}) { return d.preferred_vector; }; 123 | for (let i=1; i= upper_excl) { 47 | break; 48 | } 49 | 50 | int middle = (lower_incl + upper_excl)/2; 51 | if (columnsRightAfterGaps[middle] < aVertexOncoprintColumn) { 52 | // G(c) > middle 53 | lower_incl = middle + 1; 54 | } else if (columnsRightAfterGaps[middle] == aVertexOncoprintColumn) { 55 | // G(c) = middle + 1 56 | numGaps = middle + 1; 57 | break; 58 | } else { 59 | // columnsRightAfterGaps[middle] > column, so G(c) <= middle 60 | if (middle == 0) { 61 | // 0 <= G(c) <= 0 -> G(c) = 0 62 | numGaps = 0; 63 | break; 64 | } else if (columnsRightAfterGaps[middle-1] < aVertexOncoprintColumn) { 65 | // G(c) = middle 66 | numGaps = middle; 67 | break; 68 | } else { 69 | // columnsRightAfterGaps[middle-1] >= column, so G(c) <= middle-1 70 | upper_excl = middle; 71 | } 72 | } 73 | } 74 | 75 | // multiply it by the gap size to get the total offset 76 | return float(numGaps)*gapSize; 77 | } 78 | 79 | void main(void) { 80 | gl_Position = vec4(getUnpackedPositionVec3(), 1.0); 81 | gl_Position[0] += aVertexOncoprintColumn*columnWidth; 82 | gl_Position *= vec4(zoomX, zoomY, 1.0, 1.0); 83 | 84 | // gaps should not be affected by zoom: 85 | gl_Position[0] += getGapOffset(); 86 | 87 | // offsetY is given zoomed: 88 | gl_Position[1] += offsetY; 89 | 90 | gl_Position -= vec4(scrollX, scrollY, 0.0, 0.0); 91 | gl_Position[0] *= supersamplingRatio; 92 | gl_Position[1] *= supersamplingRatio; 93 | gl_Position = uPMatrix * uMVMatrix * gl_Position; 94 | 95 | texCoord = (aColVertex + 0.5) / texSize; 96 | }`; 97 | } 98 | 99 | export function getFragmentShaderSource() { 100 | return `precision mediump float; 101 | varying float texCoord; 102 | uniform sampler2D uSampler; 103 | void main(void) { 104 | gl_FragColor = texture2D(uSampler, vec2(texCoord, 0.5)); 105 | }`; 106 | } -------------------------------------------------------------------------------- /src/js/svgfactory.ts: -------------------------------------------------------------------------------- 1 | import makeSVGElement from './makesvgelement'; 2 | import shapeToSVG from './oncoprintshapetosvg'; 3 | import {ComputedShapeParams} from "./oncoprintshape"; 4 | import {RGBAColor} from "./oncoprintruleset"; 5 | import {rgbString} from "./utils"; 6 | 7 | function makeIdCounter() { 8 | let id = 0; 9 | return function () { 10 | id += 1; 11 | return id; 12 | }; 13 | } 14 | 15 | const gradientId = makeIdCounter(); 16 | 17 | export default { 18 | text: function(content:string,x?:number,y?:number,size?:number,family?:string,weight?:string,alignment_baseline?:string,fill?:string,text_decoration?:string) { 19 | size = size || 12; 20 | var alignment_baseline_y_offset = size; 21 | if (alignment_baseline === "middle") { 22 | alignment_baseline_y_offset = size/2; 23 | } else if (alignment_baseline === "bottom") { 24 | alignment_baseline_y_offset = 0; 25 | } 26 | var elt = makeSVGElement('text', { 27 | 'x':(x || 0), 28 | 'y':(y || 0) + alignment_baseline_y_offset, 29 | 'font-size':size, 30 | 'font-family':(family || 'serif'), 31 | 'font-weight':(weight || 'normal'), 32 | 'text-anchor':'start', 33 | 'fill':fill, 34 | 'text-decoration':text_decoration 35 | }); 36 | elt.textContent = content + ''; 37 | return elt as SVGTextElement; 38 | }, 39 | group: function(x:number|undefined,y:number|undefined) { 40 | x = x || 0; 41 | y = y || 0; 42 | return makeSVGElement('g', { 43 | 'transform':'translate('+x+','+y+')' 44 | }) as SVGGElement; 45 | }, 46 | svg: function(width:number|undefined, height:number|undefined) { 47 | return makeSVGElement('svg', { 48 | 'width':(width || 0), 49 | 'height':(height || 0), 50 | }) as SVGSVGElement; 51 | }, 52 | wrapText: function(in_dom_text_svg_elt:SVGTextElement, width:number) { 53 | const text = in_dom_text_svg_elt.textContent; 54 | in_dom_text_svg_elt.textContent = ""; 55 | 56 | const words = text.split(" "); 57 | let dy = 0; 58 | let tspan = makeSVGElement('tspan', {'x':'0', 'dy':dy}) as SVGTSpanElement; 59 | in_dom_text_svg_elt.appendChild(tspan); 60 | 61 | let curr_tspan_words:string[] = []; 62 | for (var i=0; i width) { 66 | tspan.textContent = curr_tspan_words.slice(0, curr_tspan_words.length-1).join(" "); 67 | dy = in_dom_text_svg_elt.getBBox().height; 68 | curr_tspan_words = [words[i]]; 69 | tspan = makeSVGElement('tspan', {'x':'0', 'dy':dy}) as SVGTSpanElement; 70 | in_dom_text_svg_elt.appendChild(tspan); 71 | tspan.textContent = words[i]; 72 | } 73 | } 74 | }, 75 | fromShape: function(oncoprint_shape_computed_params:ComputedShapeParams, offset_x:number, offset_y:number) { 76 | return shapeToSVG(oncoprint_shape_computed_params, offset_x, offset_y); 77 | }, 78 | polygon: function(points:[number,number][], fill:RGBAColor) { 79 | return makeSVGElement('polygon', {'points': points, 'fill':rgbString(fill), 'fill-opacity':fill[3]}) as SVGPolygonElement; 80 | }, 81 | rect: function(x:number,y:number,width:number,height:number,fillSpecification:{ 82 | type: "rgba", 83 | value: RGBAColor 84 | }| { 85 | type: "gradientId", 86 | value: string 87 | }) { 88 | let fill:string; 89 | let fillOpacity = 1; 90 | if (fillSpecification.type === "rgba") { 91 | fill = rgbString(fillSpecification.value); 92 | fillOpacity = fillSpecification.value[3]; 93 | } else { 94 | fill = `url(#${fillSpecification.value})`; 95 | } 96 | return makeSVGElement('rect', {'x':x, 'y':y, 'width':width, 'height':height, 'fill':fill, 'fill-opacity':fillOpacity}) as SVGRectElement; 97 | }, 98 | bgrect: function(width:number, height:number, fill:RGBAColor) { 99 | return makeSVGElement('rect', {'width':width, 'height':height, 'fill':rgbString(fill), 'fill-opacity':fill[3]}) as SVGRectElement; 100 | }, 101 | path: function(points:[number,number][], stroke:RGBAColor, fill:RGBAColor, linearGradient:SVGGradientElement) { 102 | let pointsStrArray = points.map(function(pt) { return pt.join(","); }); 103 | pointsStrArray[0] = 'M'+points[0]; 104 | for (var i=1; iRGBAColor) { 129 | const gradient = makeSVGElement('linearGradient', { 130 | 'id': 'gradient'+gradientId(), 131 | 'x1':0, 132 | 'y1':0, 133 | 'x2':1, 134 | 'y2':0 135 | }); 136 | for (let i=0; i<=100; i++) { 137 | gradient.appendChild( 138 | this.stop(i, colorFn(i/100)) 139 | ); 140 | } 141 | return gradient as SVGLinearGradientElement 142 | } 143 | }; 144 | 145 | 146 | -------------------------------------------------------------------------------- /src/js/utils.ts: -------------------------------------------------------------------------------- 1 | import {ComputedShapeParams} from "./oncoprintshape"; 2 | import {RGBAColor} from "./oncoprintruleset"; 3 | 4 | export type Omit = Pick>; 5 | 6 | export function cloneShallow(obj:T) { 7 | const ret:Partial = {}; 8 | for (const key of (Object.keys(obj) as (keyof T)[])) { 9 | ret[key] = obj[key]; 10 | } 11 | return ret as T; 12 | } 13 | 14 | export function extendArray(target:any[], source:any[]) { 15 | for (let i=0; i(x:T|undefined, val:T):T { 34 | return (typeof x === "undefined" ? val : x); 35 | } 36 | 37 | export function shallowExtend(target:T, source:S):T&S { 38 | const ret:Partial = {}; 39 | for (const key of Object.keys(target) as (keyof T&S)[]) { 40 | ret[key] = target[key as keyof T] as any; 41 | } 42 | for (const key of Object.keys(source) as (keyof T&S)[]) { 43 | ret[key] = source[key as keyof S] as any; 44 | } 45 | return ret as T&S; 46 | } 47 | 48 | export function objectValues(obj:T):(T[keyof T][]) { 49 | return Object.keys(obj).map(function(key:string&keyof T) { return obj[key]; }); 50 | } 51 | 52 | export function arrayFindIndex(arr:T[], predicate:(t:T)=>boolean, start_index?:number) { 53 | start_index = start_index || 0; 54 | for (let i=start_index; i b) { 66 | return 1; 67 | } else { 68 | return 0; 69 | } 70 | } 71 | 72 | export function clamp(x:number, lower:number, upper:number) { 73 | return Math.max(lower, Math.min(upper, x)); 74 | } 75 | export function z_comparator(shapeA:ComputedShapeParams, shapeB:ComputedShapeParams) { 76 | const zA = shapeA.z; 77 | const zB = shapeB.z; 78 | if (zA < zB) { 79 | return -1; 80 | } else if (zA > zB) { 81 | return 1; 82 | } else { 83 | return 0; 84 | } 85 | } 86 | 87 | export function fastParseInt10(x:string, substringStart?:number, substringEnd?:number) { 88 | // simple, fast parseInt when you know its a base-10 int and 89 | // you don't need any error handling. 90 | // Performance testing shows this is 85% faster than built-in parseInt 91 | substringStart = substringStart || 0; 92 | substringEnd = substringEnd || x.length; 93 | let ret = 0; 94 | for (let i=substringStart; i 96) { 111 | ret += nextCharCode - 87; // lower case letters start at 97. a is 97, should be 10 112 | } else if (nextCharCode > 64) { 113 | ret += nextCharCode - 55; // capital letters start at 65. A is 65, should be 10 114 | } else { 115 | // otherwise, its an integer 116 | ret += nextCharCode - 48; 117 | } 118 | } 119 | return ret; 120 | } 121 | 122 | export function rgbString(color:RGBAColor) { 123 | return `rgb(${color[0]},${color[1]},${color[2]})`; 124 | } -------------------------------------------------------------------------------- /src/js/workers/clustering-worker.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2016 The Hyve B.V. 3 | * This code is licensed under the GNU Affero General Public License, 4 | * version 3, or (at your option) any later version. 5 | */ 6 | 7 | /* 8 | * This file is part of cBioPortal. 9 | * 10 | * cBioPortal is free software: you can redistribute it and/or modify 11 | * it under the terms of the GNU Affero General Public License as 12 | * published by the Free Software Foundation, either version 3 of the 13 | * License. 14 | * 15 | * This program is distributed in the hope that it will be useful, 16 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 17 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 18 | * GNU Affero General Public License for more details. 19 | * 20 | * You should have received a copy of the GNU Affero General Public License 21 | * along with this program. If not, see . 22 | */ 23 | 24 | // @ts-ignore 25 | import clusterfck from "tayden-clusterfck"; 26 | // @ts-ignore 27 | import jStat from "jstat"; 28 | 29 | type Item = { 30 | orderedValueList:number[]; 31 | } 32 | 33 | type ProcessedItem = Item & { 34 | isAllNaNs:boolean; 35 | preProcessedValueList:number[]; 36 | } 37 | 38 | export type EntityItem = Item & { entityId: string}; 39 | export type CaseItem = Item & { caseId: string }; 40 | 41 | export type CasesAndEntities = { 42 | [caseId:string]:{ 43 | [entityId:string]:number 44 | } 45 | }; 46 | 47 | export type ClusteringMessage = { 48 | dimension: "CASES" | "ENTITIES"; 49 | casesAndEntities:CasesAndEntities; 50 | }; 51 | 52 | 53 | const ctx:Worker = (self as any as Worker); 54 | /** 55 | * "Routing" logic for this worker, based on given message. 56 | * 57 | * @param m : message object with m.dimension (CASES or ENTITIES) and m.casesAndEntitites 58 | * which is the input for the clustering method. 59 | */ 60 | ctx.onmessage = function(m:MessageEvent) { 61 | console.log('Clustering worker received message'); 62 | var result = null; 63 | if ((m.data as ClusteringMessage).dimension === "CASES") { 64 | result = hclusterCases((m.data as ClusteringMessage).casesAndEntities); 65 | } else if ((m.data as ClusteringMessage).dimension === "ENTITIES") { 66 | result = hclusterGeneticEntities((m.data as ClusteringMessage).casesAndEntities); 67 | } else { 68 | throw new Error("Illegal argument given to clustering-worker.js for m.data.dimension: " + m.data.dimension); 69 | } 70 | console.log('Posting clustering result back to main script'); 71 | ctx.postMessage(result); 72 | } 73 | 74 | /** 75 | * Returns false if any value is a valid number != 0.0, 76 | * and true otherwise. 77 | */ 78 | function isAllNaNs(values:any[]) { 79 | for (let i = 0; i < values.length; i++) { 80 | const val = values[i]; 81 | if (!isNaN(val) && val != null && val != 0.0 ) { 82 | return false; 83 | } 84 | } 85 | return true; 86 | } 87 | 88 | /** 89 | * Distance measure using 1-spearman's correlation. This function does expect that item1 and item2 90 | * are an item than contains a item.preProcessedValueList attribute which is the ranked version 91 | * of item.orderedValueList. 92 | * 93 | */ 94 | function preRankedSpearmanDist(item1:ProcessedItem, item2:ProcessedItem) { 95 | //rules for NaN values: 96 | if (item1.isAllNaNs && item2.isAllNaNs) { 97 | //return distance 0 98 | return 0; 99 | } 100 | else if (item1.isAllNaNs || item2.isAllNaNs) { 101 | //return large distance: 102 | return 3; 103 | } 104 | //take the arrays from the preProcessedValueList: 105 | var ranks1 = item1.preProcessedValueList; 106 | var ranks2 = item2.preProcessedValueList; 107 | //calculate spearman's rank correlation coefficient, using pearson's distance 108 | //for correlation of the ranks: 109 | var r = jStat.corrcoeff(ranks1, ranks2); 110 | if (isNaN(r)) { 111 | //assuming the ranks1 and ranks2 lists do not contain NaN entries (and this code DOES assume all missing values have been imputed by a valid number), 112 | //this specific scenario should not occur, unless all values are the same (and given the same rank). In this case, there is no variation, and 113 | //correlation returns NaN. In theory this could happen on small number of entities being clustered. We give this a large distance: 114 | console.log("NaN in correlation calculation"); 115 | r = -2; 116 | } 117 | return 1 - r; 118 | } 119 | 120 | /** 121 | * Prepares the data for using spearman method in the distance function. 122 | * It will pre-calculate ranks and store this in inputItems[x].preProcessedValueList. 123 | * This pre-calculation significantly improves the performance of the clustering step itself. 124 | */ 125 | function _prepareForDistanceFunction(inputItems:Item[]) { 126 | //pre-calculate ranks, and 127 | // split up into allNaN and notAllNaN 128 | var allNaN = []; 129 | var notAllNaN = []; 130 | for (var i = 0; i < inputItems.length; i++) { 131 | var inputItem = inputItems[i] as ProcessedItem; 132 | //check if all NaNs: 133 | inputItem.isAllNaNs = isAllNaNs(inputItem.orderedValueList); 134 | if (inputItem.isAllNaNs) { 135 | allNaN.push(inputItem); 136 | continue; 137 | } else { 138 | notAllNaN.push(inputItem); 139 | } 140 | //rank using fractional ranking: 141 | var ranks = jStat.rank(inputItem.orderedValueList); 142 | //store for later use: 143 | inputItem.preProcessedValueList = ranks; 144 | } 145 | return { 146 | notAllNaN: notAllNaN, 147 | allNaN: allNaN 148 | }; 149 | } 150 | 151 | 152 | 153 | /** 154 | * @param casesAndEntitites: Object with sample(or patient)Id and map 155 | * of geneticEntity/value pairs. Example: 156 | * 157 | * var a = 158 | * { 159 | * "TCGA-AO-AA98-01": 160 | * { 161 | * "TP53": 0.045, 162 | * "BRA1": -0.89 163 | * } 164 | * }, 165 | * ... 166 | * 167 | * @return the reordered list of sample(or patient) ids, after clustering. 168 | */ 169 | function hclusterCases(casesAndEntitites:CasesAndEntities):CaseItem[] { 170 | var refEntityList = null; 171 | var inputItems = []; 172 | //add orderedValueList to all items, so the values are 173 | //compared in same order: 174 | for (var caseId in casesAndEntitites) { 175 | var caseObj = casesAndEntitites[caseId]; 176 | var inputItem = new Object() as CaseItem; 177 | inputItem.caseId = caseId; 178 | inputItem.orderedValueList = []; 179 | if (refEntityList == null) { 180 | refEntityList = getRefList(caseObj); 181 | } 182 | for (var j = 0; j < refEntityList.length; j++) { 183 | var entityId = refEntityList[j]; 184 | var value = caseObj[entityId]; 185 | inputItem.orderedValueList.push(value); 186 | } 187 | inputItems.push(inputItem); 188 | } 189 | if (refEntityList.length == 1) { 190 | //this is a special case, where the "clustering" becomes a simple sorting in 1 dimension: 191 | //so, just sort and return inputItems: 192 | inputItems.sort(function (i1, i2) { 193 | var val1 = i1.orderedValueList[0]; 194 | var val2 = i2.orderedValueList[0]; 195 | //ensure NaNs are moved out (NaN or null which are seen here as equivalents to NA (not available)) to the end of the list: 196 | val1 = (val1 == null || isNaN(val1) ? Number.MAX_VALUE : val1); 197 | val2 = (val2 == null || isNaN(val2) ? Number.MAX_VALUE : val2); 198 | if (val1 > val2) { 199 | return 1; 200 | } 201 | else if (val1 < val2) { 202 | return -1; 203 | } 204 | return 0; 205 | }); 206 | return inputItems; 207 | } 208 | //else, normal clustering: 209 | var processedInputItems = _prepareForDistanceFunction(inputItems); 210 | var clusters = clusterfck.hcluster(processedInputItems.notAllNaN, preRankedSpearmanDist); 211 | return clusters.clusters(1)[0].concat(processedInputItems.allNaN); // add all nan elements to the end post-sorting 212 | } 213 | 214 | function getRefList(caseItem:CasesAndEntities[""]) { 215 | var result = []; 216 | for (var entityId in caseItem) { 217 | result.push(entityId); 218 | } 219 | return result; 220 | } 221 | 222 | /** 223 | * @param casesAndEntitites: same as used in hclusterCases above. 224 | * 225 | * @return the reordered list of entity ids, after clustering. 226 | */ 227 | function hclusterGeneticEntities(casesAndEntitites:CasesAndEntities):EntityItem[] { 228 | var refEntityList = null; 229 | var inputItems = []; 230 | var refCaseIdList = []; 231 | //add orderedValueList to all items, so the values are 232 | //compared in same order: 233 | for (var caseId in casesAndEntitites) { 234 | var caseObj = casesAndEntitites[caseId]; 235 | if (refEntityList == null) { 236 | refEntityList = getRefList(caseObj); 237 | } 238 | //refCaseIdList: 239 | refCaseIdList.push(caseId); 240 | } 241 | //iterate over genes, and get sample values: 242 | for (var i = 0; i < refEntityList.length; i++) { 243 | var entityId = refEntityList[i]; 244 | var inputItem = new Object() as EntityItem; 245 | inputItem.entityId = entityId; 246 | inputItem.orderedValueList = []; 247 | for (var j = 0; j < refCaseIdList.length; j++) { 248 | var caseId = refCaseIdList[j]; 249 | var caseObj = casesAndEntitites[caseId]; 250 | var value = caseObj[entityId]; 251 | inputItem.orderedValueList.push(value); 252 | } 253 | inputItems.push(inputItem); 254 | } 255 | var processedInputItems = _prepareForDistanceFunction(inputItems); 256 | var clusters = clusterfck.hcluster(processedInputItems.notAllNaN, preRankedSpearmanDist); 257 | return clusters.clusters(1)[0].concat(processedInputItems.allNaN); // add all nan elements to the end post-sorting 258 | } 259 | 260 | export default null; -------------------------------------------------------------------------------- /src/test/gradientCategoricalRuleset.spec.ts: -------------------------------------------------------------------------------- 1 | import OncoprintRuleSet, {RuleSetParams, RuleSetType} from "../js/oncoprintruleset"; 2 | import {assert} from "chai"; 3 | 4 | type Datum = { 5 | id:string; 6 | category:string|undefined; 7 | profile_data:number|null; 8 | truncation?:any; 9 | }; 10 | 11 | describe("GradientCategoricalRuleSet", function() { 12 | 13 | const mixParams:RuleSetParams = { 14 | type: RuleSetType.GRADIENT_AND_CATEGORICAL, 15 | legend_label: "this is a label", 16 | value_key: "profile_data", 17 | value_range: [1,8], 18 | value_stop_points: [1,2,8], 19 | colors: [[255,0,0,1],[0,0,0,1],[0,255,0,1]], 20 | null_color: [224,224,224,1], 21 | category_key: "category" 22 | }; 23 | 24 | const categoryDatum:Datum = { 25 | id:"a", 26 | category: ">8", 27 | profile_data: 8 28 | }; 29 | 30 | const gradientDatumLargest:Datum = { 31 | id:"b", 32 | category: undefined, 33 | profile_data: 8 34 | }; 35 | 36 | const gradientDatumSmallest:Datum = { 37 | id:"c", 38 | category: undefined, 39 | profile_data: 1 40 | }; 41 | 42 | const naDatum:Datum = { 43 | id:"d", 44 | category: undefined, 45 | profile_data: null, 46 | truncation: undefined 47 | }; 48 | 49 | it("Formats gradient value", function() { 50 | var mixRuleSet = OncoprintRuleSet(mixParams); 51 | var elements = mixRuleSet.getSpecificShapesForDatum([gradientDatumLargest, gradientDatumSmallest, naDatum], 12, 12, undefined, "id"); 52 | assert.equal(elements.length, 3); 53 | assert.deepEqual(elements[0][0].fill,[0,255,0,1]); 54 | assert.deepEqual(elements[1][0].fill,[255,0,0,1]); 55 | assert.deepEqual(elements[2][0].fill,[224,224,224,1]); 56 | }); 57 | 58 | it("Formats categorical value", function() { 59 | var mixRuleSet = OncoprintRuleSet(mixParams); 60 | var elements = mixRuleSet.getSpecificShapesForDatum([categoryDatum], 12, 12, undefined, "id"); 61 | assert.equal(elements.length, 1); 62 | }); 63 | 64 | it("Suppresses duplicate No Data rules", function() { 65 | var mixRuleSet = OncoprintRuleSet(mixParams); 66 | var elements = mixRuleSet.getSpecificRulesForDatum(); 67 | assert.equal(elements.length, 2); 68 | }); 69 | 70 | }); 71 | -------------------------------------------------------------------------------- /src/test/mocks/empty-module.js: -------------------------------------------------------------------------------- 1 | module.exports = ''; 2 | -------------------------------------------------------------------------------- /src/test/monolith.spec.ts: -------------------------------------------------------------------------------- 1 | import Oncoprint from "../js/oncoprint"; 2 | import * as BucketSort from "../js/bucketsort"; 3 | import binarySearch from "../js/binarysearch"; 4 | import {assert} from "chai"; 5 | import {doesCellIntersectPixel} from "../js/utils"; 6 | 7 | describe("test", function() { 8 | it("should have oncoprint object", function() { 9 | assert.isDefined(Oncoprint); 10 | }); 11 | }); 12 | 13 | describe("binarySearch", function() { 14 | it("case: empty input", function() { 15 | assert.equal(binarySearch([], 0, function(x) { return x; }, true), -1); 16 | }); 17 | 18 | it("case: key not found, return closest option false", function() { 19 | assert.equal(binarySearch([0,1,2], 1.5, function(x) { return x; }, false), -1); 20 | }); 21 | 22 | it("case: key not found, return closest option true - should give the nearest smaller index", function() { 23 | assert.equal(binarySearch([0,1,2], -0.5, function(x) { return x; }, true), 0); 24 | assert.equal(binarySearch([0,1,2], 0.5, function(x) { return x; }, true), 0); 25 | assert.equal(binarySearch([0,1,2], 1.5, function(x) { return x; }, true), 1); 26 | assert.equal(binarySearch([0,1,2], 2.5, function(x) { return x; }, true), 2); 27 | assert.equal(binarySearch([0,1,2], 3.5, function(x) { return x; }, true), 2); 28 | 29 | assert.equal(binarySearch([0,1,2,3,4,5,6,7], -1.5, function(x) { return x; }, true), 0); 30 | assert.equal(binarySearch([0,1,2,3,4,5,6,7], -0.5, function(x) { return x; }, true), 0); 31 | assert.equal(binarySearch([0,1,2,3,4,5,6,7], 0.5, function(x) { return x; }, true), 0); 32 | assert.equal(binarySearch([0,1,2,3,4,5,6,7], 1.5, function(x) { return x; }, true), 1); 33 | assert.equal(binarySearch([0,1,2,3,4,5,6,7], 2.5, function(x) { return x; }, true), 2); 34 | assert.equal(binarySearch([0,1,2,3,4,5,6,7], 3.5, function(x) { return x; }, true), 3); 35 | assert.equal(binarySearch([0,1,2,3,4,5,6,7], 4.5, function(x) { return x; }, true), 4); 36 | assert.equal(binarySearch([0,1,2,3,4,5,6,7], 5.5, function(x) { return x; }, true), 5); 37 | assert.equal(binarySearch([0,1,2,3,4,5,6,7], 6.5, function(x) { return x; }, true), 6); 38 | assert.equal(binarySearch([0,1,2,3,4,5,6,7], 7.5, function(x) { return x; }, true), 7); 39 | assert.equal(binarySearch([0,1,2,3,4,5,6,7], 8.5, function(x) { return x; }, true), 7); 40 | }); 41 | 42 | it("case: key found", function() { 43 | assert.equal(binarySearch([0,1,2], 0, function(x) { return x*x; }, true), 0); 44 | assert.equal(binarySearch([0,1,2], 1, function(x) { return x*x; }, true), 1); 45 | assert.equal(binarySearch([0,1,2], 4, function(x) { return x*x; }, true), 2); 46 | 47 | assert.equal(binarySearch([0,1,2,3,4,5,6,7], 0, function(x) { return x*x; }, true), 0); 48 | assert.equal(binarySearch([0,1,2,3,4,5,6,7], 1, function(x) { return x*x; }, true), 1); 49 | assert.equal(binarySearch([0,1,2,3,4,5,6,7], 4, function(x) { return x*x; }, true), 2); 50 | assert.equal(binarySearch([0,1,2,3,4,5,6,7], 9, function(x) { return x*x; }, true), 3); 51 | assert.equal(binarySearch([0,1,2,3,4,5,6,7], 16, function(x) { return x*x; }, true), 4); 52 | assert.equal(binarySearch([0,1,2,3,4,5,6,7], 25, function(x) { return x*x; }, true), 5); 53 | assert.equal(binarySearch([0,1,2,3,4,5,6,7], 36, function(x) { return x*x; }, true), 6); 54 | assert.equal(binarySearch([0,1,2,3,4,5,6,7], 49, function(x) { return x*x; }, true), 7); 55 | }); 56 | }); 57 | 58 | describe("bucketSort", function() { 59 | describe("bucketSort", function() { 60 | it("case: empty input", function() { 61 | assert.deepEqual( 62 | BucketSort.bucketSort([]), 63 | [] 64 | ); 65 | }); 66 | it("case: size 1 vectors", function() { 67 | assert.deepEqual( 68 | BucketSort.bucketSort([[3],[1],[2]]), 69 | [[1],[2],[3]] 70 | ); 71 | }); 72 | it("case: sorting with infinity", function() { 73 | assert.deepEqual( 74 | BucketSort.bucketSort([[Number.POSITIVE_INFINITY, 0], [0, 1]]), 75 | [[0,1],[Number.POSITIVE_INFINITY, 0]] 76 | ); 77 | assert.deepEqual( 78 | BucketSort.bucketSort([[0, 0], [Number.NEGATIVE_INFINITY, 1]]), 79 | [[Number.NEGATIVE_INFINITY,1],[0, 0]] 80 | ); 81 | }); 82 | it("case: simple size 4 vectors", function() { 83 | assert.deepEqual( 84 | BucketSort.bucketSort([ 85 | [3,3,3,3], 86 | [0,0,0,0], 87 | [2,2,2,2], 88 | [1,1,1,1] 89 | ]), 90 | [ 91 | [0,0,0,0], 92 | [1,1,1,1], 93 | [2,2,2,2], 94 | [3,3,3,3] 95 | ] 96 | ); 97 | }); 98 | it("case: general size 4 vectors", function() { 99 | assert.deepEqual( 100 | BucketSort.bucketSort([ 101 | [13,10,-11,5], 102 | [-8,1,-8,-24], 103 | [-12, 23,-17,15], 104 | [-21,6,11,4], 105 | [12,22,21,8] 106 | ]), 107 | [ 108 | [-21,6,11,4], 109 | [-12, 23,-17,15], 110 | [-8,1,-8,-24], 111 | [12,22,21,8], 112 | [13,10,-11,5] 113 | ] 114 | ); 115 | 116 | assert.deepEqual( 117 | BucketSort.bucketSort([ 118 | [13,10,-11,5], 119 | [13,1,-8,-24], 120 | [-12, 23,-17,15], 121 | [-12,6,11,4], 122 | [12,22,21,8] 123 | ]), 124 | [ 125 | [-12,6,11,4], 126 | [-12, 23,-17,15], 127 | [12,22,21,8], 128 | [13,1,-8,-24], 129 | [13,10,-11,5] 130 | ] 131 | ); 132 | assert.deepEqual( 133 | BucketSort.bucketSort([ 134 | [0,10,-11,5], 135 | [0,1,-8,-24], 136 | [0, 23,-17,15], 137 | [0,6,11,4], 138 | [0,22,21,8] 139 | ]), 140 | [ 141 | [0,1,-8,-24], 142 | [0,6,11,4], 143 | [0,10,-11,5], 144 | [0,22,21,8], 145 | [0, 23,-17,15] 146 | ] 147 | ); 148 | assert.deepEqual( 149 | BucketSort.bucketSort([ 150 | [0,10,-11,5], 151 | [0,10], 152 | [0], 153 | [0,10,-11] 154 | ]), 155 | [ 156 | [0], 157 | [0,10], 158 | [0,10,-11], 159 | [0,10,-11,5] 160 | ] 161 | ); 162 | }); 163 | it("case: size 2 vectors with compareEquals on the sample id", function() { 164 | assert.deepEqual( 165 | BucketSort.bucketSort([ 166 | {sample:"D", vector:[13,8]}, 167 | {sample:"A", vector:[13,10]}, 168 | {sample:"C", vector:[12,10]} 169 | ], function(d) { return d.vector; }, function(d1, d2) { return d1.sample.localeCompare(d2.sample); }), 170 | [ 171 | {sample:"C", vector:[12,10]}, 172 | {sample:"D", vector:[13,8]}, 173 | {sample:"A", vector:[13,10]} 174 | ] 175 | ); 176 | 177 | assert.deepEqual( 178 | BucketSort.bucketSort([ 179 | {sample:"D", vector:[13,10]}, 180 | {sample:"A", vector:[13,10]}, 181 | {sample:"C", vector:[12,10]} 182 | ], function(d) { return d.vector; }, function(d1, d2) { return d1.sample.localeCompare(d2.sample); }), 183 | [ 184 | {sample:"C", vector:[12,10]}, 185 | {sample:"A", vector:[13,10]}, 186 | {sample:"D", vector:[13,10]} 187 | ] 188 | ); 189 | }); 190 | it("case: randomized tests", function() { 191 | function randInt(magnitude:number) { 192 | var ret = Math.round(Math.random()*magnitude - Math.random()*magnitude); 193 | if (ret === 0) { 194 | // to deal w issues w negative zero 195 | ret = 0; 196 | } 197 | return ret; 198 | } 199 | 200 | function generateVector(size:number) { 201 | var ret = []; 202 | var vectorSize = size+Math.round(Math.random()*4); 203 | for (var i=0; i 2 | 3 | 4 | 5 | 6 | 7 | 8 |

        Oncoprint Genomic Alterations

        9 |
        10 |

        Oncoprint Heatmap

        11 |
        12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /test/oncoprint-glyphmap.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function() { 2 | 3 | var oncoprint = new window.Oncoprint("#oncoprint-glyphmap", 800); 4 | oncoprint.suppressRendering(); 5 | 6 | function geneticComparator() { 7 | var cna_key = 'disp_cna'; 8 | var cna_order = {'amp': 0, 'homdel': 1, 'gain': 2, 'hetloss': 3, 'diploid': 4, undefined: 5}; 9 | var mut_type_key = 'disp_mut'; 10 | var mut_order = {'trunc': 0, 'inframe': 1, 'promoter': 2, 'missense': 3, undefined: 4}; 11 | var mrna_key = 'disp_mrna'; 12 | var prot_key = 'disp_prot'; 13 | var reg_order = {'up': 0, 'down': 1, undefined: 2}; 14 | return function (d1, d2) { 15 | var keys = [cna_key, mut_type_key, mrna_key, prot_key]; 16 | var orders = [cna_order, mut_order, reg_order, reg_order]; 17 | var diff = 0; 18 | for (var i = 0; i < keys.length; i++) { 19 | var key = keys[i]; 20 | var order = orders[i]; 21 | if (d1[key] && d2[key]) { 22 | diff = order[d1[key]] - order[d2[key]]; 23 | } else if (d1[key]) { 24 | diff = -1; 25 | } else if (d2[key]) { 26 | diff = 1; 27 | } 28 | } 29 | return diff; 30 | } 31 | } 32 | 33 | var share_id = null; 34 | for (var i = 0; i < ga_data.length; i++) { 35 | var track_params = { 36 | 'rule_set_params': window.geneticrules.genetic_rule_set_different_colors_no_recurrence, 37 | 'label': ga_data[i].gene, 38 | 'target_group': 0, 39 | 'sortCmpFn': geneticComparator(), 40 | 'description': ga_data[i].desc, 41 | 'na_z': 1.1 42 | }; 43 | var new_ga_id = oncoprint.addTracks([track_params])[0]; 44 | ga_data[i].track_id = new_ga_id; 45 | if (i === 0) { 46 | share_id = new_ga_id; 47 | } else { 48 | oncoprint.shareRuleSet(share_id, new_ga_id); 49 | } 50 | } 51 | 52 | oncoprint.hideIds([], true); 53 | oncoprint.keepSorted(false); 54 | 55 | for (var i = 0; i < ga_data.length; i++) { 56 | oncoprint.setTrackData(ga_data[i].track_id, ga_data[i].data, 'sample'); 57 | oncoprint.setTrackInfo(ga_data[i].track_id, ""); 58 | oncoprint.setTrackTooltipFn(ga_data[i].track_id, function(data) { 59 | return "Sample: " + data.sample + ""; 60 | }); 61 | } 62 | 63 | oncoprint.keepSorted(true); 64 | oncoprint.releaseRendering(); 65 | 66 | }); 67 | -------------------------------------------------------------------------------- /test/oncoprint-heatmap.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function() { 2 | 3 | var oncoprint = new window.Oncoprint("#oncoprint-heatmap", 800); 4 | var TRACK_GROUP = 1; 5 | 6 | // hm_data is in heatmap-data.js 7 | 8 | var current_expansion_tracks = []; 9 | function makeExpandCallback(gene_id) { 10 | return function (parent_track_id) { 11 | oncoprint.suppressRendering(); 12 | try { 13 | for (var i = 0; i < 3; i++) { 14 | var track_params = { 15 | 'rule_set_params': { 16 | 'type': 'gradient', 17 | 'legend_label': 'Expansion', 18 | 'value_key': 'vaf', 19 | 'value_range': [-3, 3], 20 | 'colors': [[0, 255, 255,1],[0,0,0,1],[255,255,0,1]], 21 | 'value_stop_points': [-3, 0, 3], 22 | 'null_color': 'rgba(224,224,224,1)', 23 | }, 24 | 'has_column_spacing': true, 25 | 'track_padding': 5, 26 | 'label': gene_id + 'abc'[i], 27 | 'track_label_color': 'teal', 28 | 'sortCmpFn': function(d1, d2) {return 0;}, 29 | 'target_group': TRACK_GROUP, 30 | 'expansion_of': parent_track_id, 31 | 'removable': true, 32 | 'removeCallback': function (track_id) { 33 | current_expansion_tracks.splice( 34 | current_expansion_tracks.indexOf(track_id), 35 | 1 36 | ) 37 | } 38 | } 39 | var new_track_id = oncoprint.addTracks([track_params])[0]; 40 | if (current_expansion_tracks.length > 0) { 41 | oncoprint.shareRuleSet(current_expansion_tracks[0], new_track_id); 42 | } 43 | current_expansion_tracks.push(new_track_id); 44 | var data_record = hm_data.filter(function (record) { 45 | return record.gene === gene_id; 46 | })[0]; 47 | oncoprint.setTrackData(new_track_id, data_record.data, 'sample'); 48 | } 49 | } finally { 50 | oncoprint.releaseRendering(); 51 | } 52 | } 53 | } 54 | 55 | oncoprint.suppressRendering(); 56 | 57 | var share_id = null; 58 | for (var i = 0; i < hm_data.length; i++) { 59 | var track_params = { 60 | 'rule_set_params': { 61 | 'type': 'gradient', 62 | 'legend_label': 'Heatmap', 63 | 'value_key': 'vaf', 64 | 'value_range': [-3, 3], 65 | 'colors': [[255,0,0,1],[0,0,0,1],[0,0,255,1]], 66 | 'value_stop_points': [-3, 0, 3], 67 | 'null_color': 'rgba(224,224,224,1)' 68 | }, 69 | 'has_column_spacing': true, 70 | 'track_padding': 5, 71 | 'label': hm_data[i].gene, 72 | 'sortCmpFn': function(d1, d2) {return 0;}, 73 | 'target_group': TRACK_GROUP, 74 | 'expandCallback': makeExpandCallback(hm_data[i].gene), 75 | 'removable': true 76 | }; 77 | var new_hm_id = oncoprint.addTracks([track_params])[0]; 78 | hm_data[i].track_id = new_hm_id; 79 | if (i === 0) { 80 | share_id = new_hm_id; 81 | } else { 82 | oncoprint.shareRuleSet(share_id, new_hm_id); 83 | } 84 | } 85 | 86 | oncoprint.hideIds([], true); 87 | oncoprint.keepSorted(false); 88 | 89 | for (var i = 0; i < hm_data.length; i++) { 90 | oncoprint.setTrackData(hm_data[i].track_id, hm_data[i].data, 'sample'); 91 | oncoprint.setTrackInfo(hm_data[i].track_id, "VAF"); 92 | oncoprint.setTrackTooltipFn(hm_data[i].track_id, function(data) { 93 | return "Sample: " + data.sample + "
        VAF: " + data.vaf.toString().slice(0, 4) + "
        "; 94 | }); 95 | } 96 | 97 | oncoprint.keepSorted(true); 98 | oncoprint.releaseRendering(); 99 | 100 | }); 101 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "./dist/", 4 | "allowSyntheticDefaultImports": true, 5 | "noImplicitAny": true, 6 | "module": "es6", 7 | "target": "es2015", 8 | "declaration":true 9 | }, 10 | "exclude": ["./node_modules/*"], 11 | "include": [ 12 | "./node_modules/@types/**/*", 13 | "./typings/**/*", 14 | "./src/**/*" 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /typings/custom.d.ts: -------------------------------------------------------------------------------- 1 | declare module "worker-loader*" { 2 | class WebpackWorker extends Worker { 3 | constructor(); 4 | } 5 | 6 | export default WebpackWorker; 7 | } -------------------------------------------------------------------------------- /typings/missing.d.ts: -------------------------------------------------------------------------------- 1 | // allow these file patterns to be imported 2 | declare module '*.svg'; 3 | 4 | // these packages are missing typings 5 | declare module 'gl-matrix'; 6 | declare module 'tayden-clusterfck'; 7 | declare module 'jstat'; 8 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const WebpackShellPlugin = require('webpack-shell-plugin'); 3 | 4 | module.exports = env => ({ 5 | entry: path.resolve(__dirname, 'src/js/oncoprint.ts'), 6 | module: { 7 | rules: [ 8 | { 9 | test: /\.(png|jp(e*)g|svg)$/, 10 | use: [{ 11 | loader: 'url-loader', 12 | }] 13 | }, 14 | { 15 | test: /src\/js\/workers\/*/, 16 | use:[{ 17 | loader: 'worker-loader', 18 | options: { inline:true, fallback: false } 19 | }] 20 | }, 21 | { 22 | test: /src\/js\/.+\.tsx?$/, 23 | use: 'ts-loader', 24 | exclude: /node_modules/ 25 | } 26 | ] 27 | }, 28 | plugins: [ 29 | new WebpackShellPlugin({onBuildStart:['mkdir -p '+path.resolve(__dirname, 'dist')]}) 30 | ], 31 | resolve: { 32 | extensions: [ '.tsx', '.ts', '.js' ] 33 | }, 34 | output: { 35 | path: path.resolve(__dirname, 'dist'), 36 | filename: 'oncoprint.bundle.js', 37 | library: 'oncoprintjs', 38 | libraryTarget: 'commonjs-module' 39 | }, 40 | optimization: { 41 | minimize: (!env || (env.DEV !== "true")) 42 | } 43 | }); 44 | --------------------------------------------------------------------------------