@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,187 @@
1
+ import { localPoint } from '@visx/event';
2
+ import { Group } from '@visx/group';
3
+ import Pie, { PieArcDatum } from '@visx/shape/lib/shapes/Pie';
4
+ import { Text } from '@visx/text';
5
+ import { useTooltip } from '@visx/tooltip';
6
+ import clsx from 'clsx';
7
+ import { FC, useCallback } from 'react';
8
+ import { useChartTheme } from '../../providers/theme/theme-provider';
9
+ import { Legend } from '../legend';
10
+ import { BaseTooltip } from '../tooltip';
11
+ import styles from './pie-semi-circle-chart.module.scss';
12
+ import type { BaseChartProps, DataPointPercentage } from '../shared/types';
13
+
14
+ interface PieSemiCircleChartProps extends BaseChartProps< DataPointPercentage[] > {
15
+ /**
16
+ * Label text to display above the chart
17
+ */
18
+ label: string;
19
+ /**
20
+ * Note text to display below the label
21
+ */
22
+ note: string;
23
+ /**
24
+ * Direction of chart rendering
25
+ * true for clockwise, false for counter-clockwise
26
+ */
27
+ clockwise?: boolean;
28
+ /**
29
+ * Thickness of the pie chart. A value between 0 and 1
30
+ */
31
+ thickness?: number;
32
+ }
33
+
34
+ type ArcData = PieArcDatum< DataPointPercentage >;
35
+
36
+ const PieSemiCircleChart: FC< PieSemiCircleChartProps > = ( {
37
+ data,
38
+ width,
39
+ label,
40
+ note,
41
+ className,
42
+ withTooltips = false,
43
+ clockwise = true,
44
+ thickness = 0.4,
45
+ showLegend,
46
+ legendOrientation,
47
+ } ) => {
48
+ const providerTheme = useChartTheme();
49
+ const { tooltipOpen, tooltipLeft, tooltipTop, tooltipData, hideTooltip, showTooltip } =
50
+ useTooltip< DataPointPercentage >();
51
+
52
+ const centerX = width / 2;
53
+ const height = width / 2;
54
+ const radius = width / 2;
55
+ const pad = 0.03;
56
+ const innerRadius = radius * ( 1 - thickness + pad );
57
+
58
+ // Map the data to include index for color assignment
59
+ const dataWithIndex = data.map( ( d, index ) => ( {
60
+ ...d,
61
+ index,
62
+ } ) );
63
+
64
+ // Set the clockwise direction based on the prop
65
+ const startAngle = clockwise ? -Math.PI / 2 : Math.PI / 2;
66
+ const endAngle = clockwise ? Math.PI / 2 : -Math.PI / 2;
67
+
68
+ const accessors = {
69
+ value: ( d: DataPointPercentage & { index: number } ) => d.value,
70
+ sort: (
71
+ a: DataPointPercentage & { index: number },
72
+ b: DataPointPercentage & { index: number }
73
+ ) => b.value - a.value,
74
+ // Use the color property from the data object as a last resort. The theme provides colours by default.
75
+ fill: ( d: DataPointPercentage & { index: number } ) =>
76
+ d.color || providerTheme.colors[ d.index % providerTheme.colors.length ],
77
+ };
78
+
79
+ const handleMouseMove = useCallback(
80
+ ( event: React.MouseEvent, arc: ArcData ) => {
81
+ const coords = localPoint( event );
82
+ if ( ! coords ) return;
83
+
84
+ showTooltip( {
85
+ tooltipData: arc.data,
86
+ tooltipLeft: coords.x,
87
+ tooltipTop: coords.y - 10,
88
+ } );
89
+ },
90
+ [ showTooltip ]
91
+ );
92
+
93
+ const handleMouseLeave = useCallback( () => {
94
+ hideTooltip();
95
+ }, [ hideTooltip ] );
96
+
97
+ const handleArcMouseMove = useCallback(
98
+ ( arc: ArcData ) => ( event: React.MouseEvent ) => {
99
+ handleMouseMove( event, arc );
100
+ },
101
+ [ handleMouseMove ]
102
+ );
103
+
104
+ // Create legend items
105
+ const legendItems = data.map( ( item, index ) => ( {
106
+ label: item.label,
107
+ value: item.valueDisplay || item.value.toString(),
108
+ color: accessors.fill( { ...item, index } ),
109
+ } ) );
110
+
111
+ return (
112
+ <div
113
+ className={ clsx( 'pie-semi-circle-chart', styles[ 'pie-semi-circle-chart' ], className ) }
114
+ >
115
+ <svg width={ width } height={ height }>
116
+ { /* Main chart group that contains both the pie and text elements */ }
117
+ <Group top={ centerX } left={ centerX }>
118
+ { /* Pie chart */ }
119
+ <Pie< DataPointPercentage & { index: number } >
120
+ data={ dataWithIndex }
121
+ pieValue={ accessors.value }
122
+ outerRadius={ radius }
123
+ innerRadius={ innerRadius }
124
+ cornerRadius={ 3 }
125
+ padAngle={ pad }
126
+ startAngle={ startAngle }
127
+ endAngle={ endAngle }
128
+ pieSort={ accessors.sort }
129
+ >
130
+ { pie => {
131
+ return pie.arcs.map( arc => (
132
+ <g
133
+ key={ arc.data.label }
134
+ onMouseMove={ handleArcMouseMove( arc ) }
135
+ onMouseLeave={ handleMouseLeave }
136
+ >
137
+ <path d={ pie.path( arc ) || '' } fill={ accessors.fill( arc.data ) } />
138
+ </g>
139
+ ) );
140
+ } }
141
+ </Pie>
142
+
143
+ <Group>
144
+ <Text
145
+ textAnchor="middle"
146
+ verticalAnchor="start"
147
+ y={ -40 } // double font size to make room for a note
148
+ className={ styles.label }
149
+ >
150
+ { label }
151
+ </Text>
152
+ <Text
153
+ textAnchor="middle"
154
+ verticalAnchor="start"
155
+ y={ -20 } // font size with padding
156
+ className={ styles.note }
157
+ >
158
+ { note }
159
+ </Text>
160
+ </Group>
161
+ </Group>
162
+ </svg>
163
+
164
+ { withTooltips && tooltipOpen && tooltipData && (
165
+ <BaseTooltip
166
+ data={ {
167
+ label: tooltipData.label,
168
+ value: tooltipData.value,
169
+ valueDisplay: tooltipData.valueDisplay,
170
+ } }
171
+ top={ tooltipTop }
172
+ left={ tooltipLeft }
173
+ />
174
+ ) }
175
+
176
+ { showLegend && (
177
+ <Legend
178
+ items={ legendItems }
179
+ orientation={ legendOrientation }
180
+ className={ styles[ 'pie-semi-circle-chart-legend' ] }
181
+ />
182
+ ) }
183
+ </div>
184
+ );
185
+ };
186
+
187
+ export default PieSemiCircleChart;
@@ -0,0 +1,109 @@
1
+ import type { CSSProperties } from 'react';
2
+
3
+ export type DataPoint = {
4
+ label: string;
5
+ value: number;
6
+ };
7
+
8
+ export type DataPointDate = {
9
+ date: Date;
10
+ label?: string;
11
+ value: number;
12
+ };
13
+
14
+ export type SeriesData = {
15
+ group?: string;
16
+ label: string;
17
+ data: DataPointDate[] | DataPoint[];
18
+ };
19
+
20
+ export type MultipleDataPointsDate = {
21
+ label: string;
22
+ data: DataPointDate[];
23
+ };
24
+
25
+ export type DataPointPercentage = {
26
+ /**
27
+ * Label for the data point
28
+ */
29
+ label: string;
30
+ /**
31
+ * Numerical value
32
+ */
33
+ value: number;
34
+ /**
35
+ * Formatted value for display
36
+ */
37
+ valueDisplay?: string;
38
+ /**
39
+ * Percentage value
40
+ */
41
+ percentage: number;
42
+ /**
43
+ * Color code for the segment, by default colours are taken from the theme but this property can overrides it
44
+ */
45
+ color?: string;
46
+ };
47
+
48
+ /**
49
+ * Theme configuration for chart components
50
+ */
51
+ export type ChartTheme = {
52
+ /** Background color for chart components */
53
+ backgroundColor: string;
54
+ /** Background color for labels */
55
+ labelBackgroundColor?: string;
56
+ /** Array of colors used for data visualization */
57
+ colors: string[];
58
+ /** Optional CSS styles for grid lines */
59
+ gridStyles?: CSSProperties;
60
+ /** Length of axis ticks in pixels */
61
+ tickLength: number;
62
+ /** Color of the grid lines */
63
+ gridColor: string;
64
+ /** Color of the grid lines in dark mode */
65
+ gridColorDark: string;
66
+ };
67
+
68
+ /**
69
+ * Base properties shared across all chart components
70
+ */
71
+ export type BaseChartProps< T = DataPoint | DataPointDate > = {
72
+ /**
73
+ * Array of data points to display in the chart
74
+ */
75
+ data: T extends DataPoint | DataPointDate ? T[] : T;
76
+ /**
77
+ * Additional CSS class name for the chart container
78
+ */
79
+ className?: string;
80
+ /**
81
+ * Width of the chart in pixels
82
+ */
83
+ width: number;
84
+ /**
85
+ * Height of the chart in pixels
86
+ */
87
+ height?: number;
88
+ /**
89
+ * Chart margins
90
+ */
91
+ margin?: {
92
+ top: number;
93
+ right: number;
94
+ bottom: number;
95
+ left: number;
96
+ };
97
+ /**
98
+ * Whether to show tooltips on hover. False by default.
99
+ */
100
+ withTooltips?: boolean;
101
+ /**
102
+ * Whether to show legend
103
+ */
104
+ showLegend?: boolean;
105
+ /**
106
+ * Legend orientation
107
+ */
108
+ legendOrientation?: 'horizontal' | 'vertical';
109
+ };
@@ -0,0 +1,11 @@
1
+ .tooltip {
2
+ padding: 0.5rem;
3
+ background-color: rgba(0, 0, 0, 0.85);
4
+ color: white;
5
+ border-radius: 4px;
6
+ font-size: 14px;
7
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
8
+ position: absolute;
9
+ pointer-events: none;
10
+ transform: translate(-50%, -100%);
11
+ }
@@ -0,0 +1,57 @@
1
+ import styles from './base-tooltip.module.scss';
2
+ import type { CSSProperties, ComponentType, ReactNode } from 'react';
3
+
4
+ type TooltipData = {
5
+ label: string;
6
+ value: number;
7
+ valueDisplay?: string;
8
+ };
9
+
10
+ type TooltipComponentProps = {
11
+ data: TooltipData;
12
+ className?: string;
13
+ };
14
+
15
+ type TooltipCommonProps = {
16
+ top: number;
17
+ left: number;
18
+ style?: CSSProperties;
19
+ className?: string;
20
+ };
21
+
22
+ type DefaultDataTooltip = {
23
+ data: TooltipData;
24
+ component?: ComponentType< TooltipComponentProps >;
25
+ children?: never;
26
+ };
27
+
28
+ type CustomTooltip = {
29
+ children: ReactNode;
30
+ data?: never;
31
+ component?: never;
32
+ };
33
+
34
+ type BaseTooltipProps = TooltipCommonProps & ( DefaultDataTooltip | CustomTooltip );
35
+
36
+ const DefaultTooltipContent = ( { data }: TooltipComponentProps ) => (
37
+ <>
38
+ { data?.label }: { data?.valueDisplay || data?.value }
39
+ </>
40
+ );
41
+
42
+ export const BaseTooltip = ( {
43
+ data,
44
+ top,
45
+ left,
46
+ component: Component = DefaultTooltipContent,
47
+ children,
48
+ className,
49
+ }: BaseTooltipProps ) => {
50
+ return (
51
+ <div className={ styles.tooltip } style={ { top, left } } role="tooltip">
52
+ { children || <Component data={ data } className={ className } /> }
53
+ </div>
54
+ );
55
+ };
56
+
57
+ export type { BaseTooltipProps, TooltipData };
@@ -0,0 +1,2 @@
1
+ export { BaseTooltip } from './base-tooltip';
2
+ export type { BaseTooltipProps, TooltipData } from './base-tooltip';
@@ -0,0 +1,8 @@
1
+ type TooltipProps = {
2
+ data: {
3
+ label: string;
4
+ value: number;
5
+ };
6
+ };
7
+
8
+ export type { TooltipProps };
@@ -0,0 +1,90 @@
1
+ import { localPoint } from '@visx/event';
2
+ import { useTooltip } from '@visx/tooltip';
3
+ import { useCallback, type MouseEvent } from 'react';
4
+ import type { DataPoint } from '../components/shared/types';
5
+
6
+ type UseChartMouseHandlerProps = {
7
+ /**
8
+ * Whether tooltips are enabled
9
+ */
10
+ withTooltips: boolean;
11
+ };
12
+
13
+ type UseChartMouseHandlerReturn = {
14
+ /**
15
+ * Handler for mouse move events
16
+ */
17
+ onMouseMove: ( event: React.MouseEvent< SVGElement >, data: DataPoint ) => void;
18
+ /**
19
+ * Handler for mouse leave events
20
+ */
21
+ onMouseLeave: () => void;
22
+ /**
23
+ * Whether the tooltip is currently open
24
+ */
25
+ tooltipOpen: boolean;
26
+ /**
27
+ * The current tooltip data
28
+ */
29
+ tooltipData: DataPoint | null;
30
+ /**
31
+ * The current tooltip left position
32
+ */
33
+ tooltipLeft: number | undefined;
34
+ /**
35
+ * The current tooltip top position
36
+ */
37
+ tooltipTop: number | undefined;
38
+ };
39
+
40
+ /**
41
+ * Hook to handle mouse interactions for chart components
42
+ *
43
+ * @param {UseChartMouseHandlerProps} props - Hook configuration
44
+ * @return {UseChartMouseHandlerReturn} Object containing handlers and tooltip state
45
+ */
46
+ const useChartMouseHandler = ( {
47
+ withTooltips,
48
+ }: UseChartMouseHandlerProps ): UseChartMouseHandlerReturn => {
49
+ const { tooltipOpen, tooltipLeft, tooltipTop, tooltipData, hideTooltip, showTooltip } =
50
+ useTooltip< DataPoint >();
51
+
52
+ // TODO: either debounce/throttle or use useTooltipInPortal with built-in debounce
53
+ const onMouseMove = useCallback(
54
+ ( event: MouseEvent< SVGElement >, data: DataPoint ) => {
55
+ if ( ! withTooltips ) {
56
+ return;
57
+ }
58
+
59
+ const coords = localPoint( event );
60
+ if ( ! coords ) {
61
+ return;
62
+ }
63
+
64
+ showTooltip( {
65
+ tooltipData: data,
66
+ tooltipLeft: coords.x,
67
+ tooltipTop: coords.y - 10,
68
+ } );
69
+ },
70
+ [ withTooltips, showTooltip ]
71
+ );
72
+
73
+ const onMouseLeave = useCallback( () => {
74
+ if ( ! withTooltips ) {
75
+ return;
76
+ }
77
+ hideTooltip();
78
+ }, [ withTooltips, hideTooltip ] );
79
+
80
+ return {
81
+ onMouseMove,
82
+ onMouseLeave,
83
+ tooltipOpen,
84
+ tooltipData,
85
+ tooltipLeft,
86
+ tooltipTop,
87
+ };
88
+ };
89
+
90
+ export default useChartMouseHandler;
package/src/index.ts ADDED
@@ -0,0 +1,19 @@
1
+ // Charts
2
+ export { BarChart } from './components/bar-chart';
3
+ export { LineChart } from './components/line-chart';
4
+ export { PieChart } from './components/pie-chart';
5
+ export { PieSemiCircleChart } from './components/pie-semi-circle-chart';
6
+
7
+ // Chart components
8
+ export { BaseTooltip } from './components/tooltip';
9
+ export { Legend } from './components/legend';
10
+
11
+ // Providers
12
+ export { ThemeProvider } from './providers/theme';
13
+
14
+ // Hooks
15
+ export { default as useChartMouseHandler } from './hooks/use-chart-mouse-handler';
16
+
17
+ // Types
18
+ export type * from './components/shared/types';
19
+ export type { BaseTooltipProps } from './components/tooltip';
@@ -0,0 +1,2 @@
1
+ export { ThemeProvider, useChartTheme } from './theme-provider';
2
+ export { defaultTheme, jetpackTheme, wooTheme } from './themes';
@@ -0,0 +1,36 @@
1
+ import { createContext, useContext, FC, ReactNode } from 'react';
2
+ import { defaultTheme } from './themes';
3
+ import type { ChartTheme } from '../../components/shared/types';
4
+
5
+ /**
6
+ * Context for sharing theme configuration across components
7
+ */
8
+ const ThemeContext = createContext< ChartTheme >( defaultTheme );
9
+
10
+ /**
11
+ * Hook to access chart theme
12
+ * @return {object} A built theme configuration compatible with visx charts
13
+ */
14
+ const useChartTheme = () => {
15
+ const theme = useContext( ThemeContext );
16
+ return theme;
17
+ };
18
+
19
+ /**
20
+ * Props for the ThemeProvider component
21
+ */
22
+ type ThemeProviderProps = {
23
+ /** Optional partial theme override */
24
+ theme?: Partial< ChartTheme >;
25
+ /** Child components that will have access to the theme */
26
+ children: ReactNode;
27
+ };
28
+
29
+ // Provider component for chart theming
30
+ // Allows theme customization through props while maintaining default values
31
+ const ThemeProvider: FC< ThemeProviderProps > = ( { theme = {}, children } ) => {
32
+ const mergedTheme = { ...defaultTheme, ...theme };
33
+ return <ThemeContext.Provider value={ mergedTheme }>{ children }</ThemeContext.Provider>;
34
+ };
35
+
36
+ export { ThemeProvider, useChartTheme };
@@ -0,0 +1,51 @@
1
+ import type { ChartTheme } from '../../components/shared/types';
2
+
3
+ /**
4
+ * Default theme configuration
5
+ */
6
+ const defaultTheme: ChartTheme = {
7
+ backgroundColor: '#FFFFFF', // chart background color
8
+ labelBackgroundColor: '#FFFFFF', // label background color
9
+ colors: [ '#98C8DF', '#006DAB', '#A6DC80', '#1F9828', '#FF8C8F' ],
10
+ gridStyles: {
11
+ stroke: '#787C82',
12
+ strokeWidth: 1,
13
+ },
14
+ tickLength: 0,
15
+ gridColor: '',
16
+ gridColorDark: '',
17
+ };
18
+
19
+ /**
20
+ * Jetpack theme configuration
21
+ */
22
+ const jetpackTheme: ChartTheme = {
23
+ backgroundColor: '#FFFFFF', // chart background color
24
+ labelBackgroundColor: '#FFFFFF', // label background color
25
+ colors: [ '#98C8DF', '#006DAB', '#A6DC80', '#1F9828', '#FF8C8F' ],
26
+ gridStyles: {
27
+ stroke: '#787C82',
28
+ strokeWidth: 1,
29
+ },
30
+ tickLength: 0,
31
+ gridColor: '',
32
+ gridColorDark: '',
33
+ };
34
+
35
+ /**
36
+ * Woo theme configuration
37
+ */
38
+ const wooTheme: ChartTheme = {
39
+ backgroundColor: '#FFFFFF', // chart background color
40
+ labelBackgroundColor: '#FFFFFF', // label background color
41
+ colors: [ '#80C8FF', '#B999FF', '#3858E9' ],
42
+ gridStyles: {
43
+ stroke: '#787C82',
44
+ strokeWidth: 1,
45
+ },
46
+ tickLength: 0,
47
+ gridColor: '',
48
+ gridColorDark: '',
49
+ };
50
+
51
+ export { defaultTheme, jetpackTheme, wooTheme };
@@ -0,0 +1,18 @@
1
+ // We recommend using `jest` for testing. If you're testing React code, we recommend `@testing-library/react` and related packages.
2
+ // Please match the versions used elsewhere in the monorepo.
3
+ //
4
+ // Please don't add new uses of `mocha`, `chai`, `sinon`, `enzyme`, and so on. We're trying to standardize on one testing framework.
5
+ //
6
+ // The default setup is to have files named like "name.test.js" (or .jsx, .ts, or .tsx) in this `tests/` directory.
7
+ // But you could instead put them in `src/`, or put files like "name.js" (or .jsx, .ts, or .tsx) in `test` or `__tests__` directories somewhere.
8
+
9
+ // This is a placeholder test, new tests will be added soon
10
+ const placeholderFunction = () => {
11
+ return true;
12
+ };
13
+
14
+ describe( 'placeholderTest', () => {
15
+ it( 'should be a function', () => {
16
+ expect( typeof placeholderFunction ).toBe( 'function' );
17
+ } );
18
+ } );
@@ -0,0 +1,7 @@
1
+ const path = require( 'path' );
2
+ const baseConfig = require( 'jetpack-js-tools/jest/config.base.js' );
3
+
4
+ module.exports = {
5
+ ...baseConfig,
6
+ rootDir: path.join( __dirname, '..' ),
7
+ };