From 7b7018650b8f453fd139cf43199bc9056e116614 Mon Sep 17 00:00:00 2001 From: Robert Monfera Date: Wed, 16 Dec 2020 19:52:23 +0100 Subject: [PATCH] fix(legend): hierarchical legend order should follow the tree paths (#947) refactor: saving the tree path information in the tree nodes; legend item sorting based on the path; code simplifications; minor perf improvements fix #944 --- packages/osd-charts/.eslintrc.js | 3 +- .../osd-charts/.playground/playground.tsx | 189 ++++----------- packages/osd-charts/api/charts.api.md | 4 +- ...d-labels-visually-looks-correct-1-snap.png | Bin 0 -> 25143 bytes ...en-if-there-are-repeated-labels-1-snap.png | Bin 0 -> 25163 bytes .../integration/tests/legend_stories.test.ts | 5 + .../layout/types/viewmodel_types.ts | 5 +- .../layout/utils/group_by_rollup.ts | 23 +- .../layout/viewmodel/viewmodel.ts | 2 + .../partition_chart/partition.test.tsx | 220 ++++++++++++++++++ .../partition_chart/state/chart_state.tsx | 1 + .../partition_chart/state/iterables.ts | 27 +++ .../state/selectors/compute_legend.ts | 79 +++---- .../state/selectors/get_flat_hierarchy.ts | 46 ---- .../state/selectors/get_highlighted_shapes.ts | 1 + .../selectors/get_legend_items_labels.ts | 29 +-- packages/osd-charts/src/specs/constants.ts | 1 + packages/osd-charts/src/specs/settings.tsx | 11 +- packages/osd-charts/src/state/chart_state.ts | 2 +- .../selectors/get_legend_items_labels.ts | 8 +- .../legend/10_sunburst_repeated_label.tsx | 69 ++++++ .../stories/legend/legend.stories.tsx | 1 + 22 files changed, 450 insertions(+), 276 deletions(-) create mode 100644 packages/osd-charts/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-legend-piechart-repeated-labels-visually-looks-correct-1-snap.png create mode 100644 packages/osd-charts/integration/tests/__image_snapshots__/legend-stories-test-ts-legend-stories-should-have-the-same-order-as-nested-with-no-indent-even-if-there-are-repeated-labels-1-snap.png create mode 100644 packages/osd-charts/src/chart_types/partition_chart/partition.test.tsx create mode 100644 packages/osd-charts/src/chart_types/partition_chart/state/iterables.ts delete mode 100644 packages/osd-charts/src/chart_types/partition_chart/state/selectors/get_flat_hierarchy.ts create mode 100644 packages/osd-charts/stories/legend/10_sunburst_repeated_label.tsx diff --git a/packages/osd-charts/.eslintrc.js b/packages/osd-charts/.eslintrc.js index 4e1c82e3c949..82e4eeeefac6 100644 --- a/packages/osd-charts/.eslintrc.js +++ b/packages/osd-charts/.eslintrc.js @@ -32,7 +32,7 @@ module.exports = { ], rules: { /** - * depricated to be deleted + * deprecated to be deleted */ // https://github.com/typescript-eslint/typescript-eslint/issues/2077 '@typescript-eslint/camelcase': 0, @@ -92,6 +92,7 @@ module.exports = { /** * Standard rules */ + 'no-restricted-syntax': 0, // this is a good rule, for-of is good 'no-console': process.env.NODE_ENV === 'production' ? 2 : 1, 'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 1, 'prefer-template': 'error', diff --git a/packages/osd-charts/.playground/playground.tsx b/packages/osd-charts/.playground/playground.tsx index 7d0a1427dd58..13036f220483 100644 --- a/packages/osd-charts/.playground/playground.tsx +++ b/packages/osd-charts/.playground/playground.tsx @@ -17,149 +17,60 @@ * under the License. */ -import React from 'react'; - -interface Food { - label: string; - count: number; - actionLabel: string; -} +/* + * 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 Foods = Array; +import React from 'react'; -type FoodArray = Array; +import { Chart, Settings, Partition, PartitionLayout } from '../src'; export class Playground extends React.Component { - foods: Foods = [ - { label: 'pie', count: 2, actionLabel: 'tab' }, - { label: 'asparagus', count: 5, actionLabel: 'tab' }, - { label: 'brownies', count: 0, actionLabel: 'enter' }, - { label: 'popsicles', count: 3, actionLabel: 'enter' }, - ]; - - foodsAsAnArray: FoodArray = ['tab', 'tab', 'tab', 'enter']; - - getFoodsArrayAction = (foodsArray: FoodArray) => { - for (let i = 0; i < foodsArray.length; i++) { - if (foodsArray[i] === 'tab') { - // alert('tab!'); - } else if (foodsArray[i] === 'enter') { - // alert('enter!'); - } - } - }; - - getFoodAction = (foodLabel: Food[keyof Food]) => { - // eslint-disable-next-line array-callback-return - return this.foods.map(({ label, count, actionLabel }) => { - if (foodLabel === label && actionLabel === 'tab') { - let c = 0; - while (c < count) { - // alert(`${label} Tab!`); - c++; - } - } else if (foodLabel === label && actionLabel === 'enter') { - let c = 0; - while (c < count) { - // alert(`${label} Enter!`); - c++; - } - } - }); - }; - - makingFood = (ms: number, food: string) => { - return new Promise((resolve) => { - setTimeout(() => { - // console.log(`resolving the promise ${food}`, new Date()); - resolve(`done with ${food}`); - }, ms); - // console.log(`starting the promise ${food}`, new Date()); - }); - }; - - getNumberOfFood = (food: any) => { - return this.makingFood(1000, food).then(() => this.getFoodAction(food)); - }; - - getNumberOfFoodArray = () => { - return this.makingFood(1000, 'apple').then(() => this.getFoodsArrayAction(this.foodsAsAnArray)); - }; - - getAsyncNumberOfFoodArray = async () => { - // const result = await this.makingFood(2000).then(() => this.getFoodsArrayAction(this.foodsAsAnArray)); - // alert(result); - }; - - // forLoop = async () => { - // alert('start'); - // for (let index = 0; index < this.foods.length; index++) { - // const foodLabel = this.foods[index].label; - // const numFood = await this.getFoodNumber(foodLabel); - // alert(numFood); - // } - // const foodsPromiseArray = this.foods.map(async (foodObject) => { - // for (let i = 0; i < foodObject.length; i++) { - // const numFoodAction = foodObject[i].actionLabel; - // if (numFoodAction === 'enter') { - // alert ('Enter!'); - // } else if (numFoodAction === 'tab') { - // alert('tab!'); - // } - // } - // }); - // const numberOfFoods = await Promise.all(foodsPromiseArray); - // alert(numberOfFoods); - // alert('End'); - // }; - - // getFoodArray = - // async() => { - // console.log(await this.makingFood(1000, 'apricot')); - // console.log(await this.makingFood(50, 'apple')); - // const foodTimeArray = [ - // { ms: 1000, food: 'a', count: 2 }, - // { ms: 50, food: 'b', count: 1 }, - // { ms: 500, food: 'c', count: 3 }, - // ]; - - // for (let i = 0; i < foodTimeArray.length; i++) { - // void this.makingFood(foodTimeArray[i].ms, foodTimeArray[i].food); - // } - - // const foodMap = foodTimeArray.map(({ ms, food }) => { - // return this.makingFood(ms, food); - // }); - - // console.log('before the promise'); - // for (const i of foodTimeArray) { - // const j = 0; - // while (j < i.count) { - // await this.makingFood(i.ms, i.food); - // j++; - // } - // } - - // await Promise.all(); - // console.log('after the promise'); - // }; - render() { - // console.log(this.makingFood(1000, 'apricot')); - // console.log(this.makingFood(50, 'apple')); - // void this.getFoodArray(); - - return null; - // <> - //
- //
- // {/*
{this.foods.map(({ label }) => this.getNumberOfFood(label))}
*/} - // {/*
{alert(this.makingFood(50000).then(this.getFoodsArrayAction(this.foodsAsAnArray)))}
*/} - // {/*
{alert(this.getNumberOfFoodArray())}
*/} - // {/*
{alert(this.getAsyncNumberOfFoodArray())}
*/} - //
{this.makingFood(50)}
- //
- //
- // + return ( +
+ + + d.val as number} + layers={[ + { + groupByRollup: (d: any) => d.cat1, + }, + { + groupByRollup: (d: any) => d.cat2, + }, + ]} + config={{ + partitionLayout: PartitionLayout.sunburst, + }} + /> + +
+ ); } } diff --git a/packages/osd-charts/api/charts.api.md b/packages/osd-charts/api/charts.api.md index 5781c77fdbe8..e1d5681d0fd3 100644 --- a/packages/osd-charts/api/charts.api.md +++ b/packages/osd-charts/api/charts.api.md @@ -569,7 +569,7 @@ export const DEFAULT_TOOLTIP_TYPE: "vertical"; // Warning: (ae-missing-release-tag) "DefaultSettingsProps" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export type DefaultSettingsProps = 'id' | 'chartType' | 'specType' | 'rendering' | 'rotation' | 'resizeDebounce' | 'animateData' | 'showLegend' | 'debug' | 'tooltip' | 'showLegendExtra' | 'theme' | 'legendPosition' | 'hideDuplicateAxes' | 'brushAxis' | 'minBrushDelta' | 'externalPointerEvents'; +export type DefaultSettingsProps = 'id' | 'chartType' | 'specType' | 'rendering' | 'rotation' | 'resizeDebounce' | 'animateData' | 'showLegend' | 'debug' | 'tooltip' | 'showLegendExtra' | 'theme' | 'legendPosition' | 'legendMaxDepth' | 'hideDuplicateAxes' | 'brushAxis' | 'minBrushDelta' | 'externalPointerEvents'; // Warning: (ae-missing-release-tag) "Direction" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -1597,7 +1597,7 @@ export interface SettingsSpec extends Spec { legendAction?: LegendAction; // (undocumented) legendColorPicker?: LegendColorPicker; - legendMaxDepth?: number; + legendMaxDepth: number; legendPosition: Position; minBrushDelta?: number; noResults?: ComponentType | ReactChild; diff --git a/packages/osd-charts/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-legend-piechart-repeated-labels-visually-looks-correct-1-snap.png b/packages/osd-charts/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-legend-piechart-repeated-labels-visually-looks-correct-1-snap.png new file mode 100644 index 0000000000000000000000000000000000000000..4e539c4b4c5e1e82717fdb0c3cb65208246f3b5d GIT binary patch literal 25143 zcmce-bySsav@W^`Nr^>EH%Nn&bS%0R0qIag5F{4e-6bF*T>>H{BGM%#-O}CN4fkEY z-`?l!Gw#{vk2}Ua3>+$~wZ89r-}%n@JkNY)xTd-S4i-5U1OmZPQiMH&K#(^f5F{%M zH1LzH=GWWcFC?dD3Nnz=eoF8gT8I)%TH7scXV%>Q2@e^?#A`p7SS1nlr z{GWetw=t%Nfv>I}9wipUPmcIhhrK35B66{d%5dK@|$p~#Rp&wuYW z=gyk#v9z-~?uiK1J7&VxQR~8yVf-&Tbz<_i>9{rSmzqTZdCe#6nf`eq zwEQn02#D;af&@!Q1QReJgI~tz-!IGmcw5C4n^5B{%KXJJ@P!bEti0t%y~Q)ke;<}S z@vD=gN5hxd%Fa~>zG}P9?;?l@DsTAWc)&whwAl4E`Sy!KPVjIpC9q*b0 z5lP2YAt^8mV8+BeJnZ^2^;kRXw&UN04fz(;%h8+W&e_^3g9O>!jEsw~nQhZ@SZ4j_ zW1g8=JC_x2e*N^Rc<#MmYK7A}^u>q;tP{V4&>!kR@ZvueW46{^TGqpnXc0!<7!~X^!;c zXSvdUrm8TW7t`soA*C4@X&6=a=aWP|N_nYQ<@nUxJO>Kp{dT>4Hmze}Fs}Z0*QLTl zri@X&U}Upm@}F?nATa#D93YNE8ZIZ;9#ad(1zmBX&e$SGjoABfUSL_eh;cs$j&ddx>9lkdcl~g8Jo4(O|4Y zg8w*DrSuF>u%Em!jw~AiA4&1`vv*e) zR2P?*&XRY(pHswvIR8NHqgk%UZCZnk2)N51iqU9~Z6@edOF|5bQ0?uU#hqtGlJ$8k zUs;-s{aFnQ50{7iF5iotes2(M`%=`bdip`^%O$u-dO)l);$dO89sjN={r~T&BWh*@ z>7T^qUMIR&Pgx@dX8;T8!ar&a!WtoBqenmx!n20zUwKC=5hl!HZb#@P{4cSw>M;J^je1*13=e{ViP>Rd?B2io*W*8v?*oc) z$#CMHEuGew@U)1U}XNa zO=>r4>g2=Fv8_$}@UW0Pc3a#R?Y~dl#e97wISlQ^6R~_(R#)o?^~-*NJBB~Pt)l|( zeCF$`nDPY zlJZ5db{smYRH>%a+CE((9)hVCe zE!X?@*LMM5e|jogCzgP%H?sTz0~F^i;x;ND5fl;e1tWK?S;Dn-bHRyxW6Qa-uG?*E zS=nO|^I4K+F;{^BWbEYqcy^*V_N2JSL>S?*3LAx57)pv#QZUpV%O`2L=qt)hjL#a8 zlwqSZGCxe~2m9o>k{Rgea3$;CaO#v8GAq}E(`U6|FJdM@juegx9w_9><42}=WdD>t ze0;ohaFDi(|5j}~k9Sq9Sc6GgYTTd}fAsmKKvw2|9$CM8)F zeS>wAn$UtxY-Cpw3V@Po;@?QwheAlZYOr3r|0eji4l!BvITOVMGIb* zo1Fy#H;{qby)BUZF{kDSQG2FuOcy30zW7^$5RFd|8(mVHzu{r^MBti79$?dPi%$)| zztjl{$)VQ><+l`m7~V)yhF2?!Ac{Y>;zQk$Ky_Sk{)UFa`0TMh8@q$Y1Q(fs zz8KX9jVJtqISUCI_+(I|)7D@N8gX*<0>V3K=?vWHFsm*^4mWPIJJ&hF&%2Of0Xg z^&MbiG_TxW(x5)1=uqc@@o&t6v?%PXj}6geCHj=5)Vdri9_6sy;l|Ge7L%VA9IiBV z7B<%%sokMm@HsQ40_2CYsqg81Ivz$D)e;dDC5n%ScUgQ^R@O{UKxt4%2gib^iX5E( zprSFZ*u<4b%UbaSu+O3@k6>%@go$6)(HKpeIOgH%Mg85Ir=ML0G9+Rr#$NvxdO2@C{QH$_D*u zc}Mnib+y4bSt(!haZ(tK;KKy@=+^I}+EEek`_$LZ-?04I-qLir%dIcPji#cSose|D zF#8=8TqCYwGIM?2v<#O;fi#)1LnFJv2W#XH6Gp(J*g& zlZi=?sTpBoT}udul>dGA*JlZJyf^J}*F^~$ikiYmTOB01gDdyXiWWG zt-*4yx^io4Vq%YRVXWL7m|OZLyEM2nQdt^ws+9;3X)jl z<}Pw4FMrfEeiRpUdL2cA?Ltg~3<(JhG95iwyeR!nP(VShY4|)4P!qcWy5#P19;OH3 z6&15(qIyNYD^rCmT(#n-m+M-s`ONN}2FyCReKA2UdgbGT!p9B8!HFlwp%?hRCdq%oI@(&CE=0j{qNkup56jHulO+C5DG9-W2tKnD3>Mq;6SHm@oX=A;W0|wHAkDl5gXtm|fuZH@mdvECN%q;s5K?-z zVS(&hTTX~nzBbqd7pZkp*^YCaA@1|bRl%gIh^;hA@cz~%{{{#Q7KjA)xR$M81T%Ct zrv7|R_L$fs6}#KII=43eL8_DE!N&D|n?s{Yw|687oqgQ-?9Zv)zww!P3x*~q54!Kg zeMBCEZSGG#OZ!$D^0Q)yffqOE%kZ<6rh5rW!Rgm^gZNOK*E7P(Fx{1Zs*?S|YO)z= zk^EIGD94gMckX2qOaz#vN{Y=>HBJuc6rvXm?Q;n%bv>@7nLJ3+SnYBX$pv4Q?!SMB zq#3B)yL(*ubqZ?&I3s*<)br_fBR5Ww5tK2D@Gy)8Wf7>6f8V>1q6Vt#`E>6NK6cOC&XVgv_K3<`w#w|Vas0mT&5U*S z7MAOu9{u@~e*b3EtxZ;j=k>{kN(LbbS=gkKlCb8p0Nh+Cy0S7CTlnFNP!ms_qU~{M zVhZfb^Eo40dU9(7bYcR)ha){54w)-3w%r7WH-lSiVbG?1;vcET{DDnx=`gc7&+jm!v`CO#EltQ?R($WyW zlhUZHJVAu-d0oxRB;-UE@Q0#^lc_pD5~CnA>F<>RHAq3dxj*{^DDS6ljam!h0v;^B$BS0`Mr4L2of5>6Vz9Z^KKRlA+sJOuk4k%#2EFHxsia8^3WR}yL7Yr&-nOrA*BCRiVCdIOHnU-V z>%FBVvaqZ2rl#_+zY{w3EJdTz#yP}WUH&le$wAP zBCB#1Sy@|^&BtnOyq(mmSFE?Q06GczQtju@gC1A>)-lwlaMlL4l9KjfgK8IFaS&$G zV^3x=7gA?t_R7kBn4Mgce93FpU`ByZa&ap-#eP1^&bc};14w1V+U^X_3gcH4_#fZ} zTpZ#D$N~uRoLw&iL)}+)voYb1Crrn1dg9_!m~?(Y05{{$dnqDnX7%oO5tXPId1v-_ zK7OCFj+sSx4k(-L0s>uG68Fm~x{ju^Y>NJNS;moS9vdPYi8q4aPdA2mB_4HFOLsvc)e)LlP|H-3~jk58*bLt|-QbeE3K&3zW z#$NJq+gT*Jqf1l^lCB=g6gh%VLJ|>l;r8nF?r$U4IcDuoq-HhPL-_D)^i$XOjhmaA z9!&9y_=TFLuU;{dsHKIyv8%(`7rUVSfcDbJ(NmZ7yNXI()1FFE&x45=sz|eS-B`~I zJrN=*fLR0E7EL=z>B;SZp7!bKv3^i+FfwF#gp`iKsm|(HDeXpGfq&(bsEGZ_>4e$7 z)|zzdBLQCtfZM6d2@OKYoBSZEz6a8*GBRRRSWp}S-LK+!&x_0S_sj)T;~TwjKt@DH zL4iO>A|H4(AklaqkbEHM#G-xza!v>tfJOa#^8={qXq?4EVPALNMFHvzI6DCBVQBvU zArNuFr>827r(@Cb6NH_<-O@^qN(+j1Kwhf2@g%Eq>}jq!WmQo7y*$jSs~P zK)jp%=E6cFREnP#OX4bwC7vLD0LVToxj;Z;>JwY zpe;=R=Y+X%BjWH2%`Fy5@gYE2sd+Z(T^Q457Ybu*ch(Sp04Y!D1No%zF$Dccu}mC5$}HhRE?yxhl-3DxRGInCLrytSWvo%vQ>UJS z)+@aUU{=wuzaxw84RIH4FmM<*x)aQR5;3v~d!vT)=Lz4Sq48Wn1>jezB_+C?O$b)c z`}~XP@14o!oyoYXOKI07RB|jVP)xS_dma&Va2Z8hJbI$(xkIMzb$|rP%FjWDE_cdy z+ySDi=)jcT6$*l%pNKenC6b%VLrhqZddF5&pkLt0DZ)m5LE`iZn>BAiz~Ht1{vKl*-Z!}Nz3Gu_ z-}w@~Psifk>yq;Nh{^Y6L^pRS!pFxJckXV%T95BK%qk5xaeE9i{#H*3XkKhHv$B#> zQ^)=M=g$naD^J3o)lV+>w?Mkfx{$n=p57cu_RiqrCmQiRd(uT5ffN%L<~2m+Iv4r8 zpb7#Bmhs2@%qVvEw~b85E{gRS@XsTtOXLHf^CW-EG3gHyutA7Qf0{1Q1VZufq~LR} zF9aq`cZT!*U;%$@rw3W>AH~Hfd1z=oeoR&ef4rl^4z_nt3^n02QFI@+a&ZVwIu0ny zy54sJHsfjRo==;KlEXqu|G!DM3F<2hAP9ccxzwD@EDcC19Ij_tY-#ueK%LgIcDBog zo@voJ24x6|J$E=*1KJy431OMMC`!dTEoN0Yj^KAmiEbkN zLq+QYonbXjLz;SF;savKN8SalyD}HQ986oVhX%*Bp9Qq4rE+*#pMa9LwO zQHfd}t2oOmz(7oD2!J$;ARa|OMn~vs^4Gecqm)krpvP-wH_<}oqo)(OVoyd`>RN+h zD8pB#?E;H(QRZ)N_j%4L&}%EIh)a@^!W5O4tVM?KUGiP$K&dBAt_53#R#2`k-)Iew=6qXuz1mZPp)XLuR*z!k)$m*~- zxA!S=V|U*Jh?w_wiC)~!ozUU~3PHm&hu}<_99daj+g`F62~R=u)>cF6%B^N3Z7N~N z>CNS*lQx>1iY!R}qepg~Tby5JeI*kY7g=wQRH}U{L>g>JTRUS`5w|@4W$esD?Y|LK z4Bopfu@2F$mroBpd%1arb+I{H=dch=n)MK`^QItEe&@$qWVww;*$fKibA%erl8mU6 z+zl&^0e0W@D+&q=ueY(gYfT*39|@*i%3Bg?C*1=^1mf zt=q0x9Qfamem{S`=&EsthIfCZ#Lq7n&PS>{9FF$1pf6-(-iQ6KTX3cq5X{3zowxf> zb?3j!#l^6oJbt^XjL2|6Ioxn+IBQ%oC*ve3OjO=bWA<|0BKj;M3VlS$q4-Xt#2CQ2 z4rkw8dl4=BGY;da?+>abg2m8725g|VbX|CAwdVM8c#@`p1&oLZN7fH-Zg0!&TTb0JS9Rl) zlVkH$NNOpTm=NeR>}k6CkMyS~7Alch%Tub%UgvA6!eHx|zr74htg=}oC1c&7G}jCX zHtWxgl_92xL>Z8rFT+WiK01Oxgp52nJKBFYZ^F$1ACw85Qe%rZJu1|?7{d{QK)$A? zYOQzF(SJE}q4ZiMMwOR0KVyh(Hpw#K`c1^|C5(Uz<^>uGdv*IuD?ljvoP{E#`J9Z*7hy*B95-0r>MC%I9Uw0UtC*w`^!Jk3L|3FD(<^Wi9vxU&*T5}0&tQcEu_3LX?`-$2&plQ2?`Rv zxhqnLPZZWk{FDN~jk#KW{)RO{0ZC~ANUxeTwiL|w2cf_AiV8H(PA3XUpJs1G{wjLT z>(NcHa&uZ%PWJGYdcBqH>f)C$LeWqz`kgB3lJ&Lbk=)prqPikJ+lor!_&74;X4MgP zZg#wpgh()Fm2Y@gJ0<1a<ry4G$KJju%L*0ii3k=1-MQkgu*}E z>L7g73cqOR#o^hxuZTfg^u5@=j=I-2UmAP@8LZ;IAW2!Ry}xH+F_C}vlZ)u8V(Bx(2zLPyHt}kqXXw?(kL6BZaZvUzgxIKjCyW^sYNtl6&8Y%W2=Jg-Y&dS1w`9g9^~ChodlG$6{=EZ@u0 zoLnQ!!0puJvHO^E5!?88MsQDVO6(59!)2KdJc|2z4^!t)PTOW(^H z5i%Fu>6~XgkEft=(-l@9edb1rV<#0xSXlFr&>a3Nv4pV*z$cL*AJsp@hXjC(a9mb{ zmICyL)EPsLt%^T$G_mvW5jhatS#o8a2J=3Vw(jm1lhf~)8!~SnLfMiGj!&4+8_s!F z!BrgB(^+p;2({LJnf5NVhh;t`ter93xjm58THlJiB4oj*Bt`d)zl#bF3u;CjQbM@TLeuRqJ6wba zN9t);{Dev8%={e*XpD|hNpk)DHA3Ts4+x#`<>fk8=Ft{rG>Zm3{bS5$NJ5(gP|R>d zBt=yfV}m|o2JUi1zvUZ->vmGQ=d5vOy-&buj_mTbbwss%YL9%NG3ab-+a>CYWu&Kx z-|Bh!OH(w1$vkB!*urJ1t`?S>TM)?I6y?zKkq(}CjqboW?;|>#;f_{YFGhLgdp}SN z)cL}J0oOVTd}#cqUqMp*T^A1?WdGAU{Ivpbt#FO|lP`0=lJfm2fgd;xu9E73B*U{= zT35c0RXlU+Y(^~3%<+&$@`i`8L9DE_1jn`EiVaZ!fMT2oWk8K?TZAwDzBD0O3c(?S z@5}+sex>o6>&wkWr|`{`O*!1<%somApdrAcbF&LEJTu(ogIL=Ub$LZ}?*L8)gEauL zYKV3T^yW7Fe@5#F@)+7{^U(L5NMhV8|zI%amDXflSyYM-tuJ^ zcP@&se^GUKOhWf!eT5cSdWQcbvG#QR+BybtOM}cnuX(hZoa1@~eeUfe(JCJc*mhij zEFFC(p{zeNK zJII-E0GSIxYfw8o&#x%^`h*y4Z#{k_CeeYAM0XCrh4j6C4f>G(Y*jK7$ejJflF?ye zeW3day2d|zhl!BB2| zwL@-!>}G=BfxYT&w?^FUunMU-wUD<+2ZwRg_#b5=-@^x#f~leOL~{bwa}lgUp7Vsz zaDPOt3YvWvqQ0PSrBMlYUfiH~PwTM+LqxlXjg8j#*Vymm83=U0`Bl`o4>D~8rT}CO zTK$TK709aw)%0M|6837)f!2%At)Drryangup^9pdVfFzKEmoxKT=v_pX?!9gB9vK^ zlo?qxnqz(8c+6_5XL_oD<)LBLjj`z&)PYJ@eY?tCo;lB75FXn(zh9>jS8<)&m<2g~ z@IcY9k+PD;9o2c>J9HzQko)Bj(;>0f33LrC1F%})ihUlo)>toH$u zJ`Ngan=Xz7q8~PYU<&9QGJX*pcDOtl5pRWWUDt~Ik^S0gYFac)n+>`We1C@ki^v4w zs8_Wjpj%oK(P%xz|L}QH`Raxic+4Gku8yT}!Urh@lJqR9etlN(;=a5wyG|g_Q*yA& zYQ1>jN^=5QIC)>kemReGM3EG|ld(!?COpd!3uhoEW-Td^b9%!9KqJ5#7B0^S?9v6W zv;F;Zdc9rleSQyo@&|V&K5p&s4$gna!ijUAnu577!~Mw|e`}ruiX)MoO~;d>%0u?h zIJxi%Vx0go1N{WM*b#RLD7xPw9o;8E%ZD<^$cWp8tP{9tFmD31*Iu+cJxjhk*_Y~& z)wxxcsfxzp2hbU!2ekbg^Tu+FjYC7t`HBAF?CDnV;Q8OktdrxwZfm*M-r}IE6Yp>% zY+QeB2O6aje7-MS1Uop5wuXE*#7$C-o4p^2Z#s*V<`CliR|}@3bOLG;8^*NjPRACt zz-DvJcxMKOyBT0W7;vl~!_^FuG#`(}OOw?z9u~m@4jmlGsEC6{aBybxc@En`F!@YO zVAlM=Zez8xPA5(JcgH0vGQ0)nd4YMyFNZK}ywJ8y*h?cSDG9MDiXm>MrZ^iW1JeIm|%53#lWAnd6;=4bCG_ zZGLhw*Y$LW-xVNuCEtiRL!OJen*S!=>S+Hsw7kDFI+QDyT98{yK2F_)k3X~khz0HI zU47sn0iG{Ne@SWG@Z@TMflgT~jn6$<KL@mK(b5z>~=R zrdodp=zV9xOjT8d-^$P-jIm_?LDa>oD$HuFlCFC}V3~i`i_O%TBE3TSyD39V7?`zT zwc>jcKWz~m5w&xwA1C9#Y0~DcZvj z3Vb4BCdKZtF~g0$XW1iWPU^V0=_W0)z=>gF*+*bI2UJO+C<6}Zbr2%1=Z`a5B6BeQ956pDT=|Q7mC})D9pq|pHGuG8p zT~|)yGb*qL?2trmI2mm;P;a1wPCOj7;pG>gvtEKB>`~I4CDwKw0XP(;7il2D848&p z@0rw-`QP0I^v^#BCL2b2s!uRLi$J66$nnSzQmxCQro;Y^Qve+5JSG#5fT^R$&ub-{ z3~8Ks!3Bf1+Ct?rJ6~0~l=Z4ZE8ym|iKF8#uBA%Ohn%x_U$6H>f4f=b+QRI4mO^m| zcYhCI|8h?ZtH%LIqux;0B6r#X^lgIxz66sMUR&3C?$YS+jp3>T+;SSbOPsllOBN9Ehy1%YrVs&|BiVQ8(7 ztgf2Pe18qpa;&LR9u8Iv*MU^m?q%j;V&{?)VM8td=clI^oiS8h!Y&WFXPq?8B~W+~ z=9vSm)n_mmN;nYqlWh>HDKDSrB+pa=Fcf@HN;sIzmIV=f!7M~7`OEl*A>M*n&2h6C z)79I1hph2xPZSWs8=pS%SemI(6Lko;-DJQ~t;j<>%$72{UuIHk0za$h0ECcEI5Ev9 zs5Z{-pINhCpb>R31%Y9$uYUmg%Zt0{)P6E~_pZAyUVx(7Wm7&A+!Z<5Ygw&9J|-x* zv~tM{JR61;y%v|}we`ZYpC<*hKpVZzwXDzyWbZu*{Pk*~*&qOuVd)yG?_a%MoDM)n zMMqr-uG^RIg4n{p4fM$Yu7Scg@xZZ&NK!?k^Ve|+v^X`j;`TS@(Asw4SMoS-C=6Cg zmZqWvlLZjS#dE}J04EnXg!E2!D!FD0Y;4*NpBL}|A+LI^=|fx)s@~M}kjvE%E2me< zAD2KHUbGhh{`RE6+TK+faEigxAroWOdku(_J@NCN+jew{=Xrx0u)Ksi*|o!l=?D7q zg~IoCJ)p{b`z)xISR-O%ZTcjIme3)P4H{pJXaKZ>O0Z+;)U;pykLh84|GIrnid@7o zuaHUr9E8K(H<>Ma_6x~9$&nvtaDu8EZ%ILvrJ9` z$I-v`0j(b&A_lc`2OmEZzXB~-vE#`%<&(Q^_M6XU_$)$Lk01VXeHt2J2mr*!zd&hW zVRU1RIeYR3U&MOU55BZn^rCG4&Tw;`WlBsC7;S%=G17ClFs64!Dgn3cjGofIyZ&-X zrDaY12d^(hn4S?4rd8AN&P%9vgq!#&3xD;j&Uc_*GigVTVz>Lgz;U#E*B!vWBE|VmOX&4^8?cLI?$ZA>rH`0ZhKm4$NAa&$QY-BzKk=}lpwQAb z;&clSW)ma<9R{HPNq63^_otUn?LzzpaH(3~gyI^Vx#x!9b=pW?#b-HTcbf_>o2=K( z38$&QyV$8T&h%^>EdoZeC8Y2kOMXQ644f^y*fBHyC4ic^-EU+vPXKta5+JLkG2wlU z#T-k%dRDJ%{v>6g>{+-7Iy9ag^bg&6w=X@88Xbkx|9(uKxOINtFyzUQq*(rHIByS? z?fAl>CE*14d9`#n)BEoQV|ISEW}3W_ynlzTJTL*=PGuMRQ7C@}2kokW?#ZtBVQy|L zSjcq;kg_}iOuJNZPbnX01$i{$$g=|@+R-yW{Ku(_!@L~|zdh_rPlxnV70+b~MgS7( z%CcF9jeQvTx(A=t)TS2E<`EFU6r6kTUh=?i!NMZ|sCj#*+u~FmOrT|&Lvyjrk|3S0 zZ4P{1p8#DOX3>zpDL2dMclWOvzGdL_49 zt|_8E_0yPh8-=V|39uJ%e$eL1iPl&uCx56Zr*M9K2hff2Wl!e=!CWYAYBYsnxR>U0 zB}@Q7vL-gcAqE3Dvgsc~$|ZKri~cu*7ty(}xj&omV&>WD?nGrcuM!PBYIis8)XcZd z)B4T|~gf0*uf? zn8@zbpJjmO=MO2i1vK;b%RTpNVO`??+xEZ1n}_kF$74a#Y{@9U#^ zuJa;BqXttX`TkUNBRekK#v48eEwcde6B-W;FtT$bc{A7s#Zp*XU!UGTvR`J6*8oyF zzPk@JzwGX8OC{Oc3yj=wyyQ?XgU?@LDy;+pduIASodq4;(4*Sgz5A^J7X6{@?7t_) zCYXH5?398OkPBFH++uh4;EWrCh_lGf^)d~2?U$aOKrU`Pi;)qB*?pf-V1CuAF{)RS zM`_{a!B?-F-SvP6PI?W^w|}n{0hUl#mM)-k5`N78f(3QVyx8LIfbn1abI1w8VdX)K zV6_yKJDO_m2$VC;Y{_7H98$ z0*`uCK_Bv=>5o>ekdW3jOWh2FR#1NwAqBN9^C>T=F^u8!pIe&TLvEqAx1V*N;bjmsk zfAFs~-nyLEVFd=$Gos=OC7kpmB>=B*%e$(aF;jb+cdp$i$a=}ZCGU?oKm?9V-l}kZ z1pia2s8EcG{>L3FB(&^%w||fNs7dmj<&zs^WHCWs->5WEQVSApdC&#Nt-mmD>Hzar z@y8QQ2>-a@ecgO^MINeLrVpEa-qIl+u1g)Y5m$nlnKBlEg+<-HIvRVDo6E7~4i_5% z>#$G97wzu?tWfs{h~QVIPr~|`H}ADu6%fV1Sl7*OwdEVi#3b!BqQ+BQZ7~pn6A<+h z*vYM9C=n9br}N*0g(7f_H)ngquio)q7HOB)PJZEGq=oqERmx|D1u^Ne(Is^w@)2S> z;yqJR&p^ZJUarXYR=tE09tK|M8gMOPU`ijp$;>{*>Qp}0eJL;R+TgL1_!!Z~)LMf^ zWXdcaL8k)a#c2J2{Po}t+uCF+RO44I3fjMRN6b1(I z*L!tj9}|Nxt7p-i3@Bn6PCphpo#__p;~3eM7PR3jAD$z z9!XlD5hQaffNYE_c7Nm7Kl@B41R>mlrZ+8++cu7lwT{|JQOMiIKo0=SxOj*>9J)Aa zdP+}j_-_W=V3A3IH z^~JJqr7JhcuVN$`8h4EudDjR3^W##QnhIp`qrpTVyml8o1N&u@St;d4Y@68n(L+QT#P^BKQ^c^aU$=Nzsmfss>t0Lkz`fBX+09~yti zU=ZpkwWwg06D}j-L-q*(cLnoY0Me^6XO_nA7M6yL*dhEsZFZ#Ijez7BI`weci=-oj zY+!>8lPDMj{rRk|K{_kD#z#)no(~ILcKo=w-%G8z2&ZopmQu&nh)Dwr9+>Hcr#yn5 z&i`xD7^v!c@xP2~gz?cJxFqxOxq`iy*a;3!2Cf_t)!10v$qo_cCmn><-3VN^_1Yxf@$U)KRG20aFun1 zBCL!eEa+fC0irdt78rf|76gkC^>RuT;B9MZ4&9?8fARBVO7y=s`AN7*AGGPxZAri^^l0h}nu8Vp*28QOqY(VNdU zSv8x?@Gbb?^G?ZHugtd1-#}mZ4FGG)BAA{4j5_GS1L^mC*@a)RfY5Vs(Tp@B@Sb|l zsC*$}+1uxW2Pi-#*EU5kWXT#zV8{hTC*DXug5ryM`+J>|fIMGrVxTE+93Alpic?L9 zpArF2YCCX-UOk;!ebCvy2-=+iF`KqB3Nb;j;rV-P*h*jNXot?f_RQ@OHzOl?uY^-h zAUV4T{V+y&cpRm*v@&p|wbQGr;uQ12935$%lcJ_J5%Y{AHeg_m^WN11oYh{y24EqlVg@B(cNO$4qb_T++PO1) z&$pOvRL50IO+XelZP~YVp5~ohRtEO24Rl(yYP{=u&T&+fTZ2;HhZL3IcnZF=XAc1` z@;iT`RC6&1l;PyW_vNf4DylhbdFAWO*lNK#96!X>+-qw*i2NX*;v_Nu%~`z=HaXJXXY3crQeBXA1@O9AP&=m}z(Was%a7&vwbG;hXpG_xp2mx$tgd zeJuxGS$>EM=3vCfn8ZZ=)7YJp)#V~x!r5d!#+5PkwWOiCX>Kax%wx}Y&Pjj&(A6N4 z_>?W9_Ehg{I~DsgO~tRf{QBh0$?}`b7t1wHwQrYTx34ab z;$qHUAd8E$`R)q1%g1QXZH*~sK7F1~A!NOiBZpEOF7%;2ub`*f0}f$sQx(C-Ox<;H$J?S16z3NPgAQkXXBORTUESi1SVl3cbJm*fA{?%il?rngSPtxsRi zz(lUi_@^&fzFN0;w^%19g)RfseZD7Juel8@;dybD3UZIAz!jvG(CLHv7B#(etbV%l zeSSa6+UIs4{hCWJYga=oyJ0}G45-I!^~e3?ukELH^@ZMNFm6Tyr9T$La1O9ezIhb-GSBX}=z^Fox=x8lXW7dz`@h zh>DYu;Ctoks>!GrBuH}k?bSrnUDXgB#7iKuk2!K|s)_OyYk8gFpQc-v=oBFhg?V2S z^m#W~@padiZ-wECPkTOY?hNH_i8YCv7p@i*Y%3b21D{Gs?4KUzseN6?ci-gQ8ftmg zjb{HAHto$6bsBrW!0~C)QBU_6vl$l@*zBYbEG(^gS?TC_k}A3v*Or&hQ&XteO~_(B za6F*0CA#~w@tdkzXROW42PSd>?^^Zb0O7s?kgs;>vB0`mu~Z3*Hbr{vD>n0%iwr@dym5I*VbvqOnuG$58e0Lqc4(wiQVVY zJnm?It>y$=$h+=$?w7^I+}B=?4i%2N-!zxw8K%x#vCY&;$%SsVbiaQ(F~WhgQPkBz zpm}vAYM7IgQ14%==4RPPx~1AZMbD>>i0>DyB;22!g6;jajJtb(oZ0qa09@WbU!}@% zOZ(P#f@@dAL%X0qV{FoCooLtbF*}o-35Ae)H4!O06W1Pl*?GHr+{d#so4YytRFC`H zua6(sm`f+;O?@C3`VQN#I`wo>@d`?G2kwv7)zJa0YJfTn=YL^+pZZXw`t=e6r|wTaYF+O|J2 zcYkX_bSo3wOw)6>TGrykqCFUwc6Y}<&fj!wkR^vjL_WWps)~q+esO72wz*SbAx*>J)PfvMCaP1H>CU_{ zL9Na_@0zJjBUVUA$2vTOiTTHk#^=oxns?FsNFhW0ic@(y2^8?$nd z{f8gTZ`GGn6Nw+CZ?z`MYfo{=puk5Qu$6@Rmo8%xVs|U_SEri2LQX@*co1QKYqCrd zqbVIGU+VvcSF6GpPT|ete#{=^04!*m*3c6+7gW#a?S47gkNH~(TW0Y;TU*@N5+b8KmYFrnvTs&}r6M z+H%>$^qmq|nPben9_C~NSZpG;)b`cifq{|r-=#J@6)T&gaVF-R<*U&BkBsLoxVjB3 z<-Q-R+Sz#<6@x?DLZJ}>Bb4yzLS7Vo}b%eXr1cUV=dpvXn6oIRQGi3UP z2q|%6OUUcT4f~GtB4e`8T(_m$D~zO@ClR|`rM;P#_ZEDa_~jZ%I_7z97irnqV!;${ zi$c_*-;xUHy*dxh<9Xv8&8f||z92rC+jz{xIqS{BU~Agj46B8qf&$V2RoQIjbrfo; zEp9D4J0HwLH8s1-jq|_`78wVkz#0fXSHwepyuq>EtSl~9Tm;Cg=sJ4T*$Wm2fPigt zM9(|l2_s6e6_{|OJU!-LaAtkobf;IxNyD#~s%#SnDqEPXV`_=aY)Db!;d^+^_R5vm z3a2;*F7W%sONf0kpoG`ex4ZZ#^x-C`4BEgf0icaCFz@VzXYIcPFrmgs z0GGb)J_XDJMh5X$9!SBjc=Lktr!_Kxin3;9SxB;B+ z8>Xqvq7IEXco4R-v*4tRX`w0$$oceK%I-?dpHEXY<+3TAHH#r(@{uA2C|d6O`Idm? z6FV@_my-i0gFJ6)AC}{TUUK{^XLO|Bo%*)TI>6;~Nu<86$(IolxS(JoJ0WyhFQKmN z>P)e@0PyE7_|ENt_dx(pWZV`PHQX$!AmH;6>?w+^)>bayWH^W@z*3&g%>}l7m(%hK zc~=yUxsvr(hld}Nk6g?&?0qJXSx}^>3Z`c>jgKV(8=dj=qHV`Z!B$&E1au#=z1iO8 zuE*p**3Z>&Wa&Z`RZWZj`*i*PdR^i-3W8_mgkXK!S?8Gdhl zVQh6Zc`a>OHk|F^&ws^`Yprrm^7lU*vZl8!Q$SF#P#Vt~=|#rXuifG7P13P-EpC#s zxCCki@|UUWgk&jQ6jM3Qt?rhN#D))yPZT$})hgH}ETbuTbA55OkCWy%7yI^|l?5~Y zD<6%08zY#~#}$f!PN~IW?jM<3qITyRXyixz`qi#+;eA@s-+_XGiLt`8;6Iv)eMts4 ze9Eun!}R5{f90<=Xfzl(3Z4Yvg&?Y9>9L zzMb8b_#x{#GwsgWP(QVQY1~QK{hGR!l}%SnVhp}#eYQOXmzf^LHV~Ik0KWAhh5qR# z+&-I@m{((3F#S4eaNo?j)=ux~zsxFQfFh4)yo6+*fNH*;o15?5oZQn-laj*i_$VRL zm5oFyT>5%WYRD?5ZTlG`V-;PJack=XqoWexS@9!x6?ioYcY1pz>U8AHmre_wgFmeS zz4D!mlwR$#5MKW1`L;F_FoJx#HqN$EX!@79+svIb)u6kKScC9v{Pt+)WN^!s^U&#V z92=_~xCLC_ANaSjY0@*ilUGAca`WiPgVuMGe>O##KGq5ibaeFG37nt%Z4x$FQW_V> zI5G<$R|EeZ7$FpU&GrMcg2rPW9}g;B;Rt{1Lsn~T4Z46p0GE}330y~oX)1W&Ign4d3`^;0WnA8Y3v}d!Z(xm< z-g{R}giWr}DMJv$Ksm>%nR*{~#!5(cQaK^_anZTYr|f;JVo7#ycwH-cj6y!T`>S!2 zpU8=XGjpC{{Oa)Ou4$QTT8~`FM^O;uP$?8}s4+6)X1`GK=0Own*H7C>gl^4}hr#Y1%)}UvUB3|KZ(fRM^mpX zd#b&OA1}Z{1VVv>u_)%2x(&CNITH3ajPXqfs}K$6k9AJ!$>Wlr z#=iMsX9Q`ofrxuK^3~g>FL*J>-mF6Nee{l<0wS^n4h

u`$Vn-~A@`!XoXC70#5YXda~bC`^_1 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%0Dqo$Y{7 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 { 'http://localhost:9001/?path=/story/legend--legend-spacing-buffer&knob-legend buffer value=0', ); }); + it('should have the same order as nested with no indent even if there are repeated labels', async () => { + await common.expectChartAtUrlToMatchScreenshot( + 'http://localhost:9001/?path=/story/legend--piechart-repeated-labels&knob-flatLegend=true&knob-legendMaxDepth=2', + ); + }); it('should render color picker on mouse click', async () => { const action = async () => diff --git a/packages/osd-charts/src/chart_types/partition_chart/layout/types/viewmodel_types.ts b/packages/osd-charts/src/chart_types/partition_chart/layout/types/viewmodel_types.ts index 6f9b189eeb61..71e0da321059 100644 --- a/packages/osd-charts/src/chart_types/partition_chart/layout/types/viewmodel_types.ts +++ b/packages/osd-charts/src/chart_types/partition_chart/layout/types/viewmodel_types.ts @@ -148,11 +148,14 @@ interface SectorGeomSpecY { y1px: Distance; } +export type DataName = any; // todo consider narrowing it to eg. primitives + export interface ShapeTreeNode extends TreeNode, SectorGeomSpecY { yMidPx: Distance; depth: number; sortIndex: number; - dataName: any; + path: number[]; + dataName: DataName; value: number; parent: ArrayNode; } diff --git a/packages/osd-charts/src/chart_types/partition_chart/layout/utils/group_by_rollup.ts b/packages/osd-charts/src/chart_types/partition_chart/layout/utils/group_by_rollup.ts index 388349cbd74c..11a7d7a5743d 100644 --- a/packages/osd-charts/src/chart_types/partition_chart/layout/utils/group_by_rollup.ts +++ b/packages/osd-charts/src/chart_types/partition_chart/layout/utils/group_by_rollup.ts @@ -27,6 +27,7 @@ export const CHILDREN_KEY = 'children'; export const INPUT_KEY = 'inputIndex'; export const PARENT_KEY = 'parent'; export const SORT_INDEX_KEY = 'sortIndex'; +export const PATH_KEY = 'path'; interface Statistics { globalAggregate: number; @@ -45,6 +46,7 @@ export interface ArrayNode extends NodeDescriptor { [CHILDREN_KEY]: HierarchyOfArrays; [PARENT_KEY]: ArrayNode; [SORT_INDEX_KEY]: number; + [PATH_KEY]: number[]; } type HierarchyOfMaps = Map; @@ -75,6 +77,9 @@ export function childrenAccessor(n: ArrayEntry) { export function sortIndexAccessor(n: ArrayEntry) { return entryValue(n)[SORT_INDEX_KEY]; } +export function pathAccessor(n: ArrayEntry) { + return entryValue(n)[PATH_KEY]; +} const ascending: Sorter = (a, b) => a - b; const descending: Sorter = (a, b) => b - a; @@ -128,7 +133,13 @@ export function groupByRollup( function getRootArrayNode(): ArrayNode { const children: HierarchyOfArrays = []; - const bootstrap = { [AGGREGATE_KEY]: NaN, [DEPTH_KEY]: NaN, [CHILDREN_KEY]: children, [INPUT_KEY]: [] as number[] }; + const bootstrap = { + [AGGREGATE_KEY]: NaN, + [DEPTH_KEY]: NaN, + [CHILDREN_KEY]: children, + [INPUT_KEY]: [] as number[], + [PATH_KEY]: [] as number[], + }; Object.assign(bootstrap, { [PARENT_KEY]: bootstrap }); const result: ArrayNode = bootstrap as ArrayNode; return result; @@ -149,6 +160,7 @@ export function mapsToArrays(root: HierarchyOfMaps, sorter: NodeSorter): Hierarc [SORT_INDEX_KEY]: NaN, [PARENT_KEY]: parent, [INPUT_KEY]: [], + [PATH_KEY]: [], }; const newValue: ArrayNode = Object.assign( resultNode, @@ -163,7 +175,14 @@ export function mapsToArrays(root: HierarchyOfMaps, sorter: NodeSorter): Hierarc entryValue(n).sortIndex = i; return n; }); // with the current algo, decreasing order is important - return groupByMap(root, getRootArrayNode()); + const tree = groupByMap(root, getRootArrayNode()); + const buildPaths = ([, mapNode]: ArrayEntry, currentPath: number[]) => { + const newPath = [...currentPath, mapNode[SORT_INDEX_KEY]]; + mapNode[PATH_KEY] = newPath; + mapNode.children.forEach((entry) => buildPaths(entry, newPath)); + }; + buildPaths(tree[0], []); + return tree; } /** @internal */ diff --git a/packages/osd-charts/src/chart_types/partition_chart/layout/viewmodel/viewmodel.ts b/packages/osd-charts/src/chart_types/partition_chart/layout/viewmodel/viewmodel.ts index d3f5fbc0a5c5..a11b244fa2ca 100644 --- a/packages/osd-charts/src/chart_types/partition_chart/layout/viewmodel/viewmodel.ts +++ b/packages/osd-charts/src/chart_types/partition_chart/layout/viewmodel/viewmodel.ts @@ -47,6 +47,7 @@ import { parentAccessor, sortIndexAccessor, HierarchyOfArrays, + pathAccessor, } from '../utils/group_by_rollup'; import { trueBearingToStandardPositionAngle } from '../utils/math'; import { sunburst } from '../utils/sunburst'; @@ -349,6 +350,7 @@ function partToShapeTreeNode(treemapLayout: boolean, innerRadius: Radius, ringTh value: aggregateAccessor(node), parent: parentAccessor(node), sortIndex: sortIndexAccessor(node), + path: pathAccessor(node), x0, x1, y0, diff --git a/packages/osd-charts/src/chart_types/partition_chart/partition.test.tsx b/packages/osd-charts/src/chart_types/partition_chart/partition.test.tsx new file mode 100644 index 000000000000..a69a84492882 --- /dev/null +++ b/packages/osd-charts/src/chart_types/partition_chart/partition.test.tsx @@ -0,0 +1,220 @@ +/* + * 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 { Store } from 'redux'; + +import { MockSeriesSpec } from '../../mocks/specs'; +import { MockStore } from '../../mocks/store'; +import { GlobalChartState } from '../../state/chart_state'; +import { LegendItemLabel } from '../../state/selectors/get_legend_items_labels'; +import { computeLegendSelector } from './state/selectors/compute_legend'; +import { getLegendItemsLabels } from './state/selectors/get_legend_items_labels'; + +// sorting is useful to ensure tests pass even if order changes (where order doesn't matter) +const ascByLabel = (a: LegendItemLabel, b: LegendItemLabel) => (a.label < b.label ? -1 : a.label > b.label ? 1 : 0); + +describe('Retain hierarchy even with arbitrary names', () => { + type TestDatum = { cat1: string; cat2: string; val: number }; + const specJSON = { + data: [ + { cat1: 'A', cat2: 'A', val: 1 }, + { cat1: 'A', cat2: 'B', val: 1 }, + { cat1: 'B', cat2: 'A', val: 1 }, + { cat1: 'B', cat2: 'B', val: 1 }, + { cat1: 'C', cat2: 'A', val: 1 }, + { cat1: 'C', cat2: 'B', val: 1 }, + ], + valueAccessor: (d: TestDatum) => d.val, + layers: [ + { + groupByRollup: (d: TestDatum) => d.cat1, + }, + { + groupByRollup: (d: TestDatum) => d.cat2, + }, + ], + }; + let store: Store; + + beforeEach(() => { + store = MockStore.default(); + }); + + describe('getLegendItemsLabels', () => { + // todo discuss question marks about testing this selector, and also about unification with `get_legend_items_labels.test.ts` + + it('all distinct labels are present', () => { + MockStore.addSpecs([MockSeriesSpec.sunburst(specJSON)], store); + expect(getLegendItemsLabels(store.getState()).sort(ascByLabel)).toEqual([ + { depth: 2, label: 'A' }, + { depth: 2, label: 'B' }, + { depth: 1, label: 'C' }, + ]); + }); + + it('special case: one input, one label', () => { + MockStore.addSpecs([MockSeriesSpec.sunburst({ ...specJSON, data: [{ cat1: 'A', cat2: 'A', val: 1 }] })], store); + expect(getLegendItemsLabels(store.getState())).toEqual([{ depth: 2, label: 'A' }]); + }); + + it('special case: one input, two labels', () => { + MockStore.addSpecs([MockSeriesSpec.sunburst({ ...specJSON, data: [{ cat1: 'C', cat2: 'B', val: 1 }] })], store); + expect(getLegendItemsLabels(store.getState()).sort(ascByLabel)).toEqual([ + { depth: 2, label: 'B' }, + { depth: 1, label: 'C' }, + ]); + }); + + it('special case: no labels', () => { + MockStore.addSpecs([MockSeriesSpec.sunburst({ ...specJSON, data: [] })], store); + expect(getLegendItemsLabels(store.getState()).map((l) => l.label)).toEqual([]); + }); + }); + + describe('getLegendItems', () => { + // todo discuss question marks about testing this selector, and also about unification with `get_legend_items_labels.test.ts` + + it('all distinct labels are present', () => { + MockStore.addSpecs([MockSeriesSpec.sunburst(specJSON)], store); + expect(computeLegendSelector(store.getState())).toEqual([ + { + childId: 'A', + color: 'rgba(128, 0, 0, 0.5)', + dataName: 'A', + depth: 0, + label: 'A', + seriesIdentifier: { key: 'A', specId: 'spec1' }, + }, + { + childId: 'A', + color: 'rgba(128, 0, 0, 0.5)', + dataName: 'A', + depth: 1, + label: 'A', + seriesIdentifier: { key: 'A', specId: 'spec1' }, + }, + { + childId: 'B', + color: 'rgba(128, 0, 0, 0.5)', + dataName: 'B', + depth: 1, + label: 'B', + seriesIdentifier: { key: 'B', specId: 'spec1' }, + }, + { + childId: 'B', + color: 'rgba(128, 0, 0, 0.5)', + dataName: 'B', + depth: 0, + label: 'B', + seriesIdentifier: { key: 'B', specId: 'spec1' }, + }, + { + childId: 'A', + color: 'rgba(128, 0, 0, 0.5)', + dataName: 'A', + depth: 1, + label: 'A', + seriesIdentifier: { key: 'A', specId: 'spec1' }, + }, + { + childId: 'B', + color: 'rgba(128, 0, 0, 0.5)', + dataName: 'B', + depth: 1, + label: 'B', + seriesIdentifier: { key: 'B', specId: 'spec1' }, + }, + { + childId: 'C', + color: 'rgba(128, 0, 0, 0.5)', + dataName: 'C', + depth: 0, + label: 'C', + seriesIdentifier: { key: 'C', specId: 'spec1' }, + }, + { + childId: 'A', + color: 'rgba(128, 0, 0, 0.5)', + dataName: 'A', + depth: 1, + label: 'A', + seriesIdentifier: { key: 'A', specId: 'spec1' }, + }, + { + childId: 'B', + color: 'rgba(128, 0, 0, 0.5)', + dataName: 'B', + depth: 1, + label: 'B', + seriesIdentifier: { key: 'B', specId: 'spec1' }, + }, + ]); + }); + + it('special case: one input, one label', () => { + MockStore.addSpecs([MockSeriesSpec.sunburst({ ...specJSON, data: [{ cat1: 'A', cat2: 'A', val: 1 }] })], store); + expect(computeLegendSelector(store.getState())).toEqual([ + { + childId: 'A', + color: 'rgba(128, 0, 0, 0.5)', + dataName: 'A', + depth: 0, + label: 'A', + seriesIdentifier: { key: 'A', specId: 'spec1' }, + }, + { + childId: 'A', + color: 'rgba(128, 0, 0, 0.5)', + dataName: 'A', + depth: 1, + label: 'A', + seriesIdentifier: { key: 'A', specId: 'spec1' }, + }, + ]); + }); + + it('special case: one input, two labels', () => { + MockStore.addSpecs([MockSeriesSpec.sunburst({ ...specJSON, data: [{ cat1: 'C', cat2: 'B', val: 1 }] })], store); + expect(computeLegendSelector(store.getState())).toEqual([ + { + childId: 'C', + color: 'rgba(128, 0, 0, 0.5)', + dataName: 'C', + depth: 0, + label: 'C', + seriesIdentifier: { key: 'C', specId: 'spec1' }, + }, + { + childId: 'B', + color: 'rgba(128, 0, 0, 0.5)', + dataName: 'B', + depth: 1, + label: 'B', + seriesIdentifier: { key: 'B', specId: 'spec1' }, + }, + ]); + }); + + it('special case: no labels', () => { + MockStore.addSpecs([MockSeriesSpec.sunburst({ ...specJSON, data: [] })], store); + expect(getLegendItemsLabels(store.getState()).map((l) => l.label)).toEqual([]); + }); + }); +}); diff --git a/packages/osd-charts/src/chart_types/partition_chart/state/chart_state.tsx b/packages/osd-charts/src/chart_types/partition_chart/state/chart_state.tsx index 801488f4d4d8..dacdfbb2a278 100644 --- a/packages/osd-charts/src/chart_types/partition_chart/state/chart_state.tsx +++ b/packages/osd-charts/src/chart_types/partition_chart/state/chart_state.tsx @@ -70,6 +70,7 @@ export class PartitionState implements InternalChartState { } getLegendItemsLabels(globalState: GlobalChartState) { + // order doesn't matter, but it needs to return the highest depth of the label occurrence so enough horizontal width is allocated return getLegendItemsLabels(globalState); } diff --git a/packages/osd-charts/src/chart_types/partition_chart/state/iterables.ts b/packages/osd-charts/src/chart_types/partition_chart/state/iterables.ts new file mode 100644 index 000000000000..5e8d17c8c009 --- /dev/null +++ b/packages/osd-charts/src/chart_types/partition_chart/state/iterables.ts @@ -0,0 +1,27 @@ +/* + * 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. + */ + +// just like [].map except on iterables, to avoid having to materialize both input and output arrays +/** @internal */ +export function map(fun: (arg: InElem, index: number) => OutElem, iterable: Iterable) { + let i = 0; + return (function*() { + for (const next of iterable) yield fun(next, i++); + })(); +} diff --git a/packages/osd-charts/src/chart_types/partition_chart/state/selectors/compute_legend.ts b/packages/osd-charts/src/chart_types/partition_chart/state/selectors/compute_legend.ts index 6e015ef87821..a67fb9ff328f 100644 --- a/packages/osd-charts/src/chart_types/partition_chart/state/selectors/compute_legend.ts +++ b/packages/osd-charts/src/chart_types/partition_chart/state/selectors/compute_legend.ts @@ -22,42 +22,32 @@ import createCachedSelector from 're-reselect'; import { LegendItem } from '../../../../commons/legend'; import { getChartIdSelector } from '../../../../state/selectors/get_chart_id'; import { getSettingsSpecSelector } from '../../../../state/selectors/get_settings_specs'; -import { Position } from '../../../../utils/commons'; +import { identity, Position } from '../../../../utils/commons'; import { QuadViewModel } from '../../layout/types/viewmodel_types'; import { PrimitiveValue } from '../../layout/utils/group_by_rollup'; +import { map } from '../iterables'; import { partitionGeometries } from './geometries'; -import { getFlatHierarchy } from './get_flat_hierarchy'; import { getPieSpec } from './pie_spec'; /** @internal */ export const computeLegendSelector = createCachedSelector( - [getPieSpec, getSettingsSpecSelector, partitionGeometries, getFlatHierarchy], - (pieSpec, settings, geoms, sortedItems): LegendItem[] => { + [getPieSpec, getSettingsSpecSelector, partitionGeometries], + (pieSpec, { flatLegend, legendMaxDepth, legendPosition }, { quadViewModel }): LegendItem[] => { if (!pieSpec) { return []; } - const { id, layers: labelFormatters } = pieSpec; - const uniqueNames = geoms.quadViewModel.reduce>((acc, { dataName, fillColor }) => { - const key = [dataName, fillColor].join('---'); - if (!acc[key]) { - acc[key] = 0; - } - acc[key] += 1; - return acc; - }, {}); - - const { flatLegend, legendMaxDepth, legendPosition } = settings; + const uniqueNames = new Set(map(({ dataName, fillColor }) => makeKey(dataName, fillColor), quadViewModel)); const forceFlatLegend = flatLegend || legendPosition === Position.Bottom || legendPosition === Position.Top; const excluded: Set = new Set(); - let items = geoms.quadViewModel.filter(({ depth, dataName, fillColor }) => { + const items = quadViewModel.filter(({ depth, dataName, fillColor }) => { if (legendMaxDepth != null) { return depth <= legendMaxDepth; } if (forceFlatLegend) { - const key = [dataName, fillColor].join('---'); - if (uniqueNames[key] > 1 && excluded.has(key)) { + const key = makeKey(dataName, fillColor); + if (uniqueNames.has(key) && excluded.has(key)) { return false; } excluded.add(key); @@ -65,37 +55,32 @@ export const computeLegendSelector = createCachedSelector( return true; }); - if (forceFlatLegend) { - items = items.sort(({ depth: a }, { depth: b }) => a - b); - } - - return items - .sort((a, b) => { - const aIndex = findIndex(sortedItems, a); - const bIndex = findIndex(sortedItems, b); - return aIndex - bIndex; - }) - .map(({ dataName, fillColor, depth }) => { - const labelFormatter = labelFormatters[depth - 1]; - const formatter = labelFormatter?.nodeLabel; + items.sort(compareTreePaths); - return { - color: fillColor, - label: formatter ? formatter(dataName) : dataName, - dataName, - childId: dataName, - depth: forceFlatLegend ? 0 : depth - 1, - seriesIdentifier: { - key: dataName, - specId: id, - }, - }; - }); + return items.map(({ dataName, fillColor, depth }) => { + const formatter = pieSpec.layers[depth - 1]?.nodeLabel ?? identity; + return { + color: fillColor, + label: formatter(dataName), + dataName, + childId: dataName, + depth: forceFlatLegend ? 0 : depth - 1, + seriesIdentifier: { key: dataName, specId: pieSpec.id }, + }; + }); }, )(getChartIdSelector); -function findIndex(items: Array<[PrimitiveValue, number, PrimitiveValue]>, child: QuadViewModel) { - return items.findIndex( - ([dataName, depth, value]) => dataName === child.dataName && depth === child.depth && value === child.value, - ); +function makeKey(...keyParts: PrimitiveValue[]): string { + return keyParts.join('---'); +} + +function compareTreePaths({ path: a }: QuadViewModel, { path: b }: QuadViewModel): number { + for (let i = 0; i < Math.min(a.length, b.length); i++) { + const diff = a[i] - b[i]; + if (diff) { + return diff; + } + } + return a.length - b.length; // if one path is fully contained in the other, then parent (shorter) goes first } diff --git a/packages/osd-charts/src/chart_types/partition_chart/state/selectors/get_flat_hierarchy.ts b/packages/osd-charts/src/chart_types/partition_chart/state/selectors/get_flat_hierarchy.ts deleted file mode 100644 index fad11b30f5a4..000000000000 --- a/packages/osd-charts/src/chart_types/partition_chart/state/selectors/get_flat_hierarchy.ts +++ /dev/null @@ -1,46 +0,0 @@ -/* - * 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 { getChartIdSelector } from '../../../../state/selectors/get_chart_id'; -import { HierarchyOfArrays, PrimitiveValue } from '../../layout/utils/group_by_rollup'; -import { getTree } from './tree'; - -/** @internal */ -export const getFlatHierarchy = createCachedSelector( - [getTree], - (tree): Array<[PrimitiveValue, number, PrimitiveValue]> => flatHierarchy(tree), -)(getChartIdSelector); - -function flatHierarchy(tree: HierarchyOfArrays, orderedList: Array<[PrimitiveValue, number, PrimitiveValue]> = []) { - for (let i = 0; i < tree.length; i++) { - const branch = tree[i]; - const [key, arrayNode] = branch; - const { children, depth, value } = arrayNode; - - if (key !== null) { - orderedList.push([key, depth, value]); - } - if (children.length > 0) { - flatHierarchy(children, orderedList); - } - } - return orderedList; -} diff --git a/packages/osd-charts/src/chart_types/partition_chart/state/selectors/get_highlighted_shapes.ts b/packages/osd-charts/src/chart_types/partition_chart/state/selectors/get_highlighted_shapes.ts index 380c33c74503..e9f91e290494 100644 --- a/packages/osd-charts/src/chart_types/partition_chart/state/selectors/get_highlighted_shapes.ts +++ b/packages/osd-charts/src/chart_types/partition_chart/state/selectors/get_highlighted_shapes.ts @@ -27,6 +27,7 @@ import { partitionGeometries } from './geometries'; const getHighlightedLegendItemKey = (state: GlobalChartState) => state.interactions.highlightedLegendItemKey; /** @internal */ +// why is it called highlighted... when it's a legend hover related thing, not a hover over the slices? export const getHighlightedSectorsSelector = createCachedSelector( [getHighlightedLegendItemKey, partitionGeometries], (highlightedLegendItemKey, geoms): QuadViewModel[] => { diff --git a/packages/osd-charts/src/chart_types/partition_chart/state/selectors/get_legend_items_labels.ts b/packages/osd-charts/src/chart_types/partition_chart/state/selectors/get_legend_items_labels.ts index 7cb025a59b0f..354099eb79f9 100644 --- a/packages/osd-charts/src/chart_types/partition_chart/state/selectors/get_legend_items_labels.ts +++ b/packages/osd-charts/src/chart_types/partition_chart/state/selectors/get_legend_items_labels.ts @@ -19,11 +19,10 @@ import createCachedSelector from 're-reselect'; -import { SettingsSpec } from '../../../../specs'; import { getChartIdSelector } from '../../../../state/selectors/get_chart_id'; import { LegendItemLabel } from '../../../../state/selectors/get_legend_items_labels'; import { getSettingsSpecSelector } from '../../../../state/selectors/get_settings_specs'; -import { HierarchyOfArrays, CHILDREN_KEY } from '../../layout/utils/group_by_rollup'; +import { CHILDREN_KEY, HierarchyOfArrays } from '../../layout/utils/group_by_rollup'; import { Layer } from '../../specs'; import { getPieSpec } from './pie_spec'; import { getTree } from './tree'; @@ -31,32 +30,10 @@ import { getTree } from './tree'; /** @internal */ export const getLegendItemsLabels = createCachedSelector( [getPieSpec, getSettingsSpecSelector, getTree], - (pieSpec, { legendMaxDepth }, tree): LegendItemLabel[] => { - if (!pieSpec) { - return []; - } - if (isInvalidLegendMaxDepth(legendMaxDepth)) { - return []; - } - const labels = flatSlicesNames(pieSpec.layers, 0, tree).filter(({ depth }) => { - if (typeof legendMaxDepth !== 'number') { - return true; - } - return depth <= legendMaxDepth; - }); - - return labels; - }, + (pieSpec, { legendMaxDepth }, tree): LegendItemLabel[] => + pieSpec ? flatSlicesNames(pieSpec.layers, 0, tree).filter(({ depth }) => depth <= legendMaxDepth) : [], )(getChartIdSelector); -/** - * Check if the legendMaxDepth from settings is not a valid number (NaN or <=0) - * @param legendMaxDepth - SettingsSpec['legendMaxDepth'] - */ -function isInvalidLegendMaxDepth(legendMaxDepth: SettingsSpec['legendMaxDepth']): boolean { - return typeof legendMaxDepth === 'number' && (Number.isNaN(legendMaxDepth) || legendMaxDepth <= 0); -} - function flatSlicesNames( layers: Layer[], depth: number, diff --git a/packages/osd-charts/src/specs/constants.ts b/packages/osd-charts/src/specs/constants.ts index c5e2333f6aaa..68362b4eec67 100644 --- a/packages/osd-charts/src/specs/constants.ts +++ b/packages/osd-charts/src/specs/constants.ts @@ -135,6 +135,7 @@ export const DEFAULT_SETTINGS_SPEC: SettingsSpec = { visible: false, }, }, + legendMaxDepth: Infinity, legendPosition: Position.Right, showLegendExtra: false, hideDuplicateAxes: false, diff --git a/packages/osd-charts/src/specs/settings.tsx b/packages/osd-charts/src/specs/settings.tsx index 527f94269ec8..8aa9093a0b54 100644 --- a/packages/osd-charts/src/specs/settings.tsx +++ b/packages/osd-charts/src/specs/settings.tsx @@ -25,7 +25,7 @@ import { PrimitiveValue } from '../chart_types/partition_chart/layout/utils/grou import { XYChartSeriesIdentifier } from '../chart_types/xy_chart/utils/series'; import { DomainRange } from '../chart_types/xy_chart/utils/specs'; import { SeriesIdentifier } from '../commons/series_id'; -import { TooltipPortalSettings } from '../components/portal'; +import { TooltipPortalSettings } from '../components'; import { CustomTooltip } from '../components/tooltip/types'; import { ScaleContinuousType, ScaleOrdinalType } from '../scales'; import { getConnect, specComponentFactory } from '../state/spec_factory'; @@ -349,7 +349,7 @@ export interface SettingsSpec extends Spec { /** * Limit the legend to a max depth when showing a hierarchical legend */ - legendMaxDepth?: number; + legendMaxDepth: number; /** * Display the legend as a flat hierarchy */ @@ -453,6 +453,7 @@ export type DefaultSettingsProps = | 'showLegendExtra' | 'theme' | 'legendPosition' + | 'legendMaxDepth' | 'hideDuplicateAxes' | 'brushAxis' | 'minBrushDelta' @@ -466,12 +467,12 @@ export const Settings: React.FunctionComponent = getConnect() /** @internal */ export function isPointerOutEvent(event: PointerEvent | null | undefined): event is PointerOutEvent { - return event !== null && event !== undefined && event.type === PointerEventType.Out; + return event?.type === PointerEventType.Out; } /** @internal */ export function isPointerOverEvent(event: PointerEvent | null | undefined): event is PointerOverEvent { - return event !== null && event !== undefined && event.type === PointerEventType.Over; + return event?.type === PointerEventType.Over; } /** @internal */ @@ -481,7 +482,7 @@ export function isTooltipProps(config: TooltipType | TooltipProps): config is To /** @internal */ export function isTooltipType(config: TooltipType | TooltipProps): config is TooltipType { - return typeof config === 'string'; + return typeof config !== 'object'; // TooltipType is 'vertical'|'cross'|'follow'|'none' while TooltipProps is object } /** @internal */ diff --git a/packages/osd-charts/src/state/chart_state.ts b/packages/osd-charts/src/state/chart_state.ts index 6fcd3262fb38..67869ca6db5d 100644 --- a/packages/osd-charts/src/state/chart_state.ts +++ b/packages/osd-charts/src/state/chart_state.ts @@ -28,7 +28,7 @@ import { LegendItem, LegendItemExtraValues } from '../commons/legend'; import { SeriesKey, SeriesIdentifier } from '../commons/series_id'; import { TooltipInfo, TooltipAnchorPosition } from '../components/tooltip/types'; import { Spec, PointerEvent } from '../specs'; -import { DEFAULT_SETTINGS_SPEC } from '../specs/constants'; +import { DEFAULT_SETTINGS_SPEC } from '../specs'; import { Color } from '../utils/commons'; import { Dimensions } from '../utils/dimensions'; import { Logger } from '../utils/logger'; diff --git a/packages/osd-charts/src/state/selectors/get_legend_items_labels.ts b/packages/osd-charts/src/state/selectors/get_legend_items_labels.ts index d53b1250610b..bc9524bf9077 100644 --- a/packages/osd-charts/src/state/selectors/get_legend_items_labels.ts +++ b/packages/osd-charts/src/state/selectors/get_legend_items_labels.ts @@ -26,9 +26,5 @@ export interface LegendItemLabel { } /** @internal */ -export const getLegendItemsLabelsSelector = (state: GlobalChartState): LegendItemLabel[] => { - if (state.internalChartState) { - return state.internalChartState.getLegendItemsLabels(state); - } - return []; -}; +export const getLegendItemsLabelsSelector = (state: GlobalChartState): LegendItemLabel[] => + state.internalChartState?.getLegendItemsLabels(state) ?? []; diff --git a/packages/osd-charts/stories/legend/10_sunburst_repeated_label.tsx b/packages/osd-charts/stories/legend/10_sunburst_repeated_label.tsx new file mode 100644 index 000000000000..88eccb6f0cfb --- /dev/null +++ b/packages/osd-charts/stories/legend/10_sunburst_repeated_label.tsx @@ -0,0 +1,69 @@ +/* + * 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 { boolean, number } from '@storybook/addon-knobs'; +import React from 'react'; + +import { Chart, Partition, Settings } from '../../src'; +import { STORYBOOK_LIGHT_THEME } from '../shared'; + +export const Example = () => { + const flatLegend = boolean('flatLegend', false); + const legendMaxDepth = number('legendMaxDepth', 2, { + min: 0, + max: 3, + step: 1, + }); + + type TestDatum = { cat1: string; cat2: string; val: number }; + + return ( + + + d.val} + layers={[ + { + groupByRollup: (d: TestDatum) => d.cat1, + }, + { + groupByRollup: (d: TestDatum) => d.cat2, + }, + ]} + /> + + ); +}; + +Example.story = { + parameters: { + info: { + text: `Nested legend with reused node labels means that they can reoccur in various points of the legend tree.`, + }, + }, +}; diff --git a/packages/osd-charts/stories/legend/legend.stories.tsx b/packages/osd-charts/stories/legend/legend.stories.tsx index 846a71f17483..1dd13e9211b3 100644 --- a/packages/osd-charts/stories/legend/legend.stories.tsx +++ b/packages/osd-charts/stories/legend/legend.stories.tsx @@ -36,5 +36,6 @@ export { Example as displayValuesInLegendElements } from './7_display_values'; export { Example as legendSpacingBuffer } from './8_spacing_buffer'; export { Example as colorPicker } from './9_color_picker'; export { Example as piechart } from './10_sunburst'; +export { Example as piechartRepeatedLabels } from './10_sunburst_repeated_label'; export { Example as actions } from './11_legend_actions'; export { Example as margins } from './12_legend_margins';