@centreon/ui 24.12.3 → 24.12.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@centreon/ui",
3
- "version": "24.12.3",
3
+ "version": "24.12.5",
4
4
  "description": "Centreon UI Components",
5
5
  "scripts": {
6
6
  "update:deps": "pnpx npm-check-updates -i --format group",
@@ -55,21 +55,43 @@ describe('Bar stack', () => {
55
55
  it('adjusts size based on the provided width and height', () => {
56
56
  initialize({ displayLegend: false, height: '300px', width: '300px' });
57
57
 
58
- cy.findByTestId('barStack').should('have.css', 'height', '270px');
58
+ cy.get('.visx-bar-rounded')
59
+ .eq(0)
60
+ .should(
61
+ 'have.attr',
62
+ 'd',
63
+ 'M8,95.18828451882847 h193 h8v8 v138.81171548117152 a8,8 0 0 1 -8,8 h-193 a8,8 0 0 1 -8,-8 v-138.81171548117152 v-8h8z'
64
+ );
59
65
 
60
66
  cy.makeSnapshot();
61
67
  });
62
68
 
63
69
  it('renders as a horizontal bar when variant is set to "horizontal"', () => {
64
70
  initialize({ variant: 'horizontal' });
65
- cy.get('[data-variant="horizontal"]').should('exist');
71
+
72
+ cy.get('.visx-bar-rounded')
73
+ .eq(0)
74
+ .should(
75
+ 'have.attr',
76
+ 'd',
77
+ 'M8,0 h231.69874476987445 h8v8 v295 v8h-8 h-231.69874476987445 a8,8 0 0 1 -8,-8 v-295 a8,8 0 0 1 8,-8z'
78
+ );
79
+ cy.get('[data-is-vertical="false"]').should('be.visible');
66
80
 
67
81
  cy.makeSnapshot();
68
82
  });
69
83
 
70
84
  it('renders as a vertical bar when variant is set to "vertical"', () => {
71
85
  initialize({ variant: 'vertical' });
72
- cy.get('[data-variant="vertical"]').should('exist');
86
+
87
+ cy.get('.visx-bar-rounded')
88
+ .eq(0)
89
+ .should(
90
+ 'have.attr',
91
+ 'd',
92
+ 'M8,133.26359832635984 h293 h8v8 v200.73640167364016 a8,8 0 0 1 -8,8 h-293 a8,8 0 0 1 -8,-8 v-200.73640167364016 v-8h8z'
93
+ );
94
+ cy.get('[data-is-vertical="true"]').should('be.visible');
73
95
 
74
96
  cy.makeSnapshot();
75
97
  });
@@ -77,6 +99,14 @@ describe('Bar stack', () => {
77
99
  it('displays tooltip with correct information on hover', () => {
78
100
  initialize({ TooltipContent });
79
101
 
102
+ cy.get('.visx-bar-rounded')
103
+ .eq(0)
104
+ .should(
105
+ 'have.attr',
106
+ 'd',
107
+ 'M8,133.26359832635984 h293 h8v8 v200.73640167364016 a8,8 0 0 1 -8,8 h-293 a8,8 0 0 1 -8,-8 v-200.73640167364016 v-8h8z'
108
+ );
109
+
80
110
  defaultData.forEach(({ label, value }) => {
81
111
  cy.findByTestId(label).trigger('mouseover', { force: true });
82
112
 
@@ -90,6 +120,14 @@ describe('Bar stack', () => {
90
120
 
91
121
  it('conditionally displays values on rects based on displayValues prop', () => {
92
122
  initialize({ displayValues: true });
123
+
124
+ cy.get('.visx-bar-rounded')
125
+ .eq(0)
126
+ .should(
127
+ 'have.attr',
128
+ 'd',
129
+ 'M8,133.26359832635984 h293 h8v8 v200.73640167364016 a8,8 0 0 1 -8,8 h-293 a8,8 0 0 1 -8,-8 v-200.73640167364016 v-8h8z'
130
+ );
93
131
  defaultData.forEach(({ value }, index) => {
94
132
  cy.findAllByTestId('value')
95
133
  .eq(index)
@@ -106,6 +144,14 @@ describe('Bar stack', () => {
106
144
 
107
145
  it('displays values on rects in percentage unit when displayValues is set to true and unit to percentage', () => {
108
146
  initialize({ displayValues: true, unit: 'percentage' });
147
+
148
+ cy.get('.visx-bar-rounded')
149
+ .eq(0)
150
+ .should(
151
+ 'have.attr',
152
+ 'd',
153
+ 'M8,133.26359832635984 h293 h8v8 v200.73640167364016 a8,8 0 0 1 -8,8 h-293 a8,8 0 0 1 -8,-8 v-200.73640167364016 v-8h8z'
154
+ );
109
155
  defaultData.forEach(({ value }, index) => {
110
156
  cy.findAllByTestId('value')
111
157
  .eq(index)
@@ -118,21 +164,53 @@ describe('Bar stack', () => {
118
164
  });
119
165
 
120
166
  it('displays Legend component based on displayLegend prop', () => {
121
- initialize({ displayLegend: true });
122
- cy.findByTestId('Legend').should('be.visible');
123
-
124
167
  initialize({ displayLegend: false });
168
+
169
+ cy.get('.visx-bar-rounded')
170
+ .eq(0)
171
+ .should(
172
+ 'have.attr',
173
+ 'd',
174
+ 'M8,133.26359832635984 h293 h8v8 v200.73640167364016 a8,8 0 0 1 -8,8 h-293 a8,8 0 0 1 -8,-8 v-200.73640167364016 v-8h8z'
175
+ );
176
+ cy.findByTestId('Ok').should('be.visible');
125
177
  cy.findByTestId('Legend').should('not.exist');
126
178
 
127
179
  cy.makeSnapshot();
128
180
  });
129
181
 
130
- it('displays the title when the title is giving', () => {
182
+ it('displays the title when the title is given', () => {
131
183
  initialize({ title: 'host' });
184
+
185
+ cy.get('.visx-bar-rounded')
186
+ .eq(0)
187
+ .should(
188
+ 'have.attr',
189
+ 'd',
190
+ 'M8,133.26359832635984 h293 h8v8 v200.73640167364016 a8,8 0 0 1 -8,8 h-293 a8,8 0 0 1 -8,-8 v-200.73640167364016 v-8h8z'
191
+ );
192
+ cy.findByTestId('Ok').should('be.visible');
132
193
  cy.findByTestId('Title').should('be.visible');
133
194
 
134
- initialize({});
135
- cy.findByTestId('Title').should('not.exist');
195
+ cy.makeSnapshot();
196
+ });
197
+
198
+ it('displays the bars within a small display', () => {
199
+ initialize({
200
+ width: '120px',
201
+ height: '89px',
202
+ title: 'host',
203
+ displayLegend: true
204
+ });
205
+
206
+ cy.get('.visx-bar-rounded')
207
+ .eq(0)
208
+ .should(
209
+ 'have.attr',
210
+ 'd',
211
+ 'M8,20.941422594142264 h94 h8v8 v18.058577405857733 a8,8 0 0 1 -8,8 h-94 a8,8 0 0 1 -8,-8 v-18.058577405857733 v-8h8z'
212
+ );
213
+ cy.findByTestId('Ok').should('be.visible');
136
214
 
137
215
  cy.makeSnapshot();
138
216
  });
@@ -1,6 +1,6 @@
1
1
  import { Meta, StoryObj } from '@storybook/react';
2
2
 
3
- import ResponsiveBarStack from './ResponsiveBarStack';
3
+ import ResponsiveBarStack from './BarStack';
4
4
  import { BarType } from './models';
5
5
 
6
6
  import { BarStack } from '.';
@@ -47,11 +47,19 @@ const TooltipContent = ({ label, color, value }: BarType): JSX.Element => {
47
47
  };
48
48
 
49
49
  const Template = (args): JSX.Element => {
50
- return <ResponsiveBarStack height={300} width={500} {...args} />;
50
+ return (
51
+ <div style={{ width: '400px', height: '400px' }}>
52
+ <ResponsiveBarStack {...args} />
53
+ </div>
54
+ );
51
55
  };
52
56
 
53
57
  const SmallTemplate = (args): JSX.Element => {
54
- return <ResponsiveBarStack height={120} width={120} {...args} />;
58
+ return (
59
+ <div style={{ width: '150px', height: '90px' }}>
60
+ <ResponsiveBarStack {...args} />
61
+ </div>
62
+ );
55
63
  };
56
64
 
57
65
  export const Vertical: Story = {
@@ -111,7 +119,8 @@ export const Horizontal: Story = {
111
119
  data,
112
120
  displayValues: true,
113
121
  title: 'hosts',
114
- variant: 'horizontal'
122
+ variant: 'horizontal',
123
+ legendDirection: 'row'
115
124
  },
116
125
  render: Template
117
126
  };
@@ -1,41 +1,65 @@
1
1
  import { makeStyles } from 'tss-react/mui';
2
+ import { legendMaxHeight, legendMaxWidth } from './constants';
2
3
 
3
- export const useBarStackStyles = makeStyles()((theme) => ({
4
- barStackTooltip: {
5
- backgroundColor: theme.palette.background.paper,
6
- color: theme.palette.text.primary,
7
- padding: 0,
8
- position: 'relative'
9
- },
4
+ export const useStyles = makeStyles()({
10
5
  container: {
11
- alignItems: 'center',
12
- display: 'flex',
13
- gap: theme.spacing(1.5),
14
- justifyContent: 'center'
6
+ display: 'grid',
7
+ '&[data-has-title="false"]': {
8
+ gridTemplateRows: 'auto'
9
+ },
10
+ '&[data-title-variant="xs"]': {
11
+ gridTemplateRows: '40px auto'
12
+ },
13
+ '&[data-title-variant="sm"]': {
14
+ gridTemplateRows: '20px auto'
15
+ },
16
+ '&[data-title-variant="md"]': {
17
+ gridTemplateRows: '36px auto',
18
+ textOverflow: 'clip',
19
+ overflow: 'hidden'
20
+ },
21
+ height: '100%'
15
22
  },
16
- smallTitle: {
17
- fontSize: theme.typography.body1.fontSize
18
- },
19
- svgContainer: {
20
- alignItems: 'center',
21
- backgroundColor: theme.palette.background.panelGroups,
22
- borderRadius: theme.spacing(1.25),
23
- display: 'flex',
24
- justifyContent: 'center',
25
- padding: theme.spacing(1)
26
- },
27
- svgWrapper: {
28
- alignItems: 'center',
29
- display: 'flex',
30
- flexDirection: 'column',
31
- gap: theme.spacing(1),
32
- justifyContent: 'center'
23
+ clippedTitle: {
24
+ textOverflow: 'clip',
25
+ overflow: 'hidden'
26
+ }
27
+ });
28
+
29
+ export const useGraphAndLegendStyles = makeStyles()((theme) => ({
30
+ graphAndLegend: {
31
+ height: '100%',
32
+ display: 'grid',
33
+ '&[data-is-vertical="true"][data-display-legend="false"]': {
34
+ gridTemplateColumns: '1fr'
35
+ },
36
+ '&[data-is-vertical="true"][data-display-legend="true"]': {
37
+ gridTemplateColumns: `1fr ${legendMaxWidth}px`,
38
+ gap: theme.spacing(0.5)
39
+ },
40
+ '&[data-display-legend="false"][data-is-vertical="false"]': {
41
+ gridTemplateRows: '1fr'
42
+ },
43
+ '&[data-display-legend="true"][data-is-vertical="false"]': {
44
+ gridTemplateRows: `1fr ${legendMaxHeight}px`,
45
+ gap: theme.spacing(0.5)
46
+ }
33
47
  },
34
- title: {
35
- fontSize: theme.typography.h6.fontSize,
36
- fontWeight: theme.typography.fontWeightMedium,
37
- margin: 0,
48
+ legend: {
49
+ '&[data-is-vertical="false"]': {
50
+ overflowY: 'auto'
51
+ },
52
+ '&[data-is-vertical="true"]': {
53
+ alignSelf: 'center'
54
+ }
55
+ }
56
+ }));
57
+
58
+ export const useGraphStyles = makeStyles()((theme) => ({
59
+ tooltip: {
60
+ backgroundColor: theme.palette.background.paper,
61
+ color: theme.palette.text.primary,
38
62
  padding: 0,
39
- textAlign: 'center'
63
+ boxShadow: theme.shadows[3]
40
64
  }
41
65
  }));
@@ -0,0 +1,173 @@
1
+ import {
2
+ BarRounded,
3
+ BarStackHorizontal,
4
+ BarStack as BarStackVertical
5
+ } from '@visx/shape';
6
+ import { Text } from '@visx/text';
7
+ import { equals, props } from 'ramda';
8
+ import { memo, useMemo } from 'react';
9
+ import { Tooltip } from '../../components';
10
+ import { getValueByUnit } from '../common/utils';
11
+ import { useGraphStyles } from './BarStack.styles';
12
+ import { BarStackProps } from './models';
13
+ import { useGraphAndLegend } from './useGraphAndLegend';
14
+
15
+ interface Props
16
+ extends Pick<
17
+ BarStackProps,
18
+ | 'data'
19
+ | 'displayValues'
20
+ | 'onSingleBarClick'
21
+ | 'unit'
22
+ | 'TooltipContent'
23
+ | 'tooltipProps'
24
+ > {
25
+ width: number;
26
+ height: number;
27
+ isVerticalBar: boolean;
28
+ colorScale;
29
+ total: number;
30
+ }
31
+
32
+ const Graph = ({
33
+ width,
34
+ height,
35
+ isVerticalBar,
36
+ colorScale,
37
+ data,
38
+ total,
39
+ unit,
40
+ displayValues,
41
+ onSingleBarClick,
42
+ tooltipProps,
43
+ TooltipContent
44
+ }: Props): JSX.Element => {
45
+ const { classes } = useGraphStyles();
46
+
47
+ const BarStackComponent = useMemo(
48
+ () => (isVerticalBar ? BarStackVertical : BarStackHorizontal),
49
+ [isVerticalBar]
50
+ );
51
+
52
+ const normalizedHeight = useMemo(() => height - 10, [height]);
53
+
54
+ const { barStackData, xScale, yScale, keys } = useGraphAndLegend({
55
+ data,
56
+ width,
57
+ height: normalizedHeight,
58
+ isVerticalBar,
59
+ total
60
+ });
61
+
62
+ return (
63
+ <svg width="100%" height={normalizedHeight}>
64
+ <BarStackComponent
65
+ color={colorScale}
66
+ data={[barStackData]}
67
+ keys={keys}
68
+ {...(isVerticalBar ? { x: () => undefined } : { y: () => undefined })}
69
+ xScale={xScale}
70
+ yScale={yScale}
71
+ >
72
+ {(barStacks) =>
73
+ barStacks.map((barStack, index) =>
74
+ barStack.bars.map((bar) => {
75
+ const isFirstBar = equals(index, 0);
76
+ const isLastBar = equals(index, barStacks.length - 1);
77
+ const fitsInBar = isVerticalBar
78
+ ? bar.height >= 18
79
+ : (equals(unit, 'number') && bar.width > 15) ||
80
+ (equals(unit, 'percentage') && bar.width > 35);
81
+
82
+ const textX = bar.x + bar.width / 2;
83
+ const textY = bar.y + bar.height / 2;
84
+
85
+ const click = onSingleBarClick
86
+ ? (e: MouseEvent): void => {
87
+ if (!equals(e.button, 0)) {
88
+ return;
89
+ }
90
+ onSingleBarClick(bar);
91
+ }
92
+ : undefined;
93
+
94
+ return (
95
+ <Tooltip
96
+ followCursor={false}
97
+ classes={classes}
98
+ key={`bar-stack-${barStack.index}-${bar.index}`}
99
+ label={
100
+ TooltipContent && (
101
+ <TooltipContent
102
+ color={bar.color}
103
+ label={bar.key}
104
+ total={total}
105
+ value={barStack.bars[0].bar.data[barStack.key]}
106
+ {...tooltipProps}
107
+ />
108
+ )
109
+ }
110
+ position={isVerticalBar ? 'right' : 'bottom'}
111
+ >
112
+ <g data-testid={bar.key} key={bar.key}>
113
+ <BarRounded
114
+ radius={8}
115
+ cursor={onSingleBarClick ? 'pointer' : 'default'}
116
+ fill={bar.color}
117
+ height={bar.height}
118
+ key={`bar-stack-${barStack.index}-${bar.index}`}
119
+ width={isVerticalBar ? bar.width - 10 : bar.width}
120
+ x={bar.x}
121
+ y={bar.y}
122
+ left={!isVerticalBar && isFirstBar}
123
+ right={!isVerticalBar && isLastBar}
124
+ bottom={isVerticalBar && isFirstBar}
125
+ top={isVerticalBar && isLastBar}
126
+ onMouseDown={click}
127
+ />
128
+ {displayValues && fitsInBar && (
129
+ <Text
130
+ cursor={onSingleBarClick ? 'pointer' : 'default'}
131
+ data-testid="value"
132
+ fill="#000"
133
+ fontSize={12}
134
+ fontWeight={600}
135
+ textAnchor="middle"
136
+ verticalAnchor="middle"
137
+ x={textX}
138
+ y={textY}
139
+ onMouseUp={click}
140
+ >
141
+ {getValueByUnit({
142
+ total,
143
+ unit: unit || 'number',
144
+ value: barStack.bars[0].bar.data[barStack.key]
145
+ })}
146
+ </Text>
147
+ )}
148
+ </g>
149
+ </Tooltip>
150
+ );
151
+ })
152
+ )
153
+ }
154
+ </BarStackComponent>
155
+ </svg>
156
+ );
157
+ };
158
+
159
+ const propsToMemoize = [
160
+ 'width',
161
+ 'height',
162
+ 'isVerticalBar',
163
+ 'colorScale',
164
+ 'data',
165
+ 'total',
166
+ 'unit',
167
+ 'displayValues',
168
+ 'tooltipProps'
169
+ ];
170
+
171
+ export default memo(Graph, (prevProps, nextProps) =>
172
+ equals(props(propsToMemoize, prevProps), props(propsToMemoize, nextProps))
173
+ );
@@ -0,0 +1,117 @@
1
+ import { equals, props } from 'ramda';
2
+ import { memo, useMemo } from 'react';
3
+ import { useGraphAndLegendStyles } from './BarStack.styles';
4
+ import Graph from './Graph';
5
+ import { gap, legendMaxHeight, legendMaxWidth } from './constants';
6
+ import { BarStackProps } from './models';
7
+
8
+ interface Props
9
+ extends Pick<
10
+ BarStackProps,
11
+ | 'data'
12
+ | 'displayValues'
13
+ | 'onSingleBarClick'
14
+ | 'unit'
15
+ | 'TooltipContent'
16
+ | 'tooltipProps'
17
+ > {
18
+ isVerticalBar: boolean;
19
+ legend: JSX.Element;
20
+ displayLegend: boolean;
21
+ height: number;
22
+ width: number;
23
+ colorScale;
24
+ total: number;
25
+ }
26
+
27
+ const GraphAndLegend = ({
28
+ isVerticalBar,
29
+ legend,
30
+ displayLegend,
31
+ height,
32
+ width,
33
+ colorScale,
34
+ total,
35
+ unit,
36
+ data,
37
+ displayValues,
38
+ onSingleBarClick,
39
+ tooltipProps,
40
+ TooltipContent
41
+ }: Props): JSX.Element => {
42
+ const { classes } = useGraphAndLegendStyles();
43
+
44
+ const isSmall = useMemo(
45
+ () =>
46
+ (!isVerticalBar && Math.floor(height) <= 80) ||
47
+ (isVerticalBar && Math.floor(width) <= 150),
48
+ [isVerticalBar, height, width]
49
+ );
50
+
51
+ const mustDisplayLegend = useMemo(
52
+ () => displayLegend && !isSmall,
53
+ [isSmall, displayLegend]
54
+ );
55
+
56
+ const graphWidth = useMemo(
57
+ () =>
58
+ isVerticalBar ? width - (isSmall ? 0 : legendMaxWidth - gap) : width,
59
+ [width, isVerticalBar, isSmall]
60
+ );
61
+
62
+ const graphHeight = useMemo(
63
+ () =>
64
+ isVerticalBar ? height : height - (isSmall ? 0 : legendMaxHeight - gap),
65
+ [height, isVerticalBar, isSmall]
66
+ );
67
+
68
+ return (
69
+ <div
70
+ className={classes.graphAndLegend}
71
+ data-display-legend={mustDisplayLegend}
72
+ data-is-vertical={isVerticalBar}
73
+ style={{ height }}
74
+ >
75
+ <Graph
76
+ isVerticalBar={isVerticalBar}
77
+ data={data}
78
+ width={graphWidth}
79
+ height={graphHeight}
80
+ colorScale={colorScale}
81
+ unit={unit}
82
+ total={total}
83
+ displayValues={displayValues}
84
+ onSingleBarClick={onSingleBarClick}
85
+ tooltipProps={tooltipProps}
86
+ TooltipContent={TooltipContent}
87
+ />
88
+ {mustDisplayLegend && (
89
+ <div
90
+ className={classes.legend}
91
+ data-is-vertical={isVerticalBar}
92
+ data-testid="Legend"
93
+ >
94
+ {legend}
95
+ </div>
96
+ )}
97
+ </div>
98
+ );
99
+ };
100
+
101
+ const propsToMemoize = [
102
+ 'width',
103
+ 'height',
104
+ 'isVerticalBar',
105
+ 'colorScale',
106
+ 'data',
107
+ 'total',
108
+ 'unit',
109
+ 'displayValues',
110
+ 'tooltipProps',
111
+ 'displayLegend',
112
+ 'legend'
113
+ ];
114
+
115
+ export default memo(GraphAndLegend, (prevProps, nextProps) =>
116
+ equals(props(propsToMemoize, prevProps), props(propsToMemoize, nextProps))
117
+ );
@@ -1,18 +1,14 @@
1
- import { useRef } from 'react';
2
-
3
- import { Group } from '@visx/group';
4
- import { BarStackHorizontal, BarStack as BarStackVertical } from '@visx/shape';
5
- import { Text } from '@visx/text';
6
- import numeral from 'numeral';
7
- import { equals, lt } from 'ramda';
8
- import { useTranslation } from 'react-i18next';
9
-
10
- import { Tooltip } from '../../components';
11
1
  import { Legend as LegendComponent } from '../Legend';
12
2
  import { LegendProps } from '../Legend/models';
13
- import { getValueByUnit } from '../common/utils';
3
+ import { useStyles } from './BarStack.styles';
14
4
 
15
- import { useBarStackStyles } from './BarStack.styles';
5
+ import { Typography } from '@mui/material';
6
+ import numeral from 'numeral';
7
+ import { equals } from 'ramda';
8
+ import { useMemo } from 'react';
9
+ import { useTranslation } from 'react-i18next';
10
+ import GraphAndLegend from './GraphAndLegend';
11
+ import { gap, smallTitleHeight, titleHeight } from './constants';
16
12
  import { BarStackProps } from './models';
17
13
  import useResponsiveBarStack from './useResponsiveBarStack';
18
14
 
@@ -23,9 +19,8 @@ const DefaultLengd = ({ scale, direction }: LegendProps): JSX.Element => (
23
19
  const ResponsiveBarStack = ({
24
20
  title,
25
21
  data,
26
- width,
27
22
  height,
28
- size = 72,
23
+ width,
29
24
  onSingleBarClick,
30
25
  displayLegend = true,
31
26
  TooltipContent,
@@ -33,184 +28,82 @@ const ResponsiveBarStack = ({
33
28
  unit = 'number',
34
29
  displayValues,
35
30
  variant = 'vertical',
36
- legendDirection = 'column',
31
+ legendDirection,
37
32
  tooltipProps = {}
38
33
  }: BarStackProps & { height: number; width: number }): JSX.Element => {
39
34
  const { t } = useTranslation();
40
- const { classes, cx } = useBarStackStyles();
41
-
42
- const titleRef = useRef(null);
43
- const legendRef = useRef(null);
35
+ const { classes, cx } = useStyles();
44
36
 
45
37
  const {
46
- barSize,
47
- colorScale,
48
- input,
49
- keys,
50
- legendScale,
51
38
  total,
52
- xScale,
53
- yScale,
54
- svgContainerSize,
55
- isVerticalBar
39
+ titleVariant,
40
+ legendScale,
41
+ isVerticalBar,
42
+ colorScale,
43
+ formattedLegendDirection
56
44
  } = useResponsiveBarStack({
57
45
  data,
58
46
  height,
59
- legendRef,
60
- size,
61
- titleRef,
47
+ width,
62
48
  unit,
63
49
  variant,
64
- width
50
+ legendDirection
65
51
  });
66
52
 
67
- const BarStackComponent = isVerticalBar
68
- ? BarStackVertical
69
- : BarStackHorizontal;
53
+ const graphAndLegendHeight = useMemo(() => {
54
+ if (equals(titleVariant, 'xs')) {
55
+ return height - 2 * smallTitleHeight - gap;
56
+ }
70
57
 
71
- const isSmallHeight = isVerticalBar ? lt(height, 190) : lt(height, 100);
72
- const isSmallWidth = isVerticalBar ? lt(width, 80) : lt(width, 350);
73
- const mustDisplayLegend = isSmallWidth ? false : displayLegend;
58
+ if (equals(titleVariant, 'sm')) {
59
+ return height - smallTitleHeight - gap;
60
+ }
61
+
62
+ return height - titleHeight - gap;
63
+ }, [titleVariant, height]);
74
64
 
75
65
  return (
76
- <div className={classes.container} style={{ width }}>
77
- <div className={classes.svgWrapper}>
78
- {title && (
79
- <div
80
- className={cx(classes.title, isSmallHeight && classes.smallTitle)}
81
- data-testid="Title"
82
- ref={titleRef}
83
- >
84
- {`${numeral(total).format('0a').toUpperCase()} `} {t(title)}
85
- </div>
86
- )}
87
- <div
88
- className={classes.svgContainer}
89
- data-testid="barStack"
90
- style={{
91
- width: svgContainerSize.width
92
- }}
66
+ <div
67
+ className={classes.container}
68
+ data-has-title={!!title}
69
+ data-title-variant={titleVariant}
70
+ data-variant={variant}
71
+ >
72
+ {title && (
73
+ <Typography
74
+ data-testid="Title"
75
+ variant={equals(titleVariant, 'md') ? 'h6' : 'body1'}
76
+ textAlign="center"
77
+ fontWeight="bold"
78
+ className={cx(equals(titleVariant, 'md') && classes.clippedTitle)}
93
79
  >
94
- <svg
95
- data-variant={variant}
96
- height={barSize.height}
97
- width={barSize.width}
98
- >
99
- <Group>
100
- <BarStackComponent
101
- color={colorScale}
102
- data={[input]}
103
- keys={keys}
104
- {...(isVerticalBar
105
- ? { x: () => undefined }
106
- : { y: () => undefined })}
107
- xScale={xScale}
108
- yScale={yScale}
109
- >
110
- {(barStacks) =>
111
- barStacks.map((barStack) =>
112
- barStack.bars.map((bar) => {
113
- const shouldDisplayValues = (): boolean => {
114
- if (bar.height < 10) {
115
- return false;
116
- }
117
-
118
- return (
119
- (equals(unit, 'number') && bar.width > 10) ||
120
- (equals(unit, 'percentage') && bar.width > 25)
121
- );
122
- };
123
-
124
- const TextX = bar.x + bar.width / 2;
125
- const TextY = bar.y + bar.height / 2;
126
-
127
- const onClick = (): void => {
128
- onSingleBarClick?.(bar);
129
- };
130
-
131
- return (
132
- <Tooltip
133
- hasCaret
134
- classes={{
135
- tooltip: classes.barStackTooltip
136
- }}
137
- followCursor={false}
138
- key={`bar-stack-${barStack.index}-${bar.index}`}
139
- label={
140
- TooltipContent && (
141
- <TooltipContent
142
- color={bar.color}
143
- label={bar.key}
144
- title={title}
145
- total={total}
146
- value={barStack.bars[0].bar.data[barStack.key]}
147
- {...tooltipProps}
148
- />
149
- )
150
- }
151
- position={
152
- isVerticalBar ? 'right-start' : 'bottom-start'
153
- }
154
- >
155
- <g
156
- data-testid={bar.key}
157
- onClick={onClick}
158
- onKeyUp={() => undefined}
159
- >
160
- <rect
161
- cursor="pointer"
162
- fill={bar.color}
163
- height={
164
- isVerticalBar ? bar.height - 1 : bar.height
165
- }
166
- key={`bar-stack-${barStack.index}-${bar.index}`}
167
- ry={4}
168
- width={isVerticalBar ? bar.width : bar.width - 1}
169
- x={bar.x}
170
- y={bar.y}
171
- />
172
- {displayValues && shouldDisplayValues() && (
173
- <Text
174
- cursor="pointer"
175
- data-testid="value"
176
- fill="#000"
177
- fontSize={12}
178
- fontWeight={600}
179
- textAnchor="middle"
180
- verticalAnchor="middle"
181
- x={TextX}
182
- y={TextY}
183
- >
184
- {getValueByUnit({
185
- total,
186
- unit,
187
- value: barStack.bars[0].bar.data[barStack.key]
188
- })}
189
- </Text>
190
- )}
191
- </g>
192
- </Tooltip>
193
- );
194
- })
195
- )
196
- }
197
- </BarStackComponent>
198
- </Group>
199
- </svg>
200
- </div>
201
- </div>
202
- {mustDisplayLegend && (
203
- <div data-testid="Legend" ref={legendRef}>
80
+ {`${numeral(total).format('0a')}`} {t(title)}
81
+ </Typography>
82
+ )}
83
+ <GraphAndLegend
84
+ height={graphAndLegendHeight}
85
+ width={width}
86
+ isVerticalBar={isVerticalBar}
87
+ displayLegend={displayLegend}
88
+ colorScale={colorScale}
89
+ total={total}
90
+ data={data}
91
+ unit={unit}
92
+ displayValues={displayValues}
93
+ onSingleBarClick={onSingleBarClick}
94
+ tooltipProps={tooltipProps}
95
+ TooltipContent={TooltipContent}
96
+ legend={
204
97
  <Legend
205
98
  data={data}
206
- direction={legendDirection}
99
+ direction={formattedLegendDirection}
207
100
  scale={legendScale}
208
101
  title={title}
209
102
  total={total}
210
103
  unit={unit}
211
104
  />
212
- </div>
213
- )}
105
+ }
106
+ />
214
107
  </div>
215
108
  );
216
109
  };
@@ -0,0 +1,5 @@
1
+ export const legendMaxWidth = 85;
2
+ export const legendMaxHeight = 43;
3
+ export const titleHeight = 36;
4
+ export const smallTitleHeight = 20;
5
+ export const gap = 4;
@@ -12,7 +12,6 @@ export type BarStackProps = {
12
12
  displayValues?: boolean;
13
13
  legendDirection?: 'row' | 'column';
14
14
  onSingleBarClick?: (barData) => void;
15
- size?: number;
16
15
  title?: string;
17
16
  tooltipProps?: object;
18
17
  unit?: 'percentage' | 'number';
@@ -0,0 +1,84 @@
1
+ import { scaleBand, scaleLinear } from '@visx/scale';
2
+ import { pluck } from 'ramda';
3
+ import { useMemo } from 'react';
4
+ import { BarType } from './models';
5
+
6
+ interface UseGraphandLegendProps {
7
+ data: Array<BarType>;
8
+ isVerticalBar: boolean;
9
+ total: number;
10
+ width: number;
11
+ height: number;
12
+ }
13
+
14
+ interface UseGraphAndLegendState {
15
+ barStackData: Record<string, number>;
16
+ xScale;
17
+ yScale;
18
+ keys: Array<string>;
19
+ }
20
+
21
+ export const useGraphAndLegend = ({
22
+ data,
23
+ isVerticalBar,
24
+ total,
25
+ width,
26
+ height
27
+ }: UseGraphandLegendProps): UseGraphAndLegendState => {
28
+ const dataWithNonEmptyValue = useMemo(
29
+ () => data.filter(({ value }) => value),
30
+ [data]
31
+ );
32
+
33
+ const keys = useMemo(
34
+ () => pluck('label', dataWithNonEmptyValue),
35
+ [dataWithNonEmptyValue]
36
+ );
37
+
38
+ const barStackData = useMemo(
39
+ () =>
40
+ dataWithNonEmptyValue.reduce((acc, { label, value }) => {
41
+ acc[label] = value;
42
+
43
+ return acc;
44
+ }, {}),
45
+ [dataWithNonEmptyValue]
46
+ );
47
+
48
+ const yScale = useMemo(
49
+ () =>
50
+ isVerticalBar
51
+ ? scaleLinear({
52
+ domain: [0, total],
53
+ range: [height, 0]
54
+ })
55
+ : scaleBand({
56
+ domain: [0, 0],
57
+ padding: 0,
58
+ range: [height, 0]
59
+ }),
60
+ [isVerticalBar, total, height]
61
+ );
62
+
63
+ const xScale = useMemo(
64
+ () =>
65
+ isVerticalBar
66
+ ? scaleBand({
67
+ domain: [0, 0],
68
+ padding: 0,
69
+ range: [0, width]
70
+ })
71
+ : scaleLinear({
72
+ domain: [0, total],
73
+ range: [0, width]
74
+ }),
75
+ [total, width, isVerticalBar]
76
+ );
77
+
78
+ return {
79
+ barStackData,
80
+ xScale,
81
+ yScale,
82
+ keys
83
+ };
84
+ };
@@ -1,37 +1,27 @@
1
- import { scaleBand, scaleLinear, scaleOrdinal } from '@visx/scale';
2
- import { equals, gt, pluck } from 'ramda';
3
-
1
+ import { scaleOrdinal } from '@visx/scale';
2
+ import { equals, isNil, pluck } from 'ramda';
3
+ import { useMemo } from 'react';
4
4
  import { LegendScale } from '../Legend/models';
5
5
  import { getValueByUnit } from '../common/utils';
6
-
7
6
  import { BarType } from './models';
8
7
 
9
- interface Size {
10
- height: number;
11
- width: number;
12
- }
13
-
14
8
  interface UseBarStackProps {
15
9
  data: Array<BarType>;
16
10
  height: number;
17
- legendRef;
18
- size: number;
19
- titleRef;
11
+ width: number;
20
12
  unit?: 'percentage' | 'number';
21
13
  variant?: 'vertical' | 'horizontal';
22
- width: number;
14
+ legendDirection?: 'column' | 'row';
23
15
  }
16
+
24
17
  interface UseBarStackState {
25
- barSize: Size;
26
- colorScale;
27
- input;
18
+ total: number;
19
+ isSmall: boolean;
20
+ titleVariant: 'xs' | 'sm' | 'md';
28
21
  isVerticalBar: boolean;
29
- keys: Array<string>;
30
22
  legendScale: LegendScale;
31
- svgContainerSize: Size;
32
- total: number;
33
- xScale;
34
- yScale;
23
+ colorScale;
24
+ formattedLegendDirection: 'column' | 'row';
35
25
  }
36
26
 
37
27
  const useResponsiveBarStack = ({
@@ -40,87 +30,73 @@ const useResponsiveBarStack = ({
40
30
  height,
41
31
  width,
42
32
  unit = 'number',
43
- titleRef,
44
- legendRef,
45
- size
33
+ legendDirection
46
34
  }: UseBarStackProps): UseBarStackState => {
47
- const isVerticalBar = equals(variant, 'vertical');
48
-
49
- const heightOfTitle = titleRef.current?.offsetHeight || 0;
50
- const widthOfLegend = legendRef.current?.offsetWidth || 0;
51
-
52
- const horizontalGap = widthOfLegend > 0 ? 12 : 0;
53
- const verticalGap = heightOfTitle > 0 ? 8 : 0;
54
-
55
- const svgContainerSize = {
56
- height: isVerticalBar ? height - heightOfTitle - verticalGap : size,
57
- width: isVerticalBar ? size : width - widthOfLegend - horizontalGap
58
- };
59
-
60
- const barSize = {
61
- height: gt(height / 2, svgContainerSize.height - 16)
62
- ? svgContainerSize.height - 16
63
- : svgContainerSize.height - 46,
64
- width: svgContainerSize.width - 16
65
- };
66
-
67
- const total = Math.floor(data.reduce((acc, { value }) => acc + value, 0));
68
-
69
- const yScale = isVerticalBar
70
- ? scaleLinear({
71
- domain: [0, total]
72
- })
73
- : scaleBand({
74
- domain: [0, 0],
75
- padding: 0
76
- });
77
-
78
- const xScale = isVerticalBar
79
- ? scaleBand({
80
- domain: [0, 0],
81
- padding: 0
82
- })
83
- : scaleLinear({
84
- domain: [0, total]
85
- });
86
-
87
- const keys = pluck('label', data);
88
-
89
- const colorsRange = pluck('color', data);
90
-
91
- const colorScale = scaleOrdinal({
92
- domain: keys,
93
- range: colorsRange
94
- });
95
-
96
- const legendScale = {
97
- domain: data.map(({ value }) => getValueByUnit({ total, unit, value })),
98
- range: colorsRange
99
- };
100
-
101
- const xMax = barSize.width;
102
- const yMax = barSize.height;
103
-
104
- xScale.rangeRound([0, xMax]);
105
- yScale.range([yMax, 0]);
106
-
107
- const input = data.reduce((acc, { label, value }) => {
108
- acc[label] = value;
109
-
110
- return acc;
111
- }, {});
35
+ const total = useMemo(
36
+ () => Math.floor(data.reduce((acc, { value }) => acc + value, 0)),
37
+ [data]
38
+ );
39
+
40
+ const isVerticalBar = useMemo(() => equals(variant, 'vertical'), [variant]);
41
+
42
+ const isSmall = useMemo(
43
+ () => Math.floor(height) < 90,
44
+ [isVerticalBar, height]
45
+ );
46
+
47
+ const titleVariant = useMemo(() => {
48
+ if (width <= 105) {
49
+ return 'xs';
50
+ }
51
+
52
+ if (width <= 150 || isSmall) {
53
+ return 'sm';
54
+ }
55
+
56
+ return 'md';
57
+ }, [isSmall, width]);
58
+
59
+ const keys = useMemo(() => pluck('label', data), [data]);
60
+
61
+ const colorsRange = useMemo(() => pluck('color', data), [data]);
62
+
63
+ const colorScale = useMemo(
64
+ () =>
65
+ scaleOrdinal({
66
+ domain: keys,
67
+ range: colorsRange
68
+ }),
69
+ [keys, colorsRange]
70
+ );
71
+
72
+ const legendScale = useMemo(
73
+ () => ({
74
+ domain: data.map(({ value }) => getValueByUnit({ total, unit, value })),
75
+ range: colorsRange
76
+ }),
77
+ [data, colorsRange]
78
+ );
79
+
80
+ const formattedLegendDirection = useMemo(() => {
81
+ if (!isNil(legendDirection)) {
82
+ return legendDirection;
83
+ }
84
+
85
+ if (equals(variant, 'horizontal')) {
86
+ return 'row';
87
+ }
88
+
89
+ return 'column';
90
+ }, [legendDirection, variant]);
112
91
 
113
92
  return {
114
- barSize,
115
- colorScale,
116
- input,
93
+ total,
94
+ isSmall,
117
95
  isVerticalBar,
118
- keys,
96
+ titleVariant,
119
97
  legendScale,
120
- svgContainerSize,
121
- total,
122
- xScale,
123
- yScale
98
+ colorScale,
99
+ formattedLegendDirection
124
100
  };
125
101
  };
126
102
 
@@ -0,0 +1,10 @@
1
+ import { makeStyles } from 'tss-react/mui';
2
+
3
+ export const useStyles = makeStyles()((theme) => ({
4
+ container: {
5
+ padding: theme.spacing(1),
6
+ border: `1px solid ${theme.palette.divider}`,
7
+ borderRadius: theme.shape.borderRadius,
8
+ width: 'fit-content'
9
+ }
10
+ }));
@@ -1,9 +1,13 @@
1
1
  import { LegendOrdinal } from '@visx/legend';
2
2
  import { scaleOrdinal } from '@visx/scale';
3
3
 
4
+ import { equals } from 'ramda';
5
+ import { useStyles } from './Legend.styles';
4
6
  import { LegendProps } from './models';
5
7
 
6
8
  const Legend = ({ scale, direction = 'column' }: LegendProps): JSX.Element => {
9
+ const { classes } = useStyles();
10
+
7
11
  const legendScale = scaleOrdinal({
8
12
  domain: scale.domain,
9
13
  range: scale.range
@@ -12,8 +16,9 @@ const Legend = ({ scale, direction = 'column' }: LegendProps): JSX.Element => {
12
16
  return (
13
17
  <LegendOrdinal
14
18
  direction={direction}
15
- labelMargin="0 16px 0 0"
16
19
  scale={legendScale}
20
+ labelMargin={equals(direction, 'row') ? '0 12px 0 0' : '0 0 0 0'}
21
+ className={classes.container}
17
22
  />
18
23
  );
19
24
  };