lUW|R^
z6>K!{;b~=!NO_DCS)!uwh*zuE7H}x+>e<^ghSO;@i44%2{@|s-79Z?-m#JzHW}4T<
z>Q7-Y$WSJZ&H-cJfqR#-A_%#DUr29SaG&q|Ey3aeCFvaY0ouG?#x^m63=Y4EdCLt4
z-HkqxOSoO*E=%+&VOiko%E;fDHfYLQp=andmpFDj+=BdC40C3B2-_8FqWYGWqj_dy
z+NLr0a_uJ<@0361Z)n@^J;-_?r7Qh=nd**S|5$}2FG=*+g|KSQpQP&B>U2=pDXM4$
z5b^>(oQAx|-Y^199(PY^Aee(ZfwIGWObN(ez7gdIJB~Osi`WgHd@3@1mv>}O8VB{w
z_~t4B)XSYOfzsbQpOcu-pd@tPsE-BuS&1;gA_KecbzD=N?Py_81dtJR9>uuu2gFAR
z?a9KLzki1upZXw0?}dt0ljDwaz@Ngm6}bXS1^JZ1;(U5%j)booMa!T00Fl8#K(c6_9
zEXerX@L2doX~(3yp3_970cGokm{G6>y-<3mJQc}uqf2wf)w6LWz4HXQkeuyA@p*50
z1E3f+V8G>D+8CN#AF4S)>HaQXApAjY@s~HY-^+ELQX|1Nc1q8(Z+sH93%g3o)v`X5
z7Uud-?MG?-_G6s5mZ`_Z9A3A)%dU4qy_%C?n(d~LS3=G=9{l!T1@P)OMF-BVbwNOX
zqm0hv;apkS(UO6?LR;UWfqhTL$19%}V~jMsb>hz4
z$?gdM0%w4RG~+k6t&MClp%}hdL^SoeIa1s+ffY|cimXZ~2EOMJTh>*u$zVAF8;Fz&6l6#Q
zD%=%sVX)`#+&V@7?y338DqK@~W|iXE13I7>4zR(oN)ydX>zGChc3*%)s95E=JarA*
z@)KhHjhi{K^1=%qR7hUyd;ezdzWLZWg!+E!Ch)dOrL2T>sB~<|0nqJFxTH*bg|z5-
zp8-NWmmf1f^*#u!aqb^!hlvk4xN?(Vg0CvwQystz9pTYj`5b8Juw&@h>wk}hYk?GU
z`a<2hc|ud)*Ho&gp=gvEIt7w45n`ZKYMg!tV9FwrM2%jI#p-Z<4eN3^8J1!v2m^+T
zMq^w1(cpZ!I6mP@_}l#wp(i_ag@u292>jhMWPiE(*<2#jdNwUgOE0O|zbQqpaPWTSivWwPCOYlx(
zGM_?F;k3eHu7BaORgVFIAsRFy55!bxwJ`j9049I5^=`z`(Y7vut*-nvV7iB@VCL!%
ziQ636U9_!J9j3IlXZ8?+vH*Kj6acnMcO%Zf4N5^2KDoETQQWg*xhu!M6&@1#j|nnY
z6&|X}Wd1JaMYy~W$4hs7LL*%}9;RY(|HM&W|=XQO9&tBSv9T*BRc
zcomfs=hPN(W
zc8Gz(nqn(X-*TChLiZ}RDkH-~C%ELQvVHZlO@Q&ALZ_jExult>pd!QQDnR}e59)}h7w^XEOQb%k^{cj?QlF#G@qJnjgzzkzT`u^pf
zI!Cv<&X88JjDOi0*k6bx%7tc$JEjb@t=u4(IEY%7t?%D*tlR8VUV}OR9W2>nf3zb;
z@(D6as7o6$K1${XhB=IlM-rX+K_C@h*zoJxb{QrG|C7%~tRkH4xFA2fpJi9a<+X()
zJtSckotZgzL6Lw<)`D@MFrzU*a&$boRU+hLOd#8>UO%OvoFoZd-q2?Md!!%SaS=|O
zyLL8uE~OBt&HZG|yj?4LBD&nH?-@6(7%Y4zZQHW1)VOADYsk9}JI64kI3yfVHS6&e
zCBHH94AR=#Xcef15puyuhY>i?Q+Yq|xzVg_yfW^exMgKi&{Ju5ovcs92o09?TT;B%
z)>q3Cj>c~~f9vaUrf*2@n-5`1rHipERAY;6P>rb1elHK$tifsOvapUh>{=+;%(kn!
zq{BPWJHBgP>G4-%XT+f?p=ubU%($QS+P>D_YOqm{B3`uIILS-qMT#`55?o(RzMwe;
zM^i+n)ODT$Ea=2oDi--=+2~ySAqyK$_ek`(U{m9f|Ci1Db5ln;-s@XTtM-CwZ;awR
zfC9h^UG~Q8t`jLP%;f4EV2p-6|8})Z-lJ6zx
z(0213>85`pcufKgE}&8KcWn{2d=;`2%v2??o_KnH^kb$7D%;X9*OrHlj1NcXHr
z^)&PS6%tJ5k;PN6xS-PIT~DL*uptPg*X};lKCMHZL9GaX$(Ec@SG=Yok-ES@pnkXs
z6N$f09Kpo~Ecp3lQs&b~bPJZ%+5@D~6-WWzjycGF*)LUJp~0vuAHk1%|BzF<|7i#g
z7Vzh<65>qJeE<|@qMOa11)^zVyW^f&*ezb_p#MJ^5vJHAt0uu`{>vboXQid)-oO|y
z1Nu?h{Ka-*vQFq9HD2h!!+7MykQ*n#dGmjIlnG%c5W*?m1gTCe)1s!X9WcPHDpnbe
z&g@sUzF_h13e3Xw$%z)S29%ny8D@5hg8X$TXPi<1?pm4hGgFAhyQ1hZOGCE7Nzt@H1~_mP`HH5zI6rB=48J6bD5G_ENsfX@n*}VJZeS|
zXU6Phnli&Og3VL-hJw=c|-d|MhQcwh;lZ*Lvn9yGRb_
z_HqvozDqKFxP^K0z2zd*k5#qmIxmOsMm%N{VoV)^88>Z()OwNu#}PHce2>NvE*N#xL04-ffY`|wnTJc
zTRk!(61PCNJB7O4g>{})4R*~~Zsf#oHpu5^7FrVGq|1H@lYCaQj1o*lhL9ZBvzMX&
z<=LrJR92$wvD(1rtTugOPdK&gJ)9^%Dy;_N8@ni*Cz74oJt7N4kf`k6?y$^q595`R
z(l|n!I2KHkcvepcXm%AYa$#gmanj*CN-cL;ns
zQvMZ2lMV0F0t;S^l1(NV_*l*B2jl;|P3z;&?4Z@Q6FUN`+v6ke(GvL>p+j%n>v4Tn
z61A+Odt0Ijm6^Vkw$eZ%+3;O7&Shi1S|*l4cXFx9{9&uq2d=V)*rp8R+zrQlsUFy*
z{O5KilF8@jBf>+9$%67kQU^e?^EB4BBWA%{q;0DFIMuMb%TIhTW1}{fnM)h8@aZK)!Rh#GrG0ITi{0=d}E2h#%Ifi^vU&oLz=h#AO36
zB4pIf`XXS7)Jv5&3-39fl$R~5(n)eWbb9<~R!nfqJeZADZ5c=R#_(4EM(P(?aBav!
z9ZrX*H5EPf7{cxXz8PCqw`zWm;Z6jv5M<{dD1V0cof!0_Y_Yl;&Kr0%N5mT3#E%={%I=~tGJdNn9+83M8dIu-MMy2KD`SQ|vV-2eWzwe;w!^P!9v)X$_
zw@;HN%GQ1rx^HR)+gvqjSkv@VOI#=8UZ7|bP!MRp1E+amtaouGuVn@&|aPZ
zw1M?Kwrp?SZtnAP-L}kT8&}vh@iW{snXAB&E1nqnTn^M(JKHN_7e>OKjRF9P7-@e)
z$UB61^h@%i%nnz33=EOPE@Dfw5MQ6}6C13`7b`r}x5Cj;%7k;h-`S{p#LlHcd<@B6
z6x^@Txk+mrR#2R$6CY6={u5x&UYN^_{m*2wsHkAfCMXV5s4JcxaIcdET&jMM+DP4ey(XfUTRcY*t0LNr
z!#oNf#h-MKMByLV0d~bXYxg~`4u4?B1HXfFsa_V}7kGc`S66b=S3ldZ^i=!~x_00|
zpfkH_orS$^ljG#es#JGkhTsX1xww_ot(x5KpbaQx3>r-Bb1c2Bn7`w|i$G^8`eX$x
z?3V5%)J)-a2&(xrE85xLaEqpH4=)cqnpKtT
zVkLcAxz&IL^N|7irI&fY$?;J=9^~@AsQ81=XoQe|=8;z;7=R57oKH(Wii8BgW9Jp#
z2lFm1M%#%PT9T}am%WC{49zE69FEY7FL?#aEckgHe^Q)91p^Q-Er;g469H0#>lx{+
zeA(6_54%ak&5E0j`&;Y!U3eF>@Hr~B&(`mwM!F;ANr^^20S)7N;n!;cYVoZrnTLmQ
zf8Q#gXO5w|on*QsTeV+X;x@;s!tGw`%wK!az4dw6V4eDLRpWpkF$8eYc+=xa_T*JR
zRItF}F$NVpTPJF)Cd2X4Q_NwDcCZh2>s^qrjCO8m8&xRXf{gyi$Sw7YuC%j_str{v
z>j*Gds9zQDJU4)~Y@00vD68ILi5OYqO`N88SaY8DN|jKY@ylTG0A_i^{i+czaz2bsYIwSCwQHf8
zw!(EMWVdA7!$88@(_*q6C5OfxBJdUQF7{ptljMMCT3J6raJE~GLeUeewQxm8(W+Yy
zeB^$Pf$vWG@S#?)i(^Jqlpl81#KGPbqFCB#MsOA%{1qU?M{|f>+%xZ*6;9V0Z}XWH
zD9S16OTb#K!AMdg+j0i%{kJy7Rh$;bLqwXZX;ikup#l91vlm;Hbwl!r(uY!XpXVDTZPEm4~u@CE;@fLaf_nnn`P7!)Fi@S^u?VN
z+T&9-Ms9rH5-{phl~Q;I{-5G@_%m?1+d-4J2_!l)zVSI0MEyePEEp?_`U&6Ux03!y<W$)1ELc38A{%WBK~0RW~$uDpd<@h
z{{X=)VS7uyCdJM;k7<6|;9
zVq7h;Z)CES6GVZJY1J1QjY(@!xqv|}jxI`%FCqt_SA(!B8>=xDjyg8#d8HRwe>2wR
zoxE`q@z}A^4N|U({WBEFw3AxXT*H&oxH8#EYv;p~FhJASxaC!-)Nk=!+$NB6wEs<{
zEXlCg31yh1CB!5bIk?iW{7`G^e5rUF-buik(QN&H?Ch~sM=3>%LUEKd^J8?wsLHM%OLq!Pn=hf}A!tG!BZMr!rram-*(@
zzyN<9=5F^MNvfa}o#TcNv^Rz0ak`QOpfmq>EG!@4u#Tbnoz6A5*~t;c4)iA|-*?uS
z5yNMKCrZ22&8J|;4+FsrPK^K-%3ZyVGhlg#riVaGr7AL{h^6f>!Ky|WV@3PlhY!UYz3yfpeb`IIIx;DA11h5mkev+lqZN4+
zLjRZHd76X%Pn4+Yx4TR8qomlo2hRN1bXmTBwjQfD5&rP?VeEB6j(uTx}-imbFU7K
z5w~@)JDq8k2?O|~G9kHyoFnA{hfQ+!Ek|){B`nArZgm8iXQPerYFog7;oRuD30bke
zc@Z!tSAd%UdIZRmbn|1Nzp61Td}^jht>*X;+F$U-kAvMu4A7HyZ@@t_cBm%D#qCP=
zRrZAe=YRA-{9?OQF9J7*iBCJ>siy-w=T6zIWCn&>;t>%kE(WX&xjc&sshzIGRO
zOm`$Eu5s^rimdN%k1;szBHLCp{uvGR!zssf_@38|vUKAKMXaI*{L33wI7f6bc`m!$
z<9^^#)-PZ-d1^5W_)dKJrMo1D3B#S5KFxtZlEWX%-py0U0}9wY_#|VoTOVAqnVj|%
zSp?iiXbCXS-`^yI{qxU+g1G_NN3`{Mt-q&FP7<B&8K3r;e;B`8YCIC#f2eBT?8qnsA2o|vhA{c
z5M3y7cUZCHnwKsxw0w8M*pm|Ohjtt6e_O*Bo5c4Bcs7vPdq66IK=EWm@?Vxu&LM9o
z-n3-kK{DU7s~>I5y&1LPIl;OA(DZgCVCC??uII)@VU|ZTG0QbTrStkc-z`Y;1^uLd
z4}Uuep6_T2it|1xw7nIgTLE~Q;d#ZKeaoazaZh2ud}Z9CZ-qUZY>ly3&imgs1gsH=6We|zUJkVl%CSQH1&F;@)Eq$Bsh1C{W5H{xcIW^MQag_3-<>e!>1oj
zs_8XELFR36iJ50e6pPkYTr#W0dej>_P|L!IvHw%L4lr}i*lnPLG{RyRMQazu=(>Jl
z-!Y4<%srLrxky_&t4}+2VGAzlZz47Yc|X`9Gq$e6tS`XWbIJ_70t`3&|L#PC*@j@r
zK@Z)msowJfnULg{UWOIN@b6E|TU#}|I{0>Zj~b4DoB>(6Dhu*R!Ll=fUk*5c7}c6>
zj+jA315Xz$f*w2p=HY(4EG7WkN?6V`dxRFoa6ovX+@O1S}(m1h{Yg!@GF
zchb9^T%(I!D+=(TgG$H4n%cLj(6PT$^F_VZn{x@^>mddMCpRAf0+eVMi-6eyT!V%$
zXu^-ROS?$ezz-BY+qc#oIMS*gC?CHSK=p+89jNcq9IDKxsEeRmb@5@*W||r7A23mQ
zykU>FQ26EEeqf4qSPdIKhIjmK6OTWL7g97>a92MFoCH3mXw|e3)`cj{1nUF88Q4_n
z-&nk;m0jex_9CWGAq))yhaE^_**<^czN>xN~trx5JDfO7JPr8}6j
zS#-(%JoU)(SJY*9VARwq8#t<^4M;q%$3*3mz19_sJ+Uo@nE7Kq4YzkQA>=NW+dDIB
zeTI_*+&X$Khsu-$wodfzZYLLaOZV$Zn<&-%9aAL;yVZ?rvg}2TAAjLi{p|!1LcB+%
z`5_}NFM!pszv81=&t*oeZY7+;hn!9gxR+uo8D>tyJ>&{))(gmMEkL{Jocd}PaiCy5
zz1NgM9Z&feaETY0>U~nT$`I^;o`3V!3>A!``~Hk`7VJ!OC7>9QF9bpe{C!~TscU0s
z6R53+4WCOXE*A}CT0jT)BId`N3uXp}&jaztg(D7dmeu(oGY^4X`)~hh?nv!|7b_x!
z9(wb#0Da!j=4WiNh!no-vM%MZ4XEG`?oXqb-EE`jVI8ce!Wq7MDx4GZf!S`P4O@zM
zFGJp9l5*C4g?Ax31AS<1(@$5&o9|s$;aCtS3euZgO{7!Z{2(}-*_qWv+_?Ug
zt&2-g;ojDlO+h_P-ih{cKar&}=s^J+6}X|l3)p67AeZL#88aT30qm-Ur~%cHOAh2&
zLLP4Ac4CCbKc?(USS|*_VG%rid`O|whX#S514+B&sj@&u5J25S9h<>@-V|D;-i1!h
zji03ECuC43R*xIi8G9M{zEdj2O3wY9*i1mJ!BRWJ_X0|X#uqB0vu!3^)swq7yuaIU
z6+MCSp!g#4kvW^TjKpDX(Tm)o*4AJ1v8@d7=B|?lTk7eVpw&kyZ~6;ruqS2B>jBNc
zKU#A10O@2k3oQA;G7A5&Fk=D1=qX`SBcEXv117tX9pTG>-0sc`fD5e{xv^BQUJe)U
zNvjF;S{A#k#C^E?asXn~uCd2O`~!!n8+rOx=cv4F4DqhssX~B4p94+((qJ@4EpG(#
zQ$V4C4IjXGtJSCD%DD;~XkaQ;JBh2kH?1ptEPGDYHPPrmqrP7hjn*rjl#+Dg
zq&up5UGMX3cu5aCtK-@==sTnhj#s3T_%Qhv>IXGLvH@rRJIG=~KUwh~WFY-g^@-s?
z!N&h<@7lkby0UOABdvhbR!U`Ri4YTvJOapoDAZVxDIpr6ke6c2j6oDAK_g&5A}Cmi
z1Cg61)FpzjkXGIXD9?gW5lE0sp_E{NX$_E}geVeTCV@Etf&KygrEBs1khPMm>~rqf
zdw*x2lXJg&Fek1e5qv|p2V@;7d`JQhm<@(IOI^ivPEMBCJ(Om2^nb6juf>F+c%j#H
zc?8B5D{6mm&O0tMjZXFuQQ{{L;=VWk$eW0Zp3{-zOp9yP0BMl$P*?%k7J}B8b
zx+`$GO!@ZWs9rla0XKqOVNs*KCU>QhcXdRC+2HB5M~3!68di6`GMskbnrYU(+wRx_
zN`EX0KRl07E{55aN4|f~t#icHqGr<1vp3v6!#xKq=>T3%J5Os_efV(0n_J$Yo9Qf_
zMX4?7vVe6;kVd+;(0-f{e2ZJ`Rz)P*IOQD*AExDxzYZlZ(yXWo?o8qSxdnLp4_ybh
zLyN7flLjNO{|xqar5^^f8tmR?Ku>oRPK@LUC&s1;a#5Ct^HW_DTGs8MY)XA4f6wFX
z@SmgpnK#yHTzOU7o-?uY+uEZ)+*eV6;N@TdCt_0
zEVp8KNooPpmdq=&pxJhu*++>h>f;&%85lbizMfynAzpZV#J%Achha53@=^;C@l5uF
z5*tEbwEx-#Ym~_oD*OR)
z9oA`69>Mg65iVh>ndw(gei)hK4q1#@@Q#J1Z3%;h@vm{h?C8%r=kGs19gW7fcGzw`
z;`KyOT2}Fmt{*AgnSe1=S}QPvg9DsukDd$8G|?{$USR``hxDeUBs%(pGM_I0{JdLo
z{;1CfD6iT?xOkg+RB+^O{pEj%6mi?i<9v!IZHoY(E;#-@x|qYtEgo~88+_t6z&gm+R2DF+E{D9@W(I6^A@811
z%Y$MP43a7}W}P~V|B~IN&`xol6qR`cMgc
z+bcC$pvQsc4Zi36;=cDRsv>O!C8Uqc0ZFs^bBUcc1Vc^+xsh1ACk
zoYxxhMwiLT&{R?>&@8Cfu^9Z}%cWvy(bQtMF~exmlqydFhnT+eK|gEaXE6dun-aAB26E`;lHR{Z!cAO{CQqQ-0_$d?tlDaP7Etef>{l51}kilz=->Amii96
zB6^0kWhBWh@pV0sKCCzD6Ii)EY6i?k4}et5UJgHsSMO5Z=h;4Y#hPdp8*yt
z;u^Ehpm32f~X7w-2{a#BM7Umcfa7b=k6k@Gvm`-;{H80=J7Z2b6-9I&P4lb*}s
zZk8j=Pi~%U`_X^Rd8D<0uYi_IN8ptt!J2bQK*p?qj#_h|J#{t9DQkXuv|$pqF_y56
iAlk?tz!!gF+9-zI+uIssFTpQYhd5ut;WBUf)&B#%*~eP|
literal 0
HcmV?d00001
From 8f83dcbe029f554f78fb3dc1f57f9c6c6cc1d081 Mon Sep 17 00:00:00 2001
From: Luka van der Plas
Date: Thu, 21 Aug 2025 17:16:01 +0200
Subject: [PATCH 04/21] set barchart theme
---
frontend/src/app/services/theme.service.ts | 2 +-
.../src/app/visualization/barchart/barchart.directive.ts | 2 ++
frontend/src/app/visualization/chartjs-utils.ts | 9 +++++++++
3 files changed, 12 insertions(+), 1 deletion(-)
create mode 100644 frontend/src/app/visualization/chartjs-utils.ts
diff --git a/frontend/src/app/services/theme.service.ts b/frontend/src/app/services/theme.service.ts
index 1822a7ec9..9d5fbe802 100644
--- a/frontend/src/app/services/theme.service.ts
+++ b/frontend/src/app/services/theme.service.ts
@@ -1,7 +1,7 @@
import { Injectable } from '@angular/core';
import { BehaviorSubject, combineLatest, fromEvent, map, Observable, startWith } from 'rxjs';
-enum Theme {
+export enum Theme {
DARK = 'dark',
LIGHT = 'light',
}
diff --git a/frontend/src/app/visualization/barchart/barchart.directive.ts b/frontend/src/app/visualization/barchart/barchart.directive.ts
index fe23a8ea0..9a0419d72 100644
--- a/frontend/src/app/visualization/barchart/barchart.directive.ts
+++ b/frontend/src/app/visualization/barchart/barchart.directive.ts
@@ -30,6 +30,7 @@ import { takeUntil } from 'rxjs/operators';
import { DateHistogramResult, TermsResult } from '@models/aggregation';
import { ComparedQueries } from '@models/compared-queries';
import { RouterStoreService } from '@app/store/router-store.service';
+import { setThemefaults } from '../chartjs-utils';
const hintSeenSessionStorageKey = 'hasSeenTimelineZoomingHint';
const hintHidingMinDelay = 500; // milliseconds
@@ -170,6 +171,7 @@ export abstract class BarchartDirective<
chartDefault.elements.bar.hoverBackgroundColor = selectColor();
chartDefault.plugins.tooltip.displayColors = false;
chartDefault.plugins.tooltip.intersect = false;
+ setThemefaults(chartDefault);
this.comparedQueries = new ComparedQueries(this.routerStoreService);
this.comparedQueries.allQueries$.subscribe(this.updateQueries.bind(this));
}
diff --git a/frontend/src/app/visualization/chartjs-utils.ts b/frontend/src/app/visualization/chartjs-utils.ts
new file mode 100644
index 000000000..8f6dd2992
--- /dev/null
+++ b/frontend/src/app/visualization/chartjs-utils.ts
@@ -0,0 +1,9 @@
+
+import { Defaults } from 'chart.js';
+
+export const setThemefaults = (defaults: Defaults) => {
+ const style = window.getComputedStyle(document.body);
+
+ defaults.color = style.getPropertyValue('--bulma-text-strong');
+ defaults.borderColor = style.getPropertyValue('--bulma-border');
+}
From d42985d958d9e58e36d2e34394cd780d1e93edca Mon Sep 17 00:00:00 2001
From: Luka van der Plas
Date: Wed, 27 Aug 2025 16:17:43 +0200
Subject: [PATCH 05/21] set defaults in ngram chart
---
.../src/app/visualization/ngram/joyplot/joyplot.component.ts | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/frontend/src/app/visualization/ngram/joyplot/joyplot.component.ts b/frontend/src/app/visualization/ngram/joyplot/joyplot.component.ts
index 1e1dc7979..3bae60c23 100644
--- a/frontend/src/app/visualization/ngram/joyplot/joyplot.component.ts
+++ b/frontend/src/app/visualization/ngram/joyplot/joyplot.component.ts
@@ -3,6 +3,7 @@ import { Chart, ChartData, ChartOptions } from 'chart.js';
import * as _ from 'lodash';
import { NgramResults } from '@models';
import { selectColor } from '@utils/select-color';
+import { setThemefaults } from 'app/visualization/chartjs-utils';
@Component({
selector: 'ia-joyplot',
@@ -28,7 +29,9 @@ export class JoyplotComponent implements OnChanges {
chartOptions: ChartOptions;
chart: Chart;
- constructor() { }
+ constructor() {
+ setThemefaults(Chart.defaults);
+ }
get multipleTimeIntervals(): boolean {
return this.timeLabels && this.timeLabels.length > 1;
From a34dc4c8e862401cf0731cfb3dda51438464d371 Mon Sep 17 00:00:00 2001
From: Luka van der Plas
Date: Wed, 27 Aug 2025 16:27:51 +0200
Subject: [PATCH 06/21] refresh similarity chart on theme change
---
frontend/src/app/services/theme.service.ts | 3 +++
.../similarity-chart.component.ts | 16 +++++++++++++---
2 files changed, 16 insertions(+), 3 deletions(-)
diff --git a/frontend/src/app/services/theme.service.ts b/frontend/src/app/services/theme.service.ts
index 9d5fbe802..637be1d68 100644
--- a/frontend/src/app/services/theme.service.ts
+++ b/frontend/src/app/services/theme.service.ts
@@ -1,5 +1,7 @@
import { Injectable } from '@angular/core';
import { BehaviorSubject, combineLatest, fromEvent, map, Observable, startWith } from 'rxjs';
+import { setThemefaults } from 'app/visualization/chartjs-utils';
+import { Chart } from 'chart.js';
export enum Theme {
DARK = 'dark',
@@ -31,5 +33,6 @@ export class ThemeService {
setTheme(theme: Theme) {
const root = (document.getRootNode() as Document).documentElement;
root.setAttribute('data-theme', theme);
+ setThemefaults(Chart.defaults);
}
}
diff --git a/frontend/src/app/word-models/similarity-chart/similarity-chart.component.ts b/frontend/src/app/word-models/similarity-chart/similarity-chart.component.ts
index a3ea815ab..eb01edd11 100644
--- a/frontend/src/app/word-models/similarity-chart/similarity-chart.component.ts
+++ b/frontend/src/app/word-models/similarity-chart/similarity-chart.component.ts
@@ -2,9 +2,11 @@ import { Component, Input, OnChanges, OnDestroy, OnInit, SimpleChanges } from '@
import { Chart, ChartData, ChartOptions, ChartType, Filler, TooltipItem } from 'chart.js';
import Zoom from 'chartjs-plugin-zoom';
import * as _ from 'lodash';
-import { BehaviorSubject } from 'rxjs';
+import { BehaviorSubject, combineLatest, withLatestFrom } from 'rxjs';
import { selectColor } from '@utils/select-color';
import { FreqTableHeaders, WordSimilarity } from '@models';
+import { ThemeService } from '@services/theme.service';
+import { setThemefaults } from 'app/visualization/chartjs-utils';
/**
* Child component of the related words and compare similarity graphs.
@@ -39,10 +41,18 @@ export class SimilarityChartComponent implements OnInit, OnChanges, OnDestroy {
currentTimeIndex = undefined;
- constructor() {}
+ constructor(
+ private themeService: ThemeService,
+ ) {}
ngOnInit(): void {
this.graphStyle.subscribe(this.updateChart.bind(this));
+ this.themeService.theme$.pipe(
+ withLatestFrom(this.graphStyle),
+ ).subscribe(([theme, style]) => {
+ setThemefaults(Chart.defaults);
+ this.updateChart(style);
+ });
}
ngOnDestroy(): void {
@@ -202,7 +212,7 @@ export class SimilarityChartComponent implements OnInit, OnChanges, OnDestroy {
const options: ChartOptions = {
elements: {
line: {
- tension: 0, // disables bezier curves
+ tension: 0, // dthis.chart.update();isables bezier curves
},
point: {
radius: 0, // hide points
From 709d7ec40d319ddf9b40d2ff51e3390e7de5e081 Mon Sep 17 00:00:00 2001
From: Luka van der Plas
Date: Wed, 27 Aug 2025 17:21:36 +0200
Subject: [PATCH 07/21] set all chart.js themes from theme service
---
frontend/src/app/services/theme.service.ts | 16 ++++++++++++++--
.../visualization/barchart/barchart.directive.ts | 2 --
frontend/src/app/visualization/chartjs-utils.ts | 9 ---------
.../ngram/joyplot/joyplot.component.ts | 5 +----
.../similarity-chart.component.ts | 14 ++------------
5 files changed, 17 insertions(+), 29 deletions(-)
delete mode 100644 frontend/src/app/visualization/chartjs-utils.ts
diff --git a/frontend/src/app/services/theme.service.ts b/frontend/src/app/services/theme.service.ts
index 637be1d68..ad5ff48a3 100644
--- a/frontend/src/app/services/theme.service.ts
+++ b/frontend/src/app/services/theme.service.ts
@@ -1,7 +1,7 @@
import { Injectable } from '@angular/core';
import { BehaviorSubject, combineLatest, fromEvent, map, Observable, startWith } from 'rxjs';
-import { setThemefaults } from 'app/visualization/chartjs-utils';
import { Chart } from 'chart.js';
+import _ from 'lodash';
export enum Theme {
DARK = 'dark',
@@ -33,6 +33,18 @@ export class ThemeService {
setTheme(theme: Theme) {
const root = (document.getRootNode() as Document).documentElement;
root.setAttribute('data-theme', theme);
- setThemefaults(Chart.defaults);
+ this.setChartJSTheme();
+ }
+
+ setChartJSTheme() {
+ const style = window.getComputedStyle(document.body);
+ Chart.defaults.color = () => style.getPropertyValue('--bulma-text-strong');
+ Chart.defaults.borderColor = () => style.getPropertyValue('--bulma-border');
+
+ const active = _.values(Chart.instances);
+ for (let chart of active) {
+ chart.update();
+ }
+
}
}
diff --git a/frontend/src/app/visualization/barchart/barchart.directive.ts b/frontend/src/app/visualization/barchart/barchart.directive.ts
index 9a0419d72..fe23a8ea0 100644
--- a/frontend/src/app/visualization/barchart/barchart.directive.ts
+++ b/frontend/src/app/visualization/barchart/barchart.directive.ts
@@ -30,7 +30,6 @@ import { takeUntil } from 'rxjs/operators';
import { DateHistogramResult, TermsResult } from '@models/aggregation';
import { ComparedQueries } from '@models/compared-queries';
import { RouterStoreService } from '@app/store/router-store.service';
-import { setThemefaults } from '../chartjs-utils';
const hintSeenSessionStorageKey = 'hasSeenTimelineZoomingHint';
const hintHidingMinDelay = 500; // milliseconds
@@ -171,7 +170,6 @@ export abstract class BarchartDirective<
chartDefault.elements.bar.hoverBackgroundColor = selectColor();
chartDefault.plugins.tooltip.displayColors = false;
chartDefault.plugins.tooltip.intersect = false;
- setThemefaults(chartDefault);
this.comparedQueries = new ComparedQueries(this.routerStoreService);
this.comparedQueries.allQueries$.subscribe(this.updateQueries.bind(this));
}
diff --git a/frontend/src/app/visualization/chartjs-utils.ts b/frontend/src/app/visualization/chartjs-utils.ts
deleted file mode 100644
index 8f6dd2992..000000000
--- a/frontend/src/app/visualization/chartjs-utils.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-
-import { Defaults } from 'chart.js';
-
-export const setThemefaults = (defaults: Defaults) => {
- const style = window.getComputedStyle(document.body);
-
- defaults.color = style.getPropertyValue('--bulma-text-strong');
- defaults.borderColor = style.getPropertyValue('--bulma-border');
-}
diff --git a/frontend/src/app/visualization/ngram/joyplot/joyplot.component.ts b/frontend/src/app/visualization/ngram/joyplot/joyplot.component.ts
index 3bae60c23..d9bbf419c 100644
--- a/frontend/src/app/visualization/ngram/joyplot/joyplot.component.ts
+++ b/frontend/src/app/visualization/ngram/joyplot/joyplot.component.ts
@@ -3,7 +3,6 @@ import { Chart, ChartData, ChartOptions } from 'chart.js';
import * as _ from 'lodash';
import { NgramResults } from '@models';
import { selectColor } from '@utils/select-color';
-import { setThemefaults } from 'app/visualization/chartjs-utils';
@Component({
selector: 'ia-joyplot',
@@ -29,9 +28,7 @@ export class JoyplotComponent implements OnChanges {
chartOptions: ChartOptions;
chart: Chart;
- constructor() {
- setThemefaults(Chart.defaults);
- }
+ constructor() {}
get multipleTimeIntervals(): boolean {
return this.timeLabels && this.timeLabels.length > 1;
diff --git a/frontend/src/app/word-models/similarity-chart/similarity-chart.component.ts b/frontend/src/app/word-models/similarity-chart/similarity-chart.component.ts
index eb01edd11..1514ae62b 100644
--- a/frontend/src/app/word-models/similarity-chart/similarity-chart.component.ts
+++ b/frontend/src/app/word-models/similarity-chart/similarity-chart.component.ts
@@ -2,11 +2,9 @@ import { Component, Input, OnChanges, OnDestroy, OnInit, SimpleChanges } from '@
import { Chart, ChartData, ChartOptions, ChartType, Filler, TooltipItem } from 'chart.js';
import Zoom from 'chartjs-plugin-zoom';
import * as _ from 'lodash';
-import { BehaviorSubject, combineLatest, withLatestFrom } from 'rxjs';
+import { BehaviorSubject } from 'rxjs';
import { selectColor } from '@utils/select-color';
import { FreqTableHeaders, WordSimilarity } from '@models';
-import { ThemeService } from '@services/theme.service';
-import { setThemefaults } from 'app/visualization/chartjs-utils';
/**
* Child component of the related words and compare similarity graphs.
@@ -41,18 +39,10 @@ export class SimilarityChartComponent implements OnInit, OnChanges, OnDestroy {
currentTimeIndex = undefined;
- constructor(
- private themeService: ThemeService,
- ) {}
+ constructor() {}
ngOnInit(): void {
this.graphStyle.subscribe(this.updateChart.bind(this));
- this.themeService.theme$.pipe(
- withLatestFrom(this.graphStyle),
- ).subscribe(([theme, style]) => {
- setThemefaults(Chart.defaults);
- this.updateChart(style);
- });
}
ngOnDestroy(): void {
From 46f6dd835a33b6474e3536c5c4f14fbb21464e06 Mon Sep 17 00:00:00 2001
From: Luka van der Plas
Date: Wed, 27 Aug 2025 18:16:50 +0200
Subject: [PATCH 08/21] adjust neighbor_network to theme
---
backend/wordmodels/neighbor_network.py | 18 +++++++++-------
.../neighbor-network.component.html | 2 ++
.../neighbor-network.component.ts | 21 ++++++++++++++++---
3 files changed, 31 insertions(+), 10 deletions(-)
diff --git a/backend/wordmodels/neighbor_network.py b/backend/wordmodels/neighbor_network.py
index 7ccbfae19..1f58d4cb3 100644
--- a/backend/wordmodels/neighbor_network.py
+++ b/backend/wordmodels/neighbor_network.py
@@ -95,7 +95,6 @@ def _graph_vega_doc(timeframes, nodes, links):
"height": 500,
"padding": 0,
"autosize": "none",
-
"signals": [
{ "name": "cx", "update": "width / 2" },
{ "name": "cy", "update": "height / 2" },
@@ -154,7 +153,14 @@ def _graph_vega_doc(timeframes, nodes, links):
"on": [
{"events": {"signal": "fix"}, "update": "fix && fix.length"}
]
- }
+ },
+ {
+ 'name': 'theme',
+ 'description': 'Current site theme (light/dark)',
+ 'bind': {
+ 'element': '#current-theme',
+ }
+ },
],
"data": [
@@ -181,13 +187,13 @@ def _graph_vega_doc(timeframes, nodes, links):
]
}
],
-
"scales": [
{
'name': 'link-color',
'type': 'linear',
'domain': {"data": "link-data", "field": "value"},
'range': {'scheme': 'greys'},
+ 'reverse': { 'signal': 'theme === "dark"' },
},
{
'name': 'text-weight',
@@ -196,13 +202,11 @@ def _graph_vega_doc(timeframes, nodes, links):
'range': ['bold', 'normal'],
}
],
-
"marks": [
{
"name": "nodes",
"type": "text",
"zindex": 1,
-
"from": {"data": "node-data"},
"on": [
{
@@ -218,7 +222,6 @@ def _graph_vega_doc(timeframes, nodes, links):
"encode": {
"enter": {
"fontSize": {"value": 15},
- "fill": {"value": "black"},
"text": {"field": "term"},
"baseline": {"value": "middle"},
"align": {"value": "center"},
@@ -227,7 +230,8 @@ def _graph_vega_doc(timeframes, nodes, links):
},
},
"update": {
- "cursor": {"value": "pointer"}
+ "cursor": {"value": "pointer"},
+ "fill": {'signal': 'theme === "dark" ? "white" : "black"'},
},
},
"transform": [
diff --git a/frontend/src/app/word-models/neighbor-network/neighbor-network.component.html b/frontend/src/app/word-models/neighbor-network/neighbor-network.component.html
index dd2e7e55e..751470c44 100644
--- a/frontend/src/app/word-models/neighbor-network/neighbor-network.component.html
+++ b/frontend/src/app/word-models/neighbor-network/neighbor-network.component.html
@@ -1,6 +1,8 @@
diff --git a/frontend/src/app/word-models/neighbor-network/neighbor-network.component.ts b/frontend/src/app/word-models/neighbor-network/neighbor-network.component.ts
index 7aa5ae8c0..809e536d2 100644
--- a/frontend/src/app/word-models/neighbor-network/neighbor-network.component.ts
+++ b/frontend/src/app/word-models/neighbor-network/neighbor-network.component.ts
@@ -1,6 +1,7 @@
-import { Component, ElementRef, Input, OnChanges, SimpleChanges, ViewChild } from '@angular/core';
+import { AfterViewInit, Component, ElementRef, Input, OnChanges, SimpleChanges, ViewChild } from '@angular/core';
import { Corpus, FreqTableHeaders } from '@models';
import { WordmodelsService } from '@services';
+import { Theme, ThemeService } from '@services/theme.service';
import { BehaviorSubject } from 'rxjs';
import embed from 'vega-embed';
@@ -10,12 +11,13 @@ import embed from 'vega-embed';
styleUrl: './neighbor-network.component.scss',
standalone: false,
})
-export class NeighborNetworkComponent implements OnChanges {
+export class NeighborNetworkComponent implements OnChanges, AfterViewInit {
@Input({required: true}) corpus!: Corpus;
@Input({required: true}) queryText!: string;
@Input() asTable: boolean;
@ViewChild('chart') chart!: ElementRef;
+ @ViewChild('theme') themeInput!: ElementRef;
data: any;
@@ -36,12 +38,19 @@ export class NeighborNetworkComponent implements OnChanges {
key: 'similarity',
label: 'Similarity'
}
- ]
+ ];
constructor(
private wordModelsService: WordmodelsService,
+ private themeService: ThemeService,
) { }
+ ngAfterViewInit() {
+ this.themeService.theme$.subscribe(
+ theme => this.updateTheme(theme)
+ );
+ }
+
ngOnChanges(changes: SimpleChanges): void {
this.getData()
}
@@ -73,4 +82,10 @@ export class NeighborNetworkComponent implements OnChanges {
console.error(error);
});
}
+
+ updateTheme(theme: Theme) {
+ const el = this.themeInput.nativeElement as HTMLInputElement;
+ el.value = theme;
+ el.dispatchEvent(new Event('input'));
+ }
}
From db94b14cabcbc7ebfc60013cd346c30f276d4a34 Mon Sep 17 00:00:00 2001
From: Luka van der Plas
Date: Thu, 28 Aug 2025 11:20:53 +0200
Subject: [PATCH 09/21] outfactor theme indicator element to directive
---
.../theme-indicator.directive.ts | 37 +++++++++++++++++++
.../app/visualization/visualization.module.ts | 3 ++
.../neighbor-network.component.html | 2 +-
.../neighbor-network.component.ts | 16 +-------
4 files changed, 42 insertions(+), 16 deletions(-)
create mode 100644 frontend/src/app/visualization/theme-indicator.directive.ts
diff --git a/frontend/src/app/visualization/theme-indicator.directive.ts b/frontend/src/app/visualization/theme-indicator.directive.ts
new file mode 100644
index 000000000..45342a26f
--- /dev/null
+++ b/frontend/src/app/visualization/theme-indicator.directive.ts
@@ -0,0 +1,37 @@
+import { DestroyRef, Directive, ElementRef, OnInit } from '@angular/core';
+import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
+import { Theme, ThemeService } from '@services/theme.service';
+
+
+/**
+ * Lets the host element function as an indicator of the site theme (light/dark). The
+ * element will set the theme as its value and fire an input event when the theme
+ * changes.
+ *
+ * Can be used to let Vega visualisations observe the site theme. The element will be
+ * hidden from the user.
+ */
+@Directive({
+ selector: 'input[iaThemeIndicator]',
+ standalone: false,
+ host: { id: 'current-theme', hidden: '' }
+})
+export class ThemeIndicatorDirective implements OnInit {
+ constructor(
+ private themeService: ThemeService,
+ private el: ElementRef,
+ private destroyRef: DestroyRef,
+ ) {}
+
+ ngOnInit() {
+ this.themeService.theme$.pipe(
+ takeUntilDestroyed(this.destroyRef),
+ ).subscribe(theme => this.setTheme(theme));
+ }
+
+ setTheme(theme: Theme) {
+ const el = this.el.nativeElement as HTMLInputElement;
+ el.value = theme;
+ el.dispatchEvent(new Event('input'));
+ }
+}
diff --git a/frontend/src/app/visualization/visualization.module.ts b/frontend/src/app/visualization/visualization.module.ts
index 1c57a0697..79bc3b121 100644
--- a/frontend/src/app/visualization/visualization.module.ts
+++ b/frontend/src/app/visualization/visualization.module.ts
@@ -23,6 +23,7 @@ import { VisualizationFooterComponent } from './visualization-footer/visualizati
import { VisualizationComponent } from './visualization.component';
import { WordcloudComponent } from './wordcloud/wordcloud.component';
import { MapComponent } from './map/map.component';
+import { ThemeIndicatorDirective } from './theme-indicator.directive';
@NgModule({ declarations: [
@@ -39,12 +40,14 @@ import { MapComponent } from './map/map.component';
VisualizationComponent,
PaletteSelectComponent,
MapComponent,
+ ThemeIndicatorDirective,
],
exports: [
TermComparisonEditorComponent,
VisualizationFooterComponent,
FreqtableComponent,
VisualizationComponent,
+ ThemeIndicatorDirective
], imports: [
AutoCompleteModule,
ChartModule,
diff --git a/frontend/src/app/word-models/neighbor-network/neighbor-network.component.html b/frontend/src/app/word-models/neighbor-network/neighbor-network.component.html
index 751470c44..976591c0c 100644
--- a/frontend/src/app/word-models/neighbor-network/neighbor-network.component.html
+++ b/frontend/src/app/word-models/neighbor-network/neighbor-network.component.html
@@ -1,7 +1,7 @@
-
+
diff --git a/frontend/src/app/word-models/neighbor-network/neighbor-network.component.ts b/frontend/src/app/word-models/neighbor-network/neighbor-network.component.ts
index 809e536d2..4636214b4 100644
--- a/frontend/src/app/word-models/neighbor-network/neighbor-network.component.ts
+++ b/frontend/src/app/word-models/neighbor-network/neighbor-network.component.ts
@@ -11,13 +11,12 @@ import embed from 'vega-embed';
styleUrl: './neighbor-network.component.scss',
standalone: false,
})
-export class NeighborNetworkComponent implements OnChanges, AfterViewInit {
+export class NeighborNetworkComponent implements OnChanges {
@Input({required: true}) corpus!: Corpus;
@Input({required: true}) queryText!: string;
@Input() asTable: boolean;
@ViewChild('chart') chart!: ElementRef;
- @ViewChild('theme') themeInput!: ElementRef;
data: any;
@@ -42,15 +41,8 @@ export class NeighborNetworkComponent implements OnChanges, AfterViewInit {
constructor(
private wordModelsService: WordmodelsService,
- private themeService: ThemeService,
) { }
- ngAfterViewInit() {
- this.themeService.theme$.subscribe(
- theme => this.updateTheme(theme)
- );
- }
-
ngOnChanges(changes: SimpleChanges): void {
this.getData()
}
@@ -82,10 +74,4 @@ export class NeighborNetworkComponent implements OnChanges, AfterViewInit {
console.error(error);
});
}
-
- updateTheme(theme: Theme) {
- const el = this.themeInput.nativeElement as HTMLInputElement;
- el.value = theme;
- el.dispatchEvent(new Event('input'));
- }
}
From 873c57724de2889ba05f087496c0a45e3871dc97 Mon Sep 17 00:00:00 2001
From: Luka van der Plas
Date: Thu, 28 Aug 2025 11:52:14 +0200
Subject: [PATCH 10/21] add theme toggle button to menu
---
.../src/app/core/menu/menu.component.html | 6 ++++
frontend/src/app/core/menu/menu.component.ts | 30 +++++++++++++++++--
frontend/src/app/shared/icons.ts | 9 ++++++
3 files changed, 43 insertions(+), 2 deletions(-)
diff --git a/frontend/src/app/core/menu/menu.component.html b/frontend/src/app/core/menu/menu.component.html
index 6946e2b89..72091d858 100644
--- a/frontend/src/app/core/menu/menu.component.html
+++ b/frontend/src/app/core/menu/menu.component.html
@@ -58,6 +58,12 @@
+
diff --git a/frontend/src/app/core/menu/menu.component.ts b/frontend/src/app/core/menu/menu.component.ts
index bfa2bcaa8..71aceab15 100644
--- a/frontend/src/app/core/menu/menu.component.ts
+++ b/frontend/src/app/core/menu/menu.component.ts
@@ -5,8 +5,10 @@ import { User } from '@models/index';
import { environment } from '@environments/environment';
import { AuthService } from '@services/auth.service';
import { filter, map } from 'rxjs/operators';
-import { navIcons, userIcons } from '@shared/icons';
-import { ThemeService } from '@services/theme.service';
+import { navIcons, themeIcons, userIcons } from '@shared/icons';
+import * as _ from 'lodash';
+import { Theme, ThemeService } from '@services/theme.service';
+import { modulo } from '@utils/utils';
@Component({
selector: 'ia-menu',
@@ -27,9 +29,16 @@ export class MenuComponent implements OnDestroy, OnInit {
queryParams: Params;
}>;
+ themeOptions = [
+ { label: 'system theme', icon: themeIcons.system, value: undefined },
+ { label: 'light theme', icon: themeIcons.light, value: Theme.LIGHT },
+ { label: 'dark theme', icon: themeIcons.dark, value: Theme.DARK },
+ ];
+
navIcons = navIcons;
userIcons = userIcons;
+
private destroy$ = new Subject();
constructor(
@@ -39,6 +48,17 @@ export class MenuComponent implements OnDestroy, OnInit {
private themeService: ThemeService,
) {}
+ get currentThemeOption() {
+ return this.themeOptions[this.currentThemeOptionIndex];
+ }
+
+ private get currentThemeOptionIndex(): number {
+ return this.themeOptions.findIndex(
+ option => option.value == this.themeService.selection.value
+ );
+ }
+
+
ngOnDestroy() {
this.destroy$.next(undefined);
}
@@ -52,6 +72,12 @@ export class MenuComponent implements OnDestroy, OnInit {
this.menuOpen$.next(!this.menuOpen$.value);
}
+ cycleTheme() {
+ const currentIndex = this.currentThemeOptionIndex;
+ const nextIndex = modulo(currentIndex + 1, this.themeOptions.length);
+ this.themeService.selection.next(this.themeOptions[nextIndex].value);
+ }
+
public async logout() {
this.authService.logout(true).subscribe();
}
diff --git a/frontend/src/app/shared/icons.ts b/frontend/src/app/shared/icons.ts
index 7ff5014a2..5897e4599 100644
--- a/frontend/src/app/shared/icons.ts
+++ b/frontend/src/app/shared/icons.ts
@@ -59,6 +59,9 @@ import {
faArrowUp,
faArrowDown,
faRedo,
+ faCircleHalfStroke,
+ faMoon,
+ faSun,
} from '@fortawesome/free-solid-svg-icons';
type IconDefinition = SolidIconDefinition | RegularIconDefinition;
@@ -179,3 +182,9 @@ export const entityIcons: Icons = {
organization: faBuilding,
miscellaneous: faBookmark,
}
+
+export const themeIcons: Icons = {
+ system: faCircleHalfStroke,
+ light: faSun,
+ dark: faMoon,
+}
From debab7dc2f3a05c7972c56e43bfa7056bd1d4b61 Mon Sep 17 00:00:00 2001
From: Luka van der Plas
Date: Thu, 28 Aug 2025 12:03:29 +0200
Subject: [PATCH 11/21] outfactor theme button component
---
frontend/src/app/app.module.ts | 1 +
frontend/src/app/core/core.module.ts | 2 +
.../src/app/core/menu/menu.component.html | 7 +--
frontend/src/app/core/menu/menu.component.ts | 28 +-----------
.../theme-button/theme-button.component.ts | 45 +++++++++++++++++++
5 files changed, 50 insertions(+), 33 deletions(-)
create mode 100644 frontend/src/app/menu/theme-button/theme-button.component.ts
diff --git a/frontend/src/app/app.module.ts b/frontend/src/app/app.module.ts
index 41ae577ef..8c9e97b63 100644
--- a/frontend/src/app/app.module.ts
+++ b/frontend/src/app/app.module.ts
@@ -51,6 +51,7 @@ import { MatomoConfig, matomoImports } from './routing/matomo';
import { stylePreset } from './primeng-theme';
import { CoreModule } from './core/core.module';
+
export const appRoutes: Routes = [
{
path: 'search/:corpus',
diff --git a/frontend/src/app/core/core.module.ts b/frontend/src/app/core/core.module.ts
index febcc5101..c37e5cf96 100644
--- a/frontend/src/app/core/core.module.ts
+++ b/frontend/src/app/core/core.module.ts
@@ -8,6 +8,7 @@ import { FooterComponent } from './footer/footer.component';
import { DialogComponent } from './dialog/dialog.component';
import { CorpusSelectionModule } from '../corpus-selection/corpus-selection.module';
import { AlertComponent } from './alert/alert.component';
+import { ThemeButtonComponent } from '@app/menu/theme-button/theme-button.component';
/** toplevel components such as the home page and navbar */
@@ -20,6 +21,7 @@ import { AlertComponent } from './alert/alert.component';
MenuDropdownComponent,
NotificationsComponent,
AlertComponent,
+ ThemeButtonComponent,
],
imports: [SharedModule, CorpusSelectionModule],
exports: [
diff --git a/frontend/src/app/core/menu/menu.component.html b/frontend/src/app/core/menu/menu.component.html
index 72091d858..a81c64a33 100644
--- a/frontend/src/app/core/menu/menu.component.html
+++ b/frontend/src/app/core/menu/menu.component.html
@@ -58,12 +58,7 @@
-
+
diff --git a/frontend/src/app/core/menu/menu.component.ts b/frontend/src/app/core/menu/menu.component.ts
index 71aceab15..d1a34afcd 100644
--- a/frontend/src/app/core/menu/menu.component.ts
+++ b/frontend/src/app/core/menu/menu.component.ts
@@ -5,10 +5,8 @@ import { User } from '@models/index';
import { environment } from '@environments/environment';
import { AuthService } from '@services/auth.service';
import { filter, map } from 'rxjs/operators';
-import { navIcons, themeIcons, userIcons } from '@shared/icons';
+import { navIcons, userIcons } from '@shared/icons';
import * as _ from 'lodash';
-import { Theme, ThemeService } from '@services/theme.service';
-import { modulo } from '@utils/utils';
@Component({
selector: 'ia-menu',
@@ -29,12 +27,6 @@ export class MenuComponent implements OnDestroy, OnInit {
queryParams: Params;
}>;
- themeOptions = [
- { label: 'system theme', icon: themeIcons.system, value: undefined },
- { label: 'light theme', icon: themeIcons.light, value: Theme.LIGHT },
- { label: 'dark theme', icon: themeIcons.dark, value: Theme.DARK },
- ];
-
navIcons = navIcons;
userIcons = userIcons;
@@ -45,20 +37,8 @@ export class MenuComponent implements OnDestroy, OnInit {
private authService: AuthService,
private router: Router,
private route: ActivatedRoute,
- private themeService: ThemeService,
) {}
- get currentThemeOption() {
- return this.themeOptions[this.currentThemeOptionIndex];
- }
-
- private get currentThemeOptionIndex(): number {
- return this.themeOptions.findIndex(
- option => option.value == this.themeService.selection.value
- );
- }
-
-
ngOnDestroy() {
this.destroy$.next(undefined);
}
@@ -72,12 +52,6 @@ export class MenuComponent implements OnDestroy, OnInit {
this.menuOpen$.next(!this.menuOpen$.value);
}
- cycleTheme() {
- const currentIndex = this.currentThemeOptionIndex;
- const nextIndex = modulo(currentIndex + 1, this.themeOptions.length);
- this.themeService.selection.next(this.themeOptions[nextIndex].value);
- }
-
public async logout() {
this.authService.logout(true).subscribe();
}
diff --git a/frontend/src/app/menu/theme-button/theme-button.component.ts b/frontend/src/app/menu/theme-button/theme-button.component.ts
new file mode 100644
index 000000000..53b6976fa
--- /dev/null
+++ b/frontend/src/app/menu/theme-button/theme-button.component.ts
@@ -0,0 +1,45 @@
+import { Component, HostBinding, HostListener } from '@angular/core';
+import { Theme, ThemeService } from '@services/theme.service';
+import { themeIcons } from '@shared/icons';
+import { modulo } from '@utils/utils';
+
+@Component({
+ selector: 'button[ia-theme-button]',
+ template: `
+
+ `,
+ standalone: false,
+})
+export class ThemeButtonComponent {
+ options = [
+ { label: 'system theme', icon: themeIcons.system, value: undefined },
+ { label: 'light theme', icon: themeIcons.light, value: Theme.LIGHT },
+ { label: 'dark theme', icon: themeIcons.dark, value: Theme.DARK },
+ ];
+
+ constructor(
+ private themeService: ThemeService
+ ) {}
+
+ @HostBinding('attr.aria-label')
+ get ariaLabel() {
+ return this.currentOption.label;
+ }
+
+ get currentOption() {
+ return this.options[this.currentIndex];
+ }
+
+ private get currentIndex(): number {
+ return this.options.findIndex(
+ option => option.value == this.themeService.selection.value
+ );
+ }
+
+ @HostListener('click')
+ cycle() {
+ const nextIndex = modulo(this.currentIndex + 1, this.options.length);
+ this.themeService.selection.next(this.options[nextIndex].value);
+ }
+
+}
From e2c3c3acd47cd14684eb5c1c0e303f88c36099da Mon Sep 17 00:00:00 2001
From: Luka van der Plas
Date: Tue, 23 Sep 2025 14:59:02 +0200
Subject: [PATCH 12/21] update map colours with site theme
---
.../app/visualization/map/map.component.html | 3 ++
.../app/visualization/map/map.component.ts | 28 +++++++++++++++++--
2 files changed, 28 insertions(+), 3 deletions(-)
diff --git a/frontend/src/app/visualization/map/map.component.html b/frontend/src/app/visualization/map/map.component.html
index 894ccc28e..5cc345052 100644
--- a/frontend/src/app/visualization/map/map.component.html
+++ b/frontend/src/app/visualization/map/map.component.html
@@ -1,3 +1,6 @@
+
+
+
diff --git a/frontend/src/app/visualization/map/map.component.ts b/frontend/src/app/visualization/map/map.component.ts
index d7e04f41f..b09e23e36 100644
--- a/frontend/src/app/visualization/map/map.component.ts
+++ b/frontend/src/app/visualization/map/map.component.ts
@@ -1,3 +1,4 @@
+/* eslint-disable quotes */
import { Component, ElementRef, EventEmitter, Input, Output, OnChanges, SimpleChanges, ViewChild } from '@angular/core';
import { BehaviorSubject } from 'rxjs';
import embed, { VisualizationSpec } from 'vega-embed';
@@ -29,6 +30,17 @@ export class MapComponent implements OnChanges {
isLoading$ = new BehaviorSubject(false);
+ mapColors = {
+ landStroke: {
+ light: '#BDAE8A',
+ dark: '#6c5a46',
+ },
+ landFill: {
+ light: '#E5D3B3',
+ dark: '#4d3e2e',
+ },
+ };
+
private mapDataResults: MapDataResults;
constructor(
@@ -151,7 +163,14 @@ export class MapComponent implements OnChanges {
"events": { "signal": "delta" },
"update": "clamp(angles[1] + delta[1], -60, 60)"
}]
- }
+ },
+ {
+ 'name': 'theme',
+ 'description': 'Current site theme (light/dark)',
+ 'bind': {
+ 'element': '#current-theme',
+ }
+ },
],
"projections": [
@@ -189,8 +208,10 @@ export class MapComponent implements OnChanges {
"encode": {
"enter": {
"strokeWidth": { "value": 0.75 },
- "fill": { "value": "#E5D3B3" },
- "stroke": { "value": "#BDAE8A" },
+ },
+ "update": {
+ "fill": {'signal': `theme === "dark" ? "${this.mapColors.landFill.dark}" : "${this.mapColors.landFill.light}"`},
+ "stroke": {'signal': `theme === "dark" ? "${this.mapColors.landStroke.dark}" : "${this.mapColors.landStroke.light}"`},
}
},
"transform": [
@@ -222,6 +243,7 @@ export class MapComponent implements OnChanges {
}
+
async renderChart(): Promise {
const spec = this.getVegaSpec();
const aspectRatio = 2 / 3;
From d8eaa8c84e9246cd437ff337cb785f6a51ca616e Mon Sep 17 00:00:00 2001
From: Luka van der Plas
Date: Wed, 22 Oct 2025 17:56:03 +0200
Subject: [PATCH 13/21] store theme in local storage
---
.../app/menu/theme-button/theme-button.component.ts | 2 +-
frontend/src/app/services/theme.service.ts | 13 +++++++++++++
2 files changed, 14 insertions(+), 1 deletion(-)
diff --git a/frontend/src/app/menu/theme-button/theme-button.component.ts b/frontend/src/app/menu/theme-button/theme-button.component.ts
index 53b6976fa..42033d496 100644
--- a/frontend/src/app/menu/theme-button/theme-button.component.ts
+++ b/frontend/src/app/menu/theme-button/theme-button.component.ts
@@ -23,7 +23,7 @@ export class ThemeButtonComponent {
@HostBinding('attr.aria-label')
get ariaLabel() {
- return this.currentOption.label;
+ return this.currentOption?.label;
}
get currentOption() {
diff --git a/frontend/src/app/services/theme.service.ts b/frontend/src/app/services/theme.service.ts
index ad5ff48a3..56c311796 100644
--- a/frontend/src/app/services/theme.service.ts
+++ b/frontend/src/app/services/theme.service.ts
@@ -19,6 +19,7 @@ export class ThemeService {
constructor(
) {
+ this.selection.next(this.readStoredTheme());
const query = window.matchMedia('(prefers-color-scheme: dark)');
this.systemTheme$ = fromEvent(query, 'change').pipe(
startWith(query),
@@ -27,6 +28,7 @@ export class ThemeService {
this.theme$ = combineLatest([this.selection, this.systemTheme$]).pipe(
map(([selection, system]) => selection || system)
);
+ this.selection.subscribe((theme) => this.writeStoredTheme(theme));
this.theme$.subscribe((theme) => this.setTheme(theme));
}
@@ -45,6 +47,17 @@ export class ThemeService {
for (let chart of active) {
chart.update();
}
+ }
+
+ private readStoredTheme(): Theme | null {
+ return (localStorage.getItem('theme') as Theme) || null;
+ }
+ private writeStoredTheme(theme: Theme | null): void {
+ if (theme) {
+ localStorage.setItem('theme', theme);
+ } else {
+ localStorage.removeItem('theme');
+ }
}
}
From 2acd51ab43a12600987f4cb4854c62ed4de9feb7 Mon Sep 17 00:00:00 2001
From: Luka van der Plas
Date: Fri, 14 Nov 2025 14:25:43 +0100
Subject: [PATCH 14/21] add styling docs + docstrings
---
documentation/Styling.md | 67 ++++++++++++++++++++++
frontend/src/app/services/theme.service.ts | 9 ++-
2 files changed, 75 insertions(+), 1 deletion(-)
create mode 100644 documentation/Styling.md
diff --git a/documentation/Styling.md b/documentation/Styling.md
new file mode 100644
index 000000000..50fe4dcea
--- /dev/null
+++ b/documentation/Styling.md
@@ -0,0 +1,67 @@
+# Styling
+
+This document covers CSS styling for the frontend.
+
+Textcavator uses the CSS framework [bulma](https://bulma.io/documentation/).
+
+Initial and derived variables from bulma are customised in [_utilities.css](/frontend/src/_utilities.scss). This file only defines variables and mixins and can be imported in component stylesheets. [styles.csss](/frontend/src/styles.scss) includes site-wide selectors.
+
+## Dark mode
+
+Dark mode is managed by the [ThemeService](/frontend/src/app/services/theme.service.ts).
+
+The service sets `data-theme="dark"` / `data-theme="light"` on the HTML root node, which is used by CSS selectors. If you need to observe the theme in a component, you can also use `ThemeService.theme$`.
+
+## Other libraries
+
+Several other libraries are used to provide components, visualisations, etc. These are often customised to fit the site theme and/or adapt to dark mode.
+
+## PrimeNG components
+
+We use several components from [primeNG](https://v19.primeng.org/). [primeng-theme.ts](/frontend/src/app/primeng-theme.ts) defines the preset to customise primeNG styles, mostly using bulma CSS variables.
+
+## Chart.js
+
+[select-color.ts](/frontend/src/app/utils/select-color.ts) defines the colour palettes for data visualisations and a utility function to select the nth colour. There are several palettes; the default is chosen to fit the site theme. The [palette selector](/frontend/src/app/visualization/visualization-footer/palette-select/palette-select.component.ts) lets users choose a preferred palette; the `VisualizationComponent` and `WordModelsComponent` provide the chosen palette as input to visualisation components.
+
+Chart.js has no built-in "dark mode". The `ThemeService` adjusts several defaults to get readable charts in dark mode, and updates all chart instances when the theme changes.
+
+## Vega
+
+To let a Vega visualisation adapt to dark/light mode, relevant colours should depend on a signal.
+
+Add the following signal in your vega document:
+
+```json
+{
+ "name": "theme",
+ "description": "Current site theme (light/dark)",
+ "bind": { "element": "#current-theme" }
+}
+```
+
+The value of the `theme` signal will be `"dark"` or `"light"`. When you define colours in your document, let theme depend on this signal, e.g. with a mark like this:
+
+```json
+{
+ "marks": [
+ {
+ "type": "text",
+ // ...
+ "encode": {
+ "update": {
+ "fill": {"signal": "theme === \"dark\" ? \"white\" : \"black\""}
+ },
+ }
+ },
+ ]
+}
+```
+
+When you display the visualisation, include a [theme indicator](/frontend/src/app/visualization/theme-indicator.directive.ts) on your page like this:
+
+```html
+
+```
+
+The Vega graph will bind the `theme` signal to the value of this element. The element will not be visible to users.
diff --git a/frontend/src/app/services/theme.service.ts b/frontend/src/app/services/theme.service.ts
index 56c311796..1e1f4fd00 100644
--- a/frontend/src/app/services/theme.service.ts
+++ b/frontend/src/app/services/theme.service.ts
@@ -12,10 +12,17 @@ export enum Theme {
providedIn: 'root'
})
export class ThemeService {
+ /** Theme selection from the user.
+ * `null` means no explicit preference, i.e. use the system theme.
+ */
selection = new BehaviorSubject(null);
- systemTheme$: Observable;
+
+ /** Theme used by the site */
theme$: Observable;
+ /** Default dark/light preference from the browser/OS */
+ private systemTheme$: Observable;
+
constructor(
) {
From 7a6b58b4daced3f6d7b079a1f22ae87132c8e0c0 Mon Sep 17 00:00:00 2001
From: Luka van der Plas
Date: Thu, 18 Dec 2025 16:08:14 +0100
Subject: [PATCH 15/21] move theme button component to core module
---
frontend/src/app/core/core.module.ts | 2 +-
.../app/{ => core}/menu/theme-button/theme-button.component.ts | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
rename frontend/src/app/{ => core}/menu/theme-button/theme-button.component.ts (96%)
diff --git a/frontend/src/app/core/core.module.ts b/frontend/src/app/core/core.module.ts
index c37e5cf96..20326d5b4 100644
--- a/frontend/src/app/core/core.module.ts
+++ b/frontend/src/app/core/core.module.ts
@@ -8,7 +8,7 @@ import { FooterComponent } from './footer/footer.component';
import { DialogComponent } from './dialog/dialog.component';
import { CorpusSelectionModule } from '../corpus-selection/corpus-selection.module';
import { AlertComponent } from './alert/alert.component';
-import { ThemeButtonComponent } from '@app/menu/theme-button/theme-button.component';
+import { ThemeButtonComponent } from '@app/core/menu/theme-button/theme-button.component';
/** toplevel components such as the home page and navbar */
diff --git a/frontend/src/app/menu/theme-button/theme-button.component.ts b/frontend/src/app/core/menu/theme-button/theme-button.component.ts
similarity index 96%
rename from frontend/src/app/menu/theme-button/theme-button.component.ts
rename to frontend/src/app/core/menu/theme-button/theme-button.component.ts
index 42033d496..fd47842bc 100644
--- a/frontend/src/app/menu/theme-button/theme-button.component.ts
+++ b/frontend/src/app/core/menu/theme-button/theme-button.component.ts
@@ -6,7 +6,7 @@ import { modulo } from '@utils/utils';
@Component({
selector: 'button[ia-theme-button]',
template: `
-
+
`,
standalone: false,
})
From 93d47adea3e30678cebe5d74a2a836afc5aac215 Mon Sep 17 00:00:00 2001
From: Luka van der Plas
Date: Thu, 18 Dec 2025 16:15:38 +0100
Subject: [PATCH 16/21] catch if stored theme is not a known value
---
frontend/src/app/services/theme.service.ts | 12 +++++++++---
1 file changed, 9 insertions(+), 3 deletions(-)
diff --git a/frontend/src/app/services/theme.service.ts b/frontend/src/app/services/theme.service.ts
index 1e1f4fd00..9239fbd48 100644
--- a/frontend/src/app/services/theme.service.ts
+++ b/frontend/src/app/services/theme.service.ts
@@ -22,6 +22,7 @@ export class ThemeService {
/** Default dark/light preference from the browser/OS */
private systemTheme$: Observable;
+ private storageKey = 'theme';
constructor(
@@ -57,14 +58,19 @@ export class ThemeService {
}
private readStoredTheme(): Theme | null {
- return (localStorage.getItem('theme') as Theme) || null;
+ const value = localStorage.getItem(this.storageKey);
+ if ([Theme.DARK, Theme.LIGHT].map(String).includes(value)) {
+ return value as Theme;
+ } else {
+ return null;
+ }
}
private writeStoredTheme(theme: Theme | null): void {
if (theme) {
- localStorage.setItem('theme', theme);
+ localStorage.setItem(this.storageKey, theme);
} else {
- localStorage.removeItem('theme');
+ localStorage.removeItem(this.storageKey);
}
}
}
From 6c32deb995abd6a06af42f42bd4229386ac67e15 Mon Sep 17 00:00:00 2001
From: Luka van der Plas
Date: Thu, 18 Dec 2025 16:39:56 +0100
Subject: [PATCH 17/21] use dropdown for theme select
---
frontend/src/app/core/core.module.ts | 2 -
.../src/app/core/menu/menu.component.html | 15 ++++++-
frontend/src/app/core/menu/menu.component.ts | 20 ++++++++-
.../theme-button/theme-button.component.ts | 45 -------------------
4 files changed, 33 insertions(+), 49 deletions(-)
delete mode 100644 frontend/src/app/core/menu/theme-button/theme-button.component.ts
diff --git a/frontend/src/app/core/core.module.ts b/frontend/src/app/core/core.module.ts
index 20326d5b4..febcc5101 100644
--- a/frontend/src/app/core/core.module.ts
+++ b/frontend/src/app/core/core.module.ts
@@ -8,7 +8,6 @@ import { FooterComponent } from './footer/footer.component';
import { DialogComponent } from './dialog/dialog.component';
import { CorpusSelectionModule } from '../corpus-selection/corpus-selection.module';
import { AlertComponent } from './alert/alert.component';
-import { ThemeButtonComponent } from '@app/core/menu/theme-button/theme-button.component';
/** toplevel components such as the home page and navbar */
@@ -21,7 +20,6 @@ import { ThemeButtonComponent } from '@app/core/menu/theme-button/theme-button.c
MenuDropdownComponent,
NotificationsComponent,
AlertComponent,
- ThemeButtonComponent,
],
imports: [SharedModule, CorpusSelectionModule],
exports: [
diff --git a/frontend/src/app/core/menu/menu.component.html b/frontend/src/app/core/menu/menu.component.html
index a81c64a33..fc17ab6ef 100644
--- a/frontend/src/app/core/menu/menu.component.html
+++ b/frontend/src/app/core/menu/menu.component.html
@@ -58,7 +58,20 @@
-
+
+
+
+
+
+ @for (option of themeOptions; track option.value) {
+
+
+ {{option.label}}
+
+ }
+
+
+
diff --git a/frontend/src/app/core/menu/menu.component.ts b/frontend/src/app/core/menu/menu.component.ts
index d1a34afcd..9833f3c7d 100644
--- a/frontend/src/app/core/menu/menu.component.ts
+++ b/frontend/src/app/core/menu/menu.component.ts
@@ -5,8 +5,9 @@ import { User } from '@models/index';
import { environment } from '@environments/environment';
import { AuthService } from '@services/auth.service';
import { filter, map } from 'rxjs/operators';
-import { navIcons, userIcons } from '@shared/icons';
+import { navIcons, themeIcons, userIcons } from '@shared/icons';
import * as _ from 'lodash';
+import { Theme, ThemeService } from '@app/services/theme.service';
@Component({
selector: 'ia-menu',
@@ -30,6 +31,12 @@ export class MenuComponent implements OnDestroy, OnInit {
navIcons = navIcons;
userIcons = userIcons;
+ themeOptions = [
+ { label: 'auto', icon: themeIcons.system, value: undefined },
+ { label: 'light', icon: themeIcons.light, value: Theme.LIGHT },
+ { label: 'dark', icon: themeIcons.dark, value: Theme.DARK },
+ ];
+
private destroy$ = new Subject();
@@ -37,8 +44,15 @@ export class MenuComponent implements OnDestroy, OnInit {
private authService: AuthService,
private router: Router,
private route: ActivatedRoute,
+ private themeService: ThemeService,
) {}
+ get currentThemeOption() {
+ return this.themeOptions.find(
+ option => option.value == this.themeService.selection.value
+ );
+ }
+
ngOnDestroy() {
this.destroy$.next(undefined);
}
@@ -52,6 +66,10 @@ export class MenuComponent implements OnDestroy, OnInit {
this.menuOpen$.next(!this.menuOpen$.value);
}
+ setTheme(value: Theme | undefined) {
+ this.themeService.selection.next(value);
+ }
+
public async logout() {
this.authService.logout(true).subscribe();
}
diff --git a/frontend/src/app/core/menu/theme-button/theme-button.component.ts b/frontend/src/app/core/menu/theme-button/theme-button.component.ts
deleted file mode 100644
index fd47842bc..000000000
--- a/frontend/src/app/core/menu/theme-button/theme-button.component.ts
+++ /dev/null
@@ -1,45 +0,0 @@
-import { Component, HostBinding, HostListener } from '@angular/core';
-import { Theme, ThemeService } from '@services/theme.service';
-import { themeIcons } from '@shared/icons';
-import { modulo } from '@utils/utils';
-
-@Component({
- selector: 'button[ia-theme-button]',
- template: `
-
- `,
- standalone: false,
-})
-export class ThemeButtonComponent {
- options = [
- { label: 'system theme', icon: themeIcons.system, value: undefined },
- { label: 'light theme', icon: themeIcons.light, value: Theme.LIGHT },
- { label: 'dark theme', icon: themeIcons.dark, value: Theme.DARK },
- ];
-
- constructor(
- private themeService: ThemeService
- ) {}
-
- @HostBinding('attr.aria-label')
- get ariaLabel() {
- return this.currentOption?.label;
- }
-
- get currentOption() {
- return this.options[this.currentIndex];
- }
-
- private get currentIndex(): number {
- return this.options.findIndex(
- option => option.value == this.themeService.selection.value
- );
- }
-
- @HostListener('click')
- cycle() {
- const nextIndex = modulo(this.currentIndex + 1, this.options.length);
- this.themeService.selection.next(this.options[nextIndex].value);
- }
-
-}
From 087b7875d35391d8f9980bb2a8d98076e46f49c1 Mon Sep 17 00:00:00 2001
From: Luka van der Plas
Date: Thu, 18 Dec 2025 17:32:03 +0100
Subject: [PATCH 18/21] dark theme styling
---
.../corpus-selector.component.scss | 1 +
frontend/src/styles.scss | 35 +++++++++++++++++++
2 files changed, 36 insertions(+)
diff --git a/frontend/src/app/corpus-selection/corpus-selector/corpus-selector.component.scss b/frontend/src/app/corpus-selection/corpus-selector/corpus-selector.component.scss
index ae4d56636..72c3dcc52 100644
--- a/frontend/src/app/corpus-selection/corpus-selector/corpus-selector.component.scss
+++ b/frontend/src/app/corpus-selection/corpus-selector/corpus-selector.component.scss
@@ -36,6 +36,7 @@
.title-divider {
margin-top: 0px;
margin-bottom: 1rem;
+ background-color: var(--bulma-primary-soft);
}
.columns .align-bottom {
diff --git a/frontend/src/styles.scss b/frontend/src/styles.scss
index a378acedb..5a2b6d119 100644
--- a/frontend/src/styles.scss
+++ b/frontend/src/styles.scss
@@ -273,6 +273,14 @@ button {
}
}
+.input, .select {
+ &:focus {
+ outline: auto !important;
+ border: inherit !important;
+ box-shadow: inherit !important;
+ }
+}
+
// primeNG
@@ -318,3 +326,30 @@ button {
}
}
}
+
+
+// dark mode
+
+[data-theme="dark"] {
+ // increase contrast in dark mode
+
+ --bulma-border-l: 40%;
+ --bulma-background-l: 30%;
+ --bulma-shadow: 0 0 0 1px hsla(var(--bulma-shadow-h), var(--bulma-shadow-s), var(--bulma-shadow-l), 0.3);
+
+ --bulma-scheme-main-l: 5%;
+ --bulma-scheme-main-bis-l: 15%;
+ --bulma-scheme-main-ter-l: 20%;
+ --bulma-text-l: 85%;
+ --bulma-text-weak-l: 70%;
+ --bulma-link-on-scheme-l: 77%;
+ --bulma-link-text: hsl(var(--bulma-link-h), 80%, var(--bulma-link-on-scheme-l));
+
+ .control, .input, .select {
+ --bulma-input-placeholder-color: hsla(var(--bulma-text-h), var(--bulma-text-s), var(--bulma-text-strong-l), 0.5);
+ }
+
+ .navbar.is-primary {
+ --bulma-navbar-dropdown-item-background-l: 20%;
+ }
+}
From fa7d5c0cfe1e5028f8d902ba625784a7ad118993 Mon Sep 17 00:00:00 2001
From: Luka van der Plas
Date: Thu, 18 Dec 2025 18:22:33 +0100
Subject: [PATCH 19/21] use light mode in peace portal
---
frontend/src/app/services/theme.service.ts | 7 ++++++-
1 file changed, 6 insertions(+), 1 deletion(-)
diff --git a/frontend/src/app/services/theme.service.ts b/frontend/src/app/services/theme.service.ts
index 9239fbd48..3d9501980 100644
--- a/frontend/src/app/services/theme.service.ts
+++ b/frontend/src/app/services/theme.service.ts
@@ -2,6 +2,7 @@ import { Injectable } from '@angular/core';
import { BehaviorSubject, combineLatest, fromEvent, map, Observable, startWith } from 'rxjs';
import { Chart } from 'chart.js';
import _ from 'lodash';
+import { environment } from '@environments/environment';
export enum Theme {
DARK = 'dark',
@@ -27,7 +28,11 @@ export class ThemeService {
constructor(
) {
- this.selection.next(this.readStoredTheme());
+ if (environment.runInIFrame) {
+ this.selection.next(Theme.LIGHT);
+ } else {
+ this.selection.next(this.readStoredTheme());
+ }
const query = window.matchMedia('(prefers-color-scheme: dark)');
this.systemTheme$ = fromEvent(query, 'change').pipe(
startWith(query),
From 9ffec287e5007e6bd018ae44953338616d35675d Mon Sep 17 00:00:00 2001
From: Luka van der Plas
Date: Fri, 9 Jan 2026 14:25:25 +0100
Subject: [PATCH 20/21] themeservice code clarity
---
frontend/src/app/core/menu/menu.component.ts | 4 +-
frontend/src/app/services/theme.service.ts | 40 ++++++++++----------
2 files changed, 23 insertions(+), 21 deletions(-)
diff --git a/frontend/src/app/core/menu/menu.component.ts b/frontend/src/app/core/menu/menu.component.ts
index 9833f3c7d..1a228b7f6 100644
--- a/frontend/src/app/core/menu/menu.component.ts
+++ b/frontend/src/app/core/menu/menu.component.ts
@@ -49,7 +49,7 @@ export class MenuComponent implements OnDestroy, OnInit {
get currentThemeOption() {
return this.themeOptions.find(
- option => option.value == this.themeService.selection.value
+ option => option.value == this.themeService.selection$.value
);
}
@@ -67,7 +67,7 @@ export class MenuComponent implements OnDestroy, OnInit {
}
setTheme(value: Theme | undefined) {
- this.themeService.selection.next(value);
+ this.themeService.selection$.next(value);
}
public async logout() {
diff --git a/frontend/src/app/services/theme.service.ts b/frontend/src/app/services/theme.service.ts
index 3d9501980..b03031b26 100644
--- a/frontend/src/app/services/theme.service.ts
+++ b/frontend/src/app/services/theme.service.ts
@@ -16,7 +16,7 @@ export class ThemeService {
/** Theme selection from the user.
* `null` means no explicit preference, i.e. use the system theme.
*/
- selection = new BehaviorSubject(null);
+ selection$ = new BehaviorSubject(null);
/** Theme used by the site */
theme$: Observable;
@@ -25,33 +25,35 @@ export class ThemeService {
private systemTheme$: Observable;
private storageKey = 'theme';
- constructor(
+ constructor() {
+ const initialSelection = environment.runInIFrame ? Theme.LIGHT : this.readStoredSelection();
+ this.selection$.next(initialSelection);
+ this.selection$.subscribe((theme) => this.storeSelection(theme));
- ) {
- if (environment.runInIFrame) {
- this.selection.next(Theme.LIGHT);
- } else {
- this.selection.next(this.readStoredTheme());
- }
+ this.systemTheme$ = this.watchSystemTheme();
+ this.theme$ = combineLatest([this.selection$, this.systemTheme$]).pipe(
+ map(([selection, system]) => selection || system)
+ );
+ this.theme$.subscribe((theme) => this.applyTheme(theme));
+ }
+
+ private watchSystemTheme(): Observable {
const query = window.matchMedia('(prefers-color-scheme: dark)');
- this.systemTheme$ = fromEvent(query, 'change').pipe(
+ return fromEvent(query, 'change').pipe(
startWith(query),
map(list => list.matches ? Theme.DARK : Theme.LIGHT)
);
- this.theme$ = combineLatest([this.selection, this.systemTheme$]).pipe(
- map(([selection, system]) => selection || system)
- );
- this.selection.subscribe((theme) => this.writeStoredTheme(theme));
- this.theme$.subscribe((theme) => this.setTheme(theme));
}
- setTheme(theme: Theme) {
+ /** set theme in the site layout */
+ private applyTheme(theme: Theme) {
const root = (document.getRootNode() as Document).documentElement;
root.setAttribute('data-theme', theme);
- this.setChartJSTheme();
+ this.applyChartJSTheme();
}
- setChartJSTheme() {
+ /** set chartjs defaults based on current style, and update active charts */
+ private applyChartJSTheme() {
const style = window.getComputedStyle(document.body);
Chart.defaults.color = () => style.getPropertyValue('--bulma-text-strong');
Chart.defaults.borderColor = () => style.getPropertyValue('--bulma-border');
@@ -62,7 +64,7 @@ export class ThemeService {
}
}
- private readStoredTheme(): Theme | null {
+ private readStoredSelection(): Theme | null {
const value = localStorage.getItem(this.storageKey);
if ([Theme.DARK, Theme.LIGHT].map(String).includes(value)) {
return value as Theme;
@@ -71,7 +73,7 @@ export class ThemeService {
}
}
- private writeStoredTheme(theme: Theme | null): void {
+ private storeSelection(theme: Theme | null): void {
if (theme) {
localStorage.setItem(this.storageKey, theme);
} else {
From 03c37533ee7a658affa9cb8cea8393f444b5e5e0 Mon Sep 17 00:00:00 2001
From: Luka van der Plas
Date: Fri, 9 Jan 2026 14:28:18 +0100
Subject: [PATCH 21/21] remove comment typo
---
.../word-models/similarity-chart/similarity-chart.component.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/frontend/src/app/word-models/similarity-chart/similarity-chart.component.ts b/frontend/src/app/word-models/similarity-chart/similarity-chart.component.ts
index 1514ae62b..a3ea815ab 100644
--- a/frontend/src/app/word-models/similarity-chart/similarity-chart.component.ts
+++ b/frontend/src/app/word-models/similarity-chart/similarity-chart.component.ts
@@ -202,7 +202,7 @@ export class SimilarityChartComponent implements OnInit, OnChanges, OnDestroy {
const options: ChartOptions = {
elements: {
line: {
- tension: 0, // dthis.chart.update();isables bezier curves
+ tension: 0, // disables bezier curves
},
point: {
radius: 0, // hide points
|