diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4af216da4b55c..d2a2a658b2b2a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -304,6 +304,9 @@ importers: '@visx/group': specifier: ^3.12.0 version: 3.12.0(react@18.3.1) + '@visx/responsive': + specifier: 3.12.0 + version: 3.12.0(react@18.3.1) '@visx/scale': specifier: ^3.12.0 version: 3.12.0 diff --git a/projects/js-packages/charts/changelog/add-charts-pie-chart b/projects/js-packages/charts/changelog/add-charts-pie-chart new file mode 100644 index 0000000000000..81e8c5063e8ae --- /dev/null +++ b/projects/js-packages/charts/changelog/add-charts-pie-chart @@ -0,0 +1,4 @@ +Significance: patch +Type: added + +Adding new chart type - pie chart. diff --git a/projects/js-packages/charts/package.json b/projects/js-packages/charts/package.json index 41e7c7118aaa1..e51243ee01bc2 100644 --- a/projects/js-packages/charts/package.json +++ b/projects/js-packages/charts/package.json @@ -21,6 +21,7 @@ }, "dependencies": { "@react-spring/web": "9.7.3", + "@visx/responsive": "3.12.0", "@visx/axis": "^3.12.0", "@visx/group": "^3.12.0", "@visx/scale": "^3.12.0", diff --git a/projects/js-packages/charts/src/components/bar-chart/bar-chart.tsx b/projects/js-packages/charts/src/components/bar-chart/bar-chart.tsx index ee4fe451fe464..7c6f4976eaa06 100644 --- a/projects/js-packages/charts/src/components/bar-chart/bar-chart.tsx +++ b/projects/js-packages/charts/src/components/bar-chart/bar-chart.tsx @@ -5,46 +5,26 @@ import { scaleBand, scaleLinear } from '@visx/scale'; import { Bar } from '@visx/shape'; import { useTooltip } from '@visx/tooltip'; import clsx from 'clsx'; -import { FC, useCallback } from 'react'; +import { FC, useCallback, type MouseEvent } from 'react'; import { useChartTheme } from '../../providers/theme'; import { BaseTooltip } from '../tooltip'; import styles from './bar-chart.module.scss'; -import type { DataPoint } from '../shared/types'; +import type { BaseChartProps, DataPoint } from '../shared/types'; -type BarChartProps = { +interface BarChartProps extends BaseChartProps { /** * Array of data points to display in the chart */ data: DataPoint[]; - /** - * Width of the chart in pixels - */ - width: number; - /** - * Height of the chart in pixels - */ - height: number; - /** - * Chart margins - */ - margin?: { - top?: number; - right?: number; - bottom?: number; - left?: number; - }; - /** - * Whether to show tooltips on hover - */ - showTooltips?: boolean; -}; +} const BarChart: FC< BarChartProps > = ( { data, width, height, margin = { top: 20, right: 20, bottom: 40, left: 40 }, - showTooltips = false, + withTooltips = false, + className, } ) => { const theme = useChartTheme(); const { tooltipOpen, tooltipLeft, tooltipTop, tooltipData, hideTooltip, showTooltip } = @@ -66,7 +46,7 @@ const BarChart: FC< BarChartProps > = ( { } ); const handleMouseMove = useCallback( - ( event: React.MouseEvent, datum: DataPoint ) => { + ( event: MouseEvent< SVGRectElement >, datum: DataPoint ) => { const coords = localPoint( event ); if ( ! coords ) return; @@ -83,35 +63,32 @@ const BarChart: FC< BarChartProps > = ( { hideTooltip(); }, [ hideTooltip ] ); - const handleBarMouseMove = useCallback( - ( d: DataPoint ) => ( event: React.MouseEvent< SVGRectElement > ) => { - handleMouseMove( event, d ); - }, - [ handleMouseMove ] - ); - return ( -
+
- { data.map( d => ( - - ) ) } + { data.map( d => { + const handleBarMouseMove = event => handleMouseMove( event, d ); + + return ( + + ); + } ) } - { showTooltips && tooltipOpen && tooltipData && ( + { withTooltips && tooltipOpen && tooltipData && ( {} + +type TooltipData = { + date: Date; + [ key: string ]: number | Date; }; -// eslint-disable-next-line @typescript-eslint/no-explicit-any -const renderTooltip: any = ( { tooltipData } ) => { - // TODO: fix any - const datum = tooltipData?.nearestDatum?.datum; - if ( ! datum ) { - return null; - } +type TooltipDatum = { + key: string; + value: number; +}; + +const renderTooltip = ( { + tooltipData, +}: { + tooltipData?: { + nearestDatum?: { + datum: TooltipData; + key: string; + }; + datumByKey?: { [ key: string ]: { datum: TooltipData } }; + }; +} ) => { + const nearestDatum = tooltipData?.nearestDatum?.datum; + if ( ! nearestDatum ) return null; + + const tooltipPoints: TooltipDatum[] = Object.entries( tooltipData?.datumByKey || {} ) + .map( ( [ key, { datum } ] ) => ( { + key, + value: datum.value as number, + } ) ) + .sort( ( a, b ) => b.value - a.value ); return (
-
- Date: { datum.date.toLocaleDateString() } -
-
- Value: { datum.value } +
+ { nearestDatum.date.toLocaleDateString() }
+ { tooltipPoints.map( point => ( +
+ { point.key }: + { point.value } +
+ ) ) }
); }; @@ -65,35 +70,38 @@ const formatDateTick = ( value: number ) => { } ); }; -// TODO: add support for multiple data sets - const LineChart: FC< LineChartProps > = ( { data, width, height, margin = { top: 20, right: 20, bottom: 40, left: 40 }, + className, + withTooltips = true, } ) => { const providerTheme = useChartTheme(); + + if ( ! data.length ) { + return ( +
Empty...
+ ); + } + const accessors = { xAccessor: ( d: DataPointDate ) => d.date, yAccessor: ( d: DataPointDate ) => d.value, }; - // Use theme to construct XYChart theme - const chartTheme = { + const theme = buildChartTheme( { backgroundColor: providerTheme.backgroundColor, colors: providerTheme.colors, gridStyles: providerTheme.gridStyles, tickLength: providerTheme?.tickLength || 0, gridColor: providerTheme?.gridColor || '', gridColorDark: providerTheme?.gridColorDark || '', - }; - - const theme = buildChartTheme( chartTheme ); + } ); - // return ( -
+
= ( { yScale={ { type: 'linear', nice: true } } > - - - - + { data.map( ( seriesData, index ) => ( + + ) ) } + + { withTooltips && ( + + ) }
); 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 40a11b9c67364..b09bc9594c4fd 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 @@ -1,13 +1,7 @@ import { LineChart } from '../index'; +import sampleData from './sample-data'; import type { Meta } from '@storybook/react'; -const data = [ - { date: new Date( '2023-01-01' ), value: 10 }, - { date: new Date( '2023-02-01' ), value: 20 }, - { date: new Date( '2023-03-01' ), value: 15 }, - { date: new Date( '2023-04-01' ), value: 25 }, -]; - export default { title: 'JS Packages/Charts/Types/Line Chart', component: LineChart, @@ -25,10 +19,39 @@ export default { const Template = args => ; +// Default story with multiple series export const Default = Template.bind( {} ); Default.args = { width: 500, height: 300, margin: { top: 20, right: 20, bottom: 30, left: 40 }, - data, + data: sampleData, +}; + +// Story with single data series +export const SingleSeries = Template.bind( {} ); +SingleSeries.args = { + width: 500, + height: 300, + margin: { top: 20, right: 20, bottom: 30, left: 40 }, + data: [ sampleData[ 0 ] ], // Only London temperature data +}; + +// Story without tooltip +export const WithoutTooltip = Template.bind( {} ); +WithoutTooltip.args = { + width: 500, + height: 300, + margin: { top: 20, right: 20, bottom: 30, left: 40 }, + data: sampleData, + withTooltips: false, +}; + +// Story with custom dimensions +export const CustomDimensions = Template.bind( {} ); +CustomDimensions.args = { + width: 800, + height: 400, + margin: { top: 20, right: 20, bottom: 30, left: 40 }, + data: sampleData, }; diff --git a/projects/js-packages/charts/src/components/line-chart/stories/sample-data.ts b/projects/js-packages/charts/src/components/line-chart/stories/sample-data.ts new file mode 100644 index 0000000000000..1231466c476d4 --- /dev/null +++ b/projects/js-packages/charts/src/components/line-chart/stories/sample-data.ts @@ -0,0 +1,346 @@ +import type { SeriesData } from '../../shared/types'; + +// Sample data +const temperatureData: SeriesData[] = [ + { + label: 'London', + data: [ + // 2022 data + { date: new Date( '2022-01-01' ), value: 7.8 }, + { date: new Date( '2022-01-08' ), value: 7.2 }, + { date: new Date( '2022-01-15' ), value: 6.9 }, + { date: new Date( '2022-01-22' ), value: 6.5 }, + { date: new Date( '2022-01-29' ), value: 7.1 }, + { date: new Date( '2022-02-05' ), value: 8.3 }, + { date: new Date( '2022-02-12' ), value: 8.9 }, + { date: new Date( '2022-02-19' ), value: 9.2 }, + { date: new Date( '2022-02-26' ), value: 8.7 }, + { date: new Date( '2022-03-05' ), value: 9.4 }, + { date: new Date( '2022-03-12' ), value: 10.2 }, + { date: new Date( '2022-03-19' ), value: 11.5 }, + { date: new Date( '2022-03-26' ), value: 12.3 }, + { date: new Date( '2022-04-02' ), value: 13.1 }, + { date: new Date( '2022-04-09' ), value: 13.8 }, + { date: new Date( '2022-04-16' ), value: 14.6 }, + { date: new Date( '2022-04-23' ), value: 15.2 }, + { date: new Date( '2022-04-30' ), value: 15.9 }, + { date: new Date( '2022-05-07' ), value: 16.7 }, + { date: new Date( '2022-05-14' ), value: 17.4 }, + { date: new Date( '2022-05-21' ), value: 18.2 }, + { date: new Date( '2022-05-28' ), value: 18.9 }, + { date: new Date( '2022-06-04' ), value: 19.7 }, + { date: new Date( '2022-06-11' ), value: 20.5 }, + { date: new Date( '2022-06-18' ), value: 21.3 }, + { date: new Date( '2022-06-25' ), value: 22.1 }, + { date: new Date( '2022-07-02' ), value: 22.8 }, + { date: new Date( '2022-07-09' ), value: 23.6 }, + { date: new Date( '2022-07-16' ), value: 24.4 }, + { date: new Date( '2022-07-23' ), value: 25.2 }, + { date: new Date( '2022-07-30' ), value: 24.8 }, + { date: new Date( '2022-08-06' ), value: 24.1 }, + { date: new Date( '2022-08-13' ), value: 23.5 }, + { date: new Date( '2022-08-20' ), value: 22.8 }, + { date: new Date( '2022-08-27' ), value: 21.9 }, + { date: new Date( '2022-09-03' ), value: 20.7 }, + { date: new Date( '2022-09-10' ), value: 19.5 }, + { date: new Date( '2022-09-17' ), value: 18.3 }, + { date: new Date( '2022-09-24' ), value: 17.1 }, + { date: new Date( '2022-10-01' ), value: 16.2 }, + { date: new Date( '2022-10-08' ), value: 15.1 }, + { date: new Date( '2022-10-15' ), value: 14.2 }, + { date: new Date( '2022-10-22' ), value: 13.1 }, + { date: new Date( '2022-10-29' ), value: 12.2 }, + { date: new Date( '2022-11-05' ), value: 11.1 }, + { date: new Date( '2022-11-12' ), value: 10.2 }, + { date: new Date( '2022-11-19' ), value: 9.1 }, + { date: new Date( '2022-11-26' ), value: 8.2 }, + { date: new Date( '2022-12-03' ), value: 7.1 }, + { date: new Date( '2022-12-10' ), value: 6.2 }, + { date: new Date( '2022-12-17' ), value: 5.5 }, + { date: new Date( '2022-12-24' ), value: 5.2 }, + { date: new Date( '2022-12-31' ), value: 6.8 }, + // 2023 data + { date: new Date( '2023-01-01' ), value: 8.2 }, + { date: new Date( '2023-01-08' ), value: 7.9 }, + { date: new Date( '2023-01-15' ), value: 5.1 }, + { date: new Date( '2023-01-22' ), value: 4.8 }, + { date: new Date( '2023-01-29' ), value: 6.3 }, + { date: new Date( '2023-02-05' ), value: 7.2 }, + { date: new Date( '2023-02-12' ), value: 9.4 }, + { date: new Date( '2023-02-19' ), value: 8.7 }, + { date: new Date( '2023-02-26' ), value: 7.1 }, + { date: new Date( '2023-03-05' ), value: 8.3 }, + { date: new Date( '2023-03-12' ), value: 9.5 }, + { date: new Date( '2023-03-19' ), value: 11.2 }, + { date: new Date( '2023-03-26' ), value: 12.8 }, + { date: new Date( '2023-04-02' ), value: 13.4 }, + { date: new Date( '2023-04-09' ), value: 14.1 }, + { date: new Date( '2023-04-16' ), value: 15.3 }, + { date: new Date( '2023-04-23' ), value: 14.8 }, + { date: new Date( '2023-04-30' ), value: 15.7 }, + { date: new Date( '2023-05-07' ), value: 16.9 }, + { date: new Date( '2023-05-14' ), value: 17.2 }, + { date: new Date( '2023-05-21' ), value: 18.4 }, + { date: new Date( '2023-05-28' ), value: 19.1 }, + { date: new Date( '2023-06-04' ), value: 20.3 }, + { date: new Date( '2023-06-11' ), value: 21.5 }, + { date: new Date( '2023-06-18' ), value: 22.8 }, + { date: new Date( '2023-06-25' ), value: 21.9 }, + { date: new Date( '2023-07-02' ), value: 23.1 }, + { date: new Date( '2023-07-09' ), value: 22.7 }, + { date: new Date( '2023-07-16' ), value: 24.2 }, + { date: new Date( '2023-07-23' ), value: 23.8 }, + { date: new Date( '2023-07-30' ), value: 22.9 }, + { date: new Date( '2023-08-06' ), value: 23.4 }, + { date: new Date( '2023-08-13' ), value: 22.8 }, + { date: new Date( '2023-08-20' ), value: 21.9 }, + { date: new Date( '2023-08-27' ), value: 20.7 }, + { date: new Date( '2023-09-03' ), value: 19.8 }, + { date: new Date( '2023-09-10' ), value: 18.9 }, + { date: new Date( '2023-09-17' ), value: 17.6 }, + { date: new Date( '2023-09-24' ), value: 16.8 }, + { date: new Date( '2023-10-01' ), value: 15.9 }, + { date: new Date( '2023-10-08' ), value: 14.7 }, + { date: new Date( '2023-10-15' ), value: 13.8 }, + { date: new Date( '2023-10-22' ), value: 12.9 }, + { date: new Date( '2023-10-29' ), value: 11.7 }, + { date: new Date( '2023-11-05' ), value: 10.8 }, + { date: new Date( '2023-11-12' ), value: 9.9 }, + { date: new Date( '2023-11-19' ), value: 8.7 }, + { date: new Date( '2023-11-26' ), value: 7.8 }, + { date: new Date( '2023-12-03' ), value: 6.9 }, + { date: new Date( '2023-12-10' ), value: 5.8 }, + { date: new Date( '2023-12-17' ), value: 4.9 }, + { date: new Date( '2023-12-24' ), value: 5.7 }, + { date: new Date( '2023-12-31' ), value: 6.2 }, + ], + }, + { + label: 'Canberra', + data: [ + // 2022 data + { date: new Date( '2022-01-01' ), value: 27.9 }, + { date: new Date( '2022-01-08' ), value: 28.4 }, + { date: new Date( '2022-01-15' ), value: 29.2 }, + { date: new Date( '2022-01-22' ), value: 28.9 }, + { date: new Date( '2022-01-29' ), value: 28.1 }, + { date: new Date( '2022-02-05' ), value: 27.3 }, + { date: new Date( '2022-02-12' ), value: 26.5 }, + { date: new Date( '2022-02-19' ), value: 25.4 }, + { date: new Date( '2022-02-26' ), value: 24.2 }, + { date: new Date( '2022-03-05' ), value: 23.1 }, + { date: new Date( '2022-03-12' ), value: 22.3 }, + { date: new Date( '2022-03-19' ), value: 21.2 }, + { date: new Date( '2022-03-26' ), value: 20.1 }, + { date: new Date( '2022-04-02' ), value: 19.2 }, + { date: new Date( '2022-04-09' ), value: 18.1 }, + { date: new Date( '2022-04-16' ), value: 16.9 }, + { date: new Date( '2022-04-23' ), value: 15.8 }, + { date: new Date( '2022-04-30' ), value: 14.9 }, + { date: new Date( '2022-05-07' ), value: 13.8 }, + { date: new Date( '2022-05-14' ), value: 12.9 }, + { date: new Date( '2022-05-21' ), value: 11.8 }, + { date: new Date( '2022-05-28' ), value: 10.9 }, + { date: new Date( '2022-06-04' ), value: 9.8 }, + { date: new Date( '2022-06-11' ), value: 8.9 }, + { date: new Date( '2022-06-18' ), value: 8.1 }, + { date: new Date( '2022-06-25' ), value: 7.5 }, + { date: new Date( '2022-07-02' ), value: 6.9 }, + { date: new Date( '2022-07-09' ), value: 6.7 }, + { date: new Date( '2022-07-16' ), value: 7.1 }, + { date: new Date( '2022-07-23' ), value: 7.9 }, + { date: new Date( '2022-07-30' ), value: 8.8 }, + { date: new Date( '2022-08-06' ), value: 9.9 }, + { date: new Date( '2022-08-13' ), value: 11.2 }, + { date: new Date( '2022-08-20' ), value: 12.4 }, + { date: new Date( '2022-08-27' ), value: 13.6 }, + { date: new Date( '2022-09-03' ), value: 14.8 }, + { date: new Date( '2022-09-10' ), value: 16.1 }, + { date: new Date( '2022-09-17' ), value: 17.3 }, + { date: new Date( '2022-09-24' ), value: 18.5 }, + { date: new Date( '2022-10-01' ), value: 19.8 }, + { date: new Date( '2022-10-08' ), value: 21.1 }, + { date: new Date( '2022-10-15' ), value: 22.3 }, + { date: new Date( '2022-10-22' ), value: 23.5 }, + { date: new Date( '2022-10-29' ), value: 24.6 }, + { date: new Date( '2022-11-05' ), value: 25.7 }, + { date: new Date( '2022-11-12' ), value: 26.5 }, + { date: new Date( '2022-11-19' ), value: 27.4 }, + { date: new Date( '2022-11-26' ), value: 28.2 }, + { date: new Date( '2022-12-03' ), value: 28.9 }, + { date: new Date( '2022-12-10' ), value: 29.5 }, + { date: new Date( '2022-12-17' ), value: 29.1 }, + { date: new Date( '2022-12-24' ), value: 28.2 }, + { date: new Date( '2022-12-31' ), value: 28.7 }, + // 2023 data + { date: new Date( '2023-01-01' ), value: 28.5 }, + { date: new Date( '2023-01-08' ), value: 29.2 }, + { date: new Date( '2023-01-15' ), value: 30.1 }, + { date: new Date( '2023-01-22' ), value: 29.8 }, + { date: new Date( '2023-01-29' ), value: 28.9 }, + { date: new Date( '2023-02-05' ), value: 27.8 }, + { date: new Date( '2023-02-12' ), value: 26.9 }, + { date: new Date( '2023-02-19' ), value: 25.7 }, + { date: new Date( '2023-02-26' ), value: 24.8 }, + { date: new Date( '2023-03-05' ), value: 23.9 }, + { date: new Date( '2023-03-12' ), value: 22.8 }, + { date: new Date( '2023-03-19' ), value: 21.7 }, + { date: new Date( '2023-03-26' ), value: 20.8 }, + { date: new Date( '2023-04-02' ), value: 19.6 }, + { date: new Date( '2023-04-09' ), value: 18.4 }, + { date: new Date( '2023-04-16' ), value: 17.2 }, + { date: new Date( '2023-04-23' ), value: 16.1 }, + { date: new Date( '2023-04-30' ), value: 15.3 }, + { date: new Date( '2023-05-07' ), value: 14.2 }, + { date: new Date( '2023-05-14' ), value: 13.1 }, + { date: new Date( '2023-05-21' ), value: 12.3 }, + { date: new Date( '2023-05-28' ), value: 11.4 }, + { date: new Date( '2023-06-04' ), value: 10.2 }, + { date: new Date( '2023-06-11' ), value: 9.1 }, + { date: new Date( '2023-06-18' ), value: 8.3 }, + { date: new Date( '2023-06-25' ), value: 7.8 }, + { date: new Date( '2023-07-02' ), value: 7.1 }, + { date: new Date( '2023-07-09' ), value: 6.9 }, + { date: new Date( '2023-07-16' ), value: 7.2 }, + { date: new Date( '2023-07-23' ), value: 8.1 }, + { date: new Date( '2023-07-30' ), value: 9.3 }, + { date: new Date( '2023-08-06' ), value: 10.4 }, + { date: new Date( '2023-08-13' ), value: 11.6 }, + { date: new Date( '2023-08-20' ), value: 12.8 }, + { date: new Date( '2023-08-27' ), value: 13.9 }, + { date: new Date( '2023-09-03' ), value: 15.2 }, + { date: new Date( '2023-09-10' ), value: 16.4 }, + { date: new Date( '2023-09-17' ), value: 17.6 }, + { date: new Date( '2023-09-24' ), value: 18.9 }, + { date: new Date( '2023-10-01' ), value: 20.1 }, + { date: new Date( '2023-10-08' ), value: 21.3 }, + { date: new Date( '2023-10-15' ), value: 22.5 }, + { date: new Date( '2023-10-22' ), value: 23.7 }, + { date: new Date( '2023-10-29' ), value: 24.8 }, + { date: new Date( '2023-11-05' ), value: 25.9 }, + { date: new Date( '2023-11-12' ), value: 26.7 }, + { date: new Date( '2023-11-19' ), value: 27.8 }, + { date: new Date( '2023-11-26' ), value: 28.6 }, + { date: new Date( '2023-12-03' ), value: 29.4 }, + { date: new Date( '2023-12-10' ), value: 30.2 }, + { date: new Date( '2023-12-17' ), value: 29.8 }, + { date: new Date( '2023-12-24' ), value: 28.9 }, + { date: new Date( '2023-12-31' ), value: 29.3 }, + ], + }, + { + label: 'Mars', + data: [ + // 2022 data + { date: new Date( '2022-01-01' ), value: -62 }, + { date: new Date( '2022-01-08' ), value: -63 }, + { date: new Date( '2022-01-15' ), value: -64 }, + { date: new Date( '2022-01-22' ), value: -62 }, + { date: new Date( '2022-01-29' ), value: -61 }, + { date: new Date( '2022-02-05' ), value: -59 }, + { date: new Date( '2022-02-12' ), value: -56 }, + { date: new Date( '2022-02-19' ), value: -53 }, + { date: new Date( '2022-02-26' ), value: -50 }, + { date: new Date( '2022-03-05' ), value: -47 }, + { date: new Date( '2022-03-12' ), value: -44 }, + { date: new Date( '2022-03-19' ), value: -41 }, + { date: new Date( '2022-03-26' ), value: -37 }, + { date: new Date( '2022-04-02' ), value: -34 }, + { date: new Date( '2022-04-09' ), value: -31 }, + { date: new Date( '2022-04-16' ), value: -27 }, + { date: new Date( '2022-04-23' ), value: -24 }, + { date: new Date( '2022-04-30' ), value: -21 }, + { date: new Date( '2022-05-07' ), value: -17 }, + { date: new Date( '2022-05-14' ), value: -14 }, + { date: new Date( '2022-05-21' ), value: -11 }, + { date: new Date( '2022-05-28' ), value: -7 }, + { date: new Date( '2022-06-04' ), value: -4 }, + { date: new Date( '2022-06-11' ), value: -1 }, + { date: new Date( '2022-06-18' ), value: 1 }, + { date: new Date( '2022-06-25' ), value: 3 }, + { date: new Date( '2022-07-02' ), value: 6 }, + { date: new Date( '2022-07-09' ), value: 9 }, + { date: new Date( '2022-07-16' ), value: 11 }, + { date: new Date( '2022-07-23' ), value: 13 }, + { date: new Date( '2022-07-30' ), value: 16 }, + { date: new Date( '2022-08-06' ), value: 18 }, + { date: new Date( '2022-08-13' ), value: 21 }, + { date: new Date( '2022-08-20' ), value: 23 }, + { date: new Date( '2022-08-27' ), value: 21 }, + { date: new Date( '2022-09-03' ), value: 19 }, + { date: new Date( '2022-09-10' ), value: 16 }, + { date: new Date( '2022-09-17' ), value: 13 }, + { date: new Date( '2022-09-24' ), value: 9 }, + { date: new Date( '2022-10-01' ), value: 6 }, + { date: new Date( '2022-10-08' ), value: 3 }, + { date: new Date( '2022-10-15' ), value: -1 }, + { date: new Date( '2022-10-22' ), value: -4 }, + { date: new Date( '2022-10-29' ), value: -7 }, + { date: new Date( '2022-11-05' ), value: -11 }, + { date: new Date( '2022-11-12' ), value: -14 }, + { date: new Date( '2022-11-19' ), value: -17 }, + { date: new Date( '2022-11-26' ), value: -21 }, + { date: new Date( '2022-12-03' ), value: -24 }, + { date: new Date( '2022-12-10' ), value: -27 }, + { date: new Date( '2022-12-17' ), value: -31 }, + { date: new Date( '2022-12-24' ), value: -36 }, + { date: new Date( '2022-12-31' ), value: -37 }, + // 2023 data + { date: new Date( '2023-01-01' ), value: -63 }, + { date: new Date( '2023-01-08' ), value: -64 }, + { date: new Date( '2023-01-15' ), value: -65 }, + { date: new Date( '2023-01-22' ), value: -63 }, + { date: new Date( '2023-01-29' ), value: -62 }, + { date: new Date( '2023-02-05' ), value: -60 }, + { date: new Date( '2023-02-12' ), value: -58 }, + { date: new Date( '2023-02-19' ), value: -55 }, + { date: new Date( '2023-02-26' ), value: -52 }, + { date: new Date( '2023-03-05' ), value: -48 }, + { date: new Date( '2023-03-12' ), value: -45 }, + { date: new Date( '2023-03-19' ), value: -42 }, + { date: new Date( '2023-03-26' ), value: -38 }, + { date: new Date( '2023-04-02' ), value: -35 }, + { date: new Date( '2023-04-09' ), value: -32 }, + { date: new Date( '2023-04-16' ), value: -28 }, + { date: new Date( '2023-04-23' ), value: -25 }, + { date: new Date( '2023-04-30' ), value: -22 }, + { date: new Date( '2023-05-07' ), value: -18 }, + { date: new Date( '2023-05-14' ), value: -15 }, + { date: new Date( '2023-05-21' ), value: -12 }, + { date: new Date( '2023-05-28' ), value: -8 }, + { date: new Date( '2023-06-04' ), value: -5 }, + { date: new Date( '2023-06-11' ), value: -2 }, + { date: new Date( '2023-06-18' ), value: 0 }, + { date: new Date( '2023-06-25' ), value: 2 }, + { date: new Date( '2023-07-02' ), value: 5 }, + { date: new Date( '2023-07-09' ), value: 8 }, + { date: new Date( '2023-07-16' ), value: 10 }, + { date: new Date( '2023-07-23' ), value: 12 }, + { date: new Date( '2023-07-30' ), value: 15 }, + { date: new Date( '2023-08-06' ), value: 17 }, + { date: new Date( '2023-08-13' ), value: 20 }, + { date: new Date( '2023-08-20' ), value: 22 }, + { date: new Date( '2023-08-27' ), value: 20 }, + { date: new Date( '2023-09-03' ), value: 18 }, + { date: new Date( '2023-09-10' ), value: 15 }, + { date: new Date( '2023-09-17' ), value: 12 }, + { date: new Date( '2023-09-24' ), value: 8 }, + { date: new Date( '2023-10-01' ), value: 5 }, + { date: new Date( '2023-10-08' ), value: 2 }, + { date: new Date( '2023-10-15' ), value: -2 }, + { date: new Date( '2023-10-22' ), value: -5 }, + { date: new Date( '2023-10-29' ), value: -8 }, + { date: new Date( '2023-11-05' ), value: -12 }, + { date: new Date( '2023-11-12' ), value: -15 }, + { date: new Date( '2023-11-19' ), value: -18 }, + { date: new Date( '2023-11-26' ), value: -22 }, + { date: new Date( '2023-12-03' ), value: -25 }, + { date: new Date( '2023-12-10' ), value: -28 }, + { date: new Date( '2023-12-17' ), value: -32 }, + { date: new Date( '2023-12-24' ), value: -35 }, + { date: new Date( '2023-12-31' ), value: -38 }, + ], + }, +]; + +export default temperatureData; diff --git a/projects/js-packages/charts/src/components/pie-chart/index.tsx b/projects/js-packages/charts/src/components/pie-chart/index.tsx new file mode 100644 index 0000000000000..c5b0025459ea3 --- /dev/null +++ b/projects/js-packages/charts/src/components/pie-chart/index.tsx @@ -0,0 +1 @@ +export { default as PieChart } from './pie-chart'; diff --git a/projects/js-packages/charts/src/components/pie-chart/pie-chart.module.scss b/projects/js-packages/charts/src/components/pie-chart/pie-chart.module.scss new file mode 100644 index 0000000000000..cbb24ea286735 --- /dev/null +++ b/projects/js-packages/charts/src/components/pie-chart/pie-chart.module.scss @@ -0,0 +1,3 @@ +.pie-chart { + position: relative; +} diff --git a/projects/js-packages/charts/src/components/pie-chart/pie-chart.tsx b/projects/js-packages/charts/src/components/pie-chart/pie-chart.tsx new file mode 100644 index 0000000000000..712ce212792e1 --- /dev/null +++ b/projects/js-packages/charts/src/components/pie-chart/pie-chart.tsx @@ -0,0 +1,116 @@ +import { Group } from '@visx/group'; +import { Pie } from '@visx/shape'; +import clsx from 'clsx'; +import { SVGProps } from 'react'; +import useChartMouseHandler from '../../hooks/use-chart-mouse-handler'; +import { useChartTheme, defaultTheme } from '../../providers/theme'; +import { Tooltip } from '../tooltip'; +import styles from './pie-chart.module.scss'; +import type { BaseChartProps, DataPoint } from '../shared/types'; + +// TODO: add animation + +interface PieChartProps extends BaseChartProps< DataPoint[] > { + /** + * Inner radius in pixels. If > 0, creates a donut chart. Defaults to 0. + */ + innerRadius?: number; +} + +/** + * Renders a pie or donut chart using the provided data. + * + * @param {PieChartProps} props - Component props + * @return {JSX.Element} The rendered chart component + */ +const PieChart = ( { + data, + width, + height, + withTooltips = false, + innerRadius = 0, + className, +}: PieChartProps ) => { + const providerTheme = useChartTheme(); + const { onMouseMove, onMouseLeave, tooltipOpen, tooltipData, tooltipLeft, tooltipTop } = + useChartMouseHandler( { + withTooltips, + } ); + + // Calculate radius based on width/height + const radius = Math.min( width, height ) / 2; + const centerX = width / 2; + const centerY = height / 2; + + const accessors = { + value: d => d.value, + // 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 ( +
+ + + + { pie => { + return pie.arcs.map( ( arc, index ) => { + const [ centroidX, centroidY ] = pie.path.centroid( arc ); + const hasSpaceForLabel = arc.endAngle - arc.startAngle >= 0.25; + const handleMouseMove = event => onMouseMove( event, arc.data ); + + const pathProps: SVGProps< SVGPathElement > = { + d: pie.path( arc ) || '', + fill: accessors.fill( arc ), + }; + + if ( withTooltips ) { + pathProps.onMouseMove = handleMouseMove; + pathProps.onMouseLeave = onMouseLeave; + } + + return ( + + + { hasSpaceForLabel && ( + + { arc.data.label } + + ) } + + ); + } ); + } } + + + + { withTooltips && tooltipOpen && tooltipData && ( + + ) } +
+ ); +}; + +export default PieChart; diff --git a/projects/js-packages/charts/src/components/pie-chart/stories/index.stories.tsx b/projects/js-packages/charts/src/components/pie-chart/stories/index.stories.tsx new file mode 100644 index 0000000000000..ddad97895531d --- /dev/null +++ b/projects/js-packages/charts/src/components/pie-chart/stories/index.stories.tsx @@ -0,0 +1,94 @@ +import { ThemeProvider, jetpackTheme, wooTheme } from '../../../providers/theme'; +import { PieChart } from '../index'; +import type { Meta, StoryObj } from '@storybook/react'; + +const data = [ + { label: 'A', value: 30 }, + { label: 'B', value: 20 }, + { label: 'C', value: 15 }, + { label: 'D', value: 35 }, +]; + +type StoryType = StoryObj< typeof PieChart >; + +export default { + title: 'JS Packages/Charts/Types/Pie Chart', + component: PieChart, + parameters: { + layout: 'centered', + }, + argTypes: { + theme: { + control: 'select', + options: { + default: undefined, + jetpack: jetpackTheme, + woo: wooTheme, + }, + defaultValue: undefined, + }, + }, + decorators: [ + ( Story, { args } ) => ( + +
+ +
+
+ ), + ], +} satisfies Meta< typeof PieChart >; + +export const Default: StoryType = { + args: { + width: 400, + height: 400, + withTooltips: false, + data, + theme: 'default', + innerRadius: 0, + }, +}; + +export const Doughnut: StoryType = { + args: { + ...Default.args, + innerRadius: 80, + }, + parameters: { + docs: { + description: { + story: 'Doughnut chart variant with inner radius of 80px.', + }, + }, + }, +}; + +export const WithTooltips: StoryType = { + args: { + ...Default.args, + withTooltips: true, + }, + parameters: { + docs: { + description: { + story: 'Pie chart with interactive tooltips that appear on hover.', + }, + }, + }, +}; + +export const WithTooltipsDoughnut: StoryType = { + args: { + ...Default.args, + withTooltips: true, + innerRadius: 100, + }, + parameters: { + docs: { + description: { + story: 'Doughnut chart with interactive tooltips that appear on hover.', + }, + }, + }, +}; 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 988e15412b003..a6f0d30e6d68b 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 @@ -8,23 +8,9 @@ import { FC, useCallback } from 'react'; import { useChartTheme } from '../../providers/theme/theme-provider'; import { BaseTooltip } from '../tooltip'; import styles from './pie-semi-circle-chart.module.scss'; -import type { DataPointPercentage } from '../shared/types'; +import type { BaseChartProps, DataPointPercentage } from '../shared/types'; -type ArcData = PieArcDatum< DataPointPercentage >; - -interface PieSemiCircleChartProps { - /** - * Array of data points to display in the chart - */ - data: DataPointPercentage[]; - /** - * Width of the chart in pixels - */ - width: number; - /** - * Height of the chart in pixels - */ - height: number; +interface PieSemiCircleChartProps extends BaseChartProps< DataPointPercentage[] > { /** * Label text to display above the chart */ @@ -33,19 +19,18 @@ interface PieSemiCircleChartProps { * Note text to display below the label */ note: string; - /** - * Whether to show tooltips - */ - showTooltips?: boolean; } +type ArcData = PieArcDatum< DataPointPercentage >; + const PieSemiCircleChart: FC< PieSemiCircleChartProps > = ( { data, width, height, label, note, - showTooltips = false, + className, + withTooltips = false, } ) => { const providerTheme = useChartTheme(); const { tooltipOpen, tooltipLeft, tooltipTop, tooltipData, hideTooltip, showTooltip } = @@ -97,7 +82,9 @@ const PieSemiCircleChart: FC< PieSemiCircleChartProps > = ( { ); return ( -
+
{ /* Main chart group that contains both the pie and text elements */ } @@ -106,7 +93,7 @@ const PieSemiCircleChart: FC< PieSemiCircleChartProps > = ( { data={ dataWithIndex } pieValue={ accessors.value } outerRadius={ width / 2 } // half of the diameter (width) - innerRadius={ ( width / 2 ) * 0.6 } // 70% of the radius + innerRadius={ ( width / 2 ) * 0.6 } // 60% of the radius cornerRadius={ 3 } padAngle={ 0.03 } startAngle={ -Math.PI / 2 } @@ -147,7 +134,7 @@ const PieSemiCircleChart: FC< PieSemiCircleChartProps > = ( { - { showTooltips && tooltipOpen && tooltipData && ( + { withTooltips && tooltipOpen && tooltipData && ( = { + /** + * Array of data points to display in the chart + */ + data: T extends DataPoint | DataPointDate ? T[] : T; + /** + * Additional CSS class name for the chart container + */ + className?: string; + /** + * Width of the chart in pixels + */ + width: number; + /** + * Height of the chart in pixels + */ + height: number; + /** + * Chart margins + */ + margin?: { + top: number; + right: number; + bottom: number; + left: number; + }; + /** + * Whether to show tooltips on hover. False by default. + */ + withTooltips?: boolean; +}; diff --git a/projects/js-packages/charts/src/hooks/use-chart-mouse-handler.ts b/projects/js-packages/charts/src/hooks/use-chart-mouse-handler.ts new file mode 100644 index 0000000000000..8a1739a90e4ec --- /dev/null +++ b/projects/js-packages/charts/src/hooks/use-chart-mouse-handler.ts @@ -0,0 +1,90 @@ +import { localPoint } from '@visx/event'; +import { useTooltip } from '@visx/tooltip'; +import { useCallback, type MouseEvent } from 'react'; +import type { DataPoint } from '../components/shared/types'; + +type UseChartMouseHandlerProps = { + /** + * Whether tooltips are enabled + */ + withTooltips: boolean; +}; + +type UseChartMouseHandlerReturn = { + /** + * Handler for mouse move events + */ + onMouseMove: ( event: React.MouseEvent< SVGElement >, data: DataPoint ) => void; + /** + * Handler for mouse leave events + */ + onMouseLeave: () => void; + /** + * Whether the tooltip is currently open + */ + tooltipOpen: boolean; + /** + * The current tooltip data + */ + tooltipData: DataPoint | null; + /** + * The current tooltip left position + */ + tooltipLeft: number | undefined; + /** + * The current tooltip top position + */ + tooltipTop: number | undefined; +}; + +/** + * Hook to handle mouse interactions for chart components + * + * @param {UseChartMouseHandlerProps} props - Hook configuration + * @return {UseChartMouseHandlerReturn} Object containing handlers and tooltip state + */ +const useChartMouseHandler = ( { + withTooltips, +}: UseChartMouseHandlerProps ): UseChartMouseHandlerReturn => { + const { tooltipOpen, tooltipLeft, tooltipTop, tooltipData, hideTooltip, showTooltip } = + useTooltip< DataPoint >(); + + // TODO: either debounce/throttle or use useTooltipInPortal with built-in debounce + const onMouseMove = useCallback( + ( event: MouseEvent< SVGElement >, data: DataPoint ) => { + if ( ! withTooltips ) { + return; + } + + const coords = localPoint( event ); + if ( ! coords ) { + return; + } + + showTooltip( { + tooltipData: data, + tooltipLeft: coords.x, + tooltipTop: coords.y - 10, + } ); + }, + [ withTooltips, showTooltip ] + ); + + const onMouseLeave = useCallback( () => { + if ( ! withTooltips ) { + return; + } + hideTooltip(); + }, [ withTooltips, hideTooltip ] ); + + return { + onMouseMove, + onMouseLeave, + tooltipOpen, + tooltipData, + tooltipLeft, + tooltipTop, + }; +}; + +export default useChartMouseHandler; diff --git a/projects/js-packages/charts/src/index.ts b/projects/js-packages/charts/src/index.ts index 8dc8f3221948a..b52a51461252c 100644 --- a/projects/js-packages/charts/src/index.ts +++ b/projects/js-packages/charts/src/index.ts @@ -1,6 +1,17 @@ +// Charts export { default as BarChart } from './components/bar-chart'; export { LineChart } from './components/line-chart'; +export { PieChart } from './components/pie-chart'; export { PieSemiCircleChart } from './components/pie-semi-circle-chart'; -export type * from './components/shared/types'; + +// Chart components export { BaseTooltip } from './components/tooltip'; + +// Providers +export { ThemeProvider } from './providers/theme'; + +// Hooks + +// Types +export type * from './components/shared/types'; export type { BaseTooltipProps } from './components/tooltip'; diff --git a/projects/js-packages/charts/src/providers/theme/themes.ts b/projects/js-packages/charts/src/providers/theme/themes.ts index b41d14bd845a1..58bcf3c3fcb31 100644 --- a/projects/js-packages/charts/src/providers/theme/themes.ts +++ b/projects/js-packages/charts/src/providers/theme/themes.ts @@ -4,7 +4,8 @@ import type { ChartTheme } from '../../components/shared/types'; * Default theme configuration */ const defaultTheme: ChartTheme = { - backgroundColor: '#FFFFFF', + backgroundColor: '#FFFFFF', // chart background color + labelBackgroundColor: '#FFFFFF', // label background color colors: [ '#98C8DF', '#006DAB', '#A6DC80', '#1F9828', '#FF8C8F' ], gridStyles: { stroke: '#787C82', @@ -19,7 +20,8 @@ const defaultTheme: ChartTheme = { * Jetpack theme configuration */ const jetpackTheme: ChartTheme = { - backgroundColor: '#FFFFFF', + backgroundColor: '#FFFFFF', // chart background color + labelBackgroundColor: '#FFFFFF', // label background color colors: [ '#98C8DF', '#006DAB', '#A6DC80', '#1F9828', '#FF8C8F' ], gridStyles: { stroke: '#787C82', @@ -34,7 +36,8 @@ const jetpackTheme: ChartTheme = { * Woo theme configuration */ const wooTheme: ChartTheme = { - backgroundColor: '#FFFFFF', + backgroundColor: '#FFFFFF', // chart background color + labelBackgroundColor: '#FFFFFF', // label background color colors: [ '#80C8FF', '#B999FF', '#3858E9' ], gridStyles: { stroke: '#787C82',