From 897b32b4d8709dee0600b72025b91e5712dcb9d3 Mon Sep 17 00:00:00 2001 From: strasserle Date: Sat, 30 Nov 2024 17:39:28 +0100 Subject: [PATCH 01/14] spongeffect wip --- angular.json | 3 + package-lock.json | 37 + package.json | 4 + public/favicon.ico | Bin 15086 -> 15406 bytes src/app/app.component.ts | 2 +- src/app/app.routes.ts | 6 + src/app/interfaces.ts | 278 +++- .../spongeffects/spongeffects.component.html | 500 ++++++ .../spongeffects/spongeffects.component.scss | 429 +++++ .../spongeffects.component.spec.ts | 23 + .../spongeffects/spongeffects.component.ts | 1460 +++++++++++++++++ .../spongeffects/spongeffects.module.ts | 93 ++ src/app/services/backend.service.ts | 200 ++- src/app/services/version.service.spec.ts | 16 + src/app/services/version.service.ts | 24 + 15 files changed, 3063 insertions(+), 12 deletions(-) create mode 100644 src/app/routes/spongeffects/spongeffects.component.html create mode 100644 src/app/routes/spongeffects/spongeffects.component.scss create mode 100644 src/app/routes/spongeffects/spongeffects.component.spec.ts create mode 100644 src/app/routes/spongeffects/spongeffects.component.ts create mode 100644 src/app/routes/spongeffects/spongeffects.module.ts create mode 100644 src/app/services/version.service.spec.ts create mode 100644 src/app/services/version.service.ts diff --git a/angular.json b/angular.json index a335e082..72b1f4f9 100644 --- a/angular.json +++ b/angular.json @@ -100,5 +100,8 @@ } } } + }, + "cli": { + "analytics": false } } diff --git a/package-lock.json b/package-lock.json index 556f522d..574bd2ba 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,8 +18,12 @@ "@angular/platform-browser": "^19.0.0", "@angular/platform-browser-dynamic": "^19.0.0", "@angular/router": "^19.0.0", + "@types/d3-color": "^3.1.3", + "d3-color": "^3.1.0", "ngx-bootstrap": "^18.1.3", + "ngx-dropzone": "^3.1.0", "rxjs": "~7.8.0", + "simple-statistics": "^7.8.7", "tslib": "^2.3.0", "zone.js": "~0.15.0" }, @@ -4956,6 +4960,12 @@ "@types/node": "*" } }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "license": "MIT" + }, "node_modules/@types/eslint": { "version": "9.6.1", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", @@ -6834,6 +6844,15 @@ "dev": true, "license": "MIT" }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/date-format": { "version": "4.0.14", "resolved": "https://registry.npmjs.org/date-format/-/date-format-4.0.14.tgz", @@ -10599,6 +10618,15 @@ "rxjs": "^6.5.3 || ^7.4.0" } }, + "node_modules/ngx-dropzone": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/ngx-dropzone/-/ngx-dropzone-3.1.0.tgz", + "integrity": "sha512-5RBaEl07QUcY6sv/BBPyIxN6nbWY/KqTGheEKgbuGS0N1QPFY7NJUo8+X3fYUwQgLS+wjJeqPiR37dd0YNDtWA==", + "deprecated": "This package is deprecated and will no longer receive any updates. Please take a look at the official successor repo at hackingharold/ngx-dropzone", + "dependencies": { + "tslib": "^2.0.0" + } + }, "node_modules/node-addon-api": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-6.1.0.tgz", @@ -12746,6 +12774,15 @@ "node": "^18.17.0 || >=20.5.0" } }, + "node_modules/simple-statistics": { + "version": "7.8.7", + "resolved": "https://registry.npmjs.org/simple-statistics/-/simple-statistics-7.8.7.tgz", + "integrity": "sha512-ed5FwTNYvkMTfbCai1U+r3symP+lIPKWCqKdudpN4NFNMn9RtDlFtSyAQhCp4oPH0YBjWu/qnW+5q5ZkPB3uHQ==", + "license": "ISC", + "engines": { + "node": "*" + } + }, "node_modules/slash": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz", diff --git a/package.json b/package.json index 3aa19d65..339db3bc 100644 --- a/package.json +++ b/package.json @@ -20,8 +20,12 @@ "@angular/platform-browser": "^19.0.0", "@angular/platform-browser-dynamic": "^19.0.0", "@angular/router": "^19.0.0", + "@types/d3-color": "^3.1.3", + "d3-color": "^3.1.0", "ngx-bootstrap": "^18.1.3", + "ngx-dropzone": "^3.1.0", "rxjs": "~7.8.0", + "simple-statistics": "^7.8.7", "tslib": "^2.3.0", "zone.js": "~0.15.0" }, diff --git a/public/favicon.ico b/public/favicon.ico index 57614f9c967596fad0a3989bec2b1deff33034f6..4d50c9e8dd353a88cceb2f889fdaf65bdf68c858 100644 GIT binary patch literal 15406 zcmeG?cU%-#w~Ogb&1cKQs2F>Uu`42o-Pj^xu!4djRYmDrfCYDH_JRo_ETBeBq9(Rj zK|nwgOJX9HXpF{!NWcQ9iEzGi?(EFY&h8Q{$@}BI`7zvcPrrBW-Z^v5mC3ARFUT4; zlwoTl^LbGw>m!rNY;3rG$G2s&G{kl5rl+@;$qJgtWF3(QMNopnV`Jg6;_&(gjgSDl z1e6#j&=9Z^I0OMgS(#J7@EjXQ=4BDb^pF&f6EX8By{KS_0mj5sWmYdEHhl-V-u0gl z(>G0#)y?2D649=~rc9>xOzGyIv~`15c44XS;bSG92l9UMI$Ur)^w%2@k#z<<59Xfh zGTy~euwiUo=AzrMT6^a)OicS62FoL1a6~lp_4b4Q^8&$PK`=z$ssP{I?`wwnhNuJ` z2H=%|Q$>}Usv6Lsj)nKDAqMZr+xHQE3^Df}gQl_uvPQD+1XflK z%HK;tQw6YnS22X8?1qS2cOkUs3dH7Lf|M0`5PKhWqkg}whhR!Te27k-+eEEa*Tacu zjZ@Kb?y(=sHE47G59eU!iq(vl^to-_3BkMfgBE3@Z&$&rbz7mA)70tYdbFo*()%{O zarMb$pQL6kBYt*2AYZFNOMcUsDgm?=X!}4RsB+Fde&zKy#*oa)xqIsCB67Our}8zQ z^j*2?bGJcNd8|qSo;vwq2eK`_qNB53j8*NKFz8=O~<%o?Xu}{AXS;&OQ+6ywa)TPSkNHTDRbv1haGPq zl{at@1$!cd2$Zc;WPgAD1~} zxwHM9wXIs(+Jz0*BosP*mU?^K>f~!9w2Sfwg{rpO1q5a|hAFp=OG>%vmwy73H_9L; z?-I=2bO6S!UR!Kq=O9Nl+&(K-Ofc^hwhZK35ufYG`O`w9HvOf!SjOI6p_i^hOi2|) z{ZYmG;g@fKy6hoDl-$Gi0fb>Zuw>6g2t9WdqVGJy_v}6B=Ql4OE%an2d7FX_3uEW_ zUL772caGkb@mDL+W+mAA&V_-2a%|_ojBoaW-JJPgH+KPao#F}ug2JKOwCOM~ObOkl zx`9If&ZqdG=j7>+-?JTnv4x2Pf+td1^B%qCN0wG_?`(W`M_en1cEd)%^zFN$`_$=R z9}o<^XL&QbJ!i~VoL@D}?t;@7m`@tUSEJ{lc*-D0 z@T4RiEZSZKVIE2Qll-#Z%v9w&rz>(t3%(cwql3;~Hqx;xrxL#Z;x6RvxDSiVsu|7u za3T2Q9s|$)hgtvIEads}F68dK2Z`leOvlgPix^}#jJeK@S& zeHG&LF(ofQJVFD}g}=hcz{tbQCNpVdm(ciWV&#Qx{b^gaKc<+L=2eT&kn}i)Dqzf* zF`)T;v)~u6pW4?OHG;IXG?=|>KS#&47k7*EZ{pGv7_Ul%Q9;p#%s%}?^SbD4evhSn zVjkWv5NT+XCXb6x@tKzW#q`OqmcZ~REd-yrWY)gN?gKD>c@kR}+xB>8jjVog$m!WPiNhAlq~`Zr|RM;&k!&IGCUJhh}JoHK#?pv48vt z+Pl@@yeb8>r*1;rkGJ5hCe6}NJ=>4Sj~W0g_fa7m{UFZ1PLQJOQR_VqT{ zdPf^LH)MnHQ7q4%yTK2?f)n7oaSz0wz71~6(ksw5K4aN9yqO_d0crBAzPQ&r(Yh4R z4U*c8S-R@V!pdq`bf%nr=kV)}?9p#sfqOpfb@cn!;2uH#@N@5v`;ObHOjvyGE@+Qm zgSNH<)u_|tGFgQp4u5CO0ROqGN`g0J)sPJNwV51D>5v_>7VVgeo%AvlY-wgH++S;N^chcEp<(xgQ=oGmj zG8yn{tP%JKY5nkijpQ?wc~NRL;5VgtTMW-jQwOg#^1Qc`Lk}3mFcKEVrwcE%(JAxh zvyO#-sbP6t)0H^`7+Iu51e!>iqX{?xz&!-xB%M0@9kButS&Xh7wkTwg!1QGX=QovE z33D&8QM;^!x$fPkQHwH6%z~^KErv~;k|^Q62M8&a62m4M>V)7m@U5|ag9-zkcfkl_ z5R;${8Vry{7J*}Vt_d&Y2P3!;ng$p-&-7&LrOSNg-9!b{LL#D`U1dH z{!h0ZarE@^cgNdGW`qChc;PhCOg`0RNzfAhsf^Sn^sgD>oOo(;wF{c1ijihvF;YB7 zPl`ubK|}z=3MFt|cqAfq=}OdcUHmV>*G1L8$@4_+JrVP>jN&;OdXB1UA5q;637i6e zn#FMKh6L(Uh;Xtf_|&q3^?wL>M#n6PTf#rzQ9K_jnZf(sz&N@g7``Kn+~llCY)QIC z0<2?@N{=|wIa87E{)PJImrePMF@jnI1lX9xs7E|huhOY<)N8`?u=;9xae8XkCZ&Gi z^2H0f@WZ_`B63~v8)q%%Z0z{&bR!E=K_jBHW1gAjMW;9%m?qC=H8`o1rM{qz11Nve z8*j=Tt6DT^t@3k>d4V>>WK?+d0f_+0Sl$ z*U7H8oYt&^x!;`z%J+tzx(M=5_rciMWzc=v^wJi6`z?FR+PVXp6pe$oUW1IS1?C|wPVbe1e`2_5t@VxjoP-Sd{cdMKA3UUEQ*sg){yL-{8B6Hb}=&& z)2f$mJ_E70x%?aD=KTtev;4W@>@|qK`zQ00@Bt@&#+=!oAir`8?-B^VQp{@9G^WLe zE<;E`5v%hmuVW4f=RjTl5IlArsBGM(-EMTOi&^J*q_KNE)>3QrMx&-fM=e@WLb;^H zMRy?n<^%Ng5sX`v47h%vyUSD<9IRw>9)7tP?B@qV$I&0dAVnk$i;jg(W5!`kKLq;u z2SA5WW04*LJ>1-(r@JTkA3Y^J8+sPb$vSwqOZTdG+IFI6@h0cglQ}FINWFlh+N3n}QbM+pp-E-@!2G1Z=fMY-?xM)^Zbr>)#-Y^D{SARPk1H(JrdJUYI zxw%TZhGTH8j8HCw_9I4vQ_@=W&llPcAIa(;1A@cB#=#LrB`k&RuI_+!18@wMV_hN) zI*xUM89VpGhm)p2yWtW5yT}OA3=T8jU^tay< z{#&u|j0HRR3^cbNLin#p!{;7_^$D6g%4JfWNO`@C{cfp-M16Hx6-3kTLH~QEvmUA& z<-%`<^wOD$gli92Zrs*wHYA)Wg+4CR%h2g&!e*!+fnkREA(>*3(!kco|1O_96@1t4 zIp+J^Uc=WIm|wN>8}h6ccP8kQA^=T}%x6mx@_EUztf-ndUg@uq#;$c{e8o_78r9QpbltN-MGeIIn^AK^H- zxr->d&uVa!GB?4Hh#2t7%4a&3l~lvNLlsQs7}kw;xcu`;(O${S^eK;bG56&O{I_$vlo_U9>Y96 zxBrtrv=_#fVjq8xwEhW~ieuco@_SelxdRTR{RYRrx(h3Ba`h^Ca&Td{nzWRVD-3wSGv)dKLI=dtqu#NT)*)SLLZT2fjK8&5xkX;VVr zKMfi~W9wG9Ub2nG8h6uJ&hJ1zv}*eSG_2nk#!T>q75FaE;u<8KQvY4jHiDza9Ed%6 z1H99}xsARI75uCnz}&hOy*udKQagOFSF@TO?fYnl&V|wbPjF;O1vKf{37Rx%0*Q%< zFfnndk^lTy`F14kr}bWX3B0|%AUG}_$3_)TcVH1McZf|ED=bFIft~829kL zid%kesV3T}OI#~jdq6F!WfOnwA7}^XDf-Ss=z|!qo{7 z;IFjUTs%D~?5q^~x#B$nQLLsxd@XG4v=m?QKCoJtXuqL-oZ`x$pMK$Lg!GK?=J<_& zn=}tWMVBzHI{^{Jcg&8JWZkyy0=MPqZ0&1KUWbkj!>*$%J;)z}yE0k2DsK~hKe*}K zS-q3$GM;!FeFrt|+`X&wXQ_9n&Ly5xb1?bL)&RzpVmd!|>3c-Lqg=xEDps4MIL>ya z57q`tAhDBDpA7p)6cH^P=(pDT_w( zgzrr*{y|%uZ#cL5z0}B8T3fh9dD{{ntVfj8oYOQuMPv2CHQQdmZmt=!_0TC)tikQ5Bw-L@fL5ZE-lS1J127E zfnhv^!?hP%#yubL{FBbmUw((NQHh|zx@cliInI44bR9YVH}sq4{Z*V*7i`5#NcL&) zzNTr493|GQ7i6e%*VA_?= zUk#c!cNrv)JP}%Orh4_(3lRT1cjiaze>i@xOxwI2T#_?j^4bk}uV?MVwV5z;PZpj- zF`S0_%lj;*7+~|% zPv>v9(fVd?*DTn4e`?5*>d*_nbMFI;IYTa7fnjPb4D<_zfNeRT%=#I^u~r+INDR?v1w_PwD%?kYUNe`}T~sozw}{Qcbz5b^Ff#4sL!M zMg>M6866UHbck=rjt@T?zv#_IO`VbcW_oyD&kT9)CuHc^E^Z+7yT)4iwv^E9cO^r6 zt&z1&N1L9uGg|ks)nFUhx|iK2^_#rg7VWa{S2doK##+BU=BXx$e4%$F(VO8-W1_vA z&fBmH`Tf%5Io_tRzQj5&@HZ{S-(539b2=H=O!b&zIPP`og8$(8cPE`ZhvrteIDYd| zxJIopUQS3+9K`WScqCm;p;@G0>sbJd*<nk literal 15086 zcmd^G33O9Omi+`8$@{|M-I6TH3wzF-p5CV8o}7f~KxR60LK+ApEFB<$bcciv%@SmA zV{n>g85YMFFeU*Uvl=i4v)C*qgnb;$GQ=3XTe9{Y%c`mO%su)noNCCQ*@t1WXn|B(hQ7i~ zrUK8|pUkD6#lNo!bt$6)jR!&C?`P5G(`e((P($RaLeq+o0Vd~f11;qB05kdbAOm?r zXv~GYr_sibQO9NGTCdT;+G(!{4Xs@4fPak8#L8PjgJwcs-Mm#nR_Z0s&u?nDX5^~@ z+A6?}g0|=4e_LoE69pPFO`yCD@BCjgKpzMH0O4Xs{Ahc?K3HC5;l=f zg>}alhBXX&);z$E-wai+9TTRtBX-bWYY@cl$@YN#gMd~tM_5lj6W%8ah4;uZ;jP@Q zVbuel1rPA?2@x9Y+u?e`l{Z4ngfG5q5BLH5QsEu4GVpt{KIp1?U)=3+KQ;%7ec8l* zdV=zZgN5>O3G(3L2fqj3;oBbZZw$Ij@`Juz@?+yy#OPw)>#wsTewVgTK9BGt5AbZ&?K&B3GVF&yu?@(Xj3fR3n+ZP0%+wo)D9_xp>Z$`A4 zfV>}NWjO#3lqumR0`gvnffd9Ka}JJMuHS&|55-*mCD#8e^anA<+sFZVaJe7{=p*oX zE_Uv?1>e~ga=seYzh{9P+n5<+7&9}&(kwqSaz;1aD|YM3HBiy<))4~QJSIryyqp| z8nGc(8>3(_nEI4n)n7j(&d4idW1tVLjZ7QbNLXg;LB ziHsS5pXHEjGJZb59KcvS~wv;uZR-+4qEqow`;JCfB*+b^UL^3!?;-^F%yt=VjU|v z39SSqKcRu_NVvz!zJzL0CceJaS6%!(eMshPv_0U5G`~!a#I$qI5Ic(>IONej@aH=f z)($TAT#1I{iCS4f{D2+ApS=$3E7}5=+y(rA9mM#;Cky%b*Gi0KfFA`ofKTzu`AV-9 znW|y@19rrZ*!N2AvDi<_ZeR3O2R{#dh1#3-d%$k${Rx42h+i&GZo5!C^dSL34*AKp z27mTd>k>?V&X;Nl%GZ(>0s`1UN~Hfyj>KPjtnc|)xM@{H_B9rNr~LuH`Gr5_am&Ep zTjZA8hljNj5H1Ipm-uD9rC}U{-vR!eay5&6x6FkfupdpT*84MVwGpdd(}ib)zZ3Ky z7C$pnjc82(W_y_F{PhYj?o!@3__UUvpX)v69aBSzYj3 zdi}YQkKs^SyXyFG2LTRz9{(w}y~!`{EuAaUr6G1M{*%c+kP1olW9z23dSH!G4_HSK zzae-DF$OGR{ofP*!$a(r^5Go>I3SObVI6FLY)N@o<*gl0&kLo-OT{Tl*7nCz>Iq=? zcigIDHtj|H;6sR?or8Wd_a4996GI*CXGU}o;D9`^FM!AT1pBY~?|4h^61BY#_yIfO zKO?E0 zJ{Pc`9rVEI&$xxXu`<5E)&+m(7zX^v0rqofLs&bnQT(1baQkAr^kEsk)15vlzAZ-l z@OO9RF<+IiJ*O@HE256gCt!bF=NM*vh|WVWmjVawcNoksRTMvR03H{p@cjwKh(CL4 z7_PB(dM=kO)!s4fW!1p0f93YN@?ZSG` z$B!JaAJCtW$B97}HNO9(x-t30&E}Mo1UPi@Av%uHj~?T|!4JLwV;KCx8xO#b9IlUW zI6+{a@Wj|<2Y=U;a@vXbxqZNngH8^}LleE_4*0&O7#3iGxfJ%Id>+sb;7{L=aIic8 z|EW|{{S)J-wr@;3PmlxRXU8!e2gm_%s|ReH!reFcY8%$Hl4M5>;6^UDUUae?kOy#h zk~6Ee_@ZAn48Bab__^bNmQ~+k=02jz)e0d9Z3>G?RGG!65?d1>9}7iG17?P*=GUV-#SbLRw)Hu{zx*azHxWkGNTWl@HeWjA?39Ia|sCi{e;!^`1Oec zb>Z|b65OM*;eC=ZLSy?_fg$&^2xI>qSLA2G*$nA3GEnp3$N-)46`|36m*sc#4%C|h zBN<2U;7k>&G_wL4=Ve5z`ubVD&*Hxi)r@{4RCDw7U_D`lbC(9&pG5C*z#W>8>HU)h z!h3g?2UL&sS!oY5$3?VlA0Me9W5e~V;2jds*fz^updz#AJ%G8w2V}AEE?E^=MK%Xt z__Bx1cr7+DQmuHmzn*|hh%~eEc9@m05@clWfpEFcr+06%0&dZJH&@8^&@*$qR@}o3 z@Tuuh2FsLz^zH+dN&T&?0G3I?MpmYJ;GP$J!EzjeM#YLJ!W$}MVNb0^HfOA>5Fe~UNn%Zk(PT@~9}1dt)1UQ zU*B5K?Dl#G74qmg|2>^>0WtLX#Jz{lO4NT`NYB*(L#D|5IpXr9v&7a@YsGp3vLR7L zHYGHZg7{ie6n~2p$6Yz>=^cEg7tEgk-1YRl%-s7^cbqFb(U7&Dp78+&ut5!Tn(hER z|Gp4Ed@CnOPeAe|N>U(dB;SZ?NU^AzoD^UAH_vamp6Ws}{|mSq`^+VP1g~2B{%N-!mWz<`)G)>V-<`9`L4?3dM%Qh6<@kba+m`JS{Ya@9Fq*m6$$ zA1%Ogc~VRH33|S9l%CNb4zM%k^EIpqY}@h{w(aBcJ9c05oiZx#SK9t->5lSI`=&l~ z+-Ic)a{FbBhXV$Xt!WRd`R#Jk-$+_Z52rS>?Vpt2IK<84|E-SBEoIw>cs=a{BlQ7O z-?{Fy_M&84&9|KM5wt~)*!~i~E=(6m8(uCO)I=)M?)&sRbzH$9Rovzd?ZEY}GqX+~ zFbEbLz`BZ49=2Yh-|<`waK-_4!7`ro@zlC|r&I4fc4oyb+m=|c8)8%tZ-z5FwhzDt zL5kB@u53`d@%nHl0Sp)Dw`(QU&>vujEn?GPEXUW!Wi<+4e%BORl&BIH+SwRcbS}X@ z01Pk|vA%OdJKAs17zSXtO55k!;%m9>1eW9LnyAX4uj7@${O6cfii`49qTNItzny5J zH&Gj`e}o}?xjQ}r?LrI%FjUd@xflT3|7LA|ka%Q3i}a8gVm<`HIWoJGH=$EGClX^C0lysQJ>UO(q&;`T#8txuoQ_{l^kEV9CAdXuU1Ghg8 zN_6hHFuy&1x24q5-(Z7;!poYdt*`UTdrQOIQ!2O7_+AHV2hgXaEz7)>$LEdG z<8vE^Tw$|YwZHZDPM!SNOAWG$?J)MdmEk{U!!$M#fp7*Wo}jJ$Q(=8>R`Ats?e|VU?Zt7Cdh%AdnfyN3MBWw{ z$OnREvPf7%z6`#2##_7id|H%Y{vV^vWXb?5d5?a_y&t3@p9t$ncHj-NBdo&X{wrfJ zamN)VMYROYh_SvjJ=Xd!Ga?PY_$;*L=SxFte!4O6%0HEh%iZ4=gvns7IWIyJHa|hT z2;1+e)`TvbNb3-0z&DD_)Jomsg-7p_Uh`wjGnU1urmv1_oVqRg#=C?e?!7DgtqojU zWoAB($&53;TsXu^@2;8M`#z{=rPy?JqgYM0CDf4v@z=ZD|ItJ&8%_7A#K?S{wjxgd z?xA6JdJojrWpB7fr2p_MSsU4(R7=XGS0+Eg#xR=j>`H@R9{XjwBmqAiOxOL` zt?XK-iTEOWV}f>Pz3H-s*>W z4~8C&Xq25UQ^xH6H9kY_RM1$ch+%YLF72AA7^b{~VNTG}Tj#qZltz5Q=qxR`&oIlW Nr__JTFzvMr^FKp4S3v*( diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 93baaebd..b746a05b 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -11,5 +11,5 @@ import {RouterLink, RouterOutlet} from "@angular/router"; }) export class AppComponent { title = 'SPONGE-web-frontend'; - subpages = ['Browse', 'Documentation', 'Download']; + subpages = ['Browse', 'SpongEffects', 'Documentation', 'Download']; } diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts index dc0e8df6..24f82646 100644 --- a/src/app/app.routes.ts +++ b/src/app/app.routes.ts @@ -10,6 +10,8 @@ import { } from "./routes/documentation/browse-functionalities/browse-functionalities.component"; import {BrowseSidebarComponent} from "./routes/documentation/browse-sidebar/browse-sidebar.component"; import {MoreComponent} from "./routes/documentation/more/more.component"; +import {SpongEffectsComponent} from "./routes/spongeffects/spongeffects.component"; + export const routes: Routes = [ { @@ -54,6 +56,10 @@ export const routes: Routes = [ path: 'download', component: DownloadComponent }, + { + path: 'spongeffects', + component: SpongEffectsComponent + }, { path: '**', redirectTo: '' diff --git a/src/app/interfaces.ts b/src/app/interfaces.ts index b35cca48..593c3ebb 100644 --- a/src/app/interfaces.ts +++ b/src/app/interfaces.ts @@ -1,9 +1,21 @@ export interface Dataset { - "data_origin": string, - "dataset_ID": number, - "disease_name": string, - "disease_type": string, - "download_url": string + data_origin: string, + dataset_ID: number, + disease_name: string, + disease_type: string, + download_url: string +} + +export interface DatasetInfo { + dataset_ID: number, + disease_name: string, + data_origin: string, + disease_type: string, + download_url: string, + disease_subtype: string, + study_abbreviation: string, + version: number, + number_of_samples: number } export interface OverallCounts { @@ -13,3 +25,259 @@ export interface OverallCounts { disease_name: string, run_ID: number } + +export enum GeneSorting { + Betweenness = "betweenness", + Degree = "degree", + Eigenvector = "eigenvector" +} + +export enum InteractionSorting { + pAdj = "adjusted p-value", + mScor = "MScor", + Correlation = "Correlation" +} + +export interface Gene { + ensg_number: string, + gene_symbol?: string +} + +export interface CeRNA { + betweenness: number, + eigenvector: number, + gene: Gene, + node_degree: number + run: { + dataset: { + data_origin: string, + dataset_ID: number, + disease_name: string + }, + run_ID: number + } +} + +export interface CeRNAInteraction { + "correlation": number, + "gene1": Gene, + "gene2": Gene, + "mscor": number, + "p_value": number, + "run": { + "dataset": { + "data_origin": string, + "dataset_ID": number, + "disease_name": string + }, + "run_ID": number + } +} + +export interface CeRNAQuery { + disease: Dataset, + geneSorting: GeneSorting, + maxGenes: number, + minDegree: number, + minBetweenness: number, + minEigen: number, + interactionSorting: InteractionSorting, + maxInteractions: number, + maxPValue: number, + minMScore: number +} + +export interface CeRNAExpression { + "dataset": string, + "expr_value": number, + "gene": Gene, + "sample_ID": string +} + +export interface TranscriptExpression { + "dataset": string, + "transcript": string, + "expr_value": number, + "gene": { + "ensg_number": string, + "gene_symbol": Gene + }, + "sample_ID": string, +} + +export interface SurvivalRate { + "dataset": string, + "gene": Gene, + "overexpression": number, + "patient_information": { + "disease_status": number, + "sample_ID": string, + "survival_time": number + } +} + +export interface SurvivalPValue { + "dataset": string, + "gene": Gene, + "pValue": number +} + +export interface GeneCount { + "count_all": number, + "count_sign": number, + "gene": { + "ensg_number": string, + "gene_symbol": string + }, + "run": { + "dataset": { + "data_origin": string, + "dataset_ID": number, + "disease_name": string + }, + "run_ID": number + } +} + +// from spongEffects +// route responses + +export interface RunPerformance { + model_type: string, + split_type: string, + accuracy: number, + kappa: number, + accuracy_lower: number, + accuracy_upper: number, + accuracy_null: number, + accuracy_p_value: number, + mcnemar_p_value: number +} + +export interface RunClassPerformance { + prediction_class: string; + sensitivity: number; + specificity: number; + pos_pred_value: number; + neg_pred_value: number; + precision_value: number; + recall: number; + f1: number; + prevalence: number; + detection_rate: number; + detection_prevalence: number; + balanced_accuracy: number; + spongEffects_run: { + model_type: string; + split_type: string; + }; +} + +export interface EnrichmentScoreDistributions { + prediction_class: string; + enrichment_score: number; + density: number; +} + +export interface SpongEffectsGeneModules { + ensg_number: string; + gene_symbol: string; + mean_gini_decrease: number; + mean_accuracy_decrease: number; +} + +export interface SpongEffectsGeneModuleMembers { + hub_ensg_number: string; + hub_gene_symbol: string; + member_ensg_number: string; + member_gene_symbol: string; +} + +export interface SpongEffectsTranscriptModules { + enst_number: string; + gene: { + ensg_number: string; + gene_symbol: string; + }; + mean_gini_decrease: number; + mean_accuracy_decrease: number; +} + +export interface SpongEffectsTranscriptModuleMembers { + hub_enst_number: string; + hub_gene: { + ensg_number: string; + gene_symbol: string; + }; + member_enst_number: string; + member_gene: { + ensg_number: string; + gene_symbol: string; + }; +} + +export interface PredictCancerType { + meta: { + runtime: number; + level: string; + n_samples: number; + type_predict: string; + subtype_predict: string; + }; + data: { + sampleID: string; + typePrediction: string; + subtypePrediction: string; + }[]; +} + +// other interfaces for spongEffects + +export interface Metric { + name: string, + split: string + lower: number, + upper: number, + level: number +} + +export interface SelectElement { + value: string, + viewValue: string +} + +export interface CancerInfo { + text: string[], + link: string; +} + +export interface PlotData { + x: number[], + y: number[] +} + +export interface PlotlyData { + data: any, + layout?: any, + config?: any +} + +export interface Tab extends SelectElement { + icon: string +} + +export interface LinearRegression { + slope: number, + x0: number +} + +export interface ExampleExpression { + id: string; + sample1: number; + sample2: number; + sample3: number; + sample4: number; + sampleN: number; +} + + diff --git a/src/app/routes/spongeffects/spongeffects.component.html b/src/app/routes/spongeffects/spongeffects.component.html new file mode 100644 index 00000000..62e6dda9 --- /dev/null +++ b/src/app/routes/spongeffects/spongeffects.component.html @@ -0,0 +1,500 @@ +<--
+
+ +

+ Explore spongEffects on TCGA projects and custom expressions +

+
+

+ + + info What is spongEffects? + + General information about spongEffects on TCGA and custom usage + + +
+

spongEffects can be used for downstream analysis of SPONGE ceRNA networks and is capable of tumor sub-type classification and biomarker discovery via a random forest machine learning approach.

+

For more detailed information refer to the publication.

+

We trained spongEffects prediction models on the TCGA pan-cancer dataset and nine other TCGA projects that feature sufficient cancer subtyping for classification.

+

+ The {{tabs[0].viewValue}} tab below shows the spongEffects prediction results for a selected TCGA project. + You can browse the hub-nodes that spongEffects models views as potential biomarkers and compare their gene/transcript expression. +

+

+ We can use the pretrained TCGA models to classify tumor (sub-)types and predict biomarkers on new independent input data that can be uploaded via the + {{tabs[1].viewValue}} tab below. +

+

The model will predict a tumor type for every sample in the uploaded expression file individually.

+
+
+
+

Explore TCGA results or predict on custom data

+

+ + + + + + +
+ + + Select a project: + + Choose one of nine processed TCGA projects or pan-cancer + +
+
+ + Choose a TCGA project: + + + {{cancer.toString()}} + + + +
+
+
+ + + {{l}} + + +
+
+ + + +
+ +
+
+ +
+

{{getCancerInfoText().text}}

+

Publication: {{getCancerInfoText().link}}

+
+
+ +
+
+
+
+
+
+ + + Results + + View spongEffects predictions for selected project + + + + + + Model performance + + Accuracy of spongEffects modules against random modules + + + + + infoWhat does this mean? + +
+

+ The models performance is compared against a model that was trained on randomly selected SPONGE centralities. + The model that was trained on the spongEffects modules should outperform the random approach. + Dotted lines represent the overall accuracy for the test split of the data and solid lines show analogously show the training performance. + Each line start (circle) and end (diamond) mark the lower and upper balanced accuracy bounds of a specific model. +

+
+
+
+
+ spongEffects loading spinner +
+
+
+
+

+ + + Class performance + + Class specific performance + + + + + {{p.viewValue}} + + + + + + + infoWhat does this mean? + +
+

This plot shows the performance of the individual class predictions of the model that was trained with the spongEffects central modules against a model with randomly selected modules. We can strengthen the confidence in the models prediction if it is capable of outperforming its random counterpart. Interpretation of predictions should be made with more caution if this it not the case.

+
+
+
+
+ spongEffects loading spinner +
+
+
+
+

+ + + Classification + + Classification through enrichment score distributions + + + + + infoWhat does this mean? + +
+

The spongEffects enrichment score distributions can give an insight into the model capability to differentiate between the given classes.

+
+
+
+
+ spongEffects loading spinner +
+
+
+
+

+ + + Top ceRNA centralities + + View predicted centralities with the highest decrease in Gini index. + + + + + infoWhat does this mean? + +
+

The mean decrease in the Gini coefficient tells us how much each variable affects the consistency of nodes and leaves in a random forest. If the mean decrease accuracy or mean decrease Gini score is higher, it means the variable is more important in the model.

+

Module centralities that are at the top right of the plot below play a crucial role in distinguishing between the various cancer (sub-)types.

+
+
+
+ + + Select how many top rated modules you would like to include in the expression analysis and press the 'Investigate expressions' button or manually select the modules by clicking on them in the plot. + + +
+ spongEffects loading spinner +
+
+
+
+
+ +
+ + Number of top rated centralities to include + + Please provide a number between 3 and 20 + +
+
+ +
+ + Number of top rated centralities to display + + Please provide a number between 3 and 20 + +
+
+ + + + + + Include module members + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + +
ID{{el.ensemblID}}Gene symbol + {{el.symbol }} + + gene-card + + Mean Gini Decrease{{el.meanGiniDecrease}}Mean Accuracy Decrease{{el.meanAccuracyDecrease}}Description{{el.description}}
+
+
+ +
+
+ spongEffects loading spinner +
+
+
+
+ +
+
+ spongEffects loading spinner +
+
+
+
+
+
+
+

+ + + Gene Set Enrichment Analysis + + View GSEA results for the selected disease type. + + + + + infoWhat does this mean? + +
+

Gene Set Enrichment Analysis (GSEA) identifies whether specific sets of genes related to biological functions are enriched in gene expression data.

+

The information from GSEA can help find biological differences between disease (sub-)types

+
+
+
+ +
+
+ + +
+
+

+
+ +
+ + +
1
+ Expression file upload + + Upload gene/transcript expression for cancer type classification + +
+ +
+
+ + +
+ + + + + + + +
{{ displayedColsValueMap.get(column) }}{{ emp[column] }}
+
+
+ + + Drop your expression file here! + + Or click the icon to select a file + + + {{ f.name }} + + +
+
+

+ + + +
2
 Options +
+ + spongEffects prediction parameters + +
+
+
Filtering thresholds:
+ + multiple miRNA sensitivity correlation (mscor) + + Please provide a number between 0 and 10 + + + False discovery rate (FDR) + + Please provide a number between 0 and 0.5 + + + Minimal expression value + + Please provide a number between 0 and 1.000 + +

+
Further settings:
+ + Enrichment method + + {{method}} + + + + Minimum module size + + Please provide a number between 0 and 5.000 + + + Maximum module size + + Please provide a number between 0 and 5.000 + + + Apply log2 scaling factor to uploaded expression + + + Predict on subtype level (can lead to longer run times) + +
+
+
+ + + +
+
+
+ Estimated run time: {{ estimatedRunTimeText()}} +
+
+
+
+
+
+
+
+
+
+
+
+
+
1.0
+
0.5
+
+
Overall class model accuracy
+
+
+
+
+
+

+
+ Predicted type over all samples: {{this.predictedType}} +
+
+
+
+
+
+
+
--> diff --git a/src/app/routes/spongeffects/spongeffects.component.scss b/src/app/routes/spongeffects/spongeffects.component.scss new file mode 100644 index 00000000..47bf402e --- /dev/null +++ b/src/app/routes/spongeffects/spongeffects.component.scss @@ -0,0 +1,429 @@ +/* set base container dimensions */ + +#bigBox { + width: auto; + margin: 50px +} + +.container { + margin: 20px; +} + +.circle { + width: 40px; + height: 40px; + line-height: 40px; + border-radius: 50%; + font-size: 20px; + color: white; + text-align: center; + background: #4892b9; +} + +.sidebar { + min-width: 150px; + max-width: 150px; + background-color: #e7e7e7; +} + +.sidebar-tabs { + display: block; + min-width: 150px; + max-width: 150px; +} + +.sidebar-tab { + font-size: 24px; + font-weight: normal; + margin-bottom: 10px; + margin-top: 10px; + margin-right: 5px; + margin-left: 5px; + background-color: #9ccee8; +} + +#headline{ + text-align:center; +} + +.card-body { + img { + height: auto; + } +} + +/* cards */ +.mat-card-header { + width: 100%; +} + +.mat-card-header-text { + margin: 0 !important; +} + +.mat-card-title { + text-align: left; +} + +.mat-card-subtitle { + text-align: left; +} + +.mat-card-content { + margin-left: 10px; +} + +// info card +.info-card { + background-color: #9ccee8; +} + +.info-card-small { + background-color: #9ccee8; + font-size: 18px; + margin-bottom: 10px; +} + +.info-text-small-div{ + font-size: 16px; +} + +.info-list { + list-style-type: none; +} + +.spongEffects-workflow-img { + align-self: center; +} + +/* expansion panels */ +.mat-expansion-panel-header { + +} + +.mat-expansion-panel-header-title { + width: 250px !important; + margin-left: 8px; + font-size: 20px; + font-weight: 500; +} + +.mat-expansion-panel-header-description { + margin-left: auto; + width: 600px !important; +} + +.explore-card { + background-color: lightgrey; +} + +.level-toggle { + float: right; + margin-left: auto; + margin-right: 20px; +} + +.level-toggle-button { + background-color: white; +} + +.publication-link { + color: #366080; + font-style: italic; +} + +.predict-card { + background-color: lightgrey; +} + +.cancer-select-card { + height: 700px; +} + +.cancer-select { + float: left; + margin-left: auto; + margin-right: 60px; + width: 400px; +} + +.mat-select-trigger { + min-width: 500px; +} + +.cancer-select-input { + width: 500px; + max-height: 600px; +} + +.subtype-header { + text-align: left; + font-weight: bold; +} + +.result-panel { + background-color: #dfdfe0; +} + +.result-accordion .mat-expansion-panel-header-description{ + justify-content: space-between; + align-items: center; +} + +.sample-distribution-pie { + width: 100%; + height: 100%; +} + +.overall-acc-plot-div { + height: 200px; + width: 100%; +} + +#overall-acc { + height: 200px; + width: 100%; +} + +.class-acc-plot-div { + height: 400px; + width: 100%; + overflow-y: auto; +} + +.class-acc-plot { + height: 100%; + width: 100%; +} + +.measure-select-input { + float: right; + margin-left: auto; +} + +.loading-spinner { + display: flex; + justify-content: center; + align-items: flex-start; + height: 100vh; +} + +.loading-spinner img { + animation: changeColor 2s linear infinite alternate; + transition: filter 1s linear; + max-width: 100px; + max-height: 100px; +} + +@keyframes changeColor { + 0% { + filter: hue-rotate(0deg); /* Starting color */ + } + 100% { + filter: hue-rotate(180deg); /* Ending color */ + } +} + +.enrichmentPlotHolderDiv { + height: 300px; + width: 100%; + overflow-y: auto; +} + +.enrichmentPlotHolder { + width: 100%; +} +// box for cancer selection +.cancer-select-box { + min-height: 300px; + margin-left: 10px; +} + +// cancer description +.cancer-information { + height: 250px; + float: left; +} +// cancer image +.cancer-image { + width: 100%; + height: 500px; + text-align: center; +} + +.cancer-image img { + max-height: 100%; + max-width: 100%; + display: block; + margin: 0 auto; +} + +// lollipop plot +.score-select { + margin-left: 20px; + margin-right: auto; + float: left; + width: 300px; +} + +.score-select-input { + width: 200px; +} + +.top-select { + width: 200px; +} + +.select-top-n { + width: 200px; +} + +.top-ceRNA-selection-div { + width: 200px; +} + +.top-centralities-plot { + height: 300px; + width: 100%; +} + +table { + width: 100%; + min-height: 200px; + max-height: 200px; + overflow-y: auto; +} + +.modules-table { + width: 100%; + max-height: 300px; + overflow-y: auto; + border-color: lightgrey; + border-width: 2px; +} + +.mat-cell, .mat-header-cell { + text-align: center; + vertical-align: middle; +} + +.module-expression-heatmap { + width: 100%; + height: 300px; + align-items: center; + justify-content: center; +} + +.heatmaps { + overflow: auto; +} + +// PREDICT TAB + +.example-expression-button-div { + float: right; + margin-left: auto; + margin-right: 20px; +} + +.example-expression-button { + background-color: #4892b9; + color: white; +} + +.example-expression-table-div { + width: 100%; + height: auto; +} + +.example-expression-table { + width: 100%; + height: auto; +} + +.mat-form-field + .mat-form-field { + margin-left: 8px; +} + +.run-button-div { + min-width: 100%; + max-width: 100%; +} + +.run-button { + background-color: #0b608c; + min-width: 80%; + max-width: 80%; + margin: 5px; + color: white; +} + +.run-button-predict { + background-color: #4892b9; + min-width: 100%; + max-width: 100%; + margin: 5px; + color: white; +} + +.type-prediction-div { + width: 100%; + height: 400px; + flex-direction: row; +} + +.type-prediction { + display: flex; + width: 80%; + float: left; + height: 400px; + overflow-y: auto; +} + +.type-legend { + width: 15%; + float: right; +} + +.type-legend { + margin-top: 10px; + margin-right: 10px; +} + +.legend { + display: flex; + flex-direction: column; +} + +.legend-item { + display: flex; + align-items: center; + margin-bottom: 5px; +} + +.legend-text { + font-size: 14px; +} + +.child { + font-size: 12px; + display: flex; +} + +.legend-labels { + margin-right: 5px; + display: flex; + flex-direction: column; +} + +.child:not(:last-child) { + margin-bottom: 60px; /* Adjust the margin between child elements */ +} + +.percent-color-box { + width: 30px; + height: 80px; + margin-right: 10px; + background: linear-gradient(to top, darkorange, #008c00); + text-orientation: sideways; +} diff --git a/src/app/routes/spongeffects/spongeffects.component.spec.ts b/src/app/routes/spongeffects/spongeffects.component.spec.ts new file mode 100644 index 00000000..740ea223 --- /dev/null +++ b/src/app/routes/spongeffects/spongeffects.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { SpongEffectsComponent } from './spongeffects.component'; + +describe('SpongEffectsComponent', () => { + let component: SpongEffectsComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [SpongEffectsComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(SpongEffectsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/routes/spongeffects/spongeffects.component.ts b/src/app/routes/spongeffects/spongeffects.component.ts new file mode 100644 index 00000000..05cd2194 --- /dev/null +++ b/src/app/routes/spongeffects/spongeffects.component.ts @@ -0,0 +1,1460 @@ +import {AfterViewInit, Component, ElementRef, OnInit, Renderer2, ViewChild} from '@angular/core'; +import {FormControl, FormsModule, Validators} from '@angular/forms'; +import {MatTableDataSource} from '@angular/material/table'; +import {max, min, sum} from 'simple-statistics'; +import * as d3 from 'd3-color'; +import {BackendService} from "../../services/backend.service"; +// import {Helper} from '../../helper'; +import {ProgressBarMode} from '@angular/material/progress-bar'; +import {DataSource} from '@angular/cdk/collections'; +import {Observable, ReplaySubject, timer} from 'rxjs'; +import {MatOption, ThemePalette} from '@angular/material/core'; +import { + EnrichmentScoreDistributions, + CeRNAExpression, + TranscriptExpression, + ExampleExpression, + LinearRegression, + Metric, + PlotData, + PlotlyData, + RunPerformance, + SelectElement, + SpongEffectsGeneModuleMembers, + SpongEffectsGeneModules, + SpongEffectsTranscriptModuleMembers, + SpongEffectsTranscriptModules, + Tab, + CancerInfo +} from '../../interfaces'; +import {MatCard, MatCardContent, MatCardHeader} from "@angular/material/card"; +import {MatFormField} from "@angular/material/form-field"; +import {MatSelect} from "@angular/material/select"; +import {MatButtonToggle, MatButtonToggleGroup} from "@angular/material/button-toggle"; +import {MatGridList, MatGridTile} from "@angular/material/grid-list"; +import {MatTooltip} from "@angular/material/tooltip"; +import {NgForOf} from "@angular/common"; +import {MatCheckbox} from "@angular/material/checkbox"; +import {MatIcon} from "@angular/material/icon"; +import {MatSidenavContainer} from "@angular/material/sidenav"; +import {MatAccordion} from "@angular/material/expansion"; +import {MatExpansionPanel} from "@angular/material/expansion"; +import {MatExpansionPanelDescription} from "@angular/material/expansion"; +import {MatExpansionPanelTitle} from "@angular/material/expansion"; +import { MatButtonModule } from '@angular/material/button'; +import { MatCheckboxModule } from '@angular/material/checkbox'; +import { MatExpansionModule } from '@angular/material/expansion'; +import { MatFormFieldModule } from '@angular/material/form-field'; +import { MatInputModule } from '@angular/material/input'; +import { MatProgressBarModule } from '@angular/material/progress-bar'; +import { MatSelectModule } from '@angular/material/select'; +import { MatSidenavModule } from '@angular/material/sidenav'; +import { MatTooltipModule } from '@angular/material/tooltip'; +import { MatButtonToggleModule } from '@angular/material/button-toggle'; +import { MatGridListModule } from '@angular/material/grid-list'; +import { NgxDropzoneModule } from 'ngx-dropzone'; +import { MatTableModule } from '@angular/material/table'; + +declare var Plotly: any; + +export class Cancer { + value: string; + viewValue: string; + allSubTypes: string[]; + sampleSizes: number[]; + + base: string = "https://portal.gdc.cancer.gov/projects/TGCA-"; + + constructor(value: string, viewValue: string, allSubTypes: string[], sampleSizes: number[], ) { + this.value = value; + this.viewValue = viewValue; + this.allSubTypes = allSubTypes; + this.sampleSizes = sampleSizes; + } + + addSubtype(subtype: string) { + this.allSubTypes.push(subtype); + } + + addSampleSize(sampleSize: number) { + if (sampleSize != null) this.sampleSizes.push(sampleSize); + } + + totalNumberOfSamples() { + return sum(this.sampleSizes.filter(s => s >= 0)); + } + + toString() { + return this.viewValue + " - (" + this.value + ")"; + } + + getUrl() { + return this.base + this.value + } +} + + +export const spongEffectsCancerAbbreviations: string[] = ['PANCAN', 'BRCA', 'CESC', 'ESCA', 'HNSC', 'LGG', 'SARC', 'STAD', 'TGCT', 'UCEC'] + + + +export class ModuleDataSource extends DataSource { + private _dataStream = new ReplaySubject(); + + constructor(initialData: SpongEffectsGeneModules[]) { + super(); + this.setData(initialData); + }sss + + connect(): Observable { + return this._dataStream; + } + + disconnect() {} + + setData(data: SpongEffectsGeneModules[]) { + this._dataStream.next(data); + } +} + + + +const EXAMPLE_GENE_EXPR: ExampleExpression[] = [ + {id: "ENSG00000000233", sample1: 6, sample2: 5, sample3: 8, sample4: 2, sampleN: 1}, + {id: "ENSG00000000412", sample1: 2, sample2: 1, sample3: 2, sample4: 3, sampleN: 4}, + {id: "ENSG00000000442", sample1: 10, sample2: 9, sample3: 8, sample4: 0, sampleN: 7} +] + + + + +@Component({ + selector: 'app-spongeffects', + templateUrl: './spongeffects.component.html', + // imports: [ + // MatCardHeader, + // MatCard, + // MatFormField, + // MatSelect, + // MatOption, + // MatButtonToggleGroup, + // MatButtonToggle, + // MatCardContent, + // MatGridList, + // MatGridTile, + // MatTooltip, + // FormsModule, + // NgForOf, + // MatExpansionPanel, + // MatExpansionPanelTitle, + // MatExpansionPanelDescription, + // MatCheckbox, + // MatIcon, + // MatSidenavContainer, + // MatAccordion, + // MatExpansionPanel, + // MatExpansionPanelDescription, + // MatExpansionPanelTitle, + // MatButtonModule, + // MatCheckboxModule, + // MatExpansionModule, + // MatFormFieldModule, + // MatInputModule, + // MatProgressBarModule, + // MatSelectModule, + // MatSidenavModule, + // MatTooltipModule, + // MatButtonToggleModule, + // MatGridListModule, + // NgxDropzoneModule, + // MatTableModule, + // ], + styleUrls: ['./spongeffects.component.scss'] +}) + +export class SpongEffectsComponent implements OnInit, AfterViewInit { + // API + constructor(private backend: BackendService, private renderer: Renderer2) { + // this.diseases = this.backend.getDatasets(); + // this.overallCounts = this.backend.getOverallCounts(); + this.cancerInfoAvailable = this.initCancerInfo(); + } + + tabs: Tab[] = [ + {value: "explore", viewValue: "Explore", icon: "../../../assets/img/magnifying_glass.png"}, + {value: "predict", viewValue: "Predict", icon: "../../../assets/img/chip-intelligence-processor-svgrepo-com.png"} + ] + selectedTab: Tab = this.tabs[0]; + source: string = "TCGA"; + cancers: Cancer[] = []; + selectedCancer: Cancer | undefined = undefined; + levels: string[] = ["Gene", "Transcript"]; + level: string = this.levels[0]; + exploreResultsQueried: boolean = false; + + performanceMeasures: SelectElement[] = [ + {value: 'balanced_accuracy', viewValue: "Balanced Accuracy"}, + {value: 'detection_prevalence', viewValue: "Detection Prevalence"}, + {value: 'detection_rate', viewValue: "Detection Rate"}, + {value: 'f1', viewValue: "F1"}, + {value: 'neg_pred_value', viewValue: "Negative Prediction Value"}, + {value: 'pos_pred_value', viewValue: "Positive Prediction Value"}, + {value: 'precision_value', viewValue: "Precision"}, + {value: 'prevalence', viewValue: "Prevalence"}, + {value: 'recall', viewValue: "Recall"}, + {value: 'sensitivity', viewValue: "Sensitivity"}, + {value: 'specificity', viewValue: "Specificity"} + ]; + performanceMeasure: SelectElement = this.performanceMeasures[0]; + performanceSelectPanelIsOpen: boolean = false; + includeModuleMembers: boolean = false; + // loading toggles + overallAccuracyLoading: boolean = true; + classPerformanceLoading: boolean = true; + enrichmentScoreDensityLoading: boolean = true; + lollipopLoading: boolean = true; + expressionsLoading: boolean = true; + elementExpressionLoading: boolean = true; + + // plot divs + @ViewChild("sampleDistributionPie") sampleDistributionPieDiv!: ElementRef; + @ViewChild("overallAccuracyPlot") overallAccPlotDiv!: ElementRef; + @ViewChild("classModelPerformancePlot") classPerformancePlotDiv!: ElementRef; + @ViewChild("enrichmentScoresByClassPlot") enrichmentScoresByClassPlotDiv!: ElementRef; + @ViewChild("lollipopPlot") lollipopPlotDiv!: ElementRef; + @ViewChild("moduleExpressionHeatmapDiv") moduleExpressionHeatmap!: ElementRef; + @ViewChild("moduleMiRnaExpressionDiv") moduleMiRnaExpressionPlot!: ElementRef; + + // lollipop plot + modulesData!: Promise; + filteredModulesData!: SpongEffectsGeneModules[]; + + markControl: FormControl = new FormControl(3, [Validators.min(1.0), Validators.max(20)]); + topControl: FormControl = new FormControl(200, [Validators.min(3.0), Validators.max(1000)]); + + selectedModules: SpongEffectsGeneModules[] = []; + modulesTableColumns: string[] = [ + "ensemblID", + "symbol", + "meanGiniDecrease", + "meanAccuracyDecrease", + "description" + ]; + dynamicModulesData: ModuleDataSource = new ModuleDataSource(this.selectedModules); + + selectedEnsemblId: SpongEffectsGeneModules | undefined = undefined; + + // plot parameters + defaultPlotMode: string = "lines+markers"; + defaultLineWidth: number = 6; + defaultMarkerSize: number = 12; + + /* predict variables */ + // plots + @ViewChild("typePredictPie") + typePredictPiePlot!: ElementRef; + predictSubtypes: boolean = false; + logScaling: boolean = true; + + progressBarMode: ProgressBarMode = "determinate"; + progressBarValue: number = 0; + estimatedRunTime: number = 0; + + // loading values + predictionQueried: boolean = false; + predictionLoading: boolean = false; + timerRunning: boolean = false; + // file variables + uploadedExpressionFiles: File[] = []; + filesToAccept: string = "text/*,application/*"; + maxFileSize: number = 100000000; + // prediction data + predictionData: any; + predictionMeta: any; + predictedType: string = "None"; + predictedSubtype: string = "None"; + + // default parameters + mscorDefault: number = 0.1; + fdrDefault: number = 0.05; + minSizeDefault: number = 100; + maxSizeDefault: number = 2000; + minExprDefault: number = 10; + methods: string[] = ["gsva", "ssgsea", "OE"]; + methodDefault: string = this.methods[0]; + showExpressionExample: boolean = false; + + mscorControl: FormControl = new FormControl(this.mscorDefault, [Validators.min(0.0), Validators.max(10.0)]); + fdrControl: FormControl = new FormControl(this.fdrDefault, [Validators.min(0.0), Validators.max(0.5)]); + minExprControl: FormControl = new FormControl(this.minExprDefault, [Validators.min(0.0), Validators.max(1000)]); + minSizeControl: FormControl = new FormControl(this.minSizeDefault, [Validators.min(0.0), Validators.max(5000)]); + maxSizeControl: FormControl = new FormControl(this.maxSizeDefault, [Validators.min(0.0), Validators.max(5000)]); + // TODO: test runtime of other methods + + methodFunctions: Map = new Map( + [ + [this.methods[0], {slope: 0.7, x0: 15}], + [this.methods[1], {slope: 1, x0: 20}], + [this.methods[2], {slope: 2, x0: 20}], + ] + ); + + exampleExpressionData: MatTableDataSource = new MatTableDataSource(EXAMPLE_GENE_EXPR); + displayedCols: string[] = ["id", "sample1", "sample2", "sample3", "sample4", "sampleN"]; + displayedColsValueMap: Map = new Map([ + ["id", ""], + ["sample1", "sample1"], ["sample2", "sample2"], ["sample3", "sample3"], ["sample4", "sample4"], ["sampleN", "sampleN"], + ]); + + cancerInfoAvailable: Promise = new Promise(() => {}); + + ngOnInit(): void { + } + +// /** +// * retrieve TCGA cancer data from API +// */ + private async initCancerInfo() { + let response = await this.backend.getDatasetsInformation(this.source); + let cancerMap: Map = new Map(); + + response.forEach((entry: { + study_abbreviation: string, disease_name: string, disease_subtype: string, + number_of_samples: number + }) => { + // fill cancer map + let cancer = cancerMap.get(entry.study_abbreviation); + if (cancer) { + if (entry.disease_subtype != null) { + cancer.addSubtype(entry.disease_subtype); + cancer.addSampleSize(entry.number_of_samples); + } + } else { + cancerMap.set(entry.study_abbreviation, + new Cancer(entry.study_abbreviation, entry.disease_name, + [entry.disease_subtype], [entry.number_of_samples])); + } + }); + // set PANCAN subtypes + const panCanCancer = cancerMap.get('PANCAN'); + if (!panCanCancer) { + console.error("PANCAN cancer not found in response"); + } else { + panCanCancer.allSubTypes = [...cancerMap.keys()].filter(v => v != 'PANCAN'); + panCanCancer.sampleSizes = [...cancerMap.entries()] + .filter((value) => value[0] != 'PANCAN') + .map(c => c[1].totalNumberOfSamples()) + } + // remove null subtypes + cancerMap.forEach((value) => { + value.allSubTypes = value.allSubTypes.filter(v => v != null); + }); + // set class variable with spongEffects compatible cancer types + this.cancers = [...cancerMap.values()].filter(c => spongEffectsCancerAbbreviations.includes(c.value)); + this.selectedCancer = this.cancers[0]; + } + + ngAfterViewInit() { + if (this.selectedCancer != undefined) { + this.cancerInfoAvailable.then(_ => this.cancerSelected(this.selectedCancer)); + } + } + + setTab(tab: Tab) { + this.selectedTab = tab; + // reset explore tab + if (tab.value == this.tabs[0].value && this.selectedCancer != undefined) { + this.cancerSelected(this.selectedCancer).then(_ => this.exploreResultsQueried = true); + } + } + + setLevel(level: string) { + this.level = level; + if (this.selectedCancer != undefined) { + this.cancerSelected(this.selectedCancer).then(_ => this.exploreResultsQueried = true); + } + } + + + getGeneCardLink(gene: string): string { + return `https://www.genecards.org/cgi-bin/carddisp.pl?gene=${gene}`; + } + + getLevelButtonStyle(level: string): string { + if (level == this.level) { + return "background-color: #1e719b; color: white"; + } else { + return "background-color: white"; + } + } +s + getCancerImage() { + if (this.selectedCancer == undefined) return "../../../assets/img/spongEffects_logo.png"; + return "../../../assets/img/TCGA/" + this.selectedCancer.value + ".png"; + } + + setPreviewCancer(cancer: Cancer) { + this.selectedCancer = cancer; + this.plotSampleDistribution(this.getSampleDistributionData()); + } + + clearResults() { + // allow new queries + this.exploreResultsQueried = false; + // clear plot divs + this.overallAccPlotDiv.nativeElement.innerHTML = ""; + this.enrichmentScoresByClassPlotDiv.nativeElement.innerHTML = ""; + this.lollipopPlotDiv.nativeElement.innerHTML = ""; + } + + async plotResults() { + if (this.exploreResultsQueried) return null; + // get accuracy data from API + const acData: Promise = this.getOverallAccuracyData(); + // show according plot + this.plotOverallAccuracyPlot(acData).then(_ => this.overallAccuracyLoading = false); + // plot class specific performance + this.plotModelClassPerformance().then(_ => this.classPerformanceLoading = false); + // get spongEffect scores from API + const enrichmentScores: Promise> = this.getEnrichmentClassDensities(); + // plot enrichment score class distributions + this.plotEnrichmentScoresByClass(enrichmentScores).then(_ => this.enrichmentScoreDensityLoading = false); + // get top centralities + this.modulesData = this.getMeanGiniDecrease(); + // plot lollipop plot + this.plotLollipop(this.topControl.value).then(_ => this.lollipopLoading = false); + } + + async clearSelection() { + this.selectedModules = []; + this.resetLollipop(); + this.dynamicModulesData.setData(this.selectedModules); + } + + async exploreExpression() { + // plot module expression + this.getModulesExpression() + .then(config => this.plotModuleExpression(config)) + .then(_ => this.expressionsLoading = false); + } + + async cancerSelected(cancer: Cancer) { + this.clearResults(); + this.selectedCancer = cancer; + this.selectedEnsemblId = undefined; + // load sample distribution + let sampleDistData: PlotlyData = this.getSampleDistributionData(); + // plot sample distribution pie + this.plotSampleDistribution(sampleDistData); + this.plotResults().then(_ => this.exploreResultsQueried = true); + } + + async resetLollipop() { + this.plotLollipop(this.topControl.value).then(_ => this.lollipopLoading = false); + } + + getRandomID(l: number): string { + let result: string = 'TCGA-'; + const characters: string = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; + const charactersLength: number = characters.length; + let counter: number = 0; + while (counter < l) { + result += characters.charAt(Math.floor(Math.random() * charactersLength)); + counter += 1; + } + return result; + } + + getRandomIDs(n: number, l: number): string[] { + return Array.from(Array(n).keys()).map(i => this.getRandomID(l)); + } + + getRandomElements(n: number, type: string) { + let base: string = "ENS"; + let l: number = 4; + if (type == "gene") { + base += "G"; + } else if (type == "transcript") { + base += "T" + } else if (type == "mirna") { + base = "hsa_mir_" + } else { + throw new Error("type argument has to be either gene or transcript"); + } + base+="000000" + return Array.from(Array(n).keys()).map(i => base+this.getRandomValues(l, 0, 9, true).join("")); + } + + getRandomValue(min: number = 0, max: number = 1, round: boolean = false): number { + const randomValue: number = Math.random(); + return round ? Math.floor((max - (min)) * randomValue + (min)): (max - (min)) * randomValue + (min); + } + + getRandomValues(n: number, min: number = 0, max: number = 1, round: boolean = false): number[] { + return Array.from(Array(n).keys()).map(i => this.getRandomValue(min, max, round)); + } + + getSampleDistributionData(): PlotlyData { + if (this.selectedCancer == undefined) { + return {data: [], layout: {}, config: {}}; + } + let data = [ + { + values: this.selectedCancer.sampleSizes, + labels: this.selectedCancer.allSubTypes, + type: "pie", + hoverinfo: 'label+value+percent', + textinfo: 'none' + } + ]; + let layout = { + autosize: true, + showlegend: true, + legend: {"orientation": "h"}, + margin: { + b: 5, + l: 5, + r: 5, + t: 50 + }, + title: { + text: "Sample distribution of " + this.selectedCancer.value + " (" + this.selectedCancer.totalNumberOfSamples() + " samples)", + font: { + size: 14 + } + } + }; + let config = { + responsive: true + } + return {data: data, layout: layout, config: config}; + } + + async getOverallAccuracyData(): Promise { + this.overallAccuracyLoading = true; + if (this.selectedCancer == undefined) { + return []; + } + const modelPerformances = await this.backend.getRunPerformance(this.selectedCancer.viewValue, this.level); + return modelPerformances.map((entry: RunPerformance, idx: number): Metric => { + return { + name: entry.model_type, + split: entry.split_type, + lower: entry.accuracy_lower, + upper: entry.accuracy_upper, + level: idx + } + }); + } + + async cancelClick(event: any) { + event.stopPropagation(); + } + + async rePlotModelClassPerformance(): Promise { + this.plotModelClassPerformance().then(_ => this.classPerformanceLoading = false); + } + + async plotModelClassPerformance(): Promise { + this.classPerformanceLoading = true; + if (this.selectedCancer == undefined) { + return null; + } + // get data + const performanceData = await this.backend.getRunClassPerformance(this.selectedCancer.viewValue, this.level); + // transform data + const data: PlotlyData = await this.getModelClassPerformancePlotData(performanceData); + // plot data + Plotly.newPlot(this.classPerformancePlotDiv.nativeElement, data.data, data.layout, data.config); + } + + async togglePanel(event: MouseEvent, panel: MatExpansionPanel) { + event.stopPropagation(); + if (this.performanceSelectPanelIsOpen) { + panel.open() + this.resetClassAccPlot(); + } else { + panel.close() + } + } + + async getModelClassPerformancePlotData(performanceData: any[]): Promise { + // group the data by model type + const traceGroups: { [key: string]: any[] } = {}; + performanceData.forEach(entry => { + const modelType = entry.spongEffects_run.model_type; + if (!traceGroups[modelType]) { + traceGroups[modelType] = []; + } + traceGroups[modelType].push(entry); + }); + // build actual traces + const traces = []; + for (const modelType in traceGroups) { + if (traceGroups.hasOwnProperty(modelType)) { + const group = traceGroups[modelType]; + + const trace = { + x: group.map(entry => entry.prediction_class), + y: group.map(entry => entry[this.performanceMeasure.value]), + type: 'bar', + name: modelType, + }; + + traces.push(trace); + } + } + const meanTextLength: number = Math.round(sum(traces[0].x.map(d => d.length))/traces[0].x.length); + const textPad: number = meanTextLength*10.5; + const containerWidth = this.renderer.selectRootElement(this.classPerformancePlotDiv.nativeElement).offsetWidth; + const angle: number = meanTextLength > 15 ? 90: 0; + const layout = { + barmode: 'group', + autosize: true, + width: containerWidth, + xaxis: { + autosize: true, + tickangle: angle + }, + yaxis: { + title: this.performanceMeasure.viewValue + }, + margin: { + t: 25, + b: textPad, + l: 50, + r: 0 + }, + legend: { + orientation: "h", + x: 0.5, + y: 1.25 + } + }; + const config = { responsive: true }; + return {data: traces, layout: layout, config: config}; + } + + async resetClassAccPlot() { + Plotly.update(this.classPerformancePlotDiv.nativeElement); + } + + private async getEnrichmentClassDensities(): Promise> { + this.enrichmentScoreDensityLoading = true; + if (this.selectedCancer == undefined) { + return new Map(); + } + const queryResponse: EnrichmentScoreDistributions[] = await this.backend.getEnrichmentScoreDistributions(this.selectedCancer.viewValue, this.level); + const classDensities: Map = new Map(); + queryResponse.forEach((entry: { prediction_class: string; enrichment_score: any; density: any; }) => { + if (classDensities.has(entry.prediction_class) && classDensities.get(entry.prediction_class) != undefined) { + const pred_class: PlotData = classDensities.get(entry.prediction_class); + pred_class.x.push(entry.enrichment_score) + pred_class.y.push(entry.density) + } else { + classDensities.set(entry.prediction_class, { + x: [entry.enrichment_score], y: [entry.density] + }); + } + }); + return classDensities; + } + + async getMeanGiniDecrease(): Promise { + if (this.selectedCancer == undefined) { + return []; + } + this.lollipopLoading = true; + return await this.backend.getSpongEffectsGeneModules(this.selectedCancer.viewValue); + } + + async getModuleMembers(): Promise> { + if (this.selectedCancer == undefined || this.selectedModules.length == 0) { + return new Map(); + } + const members: Map = new Map(); + if (this.level == 'gene') { + const response: SpongEffectsGeneModuleMembers[] = await this.backend.getSpongEffectsGeneModuleMembers( + this.selectedCancer.viewValue, + this.selectedModules.map(s => s.ensg_number) + ) + const key: string = "hub_ensg_number"; + response.forEach((e: SpongEffectsGeneModuleMembers) => { + if (!members.has(e[key])) members.set(e[key], []); + const toPush: string[] = [e.member_ensg_number, e.member_gene_symbol] + const memberList = members.get(e[key]); + if (memberList) { + memberList.push(...toPush); + } + }); + } else { + // TODO + // const response: SpongEffectsTranscriptModuleMembers[] = await this.backend.getSpongEffectsTranscriptModuleMembers( + // this.selectedCancer.viewValue, + // this.selectedModules.map(s => s.enst_number) + // ) + } + return members; + } + + async getModulesExpression(): Promise { + // TODO + // this.expressionsLoading = true; + // let apiCall: any; + // let key: string; + // let elements: string[] = this.selectedModules.map(s => s.ensg_number); + // // include module members in heatmap + // if (this.includeModuleMembers) { + // const members: Map = await this.getModuleMembers(); + // const memberValues: string[] = [].concat([...members.values()]); + // elements.push(...memberValues); + // } + // if (this.level.toLowerCase() == "gene") { + // key = "ensg_number"; + // apiCall = this.backend.getCeRNAExpression(this.selectedCancer.viewValue, + // undefined, elements, undefined, this.level); + // } else { + // key = "enst_number"; + // apiCall = this.backend.getTranscriptExpression(this.selectedCancer.viewValue, + // elements, undefined, undefined, this.level); + // } + // // await apiCall + // const apiResponse = await apiCall; + // // split into (sub-)types + // const typeSplit: Map = new Map(); + // apiResponse.forEach(entry => { + // const dataset: string = entry.dataset; + // if (!typeSplit.has(dataset) || typeSplit.get(dataset) == undefined) { + // typeSplit.set(dataset, []); + // } + // typeSplit.get(dataset).push(entry); + // }); + // + // // build traces for each dataset + // let data: any[] = []; + // typeSplit.forEach((entry: any[], dataset: string) => { + // // transform data of entries + // let xSamples: string[] = []; + // const xSet: Set = new Set(); + // let nX: number = -1; + // let yElements: string[] = []; + // const ySet: Set = new Set(); + // let nY: number = -1; + // let zValues: number[][] = [[]]; + // + // entry.forEach(e => { + // if (!xSet.has(e.sample_ID)) { + // xSamples.push(e.sample_ID); + // nX += 1; + // } + // if (!ySet.has(e.gene[key])) { + // yElements.push(e.gene.gene_symbol + " (" + e.gene[key] + ")"); + // nY += 1; + // } + // zValues[nY][nX] = e.expr_value; + // }); + // // add trace + // data.push({ + // z: zValues, + // x: xSamples, + // y: yElements, + // type: "heatmap", + // hoverongaps: false, + // name: dataset, + // showscale: false + // }); + // }); + // // only show scale on last heatmap + // data[-1].showscale = true; + // // add x-axis subplot for each trace + // data.slice(1).forEach((d, i) => { + // let idx: string = (i+2).toString(); + // d.xaxis = 'x' + idx + // }); + // // set layout options + // const layout = { + // autosize: true, + // showlegend: true, + // legend: {orientation: "h"}, + // xaxis: { + // showgrid: false, + // showticklabels: false, + // showticks: false + // }, + // margin: { + // b: 125, + // l: 125, + // r: 50, + // t: 50 + // }, + // title: { + // text: "Module expression of selected modules", + // font: { + // size: 14 + // } + // }, + // grid: { + // rows: 1, + // columns: data.length + // }, + // annotations: [] + // }; + // let config = { + // responsive: true + // } + // return {data: data, layout: layout, config: config}; + } + + getColors(n: number): string[] { + const colors: string[] = []; + const step: number = 360 / n; + const saturation: number = 1; + const lightness: number = 0.6 + for (let i = 0; i < n; i++) { + const color = d3.hsl(i * step, saturation, lightness); + colors.push(color.toString()); + } + return colors; + } + + /** + * returns module member expression data for a given ensemblId + */ + getModuleExpressionDataTest(ensemblId: string): PlotlyData { + // TODO: get data from API and remove testing data + + if (this.selectedCancer == undefined) { + return {data: [], layout: {}, config: {}}; + } + + let data: any[] = []; + const y: number = this.getRandomValue(2, 10, true); + let y_elements: string[] = this.getRandomElements(y, this.level.toLowerCase()); + const legendGroup: string = "commonGroup"; + let x0: number = 0; + const gapWidth: number = 0.2; + const traceWidth: number = 0.6; + + this.selectedCancer.allSubTypes.forEach((subtype, index) => { + const x: number = this.getRandomValue(2, 10, true); + let x_samples: string[] = this.getRandomIDs(x, 4); + let z_values: number[][] = Array.from(Array(y).keys()).map(_ => this.getRandomValues(x, 0, 15)); + data.push( + { + x: x_samples, + y: y_elements, + z: z_values, + name: subtype, + type: "heatmap", + hoverongaps: false, + x0: x0, + dx: traceWidth + } + ); + x0 += traceWidth + gapWidth; + }) + + const layout = { + autosize: true, + showlegend: true, + xaxis: { + showgrid: false, + showticklabels: false, + showticks: false + }, + margin: { + b: 125, + l: 125, + r: 50, + t: 50 + }, + title: { + text: "Module expression of hub-node " + this.selectedEnsemblId, + font: { + size: 14 + } + }, + annotations: [] + }; + let config = { + responsive: true + } + // add type groups to heatmap + const traceColors: string[] = this.getColors(this.selectedCancer.allSubTypes.length); + const yGap: number = 0.1; + const boxHeight: number = 20; + const widthPerSample: number = 2; + + data.forEach((trace, index) => { + for (let idx = 0; idx < trace.x.length; idx++) { + layout.annotations.push({ + x: trace.x[idx], + y: trace.y.length + yGap, + hovertext: trace.name, + xref: 'x', + yref: 'y', + text: '', + showarrow: false, + align: 'center', + bgcolor: traceColors[index], + width: widthPerSample, + height: boxHeight + }); + } + }); + return {data: data, layout: layout, config: config}; + } + + getModuleMiRnaExpressionDataTest(hubId: string): PlotlyData{ + if (this.selectedCancer == undefined) { + return {data: [], layout: {}, config: {}}; + } + let miRNAs: string[] = this.getRandomElements(this.getRandomValue(10, 20, true), "mirna"); + let data: any[] = []; + this.selectedCancer.allSubTypes.forEach(subtype => { + data.push({ + x: miRNAs, + y: this.getRandomValues(miRNAs.length, 0, 12), + name: subtype, + type: "bar" + }); + }); + let layout = { + autosize: true, + margin: { + b: 5, + l: 5, + r: 5, + t: 50 + }, + title: { + text: "Module miRNA expression of " + this.selectedEnsemblId, + font: { + size: 14 + } + }, + barmode: "group" + }; + let config = { + responsive: true + }; + return {data: data, layout: layout, config: config} + } + + plotSampleDistribution(config: PlotlyData) { + Plotly.newPlot(this.sampleDistributionPieDiv.nativeElement, config.data, config.layout, config.config); + } + + plotModuleExpression(config: PlotlyData) { + Plotly.newPlot(this.moduleExpressionHeatmap.nativeElement, config.data, config.layout, config.config); + } + + plotModuleMiRnaExpression(config: PlotlyData) { + Plotly.newPlot(this.moduleMiRnaExpressionPlot.nativeElement, config.data, config.layout, config.config); + } + + async plotOverallAccuracyPlot(metricData: Promise) { + // set main layout options + let layout = { + autosize: true, + yaxis: { + showline: false, + showticklabels: false + }, + margin: { + t: 0, + b: 40, + l: 30, + r: 20 + }, + annotations: [ + // x-axis label + { + xref: "paper", + yref: "paper", + x: 0.5, + y: -0.1, + xanchor: "center", + yanchor: "top", + text: "Overall model accuracy", + showarrow: false + } + ], + legend: { + traceorder: "reversed" + } + }; + let metrics: Metric[] = await metricData; + let data = metrics.map(metric => { + const col: string = metric.name == "modules" ? "green": "orange" + // data points + return { + x: [metric.lower, metric.upper], + y: [metric.level, metric.level], + mode: this.defaultPlotMode, + name: metric.name + " (" + metric.split + ")", + text: ["Lower Bound (Accuracy)", "Upper Bound (Accuracy)"], + hovertemplate: "%{text}: %{x:.2f}", + line: { + width: this.defaultLineWidth, + color: col, + dash: metric.split == "train" ? "solid": "dash" + }, + marker: { + size: this.defaultMarkerSize, + symbol: ['circle', 'diamond'], + color: col + }, + showlegend: true + } + }); + const config = { responsive: true }; + // remove loading spinner and show plot + Plotly.newPlot("overall-acc", data, layout, config); + } + + async resetOverallAccPlot() { + Plotly.update(this.overallAccPlotDiv.nativeElement) + } + + async plotEnrichmentScoresByClass(enrichmentData: Promise>) { + // fill subtype specific data + let data: any[] = []; + const enrichmentDataResponse = await enrichmentData; + enrichmentDataResponse.forEach((plotData, subtype) => { + // push trace for each subtype + data.push({ + x: plotData.x, + y: plotData.y, + fill: "tozeroy", + type: "scatter", + mode: "lines", + opacity: 0.8, + name: subtype + }); + }); + // add subplot for each trace + data.slice(1).forEach((d, i) => { + let idx: string = (i+2).toString(); + d.xaxis = 'x' + idx + d.yaxis = 'y' + idx + }); + // determine range of display + let minScore: number = Math.round(min(data.map(d => min(d.x)))); + let maxScore: number = Math.round(max(data.map(d => max(d.x)))); + const plot_height: number = data.length * 50; + // set general layout options + let layout = { + showlegend: false, + autosize: true, + legend: {"orientation": "h"}, + grid: { + rows: data.length, + columns: 1, + pattern: 'independent', + roworder: 'bottom to top' + }, + height: plot_height, + title: "spongEffects enrichment score density for predictive classes" + }; + // set constant y axis layout + const y_axis_layout = { + showgrid: false, + automargin: true, + showticklabels: false, + }; + const annotations = []; + // add layout to each trace + data.forEach((d, index) => { + let x_axis_layout_i = { + range: [minScore, maxScore], + showgrid: false, + showticklabels: false + }; + let x_key: string = "xaxis"; + let y_key: string = "yaxis"; + let x: string = "x"; + let y: string = "y"; + if (index != 0) { + x_key = x_key + (index + 1).toString(); + y_key = y_key + (index + 1).toString(); + x = x + (index + 1).toString(); + y = y + (index + 1).toString(); + } else { + x_axis_layout_i["title"] = "spongEffects enrichment score"; + x_axis_layout_i.showticklabels = true; + } + layout[x_key] = x_axis_layout_i; + layout[y_key] = y_axis_layout; + // add class annotation + annotations.push({ + xref: x, + yref: y, + x: minScore + 1.5, + y: 0.5, + text: d.name, + align: "left", + showarrow: false, + width: 250 + }) + }); + layout["annotations"] = annotations; + const config = { responsive: true } + Plotly.newPlot(this.enrichmentScoresByClassPlotDiv.nativeElement, data, layout, config); + } + + async resetEnrichmentScoreByClass() { + Plotly.update(this.enrichmentScoresByClassPlotDiv.nativeElement); + } + + async plotLollipop(n: number) { + let giniData: SpongEffectsGeneModules[] = await this.modulesData; + const redNodes: number = this.markControl.value; + // only use selected number of top genes + let idx: number = n > giniData.length ? giniData.length: n; + giniData = giniData.slice(0, idx); + // set selected elements + this.addModules(giniData.slice(0, redNodes)) + + // selected elements + this.filteredModulesData = giniData; + + // set main layout options + let data: any[] = [{ + x: giniData.map(g => g.mean_gini_decrease), + y: giniData.map(g => g.mean_accuracy_decrease), + mode: "markers", + type: "scatter", + name: giniData.map(g => g.gene_symbol), + text: giniData.map(g => g.gene_symbol), + marker: { + size: this.defaultMarkerSize, + color: giniData.map(g => this.selectedModules.includes(g) ? "red": "grey") + } + }]; + // set main layout options + let layout = { + showlegend: false, + autosize: true, + hovermode: "closest", + margin: { + b: 50, + l: 75, + r: 25, + t: 0 + }, + xaxis: { + title: "Mean decrease in Gini-index" + }, + yaxis: { + title: "Mean decrease in accuracy" + } + } + let config = { + responsive: true + } + Plotly.newPlot(this.lollipopPlotDiv.nativeElement, data, layout, config); + + // add click handler + this.lollipopPlotDiv.nativeElement.on("plotly_click", (eventData) => { + const clickedSymbol: string = eventData.points[0].toString(); + // get modules with clicked element + const modules: SpongEffectsGeneModules[] = giniData.filter(c => c.gene_symbol == clickedSymbol); + this.addModules(modules, clickedSymbol); + // toggle color of clicked point + const updatedData = this.lollipopPlotDiv.nativeElement.data; + updatedData[0].marker.color = giniData.map(g => this.selectedModules.includes(g) ? "red": "grey"); + // update plot + Plotly.redraw(this.lollipopPlotDiv.nativeElement, updatedData, layout, config); + }); + } + + addModules(modules: SpongEffectsGeneModules[], clickedSymbol?: string) { + if (clickedSymbol == undefined) { + this.selectedModules = modules; + } else { + // modules are not inserted yet + if (this.selectedModules.filter(s => s.gene_symbol == clickedSymbol).length == 0) { + // add new modules + this.selectedModules.push(...modules); + } else { + // remove clicked modules + this.selectedModules = this.selectedModules.filter(s => s.gene_symbol != clickedSymbol); + } + } + // update dynamic table data + this.dynamicModulesData.setData(this.selectedModules); + } + + // predict functions + onExpressionUpload(event: any) { + this.uploadedExpressionFiles.push(...event.addedFiles); + // TODO: check format + } + + expressionUploaded(): boolean { + return this.uploadedExpressionFiles.length > 0; + } + + onRemoveExpression(event) { + this.uploadedExpressionFiles.splice(this.uploadedExpressionFiles.indexOf(event), 1); + this.predictionQueried = false; + } + + acceptExpressionFiles(): string { + return this.expressionUploaded() ? "none" : this.filesToAccept; + } + + flipExampleExpression() { + this.showExpressionExample = !this.showExpressionExample; + } + + estimateRunTime() { + const fileSize: number = this.uploadedExpressionFiles[0].size / (1024**2); + const refSlope: number = 0.7; + const x0: number = 17; + const st: number = this.predictSubtypes ? 4 : 1; + return refSlope * fileSize + x0; + } + + async getPredictionData(): Promise { + const uploadedFile: File = this.uploadedExpressionFiles[0]; + // send file and parameters to API and return response + return this.backend.predictCancerType( + uploadedFile, this.predictSubtypes, this.logScaling, + this.mscorControl.value, this.fdrControl.value, this.minSizeControl.value, + this.maxSizeControl.value, this.minExprControl.value, this.methodDefault + ) + } + + + getColorForValue(value: number): string { + let g: number = 140; + let r: number = value >= 0.5 ? Math.round(255*2 * (1 - value)): 255; + const b: number = 0; + return `rgb(${r},${g},${b})`; + } + + async extractPredictions(responseJson: any): Promise { + const typeGroups: Map = new Map(); + // group predictions by type + responseJson.data.forEach(entry => { + if (typeGroups.has(entry.typePrediction)) { + typeGroups.get(entry.typePrediction)?.push(entry.subtypePrediction); + } + }); + + const typeCounts: Map = new Map([...typeGroups.entries()].map(entry => { + return [entry[0], entry[1].length]; + })); + // sort by amount of samples + const sortedTypeCounts: Map = new Map([...typeCounts.entries()].sort((a, b) => a[1] - b[1])); + let x: number[] = [...sortedTypeCounts.values()]; + let y: string[] = [...sortedTypeCounts.keys()]; + // add model accuracy + let classPerformanceData = this.classPerformancePlotDiv.nativeElement.data; + // get modules data + classPerformanceData = classPerformanceData.filter(d => d.name == "modules") + if (classPerformanceData.length > 0) { + classPerformanceData = classPerformanceData[0]; + } + // create map to value + const classToMeasure: Map = new Map(); + for (let i = 0; i < classPerformanceData.x.length; i++) { + classToMeasure.set(classPerformanceData.x[i], classPerformanceData.y[i]); + } + const accValues: number[] = y.map(x_v => classToMeasure.get(x_v) ?? 0); // color based on balanced accuracy + const barColors: string[] = accValues.map(v => this.getColorForValue(v)); + // transform data + let data = [{ + x: x, + y: y, + text: accValues.map(v => "Balanced accuracy: " + v.toString()), + type: "bar", + name: "type", + orientation: "h", + marker: { + color: barColors + } + }]; + + // add subtype traces + if (this.predictSubtypes) { + const subtypeTraces: any[] = [...typeGroups.values()].map(sv => { + return { + x: sv.length, + y: y, + text: sv, + name: "subtypes", + orientation: "h" + } + }); + data.push(...subtypeTraces); + } + + const layout = { + paper_bgcolor: "white", + autosize: true, + barmode: "group", + margin: { + l: 250, + r: 25, + t: 50, + b: 50 + }, + xaxis: { + title: "Number of samples classified" + } + }; + const config = { + responsive: true + } + return {data: data, layout: layout, config: config}; + } + + async plotPredictions(plotlyData: PlotlyData): Promise { + Plotly.newPlot(this.typePredictPiePlot.nativeElement, plotlyData.data, plotlyData.layout, plotlyData.config); + } + + async processPredictions(predictionResponse: any): Promise { + // check response + if (!predictionResponse.ok) { + throw new Error(`File upload failed with status code: ${predictionResponse.status}`); + } + // save results + const predictionData = await predictionResponse.json(); + this.predictionData = predictionData.data; + this.predictionMeta = predictionData.meta[0]; + this.predictedType = predictionData.meta[0].type_predict; + this.predictedSubtype = predictionData.meta[0].subtype_predict; + // plot predictions + this.extractPredictions(predictionData) + .then(data => this.plotPredictions(data)); + } + + async startTimer(): Promise { + this.timerRunning = true; + this.progressBarValue = 0; + const totalRunTime: number = this.estimateRunTime(); + this.estimatedRunTime = totalRunTime; + const interval: number = (1000 * totalRunTime) / 100; + const progressBarTimer = timer(0, interval); + progressBarTimer.subscribe(() => { + this.estimatedRunTime = totalRunTime * (100-this.progressBarValue)/100; + if (this.progressBarValue < 100) this.progressBarValue++; + }); + } + + runButtonDisabled(): boolean { + return !this.expressionUploaded() || this.predictionLoading; + } + + estimatedRunTimeText(): string { + return this.estimatedRunTime > 0 ? Math.round(this.estimatedRunTime).toString()+"s": "Any moment...hopefully" + } + + async predict() { + this.predictionQueried = true; + this.predictionLoading = true; + // start timer of estimated run time + this.startTimer().then(_ => this.timerRunning = false); + // start workflow + this.getPredictionData() + .then(data => this.processPredictions(data)) + .then(_ => this.predictionLoading = false); + } + + buttonText(btn: string) { + if (btn == "expr") { + return this.showExpressionExample ? "Hide example file" : "Show example file"; + } + else { + return ""; + } + } + + getCancerInfoText(): CancerInfo { + if (this.selectedCancer == undefined) return {text: ["loading DB data..."], link: ""}; + let cancerInfo: CancerInfo; + switch (this.selectedCancer.value) { + case "PANCAN": { + let texts: string[] = [ + "The Pan-cancer project includes the combined data of 33 of the most common cancer forms in humans.", + ]; + cancerInfo = {text: texts, link: "https://doi.org/10.1016/j.cell.2018.03.022"}; + break; + } + case "BRCA": { + let texts: string[] = [ + "Breast cancer is the most frequently observed cancer in women and one of the main causes of death in women.", + ]; + cancerInfo = {text: texts, link: "https://www.nature.com/articles/nature11412"}; + break; + } + case "CESC": { + let texts: string[] = [ + "Cervical cancer is a form of cancer that develops in cervix tissues, i.e. the lower area of the uterus.", + ]; + cancerInfo = {text: texts, link: "https://www.nature.com/articles/nature21386"}; + break; + } + case "ESCA": { + let texts: string[] = [ + "The TCGA research revealed two predominant forms of esophageal cancer: squamous cell carcinoma, originating from the flat epithelial cells lining the esophagus, and adenocarcinoma, originating from the glandular cells responsible for producing mucus and various fluids.", + ]; + cancerInfo = {text: texts, link: "https://www.nature.com/articles/nature20805"}; + break; + } + case "HNSC": { + let texts: string[] = [ + "The majority of head and neck cancers initiate in the moist, mucous membranes that line the interior of the mouth, nasal passages, and throat. These membranes consist of squamous cells, and the head and neck cancers that develop within these cells are classified as squamous cell carcinomas.", + ]; + cancerInfo = {text: texts, link: "https://www.nature.com/articles/nature14129"}; + break; + } + case "LGG": { + let texts: string[] = [ + "Glioma is a form of cancer originating in the brain's glial cells, which play a vital role in supporting and maintaining the health of the brain's nerve cells. Tumors are categorized into grades I, II, III, or IV in accordance with criteria established by the World Health Organization. In this research, TCGA specifically investigated lower-grade gliomas, encompassing grades II and III.", + ]; + cancerInfo = {text: texts, link: "https://www.nejm.org/doi/full/10.1056/NEJMoa1402121"}; + break; + } + case "SARC": { + let texts: string[] = [ + "The term \"sarcoma\" includes a wide range of uncommon cancers that have the potential to impact soft tissues, bone structures, or even both, spanning various parts of the body.", + ]; + cancerInfo = {text: texts, link: "https://www.cell.com/cell/fulltext/S0092-8674(17)31203-5"}; + break; + } + case "STAD": { + let texts: string[] = [ + "The occurrence of stomach cancer displays significant variation influenced by a combination of genetic and environmental factors. This type of cancer is more commonly found in men, elderly individuals, and those with a familial predisposition to the disease. On a global scale, the incidence of stomach cancer differs by geographic region.", + ]; + cancerInfo = {text: texts, link: "https://www.nature.com/articles/nature13480"}; + break; + } + case "TGCT": { + let texts: string[] = [ + "Over 90% of testicular cancers originate from germ cells, which are cells within the testicles responsible for sperm production. This category of cancer is referred to as testicular germ cell cancer. Testicular germ cell cancer can be further categorized as either seminomas or nonseminomas, distinguishable through microscopic examination. Nonseminomas typically exhibit more rapid growth and dissemination compared to seminomas. When a testicular germ cell tumor contains a combination of both subtypes, it is classified as a nonseminoma. TCGA conducted research encompassing both seminomas and nonseminomas.", + ]; + cancerInfo = {text: texts, link: "https://www.cell.com/cell-reports/fulltext/S2211-1247(18)30785-X"}; + break; + } + case "UCEC": { + let texts: string[] = [ + "Endometrial cancer arises in the cells that compose the inner lining of the uterus, known as the endometrium. It ranks as one of the prevalent cancers affecting the female reproductive system in American women.", + ]; + cancerInfo = {text: texts, link: "https://www.nature.com/articles/nature12113"}; + break; + } + default: { + let texts: string[] = [ + "No information available for this cancer type." + ]; + cancerInfo = {text: texts, link: ""}; + break; + } + } + return cancerInfo; + } +} diff --git a/src/app/routes/spongeffects/spongeffects.module.ts b/src/app/routes/spongeffects/spongeffects.module.ts new file mode 100644 index 00000000..89a91fb9 --- /dev/null +++ b/src/app/routes/spongeffects/spongeffects.module.ts @@ -0,0 +1,93 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { MatButtonModule } from '@angular/material/button'; +import { MatCardModule } from '@angular/material/card'; +import { MatCheckboxModule } from '@angular/material/checkbox'; +import { MatExpansionModule } from '@angular/material/expansion'; +import { MatFormFieldModule } from '@angular/material/form-field'; +import { MatIconModule } from '@angular/material/icon'; +import { MatInputModule } from '@angular/material/input'; +import { MatProgressBarModule } from '@angular/material/progress-bar'; +import { MatSelectModule } from '@angular/material/select'; +import { MatSidenavModule } from '@angular/material/sidenav'; +import { MatTooltipModule } from '@angular/material/tooltip'; +import { MatButtonToggleModule } from '@angular/material/button-toggle'; +import { MatGridListModule } from '@angular/material/grid-list'; +import { MatTableModule } from '@angular/material/table'; +import { NgxDropzoneModule } from 'ngx-dropzone'; +import { SpongeEffectsComponent } from './spo'; +import { RouterModule } from '@angular/router'; +import {sum} from "simple-statistics"; + +@NgModule({ + declarations: [ + SpongeEffectsComponent + ], + imports: [ + CommonModule, + FormsModule, + ReactiveFormsModule, + MatButtonModule, + MatCardModule, + MatCheckboxModule, + MatExpansionModule, + MatFormFieldModule, + MatIconModule, + MatInputModule, + MatProgressBarModule, + MatSelectModule, + MatSidenavModule, + MatTooltipModule, + MatButtonToggleModule, + MatGridListModule, + MatTableModule, + NgxDropzoneModule, + RouterModule.forChild([ + { path: '', component: SpongeEffectsComponent } + ]) + ] +}) +export class SpongeEffectsModule { + + export class Cancer { + value: string; + viewValue: string; + allSubTypes: string[]; + sampleSizes: number[]; + + base: string = "https://portal.gdc.cancer.gov/projects/TGCA-"; + + constructor(value: string, viewValue: string, allSubTypes: string[], sampleSizes: number[], ) { + this.value = value; + this.viewValue = viewValue; + this.allSubTypes = allSubTypes; + this.sampleSizes = sampleSizes; + } + + addSubtype(subtype: string) { + this.allSubTypes.push(subtype); + } + + addSampleSize(sampleSize: number) { + if (sampleSize != null) this.sampleSizes.push(sampleSize); + } + + totalNumberOfSamples() { + return sum(this.sampleSizes.filter(s => s >= 0)); + } + + toString() { + return this.viewValue + " - (" + this.value + ")"; + } + + getUrl() { + return this.base + this.value + } +} + + + + + +} diff --git a/src/app/services/backend.service.ts b/src/app/services/backend.service.ts index 958a4882..30d8ab32 100644 --- a/src/app/services/backend.service.ts +++ b/src/app/services/backend.service.ts @@ -1,6 +1,26 @@ import {Injectable} from '@angular/core'; import {HttpService} from "./http.service"; -import {Dataset, OverallCounts} from "../interfaces"; +import {VersionService} from "./version.service"; +import { + Dataset, + DatasetInfo, + EnrichmentScoreDistributions, + OverallCounts, + PlotData, + RunClassPerformance, + RunPerformance, + SpongEffectsGeneModules, + SpongEffectsGeneModuleMembers, + SpongEffectsTranscriptModules, + PredictCancerType, + SpongEffectsTranscriptModuleMembers, + CeRNAInteraction, + CeRNAQuery, + CeRNA, + CeRNAExpression, + SurvivalRate, GeneCount, SurvivalPValue, Gene, TranscriptExpression +} from "../interfaces"; + @Injectable({ providedIn: 'root' @@ -8,16 +28,184 @@ import {Dataset, OverallCounts} from "../interfaces"; export class BackendService { private static API_BASE = 'https://exbio.wzw.tum.de/sponge-api' - constructor(private http: HttpService) { + constructor(private http: HttpService, private versionService: VersionService) { } getDatasets(diseaseName?: string): Promise { - const request = BackendService.API_BASE + '/dataset' + (diseaseName ? `?disease=${diseaseName}` : ''); - return this.http.getRequest(request); + const sponge_db_version = this.versionService.getCurrentVersion(); + const request = BackendService.API_BASE + '/dataset' + (diseaseName ? `?disease_name=${diseaseName}` : '') + `&sponge_db_version=${sponge_db_version}`; + return this.http.getRequest(request); + } + + getDatasetsInformation(dataOrigin: string): Promise { + const sponge_db_version = this.versionService.getCurrentVersion(); + const request = BackendService.API_BASE + '/datasets' + `?data_origin=${dataOrigin}` + `&sponge_db_version=${sponge_db_version}`; + return this.http.getRequest(request); } getOverallCounts(): Promise { - const request = BackendService.API_BASE + '/getOverallCounts'; - return this.http.getRequest(request); + const sponge_db_version = this.versionService.getCurrentVersion(); + const request = BackendService.API_BASE + '/getOverallCounts' + `?sponge_db_version=${sponge_db_version}`; + return this.http.getRequest(request); + } + + + getCeRNA(query: CeRNAQuery): Promise { + const sponge_db_version = this.versionService.getCurrentVersion(); + let request = BackendService.API_BASE + '/findceRNA?disease_name=' + query.disease.disease_name + `?sponge_db_version=${sponge_db_version}`; + + request += `&minBetweenness=${query.minBetweenness}`; + request += `&minNodeDegree=${query.minDegree}`; + request += `&minEigenvector=${query.minEigen}`; + request += `&sorting=${query.geneSorting}`; + request += `&descending=${true}`; + request += `&limit=${query.maxGenes}`; + + return this.http.getRequest(request); + } + + getCeRNAInteractionsAll(disease: string, maxPValue: number, ensgs: string[], limit?: number, offset?: number): Promise { + let request = BackendService.API_BASE + '/ceRNAInteraction/findAll?disease_name=' + disease; + request += `&ensg_number=${ensgs.join(',')}`; + request += `&pValue=${maxPValue}`; + + if (limit) { + request += `&limit=${limit}`; + } + if (offset) { + request += `&offset=${offset}`; + } + + return this.http.getRequest(request); + } + + getCeRNAInteractionsSpecific(disease: string, maxPValue: number, ensgs: string[]): Promise { + let request = BackendService.API_BASE + '/ceRNAInteraction/findSpecific?disease_name=' + disease; + request += `&ensg_number=${ensgs.join(',')}`; + request += `&pValue=${maxPValue}`; + + return this.http.getRequest(request); + } + + getCeRNAExpression(ensgs: string[], diseaseName: string): Promise { + let request = BackendService.API_BASE + '/exprValue/getceRNA?disease_name=' + diseaseName; + request += `&ensg_number=${ensgs.join(',')}`; + + return this.http.getRequest(request); + } + + getTranscriptExpression(ensts: string[], disease_name?: string): Promise { + let request = BackendService.API_BASE + `/exprValue/getTranscript?disease_name=${disease_name}`; + request += `&enst_number=${ensts.join(',')}`; + + return this.http.getRequest(request); + } + + getSurvivalRates(ensgs: string[], diseaseName: string): Promise { + let request = BackendService.API_BASE + '/survivalAnalysis/getRates?disease_name=' + diseaseName; + request += `&ensg_number=${ensgs.join(',')}`; + + return this.http.getRequest(request); + } + + getSurvivalPValues(ensgs: string[], diseaseName: string): Promise { + let request = BackendService.API_BASE + '/survivalAnalysis/getPValues?disease_name=' + diseaseName; + request += `&ensg_number=${ensgs.join(',')}`; + + return this.http.getRequest(request); + } + + getAutocomplete(query: string): Promise { + if (query.length < 2) { + return Promise.resolve([]); + } + const request = BackendService.API_BASE + '/stringSearch?searchString=' + query; + try { + return this.http.getRequest(request); + } catch (e) { + return Promise.resolve([]); + } } + + getGeneCount(ensgs: string[], onlySignificant: boolean): Promise { + if (ensgs.length === 0) { + return Promise.resolve([]); + } + let request = BackendService.API_BASE + '/getGeneCount?ensg_number=' + ensgs.join(','); + if (onlySignificant) { + request += '&minCountSign=1'; + } + return this.http.getRequest(request); + } + + +// spongEffects services: + + getRunPerformance(diseaseName: string, level: string): Promise { + const sponge_db_version = this.versionService.getCurrentVersion(); + const request = BackendService.API_BASE + '/spongEffects/getRunPerformance' + `?disease_name=${diseaseName}` + `&level=${level}` + `&sponge_db_version=${sponge_db_version}`; + return this.http.getRequest(request); + } + + getRunClassPerformance(diseaseName: string, level: string): Promise { + const sponge_db_version = this.versionService.getCurrentVersion(); + const request = BackendService.API_BASE + '/spongEffects/getRunClassPerformance' + `?disease_name=${diseaseName}` + `&level=${level}` + `&sponge_db_version=${sponge_db_version}`; + return this.http.getRequest(request); + } + + getEnrichmentScoreDistributions(diseaseName: string, level: string): Promise { + const sponge_db_version = this.versionService.getCurrentVersion(); + const request = `${BackendService.API_BASE}/spongEffects/getEnrichmentScoreDistributions?disease_name=${diseaseName}&level=${level}&sponge_db_version=${sponge_db_version}`; + return this.http.getRequest(request); + } + + getSpongEffectsGeneModules(diseaseName: string): Promise { + const sponge_db_version = this.versionService.getCurrentVersion(); + const request = `${BackendService.API_BASE}/spongEffects/getSpongEffectsGeneModules?disease_name=${diseaseName}&sponge_db_version=${sponge_db_version}`; + return this.http.getRequest(request); + } + + getSpongEffectsGeneModuleMembers(diseaseName: string, ensgNumber?: string, geneSymbol?: string): Promise { + const sponge_db_version = this.versionService.getCurrentVersion(); + let request = `${BackendService.API_BASE}/spongEffects/getSpongEffectsGeneModuleMembers?disease_name=${diseaseName}&sponge_db_version=${sponge_db_version}`; + if (ensgNumber) { + request += `&ensg_number=${ensgNumber}`; + } + if (geneSymbol) { + request += `&gene_symbol=${geneSymbol}`; + } + return this.http.getRequest(request); + } + + getSpongEffectsTranscriptModules(diseaseName: string): Promise { + const sponge_db_version = this.versionService.getCurrentVersion(); + const request = `${BackendService.API_BASE}/spongEffects/getSpongEffectsTranscriptModules?disease_name=${diseaseName}&sponge_db_version=${sponge_db_version}`; + return this.http.getRequest(request); + } + + getSpongEffectsTranscriptModuleMembers(diseaseName: string, enstNumber?: string): Promise { + const sponge_db_version = this.versionService.getCurrentVersion(); + let request = `${BackendService.API_BASE}/spongEffects/getSpongEffectsTranscriptModuleMembers?disease_name=${diseaseName}&sponge_db_version=${sponge_db_version}`; + if (enstNumber) { + request += `&enst_number=${enstNumber}`; + } + return this.http.getRequest(request); + } + + predictCancerType(file: File, subtypes: boolean, log: boolean, mscor: number, fdr: number, minSize: number, maxSize: number, minExpr: number, method: string): Promise { + const sponge_db_version = this.versionService.getCurrentVersion(); + const formData = new FormData(); + formData.append('file', file); + formData.append('subtypes', subtypes.toString()); + formData.append('log', log.toString()); + formData.append('mscor', mscor.toString()); + formData.append('fdr', fdr.toString()); + formData.append('min_size', minSize.toString()); + formData.append('max_size', maxSize.toString()); + formData.append('min_expr', minExpr.toString()); + formData.append('method', method); + const request = `${BackendService.API_BASE}/spongEffects/predictCancerType?sponge_db_version=${sponge_db_version}`; + return this.http.postRequest(request, formData); + } + } diff --git a/src/app/services/version.service.spec.ts b/src/app/services/version.service.spec.ts new file mode 100644 index 00000000..7e43b32d --- /dev/null +++ b/src/app/services/version.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; +import { VersionService } from './version.service'; + +describe('VersionService', () => { + let service: VersionService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(VersionService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); + +}); \ No newline at end of file diff --git a/src/app/services/version.service.ts b/src/app/services/version.service.ts new file mode 100644 index 00000000..f244d1cd --- /dev/null +++ b/src/app/services/version.service.ts @@ -0,0 +1,24 @@ +import {Injectable, signal, WritableSignal} from '@angular/core'; + +@Injectable({ + providedIn: 'root' + }) + export class VersionService { + private static LATEST: number = 2; + private currentVersion: WritableSignal = signal(VersionService.LATEST); + + constructor(private version: VersionService) { + } + + public getCurrentVersion(): WritableSignal { + return this.version.currentVersion; + } + + public setCurrentVersion(new_version: number): void { + this.version.currentVersion = signal(new_version); + } + + public resetToDefault(): void { + this.currentVersion = signal(VersionService.LATEST) + } +} From be867cebba71b716574994e451bc0e8fe705eec6 Mon Sep 17 00:00:00 2001 From: Nico Trummer Date: Tue, 17 Dec 2024 20:34:05 +0100 Subject: [PATCH 02/14] Add missing dependencies --- package-lock.json | 25 +- package.json | 3 +- .../class-performance-plot.component.ts | 8 +- .../spongeffects/predict/predict.component.ts | 92 +- .../spongeffects/spongeffects.component_old | 1461 ----------------- .../spongeffects/spongeffects.module.ts | 93 -- 6 files changed, 53 insertions(+), 1629 deletions(-) delete mode 100644 src/app/routes/spongeffects/spongeffects.component_old delete mode 100644 src/app/routes/spongeffects/spongeffects.module.ts diff --git a/package-lock.json b/package-lock.json index 819c98ec..b66849ff 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,10 +24,11 @@ "graphology": "^0.25.4", "graphology-layout-force": "^0.2.4", "lodash": "^4.17.21", - "ngx-dropzone": "^3.1.0", "ngx-bootstrap": "^19.0.1", + "ngx-dropzone": "^3.1.0", "rxjs": "~7.8.0", "sigma": "^3.0.0-beta.38", + "simple-statistics": "^7.8.7", "tslib": "^2.3.0", "zone.js": "~0.15.0" }, @@ -4989,12 +4990,6 @@ "@types/node": "*" } }, - "node_modules/@types/d3-color": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", - "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", - "license": "MIT" - }, "node_modules/@types/eslint": { "version": "9.6.1", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", @@ -6879,15 +6874,6 @@ "dev": true, "license": "MIT" }, - "node_modules/d3-color": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", - "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, "node_modules/date-format": { "version": "4.0.14", "resolved": "https://registry.npmjs.org/date-format/-/date-format-4.0.14.tgz", @@ -10613,9 +10599,9 @@ } }, "node_modules/nanoid": { - "version": "3.3.7", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", - "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "version": "3.3.8", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", + "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==", "dev": true, "funding": [ { @@ -10701,6 +10687,7 @@ "resolved": "https://registry.npmjs.org/ngx-dropzone/-/ngx-dropzone-3.1.0.tgz", "integrity": "sha512-5RBaEl07QUcY6sv/BBPyIxN6nbWY/KqTGheEKgbuGS0N1QPFY7NJUo8+X3fYUwQgLS+wjJeqPiR37dd0YNDtWA==", "deprecated": "This package is deprecated and will no longer receive any updates. Please take a look at the official successor repo at hackingharold/ngx-dropzone", + "license": "MIT", "dependencies": { "tslib": "^2.0.0" } diff --git a/package.json b/package.json index 3ab21bd2..59d4653b 100644 --- a/package.json +++ b/package.json @@ -27,10 +27,11 @@ "graphology": "^0.25.4", "graphology-layout-force": "^0.2.4", "lodash": "^4.17.21", - "ngx-dropzone": "^3.1.0", "ngx-bootstrap": "^19.0.1", + "ngx-dropzone": "^3.1.0", "rxjs": "~7.8.0", "sigma": "^3.0.0-beta.38", + "simple-statistics": "^7.8.7", "tslib": "^2.3.0", "zone.js": "~0.15.0" }, diff --git a/src/app/routes/spongeffects/explore/plots/class-performance-plot/class-performance-plot.component.ts b/src/app/routes/spongeffects/explore/plots/class-performance-plot/class-performance-plot.component.ts index 4b03ac42..fc88f318 100644 --- a/src/app/routes/spongeffects/explore/plots/class-performance-plot/class-performance-plot.component.ts +++ b/src/app/routes/spongeffects/explore/plots/class-performance-plot/class-performance-plot.component.ts @@ -6,7 +6,7 @@ import { MatSelectModule } from '@angular/material/select'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { MatProgressBarModule } from '@angular/material/progress-bar'; import {max, min, sum} from 'simple-statistics'; -import { +import { SelectElement, PlotlyData, } from '../../../../../interfaces'; @@ -34,9 +34,9 @@ export class ClassPerformancePlotComponent { versionService = inject(VersionsService); exploreService = inject(ExploreService); backend = inject(BackendService); - + @ViewChild("classModelPerformancePlot") classPerformancePlotDiv!: ElementRef; - plotClassPerformance: ResourceRef; + plotClassPerformance: ResourceRef; performanceMeasures: SelectElement[] = [ @@ -81,7 +81,7 @@ export class ClassPerformancePlotComponent { - + async plotModelClassPerformance(version: number, cancer: string, level: string): Promise { const performanceData = await this.backend.getRunClassPerformance(version, cancer, level); diff --git a/src/app/routes/spongeffects/predict/predict.component.ts b/src/app/routes/spongeffects/predict/predict.component.ts index 093eb6a1..77c295fd 100644 --- a/src/app/routes/spongeffects/predict/predict.component.ts +++ b/src/app/routes/spongeffects/predict/predict.component.ts @@ -1,43 +1,37 @@ -import { Component, effect, ElementRef, inject, ViewChild } from '@angular/core'; -import { MatCardModule } from '@angular/material/card'; -import { MatIconModule } from '@angular/material/icon'; -import { MatTabsModule } from '@angular/material/tabs'; -import { MatSelectModule } from '@angular/material/select'; -import { MatFormFieldModule } from '@angular/material/form-field'; -import { MatInputModule } from '@angular/material/input'; -import { MatSliderModule } from '@angular/material/slider'; -import { MatSlideToggleModule } from '@angular/material/slide-toggle'; -import { MatExpansionModule } from '@angular/material/expansion'; -import { MatListModule } from '@angular/material/list'; -import { MatDividerModule } from '@angular/material/divider'; -import { MatProgressBarModule, ProgressBarMode } from '@angular/material/progress-bar'; -import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; -import { MatTooltipModule } from '@angular/material/tooltip'; -import { MatChipsModule } from '@angular/material/chips'; -import { MatCheckboxModule } from '@angular/material/checkbox'; -import { MatRadioModule } from '@angular/material/radio'; -import { MatButtonToggleModule } from '@angular/material/button-toggle'; -import { MatMenuModule } from '@angular/material/menu'; -import { NgxDropzoneModule } from 'ngx-dropzone'; -import { MatButtonModule } from '@angular/material/button'; -import { MatTableDataSource } from '@angular/material/table'; -import { ExampleExpression, PlotlyData } from '../../../interfaces'; -import { CommonModule } from '@angular/common'; -import { MatTableModule } from '@angular/material/table'; -import { FormControl, FormGroup, FormsModule, Validators } from '@angular/forms'; -import { ReactiveFormsModule } from '@angular/forms'; -import {MatOption, ThemePalette} from '@angular/material/core'; -import { timer } from 'rxjs'; -import { BackendService } from '../../../services/backend.service'; -import { VersionsService } from '../../../services/versions.service'; -import { toSignal } from '@angular/core/rxjs-interop'; +import {Component, ElementRef, inject, ViewChild} from '@angular/core'; +import {MatCardModule} from '@angular/material/card'; +import {MatIconModule} from '@angular/material/icon'; +import {MatTabsModule} from '@angular/material/tabs'; +import {MatSelectModule} from '@angular/material/select'; +import {MatFormFieldModule} from '@angular/material/form-field'; +import {MatInputModule} from '@angular/material/input'; +import {MatSliderModule} from '@angular/material/slider'; +import {MatSlideToggleModule} from '@angular/material/slide-toggle'; +import {MatExpansionModule} from '@angular/material/expansion'; +import {MatListModule} from '@angular/material/list'; +import {MatDividerModule} from '@angular/material/divider'; +import {MatProgressBarModule, ProgressBarMode} from '@angular/material/progress-bar'; +import {MatProgressSpinnerModule} from '@angular/material/progress-spinner'; +import {MatTooltipModule} from '@angular/material/tooltip'; +import {MatChipsModule} from '@angular/material/chips'; +import {MatCheckboxModule} from '@angular/material/checkbox'; +import {MatRadioModule} from '@angular/material/radio'; +import {MatButtonToggleModule} from '@angular/material/button-toggle'; +import {MatMenuModule} from '@angular/material/menu'; +import {NgxDropzoneModule} from 'ngx-dropzone'; +import {MatButtonModule} from '@angular/material/button'; +import {MatTableDataSource, MatTableModule} from '@angular/material/table'; +import {ExampleExpression, PlotlyData} from '../../../interfaces'; +import {CommonModule} from '@angular/common'; +import {FormControl, FormGroup, FormsModule, ReactiveFormsModule, Validators} from '@angular/forms'; +import {MatOption} from '@angular/material/core'; +import {timer} from 'rxjs'; +import {BackendService} from '../../../services/backend.service'; +import {VersionsService} from '../../../services/versions.service'; declare var Plotly: any; - - - const EXAMPLE_GENE_EXPR: ExampleExpression[] = [ {id: "ENSG00000000233", sample1: 6, sample2: 5, sample3: 8, sample4: 2, sampleN: 1}, {id: "ENSG00000000412", sample1: 2, sample2: 1, sample3: 2, sample4: 3, sampleN: 4}, @@ -93,12 +87,10 @@ export class PredictComponent { ["id", ""], ["sample1", "sample1"], ["sample2", "sample2"], ["sample3", "sample3"], ["sample4", "sample4"], ["sampleN", "sampleN"], ]); - exampleExpressionFiles: File[] = [new File([JSON.stringify(EXAMPLE_GENE_EXPR)], "example_expression.txt")]; + exampleExpressionFiles: File[] = [new File([JSON.stringify(EXAMPLE_GENE_EXPR)], "example_expression.txt")]; // save example file - - - + uploadedExpressionFiles: File[] = []; @@ -149,7 +141,8 @@ export class PredictComponent { predictedSubtype: string = "None"; - constructor() { } + constructor() { + } // setInitialParams = effect(() => { @@ -173,8 +166,7 @@ export class PredictComponent { buttonText(btn: string) { if (btn == "expr") { return this.showExpressionExample ? "Hide example file" : "Show example file"; - } - else { + } else { return ""; } } @@ -207,7 +199,7 @@ export class PredictComponent { } estimateRunTime() { - const fileSize: number = this.uploadedExpressionFiles[0].size / (1024**2); + const fileSize: number = this.uploadedExpressionFiles[0].size / (1024 ** 2); const refSlope: number = 0.7; const x0: number = 17; const st: number = this.predictSubtypes ? 4 : 1; @@ -222,19 +214,19 @@ export class PredictComponent { const interval: number = (1000 * totalRunTime) / 100; const progressBarTimer = timer(0, interval); progressBarTimer.subscribe(() => { - this.estimatedRunTime = totalRunTime * (100-this.progressBarValue)/100; + this.estimatedRunTime = totalRunTime * (100 - this.progressBarValue) / 100; if (this.progressBarValue < 100) this.progressBarValue++; }); } getColorForValue(value: number): string { let g: number = 140; - let r: number = value >= 0.5 ? Math.round(255*2 * (1 - value)): 255; + let r: number = value >= 0.5 ? Math.round(255 * 2 * (1 - value)) : 255; const b: number = 0; return `rgb(${r},${g},${b})`; } - + async extractPredictions(responseJson: any): Promise { const typeGroups: Map = new Map(); // group predictions by type @@ -353,7 +345,7 @@ export class PredictComponent { async getPredictionData(): Promise { const uploadedFile: File = this.uploadedExpressionFiles[0]; - // Client-side validation + // Client-side validation if (!this.validateFileContent(uploadedFile)) { return Promise.reject('Client-side validation failed.'); } @@ -367,7 +359,7 @@ export class PredictComponent { this.formGroup.value.minSize ?? this.minSizeDefault, this.formGroup.value.maxSize ?? this.maxSizeDefault, this.formGroup.value.minExpr ?? this.minExprDefault, - this.formGroup.value.method ?? this.methodDefault, + this.formGroup.value.method ?? this.methodDefault, ) return prediction; } catch (error) { @@ -391,8 +383,6 @@ export class PredictComponent { this.predictionLoading = false; } } - - } diff --git a/src/app/routes/spongeffects/spongeffects.component_old b/src/app/routes/spongeffects/spongeffects.component_old deleted file mode 100644 index 6fe6a060..00000000 --- a/src/app/routes/spongeffects/spongeffects.component_old +++ /dev/null @@ -1,1461 +0,0 @@ -import {AfterViewInit, Component, ElementRef, OnInit, Renderer2, ViewChild} from '@angular/core'; -import {FormControl, FormsModule, Validators} from '@angular/forms'; -import {MatTableDataSource} from '@angular/material/table'; -import {max, min, sum} from 'simple-statistics'; -import * as d3 from 'd3-color'; -import {BackendService} from "../../services/backend.service"; -// import {Helper} from '../../helper'; -import {ProgressBarMode} from '@angular/material/progress-bar'; -import {DataSource} from '@angular/cdk/collections'; -import {Observable, ReplaySubject, timer} from 'rxjs'; -import {MatOption, ThemePalette} from '@angular/material/core'; -import { - EnrichmentScoreDistributions, - CeRNAExpression, - TranscriptExpression, - ExampleExpression, - LinearRegression, - Metric, - PlotData, - PlotlyData, - RunPerformance, - SelectElement, - SpongEffectsGeneModuleMembers, - SpongEffectsGeneModules, - SpongEffectsTranscriptModuleMembers, - SpongEffectsTranscriptModules, - Tab, - CancerInfo -} from '../../interfaces'; -import {MatCard, MatCardContent, MatCardHeader} from "@angular/material/card"; -import {MatFormField} from "@angular/material/form-field"; -import {MatSelect} from "@angular/material/select"; -import {MatButtonToggle, MatButtonToggleGroup} from "@angular/material/button-toggle"; -import {MatGridList, MatGridTile} from "@angular/material/grid-list"; -import {MatTooltip} from "@angular/material/tooltip"; -import {NgForOf} from "@angular/common"; -import {MatCheckbox} from "@angular/material/checkbox"; -import {MatIcon} from "@angular/material/icon"; -import {MatSidenavContainer} from "@angular/material/sidenav"; -import {MatAccordion} from "@angular/material/expansion"; -import {MatExpansionPanel} from "@angular/material/expansion"; -import {MatExpansionPanelDescription} from "@angular/material/expansion"; -import {MatExpansionPanelTitle} from "@angular/material/expansion"; -import { MatButtonModule } from '@angular/material/button'; -import { MatCheckboxModule } from '@angular/material/checkbox'; -import { MatExpansionModule } from '@angular/material/expansion'; -import { MatFormFieldModule } from '@angular/material/form-field'; -import { MatInputModule } from '@angular/material/input'; -import { MatProgressBarModule } from '@angular/material/progress-bar'; -import { MatSelectModule } from '@angular/material/select'; -import { MatSidenavModule } from '@angular/material/sidenav'; -import { MatTooltipModule } from '@angular/material/tooltip'; -import { MatButtonToggleModule } from '@angular/material/button-toggle'; -import { MatGridListModule } from '@angular/material/grid-list'; -import { NgxDropzoneModule } from 'ngx-dropzone'; -import { MatTableModule } from '@angular/material/table'; - -declare var Plotly: any; - -export class Cancer { - value: string; - viewValue: string; - allSubTypes: string[]; - sampleSizes: number[]; - - base: string = "https://portal.gdc.cancer.gov/projects/TGCA-"; - - constructor(value: string, viewValue: string, allSubTypes: string[], sampleSizes: number[], ) { - this.value = value; - this.viewValue = viewValue; - this.allSubTypes = allSubTypes; - this.sampleSizes = sampleSizes; - return [...cancerMap.values()].filter(c => spongEffectsCancerAbbreviations.includes(c.value)); - } - - addSubtype(subtype: string) { - this.allSubTypes.push(subtype); - } - - addSampleSize(sampleSize: number) { - if (sampleSize != null) this.sampleSizes.push(sampleSize); - } - - totalNumberOfSamples() { - return sum(this.sampleSizes.filter(s => s >= 0)); - } - - toString() { - return this.viewValue + " - (" + this.value + ")"; - } - - getUrl() { - return this.base + this.value - } -} - - -export const spongEffectsCancerAbbreviations: string[] = ['PANCAN', 'BRCA', 'CESC', 'ESCA', 'HNSC', 'LGG', 'SARC', 'STAD', 'TGCT', 'UCEC'] - - - -export class ModuleDataSource extends DataSource { - private _dataStream = new ReplaySubject(); - - constructor(initialData: SpongEffectsGeneModules[]) { - super(); - this.setData(initialData); - } - - connect(): Observable { - return this._dataStream; - } - - disconnect() {} - - setData(data: SpongEffectsGeneModules[]) { - this._dataStream.next(data); - } -} - - - -const EXAMPLE_GENE_EXPR: ExampleExpression[] = [ - {id: "ENSG00000000233", sample1: 6, sample2: 5, sample3: 8, sample4: 2, sampleN: 1}, - {id: "ENSG00000000412", sample1: 2, sample2: 1, sample3: 2, sample4: 3, sampleN: 4}, - {id: "ENSG00000000442", sample1: 10, sample2: 9, sample3: 8, sample4: 0, sampleN: 7} -] - - - - -@Component({ - selector: 'app-spongeffects', - templateUrl: './spongeffects.component.html', - // imports: [ - // MatCardHeader, - // MatCard, - // MatFormField, - // MatSelect, - // MatOption, - // MatButtonToggleGroup, - // MatButtonToggle, - // MatCardContent, - // MatGridList, - // MatGridTile, - // MatTooltip, - // FormsModule, - // NgForOf, - // MatExpansionPanel, - // MatExpansionPanelTitle, - // MatExpansionPanelDescription, - // MatCheckbox, - // MatIcon, - // MatSidenavContainer, - // MatAccordion, - // MatExpansionPanel, - // MatExpansionPanelDescription, - // MatExpansionPanelTitle, - // MatButtonModule, - // MatCheckboxModule, - // MatExpansionModule, - // MatFormFieldModule, - // MatInputModule, - // MatProgressBarModule, - // MatSelectModule, - // MatSidenavModule, - // MatTooltipModule, - // MatButtonToggleModule, - // MatGridListModule, - // NgxDropzoneModule, - // MatTableModule, - // ], - styleUrls: ['./spongeffects.component.scss'] -}) - -export class SpongEffectsComponent implements OnInit, AfterViewInit { - // API - constructor(private backend: BackendService, private renderer: Renderer2) { - this.diseases = this.backend.getDatasets(); - this.overallCounts = this.backend.getOverallCounts(); - this.cancerInfoAvailable = this.initCancerInfo(); - } - - tabs: Tab[] = [ - {value: "explore", viewValue: "Explore", icon: "../../../assets/img/magnifying_glass.png"}, - {value: "predict", viewValue: "Predict", icon: "../../../assets/img/chip-intelligence-processor-svgrepo-com.png"} - ] - selectedTab: Tab = this.tabs[0]; - source: string = "TCGA"; - cancers: Cancer[] = []; - selectedCancer: Cancer | undefined = undefined; - levels: string[] = ["Gene", "Transcript"]; - level: string = this.levels[0]; - exploreResultsQueried: boolean = false; - - performanceMeasures: SelectElement[] = [ - {value: 'balanced_accuracy', viewValue: "Balanced Accuracy"}, - {value: 'detection_prevalence', viewValue: "Detection Prevalence"}, - {value: 'detection_rate', viewValue: "Detection Rate"}, - {value: 'f1', viewValue: "F1"}, - {value: 'neg_pred_value', viewValue: "Negative Prediction Value"}, - {value: 'pos_pred_value', viewValue: "Positive Prediction Value"}, - {value: 'precision_value', viewValue: "Precision"}, - {value: 'prevalence', viewValue: "Prevalence"}, - {value: 'recall', viewValue: "Recall"}, - {value: 'sensitivity', viewValue: "Sensitivity"}, - {value: 'specificity', viewValue: "Specificity"} - ]; - performanceMeasure: SelectElement = this.performanceMeasures[0]; - performanceSelectPanelIsOpen: boolean = false; - includeModuleMembers: boolean = false; - // loading toggles - overallAccuracyLoading: boolean = true; - classPerformanceLoading: boolean = true; - enrichmentScoreDensityLoading: boolean = true; - lollipopLoading: boolean = true; - expressionsLoading: boolean = true; - elementExpressionLoading: boolean = true; - - // plot divs - @ViewChild("sampleDistributionPie") sampleDistributionPieDiv!: ElementRef; - @ViewChild("overallAccuracyPlot") overallAccPlotDiv!: ElementRef; - @ViewChild("classModelPerformancePlot") classPerformancePlotDiv!: ElementRef; - @ViewChild("enrichmentScoresByClassPlot") enrichmentScoresByClassPlotDiv!: ElementRef; - @ViewChild("lollipopPlot") lollipopPlotDiv!: ElementRef; - @ViewChild("moduleExpressionHeatmapDiv") moduleExpressionHeatmap!: ElementRef; - @ViewChild("moduleMiRnaExpressionDiv") moduleMiRnaExpressionPlot!: ElementRef; - - // lollipop plot - modulesData!: Promise; - filteredModulesData!: SpongEffectsGeneModules[]; - - markControl: FormControl = new FormControl(3, [Validators.min(1.0), Validators.max(20)]); - topControl: FormControl = new FormControl(200, [Validators.min(3.0), Validators.max(1000)]); - - selectedModules: SpongEffectsGeneModules[] = []; - modulesTableColumns: string[] = [ - "ensemblID", - "symbol", - "meanGiniDecrease", - "meanAccuracyDecrease", - "description" - ]; - dynamicModulesData: ModuleDataSource = new ModuleDataSource(this.selectedModules); - - selectedEnsemblId: SpongEffectsGeneModules | undefined = undefined; - - // plot parameters - defaultPlotMode: string = "lines+markers"; - defaultLineWidth: number = 6; - defaultMarkerSize: number = 12; - - /* predict variables */ - // plots - @ViewChild("typePredictPie") - typePredictPiePlot!: ElementRef; - predictSubtypes: boolean = false; - logScaling: boolean = true; - - progressBarMode: ProgressBarMode = "determinate"; - progressBarValue: number = 0; - estimatedRunTime: number = 0; - - // loading values - predictionQueried: boolean = false; - predictionLoading: boolean = false; - timerRunning: boolean = false; - // file variables - uploadedExpressionFiles: File[] = []; - filesToAccept: string = "text/*,application/*"; - maxFileSize: number = 100000000; - // prediction data - predictionData: any; - predictionMeta: any; - predictedType: string = "None"; - predictedSubtype: string = "None"; - - // default parameters - mscorDefault: number = 0.1; - fdrDefault: number = 0.05; - minSizeDefault: number = 100; - maxSizeDefault: number = 2000; - minExprDefault: number = 10; - methods: string[] = ["gsva", "ssgsea", "OE"]; - methodDefault: string = this.methods[0]; - showExpressionExample: boolean = false; - - mscorControl: FormControl = new FormControl(this.mscorDefault, [Validators.min(0.0), Validators.max(10.0)]); - fdrControl: FormControl = new FormControl(this.fdrDefault, [Validators.min(0.0), Validators.max(0.5)]); - minExprControl: FormControl = new FormControl(this.minExprDefault, [Validators.min(0.0), Validators.max(1000)]); - minSizeControl: FormControl = new FormControl(this.minSizeDefault, [Validators.min(0.0), Validators.max(5000)]); - maxSizeControl: FormControl = new FormControl(this.maxSizeDefault, [Validators.min(0.0), Validators.max(5000)]); - // TODO: test runtime of other methods - - methodFunctions: Map = new Map( - [ - [this.methods[0], {slope: 0.7, x0: 15}], - [this.methods[1], {slope: 1, x0: 20}], - [this.methods[2], {slope: 2, x0: 20}], - ] - ); - - exampleExpressionData: MatTableDataSource = new MatTableDataSource(EXAMPLE_GENE_EXPR); - displayedCols: string[] = ["id", "sample1", "sample2", "sample3", "sample4", "sampleN"]; - displayedColsValueMap: Map = new Map([ - ["id", ""], - ["sample1", "sample1"], ["sample2", "sample2"], ["sample3", "sample3"], ["sample4", "sample4"], ["sampleN", "sampleN"], - ]); - - cancerInfoAvailable: Promise = new Promise(() => {}); - - ngOnInit(): void { - } - -// /** -// * retrieve TCGA cancer data from API -// */ - private async initCancerInfo() { - let response = await this.backend.getDatasetsInformation(this.source); - let cancerMap: Map = new Map(); - - response.forEach((entry: { - study_abbreviation: string, disease_name: string, disease_subtype: string, - number_of_samples: number - }) => { - // fill cancer map - let cancer = cancerMap.get(entry.study_abbreviation); - if (cancer) { - if (entry.disease_subtype != null) { - cancer.addSubtype(entry.disease_subtype); - cancer.addSampleSize(entry.number_of_samples); - } - } else { - cancerMap.set(entry.study_abbreviation, - new Cancer(entry.study_abbreviation, entry.disease_name, - [entry.disease_subtype], [entry.number_of_samples])); - } - }); - // set PANCAN subtypes - const panCanCancer = cancerMap.get('PANCAN'); - if (!panCanCancer) { - console.error("PANCAN cancer not found in response"); - } else { - panCanCancer.allSubTypes = [...cancerMap.keys()].filter(v => v != 'PANCAN'); - panCanCancer.sampleSizes = [...cancerMap.entries()] - .filter((value) => value[0] != 'PANCAN') - .map(c => c[1].totalNumberOfSamples()) - } - // remove null subtypes - cancerMap.forEach((value) => { - value.allSubTypes = value.allSubTypes.filter(v => v != null); - }); - // set class variable with spongEffects compatible cancer types - this.cancers = [...cancerMap.values()].filter(c => spongEffectsCancerAbbreviations.includes(c.value)); - this.selectedCancer = this.cancers[0]; - } - - ngAfterViewInit() { - if (this.selectedCancer != undefined) { - this.cancerInfoAvailable.then(_ => this.cancerSelected(this.selectedCancer)); - } - } - - setTab(tab: Tab) { - this.selectedTab = tab; - // reset explore tab - if (tab.value == this.tabs[0].value && this.selectedCancer != undefined) { - this.cancerSelected(this.selectedCancer).then(_ => this.exploreResultsQueried = true); - } - } - - setLevel(level: string) { - this.level = level; - if (this.selectedCancer != undefined) { - this.cancerSelected(this.selectedCancer).then(_ => this.exploreResultsQueried = true); - } - } - - - getGeneCardLink(gene: string): string { - return `https://www.genecards.org/cgi-bin/carddisp.pl?gene=${gene}`; - } - - getLevelButtonStyle(level: string): string { - if (level == this.level) { - return "background-color: #1e719b; color: white"; - } else { - return "background-color: white"; - } - } - - getCancerImage() { - if (this.selectedCancer == undefined) return "../../../assets/img/spongEffects_logo.png"; - return "../../../assets/img/TCGA/" + this.selectedCancer.value + ".png"; - } - - setPreviewCancer(cancer: Cancer) { - this.selectedCancer = cancer; - this.plotSampleDistribution(this.getSampleDistributionData()); - } - - clearResults() { - // allow new queries - this.exploreResultsQueried = false; - // clear plot divs - this.overallAccPlotDiv.nativeElement.innerHTML = ""; - this.enrichmentScoresByClassPlotDiv.nativeElement.innerHTML = ""; - this.lollipopPlotDiv.nativeElement.innerHTML = ""; - } - - async plotResults() { - if (this.exploreResultsQueried) return null; - // get accuracy data from API - const acData: Promise = this.getOverallAccuracyData(); - // show according plot - this.plotOverallAccuracyPlot(acData).then(_ => this.overallAccuracyLoading = false); - // plot class specific performance - this.plotModelClassPerformance().then(_ => this.classPerformanceLoading = false); - // get spongEffect scores from API - const enrichmentScores: Promise> = this.getEnrichmentClassDensities(); - // plot enrichment score class distributions - this.plotEnrichmentScoresByClass(enrichmentScores).then(_ => this.enrichmentScoreDensityLoading = false); - // get top centralities - this.modulesData = this.getMeanGiniDecrease(); - // plot lollipop plot - this.plotLollipop(this.topControl.value).then(_ => this.lollipopLoading = false); - } - - async clearSelection() { - this.selectedModules = []; - this.resetLollipop(); - this.dynamicModulesData.setData(this.selectedModules); - } - - async exploreExpression() { - // plot module expression - this.getModulesExpression() - .then(config => this.plotModuleExpression(config)) - .then(_ => this.expressionsLoading = false); - } - - async cancerSelected(cancer: Cancer) { - this.clearResults(); - this.selectedCancer = cancer; - this.selectedEnsemblId = undefined; - // load sample distribution - let sampleDistData: PlotlyData = this.getSampleDistributionData(); - // plot sample distribution pie - this.plotSampleDistribution(sampleDistData); - this.plotResults().then(_ => this.exploreResultsQueried = true); - } - - async resetLollipop() { - this.plotLollipop(this.topControl.value).then(_ => this.lollipopLoading = false); - } - - getRandomID(l: number): string { - let result: string = 'TCGA-'; - const characters: string = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; - const charactersLength: number = characters.length; - let counter: number = 0; - while (counter < l) { - result += characters.charAt(Math.floor(Math.random() * charactersLength)); - counter += 1; - } - return result; - } - - getRandomIDs(n: number, l: number): string[] { - return Array.from(Array(n).keys()).map(i => this.getRandomID(l)); - } - - getRandomElements(n: number, type: string) { - let base: string = "ENS"; - let l: number = 4; - if (type == "gene") { - base += "G"; - } else if (type == "transcript") { - base += "T" - } else if (type == "mirna") { - base = "hsa_mir_" - } else { - throw new Error("type argument has to be either gene or transcript"); - } - base+="000000" - return Array.from(Array(n).keys()).map(i => base+this.getRandomValues(l, 0, 9, true).join("")); - } - - getRandomValue(min: number = 0, max: number = 1, round: boolean = false): number { - const randomValue: number = Math.random(); - return round ? Math.floor((max - (min)) * randomValue + (min)): (max - (min)) * randomValue + (min); - } - - getRandomValues(n: number, min: number = 0, max: number = 1, round: boolean = false): number[] { - return Array.from(Array(n).keys()).map(i => this.getRandomValue(min, max, round)); - } - - getSampleDistributionData(): PlotlyData { - if (this.selectedCancer == undefined) { - return {data: [], layout: {}, config: {}}; - } - let data = [ - { - values: this.selectedCancer.sampleSizes, - labels: this.selectedCancer.allSubTypes, - type: "pie", - hoverinfo: 'label+value+percent', - textinfo: 'none' - } - ]; - let layout = { - autosize: true, - showlegend: true, - legend: {"orientation": "h"}, - margin: { - b: 5, - l: 5, - r: 5, - t: 50 - }, - title: { - text: "Sample distribution of " + this.selectedCancer.value + " (" + this.selectedCancer.totalNumberOfSamples() + " samples)", - font: { - size: 14 - } - } - }; - let config = { - responsive: true - } - return {data: data, layout: layout, config: config}; - } - - async getOverallAccuracyData(): Promise { - this.overallAccuracyLoading = true; - if (this.selectedCancer == undefined) { - return []; - } - const modelPerformances = await this.backend.getOverallAccuracyData(this.selectedCancer.viewValue, this.level); - return modelPerformances.map((entry: RunPerformance, idx: number): Metric => { - return { - name: entry.model_type, - split: entry.split_type, - lower: entry.accuracy_lower, - upper: entry.accuracy_upper, - level: idx - } - }); - } - - async cancelClick(event: any) { - event.stopPropagation(); - } - - async rePlotModelClassPerformance(): Promise { - this.plotModelClassPerformance().then(_ => this.classPerformanceLoading = false); - } - - async plotModelClassPerformance(): Promise { - this.classPerformanceLoading = true; - if (this.selectedCancer == undefined) { - return null; - } - // get data - const performanceData = await this.backend.getRunClassPerformance(this.selectedCancer.viewValue, this.level); - // transform data - const data: PlotlyData = await this.getModelClassPerformancePlotData(performanceData); - // plot data - Plotly.newPlot(this.classPerformancePlotDiv.nativeElement, data.data, data.layout, data.config); - } - - async togglePanel(event: MouseEvent, panel: MatExpansionPanel) { - event.stopPropagation(); - if (this.performanceSelectPanelIsOpen) { - panel.open() - this.resetClassAccPlot(); - } else { - panel.close() - } - } - - async getModelClassPerformancePlotData(performanceData: any[]): Promise { - // group the data by model type - const traceGroups: { [key: string]: any[] } = {}; - performanceData.forEach(entry => { - const modelType = entry.spongEffects_run.model_type; - if (!traceGroups[modelType]) { - traceGroups[modelType] = []; - } - traceGroups[modelType].push(entry); - }); - // build actual traces - const traces = []; - for (const modelType in traceGroups) { - if (traceGroups.hasOwnProperty(modelType)) { - const group = traceGroups[modelType]; - - const trace = { - x: group.map(entry => entry.prediction_class), - y: group.map(entry => entry[this.performanceMeasure.value]), - type: 'bar', - name: modelType, - }; - - traces.push(trace); - } - } - const meanTextLength: number = Math.round(sum(traces[0].x.map(d => d.length))/traces[0].x.length); - const textPad: number = meanTextLength*10.5; - const containerWidth = this.renderer.selectRootElement(this.classPerformancePlotDiv.nativeElement).offsetWidth; - const angle: number = meanTextLength > 15 ? 90: 0; - const layout = { - barmode: 'group', - autosize: true, - width: containerWidth, - xaxis: { - autosize: true, - tickangle: angle - }, - yaxis: { - title: this.performanceMeasure.viewValue - }, - margin: { - t: 25, - b: textPad, - l: 50, - r: 0 - }, - legend: { - orientation: "h", - x: 0.5, - y: 1.25 - } - }; - const config = { responsive: true }; - return {data: traces, layout: layout, config: config}; - } - - async resetClassAccPlot() { - Plotly.update(this.classPerformancePlotDiv.nativeElement); - } - - private async getEnrichmentClassDensities(): Promise> { - this.enrichmentScoreDensityLoading = true; - if (this.selectedCancer == undefined) { - return new Map(); - } - const queryResponse: EnrichmentScoreDistributions[] = await this.backend.getEnrichmentScoreDistributions(this.selectedCancer.viewValue, this.level); - const classDensities: Map = new Map(); - queryResponse.forEach((entry: { prediction_class: string; enrichment_score: any; density: any; }) => { - if (classDensities.has(entry.prediction_class) && classDensities.get(entry.prediction_class) != undefined) { - const pred_class: PlotData = classDensities.get(entry.prediction_class); - pred_class.x.push(entry.enrichment_score) - pred_class.y.push(entry.density) - } else { - classDensities.set(entry.prediction_class, { - x: [entry.enrichment_score], y: [entry.density] - }); - } - }); - return classDensities; - } - - async getMeanGiniDecrease(): Promise { - if (this.selectedCancer == undefined) { - return []; - } - this.lollipopLoading = true; - return await this.backend.getSpongEffectsGeneModules(this.selectedCancer.viewValue); - } - - async getModuleMembers(): Promise> { - if (this.selectedCancer == undefined || this.selectedModules.length == 0) { - return new Map(); - } - const members: Map = new Map(); - if (this.level == 'gene') { - const response: SpongEffectsGeneModuleMembers[] = await this.backend.getSpongEffectsGeneModuleMembers( - this.selectedCancer.viewValue, - this.selectedModules.map(s => s.ensg_number) - ) - const key: string = "hub_ensg_number"; - response.forEach((e: SpongEffectsGeneModuleMembers) => { - if (!members.has(e[key])) members.set(e[key], []); - const toPush: string[] = [e.member_ensg_number, e.member_gene_symbol] - const memberList = members.get(e[key]); - if (memberList) { - memberList.push(...toPush); - } - }); - } else { - // TODO - const response: SpongEffectsTranscriptModuleMembers[] = await this.backend.getSpongEffectsTranscriptModuleMembers( - this.selectedCancer.viewValue, - this.selectedModules.map(s => s.enst_number) - ) - } - return members; - } - - async getModulesExpression(): Promise { - // TODO - this.expressionsLoading = true; - let apiCall: any; - let key: string; - let elements: string[] = this.selectedModules.map(s => s.ensg_number); - // include module members in heatmap - if (this.includeModuleMembers) { - const members: Map = await this.getModuleMembers(); - const memberValues: string[] = [].concat([...members.values()]); - elements.push(...memberValues); - } - if (this.level.toLowerCase() == "gene") { - key = "ensg_number"; - apiCall = this.backend.getCeRNAExpression(this.selectedCancer.viewValue, - undefined, elements, undefined, this.level); - } else { - key = "enst_number"; - apiCall = this.backend.getTranscriptExpression(this.selectedCancer.viewValue, - elements, undefined, undefined, this.level); - } - // await apiCall - const apiResponse = await apiCall; - // split into (sub-)types - const typeSplit: Map = new Map(); - apiResponse.forEach(entry => { - const dataset: string = entry.dataset; - if (!typeSplit.has(dataset) || typeSplit.get(dataset) == undefined) { - typeSplit.set(dataset, []); - } - typeSplit.get(dataset).push(entry); - }); - - // build traces for each dataset - let data: any[] = []; - typeSplit.forEach((entry: any[], dataset: string) => { - // transform data of entries - let xSamples: string[] = []; - const xSet: Set = new Set(); - let nX: number = -1; - let yElements: string[] = []; - const ySet: Set = new Set(); - let nY: number = -1; - let zValues: number[][] = [[]]; - - entry.forEach(e => { - if (!xSet.has(e.sample_ID)) { - xSamples.push(e.sample_ID); - nX += 1; - } - if (!ySet.has(e.gene[key])) { - yElements.push(e.gene.gene_symbol + " (" + e.gene[key] + ")"); - nY += 1; - } - zValues[nY][nX] = e.expr_value; - }); - // add trace - data.push({ - z: zValues, - x: xSamples, - y: yElements, - type: "heatmap", - hoverongaps: false, - name: dataset, - showscale: false - }); - }); - // only show scale on last heatmap - data[-1].showscale = true; - // add x-axis subplot for each trace - data.slice(1).forEach((d, i) => { - let idx: string = (i+2).toString(); - d.xaxis = 'x' + idx - }); - // set layout options - const layout = { - autosize: true, - showlegend: true, - legend: {orientation: "h"}, - xaxis: { - showgrid: false, - showticklabels: false, - showticks: false - }, - margin: { - b: 125, - l: 125, - r: 50, - t: 50 - }, - title: { - text: "Module expression of selected modules", - font: { - size: 14 - } - }, - grid: { - rows: 1, - columns: data.length - }, - annotations: [] - }; - let config = { - responsive: true - } - return {data: data, layout: layout, config: config}; - } - - getColors(n: number): string[] { - const colors: string[] = []; - const step: number = 360 / n; - const saturation: number = 1; - const lightness: number = 0.6 - for (let i = 0; i < n; i++) { - const color = d3.hsl(i * step, saturation, lightness); - colors.push(color.toString()); - } - return colors; - } - - /** - * returns module member expression data for a given ensemblId - */ - getModuleExpressionDataTest(ensemblId: string): PlotlyData { - // TODO: get data from API and remove testing data - - if (this.selectedCancer == undefined) { - return {data: [], layout: {}, config: {}}; - } - - let data: any[] = []; - const y: number = this.getRandomValue(2, 10, true); - let y_elements: string[] = this.getRandomElements(y, this.level.toLowerCase()); - const legendGroup: string = "commonGroup"; - let x0: number = 0; - const gapWidth: number = 0.2; - const traceWidth: number = 0.6; - - this.selectedCancer.allSubTypes.forEach((subtype, index) => { - const x: number = this.getRandomValue(2, 10, true); - let x_samples: string[] = this.getRandomIDs(x, 4); - let z_values: number[][] = Array.from(Array(y).keys()).map(_ => this.getRandomValues(x, 0, 15)); - data.push( - { - x: x_samples, - y: y_elements, - z: z_values, - name: subtype, - type: "heatmap", - hoverongaps: false, - x0: x0, - dx: traceWidth - } - ); - x0 += traceWidth + gapWidth; - }) - - const layout = { - autosize: true, - showlegend: true, - xaxis: { - showgrid: false, - showticklabels: false, - showticks: false - }, - margin: { - b: 125, - l: 125, - r: 50, - t: 50 - }, - title: { - text: "Module expression of hub-node " + this.selectedEnsemblId, - font: { - size: 14 - } - }, - annotations: [] - }; - let config = { - responsive: true - } - // add type groups to heatmap - const traceColors: string[] = this.getColors(this.selectedCancer.allSubTypes.length); - const yGap: number = 0.1; - const boxHeight: number = 20; - const widthPerSample: number = 2; - - data.forEach((trace, index) => { - for (let idx = 0; idx < trace.x.length; idx++) { - layout.annotations.push({ - x: trace.x[idx], - y: trace.y.length + yGap, - hovertext: trace.name, - xref: 'x', - yref: 'y', - text: '', - showarrow: false, - align: 'center', - bgcolor: traceColors[index], - width: widthPerSample, - height: boxHeight - }); - } - }); - return {data: data, layout: layout, config: config}; - } - - getModuleMiRnaExpressionDataTest(hubId: string): PlotlyData{ - if (this.selectedCancer == undefined) { - return {data: [], layout: {}, config: {}}; - } - let miRNAs: string[] = this.getRandomElements(this.getRandomValue(10, 20, true), "mirna"); - let data: any[] = []; - this.selectedCancer.allSubTypes.forEach(subtype => { - data.push({ - x: miRNAs, - y: this.getRandomValues(miRNAs.length, 0, 12), - name: subtype, - type: "bar" - }); - }); - let layout = { - autosize: true, - margin: { - b: 5, - l: 5, - r: 5, - t: 50 - }, - title: { - text: "Module miRNA expression of " + this.selectedEnsemblId, - font: { - size: 14 - } - }, - barmode: "group" - }; - let config = { - responsive: true - }; - return {data: data, layout: layout, config: config} - } - - plotSampleDistribution(config: PlotlyData) { - Plotly.newPlot(this.sampleDistributionPieDiv.nativeElement, config.data, config.layout, config.config); - } - - plotModuleExpression(config: PlotlyData) { - Plotly.newPlot(this.moduleExpressionHeatmap.nativeElement, config.data, config.layout, config.config); - } - - plotModuleMiRnaExpression(config: PlotlyData) { - Plotly.newPlot(this.moduleMiRnaExpressionPlot.nativeElement, config.data, config.layout, config.config); - } - - async plotOverallAccuracyPlot(metricData: Promise) { - // set main layout options - let layout = { - autosize: true, - yaxis: { - showline: false, - showticklabels: false - }, - margin: { - t: 0, - b: 40, - l: 30, - r: 20 - }, - annotations: [ - // x-axis label - { - xref: "paper", - yref: "paper", - x: 0.5, - y: -0.1, - xanchor: "center", - yanchor: "top", - text: "Overall model accuracy", - showarrow: false - } - ], - legend: { - traceorder: "reversed" - } - }; - let metrics: Metric[] = await metricData; - let data = metrics.map(metric => { - const col: string = metric.name == "modules" ? "green": "orange" - // data points - return { - x: [metric.lower, metric.upper], - y: [metric.level, metric.level], - mode: this.defaultPlotMode, - name: metric.name + " (" + metric.split + ")", - text: ["Lower Bound (Accuracy)", "Upper Bound (Accuracy)"], - hovertemplate: "%{text}: %{x:.2f}", - line: { - width: this.defaultLineWidth, - color: col, - dash: metric.split == "train" ? "solid": "dash" - }, - marker: { - size: this.defaultMarkerSize, - symbol: ['circle', 'diamond'], - color: col - }, - showlegend: true - } - }); - const config = { responsive: true }; - // remove loading spinner and show plot - Plotly.newPlot("overall-acc", data, layout, config); - } - - async resetOverallAccPlot() { - Plotly.update(this.overallAccPlotDiv.nativeElement) - } - - async plotEnrichmentScoresByClass(enrichmentData: Promise>) { - // fill subtype specific data - let data: any[] = []; - const enrichmentDataResponse = await enrichmentData; - enrichmentDataResponse.forEach((plotData, subtype) => { - // push trace for each subtype - data.push({ - x: plotData.x, - y: plotData.y, - fill: "tozeroy", - type: "scatter", - mode: "lines", - opacity: 0.8, - name: subtype - }); - }); - // add subplot for each trace - data.slice(1).forEach((d, i) => { - let idx: string = (i+2).toString(); - d.xaxis = 'x' + idx - d.yaxis = 'y' + idx - }); - // determine range of display - let minScore: number = Math.round(min(data.map(d => min(d.x)))); - let maxScore: number = Math.round(max(data.map(d => max(d.x)))); - const plot_height: number = data.length * 50; - // set general layout options - let layout = { - showlegend: false, - autosize: true, - legend: {"orientation": "h"}, - grid: { - rows: data.length, - columns: 1, - pattern: 'independent', - roworder: 'bottom to top' - }, - height: plot_height, - title: "spongEffects enrichment score density for predictive classes" - }; - // set constant y axis layout - const y_axis_layout = { - showgrid: false, - automargin: true, - showticklabels: false, - }; - const annotations = []; - // add layout to each trace - data.forEach((d, index) => { - let x_axis_layout_i = { - range: [minScore, maxScore], - showgrid: false, - showticklabels: false - }; - let x_key: string = "xaxis"; - let y_key: string = "yaxis"; - let x: string = "x"; - let y: string = "y"; - if (index != 0) { - x_key = x_key + (index + 1).toString(); - y_key = y_key + (index + 1).toString(); - x = x + (index + 1).toString(); - y = y + (index + 1).toString(); - } else { - x_axis_layout_i["title"] = "spongEffects enrichment score"; - x_axis_layout_i.showticklabels = true; - } - layout[x_key] = x_axis_layout_i; - layout[y_key] = y_axis_layout; - // add class annotation - annotations.push({ - xref: x, - yref: y, - x: minScore + 1.5, - y: 0.5, - text: d.name, - align: "left", - showarrow: false, - width: 250 - }) - }); - layout["annotations"] = annotations; - const config = { responsive: true } - Plotly.newPlot(this.enrichmentScoresByClassPlotDiv.nativeElement, data, layout, config); - } - - async resetEnrichmentScoreByClass() { - Plotly.update(this.enrichmentScoresByClassPlotDiv.nativeElement); - } - - async plotLollipop(n: number) { - let giniData: SpongEffectsGeneModules[] = await this.modulesData; - const redNodes: number = this.markControl.value; - // only use selected number of top genes - let idx: number = n > giniData.length ? giniData.length: n; - giniData = giniData.slice(0, idx); - // set selected elements - this.addModules(giniData.slice(0, redNodes)) - - // selected elements - this.filteredModulesData = giniData; - - // set main layout options - let data: any[] = [{ - x: giniData.map(g => g.mean_gini_decrease), - y: giniData.map(g => g.mean_accuracy_decrease), - mode: "markers", - type: "scatter", - name: giniData.map(g => g.gene_symbol), - text: giniData.map(g => g.gene_symbol), - marker: { - size: this.defaultMarkerSize, - color: giniData.map(g => this.selectedModules.includes(g) ? "red": "grey") - } - }]; - // set main layout options - let layout = { - showlegend: false, - autosize: true, - hovermode: "closest", - margin: { - b: 50, - l: 75, - r: 25, - t: 0 - }, - xaxis: { - title: "Mean decrease in Gini-index" - }, - yaxis: { - title: "Mean decrease in accuracy" - } - } - let config = { - responsive: true - } - Plotly.newPlot(this.lollipopPlotDiv.nativeElement, data, layout, config); - - // add click handler - this.lollipopPlotDiv.nativeElement.on("plotly_click", (eventData) => { - const clickedSymbol: string = eventData.points[0].toString(); - // get modules with clicked element - const modules: SpongEffectsGeneModules[] = giniData.filter(c => c.gene_symbol == clickedSymbol); - this.addModules(modules, clickedSymbol); - // toggle color of clicked point - const updatedData = this.lollipopPlotDiv.nativeElement.data; - updatedData[0].marker.color = giniData.map(g => this.selectedModules.includes(g) ? "red": "grey"); - // update plot - Plotly.redraw(this.lollipopPlotDiv.nativeElement, updatedData, layout, config); - }); - } - - addModules(modules: SpongEffectsGeneModules[], clickedSymbol?: string) { - if (clickedSymbol == undefined) { - this.selectedModules = modules; - } else { - // modules are not inserted yet - if (this.selectedModules.filter(s => s.gene_symbol == clickedSymbol).length == 0) { - // add new modules - this.selectedModules.push(...modules); - } else { - // remove clicked modules - this.selectedModules = this.selectedModules.filter(s => s.gene_symbol != clickedSymbol); - } - } - // update dynamic table data - this.dynamicModulesData.setData(this.selectedModules); - } - - // predict functions - onExpressionUpload(event: any) { - this.uploadedExpressionFiles.push(...event.addedFiles); - // TODO: check format - } - - expressionUploaded(): boolean { - return this.uploadedExpressionFiles.length > 0; - } - - onRemoveExpression(event) { - this.uploadedExpressionFiles.splice(this.uploadedExpressionFiles.indexOf(event), 1); - this.predictionQueried = false; - } - - acceptExpressionFiles(): string { - return this.expressionUploaded() ? "none" : this.filesToAccept; - } - - flipExampleExpression() { - this.showExpressionExample = !this.showExpressionExample; - } - - estimateRunTime() { - const fileSize: number = this.uploadedExpressionFiles[0].size / (1024**2); - const refSlope: number = 0.7; - const x0: number = 17; - const st: number = this.predictSubtypes ? 4 : 1; - return refSlope * fileSize + x0; - } - - async getPredictionData(): Promise { - const uploadedFile: File = this.uploadedExpressionFiles[0]; - // send file and parameters to API and return response - return this.backend.predictCancerType( - uploadedFile, this.predictSubtypes, this.logScaling, - this.mscorControl.value, this.fdrControl.value, this.minSizeControl.value, - this.maxSizeControl.value, this.minExprControl.value, this.methodDefault - ) - } - - - getColorForValue(value: number): string { - let g: number = 140; - let r: number = value >= 0.5 ? Math.round(255*2 * (1 - value)): 255; - const b: number = 0; - return `rgb(${r},${g},${b})`; - } - - async extractPredictions(responseJson: any): Promise { - const typeGroups: Map = new Map(); - // group predictions by type - responseJson.data.forEach(entry => { - if (typeGroups.has(entry.typePrediction)) { - typeGroups.get(entry.typePrediction)?.push(entry.subtypePrediction); - } - }); - - const typeCounts: Map = new Map([...typeGroups.entries()].map(entry => { - return [entry[0], entry[1].length]; - })); - // sort by amount of samples - const sortedTypeCounts: Map = new Map([...typeCounts.entries()].sort((a, b) => a[1] - b[1])); - let x: number[] = [...sortedTypeCounts.values()]; - let y: string[] = [...sortedTypeCounts.keys()]; - // add model accuracy - let classPerformanceData = this.classPerformancePlotDiv.nativeElement.data; - // get modules data - classPerformanceData = classPerformanceData.filter(d => d.name == "modules") - if (classPerformanceData.length > 0) { - classPerformanceData = classPerformanceData[0]; - } - // create map to value - const classToMeasure: Map = new Map(); - for (let i = 0; i < classPerformanceData.x.length; i++) { - classToMeasure.set(classPerformanceData.x[i], classPerformanceData.y[i]); - } - const accValues: number[] = y.map(x_v => classToMeasure.get(x_v) ?? 0); // color based on balanced accuracy - const barColors: string[] = accValues.map(v => this.getColorForValue(v)); - // transform data - let data = [{ - x: x, - y: y, - text: accValues.map(v => "Balanced accuracy: " + v.toString()), - type: "bar", - name: "type", - orientation: "h", - marker: { - color: barColors - } - }]; - - // add subtype traces - if (this.predictSubtypes) { - const subtypeTraces: any[] = [...typeGroups.values()].map(sv => { - return { - x: sv.length, - y: y, - text: sv, - name: "subtypes", - orientation: "h" - } - }); - data.push(...subtypeTraces); - } - - const layout = { - paper_bgcolor: "white", - autosize: true, - barmode: "group", - margin: { - l: 250, - r: 25, - t: 50, - b: 50 - }, - xaxis: { - title: "Number of samples classified" - } - }; - const config = { - responsive: true - } - return {data: data, layout: layout, config: config}; - } - - async plotPredictions(plotlyData: PlotlyData): Promise { - Plotly.newPlot(this.typePredictPiePlot.nativeElement, plotlyData.data, plotlyData.layout, plotlyData.config); - } - - async processPredictions(predictionResponse: any): Promise { - // check response - if (!predictionResponse.ok) { - throw new Error(`File upload failed with status code: ${predictionResponse.status}`); - } - // save results - const predictionData = await predictionResponse.json(); - this.predictionData = predictionData.data; - this.predictionMeta = predictionData.meta[0]; - this.predictedType = predictionData.meta[0].type_predict; - this.predictedSubtype = predictionData.meta[0].subtype_predict; - // plot predictions - this.extractPredictions(predictionData) - .then(data => this.plotPredictions(data)); - } - - async startTimer(): Promise { - this.timerRunning = true; - this.progressBarValue = 0; - const totalRunTime: number = this.estimateRunTime(); - this.estimatedRunTime = totalRunTime; - const interval: number = (1000 * totalRunTime) / 100; - const progressBarTimer = timer(0, interval); - progressBarTimer.subscribe(() => { - this.estimatedRunTime = totalRunTime * (100-this.progressBarValue)/100; - if (this.progressBarValue < 100) this.progressBarValue++; - }); - } - - runButtonDisabled(): boolean { - return !this.expressionUploaded() || this.predictionLoading; - } - - estimatedRunTimeText(): string { - return this.estimatedRunTime > 0 ? Math.round(this.estimatedRunTime).toString()+"s": "Any moment...hopefully" - } - - async predict() { - this.predictionQueried = true; - this.predictionLoading = true; - // start timer of estimated run time - this.startTimer().then(_ => this.timerRunning = false); - // start workflow - this.getPredictionData() - .then(data => this.processPredictions(data)) - .then(_ => this.predictionLoading = false); - } - - buttonText(btn: string) { - if (btn == "expr") { - return this.showExpressionExample ? "Hide example file" : "Show example file"; - } - else { - return ""; - } - } - - getCancerInfoText(): CancerInfo { - if (this.selectedCancer == undefined) return {text: ["loading DB data..."], link: ""}; - let cancerInfo: CancerInfo; - switch (this.selectedCancer.value) { - case "PANCAN": { - let texts: string[] = [ - "The Pan-cancer project includes the combined data of 33 of the most common cancer forms in humans.", - ]; - cancerInfo = {text: texts, link: "https://doi.org/10.1016/j.cell.2018.03.022"}; - break; - } - case "BRCA": { - let texts: string[] = [ - "Breast cancer is the most frequently observed cancer in women and one of the main causes of death in women.", - ]; - cancerInfo = {text: texts, link: "https://www.nature.com/articles/nature11412"}; - break; - } - case "CESC": { - let texts: string[] = [ - "Cervical cancer is a form of cancer that develops in cervix tissues, i.e. the lower area of the uterus.", - ]; - cancerInfo = {text: texts, link: "https://www.nature.com/articles/nature21386"}; - break; - } - case "ESCA": { - let texts: string[] = [ - "The TCGA research revealed two predominant forms of esophageal cancer: squamous cell carcinoma, originating from the flat epithelial cells lining the esophagus, and adenocarcinoma, originating from the glandular cells responsible for producing mucus and various fluids.", - ]; - cancerInfo = {text: texts, link: "https://www.nature.com/articles/nature20805"}; - break; - } - case "HNSC": { - let texts: string[] = [ - "The majority of head and neck cancers initiate in the moist, mucous membranes that line the interior of the mouth, nasal passages, and throat. These membranes consist of squamous cells, and the head and neck cancers that develop within these cells are classified as squamous cell carcinomas.", - ]; - cancerInfo = {text: texts, link: "https://www.nature.com/articles/nature14129"}; - break; - } - case "LGG": { - let texts: string[] = [ - "Glioma is a form of cancer originating in the brain's glial cells, which play a vital role in supporting and maintaining the health of the brain's nerve cells. Tumors are categorized into grades I, II, III, or IV in accordance with criteria established by the World Health Organization. In this research, TCGA specifically investigated lower-grade gliomas, encompassing grades II and III.", - ]; - cancerInfo = {text: texts, link: "https://www.nejm.org/doi/full/10.1056/NEJMoa1402121"}; - break; - } - case "SARC": { - let texts: string[] = [ - "The term \"sarcoma\" includes a wide range of uncommon cancers that have the potential to impact soft tissues, bone structures, or even both, spanning various parts of the body.", - ]; - cancerInfo = {text: texts, link: "https://www.cell.com/cell/fulltext/S0092-8674(17)31203-5"}; - break; - } - case "STAD": { - let texts: string[] = [ - "The occurrence of stomach cancer displays significant variation influenced by a combination of genetic and environmental factors. This type of cancer is more commonly found in men, elderly individuals, and those with a familial predisposition to the disease. On a global scale, the incidence of stomach cancer differs by geographic region.", - ]; - cancerInfo = {text: texts, link: "https://www.nature.com/articles/nature13480"}; - break; - } - case "TGCT": { - let texts: string[] = [ - "Over 90% of testicular cancers originate from germ cells, which are cells within the testicles responsible for sperm production. This category of cancer is referred to as testicular germ cell cancer. Testicular germ cell cancer can be further categorized as either seminomas or nonseminomas, distinguishable through microscopic examination. Nonseminomas typically exhibit more rapid growth and dissemination compared to seminomas. When a testicular germ cell tumor contains a combination of both subtypes, it is classified as a nonseminoma. TCGA conducted research encompassing both seminomas and nonseminomas.", - ]; - cancerInfo = {text: texts, link: "https://www.cell.com/cell-reports/fulltext/S2211-1247(18)30785-X"}; - break; - } - case "UCEC": { - let texts: string[] = [ - "Endometrial cancer arises in the cells that compose the inner lining of the uterus, known as the endometrium. It ranks as one of the prevalent cancers affecting the female reproductive system in American women.", - ]; - cancerInfo = {text: texts, link: "https://www.nature.com/articles/nature12113"}; - break; - } - default: { - let texts: string[] = [ - "No information available for this cancer type." - ]; - cancerInfo = {text: texts, link: ""}; - break; - } - } - return cancerInfo; - } -} diff --git a/src/app/routes/spongeffects/spongeffects.module.ts b/src/app/routes/spongeffects/spongeffects.module.ts deleted file mode 100644 index 89a91fb9..00000000 --- a/src/app/routes/spongeffects/spongeffects.module.ts +++ /dev/null @@ -1,93 +0,0 @@ -import { NgModule } from '@angular/core'; -import { CommonModule } from '@angular/common'; -import { FormsModule, ReactiveFormsModule } from '@angular/forms'; -import { MatButtonModule } from '@angular/material/button'; -import { MatCardModule } from '@angular/material/card'; -import { MatCheckboxModule } from '@angular/material/checkbox'; -import { MatExpansionModule } from '@angular/material/expansion'; -import { MatFormFieldModule } from '@angular/material/form-field'; -import { MatIconModule } from '@angular/material/icon'; -import { MatInputModule } from '@angular/material/input'; -import { MatProgressBarModule } from '@angular/material/progress-bar'; -import { MatSelectModule } from '@angular/material/select'; -import { MatSidenavModule } from '@angular/material/sidenav'; -import { MatTooltipModule } from '@angular/material/tooltip'; -import { MatButtonToggleModule } from '@angular/material/button-toggle'; -import { MatGridListModule } from '@angular/material/grid-list'; -import { MatTableModule } from '@angular/material/table'; -import { NgxDropzoneModule } from 'ngx-dropzone'; -import { SpongeEffectsComponent } from './spo'; -import { RouterModule } from '@angular/router'; -import {sum} from "simple-statistics"; - -@NgModule({ - declarations: [ - SpongeEffectsComponent - ], - imports: [ - CommonModule, - FormsModule, - ReactiveFormsModule, - MatButtonModule, - MatCardModule, - MatCheckboxModule, - MatExpansionModule, - MatFormFieldModule, - MatIconModule, - MatInputModule, - MatProgressBarModule, - MatSelectModule, - MatSidenavModule, - MatTooltipModule, - MatButtonToggleModule, - MatGridListModule, - MatTableModule, - NgxDropzoneModule, - RouterModule.forChild([ - { path: '', component: SpongeEffectsComponent } - ]) - ] -}) -export class SpongeEffectsModule { - - export class Cancer { - value: string; - viewValue: string; - allSubTypes: string[]; - sampleSizes: number[]; - - base: string = "https://portal.gdc.cancer.gov/projects/TGCA-"; - - constructor(value: string, viewValue: string, allSubTypes: string[], sampleSizes: number[], ) { - this.value = value; - this.viewValue = viewValue; - this.allSubTypes = allSubTypes; - this.sampleSizes = sampleSizes; - } - - addSubtype(subtype: string) { - this.allSubTypes.push(subtype); - } - - addSampleSize(sampleSize: number) { - if (sampleSize != null) this.sampleSizes.push(sampleSize); - } - - totalNumberOfSamples() { - return sum(this.sampleSizes.filter(s => s >= 0)); - } - - toString() { - return this.viewValue + " - (" + this.value + ")"; - } - - getUrl() { - return this.base + this.value - } -} - - - - - -} From 4faa720cb2e7a1ea12be9e325d97f38921985e29 Mon Sep 17 00:00:00 2001 From: Nico Trummer Date: Tue, 17 Dec 2024 20:42:26 +0100 Subject: [PATCH 03/14] Use lodash instead of simple-statistics --- package-lock.json | 10 --- package.json | 1 - .../spongeffects/explore/explore.component.ts | 68 +++++++++---------- .../class-performance-plot.component.ts | 39 +++++------ 4 files changed, 48 insertions(+), 70 deletions(-) diff --git a/package-lock.json b/package-lock.json index b66849ff..2368093c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28,7 +28,6 @@ "ngx-dropzone": "^3.1.0", "rxjs": "~7.8.0", "sigma": "^3.0.0-beta.38", - "simple-statistics": "^7.8.7", "tslib": "^2.3.0", "zone.js": "~0.15.0" }, @@ -12858,15 +12857,6 @@ "node": "^18.17.0 || >=20.5.0" } }, - "node_modules/simple-statistics": { - "version": "7.8.7", - "resolved": "https://registry.npmjs.org/simple-statistics/-/simple-statistics-7.8.7.tgz", - "integrity": "sha512-ed5FwTNYvkMTfbCai1U+r3symP+lIPKWCqKdudpN4NFNMn9RtDlFtSyAQhCp4oPH0YBjWu/qnW+5q5ZkPB3uHQ==", - "license": "ISC", - "engines": { - "node": "*" - } - }, "node_modules/slash": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz", diff --git a/package.json b/package.json index 59d4653b..10a3ee03 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,6 @@ "ngx-dropzone": "^3.1.0", "rxjs": "~7.8.0", "sigma": "^3.0.0-beta.38", - "simple-statistics": "^7.8.7", "tslib": "^2.3.0", "zone.js": "~0.15.0" }, diff --git a/src/app/routes/spongeffects/explore/explore.component.ts b/src/app/routes/spongeffects/explore/explore.component.ts index 778223d0..ba3ea2e3 100644 --- a/src/app/routes/spongeffects/explore/explore.component.ts +++ b/src/app/routes/spongeffects/explore/explore.component.ts @@ -1,29 +1,25 @@ -import {Component, computed, effect, Signal, inject, ResourceRef, resource, ViewChild, ElementRef} from '@angular/core'; -import {MatExpansionPanel, +import {Component, effect, inject} from '@angular/core'; +import { + MatExpansionModule, + MatExpansionPanel, MatExpansionPanelDescription, - MatExpansionPanelTitle, - MatExpansionModule + MatExpansionPanelTitle } from "@angular/material/expansion"; -import {MatIcon} from "@angular/material/icon"; +import {MatIcon, MatIconModule} from "@angular/material/icon"; import {MatFormFieldModule} from "@angular/material/form-field"; import {MatOption, MatSelect} from "@angular/material/select"; -import {FormsModule} from "@angular/forms"; -import {max, min, sum} from 'simple-statistics'; -import {FormControl, FormGroup, ReactiveFormsModule, Validators} from "@angular/forms"; -import {MatButton} from "@angular/material/button"; -import {MatButtonToggleModule } from '@angular/material/button-toggle'; +import {FormControl, FormGroup, FormsModule, ReactiveFormsModule} from "@angular/forms"; +import {sum} from 'lodash'; +import {MatButtonToggleModule} from '@angular/material/button-toggle'; import {MatProgressSpinnerModule} from "@angular/material/progress-spinner"; -import {MatIconModule} from '@angular/material/icon'; import {ExploreService} from '../../../services/explore.service'; -import {Dataset, ExploreQuery, PlotlyData, Metric, RunPerformance} from '../../../interfaces'; -import {AsyncPipe} from "@angular/common"; +import {ExploreQuery} from '../../../interfaces'; import {BrowseService} from "../../../services/browse.service"; import {VersionsService} from "../../../services/versions.service"; -import {toSignal} from "@angular/core/rxjs-interop"; import {BackendService} from "../../../services/backend.service"; -import { MatCardModule } from '@angular/material/card'; -import { ClassPerformancePlotComponent } from "./plots/class-performance-plot/class-performance-plot.component"; -import { OverallAccPlotComponent } from "./plots/overall-acc-plot/overall-acc-plot.component"; +import {MatCardModule} from '@angular/material/card'; +import {ClassPerformancePlotComponent} from "./plots/class-performance-plot/class-performance-plot.component"; +import {OverallAccPlotComponent} from "./plots/overall-acc-plot/overall-acc-plot.component"; declare const Plotly: any; @@ -36,7 +32,7 @@ export class Cancer { base: string = "https://portal.gdc.cancer.gov/projects/TGCA-"; - constructor(value: string, viewValue: string, allSubTypes: string[], sampleSizes: number[], ) { + constructor(value: string, viewValue: string, allSubTypes: string[], sampleSizes: number[],) { this.value = value; this.viewValue = viewValue; this.allSubTypes = allSubTypes; @@ -84,7 +80,7 @@ export class Cancer { MatCardModule, ClassPerformancePlotComponent, OverallAccPlotComponent -], + ], templateUrl: './explore.component.html', styleUrls: ['./explore.component.scss', '../spongeffects.component.scss'] }) @@ -96,7 +92,7 @@ export class ExploreComponent { levels = ['gene', 'transcript']; formGroup = new FormGroup({ selectedCancer: new FormControl(''), - selectedLevel: new FormControl<'gene' | 'transcript'>('gene'), + selectedLevel: new FormControl<'gene' | 'transcript'>('gene'), }); // diseaseSignal = toSignal( // this.formGroup.get('selectedCancer')!.valueChanges @@ -108,21 +104,21 @@ export class ExploreComponent { // cancers = computed(() => { // return Array.from(new Set(this.spongEffectsRuns.then(run => run.map(run => run.disease_name)))); //.sort((a, b) => a.localeCompare(b)); // }); - // cancers = computed(() => Array.from(this.spongEffectsRunDatasets())); - // cancers = computed(() => Array.from(this.diseaseSubtypeMap().keys())); - cancers = this.exploreService.spongEffectsRunDataset(); - // .filter(cancer => cancer in this.spongEffectsRuns.then(runs => runs.map(run => this.capitalize(run.disease_name)))) - // .filter(cancer => cancer.startsWith('breast')) - // .sort((a, b) => a.localeCompare(b))); - - setInitialCancer = effect(() => { - const cancers = this.cancers(); - this.formGroup.get('selectedCancer')?.setValue(cancers[0]); - }); - - ////////////////////////////////////////// - /////////// Plotting variables /////////// - ////////////////////////////////////////// + // cancers = computed(() => Array.from(this.spongEffectsRunDatasets())); + // cancers = computed(() => Array.from(this.diseaseSubtypeMap().keys())); + cancers = this.exploreService.spongEffectsRunDataset(); + // .filter(cancer => cancer in this.spongEffectsRuns.then(runs => runs.map(run => this.capitalize(run.disease_name)))) + // .filter(cancer => cancer.startsWith('breast')) + // .sort((a, b) => a.localeCompare(b))); + + setInitialCancer = effect(() => { + const cancers = this.cancers(); + this.formGroup.get('selectedCancer')?.setValue(cancers[0]); + }); + + ////////////////////////////////////////// + /////////// Plotting variables /////////// + ////////////////////////////////////////// constructor() { // watch dropdown menu @@ -145,9 +141,7 @@ export class ExploreComponent { } - - clearResults() { // // allow new queries // this.exploreResultsQueried = false; diff --git a/src/app/routes/spongeffects/explore/plots/class-performance-plot/class-performance-plot.component.ts b/src/app/routes/spongeffects/explore/plots/class-performance-plot/class-performance-plot.component.ts index fc88f318..6291265f 100644 --- a/src/app/routes/spongeffects/explore/plots/class-performance-plot/class-performance-plot.component.ts +++ b/src/app/routes/spongeffects/explore/plots/class-performance-plot/class-performance-plot.component.ts @@ -1,18 +1,15 @@ -import { Component, Renderer2, ElementRef, ViewChild, resource, ResourceRef, inject, computed } from '@angular/core'; -import { MatExpansionModule } from '@angular/material/expansion'; -import { MatIconModule } from '@angular/material/icon'; -import { MatFormFieldModule } from '@angular/material/form-field'; -import { MatSelectModule } from '@angular/material/select'; -import { FormsModule, ReactiveFormsModule } from '@angular/forms'; -import { MatProgressBarModule } from '@angular/material/progress-bar'; -import {max, min, sum} from 'simple-statistics'; -import { - SelectElement, - PlotlyData, - } from '../../../../../interfaces'; -import { VersionsService } from '../../../../../services/versions.service'; -import { BackendService } from '../../../../../services/backend.service'; -import { ExploreService } from '../../../../../services/explore.service'; +import {Component, computed, ElementRef, inject, Renderer2, resource, ResourceRef, ViewChild} from '@angular/core'; +import {MatExpansionModule} from '@angular/material/expansion'; +import {MatIconModule} from '@angular/material/icon'; +import {MatFormFieldModule} from '@angular/material/form-field'; +import {MatSelectModule} from '@angular/material/select'; +import {FormsModule, ReactiveFormsModule} from '@angular/forms'; +import {MatProgressBarModule} from '@angular/material/progress-bar'; +import {PlotlyData, SelectElement,} from '../../../../../interfaces'; +import {VersionsService} from '../../../../../services/versions.service'; +import {BackendService} from '../../../../../services/backend.service'; +import {ExploreService} from '../../../../../services/explore.service'; +import {sum} from "lodash"; declare var Plotly: any; @@ -74,14 +71,12 @@ export class ClassPerformancePlotComponent { const level = param.request.level; if (version === undefined || gene === undefined || level === undefined) return; const plot = await this.plotModelClassPerformance(version, gene, level); - return plot; - } + return plot; + } }); } - - async plotModelClassPerformance(version: number, cancer: string, level: string): Promise { const performanceData = await this.backend.getRunClassPerformance(version, cancer, level); @@ -110,8 +105,8 @@ export class ClassPerformancePlotComponent { traces.push(trace); } } - const meanTextLength: number = Math.round(sum(traces[0].x.map(d => d.length))/traces[0].x.length); - const textPad: number = meanTextLength*10.5; + const meanTextLength: number = Math.round(sum(traces[0].x.map(d => d.length)) / traces[0].x.length); + const textPad: number = meanTextLength * 10.5; const containerWidth = this.renderer.selectRootElement(this.classPerformancePlotDiv.nativeElement).offsetWidth; // const angle: number = meanTextLength > 15 ? 90: 0; // angle 90 if number of bars is greater than 10 @@ -142,7 +137,7 @@ export class ClassPerformancePlotComponent { paper_bgcolor: 'rgba(0,0,0,0)', plot_bgcolor: 'rgba(0,0,0,0)' }; - const config = { responsive: true }; + const config = {responsive: true}; const data = {data: traces, layout: layout, config: config}; return Plotly.newPlot(this.classPerformancePlotDiv.nativeElement, data.data, data.layout, data.config); From 97b7aacb4474b24a9ab4b65a3c55c927650a3286 Mon Sep 17 00:00:00 2001 From: Nico Trummer Date: Tue, 17 Dec 2024 20:50:06 +0100 Subject: [PATCH 04/14] Update explore/predict switcher --- .../spongeffects/spongeffects.component.html | 18 +++-- .../spongeffects/spongeffects.component.ts | 78 +++---------------- 2 files changed, 22 insertions(+), 74 deletions(-) diff --git a/src/app/routes/spongeffects/spongeffects.component.html b/src/app/routes/spongeffects/spongeffects.component.html index 26f71413..d7be10f8 100644 --- a/src/app/routes/spongeffects/spongeffects.component.html +++ b/src/app/routes/spongeffects/spongeffects.component.html @@ -1,13 +1,17 @@ - - Explore - - - Predict - +
+ + Explore + Predict + +
- + @if (mode() == 'explore') { + + } @else { + + }
diff --git a/src/app/routes/spongeffects/spongeffects.component.ts b/src/app/routes/spongeffects/spongeffects.component.ts index 8899cd76..0dbf8a9d 100644 --- a/src/app/routes/spongeffects/spongeffects.component.ts +++ b/src/app/routes/spongeffects/spongeffects.component.ts @@ -1,7 +1,9 @@ -import { Component, OnInit, AfterViewInit } from '@angular/core'; +import {Component, model} from '@angular/core'; import {MatDrawer, MatDrawerContainer, MatDrawerContent} from "@angular/material/sidenav"; -import {MatListItem, MatNavList} from "@angular/material/list"; -import {RouterLink, RouterOutlet} from "@angular/router"; +import {MatButtonToggle, MatButtonToggleGroup} from "@angular/material/button-toggle"; +import {ExploreComponent} from "./explore/explore.component"; +import {PredictComponent} from "./predict/predict.component"; + // import { Tab, Cancer, PlotlyData } from '../../models/spongeffects.model'; @Component({ @@ -11,71 +13,13 @@ import {RouterLink, RouterOutlet} from "@angular/router"; MatDrawer, MatDrawerContainer, MatDrawerContent, - MatListItem, - MatNavList, - RouterLink, - RouterOutlet + MatButtonToggleGroup, + MatButtonToggle, + ExploreComponent, + PredictComponent ], styleUrls: ['./spongeffects.component.scss'] }) -export class SpongEffectsComponent implements OnInit, AfterViewInit { - // tabs: Tab[] = [ - // { value: "explore", viewValue: "Explore", icon: "../../../assets/img/magnifying_glass.png" }, - // { value: "predict", viewValue: "Predict", icon: "../../../assets/img/chip-intelligence-processor-svgrepo-com.png" } - // ]; - // selectedTab: Tab = this.tabs[0]; - // cancers: Cancer[] = []; - // selectedCancer: Cancer | undefined = undefined; - // sampleDistributionData: PlotlyData | undefined; - - constructor() {} - - ngOnInit(): void { - // Initialisierungslogik - } - - ngAfterViewInit(): void { - // Logik nach dem Laden der Ansicht - } - - // setTab(tab: Tab) { - // this.selectedTab = tab; - // } - // - // setPreviewCancer(cancer: Cancer) { - // this.selectedCancer = cancer; - // this.sampleDistributionData = this.getSampleDistributionData(); - // } - - // getSampleDistributionData(): PlotlyData { - // if (!this.selectedCancer) { - // return { data: [], layout: {}, config: {} }; - // } - // return { - // data: [ - // { - // values: this.selectedCancer.sampleSizes, - // labels: this.selectedCancer.allSubTypes, - // type: "pie", - // hoverinfo: 'label+value+percent', - // textinfo: 'none' - // } - // ], - // layout: { - // autosize: true, - // showlegend: true, - // legend: { "orientation": "h" }, - // margin: { b: 5, l: 5, r: 5, t: 50 }, - // title: { - // text: "Sample distribution of " + this.selectedCancer.value + " (" + this.selectedCancer.totalNumberOfSamples() + " samples)", - // font: { size: 14 } - // } - // }, - // config: { responsive: true } - // }; - // } - - onFileSelected(files: File[]) { - // Logik für den Datei-Upload - } +export class SpongEffectsComponent { + mode = model<'explore' | 'predict'>('explore'); } From 40134b1de1110de2e7cd9b1b5904e2907967f6d8 Mon Sep 17 00:00:00 2001 From: Nico Trummer Date: Tue, 17 Dec 2024 21:01:07 +0100 Subject: [PATCH 05/14] Remove commented-out code --- .../explore/explore.component.html | 412 ++---------------- .../class-performance-plot.component.html | 57 ++- 2 files changed, 70 insertions(+), 399 deletions(-) diff --git a/src/app/routes/spongeffects/explore/explore.component.html b/src/app/routes/spongeffects/explore/explore.component.html index c1eb8b42..e49110de 100644 --- a/src/app/routes/spongeffects/explore/explore.component.html +++ b/src/app/routes/spongeffects/explore/explore.component.html @@ -1,26 +1,33 @@ - -

Explore spongEffects on TCGA projects or make predictions on custom expressions

- info What is spongEffects? + + info + What is spongEffects? + General information about spongEffects on TCGA and custom usage
-

spongEffects can be used for downstream analysis of SPONGE ceRNA networks and is capable of tumor sub-type classification and biomarker discovery via a random forest machine learning approach.

-

For more detailed information refer to the publication.

-

We trained spongEffects prediction models on the TCGA pan-cancer dataset and nine other TCGA projects that feature sufficient cancer subtyping for classification.

+

spongEffects can be used for downstream analysis of + SPONGE ceRNA networks and is capable of tumor sub-type classification and biomarker discovery via a random forest + machine learning approach.

+

For more detailed information refer to the publication. +

+

We trained spongEffects prediction models on the TCGA pan-cancer dataset and nine other TCGA projects that + feature sufficient cancer subtyping for classification.

The tab below shows the spongEffects prediction results for a selected TCGA project. - You can browse the hub-nodes that spongEffects models views as potential biomarkers and compare their gene/transcript expression. + You can browse the hub-nodes that spongEffects models views as potential biomarkers and compare their + gene/transcript expression.

- We can use the pretrained TCGA models to classify tumor (sub-)types and predict biomarkers on new independent input data that can be uploaded via the + We can use the pretrained TCGA models to classify tumor (sub-)types and predict biomarkers on new independent + input data that can be uploaded via the tab below.

The model will predict a tumor type for every sample in the uploaded expression file individually.

@@ -38,7 +45,8 @@

} - + + @for (level of levels; track level) { {{ capitalize(level) }} } @@ -49,12 +57,15 @@

- search Explore results - spongEffects predictions for selected project + + search + Explore results + + spongEffects predictions for selected project

- + @@ -69,10 +80,14 @@

- infoWhat does this mean? + + info + What does this mean? +
-

The spongEffects enrichment score distributions can give an insight into the model capability to differentiate between the given classes.

+

The spongEffects enrichment score distributions can give an insight into the model capability to + differentiate between the given classes.

@@ -90,121 +105,19 @@

- infoWhat does this mean? + + info + What does this mean? +
-

The mean decrease in the Gini coefficient tells us how much each variable affects the consistency of nodes and leaves in a random forest. If the mean decrease accuracy or mean decrease Gini score is higher, it means the variable is more important in the model.

-

Module centralities that are at the top right of the plot below play a crucial role in distinguishing between the various cancer (sub-)types.

+

The mean decrease in the Gini coefficient tells us how much each variable affects the consistency of + nodes and leaves in a random forest. If the mean decrease accuracy or mean decrease Gini score is higher, + it means the variable is more important in the model.

+

Module centralities that are at the top right of the plot below play a crucial role in distinguishing + between the various cancer (sub-)types.

- - - @@ -217,15 +130,18 @@

- infoWhat does this mean? + + info + What does this mean? +
-

Gene Set Enrichment Analysis (GSEA) identifies whether specific sets of genes related to biological functions are enriched in gene expression data.

+

Gene Set Enrichment Analysis (GSEA) identifies whether specific sets of genes related to biological + functions are enriched in gene expression data.

The information from GSEA can help find biological differences between disease (sub-)types

-
@@ -233,243 +149,3 @@

- - - - diff --git a/src/app/routes/spongeffects/explore/plots/class-performance-plot/class-performance-plot.component.html b/src/app/routes/spongeffects/explore/plots/class-performance-plot/class-performance-plot.component.html index aaa3ca76..84dbbb3a 100644 --- a/src/app/routes/spongeffects/explore/plots/class-performance-plot/class-performance-plot.component.html +++ b/src/app/routes/spongeffects/explore/plots/class-performance-plot/class-performance-plot.component.html @@ -1,37 +1,32 @@ - - Class performance - - Class specific performance - - - + + Class performance + + Class specific performance + + -
- -
+
+ +
-
-
-
+
+
+
- - - infoWhat does this mean? - -
-

This plot shows the performance of the individual class predictions of the model that was trained with the spongEffects central modules against a model with randomly selected modules. We can strengthen the confidence in the models prediction if it is capable of outperforming its random counterpart. Interpretation of predictions should be made with more caution if this it not the case.

-
-
+ + + + info + What does this mean? + + +
+

This plot shows the performance of the individual class predictions of the model that was trained with the + spongEffects central modules against a model with randomly selected modules. We can strengthen the confidence in + the models prediction if it is capable of outperforming its random counterpart. Interpretation of predictions + should be made with more caution if this it not the case.

+
+
From 29bc84b7c8123873d6abadfbf91704eab2d2a7db Mon Sep 17 00:00:00 2001 From: Nico Trummer Date: Tue, 17 Dec 2024 21:05:32 +0100 Subject: [PATCH 06/14] Remove outdated services --- .../services/backend.spongeffects.service.ts | 249 ------------------ src/app/services/version.service.spec.ts | 16 -- src/app/services/version.service.ts | 24 -- 3 files changed, 289 deletions(-) delete mode 100644 src/app/services/backend.spongeffects.service.ts delete mode 100644 src/app/services/version.service.spec.ts delete mode 100644 src/app/services/version.service.ts diff --git a/src/app/services/backend.spongeffects.service.ts b/src/app/services/backend.spongeffects.service.ts deleted file mode 100644 index ef580711..00000000 --- a/src/app/services/backend.spongeffects.service.ts +++ /dev/null @@ -1,249 +0,0 @@ -import {Injectable} from '@angular/core'; -import {HttpService} from "./http.service"; -import {VersionService} from "./version.service"; -import { - CeRNA, - CeRNAExpression, - CeRNAInteraction, - CeRNAQuery, - Dataset, - EnrichmentScoreDistributions, - Gene, - GeneCount, - OverallCounts, - PlotData, - PredictCancerType, - RunClassPerformance, - RunInfo, - RunPerformance, - SpongEffectsGeneModules, - SpongEffectsGeneModuleMembers, - SpongEffectsTranscriptModules, - SpongEffectsTranscriptModuleMembers, - SurvivalPValue, - SurvivalRate, - TranscriptExpression, - SpongEffectsRun -} from "../interfaces"; - -interface Query { - [key: string]: any; -} - -@Injectable({ - providedIn: 'root' -}) -export class BackendService { - static API_BASE = 'https://exbio.wzw.tum.de/sponge-api-v2' - - constructor(private http: HttpService, private versionService: VersionService) { - } - - async getDatasets(diseaseName?: string): Promise { - const route = 'datasets'; - - const query: Query = { - sponge_db_version: this.versionService.getCurrentVersion() - }; - - if (diseaseName) { - query['disease'] = diseaseName; - } - - return this.http.getRequest(this.getRequestURL(route, query)); - } - - getDatasetInfo(diseaseName: string): Promise { - const sponge_db_version = this.versionService.getCurrentVersion(); - const request = BackendService.API_BASE + '/dataset/runInformation?disease_name=' + diseaseName + `&sponge_db_version=${sponge_db_version}`; - return this.http.getRequest(request); - } - - getDatasetsInformation(dataOrigin?: string): Promise { - const sponge_db_version = this.versionService.getCurrentVersion(); - const request = BackendService.API_BASE + '/datasets' + `?data_origin=${dataOrigin}` + `&sponge_db_version=${sponge_db_version}`; - return this.http.getRequest(request); - } - - getOverallCounts(): Promise { - const sponge_db_version = this.versionService.getCurrentVersion(); - const request = BackendService.API_BASE + '/getOverallCounts' + `?sponge_db_version=${sponge_db_version}`; - return this.http.getRequest(request); - } - - getCeRNA(query: CeRNAQuery): Promise { - const sponge_db_version = this.versionService.getCurrentVersion(); - let request = BackendService.API_BASE + '/findceRNA?disease_name=' + query.disease.disease_name + `?sponge_db_version=${sponge_db_version}`; - - request += `&minBetweenness=${query.minBetweenness}`; - request += `&minNodeDegree=${query.minDegree}`; - request += `&minEigenvector=${query.minEigen}`; - request += `&sorting=${query.geneSorting}`; - request += `&descending=${true}`; - request += `&limit=${query.maxGenes}`; - - return this.http.getRequest(request); - } - - getCeRNAInteractionsAll(disease: string, maxPValue: number, ensgs: string[], limit?: number, offset?: number): Promise { - let request = BackendService.API_BASE + '/ceRNAInteraction/findAll?disease_name=' + disease; - request += `&ensg_number=${ensgs.join(',')}`; - request += `&pValue=${maxPValue}`; - - if (limit) { - request += `&limit=${limit}`; - } - if (offset) { - request += `&offset=${offset}`; - } - - return this.http.getRequest(request); - } - - getCeRNAInteractionsSpecific(disease: string, maxPValue: number, ensgs: string[]): Promise { - let request = BackendService.API_BASE + '/ceRNAInteraction/findSpecific?disease_name=' + disease; - request += `&ensg_number=${ensgs.join(',')}`; - request += `&pValue=${maxPValue}`; - - return this.http.getRequest(request); - } - - getCeRNAExpression(ensgs: string[], diseaseName: string): Promise { - let request = BackendService.API_BASE + '/exprValue/getceRNA?disease_name=' + diseaseName; - request += `&ensg_number=${ensgs.join(',')}`; - - return this.http.getRequest(request); - } - - getTranscriptExpression(ensts: string[], disease_name?: string): Promise { - let request = BackendService.API_BASE + `/exprValue/getTranscript?disease_name=${disease_name}`; - request += `&enst_number=${ensts.join(',')}`; - - return this.http.getRequest(request); - } - - getSurvivalRates(ensgs: string[], diseaseName: string): Promise { - let request = BackendService.API_BASE + '/survivalAnalysis/getRates?disease_name=' + diseaseName; - request += `&ensg_number=${ensgs.join(',')}`; - - return this.http.getRequest(request); - } - - getSurvivalPValues(ensgs: string[], diseaseName: string): Promise { - let request = BackendService.API_BASE + '/survivalAnalysis/getPValues?disease_name=' + diseaseName; - request += `&ensg_number=${ensgs.join(',')}`; - - return this.http.getRequest(request); - } - - getAutocomplete(query: string): Promise { - if (query.length < 2) { - return Promise.resolve([]); - } - const request = BackendService.API_BASE + '/stringSearch?searchString=' + query; - try { - return this.http.getRequest(request); - } catch (e) { - return Promise.resolve([]); - } - } - - getGeneCount(ensgs: string[], onlySignificant: boolean): Promise { - if (ensgs.length === 0) { - return Promise.resolve([]); - } - let request = BackendService.API_BASE + '/getGeneCount?ensg_number=' + ensgs.join(','); - if (onlySignificant) { - request += '&minCountSign=1'; - } - return this.http.getRequest(request); - } - - - getRunPerformance(diseaseName: string, level: string): Promise { - const sponge_db_version = this.versionService.getCurrentVersion(); - const request = BackendService.API_BASE + '/spongEffects/getRunPerformance' + `?disease_name=${diseaseName}` + `&level=${level}` + `&sponge_db_version=${sponge_db_version}`; - return this.http.getRequest(request); - } - - getRunClassPerformance(diseaseName: string, level: string): Promise { - const sponge_db_version = this.versionService.getCurrentVersion(); - const request = BackendService.API_BASE + '/spongEffects/getRunClassPerformance' + `?disease_name=${diseaseName}` + `&level=${level}` + `&sponge_db_version=${sponge_db_version}`; - return this.http.getRequest(request); - } - - getEnrichmentScoreDistributions(diseaseName: string, level: string): Promise { - const sponge_db_version = this.versionService.getCurrentVersion(); - const request = `${BackendService.API_BASE}/spongEffects/getEnrichmentScoreDistributions?disease_name=${diseaseName}&level=${level}&sponge_db_version=${sponge_db_version}`; - return this.http.getRequest(request); - } - - getSpongEffectsGeneModules(diseaseName: string): Promise { - const sponge_db_version = this.versionService.getCurrentVersion(); - const request = `${BackendService.API_BASE}/spongEffects/getSpongEffectsGeneModules?disease_name=${diseaseName}&sponge_db_version=${sponge_db_version}`; - return this.http.getRequest(request); - } - - getSpongEffectsGeneModuleMembers(diseaseName: string, ensgNumber?: string, geneSymbol?: string): Promise { - const sponge_db_version = this.versionService.getCurrentVersion(); - let request = `${BackendService.API_BASE}/spongEffects/getSpongEffectsGeneModuleMembers?disease_name=${diseaseName}&sponge_db_version=${sponge_db_version}`; - if (ensgNumber) { - request += `&ensg_number=${ensgNumber}`; - } - if (geneSymbol) { - request += `&gene_symbol=${geneSymbol}`; - } - return this.http.getRequest(request); - } - - getSpongEffectsTranscriptModules(diseaseName: string): Promise { - const sponge_db_version = this.versionService.getCurrentVersion(); - const request = `${BackendService.API_BASE}/spongEffects/getSpongEffectsTranscriptModules?disease_name=${diseaseName}&sponge_db_version=${sponge_db_version}`; - return this.http.getRequest(request); - } - - getSpongEffectsTranscriptModuleMembers(diseaseName: string, enstNumber?: string): Promise { - const sponge_db_version = this.versionService.getCurrentVersion(); - let request = `${BackendService.API_BASE}/spongEffects/getSpongEffectsTranscriptModuleMembers?disease_name=${diseaseName}&sponge_db_version=${sponge_db_version}`; - if (enstNumber) { - request += `&enst_number=${enstNumber}`; - } - return this.http.getRequest(request); - } - - getSpongEffectsRuns(dataset_ID?: number, diseaseName?: string): Promise { - const sponge_db_version = this.versionService.getCurrentVersion(); - const request = `${BackendService.API_BASE}/spongEffects/getSpongEffectsRuns?` - + (dataset_ID ? `?dataset_ID=${dataset_ID}` : '') - + (diseaseName ? `&disease_name=${diseaseName}` : '') - + `&sponge_db_version=${sponge_db_version}` - return this.http.getRequest(request); - } - - predictCancerType(file: File, subtypes: boolean, log: boolean, mscor: number, fdr: number, minSize: number, maxSize: number, minExpr: number, method: string): Promise { - const sponge_db_version = this.versionService.getCurrentVersion(); - const formData = new FormData(); - formData.append('file', file); - formData.append('subtypes', subtypes.toString()); - formData.append('log', log.toString()); - formData.append('mscor', mscor.toString()); - formData.append('fdr', fdr.toString()); - formData.append('min_size', minSize.toString()); - formData.append('max_size', maxSize.toString()); - formData.append('min_expr', minExpr.toString()); - formData.append('method', method); - const request = `${BackendService.API_BASE}/spongEffects/predictCancerType?sponge_db_version=${sponge_db_version}`; - return this.http.postRequest(request, formData); - } - - private stringify(query: Query): string { - return Object.keys(query).map(key => key + '=' + query[key]).join('&'); - } - - private getRequestURL(route: string, query: Query): string { - return `${BackendService.API_BASE}/${route}?${this.stringify(query)}`; - } - -} - - diff --git a/src/app/services/version.service.spec.ts b/src/app/services/version.service.spec.ts deleted file mode 100644 index 7e43b32d..00000000 --- a/src/app/services/version.service.spec.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { TestBed } from '@angular/core/testing'; -import { VersionService } from './version.service'; - -describe('VersionService', () => { - let service: VersionService; - - beforeEach(() => { - TestBed.configureTestingModule({}); - service = TestBed.inject(VersionService); - }); - - it('should be created', () => { - expect(service).toBeTruthy(); - }); - -}); \ No newline at end of file diff --git a/src/app/services/version.service.ts b/src/app/services/version.service.ts deleted file mode 100644 index f244d1cd..00000000 --- a/src/app/services/version.service.ts +++ /dev/null @@ -1,24 +0,0 @@ -import {Injectable, signal, WritableSignal} from '@angular/core'; - -@Injectable({ - providedIn: 'root' - }) - export class VersionService { - private static LATEST: number = 2; - private currentVersion: WritableSignal = signal(VersionService.LATEST); - - constructor(private version: VersionService) { - } - - public getCurrentVersion(): WritableSignal { - return this.version.currentVersion; - } - - public setCurrentVersion(new_version: number): void { - this.version.currentVersion = signal(new_version); - } - - public resetToDefault(): void { - this.currentVersion = signal(VersionService.LATEST) - } -} From e717ccd269bf51d61c93fb23d5999b10f34067e9 Mon Sep 17 00:00:00 2001 From: Nico Trummer Date: Tue, 17 Dec 2024 22:42:34 +0100 Subject: [PATCH 07/14] Update SpongEffects explore structure --- .../explore/explore.component.html | 23 --- .../spongeffects/explore/explore.component.ts | 138 +----------------- .../explore/form/explore-form.component.html | 14 ++ .../explore/form/explore-form.component.scss | 0 .../form/explore-form.component.spec.ts | 23 +++ .../explore/form/explore-form.component.ts | 25 ++++ .../class-performance-plot.component.ts | 29 +--- .../overall-acc-plot.component.ts | 55 ++++--- .../explore/service/explore.service.spec.ts | 16 ++ .../explore/service/explore.service.ts | 49 +++++++ .../predict/form/predict-form.component.html | 1 + .../predict/form/predict-form.component.scss | 0 .../form/predict-form.component.spec.ts | 23 +++ .../predict/form/predict-form.component.ts | 11 ++ .../spongeffects/spongeffects.component.html | 5 + .../spongeffects/spongeffects.component.ts | 21 ++- src/app/services/explore.service.ts | 82 +++-------- .../services/spong-effects.service.spec.ts | 16 ++ src/app/services/spong-effects.service.ts | 24 +++ 19 files changed, 283 insertions(+), 272 deletions(-) create mode 100644 src/app/routes/spongeffects/explore/form/explore-form.component.html create mode 100644 src/app/routes/spongeffects/explore/form/explore-form.component.scss create mode 100644 src/app/routes/spongeffects/explore/form/explore-form.component.spec.ts create mode 100644 src/app/routes/spongeffects/explore/form/explore-form.component.ts create mode 100644 src/app/routes/spongeffects/explore/service/explore.service.spec.ts create mode 100644 src/app/routes/spongeffects/explore/service/explore.service.ts create mode 100644 src/app/routes/spongeffects/predict/form/predict-form.component.html create mode 100644 src/app/routes/spongeffects/predict/form/predict-form.component.scss create mode 100644 src/app/routes/spongeffects/predict/form/predict-form.component.spec.ts create mode 100644 src/app/routes/spongeffects/predict/form/predict-form.component.ts create mode 100644 src/app/services/spong-effects.service.spec.ts create mode 100644 src/app/services/spong-effects.service.ts diff --git a/src/app/routes/spongeffects/explore/explore.component.html b/src/app/routes/spongeffects/explore/explore.component.html index e49110de..89622e07 100644 --- a/src/app/routes/spongeffects/explore/explore.component.html +++ b/src/app/routes/spongeffects/explore/explore.component.html @@ -1,6 +1,3 @@ -

- Explore spongEffects on TCGA projects or make predictions on custom expressions -

@@ -34,26 +31,6 @@

-

- -
- - Disease - - @for (cancer of cancers(); track cancer) { - {{ capitalize(cancer) }} - } - - - - - @for (level of levels; track level) { - {{ capitalize(level) }} - } - -
-

- diff --git a/src/app/routes/spongeffects/explore/explore.component.ts b/src/app/routes/spongeffects/explore/explore.component.ts index ba3ea2e3..9937b415 100644 --- a/src/app/routes/spongeffects/explore/explore.component.ts +++ b/src/app/routes/spongeffects/explore/explore.component.ts @@ -1,4 +1,4 @@ -import {Component, effect, inject} from '@angular/core'; +import {Component} from '@angular/core'; import { MatExpansionModule, MatExpansionPanel, @@ -7,60 +7,13 @@ import { } from "@angular/material/expansion"; import {MatIcon, MatIconModule} from "@angular/material/icon"; import {MatFormFieldModule} from "@angular/material/form-field"; -import {MatOption, MatSelect} from "@angular/material/select"; -import {FormControl, FormGroup, FormsModule, ReactiveFormsModule} from "@angular/forms"; -import {sum} from 'lodash'; +import {FormsModule, ReactiveFormsModule} from "@angular/forms"; import {MatButtonToggleModule} from '@angular/material/button-toggle'; import {MatProgressSpinnerModule} from "@angular/material/progress-spinner"; -import {ExploreService} from '../../../services/explore.service'; -import {ExploreQuery} from '../../../interfaces'; -import {BrowseService} from "../../../services/browse.service"; -import {VersionsService} from "../../../services/versions.service"; -import {BackendService} from "../../../services/backend.service"; import {MatCardModule} from '@angular/material/card'; import {ClassPerformancePlotComponent} from "./plots/class-performance-plot/class-performance-plot.component"; import {OverallAccPlotComponent} from "./plots/overall-acc-plot/overall-acc-plot.component"; - -declare const Plotly: any; - -export class Cancer { - value: string; - viewValue: string; - allSubTypes: string[]; - sampleSizes: number[]; - - base: string = "https://portal.gdc.cancer.gov/projects/TGCA-"; - - constructor(value: string, viewValue: string, allSubTypes: string[], sampleSizes: number[],) { - this.value = value; - this.viewValue = viewValue; - this.allSubTypes = allSubTypes; - this.sampleSizes = sampleSizes; - } - - addSubtype(subtype: string) { - this.allSubTypes.push(subtype); - } - - addSampleSize(sampleSize: number) { - if (sampleSize != null) this.sampleSizes.push(sampleSize); - } - - totalNumberOfSamples() { - return sum(this.sampleSizes.filter(s => s >= 0)); - } - - toString() { - return this.viewValue + " - (" + this.value + ")"; - } - - getUrl() { - return this.base + this.value - } -} - - @Component({ selector: 'app-explore', imports: [ @@ -70,8 +23,6 @@ export class Cancer { MatExpansionPanelDescription, MatExpansionModule, MatFormFieldModule, - MatSelect, - MatOption, FormsModule, ReactiveFormsModule, MatButtonToggleModule, @@ -85,89 +36,4 @@ export class Cancer { styleUrls: ['./explore.component.scss', '../spongeffects.component.scss'] }) export class ExploreComponent { - versionService = inject(VersionsService); - browseService = inject(BrowseService); - exploreService = inject(ExploreService); - backend = inject(BackendService); - levels = ['gene', 'transcript']; - formGroup = new FormGroup({ - selectedCancer: new FormControl(''), - selectedLevel: new FormControl<'gene' | 'transcript'>('gene'), - }); - // diseaseSignal = toSignal( - // this.formGroup.get('selectedCancer')!.valueChanges - // ) - - // cancers: Promise; - // cancers = this.exploreService.spongEffectsRunDataset().disease_name; - // diseaseSubtypeMap = this.versionService.diseaseSubtypeMap(); - // cancers = computed(() => { - // return Array.from(new Set(this.spongEffectsRuns.then(run => run.map(run => run.disease_name)))); //.sort((a, b) => a.localeCompare(b)); - // }); - // cancers = computed(() => Array.from(this.spongEffectsRunDatasets())); - // cancers = computed(() => Array.from(this.diseaseSubtypeMap().keys())); - cancers = this.exploreService.spongEffectsRunDataset(); - // .filter(cancer => cancer in this.spongEffectsRuns.then(runs => runs.map(run => this.capitalize(run.disease_name)))) - // .filter(cancer => cancer.startsWith('breast')) - // .sort((a, b) => a.localeCompare(b))); - - setInitialCancer = effect(() => { - const cancers = this.cancers(); - this.formGroup.get('selectedCancer')?.setValue(cancers[0]); - }); - - ////////////////////////////////////////// - /////////// Plotting variables /////////// - ////////////////////////////////////////// - - constructor() { - // watch dropdown menu - this.formGroup.valueChanges.subscribe((config) => { - config.selectedCancer = this.formGroup.get('selectedCancer')?.value as string; - config.selectedLevel = this.formGroup.get('selectedLevel')?.value as 'gene' | 'transcript' | null | undefined; - this.exploreService.runQuery(config as ExploreQuery); - }) - // Plot 1: Overall Accuracy - - - // effect(() => { - // this.plotData$.next(plotData.value()); - // }); - - // effect(() => { - // this.refreshSignal(); - // this.refresh(); - // }); - - } - - - clearResults() { -// // allow new queries -// this.exploreResultsQueried = false; -// // clear plot divs -// this.overallAccPlotDiv.nativeElement.innerHTML = ""; -// this.enrichmentScoresByClassPlotDiv.nativeElement.innerHTML = ""; -// this.lollipopPlotDiv.nativeElement.innerHTML = ""; - } - - public capitalize(val: string) { - return String(val).charAt(0).toUpperCase() + String(val).slice(1); - } - - ////////////////////////////////////////// - /////////// Plotting functions /////////// - ////////////////////////////////////////// - - - // refresh() { - // if (this.overallAccPlot && this.overallAccPlot.nativeElement.checkVisibility()) { - // Plotly.Plots.resize(this.overallAccPlot.nativeElement); - // } - // } - - // ngOnDestroy(): void { - // Plotly.purge(this.overallAccPlot.nativeElement); - // } - } diff --git a/src/app/routes/spongeffects/explore/form/explore-form.component.html b/src/app/routes/spongeffects/explore/form/explore-form.component.html new file mode 100644 index 00000000..20d7172d --- /dev/null +++ b/src/app/routes/spongeffects/explore/form/explore-form.component.html @@ -0,0 +1,14 @@ + + Disease + + @for (disease of diseases$(); track disease) { + {{ disease }} + } + + +
+ + Gene + Transcript + +
diff --git a/src/app/routes/spongeffects/explore/form/explore-form.component.scss b/src/app/routes/spongeffects/explore/form/explore-form.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/src/app/routes/spongeffects/explore/form/explore-form.component.spec.ts b/src/app/routes/spongeffects/explore/form/explore-form.component.spec.ts new file mode 100644 index 00000000..57b3b972 --- /dev/null +++ b/src/app/routes/spongeffects/explore/form/explore-form.component.spec.ts @@ -0,0 +1,23 @@ +import {ComponentFixture, TestBed} from '@angular/core/testing'; + +import {ExploreFormComponent} from './explore-form.component'; + +describe('FormComponent', () => { + let component: ExploreFormComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ExploreFormComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(ExploreFormComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/routes/spongeffects/explore/form/explore-form.component.ts b/src/app/routes/spongeffects/explore/form/explore-form.component.ts new file mode 100644 index 00000000..3ce24cc7 --- /dev/null +++ b/src/app/routes/spongeffects/explore/form/explore-form.component.ts @@ -0,0 +1,25 @@ +import {Component, inject} from '@angular/core'; +import {FormsModule, ReactiveFormsModule} from "@angular/forms"; +import {MatFormFieldModule} from "@angular/material/form-field"; +import {MatSelectModule} from "@angular/material/select"; +import {MatButtonToggleModule} from "@angular/material/button-toggle"; +import {ExploreService} from "../service/explore.service"; + +@Component({ + selector: 'app-explore-form', + imports: [ + FormsModule, + MatFormFieldModule, + MatSelectModule, + ReactiveFormsModule, + MatButtonToggleModule + ], + templateUrl: './explore-form.component.html', + styleUrl: './explore-form.component.scss' +}) +export class ExploreFormComponent { + exploreService = inject(ExploreService) + level$ = this.exploreService.level$; + diseases$ = this.exploreService.diseaseNames$; + disease$ = this.exploreService.selectedDisease$; +} diff --git a/src/app/routes/spongeffects/explore/plots/class-performance-plot/class-performance-plot.component.ts b/src/app/routes/spongeffects/explore/plots/class-performance-plot/class-performance-plot.component.ts index 6291265f..959d34f7 100644 --- a/src/app/routes/spongeffects/explore/plots/class-performance-plot/class-performance-plot.component.ts +++ b/src/app/routes/spongeffects/explore/plots/class-performance-plot/class-performance-plot.component.ts @@ -8,8 +8,8 @@ import {MatProgressBarModule} from '@angular/material/progress-bar'; import {PlotlyData, SelectElement,} from '../../../../../interfaces'; import {VersionsService} from '../../../../../services/versions.service'; import {BackendService} from '../../../../../services/backend.service'; -import {ExploreService} from '../../../../../services/explore.service'; import {sum} from "lodash"; +import {ExploreService} from "../../service/explore.service"; declare var Plotly: any; @@ -32,6 +32,10 @@ export class ClassPerformancePlotComponent { exploreService = inject(ExploreService); backend = inject(BackendService); + version$ = this.versionService.versionReadOnly; + level$ = this.exploreService.level$; + disease$ = this.exploreService.selectedDisease$; + @ViewChild("classModelPerformancePlot") classPerformancePlotDiv!: ElementRef; plotClassPerformance: ResourceRef; @@ -61,7 +65,7 @@ export class ClassPerformancePlotComponent { request: computed(() => { return { version: this.versionService.versionReadOnly()(), - cancer: this.exploreService.cancer$(), + cancer: this.exploreService.selectedDisease$(), level: this.exploreService.level$() } }), @@ -70,8 +74,7 @@ export class ClassPerformancePlotComponent { const gene = param.request.cancer; const level = param.request.level; if (version === undefined || gene === undefined || level === undefined) return; - const plot = await this.plotModelClassPerformance(version, gene, level); - return plot; + return await this.plotModelClassPerformance(version, gene, level); } }); } @@ -142,22 +145,4 @@ export class ClassPerformancePlotComponent { return Plotly.newPlot(this.classPerformancePlotDiv.nativeElement, data.data, data.layout, data.config); } - - async resetClassAccPlot() { - Plotly.update(this.classPerformancePlotDiv.nativeElement); - } - - async cancelClick(event: any) { - event.stopPropagation(); - } - - // async rePlotModelClassPerformance(): Promise { - // this.plotModelClassPerformance().then(_ => this.classPerformanceLoading = false); - // } - - // async plotResults() { - // this.plotModelClassPerformance().then(_ => this.classPerformanceLoading = false); - // } - - } diff --git a/src/app/routes/spongeffects/explore/plots/overall-acc-plot/overall-acc-plot.component.ts b/src/app/routes/spongeffects/explore/plots/overall-acc-plot/overall-acc-plot.component.ts index 1370d73e..b82cb1f3 100644 --- a/src/app/routes/spongeffects/explore/plots/overall-acc-plot/overall-acc-plot.component.ts +++ b/src/app/routes/spongeffects/explore/plots/overall-acc-plot/overall-acc-plot.component.ts @@ -1,15 +1,14 @@ -import { Component, ElementRef, ViewChild, inject } from '@angular/core'; -import { resource, ResourceRef, computed } from '@angular/core'; -import { PlotlyData, Metric, RunPerformance } from '../../../../../interfaces'; -import { BackendService } from '../../../../../services/backend.service'; -import { ExploreService } from '../../../../../services/explore.service'; -import { VersionsService } from '../../../../../services/versions.service'; -import { MatExpansionModule } from '@angular/material/expansion'; -import { MatIconModule } from '@angular/material/icon'; -import { MatFormFieldModule } from '@angular/material/form-field'; -import { MatSelectModule } from '@angular/material/select'; -import { FormsModule, ReactiveFormsModule } from '@angular/forms'; -import { MatProgressBarModule } from '@angular/material/progress-bar'; +import {Component, computed, ElementRef, inject, resource, ResourceRef, ViewChild} from '@angular/core'; +import {Metric, PlotlyData, RunPerformance} from '../../../../../interfaces'; +import {BackendService} from '../../../../../services/backend.service'; +import {VersionsService} from '../../../../../services/versions.service'; +import {MatExpansionModule} from '@angular/material/expansion'; +import {MatIconModule} from '@angular/material/icon'; +import {MatFormFieldModule} from '@angular/material/form-field'; +import {MatSelectModule} from '@angular/material/select'; +import {FormsModule, ReactiveFormsModule} from '@angular/forms'; +import {MatProgressBarModule} from '@angular/material/progress-bar'; +import {ExploreService} from "../../service/explore.service"; declare var Plotly: any; @@ -35,17 +34,17 @@ export class OverallAccPlotComponent { @ViewChild('overallAccuracyPlot') overallAccPlot!: ElementRef; plotOverallAccResource: ResourceRef; - // plot parameters + // plot parameters defaultPlotMode: string = "lines+markers"; defaultLineWidth: number = 4; defaultMarkerSize: number = 10; - constructor(){ + constructor() { this.plotOverallAccResource = resource({ request: computed(() => { return { version: this.versionService.versionReadOnly()(), - cancer: this.exploreService.cancer$(), + cancer: this.exploreService.selectedDisease$(), level: this.exploreService.level$() } }), @@ -59,18 +58,18 @@ export class OverallAccPlotComponent { return plot; } }); - + } async getOverallAccuracyData(version: number, cancer: string, level: string): Promise { const modelPerformances = await this.backend.getRunPerformance(version, cancer, level); return modelPerformances.map((entry: RunPerformance, idx: number): Metric => { return { - name: entry.model_type, - split: entry.split_type, - lower: entry.accuracy_lower, - upper: entry.accuracy_upper, - idx: idx + 1 + name: entry.model_type, + split: entry.split_type, + lower: entry.accuracy_lower, + upper: entry.accuracy_upper, + idx: idx + 1 } }); }; @@ -87,8 +86,8 @@ export class OverallAccPlotComponent { }, margin: { t: 40, - // b: 40, - // l: 0, + // b: 40, + // l: 0, // r: 200, }, annotations: [ @@ -116,7 +115,7 @@ export class OverallAccPlotComponent { }; const metrics: Metric[] = await metricData; const data = metrics.map(metric => { - const col: string = metric.name == "modules" ? "green": "orange" + const col: string = metric.name == "modules" ? "green" : "orange" // Add model name to y-axis labels layout.yaxis.tickvals.push(metric.idx + 1); layout.yaxis.ticktext.push(`Model ${metric.idx}`); @@ -125,13 +124,13 @@ export class OverallAccPlotComponent { x: [metric.lower, metric.upper], y: [metric.idx + 1, metric.idx + 1], mode: this.defaultPlotMode, - name: metric.name + " (" + metric.split + ")", + name: metric.name + " (" + metric.split + ")", text: ["Lower Bound (Accuracy)", "Upper Bound (Accuracy)"], hovertemplate: "%{text}: %{x:.2f}", line: { width: this.defaultLineWidth, color: col, - dash: metric.split == "train" ? "solid": "dash" + dash: metric.split == "train" ? "solid" : "dash" }, marker: { size: this.defaultMarkerSize, @@ -190,8 +189,8 @@ export class OverallAccPlotComponent { type: 'scatter' } ]; - - const config = { responsive: true }; + + const config = {responsive: true}; // remove loading spinner and show plot return Plotly.newPlot(this.overallAccPlot.nativeElement, [...data, ...customLegend], layout, config); } diff --git a/src/app/routes/spongeffects/explore/service/explore.service.spec.ts b/src/app/routes/spongeffects/explore/service/explore.service.spec.ts new file mode 100644 index 00000000..93bc70e5 --- /dev/null +++ b/src/app/routes/spongeffects/explore/service/explore.service.spec.ts @@ -0,0 +1,16 @@ +import {TestBed} from '@angular/core/testing'; + +import {ExploreService} from './explore.service'; + +describe('ExploreServiceService', () => { + let service: ExploreService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(ExploreService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/src/app/routes/spongeffects/explore/service/explore.service.ts b/src/app/routes/spongeffects/explore/service/explore.service.ts new file mode 100644 index 00000000..8788920e --- /dev/null +++ b/src/app/routes/spongeffects/explore/service/explore.service.ts @@ -0,0 +1,49 @@ +import {inject, Injectable, linkedSignal, signal} from '@angular/core'; +import {SpongEffectsService} from "../../../../services/spong-effects.service"; +import {sum} from "lodash"; + +export class Cancer { + value: string; + viewValue: string; + allSubTypes: string[]; + sampleSizes: number[]; + + base: string = "https://portal.gdc.cancer.gov/projects/TGCA-"; + + constructor(value: string, viewValue: string, allSubTypes: string[], sampleSizes: number[],) { + this.value = value; + this.viewValue = viewValue; + this.allSubTypes = allSubTypes; + this.sampleSizes = sampleSizes; + } + + addSubtype(subtype: string) { + this.allSubTypes.push(subtype); + } + + addSampleSize(sampleSize: number) { + if (sampleSize != null) this.sampleSizes.push(sampleSize); + } + + totalNumberOfSamples() { + return sum(this.sampleSizes.filter(s => s >= 0)); + } + + toString() { + return this.viewValue + " - (" + this.value + ")"; + } + + getUrl() { + return this.base + this.value + } +} + +@Injectable({ + providedIn: 'root' +}) +export class ExploreService { + spongEffectsService = inject(SpongEffectsService); + level$ = signal<'gene' | 'transcript'>('gene'); + diseaseNames$ = this.spongEffectsService.diseaseNames$; + selectedDisease$ = linkedSignal(() => this.diseaseNames$()[0]); +} diff --git a/src/app/routes/spongeffects/predict/form/predict-form.component.html b/src/app/routes/spongeffects/predict/form/predict-form.component.html new file mode 100644 index 00000000..4735c45a --- /dev/null +++ b/src/app/routes/spongeffects/predict/form/predict-form.component.html @@ -0,0 +1 @@ +

form works!

diff --git a/src/app/routes/spongeffects/predict/form/predict-form.component.scss b/src/app/routes/spongeffects/predict/form/predict-form.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/src/app/routes/spongeffects/predict/form/predict-form.component.spec.ts b/src/app/routes/spongeffects/predict/form/predict-form.component.spec.ts new file mode 100644 index 00000000..2898f9d8 --- /dev/null +++ b/src/app/routes/spongeffects/predict/form/predict-form.component.spec.ts @@ -0,0 +1,23 @@ +import {ComponentFixture, TestBed} from '@angular/core/testing'; + +import {PredictFormComponent} from './predict-form.component'; + +describe('FormComponent', () => { + let component: PredictFormComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [PredictFormComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(PredictFormComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/routes/spongeffects/predict/form/predict-form.component.ts b/src/app/routes/spongeffects/predict/form/predict-form.component.ts new file mode 100644 index 00000000..851d8f1e --- /dev/null +++ b/src/app/routes/spongeffects/predict/form/predict-form.component.ts @@ -0,0 +1,11 @@ +import {Component} from '@angular/core'; + +@Component({ + selector: 'app-predict-form', + imports: [], + templateUrl: './predict-form.component.html', + styleUrl: './predict-form.component.scss' +}) +export class PredictFormComponent { + +} diff --git a/src/app/routes/spongeffects/spongeffects.component.html b/src/app/routes/spongeffects/spongeffects.component.html index d7be10f8..1522d5d9 100644 --- a/src/app/routes/spongeffects/spongeffects.component.html +++ b/src/app/routes/spongeffects/spongeffects.component.html @@ -6,6 +6,11 @@ Predict

+ @if (mode() == 'explore') { + + } @else { + + } @if (mode() == 'explore') { diff --git a/src/app/routes/spongeffects/spongeffects.component.ts b/src/app/routes/spongeffects/spongeffects.component.ts index 0dbf8a9d..e5abd71a 100644 --- a/src/app/routes/spongeffects/spongeffects.component.ts +++ b/src/app/routes/spongeffects/spongeffects.component.ts @@ -1,8 +1,12 @@ -import {Component, model} from '@angular/core'; +import {Component, inject, model, resource} from '@angular/core'; import {MatDrawer, MatDrawerContainer, MatDrawerContent} from "@angular/material/sidenav"; import {MatButtonToggle, MatButtonToggleGroup} from "@angular/material/button-toggle"; import {ExploreComponent} from "./explore/explore.component"; import {PredictComponent} from "./predict/predict.component"; +import {VersionsService} from "../../services/versions.service"; +import {BackendService} from "../../services/backend.service"; +import {PredictFormComponent} from "./predict/form/predict-form.component"; +import {ExploreFormComponent} from "./explore/form/explore-form.component"; // import { Tab, Cancer, PlotlyData } from '../../models/spongeffects.model'; @@ -16,10 +20,23 @@ import {PredictComponent} from "./predict/predict.component"; MatButtonToggleGroup, MatButtonToggle, ExploreComponent, - PredictComponent + PredictComponent, + PredictFormComponent, + PredictFormComponent, + ExploreFormComponent ], styleUrls: ['./spongeffects.component.scss'] }) export class SpongEffectsComponent { + versionsService = inject(VersionsService); + backend = inject(BackendService); + version$ = this.versionsService.versionReadOnly(); mode = model<'explore' | 'predict'>('explore'); + + spongeEffectsRuns = resource({ + request: this.version$, + loader: (version) => ( + this.backend.getSpongEffectsRuns(version.request) + ) + }) } diff --git a/src/app/services/explore.service.ts b/src/app/services/explore.service.ts index 22faf721..3cbea5a5 100644 --- a/src/app/services/explore.service.ts +++ b/src/app/services/explore.service.ts @@ -1,9 +1,7 @@ -import {Injectable, Signal, signal, computed, ResourceRef, resource, effect} from '@angular/core'; -import { BackendService } from './backend.service'; -import { Cancer } from '../routes/spongeffects/explore/explore.component'; -import { SpongEffectsRun, Dataset } from '../interfaces'; -import { VersionsService } from './versions.service'; -import { ExploreQuery } from '../interfaces'; +import {computed, Injectable, resource, ResourceRef, Signal, signal} from '@angular/core'; +import {BackendService} from './backend.service'; +import {ExploreQuery, SpongEffectsRun} from '../interfaces'; +import {VersionsService} from './versions.service'; interface ExploreSelection { selectedCancer: string; @@ -11,7 +9,7 @@ interface ExploreSelection { } @Injectable({ - providedIn: 'root', + providedIn: 'root', }) export class ExploreService { private readonly _spongEffectsRuns$: ResourceRef; @@ -20,10 +18,7 @@ export class ExploreService { private readonly _query$ = signal(undefined); private readonly _currentData$: ResourceRef; readonly cancer$ = computed(() => this._currentData$.value()?.selectedCancer); - readonly level$ = computed(() => this._currentData$.value()?.selectedLevel); - - - + constructor(private backend: BackendService, versionsService: VersionsService) { this._version$ = versionsService.versionReadOnly(); @@ -36,11 +31,11 @@ export class ExploreService { }); this._spongEffectsRunDatasets$ = computed(() => { - const runs = this._spongEffectsRuns$.value() || []; - return [...new Set(runs.map((run: SpongEffectsRun) => run.disease_name))]; - } + const runs = this._spongEffectsRuns$.value() || []; + return [...new Set(runs.map((run: SpongEffectsRun) => run.disease_name))]; + } ); - + const requestData = computed(() => { return { version: this._version$(), @@ -52,14 +47,6 @@ export class ExploreService { request: requestData, loader: (param) => this.fetchData(param.request.version, param.request.config), }) - - effect(() => { - // const graph = this.graph$(); - // const initialState: EntityState = {[State.Hover]: false, [State.Active]: false}; - // this._nodeStates$.set(Object.fromEntries(graph.nodes().map(node => [node, initialState]))); - // this._edgeStates$.set(Object.fromEntries(graph.edges().map(edge => [edge, initialState]))); - }); - } runQuery(query: ExploreQuery) { @@ -67,53 +54,26 @@ export class ExploreService { } async fetchData(version: number, config: ExploreQuery | undefined): Promise { - if (config === undefined) { - return { - selectedCancer: "", - // selectedCancer: { - // dataset_ID: 0, - // disease_name: "", - // disease_subtype: "", - // data_origin: "", - // disease_type: "", - // download_url: "", - // sponge_db_version: 0 - // }, - selectedLevel: "" - } - } - - // const nodes = await this.backend.getNodes(version, config); - // Get gene IDs or transcript IDs respectively - // const identifiers = nodes.map(node => 'gene' in node ? node.gene.ensg_number : node.transcript.enst_number); - // const interactions = - // await this.backend.getInteractionsSpecific(version, config.dataset, config.maxPValue, identifiers, config.level); - + if (config === undefined) { return { - selectedCancer: config.selectedCancer, - selectedLevel: config.selectedLevel + selectedCancer: "", + + selectedLevel: "" } } + + return { + selectedCancer: config.selectedCancer, + selectedLevel: config.selectedLevel + } + } + spongEffectsRunDataset() { return this._spongEffectsRunDatasets$; } - // private initCancerInfo() { - // // const spongEffectsCancerAbbreviations: string[] = ['BRCA', 'CESC', 'ESCA', 'HNSC', 'LGG', 'SARC', 'STAD', 'TGCT', 'UCEC']; - - // // get cancer information from backend: for each sponge_run get dataset information - // let datasets = this.backend.getDatasets(this._version$()); - // console.log(datasets); - // // save all datasets that have a spongEffectsRun in 'cancers' - // console.log("before", datasets.then(cancers => cancers.map(cancer => cancer.disease_name))); - // let cancers = datasets.then(datasets => datasets.filter(dataset => dataset.dataset_ID in this.spongEffectsRuns.then(spongEffectsRuns => spongEffectsRuns.map(spongEffectsRun => spongEffectsRun.dataset_ID)))); - // console.log("here", cancers.then(cancers => cancers.map(cancer => cancer.disease_name))); - // return cancers - // } - - } diff --git a/src/app/services/spong-effects.service.spec.ts b/src/app/services/spong-effects.service.spec.ts new file mode 100644 index 00000000..0fb1f25e --- /dev/null +++ b/src/app/services/spong-effects.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { SpongEffectsService } from './spong-effects.service'; + +describe('SpongEffectsService', () => { + let service: SpongEffectsService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(SpongEffectsService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/src/app/services/spong-effects.service.ts b/src/app/services/spong-effects.service.ts new file mode 100644 index 00000000..7663eb54 --- /dev/null +++ b/src/app/services/spong-effects.service.ts @@ -0,0 +1,24 @@ +import {computed, inject, Injectable, resource} from '@angular/core'; +import {BackendService} from "./backend.service"; +import {VersionsService} from "./versions.service"; +import {SpongEffectsRun} from "../interfaces"; + +@Injectable({ + providedIn: 'root' +}) +export class SpongEffectsService { + backend = inject(BackendService); + versionsService = inject(VersionsService); + diseaseNames$ = computed(() => { + const runs = this.spongEffectsRuns$.value() || []; + return runs.map((run: SpongEffectsRun) => run.disease_name) + .filter((value: string, index: number, self: Array) => self.indexOf(value) === index); + }); + private readonly _version$ = this.versionsService.versionReadOnly(); + spongEffectsRuns$ = resource({ + request: this._version$, + loader: async (version) => ( + await this.backend.getSpongEffectsRuns(version.request) + ) + }); +} From e04c997e2ebdea77d8d4989ab8eb0b4fdce20e43 Mon Sep 17 00:00:00 2001 From: Nico Trummer Date: Tue, 17 Dec 2024 22:49:45 +0100 Subject: [PATCH 08/14] Use tab structure in spongEffects explore --- .../explore/explore.component.html | 154 +++++++----------- .../spongeffects/explore/explore.component.ts | 17 +- 2 files changed, 64 insertions(+), 107 deletions(-) diff --git a/src/app/routes/spongeffects/explore/explore.component.html b/src/app/routes/spongeffects/explore/explore.component.html index 89622e07..3b4782b9 100644 --- a/src/app/routes/spongeffects/explore/explore.component.html +++ b/src/app/routes/spongeffects/explore/explore.component.html @@ -1,4 +1,3 @@ -

@@ -31,98 +30,63 @@ - - - - - search - Explore results - - spongEffects predictions for selected project - -

- - - - - - - - - Classification - - Classification through enrichment score distributions - - - - - - info - What does this mean? - - -
-

The spongEffects enrichment score distributions can give an insight into the model capability to - differentiate between the given classes.

-
-
-
- -
-
- - - - - Top ceRNA centralities - - View predicted centralities with the highest decrease in Gini index. - - - - - - info - What does this mean? - - -
-

The mean decrease in the Gini coefficient tells us how much each variable affects the consistency of - nodes and leaves in a random forest. If the mean decrease accuracy or mean decrease Gini score is higher, - it means the variable is more important in the model.

-

Module centralities that are at the top right of the plot below play a crucial role in distinguishing - between the various cancer (sub-)types.

-
-
-
- - - - - Gene Set Enrichment Analysis - - View GSEA results for the selected disease type. - - - - - - info - What does this mean? - - -
-

Gene Set Enrichment Analysis (GSEA) identifies whether specific sets of genes related to biological - functions are enriched in gene expression data.

-

The information from GSEA can help find biological differences between disease (sub-)types

-
-
-
-
-
- -

-
-
-
+ + + + + + + + + + + + info + What does this mean? + + +
+

The spongEffects enrichment score distributions can give an insight into the model capability to + differentiate between the given classes.

+
+
+
+ +
+
+ + + + + info + What does this mean? + + +
+

The mean decrease in the Gini coefficient tells us how much each variable affects the consistency of + nodes and leaves in a random forest. If the mean decrease accuracy or mean decrease Gini score is higher, + it means the variable is more important in the model.

+

Module centralities that are at the top right of the plot below play a crucial role in distinguishing + between the various cancer (sub-)types.

+
+
+
+ + + + + info + What does this mean? + + +
+

Gene Set Enrichment Analysis (GSEA) identifies whether specific sets of genes related to biological + functions are enriched in gene expression data.

+

The information from GSEA can help find biological differences between disease (sub-)types

+
+
+
+
+
+
diff --git a/src/app/routes/spongeffects/explore/explore.component.ts b/src/app/routes/spongeffects/explore/explore.component.ts index 9937b415..97b68ed7 100644 --- a/src/app/routes/spongeffects/explore/explore.component.ts +++ b/src/app/routes/spongeffects/explore/explore.component.ts @@ -1,11 +1,6 @@ import {Component} from '@angular/core'; -import { - MatExpansionModule, - MatExpansionPanel, - MatExpansionPanelDescription, - MatExpansionPanelTitle -} from "@angular/material/expansion"; -import {MatIcon, MatIconModule} from "@angular/material/icon"; +import {MatExpansionModule} from "@angular/material/expansion"; +import {MatIconModule} from "@angular/material/icon"; import {MatFormFieldModule} from "@angular/material/form-field"; import {FormsModule, ReactiveFormsModule} from "@angular/forms"; import {MatButtonToggleModule} from '@angular/material/button-toggle'; @@ -13,14 +8,11 @@ import {MatProgressSpinnerModule} from "@angular/material/progress-spinner"; import {MatCardModule} from '@angular/material/card'; import {ClassPerformancePlotComponent} from "./plots/class-performance-plot/class-performance-plot.component"; import {OverallAccPlotComponent} from "./plots/overall-acc-plot/overall-acc-plot.component"; +import {MatTabsModule} from "@angular/material/tabs"; @Component({ selector: 'app-explore', imports: [ - MatExpansionPanel, - MatExpansionPanelTitle, - MatIcon, - MatExpansionPanelDescription, MatExpansionModule, MatFormFieldModule, FormsModule, @@ -30,7 +22,8 @@ import {OverallAccPlotComponent} from "./plots/overall-acc-plot/overall-acc-plot MatIconModule, MatCardModule, ClassPerformancePlotComponent, - OverallAccPlotComponent + OverallAccPlotComponent, + MatTabsModule ], templateUrl: './explore.component.html', styleUrls: ['./explore.component.scss', '../spongeffects.component.scss'] From 899de498ddf2437dbd469fcc5a706026cf2da155 Mon Sep 17 00:00:00 2001 From: Nico Trummer Date: Tue, 17 Dec 2024 22:55:47 +0100 Subject: [PATCH 09/14] Remove expansion panel leftovers from spongeEffects explore tabs --- .../class-performance-plot.component.html | 46 ++++++--------- .../overall-acc-plot.component.html | 58 +++++++++---------- 2 files changed, 45 insertions(+), 59 deletions(-) diff --git a/src/app/routes/spongeffects/explore/plots/class-performance-plot/class-performance-plot.component.html b/src/app/routes/spongeffects/explore/plots/class-performance-plot/class-performance-plot.component.html index 84dbbb3a..0534429a 100644 --- a/src/app/routes/spongeffects/explore/plots/class-performance-plot/class-performance-plot.component.html +++ b/src/app/routes/spongeffects/explore/plots/class-performance-plot/class-performance-plot.component.html @@ -1,32 +1,22 @@ - - - Class performance - - Class specific performance - - +
+ +
+
+
+
-
- -
- -
-
+ + + + info + What does this mean? + + +
+

This plot shows the performance of the individual class predictions of the model that was trained with the + spongEffects central modules against a model with randomly selected modules. We can strengthen the confidence in + the models prediction if it is capable of outperforming its random counterpart. Interpretation of predictions + should be made with more caution if this it not the case.

- - - - - info - What does this mean? - - -
-

This plot shows the performance of the individual class predictions of the model that was trained with the - spongEffects central modules against a model with randomly selected modules. We can strengthen the confidence in - the models prediction if it is capable of outperforming its random counterpart. Interpretation of predictions - should be made with more caution if this it not the case.

-
-
diff --git a/src/app/routes/spongeffects/explore/plots/overall-acc-plot/overall-acc-plot.component.html b/src/app/routes/spongeffects/explore/plots/overall-acc-plot/overall-acc-plot.component.html index e3ad144d..be0035b7 100644 --- a/src/app/routes/spongeffects/explore/plots/overall-acc-plot/overall-acc-plot.component.html +++ b/src/app/routes/spongeffects/explore/plots/overall-acc-plot/overall-acc-plot.component.html @@ -1,31 +1,27 @@ - - - Model performance - - Accuracy of spongEffects modules against random modules - - - @if (plotOverallAccResource.isLoading()) { - - } @else { - @if (!plotOverallAccResource.value()) { -

No data available

- } - } -
-
-
- - - infoWhat does this mean? - -
-

- The models performance is compared against a model that was trained on randomly selected SPONGE centralities. - The model that was trained on the spongEffects modules should outperform the random approach. - Dotted lines represent the overall accuracy for the test split of the data and solid lines show analogously show the training performance. - Each line start (circle) and end (diamond) mark the lower and upper balanced accuracy bounds of a specific model. -

-
-
-
\ No newline at end of file +@if (plotOverallAccResource.isLoading()) { + +} @else { + @if (!plotOverallAccResource.value()) { +

No data available

+ } +} +
+
+
+ + + + info + What does this mean? + + +
+

+ The models performance is compared against a model that was trained on randomly selected SPONGE centralities. + The model that was trained on the spongEffects modules should outperform the random approach. + Dotted lines represent the overall accuracy for the test split of the data and solid lines show analogously show + the training performance. + Each line start (circle) and end (diamond) mark the lower and upper balanced accuracy bounds of a specific model. +

+
+
From e1e3e66a848e3ba70a41c90ee660aedf4e1b6673 Mon Sep 17 00:00:00 2001 From: Nico Trummer Date: Tue, 17 Dec 2024 23:23:22 +0100 Subject: [PATCH 10/14] Implement info component --- src/app/components/info/info.component.html | 29 ++ src/app/components/info/info.component.scss | 0 .../components/info/info.component.spec.ts | 23 ++ src/app/components/info/info.component.ts | 29 ++ .../explore/explore.component.html | 33 -- .../spongeffects/explore/explore.component.ts | 4 +- .../spongeffects/spongeffects.component.html | 24 ++ .../spongeffects/spongeffects.component.ts | 4 +- src/app/services/explore.service.ts | 82 ----- src/app/spongeffects.interfaces.ts | 339 ------------------ 10 files changed, 111 insertions(+), 456 deletions(-) create mode 100644 src/app/components/info/info.component.html create mode 100644 src/app/components/info/info.component.scss create mode 100644 src/app/components/info/info.component.spec.ts create mode 100644 src/app/components/info/info.component.ts delete mode 100644 src/app/services/explore.service.ts delete mode 100644 src/app/spongeffects.interfaces.ts diff --git a/src/app/components/info/info.component.html b/src/app/components/info/info.component.html new file mode 100644 index 00000000..30bf405b --- /dev/null +++ b/src/app/components/info/info.component.html @@ -0,0 +1,29 @@ +@if (type() == 'modal') { +
+ +
+} @else { + + + + info + {{ title() }} + + + {{ subtitle() }} + + + + +} + + +

{{ title() }}

+

{{ subtitle() }}

+ + + +
diff --git a/src/app/components/info/info.component.scss b/src/app/components/info/info.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/src/app/components/info/info.component.spec.ts b/src/app/components/info/info.component.spec.ts new file mode 100644 index 00000000..6f7e9e07 --- /dev/null +++ b/src/app/components/info/info.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { InfoComponent } from './info.component'; + +describe('InfoComponent', () => { + let component: InfoComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [InfoComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(InfoComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/components/info/info.component.ts b/src/app/components/info/info.component.ts new file mode 100644 index 00000000..39b55bc9 --- /dev/null +++ b/src/app/components/info/info.component.ts @@ -0,0 +1,29 @@ +import {Component, inject, input, viewChild} from '@angular/core'; +import {MatButton} from "@angular/material/button"; +import {MatExpansionModule} from "@angular/material/expansion"; +import {MatIcon} from "@angular/material/icon"; +import {MatDialog, MatDialogModule} from "@angular/material/dialog"; + +@Component({ + selector: 'app-info', + imports: [ + MatButton, + MatExpansionModule, + MatIcon, + MatDialogModule + ], + templateUrl: './info.component.html', + styleUrl: './info.component.scss' +}) +export class InfoComponent { + dialog = inject(MatDialog); + dialogTemplate = viewChild('dialog'); + + title = input.required(); + subtitle = input(); + type = input<'modal' | 'panel'>('modal'); + + openDialog() { + this.dialog.open(this.dialogTemplate()) + } +} diff --git a/src/app/routes/spongeffects/explore/explore.component.html b/src/app/routes/spongeffects/explore/explore.component.html index 3b4782b9..97f1d0c9 100644 --- a/src/app/routes/spongeffects/explore/explore.component.html +++ b/src/app/routes/spongeffects/explore/explore.component.html @@ -1,36 +1,3 @@ - - - - info - What is spongEffects? - - - General information about spongEffects on TCGA and custom usage - - -
-

spongEffects can be used for downstream analysis of - SPONGE ceRNA networks and is capable of tumor sub-type classification and biomarker discovery via a random forest - machine learning approach.

-

For more detailed information refer to the publication. -

-

We trained spongEffects prediction models on the TCGA pan-cancer dataset and nine other TCGA projects that - feature sufficient cancer subtyping for classification.

-

- The tab below shows the spongEffects prediction results for a selected TCGA project. - You can browse the hub-nodes that spongEffects models views as potential biomarkers and compare their - gene/transcript expression. -

-

- We can use the pretrained TCGA models to classify tumor (sub-)types and predict biomarkers on new independent - input data that can be uploaded via the - tab below. -

-

The model will predict a tumor type for every sample in the uploaded expression file individually.

-
-
- - diff --git a/src/app/routes/spongeffects/explore/explore.component.ts b/src/app/routes/spongeffects/explore/explore.component.ts index 97b68ed7..f14cb88c 100644 --- a/src/app/routes/spongeffects/explore/explore.component.ts +++ b/src/app/routes/spongeffects/explore/explore.component.ts @@ -9,6 +9,7 @@ import {MatCardModule} from '@angular/material/card'; import {ClassPerformancePlotComponent} from "./plots/class-performance-plot/class-performance-plot.component"; import {OverallAccPlotComponent} from "./plots/overall-acc-plot/overall-acc-plot.component"; import {MatTabsModule} from "@angular/material/tabs"; +import {InfoComponent} from "../../../components/info/info.component"; @Component({ selector: 'app-explore', @@ -23,7 +24,8 @@ import {MatTabsModule} from "@angular/material/tabs"; MatCardModule, ClassPerformancePlotComponent, OverallAccPlotComponent, - MatTabsModule + MatTabsModule, + InfoComponent ], templateUrl: './explore.component.html', styleUrls: ['./explore.component.scss', '../spongeffects.component.scss'] diff --git a/src/app/routes/spongeffects/spongeffects.component.html b/src/app/routes/spongeffects/spongeffects.component.html index 1522d5d9..b58b662c 100644 --- a/src/app/routes/spongeffects/spongeffects.component.html +++ b/src/app/routes/spongeffects/spongeffects.component.html @@ -1,5 +1,29 @@ + +
+

spongEffects can be used for downstream analysis of + SPONGE ceRNA networks and is capable of tumor sub-type classification and biomarker discovery via a random + forest + machine learning approach.

+

For more detailed information refer to the publication. +

+

We trained spongEffects prediction models on the TCGA pan-cancer dataset and nine other TCGA projects that + feature sufficient cancer subtyping for classification.

+

+ The tab below shows the spongEffects prediction results for a selected TCGA project. + You can browse the hub-nodes that spongEffects models views as potential biomarkers and compare their + gene/transcript expression. +

+

+ We can use the pretrained TCGA models to classify tumor (sub-)types and predict biomarkers on new independent + input data that can be uploaded via the + tab below. +

+

The model will predict a tumor type for every sample in the uploaded expression file individually.

+
+
Explore diff --git a/src/app/routes/spongeffects/spongeffects.component.ts b/src/app/routes/spongeffects/spongeffects.component.ts index e5abd71a..6854ea94 100644 --- a/src/app/routes/spongeffects/spongeffects.component.ts +++ b/src/app/routes/spongeffects/spongeffects.component.ts @@ -7,6 +7,7 @@ import {VersionsService} from "../../services/versions.service"; import {BackendService} from "../../services/backend.service"; import {PredictFormComponent} from "./predict/form/predict-form.component"; import {ExploreFormComponent} from "./explore/form/explore-form.component"; +import {InfoComponent} from "../../components/info/info.component"; // import { Tab, Cancer, PlotlyData } from '../../models/spongeffects.model'; @@ -23,7 +24,8 @@ import {ExploreFormComponent} from "./explore/form/explore-form.component"; PredictComponent, PredictFormComponent, PredictFormComponent, - ExploreFormComponent + ExploreFormComponent, + InfoComponent ], styleUrls: ['./spongeffects.component.scss'] }) diff --git a/src/app/services/explore.service.ts b/src/app/services/explore.service.ts deleted file mode 100644 index 3cbea5a5..00000000 --- a/src/app/services/explore.service.ts +++ /dev/null @@ -1,82 +0,0 @@ -import {computed, Injectable, resource, ResourceRef, Signal, signal} from '@angular/core'; -import {BackendService} from './backend.service'; -import {ExploreQuery, SpongEffectsRun} from '../interfaces'; -import {VersionsService} from './versions.service'; - -interface ExploreSelection { - selectedCancer: string; - selectedLevel: string; -} - -@Injectable({ - providedIn: 'root', -}) -export class ExploreService { - private readonly _spongEffectsRuns$: ResourceRef; - private readonly _spongEffectsRunDatasets$: Signal; - private readonly _version$: Signal; - private readonly _query$ = signal(undefined); - private readonly _currentData$: ResourceRef; - readonly cancer$ = computed(() => this._currentData$.value()?.selectedCancer); - - - constructor(private backend: BackendService, versionsService: VersionsService) { - this._version$ = versionsService.versionReadOnly(); - - this._spongEffectsRuns$ = resource({ - request: this._version$, - loader: async (version) => ( - await backend.getSpongEffectsRuns(version.request) - ) - }); - - this._spongEffectsRunDatasets$ = computed(() => { - const runs = this._spongEffectsRuns$.value() || []; - return [...new Set(runs.map((run: SpongEffectsRun) => run.disease_name))]; - } - ); - - const requestData = computed(() => { - return { - version: this._version$(), - config: this._query$() - } - }); - - this._currentData$ = resource({ - request: requestData, - loader: (param) => this.fetchData(param.request.version, param.request.config), - }) - } - - runQuery(query: ExploreQuery) { - this._query$.set(query); - } - - async fetchData(version: number, config: ExploreQuery | undefined): Promise { - if (config === undefined) { - return { - selectedCancer: "", - - selectedLevel: "" - } - } - - - return { - selectedCancer: config.selectedCancer, - selectedLevel: config.selectedLevel - } - } - - spongEffectsRunDataset() { - return this._spongEffectsRunDatasets$; - } - - -} - - - - - diff --git a/src/app/spongeffects.interfaces.ts b/src/app/spongeffects.interfaces.ts deleted file mode 100644 index 364bef73..00000000 --- a/src/app/spongeffects.interfaces.ts +++ /dev/null @@ -1,339 +0,0 @@ -export interface Dataset { - dataset_ID: number, - disease_name: string, - data_origin: string, - disease_type: string, - download_url: string, - disease_subtype: string, - study_abbreviation: string, - sponge_db_version: string - } - - export interface DatasetInfo { - dataset_ID: number, - disease_name: string, - data_origin: string, - disease_type: string, - download_url: string, - disease_subtype: string, - study_abbreviation: string, - number_of_samples: number, - sponge_db_version: string - } - - export interface RunInfo { - "coefficient_direction": string, - "coefficient_threshold": string, - "dataset": { - "dataset_ID": number, - "disease_name": string - }, - "f_test": boolean, - "f_test_p_adj_threshold": number, - "ks": string, - "log_level": string, - "m_max": number, - "min_corr": number, - "number_of_datasets": number, - "number_of_samples": number, - "run_ID": number, - "variance_cutoff": string - } - - export interface OverallCounts { - count_interactions: number, - count_interactions_sign: number, - count_shared_miRNAs: number, - disease_name: string, - run_ID: number - } - - export enum GeneSorting { - Betweenness = "betweenness", - Degree = "degree", - Eigenvector = "eigenvector" - } - - export enum InteractionSorting { - pAdj = "adjusted p-value", - mScor = "MScor", - Correlation = "Correlation" - } - - export interface Gene { - ensg_number: string, - gene_symbol?: string - } - - export interface CeRNA { - betweenness: number, - eigenvector: number, - gene: Gene, - node_degree: number - run: { - dataset: { - data_origin: string, - dataset_ID: number, - disease_name: string - }, - run_ID: number - } - } - - export interface CeRNAInteraction { - "correlation": number, - "gene1": Gene, - "gene2": Gene, - "mscor": number, - "p_value": number, - "run": { - "dataset": { - "data_origin": string, - "dataset_ID": number, - "disease_name": string - }, - "run_ID": number - } - } - - export interface CeRNAQuery { - disease: Dataset, - geneSorting: GeneSorting, - maxGenes: number, - minDegree: number, - minBetweenness: number, - minEigen: number, - interactionSorting: InteractionSorting, - maxInteractions: number, - maxPValue: number, - minMScore: number - } - - export interface CeRNAExpression { - "dataset": string, - "expr_value": number, - "gene": Gene, - "sample_ID": string - } - - export interface TranscriptExpression { - "dataset": string, - "transcript": string, - "expr_value": number, - "gene": { - "ensg_number": string, - "gene_symbol": Gene - }, - "sample_ID": string, - } - - export interface SurvivalRate { - "dataset": string, - "gene": Gene, - "overexpression": number, - "patient_information": { - "disease_status": number, - "sample_ID": string, - "survival_time": number - } - } - - export interface SurvivalPValue { - "dataset": string, - "gene": Gene, - "pValue": number - } - - export interface GeneCount { - "count_all": number, - "count_sign": number, - "gene": { - "ensg_number": string, - "gene_symbol": string - }, - "run": { - "dataset": { - "data_origin": string, - "dataset_ID": number, - "disease_name": string - }, - "run_ID": number - } - } - - // from spongEffects - // route responses - - export interface SpongEffectsRun { - spongeEffects_run_ID: number, - m_scor_threshold: number, - p_adjust_threshold: number, - modules_cutoff: number, - bin_size: number, - min_size: number, - max_size: number, - min_expr: number, - method: string, - cv_folds: number - level: string, - sponge_run_ID: number, - variance_cutoff: number, - f_test: boolean, - f_test_p_adj_threshold: number, - coefficient_threshold: number, - coefficient_direction: string, - min_corr: number, - number_of_datasets: number, - number_of_samples: number, - ks: string, - m_max: number, - log_level: string, - sponge_db_version: string, - dataset_ID: number, - disease_name: string, - data_origin: string, - disease_type: string, - download_url: string, - disease_subtype: string, - } - - export interface RunPerformance { - model_type: string, - split_type: string, - accuracy: number, - kappa: number, - accuracy_lower: number, - accuracy_upper: number, - accuracy_null: number, - accuracy_p_value: number, - mcnemar_p_value: number - } - - export interface RunClassPerformance { - prediction_class: string; - sensitivity: number; - specificity: number; - pos_pred_value: number; - neg_pred_value: number; - precision_value: number; - recall: number; - f1: number; - prevalence: number; - detection_rate: number; - detection_prevalence: number; - balanced_accuracy: number; - spongEffects_run: { - model_type: string; - split_type: string; - }; - } - - export interface EnrichmentScoreDistributions { - prediction_class: string; - enrichment_score: number; - density: number; - } - - export interface SpongEffectsGeneModules { - ensg_number: string; - gene_symbol: string; - mean_gini_decrease: number; - mean_accuracy_decrease: number; - } - - export interface SpongEffectsGeneModuleMembers { - hub_ensg_number: string; - hub_gene_symbol: string; - member_ensg_number: string; - member_gene_symbol: string; - } - - export interface SpongEffectsTranscriptModules { - enst_number: string; - gene: { - ensg_number: string; - gene_symbol: string; - }; - mean_gini_decrease: number; - mean_accuracy_decrease: number; - } - - export interface SpongEffectsTranscriptModuleMembers { - hub_enst_number: string; - hub_gene: { - ensg_number: string; - gene_symbol: string; - }; - member_enst_number: string; - member_gene: { - ensg_number: string; - gene_symbol: string; - }; - } - - export interface PredictCancerType { - meta: { - runtime: number; - level: string; - n_samples: number; - type_predict: string; - subtype_predict: string; - }; - data: { - sampleID: string; - typePrediction: string; - subtypePrediction: string; - }[]; - } - - // other interfaces for spongEffects - - export interface Metric { - name: string, - split: string - lower: number, - upper: number, - level: number - } - - export interface SelectElement { - value: string, - viewValue: string - } - - export interface CancerInfo { - text: string[], - link: string; - } - - export interface PlotData { - x: number[], - y: number[] - } - - export interface PlotlyData { - data: any, - layout?: any, - config?: any - } - - export interface Tab extends SelectElement { - icon: string - } - - export interface LinearRegression { - slope: number, - x0: number - } - - export interface ExampleExpression { - id: string; - sample1: number; - sample2: number; - sample3: number; - sample4: number; - sampleN: number; - } - - - \ No newline at end of file From 82cf049f4ab39cde103b33946c126aa9e8cd3b43 Mon Sep 17 00:00:00 2001 From: Nico Trummer Date: Tue, 17 Dec 2024 23:33:22 +0100 Subject: [PATCH 11/14] Improve info component --- src/app/components/info/info.component.html | 20 +++++++++----- src/app/components/info/info.component.ts | 6 +++-- .../spongeffects/explore/explore.component.ts | 3 +-- .../class-performance-plot.component.html | 20 +++++--------- .../class-performance-plot.component.ts | 4 ++- .../overall-acc-plot.component.html | 26 +++++++------------ .../overall-acc-plot.component.ts | 4 ++- 7 files changed, 40 insertions(+), 43 deletions(-) diff --git a/src/app/components/info/info.component.html b/src/app/components/info/info.component.html index 30bf405b..9af08227 100644 --- a/src/app/components/info/info.component.html +++ b/src/app/components/info/info.component.html @@ -12,18 +12,26 @@ info {{ title() }} - - {{ subtitle() }} - + @if (subtitle(); as subtitle) { + + {{ subtitle }} + + } - + }

{{ title() }}

-

{{ subtitle() }}

+ @if (subtitle(); as subtitle) { +

{{ subtitle }}

+ } - +
+ + + + diff --git a/src/app/components/info/info.component.ts b/src/app/components/info/info.component.ts index 39b55bc9..f0e8351f 100644 --- a/src/app/components/info/info.component.ts +++ b/src/app/components/info/info.component.ts @@ -3,6 +3,7 @@ import {MatButton} from "@angular/material/button"; import {MatExpansionModule} from "@angular/material/expansion"; import {MatIcon} from "@angular/material/icon"; import {MatDialog, MatDialogModule} from "@angular/material/dialog"; +import {NgTemplateOutlet} from "@angular/common"; @Component({ selector: 'app-info', @@ -10,7 +11,8 @@ import {MatDialog, MatDialogModule} from "@angular/material/dialog"; MatButton, MatExpansionModule, MatIcon, - MatDialogModule + MatDialogModule, + NgTemplateOutlet ], templateUrl: './info.component.html', styleUrl: './info.component.scss' @@ -19,7 +21,7 @@ export class InfoComponent { dialog = inject(MatDialog); dialogTemplate = viewChild('dialog'); - title = input.required(); + title = input('What does this mean?'); subtitle = input(); type = input<'modal' | 'panel'>('modal'); diff --git a/src/app/routes/spongeffects/explore/explore.component.ts b/src/app/routes/spongeffects/explore/explore.component.ts index f14cb88c..7c76b15b 100644 --- a/src/app/routes/spongeffects/explore/explore.component.ts +++ b/src/app/routes/spongeffects/explore/explore.component.ts @@ -9,7 +9,6 @@ import {MatCardModule} from '@angular/material/card'; import {ClassPerformancePlotComponent} from "./plots/class-performance-plot/class-performance-plot.component"; import {OverallAccPlotComponent} from "./plots/overall-acc-plot/overall-acc-plot.component"; import {MatTabsModule} from "@angular/material/tabs"; -import {InfoComponent} from "../../../components/info/info.component"; @Component({ selector: 'app-explore', @@ -25,7 +24,7 @@ import {InfoComponent} from "../../../components/info/info.component"; ClassPerformancePlotComponent, OverallAccPlotComponent, MatTabsModule, - InfoComponent + ], templateUrl: './explore.component.html', styleUrls: ['./explore.component.scss', '../spongeffects.component.scss'] diff --git a/src/app/routes/spongeffects/explore/plots/class-performance-plot/class-performance-plot.component.html b/src/app/routes/spongeffects/explore/plots/class-performance-plot/class-performance-plot.component.html index 0534429a..7751963b 100644 --- a/src/app/routes/spongeffects/explore/plots/class-performance-plot/class-performance-plot.component.html +++ b/src/app/routes/spongeffects/explore/plots/class-performance-plot/class-performance-plot.component.html @@ -6,17 +6,9 @@
- - - - info - What does this mean? - - -
-

This plot shows the performance of the individual class predictions of the model that was trained with the - spongEffects central modules against a model with randomly selected modules. We can strengthen the confidence in - the models prediction if it is capable of outperforming its random counterpart. Interpretation of predictions - should be made with more caution if this it not the case.

-
-
+ +

This plot shows the performance of the individual class predictions of the model that was trained with the + spongEffects central modules against a model with randomly selected modules. We can strengthen the confidence in + the models prediction if it is capable of outperforming its random counterpart. Interpretation of predictions + should be made with more caution if this it not the case.

+
diff --git a/src/app/routes/spongeffects/explore/plots/class-performance-plot/class-performance-plot.component.ts b/src/app/routes/spongeffects/explore/plots/class-performance-plot/class-performance-plot.component.ts index 959d34f7..2261ebfc 100644 --- a/src/app/routes/spongeffects/explore/plots/class-performance-plot/class-performance-plot.component.ts +++ b/src/app/routes/spongeffects/explore/plots/class-performance-plot/class-performance-plot.component.ts @@ -10,6 +10,7 @@ import {VersionsService} from '../../../../../services/versions.service'; import {BackendService} from '../../../../../services/backend.service'; import {sum} from "lodash"; import {ExploreService} from "../../service/explore.service"; +import {InfoComponent} from "../../../../../components/info/info.component"; declare var Plotly: any; @@ -22,7 +23,8 @@ declare var Plotly: any; MatSelectModule, FormsModule, ReactiveFormsModule, - MatProgressBarModule + MatProgressBarModule, + InfoComponent ], templateUrl: './class-performance-plot.component.html', styleUrl: './class-performance-plot.component.scss' diff --git a/src/app/routes/spongeffects/explore/plots/overall-acc-plot/overall-acc-plot.component.html b/src/app/routes/spongeffects/explore/plots/overall-acc-plot/overall-acc-plot.component.html index be0035b7..fdbe7a1c 100644 --- a/src/app/routes/spongeffects/explore/plots/overall-acc-plot/overall-acc-plot.component.html +++ b/src/app/routes/spongeffects/explore/plots/overall-acc-plot/overall-acc-plot.component.html @@ -8,20 +8,12 @@
- - - - info - What does this mean? - - -
-

- The models performance is compared against a model that was trained on randomly selected SPONGE centralities. - The model that was trained on the spongEffects modules should outperform the random approach. - Dotted lines represent the overall accuracy for the test split of the data and solid lines show analogously show - the training performance. - Each line start (circle) and end (diamond) mark the lower and upper balanced accuracy bounds of a specific model. -

-
-
+ +

+ The models performance is compared against a model that was trained on randomly selected SPONGE centralities. + The model that was trained on the spongEffects modules should outperform the random approach. + Dotted lines represent the overall accuracy for the test split of the data and solid lines show analogously show + the training performance. + Each line start (circle) and end (diamond) mark the lower and upper balanced accuracy bounds of a specific model. +

+
diff --git a/src/app/routes/spongeffects/explore/plots/overall-acc-plot/overall-acc-plot.component.ts b/src/app/routes/spongeffects/explore/plots/overall-acc-plot/overall-acc-plot.component.ts index b82cb1f3..f1bb65ea 100644 --- a/src/app/routes/spongeffects/explore/plots/overall-acc-plot/overall-acc-plot.component.ts +++ b/src/app/routes/spongeffects/explore/plots/overall-acc-plot/overall-acc-plot.component.ts @@ -9,6 +9,7 @@ import {MatSelectModule} from '@angular/material/select'; import {FormsModule, ReactiveFormsModule} from '@angular/forms'; import {MatProgressBarModule} from '@angular/material/progress-bar'; import {ExploreService} from "../../service/explore.service"; +import {InfoComponent} from "../../../../../components/info/info.component"; declare var Plotly: any; @@ -21,7 +22,8 @@ declare var Plotly: any; MatSelectModule, FormsModule, ReactiveFormsModule, - MatProgressBarModule + MatProgressBarModule, + InfoComponent ], templateUrl: './overall-acc-plot.component.html', styleUrl: './overall-acc-plot.component.scss' From 180fd242800a7ec06a4030b2471f4cff039d569a Mon Sep 17 00:00:00 2001 From: Nico Trummer Date: Tue, 17 Dec 2024 23:36:35 +0100 Subject: [PATCH 12/14] Adjust usage of app info component --- .../spongeffects/spongeffects.component.html | 42 +- .../spongeffects/spongeffects.component.scss | 429 ------------------ 2 files changed, 20 insertions(+), 451 deletions(-) diff --git a/src/app/routes/spongeffects/spongeffects.component.html b/src/app/routes/spongeffects/spongeffects.component.html index b58b662c..5975c0cd 100644 --- a/src/app/routes/spongeffects/spongeffects.component.html +++ b/src/app/routes/spongeffects/spongeffects.component.html @@ -1,28 +1,26 @@ -
-

spongEffects can be used for downstream analysis of - SPONGE ceRNA networks and is capable of tumor sub-type classification and biomarker discovery via a random - forest - machine learning approach.

-

For more detailed information refer to the publication. -

-

We trained spongEffects prediction models on the TCGA pan-cancer dataset and nine other TCGA projects that - feature sufficient cancer subtyping for classification.

-

- The tab below shows the spongEffects prediction results for a selected TCGA project. - You can browse the hub-nodes that spongEffects models views as potential biomarkers and compare their - gene/transcript expression. -

-

- We can use the pretrained TCGA models to classify tumor (sub-)types and predict biomarkers on new independent - input data that can be uploaded via the - tab below. -

-

The model will predict a tumor type for every sample in the uploaded expression file individually.

-
+

spongEffects can be used for downstream analysis of + SPONGE ceRNA networks and is capable of tumor sub-type classification and biomarker discovery via a random + forest + machine learning approach.

+

For more detailed information refer to the publication. +

+

We trained spongEffects prediction models on the TCGA pan-cancer dataset and nine other TCGA projects that + feature sufficient cancer subtyping for classification.

+

+ The tab below shows the spongEffects prediction results for a selected TCGA project. + You can browse the hub-nodes that spongEffects models views as potential biomarkers and compare their + gene/transcript expression. +

+

+ We can use the pretrained TCGA models to classify tumor (sub-)types and predict biomarkers on new independent + input data that can be uploaded via the + tab below. +

+

The model will predict a tumor type for every sample in the uploaded expression file individually.

diff --git a/src/app/routes/spongeffects/spongeffects.component.scss b/src/app/routes/spongeffects/spongeffects.component.scss index 6543874e..e69de29b 100644 --- a/src/app/routes/spongeffects/spongeffects.component.scss +++ b/src/app/routes/spongeffects/spongeffects.component.scss @@ -1,429 +0,0 @@ -// /* set base container dimensions */ - -// #bigBox { -// width: auto; -// margin: 50px -// } - -// .container { -// margin: 20px; -// } - -// .circle { -// width: 40px; -// height: 40px; -// line-height: 40px; -// border-radius: 50%; -// font-size: 20px; -// color: white; -// text-align: center; -// background: #4892b9; -// } - -// .sidebar { -// min-width: 150px; -// max-width: 150px; -// background-color: #e7e7e7; -// } - -// .sidebar-tabs { -// display: block; -// min-width: 150px; -// max-width: 150px; -// } - -// .sidebar-tab { -// font-size: 24px; -// font-weight: normal; -// margin-bottom: 10px; -// margin-top: 10px; -// margin-right: 5px; -// margin-left: 5px; -// background-color: #9ccee8; -// } - -// #headline{ -// text-align:center; -// } - -// .card-body { -// img { -// height: auto; -// } -// } - -// /* cards */ -// .mat-card-header { -// width: 100%; -// } - -// .mat-card-header-text { -// margin: 0 !important; -// } - -// .mat-card-title { -// text-align: left; -// } - -// .mat-card-subtitle { -// text-align: left; -// } - -// .mat-card-content { -// margin-left: 10px; -// } - -// // info card -// .info-card { -// background-color: #9ccee8; -// } - -// .info-card-small { -// background-color: #9ccee8; -// font-size: 18px; -// margin-bottom: 10px; -// } - -// .info-text-small-div{ -// font-size: 16px; -// } - -// .info-list { -// list-style-type: none; -// } - -// .spongEffects-workflow-img { -// align-self: center; -// } - -// /* expansion panels */ -// .mat-expansion-panel-header { - -// } - -// .mat-expansion-panel-header-title { -// width: 250px !important; -// margin-left: 8px; -// font-size: 20px; -// font-weight: 500; -// } - -// .mat-expansion-panel-header-description { -// margin-left: auto; -// width: 600px !important; -// } - -// .explore-card { -// background-color: lightgrey; -// } - -// .level-toggle { -// float: right; -// margin-left: auto; -// margin-right: 20px; -// } - -// .level-toggle-button { -// background-color: white; -// } - -// .publication-link { -// color: #366080; -// font-style: italic; -// } - -// .predict-card { -// background-color: lightgrey; -// } - -// .cancer-select-card { -// height: 700px; -// } - -// .cancer-select { -// float: left; -// margin-left: auto; -// margin-right: 60px; -// width: 400px; -// } - -// .mat-select-trigger { -// min-width: 500px; -// } - -// .cancer-select-input { -// width: 500px; -// max-height: 600px; -// } - -// .subtype-header { -// text-align: left; -// font-weight: bold; -// } - -// .result-panel { -// background-color: #dfdfe0; -// } - -// .result-accordion .mat-expansion-panel-header-description{ -// justify-content: space-between; -// align-items: center; -// } - -// .sample-distribution-pie { -// width: 100%; -// height: 100%; -// } - -// .overall-acc-plot-div { -// height: 200px; -// width: 100%; -// } - -// #overall-acc { -// height: 200px; -// width: 100%; -// } - -// .class-acc-plot-div { -// height: 400px; -// width: 100%; -// overflow-y: auto; -// } - -// .class-acc-plot { -// height: 100%; -// width: 100%; -// } - -// .measure-select-input { -// float: right; -// margin-left: auto; -// } - -// .loading-spinner { -// display: flex; -// justify-content: center; -// align-items: flex-start; -// height: 100vh; -// } - -// .loading-spinner img { -// animation: changeColor 2s linear infinite alternate; -// transition: filter 1s linear; -// max-width: 100px; -// max-height: 100px; -// } - -// @keyframes changeColor { -// 0% { -// filter: hue-rotate(0deg); /* Starting color */ -// } -// 100% { -// filter: hue-rotate(180deg); /* Ending color */ -// } -// } - -// .enrichmentPlotHolderDiv { -// height: 300px; -// width: 100%; -// overflow-y: auto; -// } - -// .enrichmentPlotHolder { -// width: 100%; -// } -// // box for cancer selection -// .cancer-select-box { -// min-height: 300px; -// margin-left: 10px; -// } - -// // cancer description -// .cancer-information { -// height: 250px; -// float: left; -// } -// // cancer image -// .cancer-image { -// width: 100%; -// height: 500px; -// text-align: center; -// } - -// .cancer-image img { -// max-height: 100%; -// max-width: 100%; -// display: block; -// margin: 0 auto; -// } - -// // lollipop plot -// .score-select { -// margin-left: 20px; -// margin-right: auto; -// float: left; -// width: 300px; -// } - -// .score-select-input { -// width: 200px; -// } - -// .top-select { -// width: 200px; -// } - -// .select-top-n { -// width: 200px; -// } - -// .top-ceRNA-selection-div { -// width: 200px; -// } - -// .top-centralities-plot { -// height: 300px; -// width: 100%; -// } - -// table { -// width: 100%; -// min-height: 200px; -// max-height: 200px; -// overflow-y: auto; -// } - -// .modules-table { -// width: 100%; -// max-height: 300px; -// overflow-y: auto; -// border-color: lightgrey; -// border-width: 2px; -// } - -// .mat-cell, .mat-header-cell { -// text-align: center; -// vertical-align: middle; -// } - -// .module-expression-heatmap { -// width: 100%; -// height: 300px; -// align-items: center; -// justify-content: center; -// } - -// .heatmaps { -// overflow: auto; -// } - -// // PREDICT TAB - -// .example-expression-button-div { -// float: right; -// margin-left: auto; -// margin-right: 20px; -// } - -// .example-expression-button { -// background-color: #4892b9; -// color: white; -// } - -// .example-expression-table-div { -// width: 100%; -// height: auto; -// } - -// .example-expression-table { -// width: 100%; -// height: auto; -// } - -// .mat-form-field + .mat-form-field { -// margin-left: 8px; -// } - -// .run-button-div { -// min-width: 100%; -// max-width: 100%; -// } - -// .run-button { -// background-color: #0b608c; -// min-width: 80%; -// max-width: 80%; -// margin: 5px; -// color: white; -// } - -// .run-button-predict { -// background-color: #4892b9; -// min-width: 100%; -// max-width: 100%; -// margin: 5px; -// color: white; -// } - -// .type-prediction-div { -// width: 100%; -// height: 400px; -// flex-direction: row; -// } - -// .type-prediction { -// display: flex; -// width: 80%; -// float: left; -// height: 400px; -// overflow-y: auto; -// } - -// .type-legend { -// width: 15%; -// float: right; -// } - -// .type-legend { -// margin-top: 10px; -// margin-right: 10px; -// } - -// .legend { -// display: flex; -// flex-direction: column; -// } - -// .legend-item { -// display: flex; -// align-items: center; -// margin-bottom: 5px; -// } - -// .legend-text { -// font-size: 14px; -// } - -// .child { -// font-size: 12px; -// display: flex; -// } - -// .legend-labels { -// margin-right: 5px; -// display: flex; -// flex-direction: column; -// } - -// .child:not(:last-child) { -// margin-bottom: 60px; /* Adjust the margin between child elements */ -// } - -// .percent-color-box { -// width: 30px; -// height: 80px; -// margin-right: 10px; -// background: linear-gradient(to top, darkorange, #008c00); -// text-orientation: sideways; -// } From 3156908617224483dfa1e48b970e89d5ea4f3295 Mon Sep 17 00:00:00 2001 From: Nico Trummer Date: Tue, 17 Dec 2024 23:40:05 +0100 Subject: [PATCH 13/14] Improve info formatting --- src/app/components/info/info.component.html | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/app/components/info/info.component.html b/src/app/components/info/info.component.html index 9af08227..544c84d2 100644 --- a/src/app/components/info/info.component.html +++ b/src/app/components/info/info.component.html @@ -6,7 +6,7 @@
} @else { - + info @@ -23,9 +23,9 @@ } -

{{ title() }}

+

{{ title() }}

@if (subtitle(); as subtitle) { -

{{ subtitle }}

+

{{ subtitle }}

} From 2847e882908cbce721d5dccdbc4e5d03a29feac6 Mon Sep 17 00:00:00 2001 From: Nico Trummer Date: Tue, 17 Dec 2024 23:52:11 +0100 Subject: [PATCH 14/14] Use info component in more places --- src/app/components/info/info.component.html | 4 +- .../explore/explore.component.html | 60 ++++++------------- .../spongeffects/explore/explore.component.ts | 16 ++++- .../overall-acc-plot.component.ts | 54 ++++++++++------- 4 files changed, 67 insertions(+), 67 deletions(-) diff --git a/src/app/components/info/info.component.html b/src/app/components/info/info.component.html index 544c84d2..20579fc8 100644 --- a/src/app/components/info/info.component.html +++ b/src/app/components/info/info.component.html @@ -6,10 +6,10 @@
} @else { - + - info + info {{ title() }} @if (subtitle(); as subtitle) { diff --git a/src/app/routes/spongeffects/explore/explore.component.html b/src/app/routes/spongeffects/explore/explore.component.html index 97f1d0c9..c38d6d6b 100644 --- a/src/app/routes/spongeffects/explore/explore.component.html +++ b/src/app/routes/spongeffects/explore/explore.component.html @@ -1,58 +1,34 @@ - + - + - - - - info - What does this mean? - - -
-

The spongEffects enrichment score distributions can give an insight into the model capability to - differentiate between the given classes.

-
-
+ +

The spongEffects enrichment score distributions can give an insight into the model capability to + differentiate between the given classes.

+
- - - - info - What does this mean? - - -
-

The mean decrease in the Gini coefficient tells us how much each variable affects the consistency of - nodes and leaves in a random forest. If the mean decrease accuracy or mean decrease Gini score is higher, - it means the variable is more important in the model.

-

Module centralities that are at the top right of the plot below play a crucial role in distinguishing - between the various cancer (sub-)types.

-
-
+ +

The mean decrease in the Gini coefficient tells us how much each variable affects the consistency of + nodes and leaves in a random forest. If the mean decrease accuracy or mean decrease Gini score is higher, + it means the variable is more important in the model.

+

Module centralities that are at the top right of the plot below play a crucial role in distinguishing + between the various cancer (sub-)types.

+
- - - - info - What does this mean? - - -
-

Gene Set Enrichment Analysis (GSEA) identifies whether specific sets of genes related to biological - functions are enriched in gene expression data.

-

The information from GSEA can help find biological differences between disease (sub-)types

-
-
+ +

Gene Set Enrichment Analysis (GSEA) identifies whether specific sets of genes related to biological + functions are enriched in gene expression data.

+

The information from GSEA can help find biological differences between disease (sub-)types

+
diff --git a/src/app/routes/spongeffects/explore/explore.component.ts b/src/app/routes/spongeffects/explore/explore.component.ts index 7c76b15b..0d77dd42 100644 --- a/src/app/routes/spongeffects/explore/explore.component.ts +++ b/src/app/routes/spongeffects/explore/explore.component.ts @@ -1,4 +1,4 @@ -import {Component} from '@angular/core'; +import {Component, signal} from '@angular/core'; import {MatExpansionModule} from "@angular/material/expansion"; import {MatIconModule} from "@angular/material/icon"; import {MatFormFieldModule} from "@angular/material/form-field"; @@ -9,6 +9,8 @@ import {MatCardModule} from '@angular/material/card'; import {ClassPerformancePlotComponent} from "./plots/class-performance-plot/class-performance-plot.component"; import {OverallAccPlotComponent} from "./plots/overall-acc-plot/overall-acc-plot.component"; import {MatTabsModule} from "@angular/material/tabs"; +import {fromEvent} from "rxjs"; +import {InfoComponent} from "../../../components/info/info.component"; @Component({ selector: 'app-explore', @@ -24,10 +26,22 @@ import {MatTabsModule} from "@angular/material/tabs"; ClassPerformancePlotComponent, OverallAccPlotComponent, MatTabsModule, + InfoComponent, ], templateUrl: './explore.component.html', styleUrls: ['./explore.component.scss', '../spongeffects.component.scss'] }) export class ExploreComponent { + refreshSignal = signal(0); + + constructor() { + fromEvent(window, 'resize').subscribe(() => { + this.refresh(); + }); + } + + refresh() { + this.refreshSignal.update(v => v + 1); + } } diff --git a/src/app/routes/spongeffects/explore/plots/overall-acc-plot/overall-acc-plot.component.ts b/src/app/routes/spongeffects/explore/plots/overall-acc-plot/overall-acc-plot.component.ts index f1bb65ea..348d1409 100644 --- a/src/app/routes/spongeffects/explore/plots/overall-acc-plot/overall-acc-plot.component.ts +++ b/src/app/routes/spongeffects/explore/plots/overall-acc-plot/overall-acc-plot.component.ts @@ -1,4 +1,4 @@ -import {Component, computed, ElementRef, inject, resource, ResourceRef, ViewChild} from '@angular/core'; +import {Component, computed, effect, ElementRef, inject, input, resource, viewChild} from '@angular/core'; import {Metric, PlotlyData, RunPerformance} from '../../../../../interfaces'; import {BackendService} from '../../../../../services/backend.service'; import {VersionsService} from '../../../../../services/versions.service'; @@ -32,37 +32,41 @@ export class OverallAccPlotComponent { versionService = inject(VersionsService); exploreService = inject(ExploreService); backend = inject(BackendService); + refreshSignal$ = input(); - @ViewChild('overallAccuracyPlot') overallAccPlot!: ElementRef; + overallAccPlot = viewChild.required>('overallAccuracyPlot'); - plotOverallAccResource: ResourceRef; // plot parameters defaultPlotMode: string = "lines+markers"; defaultLineWidth: number = 4; defaultMarkerSize: number = 10; - constructor() { - this.plotOverallAccResource = resource({ - request: computed(() => { - return { - version: this.versionService.versionReadOnly()(), - cancer: this.exploreService.selectedDisease$(), - level: this.exploreService.level$() - } - }), - loader: async (param) => { - const version = param.request.version; - const gene = param.request.cancer; - const level = param.request.level; - if (version === undefined || gene === undefined || level === undefined) return; - const data = this.getOverallAccuracyData(version, gene, level); - const plot = await this.plotOverallAccuracyPlot(data); - return plot; + plotOverallAccResource = resource({ + request: computed(() => { + return { + version: this.versionService.versionReadOnly()(), + cancer: this.exploreService.selectedDisease$(), + level: this.exploreService.level$() } - }); + }), + loader: async (param) => { + const version = param.request.version; + const gene = param.request.cancer; + const level = param.request.level; + if (version === undefined || gene === undefined || level === undefined) return; + const data = this.getOverallAccuracyData(version, gene, level); + return await this.plotOverallAccuracyPlot(data); + } + }); + constructor() { + effect(() => { + this.refreshSignal$(); + this.refreshPlot(); + }); } + async getOverallAccuracyData(version: number, cancer: string, level: string): Promise { const modelPerformances = await this.backend.getRunPerformance(version, cancer, level); return modelPerformances.map((entry: RunPerformance, idx: number): Metric => { @@ -194,7 +198,13 @@ export class OverallAccPlotComponent { const config = {responsive: true}; // remove loading spinner and show plot - return Plotly.newPlot(this.overallAccPlot.nativeElement, [...data, ...customLegend], layout, config); + return Plotly.newPlot(this.overallAccPlot().nativeElement, [...data, ...customLegend], layout, config); } + refreshPlot() { + const plotDiv = this.overallAccPlot().nativeElement; + if (plotDiv.checkVisibility()) { + Plotly.Plots.resize(plotDiv); + } + } }