diff --git a/projects/js-packages/charts/changelog/add-charts-themes b/projects/js-packages/charts/changelog/add-charts-themes new file mode 100644 index 0000000000000..bed6b167cc232 --- /dev/null +++ b/projects/js-packages/charts/changelog/add-charts-themes @@ -0,0 +1,4 @@ +Significance: patch +Type: added + +Adding a theme provider to Automattic Charts diff --git a/projects/js-packages/charts/src/components/bar-chart/index.tsx b/projects/js-packages/charts/src/components/bar-chart/index.tsx index b8d21f7184c3c..41a22493c509d 100644 --- a/projects/js-packages/charts/src/components/bar-chart/index.tsx +++ b/projects/js-packages/charts/src/components/bar-chart/index.tsx @@ -5,6 +5,7 @@ import { scaleBand, scaleLinear } from '@visx/scale'; import { Bar } from '@visx/shape'; import { useTooltip } from '@visx/tooltip'; import React from 'react'; +import { useChartTheme } from '../../providers/theme'; import { Tooltip } from '../tooltip'; import type { DataPoint } from '../shared/types'; @@ -28,6 +29,7 @@ function BarChart( { data, width, height, margin, showTooltips = false }: BarCha const { tooltipOpen, tooltipLeft, tooltipTop, tooltipData, hideTooltip, showTooltip } = useTooltip< DataPoint >(); + const theme = useChartTheme(); const margins = { top: 20, right: 20, bottom: 40, left: 40, ...margin }; const xMax = width - margins.left - margins.right; const yMax = height - margins.top - margins.bottom; @@ -80,7 +82,7 @@ function BarChart( { data, width, height, margin, showTooltips = false }: BarCha y={ yScale( d.value ) } width={ xScale.bandwidth() } height={ yMax - ( yScale( d.value ) ?? 0 ) } - fill="#0675C4" + fill={ theme.colors[ 0 ] } onMouseMove={ getMouseMoveHandler( d ) } onMouseLeave={ showTooltips ? handleMouseLeave : undefined } /> diff --git a/projects/js-packages/charts/src/components/bar-chart/stories/index.stories.tsx b/projects/js-packages/charts/src/components/bar-chart/stories/index.stories.tsx index be5ad4064d28d..3ad3aa21d7f9d 100644 --- a/projects/js-packages/charts/src/components/bar-chart/stories/index.stories.tsx +++ b/projects/js-packages/charts/src/components/bar-chart/stories/index.stories.tsx @@ -2,7 +2,7 @@ import BarChart from '../index'; import type { Meta } from '@storybook/react'; export default { - title: 'JS Packages/Charts/Bar Chart', + title: 'JS Packages/Charts/Types/Bar Chart', component: BarChart, parameters: { layout: 'centered', diff --git a/projects/js-packages/charts/src/components/line-chart/line-chart.tsx b/projects/js-packages/charts/src/components/line-chart/line-chart.tsx index c6a87bdaa6b12..a7aa6cf8cb9db 100644 --- a/projects/js-packages/charts/src/components/line-chart/line-chart.tsx +++ b/projects/js-packages/charts/src/components/line-chart/line-chart.tsx @@ -8,6 +8,7 @@ import { } from '@visx/xychart'; import clsx from 'clsx'; import { FC } from 'react'; +import { useChartTheme } from '../../providers/theme/theme-provider'; import styles from './line-chart.module.scss'; import type { DataPointDate } from '../shared/types'; @@ -36,52 +37,6 @@ type LineChartProps = { lineColor?: string; }; -// TODO: move to a provider -// const customTheme = buildChartTheme( { -// // Customize colors -// colors: [ '#3182ce' ], -// // Customize typography -// // labelStyles: { -// // fill: '#666', -// // fontSize: 12, -// // }, -// // Customize grid styles -// gridStyles: { -// stroke: '#e2e8f0', -// strokeWidth: 1, -// }, -// } ); - -const customTheme = buildChartTheme( { - // colors - backgroundColor: 'lightblue', // used by Tooltip, Annotation - colors: [ '#3182ce' ], // categorical colors, mapped to series via `dataKey`s - - // labels - // svgLabelBig?: SVGTextProps, - // svgLabelSmall?: SVGTextProps, - // htmlLabel?: HTMLTextStyles, - - // lines - // xAxisLineStyles?: LineStyles, - // yAxisLineStyles?: LineStyles, - // xTickLineStyles?: LineStyles, - // yTickLineStyles?: LineStyles, - // tickLength: number, - - // grid - // gridColor: string, - // gridColorDark: string, // used for axis baseline if x/yxAxisLineStyles not set - // gridStyles?: CSSProperties, - gridStyles: { - stroke: '#e2e8f0', - strokeWidth: 1, - }, - tickLength: 0, - gridColor: '', - gridColorDark: '', -} ); - // eslint-disable-next-line @typescript-eslint/no-explicit-any const renderTooltip: any = ( { tooltipData } ) => { // TODO: fix any @@ -117,18 +72,30 @@ const LineChart: FC< LineChartProps > = ( { width, height, margin = { top: 20, right: 20, bottom: 40, left: 40 }, - lineColor = '#3182ce', } ) => { + const providerTheme = useChartTheme(); const accessors = { xAccessor: ( d: DataPointDate ) => d.date, yAccessor: ( d: DataPointDate ) => d.value, }; + // Use theme to construct XYChart theme + const chartTheme = { + backgroundColor: providerTheme.backgroundColor, + colors: providerTheme.colors, + gridStyles: providerTheme.gridStyles, + tickLength: providerTheme?.tickLength || 0, + gridColor: providerTheme?.gridColor || '', + gridColorDark: providerTheme?.gridColorDark || '', + }; + + const theme = buildChartTheme( chartTheme ); + // return (
= ( { dataKey="Line" data={ data } { ...accessors } - stroke={ lineColor } + stroke={ theme.colors[ 0 ] } strokeWidth={ 2 } /> diff --git a/projects/js-packages/charts/src/components/line-chart/stories/index.stories.tsx b/projects/js-packages/charts/src/components/line-chart/stories/index.stories.tsx index 1ce2430c18365..40a11b9c67364 100644 --- a/projects/js-packages/charts/src/components/line-chart/stories/index.stories.tsx +++ b/projects/js-packages/charts/src/components/line-chart/stories/index.stories.tsx @@ -9,7 +9,7 @@ const data = [ ]; export default { - title: 'JS Packages/Charts/Line Chart', + title: 'JS Packages/Charts/Types/Line Chart', component: LineChart, parameters: { layout: 'centered', diff --git a/projects/js-packages/charts/src/components/pie-semi-circle-chart/pie-semi-circle-chart.tsx b/projects/js-packages/charts/src/components/pie-semi-circle-chart/pie-semi-circle-chart.tsx index 86340e54eea84..e67868e16adb4 100644 --- a/projects/js-packages/charts/src/components/pie-semi-circle-chart/pie-semi-circle-chart.tsx +++ b/projects/js-packages/charts/src/components/pie-semi-circle-chart/pie-semi-circle-chart.tsx @@ -3,6 +3,7 @@ import { Pie } from '@visx/shape'; import { Text } from '@visx/text'; import clsx from 'clsx'; import { FC } from 'react'; +import { useChartTheme } from '../../providers/theme/theme-provider'; import styles from './pie-semi-circle-chart.module.scss'; import type { DataPointPercentage } from '../shared/types'; @@ -38,13 +39,15 @@ const PieSemiCircleChart: FC< PieSemiCircleChartProps > = ( { label, note, } ) => { + const providerTheme = useChartTheme(); const centerX = width / 2; const centerY = height; const accessors = { value: d => d.value, sort: ( a, b ) => a.value - b.value, - fill: d => d.data.color, + // Use the color property from the data object as a last resort. The theme provides colours by default. + fill: d => d.color || providerTheme.colors[ d.index ], }; return ( diff --git a/projects/js-packages/charts/src/components/pie-semi-circle-chart/stories/index.stories.tsx b/projects/js-packages/charts/src/components/pie-semi-circle-chart/stories/index.stories.tsx index 5ebd227447aef..cfc4f50d7453c 100644 --- a/projects/js-packages/charts/src/components/pie-semi-circle-chart/stories/index.stories.tsx +++ b/projects/js-packages/charts/src/components/pie-semi-circle-chart/stories/index.stories.tsx @@ -7,26 +7,23 @@ const data = [ value: 80000, valueDisplay: '$80K', percentage: 2, - color: '#3858E9', }, { label: 'MacOS', value: 30000, valueDisplay: '$30K', percentage: 5, - color: '#80C8FF', }, { label: 'Linux', value: 22000, valueDisplay: '$22K', percentage: 1, - color: '#B999FF', }, ]; export default { - title: 'JS Packages/Charts/Pie Semi Circle Chart', + title: 'JS Packages/Charts/Types/Pie Semi Circle Chart', component: PieSemiCircleChart, parameters: { layout: 'centered', diff --git a/projects/js-packages/charts/src/components/shared/types.d.ts b/projects/js-packages/charts/src/components/shared/types.d.ts index c4ced7b65e00c..44f2fc9831d57 100644 --- a/projects/js-packages/charts/src/components/shared/types.d.ts +++ b/projects/js-packages/charts/src/components/shared/types.d.ts @@ -1,3 +1,5 @@ +import type { CSSProperties } from 'react'; + export type DataPoint = { label: string; value: number; @@ -26,7 +28,25 @@ export type DataPointPercentage = { */ percentage: number; /** - * Color code for the segment + * Color code for the segment, by default colours are taken from the theme but this property can overrides it */ color?: string; }; + +/** + * Theme configuration for chart components + */ +export type ChartTheme = { + /** Background color for chart components */ + backgroundColor: string; + /** Array of colors used for data visualization */ + colors: string[]; + /** Optional CSS styles for grid lines */ + gridStyles?: CSSProperties; + /** Length of axis ticks in pixels */ + tickLength: number; + /** Color of the grid lines */ + gridColor: string; + /** Color of the grid lines in dark mode */ + gridColorDark: string; +}; diff --git a/projects/js-packages/charts/src/providers/theme/index.ts b/projects/js-packages/charts/src/providers/theme/index.ts new file mode 100644 index 0000000000000..af2ad75002ffc --- /dev/null +++ b/projects/js-packages/charts/src/providers/theme/index.ts @@ -0,0 +1,2 @@ +export { ThemeProvider, useChartTheme } from './theme-provider'; +export { defaultTheme, jetpackTheme, wooTheme } from './themes'; diff --git a/projects/js-packages/charts/src/providers/theme/stories/index.stories.tsx b/projects/js-packages/charts/src/providers/theme/stories/index.stories.tsx new file mode 100644 index 0000000000000..69e1cdd50887f --- /dev/null +++ b/projects/js-packages/charts/src/providers/theme/stories/index.stories.tsx @@ -0,0 +1,131 @@ +import { Meta, StoryObj } from '@storybook/react'; +import { ThemeProvider, jetpackTheme, wooTheme } from '../.'; +import { LineChart, BarChart, PieSemiCircleChart } from '../../..'; + +const meta: Meta< typeof LineChart > = { + title: 'JS Packages/Charts/Themes', + component: ThemeProvider, + parameters: { + layout: 'centered', + }, +}; + +export default meta; +type Story = StoryObj< typeof ThemeProvider >; + +const sampleData = [ + { date: new Date( '2024-01-01' ), value: 10, label: 'Jan 1' }, + { date: new Date( '2024-01-02' ), value: 20, label: 'Jan 2' }, + { date: new Date( '2024-01-03' ), value: 15, label: 'Jan 3' }, + { date: new Date( '2024-01-04' ), value: 25, label: 'Jan 4' }, + { date: new Date( '2024-01-05' ), value: 30, label: 'Jan 5' }, +]; + +const pieData = [ + { + label: 'Windows', + value: 80000, + valueDisplay: '80K', + percentage: 2, + }, + { + label: 'MacOS', + value: 30000, + valueDisplay: '30K', + percentage: 5, + }, + { + label: 'Linux', + value: 22000, + valueDisplay: '22K', + percentage: 1, + }, +]; + +const GridComponent = ( { children } ) => { + return ( +
+ { children } +
+ ); +}; + +export const Default: Story = { + render: () => ( + + + + + + + + ), +}; + +export const JetpackTheme: Story = { + render: () => ( + + + + + + + + ), +}; + +export const WooTheme: Story = { + render: () => ( + + + + + + + + ), +}; + +export const CustomColorTheme: Story = { + render: () => ( + + + + + + + + ), +}; diff --git a/projects/js-packages/charts/src/providers/theme/theme-provider.tsx b/projects/js-packages/charts/src/providers/theme/theme-provider.tsx new file mode 100644 index 0000000000000..88584dda80aae --- /dev/null +++ b/projects/js-packages/charts/src/providers/theme/theme-provider.tsx @@ -0,0 +1,36 @@ +import { createContext, useContext, FC, ReactNode } from 'react'; +import { defaultTheme } from './themes'; +import type { ChartTheme } from '../../components/shared/types'; + +/** + * Context for sharing theme configuration across components + */ +const ThemeContext = createContext< ChartTheme >( defaultTheme ); + +/** + * Hook to access chart theme + * @return {object} A built theme configuration compatible with visx charts + */ +const useChartTheme = () => { + const theme = useContext( ThemeContext ); + return theme; +}; + +/** + * Props for the ThemeProvider component + */ +type ThemeProviderProps = { + /** Optional partial theme override */ + theme?: Partial< ChartTheme >; + /** Child components that will have access to the theme */ + children: ReactNode; +}; + +// Provider component for chart theming +// Allows theme customization through props while maintaining default values +const ThemeProvider: FC< ThemeProviderProps > = ( { theme = {}, children } ) => { + const mergedTheme = { ...defaultTheme, ...theme }; + return { children }; +}; + +export { ThemeProvider, useChartTheme }; diff --git a/projects/js-packages/charts/src/providers/theme/themes.ts b/projects/js-packages/charts/src/providers/theme/themes.ts new file mode 100644 index 0000000000000..b41d14bd845a1 --- /dev/null +++ b/projects/js-packages/charts/src/providers/theme/themes.ts @@ -0,0 +1,48 @@ +import type { ChartTheme } from '../../components/shared/types'; + +/** + * Default theme configuration + */ +const defaultTheme: ChartTheme = { + backgroundColor: '#FFFFFF', + colors: [ '#98C8DF', '#006DAB', '#A6DC80', '#1F9828', '#FF8C8F' ], + gridStyles: { + stroke: '#787C82', + strokeWidth: 1, + }, + tickLength: 0, + gridColor: '', + gridColorDark: '', +}; + +/** + * Jetpack theme configuration + */ +const jetpackTheme: ChartTheme = { + backgroundColor: '#FFFFFF', + colors: [ '#98C8DF', '#006DAB', '#A6DC80', '#1F9828', '#FF8C8F' ], + gridStyles: { + stroke: '#787C82', + strokeWidth: 1, + }, + tickLength: 0, + gridColor: '', + gridColorDark: '', +}; + +/** + * Woo theme configuration + */ +const wooTheme: ChartTheme = { + backgroundColor: '#FFFFFF', + colors: [ '#80C8FF', '#B999FF', '#3858E9' ], + gridStyles: { + stroke: '#787C82', + strokeWidth: 1, + }, + tickLength: 0, + gridColor: '', + gridColorDark: '', +}; + +export { defaultTheme, jetpackTheme, wooTheme };