From 5f850c231b97afe810c0bb2042d4956bad9ada78 Mon Sep 17 00:00:00 2001 From: Bryan Lai <56288120+gblaih@users.noreply.github.com> Date: Wed, 17 Jan 2024 12:48:56 -0500 Subject: [PATCH] Add user color configuration for oncoprinter (#4817) * add color chooser to oncoprinter * add tests and screenshots --------- Co-authored-by: Bryan Lai --- ...efault_colors_element_chrome_1600x1000.png | Bin 0 -> 40317 bytes ...lected_colors_element_chrome_1600x1000.png | Bin 0 -> 40375 bytes .../specs/core/oncoprinterColorConfig.spec.js | 130 +++++++++++++++ src/pages/resultsView/ResultsViewPageStore.ts | 10 +- .../tools/oncoprinter/Oncoprinter.tsx | 113 ++++++++++++- .../OncoprinterClinicalAndHeatmapUtils.ts | 40 ++++- .../tools/oncoprinter/OncoprinterStore.ts | 49 ++++++ .../tools/oncoprinter/OncoprinterToolUtils.ts | 27 ++++ .../oncoprint/ClinicalTrackColorPicker.tsx | 4 - src/shared/components/oncoprint/DeltaUtils.ts | 3 - src/shared/components/oncoprint/Oncoprint.tsx | 2 - .../oncoprint/OncoprintColorModal.tsx | 107 +++++++++++++ .../oncoprint/ResultsViewOncoprint.tsx | 151 ++++-------------- 13 files changed, 499 insertions(+), 137 deletions(-) create mode 100644 end-to-end-test/remote/screenshots/reference/oncoprinter_reflects_default_colors_element_chrome_1600x1000.png create mode 100644 end-to-end-test/remote/screenshots/reference/oncoprinter_reflects_user_selected_colors_element_chrome_1600x1000.png create mode 100644 end-to-end-test/remote/specs/core/oncoprinterColorConfig.spec.js create mode 100644 src/shared/components/oncoprint/OncoprintColorModal.tsx diff --git a/end-to-end-test/remote/screenshots/reference/oncoprinter_reflects_default_colors_element_chrome_1600x1000.png b/end-to-end-test/remote/screenshots/reference/oncoprinter_reflects_default_colors_element_chrome_1600x1000.png new file mode 100644 index 0000000000000000000000000000000000000000..f04271fc518905abed4837fb2eacca97f02918fe GIT binary patch literal 40317 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 literal 0 HcmV?d00001 diff --git a/end-to-end-test/remote/screenshots/reference/oncoprinter_reflects_user_selected_colors_element_chrome_1600x1000.png b/end-to-end-test/remote/screenshots/reference/oncoprinter_reflects_user_selected_colors_element_chrome_1600x1000.png new file mode 100644 index 0000000000000000000000000000000000000000..463ee354d6446481f3bf9e191ec826d1f86c6f72 GIT binary patch literal 40375 zcmeFa30O{T`!D>MrwqwZqzHv*)sr@!&gbtum)9CC)zKrTjQs1b zzecZ8Q(5=dUqj0O`fI?E;Y0Dw)f?~L|Mgdx>nfF{x{d?tQ%40qcX-m*)Yi5)#46V; zBfenM)>Ynh7bYs`cJsTxvRbvF)oho#j!wXam8+XV^!>-E>89P$4Onn`xrpJ`wwn0e zTkDHV7xYD)5?vXiFjr?~nOocUhUDuV`QHOgL;MSBzf1kt<@xhR&3Ducp`Sl@g^vC_ z=2?7v+|NTo>qM9Ra!l=7Oi5hI_xhJyz?K9vB!{aAl)-=Z~-I0m7Q? zGp=rmKfJi%VZbx-l+d`GoSaWDuS|C7`VqA%Kv?&6vP)O6jQRMsGB;)KNec{LU*EQ; z;X~M(IqClOWhoKmX&&}JN_H0AGEx8W^<8GVSMxaE+lO!t=I(~*k#mD+BdJj2HDe^j%xx9^DP>kvFKdDW^_mTztzy0`mX zb3{VriN$6w&JR#OGrznkQ*dWdv}SJD;vj2j^TJtTk6PC~SL(7f{FYETW{Qm2<5RQM z9lM({OYWHS*w;RvT_f(+F>335tLngw+b`_Q56_KIj!5uR>hv^uc1F@Pm$%~9?b{t6 zkBr?EZ#bkZ^U1MM3T>tKo0FXt*1KSo_T>DSwU->f-hJ)ZS$DbQzO`W6`+b78+R8mL z;wjxyeoKDvif+6LX>0GS&Iwd($(25R`m}_@XI+*4pQ^qQ zO}@8oNxizh?b)Y-OXlyB7GBwSqektXb=h+-!^{)Y9=1GR>@!vFglXP6|1)PYu#K6= zCyL&DRnc4%Vts4(JK5_A6(bGz*1t&w4W%X6R+_!K%4cqFuD9>K11M|Yt_APz?XJWq zmuxFD^c>5NS-wvSOfWBswr$CiD@w36s(4Z^arW$4TEOOJyIvhy_tqlq%dS10x-UUk z+ueE`q@<)gRfIzAa^@Nv8{c&4+G}|AEV(wxF;^@ZU#B{mW}fQy#VtI%hCAZ$QEI+NIbpf24?(pfwp^6Ga+|F0hJ>f=j=SU+i zNzg{*_DB1%h-=n2e0X$L%4Fo1*EcS^_4OuLzPsmpR(j_x-0Jh|8~F;gp{&g;xi$OJ zN?$|p4-o>bIiAgZX2<6%H`*K1#yXWBo6IX?Hck@!ZrIn`L&70p=;$fq_T|jI{Nmp3 zt>Fj0DPUzZotzYit`3h9`>593(n8m@R|ROBeR!xaQ}smEanbA3O~JT|%~>-o%aX&6 z9vz&t_iN6!eeYMSTQ@$!tyf|4cE=f5``Q9kK@&)!)=vdTM(_xa8$6)lVapiEgqq~8 zZ!Ej?7h{!OyW;@$7=McopKa6YBv(rH6f zm^x%~V@GX7Kwu!mtwz|Qz$3%Po(c(hcE`LZq3hd&5guvj-PPi52HB@(%UG2L=PUPx zUcEXWgz&jImS55FWNE2hv}Q=g`+a4an>X|Eh-g*GrF4uYVYqm2{m18v8)i)yX)E*n z^UIm0ARUbmS>FI{rL)-qA}yaT`8B^W@>{!p)v6J`Q{|#>fiZY=^z?FLFNd#QZ=KR3 z3!x}nr4(M#I%<%P^D<)R6Q@M-p_Rjq1t5<#Q?s{DqZ=||<_3Er=&t@tsN2m%o z9jy&NK>RPc`>o8|ySqFj-L2jyHNDzuU6A7D7B^2^d?#dc>zCJl!vq`;f``8)cUgUV z*rMgy*Hw1grhM7;M?bcRC43xsJ-O53gfV0a*Hl@bV1qPw+xHHyxmzGXby8eiWG&x3 zHu77ta_w5~utg4rV4FKu@6SQg^t6@bYs$S(hb;7}D~{z!>3ZK_L*jJEkRkK+5>DQ^ zBXiH{eGWv=r&rgeUXE~Gw6DxX4r^}u;h~drbGC@_&YdmkJlYbFuAg6CQO&(_BRS`x zQ_CDt(aIdr8?!`2JV^a*xVEL~L0QjV)YemKyuL@eo(kl{Jm-K+fsmXO$2ZTa!eT4&3aoHy}C z=A{Xe3%1^yB`WH@aMvrFPcLRvfW1^!uO5m0ul#zyEc(u!)A2@`g^;9M?pdzduwl}! zS69nlTv$&0%D-rjC!Y2k^j_hG<->#sTT;^h!=nSYcfA%%7TvW# zGJ`j+69R3{+_@LlU)kV1mGxE9z6P!R`xm9OKNWA!RPIe(_*yRw3zANP4$3$qZvB-_ zNqgpkg^hpIm!#i?7<>O5=xLh~9MADH?cw z@J0IU*|U$Od5pci_v;oIH!Cnx2$^UwBr}XaET2wb{%Uz2VSll;>_9B7X z_SZ$pm=_+!KG|gO?QCp`=nK95e35r#dG!akK9?Wg z-m9As14Tte`Al0(8i%Z&Teog)x@|fdGFj-Krt;Gs|$j|ZMpw-aQ zo}vpDL|wdixFugXB969k%a$pA%DwVrymfxLk;$8Oa>jD9IKfcMf7(Uwj*b;j%))v& ze!Ae1@bT~{&nq!8J77t4!yb)R^%LM<_3X?7FBPGyTkoG(Ge>2;zH`$MFi{0@;jHln z?~@$xm~n5RAWTe5_?4W4_kaJ)4Jj*Nu^b|>fKGdykOVP zl$7KSf-S7GZrzg$%ROTBQAuyp3u}f4wYKiTh+ZAF z%6-`oaH}*mt1G<4SN^*4udZ%hzHHgRCHq^?-?EX<1rd_H5HpW|%x&v3C2+(ir~#0S z!ShfB;vI-eSS1Nx@4b^hr4_b_*rF!)>~eGSx!_hZ+}>F|@D0Wh?iNTmyT*@8WvxGG z$V`cnB^3jyH+GBh+7mfG@1 z`@d)9=htGPhzT!Nkdy$ET z4{n`B5oF2;x^+w9tnSNXwqc^nVMjobRpmcO|R?{?qk zZfSldZV$JF?5(vRBglu*Q)OYO{PbMyWXdOsZjdm0F&!5F`kv;Xlq#s-$eTA$=8y&y zaQrxcY<&UOb_3J=@H`(r>4d6N>Nj7JT!OgG^bg%rxGFb%NeG{`sZI9mpdHtu;82lo z5)462s#n0Nid*17y=3o~lwNOgXPId!BT@;&MGG@Q$bAuBq4xGl*aE zyd+<3`QX~M8xFxi^;zS~%RK~5J6r3zD<-FWj117a2tSXMMFY9QrLJ9H6+1s&(g>C~ z1gT8A2L7_=vz#155UUTbrEy<>@r>#SOza&wK{PtfKNE!Zk|9hd2CqIv{M8c-BbwwIGY~qtwoLIoat9R9!c1$5W`0+gp z>G9y9lfP`OYByre6Y87GI>G-E25~sf+>qaw^}ASxxi)qLb9=k1tb2HSs%_iBT;1Hm z+*MPWHGHfBb7J#unr5*r^J(2{_GO-G{SPGk+sNxS9^>|J$Yae+YY+Qb@8+{U4`w|_ zRPw+^QH%3R3PEHG4@!UqLb9panpq|<(Nsbc4X~u59)vE z_uKm$iD=A*VLfZstdqgPYH(0WN=kHpzz?v^Tljk17>~sIThH3-wB2%2dcTWrckM8N zKlBt4x@9X@4z(RIVuVJhyno#Owr{?kYP*@O;@n~YYZ9?zSik%QnK{LpvzL~K*=36p ztfy9cZ~Xi2M_y;m^Ni7gTsg8F;81*ecV8R zjBik|HE^A(%ldld`1ttJXV04Tu}ewl=c0o|m6%NpP2f;suNyNbyNAPDd?}O&^G4Dz zefXC9R(ahWEpzA0+41&{c{&+%r%sK3uH5GgS-#w4<94Un^shfq$flZRBNNuMyfk3_ zvU48M;(Pe;K#KXn{aU4=F$c!vAIr};5 zVwL0@68i5RI!RBJvxnWS81{LfQ32NVR3b!-7SNea5Q&qz#6)ZMUV&EK$4>fYWJ zm2~TEiI@wsbnaX)7$jHkrc)3WhHFsH`0?W#T?DzgV_jH#vMz|>9x!EA0f!mAS`uI%wFIR8#DmqHsc-TBLXxm=C+opLJ zvEpQE1&VIG1>?>~lGkV)q!(*Ubq9uJI1)*D14ADoV!x5He?tcAp{vrFtxz7!I?SeU z&diJXnQwn*1OIc}uCR*UJUT!D31Z#^7_=rq+cKcn$aCJf&0(VUlJ65{uJrln=hi)Q z+Ty*Dx8Tb*!Lg#KNW~a_`Gb<~ab&EN_5P4kQK?|axysRy;UpnDXGXO6pJU5BAtAd8 zDH1+sf+h5y<>k$WH4_JC3#RsL>_jlE_hvZr{xEbJ5yV}mr>6mP4SxG&^&l}`BTqOr zAPBozm&5nZ1JT=cGeq~9Eupy~quDFMmeHA_tY=p!V77|Qan&l51mta|4;nTmefE+i zVIY9GMHKa9MLyML9d77?1YXZ9IWzNh_RO=nmi|i2kyfmSfu#`3Rv9XhV$75TA4v3b zWu_#vYdk>=OBByG00l>`WagCBl+IAD#bM?!TZ3fnW5hLwzqn&ZF>_!myN>Hn6j`>6 zq*qa~2w-2W&72h}%pun9KIWUM%sD=uY73Yx>9GC%H@8AOLvkE71A6CgU^FJF&2Em? ztWp`r@`xf&FpJK}ShfyPxX0WIkN25TQ~rYDMk($z0{f>p;Xh@C{yS`vC!SetXHEqj zS6{nfhV5LZ*n|lt6W2}a5Ecy52wA1Kdiy-F6`PNxYx=3!WX}~BvD!cDu)3C$f^E@E zZaur4XI>!!ULQ~CY}YDnRT~@>+H6^o=EqpG4R)}3Cj)<0Br zh_jy5ZsC`bOU9?pzWgvWq0WD}m_l{6t}6Rx#dp}>q_a=$zu7anjs)Yk8WvlUvomKG znU0s^3~~CQuHNxmBfZ!sRIF=WXFE4%-mL!VvY$uu&*{b){nG>K{dtc+C&eDpL4#SE z%h8-Q5-Y2!JY_E3KN!+-2j>FnHGGQ<*cL*)FB>=E)G4r4|H1|qI%h7}%f@p7K5RjC@1ZJw*t)OJ*) zd+ENU5a+Y3tn)|{rz6hQjCh}InM)f2p!yP6tYl3DykT;s*Ow*+3>h`)B=WP!;d&v! zS8-;ml|lKFW8LLr-9AQ#SWy7N8Bs%uM!*0;cz49HW$7ly#xHK~)E*^N3JS3xLR)gvU=SG% zl3hQ(8oi8LPbqc;bt$5TvCYbomjpU|eH-Z3T`5RW5CkRHuE%ayX8MUt7)f?1r5j35 z&(+!V&{1-#tmXNA4IgM4Fzr>P5%#dNv-6w^`>v<+yV2LTclqi0>+0&#uj;3SGtckd z(4~vuh~oa%PpJ#;mp-A#NgJ!2o6^@~4T4ZbU?bM@@xcLARaN6&-?!GkSTKj30A=6j z=Y!fAjzIF}1cDWmyP}zyP*fvCc6SAWBs0DtEK?0Tp8_}jGgZgyVAt2@om*Im)n?^2 zKx4Xy8!jT>1>xgQA090ZM80dA<=Z=6>SBpU8!jQ;7PNi8S1`=IwK|9gtw!?3`qB4E z1h&wL*0=zA8NBqd|E67(>_TpL1`IdZp9GoZwcz1_HY-Cd<(@ss-v7b#u=N-#Xj&M# ziU^emK(5R=Acw+|xp1SN@3Z~*cz}GOw7)9QOvUW(Di(<w{!%pi_af%@Ym5Gv#y{L%SCyHy>u_egah}X<^?rMp zwL!n%rBY=wGabQY%z2-0G4m>pv_Y0#GJ}G5V+0h{D3Q|DUg@_v#dXQ#ZF{)kJ4Bv8 ze=rzPMaoyt6P+{XJYWR64wb%g%}7JrWlr1o0t{9Gx;`%Hj=+X3!tU^m|1L#%Iv1J9 zL<%FStEoM1b2(wkW=TqqggaJ@AbH}__Fh$U*V{XDC5fxlhH~-#t)(@lOnAnO0cCC6 zO^RbXT!GXvQ_R0~9m{Ei4YN30>%{L{>r}ijyw+9*JE>aZB%oS}EjnK7l+}hm7N)i+ zp2%oXRB<&6uWd4_W)2k(&To<5z#iz_N9Gy_8i)>rJBEAkpJhg-(L+6P+fBHopeS=R z(<=+39$Nzkza44)YQxwz@jO?$g3T3+d=Y>LbzrRhT%*dbh53Wj_rK|D?IOuPMWT zoL>o2!EQZY^r8DN053!U1BBrN5Ee#h<}o*^eks_aB0Vn$@-ASx9HY95!JolV)FnSc1qY&8O985;?wdQV;9 zkf1|@hDjmqtH;i#J#TY~`}`)JJpS}3&N(c%m_HAR96q?t>la!S4=-!;qRc1Y2V$yH z04hCS(xzwV+8hu8tymU~>9i;NJ!4-`COqDY;$Zzhc7`KcX4(N;NB{c2g-<{KKU#PH z^Jbwz{p+8*$L&95^6zHi|NKCI*q8sGFXUxew6P1BxB}~uMUo|i>@?Vpy0bEKgcH3R z$okm4oE_Xykoql@!45_}8=K0C9z{!O_2mG#K5l_I2b;3D!^&mH#F$^+&T{?hsJy)#00P1fbRYx%9M+Uq z7Z6|yU+)y%e09p-KNT#?2jd$s(F)iVro}O9)2{*XiS)cJvbU!K1D`_rD4arEIUbl3 z7PjW5L7FFgh%tyor{A$I3x~mFHw&C=gw#6aZ3)B`8p>}74U~m^Aen`L_zA%BgpF$d za{WgCz`zwqetPmP-a8-Bz6wYtQ>5u+fB^^wW7)hlzbxt8ZZk=Id+xrq0Hi>v5PXrK zYq1+|2*7yMbL;u8XP>t)eHxHgFn4Y`HW?Bq2QuyJyZg0qSFQ`e;HTWZG(_H{1zXaK zw3YJ!!t~twVDAE0qLoiZiB|!yu(SS6Jk6P)5W!6MBshh@N7NCX~{mWSn= zz~VcG;Fl(6ag~|iA-4Q26HFEo5n{H!Qa#vOMTon%&8;tJtWx{IBCW-~!AQLMBPBdp z-+8G+X`%MV{dkjb#c`)c5LT!3KZf z{_Hq0m?TvGiE}0J1qzgk1iVg>JPcD{f+2^G$$fA<@)?j=5yT`3lzDqc>cGOn!bX>f zoPSI}SS8g7SG>BRpWoGxlJC~n<|d6Sb_MBi6H2R+(M~=IT%O_^`Vqm?hZ7Qwa+3f! zk%N!`&}CCsSzmaFRT4$Q<8~qCOF&q%ObLID)USq>l^DPtrhr-!TEs)pHTeFA{oY_7 z@(qFV(S|#^=gTYK>SsZ>jkCwE^x<9V0L+Rvav*J`dx|tc2|AnqokxTf;iM6S7BYgY zKa$Bw$gp!7c0x?7)!&+@W-4p}TCmy(w{TedWSf0(tQ;+@6`D2G`r*>ujzBRe?D;ef zq3iRo?|GTo%>?G1 z?(3XDQLn`BueOp~de^MrFfu>m?mKtb8Cs?E?4^g-Lx`HF5P&HIj83cic?xpWl$pUg zi30Vu^u!Vt(?A3GCuK7NxGJ3qldqFw$ARP7VRJTvv7`8QiX&S_?^*zkk_7116mH^G zy~I+bDvs5`jCEhD3c9FhzyOT-eD(HoA-Z1o z72si355CGem(>!~n{d3DFy%ZIDt)BeWeJZV2_N=S9AVFb!8y$lW&qpXf4_HXazhK& z<^^DwMCT8fL>?#G)?w8j{N?1%*x4Ypyx}_8@JP=BuP?gc>PKMoghAtEGc`QfqV6N< z-e3sXYFV1#k4$;vi{)dvi>_{##^32cb=H-oD8GCevv#H-u%9t;fOZgeEG{QIdj?RT z5W?H{*UgRtz{o!sMqaWe1uDogivvc@5dK()?5iz?a3cc)0+zeFDk&%^JcPvaEaoU; zcCwV6)?#V01V$yZGbh{~@l4X9VV=h9X^ZxJBvdMRsP=|ik0_6@#`!qVk>SWuqhc+w zyexcN!^MB+f@}n0r{v!5sp84)Ldf#T242Hf27?3Fu183UFwZGHU#*=gyhLJ7U_)*J z!@k|+`}#$DKK*S%nIl4wLiAro$b>-Apb7um`AQ)<8?NiE(zvnxkte_z!qEg<+54?@ zl?LDkv2g&iYMhlih78Sp#in!~4p;T(BmH@fvBVT7Va?8;dwXWNG3mazG=^TaR2ZGW z&Sa}=1kZCHGcD&)+XwYr>}{+CJSBq411SCiNc2SxpVPr{!FVW1nCRtZ6w3inmO@An z%8`+gEKnCfW6-FT@tFd&fu9q=Xq%x+fi8*y)|2oe0BfA;XsM+r1|BFoC+A5ufsVoS zH9+)4EwW0io&;bXk~Wdi(RPsCR9kR($S49`nZe8^kUueVQ^iw4JVVKo-c9aqR!8(l z8~H(mE@nfK1`_~f-#0zNXF(wlkP8tN!<^G|)4BMh1_zaO*T@mZ2MjfVn{ABwpJPJ- z9GI*>h^N?uy-aL*)qm=xQ94hfg0|CwJ%bgZby+J6X!+<{K*QOkjh3TZ6BajA(!IXk za;5K-rv(pxyxmp*;ZaEHkzoO+PA$^|{_WN?F(Y7y=Wk0oedT)6@z0-16Q!y81)y^> z6b8}*i&D0gIQJAO;7H3Qlc zY)+7E($KrEC%V(W1k&ihDG{2~khXfXTG?cBo|mie-tMMty-b5HV)PFBdY zSbJd0msL?UE#~EoHy^!UuUqdMsONWncem=2t$%BZm`%L?bfIcYQN`J{+I&MU-RrG0 z5NW@hBxBccwxRX*ZRKyfgI?C(dhk%dFS*g7{bualm*eVuZ=`u>Ub~jDqxwsufzGZg zGo*^!7dgKvero~M(-*-$`u8Z=g$oNle0Y9z1W(N3j;esfZ%JR|!7j18EG#ZQdK3mg z|HsXDEwF61Lr@YkCvf$#nJ45A5fk`Cy9l z!XRtZ+f%T@xyf6<3iUacE)$28mgYi2H}M(;E7%Jo7f#^w!2<^v#`6-B51Y1UTsU)@ z#;ozFP%T{G@oZR9UQZ3{uiS9`e#N;?UEdJO>hv>{&svI2Dg;bJO_2{!qX_DfhYCpk z^l`(-c|Vo!X)RLtbpFUEfGXGP**={}U@zTy6Hmxj3^&aUP6f=}ZzsaxGIDY}UMh`L z&O}LR`&unbXUQ~|)`C0t?*o(Rf8p8d^0eqz8(3geW49LeB6Aj-o15>O^E4$(i`@&| zZ|~62;n}@=ck3M>Nfb3THSgTL8-M@InMqr=Z26Q2#?{=qb?TD4_wE(GeS2I}U47Jo z1q)jDz#2C!di!?uu3e%B5CjOdf3DEI@Y=O&ckbQ0b>G^xCB)G#kzZS>W0s)lx6;H= zKp_;50Rct~^3u1`JUCo+(v9Vb*XFLtgcB=R2Y0huC9#wkmV3cV-Xi9*6MBEq(TU zT90wgM85V7XCh^sF+HY&!%DIgWh~0nV_=>}!UKvcK^k6!>#UPp)a~3#8iF zV`eo@P*EA9)<0Jvq2+8QbN_jE*Aey@^;4N)eJ|v4Xvf$ZD^b`iFwpYxmk zztqaukLj6pD3f!2z4P6%zeJFPM%R)x!ujbu2JA-vPu*l!&H?<6)Nk)U0!z%b{J-YH z72~)&-dKGrJ>{V>^Y ztt0;YepO`in+b)h6uoo>LY6J^67kP5(oqv>nzhc!a%A1C|G}XD$6Q=n^L*Nygd&lT z|1hJ{(!@bi_jL~bA3UbLeeD>_(mI~T|E0(OpS;;WY}7wI!T-{X{_n;A#1{WMS36|! zz8#fX5eNTw@YH_|rT%-mD2BY|u1gK(9X8L%Q0B0tq{J+F&XLdYxsDU zyH7SUY&HFU_Zrv2jP#SeJ2klcdyO@?`1pOiW(+$sT}W!WkC*LvDdCmYK3j%wo9D#Ixa9xP*lTOm7Ys5*qY`c{p0FlYW`_fHTdGzI=VJa4Mc) zm;kd!LwR_{O0QuSou-muW31tG2otA&A212|*L$VYytzZO2~f)yrodL-VWzrZI{nF51VsDi&5a| zLdI8dPosyjF|zf=NY0_aJi~(`Ax;RdY(_O;U$c1M695}xP`KKbP!}CS1!u52A2+n| z_^#}D`#L!Te%SEA1B(-{KSOMjRXmTFR|F;zdjOsqC38{1DTWA%fqpm2_-f#&Ra5#p z->p{~!1-BZ3?nQy6EO;^twp5OJ^XO$bb$A%HU<_qVois>BvGJhEQNu(U6mb>p8q&Z zxqSeYvZIu*B5S4}l>owqwa=Bz9Ot`|TyR0otxTRCpV&HqYbeqr&efv&7hvok8LJfh zAJ*$5tk1-4n6MW!Dm6l0-BA%Fupq~4Co}8!(h7JofQU=eVEtu!5!Yw(ZZC?%&C?_ z0P^xT#6ou>e@D(H%YMcpBn9cao~0s3fHL5=u6|E`3?XZ{XRf6B4XQnXJ0H6hX?N>~ zO+y{1h#E;j01CuDlAB|N&IpQK^|xDF5wu3V`5u_>Wj2Uy+=637IX$YLy`TJj0fl+s z6nmF7WVQhG*__|kolmG0zX}oMjuC{Gjp%K?oY>vW3eb_SgxKOke58_WM-V`2VU&98 zfv&IjH=~eCuGD7GgMAG(06RP(Ka3y(ADop09ViqxYSR4kv9Vq#P76YyuL)UWdlb|* zA>c%R6M~oGh@a`%kez=#WU}64+stXI?-5ZZqK^$hEeeGuzEO!N>gcE#6zJ5JBZA!r zlPxSPDhT-vg1Gn%4<{0lZ{L_Y>^6`PtX*ECp_uzOc11{5SfJ+byPa=0ME0F!$bltq zS4W;oomW+8hRp-%@RA^BNrfX`*j9*G7V#V!&k_k-CXioBp%B`g0oYrXJUT6!SsQO} zF2ggn2olNuycFnYMwWhhr7cw+QJo8t4I1xh+3`Hohr*?xuy7fCF9#qWYEaoL!at_y z)vJ}-i}!u2y5J#Dai>UYCNi$5qH02Q+IZiURCnHJ`QhPN)Vv=;P)2&^bACmtXFz1(TjKEb>sQ)$qZ115;S@uLhZs&V$hxA-x6vqMKS~}aA=vSxhE^Zpoc119qMgaf zjYAX>L^49SmLk`p*AoWjBRxu$dBMtVO7=CmsrD#|(vbr=3u%(0JK;N`upt0}pQg3s zL{_yp#l4(IPB;1X1Og%^2Cijk)g`PVYXeRQ1#)ee5wsH8ktL>8=J8!!CMLjAZNzpO ziW=7VHxqpQ0sn}bxrF^!5502ITV!4Kj)``BSLv{xf|6g@vmGWC1|;;$=dcbn;erdK zJQuPqU>~MaDh#SBK{5;pXejzP%age1zqd613f45bju45^6i6HHU}(e<^p-iXN}OUz z*ziPDZ&7INMoRA@JeomWVU%Ubtz*57s9Zj6aW@B*&Z;OByZ6q@2>eC;E1V}J>YYfU zIMb0ai-v34+e?>qzPg_P1(98CM`$(2*{_^ZT6A% zSyAHb-AJ9qEdMuV1Cu47G-;*ShuI7@oLSRC`Hd&?jcN-glZB)bDaYCm^-EyW55Fur zgs22_)4|M0xok~cKbF`sX}a6mD=*cA1G-z3`=qJZj8&yx*4LwdDti_v^^!6P(&b|m zj#85t2vdp^LFeWHgym7zaQO!HC(w~Y&z2v15nj@1LkVM1(OT7MPnCNsl?z|J%7BVe zn?LE2I2D0YhZ9stq>?QCOuar3qcgX*-U*zL9!`4K@;4+mUQeMTnP^b&^F0+g+@|ctVW`BCAHzMTV@(5(UQYAJ@N?K~H<2;7SmRHI!Rmf$`VxXgHn=n#U_Cs*FXs_k^5PM>C2nzdii1*w zpGy<=wT!GTewY`f&MkSx(`?hZ3DHJ)I;3R7k{8wAOp7^yt}-R>wpu@0@gCXcl6Nqi z+6(+#)A4%~IK#yjy274tD1hZe7{+H&#|fAhsCP9$+*=W~3H`cm)Zl^qh{`#4JkvKt znv@-3`AyQQql`k1zz z^^whyokA?m^LN44Oqs#_eTZ3O8Roje$;{3lgk`JwD=j>;natV?wMu3;!!52K)qK2E zX&mcYnRC#2sW5C>g;}H4je^b_Ze*RpC0Z-Sx$A|wtYamD%yw%3Hc=NwM9RDktZaFc zSUjYz9BnwNj`C-0MZfRdZ1poY@7L6rekKIc{`~-`#xADin@nvSzQG`(sBkzL z3KYxNcPSsG|B+JA5&118`L1WeT@meFBZtNtKlTFkbPEV7>T^Y%mz0hpQpvY7G!_4r z9yqWVo69QhrPPmCW@WK2@`v~C$?m6?JfyAifI4mLaGo%72qpi=C{&G8bZbu&e1jUQ zouRfvfmEE+Wr4c%`CIOICcV~o3tRjx;Ry2Z@yJnrJ{J-aixSz_pX!ivi~a_1!A|Q8 zUTUd?!X$arUJ=Ft+JB|{8P*CLs4=Y4dGVa60gufmQz#*wyuh0yJhAmC<`m3~6+&U~Pa@DWkC%YKjtPIB~`T)?V>lWwRccG?$qn1K4E8 zaQo0JtZ7~d0oSrMax!zoc*m~;o6G@kH<$l5$t324<(n67d?Nhug$Rqf(@sue8&r%8 zNFI5Mir;N}zQ5Ly10(#9#Wqli$rWoZ1%*X$?(qGe=OVkZ@;zJ?O>6iTX(YUXtaHR= z$O&*cG^F$?Q6?FNwMalV*~^_3@5*{H*wyl+FV`z|B049K=R;*^iAYp!Lir%_>hds9 zAJ<&;7OT+j`>ww^(QbB^1(Ic?26u)#xg#oXheCea7fXjV0hvvCdx402ReAnsfAkAW zM?0}>z!w$JcZj?(`5$kLoX|Ufj5ex4qhjTR&v94U=MbD4iB;->k=S10tpMS57Pi&> zQoB(EY(1;wvPdKhF-uf`k+%8<5oe4`0Q`r8Efrn%$BLt3YV6P}!%*Qag z(Rc^cW-3D&Z`d2H#2lJ(jkURE39QXs;eLZ5zBgAG*@ElXmQjfyLm)j2J)U%#z}l_) zvAVY}9uN zM58AuboWtA-ElId>p==-BCDw%)Gqz5DK1Tk`7S5OE7wEk7d^Y|+4RqmD&t}5%72cz zbhD|ehX53zNa@vI`Fr|f_|xqfa>=ZQEZVLq5dgDD!At;ZXN5lH{E)7@eV?#N;x2XS z)VziqdziP)9f|@eW%;Z_z1A-u>on%Q=JD@RKgOED6!d z7y*To84Z)h(9wJLPVB1x8e~vi`gDgwb-UtTF_*QB0HV0mDu&XS(RlB0>^NpaZ?QL@ zO=O*j*l;Y2$<>zJAu-G4peI5Yl3;JUJ3}an%-rik7=rx4C?ly&%*5DgO^SC&$hmW7 zFs;x6syGae1N9CHb*%6zUf7>LY$O*3F34fziBGSNdcv*BpNg=BJb0|+9p$|s^2=?4 z!{5GpcNX5{0rHu-CJjYqoqUXQ^Af(&TN+OHA zV{JHC7ZMZC#_C)yEJCdXD5v-<>OQmq1^ws6XkykH6_z!XPCnz2li;dn>z1d z8z84|uduubTZ6Uv67_6B|Jy#Y`Spjl-91muVuKev5nlAkL1w&Jx7d1|xx=EVC~-!&F?L7=BVIbI5)(z^y_CL6l`@ppwSpPKhEYy^JeJ5mU%TcT z4!HC2%Mib?41T;~y!J;5NaLFC+I8vjt5+MgY~30UgdJr*!^IvpF0;fJ=DT;BSR*}; zK%PfR^xeCGIPb@gJZo1TZLy$A)Hqe85nzL0ft1Xqkk{Cv;$ju}-H&i>6s~Yt=+taX zs5k511|<^{$=&*ExE*r3N*YMmB2DW^5G%O&jmXy`ubXoyjwR-K3z^ltaFzA(CG2dHQju37}*oSJhjWZ=ab^S7PYb^nF#A(+D&HU{z-){FywJK zIg~;;QLMjt@31M7V{kENu$PiOXx1>+5)Lyz&wd)_V~HwDx~X4UBDZh0Y{BJioflbG ze+pnYKxxgdkHu!8rAlj96Wznecw@mpHWM<-#-8{2^{kV)%$cQN3-Ye{09Rx-mX-#- zQHj{gca^m=m=&|8<;*@S&1Lb{l{VcHC#Ayt9=-9`h2%)6h#}nm2A7!u?l2G7^V_yu*vj0*hIv3&Mhkkj#mb8594dOlFbZet?`|*1G~jXBzc_6hd(!!N+q^Ve%_kpXJzII-2q0kc znIYLsX6(tVor-%X^Zac7WK-kG8Ek@M#Aa(YXQFNl<*^xXIh$}8Y8u9u@$0mgM<}u- zkIH@>79DtI4anHo_17guer(7ecU-CxIRnP}(_J?nWl+0U1T_5s8sxHu ze?aj-WF-npOMO@%R8LL9mCOqqNjPz=l-);1R1-QO&q4pl4g=yCIa&M|Tf3@M8Ihxp zk{AC$uQ5Ek=Yp>w#z;y^Vtv1H+Uc5E5d(M;ulHt_PyaDUSSqS%iE zVd8r$tZYhpGrvC6aq&1ZXxQy5yC&yp{(J^CVx_bVwccR~Q}ynBEnWFKTujkp@YoBj z`@QNiwSFRtI<-#<5HcMQ{*DiA$(d+Wx?KCEMIe=cqA_>~720Y$SMZR9Pv4<}`_;9r z1O}Ex0dOrE+^HDLKR~bmjtAkjX9)PUR0{S5^JvK*N+QnYU?w_8ob4FYMi?w?!-TDgV;o+V&V%d{Ddm`-or%-M#C$Da${gBM4G73D>vVda z(8aa-Mi6%4sVe#dj5(oD!@7Zi$FvO0*$L|w^A5%7!1mWrm<8cnsy!i38;QlAe6e?F zFC`pj1*}R&hi0mep^{?C-BH{S!CWfv38#8?j2in6B`-q5kY-VtKPjyRn;f7vE9L-N z11pptb0Hd-45(yO+KFzMwx~g?8HJ*jKs5y2P5#jL0ln{HwbVAo=& zB0%aEBqyc(gfS$N0^5r zvLxYd?H+C)!Aen9sE$Eym*0O-0cBPQF^%T<>PkIz+B*9X`D#Kh%p&B?T18f#;9sTx zJFOOkJ2$VQj?%6uAeBO&0t9E%C^mp5f%GL8!1X>h10* zMKM_MBeWj|B9}S{LwPvQwrRp??3E?Cc z0G$||5411>K>H{RShA5V&+gez-F6`9lXyz!%NxP6yQf5SmDa{QKr_otsL(=D$vlxz zn}L3c&BMc~?gI@&Z4fx@pQ6y-Uev)gqpZvZ?IbEj3_U`KdQ{k~mBV8a5A`CE8-_4t z;mem#1$!HZ71kj)QEvdM_TPG6Qe2?ig1!d z8b0*MPwwAl1Kuf~J$}UgMaau^eA}1ujF9lC?Ng-cO5)K(UCzPYhgBz)-CKl2MA+~=Vz^nfeoaObu4)V-8|Khg+dkdXy zq!U~0G?e!)>ffb8x5J)UuCISRaa6xV=M$iVe@yGvt~N0UsbJUoJsfUPiR>H9?LXkR zQM`K$Hog}7d4?Oz^#Q*P^~-%e9EM{5@~av`T_O92pO+5)rlWp2xCsZ-zJ7lD)aQZe z&E2)KNk8`d)Xu}ArwbL@KoQ>wdz(=r66(tt)001;8-vPs9lhQEu>ONW{@cO8e;9m! z+FAW``>%r^{~vuvZWWl2*X5FKnlHNOH7HN;L9b_Cxu%D7>BF{+sc-o0(z*LL5O_y_ zzun(OHX7M){wg&_Ci8v4VNH|NU0iaaX$F>)931+oVBLR2d@ogc@$%&ggjp%sg7mft zWIV4?0*Yy<%5{k*+C`#>s^w9Giyz8dhUT-aE$S4HA`E_H%8SDgGCRF`_C4z|t!=QG z37CR+OHe!?MzDcTIXWntRAu=S+5{DNJ8nZ7P#<_?>OR%+M6?eEMu>1_)Ji6$24E<_ z*QULpAU_lwYB%(4QT@C2HB1+%;sn@a3cyK`#TO0>S~8PA4UG++0Mta4t4PMDnxcd= z6sAJ|QUMM>Giz9K@An(_=(^*dXR3hyf>bC=y+hvJx88{h90}lsCwPvoDFlpqt!F>_ zzyX}V$^*gjitj+b{pZxoLN-F(s49m=$304^n#q$ zx}vjy*3esB-11=AvMdfvmEIgdpr|BBH9y3w3kwQTQD(ey8x!?*>%8n%fuvdgaM!Pk zQAc*x`xjstolx#|{7hZeQH}Wvrwj_|gZ?b1cWCJMb-qpn#*DCpl>Wq?EJStV#dWbC zH~*`BSN|2QD-BTf(v6lBJFW8-Te$l44>S&(IOI@cUssw$>A)&ssR8cyb7c>_AGzk? zz|TqHYnnZ`nBPc@e)cYZR7lRIQ;CO1Zc`~+puTN{)Fso>?Q%}rxW?CC3=BSVuSp;? z{h{z+qjQHnTgnvr!tCV*R=*GZUfAY@MonZlp)_#%oH?Fo4k?XHV*TF5qeqX{n>%*w zSi)ja_f*ucKF=?&t<5B`8C*ea6O-9^2NIDL4hguqxrx<$`SJ|CqHNxxPh-D?7ZSS0 zx3B)qp|tYp=n(ZvN}AQgY2lTZIY1UrFGUFnl>Jccv-xhI@PhM9QLIJT#A{#Q#1HaF zTZ%WATr)8>J$3eMc8Uek1QjC+ zzx+7=XSTiFP7fbGv>7pS!-o&w zeD~G!&R)C|IdN(&=7G_CuA0{aLPS*@uVb(b3Sqp#2wH0s)c!sPjzZvJ&7^`N@0?gT9d2M4|5C6>bI9U^EX8`WzOz*V%_Mo8##%&F%;ixnXbVqGtQe1l+fqgMYlPxWvCxr12{wtsI<2uwTE|fhFFpSH4x6XkW6{lAtVX}IbfIOn`a{|Nyy;f;AP1=gDk>@x{h-OupJxnZi&g&!!%ygD93CEy)>j2v@M;_Mivf5^PJZ*X z4RH1S(#ju~+n`L*CnZIBlJ_z1A%h2>gt$O`eJbivZR`H+#d@)LbSf1^vt)U6(h-t7 zhwd;Zo;+CwKglZq5&-?zW};<0wbMY?CDwN1)kYS(cGaQt6?&mH*v8$sk!dLlv}gk! z%{$xJ5M`eB>x zvTY}zbJQ7(N_pX3p})iqnJI}M;8@YS41nC~vqga7V|DdYyiX22DR$r_x)C+$!aD(M z>fcDBC2>CJ?syTsMhu1iX?Xtl5=-5U8+A5pSc%t1DJdz*^N)cnkqpVBH^~&gdSxhe zb`#z;r&V2D-QM2*(MGFnH3-SnR(chpQJKOG2q`MveLkZ$yzP?|H zWevhOAj3%k*#s`LnI|runVC5ZV&BYcj_K{QQ1Qfo;B;KgX^K;|j%!=eo^sS-Uy6zG zym(RQdH9lN6rC)wjE;}@kBONR7Z=wKx0esCg~V1~^2c%+-rZtOua2U=8PqZg?*aIa zBlNQR2_q0llE{DR3K&IAjg99~oJ>4S%_0)?5^N43k8CJ2g%G1u`A^LVr%yjf@E0`T zK-6Kx=+RNh$>CVR@`i?ddbtPM7uiUi)xbK=6BSKFH}062n2+~UdKSoe;}sDS@VUBs zdY}r*5hzNn1O9m@x-HUMN>o;?U}30jw%_6rmmk$VVU``)Ho&rCYa7k%UI?EAsgZw_u^afB)Y4`Q<1u zE4@Afa`5p}ienHlq36no;ln?|q`82;Pl&`^mlYHgbar-bY-&oyPB9-6PUF2_=)ScA zL7r={3MA2HaOhY8jlci?3s9VDD8O>;&*i&drBmpe4jEuzV1P~=mE)&OS?%Jo1kGoj zl;Le-h$NxWu}%Do7i&I$`9f^g+uKWVD(Z>Hkp}9L&0|TT`TP5$DW)_+i7+3xf;>I#?(TMk{yvKW=4+1tBT`wp@>Ez@9?F{Gw(fw-583mv zuFgxH(U*5jWkaCBD-F?plG-m4R4q4GgQ9QOqZl1*mS-u8mNDL*&mg}&Pn@Vh_b@`6 zQ>PK2=t*5mPVtprC?j(dFQ|f$Gy4j-La>G~YW9yjHM{`rho$ik4fWCK>+PZ5R0InL zSc8I06lV=tp{9nwzyJ|zYik5mY%I*orYkD)2T=g>OEz=U=mjt&czsufLMV+GJ2sPy zDSdr?tx)-;|9;R{7%3PqI!MW+q(#sZs%Qu@d6$_Py`TcR;l*3vMLmxnudtw|T7AdS z$L0mza!KKXix)52#9q5Lo96KN@!zmm6l^O7qxnV}k$7XHjoye-}oV_^{zr`phA2{{HB;IS1J)uzzbA3;X=E)D|=JTWTf1BovFvcR+u+LJ%pXpx(ku1zAw?WX?x?5XL#MrvuZO~YtWks#l^)omtX}vMvwc$U@yJozTN{< zrcMQgJ)R3&6^+lZCS;}pr@RB5Bom-J(exFq-7i5U;+u1_6F}Jd+K~UQWrVNGlWB?X=i;VuIS0Vx>W&5QjuCV0^5e-I62}XHnl9eZ25V zIKT@HG;9?rDl5;*SqmTvH)GbUqg2#0wSZc&AW#pXlAf6v5D<_C**2VutBNYGAWiWK zpYqDed5NlfvDGPSQrAA#3u z{ktdnvI=tE38Cc8=)8-lVum^N1tA(?^kQy)EBE~ATx!Q6ui%Gr zy*v{qu3)o1I++-tBS=+ktu!K@LNjMJwYD0+xk+-DXWY2I;a9<+)!ne65Djv#-#syH zap7C)jZa#kxv#gZ?-J}j5KAL^=u_TBY_^h8I3frV=)MPmOT8edTtMy|o*(^WC?t^S zCwLj2(u7HqDr5+YqZO)VY&;8g8?|jj!=`})2jWF`La_bN_|oP_{btCNPtcp>xudxw zWqa_JC0}&iGBh-dxq3Ac>K#1`*Xrmjhkr;2|;Bo zcxH3){s%+o5_%O7mw;k0={b6(B<4B(pBIQ)m&}?yo12FRjcumNANVmJuU?{;U!p}6 zwT2|~>}QhFGs~~ZrhAZ3^X;q6n^tU%UKHJ|&ePm{?M_sA%88~1o0?ocdMyw+1q8)~ zzpLQVDFfF~Z+CuIXW^L=-=ASXc|LCLL@!XlO$24=HDKToK8YQFQ5Pl-n^tW>bACG> z+B+E0sKuDV2{!oe$~*Wo%Ho-D3@sB%xRi!g_0{fftO7WQQ@1ks`4qfsyA&?LRA}Qa!Bw^y^oO7!~@4d2&x)6UT zKvP8Fh0R}=r$4zWqKJtSS5ZzFjiAWYOihBr3;^^3U)JpWI7=NT5|neOpTG-{#|#6&PmEZ7wg z#ZWAu39*2J!HyH7s6Osp3ij+7sMTtvmY3qyYTESa-*4U= zP>SGT#YshxTtmRLH2Y@e6%@FpmO(%WY6dKnMlxdEplY?uTf)#P^VM~A*G-{_&tg2)kw!Q^X?jy?b^!qfFXvq2bDrFOK$NUQlDK1}&-XHp&#m|aX5V@v5sJhpCI-g$%FH2Y?vrFl4yCs^ zbI5S=FTx;iw!Ii5dUNq=bl+iKWh_T4^&O=ua9OKcY#)$2_-F*54_DFDuQBW z^T|v(p`o+0vxUI~Gtto`f~e`ed-tSESVk5Ox`&{@gZdLI>q)H7?cRCEwf3%t-Y%Vk z6^@^L(ux=TiCJvYba-igcqfJ8Bqb5Sg=$uT&;Wg?RD7_wg=@A8Z_?`lEy&IYw=CX*$R9Ui8z{&?9k6QR4;K7FE`#3RIfcZzNM zxpRAfJ_aRoUjZWq!&mfAe7C!eLM@!YP^t!?8msuaJ%d1>qkr!+FG^QWFGnWe12HJe z(kvK_79(DVEn;l&_4Q?)_Ojf^Ch4(8Grh!e_i}bDZhmp-0+tCFuT>PAg#Y^W*4}q} z=$k5!&NZtf1Sv=AwhE8J<8G-kK^?=nU-KtAHh6>IT6;&wf}*0}#^M@k#FafwX%f5U zQCF6GKNFL80bJ}nNiK=X4LBpT?*VC-09yg*pFwdr@D)TO`jQ$ZsggxsHF3FhL;5-G z%o{Ll*yal${(BD|>;gp%I=lDt5rU%`^~S4@^RW5$VEe8aD-8;rx~r4pA`H*c19g|* zO_$O}=st_(HGO({sFmB-z5VT)DCx z`W!wqQ0Q*Qj)Chhm?|^Zon2JrY}}>2V|;vkS_XGg$p7k}L4%H0eIJ`7{?3|=8MP`B z%KVHAsb*5UVUgi&O~*B{we`^yliG*u;U$+D?0J-d-8Z0J=FFL61rzLY5UK)FCYam= zvE2xNpFMZ9N4G&Mf13N~V5;@k8inOhxCklgme|(cZ3T@X6EtmcG}PCuebNtBhLPJE z_^~`0p}F6q;^*hX6^;xW>mt+Mr2QIf6@1~qiW~l=*gRBOyfah$`%-{9B)cTasBMt_ zhiZ7Z&I&~csGMqA876emun(mBP{>nLTFwdgc|F;q;bBsEn1nM_N`E48p}9Yo#I<+i9LTMP?30xq9c^Ix!fSTP zvX-qYu*6n%C&$`jg+#ah@WY!?Fkp_cHj}u+DyDktL0{+GfaQ}o3Z62Fd;sN%!$p+^ zrN<}xF)ps{0E$E`94IO=!@v5D;0HO5_;|L$uG4tE zZOr;M8omc%Pg%W1=i?moC0H22NaUH&&`=4?2np#R?glYrIt zNe+|P9Z4k-&KTMbxFz$C#nuHaBRQ{7K_ceJ+dyq4Ghr5*Ln1&-=tJDaK`Tkf$BO5j zC>RiPP!+a48?0D{_Vg56&iV=uX`Sbb0D#qTHo_MyWTd4q4eU@qJJ&w(+3ghTNLe8Yu8akE{CN2gTU%SP z;fRz}{SBi=HiU%qW}Agu)a%qK8yE@h%w{30>7atF^+o^_NZhk$&k%+6_gLz#{qjrr zrhM0)=H{EA*<$&!BoG&^SfX1N1;@sAhXU-)c46mXjG7V<7^K--JxLt?=XNlCiuprCe)_m$n68!uY%tW>v_N9?{sjlhelJ*#R5 z>r6ScKl;{Z)G$vUR6gB!X+ZP#19s@%>xdbS%@udbw`M%NQ?Z=@X@iIa&9&U_EeER6 zk)>mO=QV-gyo!#ObX12^EX(45itn8~rKQNE%51~iU0Ji*a*WNh1L)Y`?Qe9-y7^zB zD@WdKDRu08%U*^h5ea(>=H%A3uW7lqh%YQ!8F`&Qe`$$T#p)J>)v0cwxslO~3je&f zjK?dVfuj19uKy2hnt|W{GPDSmN)3HH-+GzPt*t%Ff4;DK^~Q{ecRJ@5SKf_Zp4Mmh zneB$37e#!OmT&Gp)N;>m|5<7IHD3=_^-6u$;YX7Siy6(&n5Bih(uWDQxy3gh7d$9G zd1{2BJnhul)5Qh%)_iEW>w2%irl%)j`=%G+-27?N-uLwMwAvCCWlq#U3EVAM>3XwqG1%j+9PX$&zlqCq_p1_YEnr2Z#{^S#X0!=4$&_ zpg<8g6QznxrYj}`(bZmoHfr!;1F0;r7I<_ew!L0YR1H!Bexy_^hn-)L+l8% zkg*gX3>Gj*gM0rX=#ndoX_T$uvZYnl)zyfxM>U+4*X_3e{N#ZWai)3s!R6dEyX%A) z0xZ;~P{jjBvbIsdQJNdiN(s1m)BVw%ilQwrp>EYDwuo3**lkq^lDLGyfXmpCg+Yv} zt@W0~TA6(;zYK?F-a%ot5oRAPBYCE=!rCis^`$@=8EH{ASd|2hR444TGfQ)Oug!e66OwB8bdG_KLyd+V#!baTb zfiy?z2_K<=@vMeqd)uLXJ#QJ==unnCzb%C@45UlZ$&I+Ux>LGGC2AW z%r)OdYjha_rr5ZXW*P@Q=6>CO@IVIdqM3`AhQZ%5y@Vx^sbo8lDvNuuSD)7h`AIM^ zVj$?wh+)zk;-!hRElrU$l9<0BKcq691XkiN(WX>br#us>p8wTb)DDor)e!f)Owb}) zw{5#Tkx9@2Vr|M8Hf)$KC^0TCKi~7_xdD3;60*9z{KIfhD+Nq(Dns(c2f=Mb`jRef zkBS4HChxxVQu5nk{f4!_M`%N51B3BOr2_p;l*%UbbnN_>Rk1(~X#f#;eEPHwP8%T{ z;Ktp>ogzC02KfAo7o*r$xW6c%RbC)-Jv9QL%Y)y3D?EKV9BEkzNGZhCR!c9zcuLf$ zLBJ;Fr&D6pK8StV0M6CwezIXTplLQm(FJbYm_VUB6KPF`>+jj~j`{`r%<^U=X5FYc z8Yv#X1Sn0b|ol#-QPr>hs%{#VWbepogF7=$LH_R$fL{&H#9$j8SgOUci#8A~5KzpyHn+bDV?Mg#9vt2V{N$gF?W zz5Dlbj$_}r%$p<7D8I{h%^tuizre2L0GpJhfD_ES!Fad0bkdRi7@b!_tj3E@p`+C| zOVv8OaE#5GZJ2F>SWv0o{OaDOqhdUn->RW6ID(YM0|&mNrtbA^BC_DISRBYjB3C)S z1XM4)?y_}%i$}^7dmyS9J7)~q#<3G8NI7Y*M*An`Kn(L>=zxF#RWf%@4i_|2ANro; zyLCzU(|=y+JbQMLI4l_jC_?!F_Av}vGLdR;Iv--^nn$zn^8|&57s`*2yjd5jfussx zE@~oOKpd6_{>*iKGo2t3*oUUA7Q#e{Xel8T@qhn&4;!0q>UHb>!t3){wXgYI0Gdc= zw5;r!v2TtQMjSIP6U-CVv@2u=MobqgyqG-@D}PAGh*7x$=ZX^U;V8uc+m6PWy+95< zwwA&h2)=A*tiGSiQu{^i`s>wy@VLt?wNG?L1r#yZ!t(=*r3Q&jo*%?zkY2UDyfd`c z<^6wkLIZBA(@kti^$LOJrKOwLJMe=AOEvZBr=PMoy_g&4nMF0$6@`marjVe3jT<+vJMX@bx#T?J()t2wYWMcr^t!)AHNsKC z<8DN$z|b3X%iQ#|fB)y$6$WE4*v%KJ3}>+ASZ{AD_`>>VHcd7Zh$o2CHk?M%-bR@R zE-Vj%k>W8s>YgqWJq{f{EQAO`Hc#W3k`jNe5Zv$W;}d}i0{bFn4$U`PnA^|Z0@mlq zt-O6(;(3J8$Rh+W7eG>vVISe?Z1(6pSrd{b(mHiZ z*c8|Y>|L1>Th5V}LKvP5`~;2rFby46yP6J@&MmwH@Lcf=N3@7u$Ld21T*~T+Yx;J=4rQ3vuztF5(-e~GS+A`nUq%CDb?}Q zr~47#xTKnm8q{vu$Jkg@R-w2_&6Ca;^yP`$EkX@Rr8FR=3FSAH?znC~Rh>}7h47{b z+l^uy@a?ynVN{lcmWYw&Vro*7S!2N3mk1Qn{)Up>>Am;R`n6*TrX1TCkR$GE+INH{ z=6c#eAmn@O@2sci??73}&1tCw7XJADrsH-bRbY=mr1HOjABLi7ft#Bb{ZPv(N=ODh z1;`P1gl)cix^@^Uvx_1AE!>UV!a(UhEK0^Hixo_IOgYl};SpealTmOms9Dj^XpSm! z%FO(5P|ENi{+cx>T$z{{CI5~Z+f;nBlnG+=!BARF{AF)+Bb<#eS@sY;SZXnUwx!tL zJCzcgeWRM)s2>I4!8-@qV_2pPk%UnS40-9F$T&rT_Z;&4$9&UspVZ4ZYfZ7pbW1od z{Lox8Gp01j?lT3daC*Y~!TJ6Uu=?WRbrI7I44T#%_Y*W8{*VrBTA@+6LNLm=z^+`W z2U7`SHGoi>C5f2zCHT-G9wVZAI@YHl1|6ik4z|;)SHi3Rshv7=W)N55np{}|s*xvd z=lP2l=cl{yc5t|G&(fs?W2+M zA_)ndKm4#5I|_7Ejr)mxdfd0j~W>oiJFE5sWg%kQ5xRxM32Xb z&hp7X+>s86e?Gji{$QHoq}pWXkhPWS=l*xfgb8oU3}8mkLZ#|;>GHQ=UKF@qgH;YG zzw%sp`9$_Y9tx~dF$wXWWp5+7K2d%@LT8+e(S>!W(zcKUi^U+5U`px(&2+g8CTO)`1pZ?g-7GIVAD?T5sBN`OTZzv&+h>W_17t^?77`w=V@g#dHV%X-naPvRV>ppDAT=t!gx-DomYh2m0E5nU ziY~N{vx%F%$U&ttlTb_Ya`7fhaHDuNS?H{O36rdLd8cE?zN{+I;;7e%K*B3M+&Qt*ifF-o+?AObO+T(`lzr1BQ!H%4{41R#h>2|V zV!}zD*}s2(K9|i=z{ai58ecW7kCjz4<&s##QRgHp68?W7lrvw%6B7?c5r%MboyIVufQhdDFqQIuY&r(a)#X+-x_SXl? z&?|DFIk!7_*dasaVy0xesXtpF8#A68Ep4Il(GAZtf(iT70=RKW<^)QEgY%$@#cbL1 zdA)x1d1)P69;iR@ds?;5hR?IQo?Cs@Zuao>2b!@Lz06`WybJ3645wX7*^riRxhu%9 zY{saAXCp`acDacljWC3gefIBvWFP4i5qo>uawE%xaoXWt=D{8AUU#eRTG!6fF!pWh z@{j}dajlzPILo>#zni#Z!3dA(w|_sJwbjRPWaeU@^cuHoXaCX5vRCYgZ#Z=t{#o|> fzwKXnV!OqEb8XeO$^HMrzX_i@+U@;#uK#}nPdk@b literal 0 HcmV?d00001 diff --git a/end-to-end-test/remote/specs/core/oncoprinterColorConfig.spec.js b/end-to-end-test/remote/specs/core/oncoprinterColorConfig.spec.js new file mode 100644 index 00000000000..52fc007dc61 --- /dev/null +++ b/end-to-end-test/remote/specs/core/oncoprinterColorConfig.spec.js @@ -0,0 +1,130 @@ +var assertScreenShotMatch = require('../../../shared/lib/testUtils') + .assertScreenShotMatch; +var assert = require('assert'); +var waitForOncoprint = require('../../../shared/specUtils').waitForOncoprint; +var goToUrlAndSetLocalStorage = require('../../../shared/specUtils') + .goToUrlAndSetLocalStorage; +var getNthOncoprintTrackOptionsElements = require('../../../shared/specUtils') + .getNthOncoprintTrackOptionsElements; +var { + checkOncoprintElement, + getElementByTestHandle, +} = require('../../../shared/specUtils.js'); + +const TIMEOUT = 6000; + +const ONCOPRINT_TIMEOUT = 60000; + +const CBIOPORTAL_URL = process.env.CBIOPORTAL_URL.replace(/\/$/, ''); + +describe('oncoprinter clinical example data, color configuration', () => { + it('oncoprinter color configuration modal reflects user selected colors', function() { + goToUrlAndSetLocalStorage(`${CBIOPORTAL_URL}/oncoprinter`); + $('.oncoprinterClinicalExampleData').waitForExist(); + $('.oncoprinterClinicalExampleData').click(); + $('.oncoprinterSubmit').click(); + waitForOncoprint(TIMEOUT); + + var trackOptionsElts = getNthOncoprintTrackOptionsElements(2); + // open menu + $(trackOptionsElts.button_selector).click(); + $(trackOptionsElts.dropdown_selector).waitForDisplayed({ + timeout: 1000, + }); + // click "Edit Colors" to open modal + $(trackOptionsElts.dropdown_selector + ' li:nth-child(11)').click(); + browser.pause(1000); + + // select new colors for track values + getElementByTestHandle('color-picker-icon').click(); + $('.circle-picker').waitForDisplayed({ timeout: 1000 }); + $('.circle-picker [title="#990099"]').click(); + waitForOncoprint(ONCOPRINT_TIMEOUT); + getElementByTestHandle('color-picker-icon').waitForDisplayed(); + getElementByTestHandle('color-picker-icon').click(); + $('.circle-picker').waitForDisplayed({ reverse: true }); + + $$('[data-test="color-picker-icon"]')[1].click(); + $('.circle-picker').waitForDisplayed({ timeout: 1000 }); + $('.circle-picker [title="#109618"]').click(); + waitForOncoprint(ONCOPRINT_TIMEOUT); + getElementByTestHandle('color-picker-icon').waitForDisplayed(); + $$('[data-test="color-picker-icon"]')[1].click(); + $('.circle-picker').waitForDisplayed({ reverse: true }); + + $$('[data-test="color-picker-icon"]')[2].click(); + $('.circle-picker').waitForDisplayed({ timeout: 1000 }); + $('.circle-picker [title="#8b0707"]').click(); + waitForOncoprint(ONCOPRINT_TIMEOUT); + + assert.strictEqual( + $('[data-test="color-picker-icon"] rect').getAttribute('fill'), + '#990099' + ); + assert.strictEqual( + $$('[data-test="color-picker-icon"] rect')[1].getAttribute('fill'), + '#109618' + ); + assert.strictEqual( + $$('[data-test="color-picker-icon"] rect')[2].getAttribute('fill'), + '#8b0707' + ); + }); + + it('oncoprinter reflects user selected colors', () => { + // close modal + $('a.tabAnchor_oncoprint').click(); + var res = checkOncoprintElement(); + assertScreenShotMatch(res); + }); + + it('oncoprinter reset colors button is visible when default colors not used', () => { + // click "Edit Colors" to open modal and check "Reset Colors" button in modal + var trackOptionsElts = getNthOncoprintTrackOptionsElements(2); + $(trackOptionsElts.button_selector).click(); + $(trackOptionsElts.dropdown_selector).waitForDisplayed({ + timeout: 1000, + }); + $(trackOptionsElts.dropdown_selector + ' li:nth-child(11)').click(); + getElementByTestHandle('resetColors').waitForDisplayed(); + }); + + it('oncoprinter color configuration modal reflects default colors', () => { + // click "Reset Colors" track + getElementByTestHandle('resetColors').click(); + waitForOncoprint(ONCOPRINT_TIMEOUT); + + assert.strictEqual( + $('[data-test="color-picker-icon"] rect').getAttribute('fill'), + '#dc3912' + ); + assert.strictEqual( + $$('[data-test="color-picker-icon"] rect')[1].getAttribute('fill'), + '#3366cc' + ); + assert.strictEqual( + $$('[data-test="color-picker-icon"] rect')[2].getAttribute('fill'), + '#ff9900' + ); + }); + + it('oncoprinter reflects default colors', () => { + // close modal + $('a.tabAnchor_oncoprint').click(); + var res = checkOncoprintElement(); + assertScreenShotMatch(res); + }); + + it('oncoprinter reset colors button is hidden when default colors are used', () => { + // click "Edit Colors" to open modal and check "Reset Colors" button in modal + var trackOptionsElts = getNthOncoprintTrackOptionsElements(2); + $(trackOptionsElts.button_selector).click(); + $(trackOptionsElts.dropdown_selector).waitForDisplayed({ + timeout: 1000, + }); + $(trackOptionsElts.dropdown_selector + ' li:nth-child(11)').click(); + getElementByTestHandle('resetColors').waitForDisplayed({ + reverse: true, + }); + }); +}); diff --git a/src/pages/resultsView/ResultsViewPageStore.ts b/src/pages/resultsView/ResultsViewPageStore.ts index 4c154922fcb..82500ac82ee 100644 --- a/src/pages/resultsView/ResultsViewPageStore.ts +++ b/src/pages/resultsView/ResultsViewPageStore.ts @@ -435,6 +435,8 @@ interface IResultsViewExclusionSettings { ) => void; } +const ONCOPRINT_COLOR_CONFIG = 'clinicalTracksColorConfig'; + /* fields and methods in the class below are ordered based on roughly /* chronological setup concerns, rather than on encapsulation and public API */ /* tslint:disable: member-ordering */ @@ -496,7 +498,7 @@ export class ResultsViewPageStore extends AnalysisStore ); const clinicalTracksColorConfig = localStorage.getItem( - 'clinicalTracksColorConfig' + ONCOPRINT_COLOR_CONFIG ); if (clinicalTracksColorConfig !== null) { this._userSelectedStudiesToClinicalTracksColors = JSON.parse( @@ -589,8 +591,8 @@ export class ResultsViewPageStore extends AnalysisStore @observable _userSelectedStudiesToClinicalTracksColors: { [studies: string]: { - [label: string]: { - [value: string]: RGBAColor; + [trackLabel: string]: { + [attributeValue: string]: RGBAColor; }; }; } = { global: {} }; @@ -641,7 +643,7 @@ export class ResultsViewPageStore extends AnalysisStore ] = color; } localStorage.setItem( - 'clinicalTracksColorConfig', + ONCOPRINT_COLOR_CONFIG, JSON.stringify(this._userSelectedStudiesToClinicalTracksColors) ); } diff --git a/src/pages/staticPages/tools/oncoprinter/Oncoprinter.tsx b/src/pages/staticPages/tools/oncoprinter/Oncoprinter.tsx index 158d82a988a..eaa6db3ca3a 100644 --- a/src/pages/staticPages/tools/oncoprinter/Oncoprinter.tsx +++ b/src/pages/staticPages/tools/oncoprinter/Oncoprinter.tsx @@ -8,7 +8,7 @@ import OncoprintControls, { } from 'shared/components/oncoprint/controls/OncoprintControls'; import { percentAltered } from '../../../../shared/components/oncoprint/OncoprintUtils'; import { getServerConfig } from 'config/config'; -import { OncoprintJS } from 'oncoprintjs'; +import { OncoprintJS, RGBAColor } from 'oncoprintjs'; import fileDownload from 'react-file-download'; import { FadeInteraction, svgToPdfDownload } from 'cbioportal-frontend-commons'; import classNames from 'classnames'; @@ -19,12 +19,34 @@ import WindowStore from '../../../../shared/components/window/WindowStore'; import { getGeneticTrackKey } from './OncoprinterGeneticUtils'; import InfoBanner from '../../../../shared/components/banners/InfoBanner'; import '../../../../globalStyles/oncoprintStyles.scss'; +import _ from 'lodash'; +import { + MUTATION_SPECTRUM_CATEGORIES, + MUTATION_SPECTRUM_FILLS, +} from 'shared/cache/ClinicalDataCache'; +import { + hexToRGBA, + RESERVED_CLINICAL_VALUE_COLORS, + rgbaToHex, +} from 'shared/lib/Colors'; +import { + getClinicalTrackColor, + getClinicalTrackValues, +} from 'shared/components/oncoprint/ResultsViewOncoprint'; +import { Modal } from 'react-bootstrap'; +import ClinicalTrackColorPicker from 'shared/components/oncoprint/ClinicalTrackColorPicker'; +import classnames from 'classnames'; +import { getDefaultClinicalAttributeColoringForStringDatatype } from './OncoprinterToolUtils'; +import { OncoprintColorModal } from 'shared/components/oncoprint/OncoprintColorModal'; interface IOncoprinterProps { divId: string; store: OncoprinterStore; } +const DEFAULT_UNKNOWN_COLOR = [255, 255, 255, 1]; +const DEFAULT_MIXED_COLOR = [220, 57, 18, 1]; + @observer export default class Oncoprinter extends React.Component< IOncoprinterProps, @@ -379,9 +401,92 @@ export default class Oncoprinter extends React.Component< return WindowStore.size.width - 75; } + @action.bound + public handleSelectedClinicalTrackColorChange( + value: string, + color: RGBAColor | undefined + ) { + if (this.selectedClinicalTrack) { + this.props.store.setUserSelectedClinicalTrackColor( + this.selectedClinicalTrack.label, + value, + color + ); + } + } + + @observable trackKeySelectedForEdit: string | null = null; + + @action.bound + setTrackKeySelectedForEdit(key: string | null) { + this.trackKeySelectedForEdit = key; + } + + // if trackKeySelectedForEdit is null ('Edit Colors' has not been selected in an individual track menu), + // selectedClinicalTrack will be undefined + @computed get selectedClinicalTrack() { + return _.find( + this.props.store.clinicalTracks, + t => t.key === this.trackKeySelectedForEdit + ); + } + + @computed get defaultClinicalAttributeColoringForStringDatatype() { + if (this.selectedClinicalTrack) { + return _.mapValues( + getDefaultClinicalAttributeColoringForStringDatatype( + this.selectedClinicalTrack.data + ), + hexToRGBA + ); + } + return _.mapValues(RESERVED_CLINICAL_VALUE_COLORS, hexToRGBA); + } + + @autobind + private getSelectedClinicalTrackDefaultColorForValue( + attributeValue: string + ) { + if (!this.selectedClinicalTrack) { + return DEFAULT_UNKNOWN_COLOR; + } + switch (this.selectedClinicalTrack.datatype) { + case 'counts': + return MUTATION_SPECTRUM_FILLS[ + _.indexOf(MUTATION_SPECTRUM_CATEGORIES, attributeValue) + ]; + case 'string': + // Mixed refers to when an event has multiple values (i.e. Sample Type for a patient event may have both Primary and Recurrence values) + if (attributeValue === 'Mixed') { + return DEFAULT_MIXED_COLOR; + } else { + return this + .defaultClinicalAttributeColoringForStringDatatype[ + attributeValue + ]; + } + default: + return DEFAULT_UNKNOWN_COLOR; + } + } + public render() { return (
+ {this.selectedClinicalTrack && ( + + )}
diff --git a/src/pages/staticPages/tools/oncoprinter/OncoprinterClinicalAndHeatmapUtils.ts b/src/pages/staticPages/tools/oncoprinter/OncoprinterClinicalAndHeatmapUtils.ts index 65ef8e2e723..a84d0334dd5 100644 --- a/src/pages/staticPages/tools/oncoprinter/OncoprinterClinicalAndHeatmapUtils.ts +++ b/src/pages/staticPages/tools/oncoprinter/OncoprinterClinicalAndHeatmapUtils.ts @@ -11,6 +11,9 @@ import { MUTATION_SPECTRUM_FILLS } from '../../../../shared/cache/ClinicalDataCa import { MolecularProfile } from 'cbioportal-ts-api-client'; import { AlterationTypeConstants } from 'shared/constants'; import { capitalize } from 'cbioportal-frontend-commons'; +import { hexToRGBA } from 'shared/lib/Colors'; +import { RGBAColor } from 'oncoprintjs'; +import { getDefaultClinicalAttributeColoringForStringDatatype } from './OncoprinterToolUtils'; export const ONCOPRINTER_VAL_NA = 'N/A'; @@ -470,6 +473,11 @@ export function getHeatmapTrackKey(attributeName: string) { export function getClinicalTracks( attributes: OncoprinterClinicalTrackSpec[], parsedLines: OncoprinterOrderedValuesInputLine[], + userSelectedClinicalTracksColors: { + [label: string]: { + [value: string]: RGBAColor; + }; + }, excludedSampleIds?: string[] ) { let attributeToOncoprintData = getClinicalOncoprintData( @@ -486,10 +494,20 @@ export function getClinicalTracks( attributes.map(attr => { const data = attributeToOncoprintData[attr.trackName]; - let datatype, numberRange, countsCategoryFills; + let datatype, category_to_color, numberRange, countsCategoryFills; switch (attr.datatype) { case ClinicalTrackDataType.STRING: datatype = 'string'; + category_to_color = Object.assign( + {}, + _.mapValues( + getDefaultClinicalAttributeColoringForStringDatatype( + data + ), + hexToRGBA + ), + userSelectedClinicalTracksColors[attr.trackName] + ); break; case ClinicalTrackDataType.NUMBER: case ClinicalTrackDataType.LOG_NUMBER: @@ -503,7 +521,24 @@ export function getClinicalTracks( attr.countsCategories!.length === MUTATION_SPECTRUM_FILLS.length ) { - countsCategoryFills = MUTATION_SPECTRUM_FILLS; + countsCategoryFills = attr.countsCategories!.map( + (label, i) => { + if ( + userSelectedClinicalTracksColors[ + attr.trackName + ] && + userSelectedClinicalTracksColors[ + attr.trackName + ][label] + ) { + return userSelectedClinicalTracksColors[ + attr.trackName + ][label]; + } else { + return MUTATION_SPECTRUM_FILLS[i]; + } + } + ); } break; } @@ -511,6 +546,7 @@ export function getClinicalTracks( key: getClinicalTrackKey(attr.trackName), attributeId: attr.trackName, label: attr.trackName, + category_to_color, data, datatype, description: '', diff --git a/src/pages/staticPages/tools/oncoprinter/OncoprinterStore.ts b/src/pages/staticPages/tools/oncoprinter/OncoprinterStore.ts index 9d6cecf9826..81de7451567 100644 --- a/src/pages/staticPages/tools/oncoprinter/OncoprinterStore.ts +++ b/src/pages/staticPages/tools/oncoprinter/OncoprinterStore.ts @@ -39,6 +39,7 @@ import { parseHeatmapInput, } from './OncoprinterClinicalAndHeatmapUtils'; import internalClient from 'shared/api/cbioportalInternalClientInstance'; +import { RGBAColor } from 'oncoprintjs'; export type OncoprinterDriverAnnotationSettings = Pick< DriverAnnotationSettings, @@ -54,6 +55,8 @@ function genomeNexusKey2(l:{chromosome:string, start:number, end:number, referen return `${l.chromosome}_${l.start}_${l.end}_${l.referenceAllele}_${l.variantAllele}`; }*/ +const ONCOPRINTER_COLOR_CONFIG = 'oncoprinterClinicalTracksColorConfig'; + export default class OncoprinterStore { // NOTE: we are not annotating hotspot because that needs nucleotide positions // we are not annotating COSMIC because that needs keywords @@ -70,9 +73,24 @@ export default class OncoprinterStore { @observable hideGermlineMutations = false; @observable customDriverWarningHidden: boolean; + @observable _userSelectedClinicalTracksColors: { + [trackLabel: string]: { + [attributeValue: string]: RGBAColor; + }; + } = {}; + constructor() { makeObservable(this); this.initialize(); + + const clinicalTracksColorConfig = localStorage.getItem( + ONCOPRINTER_COLOR_CONFIG + ); + if (clinicalTracksColorConfig !== null) { + this._userSelectedClinicalTracksColors = JSON.parse( + clinicalTracksColorConfig + ); + } } private initialize() { @@ -572,6 +590,7 @@ export default class OncoprinterStore { return getClinicalTracks( result.headers, result.data, + this.userSelectedClinicalTracksColors, this.sampleIdsNotInInputOrder ); } @@ -589,4 +608,34 @@ export default class OncoprinterStore { this.sampleIdsNotInInputOrder ); } + + @action.bound + public setUserSelectedClinicalTrackColor( + label: string, + value: string, + color: RGBAColor | undefined + ) { + // if color is undefined, delete color from userSelectedClinicalAttributeColors if exists + // else, set the color in userSelectedClinicalAttributeColors + if ( + !color && + this._userSelectedClinicalTracksColors[label] && + this._userSelectedClinicalTracksColors[label][value] + ) { + delete this._userSelectedClinicalTracksColors[label][value]; + } else if (color) { + if (!this._userSelectedClinicalTracksColors[label]) { + this._userSelectedClinicalTracksColors[label] = {}; + } + this._userSelectedClinicalTracksColors[label][value] = color; + } + localStorage.setItem( + ONCOPRINTER_COLOR_CONFIG, + JSON.stringify(this._userSelectedClinicalTracksColors) + ); + } + + @computed get userSelectedClinicalTracksColors() { + return this._userSelectedClinicalTracksColors; + } } diff --git a/src/pages/staticPages/tools/oncoprinter/OncoprinterToolUtils.ts b/src/pages/staticPages/tools/oncoprinter/OncoprinterToolUtils.ts index 7e6dbae7a94..df24ba05c09 100644 --- a/src/pages/staticPages/tools/oncoprinter/OncoprinterToolUtils.ts +++ b/src/pages/staticPages/tools/oncoprinter/OncoprinterToolUtils.ts @@ -1,3 +1,8 @@ +import _ from 'lodash'; +import { ClinicalTrackDatum } from 'shared/components/oncoprint/Oncoprint'; +import { makeUniqueColorGetter } from 'shared/components/plots/PlotUtils'; +import { RESERVED_CLINICAL_VALUE_COLORS } from 'shared/lib/Colors'; + export function getDataForSubmission( fileInput: HTMLInputElement | null, stringInput: string @@ -21,3 +26,25 @@ export function getDataForSubmission( } }); } + +export function getDefaultClinicalAttributeColoringForStringDatatype( + data: ClinicalTrackDatum[] +) { + const colorGetter = makeUniqueColorGetter( + _.values(RESERVED_CLINICAL_VALUE_COLORS) + ); + let categoryToColor: { [value: string]: string } = {}; + for (const d of data) { + if ( + (d.attr_val as string) !== '' && + !((d.attr_val as string) in categoryToColor) + ) { + categoryToColor[d.attr_val as string] = colorGetter(); + } + } + categoryToColor = Object.assign( + categoryToColor, + RESERVED_CLINICAL_VALUE_COLORS + ); + return categoryToColor; +} diff --git a/src/shared/components/oncoprint/ClinicalTrackColorPicker.tsx b/src/shared/components/oncoprint/ClinicalTrackColorPicker.tsx index 4e211c696db..9aaba2ad5ff 100644 --- a/src/shared/components/oncoprint/ClinicalTrackColorPicker.tsx +++ b/src/shared/components/oncoprint/ClinicalTrackColorPicker.tsx @@ -15,19 +15,16 @@ import { rgbaToHex, } from 'shared/lib/Colors'; import { COLORS } from 'pages/studyView/StudyViewUtils'; -import { ResultsViewPageStore } from 'pages/resultsView/ResultsViewPageStore'; import { RGBAColor } from 'oncoprintjs'; import _ from 'lodash'; export interface IGroupCheckboxProps { - store: ResultsViewPageStore; handleClinicalTrackColorChange?: ( value: string, color: RGBAColor | undefined ) => void; clinicalTrackValue: any; color: RGBAColor; - setClinicalTrackColorChanged: (changed: boolean) => void; } const COLOR_UNDEFINED = '#FFFFFF'; @@ -59,7 +56,6 @@ export default class ClinicalTrackColorPicker extends React.Component< ); } // set changed track key - this.props.setClinicalTrackColorChanged(true); }; @computed get colorList() { diff --git a/src/shared/components/oncoprint/DeltaUtils.ts b/src/shared/components/oncoprint/DeltaUtils.ts index f8bbffb20f8..2a121f72877 100644 --- a/src/shared/components/oncoprint/DeltaUtils.ts +++ b/src/shared/components/oncoprint/DeltaUtils.ts @@ -1345,7 +1345,6 @@ function transitionClinicalTrack( // update ruleset if color has changed for selected track if ( - nextProps.clinicalTrackColorChanged && nextProps.trackKeySelectedForEdit && getTrackSpecKeyToTrackId()[nextProps.trackKeySelectedForEdit] === trackId @@ -1355,8 +1354,6 @@ function transitionClinicalTrack( rule_set_params.exclude_from_legend = !nextProps.showClinicalTrackLegends; rule_set_params.na_legend_label = nextSpec.na_legend_label; oncoprint.setRuleSet(trackId, rule_set_params); - nextProps.setClinicalTrackColorChanged && - nextProps.setClinicalTrackColorChanged(false); } } } diff --git a/src/shared/components/oncoprint/Oncoprint.tsx b/src/shared/components/oncoprint/Oncoprint.tsx index 52dbebbfe13..4bd8795493d 100644 --- a/src/shared/components/oncoprint/Oncoprint.tsx +++ b/src/shared/components/oncoprint/Oncoprint.tsx @@ -301,8 +301,6 @@ export interface IOncoprintProps { trackKeySelectedForEdit?: string | null; setTrackKeySelectedForEdit?: (key: string | null) => void; - clinicalTrackColorChanged?: boolean; - setClinicalTrackColorChanged?: (changed: boolean) => void; suppressRendering?: boolean; onSuppressRendering?: () => void; diff --git a/src/shared/components/oncoprint/OncoprintColorModal.tsx b/src/shared/components/oncoprint/OncoprintColorModal.tsx new file mode 100644 index 00000000000..4fcc84b4b92 --- /dev/null +++ b/src/shared/components/oncoprint/OncoprintColorModal.tsx @@ -0,0 +1,107 @@ +import { action, makeObservable, observable } from 'mobx'; +import { observer } from 'mobx-react'; +import * as React from 'react'; +import { ClinicalTrackSpec } from './Oncoprint'; +import { + getClinicalTrackColor, + getClinicalTrackValues, +} from './ResultsViewOncoprint'; +import { Modal } from 'react-bootstrap'; +import ClinicalTrackColorPicker from './ClinicalTrackColorPicker'; +import { RGBAColor } from 'oncoprintjs'; +import classnames from 'classnames'; +import _ from 'lodash'; +import { rgbaToHex } from 'shared/lib/Colors'; + +interface IOncoprintColorModalProps { + setTrackKeySelectedForEdit: (key: string | null) => void; + selectedClinicalTrack: ClinicalTrackSpec; + handleSelectedClinicalTrackColorChange: ( + value: string, + color: RGBAColor | undefined + ) => void; + getSelectedClinicalTrackDefaultColorForValue: (value: string) => number[]; +} + +export const OncoprintColorModal: React.FC = observer( + ({ + setTrackKeySelectedForEdit, + selectedClinicalTrack, + handleSelectedClinicalTrackColorChange, + getSelectedClinicalTrackDefaultColorForValue, + }: IOncoprintColorModalProps) => { + const clinicalTrackValues = getClinicalTrackValues( + selectedClinicalTrack + ); + + return ( + setTrackKeySelectedForEdit(null)}> + + + Color Configuration: {selectedClinicalTrack.label} + + + + + + + + + + + + {clinicalTrackValues.map(value => ( + + + + + ))} + +
ValueColor
{value} + +
+ +
+
+ ); + } +); diff --git a/src/shared/components/oncoprint/ResultsViewOncoprint.tsx b/src/shared/components/oncoprint/ResultsViewOncoprint.tsx index c31f1a9ff74..fde9efce8e2 100644 --- a/src/shared/components/oncoprint/ResultsViewOncoprint.tsx +++ b/src/shared/components/oncoprint/ResultsViewOncoprint.tsx @@ -96,6 +96,7 @@ import { Modal } from 'react-bootstrap'; import ClinicalTrackColorPicker from './ClinicalTrackColorPicker'; import { hexToRGBA, rgbaToHex } from 'shared/lib/Colors'; import classnames from 'classnames'; +import { OncoprintColorModal } from './OncoprintColorModal'; interface IResultsViewOncoprintProps { divId: string; @@ -104,6 +105,9 @@ interface IResultsViewOncoprintProps { addOnBecomeVisibleListener?: (callback: () => void) => void; } +const DEFAULT_UNKNOWN_COLOR = [255, 255, 255, 1]; +const DEFAULT_MIXED_COLOR = [48, 97, 194, 1]; + export enum SortByUrlParamValue { CASE_ID = 'case_id', CASE_LIST = 'case_list', @@ -147,7 +151,7 @@ export function getClinicalTrackValues(track: ClinicalTrackSpec): any[] { const values = _(track.data) .map(d => d.attr_val) .uniq() - .without(undefined) + .without(undefined, '') .value(); return values.sort((a: string, b: string) => a < b ? -1 : a > b ? 1 : 0 @@ -166,11 +170,11 @@ export function getClinicalTrackColor( return track.countsCategoryFills[valueIndex]; } else if (track.datatype === 'string' && track.category_to_color) { if (value === 'Mixed') { - return track.category_to_color[value] || [48, 97, 194, 1]; + return track.category_to_color[value] || DEFAULT_MIXED_COLOR; } return track.category_to_color[value]; } else { - return [255, 255, 255, 1] as RGBAColor; + return DEFAULT_UNKNOWN_COLOR as RGBAColor; } } @@ -1890,6 +1894,8 @@ export default class ResultsViewOncoprint extends React.Component< this.trackKeySelectedForEdit = key; } + // if trackKeySelectedForEdit is null ('Edit Colors' has not been selected in an individual track menu), + // selectedClinicalTrack will be undefined @computed get selectedClinicalTrack() { return _.find( this.clinicalTracks.result, @@ -1898,42 +1904,30 @@ export default class ResultsViewOncoprint extends React.Component< } @autobind - private getDefaultSelectedClinicalTrackColor(value: string) { + private getSelectedClinicalTrackDefaultColorForValue( + attributeValue: string + ) { if (!this.selectedClinicalTrack) { - return [255, 255, 255, 1]; + return DEFAULT_UNKNOWN_COLOR; } if (this.selectedClinicalTrack.datatype === 'counts') { return MUTATION_SPECTRUM_FILLS[ - _.indexOf(MUTATION_SPECTRUM_CATEGORIES, value) + _.indexOf(MUTATION_SPECTRUM_CATEGORIES, attributeValue) ]; } else if (this.selectedClinicalTrack.datatype === 'string') { // Mixed refers to when an event has multiple values (i.e. Sample Type for a patient event may have both Primary and Recurrence values) - if (value === 'Mixed') { - return [48, 97, 194, 1]; + if (attributeValue === 'Mixed') { + return DEFAULT_MIXED_COLOR; } else { return hexToRGBA( this.props.store.clinicalDataCache.get( this.props.store.clinicalAttributeIdToClinicalAttribute .result![this.selectedClinicalTrack!.attributeId] - ).result!.categoryToColor![value] + ).result!.categoryToColor![attributeValue] ); } } - return [255, 255, 255, 1]; - } - - @computed get selectedClinicalTrackValues() { - if (this.selectedClinicalTrack) { - return getClinicalTrackValues(this.selectedClinicalTrack); - } - return []; - } - - @observable clinicalTrackColorChanged: boolean = false; - - @action.bound - setClinicalTrackColorChanged(changed: boolean) { - this.clinicalTrackColorChanged = changed; + return DEFAULT_UNKNOWN_COLOR; } public render() { @@ -1974,97 +1968,18 @@ export default class ResultsViewOncoprint extends React.Component< {this.selectedClinicalTrack && ( - this.setTrackKeySelectedForEdit(null)} - > - - - Color Configuration:{' '} - {this.selectedClinicalTrack.label} - - - - - - - - - - - - {this.selectedClinicalTrackValues.map( - value => ( - - - - - ) - )} - -
ValueColor
{value} - -
- -
-
+ )}