@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.
- package/CHANGELOG.md +27 -0
- package/LICENSE.txt +357 -0
- package/README.md +32 -0
- package/SECURITY.md +47 -0
- package/index.ts +19 -0
- package/package.json +63 -0
- package/src/components/bar-chart/bar-chart.module.scss +7 -0
- package/src/components/bar-chart/bar-chart.tsx +155 -0
- package/src/components/bar-chart/index.tsx +1 -0
- package/src/components/legend/base-legend.tsx +71 -0
- package/src/components/legend/index.ts +2 -0
- package/src/components/legend/legend.module.scss +36 -0
- package/src/components/legend/types.ts +14 -0
- package/src/components/line-chart/index.tsx +1 -0
- package/src/components/line-chart/line-chart.module.scss +25 -0
- package/src/components/line-chart/line-chart.tsx +159 -0
- package/src/components/pie-chart/index.tsx +1 -0
- package/src/components/pie-chart/pie-chart.module.scss +3 -0
- package/src/components/pie-chart/pie-chart.tsx +135 -0
- package/src/components/pie-semi-circle-chart/index.tsx +1 -0
- package/src/components/pie-semi-circle-chart/pie-semi-circle-chart.module.scss +19 -0
- package/src/components/pie-semi-circle-chart/pie-semi-circle-chart.tsx +187 -0
- package/src/components/shared/types.d.ts +109 -0
- package/src/components/tooltip/base-tooltip.module.scss +11 -0
- package/src/components/tooltip/base-tooltip.tsx +57 -0
- package/src/components/tooltip/index.ts +2 -0
- package/src/components/tooltip/types.ts +8 -0
- package/src/hooks/use-chart-mouse-handler.ts +90 -0
- package/src/index.ts +19 -0
- package/src/providers/theme/index.ts +2 -0
- package/src/providers/theme/theme-provider.tsx +36 -0
- package/src/providers/theme/themes.ts +51 -0
- package/tests/index.test.js +18 -0
- 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,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,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
|
+
}
|