From bbf23fc0cc7ae07b5f88961b38d9e59c07c20f68 Mon Sep 17 00:00:00 2001 From: Marco Vettorello Date: Mon, 17 Aug 2020 18:51:42 +0200 Subject: [PATCH] feat: small-multiples for xy axis chart --- .playground/index.html | 8 +- .playground/playground.tsx | 346 +-- .playground/tsconfig.json | 2 +- ...es-is-true-rotation-negative-90-2-snap.png | Bin 0 -> 8343 bytes package.json | 4 +- .../xy_chart/annotations/line/dimensions.ts | 164 +- .../xy_chart/annotations/line/types.ts | 4 + .../xy_chart/annotations/rect/dimensions.ts | 2 + .../xy_chart/annotations/utils.test.ts | 2294 ++++++++--------- src/chart_types/xy_chart/annotations/utils.ts | 11 +- src/chart_types/xy_chart/axes/axes_sizes.ts | 112 + .../xy_chart/domains/x_domain.test.ts | 34 +- src/chart_types/xy_chart/domains/x_domain.ts | 1 + .../xy_chart/domains/y_domain.test.ts | 10 +- src/chart_types/xy_chart/domains/y_domain.ts | 177 +- .../renderer/canvas/annotations/lines.ts | 9 +- .../xy_chart/renderer/canvas/axes/index.ts | 40 +- .../xy_chart/renderer/canvas/axes/line.ts | 2 +- .../xy_chart/renderer/canvas/axes/tick.ts | 2 +- .../renderer/canvas/axes/tick_label.ts | 4 +- .../xy_chart/renderer/canvas/axes/title.ts | 8 +- .../xy_chart/renderer/canvas/bars.ts | 4 +- .../xy_chart/renderer/canvas/grids.ts | 13 +- .../xy_chart/renderer/canvas/renderers.ts | 10 +- .../xy_chart/renderer/canvas/xy_chart.tsx | 22 +- .../xy_chart/renderer/dom/highlighter.tsx | 4 +- .../rendering/rendering.areas.test.ts | 1174 ++++----- .../xy_chart/rendering/rendering.bars.test.ts | 1623 ++++++------ .../xy_chart/rendering/rendering.ts | 57 +- .../state/selectors/compute_annotations.ts | 5 +- .../selectors/compute_axis_visible_ticks.ts | 19 +- .../state/selectors/compute_series_domains.ts | 16 +- .../selectors/compute_series_geometries.ts | 12 +- .../compute_small_multiple_scales.ts | 63 + .../state/selectors/count_bars_in_cluster.ts | 36 +- .../state/selectors/get_debug_state.ts | 178 +- .../xy_chart/state/selectors/get_specs.ts | 29 +- .../get_tooltip_values_highlighted_geoms.ts | 3 +- src/chart_types/xy_chart/state/utils/types.ts | 12 +- src/chart_types/xy_chart/state/utils/utils.ts | 386 ++- .../xy_chart/utils/axis_utils.test.ts | 50 +- src/chart_types/xy_chart/utils/axis_utils.ts | 287 ++- src/chart_types/xy_chart/utils/dimensions.ts | 75 +- src/chart_types/xy_chart/utils/fill_series.ts | 44 +- .../xy_chart/utils/fit_function_utils.ts | 17 +- .../xy_chart/utils/group_data_series.ts | 47 + src/chart_types/xy_chart/utils/scales.ts | 29 - src/chart_types/xy_chart/utils/series.test.ts | 148 +- src/chart_types/xy_chart/utils/series.ts | 328 ++- src/chart_types/xy_chart/utils/specs.ts | 11 +- .../stacked_percent_series_utils.test.ts | 60 +- src/mocks/annotations/annotations.ts | 51 + src/mocks/geometries.ts | 4 + src/mocks/series/series.ts | 7 +- src/mocks/series/series_identifiers.ts | 6 +- src/specs/constants.ts | 2 + src/specs/index.ts | 3 + src/specs/index_order.ts | 44 + src/specs/small_multiples.ts | 42 + src/utils/geometry.ts | 4 + stories/axes/8_custom_domain.tsx | 1 - yarn.lock | 15 +- 62 files changed, 4132 insertions(+), 4043 deletions(-) create mode 100644 integration/tests/__image_snapshots__/bar-stories-test-ts-bar-series-stories-test-histogram-mode-ordinal-enable-histogram-mode-is-true-has-histogram-bar-series-is-true-rotation-negative-90-2-snap.png create mode 100644 src/chart_types/xy_chart/axes/axes_sizes.ts create mode 100644 src/chart_types/xy_chart/state/selectors/compute_small_multiple_scales.ts create mode 100644 src/chart_types/xy_chart/utils/group_data_series.ts create mode 100644 src/mocks/annotations/annotations.ts create mode 100644 src/specs/index_order.ts create mode 100644 src/specs/small_multiples.ts diff --git a/.playground/index.html b/.playground/index.html index 224fb91162..23ce129c83 100644 --- a/.playground/index.html +++ b/.playground/index.html @@ -14,13 +14,13 @@ overflow: hidden; } - .chart { + #root { background: white; /*display: inline-block; position: relative; */ - width: 100%; - height: 100%; + width: 800px; + height: 600px; overflow: auto; } @@ -38,7 +38,7 @@ } #root { height: 100%; - wwidth: 100%; + width: 100%; } label { diff --git a/.playground/playground.tsx b/.playground/playground.tsx index 1de5c98987..63ba2575f1 100644 --- a/.playground/playground.tsx +++ b/.playground/playground.tsx @@ -16,277 +16,87 @@ * specific language governing permissions and limitations * under the License. */ -/* eslint-disable no-console */ import React from 'react'; -import { Chart, Heatmap, HeatmapConfig, RecursivePartial, ScaleType, Settings } from '../src'; -import { HeatmapSpec } from '../src/chart_types/heatmap/specs'; -import { BABYNAME_DATA } from '../src/utils/data_samples/babynames'; +import { + LineSeries, + Axis, + Chart, + IndexOrder, + SmallMultiples, + Position, + ScaleType, + Settings, + DataGenerator, +} from '../src'; -export const SWIM_LANE_DATA = [ - { - laneLabel: 'Overall', - time: 1572825600, - value: 0, - }, - { - laneLabel: 'Overall', - time: 1572829200, - value: 0, - }, - { - laneLabel: 'Overall', - time: 1572832800, - value: 0, - }, - { - laneLabel: 'Overall', - time: 1572836400, - value: 0, - }, - { - laneLabel: 'Overall', - time: 1572840000, - value: 0, - }, - { - laneLabel: 'Overall', - time: 1572843600, - value: 1.066358, - }, - { - laneLabel: 'Overall', - time: 1572847200, - value: 1.813946, - }, - { - laneLabel: 'Overall', - time: 1572850800, - value: 0, - }, - { - laneLabel: 'Overall', - time: 1572854400, - value: 0.05191579, - }, - { - laneLabel: 'Overall', - time: 1572858000, - value: 1.63678, - }, - { - laneLabel: 'Overall', - time: 1572861600, - value: 2.031104, - }, - { - laneLabel: 'Overall', - time: 1572865200, - value: 0, - }, - { - laneLabel: 'Overall', - time: 1572868800, - value: 1.09738, - }, - { - laneLabel: 'Overall', - time: 1572872400, - value: 0, - }, - { - laneLabel: 'Overall', - time: 1572876000, - value: 0.2232534, - }, - { - laneLabel: 'Overall', - time: 1572879600, - value: 19.49729, - }, - { - laneLabel: 'Overall', - time: 1572883200, - value: 34.10214, - }, - { - laneLabel: 'Overall', - time: 1572886800, - value: 0, - }, - { - laneLabel: 'Overall', - time: 1572890400, - value: 55.18972, - }, - { - laneLabel: 'Overall', - time: 1572894000, - value: 0.9794427671013135, - }, - { - laneLabel: 'Overall', - time: 1572897600, - value: 1.2711643855082817, - }, - { - laneLabel: 'Overall', - time: 1572901200, - value: 0.12110509647944609, - }, - { - laneLabel: 'Overall', - time: 1572904800, - value: 0.9807310648820486, - }, - { - laneLabel: 'Overall', - time: 1572908400, - value: 1.0793822204067567, - }, -]; -export class Playground extends React.Component { - constructor(props: any) { - super(props); - this.state = { - highlightedData: { - x: [1572874200000, 1572897600000], - y: ['Overall', 'test'], - }, - }; - } +const dg = new DataGenerator(); - onBrushEnd: HeatmapConfig['onBrushEnd'] = (e) => { - console.log('___onBrushEnd___', e); - this.setState({ - highlightedData: { x: e.x as any[], y: e.y as any[] }, - }); - }; +const data = dg.generateGroupedSeries(50, 9).map(({ x, y, g }) => { + switch (g) { + case 'a': + return { x, y, v: 'a', h: 1 }; + case 'b': + return { x, y, v: 'b', h: 1 }; + case 'c': + return { x, y, v: 'c', h: 1 }; + case 'd': + return { x, y, v: 'a', h: 2 }; + case 'e': + return { x, y, v: 'b', h: 2 }; + case 'f': + return { x, y, v: 'c', h: 2 }; + case 'g': + return { x, y, v: 'a', h: 3 }; + case 'h': + return { x, y, v: 'b', h: 3 }; + case 'i': + return { x, y, v: 'c', h: 3 }; + default: + return { x, y, v: 'x', h: -2 }; + } +}); - render() { - const heatmapConfig: RecursivePartial = { - grid: { - cellHeight: { - max: 30, - }, - stroke: { - width: 1, - color: '#D3DAE6', - }, - }, - cell: { - maxWidth: 'fill', - maxHeight: 'fill', - label: { - visible: false, - }, - border: { - stroke: '#D3DAE6', - strokeWidth: 0, +export const Playground = () => ( + + year > 1950).map((d) => { - return [d[0], d[1], d[2], -d[3]]; - }), - ); - return ( -
-
- - - ({ ...v, time: v.time * 1000 }))} - highlightedData={this.state.highlightedData} - xAccessor="time" - yAccessor={(d) => d.laneLabel} - valueAccessor="value" - valueFormatter={(d) => d.toFixed(2)} - ySortPredicate="numAsc" - xScaleType={ScaleType.Time} - config={heatmapConfig} - /> - -
-
-
- - - year > 1950) - // .map((d, i) => { - // return [d[0], d[1], d[2], d[3] > 20000 ? -d[3] : d[3]]; - // }) - } - xAccessor={(d) => d[2]} - yAccessor={(d) => d[0]} - valueAccessor={(d) => d[3]} - valueFormatter={(value) => value.toFixed(0.2)} - xSortPredicate="alphaAsc" - config={{ - grid: { - cellHeight: { - min: 40, - max: 40, // 'fill', - }, - stroke: { - width: 0, - }, - }, - cell: { - maxWidth: 'fill', - maxHeight: 20, - label: { - visible: true, - }, - border: { - stroke: 'white', - strokeWidth: 1, - }, - }, - yAxisLabel: { - visible: true, - width: 'auto', - textColor: '#6a717d', - }, - }} - /> - -
-
- ); - } -} -/* eslint-enable no-console */ + }} + /> + + Number(d).toFixed(2)} /> + + { + return [datum.v]; + }} + order={['alphaDesc', 'alphaAsc']} + /> + + { + return [datum.h]; + }} + order={['alphaDesc', 'alphaAsc']} + /> + + + + +
+); diff --git a/.playground/tsconfig.json b/.playground/tsconfig.json index 02b75ef178..c3f04c4e63 100644 --- a/.playground/tsconfig.json +++ b/.playground/tsconfig.json @@ -2,7 +2,7 @@ "extends": "../tsconfig", "compilerOptions": { "downlevelIteration": true, - "target": "es5" + "target": "es2020" }, "include": ["../src/**/*", "./**/*"], "exclude": ["../**/*.test.*"] diff --git a/integration/tests/__image_snapshots__/bar-stories-test-ts-bar-series-stories-test-histogram-mode-ordinal-enable-histogram-mode-is-true-has-histogram-bar-series-is-true-rotation-negative-90-2-snap.png b/integration/tests/__image_snapshots__/bar-stories-test-ts-bar-series-stories-test-histogram-mode-ordinal-enable-histogram-mode-is-true-has-histogram-bar-series-is-true-rotation-negative-90-2-snap.png new file mode 100644 index 0000000000000000000000000000000000000000..bac48a2bec8591e742c6d7f3aa9abd1ef6c2a4d0 GIT binary patch literal 8343 zcmeHNX;@R|y4~t&Id$UnSc|BDEmRZ~1O#LVY3qOiB~?UJhS~iiCLX)<;h$vzN3FldirAaslPwrw-*?B( zrwRr`A9e+$DW-d>&Y9g&-*UY`UHg%_oc54Qopw+B-Zvk|EBiq%LBhN9j~X;_oQdYT5hdSns=NZ zDf!YzZDA~QXwzGM0DXQutZ;h*YYc{S>4+0=8HQchc{LEjwr#kz7Q;T#>{y1~Ila#u z!@jtFPzk&6>*p}mA9j7MhG8oYWUauyeR2g|ocMIVFz;l{wgz${{h)nIy5dV9%y>)8lNwoS~k^(8;`=AJF$JMaun z=M)GzsqwCS9o(nKo@+;5@0~hx%(BD!#>TjBY~$S~drLw@t)`-8-QqyL_{NPJ@Xbf_ zZd0mi3pGpAhMXT+vheac^vG#_lC;s%VW_O*%wxNS_RluW{P1kx;|EVqj{7Yb=+tZ1 z<>hV9KDr$*en~VW57n+#HO%VJT!wwS-y!Uqx}qXI&-4(F>_%Arr@4zZuyzl=`BygJ zU)rtzzxXfMeeFoQptU{DCdq>8cf8m&P0Ngz6>I3xlDLw%pU5jy+*~b}+JD4`LUR60 zbgu66yf1_Emo7?1A6zCLqpedTbI%8yb?rXg z_xti2=SG_?0oVsGai%)@`jX<~zvQ?;&78V)WfmY&mUKAwHUE)-^i)_`+uDfPd8}!}h37978KDEloT{WpYMVEXTer zhbJ*-J8aF~UC}Icd&#(MlYJ<~WG5?rC(E9QE7#<%z%Jx>3Iuw-zP`M{8tT$wBYrC( z+KiicC~?} zc%=%LdhH_-r7p>(j^#ZIp-aSVWj+37h3F+Q!Vq4Ewl$`me|HsUvH_U5pzsud z?CstBr_a_=MsAqi%gf{8G%C+ezf)H$$~i$maB(X*-hp45j`5s*-|j+MfngSG69GYS zr-ZQ$Tizr`h*AiQp}Np<#oIsMjQxT8Tm&UaS!}Z~v z{r$;N`t^H=mec$Gz7J4fdBw$T*Hw)u+$2x#L_slIjXH4ac@R=H?ZD7)!l7_jlYM2P zOZs77$t82FODe93Lb^h#YI*)cbfPkb<=a&{cEI{~)=-7i!TKMcUEXbCGT(a|_Y#q` zhSp$xwXJZdPQ)TD8Ahq-v#W#nGwc@%HnqMp^BK^)@x;811im4l%fV6JC!K_qz98rk z;CKP!?8AyBxhR1yP9QL1iB@J8m6X|DIceP^f4(m`tN2v8PEJMy?H(~B{UbF|hC;^2 z#u7+F_m-v8Ch`VCg>{@+XAJ9I9~4TYxwyE{7N*iJU%vdhr)PY+QG*x|5Re`|-=}7v z0UMC!XDxYkcdux+lc-IYAEqmu>GC-Oqi$g+G$ryxrJsx6qqmpO_CEMZL)>Idkxe&} zMI{)(qKLzBVdE9D%>gY2UcSDqElEa3VT&I?z_dsTNl#_Sl>}GXcKaN=R!6A6`zK1i z2Ho!Eo>>NuNG>R@#c2rZkEG&lPKwF(_Q+e1ts3zvfm4g~MTA*LuT=#vK z8k2Qf4>jc;$*ho6&CLlFe*{J?6Ti*o7=8WVM)&4j22nIBIGS8@rLOq02jVz*=GjbG zNCfgH7nR`gKe|+wS61d^W@mgH#e+o$%tti&=h8F+bs3fMt5QgD$VUy!>Q!pqFdN zSRBFvzq^pt9H%=x_vr1j$c4oVKuo?BIvs!+Awpjjyohy+DjxQle%FAr09SbIZejnF zA!yiSt0JZ%xU3v}(cQ+V2JSE5Vp@D(aTbkDQii(Q(6?6O^vj3P0hMndFNtLV{qt#M4WF`)r9D&k? zTX1{w>nCQ5Z!U(0?SWYw7<45lQkdeD+pMvHpdJ?jLrMMo7LXaU@zem?-!8!e^C3Wt z7u8HP!|I}PN)7J!QpM3`|27F6;XsVKfbIcQKUy_~RTJ4}z0|vR$2MGPNuBJRa zbgD1^B691!5l-uQMjkD_?Ezrq6}|-s13U{FSok;0An6~kW7y;9Z~l8;{(rkU_^6v5 zdl9=Kx_JM-32H%95)*#kI)DRt095>y)yH z|FVhwpPb;md(L_Zy13`j$Nz>!cqx z_jc07?QaO9G+hx`9q8Ie^YQTk`FG(R0Xk?vv;|T(ydvNWS7wA9KyRP7pb5=$USZ+k z@NfrNFw+*?0rCs|{SyN&4mxqPQGLGBkt%09cz@%@jXF4+{iuPitHS}8=`rWDK%3`6r@QBeU(UWx7j zBNa3GCO6sp{^;=pr10=8Z^Z++;@;i6cfE(Rz;)*jdUwHH-QC^!;qsk3cM^0$m-4bG zRjKez`QfqfemqzlcvTuA;?4?(nL3(vWo1TirGu2p1o_CXRAtt+(n1}^JGKIb6aSOk z;vcu%yE#rWd~P|0(c|NQQvbyO1cHtS1>(74dYZLH_-@o$yo$8CZN%QAx#H8+2N^p^ z^_Ix-SzBAL#m&Atlx|t|v~#}JofSVej#q?$?#Gp}&2^R30)Jp+IqP{&4OEGNH4lHz4)!YJZj>;+ID7C?`n`>OKiAbd`v0AriOItu&TCM2dC>wk1WbioSmx21EE5bb`I?)X&(sOS_0iH^R@QL6OQ2qF&$RY@ z`lrtj**|Cy6xU0<)x$nKX-PEDgplVXSm8@s2TH7_uM$U_G-FzzWtc9KqAh|+2`T=P z3fDTC`^@OelT{RVhz;r*!bciogx7BOm3ex$|F+*u_TibX0e7v)wU~K|9WH!$`pK`? zHyZ22Zav(bZ0tjs>vD8-$Z}+fr|6{x8NwxP^Z{7VQm@W?&Ou!cePpKG(5(rGyWIZ% z$+E36xcL(v?9?PKm+KtRYOHGN=M05cez>t9x-!_J9Xdt`8*XZ;8halzIoo@4v@rJg z-o1M-uT@J1VcHzr`(p^KldjpsT7#co-C3<>zT4Q?*lEDk-aa-uIyx{o_#Tuvdh5QS zQ0Kt)4Yz(-p;XVGAC_@V#jkU$;=m(niOXX8)?Zi0RrA{v7ICaG*`z0)^_h0+55MFE zN7si-JpgBi!{s$SR>=B7FN!qMb{2w%HZi50$407z z?=xfAYfJ}jvhKBTu`vd){1dp%pYr z^DYMrm$g}?$QOq3DlUOX>Swd{$I#utL2$Ph8ng*{C^f~edOPYe!7$qM{B=A++o)fM zD=iKRBN8sHQsIg+6k$CP4*>-`t=Jp#=&5!A)d`*m)uY=#x5 z?vsO-f0@$eo3Om7Lqzpd6YPsdiwX#w6PL6!Mxr#Vcg%v8Nij=Q+y?fyiK(z3qz;E8 z*70vtbL1Tf&iA%l*m>$R(?lC2|Q34fSYCPXksd&A0lFz`)J- zW?I9z>mm!G|3ux*y~8Q-0LZ1~Uun<%!l zDz!tuG1Ko;3 z54FfH>E}QyynK9Ekdm~!%tYFx)0e~p6xPnZ+K_+=KEDilpmCX~jR;G|ZApp+=`bKr z-rtR!V(4C5_2mvAI7`NjO{N}|gTbsv{v(Dk0o1)fYmEd1o?sL*Gb*UwVShwEs*QgM zrF9dW=wL8sC%EFXR*)BwrxT;}s<**_d3sbrDNN1TzhOO$$rip7oWx9neuP2jRF!AF zxRs1DdzXy+YP9!v&8DBP)3x+(qZRkyf$sA6EYx)(er!~uxyy&?MKyj=rr9~E6dMP*m=3dem=$*$Sf+yA7U5oQQ~id#Ib?7>;-z?sM7a<-Lp5=eCH zZKTFjwE%wG+1uNrH&@zqhs&i4ZNOHBeglud!Tn$W9R`LCc`4)zZ9oVocm{B`@n^I5 zyKjvxpf4=_x&5~=F^j~0dTEs?5-#yxJlj%<{CSV`Jr zJYXMH%>#e7!^>xDKRjLKv|03MB$|a#-QbJt`1@0ly*r(fVwCPro zd60L{JOe3c)l@a#5cU-e-YA@~G{eSIc-aZu7n{f}z!w>J_eOFVwt6q0C~3$%Nu4Ma z%Sor{pjz;&5yXRHkNS|Lr;z@(q?qa_I8z8OuB*l(DnZ9Q<5mk*kbQD<@NzF#=)(?W zSm89Sqg$ZF(*SGS8J2$AyN0MT5Io34x1j}Yq{|xBK9E=3*>IM+Y}*wL#bP6xBUD~I zP`qdN{gVX77%RaOz9!_|Rc*@R+el9|C&(EgufGM_m>SqYfVAK=!|mPkt7xukEhZlv zO5I~XFg*9>@u4(x;|%$vXRKC&OYliV9#D5k5Ri!L)l8PQq*+j-Le$T{`Q^TAo%zL2 z*EFMy2ME-HDHo8-g!NLccs!#G(zm7bV)uq?>&+%|Gf~()$y6*M<2N`*v$LFjehA_f z6dI%BFkAh)-QY{jfLV+vqfJybJ~xHTmWf3Huf9_nLoO!Z`RCT74JxF8rQvfImX{5F za%amuyR>8=au3k6B+C#A3h)p$RRQQbhKADD+3ooXO3e#=4F#hAaTpLAl8j_n%Q4ul z76>4bY2$*x*f05%vrCM&(U*c7aC;nzok3Qj4Q#T_oyyI}>)-dtEw zije%;p4gg39ZG@@D!aVgxR|*_L-Gu%7x#*0^uZa}-SPBLJ=1}6dy&J~ZhsF5`YRx# z&LshvZULG?MxjZOh7P3p0Rg=~M!G{2LSR72lv%qratgR$C?S0jcNIsJH>5;@CdlN& zig0i&^>c+}_qR_kp~@|)o9|BrWe}PVp+%JS(B+OOnTEDd7j+j5Fok!lxu0zNT9=1? zGVMs8a|{9Zj1FW+&uBXSO+A~RQVOyc^XofQwqtN+D%emL~ zFthjVnIe$xv2G`FD^t#4W|x#0sJNd0@kO*6M^m)?i(9t4;P~kYI)k~u^APTX0B=9~ nzX1QiQTsoWt$%hdFC)G0wY+=D*by>Y3?uBf+gJ4c>0kd10yq{l literal 0 HcmV?d00001 diff --git a/package.json b/package.json index fb14638a93..8a018d65a9 100644 --- a/package.json +++ b/package.json @@ -58,7 +58,7 @@ "@popperjs/core": "^2.4.0", "chroma-js": "^2.1.0", "classnames": "^2.2.6", - "d3-array": "^1.2.4", + "d3-array": "^2.5.1", "d3-collection": "^1.0.7", "d3-color": "^1.4.0", "d3-interpolate": "^1.4.0", @@ -110,7 +110,7 @@ "@types/classnames": "^2.2.7", "@types/color": "^3.0.1", "@types/core-js": "^2.5.2", - "@types/d3-array": "^1.2.6", + "@types/d3-array": "^2.0.0", "@types/d3-collection": "^1.0.8", "@types/d3-color": "^1.2.2", "@types/d3-interpolate": "^1.3.1", diff --git a/src/chart_types/xy_chart/annotations/line/dimensions.ts b/src/chart_types/xy_chart/annotations/line/dimensions.ts index 240628bd6a..633d52d37a 100644 --- a/src/chart_types/xy_chart/annotations/line/dimensions.ts +++ b/src/chart_types/xy_chart/annotations/line/dimensions.ts @@ -22,7 +22,8 @@ import { isContinuousScale, isBandScale } from '../../../../scales/types'; import { Position, Rotation } from '../../../../utils/commons'; import { Dimensions } from '../../../../utils/dimensions'; import { GroupId } from '../../../../utils/ids'; -import { isHorizontalRotation } from '../../state/utils/common'; +import { SmallMultipleScales } from '../../state/selectors/compute_small_multiple_scales'; +import { isHorizontalRotation, isVerticalRotation } from '../../state/utils/common'; import { computeXScaleOffset } from '../../state/utils/utils'; import { AnnotationDomainTypes, LineAnnotationSpec, LineAnnotationDatum } from '../../utils/specs'; import { AnnotationMarker } from '../types'; @@ -34,6 +35,7 @@ export const DEFAULT_LINE_OVERFLOW = 0; function computeYDomainLineAnnotationDimensions( annotationSpec: LineAnnotationSpec, yScale: Scale, + { vertical: smVerticalScale, horizontal: smHorizontalScale }: SmallMultipleScales, chartRotation: Rotation, chartDimensions: Dimensions, lineColor: string, @@ -72,33 +74,46 @@ function computeYDomainLineAnnotationDimensions( return; } - const markerPosition = getMarkerPositionForYAnnotation( - chartDimensions, - chartRotation, - markerDimensions, - anchorPosition, - annotationValueYPosition, - ); - const linePathPoints = getYLinePath(chartDimensions, annotationValueYPosition, chartRotation); - - const annotationMarker: AnnotationMarker | undefined = marker - ? { - icon: marker, - color: lineColor, - dimension: { ...markerDimensions }, - position: markerPosition, + smVerticalScale.domain.forEach((verticalValue) => { + smHorizontalScale.domain.forEach((horizontalValue) => { + if (annotationValueYPosition == null) { + return; } - : undefined; - const lineProp: AnnotationLineProps = { - linePathPoints, - marker: annotationMarker, - details: { - detailsText: datum.details, - headerText: datum.header || dataValue.toString(), - }, - }; - - lineProps.push(lineProp); + + const topPos = smVerticalScale.scaleOrThrow(verticalValue); + const leftPos = smHorizontalScale.scaleOrThrow(horizontalValue); + const width = smHorizontalScale.bandwidth; + const height = smVerticalScale.bandwidth; + + const markerPosition = getMarkerPositionForYAnnotation( + chartDimensions, + chartRotation, + markerDimensions, + anchorPosition, + annotationValueYPosition, + ); + const linePathPoints = getYLinePath({ width, height, top: topPos, left: leftPos }, annotationValueYPosition); + + const annotationMarker: AnnotationMarker | undefined = marker + ? { + icon: marker, + color: lineColor, + dimension: { ...markerDimensions }, + position: markerPosition, + } + : undefined; + const lineProp: AnnotationLineProps = { + linePathPoints, + marker: annotationMarker, + details: { + detailsText: datum.details, + headerText: datum.header || dataValue.toString(), + }, + }; + + lineProps.push(lineProp); + }); + }); }); return lineProps; @@ -107,6 +122,7 @@ function computeYDomainLineAnnotationDimensions( function computeXDomainLineAnnotationDimensions( annotationSpec: LineAnnotationSpec, xScale: Scale, + { vertical: smVerticalScale, horizontal: smHorizontalScale }: SmallMultipleScales, chartRotation: Rotation, chartDimensions: Dimensions, lineColor: string, @@ -160,32 +176,46 @@ function computeXDomainLineAnnotationDimensions( return; } - const markerPosition = getMarkerPositionForXAnnotation( - chartDimensions, - chartRotation, - markerDimensions, - anchorPosition, - annotationValueXPosition, - ); - const linePathPoints = getXLinePath(chartDimensions, annotationValueXPosition, chartRotation); - - const annotationMarker: AnnotationMarker | undefined = marker - ? { - icon: marker, - color: lineColor, - dimension: { ...markerDimensions }, - position: markerPosition, + smVerticalScale.domain.forEach((verticalValue) => { + smHorizontalScale.domain.forEach((horizontalValue) => { + if (annotationValueXPosition == null) { + return; } - : undefined; - const lineProp: AnnotationLineProps = { - linePathPoints, - details: { - detailsText: datum.details, - headerText: datum.header || dataValue.toString(), - }, - marker: annotationMarker, - }; - lineProps.push(lineProp); + + const topPos = smVerticalScale.scaleOrThrow(verticalValue); + const leftPos = smHorizontalScale.scaleOrThrow(horizontalValue); + const width = smHorizontalScale.bandwidth; + const height = smVerticalScale.bandwidth; + + const markerPosition = getMarkerPositionForXAnnotation( + chartDimensions, + chartRotation, + markerDimensions, + anchorPosition, + annotationValueXPosition, + ); + + const linePathPoints = getXLinePath({ width, height, top: topPos, left: leftPos }, annotationValueXPosition); + + const annotationMarker: AnnotationMarker | undefined = marker + ? { + icon: marker, + color: lineColor, + dimension: { ...markerDimensions }, + position: markerPosition, + } + : undefined; + const lineProp: AnnotationLineProps = { + linePathPoints, + details: { + detailsText: datum.details, + headerText: datum.header || dataValue.toString(), + }, + marker: annotationMarker, + }; + lineProps.push(lineProp); + }); + }); }); return lineProps; @@ -199,6 +229,7 @@ export function computeLineAnnotationDimensions( yScales: Map, xScale: Scale, isHistogramMode: boolean, + smallMultipleScales: SmallMultipleScales, axisPosition?: Position, ): AnnotationLineProps[] | null { const { domainType, hideLines } = annotationSpec; @@ -215,6 +246,7 @@ export function computeLineAnnotationDimensions( return computeXDomainLineAnnotationDimensions( annotationSpec, xScale, + smallMultipleScales, chartRotation, chartDimensions, lineColor, @@ -232,6 +264,7 @@ export function computeLineAnnotationDimensions( return computeYDomainLineAnnotationDimensions( annotationSpec, yScale, + smallMultipleScales, chartRotation, chartDimensions, lineColor, @@ -276,11 +309,7 @@ function getDefaultMarkerPositionFromAxis( return Position.Bottom; } -function getXLinePath( - { width, height }: Pick, - value: number, - rotation: Rotation, -): AnnotationLinePathPoints { +function getXLinePath({ height, top, left }: Dimensions, value: number): AnnotationLinePathPoints { return { start: { x1: value, @@ -288,28 +317,33 @@ function getXLinePath( }, end: { x2: value, - y2: rotation === -90 || rotation === 90 ? width : height, + y2: height, + }, + transform: { + x: left, + y: top, }, }; } -function getYLinePath( - { width, height }: Pick, - value: number, - rotation: Rotation, -): AnnotationLinePathPoints { + +function getYLinePath({ width, top, left }: Dimensions, value: number): AnnotationLinePathPoints { return { start: { x1: 0, y1: value, }, end: { - x2: rotation === -90 || rotation === 90 ? height : width, + x2: width, y2: value, }, + transform: { + x: left, + y: top, + }, }; } -function getMarkerPositionForXAnnotation( +export function getMarkerPositionForXAnnotation( { width, height }: Pick, rotation: Rotation, { width: mWidth, height: mHeight }: Pick, diff --git a/src/chart_types/xy_chart/annotations/line/types.ts b/src/chart_types/xy_chart/annotations/line/types.ts index 0885f41f17..27c406cab8 100644 --- a/src/chart_types/xy_chart/annotations/line/types.ts +++ b/src/chart_types/xy_chart/annotations/line/types.ts @@ -34,6 +34,10 @@ export interface AnnotationLinePathPoints { x2: number; y2: number; }; + transform: { + x: number; + y: number; + }; } /** @internal */ diff --git a/src/chart_types/xy_chart/annotations/rect/dimensions.ts b/src/chart_types/xy_chart/annotations/rect/dimensions.ts index bc954349a9..a4af2c7b39 100644 --- a/src/chart_types/xy_chart/annotations/rect/dimensions.ts +++ b/src/chart_types/xy_chart/annotations/rect/dimensions.ts @@ -22,6 +22,7 @@ import { isBandScale, isContinuousScale } from '../../../../scales/types'; import { GroupId } from '../../../../utils/ids'; import { Point } from '../../../../utils/point'; import { PrimitiveValue } from '../../../partition_chart/layout/utils/group_by_rollup'; +import { SmallMultipleScales } from '../../state/selectors/compute_small_multiple_scales'; import { RectAnnotationDatum, RectAnnotationSpec } from '../../utils/specs'; import { Bounds } from '../types'; import { AnnotationRectProps } from './types'; @@ -39,6 +40,7 @@ export function computeRectAnnotationDimensions( annotationSpec: RectAnnotationSpec, yScales: Map, xScale: Scale, + smallMultiplesScales: SmallMultipleScales, isHistogram: boolean = false, ): AnnotationRectProps[] | null { const { dataValues } = annotationSpec; diff --git a/src/chart_types/xy_chart/annotations/utils.test.ts b/src/chart_types/xy_chart/annotations/utils.test.ts index 2bd05f00f2..4b5807929b 100644 --- a/src/chart_types/xy_chart/annotations/utils.test.ts +++ b/src/chart_types/xy_chart/annotations/utils.test.ts @@ -21,6 +21,7 @@ import { RecursivePartial } from '@elastic/eui'; import React from 'react'; import { ChartTypes } from '../..'; +import { MockAnnotationLineProps } from '../../../mocks/annotations/annotations'; import { MockGlobalSpec, MockSeriesSpec, MockAnnotationSpec } from '../../../mocks/specs'; import { MockStore } from '../../../mocks/store'; import { Scale, ScaleBand, ScaleContinuous } from '../../../scales'; @@ -66,6 +67,19 @@ describe('annotation utils', () => { const ordinalData = ['a', 'b', 'c', 'd', 'a', 'b', 'c']; const ordinalScale = new ScaleBand(ordinalData, [minRange, maxRange]); + const groupId = 'foo-group'; + + const ordinalBarChart = MockSeriesSpec.bar({ + xScaleType: ScaleType.Ordinal, + groupId, + data: [ + { x: 'a', y: 1 }, + { x: 'b', y: 0 }, + { x: 'c', y: 10 }, + { x: 'd', y: 5 }, + ], + }); + const chartDimensions: Dimensions = { width: 10, height: 20, @@ -73,7 +87,6 @@ describe('annotation utils', () => { left: 15, }; - const groupId = 'foo-group'; const style: RecursivePartial = { tickLine: { size: 10, @@ -114,16 +127,6 @@ describe('annotation utils', () => { test('should compute rect annotation in x ordinal scale', () => { const store = MockStore.default(); const settings = MockGlobalSpec.settingsNoMargins(); - const spec = MockSeriesSpec.bar({ - xScaleType: ScaleType.Ordinal, - groupId, - data: [ - { x: 'a', y: 1 }, - { x: 'b', y: 0 }, - { x: 'c', y: 10 }, - { x: 'd', y: 5 }, - ], - }); const lineAnnotation = MockAnnotationSpec.line({ id: 'foo', @@ -138,19 +141,18 @@ describe('annotation utils', () => { dataValues: [{ coordinates: { x0: 'a', x1: 'b', y0: 3, y1: 5 } }], }); - MockStore.addSpecs([settings, spec, lineAnnotation, rectAnnotation], store); + MockStore.addSpecs([settings, ordinalBarChart, lineAnnotation, rectAnnotation], store); const dimensions = computeAnnotationDimensionsSelector(store.getState()); const expectedDimensions = new Map(); expectedDimensions.set('foo', [ - { + MockAnnotationLineProps.default({ linePathPoints: { start: { x1: 0, y1: 80 }, end: { x2: 100, y2: 80 }, }, - marker: undefined, details: { detailsText: 'foo', headerText: '2' }, - }, + }), ]); expectedDimensions.set('rect', [{ details: undefined, rect: { x: 0, y: 50, width: 50, height: 20 } }]); @@ -158,1173 +160,1153 @@ describe('annotation utils', () => { }); test('should compute annotation dimensions also with missing axis', () => { - const chartRotation: Rotation = 0; - const yScales: Map = new Map(); - yScales.set(groupId, continuousScale); - - const xScale: Scale = ordinalScale; + const store = MockStore.default({ width: 10, height: 20, top: 0, left: 0 }); + const settings = MockGlobalSpec.settingsNoMargins(); - const annotations: AnnotationSpec[] = []; - const id = 'foo'; - const lineAnnotation: LineAnnotationSpec = { - chartType: ChartTypes.XYAxis, - specType: SpecTypes.Annotation, - annotationType: AnnotationTypes.Line, - id, + const lineAnnotation = MockAnnotationSpec.line({ + id: 'foo', domainType: AnnotationDomainTypes.YDomain, dataValues: [{ dataValue: 2, details: 'foo' }], groupId, style: DEFAULT_ANNOTATION_LINE_STYLE, - }; + }); - annotations.push(lineAnnotation); + MockStore.addSpecs([settings, ordinalBarChart, lineAnnotation], store); - const dimensions = computeAnnotationDimensions( - annotations, - chartDimensions, - chartRotation, - yScales, - xScale, - [], // empty axesSpecs - false, - ); + const dimensions = computeAnnotationDimensionsSelector(store.getState()); expect(dimensions.size).toEqual(1); }); test('should compute line annotation dimensions for yDomain on a yScale (chartRotation 0, left axis)', () => { - const chartRotation: Rotation = 0; - const yScales: Map = new Map(); - yScales.set(groupId, continuousScale); - - const xScale: Scale = ordinalScale; + const store = MockStore.default({ width: 10, height: 100, top: 0, left: 0 }); + const settings = MockGlobalSpec.settingsNoMargins(); - const id = 'foo-line'; - const lineAnnotation: LineAnnotationSpec = { - chartType: ChartTypes.XYAxis, - specType: SpecTypes.Annotation, - annotationType: AnnotationTypes.Line, - id, + const lineAnnotation = MockAnnotationSpec.line({ + id: 'foo-line', domainType: AnnotationDomainTypes.YDomain, dataValues: [{ dataValue: 2, details: 'foo' }], groupId, style: DEFAULT_ANNOTATION_LINE_STYLE, - }; + }); - const dimensions = computeLineAnnotationDimensions( - lineAnnotation, - chartDimensions, - chartRotation, - yScales, - xScale, - false, - Position.Left, + MockStore.addSpecs( + [ + settings, + ordinalBarChart, + lineAnnotation, + MockGlobalSpec.axis({ + ...verticalAxisSpec, + hide: true, + }), + ], + store, ); - const expectedDimensions: AnnotationLineProps[] = [ - { - linePathPoints: { - start: { x1: 0, y1: 20 }, - end: { x2: 10, y2: 20 }, - }, - details: { detailsText: 'foo', headerText: '2' }, - }, - ]; - expect(dimensions).toEqual(expectedDimensions); - }); - - test('should compute line annotation dimensions for yDomain on a yScale (chartRotation 0, right axis)', () => { - const chartRotation: Rotation = 0; - const yScales: Map = new Map(); - yScales.set(groupId, continuousScale); - - const xScale: Scale = ordinalScale; - const annotationId = 'foo-line'; - const lineAnnotation: LineAnnotationSpec = { - chartType: ChartTypes.XYAxis, - specType: SpecTypes.Annotation, - annotationType: AnnotationTypes.Line, - id: annotationId, - domainType: AnnotationDomainTypes.YDomain, - dataValues: [{ dataValue: 2, details: 'foo' }], - groupId, - style: DEFAULT_ANNOTATION_LINE_STYLE, - }; + const dimensions = computeAnnotationDimensionsSelector(store.getState()); - const dimensions = computeLineAnnotationDimensions( - lineAnnotation, - chartDimensions, - chartRotation, - yScales, - xScale, - false, - Position.Right, - ); const expectedDimensions: AnnotationLineProps[] = [ - { + MockAnnotationLineProps.default({ linePathPoints: { start: { x1: 0, y1: 20 }, end: { x2: 10, y2: 20 }, }, details: { detailsText: 'foo', headerText: '2' }, - }, - ]; - expect(dimensions).toEqual(expectedDimensions); - }); - - test('should compute line annotation dimensions for yDomain on a yScale (chartRotation 90)', () => { - const chartRotation: Rotation = 90; - const yScales: Map = new Map(); - yScales.set(groupId, continuousScale); - - const xScale: Scale = ordinalScale; - - const annotationId = 'foo-line'; - const lineAnnotation: LineAnnotationSpec = { - chartType: ChartTypes.XYAxis, - specType: SpecTypes.Annotation, - annotationType: AnnotationTypes.Line, - id: annotationId, - domainType: AnnotationDomainTypes.YDomain, - dataValues: [{ dataValue: 2, details: 'foo' }], - groupId, - style: DEFAULT_ANNOTATION_LINE_STYLE, - }; - - const dimensions = computeLineAnnotationDimensions( - lineAnnotation, - chartDimensions, - chartRotation, - yScales, - xScale, - false, - Position.Left, - ); - const expectedDimensions: AnnotationLineProps[] = [ - { - linePathPoints: { - start: { x1: 0, y1: 20 }, - end: { x2: 20, y2: 20 }, - }, - details: { detailsText: 'foo', headerText: '2' }, - marker: undefined, - }, - ]; - expect(dimensions).toEqual(expectedDimensions); - }); - - test('should not compute line annotation dimensions for yDomain if no corresponding yScale', () => { - const chartRotation: Rotation = 0; - const yScales: Map = new Map(); - const xScale: Scale = ordinalScale; - - const annotationId = 'foo-line'; - const lineAnnotation: LineAnnotationSpec = { - chartType: ChartTypes.XYAxis, - specType: SpecTypes.Annotation, - annotationType: AnnotationTypes.Line, - id: annotationId, - domainType: AnnotationDomainTypes.YDomain, - dataValues: [], - groupId, - style: DEFAULT_ANNOTATION_LINE_STYLE, - }; - - const dimensions = computeLineAnnotationDimensions( - lineAnnotation, - chartDimensions, - chartRotation, - yScales, - xScale, - false, - Position.Left, - ); - expect(dimensions).toEqual(null); - }); - - test('should compute line annotation dimensions for xDomain (chartRotation 0, ordinal scale)', () => { - const chartRotation: Rotation = 0; - const yScales: Map = new Map(); - const xScale: Scale = ordinalScale; - - const annotationId = 'foo-line'; - const lineAnnotation: LineAnnotationSpec = { - chartType: ChartTypes.XYAxis, - specType: SpecTypes.Annotation, - annotationType: AnnotationTypes.Line, - id: annotationId, - domainType: AnnotationDomainTypes.XDomain, - dataValues: [{ dataValue: 'a', details: 'foo' }], - groupId, - style: DEFAULT_ANNOTATION_LINE_STYLE, - }; - - const dimensions = computeLineAnnotationDimensions( - lineAnnotation, - chartDimensions, - chartRotation, - yScales, - xScale, - false, - Position.Left, - ); - const expectedDimensions: AnnotationLineProps[] = [ - { - linePathPoints: { - start: { x1: 12.5, y1: 0 }, - end: { x2: 12.5, y2: 20 }, - }, - details: { detailsText: 'foo', headerText: 'a' }, - }, - ]; - expect(dimensions).toEqual(expectedDimensions); - }); - - test('should compute line annotation dimensions for xDomain (chartRotation 0, continuous scale, top axis)', () => { - const chartRotation: Rotation = 0; - const yScales: Map = new Map(); - const xScale: Scale = continuousScale; - - const annotationId = 'foo-line'; - const lineAnnotation: LineAnnotationSpec = { - chartType: ChartTypes.XYAxis, - specType: SpecTypes.Annotation, - annotationType: AnnotationTypes.Line, - id: annotationId, - domainType: AnnotationDomainTypes.XDomain, - dataValues: [{ dataValue: 2, details: 'foo' }], - groupId, - style: DEFAULT_ANNOTATION_LINE_STYLE, - }; - - const dimensions = computeLineAnnotationDimensions( - lineAnnotation, - chartDimensions, - chartRotation, - yScales, - xScale, - false, - Position.Top, - ); - const expectedDimensions: AnnotationLineProps[] = [ - { - linePathPoints: { - start: { x1: 25, y1: 0 }, - end: { x2: 25, y2: 20 }, - }, - details: { detailsText: 'foo', headerText: '2' }, - marker: undefined, - }, + }), ]; - expect(dimensions).toEqual(expectedDimensions); - }); - - test('should compute line annotation dimensions for xDomain (chartRotation 0, continuous scale, bottom axis)', () => { - const chartRotation: Rotation = 0; - const yScales: Map = new Map(); - const xScale: Scale = continuousScale; - - const annotationId = 'foo-line'; - const lineAnnotation: LineAnnotationSpec = { - chartType: ChartTypes.XYAxis, - specType: SpecTypes.Annotation, - annotationType: AnnotationTypes.Line, - id: annotationId, - domainType: AnnotationDomainTypes.XDomain, - dataValues: [{ dataValue: 2, details: 'foo' }], - groupId, - style: DEFAULT_ANNOTATION_LINE_STYLE, - }; - - const dimensions = computeLineAnnotationDimensions( - lineAnnotation, - chartDimensions, - chartRotation, - yScales, - xScale, - false, - Position.Bottom, - ); - const expectedDimensions: AnnotationLineProps[] = [ - { - linePathPoints: { - start: { x1: 25, y1: 0 }, - end: { x2: 25, y2: 20 }, - }, - details: { detailsText: 'foo', headerText: '2' }, - marker: undefined, - }, - ]; - expect(dimensions).toEqual(expectedDimensions); - }); - - test('should compute line annotation dimensions for xDomain on a xScale (chartRotation 90, ordinal scale)', () => { - const chartRotation: Rotation = 90; - const yScales: Map = new Map(); - - const xScale: Scale = ordinalScale; - - const annotationId = 'foo-line'; - const lineAnnotation: LineAnnotationSpec = { - chartType: ChartTypes.XYAxis, - specType: SpecTypes.Annotation, - annotationType: AnnotationTypes.Line, - id: annotationId, - domainType: AnnotationDomainTypes.XDomain, - dataValues: [{ dataValue: 'a', details: 'foo' }], - groupId, - style: DEFAULT_ANNOTATION_LINE_STYLE, - }; - - const dimensions = computeLineAnnotationDimensions( - lineAnnotation, - chartDimensions, - chartRotation, - yScales, - xScale, - false, - Position.Left, - ); - const expectedDimensions: AnnotationLineProps[] = [ - { - linePathPoints: { - start: { x1: 12.5, y1: 0 }, - end: { x2: 12.5, y2: 10 }, - }, - details: { detailsText: 'foo', headerText: 'a' }, - marker: undefined, - }, - ]; - expect(dimensions).toEqual(expectedDimensions); - }); - - test('should compute line annotation dimensions for xDomain on a xScale (chartRotation 90, continuous scale)', () => { - const chartRotation: Rotation = 90; - const yScales: Map = new Map(); - - const xScale: Scale = continuousScale; - - const annotationId = 'foo-line'; - const lineAnnotation: LineAnnotationSpec = { - chartType: ChartTypes.XYAxis, - specType: SpecTypes.Annotation, - annotationType: AnnotationTypes.Line, - id: annotationId, - domainType: AnnotationDomainTypes.XDomain, - dataValues: [{ dataValue: 2, details: 'foo' }], - groupId, - style: DEFAULT_ANNOTATION_LINE_STYLE, - }; - - const dimensions = computeLineAnnotationDimensions( - lineAnnotation, - chartDimensions, - chartRotation, - yScales, - xScale, - false, - Position.Left, - ); - const expectedDimensions: AnnotationLineProps[] = [ - { - linePathPoints: { - start: { x1: 25, y1: 0 }, - end: { x2: 25, y2: 10 }, - }, - details: { detailsText: 'foo', headerText: '2' }, - marker: undefined, - }, - ]; - expect(dimensions).toEqual(expectedDimensions); - }); - - test('should compute line annotation dimensions for xDomain on a xScale (chartRotation -90, continuous scale)', () => { - const chartRotation: Rotation = -90; - const yScales: Map = new Map(); - - const xScale: Scale = continuousScale; - - const annotationId = 'foo-line'; - const lineAnnotation: LineAnnotationSpec = { - chartType: ChartTypes.XYAxis, - specType: SpecTypes.Annotation, - annotationType: AnnotationTypes.Line, - id: annotationId, - domainType: AnnotationDomainTypes.XDomain, - dataValues: [{ dataValue: 2, details: 'foo' }], - groupId, - style: DEFAULT_ANNOTATION_LINE_STYLE, - }; - - const dimensions = computeLineAnnotationDimensions( - lineAnnotation, - chartDimensions, - chartRotation, - yScales, - xScale, - false, - Position.Left, - ); - const expectedDimensions: AnnotationLineProps[] = [ - { - linePathPoints: { - start: { x1: 25, y1: 0 }, - end: { x2: 25, y2: 10 }, - }, - details: { detailsText: 'foo', headerText: '2' }, - marker: undefined, - }, - ]; - expect(dimensions).toEqual(expectedDimensions); - }); - - test('should compute line annotation dimensions for xDomain (chartRotation 180, continuous scale, top axis)', () => { - const chartRotation: Rotation = 180; - const yScales: Map = new Map(); - - const xScale: Scale = continuousScale; - - const annotationId = 'foo-line'; - const lineAnnotation: LineAnnotationSpec = { - chartType: ChartTypes.XYAxis, - specType: SpecTypes.Annotation, - annotationType: AnnotationTypes.Line, - id: annotationId, - domainType: AnnotationDomainTypes.XDomain, - dataValues: [{ dataValue: 2, details: 'foo' }], - groupId, - style: DEFAULT_ANNOTATION_LINE_STYLE, - }; - - const dimensions = computeLineAnnotationDimensions( - lineAnnotation, - chartDimensions, - chartRotation, - yScales, - xScale, - false, - Position.Top, - ); - const expectedDimensions: AnnotationLineProps[] = [ - { - linePathPoints: { - start: { x1: 25, y1: 0 }, - end: { x2: 25, y2: 20 }, - }, - details: { detailsText: 'foo', headerText: '2' }, - marker: undefined, - }, - ]; - expect(dimensions).toEqual(expectedDimensions); - }); - - test('should compute line annotation dimensions for xDomain (chartRotation 180, continuous scale, bottom axis)', () => { - const chartRotation: Rotation = 180; - const yScales: Map = new Map(); - const xScale: Scale = continuousScale; - - const annotationId = 'foo-line'; - const lineAnnotation: LineAnnotationSpec = { - chartType: ChartTypes.XYAxis, - specType: SpecTypes.Annotation, - annotationType: AnnotationTypes.Line, - id: annotationId, - domainType: AnnotationDomainTypes.XDomain, - dataValues: [{ dataValue: 2, details: 'foo' }], - groupId, - style: DEFAULT_ANNOTATION_LINE_STYLE, - }; - - const dimensions = computeLineAnnotationDimensions( - lineAnnotation, - chartDimensions, - chartRotation, - yScales, - xScale, - false, - Position.Bottom, - ); - const expectedDimensions: AnnotationLineProps[] = [ - { - details: { detailsText: 'foo', headerText: '2' }, - linePathPoints: { - start: { x1: 25, y1: 0 }, - end: { x2: 25, y2: 20 }, - }, - marker: undefined, - }, - ]; - expect(dimensions).toEqual(expectedDimensions); - }); - - test('should not compute annotation line values for invalid data values or AnnotationSpec.hideLines', () => { - const chartRotation: Rotation = 0; - const yScales: Map = new Map(); - yScales.set(groupId, continuousScale); - - const xScale: Scale = ordinalScale; - - const annotationId = 'foo-line'; - const invalidXLineAnnotation: AnnotationSpec = { - chartType: ChartTypes.XYAxis, - specType: SpecTypes.Annotation, - annotationType: AnnotationTypes.Line, - id: annotationId, - domainType: AnnotationDomainTypes.XDomain, - dataValues: [{ dataValue: 'e', details: 'foo' }], - groupId, - style: DEFAULT_ANNOTATION_LINE_STYLE, - }; - - const emptyXDimensions = computeLineAnnotationDimensions( - invalidXLineAnnotation, - chartDimensions, - chartRotation, - yScales, - xScale, - false, - Position.Right, - ); - - expect(emptyXDimensions).toEqual([]); - - const invalidStringXLineAnnotation: AnnotationSpec = { - chartType: ChartTypes.XYAxis, - specType: SpecTypes.Annotation, - annotationType: AnnotationTypes.Line, - id: annotationId, - domainType: AnnotationDomainTypes.XDomain, - dataValues: [{ dataValue: '', details: 'foo' }], - groupId, - style: DEFAULT_ANNOTATION_LINE_STYLE, - }; - - const invalidStringXDimensions = computeLineAnnotationDimensions( - invalidStringXLineAnnotation, - chartDimensions, - chartRotation, - yScales, - continuousScale, - false, - Position.Right, - ); - - expect(invalidStringXDimensions).toEqual([]); - - const outOfBoundsXLineAnnotation: AnnotationSpec = { - chartType: ChartTypes.XYAxis, - specType: SpecTypes.Annotation, - annotationType: AnnotationTypes.Line, - id: annotationId, - domainType: AnnotationDomainTypes.XDomain, - dataValues: [{ dataValue: -999, details: 'foo' }], - groupId, - style: DEFAULT_ANNOTATION_LINE_STYLE, - }; - - const emptyOutOfBoundsXDimensions = computeLineAnnotationDimensions( - outOfBoundsXLineAnnotation, - chartDimensions, - chartRotation, - yScales, - continuousScale, - false, - Position.Right, - ); - expect(emptyOutOfBoundsXDimensions).toHaveLength(0); - - const invalidYLineAnnotation: AnnotationSpec = { - chartType: ChartTypes.XYAxis, - specType: SpecTypes.Annotation, - annotationType: AnnotationTypes.Line, - id: annotationId, - domainType: AnnotationDomainTypes.YDomain, - dataValues: [{ dataValue: 'e', details: 'foo' }], - groupId, - style: DEFAULT_ANNOTATION_LINE_STYLE, - }; - - const emptyOutOfBoundsYDimensions = computeLineAnnotationDimensions( - invalidYLineAnnotation, - chartDimensions, - chartRotation, - yScales, - xScale, - false, - Position.Right, - ); - - expect(emptyOutOfBoundsYDimensions).toHaveLength(0); - - const outOfBoundsYLineAnnotation: AnnotationSpec = { - chartType: ChartTypes.XYAxis, - specType: SpecTypes.Annotation, - annotationType: AnnotationTypes.Line, - id: annotationId, - domainType: AnnotationDomainTypes.YDomain, - dataValues: [{ dataValue: -999, details: 'foo' }], - groupId, - style: DEFAULT_ANNOTATION_LINE_STYLE, - }; - - const outOfBoundsYAnn = computeLineAnnotationDimensions( - outOfBoundsYLineAnnotation, - chartDimensions, - chartRotation, - yScales, - xScale, - false, - Position.Right, - ); - - expect(outOfBoundsYAnn).toHaveLength(0); - - const invalidStringYLineAnnotation: AnnotationSpec = { - chartType: ChartTypes.XYAxis, - specType: SpecTypes.Annotation, - annotationType: AnnotationTypes.Line, - id: annotationId, - domainType: AnnotationDomainTypes.YDomain, - dataValues: [{ dataValue: '', details: 'foo' }], - groupId, - style: DEFAULT_ANNOTATION_LINE_STYLE, - }; - - const invalidStringYDimensions = computeLineAnnotationDimensions( - invalidStringYLineAnnotation, - chartDimensions, - chartRotation, - yScales, - continuousScale, - false, - Position.Right, - ); - - expect(invalidStringYDimensions).toEqual([]); - - const validHiddenAnnotation: AnnotationSpec = { - chartType: ChartTypes.XYAxis, - specType: SpecTypes.Annotation, - annotationType: AnnotationTypes.Line, - id: annotationId, - domainType: AnnotationDomainTypes.XDomain, - dataValues: [{ dataValue: 2, details: 'foo' }], - groupId, - style: DEFAULT_ANNOTATION_LINE_STYLE, - hideLines: true, - }; - - const hiddenAnnotationDimensions = computeLineAnnotationDimensions( - validHiddenAnnotation, - chartDimensions, - chartRotation, - yScales, - continuousScale, - false, - Position.Right, - ); - - expect(hiddenAnnotationDimensions).toEqual(null); - }); - - test('should compute the tooltip state for an annotation line', () => { - const cursorPosition: Point = { x: 16, y: 7 }; - const annotationLines: AnnotationLineProps[] = [ - { - linePathPoints: { - start: { x1: 1, y1: 2 }, - end: { x2: 3, y2: 4 }, - }, - details: {}, - marker: { - icon: React.createElement('div'), - color: 'red', - dimension: { width: 10, height: 10 }, - position: { top: 0, left: 0 }, - }, - }, - { - linePathPoints: { - start: { x1: 0, y1: 10 }, - end: { x2: 20, y2: 10 }, - }, - details: {}, - marker: { - icon: React.createElement('div'), - color: 'red', - dimension: { width: 20, height: 20 }, - position: { top: 0, left: 0 }, - }, - }, - ]; - - const localAxesSpecs: AxisSpec[] = []; - - // missing annotation axis (xDomain) - const missingTooltipState = computeLineAnnotationTooltipState( - cursorPosition, - annotationLines, - groupId, - AnnotationDomainTypes.XDomain, - localAxesSpecs, - chartDimensions, - ); - - expect(missingTooltipState).toBeNull(); - - // add axis for xDomain annotation - localAxesSpecs.push(horizontalAxisSpec); - - const xDomainTooltipState = computeLineAnnotationTooltipState( - cursorPosition, - annotationLines, - groupId, - AnnotationDomainTypes.XDomain, - localAxesSpecs, - chartDimensions, - ); - const expectedXDomainTooltipState = { - isVisible: true, - annotationType: AnnotationTypes.Line, - anchor: { - height: 10, - left: 15, - top: 5, - width: 10, - }, - }; - expect(xDomainTooltipState).toMatchObject(expectedXDomainTooltipState); - - // rotated xDomain - const xDomainRotatedTooltipState = computeLineAnnotationTooltipState( - { x: 24, y: 23 }, - annotationLines, - groupId, - AnnotationDomainTypes.XDomain, - localAxesSpecs, - chartDimensions, - ); - const expectedXDomainRotatedTooltipState: AnnotationTooltipState = { - isVisible: true, - anchor: { - left: 15, - top: 5, - }, - annotationType: AnnotationTypes.Line, - }; - - expect(xDomainRotatedTooltipState).toMatchObject(expectedXDomainRotatedTooltipState); - - // add axis for yDomain annotation - localAxesSpecs.push(verticalAxisSpec); - - const yDomainTooltipState = computeLineAnnotationTooltipState( - cursorPosition, - annotationLines, - groupId, - AnnotationDomainTypes.YDomain, - localAxesSpecs, - chartDimensions, - ); - const expectedYDomainTooltipState: AnnotationTooltipState = { - isVisible: true, - anchor: { - left: 15, - top: 5, - }, - annotationType: AnnotationTypes.Line, - }; - - expect(yDomainTooltipState).toMatchObject(expectedYDomainTooltipState); - - const flippedYDomainTooltipState = computeLineAnnotationTooltipState( - { x: 24, y: 23 }, - annotationLines, - groupId, - AnnotationDomainTypes.YDomain, - localAxesSpecs, - chartDimensions, - ); - const expectedFlippedYDomainTooltipState: AnnotationTooltipState = { - isVisible: true, - anchor: { - left: 15, - top: 5, - }, - annotationType: AnnotationTypes.Line, - }; - - expect(flippedYDomainTooltipState).toMatchObject(expectedFlippedYDomainTooltipState); - - const rotatedYDomainTooltipState = computeLineAnnotationTooltipState( - { x: 25, y: 15 }, - annotationLines, - groupId, - AnnotationDomainTypes.YDomain, - localAxesSpecs, - chartDimensions, - ); - const expectedRotatedYDomainTooltipState: AnnotationTooltipState = { - isVisible: true, - anchor: { - left: 15, - top: 5, - }, - annotationType: AnnotationTypes.Line, - }; - - expect(rotatedYDomainTooltipState).toMatchObject(expectedRotatedYDomainTooltipState); - }); - - test('should compute the tooltip state for an annotation', () => { - const annotations: AnnotationSpec[] = []; - const annotationId = 'foo'; - const lineAnnotation: LineAnnotationSpec = { - chartType: ChartTypes.XYAxis, - specType: SpecTypes.Annotation, - annotationType: AnnotationTypes.Line, - id: annotationId, - domainType: AnnotationDomainTypes.YDomain, - dataValues: [{ dataValue: 2, details: 'foo' }], - groupId, - style: DEFAULT_ANNOTATION_LINE_STYLE, - }; - - const cursorPosition: Point = { x: 16, y: 7 }; - - const annotationLines: AnnotationLineProps[] = [ - { - linePathPoints: { start: { x1: 1, y1: 2 }, end: { x2: 3, y2: 4 } }, - details: {}, - marker: { - icon: React.createElement('div'), - color: 'red', - dimension: { width: 10, height: 10 }, - position: { top: 0, left: 0 }, - }, - }, - ]; - const chartRotation: Rotation = 0; - const localAxesSpecs: AxisSpec[] = []; - - const annotationDimensions = new Map(); - annotationDimensions.set(annotationId, annotationLines); - - // missing annotations - const missingSpecTooltipState = computeAnnotationTooltipState( - cursorPosition, - annotationDimensions, - annotations, - chartRotation, - localAxesSpecs, - chartDimensions, - ); - - expect(missingSpecTooltipState).toBe(null); - - // add valid annotation axis - annotations.push(lineAnnotation); - localAxesSpecs.push(verticalAxisSpec); - - // hide tooltipState - lineAnnotation.hideTooltips = true; - - const hideTooltipState = computeAnnotationTooltipState( - cursorPosition, - annotationDimensions, - annotations, - chartRotation, - localAxesSpecs, - chartDimensions, - ); - - expect(hideTooltipState).toBe(null); - - // show tooltipState, hide lines - lineAnnotation.hideTooltips = false; - lineAnnotation.hideLines = true; - - const hideLinesTooltipState = computeAnnotationTooltipState( - cursorPosition, - annotationDimensions, - annotations, - chartRotation, - localAxesSpecs, - chartDimensions, - ); - - expect(hideLinesTooltipState).toBe(null); - - // show tooltipState & lines - lineAnnotation.hideTooltips = false; - lineAnnotation.hideLines = false; - - const tooltipState = computeAnnotationTooltipState( - cursorPosition, - annotationDimensions, - annotations, - chartRotation, - localAxesSpecs, - chartDimensions, - ); - - const expectedTooltipState = { - isVisible: true, - annotationType: AnnotationTypes.Line, - anchor: { - height: 10, - left: 15, - top: 5, - width: 10, - }, - }; - - expect(tooltipState).toMatchObject(expectedTooltipState); - - // rect annotation tooltip - const annotationRectangle: RectAnnotationSpec = { - chartType: ChartTypes.XYAxis, - specType: SpecTypes.Annotation, - id: 'rect', - groupId, - annotationType: AnnotationTypes.Rectangle, - dataValues: [{ coordinates: { x0: 1, x1: 2, y0: 3, y1: 5 } }], - }; - - const rectAnnotations: RectAnnotationSpec[] = []; - rectAnnotations.push(annotationRectangle); - - const rectAnnotationDimensions = [{ rect: { x: 2, y: 3, width: 3, height: 5 } }]; - annotationDimensions.set(annotationRectangle.id, rectAnnotationDimensions); - - const rectTooltipState = computeAnnotationTooltipState( - { x: 18, y: 9 }, - annotationDimensions, - rectAnnotations, - chartRotation, - localAxesSpecs, - chartDimensions, - ); - - expect(rectTooltipState).toMatchObject({ - isVisible: true, - annotationType: AnnotationTypes.Rectangle, - anchor: { - left: 18, - top: 9, - }, - }); - annotationRectangle.hideTooltips = true; - - const rectHideTooltipState = computeAnnotationTooltipState( - { x: 3, y: 4 }, - annotationDimensions, - rectAnnotations, - chartRotation, - localAxesSpecs, - chartDimensions, - ); - - expect(rectHideTooltipState).toBe(null); - }); - - test('should get associated axis for an annotation', () => { - const localAxesSpecs: AxisSpec[] = []; - - const noAxis = getAnnotationAxis(localAxesSpecs, groupId, AnnotationDomainTypes.XDomain, 0); - expect(noAxis).toBeUndefined(); - - localAxesSpecs.push(horizontalAxisSpec); - localAxesSpecs.push(verticalAxisSpec); - - const xAnnotationAxisPosition = getAnnotationAxis(localAxesSpecs, groupId, AnnotationDomainTypes.XDomain, 0); - expect(xAnnotationAxisPosition).toEqual(Position.Bottom); - - const yAnnotationAxisPosition = getAnnotationAxis(localAxesSpecs, groupId, AnnotationDomainTypes.YDomain, 0); - expect(yAnnotationAxisPosition).toEqual(Position.Left); - }); - test('should not compute rectangle annotation dimensions when no yScale', () => { - const yScales: Map = new Map(); - yScales.set(groupId, continuousScale); - - const xScale: Scale = continuousScale; - - const annotationRectangle: RectAnnotationSpec = { - chartType: ChartTypes.XYAxis, - specType: SpecTypes.Annotation, - id: 'rect', - groupId: 'foo', - annotationType: AnnotationTypes.Rectangle, - dataValues: [{ coordinates: { x0: 1, x1: 2, y0: 3, y1: 5 } }], - }; - - const noYScale = computeRectAnnotationDimensions(annotationRectangle, yScales, xScale); - - expect(noYScale).toBe(null); - }); - test('should skip computing rectangle annotation dimensions when annotation data invalid', () => { - const yScales: Map = new Map(); - yScales.set(groupId, continuousScale); - - const xScale: Scale = continuousScale; - - const annotationRectangle: RectAnnotationSpec = { - chartType: ChartTypes.XYAxis, - specType: SpecTypes.Annotation, - id: 'rect', - groupId, - annotationType: AnnotationTypes.Rectangle, - dataValues: [ - { coordinates: { x0: 1, x1: 2, y0: -10, y1: 5 } }, - { coordinates: { x0: null, x1: null, y0: null, y1: null } }, - ], - }; - - const skippedInvalid = computeRectAnnotationDimensions(annotationRectangle, yScales, xScale); - - expect(skippedInvalid).toHaveLength(1); - }); - test('should compute rectangle dimensions shifted for histogram mode', () => { - const yScales: Map = new Map(); - yScales.set( - groupId, - new ScaleContinuous( - { - type: ScaleType.Linear, - domain: continuousData, - range: [minRange, maxRange], - }, - { bandwidth: 0, minInterval: 1 }, - ), - ); - - const xScale: Scale = new ScaleContinuous( - { type: ScaleType.Linear, domain: continuousData, range: [minRange, maxRange] }, - { bandwidth: 72, minInterval: 1 }, - ); - - const annotationRectangle: RectAnnotationSpec = { - chartType: ChartTypes.XYAxis, - specType: SpecTypes.Annotation, - id: 'rect', - groupId, - annotationType: AnnotationTypes.Rectangle, - dataValues: [ - { coordinates: { x0: 1, x1: null, y0: null, y1: null } }, - { coordinates: { x0: null, x1: 1, y0: null, y1: null } }, - { coordinates: { x0: null, x1: null, y0: 1, y1: null } }, - { coordinates: { x0: null, x1: null, y0: null, y1: 1 } }, - ], - }; - - const dimensions = computeRectAnnotationDimensions(annotationRectangle, yScales, xScale); - - const [dims1, dims2, dims3, dims4] = dimensions; - expect(dims1.rect.x).toBe(10); - expect(dims1.rect.y).toBe(100); - expect(dims1.rect.height).toBe(100); - expect(dims1.rect.width).toBeCloseTo(100); - - expect(dims2.rect.x).toBe(0); - expect(dims2.rect.y).toBe(100); - expect(dims2.rect.width).toBe(20); - expect(dims2.rect.height).toBe(100); - - expect(dims3.rect.x).toBe(0); - expect(dims3.rect.y).toBe(100); - expect(dims3.rect.width).toBeCloseTo(110); - expect(dims3.rect.height).toBe(90); - - expect(dims4.rect.x).toBe(0); - expect(dims4.rect.y).toBe(10); - expect(dims4.rect.width).toBeCloseTo(110); - expect(dims4.rect.height).toBe(10); - }); - - test('should determine if a point is within a rectangle annotation', () => { - const cursorPosition = { x: 3, y: 4 }; - - const outOfXBounds: Bounds = { startX: 4, endX: 5, startY: 3, endY: 5 }; - const outOfYBounds: Bounds = { startX: 2, endX: 4, startY: 5, endY: 6 }; - const withinBounds: Bounds = { startX: 2, endX: 4, startY: 3, endY: 5 }; - const withinBoundsReverseXScale: Bounds = { startX: 4, endX: 2, startY: 3, endY: 5 }; - const withinBoundsReverseYScale: Bounds = { startX: 2, endX: 4, startY: 5, endY: 3 }; - - // chart rotation 0 - expect(isWithinRectBounds(cursorPosition, outOfXBounds)).toBe(false); - expect(isWithinRectBounds(cursorPosition, outOfYBounds)).toBe(false); - expect(isWithinRectBounds(cursorPosition, withinBounds)).toBe(true); - expect(isWithinRectBounds(cursorPosition, withinBoundsReverseXScale)).toBe(false); - expect(isWithinRectBounds(cursorPosition, withinBoundsReverseYScale)).toBe(false); - - // chart rotation 180 - expect(isWithinRectBounds(cursorPosition, outOfXBounds)).toBe(false); - expect(isWithinRectBounds(cursorPosition, outOfYBounds)).toBe(false); - expect(isWithinRectBounds(cursorPosition, withinBounds)).toBe(true); - expect(isWithinRectBounds(cursorPosition, withinBoundsReverseXScale)).toBe(false); - expect(isWithinRectBounds(cursorPosition, withinBoundsReverseYScale)).toBe(false); - - // chart rotation 90 - expect(isWithinRectBounds(cursorPosition, outOfXBounds)).toBe(false); - expect(isWithinRectBounds(cursorPosition, outOfYBounds)).toBe(false); - expect(isWithinRectBounds(cursorPosition, withinBounds)).toBe(true); - expect(isWithinRectBounds(cursorPosition, withinBoundsReverseXScale)).toBe(false); - expect(isWithinRectBounds(cursorPosition, withinBoundsReverseYScale)).toBe(false); - - // chart rotation -90 - expect(isWithinRectBounds(cursorPosition, outOfXBounds)).toBe(false); - expect(isWithinRectBounds(cursorPosition, outOfYBounds)).toBe(false); - expect(isWithinRectBounds(cursorPosition, withinBounds)).toBe(true); - expect(isWithinRectBounds(cursorPosition, withinBoundsReverseXScale)).toBe(false); - expect(isWithinRectBounds(cursorPosition, withinBoundsReverseYScale)).toBe(false); - }); - test('should compute tooltip state for rect annotation', () => { - const cursorPosition = { x: 18, y: 9 }; - const annotationRects = [{ rect: { x: 2, y: 3, width: 3, height: 5 } }]; - - const visibleTooltip = computeRectAnnotationTooltipState(cursorPosition, annotationRects, 0, chartDimensions); - const expectedVisibleTooltipState: AnnotationTooltipState = { - isVisible: true, - annotationType: AnnotationTypes.Rectangle, - anchor: { - top: cursorPosition.y, - left: cursorPosition.x, - }, - }; - - expect(visibleTooltip).toEqual(expectedVisibleTooltipState); - }); - - test('should get rotated cursor position', () => { - const cursorPosition = { x: 1, y: 2 }; - - expect(getTransformedCursor(cursorPosition, chartDimensions, 0)).toEqual(cursorPosition); - expect(getTransformedCursor(cursorPosition, chartDimensions, 90)).toEqual({ x: 2, y: 9 }); - expect(getTransformedCursor(cursorPosition, chartDimensions, -90)).toEqual({ x: 18, y: 1 }); - expect(getTransformedCursor(cursorPosition, chartDimensions, 180)).toEqual({ x: 9, y: 18 }); - }); - - describe('#invertTranformedCursor', () => { - const cursorPosition = { x: 1, y: 2 }; - - it.each([0, 90, -90, 180])('Should invert rotated cursor - rotation %d', (rotation) => { - expect( - invertTranformedCursor( - getTransformedCursor(cursorPosition, chartDimensions, rotation), - chartDimensions, - rotation, - ), - ).toEqual(cursorPosition); - }); - - it.each([0, 90, -90, 180])('Should invert rotated projected cursor - rotation %d', (rotation) => { - expect( - invertTranformedCursor( - getTransformedCursor(cursorPosition, chartDimensions, rotation, true), - chartDimensions, - rotation, - true, - ), - ).toEqual(cursorPosition); - }); + expect(dimensions.get('foo-line')).toEqual(expectedDimensions); }); + // + // test('should compute line annotation dimensions for yDomain on a yScale (chartRotation 0, right axis)', () => { + // const chartRotation: Rotation = 0; + // const yScales: Map = new Map(); + // yScales.set(groupId, continuousScale); + // + // const xScale: Scale = ordinalScale; + // + // const annotationId = 'foo-line'; + // const lineAnnotation: LineAnnotationSpec = { + // chartType: ChartTypes.XYAxis, + // specType: SpecTypes.Annotation, + // annotationType: AnnotationTypes.Line, + // id: annotationId, + // domainType: AnnotationDomainTypes.YDomain, + // dataValues: [{ dataValue: 2, details: 'foo' }], + // groupId, + // style: DEFAULT_ANNOTATION_LINE_STYLE, + // }; + // + // const dimensions = computeLineAnnotationDimensions( + // lineAnnotation, + // chartDimensions, + // chartRotation, + // yScales, + // xScale, + // false, + // Position.Right, + // ); + // const expectedDimensions: AnnotationLineProps[] = [ + // MockAnnotationLineProps.default({ + // linePathPoints: { + // start: { x1: 0, y1: 20 }, + // end: { x2: 10, y2: 20 }, + // }, + // details: { detailsText: 'foo', headerText: '2' }, + // }), + // ]; + // expect(dimensions).toEqual(expectedDimensions); + // }); + // + // test('should compute line annotation dimensions for yDomain on a yScale (chartRotation 90)', () => { + // const chartRotation: Rotation = 90; + // const yScales: Map = new Map(); + // yScales.set(groupId, continuousScale); + // + // const xScale: Scale = ordinalScale; + // + // const annotationId = 'foo-line'; + // const lineAnnotation: LineAnnotationSpec = { + // chartType: ChartTypes.XYAxis, + // specType: SpecTypes.Annotation, + // annotationType: AnnotationTypes.Line, + // id: annotationId, + // domainType: AnnotationDomainTypes.YDomain, + // dataValues: [{ dataValue: 2, details: 'foo' }], + // groupId, + // style: DEFAULT_ANNOTATION_LINE_STYLE, + // }; + // + // const dimensions = computeLineAnnotationDimensions( + // lineAnnotation, + // chartDimensions, + // chartRotation, + // yScales, + // xScale, + // false, + // Position.Left, + // ); + // const expectedDimensions: AnnotationLineProps[] = [ + // MockAnnotationLineProps.default({ + // linePathPoints: { + // start: { x1: 0, y1: 20 }, + // end: { x2: 20, y2: 20 }, + // }, + // details: { detailsText: 'foo', headerText: '2' }, + // }), + // ]; + // expect(dimensions).toEqual(expectedDimensions); + // }); + // + // test('should not compute line annotation dimensions for yDomain if no corresponding yScale', () => { + // const chartRotation: Rotation = 0; + // const yScales: Map = new Map(); + // const xScale: Scale = ordinalScale; + // + // const annotationId = 'foo-line'; + // const lineAnnotation: LineAnnotationSpec = { + // chartType: ChartTypes.XYAxis, + // specType: SpecTypes.Annotation, + // annotationType: AnnotationTypes.Line, + // id: annotationId, + // domainType: AnnotationDomainTypes.YDomain, + // dataValues: [], + // groupId, + // style: DEFAULT_ANNOTATION_LINE_STYLE, + // }; + // + // const dimensions = computeLineAnnotationDimensions( + // lineAnnotation, + // chartDimensions, + // chartRotation, + // yScales, + // xScale, + // false, + // Position.Left, + // ); + // expect(dimensions).toEqual(null); + // }); + // + // test('should compute line annotation dimensions for xDomain (chartRotation 0, ordinal scale)', () => { + // const chartRotation: Rotation = 0; + // const yScales: Map = new Map(); + // const xScale: Scale = ordinalScale; + // + // const annotationId = 'foo-line'; + // const lineAnnotation: LineAnnotationSpec = { + // chartType: ChartTypes.XYAxis, + // specType: SpecTypes.Annotation, + // annotationType: AnnotationTypes.Line, + // id: annotationId, + // domainType: AnnotationDomainTypes.XDomain, + // dataValues: [{ dataValue: 'a', details: 'foo' }], + // groupId, + // style: DEFAULT_ANNOTATION_LINE_STYLE, + // }; + // + // const dimensions = computeLineAnnotationDimensions( + // lineAnnotation, + // chartDimensions, + // chartRotation, + // yScales, + // xScale, + // false, + // Position.Left, + // ); + // const expectedDimensions: AnnotationLineProps[] = [ + // MockAnnotationLineProps.default({ + // linePathPoints: { + // start: { x1: 12.5, y1: 0 }, + // end: { x2: 12.5, y2: 20 }, + // }, + // details: { detailsText: 'foo', headerText: 'a' }, + // }), + // ]; + // expect(dimensions).toEqual(expectedDimensions); + // }); + // + // test('should compute line annotation dimensions for xDomain (chartRotation 0, continuous scale, top axis)', () => { + // const chartRotation: Rotation = 0; + // const yScales: Map = new Map(); + // const xScale: Scale = continuousScale; + // + // const annotationId = 'foo-line'; + // const lineAnnotation: LineAnnotationSpec = { + // chartType: ChartTypes.XYAxis, + // specType: SpecTypes.Annotation, + // annotationType: AnnotationTypes.Line, + // id: annotationId, + // domainType: AnnotationDomainTypes.XDomain, + // dataValues: [{ dataValue: 2, details: 'foo' }], + // groupId, + // style: DEFAULT_ANNOTATION_LINE_STYLE, + // }; + // + // const dimensions = computeLineAnnotationDimensions( + // lineAnnotation, + // chartDimensions, + // chartRotation, + // yScales, + // xScale, + // false, + // Position.Top, + // ); + // const expectedDimensions: AnnotationLineProps[] = [ + // MockAnnotationLineProps.default({ + // linePathPoints: { + // start: { x1: 25, y1: 0 }, + // end: { x2: 25, y2: 20 }, + // }, + // details: { detailsText: 'foo', headerText: '2' }, + // }), + // ]; + // expect(dimensions).toEqual(expectedDimensions); + // }); + // + // test('should compute line annotation dimensions for xDomain (chartRotation 0, continuous scale, bottom axis)', () => { + // const chartRotation: Rotation = 0; + // const yScales: Map = new Map(); + // const xScale: Scale = continuousScale; + // + // const annotationId = 'foo-line'; + // const lineAnnotation: LineAnnotationSpec = { + // chartType: ChartTypes.XYAxis, + // specType: SpecTypes.Annotation, + // annotationType: AnnotationTypes.Line, + // id: annotationId, + // domainType: AnnotationDomainTypes.XDomain, + // dataValues: [{ dataValue: 2, details: 'foo' }], + // groupId, + // style: DEFAULT_ANNOTATION_LINE_STYLE, + // }; + // + // const dimensions = computeLineAnnotationDimensions( + // lineAnnotation, + // chartDimensions, + // chartRotation, + // yScales, + // xScale, + // false, + // Position.Bottom, + // ); + // const expectedDimensions: AnnotationLineProps[] = [ + // MockAnnotationLineProps.default({ + // linePathPoints: { + // start: { x1: 25, y1: 0 }, + // end: { x2: 25, y2: 20 }, + // }, + // details: { detailsText: 'foo', headerText: '2' }, + // + // }), + // ]; + // expect(dimensions).toEqual(expectedDimensions); + // }); + // + // test('should compute line annotation dimensions for xDomain on a xScale (chartRotation 90, ordinal scale)', () => { + // const chartRotation: Rotation = 90; + // const yScales: Map = new Map(); + // + // const xScale: Scale = ordinalScale; + // + // const annotationId = 'foo-line'; + // const lineAnnotation: LineAnnotationSpec = { + // chartType: ChartTypes.XYAxis, + // specType: SpecTypes.Annotation, + // annotationType: AnnotationTypes.Line, + // id: annotationId, + // domainType: AnnotationDomainTypes.XDomain, + // dataValues: [{ dataValue: 'a', details: 'foo' }], + // groupId, + // style: DEFAULT_ANNOTATION_LINE_STYLE, + // }; + // + // const dimensions = computeLineAnnotationDimensions( + // lineAnnotation, + // chartDimensions, + // chartRotation, + // yScales, + // xScale, + // false, + // Position.Left, + // ); + // const expectedDimensions: AnnotationLineProps[] = [ + // MockAnnotationLineProps.default({ + // linePathPoints: { + // start: { x1: 12.5, y1: 0 }, + // end: { x2: 12.5, y2: 10 }, + // }, + // details: { detailsText: 'foo', headerText: 'a' }, + // + // }), + // ]; + // expect(dimensions).toEqual(expectedDimensions); + // }); + // + // test('should compute line annotation dimensions for xDomain on a xScale (chartRotation 90, continuous scale)', () => { + // const chartRotation: Rotation = 90; + // const yScales: Map = new Map(); + // + // const xScale: Scale = continuousScale; + // + // const annotationId = 'foo-line'; + // const lineAnnotation: LineAnnotationSpec = { + // chartType: ChartTypes.XYAxis, + // specType: SpecTypes.Annotation, + // annotationType: AnnotationTypes.Line, + // id: annotationId, + // domainType: AnnotationDomainTypes.XDomain, + // dataValues: [{ dataValue: 2, details: 'foo' }], + // groupId, + // style: DEFAULT_ANNOTATION_LINE_STYLE, + // }; + // + // const dimensions = computeLineAnnotationDimensions( + // lineAnnotation, + // chartDimensions, + // chartRotation, + // yScales, + // xScale, + // false, + // Position.Left, + // ); + // const expectedDimensions: AnnotationLineProps[] = [ + // MockAnnotationLineProps.default({ + // linePathPoints: { + // start: { x1: 25, y1: 0 }, + // end: { x2: 25, y2: 10 }, + // }, + // details: { detailsText: 'foo', headerText: '2' }, + // + // }), + // ]; + // expect(dimensions).toEqual(expectedDimensions); + // }); + // + // test('should compute line annotation dimensions for xDomain on a xScale (chartRotation -90, continuous scale)', () => { + // const chartRotation: Rotation = -90; + // const yScales: Map = new Map(); + // + // const xScale: Scale = continuousScale; + // + // const annotationId = 'foo-line'; + // const lineAnnotation: LineAnnotationSpec = { + // chartType: ChartTypes.XYAxis, + // specType: SpecTypes.Annotation, + // annotationType: AnnotationTypes.Line, + // id: annotationId, + // domainType: AnnotationDomainTypes.XDomain, + // dataValues: [{ dataValue: 2, details: 'foo' }], + // groupId, + // style: DEFAULT_ANNOTATION_LINE_STYLE, + // }; + // + // const dimensions = computeLineAnnotationDimensions( + // lineAnnotation, + // chartDimensions, + // chartRotation, + // yScales, + // xScale, + // false, + // Position.Left, + // ); + // const expectedDimensions: AnnotationLineProps[] = [ + // MockAnnotationLineProps.default({ + // linePathPoints: { + // start: { x1: 25, y1: 0 }, + // end: { x2: 25, y2: 10 }, + // }, + // details: { detailsText: 'foo', headerText: '2' }, + // + // }), + // ]; + // expect(dimensions).toEqual(expectedDimensions); + // }); + // + // test('should compute line annotation dimensions for xDomain (chartRotation 180, continuous scale, top axis)', () => { + // const chartRotation: Rotation = 180; + // const yScales: Map = new Map(); + // + // const xScale: Scale = continuousScale; + // + // const annotationId = 'foo-line'; + // const lineAnnotation: LineAnnotationSpec = { + // chartType: ChartTypes.XYAxis, + // specType: SpecTypes.Annotation, + // annotationType: AnnotationTypes.Line, + // id: annotationId, + // domainType: AnnotationDomainTypes.XDomain, + // dataValues: [{ dataValue: 2, details: 'foo' }], + // groupId, + // style: DEFAULT_ANNOTATION_LINE_STYLE, + // }; + // + // const dimensions = computeLineAnnotationDimensions( + // lineAnnotation, + // chartDimensions, + // chartRotation, + // yScales, + // xScale, + // false, + // Position.Top, + // ); + // const expectedDimensions: AnnotationLineProps[] = [ + // MockAnnotationLineProps.default({ + // linePathPoints: { + // start: { x1: 25, y1: 0 }, + // end: { x2: 25, y2: 20 }, + // }, + // details: { detailsText: 'foo', headerText: '2' }, + // }, + // ]; + // expect(dimensions).toEqual(expectedDimensions); + // }); + // + // test('should compute line annotation dimensions for xDomain (chartRotation 180, continuous scale, bottom axis)', () => { + // const chartRotation: Rotation = 180; + // const yScales: Map = new Map(); + // const xScale: Scale = continuousScale; + // + // const annotationId = 'foo-line'; + // const lineAnnotation: LineAnnotationSpec = { + // chartType: ChartTypes.XYAxis, + // specType: SpecTypes.Annotation, + // annotationType: AnnotationTypes.Line, + // id: annotationId, + // domainType: AnnotationDomainTypes.XDomain, + // dataValues: [{ dataValue: 2, details: 'foo' }], + // groupId, + // style: DEFAULT_ANNOTATION_LINE_STYLE, + // }; + // + // const dimensions = computeLineAnnotationDimensions( + // lineAnnotation, + // chartDimensions, + // chartRotation, + // yScales, + // xScale, + // false, + // Position.Bottom, + // ); + // const expectedDimensions: AnnotationLineProps[] = [ + // MockAnnotationLineProps.default({ + // details: { detailsText: 'foo', headerText: '2' }, + // linePathPoints: { + // start: { x1: 25, y1: 0 }, + // end: { x2: 25, y2: 20 }, + // + // }, + // }), + // ]; + // expect(dimensions).toEqual(expectedDimensions); + // }); + // + // test('should not compute annotation line values for invalid data values or AnnotationSpec.hideLines', () => { + // const chartRotation: Rotation = 0; + // const yScales: Map = new Map(); + // yScales.set(groupId, continuousScale); + // + // const xScale: Scale = ordinalScale; + // + // const annotationId = 'foo-line'; + // const invalidXLineAnnotation: AnnotationSpec = { + // chartType: ChartTypes.XYAxis, + // specType: SpecTypes.Annotation, + // annotationType: AnnotationTypes.Line, + // id: annotationId, + // domainType: AnnotationDomainTypes.XDomain, + // dataValues: [{ dataValue: 'e', details: 'foo' }], + // groupId, + // style: DEFAULT_ANNOTATION_LINE_STYLE, + // }; + // + // const emptyXDimensions = computeLineAnnotationDimensions( + // invalidXLineAnnotation, + // chartDimensions, + // chartRotation, + // yScales, + // xScale, + // false, + // Position.Right, + // ); + // + // expect(emptyXDimensions).toEqual([]); + // + // const invalidStringXLineAnnotation: AnnotationSpec = { + // chartType: ChartTypes.XYAxis, + // specType: SpecTypes.Annotation, + // annotationType: AnnotationTypes.Line, + // id: annotationId, + // domainType: AnnotationDomainTypes.XDomain, + // dataValues: [{ dataValue: '', details: 'foo' }], + // groupId, + // style: DEFAULT_ANNOTATION_LINE_STYLE, + // }; + // + // const invalidStringXDimensions = computeLineAnnotationDimensions( + // invalidStringXLineAnnotation, + // chartDimensions, + // chartRotation, + // yScales, + // continuousScale, + // false, + // Position.Right, + // ); + // + // expect(invalidStringXDimensions).toEqual([]); + // + // const outOfBoundsXLineAnnotation: AnnotationSpec = { + // chartType: ChartTypes.XYAxis, + // specType: SpecTypes.Annotation, + // annotationType: AnnotationTypes.Line, + // id: annotationId, + // domainType: AnnotationDomainTypes.XDomain, + // dataValues: [{ dataValue: -999, details: 'foo' }], + // groupId, + // style: DEFAULT_ANNOTATION_LINE_STYLE, + // }; + // + // const emptyOutOfBoundsXDimensions = computeLineAnnotationDimensions( + // outOfBoundsXLineAnnotation, + // chartDimensions, + // chartRotation, + // yScales, + // continuousScale, + // false, + // Position.Right, + // ); + // expect(emptyOutOfBoundsXDimensions).toHaveLength(0); + // + // const invalidYLineAnnotation: AnnotationSpec = { + // chartType: ChartTypes.XYAxis, + // specType: SpecTypes.Annotation, + // annotationType: AnnotationTypes.Line, + // id: annotationId, + // domainType: AnnotationDomainTypes.YDomain, + // dataValues: [{ dataValue: 'e', details: 'foo' }], + // groupId, + // style: DEFAULT_ANNOTATION_LINE_STYLE, + // }; + // + // const emptyOutOfBoundsYDimensions = computeLineAnnotationDimensions( + // invalidYLineAnnotation, + // chartDimensions, + // chartRotation, + // yScales, + // xScale, + // false, + // Position.Right, + // ); + // + // expect(emptyOutOfBoundsYDimensions).toHaveLength(0); + // + // const outOfBoundsYLineAnnotation: AnnotationSpec = { + // chartType: ChartTypes.XYAxis, + // specType: SpecTypes.Annotation, + // annotationType: AnnotationTypes.Line, + // id: annotationId, + // domainType: AnnotationDomainTypes.YDomain, + // dataValues: [{ dataValue: -999, details: 'foo' }], + // groupId, + // style: DEFAULT_ANNOTATION_LINE_STYLE, + // }; + // + // const outOfBoundsYAnn = computeLineAnnotationDimensions( + // outOfBoundsYLineAnnotation, + // chartDimensions, + // chartRotation, + // yScales, + // xScale, + // false, + // Position.Right, + // ); + // + // expect(outOfBoundsYAnn).toHaveLength(0); + // + // const invalidStringYLineAnnotation: AnnotationSpec = { + // chartType: ChartTypes.XYAxis, + // specType: SpecTypes.Annotation, + // annotationType: AnnotationTypes.Line, + // id: annotationId, + // domainType: AnnotationDomainTypes.YDomain, + // dataValues: [{ dataValue: '', details: 'foo' }], + // groupId, + // style: DEFAULT_ANNOTATION_LINE_STYLE, + // }; + // + // const invalidStringYDimensions = computeLineAnnotationDimensions( + // invalidStringYLineAnnotation, + // chartDimensions, + // chartRotation, + // yScales, + // continuousScale, + // false, + // Position.Right, + // ); + // + // expect(invalidStringYDimensions).toEqual([]); + // + // const validHiddenAnnotation: AnnotationSpec = { + // chartType: ChartTypes.XYAxis, + // specType: SpecTypes.Annotation, + // annotationType: AnnotationTypes.Line, + // id: annotationId, + // domainType: AnnotationDomainTypes.XDomain, + // dataValues: [{ dataValue: 2, details: 'foo' }], + // groupId, + // style: DEFAULT_ANNOTATION_LINE_STYLE, + // hideLines: true, + // }; + // + // const hiddenAnnotationDimensions = computeLineAnnotationDimensions( + // validHiddenAnnotation, + // chartDimensions, + // chartRotation, + // yScales, + // continuousScale, + // false, + // Position.Right, + // ); + // + // expect(hiddenAnnotationDimensions).toEqual(null); + // }); + // + // test('should compute the tooltip state for an annotation line', () => { + // const cursorPosition: Point = { x: 16, y: 7 }; + // const annotationLines: AnnotationLineProps[] = [ + // MockAnnotationLineProps.default({ + // linePathPoints: { + // start: { x1: 1, y1: 2 }, + // end: { x2: 3, y2: 4 }, + // }, + // marker: { + // icon: React.createElement('div'), + // color: 'red', + // dimension: { width: 10, height: 10 }, + // position: { top: 0, left: 0 }, + // }, + // }), + // MockAnnotationLineProps.default({ + // linePathPoints: { + // start: { x1: 0, y1: 10 }, + // end: { x2: 20, y2: 10 }, + // }, + // marker: { + // icon: React.createElement('div'), + // color: 'red', + // dimension: { width: 20, height: 20 }, + // position: { top: 0, left: 0 }, + // }, + // }), + // ]; + // + // const localAxesSpecs: AxisSpec[] = []; + // + // // missing annotation axis (xDomain) + // const missingTooltipState = computeLineAnnotationTooltipState( + // cursorPosition, + // annotationLines, + // groupId, + // AnnotationDomainTypes.XDomain, + // localAxesSpecs, + // chartDimensions, + // ); + // + // expect(missingTooltipState).toBeNull(); + // + // // add axis for xDomain annotation + // localAxesSpecs.push(horizontalAxisSpec); + // + // const xDomainTooltipState = computeLineAnnotationTooltipState( + // cursorPosition, + // annotationLines, + // groupId, + // AnnotationDomainTypes.XDomain, + // localAxesSpecs, + // chartDimensions, + // ); + // const expectedXDomainTooltipState = { + // isVisible: true, + // annotationType: AnnotationTypes.Line, + // anchor: { + // height: 10, + // left: 15, + // top: 5, + // width: 10, + // }, + // }; + // expect(xDomainTooltipState).toMatchObject(expectedXDomainTooltipState); + // + // // rotated xDomain + // const xDomainRotatedTooltipState = computeLineAnnotationTooltipState( + // { x: 24, y: 23 }, + // annotationLines, + // groupId, + // AnnotationDomainTypes.XDomain, + // localAxesSpecs, + // chartDimensions, + // ); + // const expectedXDomainRotatedTooltipState: AnnotationTooltipState = { + // isVisible: true, + // anchor: { + // left: 15, + // top: 5, + // }, + // annotationType: AnnotationTypes.Line, + // }; + // + // expect(xDomainRotatedTooltipState).toMatchObject(expectedXDomainRotatedTooltipState); + // + // // add axis for yDomain annotation + // localAxesSpecs.push(verticalAxisSpec); + // + // const yDomainTooltipState = computeLineAnnotationTooltipState( + // cursorPosition, + // annotationLines, + // groupId, + // AnnotationDomainTypes.YDomain, + // localAxesSpecs, + // chartDimensions, + // ); + // const expectedYDomainTooltipState: AnnotationTooltipState = { + // isVisible: true, + // anchor: { + // left: 15, + // top: 5, + // }, + // annotationType: AnnotationTypes.Line, + // }; + // + // expect(yDomainTooltipState).toMatchObject(expectedYDomainTooltipState); + // + // const flippedYDomainTooltipState = computeLineAnnotationTooltipState( + // { x: 24, y: 23 }, + // annotationLines, + // groupId, + // AnnotationDomainTypes.YDomain, + // localAxesSpecs, + // chartDimensions, + // ); + // const expectedFlippedYDomainTooltipState: AnnotationTooltipState = { + // isVisible: true, + // anchor: { + // left: 15, + // top: 5, + // }, + // annotationType: AnnotationTypes.Line, + // }; + // + // expect(flippedYDomainTooltipState).toMatchObject(expectedFlippedYDomainTooltipState); + // + // const rotatedYDomainTooltipState = computeLineAnnotationTooltipState( + // { x: 25, y: 15 }, + // annotationLines, + // groupId, + // AnnotationDomainTypes.YDomain, + // localAxesSpecs, + // chartDimensions, + // ); + // const expectedRotatedYDomainTooltipState: AnnotationTooltipState = { + // isVisible: true, + // anchor: { + // left: 15, + // top: 5, + // }, + // annotationType: AnnotationTypes.Line, + // }; + // + // expect(rotatedYDomainTooltipState).toMatchObject(expectedRotatedYDomainTooltipState); + // }); + // + // test('should compute the tooltip state for an annotation', () => { + // const annotations: AnnotationSpec[] = []; + // const annotationId = 'foo'; + // const lineAnnotation: LineAnnotationSpec = { + // chartType: ChartTypes.XYAxis, + // specType: SpecTypes.Annotation, + // annotationType: AnnotationTypes.Line, + // id: annotationId, + // domainType: AnnotationDomainTypes.YDomain, + // dataValues: [{ dataValue: 2, details: 'foo' }], + // groupId, + // style: DEFAULT_ANNOTATION_LINE_STYLE, + // }; + // + // const cursorPosition: Point = { x: 16, y: 7 }; + // + // const annotationLines: AnnotationLineProps[] = [ + // MockAnnotationLineProps.default({ + // linePathPoints: { + // start: { x1: 1, y1: 2 }, + // end: { x2: 3, y2: 4 }, + // }, + // marker: { + // icon: React.createElement('div'), + // color: 'red', + // dimension: { width: 10, height: 10 }, + // position: { top: 0, left: 0 }, + // }, + // }), + // ]; + // const chartRotation: Rotation = 0; + // const localAxesSpecs: AxisSpec[] = []; + // + // const annotationDimensions = new Map(); + // annotationDimensions.set(annotationId, annotationLines); + // + // // missing annotations + // const missingSpecTooltipState = computeAnnotationTooltipState( + // cursorPosition, + // annotationDimensions, + // annotations, + // chartRotation, + // localAxesSpecs, + // chartDimensions, + // ); + // + // expect(missingSpecTooltipState).toBe(null); + // + // // add valid annotation axis + // annotations.push(lineAnnotation); + // localAxesSpecs.push(verticalAxisSpec); + // + // // hide tooltipState + // lineAnnotation.hideTooltips = true; + // + // const hideTooltipState = computeAnnotationTooltipState( + // cursorPosition, + // annotationDimensions, + // annotations, + // chartRotation, + // localAxesSpecs, + // chartDimensions, + // ); + // + // expect(hideTooltipState).toBe(null); + // + // // show tooltipState, hide lines + // lineAnnotation.hideTooltips = false; + // lineAnnotation.hideLines = true; + // + // const hideLinesTooltipState = computeAnnotationTooltipState( + // cursorPosition, + // annotationDimensions, + // annotations, + // chartRotation, + // localAxesSpecs, + // chartDimensions, + // ); + // + // expect(hideLinesTooltipState).toBe(null); + // + // // show tooltipState & lines + // lineAnnotation.hideTooltips = false; + // lineAnnotation.hideLines = false; + // + // const tooltipState = computeAnnotationTooltipState( + // cursorPosition, + // annotationDimensions, + // annotations, + // chartRotation, + // localAxesSpecs, + // chartDimensions, + // ); + // + // const expectedTooltipState = { + // isVisible: true, + // annotationType: AnnotationTypes.Line, + // anchor: { + // height: 10, + // left: 15, + // top: 5, + // width: 10, + // }, + // }; + // + // expect(tooltipState).toMatchObject(expectedTooltipState); + // + // // rect annotation tooltip + // const annotationRectangle: RectAnnotationSpec = { + // chartType: ChartTypes.XYAxis, + // specType: SpecTypes.Annotation, + // id: 'rect', + // groupId, + // annotationType: AnnotationTypes.Rectangle, + // dataValues: [{ coordinates: { x0: 1, x1: 2, y0: 3, y1: 5 } }], + // }; + // + // const rectAnnotations: RectAnnotationSpec[] = []; + // rectAnnotations.push(annotationRectangle); + // + // const rectAnnotationDimensions = [{ rect: { x: 2, y: 3, width: 3, height: 5 } }]; + // annotationDimensions.set(annotationRectangle.id, rectAnnotationDimensions); + // + // const rectTooltipState = computeAnnotationTooltipState( + // { x: 18, y: 9 }, + // annotationDimensions, + // rectAnnotations, + // chartRotation, + // localAxesSpecs, + // chartDimensions, + // ); + // + // expect(rectTooltipState).toMatchObject({ + // isVisible: true, + // annotationType: AnnotationTypes.Rectangle, + // anchor: { + // left: 18, + // top: 9, + // }, + // }); + // annotationRectangle.hideTooltips = true; + // + // const rectHideTooltipState = computeAnnotationTooltipState( + // { x: 3, y: 4 }, + // annotationDimensions, + // rectAnnotations, + // chartRotation, + // localAxesSpecs, + // chartDimensions, + // ); + // + // expect(rectHideTooltipState).toBe(null); + // }); + // + // test('should get associated axis for an annotation', () => { + // const localAxesSpecs: AxisSpec[] = []; + // + // const noAxis = getAnnotationAxis(localAxesSpecs, groupId, AnnotationDomainTypes.XDomain, 0); + // expect(noAxis).toBeUndefined(); + // + // localAxesSpecs.push(horizontalAxisSpec); + // localAxesSpecs.push(verticalAxisSpec); + // + // const xAnnotationAxisPosition = getAnnotationAxis(localAxesSpecs, groupId, AnnotationDomainTypes.XDomain, 0); + // expect(xAnnotationAxisPosition).toEqual(Position.Bottom); + // + // const yAnnotationAxisPosition = getAnnotationAxis(localAxesSpecs, groupId, AnnotationDomainTypes.YDomain, 0); + // expect(yAnnotationAxisPosition).toEqual(Position.Left); + // }); + // test('should not compute rectangle annotation dimensions when no yScale', () => { + // const yScales: Map = new Map(); + // yScales.set(groupId, continuousScale); + // + // const xScale: Scale = continuousScale; + // + // const annotationRectangle: RectAnnotationSpec = { + // chartType: ChartTypes.XYAxis, + // specType: SpecTypes.Annotation, + // id: 'rect', + // groupId: 'foo', + // annotationType: AnnotationTypes.Rectangle, + // dataValues: [{ coordinates: { x0: 1, x1: 2, y0: 3, y1: 5 } }], + // }; + // + // const noYScale = computeRectAnnotationDimensions(annotationRectangle, yScales, xScale); + // + // expect(noYScale).toBe(null); + // }); + // test('should skip computing rectangle annotation dimensions when annotation data invalid', () => { + // const yScales: Map = new Map(); + // yScales.set(groupId, continuousScale); + // + // const xScale: Scale = continuousScale; + // + // const annotationRectangle: RectAnnotationSpec = { + // chartType: ChartTypes.XYAxis, + // specType: SpecTypes.Annotation, + // id: 'rect', + // groupId, + // annotationType: AnnotationTypes.Rectangle, + // dataValues: [ + // { coordinates: { x0: 1, x1: 2, y0: -10, y1: 5 } }, + // { coordinates: { x0: null, x1: null, y0: null, y1: null } }, + // ], + // }; + // + // const skippedInvalid = computeRectAnnotationDimensions(annotationRectangle, yScales, xScale); + // + // expect(skippedInvalid).toHaveLength(1); + // }); + // test('should compute rectangle dimensions shifted for histogram mode', () => { + // const yScales: Map = new Map(); + // yScales.set( + // groupId, + // new ScaleContinuous( + // { + // type: ScaleType.Linear, + // domain: continuousData, + // range: [minRange, maxRange], + // }, + // { bandwidth: 0, minInterval: 1 }, + // ), + // ); + // + // const xScale: Scale = new ScaleContinuous( + // { type: ScaleType.Linear, domain: continuousData, range: [minRange, maxRange] }, + // { bandwidth: 72, minInterval: 1 }, + // ); + // + // const annotationRectangle: RectAnnotationSpec = { + // chartType: ChartTypes.XYAxis, + // specType: SpecTypes.Annotation, + // id: 'rect', + // groupId, + // annotationType: AnnotationTypes.Rectangle, + // dataValues: [ + // { coordinates: { x0: 1, x1: null, y0: null, y1: null } }, + // { coordinates: { x0: null, x1: 1, y0: null, y1: null } }, + // { coordinates: { x0: null, x1: null, y0: 1, y1: null } }, + // { coordinates: { x0: null, x1: null, y0: null, y1: 1 } }, + // ], + // }; + // + // const dimensions = computeRectAnnotationDimensions(annotationRectangle, yScales, xScale); + // + // const [dims1, dims2, dims3, dims4] = dimensions; + // expect(dims1.rect.x).toBe(10); + // expect(dims1.rect.y).toBe(100); + // expect(dims1.rect.height).toBe(100); + // expect(dims1.rect.width).toBeCloseTo(100); + // + // expect(dims2.rect.x).toBe(0); + // expect(dims2.rect.y).toBe(100); + // expect(dims2.rect.width).toBe(20); + // expect(dims2.rect.height).toBe(100); + // + // expect(dims3.rect.x).toBe(0); + // expect(dims3.rect.y).toBe(100); + // expect(dims3.rect.width).toBeCloseTo(110); + // expect(dims3.rect.height).toBe(90); + // + // expect(dims4.rect.x).toBe(0); + // expect(dims4.rect.y).toBe(10); + // expect(dims4.rect.width).toBeCloseTo(110); + // expect(dims4.rect.height).toBe(10); + // }); + // + // test('should determine if a point is within a rectangle annotation', () => { + // const cursorPosition = { x: 3, y: 4 }; + // + // const outOfXBounds: Bounds = { startX: 4, endX: 5, startY: 3, endY: 5 }; + // const outOfYBounds: Bounds = { startX: 2, endX: 4, startY: 5, endY: 6 }; + // const withinBounds: Bounds = { startX: 2, endX: 4, startY: 3, endY: 5 }; + // const withinBoundsReverseXScale: Bounds = { startX: 4, endX: 2, startY: 3, endY: 5 }; + // const withinBoundsReverseYScale: Bounds = { startX: 2, endX: 4, startY: 5, endY: 3 }; + // + // // chart rotation 0 + // expect(isWithinRectBounds(cursorPosition, outOfXBounds)).toBe(false); + // expect(isWithinRectBounds(cursorPosition, outOfYBounds)).toBe(false); + // expect(isWithinRectBounds(cursorPosition, withinBounds)).toBe(true); + // expect(isWithinRectBounds(cursorPosition, withinBoundsReverseXScale)).toBe(false); + // expect(isWithinRectBounds(cursorPosition, withinBoundsReverseYScale)).toBe(false); + // + // // chart rotation 180 + // expect(isWithinRectBounds(cursorPosition, outOfXBounds)).toBe(false); + // expect(isWithinRectBounds(cursorPosition, outOfYBounds)).toBe(false); + // expect(isWithinRectBounds(cursorPosition, withinBounds)).toBe(true); + // expect(isWithinRectBounds(cursorPosition, withinBoundsReverseXScale)).toBe(false); + // expect(isWithinRectBounds(cursorPosition, withinBoundsReverseYScale)).toBe(false); + // + // // chart rotation 90 + // expect(isWithinRectBounds(cursorPosition, outOfXBounds)).toBe(false); + // expect(isWithinRectBounds(cursorPosition, outOfYBounds)).toBe(false); + // expect(isWithinRectBounds(cursorPosition, withinBounds)).toBe(true); + // expect(isWithinRectBounds(cursorPosition, withinBoundsReverseXScale)).toBe(false); + // expect(isWithinRectBounds(cursorPosition, withinBoundsReverseYScale)).toBe(false); + // + // // chart rotation -90 + // expect(isWithinRectBounds(cursorPosition, outOfXBounds)).toBe(false); + // expect(isWithinRectBounds(cursorPosition, outOfYBounds)).toBe(false); + // expect(isWithinRectBounds(cursorPosition, withinBounds)).toBe(true); + // expect(isWithinRectBounds(cursorPosition, withinBoundsReverseXScale)).toBe(false); + // expect(isWithinRectBounds(cursorPosition, withinBoundsReverseYScale)).toBe(false); + // }); + // test('should compute tooltip state for rect annotation', () => { + // const cursorPosition = { x: 18, y: 9 }; + // const annotationRects = [{ rect: { x: 2, y: 3, width: 3, height: 5 } }]; + // + // const visibleTooltip = computeRectAnnotationTooltipState(cursorPosition, annotationRects, 0, chartDimensions); + // const expectedVisibleTooltipState: AnnotationTooltipState = { + // isVisible: true, + // annotationType: AnnotationTypes.Rectangle, + // anchor: { + // top: cursorPosition.y, + // left: cursorPosition.x, + // }, + // }; + // + // expect(visibleTooltip).toEqual(expectedVisibleTooltipState); + // }); + // + // test('should get rotated cursor position', () => { + // const cursorPosition = { x: 1, y: 2 }; + // + // expect(getTransformedCursor(cursorPosition, chartDimensions, 0)).toEqual(cursorPosition); + // expect(getTransformedCursor(cursorPosition, chartDimensions, 90)).toEqual({ x: 2, y: 9 }); + // expect(getTransformedCursor(cursorPosition, chartDimensions, -90)).toEqual({ x: 18, y: 1 }); + // expect(getTransformedCursor(cursorPosition, chartDimensions, 180)).toEqual({ x: 9, y: 18 }); + // }); + // + // describe('#invertTranformedCursor', () => { + // const cursorPosition = { x: 1, y: 2 }; + // + // it.each([0, 90, -90, 180])('Should invert rotated cursor - rotation %d', (rotation) => { + // expect( + // invertTranformedCursor( + // getTransformedCursor(cursorPosition, chartDimensions, rotation), + // chartDimensions, + // rotation, + // ), + // ).toEqual(cursorPosition); + // }); + // + // it.each([0, 90, -90, 180])('Should invert rotated projected cursor - rotation %d', (rotation) => { + // expect( + // invertTranformedCursor( + // getTransformedCursor(cursorPosition, chartDimensions, rotation, true), + // chartDimensions, + // rotation, + // true, + // ), + // ).toEqual(cursorPosition); + // }); + // }); }); diff --git a/src/chart_types/xy_chart/annotations/utils.ts b/src/chart_types/xy_chart/annotations/utils.ts index ed1feb0aa8..c9df1e7eb4 100644 --- a/src/chart_types/xy_chart/annotations/utils.ts +++ b/src/chart_types/xy_chart/annotations/utils.ts @@ -22,6 +22,7 @@ import { Rotation, Position } from '../../../utils/commons'; import { Dimensions } from '../../../utils/dimensions'; import { AnnotationId, GroupId } from '../../../utils/ids'; import { Point } from '../../../utils/point'; +import { SmallMultipleScales } from '../state/selectors/compute_small_multiple_scales'; import { isHorizontalRotation } from '../state/utils/common'; import { getAxesSpecForSpecId } from '../state/utils/spec'; import { @@ -139,6 +140,7 @@ export function computeAnnotationDimensions( xScale: Scale, axesSpecs: AxisSpec[], isHistogramModeEnabled: boolean, + smallMultipleScales: SmallMultipleScales, ): Map { const annotationDimensions = new Map(); @@ -155,6 +157,7 @@ export function computeAnnotationDimensions( yScales, xScale, isHistogramModeEnabled, + smallMultipleScales, annotationAxisPosition, ); @@ -162,7 +165,13 @@ export function computeAnnotationDimensions( annotationDimensions.set(id, dimensions); } } else if (isRectAnnotation(annotationSpec)) { - const dimensions = computeRectAnnotationDimensions(annotationSpec, yScales, xScale, isHistogramModeEnabled); + const dimensions = computeRectAnnotationDimensions( + annotationSpec, + yScales, + xScale, + smallMultipleScales, + isHistogramModeEnabled, + ); if (dimensions) { annotationDimensions.set(id, dimensions); diff --git a/src/chart_types/xy_chart/axes/axes_sizes.ts b/src/chart_types/xy_chart/axes/axes_sizes.ts new file mode 100644 index 0000000000..9f127c29fb --- /dev/null +++ b/src/chart_types/xy_chart/axes/axes_sizes.ts @@ -0,0 +1,112 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { Position } from '../../../utils/commons'; +import { getSimplePadding } from '../../../utils/dimensions'; +import { AxisId } from '../../../utils/ids'; +import { AxisStyle, Theme } from '../../../utils/themes/theme'; +import { getSpecsById } from '../state/utils/spec'; +import { AxisTicksDimensions, shouldShowTicks } from '../utils/axis_utils'; +import { AxisSpec } from '../utils/specs'; + +/** + * Compute the axes required size around the chart + * @param chartTheme the theme style of the chart + * @param axisDimensions the axis dimensions + * @param axesStyles a map with all the custom axis styles + * @param axisSpecs the axis specs + * @internal + */ +export function computeAxesSizes( + { axes: sharedAxesStyles, chartMargins }: Theme, + axisDimensions: Map, + axesStyles: Map, + axisSpecs: AxisSpec[], +): { left: number; right: number; top: number; bottom: number; margin: { left: number } } { + const axisMainSize = { + left: 0, + right: 0, + top: 0, + bottom: 0, + }; + const axisLabelOverflow = { + left: 0, + right: 0, + top: 0, + bottom: 0, + }; + + axisDimensions.forEach(({ maxLabelBboxWidth = 0, maxLabelBboxHeight = 0 }, id) => { + const axisSpec = getSpecsById(axisSpecs, id); + if (!axisSpec || axisSpec.hide) { + return; + } + const { tickLine, axisTitle, tickLabel } = axesStyles.get(id) ?? sharedAxesStyles; + const showTicks = shouldShowTicks(tickLine, axisSpec.hide); + const { position, title } = axisSpec; + const titlePadding = getSimplePadding(axisTitle.padding); + const labelPadding = getSimplePadding(tickLabel.padding); + const labelPaddingSum = tickLabel.visible ? labelPadding.inner + labelPadding.outer : 0; + + const tickDimension = showTicks ? tickLine.size + tickLine.padding : 0; + const titleHeight = + title !== undefined && axisTitle.visible ? axisTitle.fontSize + titlePadding.outer + titlePadding.inner : 0; + const axisDimension = labelPaddingSum + tickDimension + titleHeight; + const maxAxisHeight = tickLabel.visible ? maxLabelBboxHeight + axisDimension : axisDimension; + const maxAxisWidth = tickLabel.visible ? maxLabelBboxWidth + axisDimension : axisDimension; + + switch (position) { + case Position.Top: + axisMainSize.top += maxAxisHeight + chartMargins.top; + // find the max half label size to accommodate the left/right labels + // TODO use first and last labels + axisLabelOverflow.left = Math.max(axisLabelOverflow.left, maxLabelBboxWidth / 2); + axisLabelOverflow.right = Math.max(axisLabelOverflow.right, maxLabelBboxWidth / 2); + break; + case Position.Bottom: + axisMainSize.bottom += maxAxisHeight + chartMargins.bottom; + // find the max half label size to accommodate the left/right labels + // TODO use first and last labels + axisLabelOverflow.left = Math.max(axisLabelOverflow.left, maxLabelBboxWidth / 2); + axisLabelOverflow.right = Math.max(axisLabelOverflow.right, maxLabelBboxWidth / 2); + break; + case Position.Right: + axisMainSize.right += maxAxisWidth + chartMargins.right; + // TODO use first and last labels + axisLabelOverflow.top = Math.max(axisLabelOverflow.top, maxLabelBboxHeight / 2); + axisLabelOverflow.bottom = Math.max(axisLabelOverflow.bottom, maxLabelBboxHeight / 2); + break; + case Position.Left: + default: + axisMainSize.left += maxAxisWidth + chartMargins.left; + // TODO use first and last labels + axisLabelOverflow.top = Math.max(axisLabelOverflow.top, maxLabelBboxHeight / 2); + axisLabelOverflow.bottom = Math.max(axisLabelOverflow.bottom, maxLabelBboxHeight / 2); + } + }); + const left = Math.max(axisLabelOverflow.left + chartMargins.left, axisMainSize.left); + return { + margin: { + left: left - axisMainSize.left, + }, + left, + right: Math.max(axisLabelOverflow.right + chartMargins.right, axisMainSize.right), + top: Math.max(axisLabelOverflow.top + chartMargins.top, axisMainSize.top), + bottom: Math.max(axisLabelOverflow.bottom + chartMargins.bottom, axisMainSize.bottom), + }; +} diff --git a/src/chart_types/xy_chart/domains/x_domain.test.ts b/src/chart_types/xy_chart/domains/x_domain.test.ts index e89e4db67d..6372a3589d 100644 --- a/src/chart_types/xy_chart/domains/x_domain.test.ts +++ b/src/chart_types/xy_chart/domains/x_domain.test.ts @@ -21,7 +21,7 @@ import { ChartTypes } from '../..'; import { MockSeriesSpecs } from '../../../mocks/specs'; import { ScaleType } from '../../../scales/constants'; import { SpecTypes, Direction, BinAgg } from '../../../specs/constants'; -import { getDataSeriesBySpecId } from '../utils/series'; +import { getDataSeriesFromSpecs } from '../utils/series'; import { BasicSeriesSpec, SeriesTypes } from '../utils/specs'; import { convertXScaleTypes, findMinInterval, mergeXDomain } from './x_domain'; @@ -222,7 +222,7 @@ describe('X Domain', () => { ], }; const specDataSeries: BasicSeriesSpec[] = [ds1, ds2]; - const { xValues } = getDataSeriesBySpecId(specDataSeries); + const { xValues } = getDataSeriesFromSpecs(specDataSeries); const mergedDomain = mergeXDomain( [ { @@ -269,7 +269,7 @@ describe('X Domain', () => { }; const specDataSeries = [ds1, ds2]; - const { xValues } = getDataSeriesBySpecId(specDataSeries); + const { xValues } = getDataSeriesFromSpecs(specDataSeries); const mergedDomain = mergeXDomain( [ { @@ -316,7 +316,7 @@ describe('X Domain', () => { }; const specDataSeries = [ds1, ds2]; - const { xValues } = getDataSeriesBySpecId(specDataSeries); + const { xValues } = getDataSeriesFromSpecs(specDataSeries); const mergedDomain = mergeXDomain( [ { @@ -367,7 +367,7 @@ describe('X Domain', () => { }; const specDataSeries = [ds1, ds2]; - const { xValues } = getDataSeriesBySpecId(specDataSeries); + const { xValues } = getDataSeriesFromSpecs(specDataSeries); const mergedDomain = mergeXDomain( [ { @@ -418,7 +418,7 @@ describe('X Domain', () => { }; const specDataSeries = [ds1, ds2]; - const { xValues } = getDataSeriesBySpecId(specDataSeries); + const { xValues } = getDataSeriesFromSpecs(specDataSeries); const mergedDomain = mergeXDomain( [ { @@ -475,7 +475,7 @@ describe('X Domain', () => { min: 0, }; - const { xValues } = getDataSeriesBySpecId(specDataSeries); + const { xValues } = getDataSeriesFromSpecs(specDataSeries); const getResult = () => mergeXDomain( [ @@ -535,7 +535,7 @@ describe('X Domain', () => { }; const specDataSeries = [ds1, ds2]; - const { xValues } = getDataSeriesBySpecId(specDataSeries); + const { xValues } = getDataSeriesFromSpecs(specDataSeries); const mergedDomain = mergeXDomain( [ { @@ -586,7 +586,7 @@ describe('X Domain', () => { }; const specDataSeries = [ds1, ds2]; - const { xValues } = getDataSeriesBySpecId(specDataSeries); + const { xValues } = getDataSeriesFromSpecs(specDataSeries); const mergedDomain = mergeXDomain( [ { @@ -637,7 +637,7 @@ describe('X Domain', () => { }; const specDataSeries = [ds1, ds2]; - const { xValues } = getDataSeriesBySpecId(specDataSeries); + const { xValues } = getDataSeriesFromSpecs(specDataSeries); const mergedDomain = mergeXDomain( [ { @@ -682,7 +682,7 @@ describe('X Domain', () => { }; const specDataSeries = [ds1, ds2]; - const { xValues } = getDataSeriesBySpecId(specDataSeries); + const { xValues } = getDataSeriesFromSpecs(specDataSeries); const mergedDomain = mergeXDomain( [ @@ -891,12 +891,12 @@ describe('X Domain', () => { ]); it('should sort ordinal xValues by descending sum by default', () => { - const { xValues } = getDataSeriesBySpecId(ordinalSpecs, [], {}); + const { xValues } = getDataSeriesFromSpecs(ordinalSpecs, [], {}); expect(xValues).toEqual(new Set(['c', 'd', 'b', 'a'])); }); it('should sort ordinal xValues by descending sum', () => { - const { xValues } = getDataSeriesBySpecId(ordinalSpecs, [], { + const { xValues } = getDataSeriesFromSpecs(ordinalSpecs, [], { binAgg: BinAgg.None, direction: Direction.Descending, }); @@ -904,7 +904,7 @@ describe('X Domain', () => { }); it('should sort ordinal xValues by ascending sum', () => { - const { xValues } = getDataSeriesBySpecId(ordinalSpecs, [], { + const { xValues } = getDataSeriesFromSpecs(ordinalSpecs, [], { binAgg: BinAgg.None, direction: Direction.Ascending, }); @@ -912,12 +912,12 @@ describe('X Domain', () => { }); it('should NOT sort ordinal xValues sum', () => { - const { xValues } = getDataSeriesBySpecId(ordinalSpecs, [], undefined); + const { xValues } = getDataSeriesFromSpecs(ordinalSpecs, [], undefined); expect(xValues).toEqual(new Set(['a', 'b', 'c', 'd'])); }); it('should NOT sort ordinal xValues sum when undefined', () => { - const { xValues } = getDataSeriesBySpecId(ordinalSpecs, [], { + const { xValues } = getDataSeriesFromSpecs(ordinalSpecs, [], { binAgg: BinAgg.None, direction: Direction.Descending, }); @@ -925,7 +925,7 @@ describe('X Domain', () => { }); it('should NOT sort linear xValue by descending sum', () => { - const { xValues } = getDataSeriesBySpecId(linearSpecs, [], { + const { xValues } = getDataSeriesFromSpecs(linearSpecs, [], { direction: Direction.Descending, }); expect(xValues).toEqual(new Set([1, 2, 3, 4])); diff --git a/src/chart_types/xy_chart/domains/x_domain.ts b/src/chart_types/xy_chart/domains/x_domain.ts index 55f07d745f..1a3a054930 100644 --- a/src/chart_types/xy_chart/domains/x_domain.ts +++ b/src/chart_types/xy_chart/domains/x_domain.ts @@ -30,6 +30,7 @@ import { XDomain } from './types'; * @param specs an array of [{ seriesType, xScaleType }] * @param xValues a set of unique x values from all specs * @param customXDomain if specified, a custom xDomain + * @param fallbackScale * @returns a merged XDomain between all series. * @internal */ diff --git a/src/chart_types/xy_chart/domains/y_domain.test.ts b/src/chart_types/xy_chart/domains/y_domain.test.ts index 7402c714ff..cea56ed189 100644 --- a/src/chart_types/xy_chart/domains/y_domain.test.ts +++ b/src/chart_types/xy_chart/domains/y_domain.test.ts @@ -26,7 +26,7 @@ import { Position } from '../../../utils/commons'; import { BARCHART_1Y0G } from '../../../utils/data_samples/test_dataset'; import { computeSeriesDomainsSelector } from '../state/selectors/compute_series_domains'; import { BasicSeriesSpec, SeriesTypes, DEFAULT_GLOBAL_ID, StackMode } from '../utils/specs'; -import { coerceYScaleTypes, splitSpecsByGroupId } from './y_domain'; +import { coerceYScaleTypes, groupSeriesByYGroup } from './y_domain'; const DEMO_AREA_SPEC_1 = { id: 'a', @@ -243,7 +243,7 @@ describe('Y Domain', () => { yAccessors: ['y'], data: BARCHART_1Y0G, }; - const splittedSpecs = splitSpecsByGroupId([spec1, spec2]); + const splittedSpecs = groupSeriesByYGroup([spec1, spec2]); const groupKeys = [...splittedSpecs.keys()]; const groupValues = [...splittedSpecs.values()]; expect(groupKeys).toEqual(['group1', 'group2']); @@ -280,7 +280,7 @@ describe('Y Domain', () => { stackAccessors: ['x'], data: BARCHART_1Y0G, }; - const splittedSpecs = splitSpecsByGroupId([spec1, spec2]); + const splittedSpecs = groupSeriesByYGroup([spec1, spec2]); const groupKeys = [...splittedSpecs.keys()]; const groupValues = [...splittedSpecs.values()]; expect(groupKeys).toEqual(['group1', 'group2']); @@ -317,7 +317,7 @@ describe('Y Domain', () => { stackAccessors: ['x'], data: BARCHART_1Y0G, }; - const splittedSpecs = splitSpecsByGroupId([spec1, spec2]); + const splittedSpecs = groupSeriesByYGroup([spec1, spec2]); const groupKeys = [...splittedSpecs.keys()]; const groupValues = [...splittedSpecs.values()]; expect(groupKeys).toEqual(['group']); @@ -365,7 +365,7 @@ describe('Y Domain', () => { stackAccessors: ['x'], data: BARCHART_1Y0G, }; - const splittedSpecs = splitSpecsByGroupId([spec1, spec2, spec3]); + const splittedSpecs = groupSeriesByYGroup([spec1, spec2, spec3]); const groupKeys = [...splittedSpecs.keys()]; const groupValues = [...splittedSpecs.values()]; expect(groupKeys).toEqual(['group1', 'group2']); diff --git a/src/chart_types/xy_chart/domains/y_domain.ts b/src/chart_types/xy_chart/domains/y_domain.ts index 265edd8fac..ebc17a6e7f 100644 --- a/src/chart_types/xy_chart/domains/y_domain.ts +++ b/src/chart_types/xy_chart/domains/y_domain.ts @@ -25,7 +25,8 @@ import { computeContinuousDataDomain } from '../../../utils/domain'; import { GroupId } from '../../../utils/ids'; import { Logger } from '../../../utils/logger'; import { isCompleteBound, isLowerBound, isUpperBound } from '../utils/axis_type_utils'; -import { DataSeries, FormattedDataSeries } from '../utils/series'; +import { groupBy } from '../utils/group_data_series'; +import { DataSeries } from '../utils/series'; import { BasicSeriesSpec, YDomainRange, DEFAULT_GLOBAL_ID, SeriesTypes, StackMode } from '../utils/specs'; import { YDomain } from './types'; @@ -42,76 +43,44 @@ interface GroupSpecs { /** @internal */ export function mergeYDomain( - { - stacked, - nonStacked, - }: { - stacked: FormattedDataSeries[]; - nonStacked: FormattedDataSeries[]; - }, + dataSeries: DataSeries[], specs: YBasicSeriesSpec[], domainsByGroupId: Map, ): YDomain[] { - // group specs by group ids - const specsByGroupIds = splitSpecsByGroupId(specs); - const specsByGroupIdsEntries = [...specsByGroupIds.entries()]; - const globalId = DEFAULT_GLOBAL_ID; + const dataSeriesByGroupId = groupBy( + dataSeries, + ({ spec: { useDefaultGroupDomain, groupId } }) => { + return useDefaultGroupDomain ? DEFAULT_GLOBAL_ID : groupId; + }, + true, + ); - const yDomains = specsByGroupIdsEntries.map(([groupId, groupSpecs]) => { + return dataSeriesByGroupId.map((groupedDataSeries) => { + const [{ groupId }] = groupedDataSeries; + const stacked = groupedDataSeries.filter(({ stackMode }) => stackMode); + const nonStacked = groupedDataSeries.filter(({ stackMode }) => stackMode === undefined); const customDomain = domainsByGroupId.get(groupId); - const emptyDS: FormattedDataSeries = { - dataSeries: [], - groupId, - counts: { area: 0, bubble: 0, bar: 0, line: 0 }, - }; - const stackedDS = stacked.find((d) => d.groupId === groupId) ?? emptyDS; - const nonStackedDS = nonStacked.find((d) => d.groupId === groupId) ?? emptyDS; - const nonZeroBaselineSpecs = - stackedDS.counts.bar + stackedDS.counts.area + nonStackedDS.counts.bar + nonStackedDS.counts.area; - return mergeYDomainForGroup( - stackedDS.dataSeries, - nonStackedDS.dataSeries, - groupId, - groupSpecs, - nonZeroBaselineSpecs > 0, - customDomain, + const hasNonZeroBaselineTypes = groupedDataSeries.some( + ({ seriesType }) => seriesType === SeriesTypes.Bar || SeriesTypes.Area, ); - }); - - const globalGroupIds: Set = specs.reduce>((acc, { groupId, useDefaultGroupDomain }) => { - if (groupId !== globalId && useDefaultGroupDomain) { - acc.add(groupId); - } - return acc; - }, new Set()); - globalGroupIds.add(globalId); - - const globalYDomains = yDomains.filter((domain) => globalGroupIds.has(domain.groupId)); - let globalYDomain = [Number.MAX_SAFE_INTEGER, Number.MIN_SAFE_INTEGER]; - globalYDomains.forEach((domain) => { - globalYDomain = [Math.min(globalYDomain[0], domain.domain[0]), Math.max(globalYDomain[1], domain.domain[1])]; - }); - return yDomains.map((domain) => { - if (globalGroupIds.has(domain.groupId)) { - return { - ...domain, - domain: globalYDomain, - }; - } - return domain; + return mergeYDomainForGroup(stacked, nonStacked, hasNonZeroBaselineTypes, customDomain); }); } function mergeYDomainForGroup( stacked: DataSeries[], nonStacked: DataSeries[], - groupId: GroupId, - groupSpecs: GroupSpecs, hasZeroBaselineSpecs: boolean, customDomain?: YDomainRange, ): YDomain { - const groupYScaleType = coerceYScaleTypes([...groupSpecs.stacked, ...groupSpecs.nonStacked]); - const { stackMode } = groupSpecs; + const dataSeries = [...stacked, ...nonStacked]; + + const yScaleTypes = dataSeries.map(({ spec: { yScaleType } }) => ({ + yScaleType, + })); + const groupYScaleType = coerceYScaleTypes(yScaleTypes); + const stackMode = dataSeries.length > 0 ? dataSeries[0].stackMode : undefined; + const groupId = dataSeries.length > 0 ? dataSeries[0].groupId : 'missing'; let domain: number[]; if (stackMode === StackMode.Percentage) { @@ -119,9 +88,7 @@ function mergeYDomainForGroup( } else { // TODO remove when removing yScaleToDataExtent const newCustomDomain = customDomain ? { ...customDomain } : {}; - const shouldScaleToExtent = - groupSpecs.stacked.some(({ yScaleToDataExtent }) => yScaleToDataExtent) || - groupSpecs.nonStacked.some(({ yScaleToDataExtent }) => yScaleToDataExtent); + const shouldScaleToExtent = dataSeries.some(({ spec: { yScaleToDataExtent } }) => yScaleToDataExtent); if (customDomain?.fit !== true && shouldScaleToExtent) { newCustomDomain.fit = true; } @@ -163,15 +130,16 @@ function mergeYDomainForGroup( }; } -function computeYDomain(dataseries: DataSeries[], hasZeroBaselineSpecs: boolean) { +function computeYDomain(dataSeries: DataSeries[], hasZeroBaselineSpecs: boolean) { const yValues = new Set(); - dataseries.forEach((ds) => { - ds.data.forEach((datum) => { + dataSeries.forEach(({ data }) => { + for (let i = 0; i < data.length; i++) { + const datum = data[i]; yValues.add(datum.y1); if (hasZeroBaselineSpecs && datum.y0 != null) { yValues.add(datum.y0); } - }); + } }); if (yValues.size === 0) { return []; @@ -180,17 +148,13 @@ function computeYDomain(dataseries: DataSeries[], hasZeroBaselineSpecs: boolean) } /** @internal */ -export function splitSpecsByGroupId(specs: YBasicSeriesSpec[]) { +export function groupSeriesByYGroup(specs: YBasicSeriesSpec[]) { const specsByGroupIds = new Map< GroupId, { stackMode: StackMode | undefined; stacked: YBasicSeriesSpec[]; nonStacked: YBasicSeriesSpec[] } >(); - // After mobx->redux https://github.com/elastic/elastic-charts/pull/281 we keep the specs untouched on mount - // in MobX version, the stackAccessors was programmatically added to every histogram specs - // in ReduX version, we left untouched the specs, so we have to manually check that - const isHistogramEnabled = specs.some( - ({ seriesType, enableHistogramMode }) => seriesType === SeriesTypes.Bar && enableHistogramMode, - ); + + const histogramEnabled = isHistogramEnabled(specs); // split each specs by groupId and by stacked or not specs.forEach((spec) => { const group = specsByGroupIds.get(spec.groupId) || { @@ -198,12 +162,8 @@ export function splitSpecsByGroupId(specs: YBasicSeriesSpec[]) { stacked: [], nonStacked: [], }; - // stack every bars if using histogram mode - // independenyly from lines and areas - if ( - (spec.seriesType === SeriesTypes.Bar && isHistogramEnabled) || - (spec.stackAccessors && spec.stackAccessors.length > 0) - ) { + + if (isStackedSpec(spec, histogramEnabled)) { group.stacked.push(spec); } else { group.nonStacked.push(spec); @@ -220,6 +180,53 @@ export function splitSpecsByGroupId(specs: YBasicSeriesSpec[]) { return specsByGroupIds; } +/** + * Histogram mode is forced on every specs if at least one specs has that prop flagged + * @remarks + * After mobx->redux https://github.com/elastic/elastic-charts/pull/281 we keep the specs untouched on mount + * in MobX version, the stackAccessors was programmatically added to every histogram specs + * in ReduX version, we left untouched the specs, so we have to manually check that + * @param specs + * @internal + */ +export function isHistogramEnabled(specs: YBasicSeriesSpec[]) { + return specs.some(({ seriesType, enableHistogramMode }) => seriesType === SeriesTypes.Bar && enableHistogramMode); +} + +/** + * Return true if the passed spec needs to be rendered as stack + * @param spec + * @param histogramEnabled + * @internal + */ +export function isStackedSpec(spec: YBasicSeriesSpec, histogramEnabled: boolean) { + const isBarAndHistogram = spec.seriesType === SeriesTypes.Bar && histogramEnabled; + const hasStackAccessors = spec.stackAccessors && spec.stackAccessors.length > 0; + return isBarAndHistogram || hasStackAccessors; +} + +/** + * Get the stack mode for every groupId + * @param specs + * @internal + */ +export function getStackModeForYGroup(specs: YBasicSeriesSpec[]) { + return specs.reduce>((acc, { groupId, stackMode }) => { + if (!acc[groupId]) { + acc[groupId] = undefined; + } + + if (acc[groupId] === undefined && stackMode !== undefined) { + acc[groupId] = stackMode; + } + if (stackMode !== undefined && acc[groupId] !== stackMode) { + Logger.warn(`Is not possible to mix different stackModes, please align all stackMode on the same GroupId + to the same mode. The default behaviour will be to use the first encountered stackMode on the series`); + } + return acc; + }, {}); +} + /** * Coerce the scale types of a set of specification to a generic one. * If there is at least one bar series type, than the response will specity @@ -228,13 +235,13 @@ export function splitSpecsByGroupId(specs: YBasicSeriesSpec[]) { * If there are multiple continuous scale types, is coerced to linear. * If there are at least one Ordinal scale type, is coerced to ordinal. * If none of the above, than coerce to the specified scale. - * @returns {ChartScaleType} + * @returns {ScaleContinuousType} * @internal */ -export function coerceYScaleTypes(specs: Pick[]): ScaleContinuousType { +export function coerceYScaleTypes(scales: { yScaleType: ScaleContinuousType }[]): ScaleContinuousType { const scaleTypes = new Set(); - specs.forEach((spec) => { - scaleTypes.add(spec.yScaleType); + scales.forEach(({ yScaleType }) => { + scaleTypes.add(yScaleType); }); return coerceYScale(scaleTypes); } @@ -247,3 +254,13 @@ function coerceYScale(scaleTypes: Set): ScaleContinuousType } return ScaleType.Linear; } + +export function getYScaleTypeByGroupId(specs: BasicSeriesSpec[]): Map { + const groups = groupBy(specs, ['groupId'], true); + return groups.reduce((acc, group) => { + const scaleType = coerceYScaleTypes(group); + const [{ groupId }] = group; + acc.set(groupId, scaleType); + return acc; + }, new Map()); +} diff --git a/src/chart_types/xy_chart/renderer/canvas/annotations/lines.ts b/src/chart_types/xy_chart/renderer/canvas/annotations/lines.ts index e9bbab59c0..8beecf3951 100644 --- a/src/chart_types/xy_chart/renderer/canvas/annotations/lines.ts +++ b/src/chart_types/xy_chart/renderer/canvas/annotations/lines.ts @@ -33,12 +33,13 @@ export function renderLineAnnotations( const { start: { x1, y1 }, end: { x2, y2 }, + transform: { x, y }, } = annotation.linePathPoints; return { - x1, - y1, - x2, - y2, + x1: x1 + x, + y1: y1 + y, + x2: x2 + x, + y2: y2 + y, }; }); const strokeColor = stringToRGB(lineStyle.line.stroke); diff --git a/src/chart_types/xy_chart/renderer/canvas/axes/index.ts b/src/chart_types/xy_chart/renderer/canvas/axes/index.ts index f73a6f1648..3f6f59cbd7 100644 --- a/src/chart_types/xy_chart/renderer/canvas/axes/index.ts +++ b/src/chart_types/xy_chart/renderer/canvas/axes/index.ts @@ -22,7 +22,7 @@ import { Dimensions } from '../../../../../utils/dimensions'; import { AxisId } from '../../../../../utils/ids'; import { AxisStyle } from '../../../../../utils/themes/theme'; import { getSpecsById } from '../../../state/utils/spec'; -import { AxisTick, AxisTicksDimensions, shouldShowTicks } from '../../../utils/axis_utils'; +import { AxisGeometry, AxisTick, AxisTicksDimensions, shouldShowTicks } from '../../../utils/axis_utils'; import { AxisSpec } from '../../../utils/specs'; import { renderDebugRect } from '../utils/debug'; import { renderLine } from './line'; @@ -34,8 +34,8 @@ import { renderTitle } from './title'; export interface AxisProps { axisStyle: AxisStyle; axisSpec: AxisSpec; - axisTicksDimensions: AxisTicksDimensions; - axisPosition: Dimensions; + position: Dimensions; + dimension: AxisTicksDimensions; ticks: AxisTick[]; debug: boolean; chartDimensions: Dimensions; @@ -43,10 +43,8 @@ export interface AxisProps { /** @internal */ export interface AxesProps { - axesVisibleTicks: Map; axesSpecs: AxisSpec[]; - axesTicksDimensions: Map; - axesPositions: Map; + axesGeometries: AxisGeometry[]; axesStyles: Map; sharedAxesStyle: AxisStyle; debug: boolean; @@ -55,22 +53,12 @@ export interface AxesProps { /** @internal */ export function renderAxes(ctx: CanvasRenderingContext2D, props: AxesProps) { - const { - axesVisibleTicks, - axesSpecs, - axesTicksDimensions, - axesPositions, - axesStyles, - sharedAxesStyle, - debug, - chartDimensions, - } = props; - axesVisibleTicks.forEach((ticks, axisId) => { + const { axesSpecs, axesGeometries, axesStyles, sharedAxesStyle, debug, chartDimensions } = props; + axesGeometries.forEach((geometry) => { + const { axisId, position, dimension, visibleTicks: ticks } = geometry; const axisSpec = getSpecsById(axesSpecs, axisId); - const axisTicksDimensions = axesTicksDimensions.get(axisId); - const axisPosition = axesPositions.get(axisId); - if (!ticks || !axisSpec || !axisTicksDimensions || !axisPosition || axisSpec.hide) { + if (!axisSpec || !dimension || !position || axisSpec.hide) { return; } @@ -78,8 +66,8 @@ export function renderAxes(ctx: CanvasRenderingContext2D, props: AxesProps) { renderAxis(ctx, { axisSpec, - axisTicksDimensions, - axisPosition, + position, + dimension, ticks, axisStyle, debug, @@ -90,15 +78,15 @@ export function renderAxes(ctx: CanvasRenderingContext2D, props: AxesProps) { function renderAxis(ctx: CanvasRenderingContext2D, props: AxisProps) { withContext(ctx, (ctx) => { - const { ticks, axisPosition, debug, axisStyle, axisSpec } = props; + const { ticks, position, debug, axisStyle, axisSpec } = props; const showTicks = shouldShowTicks(axisStyle.tickLine, axisSpec.hide); - ctx.translate(axisPosition.left, axisPosition.top); + ctx.translate(position.left, position.top); if (debug) { renderDebugRect(ctx, { x: 0, y: 0, - width: axisPosition.width, - height: axisPosition.height, + width: position.width, + height: position.height, }); } diff --git a/src/chart_types/xy_chart/renderer/canvas/axes/line.ts b/src/chart_types/xy_chart/renderer/canvas/axes/line.ts index d0b09fc069..85412658ae 100644 --- a/src/chart_types/xy_chart/renderer/canvas/axes/line.ts +++ b/src/chart_types/xy_chart/renderer/canvas/axes/line.ts @@ -24,7 +24,7 @@ import { isVerticalAxis } from '../../../utils/axis_type_utils'; /** @internal */ export function renderLine( ctx: CanvasRenderingContext2D, - { axisSpec: { position }, axisPosition, axisStyle: { axisLine } }: AxisProps, + { axisSpec: { position }, position: axisPosition, axisStyle: { axisLine } }: AxisProps, ) { if (!axisLine.visible) { return; diff --git a/src/chart_types/xy_chart/renderer/canvas/axes/tick.ts b/src/chart_types/xy_chart/renderer/canvas/axes/tick.ts index 4ef06739ab..0cbb96d6c6 100644 --- a/src/chart_types/xy_chart/renderer/canvas/axes/tick.ts +++ b/src/chart_types/xy_chart/renderer/canvas/axes/tick.ts @@ -29,7 +29,7 @@ import { renderLine } from '../primitives/line'; export function renderTick(ctx: CanvasRenderingContext2D, tick: AxisTick, props: AxisProps) { const { axisSpec: { position }, - axisPosition, + position: axisPosition, axisStyle: { tickLine }, } = props; if (isVerticalAxis(position)) { diff --git a/src/chart_types/xy_chart/renderer/canvas/axes/tick_label.ts b/src/chart_types/xy_chart/renderer/canvas/axes/tick_label.ts index c4b646e61d..4f0d54c8a9 100644 --- a/src/chart_types/xy_chart/renderer/canvas/axes/tick_label.ts +++ b/src/chart_types/xy_chart/renderer/canvas/axes/tick_label.ts @@ -28,8 +28,8 @@ import { renderDebugRectCenterRotated } from '../utils/debug'; export function renderTickLabel(ctx: CanvasRenderingContext2D, tick: AxisTick, showTicks: boolean, props: AxisProps) { const { axisSpec: { position, labelFormat }, - axisTicksDimensions, - axisPosition, + dimension: axisTicksDimensions, + position: axisPosition, debug, axisStyle, } = props; diff --git a/src/chart_types/xy_chart/renderer/canvas/axes/title.ts b/src/chart_types/xy_chart/renderer/canvas/axes/title.ts index 8cf282a903..02b8c2d5a4 100644 --- a/src/chart_types/xy_chart/renderer/canvas/axes/title.ts +++ b/src/chart_types/xy_chart/renderer/canvas/axes/title.ts @@ -43,9 +43,9 @@ export function renderTitle(ctx: CanvasRenderingContext2D, props: AxisProps) { function renderVerticalTitle(ctx: CanvasRenderingContext2D, props: AxisProps) { const { - axisPosition: { height }, + position: { height }, axisSpec: { title, position, hide: hideAxis }, - axisTicksDimensions: { maxLabelBboxWidth }, + dimension: { maxLabelBboxWidth }, axisStyle: { axisTitle, tickLine, tickLabel }, debug, } = props; @@ -84,9 +84,9 @@ function renderVerticalTitle(ctx: CanvasRenderingContext2D, props: AxisProps) { } function renderHorizontalTitle(ctx: CanvasRenderingContext2D, props: AxisProps) { const { - axisPosition: { width }, + position: { width }, axisSpec: { title, position, hide: hideAxis }, - axisTicksDimensions: { maxLabelBboxHeight }, + dimension: { maxLabelBboxHeight }, axisStyle: { axisTitle, tickLine, tickLabel }, debug, } = props; diff --git a/src/chart_types/xy_chart/renderer/canvas/bars.ts b/src/chart_types/xy_chart/renderer/canvas/bars.ts index 8cc18dbb3c..12eb1728e6 100644 --- a/src/chart_types/xy_chart/renderer/canvas/bars.ts +++ b/src/chart_types/xy_chart/renderer/canvas/bars.ts @@ -38,14 +38,14 @@ export function renderBars( withClip(ctx, clippings, (ctx: CanvasRenderingContext2D) => { // ctx.scale(1, -1); // D3 and Canvas2d use a left-handed coordinate system (+y = down) but the ViewModel uses +y = up, so we must locally invert Y barGeometries.forEach((barGeometry) => { - const { x, y, width, height, color, seriesStyle } = barGeometry; + const { x, y, width, height, color, seriesStyle, transform } = barGeometry; const geometryStateStyle = getGeometryStateStyle( barGeometry.seriesIdentifier, highlightedLegendItem || null, sharedStyle, ); const { fill, stroke } = buildBarStyles(color, seriesStyle.rect, seriesStyle.rectBorder, geometryStateStyle); - const rect = { x, y, width, height }; + const rect = { x: x + transform.x, y: y + transform.y, width, height }; renderRect(ctx, rect, fill, stroke); }); }); diff --git a/src/chart_types/xy_chart/renderer/canvas/grids.ts b/src/chart_types/xy_chart/renderer/canvas/grids.ts index 22e056ca92..27e6c08118 100644 --- a/src/chart_types/xy_chart/renderer/canvas/grids.ts +++ b/src/chart_types/xy_chart/renderer/canvas/grids.ts @@ -26,13 +26,13 @@ import { AxisStyle } from '../../../../utils/themes/theme'; import { stringToRGB } from '../../../partition_chart/layout/utils/color_library_wrappers'; import { getSpecsById } from '../../state/utils/spec'; import { isVerticalGrid } from '../../utils/axis_type_utils'; -import { AxisLinePosition } from '../../utils/axis_utils'; +import { AxisGeometry, AxisLinePosition } from '../../utils/axis_utils'; import { AxisSpec } from '../../utils/specs'; import { renderMultiLine, MIN_STROKE_WIDTH } from './primitives/line'; interface GridProps { sharedAxesStyle: AxisStyle; - axesGridLinesPositions: Map; + axesGeometries: AxisGeometry[]; axesSpecs: AxisSpec[]; chartDimensions: Dimensions; axesStyles: Map; @@ -40,12 +40,13 @@ interface GridProps { /** @internal */ export function renderGrids(ctx: CanvasRenderingContext2D, props: GridProps) { - const { axesGridLinesPositions, axesSpecs, chartDimensions, sharedAxesStyle, axesStyles } = props; + const { axesGeometries, axesSpecs, chartDimensions, sharedAxesStyle, axesStyles } = props; withContext(ctx, (ctx) => { ctx.translate(chartDimensions.left, chartDimensions.top); - axesGridLinesPositions.forEach((axisGridLinesPositions, axisId) => { + axesGeometries.forEach((axisGeometry) => { + const { axisId, gridLinePositions } = axisGeometry; const axisSpec = getSpecsById(axesSpecs, axisId); - if (axisSpec && axisGridLinesPositions.length > 0) { + if (axisSpec && gridLinePositions.length > 0) { const axisStyle = axesStyles.get(axisSpec.id) ?? sharedAxesStyle; const themeConfig = isVerticalGrid(axisSpec.position) ? axisStyle.gridLine.vertical @@ -64,7 +65,7 @@ export function renderGrids(ctx: CanvasRenderingContext2D, props: GridProps) { width: gridLine.strokeWidth, dash: gridLine.dash, }; - const lines = axisGridLinesPositions.map(([x1, y1, x2, y2]) => ({ x1, y1, x2, y2 })); + const lines = gridLinePositions.map(([x1, y1, x2, y2]) => ({ x1, y1, x2, y2 })); renderMultiLine(ctx, lines, stroke); } }); diff --git a/src/chart_types/xy_chart/renderer/canvas/renderers.ts b/src/chart_types/xy_chart/renderer/canvas/renderers.ts index 03556e2404..c7eee453e9 100644 --- a/src/chart_types/xy_chart/renderer/canvas/renderers.ts +++ b/src/chart_types/xy_chart/renderer/canvas/renderers.ts @@ -51,11 +51,9 @@ export function renderXYChartCanvas2d( highlightedLegendItem, annotationDimensions, annotationSpecs, - axisTickPositions, + axesGeometries, axesSpecs, - axesTicksDimensions, axesStyles, - axesGridLinesPositions, debug, } = props; const transform = { @@ -72,10 +70,8 @@ export function renderXYChartCanvas2d( (ctx: CanvasRenderingContext2D) => { renderAxes(ctx, { - axesPositions: axisTickPositions.axisPositions, axesSpecs, - axesTicksDimensions, - axesVisibleTicks: axisTickPositions.axisVisibleTicks, + axesGeometries, chartDimensions, debug, axesStyles, @@ -86,7 +82,7 @@ export function renderXYChartCanvas2d( renderGrids(ctx, { axesSpecs, chartDimensions, - axesGridLinesPositions, + axesGeometries, axesStyles, sharedAxesStyle: theme.axes, }); diff --git a/src/chart_types/xy_chart/renderer/canvas/xy_chart.tsx b/src/chart_types/xy_chart/renderer/canvas/xy_chart.tsx index 55a7328d29..fd2a271eef 100644 --- a/src/chart_types/xy_chart/renderer/canvas/xy_chart.tsx +++ b/src/chart_types/xy_chart/renderer/canvas/xy_chart.tsx @@ -37,8 +37,7 @@ import { LIGHT_THEME } from '../../../../utils/themes/light_theme'; import { Theme, AxisStyle } from '../../../../utils/themes/theme'; import { AnnotationDimensions } from '../../annotations/types'; import { computeAnnotationDimensionsSelector } from '../../state/selectors/compute_annotations'; -import { computeAxisTicksDimensionsSelector } from '../../state/selectors/compute_axis_ticks_dimensions'; -import { AxisVisibleTicks, computeAxisVisibleTicksSelector } from '../../state/selectors/compute_axis_visible_ticks'; +import { computeAxesGeometriesSelector } from '../../state/selectors/compute_axis_visible_ticks'; import { computeChartDimensionsSelector } from '../../state/selectors/compute_chart_dimensions'; import { computeChartTransformSelector } from '../../state/selectors/compute_chart_transform'; import { computeSeriesGeometriesSelector } from '../../state/selectors/compute_series_geometries'; @@ -47,7 +46,7 @@ import { getHighlightedSeriesSelector } from '../../state/selectors/get_highligh import { getAnnotationSpecsSelector, getAxisSpecsSelector } from '../../state/selectors/get_specs'; import { isChartEmptySelector } from '../../state/selectors/is_chart_empty'; import { Geometries, Transform } from '../../state/utils/types'; -import { AxisLinePosition, AxisTicksDimensions } from '../../utils/axis_utils'; +import { AxisGeometry } from '../../utils/axis_utils'; import { IndexedGeometryMap } from '../../utils/indexed_geometry_map'; import { AxisSpec, AnnotationSpec } from '../../utils/specs'; import { renderXYChartCanvas2d } from './renderers'; @@ -66,10 +65,8 @@ export interface ReactiveChartStateProps { chartTransform: Transform; highlightedLegendItem?: LegendItem; axesSpecs: AxisSpec[]; - axesTicksDimensions: Map; + axesGeometries: AxisGeometry[]; axesStyles: Map; - axisTickPositions: AxisVisibleTicks; - axesGridLinesPositions: Map; annotationDimensions: Map; annotationSpecs: AnnotationSpec[]; } @@ -208,15 +205,8 @@ const DEFAULT_PROPS: ReactiveChartStateProps = { }, axesSpecs: [], - axisTickPositions: { - axisGridLinesPositions: new Map(), - axisPositions: new Map(), - axisTicks: new Map(), - axisVisibleTicks: new Map(), - }, - axesTicksDimensions: new Map(), + axesGeometries: [], axesStyles: new Map(), - axesGridLinesPositions: new Map(), annotationDimensions: new Map(), annotationSpecs: [], }; @@ -241,10 +231,8 @@ const mapStateToProps = (state: GlobalChartState): ReactiveChartStateProps => { chartDimensions: computeChartDimensionsSelector(state).chartDimensions, chartTransform: computeChartTransformSelector(state), axesSpecs: getAxisSpecsSelector(state), - axisTickPositions: computeAxisVisibleTicksSelector(state), - axesTicksDimensions: computeAxisTicksDimensionsSelector(state), + axesGeometries: computeAxesGeometriesSelector(state), axesStyles: getAxesStylesSelector(state), - axesGridLinesPositions: computeAxisVisibleTicksSelector(state).axisGridLinesPositions, annotationDimensions: computeAnnotationDimensionsSelector(state), annotationSpecs: getAnnotationSpecsSelector(state), }; diff --git a/src/chart_types/xy_chart/renderer/dom/highlighter.tsx b/src/chart_types/xy_chart/renderer/dom/highlighter.tsx index c3f749342b..5b70a2d8da 100644 --- a/src/chart_types/xy_chart/renderer/dom/highlighter.tsx +++ b/src/chart_types/xy_chart/renderer/dom/highlighter.tsx @@ -78,8 +78,8 @@ class HighlighterComponent extends React.Component { return ( { - describe('Empty line for missing data', () => { - const pointSeriesSpec: AreaSeriesSpec = { - chartType: ChartTypes.XYAxis, - specType: SpecTypes.Series, - id: SPEC_ID, - groupId: GROUP_ID, - seriesType: SeriesTypes.Area, - data: [ - [0, 10], - [1, 5], - ], - xAccessor: 0, - yAccessors: [1], - xScaleType: ScaleType.Ordinal, - yScaleType: ScaleType.Linear, - }; - const pointSeriesMap = [pointSeriesSpec]; - const pointSeriesDomains = computeSeriesDomains(pointSeriesMap, new Map()); - const xScale = computeXScale({ - xDomain: pointSeriesDomains.xDomain, - totalBarsInCluster: pointSeriesMap.length, - range: [0, 100], - }); - const yScales = computeYScales({ - yDomains: pointSeriesDomains.yDomain, - range: [100, 0], - }); - let renderedArea: { - areaGeometry: AreaGeometry; - indexedGeometryMap: IndexedGeometryMap; - }; +function initStore(specs: Spec[], vizColors: string[] = ['red'], width = 100): Store { + const store = MockStore.default({ width, height: 100, top: 0, left: 0 }); + MockStore.addSpecs( + [ + ...specs, + MockGlobalSpec.settingsNoMargins({ + theme: { + colors: { + vizColors, + }, + }, + }), + ], + store, + ); + return store; +} - beforeEach(() => { - renderedArea = renderArea( - 25, // adding a ideal 25px shift, generally applied by renderGeometries - { ...pointSeriesDomains.formattedDataSeries.nonStacked[0].dataSeries[0], data: [] }, - xScale, - yScales.get(GROUP_ID)!, - 'red', - CurveType.LINEAR, - false, - 0, - LIGHT_THEME.areaSeriesStyle, - { - enabled: false, - }, - ); - }); - test('Render geometry but empty upper and lower lines and area paths', () => { - const { - areaGeometry: { lines, area, color, seriesIdentifier, transform }, - } = renderedArea; - expect(lines.length).toBe(0); - expect(area).toBe(''); - expect(color).toBe('red'); - expect(seriesIdentifier.seriesKeys).toEqual([1]); - expect(seriesIdentifier.specId).toEqual(SPEC_ID); - expect(transform).toEqual({ x: 25, y: 0 }); - }); +describe('Rendering points - areas', () => { + test('Missing geometry if no data', () => { + const store = initStore([ + MockSeriesSpec.area({ + id: SPEC_ID, + groupId: GROUP_ID, + xScaleType: ScaleType.Ordinal, + yScaleType: ScaleType.Linear, + xAccessor: 0, + yAccessors: [1], + data: [], + }), + ]); + const { + geometries: { areas }, + } = computeSeriesGeometriesSelector(store.getState()); + expect(areas).toHaveLength(0); }); describe('Single series area chart - ordinal', () => { - const pointSeriesSpec: AreaSeriesSpec = { - chartType: ChartTypes.XYAxis, - specType: SpecTypes.Series, - id: SPEC_ID, - groupId: GROUP_ID, - seriesType: SeriesTypes.Area, - data: [ - [0, 10], - [1, 5], - ], - xAccessor: 0, - yAccessors: [1], - xScaleType: ScaleType.Ordinal, - yScaleType: ScaleType.Linear, - }; - const pointSeriesMap = [pointSeriesSpec]; - const pointSeriesDomains = computeSeriesDomains(pointSeriesMap, new Map()); - const xScale = computeXScale({ - xDomain: pointSeriesDomains.xDomain, - totalBarsInCluster: pointSeriesMap.length, - range: [0, 100], - }); - const yScales = computeYScales({ yDomains: pointSeriesDomains.yDomain, range: [100, 0] }); - let renderedArea: { - areaGeometry: AreaGeometry; - indexedGeometryMap: IndexedGeometryMap; - }; - + let areaGeometry: AreaGeometry; + let geometriesIndex: IndexedGeometryMap; beforeEach(() => { - renderedArea = renderArea( - 25, // adding a ideal 25px shift, generally applied by renderGeometries - pointSeriesDomains.formattedDataSeries.nonStacked[0].dataSeries[0], - xScale, - yScales.get(GROUP_ID)!, - 'red', - CurveType.LINEAR, - false, - 0, - LIGHT_THEME.areaSeriesStyle, - { - enabled: false, - }, - ); + const store = initStore([ + MockSeriesSpec.area({ + id: SPEC_ID, + groupId: GROUP_ID, + xScaleType: ScaleType.Ordinal, + yScaleType: ScaleType.Linear, + xAccessor: 0, + yAccessors: [1], + data: [ + [0, 10], + [1, 5], + ], + }), + ]); + const geometries = computeSeriesGeometriesSelector(store.getState()); + [areaGeometry] = geometries.geometries.areas; + geometriesIndex = geometries.geometriesIndex; }); test('Can render an line and area paths', () => { - const { - areaGeometry: { lines, area, color, seriesIdentifier, transform }, - } = renderedArea; + const { lines, area, color, seriesIdentifier, transform } = areaGeometry; expect(lines[0]).toBe('M0,0L50,50'); expect(area).toBe('M0,0L50,50L50,100L0,100Z'); expect(color).toBe('red'); @@ -152,11 +103,7 @@ describe('Rendering points - areas', () => { }); test('Can render two points', () => { - const { - areaGeometry: { points }, - indexedGeometryMap, - } = renderedArea; - + const { points } = areaGeometry; expect(points[0]).toEqual(({ x: 0, y: 0, @@ -167,7 +114,7 @@ describe('Rendering points - areas', () => { yAccessor: 1, splitAccessors: new Map(), seriesKeys: [1], - key: 'spec{spec_1}yAccessor{1}splitAccessors{}', + key: 'groupId{group_1}spec{spec_1}yAccessor{1}splitAccessors{}', }, styleOverrides: undefined, value: { @@ -192,7 +139,7 @@ describe('Rendering points - areas', () => { yAccessor: 1, splitAccessors: new Map(), seriesKeys: [1], - key: 'spec{spec_1}yAccessor{1}splitAccessors{}', + key: 'groupId{group_1}spec{spec_1}yAccessor{1}splitAccessors{}', }, styleOverrides: undefined, value: { @@ -207,123 +154,76 @@ describe('Rendering points - areas', () => { y: 0, }, } as unknown) as PointGeometry); - expect(indexedGeometryMap.size).toEqual(points.length); + expect(geometriesIndex.size).toEqual(points.length); }); }); describe('Multi series area chart - ordinal', () => { - const spec1Id = 'spec_1'; - const spec2Id = 'spec_2'; - const pointSeriesSpec1: AreaSeriesSpec = { - chartType: ChartTypes.XYAxis, - specType: SpecTypes.Series, - id: spec1Id, - groupId: GROUP_ID, - seriesType: SeriesTypes.Area, - data: [ - [0, 10], - [1, 5], - ], - xAccessor: 0, - yAccessors: [1], - xScaleType: ScaleType.Ordinal, - yScaleType: ScaleType.Linear, - }; - const pointSeriesSpec2: AreaSeriesSpec = { - chartType: ChartTypes.XYAxis, - specType: SpecTypes.Series, - id: spec2Id, - groupId: GROUP_ID, - seriesType: SeriesTypes.Area, - data: [ - [0, 20], - [1, 10], - ], - xAccessor: 0, - yAccessors: [1], - xScaleType: ScaleType.Ordinal, - yScaleType: ScaleType.Linear, - }; - const pointSeriesMap = [pointSeriesSpec1, pointSeriesSpec2]; - const pointSeriesDomains = computeSeriesDomains(pointSeriesMap, new Map()); - const xScale = computeXScale({ - xDomain: pointSeriesDomains.xDomain, - totalBarsInCluster: pointSeriesMap.length, - range: [0, 100], - }); - const yScales = computeYScales({ yDomains: pointSeriesDomains.yDomain, range: [100, 0] }); - - let firstLine: { - areaGeometry: AreaGeometry; - indexedGeometryMap: IndexedGeometryMap; - }; - let secondLine: { - areaGeometry: AreaGeometry; - indexedGeometryMap: IndexedGeometryMap; - }; - + let geometries: ComputedGeometries; beforeEach(() => { - firstLine = renderArea( - 25, // adding a ideal 25px shift, generally applied by renderGeometries - pointSeriesDomains.formattedDataSeries.nonStacked[0].dataSeries[0], - xScale, - yScales.get(GROUP_ID)!, - 'red', - CurveType.LINEAR, - false, - 0, - LIGHT_THEME.areaSeriesStyle, - { - enabled: false, - }, - ); - secondLine = renderArea( - 25, // adding a ideal 25px shift, generally applied by renderGeometries - pointSeriesDomains.formattedDataSeries.nonStacked[0].dataSeries[1], - xScale, - yScales.get(GROUP_ID)!, - 'blue', - CurveType.LINEAR, - false, - 0, - LIGHT_THEME.areaSeriesStyle, - { - enabled: false, - }, + const store = initStore( + [ + MockSeriesSpec.area({ + id: 'spec_1', + groupId: GROUP_ID, + xScaleType: ScaleType.Ordinal, + yScaleType: ScaleType.Linear, + xAccessor: 0, + yAccessors: [1], + data: [ + [0, 10], + [1, 5], + ], + }), + MockSeriesSpec.area({ + id: 'spec_2', + groupId: GROUP_ID, + xScaleType: ScaleType.Ordinal, + yScaleType: ScaleType.Linear, + xAccessor: 0, + yAccessors: [1], + data: [ + [0, 20], + [1, 10], + ], + }), + ], + ['red', 'blue'], ); + geometries = computeSeriesGeometriesSelector(store.getState()); }); test('Can render two ordinal areas', () => { - expect(firstLine.areaGeometry.lines[0]).toBe('M0,50L50,75'); - expect(firstLine.areaGeometry.area).toBe('M0,50L50,75L50,100L0,100Z'); - expect(firstLine.areaGeometry.color).toBe('red'); - expect(firstLine.areaGeometry.seriesIdentifier.seriesKeys).toEqual([1]); - expect(firstLine.areaGeometry.seriesIdentifier.specId).toEqual(spec1Id); - expect(firstLine.areaGeometry.transform).toEqual({ x: 25, y: 0 }); + const { areas } = geometries.geometries; + const [firstArea, secondArea] = areas; + expect(firstArea.lines[0]).toBe('M0,50L50,75'); + expect(firstArea.area).toBe('M0,50L50,75L50,100L0,100Z'); + expect(firstArea.color).toBe('red'); + expect(firstArea.seriesIdentifier.seriesKeys).toEqual([1]); + expect(firstArea.seriesIdentifier.specId).toEqual('spec_1'); + expect(firstArea.transform).toEqual({ x: 25, y: 0 }); - expect(secondLine.areaGeometry.lines[0]).toBe('M0,0L50,50'); - expect(secondLine.areaGeometry.area).toBe('M0,0L50,50L50,100L0,100Z'); - expect(secondLine.areaGeometry.color).toBe('blue'); - expect(secondLine.areaGeometry.seriesIdentifier.seriesKeys).toEqual([1]); - expect(secondLine.areaGeometry.seriesIdentifier.specId).toEqual(spec2Id); - expect(secondLine.areaGeometry.transform).toEqual({ x: 25, y: 0 }); + expect(secondArea.lines[0]).toBe('M0,0L50,50'); + expect(secondArea.area).toBe('M0,0L50,50L50,100L0,100Z'); + expect(secondArea.color).toBe('blue'); + expect(secondArea.seriesIdentifier.seriesKeys).toEqual([1]); + expect(secondArea.seriesIdentifier.specId).toEqual('spec_2'); + expect(secondArea.transform).toEqual({ x: 25, y: 0 }); }); test('can render first spec points', () => { - const { - areaGeometry: { points }, - indexedGeometryMap, - } = firstLine; - expect(points.length).toEqual(2); - expect(points[0]).toEqual(({ + const { areas } = geometries.geometries; + const [firstArea] = areas; + expect(firstArea.points.length).toEqual(2); + expect(firstArea.points[0]).toEqual(({ x: 0, y: 50, radius: 0, color: 'red', seriesIdentifier: { - specId: spec1Id, + specId: 'spec_1', yAccessor: 1, splitAccessors: new Map(), seriesKeys: [1], - key: 'spec{spec_1}yAccessor{1}splitAccessors{}', + key: 'groupId{group_1}spec{spec_1}yAccessor{1}splitAccessors{}', }, styleOverrides: undefined, value: { @@ -338,17 +238,17 @@ describe('Rendering points - areas', () => { y: 0, }, } as unknown) as PointGeometry); - expect(points[1]).toEqual(({ + expect(firstArea.points[1]).toEqual(({ x: 50, y: 75, radius: 0, color: 'red', seriesIdentifier: { - specId: spec1Id, + specId: 'spec_1', yAccessor: 1, splitAccessors: new Map(), seriesKeys: [1], - key: 'spec{spec_1}yAccessor{1}splitAccessors{}', + key: 'groupId{group_1}spec{spec_1}yAccessor{1}splitAccessors{}', }, styleOverrides: undefined, value: { @@ -363,25 +263,22 @@ describe('Rendering points - areas', () => { y: 0, }, } as unknown) as PointGeometry); - expect(indexedGeometryMap.size).toEqual(points.length); }); test('can render second spec points', () => { - const { - areaGeometry: { points }, - indexedGeometryMap, - } = secondLine; - expect(points.length).toEqual(2); - expect(points[0]).toEqual(({ + const { areas } = geometries.geometries; + const [, secondArea] = areas; + expect(secondArea.points.length).toEqual(2); + expect(secondArea.points[0]).toEqual(({ x: 0, y: 0, radius: 0, color: 'blue', seriesIdentifier: { - specId: spec2Id, + specId: 'spec_2', yAccessor: 1, splitAccessors: new Map(), seriesKeys: [1], - key: 'spec{spec_2}yAccessor{1}splitAccessors{}', + key: 'groupId{group_1}spec{spec_2}yAccessor{1}splitAccessors{}', }, styleOverrides: undefined, value: { @@ -396,17 +293,17 @@ describe('Rendering points - areas', () => { y: 0, }, } as unknown) as PointGeometry); - expect(points[1]).toEqual(({ + expect(secondArea.points[1]).toEqual(({ x: 50, y: 50, radius: 0, color: 'blue', seriesIdentifier: { - specId: spec2Id, + specId: 'spec_2', yAccessor: 1, splitAccessors: new Map(), seriesKeys: [1], - key: 'spec{spec_2}yAccessor{1}splitAccessors{}', + key: 'groupId{group_1}spec{spec_2}yAccessor{1}splitAccessors{}', }, styleOverrides: undefined, value: { @@ -421,69 +318,51 @@ describe('Rendering points - areas', () => { y: 0, }, } as unknown) as PointGeometry); - expect(indexedGeometryMap.size).toEqual(points.length); }); - }); - describe('Single series area chart - linear', () => { - const pointSeriesSpec: AreaSeriesSpec = { - chartType: ChartTypes.XYAxis, - specType: SpecTypes.Series, - id: SPEC_ID, - groupId: GROUP_ID, - seriesType: SeriesTypes.Area, - data: [ - [0, 10], - [1, 5], - ], - xAccessor: 0, - yAccessors: [1], - xScaleType: ScaleType.Linear, - yScaleType: ScaleType.Linear, - }; - const pointSeriesMap = [pointSeriesSpec]; - const pointSeriesDomains = computeSeriesDomains(pointSeriesMap, new Map()); - const xScale = computeXScale({ - xDomain: pointSeriesDomains.xDomain, - totalBarsInCluster: pointSeriesMap.length, - range: [0, 100], + test('has the right number of geometry in the indexes', () => { + const { areas } = geometries.geometries; + const [firstArea] = areas; + expect(geometries.geometriesIndex.size).toEqual(firstArea.points.length); }); - const yScales = computeYScales({ yDomains: pointSeriesDomains.yDomain, range: [100, 0] }); - - let renderedArea: { - areaGeometry: AreaGeometry; - indexedGeometryMap: IndexedGeometryMap; - }; + }); + describe('Single series area chart - linear', () => { + let geometries: ComputedGeometries; beforeEach(() => { - renderedArea = renderArea( - 0, // not applied any shift, renderGeometries applies it only with mixed charts - pointSeriesDomains.formattedDataSeries.nonStacked[0].dataSeries[0], - xScale, - yScales.get(GROUP_ID)!, - 'red', - CurveType.LINEAR, - false, - 0, - LIGHT_THEME.areaSeriesStyle, - { - enabled: false, - }, + const store = initStore( + [ + MockSeriesSpec.area({ + id: 'spec_1', + groupId: GROUP_ID, + xScaleType: ScaleType.Linear, + yScaleType: ScaleType.Linear, + xAccessor: 0, + yAccessors: [1], + data: [ + [0, 10], + [1, 5], + ], + }), + ], + ['red'], ); + geometries = computeSeriesGeometriesSelector(store.getState()); }); + test('Can render a linear area', () => { - expect(renderedArea.areaGeometry.lines[0]).toBe('M0,0L100,50'); - expect(renderedArea.areaGeometry.area).toBe('M0,0L100,50L100,100L0,100Z'); - expect(renderedArea.areaGeometry.color).toBe('red'); - expect(renderedArea.areaGeometry.seriesIdentifier.seriesKeys).toEqual([1]); - expect(renderedArea.areaGeometry.seriesIdentifier.specId).toEqual(SPEC_ID); - expect(renderedArea.areaGeometry.transform).toEqual({ x: 0, y: 0 }); + const { areas } = geometries.geometries; + const [firstArea] = areas; + expect(firstArea.lines[0]).toBe('M0,0L100,50'); + expect(firstArea.area).toBe('M0,0L100,50L100,100L0,100Z'); + expect(firstArea.color).toBe('red'); + expect(firstArea.seriesIdentifier.seriesKeys).toEqual([1]); + expect(firstArea.seriesIdentifier.specId).toEqual(SPEC_ID); + expect(firstArea.transform).toEqual({ x: 0, y: 0 }); }); test('Can render two points', () => { - const { - areaGeometry: { points }, - indexedGeometryMap, - } = renderedArea; - expect(points[0]).toEqual(({ + const { areas } = geometries.geometries; + const [firstArea] = areas; + expect(firstArea.points[0]).toEqual(({ x: 0, y: 0, radius: 0, @@ -493,7 +372,7 @@ describe('Rendering points - areas', () => { yAccessor: 1, splitAccessors: new Map(), seriesKeys: [1], - key: 'spec{spec_1}yAccessor{1}splitAccessors{}', + key: 'groupId{group_1}spec{spec_1}yAccessor{1}splitAccessors{}', }, styleOverrides: undefined, value: { @@ -508,7 +387,7 @@ describe('Rendering points - areas', () => { y: 0, }, } as unknown) as PointGeometry); - expect(points[1]).toEqual(({ + expect(firstArea.points[1]).toEqual(({ x: 100, y: 50, radius: 0, @@ -518,7 +397,7 @@ describe('Rendering points - areas', () => { yAccessor: 1, splitAccessors: new Map(), seriesKeys: [1], - key: 'spec{spec_1}yAccessor{1}splitAccessors{}', + key: 'groupId{group_1}spec{spec_1}yAccessor{1}splitAccessors{}', }, styleOverrides: undefined, value: { @@ -533,122 +412,76 @@ describe('Rendering points - areas', () => { y: 0, }, } as unknown) as PointGeometry); - expect(indexedGeometryMap.size).toEqual(points.length); + expect(geometries.geometriesIndex.size).toEqual(firstArea.points.length); }); }); - describe('Multi series area chart - linear', () => { - const spec1Id = 'spec_1'; - const spec2Id = 'spec_2'; - const pointSeriesSpec1: AreaSeriesSpec = { - chartType: ChartTypes.XYAxis, - specType: SpecTypes.Series, - id: spec1Id, - groupId: GROUP_ID, - seriesType: SeriesTypes.Area, - data: [ - [0, 10], - [1, 5], - ], - xAccessor: 0, - yAccessors: [1], - xScaleType: ScaleType.Linear, - yScaleType: ScaleType.Linear, - }; - const pointSeriesSpec2: AreaSeriesSpec = { - chartType: ChartTypes.XYAxis, - specType: SpecTypes.Series, - id: spec2Id, - groupId: GROUP_ID, - seriesType: SeriesTypes.Area, - data: [ - [0, 20], - [1, 10], - ], - xAccessor: 0, - yAccessors: [1], - xScaleType: ScaleType.Linear, - yScaleType: ScaleType.Linear, - }; - const pointSeriesMap = [pointSeriesSpec1, pointSeriesSpec2]; - const pointSeriesDomains = computeSeriesDomains(pointSeriesMap, new Map()); - const xScale = computeXScale({ - xDomain: pointSeriesDomains.xDomain, - totalBarsInCluster: pointSeriesMap.length, - range: [0, 100], - }); - const yScales = computeYScales({ yDomains: pointSeriesDomains.yDomain, range: [100, 0] }); - - let firstLine: { - areaGeometry: AreaGeometry; - indexedGeometryMap: IndexedGeometryMap; - }; - let secondLine: { - areaGeometry: AreaGeometry; - indexedGeometryMap: IndexedGeometryMap; - }; + describe('Multi series area chart - linear', () => { + let geometries: ComputedGeometries; beforeEach(() => { - firstLine = renderArea( - 0, // not applied any shift, renderGeometries applies it only with mixed charts - pointSeriesDomains.formattedDataSeries.nonStacked[0].dataSeries[0], - xScale, - yScales.get(GROUP_ID)!, - 'red', - CurveType.LINEAR, - false, - 0, - LIGHT_THEME.areaSeriesStyle, - { - enabled: false, - }, - ); - secondLine = renderArea( - 0, // not applied any shift, renderGeometries applies it only with mixed charts - pointSeriesDomains.formattedDataSeries.nonStacked[0].dataSeries[1], - xScale, - yScales.get(GROUP_ID)!, - 'blue', - CurveType.LINEAR, - false, - 0, - LIGHT_THEME.areaSeriesStyle, - { - enabled: false, - }, + const store = initStore( + [ + MockSeriesSpec.area({ + id: 'spec_1', + groupId: GROUP_ID, + xScaleType: ScaleType.Linear, + yScaleType: ScaleType.Linear, + xAccessor: 0, + yAccessors: [1], + data: [ + [0, 10], + [1, 5], + ], + }), + MockSeriesSpec.area({ + id: 'spec_2', + groupId: GROUP_ID, + xScaleType: ScaleType.Linear, + yScaleType: ScaleType.Linear, + xAccessor: 0, + yAccessors: [1], + data: [ + [0, 20], + [1, 10], + ], + }), + ], + ['red', 'blue'], ); + geometries = computeSeriesGeometriesSelector(store.getState()); }); test('can render two linear areas', () => { - expect(firstLine.areaGeometry.lines[0]).toBe('M0,50L100,75'); - expect(firstLine.areaGeometry.area).toBe('M0,50L100,75L100,100L0,100Z'); - expect(firstLine.areaGeometry.color).toBe('red'); - expect(firstLine.areaGeometry.seriesIdentifier.seriesKeys).toEqual([1]); - expect(firstLine.areaGeometry.seriesIdentifier.specId).toEqual(spec1Id); - expect(firstLine.areaGeometry.transform).toEqual({ x: 0, y: 0 }); + const { areas } = geometries.geometries; + const [firstArea, secondArea] = areas; + expect(firstArea.lines[0]).toBe('M0,50L100,75'); + expect(firstArea.area).toBe('M0,50L100,75L100,100L0,100Z'); + expect(firstArea.color).toBe('red'); + expect(firstArea.seriesIdentifier.seriesKeys).toEqual([1]); + expect(firstArea.seriesIdentifier.specId).toEqual('spec_1'); + expect(firstArea.transform).toEqual({ x: 0, y: 0 }); - expect(secondLine.areaGeometry.lines[0]).toBe('M0,0L100,50'); - expect(secondLine.areaGeometry.area).toBe('M0,0L100,50L100,100L0,100Z'); - expect(secondLine.areaGeometry.color).toBe('blue'); - expect(secondLine.areaGeometry.seriesIdentifier.seriesKeys).toEqual([1]); - expect(secondLine.areaGeometry.seriesIdentifier.specId).toEqual(spec2Id); - expect(secondLine.areaGeometry.transform).toEqual({ x: 0, y: 0 }); + expect(secondArea.lines[0]).toBe('M0,0L100,50'); + expect(secondArea.area).toBe('M0,0L100,50L100,100L0,100Z'); + expect(secondArea.color).toBe('blue'); + expect(secondArea.seriesIdentifier.seriesKeys).toEqual([1]); + expect(secondArea.seriesIdentifier.specId).toEqual('spec_2'); + expect(secondArea.transform).toEqual({ x: 0, y: 0 }); }); test('can render first spec points', () => { - const { - areaGeometry: { points }, - indexedGeometryMap, - } = firstLine; - expect(points.length).toEqual(2); - expect(points[0]).toEqual(({ + const { areas } = geometries.geometries; + const [firstArea] = areas; + expect(firstArea.points.length).toEqual(2); + expect(firstArea.points[0]).toEqual(({ x: 0, y: 50, radius: 0, color: 'red', seriesIdentifier: { - specId: spec1Id, + specId: 'spec_1', yAccessor: 1, splitAccessors: new Map(), seriesKeys: [1], - key: 'spec{spec_1}yAccessor{1}splitAccessors{}', + key: 'groupId{group_1}spec{spec_1}yAccessor{1}splitAccessors{}', }, styleOverrides: undefined, value: { @@ -663,17 +496,17 @@ describe('Rendering points - areas', () => { y: 0, }, } as unknown) as PointGeometry); - expect(points[1]).toEqual(({ + expect(firstArea.points[1]).toEqual(({ x: 100, y: 75, radius: 0, color: 'red', seriesIdentifier: { - specId: spec1Id, + specId: 'spec_1', yAccessor: 1, splitAccessors: new Map(), seriesKeys: [1], - key: 'spec{spec_1}yAccessor{1}splitAccessors{}', + key: 'groupId{group_1}spec{spec_1}yAccessor{1}splitAccessors{}', }, styleOverrides: undefined, value: { @@ -688,25 +521,23 @@ describe('Rendering points - areas', () => { y: 0, }, } as unknown) as PointGeometry); - expect(indexedGeometryMap.size).toEqual(points.length); + expect(geometries.geometriesIndex.size).toEqual(firstArea.points.length); }); test('can render second spec points', () => { - const { - areaGeometry: { points }, - indexedGeometryMap, - } = secondLine; - expect(points.length).toEqual(2); - expect(points[0]).toEqual(({ + const { areas } = geometries.geometries; + const [, secondArea] = areas; + expect(secondArea.points.length).toEqual(2); + expect(secondArea.points[0]).toEqual(({ x: 0, y: 0, radius: 0, color: 'blue', seriesIdentifier: { - specId: spec2Id, + specId: 'spec_2', yAccessor: 1, splitAccessors: new Map(), seriesKeys: [1], - key: 'spec{spec_2}yAccessor{1}splitAccessors{}', + key: 'groupId{group_1}spec{spec_2}yAccessor{1}splitAccessors{}', }, styleOverrides: undefined, value: { @@ -721,17 +552,17 @@ describe('Rendering points - areas', () => { y: 0, }, } as unknown) as PointGeometry); - expect(points[1]).toEqual(({ + expect(secondArea.points[1]).toEqual(({ x: 100, y: 50, radius: 0, color: 'blue', seriesIdentifier: { - specId: spec2Id, + specId: 'spec_2', yAccessor: 1, splitAccessors: new Map(), seriesKeys: [1], - key: 'spec{spec_2}yAccessor{1}splitAccessors{}', + key: 'groupId{group_1}spec{spec_2}yAccessor{1}splitAccessors{}', }, styleOverrides: undefined, value: { @@ -746,69 +577,46 @@ describe('Rendering points - areas', () => { y: 0, }, } as unknown) as PointGeometry); - expect(indexedGeometryMap.size).toEqual(points.length); + expect(geometries.geometriesIndex.size).toEqual(secondArea.points.length); }); }); describe('Single series area chart - time', () => { - const pointSeriesSpec: AreaSeriesSpec = { - chartType: ChartTypes.XYAxis, - specType: SpecTypes.Series, - id: SPEC_ID, - groupId: GROUP_ID, - seriesType: SeriesTypes.Area, - data: [ - [1546300800000, 10], - [1546387200000, 5], - ], - xAccessor: 0, - yAccessors: [1], - xScaleType: ScaleType.Time, - yScaleType: ScaleType.Linear, - }; - const pointSeriesMap = [pointSeriesSpec]; - const pointSeriesDomains = computeSeriesDomains(pointSeriesMap, new Map()); - const xScale = computeXScale({ - xDomain: pointSeriesDomains.xDomain, - totalBarsInCluster: pointSeriesMap.length, - range: [0, 100], - }); - const yScales = computeYScales({ yDomains: pointSeriesDomains.yDomain, range: [100, 0] }); - - let renderedArea: { - areaGeometry: AreaGeometry; - indexedGeometryMap: IndexedGeometryMap; - }; - + let geometries: ComputedGeometries; beforeEach(() => { - renderedArea = renderArea( - 0, // not applied any shift, renderGeometries applies it only with mixed charts - pointSeriesDomains.formattedDataSeries.nonStacked[0].dataSeries[0], - xScale, - yScales.get(GROUP_ID)!, - 'red', - CurveType.LINEAR, - false, - 0, - LIGHT_THEME.areaSeriesStyle, - { - enabled: false, - }, + const store = initStore( + [ + MockSeriesSpec.area({ + id: 'spec_1', + groupId: GROUP_ID, + xScaleType: ScaleType.Time, + yScaleType: ScaleType.Linear, + xAccessor: 0, + yAccessors: [1], + data: [ + [1546300800000, 10], + [1546387200000, 5], + ], + }), + ], + ['red'], ); + geometries = computeSeriesGeometriesSelector(store.getState()); }); + test('Can render a time area', () => { - expect(renderedArea.areaGeometry.lines[0]).toBe('M0,0L100,50'); - expect(renderedArea.areaGeometry.area).toBe('M0,0L100,50L100,100L0,100Z'); - expect(renderedArea.areaGeometry.color).toBe('red'); - expect(renderedArea.areaGeometry.seriesIdentifier.seriesKeys).toEqual([1]); - expect(renderedArea.areaGeometry.seriesIdentifier.specId).toEqual(SPEC_ID); - expect(renderedArea.areaGeometry.transform).toEqual({ x: 0, y: 0 }); + const { areas } = geometries.geometries; + const [firstArea] = areas; + expect(firstArea.lines[0]).toBe('M0,0L100,50'); + expect(firstArea.area).toBe('M0,0L100,50L100,100L0,100Z'); + expect(firstArea.color).toBe('red'); + expect(firstArea.seriesIdentifier.seriesKeys).toEqual([1]); + expect(firstArea.seriesIdentifier.specId).toEqual(SPEC_ID); + expect(firstArea.transform).toEqual({ x: 0, y: 0 }); }); test('Can render two points', () => { - const { - areaGeometry: { points }, - indexedGeometryMap, - } = renderedArea; - expect(points[0]).toEqual(({ + const { areas } = geometries.geometries; + const [firstArea] = areas; + expect(firstArea.points[0]).toEqual(({ x: 0, y: 0, radius: 0, @@ -818,7 +626,7 @@ describe('Rendering points - areas', () => { yAccessor: 1, splitAccessors: new Map(), seriesKeys: [1], - key: 'spec{spec_1}yAccessor{1}splitAccessors{}', + key: 'groupId{group_1}spec{spec_1}yAccessor{1}splitAccessors{}', }, styleOverrides: undefined, value: { @@ -833,7 +641,7 @@ describe('Rendering points - areas', () => { y: 0, }, } as unknown) as PointGeometry); - expect(points[1]).toEqual(({ + expect(firstArea.points[1]).toEqual(({ x: 100, y: 50, radius: 0, @@ -843,7 +651,7 @@ describe('Rendering points - areas', () => { yAccessor: 1, splitAccessors: new Map(), seriesKeys: [1], - key: 'spec{spec_1}yAccessor{1}splitAccessors{}', + key: 'groupId{group_1}spec{spec_1}yAccessor{1}splitAccessors{}', }, styleOverrides: undefined, value: { @@ -858,107 +666,59 @@ describe('Rendering points - areas', () => { y: 0, }, } as unknown) as PointGeometry); - expect(indexedGeometryMap.size).toEqual(points.length); + expect(geometries.geometriesIndex.size).toEqual(firstArea.points.length); }); }); describe('Multi series area chart - time', () => { - const spec1Id = 'spec_1'; - const spec2Id = 'spec_2'; - const pointSeriesSpec1: AreaSeriesSpec = { - chartType: ChartTypes.XYAxis, - specType: SpecTypes.Series, - id: spec1Id, - groupId: GROUP_ID, - seriesType: SeriesTypes.Area, - data: [ - [1546300800000, 10], - [1546387200000, 5], - ], - xAccessor: 0, - yAccessors: [1], - xScaleType: ScaleType.Time, - yScaleType: ScaleType.Linear, - }; - const pointSeriesSpec2: AreaSeriesSpec = { - chartType: ChartTypes.XYAxis, - specType: SpecTypes.Series, - id: spec2Id, - groupId: GROUP_ID, - seriesType: SeriesTypes.Area, - data: [ - [1546300800000, 20], - [1546387200000, 10], - ], - xAccessor: 0, - yAccessors: [1], - xScaleType: ScaleType.Time, - yScaleType: ScaleType.Linear, - }; - const pointSeriesMap = [pointSeriesSpec1, pointSeriesSpec2]; - const pointSeriesDomains = computeSeriesDomains(pointSeriesMap, new Map()); - const xScale = computeXScale({ - xDomain: pointSeriesDomains.xDomain, - totalBarsInCluster: pointSeriesMap.length, - range: [0, 100], - }); - const yScales = computeYScales({ yDomains: pointSeriesDomains.yDomain, range: [100, 0] }); - - let firstLine: { - areaGeometry: AreaGeometry; - indexedGeometryMap: IndexedGeometryMap; - }; - let secondLine: { - areaGeometry: AreaGeometry; - indexedGeometryMap: IndexedGeometryMap; - }; - + let geometries: ComputedGeometries; beforeEach(() => { - firstLine = renderArea( - 0, // not applied any shift, renderGeometries applies it only with mixed charts - pointSeriesDomains.formattedDataSeries.nonStacked[0].dataSeries[0], - xScale, - yScales.get(GROUP_ID)!, - 'red', - CurveType.LINEAR, - false, - 0, - LIGHT_THEME.areaSeriesStyle, - { - enabled: false, - }, - ); - secondLine = renderArea( - 0, // not applied any shift, renderGeometries applies it only with mixed charts - pointSeriesDomains.formattedDataSeries.nonStacked[0].dataSeries[1], - xScale, - yScales.get(GROUP_ID)!, - 'blue', - CurveType.LINEAR, - false, - 0, - LIGHT_THEME.areaSeriesStyle, - { - enabled: false, - }, + const store = initStore( + [ + MockSeriesSpec.area({ + id: 'spec_1', + groupId: GROUP_ID, + xScaleType: ScaleType.Time, + yScaleType: ScaleType.Linear, + xAccessor: 0, + yAccessors: [1], + data: [ + [1546300800000, 10], + [1546387200000, 5], + ], + }), + MockSeriesSpec.area({ + id: 'spec_2', + groupId: GROUP_ID, + xScaleType: ScaleType.Time, + yScaleType: ScaleType.Linear, + xAccessor: 0, + yAccessors: [1], + data: [ + [1546300800000, 20], + [1546387200000, 10], + ], + }), + ], + ['red', 'blue'], ); + geometries = computeSeriesGeometriesSelector(store.getState()); }); + test('can render first spec points', () => { - const { - areaGeometry: { points }, - indexedGeometryMap, - } = firstLine; - expect(points.length).toEqual(2); - expect(points[0]).toEqual(({ + const { areas } = geometries.geometries; + const [firstArea] = areas; + expect(firstArea.points.length).toEqual(2); + expect(firstArea.points[0]).toEqual(({ x: 0, y: 50, radius: 0, color: 'red', seriesIdentifier: { - specId: spec1Id, + specId: 'spec_1', yAccessor: 1, splitAccessors: new Map(), seriesKeys: [1], - key: 'spec{spec_1}yAccessor{1}splitAccessors{}', + key: 'groupId{group_1}spec{spec_1}yAccessor{1}splitAccessors{}', }, styleOverrides: undefined, value: { @@ -973,17 +733,17 @@ describe('Rendering points - areas', () => { y: 0, }, } as unknown) as PointGeometry); - expect(points[1]).toEqual(({ + expect(firstArea.points[1]).toEqual(({ x: 100, y: 75, radius: 0, color: 'red', seriesIdentifier: { - specId: spec1Id, + specId: 'spec_1', yAccessor: 1, splitAccessors: new Map(), seriesKeys: [1], - key: 'spec{spec_1}yAccessor{1}splitAccessors{}', + key: 'groupId{group_1}spec{spec_1}yAccessor{1}splitAccessors{}', }, styleOverrides: undefined, value: { @@ -998,25 +758,24 @@ describe('Rendering points - areas', () => { y: 0, }, } as unknown) as PointGeometry); - expect(indexedGeometryMap.size).toEqual(points.length); + expect(geometries.geometriesIndex.size).toEqual(firstArea.points.length); }); test('can render second spec points', () => { - const { - areaGeometry: { points }, - indexedGeometryMap, - } = secondLine; - expect(points.length).toEqual(2); - expect(points[0]).toEqual(({ + const { areas } = geometries.geometries; + const [, secondArea] = areas; + + expect(secondArea.points.length).toEqual(2); + expect(secondArea.points[0]).toEqual(({ x: 0, y: 0, radius: 0, color: 'blue', seriesIdentifier: { - specId: spec2Id, + specId: 'spec_2', yAccessor: 1, splitAccessors: new Map(), seriesKeys: [1], - key: 'spec{spec_2}yAccessor{1}splitAccessors{}', + key: 'groupId{group_1}spec{spec_2}yAccessor{1}splitAccessors{}', }, styleOverrides: undefined, value: { @@ -1031,17 +790,17 @@ describe('Rendering points - areas', () => { y: 0, }, } as unknown) as PointGeometry); - expect(points[1]).toEqual(({ + expect(secondArea.points[1]).toEqual(({ x: 100, y: 50, radius: 0, color: 'blue', seriesIdentifier: { - specId: spec2Id, + specId: 'spec_2', yAccessor: 1, splitAccessors: new Map(), seriesKeys: [1], - key: 'spec{spec_2}yAccessor{1}splitAccessors{}', + key: 'groupId{group_1}spec{spec_2}yAccessor{1}splitAccessors{}', }, styleOverrides: undefined, value: { @@ -1056,84 +815,64 @@ describe('Rendering points - areas', () => { y: 0, }, } as unknown) as PointGeometry); - expect(indexedGeometryMap.size).toEqual(points.length); + expect(geometries.geometriesIndex.size).toEqual(secondArea.points.length); }); }); describe('Single series area chart - y log', () => { - const pointSeriesSpec: AreaSeriesSpec = { - chartType: ChartTypes.XYAxis, - specType: SpecTypes.Series, - id: SPEC_ID, - groupId: GROUP_ID, - seriesType: SeriesTypes.Area, - data: [ - [0, 10], - [1, 5], - [2, null], - [3, 5], - [4, 5], - [5, 0], - [6, 10], - [7, 10], - [8, 10], - ], - xAccessor: 0, - yAccessors: [1], - xScaleType: ScaleType.Linear, - yScaleType: ScaleType.Log, - }; - const pointSeriesMap = [pointSeriesSpec]; - const pointSeriesDomains = computeSeriesDomains(pointSeriesMap, new Map()); - const xScale = computeXScale({ - xDomain: pointSeriesDomains.xDomain, - totalBarsInCluster: pointSeriesMap.length, - range: [0, 90], - }); - const yScales = computeYScales({ yDomains: pointSeriesDomains.yDomain, range: [100, 0] }); - - let renderedArea: { - areaGeometry: AreaGeometry; - indexedGeometryMap: IndexedGeometryMap; - }; - + let geometries: ComputedGeometries; beforeEach(() => { - renderedArea = renderArea( - 0, // not applied any shift, renderGeometries applies it only with mixed charts - pointSeriesDomains.formattedDataSeries.nonStacked[0].dataSeries[0], - xScale, - yScales.get(GROUP_ID)!, - 'red', - CurveType.LINEAR, - false, - 0, - LIGHT_THEME.areaSeriesStyle, - { - enabled: false, - }, + const store = initStore( + [ + MockSeriesSpec.area({ + id: 'spec_1', + groupId: GROUP_ID, + xScaleType: ScaleType.Linear, + yScaleType: ScaleType.Log, + xAccessor: 0, + yAccessors: [1], + data: [ + [0, 10], + [1, 5], + [2, null], + [3, 5], + [4, 5], + [5, 0], + [6, 10], + [7, 10], + [8, 10], + ], + }), + ], + ['red'], + 90, ); + geometries = computeSeriesGeometriesSelector(store.getState()); }); + test('Can render a splitted area and line', () => { - // expect(renderedArea.lineGeometry.line).toBe('ss'); - expect(renderedArea.areaGeometry.lines[0].split('M').length - 1).toBe(3); - expect(renderedArea.areaGeometry.area.split('M').length - 1).toBe(3); - expect(renderedArea.areaGeometry.color).toBe('red'); - expect(renderedArea.areaGeometry.seriesIdentifier.seriesKeys).toEqual([1]); - expect(renderedArea.areaGeometry.seriesIdentifier.specId).toEqual(SPEC_ID); - expect(renderedArea.areaGeometry.transform).toEqual({ x: 0, y: 0 }); + const { areas } = geometries.geometries; + const [firstArea] = areas; + expect(firstArea.lines[0].split('M').length - 1).toBe(3); + expect(firstArea.area.split('M').length - 1).toBe(3); + expect(firstArea.color).toBe('red'); + expect(firstArea.seriesIdentifier.seriesKeys).toEqual([1]); + expect(firstArea.seriesIdentifier.specId).toEqual(SPEC_ID); + expect(firstArea.transform).toEqual({ x: 0, y: 0 }); }); test('Can render points', () => { const { - areaGeometry: { points }, - indexedGeometryMap, - } = renderedArea; + geometriesIndex, + geometries: { areas }, + } = geometries; + const [{ points }] = areas; // all the points minus the undefined ones on a log scale expect(points.length).toBe(7); // all the points expect null geometries - expect(indexedGeometryMap.size).toEqual(8); - const nullIndexdGeometry = indexedGeometryMap.find(2)!; + expect(geometriesIndex.size).toEqual(8); + const nullIndexdGeometry = geometriesIndex.find(2)!; expect(nullIndexdGeometry).toEqual([]); - const zeroValueIndexdGeometry = indexedGeometryMap.find(5)!; + const zeroValueIndexdGeometry = geometriesIndex.find(5)!; expect(zeroValueIndexdGeometry).toBeDefined(); expect(zeroValueIndexdGeometry.length).toBe(1); // moved to the bottom of the chart @@ -1169,8 +908,11 @@ describe('Rendering points - areas', () => { stackAccessors: [0], stackMode: StackMode.Percentage, }); - const pointSeriesDomains = computeSeriesDomains([pointSeriesSpec1, pointSeriesSpec2]); - expect(pointSeriesDomains.formattedDataSeries.stacked[0].dataSeries[0].data).toMatchObject([ + + const store = initStore([pointSeriesSpec1, pointSeriesSpec2]); + const domains = computeSeriesDomainsSelector(store.getState()); + + expect(domains.formattedDataSeries[0].data).toMatchObject([ { datum: [1546300800000, 0], initialY0: null, @@ -1216,8 +958,10 @@ describe('Rendering points - areas', () => { yScaleType: ScaleType.Linear, stackAccessors: [0], }); - const pointSeriesDomains = computeSeriesDomains([pointSeriesSpec1, pointSeriesSpec2]); - expect(pointSeriesDomains.formattedDataSeries.stacked[0].dataSeries[0].data).toMatchObject([ + const store = initStore([pointSeriesSpec1, pointSeriesSpec2]); + const domains = computeSeriesDomainsSelector(store.getState()); + + expect(domains.formattedDataSeries[0].data).toMatchObject([ { datum: [1546300800000, null], initialY0: null, @@ -1238,7 +982,7 @@ describe('Rendering points - areas', () => { }, ]); - expect(pointSeriesDomains.formattedDataSeries.stacked[0].dataSeries[1].data).toEqual([ + expect(domains.formattedDataSeries[1].data).toEqual([ { datum: [1546300800000, 3], initialY0: null, @@ -1260,90 +1004,90 @@ describe('Rendering points - areas', () => { ]); }); - describe('Error guards for scaled values', () => { - const pointSeriesSpec: AreaSeriesSpec = { - chartType: ChartTypes.XYAxis, - specType: SpecTypes.Series, - id: SPEC_ID, - groupId: GROUP_ID, - seriesType: SeriesTypes.Area, - data: [ - [0, 10], - [1, 5], - ], - xAccessor: 0, - yAccessors: [1], - xScaleType: ScaleType.Ordinal, - yScaleType: ScaleType.Linear, - }; - const pointSeriesMap = [pointSeriesSpec]; - const pointSeriesDomains = computeSeriesDomains(pointSeriesMap, new Map()); - const xScale = computeXScale({ - xDomain: pointSeriesDomains.xDomain, - totalBarsInCluster: pointSeriesMap.length, - range: [0, 100], - }); - const yScales = computeYScales({ yDomains: pointSeriesDomains.yDomain, range: [100, 0] }); - let renderedArea: { - areaGeometry: AreaGeometry; - indexedGeometryMap: IndexedGeometryMap; - }; - - beforeEach(() => { - renderedArea = renderArea( - 25, // adding a ideal 25px shift, generally applied by renderGeometries - pointSeriesDomains.formattedDataSeries.nonStacked[0].dataSeries[0], - xScale, - yScales.get(GROUP_ID)!, - 'red', - CurveType.LINEAR, - false, - 0, - LIGHT_THEME.areaSeriesStyle, - { - enabled: false, - }, - ); - }); - - describe('xScale values throw error', () => { - beforeAll(() => { - jest.spyOn(xScale, 'scaleOrThrow').mockImplementation(() => { - throw new Error(); - }); - }); - - test('Should include no lines nor area', () => { - const { - areaGeometry: { lines, area, color, seriesIdentifier, transform }, - } = renderedArea; - expect(lines).toHaveLength(0); - expect(area).toBe(''); - expect(color).toBe('red'); - expect(seriesIdentifier.seriesKeys).toEqual([1]); - expect(seriesIdentifier.specId).toEqual(SPEC_ID); - expect(transform).toEqual({ x: 25, y: 0 }); - }); - }); - - describe('yScale values throw error', () => { - beforeAll(() => { - jest.spyOn(yScales.get(GROUP_ID)!, 'scaleOrThrow').mockImplementation(() => { - throw new Error(); - }); - }); - - test('Should include no lines nor area', () => { - const { - areaGeometry: { lines, area, color, seriesIdentifier, transform }, - } = renderedArea; - expect(lines).toHaveLength(0); - expect(area).toBe(''); - expect(color).toBe('red'); - expect(seriesIdentifier.seriesKeys).toEqual([1]); - expect(seriesIdentifier.specId).toEqual(SPEC_ID); - expect(transform).toEqual({ x: 25, y: 0 }); - }); - }); - }); + // describe('Error guards for scaled values', () => { + // const pointSeriesSpec: AreaSeriesSpec = { + // chartType: ChartTypes.XYAxis, + // specType: SpecTypes.Series, + // id: SPEC_ID, + // groupId: GROUP_ID, + // seriesType: SeriesTypes.Area, + // data: [ + // [0, 10], + // [1, 5], + // ], + // xAccessor: 0, + // yAccessors: [1], + // xScaleType: ScaleType.Ordinal, + // yScaleType: ScaleType.Linear, + // }; + // const pointSeriesMap = [pointSeriesSpec]; + // const pointSeriesDomains = computeSeriesDomains(pointSeriesMap, new Map()); + // const xScale = computeXScale({ + // xDomain: pointSeriesDomains.xDomain, + // totalBarsInCluster: pointSeriesMap.length, + // range: [0, 100], + // }); + // const yScales = computeYScales({ yDomains: pointSeriesDomains.yDomain, range: [100, 0] }); + // let renderedArea: { + // areaGeometry: AreaGeometry; + // indexedGeometryMap: IndexedGeometryMap; + // }; + // + // beforeEach(() => { + // renderedArea = renderArea( + // 25, // adding a ideal 25px shift, generally applied by renderGeometries + // pointSeriesDomains.formattedDataSeries.nonStacked[0].dataSeries[0], + // xScale, + // yScales.get(GROUP_ID)!, + // 'red', + // CurveType.LINEAR, + // false, + // 0, + // LIGHT_THEME.areaSeriesStyle, + // { + // enabled: false, + // }, + // ); + // }); + // + // describe('xScale values throw error', () => { + // beforeAll(() => { + // jest.spyOn(xScale, 'scaleOrThrow').mockImplementation(() => { + // throw new Error(); + // }); + // }); + // + // test('Should include no lines nor area', () => { + // const { + // areaGeometry: { lines, area, color, seriesIdentifier, transform }, + // } = renderedArea; + // expect(lines).toHaveLength(0); + // expect(area).toBe(''); + // expect(color).toBe('red'); + // expect(seriesIdentifier.seriesKeys).toEqual([1]); + // expect(seriesIdentifier.specId).toEqual(SPEC_ID); + // expect(transform).toEqual({ x: 25, y: 0 }); + // }); + // }); + // + // describe('yScale values throw error', () => { + // beforeAll(() => { + // jest.spyOn(yScales.get(GROUP_ID)!, 'scaleOrThrow').mockImplementation(() => { + // throw new Error(); + // }); + // }); + // + // test('Should include no lines nor area', () => { + // const { + // areaGeometry: { lines, area, color, seriesIdentifier, transform }, + // } = renderedArea; + // expect(lines).toHaveLength(0); + // expect(area).toBe(''); + // expect(color).toBe('red'); + // expect(seriesIdentifier.seriesKeys).toEqual([1]); + // expect(seriesIdentifier.specId).toEqual(SPEC_ID); + // expect(transform).toEqual({ x: 25, y: 0 }); + // }); + // }); + // }); }); diff --git a/src/chart_types/xy_chart/rendering/rendering.bars.test.ts b/src/chart_types/xy_chart/rendering/rendering.bars.test.ts index 1d479f1a96..ba903ad01a 100644 --- a/src/chart_types/xy_chart/rendering/rendering.bars.test.ts +++ b/src/chart_types/xy_chart/rendering/rendering.bars.test.ts @@ -17,196 +17,40 @@ * under the License. */ -import { ChartTypes } from '../..'; import { MockBarGeometry } from '../../../mocks'; +import { MockGlobalSpec, MockSeriesSpec } from '../../../mocks/specs'; +import { MockStore } from '../../../mocks/store'; import { ScaleType } from '../../../scales/constants'; -import { SpecTypes } from '../../../specs/constants'; import { identity } from '../../../utils/commons'; -import { GroupId } from '../../../utils/ids'; -import { LIGHT_THEME } from '../../../utils/themes/light_theme'; -import { computeSeriesDomains } from '../state/utils/utils'; -import { computeXScale, computeYScales } from '../utils/scales'; -import { BarSeriesSpec, DomainRange, SeriesTypes } from '../utils/specs'; -import { renderBars } from './rendering'; +import { computeSeriesGeometriesSelector } from '../state/selectors/compute_series_geometries'; const SPEC_ID = 'spec_1'; const GROUP_ID = 'group_1'; describe('Rendering bars', () => { - describe('Single series bar chart - ordinal', () => { - const barSeriesSpec: BarSeriesSpec = { - chartType: ChartTypes.XYAxis, - specType: SpecTypes.Series, - id: SPEC_ID, - groupId: GROUP_ID, - seriesType: SeriesTypes.Bar, - data: [ - [-200, 0], - [0, 10], - [1, 5], - ], // first datum should be skipped as it's out of domain - xAccessor: 0, - yAccessors: [1], - xScaleType: ScaleType.Ordinal, - yScaleType: ScaleType.Linear, - }; - const barSeriesMap = [barSeriesSpec]; - const customDomain = [0, 1]; - const barSeriesDomains = computeSeriesDomains(barSeriesMap, new Map(), [], customDomain); - const xScale = computeXScale({ - xDomain: barSeriesDomains.xDomain, - totalBarsInCluster: barSeriesMap.length, - range: [0, 100], - }); - const yScales = computeYScales({ yDomains: barSeriesDomains.yDomain, range: [100, 0] }); - - test('Can render two bars within domain', () => { - const { barGeometries } = renderBars( - 0, - barSeriesDomains.formattedDataSeries.nonStacked[0].dataSeries[0], - xScale, - yScales.get(GROUP_ID)!, - 'red', - LIGHT_THEME.barSeriesStyle, - ); - - const getBarGeometry = MockBarGeometry.fromBaseline( - { - x: 0, - y: 0, - width: 50, - height: 100, - color: 'red', - value: { - accessor: 'y1', - x: 0, - y: 10, - mark: null, - datum: [0, 10], - }, - seriesIdentifier: { - specId: SPEC_ID, - key: 'spec{spec_1}yAccessor{1}splitAccessors{}', - yAccessor: 1, - splitAccessors: new Map(), - seriesKeys: [1], - }, - }, - 'displayValue', - ); - expect(barGeometries[0]).toEqual(getBarGeometry()); - expect(barGeometries[1]).toEqual( - getBarGeometry({ - x: 50, - y: 50, - width: 50, - height: 50, - value: { - x: 1, - y: 5, - datum: [1, 5], - }, + test('Can render two bars within domain', () => { + const store = MockStore.default({ width: 100, height: 100, top: 0, left: 0 }); + MockStore.addSpecs( + [ + MockSeriesSpec.bar({ + id: SPEC_ID, + groupId: GROUP_ID, + xScaleType: ScaleType.Ordinal, + yScaleType: ScaleType.Linear, + xAccessor: 0, + yAccessors: [1], + data: [ + [-200, 0], + [0, 10], + [1, 5], + ], // first datum should be skipped as it's out of domain }), - ); - expect(barGeometries.length).toBe(2); - }); - test('Can render bars with value labels', () => { - const valueFormatter = identity; - const { barGeometries } = renderBars( - 0, - barSeriesDomains.formattedDataSeries.nonStacked[0].dataSeries[0], - xScale, - yScales.get(GROUP_ID)!, - 'red', - LIGHT_THEME.barSeriesStyle, - { valueFormatter, showValueLabel: true, isAlternatingValueLabel: true }, - ); - expect(barGeometries[0].displayValue).toBeDefined(); - }); - - test('Can hide value labels if no formatter or showValueLabels is false/undefined', () => { - const { barGeometries } = renderBars( - 0, - barSeriesDomains.formattedDataSeries.nonStacked[0].dataSeries[0], - xScale, - yScales.get(GROUP_ID)!, - 'red', - LIGHT_THEME.barSeriesStyle, - {}, - ); - expect(barGeometries[0].displayValue).toBeUndefined(); - }); - - test('Can render bars with alternating value labels', () => { - const valueFormatter = identity; - const { barGeometries } = renderBars( - 0, - barSeriesDomains.formattedDataSeries.nonStacked[0].dataSeries[0], - xScale, - yScales.get(GROUP_ID)!, - 'red', - LIGHT_THEME.barSeriesStyle, - { valueFormatter, showValueLabel: true, isAlternatingValueLabel: true }, - ); - expect(barGeometries[0].displayValue?.text).toBeDefined(); - expect(barGeometries[1].displayValue?.text).toBeUndefined(); - }); - - test('Can render bars with contained value labels', () => { - const valueFormatter = identity; - const { barGeometries } = renderBars( - 0, - barSeriesDomains.formattedDataSeries.nonStacked[0].dataSeries[0], - xScale, - yScales.get(GROUP_ID)!, - 'red', - LIGHT_THEME.barSeriesStyle, - { valueFormatter, showValueLabel: true, isValueContainedInElement: true }, - ); - expect(barGeometries[0].displayValue?.width).toBe(50); - }); - }); - describe('Multi series bar chart - ordinal', () => { - const spec1Id = 'bar1'; - const spec2Id = 'bar2'; - const barSeriesSpec1: BarSeriesSpec = { - chartType: ChartTypes.XYAxis, - specType: SpecTypes.Series, - id: spec1Id, - groupId: GROUP_ID, - seriesType: SeriesTypes.Bar, - data: [ - [0, 10], - [1, 5], + MockGlobalSpec.settingsNoMargins({ xDomain: [0, 1], theme: { colors: { vizColors: ['red'] } } }), ], - xAccessor: 0, - yAccessors: [1], - xScaleType: ScaleType.Ordinal, - yScaleType: ScaleType.Linear, - }; - const barSeriesSpec2: BarSeriesSpec = { - chartType: ChartTypes.XYAxis, - specType: SpecTypes.Series, - id: spec2Id, - groupId: GROUP_ID, - seriesType: SeriesTypes.Bar, - data: [ - [0, 20], - [1, 10], - ], - xAccessor: 0, - yAccessors: [1], - xScaleType: ScaleType.Ordinal, - yScaleType: ScaleType.Linear, - }; - const barSeriesMap = [barSeriesSpec1, barSeriesSpec2]; - const barSeriesDomains = computeSeriesDomains(barSeriesMap, new Map()); - const xScale = computeXScale({ - xDomain: barSeriesDomains.xDomain, - totalBarsInCluster: barSeriesMap.length, - range: [0, 100], - }); - const yScales = computeYScales({ yDomains: barSeriesDomains.yDomain, range: [100, 0] }); + store, + ); + const { geometries } = computeSeriesGeometriesSelector(store.getState()); + const getBarGeometry = MockBarGeometry.fromBaseline( { x: 0, @@ -222,8 +66,8 @@ describe('Rendering bars', () => { datum: [0, 10], }, seriesIdentifier: { - specId: spec1Id, - key: 'spec{bar1}yAccessor{1}splitAccessors{}', + specId: SPEC_ID, + key: 'groupId{group_1}spec{spec_1}yAccessor{1}splitAccessors{}', yAccessor: 1, splitAccessors: new Map(), seriesKeys: [1], @@ -231,630 +75,819 @@ describe('Rendering bars', () => { }, 'displayValue', ); - - test('can render first spec bars', () => { - const { barGeometries } = renderBars( - 0, - barSeriesDomains.formattedDataSeries.nonStacked[0].dataSeries[0], - xScale, - yScales.get(GROUP_ID)!, - 'red', - LIGHT_THEME.barSeriesStyle, - ); - expect(barGeometries.length).toEqual(2); - expect(barGeometries[0]).toEqual( - getBarGeometry({ - x: 0, - y: 50, - width: 25, - height: 50, - value: { - x: 0, - y: 10, - datum: [0, 10], - }, - }), - ); - expect(barGeometries[1]).toEqual( - getBarGeometry({ - x: 50, - y: 75, - width: 25, - height: 25, - value: { - x: 1, - y: 5, - datum: [1, 5], - }, - }), - ); - }); - test('can render second spec bars', () => { - const { barGeometries } = renderBars( - 1, - barSeriesDomains.formattedDataSeries.nonStacked[0].dataSeries[1], - xScale, - yScales.get(GROUP_ID)!, - 'blue', - LIGHT_THEME.barSeriesStyle, - ); - const getBarGeometry = MockBarGeometry.fromBaseline( - { - x: 0, - y: 0, - width: 50, - height: 100, - color: 'blue', - value: { - accessor: 'y1', - x: 0, - y: 10, - datum: [0, 10], - }, - seriesIdentifier: { - specId: spec2Id, - key: 'spec{bar2}yAccessor{1}splitAccessors{}', - yAccessor: 1, - splitAccessors: new Map(), - seriesKeys: [1], - }, - }, - 'displayValue', - ); - expect(barGeometries.length).toEqual(2); - expect(barGeometries[0]).toEqual( - getBarGeometry({ - x: 25, - y: 0, - width: 25, - height: 100, - value: { - x: 0, - y: 20, - datum: [0, 20], - }, - }), - ); - expect(barGeometries[1]).toEqual( - getBarGeometry({ - x: 75, - y: 50, - width: 25, - height: 50, - value: { - x: 1, - y: 10, - datum: [1, 10], - }, - }), - ); - }); - }); - describe('Single series bar chart - linear', () => { - const barSeriesSpec: BarSeriesSpec = { - chartType: ChartTypes.XYAxis, - specType: SpecTypes.Series, - id: SPEC_ID, - groupId: GROUP_ID, - seriesType: SeriesTypes.Bar, - data: [ - [0, 10], - [1, 5], - ], - xAccessor: 0, - yAccessors: [1], - xScaleType: ScaleType.Linear, - yScaleType: ScaleType.Linear, - }; - const barSeriesMap = [barSeriesSpec]; - const barSeriesDomains = computeSeriesDomains(barSeriesMap, new Map()); - const xScale = computeXScale({ - xDomain: barSeriesDomains.xDomain, - totalBarsInCluster: barSeriesMap.length, - range: [0, 100], - }); - const yScales = computeYScales({ yDomains: barSeriesDomains.yDomain, range: [100, 0] }); - - test('Can render two bars', () => { - const { barGeometries } = renderBars( - 0, - barSeriesDomains.formattedDataSeries.nonStacked[0].dataSeries[0], - xScale, - yScales.get(GROUP_ID)!, - 'red', - LIGHT_THEME.barSeriesStyle, - ); - const getBarGeometry = MockBarGeometry.fromBaseline( - { - x: 0, - y: 0, - width: 50, - height: 100, - color: 'red', - value: { - accessor: 'y1', - x: 0, - y: 10, - mark: null, - datum: [0, 10], - }, - seriesIdentifier: { - specId: SPEC_ID, - key: 'spec{spec_1}yAccessor{1}splitAccessors{}', - yAccessor: 1, - splitAccessors: new Map(), - seriesKeys: [1], - }, + expect(geometries.bars[0]).toEqual(getBarGeometry()); + expect(geometries.bars[1]).toEqual( + getBarGeometry({ + x: 50, + y: 50, + width: 50, + height: 50, + value: { + x: 1, + y: 5, }, - 'displayValue', - ); - expect(barGeometries[0]).toEqual(getBarGeometry()); - expect(barGeometries[1]).toEqual( - getBarGeometry({ - x: 50, - y: 50, - width: 50, - height: 50, - value: { - x: 1, - y: 5, - datum: [1, 5], - }, - }), - ); - }); - }); - describe('Single series bar chart - log', () => { - const barSeriesSpec: BarSeriesSpec = { - chartType: ChartTypes.XYAxis, - specType: SpecTypes.Series, - id: SPEC_ID, - groupId: GROUP_ID, - seriesType: SeriesTypes.Bar, - data: [ - [1, 0], - [2, 1], - [3, 2], - [4, 3], - [5, 4], - [6, 5], - ], - xAccessor: 0, - yAccessors: [1], - xScaleType: ScaleType.Linear, - yScaleType: ScaleType.Log, - }; - const barSeriesDomains = computeSeriesDomains([barSeriesSpec], new Map()); - const xScale = computeXScale({ - xDomain: barSeriesDomains.xDomain, - totalBarsInCluster: 1, - range: [0, 100], - }); - const yScales = computeYScales({ yDomains: barSeriesDomains.yDomain, range: [100, 0] }); - - test('Can render correct bar height', () => { - const { barGeometries } = renderBars( - 0, - barSeriesDomains.formattedDataSeries.nonStacked[0].dataSeries[0], - xScale, - yScales.get(GROUP_ID)!, - 'red', - LIGHT_THEME.barSeriesStyle, - ); - expect(barGeometries.length).toBe(6); - expect(barGeometries[0].height).toBe(0); - expect(barGeometries[1].height).toBe(0); - expect(barGeometries[2].height).toBeGreaterThan(0); - expect(barGeometries[3].height).toBeGreaterThan(0); - }); + }), + ); + expect(geometries.bars.length).toBe(2); }); - describe('Multi series bar chart - linear', () => { - const spec1Id = 'bar1'; - const spec2Id = 'bar2'; - const barSeriesSpec1: BarSeriesSpec = { - chartType: ChartTypes.XYAxis, - specType: SpecTypes.Series, - id: spec1Id, - groupId: GROUP_ID, - seriesType: SeriesTypes.Bar, - data: [ - [0, 10], - [1, 5], - ], - xAccessor: 0, - yAccessors: [1], - xScaleType: ScaleType.Linear, - yScaleType: ScaleType.Linear, - }; - const barSeriesSpec2: BarSeriesSpec = { - chartType: ChartTypes.XYAxis, - specType: SpecTypes.Series, - id: spec2Id, - groupId: GROUP_ID, - seriesType: SeriesTypes.Bar, - data: [ - [0, 20], - [1, 10], - ], - xAccessor: 0, - yAccessors: [1], - xScaleType: ScaleType.Linear, - yScaleType: ScaleType.Linear, - }; - const barSeriesMap = [barSeriesSpec1, barSeriesSpec2]; - const barSeriesDomains = computeSeriesDomains(barSeriesMap, new Map()); - const xScale = computeXScale({ - xDomain: barSeriesDomains.xDomain, - totalBarsInCluster: barSeriesMap.length, - range: [0, 100], - }); - const yScales = computeYScales({ yDomains: barSeriesDomains.yDomain, range: [100, 0] }); - test('can render first spec bars', () => { - const { barGeometries } = renderBars( - 0, - barSeriesDomains.formattedDataSeries.nonStacked[0].dataSeries[0], - xScale, - yScales.get(GROUP_ID)!, - 'red', - LIGHT_THEME.barSeriesStyle, - ); - const getBarGeometry = MockBarGeometry.fromBaseline( - { - x: 0, - y: 50, - width: 25, - height: 50, - color: 'red', - value: { - accessor: 'y1', - x: 0, - y: 10, - datum: [0, 10], - }, - seriesIdentifier: { - specId: spec1Id, - key: 'spec{bar1}yAccessor{1}splitAccessors{}', - yAccessor: 1, - splitAccessors: new Map(), - seriesKeys: [1], - }, - }, - 'displayValue', - ); - expect(barGeometries.length).toEqual(2); - expect(barGeometries[0]).toEqual(getBarGeometry()); - expect(barGeometries[1]).toEqual( - getBarGeometry({ - x: 50, - y: 75, - width: 25, - height: 25, - value: { - x: 1, - y: 5, - datum: [1, 5], - }, - }), - ); - }); - test('can render second spec bars', () => { - const { barGeometries } = renderBars( - 1, - barSeriesDomains.formattedDataSeries.nonStacked[0].dataSeries[1], - xScale, - yScales.get(GROUP_ID)!, - 'blue', - LIGHT_THEME.barSeriesStyle, - ); - const getBarGeometry = MockBarGeometry.fromBaseline( - { - x: 25, - y: 0, - width: 25, - height: 100, - color: 'blue', - value: { - accessor: 'y1', - x: 0, - y: 20, - datum: [0, 20], - }, - seriesIdentifier: { - specId: spec2Id, - key: 'spec{bar2}yAccessor{1}splitAccessors{}', - yAccessor: 1, - splitAccessors: new Map(), - seriesKeys: [1], - }, - }, - 'displayValue', - ); - expect(barGeometries.length).toEqual(2); - expect(barGeometries[0]).toEqual(getBarGeometry()); - expect(barGeometries[1]).toEqual( - getBarGeometry({ - x: 75, - y: 50, - width: 25, - height: 50, - color: 'blue', - value: { - x: 1, - y: 10, - datum: [1, 10], - }, - }), - ); - }); - }); - describe('Multi series bar chart - time', () => { - const spec1Id = 'bar1'; - const spec2Id = 'bar2'; - const barSeriesSpec1: BarSeriesSpec = { - chartType: ChartTypes.XYAxis, - specType: SpecTypes.Series, - id: spec1Id, - groupId: GROUP_ID, - seriesType: SeriesTypes.Bar, - data: [ - [1546300800000, 10], - [1546387200000, 5], - ], - xAccessor: 0, - yAccessors: [1], - xScaleType: ScaleType.Time, - yScaleType: ScaleType.Linear, - }; - const barSeriesSpec2: BarSeriesSpec = { - chartType: ChartTypes.XYAxis, - specType: SpecTypes.Series, - id: spec2Id, - groupId: GROUP_ID, - seriesType: SeriesTypes.Bar, - data: [ - [1546300800000, 20], - [1546387200000, 10], - ], - xAccessor: 0, - yAccessors: [1], - xScaleType: ScaleType.Time, - yScaleType: ScaleType.Linear, - }; - const barSeriesMap = [barSeriesSpec1, barSeriesSpec2]; - const barSeriesDomains = computeSeriesDomains(barSeriesMap, new Map()); - const xScale = computeXScale({ - xDomain: barSeriesDomains.xDomain, - totalBarsInCluster: barSeriesMap.length, - range: [0, 100], + describe('Single series bar chart - ordinal', () => { + test('Can render bars with value labels', () => { + const store = MockStore.default({ width: 100, height: 100, top: 0, left: 0 }); + MockStore.addSpecs( + [ + MockSeriesSpec.bar({ + id: SPEC_ID, + groupId: GROUP_ID, + xScaleType: ScaleType.Ordinal, + yScaleType: ScaleType.Linear, + xAccessor: 0, + yAccessors: [1], + data: [ + [-200, 0], + [0, 10], + [1, 5], + ], // first datum should be skipped as it's out of domain + displayValueSettings: { + showValueLabel: true, + isAlternatingValueLabel: true, + valueFormatter: identity, + }, + }), + MockGlobalSpec.settingsNoMargins({ xDomain: [0, 1], theme: { colors: { vizColors: ['red'] } } }), + ], + store, + ); + const { geometries } = computeSeriesGeometriesSelector(store.getState()); + expect(geometries.bars[0].displayValue).toBeDefined(); }); - const yScales = computeYScales({ yDomains: barSeriesDomains.yDomain, range: [100, 0] }); - test('can render first spec bars', () => { - const { barGeometries } = renderBars( - 0, - barSeriesDomains.formattedDataSeries.nonStacked[0].dataSeries[0], - xScale, - yScales.get(GROUP_ID)!, - 'red', - LIGHT_THEME.barSeriesStyle, - ); - const getBarGeometry = MockBarGeometry.fromBaseline( - { - x: 0, - y: 50, - width: 25, - height: 50, - color: 'red', - value: { - accessor: 'y1', - x: 1546300800000, - y: 10, - datum: [1546300800000, 10], - }, - seriesIdentifier: { - specId: spec1Id, - key: 'spec{bar1}yAccessor{1}splitAccessors{}', - yAccessor: 1, - splitAccessors: new Map(), - seriesKeys: [1], - }, - }, - 'displayValue', - ); - expect(barGeometries.length).toEqual(2); - expect(barGeometries[0]).toEqual(getBarGeometry()); - expect(barGeometries[1]).toEqual( - getBarGeometry({ - x: 50, - y: 75, - width: 25, - height: 25, - value: { - accessor: 'y1', - x: 1546387200000, - y: 5, - mark: null, - datum: [1546387200000, 5], - }, - }), - ); + test('Can hide value labels if no formatter or showValueLabels is false/undefined', () => { + const store = MockStore.default({ width: 100, height: 100, top: 0, left: 0 }); + MockStore.addSpecs( + [ + MockSeriesSpec.bar({ + id: SPEC_ID, + groupId: GROUP_ID, + xScaleType: ScaleType.Ordinal, + yScaleType: ScaleType.Linear, + xAccessor: 0, + yAccessors: [1], + data: [ + [-200, 0], + [0, 10], + [1, 5], + ], // first datum should be skipped as it's out of domain + displayValueSettings: { + showValueLabel: false, + isAlternatingValueLabel: true, + valueFormatter: identity, + }, + }), + MockGlobalSpec.settingsNoMargins({ xDomain: [0, 1], theme: { colors: { vizColors: ['red'] } } }), + ], + store, + ); + const { geometries } = computeSeriesGeometriesSelector(store.getState()); + expect(geometries.bars[0].displayValue).toBeUndefined(); }); - test('can render second spec bars', () => { - const { barGeometries } = renderBars( - 1, - barSeriesDomains.formattedDataSeries.nonStacked[0].dataSeries[1], - xScale, - yScales.get(GROUP_ID)!, - 'blue', - LIGHT_THEME.barSeriesStyle, - ); - const getBarGeometry = MockBarGeometry.fromBaseline( - { - x: 25, - y: 0, - width: 25, - height: 100, - color: 'blue', - value: { - accessor: 'y1', - x: 1546300800000, - y: 20, - mark: null, - datum: [1546300800000, 20], - }, - seriesIdentifier: { - specId: spec2Id, - key: 'spec{bar2}yAccessor{1}splitAccessors{}', - yAccessor: 1, - splitAccessors: new Map(), - seriesKeys: [1], - }, - }, - 'displayValue', - ); - expect(barGeometries.length).toEqual(2); - expect(barGeometries[0]).toEqual(getBarGeometry()); - expect(barGeometries[1]).toEqual( - getBarGeometry({ - x: 75, - y: 50, - width: 25, - height: 50, - value: { - accessor: 'y1', - x: 1546387200000, - y: 10, - mark: null, - datum: [1546387200000, 10], - }, - }), - ); - }); - }); - describe('Remove points datum is not in domain', () => { - const barSeriesSpec: BarSeriesSpec = { - chartType: ChartTypes.XYAxis, - specType: SpecTypes.Series, - id: SPEC_ID, - groupId: GROUP_ID, - seriesType: SeriesTypes.Bar, - data: [ - [0, 0], - [1, 1], - [2, 10], - [3, 3], - ], - xAccessor: 0, - yAccessors: [1], - xScaleType: ScaleType.Linear, - yScaleType: ScaleType.Linear, - }; - const customYDomain = new Map(); - customYDomain.set(GROUP_ID, { - max: 1, - }); - const barSeriesDomains = computeSeriesDomains([barSeriesSpec], customYDomain, [], { - max: 2, - }); - const xScale = computeXScale({ - xDomain: barSeriesDomains.xDomain, - totalBarsInCluster: 1, - range: [0, 100], - }); - const yScales = computeYScales({ yDomains: barSeriesDomains.yDomain, range: [100, 0] }); + test('Can render bars with alternating value labels', () => { + const store = MockStore.default({ width: 100, height: 100, top: 0, left: 0 }); + MockStore.addSpecs( + [ + MockSeriesSpec.bar({ + id: SPEC_ID, + groupId: GROUP_ID, + xScaleType: ScaleType.Ordinal, + yScaleType: ScaleType.Linear, + xAccessor: 0, + yAccessors: [1], + data: [ + [-200, 0], + [0, 10], + [1, 5], + ], // first datum should be skipped as it's out of domain + displayValueSettings: { + showValueLabel: true, + isAlternatingValueLabel: true, + valueFormatter: identity, + }, + }), + MockGlobalSpec.settingsNoMargins({ xDomain: [0, 1], theme: { colors: { vizColors: ['red'] } } }), + ], + store, + ); + const { geometries } = computeSeriesGeometriesSelector(store.getState()); - test('Can render 3 bars', () => { - const { barGeometries, indexedGeometryMap } = renderBars( - 0, - barSeriesDomains.formattedDataSeries.nonStacked[0].dataSeries[0], - xScale, - yScales.get(GROUP_ID)!, - 'red', - LIGHT_THEME.barSeriesStyle, - ); - expect(barGeometries.length).toBe(3); - // will be cut by the clipping areas in the rendering component - expect(barGeometries[2].height).toBe(1000); - expect(indexedGeometryMap.size).toBe(3); + expect(geometries.bars[0].displayValue?.text).toBeDefined(); + expect(geometries.bars[1].displayValue?.text).toBeUndefined(); }); - }); - describe('Renders minBarHeight', () => { - const minBarHeight = 8; - const data = [ - [1, -100000], - [2, -10000], - [3, -1000], - [4, -100], - [5, -10], - [6, -1], - [7, 0], - [8, -1], - [9, 0], - [10, 0], - [11, 1], - [12, 0], - [13, 1], - [14, 10], - [15, 100], - [16, 1000], - [17, 10000], - [18, 100000], - ]; - const barSeriesSpec: BarSeriesSpec = { - chartType: ChartTypes.XYAxis, - specType: SpecTypes.Series, - id: SPEC_ID, - groupId: GROUP_ID, - seriesType: SeriesTypes.Bar, - data, - xAccessor: 0, - yAccessors: [1], - xScaleType: ScaleType.Linear, - yScaleType: ScaleType.Linear, - minBarHeight, - }; - const customYDomain = new Map(); - const barSeriesDomains = computeSeriesDomains([barSeriesSpec], customYDomain); - const xScale = computeXScale({ - xDomain: barSeriesDomains.xDomain, - totalBarsInCluster: 1, - range: [0, 100], - }); - const yScales = computeYScales({ yDomains: barSeriesDomains.yDomain, range: [100, 0] }); - const expected = [-50, -8, -8, -8, -8, -8, 0, -8, 0, 0, 8, 0, 8, 8, 8, 8, 8, 50]; + test('Can render bars with contained value labels', () => { + const store = MockStore.default({ width: 100, height: 100, top: 0, left: 0 }); + MockStore.addSpecs( + [ + MockSeriesSpec.bar({ + id: SPEC_ID, + groupId: GROUP_ID, + xScaleType: ScaleType.Ordinal, + yScaleType: ScaleType.Linear, + xAccessor: 0, + yAccessors: [1], + data: [ + [-200, 0], + [0, 10], + [1, 5], + ], // first datum should be skipped as it's out of domain + displayValueSettings: { + showValueLabel: true, + isValueContainedInElement: true, + valueFormatter: identity, + }, + }), + MockGlobalSpec.settingsNoMargins({ xDomain: [0, 1], theme: { colors: { vizColors: ['red'] } } }), + ], + store, + ); + const { geometries } = computeSeriesGeometriesSelector(store.getState()); - it('should render correct heights with positive minBarHeight', () => { - const { barGeometries } = renderBars( - 0, - barSeriesDomains.formattedDataSeries.nonStacked[0].dataSeries[0], - xScale, - yScales.get(GROUP_ID)!, - 'red', - LIGHT_THEME.barSeriesStyle, - undefined, - undefined, - minBarHeight, - ); - const barHeights = barGeometries.map(({ height }) => height); - expect(barHeights).toEqual(expected); - }); - it('should render correct heights with negative minBarHeight', () => { - const { barGeometries } = renderBars( - 0, - barSeriesDomains.formattedDataSeries.nonStacked[0].dataSeries[0], - xScale, - yScales.get(GROUP_ID)!, - 'red', - LIGHT_THEME.barSeriesStyle, - undefined, - undefined, - -minBarHeight, - ); - const barHeights = barGeometries.map(({ height }) => height); - expect(barHeights).toEqual(expected); + expect(geometries.bars[0].displayValue?.width).toBe(50); }); }); + // describe('Multi series bar chart - ordinal', () => { + // const spec1Id = 'bar1'; + // const spec2Id = 'bar2'; + // const barSeriesSpec1: BarSeriesSpec = { + // chartType: ChartTypes.XYAxis, + // specType: SpecTypes.Series, + // id: spec1Id, + // groupId: GROUP_ID, + // seriesType: SeriesTypes.Bar, + // data: [ + // [0, 10], + // [1, 5], + // ], + // xAccessor: 0, + // yAccessors: [1], + // xScaleType: ScaleType.Ordinal, + // yScaleType: ScaleType.Linear, + // }; + // const barSeriesSpec2: BarSeriesSpec = { + // chartType: ChartTypes.XYAxis, + // specType: SpecTypes.Series, + // id: spec2Id, + // groupId: GROUP_ID, + // seriesType: SeriesTypes.Bar, + // data: [ + // [0, 20], + // [1, 10], + // ], + // xAccessor: 0, + // yAccessors: [1], + // xScaleType: ScaleType.Ordinal, + // yScaleType: ScaleType.Linear, + // }; + // const barSeriesMap = [barSeriesSpec1, barSeriesSpec2]; + // const barSeriesDomains = computeSeriesDomains(barSeriesMap, new Map()); + // const xScale = computeXScale({ + // xDomain: barSeriesDomains.xDomain, + // totalBarsInCluster: barSeriesMap.length, + // range: [0, 100], + // }); + // const yScales = computeYScales({ yDomains: barSeriesDomains.yDomain, range: [100, 0] }); + // const getBarGeometry = MockBarGeometry.fromBaseline( + // { + // x: 0, + // y: 0, + // width: 50, + // height: 100, + // color: 'red', + // value: { + // accessor: 'y1', + // x: 0, + // y: 10, + // mark: null, + // }, + // seriesIdentifier: { + // specId: spec1Id, + // key: 'spec{bar1}yAccessor{1}splitAccessors{}', + // yAccessor: 1, + // splitAccessors: new Map(), + // seriesKeys: [1], + // }, + // }, + // 'displayValue', + // ); + // + // test('can render first spec bars', () => { + // const { barGeometries } = renderBars( + // 0, + // barSeriesDomains.formattedDataSeries.nonStacked[0].dataSeries[0], + // xScale, + // yScales.get(GROUP_ID)!, + // 'red', + // LIGHT_THEME.barSeriesStyle, + // ); + // expect(barGeometries.length).toEqual(2); + // expect(barGeometries[0]).toEqual( + // getBarGeometry({ + // x: 0, + // y: 50, + // width: 25, + // height: 50, + // value: { + // x: 0, + // y: 10, + // }, + // }), + // ); + // expect(barGeometries[1]).toEqual( + // getBarGeometry({ + // x: 50, + // y: 75, + // width: 25, + // height: 25, + // value: { + // x: 1, + // y: 5, + // }, + // }), + // ); + // }); + // test('can render second spec bars', () => { + // const { barGeometries } = renderBars( + // 1, + // barSeriesDomains.formattedDataSeries.nonStacked[0].dataSeries[1], + // xScale, + // yScales.get(GROUP_ID)!, + // 'blue', + // LIGHT_THEME.barSeriesStyle, + // ); + // const getBarGeometry = MockBarGeometry.fromBaseline( + // { + // x: 0, + // y: 0, + // width: 50, + // height: 100, + // color: 'blue', + // value: { + // accessor: 'y1', + // x: 0, + // y: 10, + // }, + // seriesIdentifier: { + // specId: spec2Id, + // key: 'spec{bar2}yAccessor{1}splitAccessors{}', + // yAccessor: 1, + // splitAccessors: new Map(), + // seriesKeys: [1], + // }, + // }, + // 'displayValue', + // ); + // expect(barGeometries.length).toEqual(2); + // expect(barGeometries[0]).toEqual( + // getBarGeometry({ + // x: 25, + // y: 0, + // width: 25, + // height: 100, + // value: { + // x: 0, + // y: 20, + // }, + // }), + // ); + // expect(barGeometries[1]).toEqual( + // getBarGeometry({ + // x: 75, + // y: 50, + // width: 25, + // height: 50, + // value: { + // x: 1, + // y: 10, + // }, + // }), + // ); + // }); + // }); + // describe('Single series bar chart - linear', () => { + // const barSeriesSpec: BarSeriesSpec = { + // chartType: ChartTypes.XYAxis, + // specType: SpecTypes.Series, + // id: SPEC_ID, + // groupId: GROUP_ID, + // seriesType: SeriesTypes.Bar, + // data: [ + // [0, 10], + // [1, 5], + // ], + // xAccessor: 0, + // yAccessors: [1], + // xScaleType: ScaleType.Linear, + // yScaleType: ScaleType.Linear, + // }; + // const barSeriesMap = [barSeriesSpec]; + // const barSeriesDomains = computeSeriesDomains(barSeriesMap, new Map()); + // const xScale = computeXScale({ + // xDomain: barSeriesDomains.xDomain, + // totalBarsInCluster: barSeriesMap.length, + // range: [0, 100], + // }); + // const yScales = computeYScales({ yDomains: barSeriesDomains.yDomain, range: [100, 0] }); + // + // test('Can render two bars', () => { + // const { barGeometries } = renderBars( + // 0, + // barSeriesDomains.formattedDataSeries.nonStacked[0].dataSeries[0], + // xScale, + // yScales.get(GROUP_ID)!, + // 'red', + // LIGHT_THEME.barSeriesStyle, + // ); + // const getBarGeometry = MockBarGeometry.fromBaseline( + // { + // x: 0, + // y: 0, + // width: 50, + // height: 100, + // color: 'red', + // value: { + // accessor: 'y1', + // x: 0, + // y: 10, + // mark: null, + // }, + // seriesIdentifier: { + // specId: SPEC_ID, + // key: 'spec{spec_1}yAccessor{1}splitAccessors{}', + // yAccessor: 1, + // splitAccessors: new Map(), + // seriesKeys: [1], + // }, + // }, + // 'displayValue', + // ); + // expect(barGeometries[0]).toEqual(getBarGeometry()); + // expect(barGeometries[1]).toEqual( + // getBarGeometry({ + // x: 50, + // y: 50, + // width: 50, + // height: 50, + // value: { + // x: 1, + // y: 5, + // }, + // }), + // ); + // }); + // }); + // describe('Single series bar chart - log', () => { + // const barSeriesSpec: BarSeriesSpec = { + // chartType: ChartTypes.XYAxis, + // specType: SpecTypes.Series, + // id: SPEC_ID, + // groupId: GROUP_ID, + // seriesType: SeriesTypes.Bar, + // data: [ + // [1, 0], + // [2, 1], + // [3, 2], + // [4, 3], + // [5, 4], + // [6, 5], + // ], + // xAccessor: 0, + // yAccessors: [1], + // xScaleType: ScaleType.Linear, + // yScaleType: ScaleType.Log, + // }; + // const barSeriesDomains = computeSeriesDomains([barSeriesSpec], new Map()); + // const xScale = computeXScale({ + // xDomain: barSeriesDomains.xDomain, + // totalBarsInCluster: 1, + // range: [0, 100], + // }); + // const yScales = computeYScales({ yDomains: barSeriesDomains.yDomain, range: [100, 0] }); + // + // test('Can render correct bar height', () => { + // const { barGeometries } = renderBars( + // 0, + // barSeriesDomains.formattedDataSeries.nonStacked[0].dataSeries[0], + // xScale, + // yScales.get(GROUP_ID)!, + // 'red', + // LIGHT_THEME.barSeriesStyle, + // ); + // expect(barGeometries.length).toBe(6); + // expect(barGeometries[0].height).toBe(0); + // expect(barGeometries[1].height).toBe(0); + // expect(barGeometries[2].height).toBeGreaterThan(0); + // expect(barGeometries[3].height).toBeGreaterThan(0); + // }); + // }); + // describe('Multi series bar chart - linear', () => { + // const spec1Id = 'bar1'; + // const spec2Id = 'bar2'; + // const barSeriesSpec1: BarSeriesSpec = { + // chartType: ChartTypes.XYAxis, + // specType: SpecTypes.Series, + // id: spec1Id, + // groupId: GROUP_ID, + // seriesType: SeriesTypes.Bar, + // data: [ + // [0, 10], + // [1, 5], + // ], + // xAccessor: 0, + // yAccessors: [1], + // xScaleType: ScaleType.Linear, + // yScaleType: ScaleType.Linear, + // }; + // const barSeriesSpec2: BarSeriesSpec = { + // chartType: ChartTypes.XYAxis, + // specType: SpecTypes.Series, + // id: spec2Id, + // groupId: GROUP_ID, + // seriesType: SeriesTypes.Bar, + // data: [ + // [0, 20], + // [1, 10], + // ], + // xAccessor: 0, + // yAccessors: [1], + // xScaleType: ScaleType.Linear, + // yScaleType: ScaleType.Linear, + // }; + // const barSeriesMap = [barSeriesSpec1, barSeriesSpec2]; + // const barSeriesDomains = computeSeriesDomains(barSeriesMap, new Map()); + // const xScale = computeXScale({ + // xDomain: barSeriesDomains.xDomain, + // totalBarsInCluster: barSeriesMap.length, + // range: [0, 100], + // }); + // const yScales = computeYScales({ yDomains: barSeriesDomains.yDomain, range: [100, 0] }); + // + // test('can render first spec bars', () => { + // const { barGeometries } = renderBars( + // 0, + // barSeriesDomains.formattedDataSeries.nonStacked[0].dataSeries[0], + // xScale, + // yScales.get(GROUP_ID)!, + // 'red', + // LIGHT_THEME.barSeriesStyle, + // ); + // const getBarGeometry = MockBarGeometry.fromBaseline( + // { + // x: 0, + // y: 50, + // width: 25, + // height: 50, + // color: 'red', + // value: { + // accessor: 'y1', + // x: 0, + // y: 10, + // }, + // seriesIdentifier: { + // specId: spec1Id, + // key: 'spec{bar1}yAccessor{1}splitAccessors{}', + // yAccessor: 1, + // splitAccessors: new Map(), + // seriesKeys: [1], + // }, + // }, + // 'displayValue', + // ); + // expect(barGeometries.length).toEqual(2); + // expect(barGeometries[0]).toEqual(getBarGeometry()); + // expect(barGeometries[1]).toEqual( + // getBarGeometry({ + // x: 50, + // y: 75, + // width: 25, + // height: 25, + // value: { + // x: 1, + // y: 5, + // }, + // }), + // ); + // }); + // test('can render second spec bars', () => { + // const { barGeometries } = renderBars( + // 1, + // barSeriesDomains.formattedDataSeries.nonStacked[0].dataSeries[1], + // xScale, + // yScales.get(GROUP_ID)!, + // 'blue', + // LIGHT_THEME.barSeriesStyle, + // ); + // const getBarGeometry = MockBarGeometry.fromBaseline( + // { + // x: 25, + // y: 0, + // width: 25, + // height: 100, + // color: 'blue', + // value: { + // accessor: 'y1', + // x: 0, + // y: 20, + // }, + // seriesIdentifier: { + // specId: spec2Id, + // key: 'spec{bar2}yAccessor{1}splitAccessors{}', + // yAccessor: 1, + // splitAccessors: new Map(), + // seriesKeys: [1], + // }, + // }, + // 'displayValue', + // ); + // expect(barGeometries.length).toEqual(2); + // expect(barGeometries[0]).toEqual(getBarGeometry()); + // expect(barGeometries[1]).toEqual( + // getBarGeometry({ + // x: 75, + // y: 50, + // width: 25, + // height: 50, + // color: 'blue', + // value: { + // x: 1, + // y: 10, + // }, + // }), + // ); + // }); + // }); + // describe('Multi series bar chart - time', () => { + // const spec1Id = 'bar1'; + // const spec2Id = 'bar2'; + // const barSeriesSpec1: BarSeriesSpec = { + // chartType: ChartTypes.XYAxis, + // specType: SpecTypes.Series, + // id: spec1Id, + // groupId: GROUP_ID, + // seriesType: SeriesTypes.Bar, + // data: [ + // [1546300800000, 10], + // [1546387200000, 5], + // ], + // xAccessor: 0, + // yAccessors: [1], + // xScaleType: ScaleType.Time, + // yScaleType: ScaleType.Linear, + // }; + // const barSeriesSpec2: BarSeriesSpec = { + // chartType: ChartTypes.XYAxis, + // specType: SpecTypes.Series, + // id: spec2Id, + // groupId: GROUP_ID, + // seriesType: SeriesTypes.Bar, + // data: [ + // [1546300800000, 20], + // [1546387200000, 10], + // ], + // xAccessor: 0, + // yAccessors: [1], + // xScaleType: ScaleType.Time, + // yScaleType: ScaleType.Linear, + // }; + // const barSeriesMap = [barSeriesSpec1, barSeriesSpec2]; + // const barSeriesDomains = computeSeriesDomains(barSeriesMap, new Map()); + // const xScale = computeXScale({ + // xDomain: barSeriesDomains.xDomain, + // totalBarsInCluster: barSeriesMap.length, + // range: [0, 100], + // }); + // const yScales = computeYScales({ yDomains: barSeriesDomains.yDomain, range: [100, 0] }); + // + // test('can render first spec bars', () => { + // const { barGeometries } = renderBars( + // 0, + // barSeriesDomains.formattedDataSeries.nonStacked[0].dataSeries[0], + // xScale, + // yScales.get(GROUP_ID)!, + // 'red', + // LIGHT_THEME.barSeriesStyle, + // ); + // const getBarGeometry = MockBarGeometry.fromBaseline( + // { + // x: 0, + // y: 50, + // width: 25, + // height: 50, + // color: 'red', + // value: { + // accessor: 'y1', + // x: 1546300800000, + // y: 10, + // }, + // seriesIdentifier: { + // specId: spec1Id, + // key: 'spec{bar1}yAccessor{1}splitAccessors{}', + // yAccessor: 1, + // splitAccessors: new Map(), + // seriesKeys: [1], + // }, + // }, + // 'displayValue', + // ); + // expect(barGeometries.length).toEqual(2); + // expect(barGeometries[0]).toEqual(getBarGeometry()); + // expect(barGeometries[1]).toEqual( + // getBarGeometry({ + // x: 50, + // y: 75, + // width: 25, + // height: 25, + // value: { + // accessor: 'y1', + // x: 1546387200000, + // y: 5, + // mark: null, + // }, + // }), + // ); + // }); + // test('can render second spec bars', () => { + // const { barGeometries } = renderBars( + // 1, + // barSeriesDomains.formattedDataSeries.nonStacked[0].dataSeries[1], + // xScale, + // yScales.get(GROUP_ID)!, + // 'blue', + // LIGHT_THEME.barSeriesStyle, + // ); + // const getBarGeometry = MockBarGeometry.fromBaseline( + // { + // x: 25, + // y: 0, + // width: 25, + // height: 100, + // color: 'blue', + // value: { + // accessor: 'y1', + // x: 1546300800000, + // y: 20, + // mark: null, + // }, + // seriesIdentifier: { + // specId: spec2Id, + // key: 'spec{bar2}yAccessor{1}splitAccessors{}', + // yAccessor: 1, + // splitAccessors: new Map(), + // seriesKeys: [1], + // }, + // }, + // 'displayValue', + // ); + // + // expect(barGeometries.length).toEqual(2); + // expect(barGeometries[0]).toEqual(getBarGeometry()); + // expect(barGeometries[1]).toEqual( + // getBarGeometry({ + // x: 75, + // y: 50, + // width: 25, + // height: 50, + // value: { + // accessor: 'y1', + // x: 1546387200000, + // y: 10, + // mark: null, + // }, + // }), + // ); + // }); + // }); + // describe('Remove points datum is not in domain', () => { + // const barSeriesSpec: BarSeriesSpec = { + // chartType: ChartTypes.XYAxis, + // specType: SpecTypes.Series, + // id: SPEC_ID, + // groupId: GROUP_ID, + // seriesType: SeriesTypes.Bar, + // data: [ + // [0, 0], + // [1, 1], + // [2, 10], + // [3, 3], + // ], + // xAccessor: 0, + // yAccessors: [1], + // xScaleType: ScaleType.Linear, + // yScaleType: ScaleType.Linear, + // }; + // const customYDomain = new Map(); + // customYDomain.set(GROUP_ID, { + // max: 1, + // }); + // const barSeriesDomains = computeSeriesDomains([barSeriesSpec], customYDomain, [], { + // max: 2, + // }); + // const xScale = computeXScale({ + // xDomain: barSeriesDomains.xDomain, + // totalBarsInCluster: 1, + // range: [0, 100], + // }); + // const yScales = computeYScales({ yDomains: barSeriesDomains.yDomain, range: [100, 0] }); + // + // test('Can render 3 bars', () => { + // const { barGeometries, indexedGeometryMap } = renderBars( + // 0, + // barSeriesDomains.formattedDataSeries.nonStacked[0].dataSeries[0], + // xScale, + // yScales.get(GROUP_ID)!, + // 'red', + // LIGHT_THEME.barSeriesStyle, + // ); + // expect(barGeometries.length).toBe(3); + // // will be cut by the clipping areas in the rendering component + // expect(barGeometries[2].height).toBe(1000); + // expect(indexedGeometryMap.size).toBe(3); + // }); + // }); + // describe('Renders minBarHeight', () => { + // const minBarHeight = 8; + // const data = [ + // [1, -100000], + // [2, -10000], + // [3, -1000], + // [4, -100], + // [5, -10], + // [6, -1], + // [7, 0], + // [8, -1], + // [9, 0], + // [10, 0], + // [11, 1], + // [12, 0], + // [13, 1], + // [14, 10], + // [15, 100], + // [16, 1000], + // [17, 10000], + // [18, 100000], + // ]; + // const barSeriesSpec: BarSeriesSpec = { + // chartType: ChartTypes.XYAxis, + // specType: SpecTypes.Series, + // id: SPEC_ID, + // groupId: GROUP_ID, + // seriesType: SeriesTypes.Bar, + // data, + // xAccessor: 0, + // yAccessors: [1], + // xScaleType: ScaleType.Linear, + // yScaleType: ScaleType.Linear, + // minBarHeight, + // }; + // + // const customYDomain = new Map(); + // const barSeriesDomains = computeSeriesDomains([barSeriesSpec], customYDomain); + // const xScale = computeXScale({ + // xDomain: barSeriesDomains.xDomain, + // totalBarsInCluster: 1, + // range: [0, 100], + // }); + // const yScales = computeYScales({ yDomains: barSeriesDomains.yDomain, range: [100, 0] }); + // const expected = [-50, -8, -8, -8, -8, -8, 0, -8, 0, 0, 8, 0, 8, 8, 8, 8, 8, 50]; + // + // it('should render correct heights with positive minBarHeight', () => { + // const { barGeometries } = renderBars( + // 0, + // barSeriesDomains.formattedDataSeries.nonStacked[0].dataSeries[0], + // xScale, + // yScales.get(GROUP_ID)!, + // 'red', + // LIGHT_THEME.barSeriesStyle, + // undefined, + // undefined, + // minBarHeight, + // ); + // const barHeights = barGeometries.map(({ height }) => height); + // expect(barHeights).toEqual(expected); + // }); + // it('should render correct heights with negative minBarHeight', () => { + // const { barGeometries } = renderBars( + // 0, + // barSeriesDomains.formattedDataSeries.nonStacked[0].dataSeries[0], + // xScale, + // yScales.get(GROUP_ID)!, + // 'red', + // LIGHT_THEME.barSeriesStyle, + // undefined, + // undefined, + // -minBarHeight, + // ); + // const barHeights = barGeometries.map(({ height }) => height); + // expect(barHeights).toEqual(expected); + // }); + // }); }); diff --git a/src/chart_types/xy_chart/rendering/rendering.ts b/src/chart_types/xy_chart/rendering/rendering.ts index 0b0a3eb4be..cf99954cbb 100644 --- a/src/chart_types/xy_chart/rendering/rendering.ts +++ b/src/chart_types/xy_chart/rendering/rendering.ts @@ -27,6 +27,7 @@ import { MarkBuffer, StackMode } from '../../../specs'; import { CanvasTextBBoxCalculator } from '../../../utils/bbox/canvas_text_bbox_calculator'; import { mergePartial, Color, getDistance } from '../../../utils/commons'; import { CurveType, getCurveFactory } from '../../../utils/curves'; +import { Dimensions } from '../../../utils/dimensions'; import { PointGeometry, BarGeometry, @@ -166,13 +167,13 @@ function renderPoints( dataSeries: DataSeries, xScale: Scale, yScale: Scale, + panel: Dimensions, color: Color, lineStyle: LineStyle, hasY0Accessors: boolean, markSizeOptions: MarkSizeOptions, styleAccessor?: PointStyleAccessor, spatial = false, - stackMode?: StackMode, ): { pointGeometries: PointGeometry[]; indexedGeometryMap: IndexedGeometryMap; @@ -220,7 +221,7 @@ function renderPoints( if (y === null) { return acc; } - const originalY = getDatumYValue(datum, index === 0, hasY0Accessors, stackMode); + const originalY = getDatumYValue(datum, index === 0, hasY0Accessors, dataSeries.stackMode); const seriesIdentifier: XYChartSeriesIdentifier = { key: dataSeries.key, specId: dataSeries.specId, @@ -243,7 +244,7 @@ function renderPoints( }, transform: { x: shift, - y: 0, + y: panel.top, }, seriesIdentifier, styleOverrides, @@ -301,6 +302,7 @@ export function renderBars( dataSeries: DataSeries, xScale: Scale, yScale: Scale, + panel: Dimensions, color: Color, sharedSeriesStyle: BarSeriesStyle, displayValueSettings?: DisplayValueSpec, @@ -354,7 +356,6 @@ export function renderBars( if (y === null || y0Scaled === null) { return; } - let height = y0Scaled - y; // handle minBarHeight adjustment @@ -422,7 +423,11 @@ export function renderBars( const barGeometry: BarGeometry = { displayValue, x, - y, // top most value + y, + transform: { + x: panel.left, + y: panel.top, + }, width, height, color, @@ -454,6 +459,7 @@ export function renderLine( dataSeries: DataSeries, xScale: Scale, yScale: Scale, + panel: Dimensions, color: Color, curve: CurveType, hasY0Accessors: boolean, @@ -467,7 +473,6 @@ export function renderLine( indexedGeometryMap: IndexedGeometryMap; } { const isLogScale = isLogarithmicScale(yScale); - const pathGenerator = line() .x(({ x }) => xScale.scaleOrThrow(x) - xScaleOffset) .y((datum) => { @@ -485,14 +490,13 @@ export function renderLine( return yValue !== null && !(isLogScale && yValue <= 0) && xScale.isValueInDomain(datum.x); }) .curve(getCurveFactory(curve)); - const y = 0; - const x = shift; const { pointGeometries, indexedGeometryMap } = renderPoints( shift - xScaleOffset, dataSeries, xScale, yScale, + panel, color, seriesStyle.line, hasY0Accessors, @@ -500,7 +504,7 @@ export function renderLine( pointStyleAccessor, ); - const clippedRanges = getClippedRanges(dataSeries.data, xScale, xScaleOffset); + const clippedRanges = getClippedRanges(dataSeries.data, xScale, xScaleOffset, panel); let linePath: string; try { @@ -515,8 +519,8 @@ export function renderLine( points: pointGeometries, color, transform: { - x, - y, + x: panel.left + shift, + y: panel.top, }, seriesIdentifier: { key: dataSeries.key, @@ -543,6 +547,7 @@ export function renderBubble( xScale: Scale, yScale: Scale, color: Color, + panel: Dimensions, hasY0Accessors: boolean, seriesStyle: BubbleSeriesStyle, markSizeOptions: MarkSizeOptions, @@ -557,6 +562,7 @@ export function renderBubble( dataSeries, xScale, yScale, + panel, color, seriesStyle.point, hasY0Accessors, @@ -590,6 +596,7 @@ export function renderArea( dataSeries: DataSeries, xScale: Scale, yScale: Scale, + panel: Dimensions, color: Color, curve: CurveType, hasY0Accessors: boolean, @@ -599,12 +606,12 @@ export function renderArea( isStacked = false, pointStyleAccessor?: PointStyleAccessor, hasFit?: boolean, - stackMode?: StackMode, ): { areaGeometry: AreaGeometry; indexedGeometryMap: IndexedGeometryMap; } { const isLogScale = isLogarithmicScale(yScale); + const pathGenerator = area() .x(({ x }) => xScale.scaleOrThrow(x) - xScaleOffset) .y1((datum) => { @@ -616,11 +623,7 @@ export function renderArea( return yScale.isInverted ? yScale.range[1] : yScale.range[0]; }) .y0(({ y0 }) => { - if (y0 === null || (isLogScale && y0 <= 0)) { - return yScale.range[0]; - } - - return yScale.scaleOrThrow(y0); + return y0 === null || (isLogScale && y0 <= 0) ? yScale.range[0] : yScale.scaleOrThrow(y0); }) .defined((datum) => { const yValue = getYValue(datum); @@ -628,7 +631,7 @@ export function renderArea( }) .curve(getCurveFactory(curve)); - const clippedRanges = getClippedRanges(dataSeries.data, xScale, xScaleOffset); + const clippedRanges = getClippedRanges(dataSeries.data, xScale, xScaleOffset, panel); let y1Line: string | null; @@ -662,13 +665,13 @@ export function renderArea( dataSeries, xScale, yScale, + panel, color, seriesStyle.line, hasY0Accessors, markSizeOptions, pointStyleAccessor, false, - stackMode, ); let areaPath: string; @@ -686,8 +689,8 @@ export function renderArea( points: pointGeometries, color, transform: { - y: 0, - x: shift, + y: panel.top, + x: panel.left + shift, }, seriesIdentifier: { key: dataSeries.key, @@ -723,18 +726,24 @@ export function isDatumFilled({ filled, initialY1 }: DataSeriesDatum) { * @param dataset * @param xScale * @param xScaleOffset + * @param panel * @internal */ -export function getClippedRanges(dataset: DataSeriesDatum[], xScale: Scale, xScaleOffset: number): ClippedRanges { +export function getClippedRanges( + dataset: DataSeriesDatum[], + xScale: Scale, + xScaleOffset: number, + panel: Dimensions, +): ClippedRanges { let firstNonNullX: number | null = null; let hasNull = false; - return dataset.reduce((acc, data) => { const xScaled = xScale.scale(data.x); if (xScaled === null) { return acc; } - const xValue = xScaled - xScaleOffset + xScale.bandwidth / 2; + + const xValue = panel.left + xScaled - xScaleOffset + xScale.bandwidth / 2; if (isDatumFilled(data)) { const endXValue = xScale.range[1] - xScale.bandwidth * (2 / 3); diff --git a/src/chart_types/xy_chart/state/selectors/compute_annotations.ts b/src/chart_types/xy_chart/state/selectors/compute_annotations.ts index 3d2c1ebda2..b151d68aa9 100644 --- a/src/chart_types/xy_chart/state/selectors/compute_annotations.ts +++ b/src/chart_types/xy_chart/state/selectors/compute_annotations.ts @@ -26,6 +26,7 @@ import { AnnotationDimensions } from '../../annotations/types'; import { computeAnnotationDimensions } from '../../annotations/utils'; import { computeChartDimensionsSelector } from './compute_chart_dimensions'; import { computeSeriesGeometriesSelector } from './compute_series_geometries'; +import { computeSmallMultipleScalesSelector } from './compute_small_multiple_scales'; import { getAxisSpecsSelector, getAnnotationSpecsSelector } from './get_specs'; import { isHistogramModeEnabledSelector } from './is_histogram_mode_enabled'; @@ -38,7 +39,7 @@ export const computeAnnotationDimensionsSelector = createCachedSelector( computeSeriesGeometriesSelector, getAxisSpecsSelector, isHistogramModeEnabledSelector, - getAxisSpecsSelector, + computeSmallMultipleScalesSelector, ], ( annotationSpecs, @@ -47,6 +48,7 @@ export const computeAnnotationDimensionsSelector = createCachedSelector( { scales: { yScales, xScale } }, axesSpecs, isHistogramMode, + smallMultipleScales, ): Map => computeAnnotationDimensions( annotationSpecs, @@ -56,5 +58,6 @@ export const computeAnnotationDimensionsSelector = createCachedSelector( xScale, axesSpecs, isHistogramMode, + smallMultipleScales, ), )(getChartIdSelector); diff --git a/src/chart_types/xy_chart/state/selectors/compute_axis_visible_ticks.ts b/src/chart_types/xy_chart/state/selectors/compute_axis_visible_ticks.ts index 8e764b0386..81c9313ab1 100644 --- a/src/chart_types/xy_chart/state/selectors/compute_axis_visible_ticks.ts +++ b/src/chart_types/xy_chart/state/selectors/compute_axis_visible_ticks.ts @@ -22,9 +22,7 @@ import createCachedSelector from 're-reselect'; import { getChartIdSelector } from '../../../../state/selectors/get_chart_id'; import { getChartThemeSelector } from '../../../../state/selectors/get_chart_theme'; import { getSettingsSpecSelector } from '../../../../state/selectors/get_settings_specs'; -import { Dimensions } from '../../../../utils/dimensions'; -import { AxisId } from '../../../../utils/ids'; -import { getAxisTicksPositions, AxisTick, AxisLinePosition, defaultTickFormatter } from '../../utils/axis_utils'; +import { getAxisTicksPositions, AxisGeometry, defaultTickFormatter } from '../../utils/axis_utils'; import { computeAxisTicksDimensionsSelector } from './compute_axis_ticks_dimensions'; import { computeChartDimensionsSelector } from './compute_chart_dimensions'; import { computeSeriesDomainsSelector } from './compute_series_domains'; @@ -35,14 +33,7 @@ import { getAxisSpecsSelector, getSeriesSpecsSelector } from './get_specs'; import { isHistogramModeEnabledSelector } from './is_histogram_mode_enabled'; /** @internal */ -export interface AxisVisibleTicks { - axisPositions: Map; - axisTicks: Map; - axisVisibleTicks: Map; - axisGridLinesPositions: Map; -} -/** @internal */ -export const computeAxisVisibleTicksSelector = createCachedSelector( +export const computeAxesGeometriesSelector = createCachedSelector( [ computeChartDimensionsSelector, getChartThemeSelector, @@ -68,9 +59,9 @@ export const computeAxisVisibleTicksSelector = createCachedSelector( isHistogramMode, barsPadding, seriesSpecs, - ): AxisVisibleTicks => { + ): Array => { const fallBackTickFormatter = seriesSpecs.find(({ tickFormat }) => tickFormat)?.tickFormat ?? defaultTickFormatter; - const { xDomain, yDomain } = seriesDomainsAndData; + const { xDomain, yDomain, smVDomain, smHDomain } = seriesDomainsAndData; return getAxisTicksPositions( chartDimensions, chartTheme, @@ -80,6 +71,8 @@ export const computeAxisVisibleTicksSelector = createCachedSelector( axesStyles, xDomain, yDomain, + smVDomain, + smHDomain, totalBarsInCluster, isHistogramMode, fallBackTickFormatter, diff --git a/src/chart_types/xy_chart/state/selectors/compute_series_domains.ts b/src/chart_types/xy_chart/state/selectors/compute_series_domains.ts index f3bd0fe840..12f3edc396 100644 --- a/src/chart_types/xy_chart/state/selectors/compute_series_domains.ts +++ b/src/chart_types/xy_chart/state/selectors/compute_series_domains.ts @@ -24,16 +24,22 @@ import { getChartIdSelector } from '../../../../state/selectors/get_chart_id'; import { getSettingsSpecSelector } from '../../../../state/selectors/get_settings_specs'; import { SeriesDomainsAndData } from '../utils/types'; import { computeSeriesDomains } from '../utils/utils'; -import { getSeriesSpecsSelector } from './get_specs'; +import { getSeriesSpecsSelector, getSmallMultiplesIndexOrderSelector } from './get_specs'; import { mergeYCustomDomainsByGroupIdSelector } from './merge_y_custom_domains'; const getDeselectedSeriesSelector = (state: GlobalChartState) => state.interactions.deselectedDataSeries; /** @internal */ export const computeSeriesDomainsSelector = createCachedSelector( - [getSeriesSpecsSelector, mergeYCustomDomainsByGroupIdSelector, getDeselectedSeriesSelector, getSettingsSpecSelector], - (seriesSpecs, customYDomainsByGroupId, deselectedDataSeries, settingsSpec): SeriesDomainsAndData => { - const domains = computeSeriesDomains( + [ + getSeriesSpecsSelector, + mergeYCustomDomainsByGroupIdSelector, + getDeselectedSeriesSelector, + getSettingsSpecSelector, + getSmallMultiplesIndexOrderSelector, + ], + (seriesSpecs, customYDomainsByGroupId, deselectedDataSeries, settingsSpec, smallMultiples): SeriesDomainsAndData => { + return computeSeriesDomains( seriesSpecs, customYDomainsByGroupId, deselectedDataSeries, @@ -41,7 +47,7 @@ export const computeSeriesDomainsSelector = createCachedSelector( settingsSpec.orderOrdinalBinsBy, // @ts-ignore blind sort option for vislib settingsSpec.enableVislibSeriesSort, + smallMultiples, ); - return domains; }, )(getChartIdSelector); diff --git a/src/chart_types/xy_chart/state/selectors/compute_series_geometries.ts b/src/chart_types/xy_chart/state/selectors/compute_series_geometries.ts index 5b63d1983e..a21f5b84e0 100644 --- a/src/chart_types/xy_chart/state/selectors/compute_series_geometries.ts +++ b/src/chart_types/xy_chart/state/selectors/compute_series_geometries.ts @@ -26,6 +26,7 @@ import { ComputedGeometries } from '../utils/types'; import { computeSeriesGeometries } from '../utils/utils'; import { computeChartDimensionsSelector } from './compute_chart_dimensions'; import { computeSeriesDomainsSelector } from './compute_series_domains'; +import { computeSmallMultipleScalesSelector } from './compute_small_multiple_scales'; import { getSeriesColorsSelector } from './get_series_color_map'; import { getSeriesSpecsSelector, getAxisSpecsSelector } from './get_specs'; import { isHistogramModeEnabledSelector } from './is_histogram_mode_enabled'; @@ -38,8 +39,8 @@ export const computeSeriesGeometriesSelector = createCachedSelector( computeSeriesDomainsSelector, getSeriesColorsSelector, getChartThemeSelector, - computeChartDimensionsSelector, getAxisSpecsSelector, + computeSmallMultipleScalesSelector, isHistogramModeEnabledSelector, ], ( @@ -48,21 +49,18 @@ export const computeSeriesGeometriesSelector = createCachedSelector( seriesDomainsAndData, seriesColors, chartTheme, - chartDimensions, axesSpecs, + smallMultiplesScales, isHistogramMode, ): ComputedGeometries => { - const { xDomain, yDomain, formattedDataSeries } = seriesDomainsAndData; return computeSeriesGeometries( seriesSpecs, - xDomain, - yDomain, - formattedDataSeries, + seriesDomainsAndData, seriesColors, chartTheme, - chartDimensions.chartDimensions, settingsSpec.rotation, axesSpecs, + smallMultiplesScales, isHistogramMode, ); }, diff --git a/src/chart_types/xy_chart/state/selectors/compute_small_multiple_scales.ts b/src/chart_types/xy_chart/state/selectors/compute_small_multiple_scales.ts new file mode 100644 index 0000000000..0108c1f072 --- /dev/null +++ b/src/chart_types/xy_chart/state/selectors/compute_small_multiple_scales.ts @@ -0,0 +1,63 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import createCachedSelector from 're-reselect'; + +import { ScaleBand } from '../../../../scales'; +import { getChartIdSelector } from '../../../../state/selectors/get_chart_id'; +import { getSettingsSpecSelector } from '../../../../state/selectors/get_settings_specs'; +import { Domain } from '../../../../utils/domain'; +import { isHorizontalRotation } from '../utils/common'; +import { computeChartDimensionsSelector } from './compute_chart_dimensions'; +import { computeSeriesDomainsSelector } from './compute_series_domains'; + +/** @internal */ +export const DEFAULT_SINGLE_PANEL_SM_VALUE = '__ECH_DEFAULT_SINGLE_PANEL_SM_VALUE__'; +export const DEFAULT_SM_PANEL_PADDING = 0.1; + +/** @internal */ +export interface SmallMultipleScales { + horizontal: ScaleBand; + vertical: ScaleBand; +} + +/** @internal */ +export const computeSmallMultipleScalesSelector = createCachedSelector( + [getSettingsSpecSelector, computeSeriesDomainsSelector, computeChartDimensionsSelector], + ({ rotation }, { smHDomain, smVDomain }, { chartDimensions: { width, height } }): SmallMultipleScales => { + const isChartHorizontalOriented = isHorizontalRotation(rotation); + const rotatedWidth = isChartHorizontalOriented ? width : height; + const rotatedHeight = isChartHorizontalOriented ? height : width; + + return { + horizontal: getScale(smHDomain, rotatedWidth), + vertical: getScale(smVDomain, rotatedHeight), + }; + }, +)(getChartIdSelector); + +function getScale(domain: Domain, maxRange: number) { + const singlePanelSmallMultiple = domain.length === 0; + return new ScaleBand( + singlePanelSmallMultiple ? [DEFAULT_SINGLE_PANEL_SM_VALUE] : domain, + [0, maxRange], + undefined, + singlePanelSmallMultiple ? 0 : DEFAULT_SM_PANEL_PADDING, + ); +} diff --git a/src/chart_types/xy_chart/state/selectors/count_bars_in_cluster.ts b/src/chart_types/xy_chart/state/selectors/count_bars_in_cluster.ts index 903eab8027..e004239953 100644 --- a/src/chart_types/xy_chart/state/selectors/count_bars_in_cluster.ts +++ b/src/chart_types/xy_chart/state/selectors/count_bars_in_cluster.ts @@ -19,17 +19,43 @@ import createCachedSelector from 're-reselect'; +import { SeriesTypes } from '../../../../specs'; import { getChartIdSelector } from '../../../../state/selectors/get_chart_id'; -import { countBarsInCluster } from '../../utils/scales'; +import { groupBy } from '../../utils/group_data_series'; +import { getBarIndexKey } from '../utils/utils'; import { computeSeriesDomainsSelector } from './compute_series_domains'; +import { isHistogramModeEnabledSelector } from './is_histogram_mode_enabled'; /** @internal */ export const countBarsInClusterSelector = createCachedSelector( - [computeSeriesDomainsSelector], - (seriesDomainsAndData): number => { + [computeSeriesDomainsSelector, isHistogramModeEnabledSelector], + (seriesDomainsAndData, isHistogramEnabled): number => { const { formattedDataSeries } = seriesDomainsAndData; - const { totalBarsInCluster } = countBarsInCluster(formattedDataSeries.stacked, formattedDataSeries.nonStacked); - return totalBarsInCluster; + const barDataSeries = formattedDataSeries.filter(({ seriesType }) => seriesType === SeriesTypes.Bar); + + const dataSeriesGroupedByPanel = groupBy( + barDataSeries, + ['smVerticalAccessorValue', 'smHorizontalAccessorValue'], + false, + ); + + const barIndexByPanel = Object.keys(dataSeriesGroupedByPanel).reduce>((acc, panelKey) => { + const panelBars = dataSeriesGroupedByPanel[panelKey]; + const barDataSeriesByBarIndex = groupBy( + panelBars, + (d) => { + return getBarIndexKey(d, isHistogramEnabled); + }, + false, + ); + + acc[panelKey] = Object.keys(barDataSeriesByBarIndex); + return acc; + }, {}); + + return Object.values(barIndexByPanel).reduce((acc, curr) => { + return Math.max(acc, curr.length); + }, 0); }, )(getChartIdSelector); diff --git a/src/chart_types/xy_chart/state/selectors/get_debug_state.ts b/src/chart_types/xy_chart/state/selectors/get_debug_state.ts index fac3519cad..12f1d3f19a 100644 --- a/src/chart_types/xy_chart/state/selectors/get_debug_state.ts +++ b/src/chart_types/xy_chart/state/selectors/get_debug_state.ts @@ -34,7 +34,8 @@ import { import { AreaGeometry, BandedAccessorType, LineGeometry, BarGeometry } from '../../../../utils/geometry'; import { FillStyle, Visible, StrokeStyle, Opacity } from '../../../../utils/themes/theme'; import { isVerticalAxis } from '../../utils/axis_type_utils'; -import { computeAxisVisibleTicksSelector, AxisVisibleTicks } from './compute_axis_visible_ticks'; +import { AxisGeometry } from '../../utils/axis_utils'; +import { computeAxesGeometriesSelector } from './compute_axis_visible_ticks'; import { computeLegendSelector } from './compute_legend'; import { computeSeriesGeometriesSelector } from './compute_series_geometries'; import { getAxisSpecsSelector } from './get_specs'; @@ -44,7 +45,7 @@ import { getAxisSpecsSelector } from './get_specs'; * @internal */ export const getDebugStateSelector = createCachedSelector( - [computeSeriesGeometriesSelector, computeLegendSelector, computeAxisVisibleTicksSelector, getAxisSpecsSelector], + [computeSeriesGeometriesSelector, computeLegendSelector, computeAxesGeometriesSelector, getAxisSpecsSelector], ({ geometries }, legend, axes, axesSpecs): DebugState => { const seriesNameMap = getSeriesNameMap(legend); @@ -58,18 +59,23 @@ export const getDebugStateSelector = createCachedSelector( }, )(getChartIdSelector); -const getAxes = (ticks: AxisVisibleTicks, axesSpecs: AxisSpec[]): DebugStateAxes | undefined => { +function getAxes(axesGeoms: AxisGeometry[], axesSpecs: AxisSpec[]): DebugStateAxes | undefined { if (axesSpecs.length === 0) { return; } return axesSpecs.reduce( (acc, { position, title, id }) => { - const axisTicks = ticks.axisVisibleTicks.get(id) ?? []; - const labels = axisTicks.map(({ label }) => label); - const values = axisTicks.map(({ value }) => value); - const grids = ticks.axisGridLinesPositions.get(id) ?? []; - const gridlines = grids.map(([x, y]) => ({ x, y })); + const geom = axesGeoms.find(({ axisId }) => axisId === id); + if (!geom) { + return acc; + } + + const { ticks, gridLinePositions } = geom; + const labels = ticks.map(({ label }) => label); + const values = ticks.map(({ value }) => value); + + const gridlines = gridLinePositions.map(([x, y]) => ({ x, y })); if (isVerticalAxis(position)) { acc.y.push({ @@ -99,9 +105,9 @@ const getAxes = (ticks: AxisVisibleTicks, axesSpecs: AxisSpec[]): DebugStateAxes x: [], }, ); -}; +} -const getBarsState = (seriesNameMap: Map, barGeometries: BarGeometry[]): DebugStateBar[] => { +function getBarsState(seriesNameMap: Map, barGeometries: BarGeometry[]): DebugStateBar[] { const buckets = new Map(); barGeometries.forEach( @@ -136,86 +142,90 @@ const getBarsState = (seriesNameMap: Map, barGeometries: BarGeom ); return [...buckets.values()]; -}; - -const getLineState = (seriesNameMap: Map) => ({ - line: path, - points, - color, - seriesIdentifier: { key }, - seriesLineStyle, - seriesPointStyle, -}: LineGeometry): DebugStateLine => { - const name = seriesNameMap.get(key) ?? ''; - - return { - path, +} + +function getLineState(seriesNameMap: Map) { + return ({ + line: path, + points, color, - key, - name, - visible: hasVisibleStyle(seriesLineStyle), - visiblePoints: hasVisibleStyle(seriesPointStyle), - points: points.map(({ value: { x, y, mark } }) => ({ x, y, mark })), - }; -}; - -const getAreaState = (seriesNameMap: Map) => ({ - area: path, - lines, - points, - color, - seriesIdentifier: { key }, - seriesAreaStyle, - seriesPointStyle, - seriesAreaLineStyle, -}: AreaGeometry): DebugStateArea => { - const [y1Path, y0Path] = lines; - const linePoints = points.reduce<{ - y0: DebugStateValue[]; - y1: DebugStateValue[]; - }>( - (acc, { value: { accessor, ...value } }) => { - if (accessor === BandedAccessorType.Y0) { - acc.y0.push(value); - } else { - acc.y1.push(value); - } + seriesIdentifier: { key }, + seriesLineStyle, + seriesPointStyle, + }: LineGeometry): DebugStateLine => { + const name = seriesNameMap.get(key) ?? ''; - return acc; - }, - { - y0: [], - y1: [], - }, - ); - const lineVisible = hasVisibleStyle(seriesAreaLineStyle); - const visiblePoints = hasVisibleStyle(seriesPointStyle); - const name = seriesNameMap.get(key) ?? ''; + return { + path, + color, + key, + name, + visible: hasVisibleStyle(seriesLineStyle), + visiblePoints: hasVisibleStyle(seriesPointStyle), + points: points.map(({ value: { x, y, mark } }) => ({ x, y, mark })), + }; + }; +} - return { - path, +function getAreaState(seriesNameMap: Map) { + return ({ + area: path, + lines, + points, color, - key, - name, - visible: hasVisibleStyle(seriesAreaStyle), - lines: { - y0: y0Path - ? { - visible: lineVisible, - path: y0Path, - points: linePoints.y0, - visiblePoints, - } - : undefined, - y1: { - visible: lineVisible, - path: y1Path, - points: linePoints.y1, - visiblePoints, + seriesIdentifier: { key }, + seriesAreaStyle, + seriesPointStyle, + seriesAreaLineStyle, + }: AreaGeometry): DebugStateArea => { + const [y1Path, y0Path] = lines; + const linePoints = points.reduce<{ + y0: DebugStateValue[]; + y1: DebugStateValue[]; + }>( + (acc, { value: { accessor, ...value } }) => { + if (accessor === BandedAccessorType.Y0) { + acc.y0.push(value); + } else { + acc.y1.push(value); + } + + return acc; }, - }, + { + y0: [], + y1: [], + }, + ); + const lineVisible = hasVisibleStyle(seriesAreaLineStyle); + const visiblePoints = hasVisibleStyle(seriesPointStyle); + const name = seriesNameMap.get(key) ?? ''; + + return { + path, + color, + key, + name, + visible: hasVisibleStyle(seriesAreaStyle), + lines: { + y0: y0Path + ? { + visible: lineVisible, + path: y0Path, + points: linePoints.y0, + visiblePoints, + } + : undefined, + y1: { + visible: lineVisible, + path: y1Path, + points: linePoints.y1, + visiblePoints, + }, + }, + }; }; -}; +} /** * returns series key to name mapping diff --git a/src/chart_types/xy_chart/state/selectors/get_specs.ts b/src/chart_types/xy_chart/state/selectors/get_specs.ts index 23e5c9cf62..d86d2b4513 100644 --- a/src/chart_types/xy_chart/state/selectors/get_specs.ts +++ b/src/chart_types/xy_chart/state/selectors/get_specs.ts @@ -20,6 +20,7 @@ import createCachedSelector from 're-reselect'; import { ChartTypes } from '../../..'; +import { IndexOrderSpec, SmallMultiplesSpec } from '../../../../specs'; import { SpecTypes } from '../../../../specs/constants'; import { GlobalChartState } from '../../../../state/chart_state'; import { getChartIdSelector } from '../../../../state/selectors/get_chart_id'; @@ -35,11 +36,35 @@ export const getAxisSpecsSelector = createCachedSelector([getSpecs], (specs): Ax /** @internal */ export const getSeriesSpecsSelector = createCachedSelector([getSpecs], (specs) => { - const seriesSpec = getSpecsFromStore(specs, ChartTypes.XYAxis, SpecTypes.Series); - return seriesSpec; + return getSpecsFromStore(specs, ChartTypes.XYAxis, SpecTypes.Series); })(getChartIdSelector); /** @internal */ export const getAnnotationSpecsSelector = createCachedSelector([getSpecs], (specs) => getSpecsFromStore(specs, ChartTypes.XYAxis, SpecTypes.Annotation), )(getChartIdSelector); + +/** @internal */ +export const getSmallMultiplesIndexOrderSelector = createCachedSelector([getSpecs], (specs) => { + const smallMultiples = getSpecsFromStore(specs, ChartTypes.Global, SpecTypes.SmallMultiples); + if (smallMultiples.length !== 1) { + return undefined; + } + const indexOrders = getSpecsFromStore(specs, ChartTypes.Global, SpecTypes.IndexOrder); + const [smallMultiplesConfig] = smallMultiples; + + let verticalIndex: IndexOrderSpec | undefined; + let horizontalIndex: IndexOrderSpec | undefined; + + if (smallMultiplesConfig.verticalIndex) { + verticalIndex = indexOrders.find((d) => d.id === smallMultiplesConfig.verticalIndex); + } + if (smallMultiplesConfig.horizontalIndex) { + horizontalIndex = indexOrders.find((d) => d.id === smallMultiplesConfig.horizontalIndex); + } + + return { + verticalIndex, + horizontalIndex, + }; +})(getChartIdSelector); diff --git a/src/chart_types/xy_chart/state/selectors/get_tooltip_values_highlighted_geoms.ts b/src/chart_types/xy_chart/state/selectors/get_tooltip_values_highlighted_geoms.ts index 240dc18fbd..734eae4edb 100644 --- a/src/chart_types/xy_chart/state/selectors/get_tooltip_values_highlighted_geoms.ts +++ b/src/chart_types/xy_chart/state/selectors/get_tooltip_values_highlighted_geoms.ts @@ -199,7 +199,8 @@ function getTooltipAndHighlightFromValue( return { tooltip: { header, - values, + // to avoid creating a breaking change because of a different sorting order on tooltip + values: values.reverse(), }, highlightedGeometries, }; diff --git a/src/chart_types/xy_chart/state/utils/types.ts b/src/chart_types/xy_chart/state/utils/types.ts index 9c2323a152..320d32247f 100644 --- a/src/chart_types/xy_chart/state/utils/types.ts +++ b/src/chart_types/xy_chart/state/utils/types.ts @@ -18,11 +18,12 @@ */ import { SeriesKey } from '../../../../commons/series_id'; import { Scale } from '../../../../scales'; +import { Domain } from '../../../../utils/domain'; import { PointGeometry, BarGeometry, AreaGeometry, LineGeometry, BubbleGeometry } from '../../../../utils/geometry'; import { GroupId } from '../../../../utils/ids'; -import { XDomain, YDomain } from '../../domains/types'; +import { BaseDomain, XDomain, YDomain } from '../../domains/types'; import { IndexedGeometryMap } from '../../utils/indexed_geometry_map'; -import { SeriesCollectionValue, FormattedDataSeries } from '../../utils/series'; +import { SeriesCollectionValue, FormattedDataSeries, DataSeries } from '../../utils/series'; /** @internal */ export interface Transform { @@ -78,10 +79,9 @@ export interface ComputedGeometries { export interface SeriesDomainsAndData { xDomain: XDomain; yDomain: YDomain[]; - formattedDataSeries: { - stacked: FormattedDataSeries[]; - nonStacked: FormattedDataSeries[]; - }; + smVDomain: Domain; + smHDomain: Domain; + formattedDataSeries: DataSeries[]; seriesCollection: Map; } diff --git a/src/chart_types/xy_chart/state/utils/utils.ts b/src/chart_types/xy_chart/state/utils/utils.ts index 40537fb4da..f63b753ff0 100644 --- a/src/chart_types/xy_chart/state/utils/utils.ts +++ b/src/chart_types/xy_chart/state/utils/utils.ts @@ -20,6 +20,7 @@ import { SeriesKey, SeriesIdentifier } from '../../../../commons/series_id'; import { Scale } from '../../../../scales'; import { ScaleType } from '../../../../scales/constants'; +import { IndexOrderSpec } from '../../../../specs'; import { OrderBy } from '../../../../specs/settings'; import { mergePartial, Rotation, Color, isUniqueArray } from '../../../../utils/commons'; import { CurveType } from '../../../../utils/curves'; @@ -28,23 +29,23 @@ import { Domain } from '../../../../utils/domain'; import { PointGeometry, BarGeometry, AreaGeometry, LineGeometry, BubbleGeometry } from '../../../../utils/geometry'; import { GroupId, SpecId } from '../../../../utils/ids'; import { ColorConfig, Theme } from '../../../../utils/themes/theme'; -import { XDomain, YDomain } from '../../domains/types'; +import { XDomain } from '../../domains/types'; import { mergeXDomain } from '../../domains/x_domain'; -import { mergeYDomain, splitSpecsByGroupId } from '../../domains/y_domain'; +import { groupSeriesByYGroup, isStackedSpec, mergeYDomain } from '../../domains/y_domain'; import { renderArea, renderBars, renderLine, renderBubble, isDatumFilled } from '../../rendering/rendering'; import { defaultTickFormatter } from '../../utils/axis_utils'; import { fillSeries } from '../../utils/fill_series'; +import { groupBy } from '../../utils/group_data_series'; import { IndexedGeometryMap } from '../../utils/indexed_geometry_map'; -import { computeXScale, computeYScales, countBarsInCluster } from '../../utils/scales'; +import { computeXScale, computeYScales } from '../../utils/scales'; import { DataSeries, SeriesCollectionValue, getSeriesIndex, - FormattedDataSeries, - getFormattedDataseries, - getDataSeriesBySpecId, - getSeriesKey, + getFormattedDataSeries, + getDataSeriesFromSpecs, XYChartSeriesIdentifier, + getSeriesKey, } from '../../utils/series'; import { AxisSpec, @@ -60,9 +61,9 @@ import { FitConfig, isBubbleSeriesSpec, YDomainRange, - SeriesTypes, StackMode, } from '../../utils/specs'; +import { SmallMultipleScales } from '../selectors/compute_small_multiple_scales'; import { getSpecsById, getAxesSpecForSpecId } from './spec'; import { SeriesDomainsAndData, ComputedGeometries, GeometriesCounts, Transform, LastValues } from './types'; @@ -91,10 +92,9 @@ export function updateDeselectedDataSeries( } /** - * Return map assocition between `seriesKey` and only the custom colors string + * Return map association between `seriesKey` and only the custom colors string * @param seriesSpecs * @param seriesCollection - * @param seriesColorOverrides color override from legend * @internal */ export function getCustomSeriesColors( @@ -131,78 +131,43 @@ export function getCustomSeriesColors( return updatedCustomSeriesColors; } -function getLastValues( - formattedDataSeries: { - stacked: FormattedDataSeries[]; - nonStacked: FormattedDataSeries[]; - }, - xDomain: XDomain, -): Map { +function getLastValues(dataSeries: DataSeries[], xDomain: XDomain): Map { const lastValues = new Map(); if (xDomain.scaleType === ScaleType.Ordinal) { return lastValues; } // we need to get the latest - formattedDataSeries.stacked.forEach(({ dataSeries, stackMode }) => { - dataSeries.forEach((series) => { - if (series.data.length === 0) { - return; - } - - const last = series.data[series.data.length - 1]; - if (!last) { - return; - } - if (isDatumFilled(last)) { - return; - } - - if (last.x !== xDomain.domain[xDomain.domain.length - 1]) { - // we have a dataset that is not filled with all x values - // and the last value of the series is not the last value for every series - // let's skip it - return; - } - - const { y0, y1, initialY0, initialY1 } = last; - const seriesKey = getSeriesKey(series as XYChartSeriesIdentifier); - - if (stackMode === StackMode.Percentage) { - const y1InPercentage = y1 === null || y0 === null ? null : y1 - y0; - lastValues.set(seriesKey, { y0, y1: y1InPercentage }); - return; - } - if (initialY0 !== null || initialY1 !== null) { - lastValues.set(seriesKey, { y0: initialY0, y1: initialY1 }); - } - }); - }); + dataSeries.forEach((series) => { + if (series.data.length === 0) { + return; + } - formattedDataSeries.nonStacked.forEach(({ dataSeries }) => { - dataSeries.forEach((series) => { - if (series.data.length === 0) { - return; - } - const last = series.data[series.data.length - 1]; - if (!last) { - return; - } - if (isDatumFilled(last)) { - return; - } + const last = series.data[series.data.length - 1]; + if (!last) { + return; + } + if (isDatumFilled(last)) { + return; + } - if (last.x !== xDomain.domain[xDomain.domain.length - 1]) { - // we have a dataset that is not filled with all x values - // and the last value of the series is not the last value for every series - // let's skip it - return; - } + if (last.x !== xDomain.domain[xDomain.domain.length - 1]) { + // we have a dataset that is not filled with all x values + // and the last value of the series is not the last value for every series + // let's skip it + return; + } - const { initialY1, initialY0 } = last; - const seriesKey = getSeriesKey(series as XYChartSeriesIdentifier); + const { y0, y1, initialY0, initialY1 } = last; + const seriesKey = getSeriesKey(series as XYChartSeriesIdentifier, series.groupId); + if (series.stackMode === StackMode.Percentage) { + const y1InPercentage = y1 === null || y0 === null ? null : y1 - y0; + lastValues.set(seriesKey, { y0, y1: y1InPercentage }); + return; + } + if (initialY0 !== null || initialY1 !== null) { lastValues.set(seriesKey, { y0: initialY0, y1: initialY1 }); - }); + } }); return lastValues; } @@ -218,6 +183,7 @@ function getLastValues( * @param enableVislibSeriesSort is optional; if not specified in , * then all series will be factored into computations. Otherwise, selectedDataSeries * is used to restrict the computation for just the selected series + * @param smallMultiples * @returns `SeriesDomainsAndData` * @internal */ @@ -228,34 +194,25 @@ export function computeSeriesDomains( customXDomain?: DomainRange | Domain, orderOrdinalBinsBy?: OrderBy, enableVislibSeriesSort?: boolean, + smallMultiples?: { verticalIndex?: IndexOrderSpec; horizontalIndex?: IndexOrderSpec }, ): SeriesDomainsAndData { - const { dataSeriesBySpecId, xValues, seriesCollection, fallbackScale } = getDataSeriesBySpecId( + const { dataSeries, xValues, seriesCollection, fallbackScale, smHValues, smVValues } = getDataSeriesFromSpecs( seriesSpecs, deselectedDataSeries, orderOrdinalBinsBy, enableVislibSeriesSort, + smallMultiples, ); // compute the x domain merging any custom domain const xDomain = mergeXDomain(seriesSpecs, xValues, customXDomain, fallbackScale); - const specsByGroupIds = splitSpecsByGroupId(seriesSpecs); + const specsByGroupIds = groupSeriesByYGroup(seriesSpecs); // fill series with missing x values - const filledDataSeriesBySpecId = fillSeries( - dataSeriesBySpecId, - xValues, - seriesSpecs, - xDomain.scaleType, - specsByGroupIds, - ); - const formattedDataSeries = getFormattedDataseries( - filledDataSeriesBySpecId, - xValues, - xDomain.scaleType, - seriesSpecs, - specsByGroupIds, - ); + const filledDataSeries = fillSeries(dataSeries, xValues, xDomain.scaleType, specsByGroupIds); + + const formattedDataSeries = getFormattedDataSeries(seriesSpecs, filledDataSeries, xValues, xDomain.scaleType); // let's compute the yDomain after computing all stacked values const yDomain = mergeYDomain(formattedDataSeries, seriesSpecs, customYDomainsByGroupId); @@ -276,6 +233,8 @@ export function computeSeriesDomains( return { xDomain, yDomain, + smHDomain: [...smHValues], + smVDomain: [...smVValues], formattedDataSeries, seriesCollection: updatedSeriesCollection, }; @@ -284,153 +243,82 @@ export function computeSeriesDomains( /** @internal */ export function computeSeriesGeometries( seriesSpecs: BasicSeriesSpec[], - xDomain: XDomain, - yDomain: YDomain[], - formattedDataSeries: { - stacked: FormattedDataSeries[]; - nonStacked: FormattedDataSeries[]; - }, + { xDomain, yDomain, formattedDataSeries }: SeriesDomainsAndData, seriesColorMap: Map, chartTheme: Theme, - chartDims: Dimensions, chartRotation: Rotation, axesSpecs: AxisSpec[], + smallMultiplesScales: SmallMultipleScales, enableHistogramMode: boolean, ): ComputedGeometries { const chartColors: ColorConfig = chartTheme.colors; - const barsPadding = enableHistogramMode ? chartTheme.scales.histogramPadding : chartTheme.scales.barsPadding; - const width = [0, 180].includes(chartRotation) ? chartDims.width : chartDims.height; - const height = [0, 180].includes(chartRotation) ? chartDims.height : chartDims.width; - // const { width, height } = chartDims; - const { stacked, nonStacked } = formattedDataSeries; + const barDataSeries = formattedDataSeries.filter(({ spec }) => isBarSeriesSpec(spec)); + // compute max bar in cluster per panel + const dataSeriesGroupedByPanel = groupBy( + barDataSeries, + ['smVerticalAccessorValue', 'smHorizontalAccessorValue'], + false, + ); - // compute how many series are clustered - const { stackedBarsInCluster, totalBarsInCluster } = countBarsInCluster(stacked, nonStacked); - // compute scales - const xScale = computeXScale({ - xDomain, - totalBarsInCluster, - range: [0, width], - barsPadding, - enableHistogramMode, - }); - const yScales = computeYScales({ yDomains: yDomain, range: [height, 0] }); + const barIndexByPanel = Object.keys(dataSeriesGroupedByPanel).reduce>((acc, panelKey) => { + const panelBars = dataSeriesGroupedByPanel[panelKey]; + const barDataSeriesByBarIndex = groupBy( + panelBars, + (d) => { + return getBarIndexKey(d, enableHistogramMode); + }, + false, + ); - // compute colors + acc[panelKey] = Object.keys(barDataSeriesByBarIndex); + return acc; + }, {}); - // compute geometries - const points: PointGeometry[] = []; - const areas: AreaGeometry[] = []; - const bars: BarGeometry[] = []; - const lines: LineGeometry[] = []; - const bubbles: BubbleGeometry[] = []; - const geometriesIndex = new IndexedGeometryMap(); - let orderIndex = 0; - const geometriesCounts: GeometriesCounts = { - points: 0, - bars: 0, - areas: 0, - areasPoints: 0, - lines: 0, - linePoints: 0, - bubbles: 0, - bubblePoints: 0, - }; + const { horizontal, vertical } = smallMultiplesScales; - formattedDataSeries.stacked.forEach((dataSeriesGroup) => { - const { groupId, dataSeries, counts, stackMode } = dataSeriesGroup; - const yScale = yScales.get(groupId); - if (!yScale) { - return; - } + const yScales = computeYScales({ yDomains: yDomain, range: [vertical.bandwidth, 0] }); - const geometries = renderGeometries( - orderIndex, - totalBarsInCluster, - true, - dataSeries, - xScale, - yScale, - seriesSpecs, - seriesColorMap, - chartColors.defaultVizColor, - axesSpecs, - chartTheme, - enableHistogramMode, - stackMode, - ); - orderIndex = counts[SeriesTypes.Bar] > 0 ? orderIndex + 1 : orderIndex; - areas.push(...geometries.areas); - lines.push(...geometries.lines); - bars.push(...geometries.bars); - bubbles.push(...geometries.bubbles); - points.push(...geometries.points); - geometriesIndex.merge(geometries.indexedGeometryMap); - // update counts - geometriesCounts.points += geometries.geometriesCounts.points; - geometriesCounts.bars += geometries.geometriesCounts.bars; - geometriesCounts.areas += geometries.geometriesCounts.areas; - geometriesCounts.areasPoints += geometries.geometriesCounts.areasPoints; - geometriesCounts.lines += geometries.geometriesCounts.lines; - geometriesCounts.linePoints += geometries.geometriesCounts.linePoints; - geometriesCounts.bubbles += geometries.geometriesCounts.bubbles; - geometriesCounts.bubblePoints += geometries.geometriesCounts.bubblePoints; - }); - orderIndex = 0; - formattedDataSeries.nonStacked.forEach((dataSeriesGroup) => { - const { groupId, dataSeries, counts } = dataSeriesGroup; - const yScale = yScales.get(groupId); - if (!yScale) { - return; - } + const { areas, bars, bubbles, lines, points, indexedGeometryMap, geometriesCounts } = renderGeometries( + formattedDataSeries, + xDomain, + yScales, + vertical, + horizontal, + barIndexByPanel, + seriesSpecs, + seriesColorMap, + chartColors.defaultVizColor, + axesSpecs, + chartTheme, + enableHistogramMode, + ); - const geometries = renderGeometries( - stackedBarsInCluster + orderIndex, - totalBarsInCluster, - false, - dataSeries, - xScale, - yScale, - seriesSpecs, - seriesColorMap, - chartColors.defaultVizColor, - axesSpecs, - chartTheme, - enableHistogramMode, - ); - orderIndex = counts[SeriesTypes.Bar] > 0 ? orderIndex + counts[SeriesTypes.Bar] : orderIndex; - - areas.push(...geometries.areas); - lines.push(...geometries.lines); - bars.push(...geometries.bars); - bubbles.push(...geometries.bubbles); - points.push(...geometries.points); - - geometriesIndex.merge(geometries.indexedGeometryMap); - // update counts - geometriesCounts.points += geometries.geometriesCounts.points; - geometriesCounts.bars += geometries.geometriesCounts.bars; - geometriesCounts.areas += geometries.geometriesCounts.areas; - geometriesCounts.areasPoints += geometries.geometriesCounts.areasPoints; - geometriesCounts.lines += geometries.geometriesCounts.lines; - geometriesCounts.linePoints += geometries.geometriesCounts.linePoints; - geometriesCounts.bubbles += geometries.geometriesCounts.bubbles; - geometriesCounts.bubblePoints += geometries.geometriesCounts.bubblePoints; + const totalBarsInCluster = Object.values(barIndexByPanel).reduce((acc, curr) => { + return Math.max(acc, curr.length); + }, 0); + + const xScale = computeXScale({ + xDomain, + totalBarsInCluster, + range: [0, horizontal.bandwidth], + barsPadding: enableHistogramMode ? chartTheme.scales.histogramPadding : chartTheme.scales.barsPadding, + enableHistogramMode, }); + return { scales: { xScale, yScales, }, geometries: { - points, areas, bars, - lines, bubbles, + lines, + points, }, - geometriesIndex, + geometriesIndex: indexedGeometryMap, geometriesCounts, }; } @@ -487,19 +375,18 @@ export function computeXScaleOffset( } function renderGeometries( - indexOffset: number, - clusteredCount: number, - isStacked: boolean, dataSeries: DataSeries[], - xScale: Scale, - yScale: Scale, + xDomain: XDomain, + yScales: Map, + smVScale: Scale, + smHScale: Scale, + barIndexOrderPerPanel: Record, seriesSpecs: BasicSeriesSpec[], seriesColorsMap: Map, defaultColor: string, axesSpecs: AxisSpec[], chartTheme: Theme, enableHistogramMode: boolean, - stackMode?: StackMode, ): { points: PointGeometry[]; bars: BarGeometry[]; @@ -529,18 +416,50 @@ function renderGeometries( bubbles: 0, bubblePoints: 0, }; - let barIndexOffset = 0; + const barsPadding = enableHistogramMode ? chartTheme.scales.histogramPadding : chartTheme.scales.barsPadding; + for (i = 0; i < len; i++) { const ds = dataSeries[i]; const spec = getSpecsById(seriesSpecs, ds.specId); if (spec === undefined) { continue; } + // compute the y scale + const yScale = yScales.get(ds.groupId); + if (!yScale) { + continue; + } + // compute the panel unique key + const barPanelKey = [ds.smVerticalAccessorValue, ds.smHorizontalAccessorValue].join('|'); + const barIndexOrder = barIndexOrderPerPanel[barPanelKey]; + // compute x scale + const xScale = computeXScale({ + xDomain, + totalBarsInCluster: barIndexOrder?.length ?? 0, + range: [0, smHScale.bandwidth], + barsPadding, + enableHistogramMode, + }); - const color = seriesColorsMap.get(getSeriesKey(ds)) || defaultColor; + const { stackMode } = ds; + + const panel: Dimensions = { + width: smHScale.bandwidth, + height: smVScale.bandwidth, + top: smVScale.scale(ds.smVerticalAccessorValue) || 0, + left: smHScale.scale(ds.smHorizontalAccessorValue) || 0, + }; + + const color = seriesColorsMap.get(ds.key) || defaultColor; if (isBarSeriesSpec(spec)) { - const shift = isStacked ? indexOffset : indexOffset + barIndexOffset; + const key = getBarIndexKey(ds, enableHistogramMode); + const shift = barIndexOrder.indexOf(key); + + if (shift === -1) { + // skip bar dataSeries if index is not available + continue; + } const barSeriesStyle = mergePartial(chartTheme.barSeriesStyle, spec.barSeriesStyle, { mergeOptionalPartialValues: true, }); @@ -557,6 +476,7 @@ function renderGeometries( ds, xScale, yScale, + panel, color, barSeriesStyle, displayValueSettings, @@ -567,18 +487,18 @@ function renderGeometries( indexedGeometryMap.merge(renderedBars.indexedGeometryMap); bars.push(...renderedBars.barGeometries); geometriesCounts.bars += renderedBars.barGeometries.length; - barIndexOffset += 1; } else if (isBubbleSeriesSpec(spec)) { - const bubbleShift = clusteredCount > 0 ? clusteredCount : 1; + const bubbleShift = barIndexOrder && barIndexOrder.length > 0 ? barIndexOrder.length : 1; const bubbleSeriesStyle = spec.bubbleSeriesStyle ? mergePartial(chartTheme.bubbleSeriesStyle, spec.bubbleSeriesStyle, { mergeOptionalPartialValues: true }) : chartTheme.bubbleSeriesStyle; const renderedBubbles = renderBubble( - (xScale.bandwidth * bubbleShift) / 2, + (xScale.bandwidth * bubbleShift) / 2 + panel.left, ds, xScale, yScale, color, + panel, isBandedSpec(spec.y0Accessors), bubbleSeriesStyle, { @@ -593,7 +513,7 @@ function renderGeometries( geometriesCounts.bubblePoints += renderedBubbles.bubbleGeometry.points.length; geometriesCounts.bubbles += 1; } else if (isLineSeriesSpec(spec)) { - const lineShift = clusteredCount > 0 ? clusteredCount : 1; + const lineShift = barIndexOrder && barIndexOrder.length > 0 ? barIndexOrder.length : 1; const lineSeriesStyle = spec.lineSeriesStyle ? mergePartial(chartTheme.lineSeriesStyle, spec.lineSeriesStyle, { mergeOptionalPartialValues: true }) : chartTheme.lineSeriesStyle; @@ -606,6 +526,7 @@ function renderGeometries( ds, xScale, yScale, + panel, color, spec.curve || CurveType.LINEAR, isBandedSpec(spec.y0Accessors), @@ -623,7 +544,7 @@ function renderGeometries( geometriesCounts.linePoints += renderedLines.lineGeometry.points.length; geometriesCounts.lines += 1; } else if (isAreaSeriesSpec(spec)) { - const areaShift = clusteredCount > 0 ? clusteredCount : 1; + const areaShift = barIndexOrder && barIndexOrder.length > 0 ? barIndexOrder.length : 1; const areaSeriesStyle = spec.areaSeriesStyle ? mergePartial(chartTheme.areaSeriesStyle, spec.areaSeriesStyle, { mergeOptionalPartialValues: true }) : chartTheme.areaSeriesStyle; @@ -634,6 +555,7 @@ function renderGeometries( ds, xScale, yScale, + panel, color, spec.curve || CurveType.LINEAR, isBandedSpec(spec.y0Accessors), @@ -643,10 +565,9 @@ function renderGeometries( enabled: spec.markSizeAccessor !== undefined, ratio: chartTheme.markSizeRatio, }, - isStacked, + spec.stackAccessors ? spec.stackAccessors.length > 0 : false, spec.pointStyleAccessor, hasFitFnConfigured(spec.fit), - stackMode, ); indexedGeometryMap.merge(renderedAreas.indexedGeometryMap); areas.push(renderedAreas.areaGeometry); @@ -699,3 +620,16 @@ export function computeChartTransform(chartDimensions: Dimensions, chartRotation function hasFitFnConfigured(fit?: Fit | FitConfig) { return Boolean(fit && ((fit as FitConfig).type || fit) !== Fit.None); } + +/** @internal */ +export function getBarIndexKey( + { spec, specId, groupId, yAccessor, splitAccessors }: DataSeries, + histogramModeEnabled: boolean, +) { + const isStacked = isStackedSpec(spec, histogramModeEnabled); + if (isStacked) { + return [groupId, '__stacked__'].join('__-__'); + } + + return [groupId, specId, ...splitAccessors.values(), yAccessor].join('__-__'); +} diff --git a/src/chart_types/xy_chart/utils/axis_utils.test.ts b/src/chart_types/xy_chart/utils/axis_utils.test.ts index 00135cf422..447d8a827c 100644 --- a/src/chart_types/xy_chart/utils/axis_utils.test.ts +++ b/src/chart_types/xy_chart/utils/axis_utils.test.ts @@ -983,12 +983,14 @@ describe('Axis computational utils', () => { axesStyles, xDomain, [yDomain], + [], + [], 1, false, (v) => `${v}`, ); - expect(axisTicksPosition.axisPositions.get(verticalAxisSpecWTitle.id)).toEqual({ + expect(axisTicksPosition.find(({ axisId }) => axisId === verticalAxisSpecWTitle.id)).toEqual({ top: 0, left: 10, width: 50, @@ -1011,12 +1013,14 @@ describe('Axis computational utils', () => { axesStyles, xDomain, [yDomain], + [], + [], 1, false, (v) => `${v}`, ); - expect(axisTicksPosition.axisPositions.get(verticalAxisSpecWTitle.id)).toEqual({ + expect(axisTicksPosition.find(({ axisId }) => axisId === verticalAxisSpecWTitle.id)).toEqual({ top: 0, left: 10, width: 10, @@ -1211,14 +1215,16 @@ describe('Axis computational utils', () => { axisStyles, xDomain, [yDomain], + [], + [], 1, false, (v) => `${v}`, ); - expect(axisTicksPosition.axisPositions.size).toBe(0); - expect(axisTicksPosition.axisTicks.size).toBe(0); - expect(axisTicksPosition.axisGridLinesPositions.size).toBe(0); - expect(axisTicksPosition.axisVisibleTicks.size).toBe(0); + expect(axisTicksPosition).toHaveLength(0); + // expect(axisTicksPosition.axisTicks.size).toBe(0); + // expect(axisTicksPosition.axisGridLinesPositions.size).toBe(0); + // expect(axisTicksPosition.axisVisibleTicks.size).toBe(0); }); test('should compute axis ticks positions', () => { @@ -1241,6 +1247,8 @@ describe('Axis computational utils', () => { axisStyles, xDomain, [yDomain], + [], + [], 1, false, (v) => `${v}`, @@ -1260,7 +1268,9 @@ describe('Axis computational utils', () => { [0, 100, 100, 100], ]; - expect(axisTicksPosition.axisGridLinesPositions.get(verticalAxisSpec.id)).toEqual(expectedVerticalAxisGridLines); + expect(axisTicksPosition.find(({ axisId }) => axisId === verticalAxisSpec.id)).toEqual( + expectedVerticalAxisGridLines, + ); const axisTicksPositionWithTopLegend = getAxisTicksPositions( { @@ -1274,6 +1284,8 @@ describe('Axis computational utils', () => { axisStyles, xDomain, [yDomain], + [], + [], 1, false, (v) => `${v}`, @@ -1285,7 +1297,9 @@ describe('Axis computational utils', () => { left: 100, top: 0, }; - const verticalAxisWithTopLegendPosition = axisTicksPositionWithTopLegend.axisPositions.get(verticalAxisSpec.id); + const verticalAxisWithTopLegendPosition = axisTicksPositionWithTopLegend.find( + ({ axisId }) => axisId === verticalAxisSpec.id, + ); expect(verticalAxisWithTopLegendPosition).toEqual(expectedPositionWithTopLegend); const ungroupedAxisSpec = { ...verticalAxisSpec, groupId: 'foo' }; @@ -1303,6 +1317,8 @@ describe('Axis computational utils', () => { axisStyles, xDomain, [yDomain], + [], + [], 1, false, (v) => `${v}`, @@ -1708,6 +1724,8 @@ describe('Axis computational utils', () => { axesStyles, xDomain, [yDomain], + [], + [], 1, false, customFotmatter, @@ -1717,7 +1735,9 @@ describe('Axis computational utils', () => { .slice() .reverse() .map(customFotmatter); - expect(axisTicksPosition.axisTicks.get(verticalAxisSpec.id)!.map(({ label }) => label)).toEqual(expected); + expect( + axisTicksPosition.find(({ axisId }) => axisId === verticalAxisSpec.id)!.ticks.map(({ label }) => label), + ).toEqual(expected); }); it('should not use custom formatter with x axis', () => { @@ -1739,13 +1759,17 @@ describe('Axis computational utils', () => { axesStyles, xDomain, [yDomain], + [], + [], 1, false, customFotmatter, ); const expected = axis1Dims.tickValues.slice().map(defaultTickFormatter); - expect(axisTicksPosition.axisTicks.get(horizontalAxisSpec.id)!.map(({ label }) => label)).toEqual(expected); + expect( + axisTicksPosition.find(({ axisId }) => axisId === horizontalAxisSpec.id)!.ticks.map(({ label }) => label), + ).toEqual(expected); }); it('should use custom axis tick formatter to get labels for x axis', () => { @@ -1772,13 +1796,17 @@ describe('Axis computational utils', () => { axesStyles, xDomain, [yDomain], + [], + [], 1, false, customFotmatter, ); const expected = axis1Dims.tickValues.slice().map(customAxisFotmatter); - expect(axisTicksPosition.axisTicks.get(spec.id)!.map(({ label }) => label)).toEqual(expected); + expect(axisTicksPosition.find(({ axisId }) => axisId === spec.id)!.ticks.map(({ label }) => label)).toEqual( + expected, + ); }); }); }); diff --git a/src/chart_types/xy_chart/utils/axis_utils.ts b/src/chart_types/xy_chart/utils/axis_utils.ts index a294aee923..47161687d4 100644 --- a/src/chart_types/xy_chart/utils/axis_utils.ts +++ b/src/chart_types/xy_chart/utils/axis_utils.ts @@ -17,7 +17,7 @@ * under the License. */ -import { Scale } from '../../../scales'; +import { Scale, ScaleBand } from '../../../scales'; import { BBox, BBoxCalculator } from '../../../utils/bbox/bbox_calculator'; import { Position, @@ -29,6 +29,7 @@ import { mergePartial, } from '../../../utils/commons'; import { Dimensions, Margins, getSimplePadding } from '../../../utils/dimensions'; +import { Domain } from '../../../utils/domain'; import { AxisId } from '../../../utils/ids'; import { Logger } from '../../../utils/logger'; import { AxisStyle, Theme, TextAlignment, TextOffset } from '../../../utils/themes/theme'; @@ -78,6 +79,11 @@ export const defaultTickFormatter = (tick: any) => `${tick}`; * @param totalBarsInCluster the total number of grouped series * @param bboxCalculator an instance of the boundingbox calculator * @param chartRotation the rotation of the chart + * @param gridLine + * @param tickLabel + * @param fallBackTickFormatter + * @param barsPadding + * @param enableHistogramMode * @internal */ export function computeAxisTicksDimensions( @@ -349,11 +355,13 @@ function getVerticalAlign( /** * Gets the computed x/y coordinates & alignment properties for an axis tick label. * @param isVerticalAxis if the axis is vertical (in contrast to horizontal) - * @param tickSize length of tick line - * @param tickPadding amount of padding between label and tick line * @param tickPosition position of tick relative to axis line origin and other ticks along it * @param position position of where the axis sits relative to the visualization - * @param axisTicksDimensions computed axis dimensions and values (from computeTickDimensions) + * @param axisPosition + * @param tickDimensions + * @param showTicks + * @param textOffset + * @param textAlignment * @internal */ export function getTickLabelProps( @@ -678,6 +686,17 @@ export function shouldShowTicks({ visible, strokeWidth, size }: AxisStyle['tickL return !axisHidden && visible && size > 0 && strokeWidth >= MIN_STROKE_WIDTH; } +export interface AxisGeometry { + position: Dimensions; + dimension: AxisTicksDimensions; + ticks: AxisTick[]; + visibleTicks: AxisTick[]; + gridLinePositions: AxisLinePosition[]; + axisId: AxisId; + smVerticalValue: unknown; + smHorizontalValue: unknown; +} + /** @internal */ export function getAxisTicksPositions( computedChartDims: { @@ -691,123 +710,191 @@ export function getAxisTicksPositions( axesStyles: Map, xDomain: XDomain, yDomain: YDomain[], + smVDomain: Domain, + smHDomain: Domain, totalGroupsCount: number, enableHistogramMode: boolean, fallBackTickFormatter: TickFormatter, barsPadding?: number, -): { - axisPositions: Map; - axisTicks: Map; - axisVisibleTicks: Map; - axisGridLinesPositions: Map; -} { - const axisPositions: Map = new Map(); - const axisVisibleTicks: Map = new Map(); - const axisTicks: Map = new Map(); - const axisGridLinesPositions: Map = new Map(); +): Array { + const axesGeometries: Array = []; + const { chartDimensions } = computedChartDims; - let cumTopSum = 0; - let cumBottomSum = chartPaddings.bottom; - let cumLeftSum = computedChartDims.leftMargin; - let cumRightSum = chartPaddings.right; + + // compute the anchor point for every axis group + + const anchorPointByAxisGroups = [...axisDimensions.entries()] + // .sort((a, b) => { + // const axisSpecA = getSpecsById(axisSpecs, a[0]); + // const axisSpecB = getSpecsById(axisSpecs, b[0]); + // return isVerticalAxis(axisSpecA.position); + // }) + .reduce( + (acc, [axisId, dimension]) => { + const axisSpec = getSpecsById(axisSpecs, axisId); + if (!axisSpec) { + return acc; + } + + const { axisTitle, tickLine, tickLabel } = axesStyles.get(axisId) ?? sharedAxesStyle; + const labelPadding = getSimplePadding(tickLabel.padding); + const showTicks = shouldShowTicks(tickLine, axisSpec.hide); + const axisTitleHeight = axisSpec.title !== undefined ? axisTitle.fontSize : 0; + const tickDimension = showTicks ? tickLine.size + tickLine.padding : 0; + const labelPaddingSum = tickLabel.visible ? labelPadding.inner + labelPadding.outer : 0; + + const { dimensions, topIncrement, bottomIncrement, leftIncrement, rightIncrement } = getAxisPosition( + chartDimensions, + chartMargins, + axisTitleHeight, + axisTitle, + axisSpec, + dimension, + acc.top, + acc.bottom, + acc.left, + acc.right, + labelPaddingSum, + tickDimension, + tickLabel.visible, + ); + const anchor = { + top: acc.top + topIncrement, + bottom: acc.bottom + bottomIncrement, + left: acc.left + leftIncrement, + right: acc.right + rightIncrement, + }; + acc.pos.set(axisId, { + anchor: { + top: acc.top, + left: acc.left, + right: acc.right, + bottom: acc.bottom, + }, + dimensions, + }); + return { + ...anchor, + pos: acc.pos, + }; + }, + { + top: 0, + bottom: chartPaddings.bottom, + left: computedChartDims.leftMargin, + right: chartPaddings.right, + pos: new Map< + AxisId, + { + anchor: { left: number; right: number; top: number; bottom: number }; + dimensions: Dimensions; + } + >(), + }, + ).pos; axisDimensions.forEach((axisDim, id) => { const axisSpec = getSpecsById(axisSpecs, id); - + const anchorPoint = anchorPointByAxisGroups.get(id); // Consider refactoring this so this condition can be tested // Given some of the values that get passed around, maybe re-write as a reduce instead of forEach? - if (!axisSpec) { + if (!axisSpec || !anchorPoint) { return; } - const minMaxRanges = getMinMaxRange(axisSpec.position, chartRotation, chartDimensions); - - const scale = getScaleForAxisSpec( - axisSpec, - xDomain, - yDomain, - totalGroupsCount, - chartRotation, - minMaxRanges.minRange, - minMaxRanges.maxRange, - barsPadding, - enableHistogramMode, - ); - if (!scale) { - throw new Error(`Cannot compute scale for axis spec ${axisSpec.id}`); - } - const tickFormatOptions = { - timeZone: xDomain.timeZone, - }; - const { axisTitle, tickLine, tickLabel, gridLine } = axesStyles.get(id) ?? sharedAxesStyle; const isVertical = isVerticalAxis(axisSpec.position); - // TODO: Find the true cause of the this offset error - const rotationOffset = - enableHistogramMode && - ((isVertical && [-90].includes(chartRotation)) || (!isVertical && [180].includes(chartRotation))) - ? scale.step - : 0; - - const allTicks = getAvailableTicks( - axisSpec, - scale, - totalGroupsCount, - enableHistogramMode, - isVertical ? fallBackTickFormatter : defaultTickFormatter, - rotationOffset, - tickFormatOptions, - ); + let smAxisDomain = isVertical ? smVDomain : smHDomain; - const visibleTicks = getVisibleTicks(allTicks, axisSpec, axisDim); - const axisSpecConfig = axisSpec.gridLine; - const gridLineThemeStyles = isVertical ? gridLine.vertical : gridLine.horizontal; - const gridLineStyles = axisSpecConfig ? mergePartial(gridLineThemeStyles, axisSpecConfig) : gridLineThemeStyles; - - if (axisSpec.showGridLines ?? gridLineStyles.visible) { - const gridLines = visibleTicks.map( - (tick: AxisTick): AxisLinePosition => computeAxisGridLinePositions(isVertical, tick.position, chartDimensions), - ); - axisGridLinesPositions.set(id, gridLines); + if (smAxisDomain.length === 0) { + smAxisDomain = ['base']; } - const labelPadding = getSimplePadding(tickLabel.padding); - const showTicks = shouldShowTicks(tickLine, axisSpec.hide); - const axisTitleHeight = axisSpec.title !== undefined ? axisTitle.fontSize : 0; - const tickDimension = showTicks ? tickLine.size + tickLine.padding : 0; - const labelPaddingSum = tickLabel.visible ? labelPadding.inner + labelPadding.outer : 0; - - const axisPosition = getAxisPosition( - chartDimensions, - chartMargins, - axisTitleHeight, - axisTitle, - axisSpec, - axisDim, - cumTopSum, - cumBottomSum, - cumLeftSum, - cumRightSum, - labelPaddingSum, - tickDimension, - tickLabel.visible, + const smPanelScale = new ScaleBand( + smAxisDomain, + [0, isVertical ? anchorPoint.dimensions.height : anchorPoint.dimensions.width], + undefined, + smAxisDomain.length > 1 ? 0.1 : 0, ); - cumTopSum += axisPosition.topIncrement; - cumBottomSum += axisPosition.bottomIncrement; - cumLeftSum += axisPosition.leftIncrement; - cumRightSum += axisPosition.rightIncrement; + smAxisDomain.forEach((smDomainValue) => { + const axisSize = { + width: isVertical ? chartDimensions.width : smPanelScale.bandwidth, + height: isVertical ? smPanelScale.bandwidth : chartDimensions.height, + top: 0, + left: 0, + }; + + const minMaxRanges = getMinMaxRange(axisSpec.position, chartRotation, axisSize); + + const scale = getScaleForAxisSpec( + axisSpec, + xDomain, + yDomain, + totalGroupsCount, + chartRotation, + minMaxRanges.minRange, + minMaxRanges.maxRange, + barsPadding, + enableHistogramMode, + ); - axisPositions.set(id, axisPosition.dimensions); - axisVisibleTicks.set(id, visibleTicks); - axisTicks.set(id, allTicks); + if (!scale) { + throw new Error(`Cannot compute scale for axis spec ${axisSpec.id}`); + } + const tickFormatOptions = { + timeZone: xDomain.timeZone, + }; + const { gridLine } = axesStyles.get(id) ?? sharedAxesStyle; + // TODO: Find the true cause of the this offset error + const rotationOffset = + enableHistogramMode && + ((isVertical && [-90].includes(chartRotation)) || (!isVertical && [180].includes(chartRotation))) + ? scale.step + : 0; + + const allTicks = getAvailableTicks( + axisSpec, + scale, + totalGroupsCount, + enableHistogramMode, + isVertical ? fallBackTickFormatter : defaultTickFormatter, + rotationOffset, + tickFormatOptions, + ); + const visibleTicks = getVisibleTicks(allTicks, axisSpec, axisDim); + + const axisSpecConfig = axisSpec.gridLine; + const gridLineThemeStyles = isVertical ? gridLine.vertical : gridLine.horizontal; + const gridLineStyles = axisSpecConfig ? mergePartial(gridLineThemeStyles, axisSpecConfig) : gridLineThemeStyles; + + const gridLines = + axisSpec.showGridLines ?? gridLineStyles.visible + ? visibleTicks.map( + (tick: AxisTick): AxisLinePosition => + computeAxisGridLinePositions(isVertical, tick.position, chartDimensions), + ) + : []; + + const position = { + top: anchorPoint.dimensions.top + (isVertical ? smPanelScale.scale(smDomainValue) ?? 0 : 0), + left: anchorPoint.dimensions.left + (!isVertical ? smPanelScale.scale(smDomainValue) ?? 0 : 0), + width: isVertical ? anchorPoint.dimensions.width : smPanelScale.bandwidth, + height: isVertical ? smPanelScale.bandwidth : anchorPoint.dimensions.height, + }; + + axesGeometries.push({ + axisId: axisSpec.id, + position, + dimension: axisDim, + ticks: allTicks, + visibleTicks, + gridLinePositions: gridLines, + smHorizontalValue: isVertical ? null : smDomainValue, + smVerticalValue: isVertical ? smDomainValue : null, + }); + }); }); - - return { - axisPositions, - axisTicks, - axisVisibleTicks, - axisGridLinesPositions, - }; + return axesGeometries; } /** @internal */ diff --git a/src/chart_types/xy_chart/utils/dimensions.ts b/src/chart_types/xy_chart/utils/dimensions.ts index 7d4642d3b2..83024c7a27 100644 --- a/src/chart_types/xy_chart/utils/dimensions.ts +++ b/src/chart_types/xy_chart/utils/dimensions.ts @@ -17,12 +17,11 @@ * under the License. */ -import { Position } from '../../../utils/commons'; -import { Dimensions, getSimplePadding } from '../../../utils/dimensions'; +import { Dimensions } from '../../../utils/dimensions'; import { AxisId } from '../../../utils/ids'; import { Theme, AxisStyle } from '../../../utils/themes/theme'; -import { getSpecsById } from '../state/utils/spec'; -import { AxisTicksDimensions, shouldShowTicks } from './axis_utils'; +import { computeAxesSizes } from '../axes/axes_sizes'; +import { AxisTicksDimensions } from './axis_utils'; import { AxisSpec } from './specs'; /** @@ -50,14 +49,16 @@ export interface ChartDimensions { * Compute the chart dimensions. It's computed removing from the parent dimensions * the axis spaces, the legend and any other specified style margin and padding. * @param parentDimensions the parent dimension - * @param chartTheme the theme style of the chart + * @param theme * @param axisDimensions the axis dimensions + * @param axesStyles * @param axisSpecs the axis specs + * @param legendSizing * @internal */ export function computeChartDimensions( parentDimensions: Dimensions, - { chartMargins, chartPaddings, axes: sharedAxesStyles }: Theme, + theme: Theme, axisDimensions: Map, axesStyles: Map, axisSpecs: AxisSpec[], @@ -82,64 +83,16 @@ export function computeChartDimensions( }; } - let vLeftAxisSpecWidth = 0; - let vRightAxisSpecWidth = 0; - let hTopAxisSpecHeight = 0; - let hBottomAxisSpecHeight = 0; - let horizontalEdgeLabelOverflow = 0; - let verticalEdgeLabelOverflow = 0; - axisDimensions.forEach(({ maxLabelBboxWidth = 0, maxLabelBboxHeight = 0 }, id) => { - const axisSpec = getSpecsById(axisSpecs, id); - if (!axisSpec || axisSpec.hide) { - return; - } - const { tickLine, axisTitle, tickLabel } = axesStyles.get(id) ?? sharedAxesStyles; - const showTicks = shouldShowTicks(tickLine, axisSpec.hide); - const { position, title } = axisSpec; - const titlePadding = getSimplePadding(axisTitle.padding); - const labelPadding = getSimplePadding(tickLabel.padding); - const labelPaddingSum = tickLabel.visible ? labelPadding.inner + labelPadding.outer : 0; + const axisSizes = computeAxesSizes(theme, axisDimensions, axesStyles, axisSpecs); - const tickDimension = showTicks ? tickLine.size + tickLine.padding : 0; - const titleHeight = - title !== undefined && axisTitle.visible ? axisTitle.fontSize + titlePadding.outer + titlePadding.inner : 0; - const axisDimension = labelPaddingSum + tickDimension + titleHeight; - const maxAxisHeight = tickLabel.visible ? maxLabelBboxHeight + axisDimension : axisDimension; - const maxAxisWidth = tickLabel.visible ? maxLabelBboxWidth + axisDimension : axisDimension; - switch (position) { - case Position.Top: - hTopAxisSpecHeight += maxAxisHeight + chartMargins.top; - // find the max half label size to accomodate the left/right labels - horizontalEdgeLabelOverflow = Math.max(horizontalEdgeLabelOverflow, maxLabelBboxWidth / 2); - break; - case Position.Bottom: - hBottomAxisSpecHeight += maxAxisHeight + chartMargins.bottom; - // find the max half label size to accomodate the left/right labels - horizontalEdgeLabelOverflow = Math.max(horizontalEdgeLabelOverflow, maxLabelBboxWidth / 2); - break; - case Position.Right: - vRightAxisSpecWidth += maxAxisWidth + chartMargins.right; - verticalEdgeLabelOverflow = Math.max(verticalEdgeLabelOverflow, maxLabelBboxHeight / 2); - break; - case Position.Left: - default: - vLeftAxisSpecWidth += maxAxisWidth + chartMargins.left; - verticalEdgeLabelOverflow = Math.max(verticalEdgeLabelOverflow, maxLabelBboxHeight / 2); - } - }); - const chartLeftAxisMaxWidth = Math.max(vLeftAxisSpecWidth, horizontalEdgeLabelOverflow + chartMargins.left); - const chartRightAxisMaxWidth = Math.max(vRightAxisSpecWidth, horizontalEdgeLabelOverflow + chartMargins.right); - const chartTopAxisMaxHeight = Math.max(hTopAxisSpecHeight, verticalEdgeLabelOverflow + chartMargins.top); - const chartBottomAxisMaxHeight = Math.max(hBottomAxisSpecHeight, verticalEdgeLabelOverflow + chartMargins.bottom); - - const chartWidth = parentDimensions.width - chartLeftAxisMaxWidth - chartRightAxisMaxWidth; - const chartHeight = parentDimensions.height - chartTopAxisMaxHeight - chartBottomAxisMaxHeight; - - const top = chartTopAxisMaxHeight + chartPaddings.top; - const left = chartLeftAxisMaxWidth + chartPaddings.left; + const chartWidth = parentDimensions.width - axisSizes.left - axisSizes.right; + const chartHeight = parentDimensions.height - axisSizes.top - axisSizes.bottom; + const { chartPaddings } = theme; + const top = axisSizes.top + chartPaddings.top; + const left = axisSizes.left + chartPaddings.left; return { - leftMargin: chartLeftAxisMaxWidth - vLeftAxisSpecWidth, + leftMargin: axisSizes.margin.left, chartDimensions: { top, left, diff --git a/src/chart_types/xy_chart/utils/fill_series.ts b/src/chart_types/xy_chart/utils/fill_series.ts index dd8f318c4d..8f2be5ac57 100644 --- a/src/chart_types/xy_chart/utils/fill_series.ts +++ b/src/chart_types/xy_chart/utils/fill_series.ts @@ -17,20 +17,17 @@ * under the License. */ import { ScaleType } from '../../../scales/constants'; -import { SpecId, GroupId } from '../../../utils/ids'; +import { GroupId } from '../../../utils/ids'; import { YBasicSeriesSpec } from '../domains/y_domain'; -import { getSpecsById } from '../state/utils/spec'; import { DataSeries } from './series'; -import { SeriesSpecs, StackMode, BasicSeriesSpec, isLineSeriesSpec, isAreaSeriesSpec } from './specs'; +import { StackMode, BasicSeriesSpec, isLineSeriesSpec, isAreaSeriesSpec } from './specs'; /** - * Fill missing x values in all data series * @internal */ export function fillSeries( - series: Map, + dataSeries: DataSeries[], xValues: Set, - seriesSpecs: SeriesSpecs, groupScaleType: ScaleType, specsByGroupIds: Map< GroupId, @@ -40,25 +37,22 @@ export function fillSeries( nonStacked: YBasicSeriesSpec[]; } >, -): Map { +): DataSeries[] { const sortedXValues = [...xValues.values()]; - const filledSeries: Map = new Map(); - series.forEach((dataSeries, key) => { - const spec = getSpecsById(seriesSpecs, key); - if (!spec) { - return; - } - const group = specsByGroupIds.get(spec.groupId); - if (!group) { - return; - } - const isStacked = Boolean(group.stacked.find(({ id }) => id === key)); - const noFillRequired = isXFillNotRequired(spec, groupScaleType, isStacked); + return dataSeries + .map((series) => { + const { key, spec, data } = series; + const group = specsByGroupIds.get(spec.groupId); + if (!group) { + return undefined; + } + const isStacked = Boolean(group.stacked.find(({ id }) => id === key)); + + const noFillRequired = isXFillNotRequired(spec, groupScaleType, isStacked); - const filledDataSeries = dataSeries.map(({ data, ...rest }) => { if (data.length === xValues.size || noFillRequired) { return { - ...rest, + ...series, data, }; } @@ -89,13 +83,11 @@ export function fillSeries( }); } return { - ...rest, + ...series, data: filledData, }; - }); - filledSeries.set(key, filledDataSeries); - }); - return filledSeries; + }) + .filter((d) => d) as DataSeries[]; } function isXFillNotRequired(spec: BasicSeriesSpec, groupScaleType: ScaleType, isStacked: boolean) { diff --git a/src/chart_types/xy_chart/utils/fit_function_utils.ts b/src/chart_types/xy_chart/utils/fit_function_utils.ts index d17dba29b8..5416f5f6d2 100644 --- a/src/chart_types/xy_chart/utils/fit_function_utils.ts +++ b/src/chart_types/xy_chart/utils/fit_function_utils.ts @@ -25,14 +25,11 @@ import { isAreaSeriesSpec, isLineSeriesSpec, SeriesSpecs, BasicSeriesSpec } from /** @internal */ export const applyFitFunctionToDataSeries = ( - dataseries: DataSeries[], + dataSeries: DataSeries[], seriesSpecs: SeriesSpecs, xScaleType: ScaleType, ): DataSeries[] => { - const len = dataseries.length; - const formattedValues: DataSeries[] = []; - for (let i = 0; i < len; i++) { - const { specId, data, ...rest } = dataseries[i]; + return dataSeries.map(({ specId, data, ...rest }) => { const spec = getSpecsById(seriesSpecs, specId); if ( @@ -43,14 +40,12 @@ export const applyFitFunctionToDataSeries = ( ) { const fittedData = fitFunction(data, spec.fit, xScaleType); - formattedValues.push({ + return { specId, ...rest, data: fittedData, - }); - } else { - formattedValues.push({ specId, data, ...rest }); + }; } - } - return formattedValues; + return { specId, data, ...rest }; + }); }; diff --git a/src/chart_types/xy_chart/utils/group_data_series.ts b/src/chart_types/xy_chart/utils/group_data_series.ts new file mode 100644 index 0000000000..59c39e09cd --- /dev/null +++ b/src/chart_types/xy_chart/utils/group_data_series.ts @@ -0,0 +1,47 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +type Group = Record; +type GroupByKeyFn = (data: T) => string; +type GroupKeysOrKeyFn = Array | GroupByKeyFn; + +export function groupBy(data: T[], keysOrKeyFn: GroupKeysOrKeyFn, asArray: false): Group; +export function groupBy(data: T[], keysOrKeyFn: GroupKeysOrKeyFn, asArray: true): T[][]; +export function groupBy(data: T[], keysOrKeyFn: GroupKeysOrKeyFn, asArray: boolean): T[][] | Group { + const keyFn = Array.isArray(keysOrKeyFn) ? getUniqueKey(keysOrKeyFn) : keysOrKeyFn; + const grouped = data.reduce>((acc, curr) => { + const key = keyFn(curr); + if (!acc[key]) { + acc[key] = []; + } + acc[key].push(curr); + return acc; + }, {}); + return asArray ? Object.values(grouped) : grouped; +} + +export function getUniqueKey(keys: Array, concat = '|') { + return (data: T): string => { + return keys + .map((key) => { + return data[key]; + }) + .join(concat); + }; +} diff --git a/src/chart_types/xy_chart/utils/scales.ts b/src/chart_types/xy_chart/utils/scales.ts index 579bf4031e..5bc6a64ff4 100644 --- a/src/chart_types/xy_chart/utils/scales.ts +++ b/src/chart_types/xy_chart/utils/scales.ts @@ -24,35 +24,6 @@ import { XDomain, YDomain } from '../domains/types'; import { FormattedDataSeries } from './series'; import { SeriesTypes } from './specs'; -/** - * Count the max number of bars in cluster value. - * Doesn't take in consideration areas, lines or points. - * @param stacked all the stacked formatted dataseries - * @param nonStacked all the non-stacked formatted dataseries - * @internal - */ -export function countBarsInCluster( - stacked: FormattedDataSeries[], - nonStacked: FormattedDataSeries[], -): { - nonStackedBarsInCluster: number; - stackedBarsInCluster: number; - totalBarsInCluster: number; -} { - // along x axis, we count one "space" per bar series. - // we ignore the points, areas, lines as they are - // aligned with the x value and doesn't occupy space - const nonStackedBarsInCluster = nonStacked.reduce((acc, ns) => acc + ns.counts[SeriesTypes.Bar], 0); - // count stacked bars groups as 1 per group - const stackedBarsInCluster = stacked.reduce((acc, ns) => acc + (ns.counts[SeriesTypes.Bar] > 0 ? 1 : 0), 0); - const totalBarsInCluster = nonStackedBarsInCluster + stackedBarsInCluster; - return { - nonStackedBarsInCluster, - stackedBarsInCluster, - totalBarsInCluster, - }; -} - function getBandScaleRange( isInverse: boolean, isSingleValueHistogram: boolean, diff --git a/src/chart_types/xy_chart/utils/series.test.ts b/src/chart_types/xy_chart/utils/series.test.ts index 1404d6e040..f7638307d3 100644 --- a/src/chart_types/xy_chart/utils/series.test.ts +++ b/src/chart_types/xy_chart/utils/series.test.ts @@ -18,6 +18,7 @@ */ import { ChartTypes } from '../..'; +import { MockDataSeries } from '../../../mocks/series'; import { MockSeriesIdentifier } from '../../../mocks/series/series_identifiers'; import { MockSeriesSpec, MockGlobalSpec } from '../../../mocks/specs'; import { MockStore } from '../../../mocks/store'; @@ -28,19 +29,18 @@ import { AccessorFn } from '../../../utils/accessor'; import { Position } from '../../../utils/commons'; import * as TestDataset from '../../../utils/data_samples/test_dataset'; import { ColorConfig } from '../../../utils/themes/theme'; -import { splitSpecsByGroupId } from '../domains/y_domain'; import { computeSeriesDomainsSelector } from '../state/selectors/compute_series_domains'; import { SeriesCollectionValue, - getFormattedDataseries, + getFormattedDataSeries, getSeriesColors, getSortedDataSeriesColorsValuesMap, - getDataSeriesBySpecId, - splitSeriesDataByAccessors, + getDataSeriesFromSpecs, XYChartSeriesIdentifier, - extractYandMarkFromDatum, + extractYAndMarkFromDatum, getSeriesName, DataSeries, + splitSeriesDataByAccessors, } from './series'; import { BasicSeriesSpec, LineSeriesSpec, SeriesTypes, AreaSeriesSpec } from './specs'; import { formatStackedDataSeriesValues } from './stacked_series_utils'; @@ -50,13 +50,13 @@ const dg = new SeededDataGenerator(); describe('Series', () => { test('Can split dataset into 1Y0G series', () => { const splitSeries = splitSeriesDataByAccessors( - { + MockSeriesSpec.bar({ id: 'spec1', data: TestDataset.BARCHART_1Y0G, xAccessor: 'x', yAccessors: ['y1'], splitSeriesAccessors: ['y'], - }, + }), new Map(), ); @@ -64,63 +64,63 @@ describe('Series', () => { }); test('Can split dataset into 1Y1G series', () => { const splitSeries = splitSeriesDataByAccessors( - { + MockSeriesSpec.bar({ id: 'spec1', data: TestDataset.BARCHART_1Y1G, xAccessor: 'x', yAccessors: ['y'], - }, + }), new Map(), ); expect([...splitSeries.dataSeries.values()]).toMatchSnapshot(); }); test('Can split dataset into 1Y2G series', () => { const splitSeries = splitSeriesDataByAccessors( - { + MockSeriesSpec.bar({ id: 'spec1', data: TestDataset.BARCHART_1Y2G, xAccessor: 'x', yAccessors: ['y'], splitSeriesAccessors: ['g1', 'g2'], - }, + }), new Map(), ); expect([...splitSeries.dataSeries.values()]).toMatchSnapshot(); }); test('Can split dataset into 2Y0G series', () => { const splitSeries = splitSeriesDataByAccessors( - { + MockSeriesSpec.bar({ id: 'spec1', data: TestDataset.BARCHART_2Y0G, xAccessor: 'x', yAccessors: ['y1', 'y2'], - }, + }), new Map(), ); expect([...splitSeries.dataSeries.values()]).toMatchSnapshot(); }); test('Can split dataset into 2Y1G series', () => { const splitSeries = splitSeriesDataByAccessors( - { + MockSeriesSpec.bar({ id: 'spec1', data: TestDataset.BARCHART_2Y1G, xAccessor: 'x', yAccessors: ['y1', 'y2'], splitSeriesAccessors: ['g'], - }, + }), new Map(), ); expect([...splitSeries.dataSeries.values()]).toMatchSnapshot(); }); test('Can split dataset into 2Y2G series', () => { const splitSeries = splitSeriesDataByAccessors( - { + MockSeriesSpec.bar({ id: 'spec1', data: TestDataset.BARCHART_2Y2G, xAccessor: 'x', yAccessors: ['y1', 'y2'], splitSeriesAccessors: ['g1', 'g2'], - }, + }), new Map(), ); expect([...splitSeries.dataSeries.values()]).toMatchSnapshot(); @@ -128,13 +128,13 @@ describe('Series', () => { it('should get sum of all xValues', () => { const xValueSums = new Map(); splitSeriesDataByAccessors( - { + MockSeriesSpec.bar({ id: 'spec1', data: TestDataset.BARCHART_1Y1G_ORDINAL, xAccessor: 'x', yAccessors: ['y'], splitSeriesAccessors: ['g'], - }, + }), xValueSums, ); expect(xValueSums).toEqual( @@ -167,15 +167,13 @@ describe('Series', () => { store, ); - const { - formattedDataSeries: { stacked }, - } = computeSeriesDomainsSelector(store.getState()); + const { formattedDataSeries } = computeSeriesDomainsSelector(store.getState()); - expect(stacked[0].dataSeries).toMatchSnapshot(); + expect(formattedDataSeries).toMatchSnapshot(); }); test('Can stack multiple dataseries', () => { const dataSeries: DataSeries[] = [ - { + MockDataSeries.default({ specId: 'spec1', yAccessor: 'y1', splitAccessors: new Map(), @@ -187,8 +185,8 @@ describe('Series', () => { { x: 3, y1: 3, mark: null, y0: null, initialY1: 3, initialY0: null, datum: undefined }, { x: 4, y1: 4, mark: null, y0: null, initialY1: 4, initialY0: null, datum: undefined }, ], - }, - { + }), + MockDataSeries.default({ specId: 'spec1', yAccessor: 'y1', splitAccessors: new Map(), @@ -200,8 +198,8 @@ describe('Series', () => { { x: 3, y1: 3, mark: null, y0: null, initialY1: 3, initialY0: null, datum: undefined }, { x: 4, y1: 4, mark: null, y0: null, initialY1: 4, initialY0: null, datum: undefined }, ], - }, - { + }), + MockDataSeries.default({ specId: 'spec1', yAccessor: 'y1', splitAccessors: new Map(), @@ -213,8 +211,8 @@ describe('Series', () => { { x: 3, y1: 3, mark: null, y0: null, initialY1: 3, initialY0: null, datum: undefined }, { x: 4, y1: 4, mark: null, y0: null, initialY1: 4, initialY0: null, datum: undefined }, ], - }, - { + }), + MockDataSeries.default({ specId: 'spec1', yAccessor: 'y1', splitAccessors: new Map(), @@ -226,7 +224,7 @@ describe('Series', () => { { x: 3, y1: 3, mark: null, y0: null, initialY1: 3, initialY0: null, datum: undefined }, { x: 4, y1: 4, mark: null, y0: null, initialY1: 4, initialY0: null, datum: undefined }, ], - }, + }), ]; const xValues = new Set([1, 2, 3, 4]); const stackedValues = formatStackedDataSeriesValues(dataSeries, xValues); @@ -251,16 +249,14 @@ describe('Series', () => { }), store, ); - const { - formattedDataSeries: { stacked }, - } = computeSeriesDomainsSelector(store.getState()); + const { formattedDataSeries } = computeSeriesDomainsSelector(store.getState()); - expect(stacked[0].dataSeries).toMatchSnapshot(); + expect(formattedDataSeries).toMatchSnapshot(); }); test('Can stack high volume of dataseries', () => { const maxArrayItems = 1000; const dataSeries: DataSeries[] = [ - { + MockDataSeries.default({ specId: 'spec1', yAccessor: 'y1', splitAccessors: new Map(), @@ -269,8 +265,8 @@ describe('Series', () => { data: new Array(maxArrayItems) .fill(0) .map((d, i) => ({ x: i, y1: i, mark: null, y0: null, initialY1: i, initialY0: null, datum: undefined })), - }, - { + }), + MockDataSeries.default({ specId: 'spec1', yAccessor: 'y1', splitAccessors: new Map(), @@ -279,7 +275,7 @@ describe('Series', () => { data: new Array(maxArrayItems) .fill(0) .map((d, i) => ({ x: i, y1: i, mark: null, y0: null, initialY1: i, initialY0: null, datum: undefined })), - }, + }), ]; const xValues = new Set(new Array(maxArrayItems).fill(0).map((d, i) => i)); const stackedValues = formatStackedDataSeriesValues(dataSeries, xValues); @@ -312,8 +308,8 @@ describe('Series', () => { store, ); - const seriesDomains = computeSeriesDomainsSelector(store.getState()); - expect(seriesDomains.formattedDataSeries.stacked[0].dataSeries).toMatchSnapshot(); + const { formattedDataSeries } = computeSeriesDomainsSelector(store.getState()); + expect(formattedDataSeries).toMatchSnapshot(); }); test('Can stack multiple dataseries with scale to extent', () => { const store = MockStore.default(); @@ -353,8 +349,8 @@ describe('Series', () => { store, ); - const seriesDomains = computeSeriesDomainsSelector(store.getState()); - expect(seriesDomains.formattedDataSeries.stacked[0].dataSeries).toMatchSnapshot(); + const { formattedDataSeries } = computeSeriesDomainsSelector(store.getState()); + expect(formattedDataSeries).toMatchSnapshot(); }); test('Can stack simple dataseries with y0', () => { const store = MockStore.default(); @@ -386,8 +382,8 @@ describe('Series', () => { store, ); - const seriesDomains = computeSeriesDomainsSelector(store.getState()); - expect(seriesDomains.formattedDataSeries.stacked[0].dataSeries).toMatchSnapshot(); + const { formattedDataSeries } = computeSeriesDomainsSelector(store.getState()); + expect(formattedDataSeries).toMatchSnapshot(); }); test('Can stack simple dataseries with scale to extent with y0', () => { const store = MockStore.default(); @@ -419,8 +415,8 @@ describe('Series', () => { store, ); - const seriesDomains = computeSeriesDomainsSelector(store.getState()); - expect(seriesDomains.formattedDataSeries.stacked[0].dataSeries).toMatchSnapshot(); + const { formattedDataSeries } = computeSeriesDomainsSelector(store.getState()); + expect(formattedDataSeries).toMatchSnapshot(); }); test('should split an array of specs into data series', () => { @@ -452,9 +448,9 @@ describe('Series', () => { hideInLegend: false, }; - const splittedDataSeries = getDataSeriesBySpecId([spec1, spec2]); - expect(splittedDataSeries.dataSeriesBySpecId.get('spec1')).toMatchSnapshot(); - expect(splittedDataSeries.dataSeriesBySpecId.get('spec2')).toMatchSnapshot(); + const { dataSeries } = getDataSeriesFromSpecs([spec1, spec2]); + expect(dataSeries.filter(({ specId }) => specId === 'spec1')).toMatchSnapshot(); + expect(dataSeries.filter(({ specId }) => specId === 'spec2')).toMatchSnapshot(); }); test('should compute data series for stacked specs', () => { const spec1: BasicSeriesSpec = { @@ -485,17 +481,11 @@ describe('Series', () => { hideInLegend: false, }; const xValues = new Set([0, 1, 2, 3]); - const splittedDataSeries = getDataSeriesBySpecId([spec1, spec2]); - const specsByGroupIds = splitSpecsByGroupId([spec1, spec2]); - - const stackedDataSeries = getFormattedDataseries( - splittedDataSeries.dataSeriesBySpecId, - xValues, - ScaleType.Linear, - [spec1, spec2], - specsByGroupIds, - ); - expect(stackedDataSeries.stacked).toMatchSnapshot(); + + const { dataSeries } = getDataSeriesFromSpecs([spec1, spec2]); + const stackedDataSeries = getFormattedDataSeries([spec1, spec2], dataSeries, xValues, ScaleType.Linear); + + expect(stackedDataSeries).toMatchSnapshot(); }); describe('#getSeriesColors', () => { @@ -565,12 +555,12 @@ describe('Series', () => { }); }); test('should only include deselectedDataSeries when splitting series if deselectedDataSeries is defined', () => { - const specId = 'splitSpec'; + const id = 'splitSpec'; const splitSpec: BasicSeriesSpec = { specType: SpecTypes.Series, chartType: ChartTypes.XYAxis, - id: specId, + id, groupId: 'group', seriesType: SeriesTypes.Line, yScaleType: ScaleType.Log, @@ -582,23 +572,23 @@ describe('Series', () => { hideInLegend: false, }; - const allSeries = getDataSeriesBySpecId([splitSpec]); - expect(allSeries.dataSeriesBySpecId.get(specId)?.length).toBe(2); + const allSeries = getDataSeriesFromSpecs([splitSpec]); + expect(allSeries.dataSeries.find(({ specId }) => specId === id)).toHaveLength(2); - const emptyDeselected = getDataSeriesBySpecId([splitSpec]); - expect(emptyDeselected.dataSeriesBySpecId.get(specId)?.length).toBe(2); + const emptyDeselected = getDataSeriesFromSpecs([splitSpec]); + expect(emptyDeselected.dataSeries.find(({ specId }) => specId === id)).toHaveLength(2); const deselectedDataSeries: XYChartSeriesIdentifier[] = [ { - specId, + specId: id, yAccessor: splitSpec.yAccessors[0], splitAccessors: new Map(), seriesKeys: [], key: 'spec{splitSpec}yAccessor{y1}splitAccessors{}', }, ]; - const subsetSplit = getDataSeriesBySpecId([splitSpec], deselectedDataSeries); - expect(subsetSplit.dataSeriesBySpecId.get(specId)?.length).toBe(1); + const subsetSplit = getDataSeriesFromSpecs([splitSpec], deselectedDataSeries); + expect(subsetSplit.dataSeries.find(({ specId }) => specId === id)).toHaveLength(1); }); test('should sort series color by series spec sort index', () => { @@ -675,26 +665,26 @@ describe('Series', () => { expect(getSortedDataSeriesColorsValuesMap(seriesCollection)).toEqual(undefinedSortedColorValues); }); test('clean datum shall parse string as number for y values', () => { - let datum = extractYandMarkFromDatum([0, 1, 2], 1, [], 2); + let datum = extractYAndMarkFromDatum([0, 1, 2], 1, [], 2); expect(datum).toBeDefined(); expect(datum?.y1).toBe(1); expect(datum?.y0).toBe(2); - datum = extractYandMarkFromDatum([0, '1', 2], 1, [], 2); + datum = extractYAndMarkFromDatum([0, '1', 2], 1, [], 2); expect(datum).toBeDefined(); expect(datum?.y1).toBe(1); expect(datum?.y0).toBe(2); - datum = extractYandMarkFromDatum([0, '1', '2'], 1, [], 2); + datum = extractYAndMarkFromDatum([0, '1', '2'], 1, [], 2); expect(datum).toBeDefined(); expect(datum?.y1).toBe(1); expect(datum?.y0).toBe(2); - datum = extractYandMarkFromDatum([0, 1, '2'], 1, [], 2); + datum = extractYAndMarkFromDatum([0, 1, '2'], 1, [], 2); expect(datum).toBeDefined(); expect(datum?.y1).toBe(1); expect(datum?.y0).toBe(2); - datum = extractYandMarkFromDatum([0, 'invalid', 'invalid'], 1, [], 2); + datum = extractYAndMarkFromDatum([0, 'invalid', 'invalid'], 1, [], 2); expect(datum).toBeDefined(); expect(datum?.y1).toBe(null); expect(datum?.y0).toBe(null); @@ -919,13 +909,13 @@ describe('Series', () => { test('Can split dataset into 2Y2G series', () => { const xAccessor: AccessorFn = (d) => d.x; const splitSeries = splitSeriesDataByAccessors( - { + MockSeriesSpec.bar({ id: 'spec1', data: TestDataset.BARCHART_2Y2G, xAccessor, yAccessors: ['y1', 'y2'], splitSeriesAccessors: ['g1', 'g2'], - }, + }), new Map(), ); expect([...splitSeries.dataSeries.values()].length).toBe(8); @@ -935,12 +925,12 @@ describe('Series', () => { test('Can split dataset with custom _all xAccessor', () => { const xAccessor: AccessorFn = () => '_all'; const splitSeries = splitSeriesDataByAccessors( - { + MockSeriesSpec.bar({ id: 'spec1', data: TestDataset.BARCHART_2Y2G, xAccessor, yAccessors: ['y1'], - }, + }), new Map(), ); expect([...splitSeries.dataSeries.values()].length).toBe(1); diff --git a/src/chart_types/xy_chart/utils/series.ts b/src/chart_types/xy_chart/utils/series.ts index d00eff0764..de2bc970d3 100644 --- a/src/chart_types/xy_chart/utils/series.ts +++ b/src/chart_types/xy_chart/utils/series.ts @@ -19,7 +19,7 @@ import { SeriesIdentifier, SeriesKey } from '../../../commons/series_id'; import { ScaleType } from '../../../scales/constants'; -import { BinAgg, Direction, XScaleType } from '../../../specs'; +import { IndexOrderSpec, BinAgg, Direction, XScaleType } from '../../../specs'; import { OrderBy } from '../../../specs/settings'; import { ColorOverrides } from '../../../state/chart_state'; import { Accessor, AccessorFn, getAccessorValue } from '../../../utils/accessor'; @@ -27,9 +27,10 @@ import { Datum, Color } from '../../../utils/commons'; import { GroupId, SpecId } from '../../../utils/ids'; import { Logger } from '../../../utils/logger'; import { ColorConfig } from '../../../utils/themes/theme'; -import { YBasicSeriesSpec } from '../domains/y_domain'; +import { groupSeriesByYGroup, isHistogramEnabled, isStackedSpec, YBasicSeriesSpec } from '../domains/y_domain'; import { LastValues } from '../state/utils/types'; import { applyFitFunctionToDataSeries } from './fit_function_utils'; +import { groupBy } from './group_data_series'; import { BasicSeriesSpec, SeriesTypes, SeriesSpecs, SeriesNameConfigOptions, StackMode } from './specs'; import { formatStackedDataSeriesValues, datumXSortPredicate } from './stacked_series_utils'; @@ -69,12 +70,18 @@ export interface DataSeriesDatum { export interface XYChartSeriesIdentifier extends SeriesIdentifier { yAccessor: string | number; splitAccessors: Map; // does the map have a size vs making it optional + smVerticalAccessorValue?: string | number; + smHorizontalAccessorValue?: string | number; seriesKeys: (string | number)[]; } /** @internal */ export type DataSeries = XYChartSeriesIdentifier & { + groupId: GroupId; + seriesType: SeriesTypes; data: DataSeriesDatum[]; + stackMode: StackMode | undefined; + spec: Exclude; }; /** @internal */ @@ -113,27 +120,34 @@ export function getSeriesIndex(series: SeriesIdentifier[], target: SeriesIdentif * @internal */ export function splitSeriesDataByAccessors( - { + spec: BasicSeriesSpec, + xValueSums: Map, + enableVislibSeriesSort = false, + stackMode?: StackMode, + smallMultiples?: { verticalIndex?: IndexOrderSpec; horizontalIndex?: IndexOrderSpec }, +): { + dataSeries: Map; + xValues: Array; + smVValues: Set; + smHValues: Set; +} { + const { + seriesType, id: specId, + groupId, data, xAccessor, yAccessors, y0Accessors, markSizeAccessor, splitSeriesAccessors = [], - }: Pick< - BasicSeriesSpec, - 'id' | 'data' | 'xAccessor' | 'yAccessors' | 'y0Accessors' | 'splitSeriesAccessors' | 'markSizeAccessor' - >, - xValueSums: Map, - enableVislibSeriesSort = false, -): { - dataSeries: Map; - xValues: Array; -} { + } = spec; const dataSeries = new Map(); const xValues: Array = []; + const smVValues: Set = new Set(); + const smHValues: Set = new Set(); const nonNumericValues: any[] = []; + const accessors = [...splitSeriesAccessors]; if (enableVislibSeriesSort) { /* @@ -142,29 +156,44 @@ export function splitSeriesDataByAccessors( * The difference from below is that it loops through all the yAsccessors before the data. */ yAccessors.forEach((accessor, index) => { - data.forEach((datum) => { - const splitAccessors = getSplitAccessors(datum, splitSeriesAccessors); + for (let i = 0; i < data.length; i++) { + const datum = data[i]; + const splitAccessors = getSplitAccessors(datum, accessors); // if splitSeriesAccessors are defined we should have at least one split value to include datum - if (splitSeriesAccessors.length > 0 && splitAccessors.size < 1) { - return; + if (accessors.length > 0 && splitAccessors.size < 1) { + continue; } // skip if the datum is not an object or null if (typeof datum !== 'object' || datum === null) { - return; + continue; } - const x = getAccessorValue(datum, xAccessor); // skip if the x value is not a string or a number if (typeof x !== 'string' && typeof x !== 'number') { - return; + continue; } xValues.push(x); let sum = xValueSums.get(x) ?? 0; - const cleanedDatum = extractYandMarkFromDatum( + // extract small multiples aggregation values + const smH = smallMultiples?.horizontalIndex?.by + ? smallMultiples.horizontalIndex?.by(spec, datum).join('___') + : undefined; + if (smH !== undefined) { + smHValues.add(smH); + } + + const smV = smallMultiples?.verticalIndex?.by + ? smallMultiples.verticalIndex.by(spec, datum).join('___') + : undefined; + if (smV) { + smVValues.add(smV); + } + + const cleanedDatum = extractYAndMarkFromDatum( datum, accessor, nonNumericValues, @@ -172,54 +201,72 @@ export function splitSeriesDataByAccessors( markSizeAccessor, ); const seriesKeys = [...splitAccessors.values(), accessor]; - const seriesKey = getSeriesKey({ + const seriesIdentifier = { specId, + groupId, + seriesType, yAccessor: accessor, splitAccessors, - }); + smVerticalAccessorValue: smV, + smHorizontalAccessorValue: smH, + stackMode, + }; + const seriesKey = getSeriesKey(seriesIdentifier, groupId); sum += cleanedDatum.y1 ?? 0; - const newDatum = { x, ...cleanedDatum }; + const newDatum = { x, ...cleanedDatum, smH, smV }; const series = dataSeries.get(seriesKey); if (series) { series.data.push(newDatum); } else { dataSeries.set(seriesKey, { - specId, - yAccessor: accessor, - splitAccessors, - data: [newDatum], - key: seriesKey, + ...seriesIdentifier, seriesKeys, + key: seriesKey, + data: [newDatum], + spec, }); } xValueSums.set(x, sum); - }); + } }); } else { - data.forEach((datum) => { - const splitAccessors = getSplitAccessors(datum, splitSeriesAccessors); + for (let i = 0; i < data.length; i++) { + const datum = data[i]; + const splitAccessors = getSplitAccessors(datum, accessors); // if splitSeriesAccessors are defined we should have at least one split value to include datum - if (splitSeriesAccessors.length > 0 && splitAccessors.size < 1) { - return; + if (accessors.length > 0 && splitAccessors.size < 1) { + continue; } // skip if the datum is not an object or null if (typeof datum !== 'object' || datum === null) { - return; + continue; } - const x = getAccessorValue(datum, xAccessor); - // skip if the x value is not a string or a number if (typeof x !== 'string' && typeof x !== 'number') { - return; + continue; } xValues.push(x); let sum = xValueSums.get(x) ?? 0; + // extract small multiples aggregation values + const smH = smallMultiples?.horizontalIndex?.by + ? smallMultiples.horizontalIndex?.by(spec, datum).join('___') + : undefined; + if (smH !== undefined) { + smHValues.add(smH); + } + + const smV = smallMultiples?.verticalIndex?.by + ? smallMultiples.verticalIndex.by(spec, datum).join('___') + : undefined; + if (smV) { + smVValues.add(smV); + } yAccessors.forEach((accessor, index) => { - const cleanedDatum = extractYandMarkFromDatum( + const cleanedDatum = extractYAndMarkFromDatum( datum, accessor, nonNumericValues, @@ -227,29 +274,35 @@ export function splitSeriesDataByAccessors( markSizeAccessor, ); const seriesKeys = [...splitAccessors.values(), accessor]; - const seriesKey = getSeriesKey({ + const seriesIdentifier = { specId, + groupId, + seriesType, yAccessor: accessor, splitAccessors, - }); + smVerticalAccessorValue: smV, + smHorizontalAccessorValue: smH, + stackMode, + }; + const seriesKey = getSeriesKey(seriesIdentifier, groupId); sum += cleanedDatum.y1 ?? 0; - const newDatum = { x, ...cleanedDatum }; + const newDatum = { x, ...cleanedDatum, smH, smV }; const series = dataSeries.get(seriesKey); if (series) { series.data.push(newDatum); } else { dataSeries.set(seriesKey, { - specId, - yAccessor: accessor, - splitAccessors, - data: [newDatum], - key: seriesKey, + ...seriesIdentifier, seriesKeys, + key: seriesKey, + data: [newDatum], + spec, }); } + + xValueSums.set(x, sum); }); - xValueSums.set(x, sum); - }); + } } if (nonNumericValues.length > 0) { @@ -258,10 +311,11 @@ export function splitSeriesDataByAccessors( `(${nonNumericValues.map((v) => JSON.stringify(v)).join(', ')})`, ); } - return { dataSeries, xValues, + smVValues, + smHValues, }; } @@ -269,16 +323,26 @@ export function splitSeriesDataByAccessors( * Gets global series key to id any series as a string * @internal */ -export function getSeriesKey({ - specId, - yAccessor, - splitAccessors, -}: Pick): string { +export function getSeriesKey( + { + specId, + yAccessor, + splitAccessors, + smVerticalAccessorValue, + smHorizontalAccessorValue, + }: Pick< + XYChartSeriesIdentifier, + 'specId' | 'yAccessor' | 'splitAccessors' | 'smVerticalAccessorValue' | 'smHorizontalAccessorValue' + >, + groupId: GroupId, +): string { const joinedAccessors = [...splitAccessors.entries()] .sort(([a], [b]) => (a > b ? 1 : -1)) .map(([key, value]) => `${key}-${value}`) .join('|'); - return `spec{${specId}}yAccessor{${yAccessor}}splitAccessors{${joinedAccessors}}`; + const smV = smVerticalAccessorValue ? `smV{${smVerticalAccessorValue}}` : ''; + const smH = smHorizontalAccessorValue ? `smH{${smHorizontalAccessorValue}}` : ''; + return `groupId{${groupId}}spec{${specId}}yAccessor{${yAccessor}}splitAccessors{${joinedAccessors}}${smV}${smH}`; } /** @@ -302,7 +366,7 @@ function getSplitAccessors(datum: Datum, accessors: Accessor[] = []): Map, +export function getFormattedDataSeries( + seriesSpecs: SeriesSpecs, + availableDataSeries: DataSeries[], xValues: Set, xScaleType: ScaleType, - seriesSpecs: SeriesSpecs, - specsByGroupIdsEntries: Map< - GroupId, - { - stackMode: StackMode | undefined; - stacked: YBasicSeriesSpec[]; - nonStacked: YBasicSeriesSpec[]; - } - >, -): { - stacked: FormattedDataSeries[]; - nonStacked: FormattedDataSeries[]; -} { - const stackedFormattedDataSeries: { - groupId: GroupId; - dataSeries: DataSeries[]; - counts: DataSeriesCounts; - stackMode?: StackMode; - }[] = []; - const nonStackedFormattedDataSeries: { - groupId: GroupId; - dataSeries: DataSeries[]; - counts: DataSeriesCounts; - }[] = []; - - [...specsByGroupIdsEntries.entries()].forEach(([groupId, groupSpecs]) => { - const { stackMode } = groupSpecs; - // format stacked data series - const stackedDataSeries = getDataSeriesBySpecGroup(groupSpecs.stacked, availableDataSeries); - const fittedStackedDataSeries = applyFitFunctionToDataSeries( - getSortedDataSeries(stackedDataSeries.dataSeries, xValues, xScaleType), - seriesSpecs, - xScaleType, - ); - const fittedAndStackedDataSeries = formatStackedDataSeriesValues(fittedStackedDataSeries, xValues, stackMode); +): DataSeries[] { + const histogramEnabled = isHistogramEnabled(seriesSpecs); + + // apply fit function to every data series + const fittedDataSeries = applyFitFunctionToDataSeries( + getSortedDataSeries(availableDataSeries, xValues, xScaleType), + seriesSpecs, + xScaleType, + ); - stackedFormattedDataSeries.push({ - groupId, - counts: stackedDataSeries.counts, - dataSeries: fittedAndStackedDataSeries, - stackMode, - }); + // apply fitting for stacked DataSeries by YGroup, Panel + const stackedDataSeries = fittedDataSeries.filter(({ spec }) => isStackedSpec(spec, histogramEnabled)); + const stackedGroups = groupBy( + stackedDataSeries, + ['smHorizontalAccessorValue', 'smVerticalAccessorValue', 'groupId'], + true, + ); - // format non stacked data series - const nonStackedDataSeries = getDataSeriesBySpecGroup(groupSpecs.nonStacked, availableDataSeries); - const fittedNonStackedDataSeries = applyFitFunctionToDataSeries( - getSortedDataSeries(nonStackedDataSeries.dataSeries, xValues, xScaleType), - seriesSpecs, - xScaleType, - ); - nonStackedFormattedDataSeries.push({ - groupId, - counts: nonStackedDataSeries.counts, - dataSeries: fittedNonStackedDataSeries, - }); - }); - return { - stacked: stackedFormattedDataSeries.filter((ds) => ds.dataSeries.length > 0), - nonStacked: nonStackedFormattedDataSeries.filter((ds) => ds.dataSeries.length > 0), - }; + const fittedAndStackedDataSeries = stackedGroups.reduce((acc, dataSeries) => { + const [{ stackMode }] = dataSeries; + const formatted = formatStackedDataSeriesValues(dataSeries, xValues, stackMode); + return [...acc, ...formatted]; + }, []); + // get already fitted non stacked dataSeries + const nonStackedDataSeries = fittedDataSeries.filter(({ spec }) => !isStackedSpec(spec, histogramEnabled)); + + return [...fittedAndStackedDataSeries, ...nonStackedDataSeries]; } function getDataSeriesBySpecGroup( @@ -428,9 +460,27 @@ function getDataSeriesBySpecGroup( if (!ds) { return acc; } - acc.dataSeries.push(...ds); - acc.counts[seriesType] += ds.length; + if (seriesType === SeriesTypes.Bar) { + // for bar series, count the max number of bars per panel + const barCounts = ds.reduce>((countAcc, dsCurrent) => { + const key = `${dsCurrent?.smHorizontalAccessorValue ?? 'global'}___${dsCurrent?.smVerticalAccessorValue ?? + 'global'}`; + let count = countAcc[key]; + if (count === undefined) { + count = 0; + } + count++; + return { + ...countAcc, + [key]: count, + }; + }, {}); + const maxBarCounts = Math.max(...Object.values(barCounts)); + acc.counts[seriesType] += maxBarCounts; + } else { + acc.counts[seriesType] += ds.length; + } return acc; }, { @@ -449,29 +499,43 @@ function getDataSeriesBySpecGroup( * * @param seriesSpecs the map for all the series spec * @param deselectedDataSeries the array of deselected/hidden data series +<<<<<<< HEAD * @param enableVislibSeriesSort is optional; if not specified in , +======= + * @param smallMultiples +>>>>>>> feat: small-multiples for xy axis chart * @internal */ -export function getDataSeriesBySpecId( +export function getDataSeriesFromSpecs( seriesSpecs: BasicSeriesSpec[], deselectedDataSeries: SeriesIdentifier[] = [], orderOrdinalBinsBy?: OrderBy, enableVislibSeriesSort?: boolean, + smallMultiples?: { verticalIndex?: IndexOrderSpec; horizontalIndex?: IndexOrderSpec }, ): { - dataSeriesBySpecId: Map; + dataSeries: DataSeries[]; seriesCollection: Map; xValues: Set; + smVValues: Set; + smHValues: Set; fallbackScale?: XScaleType; } { - const dataSeriesBySpecId = new Map(); + let globalDataSeries: DataSeries[] = []; const seriesCollection = new Map(); const mutatedXValueSums = new Map(); // the unique set of values along the x axis const globalXValues: Set = new Set(); + // the unique set of values along for the vertical small multiple grid + let globalSMVValues: Set = new Set(); + // the unique set of values along for the horizontal small multiple grid + let globalSMHValues: Set = new Set(); + let isNumberArray = true; let isOrdinalScale = false; + + const specsByYGroup = groupSeriesByYGroup(seriesSpecs); // eslint-disable-next-line no-restricted-syntax for (const spec of seriesSpecs) { // check scale type and cast to Ordinal if we found at least one series @@ -480,9 +544,16 @@ export function getDataSeriesBySpecId( isOrdinalScale = true; } - const { dataSeries, xValues } = splitSeriesDataByAccessors(spec, mutatedXValueSums, enableVislibSeriesSort); + const specGroup = specsByYGroup.get(spec.groupId); + const { dataSeries, xValues, smVValues, smHValues } = splitSeriesDataByAccessors( + spec, + mutatedXValueSums, + enableVislibSeriesSort, + specGroup?.stackMode, + smallMultiples, + ); - // filter deleselected dataseries + // filter deselected DataSeries let filteredDataSeries: DataSeries[] = [...dataSeries.values()]; if (deselectedDataSeries.length > 0) { filteredDataSeries = filteredDataSeries.filter( @@ -490,7 +561,7 @@ export function getDataSeriesBySpecId( ); } - dataSeriesBySpecId.set(spec.id, filteredDataSeries); + globalDataSeries = [...globalDataSeries, ...filteredDataSeries]; const banded = spec.y0Accessors && spec.y0Accessors.length > 0; @@ -513,6 +584,8 @@ export function getDataSeriesBySpecId( } globalXValues.add(xValue); } + globalSMVValues = new Set([...globalSMVValues, ...smVValues]); + globalSMHValues = new Set([...globalSMHValues, ...smHValues]); } const xValues = @@ -528,9 +601,12 @@ export function getDataSeriesBySpecId( ); return { - dataSeriesBySpecId, + dataSeries: globalDataSeries, seriesCollection, + // keep the user order for ordinal scales xValues, + smVValues: globalSMVValues, + smHValues: globalSMHValues, fallbackScale: !isOrdinalScale && !isNumberArray ? ScaleType.Ordinal : undefined, }; } diff --git a/src/chart_types/xy_chart/utils/specs.ts b/src/chart_types/xy_chart/utils/specs.ts index bbedb5e024..7e3989d66a 100644 --- a/src/chart_types/xy_chart/utils/specs.ts +++ b/src/chart_types/xy_chart/utils/specs.ts @@ -458,7 +458,16 @@ export interface SeriesScales { yScaleToDataExtent?: boolean; } +export interface SmallMultiplesAccessors { + smallMultiple?: { + horizontalAccessor?: Accessor; + verticalAccessor?: Accessor; + shouldWrap?: boolean; + }; +} + /** @public */ + export type BasicSeriesSpec = SeriesSpec & SeriesAccessors & SeriesScales & { @@ -468,7 +477,7 @@ export type BasicSeriesSpec = SeriesSpec & * Only used with line/area series */ markFormat?: TickFormatter; - }; + } & SmallMultiplesAccessors; export type SeriesSpecs = Array; diff --git a/src/chart_types/xy_chart/utils/stacked_percent_series_utils.test.ts b/src/chart_types/xy_chart/utils/stacked_percent_series_utils.test.ts index e9b314ad96..aa8951d88f 100644 --- a/src/chart_types/xy_chart/utils/stacked_percent_series_utils.test.ts +++ b/src/chart_types/xy_chart/utils/stacked_percent_series_utils.test.ts @@ -70,18 +70,18 @@ describe('Stacked Series Utils', () => { store, ); const { formattedDataSeries } = computeSeriesDomainsSelector(store.getState()); - const { stacked } = formattedDataSeries; - const [data0] = stacked[0].dataSeries[0].data; + + const [data0] = formattedDataSeries[0].data; expect(data0.initialY1).toBe(10); expect(data0.y0).toBe(0); expect(data0.y1).toBe(0.1); - const [data1] = stacked[0].dataSeries[1].data; + const [data1] = formattedDataSeries[1].data; expect(data1.initialY1).toBe(20); expect(data1.y0).toBe(0.1); expect(data1.y1).toBeCloseTo(0.3); - const [data2] = stacked[0].dataSeries[2].data; + const [data2] = formattedDataSeries[2].data; expect(data2.initialY1).toBe(70); expect(data2.y0).toBeCloseTo(0.3); expect(data2.y1).toBe(1); @@ -100,16 +100,14 @@ describe('Stacked Series Utils', () => { ], store, ); - const { - formattedDataSeries: { stacked }, - } = computeSeriesDomainsSelector(store.getState()); + const { formattedDataSeries } = computeSeriesDomainsSelector(store.getState()); - const [data0] = stacked[0].dataSeries[0].data; + const [data0] = formattedDataSeries[0].data; expect(data0.initialY1).toBe(10); expect(data0.y0).toBe(0); expect(data0.y1).toBe(0.25); - expect(stacked[0].dataSeries[1].data[0]).toMatchObject({ + expect(formattedDataSeries[1].data[0]).toMatchObject({ initialY0: null, initialY1: null, x: 0, @@ -118,7 +116,7 @@ describe('Stacked Series Utils', () => { mark: null, }); - const [data2] = stacked[0].dataSeries[2].data; + const [data2] = formattedDataSeries[2].data; expect(data2.initialY1).toBe(30); expect(data2.y0).toBe(0.25); expect(data2.y1).toBe(1); @@ -138,23 +136,21 @@ describe('Stacked Series Utils', () => { ], store, ); - const { - formattedDataSeries: { stacked }, - } = computeSeriesDomainsSelector(store.getState()); + const { formattedDataSeries } = computeSeriesDomainsSelector(store.getState()); - const [data0] = stacked[0].dataSeries[0].data; + const [data0] = formattedDataSeries[0].data; expect(data0.initialY0).toBe(2); expect(data0.initialY1).toBe(10); expect(data0.y0).toBe(0.02); expect(data0.y1).toBe(0.1); - const [data1] = stacked[0].dataSeries[1].data; + const [data1] = formattedDataSeries[1].data; expect(data1.initialY0).toBe(4); expect(data1.initialY1).toBe(20); expect(data1.y0).toBe(0.14); expect(data1.y1).toBeCloseTo(0.3, 5); - const [data2] = stacked[0].dataSeries[2].data; + const [data2] = formattedDataSeries[2].data; expect(data2.initialY0).toBe(6); expect(data2.initialY1).toBe(70); expect(data2.y0).toBeCloseTo(0.36); @@ -175,23 +171,21 @@ describe('Stacked Series Utils', () => { ], store, ); - const { - formattedDataSeries: { stacked }, - } = computeSeriesDomainsSelector(store.getState()); + const { formattedDataSeries } = computeSeriesDomainsSelector(store.getState()); - const [data0] = stacked[0].dataSeries[0].data; + const [data0] = formattedDataSeries[0].data; expect(data0.initialY0).toBe(2); expect(data0.initialY1).toBe(10); expect(data0.y0).toBe(0.02); expect(data0.y1).toBe(0.1); - const [data1] = stacked[0].dataSeries[1].data; + const [data1] = formattedDataSeries[1].data; expect(data1.initialY0).toBe(null); expect(data1.initialY1).toBe(null); expect(data1.y0).toBe(0.1); expect(data1.y1).toBe(0.1); - const [data2] = stacked[0].dataSeries[2].data; + const [data2] = formattedDataSeries[2].data; expect(data2.initialY0).toBe(6); expect(data2.initialY1).toBe(90); expect(data2.y0).toBe(0.16); @@ -213,14 +207,12 @@ describe('Stacked Series Utils', () => { ], store, ); - const { - formattedDataSeries: { stacked }, - } = computeSeriesDomainsSelector(store.getState()); + const { formattedDataSeries } = computeSeriesDomainsSelector(store.getState()); - expect(stacked[0].dataSeries.length).toBe(2); - expect(stacked[0].dataSeries[0].data.length).toBe(4); - expect(stacked[0].dataSeries[1].data.length).toBe(4); - expect(stacked[0].dataSeries[0].data[0]).toMatchObject({ + expect(formattedDataSeries).toHaveLength(2); + expect(formattedDataSeries[0].data).toHaveLength(4); + expect(formattedDataSeries[1].data).toHaveLength(4); + expect(formattedDataSeries[0].data[0]).toMatchObject({ initialY0: null, initialY1: 10, x: 1, @@ -228,7 +220,7 @@ describe('Stacked Series Utils', () => { y1: 0.1, mark: null, }); - expect(stacked[0].dataSeries[0].data[1]).toMatchObject({ + expect(formattedDataSeries[0].data[1]).toMatchObject({ initialY0: null, initialY1: 20, x: 2, @@ -236,7 +228,7 @@ describe('Stacked Series Utils', () => { y1: 1, mark: null, }); - expect(stacked[0].dataSeries[0].data[3]).toMatchObject({ + expect(formattedDataSeries[0].data[3]).toMatchObject({ initialY0: null, initialY1: 40, x: 4, @@ -244,7 +236,7 @@ describe('Stacked Series Utils', () => { y1: 1, mark: null, }); - expect(stacked[0].dataSeries[1].data[0]).toMatchObject({ + expect(formattedDataSeries[1].data[0]).toMatchObject({ initialY0: null, initialY1: 90, x: 1, @@ -252,7 +244,7 @@ describe('Stacked Series Utils', () => { y1: 1, mark: null, }); - expect(stacked[0].dataSeries[1].data[1]).toMatchObject({ + expect(formattedDataSeries[1].data[1]).toMatchObject({ initialY0: null, initialY1: null, x: 2, @@ -263,7 +255,7 @@ describe('Stacked Series Utils', () => { x: 2, }, }); - expect(stacked[0].dataSeries[1].data[2]).toMatchObject({ + expect(formattedDataSeries[1].data[2]).toMatchObject({ initialY0: null, initialY1: 30, x: 3, diff --git a/src/mocks/annotations/annotations.ts b/src/mocks/annotations/annotations.ts new file mode 100644 index 0000000000..bcdc19ac17 --- /dev/null +++ b/src/mocks/annotations/annotations.ts @@ -0,0 +1,51 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { AnnotationLineProps } from '../../chart_types/xy_chart/annotations/line/types'; +import { mergePartial, RecursivePartial } from '../../utils/commons'; + +/** @internal */ +export class MockAnnotationLineProps { + private static readonly base: AnnotationLineProps = { + linePathPoints: { + start: { x1: 0, y1: 0 }, + end: { x2: 0, y2: 0 }, + transform: { + x: 0, + y: 0, + }, + }, + details: {}, + }; + + static default(partial?: RecursivePartial) { + return mergePartial(MockAnnotationLineProps.base, partial, { + mergeOptionalPartialValues: true, + }); + } + + static fromPoints(x1 = 0, y1 = 0, x2 = 0, y2 = 0): AnnotationLineProps { + return MockAnnotationLineProps.default({ + linePathPoints: { + start: { x1, y1 }, + end: { x2, y2 }, + }, + }); + } +} diff --git a/src/mocks/geometries.ts b/src/mocks/geometries.ts index 27393e87a4..7f2e37ab16 100644 --- a/src/mocks/geometries.ts +++ b/src/mocks/geometries.ts @@ -85,6 +85,10 @@ export class MockBarGeometry { datum: { x: 0, y: 0 }, }, seriesStyle: barSeriesStyle, + transform: { + x: 0, + y: 0, + }, }; static default(partial?: RecursivePartial) { diff --git a/src/mocks/series/series.ts b/src/mocks/series/series.ts index 3351b38ea3..6f8c072d88 100644 --- a/src/mocks/series/series.ts +++ b/src/mocks/series/series.ts @@ -26,8 +26,9 @@ import { XYChartSeriesIdentifier, FormattedDataSeries, } from '../../chart_types/xy_chart/utils/series'; -import { DEFAULT_GLOBAL_ID } from '../../specs'; +import { DEFAULT_GLOBAL_ID, SeriesTypes } from '../../specs'; import { mergePartial } from '../../utils/commons'; +import { MockSeriesSpec } from '../specs'; import { getRandomNumberGenerator } from '../utils'; import { fitFunctionData } from './data'; @@ -49,6 +50,10 @@ export class MockDataSeries { splitAccessors: new Map(), key: 'spec1', data: [], + groupId: 'group1', + seriesType: SeriesTypes.Bar, + stackMode: undefined, + spec: MockSeriesSpec.bar(), }; static default(partial?: Partial) { diff --git a/src/mocks/series/series_identifiers.ts b/src/mocks/series/series_identifiers.ts index f92688fd78..89db922b8d 100644 --- a/src/mocks/series/series_identifiers.ts +++ b/src/mocks/series/series_identifiers.ts @@ -19,7 +19,7 @@ import { SeriesCollectionValue, - getDataSeriesBySpecId, + getDataSeriesFromSpecs, XYChartSeriesIdentifier, } from '../../chart_types/xy_chart/utils/series'; import { BasicSeriesSpec } from '../../specs'; @@ -34,7 +34,7 @@ export class MockSeriesCollection { } static fromSpecs(seriesSpecs: BasicSeriesSpec[]) { - const { seriesCollection } = getDataSeriesBySpecId(seriesSpecs, []); + const { seriesCollection } = getDataSeriesFromSpecs(seriesSpecs, []); return seriesCollection; } @@ -57,7 +57,7 @@ export class MockSeriesIdentifier { } static fromSpecs(specs: BasicSeriesSpec[]): XYChartSeriesIdentifier[] { - const { seriesCollection } = getDataSeriesBySpecId(specs); + const { seriesCollection } = getDataSeriesFromSpecs(specs); return [...seriesCollection.values()].map(({ seriesIdentifier }) => seriesIdentifier); } diff --git a/src/specs/constants.ts b/src/specs/constants.ts index e4b691647c..c5e2333f6a 100644 --- a/src/specs/constants.ts +++ b/src/specs/constants.ts @@ -29,6 +29,8 @@ export const SpecTypes = Object.freeze({ Axis: 'axis' as const, Annotation: 'annotation' as const, Settings: 'settings' as const, + IndexOrder: 'index_order' as const, + SmallMultiples: 'small_multiples' as const, }); /** @public */ export type SpecTypes = $Values; diff --git a/src/specs/index.ts b/src/specs/index.ts index 057f1c7e5d..57aaa02457 100644 --- a/src/specs/index.ts +++ b/src/specs/index.ts @@ -28,6 +28,9 @@ export interface Spec { specType: string; } +export * from './index_order'; +export * from './small_multiples'; + export * from './settings'; export * from './constants'; export * from '../chart_types/specs'; diff --git a/src/specs/index_order.ts b/src/specs/index_order.ts new file mode 100644 index 0000000000..02d5adf638 --- /dev/null +++ b/src/specs/index_order.ts @@ -0,0 +1,44 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import React from 'react'; + +import { Spec } from '.'; +import { ChartTypes } from '../chart_types'; +import { getConnect, specComponentFactory } from '../state/spec_factory'; +import { SpecTypes } from './constants'; + +export type IndexOrderBy = (spec: Spec, datum: any) => Array; +export type IndexOrderSort = Array; + +export interface IndexOrderSpec extends Spec { + by: IndexOrderBy; + order: IndexOrderSort; +} +const DEFAULT_INDEX_ORDER_PROPS = { + chartType: ChartTypes.Global, + specType: SpecTypes.IndexOrder, +}; + +type DefaultIndexOrderProps = 'chartType' | 'specType'; + +export type IndexOrderProps = Pick; + +export const IndexOrder: React.FunctionComponent = getConnect()( + specComponentFactory(DEFAULT_INDEX_ORDER_PROPS), +); diff --git a/src/specs/small_multiples.ts b/src/specs/small_multiples.ts new file mode 100644 index 0000000000..81fe09c6bf --- /dev/null +++ b/src/specs/small_multiples.ts @@ -0,0 +1,42 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import React from 'react'; + +import { Spec } from '.'; +import { ChartTypes } from '../chart_types'; +import { getConnect, specComponentFactory } from '../state/spec_factory'; +import { SpecTypes } from './constants'; + +export interface SmallMultiplesSpec extends Spec { + verticalIndex?: string; + horizontalIndex?: string; +} +const DEFAULT_SMALL_MULTIPLES_PROPS = { + id: '__global__small_multiples___', + chartType: ChartTypes.Global, + specType: SpecTypes.SmallMultiples, +}; + +type DefaultIndexOrderProps = 'id' | 'chartType' | 'specType'; + +export type SmallMultiplesProps = Partial>; + +export const SmallMultiples: React.FunctionComponent = getConnect()( + specComponentFactory(DEFAULT_SMALL_MULTIPLES_PROPS), +); diff --git a/src/utils/geometry.ts b/src/utils/geometry.ts index 5e6ad42c3e..1890a5dfd9 100644 --- a/src/utils/geometry.ts +++ b/src/utils/geometry.ts @@ -71,6 +71,10 @@ export interface BarGeometry { y: number; width: number; height: number; + transform: { + x: number; + y: number; + }; color: Color; displayValue?: { text: any; diff --git a/stories/axes/8_custom_domain.tsx b/stories/axes/8_custom_domain.tsx index c254698e60..3fe1134241 100644 --- a/stories/axes/8_custom_domain.tsx +++ b/stories/axes/8_custom_domain.tsx @@ -91,7 +91,6 @@ export const Example = () => { xAccessor="x" yAccessors={['y']} stackAccessors={['x']} - splitSeriesAccessors={['g']} data={[ { x: 0, y: 3 }, { x: 1, y: 2 }, diff --git a/yarn.lock b/yarn.lock index 22d16a38a7..cb3cd2340b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5519,10 +5519,10 @@ resolved "https://registry.yarnpkg.com/@types/core-js/-/core-js-2.5.2.tgz#d4c25420044d4a5b65e00a82fc04b7824b62691f" integrity sha512-+NPqjXgyA02xTHKJDeDca9u8Zr42ts6jhdND4C3PrPeQ35RJa0dmfAedXW7a9K4N1QcBbuWI1nSfGK4r1eVFCQ== -"@types/d3-array@^1.2.6": - version "1.2.7" - resolved "https://registry.yarnpkg.com/@types/d3-array/-/d3-array-1.2.7.tgz#34dc654d34fc058c41c31dbca1ed68071a8fcc17" - integrity sha512-51vHWuUyDOi+8XuwPrTw3cFqyh2Slg9y8COYkRfjCPG9TfYqY0hoNPzv/8BrcAy0FeQBzqEo/D/8Nk2caOQJnA== +"@types/d3-array@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@types/d3-array/-/d3-array-2.0.0.tgz#a0d63a296a2d8435a9ec59393dcac746c6174a96" + integrity sha512-rGqfPVowNDTszSFvwoZIXvrPG7s/qKzm9piCRIH6xwTTRu7pPZ3ootULFnPkTt74B6i5lN0FpLQL24qGOw1uZA== "@types/d3-collection@^1.0.8": version "1.0.8" @@ -9542,11 +9542,16 @@ cz-conventional-changelog@^3.3.0: optionalDependencies: "@commitlint/load" ">6.1.1" -d3-array@^1.2.0, d3-array@^1.2.4: +d3-array@^1.2.0: version "1.2.4" resolved "https://registry.yarnpkg.com/d3-array/-/d3-array-1.2.4.tgz#635ce4d5eea759f6f605863dbcfc30edc737f71f" integrity sha512-KHW6M86R+FUPYGb3R5XiYjXPq7VzwxZ22buHhAEVG5ztoEcZZMLov530mmccaqA1GghZArjQV46fuc8kUqhhHw== +d3-array@^2.5.1: + version "2.5.1" + resolved "https://registry.yarnpkg.com/d3-array/-/d3-array-2.5.1.tgz#cc785e1c4b560a34b8c77af9e6709bdf3f2ee117" + integrity sha512-cKvAlQZUKhXInw5mosJMtAYsY3dDYwTess/WOFUQTGcr8xV04SZMJs6n6QznsqZC5vJTkvZuCgsH9fo981ysPA== + d3-collection@1, d3-collection@^1.0.7: version "1.0.7" resolved "https://registry.yarnpkg.com/d3-collection/-/d3-collection-1.0.7.tgz#349bd2aa9977db071091c13144d5e4f16b5b310e"