From 42636d0f40cbaffb386e9339df524933c67612fc Mon Sep 17 00:00:00 2001
From: Leonardo <leomilho@gmail.com>
Date: Mon, 6 Jan 2025 19:39:09 +0100
Subject: [PATCH] fix: correct placement for plotly import

---
 .editorconfig         |  15 +++
 .prettierrc           |  12 ++
 bun.lockb             | Bin 107362 -> 107724 bytes
 eslint.config.js      |  29 ++++
 package.json          |   3 +-
 src/utils/insights.ts | 299 +++++++++++++++++++++---------------------
 6 files changed, 206 insertions(+), 152 deletions(-)
 create mode 100644 .editorconfig
 create mode 100644 .prettierrc
 create mode 100644 eslint.config.js

diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..44b9cee
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,15 @@
+# Editor configuration, see http://editorconfig.org
+root = true
+
+[*]
+charset = utf-8
+indent_style = space
+indent_size = 2
+insert_final_newline = true
+trim_trailing_whitespace = true
+max_line_length = 140
+end_of_line = lf
+
+[*.md]
+max_line_length = off
+trim_trailing_whitespace = false
diff --git a/.prettierrc b/.prettierrc
new file mode 100644
index 0000000..1ced9be
--- /dev/null
+++ b/.prettierrc
@@ -0,0 +1,12 @@
+{
+  "printWidth": 120,
+  "trailingComma": "all",
+  "singleQuote": true,
+  "tabWidth": 2,
+  "semi": true,
+  "jsxSingleQuote": false,
+  "quoteProps": "as-needed",
+  "bracketSpacing": true,
+  "jsxBracketSameLine": false,
+  "arrowParens": "avoid"
+}
diff --git a/bun.lockb b/bun.lockb
index 4eb3dfd4c70e7f1bfd927fe0aa7a062f8d58152e..d671131c56372be563921e4e7d38461c9a77d221 100755
GIT binary patch
delta 15149
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

delta 14830
zcmeHOd0bW1_CNb_FLF@C0fEZ}5yb(Ki;NcmIciRbh=@ky6euAH4yZXISnpT%Qq#7!
zr8LE~XIYt%ne(hoPEAUMX=Um2XWHvEsHwm2I{O^1^sLu!_~-ZebUv>0-D~Z&*Is+=
zwf8w2&wcN0&o2&nE_O!D7~&WeoiQNz(y07%))&IQ*t((P<|(VA%VR?xX?p02SF`um
zFoiC^CAp!)dpDPiE>V0GrSb<9Rp68di(-pQi<Ae7OD9q7D^P3+`D$QOpviOa)b3GW
z6W}_lqId&?mYfJseLZERb;)-D>^N6hXvKGp6r~yJm)a!GC@qP7V2YxA>LuGv$)BM+
z^Cb9I(3=N6>UScr1+cWZw2&0WfT#X^fsKJy1FsqVUomjfgd#Gpd;=ciO0H~&Rt_{+
z0rUml0@8rKGjJJ3Lk5c{j4LcFo;oEEa^i>bvn+u5@T;sNOXFddfQlTL^hb>skX&r;
zAzkcY@Rux#(j0Opcyirg$kU-m^3RO=AB}crfMoBOQNII7cK-&XP&{SG9|HO*N<rmB
zL*ZUS!3Cs_+DV1Vo9KWH%)#8y5C#Rv7*Cs8d_To-QvUdfguM;<1Ct7iq7bbV@RS3w
z-0l&ome^(M2;S2pv?2wzC~Sejiqa1FGh*2Wc*?*x4Xgr&LO#pD5x~~q(+ms)hJd#k
zcs@u`V6yTk5dSLQf?qV2T|mmG*MOJ_@5=HpsaV1<ctltDhs*96kYcnDK9PHg?PaqC
zKs2ko3^|#rGw?ki&B1m9#}*b$D?%5glq<Nb9cB9ssHb#Y0VKQgfMjoK$=In-4N_`4
zNyP_IK`|b0U_Reu4d|8&j=UcPq=;5`mON!~)Yt;$N$_O0Fj6LA{*;*$#*8nX+J`fn
zmc9m#Q0EJPlo+F&(#jg3KA%9UpN)DNOnG!ec`4+S+GF@*wov<=7`Ye=fHZ`$aZyv@
zMp-!DYYT|5LP|Cv9b1-Pq)Y%$Z4E62m|s?wKQo&%&w!Z0sHJd1I%Q0LNue)z8ecyH
zQ-NeH7D(d{<msN#VRbI4b+xOk{cDn}Jq~ORt;@X1vtz~RWGQ_UNIT2K31bQ|5M_E<
z{-jAW6=fadl+{~Pq=#AHDGJY~8dH;B7Ii=7rge8|XEIP9FOcjNl#n%K|1`O?Rid39
zrEwFdPbgDj71M!VcT_e;Haw+C2Z|>Y>w+VEa-)EXMKD8dlmf}UQ9zp793X|FC)}iN
z2BHnQaj=&R;TE7C=({Ao0;JI90!a^})PsXe(a~IvH+#!rVL4FfD=M@4DGHWqWipVK
zLVE-4eWfGK!DHK~ya}Ehyl7x8kUBhU;AR8Y0%=ybf&bY@+SxfkI{Gl=G~g*ZRv;Nr
zpb-x<a331b$od-maf5dmd?y3vLQW&~Hu&pCN2d+_*A0B$z~u%$4y1P2trTx6RA<Tj
zI+s-wq(0cFW_hzpX8VES!?WLs<AYmzS#B!)-WF+W0IzDHvAw(&_vYN*Qe#;>ucc;t
z7CUAd4+&4TVy{l+D_f-4Hlmb7CDxwj`Dtt|ufqLfUW<D$x3|*NQJ!do?isK4bFme?
zs+Gn*<F&Z^al609hVeYy7xF6Hck)`?8Mg;$a3T-)zws*E&+%H^+j4uLrVhq*4$<wM
z4REnFyed$$U4d*MUm2L9reZx|{h+tAelE2HTn}&xC*7BLm0h!4ge;jnR@)=-kbY9v
zHjy~K(l15bfKn{v9=y7hi`DVkAWcof8W|;J7{vyj=g?FiL=|afj4YeiIyAPL+k-WB
zn&$;;sz0WeMsDR{!7g<yIIJ=9#o<z`!6oW#&-PC8G8j*)VnMtrL{mo~SFrHOc!-NV
z%k8Z-_CC*Rt*PEft9aDdc(uQa_2adunaAy+nz{)!a`33Jodq{opQIQh>Hxho8>Rkw
zX@9-U2kG5cm*t=&Yo9?$wm4T`8;NBh^(Lbv^|qoU`%tlXWNj8ovTYSggLKQ^)YnE~
z@nNc3rdm;uYWq==YR$2aOT&364df#`q*_ssYL`*cC(d>ca#~M1bumgbVv9b(dw5lQ
z&GwyH%ObeFgJv5Yh*0ZZEHg{&U0&NkV+yx-)YL?53yH8{<@pINHl0^>)NC6`Ms3t{
zD8-{zp?OflgUkril#Vty3(xm=sZW5z-UnauF~KRpic(~9wl(1L`N+sruMiYWu{yQ^
zcM#m@`nvGew{s7J8(Cj>rUBO-ZxX}n>y|g*z6Y0AUpKIgq6{-RR?Y2QH1;je>!PWV
z*eH9V#>T^1y3{Ig<fY`Ufy*#C)`#asX>2C1iqg~<+RBw;;nh(t_9?eJHMK=MMTvv9
zou))h3|EwK;IJOSJqvCKI6c&sE8#pSI*s+<dC{7>yuDniGM1;n;ZXnwFehqw2gx<k
zxk=!#$r^Rr46YHc4s_X?c7#KGWvdiewZ~{`33yuKjd)m$OI;7Hzs_MRQm=sP3r=@N
zO~jiDZ54Wttdi%&YPOw_b>|~vQ!Ljy@lRsY)T9WxqLB=k@CU(lN0X*{YSe(ERFnq3
z<h5}cb8vgSrry&TiK}-+yU+$+6|b@LycYLRZcotI6rPu$saqoDf`{!^F7+}vYNK1T
zMRd^<J|soWM~TuA1405WBe`z58kUdn+C)vYM9Dev)YCbh=eaai!mDuK#A{uedJXkM
z(MI9<E*C4}c}be>WhZUyBZE`eMP8evu{dt;rm6Fz<!CKDEYQUc@hZrgVu-!<)+8Ik
z?a7+@1Z3oshrWI7=T(qB79$sxhdyPy!S#|F$O0BCL#@}@dV<4ZPEGYfflR{;ly;I>
z=V;yi0FKtEtcyf`(Z(vdVsNq!xnf%dt~XzaP<>f1hXtD*9wza+*cA0%lqj^wv?!Ol
zl;pZCghPD~oU}!~S`uX5;XwckgTTR2JRsJfL>6?p`dK|ke*2@j+qa?MDF39Vi@{Nj
zVc^)GkAsWnA<-$SkE`AXWP6`_j_kbv4o*o6>}#Ia6H5#ZQ~y{G@OdCO>Jbx;lvoOm
zdbH_LJPVH23Ox05*&@4{kGLX~P-XbFs-C0Ob`l(1BQL@Epd;BoMYkASs_yealxRxj
z!fu53FiL(rt&qGfaMXtJuzpEiU<QGaPr<|BXp*sGAi#~`sa%k>fxZaNcv@q5{v8|{
zLH0$vY@M*sQu#<%ime=_9(-g%in<de=|Wfsm+cm~F8ahrA{yk3K4><a=k?RrDqhu3
zQ$Iir1;?rf$5)e{=_^GY2~MUTO?EZ7E_`I`6!oH^B_A=}xIJA{i{K`O9M5vR4Xoo;
zkX?j~{Kce1yVOu@rZh7Bt;bdbZjkwwrEUgC8;?xD>)^<i<ho%%<OS9@MqLgrnTI%2
zY$r*smmGNW#S}nRR$tnKlB~6&v22lsQZLe@%`8*19O%uT&P-$8yf#x)Gw^bV<w4!V
zCwcW#lpHWJowtLd-5%Q_mWL+>K^&3+$(0Qb;Q)v2lr83YS(;i48I2UJ+q+m8uZ3(}
zx}u;5k_Ecdmm6^ZFgTg6E@UU1Cr!-xEH{UC?}9UW3wFWrs%$t8XAm&xlXnZ;9K1UM
zPU@4bCU|~GE+5M%0~`{B`a#~!H|mg+Nb$q<bu>n6rnyMZc5t!Yyf#O(Jq%e2ugghM
zccEnT0S|99;8Kz1vJbL4v!0`+_Xcsgh40;RSm=tEax4ky7aPy*cWdlLo_Duq`8JC`
zeRmpj^4hyoD*P1vbb`A?D=-jb1BHO-B4nV}#NZ+%9js-=0>XNyqZWl*@d6=Q`b9|g
z;E8_Sj?^w&KPbwA!uZ16fC}nnsIDxL)b0im-ebuBH%NBwHS7>lf5QzPSfO};8D&(A
z1`=NYqKl9kj3EZs?MQ|Tb!kH+d66N%9m&o(LrzF~51@bjdO!jMZk{G&B8VI+15scf
z1d)6;h~#rYB!39x37QWg`QrvI0+K#A_@@l4GH?a;PYqUq$k1~J|Ga@O82A#9I(W^%
zHx2oAAZ5UQgFghMksb!oMM!qvB?cEE@y9^qSjC69(M3oh`4~h7PJ-wnqz0c7gX>P%
z1oCh6+W$6I{7)O|?I?182N7N}93Z5DTn5oVu7K#e9sR+3Ql-*J78rl|-T*xpR9%dd
z^leB*Z`{ai6Cm+Ebo&)jy{{oBq+&Az&A&h238`I6rgYXD_!$OnM;c#%VJML5xzE^u
z2%usIN6j5ZZLonMq=idIzH)d#1!=W0q=aO%ouM6W$O);~9yhA*WbhFNb~fZu26hF~
zMM!(pARuK`4v?}n&!`7_gOvityN?Lq14nqN<kASJFdkBf;qj<K>1KgprpRdiN03y<
z;YMzc2NGXm@Dqu_bvu%15^jW(je20RE+{i9rT}R?GYkbn>R`5ka}7Bm<2MRJD`+V!
zAP%??H(EMNfMn$<5*kQFfg4$S8c3m7VbtG_RKE&xs#<N-6O!Gv27epMD3gHkOCv-1
zFC&|Qyo$-pdXfTPz>S)}3?%uhKyq(0klfe;#6RUt1GgHujfA)eX|hil{O!niW>IJ!
zX?<eU{vJ|qpBZ|81d%7BLwWq@PR*(17e-4$#>+>C>M4GnIMiM+YEh&=2I^#%Y4m|0
z8ukC-BeyNtZ}99TH}TBXFTy_`xqm)#|9s^B`N+jqK#y9w2&wqzBUi@Z&qpqj;s4De
z*N2{P^^e?|ym3pjd7o_=JZYN`U%4%VUFH|I<?{33GPh^2?|9YrT)up}55EEKd!D`{
zm-pY{!(ZN!!G7RZ!Tk(w<jxHC6Mtc6E?>XXhx_cxU{`tGu3Uc4E+4)V+;y(*&gEXa
zeR%2a40eNW1@|VnkUbfeTMB<*&)t079v^;WPX_(>)81SjxYvi5@6BKy{B3Y=fs5Lg
z!EAiyzFa<IpAWAC*N8{#&*h!=`|ySPGnmS2!JPuv{Xhn5%pW_D%jX~P;g`Vq@NNfl
zdD1})@L&e><rl!62bXy$1OLNTbqE7IgaLwUS(ASFx<xfV(8#aJd?%7wmipElJ<-7f
z|4C;JDVRDj9|2VCLGr&&DVq6HCl-)wXwBZEjXl_dHAhZ-V`Eu0n@>(;w$nXu@~Am+
z;-W`Qr@Hg;-t&#)#NZYpqXkQ=xOq!nWT3IJvot0Km&sEnGil4gWu9i}J@7HesE$rH
zR84P54iH^U4H>;L(d#Mcn<pH4(Z^pl>B_1<?%-UdzxR@qd?bT(PVGqoTrCWlin4hG
z#Suu?tE5U?$Nk0W0M=H_X~~9GoP&(sJhy{(fOdj*fp&xTfcAp+f%bzAfDVEVfewS-
z0=*4-2lOuJJ<t)*QP45aanK3S`=Ad%AA&vteGEDY!ke(a@;nNE1I++Y_-28+ib6j&
zy&?)lCy0(mk)RGBI&OXk`UmKH&_6*xfPMu11o|0t6?6@B9rO$62IwZ}7KkFjKo*b(
z$O@t}_Ic0+5WUm30^ukdqy(VwH59%A(cbYQXalHHOl!s3Rs0pj$3b*#nFA^ZjRMhm
zs036BngF81Vlaq~o~=OsAeuayY03)90y<XHk^3Tu{ub~Vs19@*^f~AZ=q%_9(3haE
z=x;pdQ1}`|FWq(!(p^ujVBkg=T?={!v>H?mdJeP@v>3DiM1Q5CGZr1#yg-2<92n`)
ztY;jhF`fIr1#JiI0PO_r0_~>1l<h%*ax_#t=Ff5pmq4F`mV%anPNAw6bOQ7~=m_X-
z&;d{qC>fLjN(FTXrGa{YdV=ynl);BU!$BiJBSCkG>;TrXA|1s6phD37prN2)pdp|P
z5dL7PWP<Jn<%00nTm1+z7)VEkHgJztCat;<@N^jL2BgDdf8fU;Jr;nqts%taojNTp
zHmyy3n<>TOasX@YLqaoFSGM>BvXh?K6f2rciYHAlO)gC`#gY=v25Jnt0Ud9k3Zf)#
z1oQ+^LQ-;4l2WqX65T>s=qSo@%6H0p%71D?IZt^_rcBe+Ey;;U-z<|J<s9t+<RZ~2
zV_Xjn7O#b}4z&4*lcB6sjB3OD*+6kAgW1KRHmn896V+{4m}3kya1^dA=y^H);`H>j
z7B;G@Gd95)uat_qHq4$d8w$;#ux?MnlRjIU%+eJSoiRu^)cB*O=JiKQ{xUl*S~hid
zRg^U%GK^&=yaolF@+)U87*d=$Z(N;)B{*XfNrAGBdVT!x=|MAIIzvqpo!y)<iOO5z
z<uDf6?h`0rF6uu$oEOrw<D>bnd~C7N%5I996mdO_6**{;B5Ny0luVzo{_6hcbthur
zU)-ajye+dkD0#_}z00kF%suyCax1JBo1nm65jQ$P?-l5^gkF;uj~`FAt{5lv;^D2-
zOEf>tyXrHmcK5ymcDEt(qrP{aPkrT+ebootH8(9{P&*drFh2wAaPZWChv)1sb8EB{
z^V-1)mv{!?Fh2t<{iIcCn+pY_-Fo+mV^ByiKko~hySdqQ?`ad<3dR?Jm2do%({Xgb
zq_*ywr$k6NY@449p8xu=^RsqCZnzb8h-@gZV`4lY_zaSUrZr|y-Q~>an8R+pA3enC
za2A+ge%vVjQF{5SE#EhDYnY!i<_E|5jPvY#TU)2NL{9X;qmrU*KT@_w{CKU2yJ?OH
zYtQ1?qheDud@<%`op`K0o5#KpjXS_I^Y!3p@E-3OD;Ix?#+V;xEFKM3(W3(kWL8lf
ziC)YPKnGOE`iH&NW2)@M8564niG{Fanja(fc3^Q1^E*;y_~K2gcb%CC^%&TVQ<Qna
zw<FY-iinPI!zwlq=PM3&gr~>F)s9F&^W#-VTaQ)QtK-k<jbjp=iOQ2=b|;qUFu!8`
zX`c6Sty#~-7B)AA%$tcN6EKqnii`;OZhnh8eO{ks5qv569;a(7D?~*EiwibCO5Ohc
z*9GgRy#A={kQU^vCgNxWqOeSS14TAeWOar?A78luKFX;4r*isS4fNxkZ~z-DDa5ff
zVM}68&ls#3MF!A}kDQEu$=+rxt$SBz5=?lBP{i8^d&5?ihI7)e@{TRoCDFYLtm#Hu
z+2WiDcXY?JWVmW1%)e>m`1R_QW1v3L#S>9~pw~M^#>}@MyzJ2?G+6nhcSL$%D}736
z>oE2XBddSM2_xSdj_HnF-cj?8!aFs0{Cb7D<8UYSJI~V}*EC}56cc0reTaW2Jb%o9
zxDv~PScGUChqRByn;u@7D|1f_?z(MlU@y!`S3F>l<S9afLW22)^Wnlpe|_XgG<PeQ
z-$cK7>d_-ddkvrNt})_Vze(+l!}^h&Xc~{s8%%^4L85wP{b?g(Cl35(O_u}~k2d)U
zER&UpjR}ZbL!-YWxQ!kaF^Nz(FERiQ^SkNJ>FM^CmtR_MWwFjg+8&iKQ2~Vn^F!+e
zYc?lO-t-?qZUysGZ2upcu7Ah-LvMG@EOCTvo8N9nzmXX;>)EfIZiP1mbHRsG0$=gj
z9p<;+1wGd#t^1})m|HJUWVsNn5^<5#%f(7?jyrB<*zfSRE%o%x>6oKWJ2{&R0+;U_
zR@?drrX)e`ii5=EBn-{`s(Myg&D*_d*BydFOjl<-Ht~EkrFV_y_Jbnuc+$sILywKF
z%6PFU6)u~fX_x#o(pEfgrWFct&RDzwVf&xl4ZbcEPb33Yh;pd24PrNh3FcSgYsZc*
zUo&uG4qCZjH34TPbVzSZlaq6vPHz3uPShB_)(IanbXn99Fwv91Ukp!1Tx2H+=11yp
zZ%o@Zc4%rF3+swG#G^<tKU=@*({^*K9;>fd*qrwAYg@yfRs58Uq_K)VDS#TmQ(#th
zDbA#@c!&9AdDisKs}fFZyh)LO%Xn?C7SXAg4fB(8zZr8K;j7QAphuOAvK+hP-^4PL
zRf$`OQNnXk@+Iwfg5S`*&;HrS4^Iq=L98;!DSdt*am3%h-PHXH-3NM<D5FGn8fIe+
zhT06#%jvd0@Y0BR-^vXt7W1`6%<GPLtr5=v9Ct7j>(n!+TpX*{3lT%pn4d>1UP(j9
z`W;*0acFiA>*Lc9HoY%@E7r~$u{Di76MPvSQWmK{A3HyMk3G`DkRSSWwX2w<>F*wo
zY3v!i9klO(PzJ&zz5Bj#%xj6o`%AoS(HznhE~b!z`5An_@P~eB*L%Q2ZiQ5_w+F`A
zPkahJHdy%f1Xmzp?qb2BPfr%ZCWz@hQ8PzuAi$2&2M{hc_R{sD2<C`>z0A5`A$s9w
zD!M`(41HN&W0=ik*`neu<czKoEH2!|eAP#g$uwYJ!Fn@$%W#Z`Ub~}qZw@Jntc<g;
z#Uct7Y8CWopPeAGdb3E?inAlJx>t6VDpvM};W}{<x-3!H;Hty?#((c~>&mC~U-%==
z1u`_sagozUPvM3`!q90*7_Qx6els6<{-!hIiD|UZY29IZvPAd3P%uB$Z}Ra|Q$Ofh
z_c;{k1q$I`CnonrP~H@`(s2sf)t3b&m_HcU{>z5uTKw7~D91``<~RD|uFk2nYMcAH
zYeos5e(<MMbnFLfm11;%=5K}hMPgb%R>Ve&I+8ytn)ip+G7;I|)QV%R1t)H-*p$xv
zJoUVrEY6Z>g=m$I##m?Rrh=WV6Ymly9d_Ko*EsnicSOVuK-f-+c?4TT*g*7rP>j!D
zaUwW_1v$(g4&1l9xbFUk?+>C~C02h294oSMBs7%Grmu*tXlfi6f2VPSo{Yv~{y-KK
zZ2lr)N%Xx7J8bUW&B8hxQ+%_x*fkK#Wv-|gjE)}{-;uM+MdM7o;hUfHZ)*BtQflk#
zv)#>J?<0EP9oe$4j~F<Z1qPcx99T3tH2)!=<<s1nr~8OS&`dCYo8WXUPRMv+=vcRg
z`D21NvXx=U9d;$UYrMs$aKmB#Jfg{ctv*pdy6_LTLMP!n2>YA)bA=_>iniz4UcKg4
zFn`#v?$x3X9<h9l*A8>w!^B85b(p`1sQIwl&uxGBHp#6pMJ#~=dsu8BJ@Y3Pn+wZ&
z9R7UiD!1NR@i`O{&EI5PI6La|Z}&8w?^ZB>wQ=~RhYKTrDOlvLsqHJAS%{wbJCBoo
z7q;*C>8>7bjhlVNO=z&@Vs;jsX)lh@EG3JL;1bQBlf>V5deh~dXTsghemi1@LCeko
zA~+je|Ms*PQ|tKcsg*fnF-@AjN+*9UuJ6(O3CQgKEIKm3#l^N3hNF%%4ja|EEHM~9
zrTuHR&cy>KCf1qsuQld$9v_T97~twUL_D9(dbPwSS5qFu&lV?6c-1$x{G(y3mX(XE
z*({VT5cVNDbR#Gic?3_3nG&ibY$aGAY6;M82-AYv4wg@lza%BR$7VFWdEXG(;X7-@
ZkQ`R9<v<R*zqfdGIkRq=w49A-@?Y)g4^998

diff --git a/eslint.config.js b/eslint.config.js
new file mode 100644
index 0000000..48cc793
--- /dev/null
+++ b/eslint.config.js
@@ -0,0 +1,29 @@
+import js from "@eslint/js";
+import globals from "globals";
+import reactHooks from "eslint-plugin-react-hooks";
+import reactRefresh from "eslint-plugin-react-refresh";
+import tseslint from "typescript-eslint";
+
+export default tseslint.config(
+  { ignores: ["dist"] },
+  {
+    extends: [js.configs.recommended, ...tseslint.configs.recommended],
+    files: ["**/*.{ts,tsx}"],
+    languageOptions: {
+      ecmaVersion: 2020,
+      globals: globals.browser,
+    },
+    plugins: {
+      "react-hooks": reactHooks,
+      "react-refresh": reactRefresh,
+    },
+    rules: {
+      ...reactHooks.configs.recommended.rules,
+      "react-refresh/only-export-components": ["warn", { allowConstantExport: true }],
+      "@typescript-eslint/no-explicit-any": "off",
+      "@typescript-eslint/no-unused-vars": "off",
+      "react-hooks/exhaustive-deps": "off",
+      "react/display-name": "off",
+    },
+  },
+);
diff --git a/package.json b/package.json
index 0bc5b33..5091ba2 100644
--- a/package.json
+++ b/package.json
@@ -12,7 +12,8 @@
   "devDependencies": {
     "@types/bun": "latest",
     "@types/d3": "^7.4.3",
-    "@types/nodemailer": "^6.4.17"
+    "@types/nodemailer": "^6.4.17",
+    "@types/plotly.js": "^2.35.1"
   },
   "peerDependencies": {
     "typescript": "^5.0.0"
diff --git a/src/utils/insights.ts b/src/utils/insights.ts
index b639f2f..d80c793 100644
--- a/src/utils/insights.ts
+++ b/src/utils/insights.ts
@@ -1,10 +1,11 @@
-import type { PlotType } from 'plotly.js';
-import { scaleOrdinal, scaleQuantize } from 'd3';
-import { JSDOM } from 'jsdom';
-import svg2img from 'svg2img';
-import 'canvas';
+import { scaleOrdinal, scaleQuantize } from "d3";
+import { JSDOM } from "jsdom";
+import svg2img from "svg2img";
+import "canvas";
 import { visualizationColors, type GraphQueryResultFromBackend, type GraphQueryResultMetaFromBackend } from "ts-common";
 
+import { type PlotType } from "plotly.js";
+
 const dom = new JSDOM();
 
 // @ts-ignore
@@ -16,15 +17,16 @@ global.getComputedStyle = dom.window.getComputedStyle;
 global.Element = dom.window.Element;
 global.HTMLElement = dom.window.HTMLElement;
 
-dom.window.HTMLCanvasElement.prototype.getContext = function () { return null; };
-dom.window.URL.createObjectURL = function () { return '' };
-
-// @ts-ignore
-const { newPlot } = await import('plotly.js');
+dom.window.HTMLCanvasElement.prototype.getContext = function () {
+  return null;
+};
+dom.window.URL.createObjectURL = function () {
+  return "";
+};
 
 export enum VariableType {
-  statistic = 'statistic',
-  visualization = 'visualization',
+  statistic = "statistic",
+  visualization = "visualization",
 }
 
 async function replaceAllAsync(string: string, regexp: RegExp, replacerFunction: CallableFunction) {
@@ -33,7 +35,6 @@ async function replaceAllAsync(string: string, regexp: RegExp, replacerFunction:
   return string.replace(regexp, () => replacements[i++]);
 }
 
-
 export async function populateTemplate(html: string, result: GraphQueryResultMetaFromBackend, openVisualizationArray: any[]) {
   const regex = /\ *?{\{\ *?(\w*?):([\w ]*?)\ *?\}\}\ *?/gm;
 
@@ -42,10 +43,10 @@ export async function populateTemplate(html: string, result: GraphQueryResultMet
 
     switch (type) {
       case VariableType.statistic:
-        const [nodeType, feature, statistic] = name.split(' ');
+        const [nodeType, feature, statistic] = name.split(" ");
         const node = result.metaData.nodes.types[nodeType];
         const attribute = node?.attributes[feature].statistics as any;
-        if (attribute == null) return '';
+        if (attribute == null) return "";
         const value = attribute[statistic];
         return ` ${value} `;
 
@@ -53,7 +54,7 @@ export async function populateTemplate(html: string, result: GraphQueryResultMet
         const activeVisualization = openVisualizationArray.find((x) => x.name == name); // TODO: enforce type
 
         if (!activeVisualization) {
-          throw new Error('Tried to render non-existing visualization');
+          throw new Error("Tried to render non-existing visualization");
         }
         let xAxisData = getAttributeValues(result, activeVisualization.selectedEntity, activeVisualization.xAxisLabel!);
         let yAxisData: (string | number)[] = [];
@@ -71,9 +72,9 @@ export async function populateTemplate(html: string, result: GraphQueryResultMet
         const stack = activeVisualization.stack;
         const showAxis = true;
 
-        const xAxisLabel = '';
-        const yAxisLabel = '';
-        const zAxisLabel = '';
+        const xAxisLabel = "";
+        const yAxisLabel = "";
+        const zAxisLabel = "";
 
         const plotType = activeVisualization.plotType;
 
@@ -87,7 +88,7 @@ export async function populateTemplate(html: string, result: GraphQueryResultMet
           zAxisLabel,
           showAxis,
           groupBy,
-          stack,
+          stack
         );
 
         const layout2 = {
@@ -97,10 +98,11 @@ export async function populateTemplate(html: string, result: GraphQueryResultMet
           title: activeVisualization.title,
         };
 
-        const plot = await newPlot(dom.window.document.createElement('div'), plotData, layout2);
-        const svgString = plot.querySelector('svg')?.outerHTML;
+        const { newPlot } = await import("plotly.js");
+        const plot = await newPlot(dom.window.document.createElement("div"), plotData, layout2);
+        const svgString = plot.querySelector("svg")?.outerHTML;
         if (!svgString) {
-          return '';
+          return "";
         }
         const dataURI = await svgToBase64(svgString);
 
@@ -113,67 +115,64 @@ const svgToBase64 = (svgString: string) => {
   return new Promise((resolve, reject) => {
     svg2img(svgString, (error: any, buffer: Buffer) => {
       if (error != null) reject(error);
-      resolve(buffer.toString('base64'));
+      resolve(buffer.toString("base64"));
     });
-  })
-}
+  });
+};
 
-export const plotTypeOptions = ['bar', 'scatter', 'line', 'histogram', 'pie'] as const;
+export const plotTypeOptions = ["bar", "scatter", "line", "histogram", "pie"] as const;
 export type SupportedPlotType = (typeof plotTypeOptions)[number];
 
 const groupByTime = (xAxisData: string[], groupBy: string, additionalVariableData?: (string | number)[]) => {
   // Function to parse the date-time string into a JavaScript Date object
   const parseDate = (dateStr: string) => {
     // Remove nanoseconds part and use just the standard "YYYY-MM-DD HH:MM:SS" part
-    const cleanedDateStr = dateStr.split('.')[0];
+    const cleanedDateStr = dateStr.split(".")[0];
     return new Date(cleanedDateStr);
   };
 
   // Grouping logic
-  const groupedData = xAxisData.reduce(
-    (acc, dateStr, index) => {
-      const date = parseDate(dateStr);
-      let groupKey: string;
-
-      if (groupBy === 'yearly') {
-        groupKey = date.getFullYear().toString(); // Group by year (e.g., "2012")
-      } else if (groupBy === 'quarterly') {
-        const month = date.getMonth() + 1; // Adjust month for zero-indexed months
-        const quarter = Math.floor((month - 1) / 3) + 1; // Calculate quarter (Q1-Q4)
-        groupKey = `${date.getFullYear()}-Q${quarter}`;
-      } else if (groupBy === 'monthly') {
-        // Group by month, e.g., "2012-07"
-        groupKey = `${date.getFullYear()}-${(date.getMonth() + 1).toString().padStart(2, '0')}`;
-      } else {
-        // Default case: group by year (or some other grouping logic)
-        groupKey = date.getFullYear().toString();
-      }
+  const groupedData = xAxisData.reduce((acc, dateStr, index) => {
+    const date = parseDate(dateStr);
+    let groupKey: string;
+
+    if (groupBy === "yearly") {
+      groupKey = date.getFullYear().toString(); // Group by year (e.g., "2012")
+    } else if (groupBy === "quarterly") {
+      const month = date.getMonth() + 1; // Adjust month for zero-indexed months
+      const quarter = Math.floor((month - 1) / 3) + 1; // Calculate quarter (Q1-Q4)
+      groupKey = `${date.getFullYear()}-Q${quarter}`;
+    } else if (groupBy === "monthly") {
+      // Group by month, e.g., "2012-07"
+      groupKey = `${date.getFullYear()}-${(date.getMonth() + 1).toString().padStart(2, "0")}`;
+    } else {
+      // Default case: group by year (or some other grouping logic)
+      groupKey = date.getFullYear().toString();
+    }
 
-      // Initialize the group if it doesn't exist
-      if (!acc[groupKey]) {
-        acc[groupKey] = additionalVariableData
-          ? typeof additionalVariableData[0] === 'number'
-            ? 0 // Initialize sum for numbers
-            : [] // Initialize array for strings
-          : 0; // Initialize count for no additional data
-      }
+    // Initialize the group if it doesn't exist
+    if (!acc[groupKey]) {
+      acc[groupKey] = additionalVariableData
+        ? typeof additionalVariableData[0] === "number"
+          ? 0 // Initialize sum for numbers
+          : [] // Initialize array for strings
+        : 0; // Initialize count for no additional data
+    }
 
-      // Aggregate additional variable if provided
-      if (additionalVariableData) {
-        if (typeof additionalVariableData[index] === 'number') {
-          acc[groupKey] = (acc[groupKey] as number) + (additionalVariableData[index] as number);
-        } else if (typeof additionalVariableData[index] === 'string') {
-          acc[groupKey] = [...(acc[groupKey] as string[]), additionalVariableData[index] as string];
-        }
-      } else {
-        // Increment the count if no additionalVariableData
-        acc[groupKey] = (acc[groupKey] as number) + 1;
+    // Aggregate additional variable if provided
+    if (additionalVariableData) {
+      if (typeof additionalVariableData[index] === "number") {
+        acc[groupKey] = (acc[groupKey] as number) + (additionalVariableData[index] as number);
+      } else if (typeof additionalVariableData[index] === "string") {
+        acc[groupKey] = [...(acc[groupKey] as string[]), additionalVariableData[index] as string];
       }
+    } else {
+      // Increment the count if no additionalVariableData
+      acc[groupKey] = (acc[groupKey] as number) + 1;
+    }
 
-      return acc;
-    },
-    {} as Record<string, number | string[]>,
-  );
+    return acc;
+  }, {} as Record<string, number | string[]>);
 
   // Extract grouped data into arrays for Plotly
   const xValuesGrouped = Object.keys(groupedData);
@@ -198,17 +197,17 @@ export const preparePlotData = (
   zAxisLabel?: string,
   showAxis = true,
   groupBy?: string,
-  stack?: boolean,
+  stack?: boolean
 ): { plotData: Partial<Plotly.PlotData>[]; layout: Partial<Plotly.Layout> } => {
-  const primaryColor = '#a2aab9';  // '--clr-sec--400'
+  const primaryColor = "#a2aab9"; // '--clr-sec--400'
   const lengthLabelsX = 7; // !TODO computed number of elements based
   const lengthLabelsY = 8; // !TODO computed number of elements based
   const mainColors = visualizationColors.GPCat.colors[14];
 
   const sharedTickFont = {
-    family: 'monospace',
+    family: "monospace",
     size: 12,
-    color: '#374151', // !TODO get GP value
+    color: "#374151", // !TODO get GP value
   };
 
   let xValues: (string | number)[] = [];
@@ -218,9 +217,9 @@ export const preparePlotData = (
   let colorDataZ: string[] = [];
   let colorbar: any = {};
 
-  if (zAxisData && zAxisData.length > 0 && typeof zAxisData[0] === 'number') {
+  if (zAxisData && zAxisData.length > 0 && typeof zAxisData[0] === "number") {
     const mainColorsSeq = visualizationColors.GPSeq.colors[9];
-    const numericZAxisData = zAxisData.filter((item): item is number => typeof item === 'number');
+    const numericZAxisData = zAxisData.filter((item): item is number => typeof item === "number");
     const zMin = numericZAxisData.reduce((min, val) => (val < min ? val : min), zAxisData[0]);
     const zMax = numericZAxisData.reduce((max, val) => (val > max ? val : max), zAxisData[0]);
 
@@ -230,7 +229,7 @@ export const preparePlotData = (
     colorDataZ = zAxisData?.map((item) => colorScale(item) || primaryColor);
 
     colorbar = {
-      title: 'Color Legend',
+      title: "Color Legend",
       tickvals: [zMin, zMax],
       ticktext: [`${zMin}`, `${zMax}`],
     };
@@ -243,10 +242,10 @@ export const preparePlotData = (
       colorDataZ = zAxisData?.map((item) => colorScale(String(item)) || primaryColor);
       const sortedDomain = uniqueZAxisData.sort();
       colorbar = {
-        title: 'Color Legend',
+        title: "Color Legend",
         tickvals: sortedDomain,
         ticktext: sortedDomain.map((val) => String(val)),
-        tickmode: 'array',
+        tickmode: "array",
       };
     }
   }
@@ -286,24 +285,21 @@ export const preparePlotData = (
   let truncatedYLabels: string[] = [];
   let yAxisRange: number[] = [];
 
-  if (typeof xValues[0] === 'string') {
+  if (typeof xValues[0] === "string") {
     truncatedXLabels = computeStringTickValues(xValues, 2, lengthLabelsX);
   }
 
-  if (typeof yValues[0] === 'string' && (plotType === 'scatter' || plotType === 'line')) {
+  if (typeof yValues[0] === "string" && (plotType === "scatter" || plotType === "line")) {
     truncatedYLabels = computeStringTickValues(yValues, 2, lengthLabelsY);
   }
   const plotData = (() => {
     switch (plotType) {
-      case 'bar':
-        if (typeof xAxisData[0] === 'string' && groupBy == undefined) {
-          const frequencyMap = xAxisData.reduce(
-            (acc, item) => {
-              acc[item] = (acc[item] || 0) + 1;
-              return acc;
-            },
-            {} as Record<string, number>,
-          );
+      case "bar":
+        if (typeof xAxisData[0] === "string" && groupBy == undefined) {
+          const frequencyMap = xAxisData.reduce((acc, item) => {
+            acc[item] = (acc[item] || 0) + 1;
+            return acc;
+          }, {} as Record<string, number>);
 
           const sortedEntries = Object.entries(frequencyMap).sort((a, b) => b[1] - a[1]);
 
@@ -318,36 +314,36 @@ export const preparePlotData = (
 
           return [
             {
-              type: 'bar' as PlotType,
+              type: "bar" as PlotType,
               x: xValues,
               y: yValues,
               marker: {
                 color: colorDataZ?.length != 0 ? colorDataZ : primaryColor,
               },
               customdata: sortedLabels,
-              hovertemplate: '<b>%{customdata}</b>: %{y}<extra></extra>',
+              hovertemplate: "<b>%{customdata}</b>: %{y}<extra></extra>",
             },
           ];
         } else {
           return [
             {
-              type: 'bar' as PlotType,
+              type: "bar" as PlotType,
               x: xValues,
               y: yValues,
               marker: { color: primaryColor },
               customdata: xValues,
-              hovertemplate: '<b>%{customdata}</b>: %{y}<extra></extra>',
+              hovertemplate: "<b>%{customdata}</b>: %{y}<extra></extra>",
             },
           ];
         }
 
-      case 'scatter':
+      case "scatter":
         return [
           {
-            type: 'scatter' as PlotType,
+            type: "scatter" as PlotType,
             x: xValues,
             y: yValues,
-            mode: 'markers' as 'markers',
+            mode: "markers" as "markers",
             marker: {
               color: zAxisData && zAxisData.length > 0 ? colorDataZ : primaryColor,
               size: 7,
@@ -357,28 +353,28 @@ export const preparePlotData = (
               xValues.length === 0
                 ? yValues.map((y) => `Y: ${y}`)
                 : yValues.length === 0
-                  ? xValues.map((x) => `X: ${x}`)
-                  : xValues.map((x, index) => {
+                ? xValues.map((x) => `X: ${x}`)
+                : xValues.map((x, index) => {
                     const zValue = zAxisData && zAxisData.length > 0 ? zAxisData[index] : null;
                     return zValue ? `X: ${x} | Y: ${yValues[index]} | Color: ${zValue}` : `X: ${x} | Y: ${yValues[index]}`;
                   }),
-            hovertemplate: '<b>%{customdata}</b><extra></extra>',
+            hovertemplate: "<b>%{customdata}</b><extra></extra>",
           },
         ];
-      case 'line':
+      case "line":
         return [
           {
-            type: 'scatter' as PlotType,
+            type: "scatter" as PlotType,
             x: xValues,
             y: yValues,
-            mode: 'lines' as 'lines',
+            mode: "lines" as "lines",
             line: { color: primaryColor },
-            customdata: xValues.map((label) => (label === 'undefined' || label === 'null' || label === '' ? 'nonData' : '')),
-            hovertemplate: '<b>%{customdata}</b><extra></extra>',
+            customdata: xValues.map((label) => (label === "undefined" || label === "null" || label === "" ? "nonData" : "")),
+            hovertemplate: "<b>%{customdata}</b><extra></extra>",
           },
         ];
-      case 'histogram':
-        if (typeof xAxisData[0] === 'string') {
+      case "histogram":
+        if (typeof xAxisData[0] === "string") {
           if (zAxisData && zAxisData?.length > 0) {
             const frequencyMap = xAxisData.reduce(
               (acc, item, index) => {
@@ -397,7 +393,7 @@ export const preparePlotData = (
                 acc[item].colors.push(color);
                 acc[item].zValues.push(zAxisData[index].toString());
                 // Group and count zValues
-                const zValue = zAxisData[index] || '(Empty)';
+                const zValue = zAxisData[index] || "(Empty)";
                 acc[item].zValueCounts[zValue] = (acc[item].zValueCounts[zValue] || 0) + 1;
 
                 return acc;
@@ -410,7 +406,7 @@ export const preparePlotData = (
                   zValues: string[];
                   zValueCounts: Record<string, number>; // To store grouped counts
                 }
-              >,
+              >
             );
             const colorToLegendName = new Map();
             const sortedCategories = Object.entries(frequencyMap).sort((a, b) => b[1].count - a[1].count);
@@ -449,32 +445,29 @@ export const preparePlotData = (
               });
 
               const customdata = colorData.x.map((label, idx) => {
-                const colorTranslation = colorToLegendName.get(color) === ' ' ? '(Empty)' : colorToLegendName.get(color);
+                const colorTranslation = colorToLegendName.get(color) === " " ? "(Empty)" : colorToLegendName.get(color);
                 const percentage = ((100 * frequencyMap[label].zValueCounts[colorTranslation]) / frequencyMap[label].count).toFixed(1);
-                return [label, !stack ? frequencyMap[label]?.zValueCounts[colorTranslation] || 0 : percentage, colorTranslation || ' '];
+                return [label, !stack ? frequencyMap[label]?.zValueCounts[colorTranslation] || 0 : percentage, colorTranslation || " "];
               });
               return {
                 x: colorData.x,
                 y: yValues,
-                type: 'bar' as PlotType,
+                type: "bar" as PlotType,
                 name: legendName,
                 marker: { color: color },
                 customdata: customdata,
                 hovertemplate:
-                  '<b>X: %{customdata[0]}</b><br>' + '<b>Y: %{customdata[1]}</b><br>' + '<b>Color: %{customdata[2]}</b><extra></extra>',
-                ...(stack ? { stackgroup: 'one' } : {}),
+                  "<b>X: %{customdata[0]}</b><br>" + "<b>Y: %{customdata[1]}</b><br>" + "<b>Color: %{customdata[2]}</b><extra></extra>",
+                ...(stack ? { stackgroup: "one" } : {}),
               };
             });
 
             return traces;
           } else {
-            const frequencyMap = xAxisData.reduce(
-              (acc, item) => {
-                acc[item] = (acc[item] || 0) + 1;
-                return acc;
-              },
-              {} as Record<string, number>,
-            );
+            const frequencyMap = xAxisData.reduce((acc, item) => {
+              acc[item] = (acc[item] || 0) + 1;
+              return acc;
+            }, {} as Record<string, number>);
 
             const sortedEntries = Object.entries(frequencyMap).sort((a, b) => b[1] - a[1]);
 
@@ -483,12 +476,12 @@ export const preparePlotData = (
 
             return [
               {
-                type: 'bar' as PlotType,
+                type: "bar" as PlotType,
                 x: sortedLabels,
                 y: sortedFrequencies,
                 marker: { color: primaryColor },
                 customdata: sortedLabels,
-                hovertemplate: '<b>%{customdata}</b>: %{y}<extra></extra>',
+                hovertemplate: "<b>%{customdata}</b>: %{y}<extra></extra>",
               },
             ];
           }
@@ -511,7 +504,7 @@ export const preparePlotData = (
 
             // Assign data points to bins
             numericXAxisData.forEach((xValue, index) => {
-              const zValue = zAxisData ? zAxisData[index] || '(Empty)' : '(Empty)';
+              const zValue = zAxisData ? zAxisData[index] || "(Empty)" : "(Empty)";
               const binIndex = Math.floor((xValue - xMin) / binSize);
               const bin = bins[Math.min(binIndex, bins.length - 1)]; // Ensure the last value falls into the final bin
 
@@ -545,23 +538,23 @@ export const preparePlotData = (
               const colorData = tracesByColor[color];
               const customdata = colorData.x.map((binLabel, idx) => {
                 const countForColor = colorData.y[idx];
-                const percentage = stack ? countForColor.toFixed(1) + '%' : countForColor.toFixed(0);
+                const percentage = stack ? countForColor.toFixed(1) + "%" : countForColor.toFixed(0);
                 return [binLabel, countForColor, percentage, legendName];
               });
 
               return {
                 x: colorData.x,
                 y: colorData.y,
-                type: 'bar' as PlotType,
+                type: "bar" as PlotType,
                 name: legendName,
                 marker: { color },
                 customdata,
                 autobinx: true,
                 hovertemplate:
-                  '<b>Bin: %{customdata[0]}</b><br>' +
-                  '<b>Count/Percentage: %{customdata[2]}</b><br>' +
-                  '<b>Group: %{customdata[3]}</b><extra></extra>',
-                ...(stack ? { stackgroup: 'one' } : {}),
+                  "<b>Bin: %{customdata[0]}</b><br>" +
+                  "<b>Count/Percentage: %{customdata[2]}</b><br>" +
+                  "<b>Group: %{customdata[3]}</b><extra></extra>",
+                ...(stack ? { stackgroup: "one" } : {}),
               };
             });
 
@@ -570,7 +563,7 @@ export const preparePlotData = (
             // No zAxisData, simple histogram logic
             return [
               {
-                type: 'histogram' as PlotType,
+                type: "histogram" as PlotType,
                 x: xAxisData,
                 marker: { color: primaryColor },
                 customdata: xAxisData,
@@ -578,10 +571,10 @@ export const preparePlotData = (
             ];
           }
         }
-      case 'pie':
+      case "pie":
         return [
           {
-            type: 'pie' as PlotType,
+            type: "pie" as PlotType,
             labels: xValues.map(String),
             values: xAxisData,
             marker: { colors: mainColors },
@@ -593,22 +586,22 @@ export const preparePlotData = (
   })();
 
   const layout: Partial<Plotly.Layout> = {
-    barmode: 'stack',
+    barmode: "stack",
     xaxis: {
       title: {
-        text: showAxis ? (xAxisLabel ? xAxisLabel : '') : '',
+        text: showAxis ? (xAxisLabel ? xAxisLabel : "") : "",
         standoff: 30,
       },
       tickfont: sharedTickFont,
       showgrid: false,
       visible: showAxis,
-      ...(typeof xAxisData[0] === 'string' || (plotType === 'histogram' && sortedLabels.length > 0)
-        ? { type: 'category', categoryarray: sortedLabels, categoryorder: 'array' }
+      ...(typeof xAxisData[0] === "string" || (plotType === "histogram" && sortedLabels.length > 0)
+        ? { type: "category", categoryarray: sortedLabels, categoryorder: "array" }
         : {}),
       showline: true,
       zeroline: false,
-      tickvals: typeof xValues[0] == 'string' ? xValues : undefined,
-      ticktext: typeof xValues[0] == 'string' ? truncatedXLabels : undefined,
+      tickvals: typeof xValues[0] == "string" ? xValues : undefined,
+      ticktext: typeof xValues[0] == "string" ? truncatedXLabels : undefined,
     },
 
     yaxis: {
@@ -618,44 +611,48 @@ export const preparePlotData = (
       zeroline: false,
       tickfont: sharedTickFont,
       title: {
-        text: showAxis ? (yAxisLabel ? yAxisLabel : '') : '',
+        text: showAxis ? (yAxisLabel ? yAxisLabel : "") : "",
         standoff: 30,
       },
-      tickvals: typeof yValues[0] === 'string' && (plotType === 'scatter' || plotType === 'line') ? yValues : undefined,
-      ticktext: typeof yValues[0] === 'string' && (plotType === 'scatter' || plotType === 'line') ? truncatedYLabels : undefined,
+      tickvals: typeof yValues[0] === "string" && (plotType === "scatter" || plotType === "line") ? yValues : undefined,
+      ticktext: typeof yValues[0] === "string" && (plotType === "scatter" || plotType === "line") ? truncatedYLabels : undefined,
     },
     font: {
-      family: 'Inter',
+      family: "Inter",
       size: 12,
-      color: '#374151',
+      color: "#374151",
     },
     hoverlabel: {
-      bgcolor: 'rgba(255, 255, 255, 0.8)',
-      bordercolor: 'rgba(0, 0, 0, 0.2)',
+      bgcolor: "rgba(255, 255, 255, 0.8)",
+      bordercolor: "rgba(0, 0, 0, 0.2)",
       font: {
-        family: 'monospace',
+        family: "monospace",
         size: 14,
-        color: '#374151',
+        color: "#374151",
       },
     },
   };
   return { plotData, layout };
 };
 
-export const getAttributeValues = (query: GraphQueryResultFromBackend, selectedEntity: string, attributeKey: string | number | undefined): any[] => {
+export const getAttributeValues = (
+  query: GraphQueryResultFromBackend,
+  selectedEntity: string,
+  attributeKey: string | number | undefined
+): any[] => {
   if (!selectedEntity || !attributeKey) {
     return [];
   }
 
-  if (attributeKey == ' ') {
+  if (attributeKey == " ") {
     return [];
   }
   return query.nodes
     .filter((item) => item.label === selectedEntity)
     .map((item) => {
       // Check if the attribute exists, return its value if it does, or an empty string otherwise
-      return item.attributes && attributeKey in item.attributes && item.attributes[attributeKey] != ''
+      return item.attributes && attributeKey in item.attributes && item.attributes[attributeKey] != ""
         ? item.attributes[attributeKey]
-        : 'NoData';
+        : "NoData";
     });
-};
\ No newline at end of file
+};
-- 
GitLab