@automattic/charts 0.1.0

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.
Files changed (34) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/LICENSE.txt +357 -0
  3. package/README.md +32 -0
  4. package/SECURITY.md +47 -0
  5. package/index.ts +19 -0
  6. package/package.json +63 -0
  7. package/src/components/bar-chart/bar-chart.module.scss +7 -0
  8. package/src/components/bar-chart/bar-chart.tsx +155 -0
  9. package/src/components/bar-chart/index.tsx +1 -0
  10. package/src/components/legend/base-legend.tsx +71 -0
  11. package/src/components/legend/index.ts +2 -0
  12. package/src/components/legend/legend.module.scss +36 -0
  13. package/src/components/legend/types.ts +14 -0
  14. package/src/components/line-chart/index.tsx +1 -0
  15. package/src/components/line-chart/line-chart.module.scss +25 -0
  16. package/src/components/line-chart/line-chart.tsx +159 -0
  17. package/src/components/pie-chart/index.tsx +1 -0
  18. package/src/components/pie-chart/pie-chart.module.scss +3 -0
  19. package/src/components/pie-chart/pie-chart.tsx +135 -0
  20. package/src/components/pie-semi-circle-chart/index.tsx +1 -0
  21. package/src/components/pie-semi-circle-chart/pie-semi-circle-chart.module.scss +19 -0
  22. package/src/components/pie-semi-circle-chart/pie-semi-circle-chart.tsx +187 -0
  23. package/src/components/shared/types.d.ts +109 -0
  24. package/src/components/tooltip/base-tooltip.module.scss +11 -0
  25. package/src/components/tooltip/base-tooltip.tsx +57 -0
  26. package/src/components/tooltip/index.ts +2 -0
  27. package/src/components/tooltip/types.ts +8 -0
  28. package/src/hooks/use-chart-mouse-handler.ts +90 -0
  29. package/src/index.ts +19 -0
  30. package/src/providers/theme/index.ts +2 -0
  31. package/src/providers/theme/theme-provider.tsx +36 -0
  32. package/src/providers/theme/themes.ts +51 -0
  33. package/tests/index.test.js +18 -0
  34. package/tests/jest.config.cjs +7 -0
@@ -0,0 +1,155 @@
1
+ import { AxisLeft, AxisBottom } from '@visx/axis';
2
+ import { localPoint } from '@visx/event';
3
+ import { Group } from '@visx/group';
4
+ import { scaleBand, scaleLinear } from '@visx/scale';
5
+ import { Bar } from '@visx/shape';
6
+ import { useTooltip } from '@visx/tooltip';
7
+ import clsx from 'clsx';
8
+ import { FC, useCallback, type MouseEvent } from 'react';
9
+ import { useChartTheme } from '../../providers/theme';
10
+ import { Legend } from '../legend';
11
+ import { BaseTooltip } from '../tooltip';
12
+ import styles from './bar-chart.module.scss';
13
+ import type { BaseChartProps, SeriesData } from '../shared/types';
14
+
15
+ interface BarChartProps extends BaseChartProps< SeriesData[] > {}
16
+
17
+ type BarChartTooltipData = { value: number; xLabel: string; yLabel: string; seriesIndex: number };
18
+
19
+ const BarChart: FC< BarChartProps > = ( {
20
+ data,
21
+ width,
22
+ height,
23
+ margin = { top: 20, right: 20, bottom: 40, left: 40 },
24
+ withTooltips = false,
25
+ showLegend = false,
26
+ legendOrientation = 'horizontal',
27
+ className,
28
+ } ) => {
29
+ const theme = useChartTheme();
30
+ const { tooltipOpen, tooltipLeft, tooltipTop, tooltipData, hideTooltip, showTooltip } =
31
+ useTooltip< BarChartTooltipData >();
32
+
33
+ const handleMouseMove = useCallback(
34
+ (
35
+ event: MouseEvent< SVGRectElement >,
36
+ value: number,
37
+ xLabel: string,
38
+ yLabel: string,
39
+ seriesIndex: number
40
+ ) => {
41
+ const coords = localPoint( event );
42
+ if ( ! coords ) return;
43
+
44
+ showTooltip( {
45
+ tooltipData: { value, xLabel, yLabel, seriesIndex },
46
+ tooltipLeft: coords.x,
47
+ tooltipTop: coords.y - 10,
48
+ } );
49
+ },
50
+ [ showTooltip ]
51
+ );
52
+
53
+ const handleMouseLeave = useCallback( () => {
54
+ hideTooltip();
55
+ }, [ hideTooltip ] );
56
+
57
+ if ( ! data?.length ) {
58
+ return <div className={ clsx( 'bar-chart-empty', styles[ 'bat-chart-empty' ] ) }>Empty...</div>;
59
+ }
60
+
61
+ const margins = margin;
62
+ const xMax = width - margins.left - margins.right;
63
+ const yMax = height - margins.top - margins.bottom;
64
+
65
+ // Get labels for x-axis from the first series (assuming all series have same labels)
66
+ const labels = data[ 0 ].data?.map( d => d?.label );
67
+
68
+ // Create scales
69
+ const xScale = scaleBand< string >( {
70
+ range: [ 0, xMax ],
71
+ domain: labels,
72
+ padding: 0.2,
73
+ } );
74
+
75
+ const innerScale = scaleBand( {
76
+ range: [ 0, xScale.bandwidth() ],
77
+ domain: data.map( ( _, i ) => i.toString() ),
78
+ padding: 0.1,
79
+ } );
80
+
81
+ const yScale = scaleLinear< number >( {
82
+ range: [ yMax, 0 ],
83
+ domain: [
84
+ 0,
85
+ Math.max( ...data.map( series => Math.max( ...series.data.map( d => d?.value || 0 ) ) ) ),
86
+ ],
87
+ } );
88
+
89
+ // Create legend items from group labels, this iterates over groups rather than data points
90
+ const legendItems = data.map( ( group, index ) => ( {
91
+ label: group.label, // Label for each unique group
92
+ value: '', // Empty string since we don't want to show a specific value
93
+ color: theme.colors[ index % theme.colors.length ],
94
+ } ) );
95
+
96
+ return (
97
+ <div className={ clsx( 'bar-chart', className, styles[ 'bar-chart' ] ) }>
98
+ <svg width={ width } height={ height }>
99
+ <Group left={ margins.left } top={ margins.top }>
100
+ { data.map( ( series, seriesIndex ) => (
101
+ <Group key={ seriesIndex }>
102
+ { series.data.map( d => {
103
+ const xPos = xScale( d.label );
104
+ if ( xPos === undefined ) return null;
105
+
106
+ const barWidth = innerScale.bandwidth();
107
+ const barX = xPos + ( innerScale( seriesIndex.toString() ) ?? 0 );
108
+
109
+ const handleBarMouseMove = event =>
110
+ handleMouseMove( event, d.value, d.label, series.label, seriesIndex );
111
+
112
+ return (
113
+ <Bar
114
+ key={ `bar-${ seriesIndex }-${ d.label }` }
115
+ x={ barX }
116
+ y={ yScale( d.value ) }
117
+ width={ barWidth }
118
+ height={ yMax - ( yScale( d.value ) ?? 0 ) }
119
+ fill={ theme.colors[ seriesIndex % theme.colors.length ] }
120
+ onMouseMove={ withTooltips ? handleBarMouseMove : undefined }
121
+ onMouseLeave={ withTooltips ? handleMouseLeave : undefined }
122
+ />
123
+ );
124
+ } ) }
125
+ </Group>
126
+ ) ) }
127
+ <AxisLeft scale={ yScale } />
128
+ <AxisBottom scale={ xScale } top={ yMax } />
129
+ </Group>
130
+ </svg>
131
+
132
+ { withTooltips && tooltipOpen && tooltipData && (
133
+ <BaseTooltip top={ tooltipTop } left={ tooltipLeft }>
134
+ <div>
135
+ <div>{ tooltipData.yLabel }</div>
136
+ <div>
137
+ { tooltipData.xLabel }: { tooltipData.value }
138
+ </div>
139
+ </div>
140
+ </BaseTooltip>
141
+ ) }
142
+
143
+ { showLegend && (
144
+ <Legend
145
+ items={ legendItems }
146
+ orientation={ legendOrientation }
147
+ className={ styles[ 'bar-chart-legend' ] }
148
+ />
149
+ ) }
150
+ </div>
151
+ );
152
+ };
153
+
154
+ BarChart.displayName = 'BarChart';
155
+ export default BarChart;
@@ -0,0 +1 @@
1
+ export { default as BarChart } from './bar-chart';
@@ -0,0 +1,71 @@
1
+ import { LegendOrdinal } from '@visx/legend';
2
+ import { scaleOrdinal } from '@visx/scale';
3
+ import clsx from 'clsx';
4
+ import { FC } from 'react';
5
+ import styles from './legend.module.scss';
6
+ import type { LegendProps } from './types';
7
+
8
+ /**
9
+ * Base legend component that displays color-coded items with labels using visx
10
+ * @param {object} props - Component properties
11
+ * @param {Array} props.items - Array of legend items to display
12
+ * @param {string} props.className - Additional CSS class names
13
+ * @param {string} props.orientation - Layout orientation (horizontal/vertical)
14
+ * @return {JSX.Element} Rendered legend component
15
+ */
16
+ const orientationToFlexDirection = {
17
+ horizontal: 'row' as const,
18
+ vertical: 'column' as const,
19
+ };
20
+
21
+ export const BaseLegend: FC< LegendProps > = ( {
22
+ items,
23
+ className,
24
+ orientation = 'horizontal',
25
+ } ) => {
26
+ const legendScale = scaleOrdinal( {
27
+ domain: items.map( item => item.label ),
28
+ range: items.map( item => item.color ),
29
+ } );
30
+
31
+ return (
32
+ <div
33
+ className={ clsx( styles.legend, styles[ `legend--${ orientation }` ], className ) }
34
+ role="list"
35
+ >
36
+ <LegendOrdinal
37
+ scale={ legendScale }
38
+ direction={ orientationToFlexDirection[ orientation ] }
39
+ shape="rect"
40
+ shapeWidth={ 16 }
41
+ shapeHeight={ 16 }
42
+ className={ styles[ 'legend-items' ] }
43
+ >
44
+ { labels => (
45
+ <div className={ styles[ `legend--${ orientation }` ] }>
46
+ { labels.map( label => (
47
+ <div key={ label.text } className={ styles[ 'legend-item' ] }>
48
+ <svg width={ 16 } height={ 16 }>
49
+ <rect
50
+ width={ 16 }
51
+ height={ 16 }
52
+ fill={ label.value }
53
+ className={ styles[ 'legend-item-swatch' ] }
54
+ />
55
+ </svg>
56
+ <span className={ styles[ 'legend-item-label' ] }>
57
+ { label.text }
58
+ { items.find( item => item.label === label.text )?.value && (
59
+ <span className={ styles[ 'legend-item-value' ] }>
60
+ { items.find( item => item.label === label.text )?.value }
61
+ </span>
62
+ ) }
63
+ </span>
64
+ </div>
65
+ ) ) }
66
+ </div>
67
+ ) }
68
+ </LegendOrdinal>
69
+ </div>
70
+ );
71
+ };
@@ -0,0 +1,2 @@
1
+ export { BaseLegend as Legend } from './base-legend';
2
+ export type { LegendProps } from './types';
@@ -0,0 +1,36 @@
1
+ .legend {
2
+ &--horizontal {
3
+ display: flex;
4
+ flex-direction: row;
5
+ flex-wrap: wrap;
6
+ gap: 16px;
7
+ }
8
+
9
+ &--vertical {
10
+ display: flex;
11
+ flex-direction: column;
12
+ gap: 8px;
13
+ }
14
+ }
15
+
16
+ .legend-item {
17
+ display: flex;
18
+ align-items: center;
19
+ gap: 8px;
20
+ font-size: 0.875rem;
21
+ }
22
+
23
+ .legend-item-swatch {
24
+ border-radius: 2px;
25
+ }
26
+
27
+ .legend-item-label {
28
+ color: var(--jp-gray-80, #2c3338);
29
+ display: flex;
30
+ align-items: center;
31
+ gap: 0.5rem;
32
+ }
33
+
34
+ .legend-item-value {
35
+ font-weight: 500;
36
+ }
@@ -0,0 +1,14 @@
1
+ import { scaleOrdinal } from '@visx/scale';
2
+
3
+ export type LegendItem = {
4
+ label: string;
5
+ value: number | string;
6
+ color: string;
7
+ };
8
+
9
+ export type LegendProps = {
10
+ items: LegendItem[];
11
+ className?: string;
12
+ orientation?: 'horizontal' | 'vertical';
13
+ scale?: ReturnType< typeof scaleOrdinal >;
14
+ };
@@ -0,0 +1 @@
1
+ export { default as LineChart } from './line-chart';
@@ -0,0 +1,25 @@
1
+ .line-chart {
2
+ position: relative;
3
+
4
+ &__tooltip {
5
+ background: #fff;
6
+ padding: 0.5rem;
7
+ }
8
+
9
+ &__tooltip-date {
10
+ font-weight: bold;
11
+ padding-bottom: 10px;
12
+ }
13
+
14
+ &__tooltip-row {
15
+ display: flex;
16
+ align-items: center;
17
+ padding: 4px 0;
18
+ justify-content: space-between;
19
+ }
20
+
21
+ &__tooltip-label {
22
+ font-weight: 500;
23
+ padding-right: 1rem;
24
+ }
25
+ }
@@ -0,0 +1,159 @@
1
+ import {
2
+ XYChart,
3
+ AnimatedLineSeries,
4
+ AnimatedAxis,
5
+ AnimatedGrid,
6
+ Tooltip,
7
+ buildChartTheme,
8
+ } from '@visx/xychart';
9
+ import clsx from 'clsx';
10
+ import { FC } from 'react';
11
+ import { useChartTheme } from '../../providers/theme/theme-provider';
12
+ import { Legend } from '../legend';
13
+ import styles from './line-chart.module.scss';
14
+ import type { BaseChartProps, DataPointDate, SeriesData } from '../shared/types';
15
+
16
+ // TODO: revisit grid and axis options - accept as props for frid lines, axis, values: x, y, all, none
17
+
18
+ interface LineChartProps extends BaseChartProps< SeriesData[] > {}
19
+
20
+ type TooltipData = {
21
+ date: Date;
22
+ [ key: string ]: number | Date;
23
+ };
24
+
25
+ type TooltipDatum = {
26
+ key: string;
27
+ value: number;
28
+ };
29
+
30
+ const renderTooltip = ( {
31
+ tooltipData,
32
+ }: {
33
+ tooltipData?: {
34
+ nearestDatum?: {
35
+ datum: TooltipData;
36
+ key: string;
37
+ };
38
+ datumByKey?: { [ key: string ]: { datum: TooltipData } };
39
+ };
40
+ } ) => {
41
+ const nearestDatum = tooltipData?.nearestDatum?.datum;
42
+ if ( ! nearestDatum ) return null;
43
+
44
+ const tooltipPoints: TooltipDatum[] = Object.entries( tooltipData?.datumByKey || {} )
45
+ .map( ( [ key, { datum } ] ) => ( {
46
+ key,
47
+ value: datum.value as number,
48
+ } ) )
49
+ .sort( ( a, b ) => b.value - a.value );
50
+
51
+ return (
52
+ <div className={ styles[ 'line-chart__tooltip' ] }>
53
+ <div className={ styles[ 'line-chart__tooltip-date' ] }>
54
+ { nearestDatum.date.toLocaleDateString() }
55
+ </div>
56
+ { tooltipPoints.map( point => (
57
+ <div key={ point.key } className={ styles[ 'line-chart__tooltip-row' ] }>
58
+ <span className={ styles[ 'line-chart__tooltip-label' ] }>{ point.key }:</span>
59
+ <span className={ styles[ 'line-chart__tooltip-value' ] }>{ point.value }</span>
60
+ </div>
61
+ ) ) }
62
+ </div>
63
+ );
64
+ };
65
+
66
+ const formatDateTick = ( value: number ) => {
67
+ const date = new Date( value );
68
+ return date.toLocaleDateString( undefined, {
69
+ month: 'short',
70
+ day: 'numeric',
71
+ } );
72
+ };
73
+
74
+ const LineChart: FC< LineChartProps > = ( {
75
+ data,
76
+ width,
77
+ height,
78
+ margin = { top: 20, right: 20, bottom: 40, left: 40 },
79
+ className,
80
+ withTooltips = true,
81
+ showLegend = false,
82
+ legendOrientation = 'horizontal',
83
+ } ) => {
84
+ const providerTheme = useChartTheme();
85
+
86
+ if ( ! data?.length ) {
87
+ return (
88
+ <div className={ clsx( 'line-chart-empty', styles[ 'line-chart-empty' ] ) }>Empty...</div>
89
+ );
90
+ }
91
+
92
+ // Create legend items from group labels, this iterates over groups rather than data points
93
+ const legendItems = data.map( ( group, index ) => ( {
94
+ label: group.label, // Label for each unique group
95
+ value: '', // Empty string since we don't want to show a specific value
96
+ color: providerTheme.colors[ index % providerTheme.colors.length ],
97
+ } ) );
98
+
99
+ const accessors = {
100
+ xAccessor: ( d: DataPointDate ) => d.date,
101
+ yAccessor: ( d: DataPointDate ) => d.value,
102
+ };
103
+
104
+ const theme = buildChartTheme( {
105
+ backgroundColor: providerTheme.backgroundColor,
106
+ colors: providerTheme.colors,
107
+ gridStyles: providerTheme.gridStyles,
108
+ tickLength: providerTheme?.tickLength || 0,
109
+ gridColor: providerTheme?.gridColor || '',
110
+ gridColorDark: providerTheme?.gridColorDark || '',
111
+ } );
112
+
113
+ return (
114
+ <div className={ clsx( 'line-chart', styles[ 'line-chart' ], className ) }>
115
+ <XYChart
116
+ theme={ theme }
117
+ width={ width }
118
+ height={ height }
119
+ margin={ margin }
120
+ xScale={ { type: 'time' } }
121
+ yScale={ { type: 'linear', nice: true } }
122
+ >
123
+ <AnimatedGrid columns={ false } numTicks={ 4 } />
124
+ <AnimatedAxis orientation="bottom" numTicks={ 5 } tickFormat={ formatDateTick } />
125
+ <AnimatedAxis orientation="left" numTicks={ 4 } />
126
+
127
+ { data.map( ( seriesData, index ) => (
128
+ <AnimatedLineSeries
129
+ key={ seriesData?.label }
130
+ dataKey={ seriesData?.label }
131
+ data={ seriesData.data as DataPointDate[] } // TODO: this needs fixing or a more specific type for each chart
132
+ { ...accessors }
133
+ stroke={ theme.colors[ index % theme.colors.length ] }
134
+ strokeWidth={ 2 }
135
+ />
136
+ ) ) }
137
+
138
+ { withTooltips && (
139
+ <Tooltip
140
+ snapTooltipToDatumX
141
+ snapTooltipToDatumY
142
+ showSeriesGlyphs
143
+ renderTooltip={ renderTooltip }
144
+ />
145
+ ) }
146
+ </XYChart>
147
+
148
+ { showLegend && (
149
+ <Legend
150
+ items={ legendItems }
151
+ orientation={ legendOrientation }
152
+ className={ styles[ 'line-chart-legend' ] }
153
+ />
154
+ ) }
155
+ </div>
156
+ );
157
+ };
158
+
159
+ export default LineChart;
@@ -0,0 +1 @@
1
+ export { default as PieChart } from './pie-chart';
@@ -0,0 +1,3 @@
1
+ .pie-chart {
2
+ position: relative;
3
+ }
@@ -0,0 +1,135 @@
1
+ import { Group } from '@visx/group';
2
+ import { Pie } from '@visx/shape';
3
+ import clsx from 'clsx';
4
+ import { SVGProps } from 'react';
5
+ import useChartMouseHandler from '../../hooks/use-chart-mouse-handler';
6
+ import { useChartTheme, defaultTheme } from '../../providers/theme';
7
+ import { Legend } from '../legend';
8
+ import { BaseTooltip } from '../tooltip';
9
+ import styles from './pie-chart.module.scss';
10
+ import type { BaseChartProps, DataPoint } from '../shared/types';
11
+
12
+ // TODO: add animation
13
+
14
+ interface PieChartProps extends BaseChartProps< DataPoint[] > {
15
+ /**
16
+ * Inner radius in pixels. If > 0, creates a donut chart. Defaults to 0.
17
+ */
18
+ innerRadius?: number;
19
+ }
20
+
21
+ /**
22
+ * Renders a pie or donut chart using the provided data.
23
+ *
24
+ * @param {PieChartProps} props - Component props
25
+ * @return {JSX.Element} The rendered chart component
26
+ */
27
+ const PieChart = ( {
28
+ data,
29
+ width,
30
+ height,
31
+ withTooltips = false,
32
+ innerRadius = 0,
33
+ className,
34
+ showLegend,
35
+ legendOrientation,
36
+ }: PieChartProps ) => {
37
+ const providerTheme = useChartTheme();
38
+ const { onMouseMove, onMouseLeave, tooltipOpen, tooltipData, tooltipLeft, tooltipTop } =
39
+ useChartMouseHandler( {
40
+ withTooltips,
41
+ } );
42
+
43
+ // Calculate radius based on width/height
44
+ const radius = Math.min( width, height ) / 2;
45
+ const centerX = width / 2;
46
+ const centerY = height / 2;
47
+
48
+ const accessors = {
49
+ value: d => d.value,
50
+ // Use the color property from the data object as a last resort. The theme provides colours by default.
51
+ fill: d => d.color || providerTheme.colors[ d.index ],
52
+ };
53
+
54
+ // Create legend items from data
55
+ const legendItems = data.map( ( item, index ) => ( {
56
+ label: item.label,
57
+ value: item.value.toString(),
58
+ color: providerTheme.colors[ index % providerTheme.colors.length ],
59
+ } ) );
60
+
61
+ return (
62
+ <div className={ clsx( 'pie-chart', styles[ 'pie-chart' ], className ) }>
63
+ <svg width={ width } height={ height }>
64
+ <Group top={ centerY } left={ centerX }>
65
+ <Pie
66
+ data={ data }
67
+ pieValue={ accessors.value }
68
+ outerRadius={ radius - 20 } // Leave space for labels/tooltips
69
+ innerRadius={ innerRadius }
70
+ >
71
+ { pie => {
72
+ return pie.arcs.map( ( arc, index ) => {
73
+ const [ centroidX, centroidY ] = pie.path.centroid( arc );
74
+ const hasSpaceForLabel = arc.endAngle - arc.startAngle >= 0.25;
75
+ const handleMouseMove = event => onMouseMove( event, arc.data );
76
+
77
+ const pathProps: SVGProps< SVGPathElement > = {
78
+ d: pie.path( arc ) || '',
79
+ fill: accessors.fill( arc ),
80
+ };
81
+
82
+ if ( withTooltips ) {
83
+ pathProps.onMouseMove = handleMouseMove;
84
+ pathProps.onMouseLeave = onMouseLeave;
85
+ }
86
+
87
+ return (
88
+ <g key={ `arc-${ index }` }>
89
+ <path { ...pathProps } />
90
+ { hasSpaceForLabel && (
91
+ <text
92
+ x={ centroidX }
93
+ y={ centroidY }
94
+ dy=".33em"
95
+ fill={
96
+ providerTheme.labelBackgroundColor || defaultTheme.labelBackgroundColor
97
+ }
98
+ fontSize={ 12 }
99
+ textAnchor="middle"
100
+ pointerEvents="none"
101
+ >
102
+ { arc.data.label }
103
+ </text>
104
+ ) }
105
+ </g>
106
+ );
107
+ } );
108
+ } }
109
+ </Pie>
110
+ </Group>
111
+ </svg>
112
+
113
+ { showLegend && (
114
+ <Legend
115
+ items={ legendItems }
116
+ orientation={ legendOrientation }
117
+ className={ styles[ 'pie-chart-legend' ] }
118
+ />
119
+ ) }
120
+
121
+ { withTooltips && tooltipOpen && tooltipData && (
122
+ <BaseTooltip
123
+ data={ tooltipData }
124
+ top={ tooltipTop }
125
+ left={ tooltipLeft }
126
+ style={ {
127
+ transform: 'translate(-50%, -100%)',
128
+ } }
129
+ />
130
+ ) }
131
+ </div>
132
+ );
133
+ };
134
+
135
+ export default PieChart;
@@ -0,0 +1 @@
1
+ export { default as PieSemiCircleChart } from './pie-semi-circle-chart';
@@ -0,0 +1,19 @@
1
+ .pie-semi-circle-chart {
2
+ position: relative;
3
+ text-align: center;
4
+
5
+ &-legend {
6
+ margin-top: 1rem;
7
+ }
8
+
9
+ .label {
10
+ margin-bottom: 0px; // Add space between label and pie chart
11
+ font-weight: 600; // Make label more prominent than note
12
+ font-size: 16px; // Set explicit font size
13
+ }
14
+
15
+ .note {
16
+ margin-top: 0px; // Add space between pie chart and note
17
+ font-size: 14px; // Slightly smaller text for hierarchy
18
+ }
19
+ }