diff --git a/packages/charts/package.json b/packages/charts/package.json index 10d4b98ce84..215d1f2a576 100644 --- a/packages/charts/package.json +++ b/packages/charts/package.json @@ -36,7 +36,7 @@ "dependencies": { "clsx": "2.1.1", "react-content-loader": "7.1.1", - "recharts": "2.15.4" + "recharts": "3.1.2" }, "peerDependencies": { "@ui5/webcomponents-react": "~2.13.0", diff --git a/packages/charts/src/components/BarChart/BarChart.stories.tsx b/packages/charts/src/components/BarChart/BarChart.stories.tsx index 6211e1bd9bc..e9132736cc7 100644 --- a/packages/charts/src/components/BarChart/BarChart.stories.tsx +++ b/packages/charts/src/components/BarChart/BarChart.stories.tsx @@ -86,7 +86,18 @@ export const WithDataLabels: Story = { export const WithFormatter: Story = { args: { - dimensions: [{ accessor: 'name', formatter: (element) => element.slice(0, 3) }], + dimensions: [ + { + accessor: 'name', + formatter: (element) => { + //todo: remove once issue has been fixed (should never be number in this case) + if (typeof element === 'string') { + return element.slice(0, 3); + } + return element; + }, + }, + ], measures: [ { accessor: 'users', diff --git a/packages/charts/src/components/BarChart/BarChart.tsx b/packages/charts/src/components/BarChart/BarChart.tsx index 2d0e5c2dad3..d1efb1e4e7b 100644 --- a/packages/charts/src/components/BarChart/BarChart.tsx +++ b/packages/charts/src/components/BarChart/BarChart.tsx @@ -2,7 +2,7 @@ import { enrichEventWithDetails, ThemingParameters, useIsRTL, useSyncRef } from '@ui5/webcomponents-react-base'; import type { CSSProperties } from 'react'; -import { forwardRef, useCallback } from 'react'; +import { useRef, forwardRef, useCallback } from 'react'; import { Bar, BarChart as BarChartLib, @@ -17,7 +17,6 @@ import { YAxis, } from 'recharts'; import type { YAxisProps } from 'recharts'; -import { getValueByDataKey } from 'recharts/lib/util/ChartUtils.js'; import { useCancelAnimationFallback } from '../../hooks/useCancelAnimationFallback.js'; import { useChartMargin } from '../../hooks/useChartMargin.js'; import { useLabelFormatter } from '../../hooks/useLabelFormatter.js'; @@ -27,7 +26,7 @@ import { useObserveXAxisHeights } from '../../hooks/useObserveXAxisHeights.js'; import { useOnClickInternal } from '../../hooks/useOnClickInternal.js'; import { usePrepareDimensionsAndMeasures } from '../../hooks/usePrepareDimensionsAndMeasures.js'; import { useTooltipFormatter } from '../../hooks/useTooltipFormatter.js'; -import type { IChartBaseProps } from '../../interfaces/IChartBaseProps.js'; +import type { ActivePayload, IChartBaseProps } from '../../interfaces/IChartBaseProps.js'; import type { IChartDimension } from '../../interfaces/IChartDimension.js'; import type { IChartMeasure } from '../../interfaces/IChartMeasure.js'; import { ChartContainer } from '../../internal/ChartContainer.js'; @@ -48,12 +47,6 @@ const measureDefaults = { opacity: 1, }; -const valueAccessor = - (attribute) => - ({ payload }) => { - return getValueByDataKey(payload, attribute); - }; - interface MeasureConfig extends IChartMeasure { /** * Bar Width @@ -165,7 +158,6 @@ const BarChart = forwardRef((props, ref) => { ...props.chartConfig, }; const referenceLine = chartConfig.referenceLine; - const { dimensions, measures } = usePrepareDimensionsAndMeasures( props.dimensions, props.measures, @@ -187,7 +179,7 @@ const BarChart = forwardRef((props, ref) => { : 0; const [componentRef, chartRef] = useSyncRef(ref); - + const activePayloadsRef = useRef(measures); const onItemLegendClick = useLegendItemClick(onLegendClick); const tooltipLabelFormatter = useLabelFormatter(primaryDimension?.formatter); @@ -210,7 +202,7 @@ const BarChart = forwardRef((props, ref) => { [onDataPointClick], ); - const onClickInternal = useOnClickInternal(onClick); + const onClickInternal = useOnClickInternal(onClick, dataset, activePayloadsRef); const isBigDataSet = dataset?.length > 30; const primaryDimensionAccessor = primaryDimension?.accessor; @@ -313,17 +305,28 @@ const BarChart = forwardRef((props, ref) => { })} {isMounted && measures.map((element, index) => { + const color = element.color ?? `var(--sapChart_OrderedColor_${(index % 12) + 1})`; + const dataKey = element.accessor; + const name = element.label ?? element.accessor; + const opacity = element.opacity ?? 1; + activePayloadsRef.current[index].color = color; + activePayloadsRef.current[index].stroke = color; + activePayloadsRef.current[index].dataKey = dataKey; + activePayloadsRef.current[index].hide = element.hide; + activePayloadsRef.current[index].name = name; + activePayloadsRef.current[index].fillOpacity = opacity; + activePayloadsRef.current[index].strokeOpacity = opacity; return ( ((props, ref) => { onAnimationEnd={handleBarAnimationEnd} > } /> {dataset.map((data, i) => { @@ -364,7 +366,6 @@ const BarChart = forwardRef((props, ref) => { label={referenceLine?.label} /> )} - {/*ToDo: remove conditional rendering once `active` is working again (https://github.com/recharts/recharts/issues/2703)*/} {tooltipConfig?.active !== false && ( { }), ); - cy.contains('Users').click(); + cy.get('[class="recharts-legend-wrapper"]').findByText('Users').realClick(); cy.get('@onLegendClick').should( 'have.been.calledWith', Cypress.sinon.match({ diff --git a/packages/charts/src/components/BulletChart/BulletChart.stories.tsx b/packages/charts/src/components/BulletChart/BulletChart.stories.tsx index ed746d247f9..7dcdfd8e75e 100644 --- a/packages/charts/src/components/BulletChart/BulletChart.stories.tsx +++ b/packages/charts/src/components/BulletChart/BulletChart.stories.tsx @@ -52,7 +52,18 @@ export const Default: Story = {}; export const WithCustomSize: Story = { args: { layout: 'vertical', - dimensions: [{ accessor: 'name', formatter: (element) => element.slice(0, 3) }], + dimensions: [ + { + accessor: 'name', + formatter: (element) => { + //todo: remove once issue has been fixed (should never be number in this case) + if (typeof element === 'string') { + return element.slice(0, 3); + } + return element; + }, + }, + ], measures: [ { accessor: 'users', diff --git a/packages/charts/src/components/BulletChart/BulletChart.tsx b/packages/charts/src/components/BulletChart/BulletChart.tsx index 855683c5bc3..e7282129a54 100644 --- a/packages/charts/src/components/BulletChart/BulletChart.tsx +++ b/packages/charts/src/components/BulletChart/BulletChart.tsx @@ -2,7 +2,7 @@ import { enrichEventWithDetails, ThemingParameters, useIsRTL, useSyncRef } from '@ui5/webcomponents-react-base'; import type { CSSProperties } from 'react'; -import { forwardRef, useCallback, useMemo } from 'react'; +import { useRef, forwardRef, useCallback, useMemo } from 'react'; import { Bar, Brush, @@ -24,7 +24,7 @@ import { useObserveXAxisHeights } from '../../hooks/useObserveXAxisHeights.js'; import { useOnClickInternal } from '../../hooks/useOnClickInternal.js'; import { usePrepareDimensionsAndMeasures } from '../../hooks/usePrepareDimensionsAndMeasures.js'; import { useTooltipFormatter } from '../../hooks/useTooltipFormatter.js'; -import type { IChartBaseProps } from '../../interfaces/IChartBaseProps.js'; +import type { ActivePayload, IChartBaseProps } from '../../interfaces/IChartBaseProps.js'; import type { IChartDimension } from '../../interfaces/IChartDimension.js'; import type { IChartMeasure } from '../../interfaces/IChartMeasure.js'; import { ChartContainer } from '../../internal/ChartContainer.js'; @@ -172,17 +172,22 @@ const BulletChart = forwardRef((props, ref) => dimensionDefaults, measureDefaults, ); + const activePayloadsRef = useRef(measures); const sortedMeasures = useMemo(() => { - return measures.sort((measure) => { - if (measure.type === 'comparison') { + return [...measures].sort((a, b) => { + if (a.type === 'primary' && b.type !== 'primary') { + return -1; + } + if (b.type === 'primary' && a.type !== 'primary') { return 1; } - - if (measure.type === 'primary') { + if (a.type === 'comparison' && b.type !== 'comparison') { + return 1; + } + if (b.type === 'comparison' && a.type !== 'comparison') { return -1; } - return 0; }); }, [measures]); @@ -239,7 +244,8 @@ const BulletChart = forwardRef((props, ref) => ); const onItemLegendClick = useLegendItemClick(onLegendClick); - const onClickInternal = useOnClickInternal(onClick); + //todo: implement activePayloadsRef + const onClickInternal = useOnClickInternal(onClick, dataset, activePayloadsRef); const isBigDataSet = dataset?.length > 30; const primaryDimensionAccessor = primaryDimension?.accessor; diff --git a/packages/charts/src/components/ColumnChart/ColumnChart.tsx b/packages/charts/src/components/ColumnChart/ColumnChart.tsx index a0d7c4ac5e8..5a2a732fb8c 100644 --- a/packages/charts/src/components/ColumnChart/ColumnChart.tsx +++ b/packages/charts/src/components/ColumnChart/ColumnChart.tsx @@ -2,7 +2,7 @@ import { enrichEventWithDetails, ThemingParameters, useIsRTL, useSyncRef } from '@ui5/webcomponents-react-base'; import type { CSSProperties } from 'react'; -import { forwardRef, useCallback } from 'react'; +import { useRef, forwardRef, useCallback } from 'react'; import { Bar as Column, BarChart as ColumnChartLib, @@ -17,7 +17,6 @@ import { YAxis, } from 'recharts'; import type { YAxisProps } from 'recharts'; -import { getValueByDataKey } from 'recharts/lib/util/ChartUtils.js'; import { useCancelAnimationFallback } from '../../hooks/useCancelAnimationFallback.js'; import { useChartMargin } from '../../hooks/useChartMargin.js'; import { useLabelFormatter } from '../../hooks/useLabelFormatter.js'; @@ -27,7 +26,7 @@ import { useObserveXAxisHeights } from '../../hooks/useObserveXAxisHeights.js'; import { useOnClickInternal } from '../../hooks/useOnClickInternal.js'; import { usePrepareDimensionsAndMeasures } from '../../hooks/usePrepareDimensionsAndMeasures.js'; import { useTooltipFormatter } from '../../hooks/useTooltipFormatter.js'; -import type { IChartBaseProps } from '../../interfaces/IChartBaseProps.js'; +import type { ActivePayload, IChartBaseProps } from '../../interfaces/IChartBaseProps.js'; import type { IChartDimension } from '../../interfaces/IChartDimension.js'; import type { IChartMeasure } from '../../interfaces/IChartMeasure.js'; import { ChartContainer } from '../../internal/ChartContainer.js'; @@ -116,12 +115,6 @@ const measureDefaults = { opacity: 1, }; -const valueAccessor = - (attribute) => - ({ payload }) => { - return getValueByDataKey(payload, attribute); - }; - /** * A `ColumnChart` is a data visualization where each category is represented by a rectangle, with the height of the rectangle being proportional to the values being plotted. */ @@ -183,6 +176,7 @@ const ColumnChart = forwardRef((props, ref) => const tooltipLabelFormatter = useLabelFormatter(primaryDimension?.formatter); const [componentRef, chartRef] = useSyncRef(ref); + const activePayloadsRef = useRef(measures); const dataKeys = measures.map(({ accessor }) => accessor); const colorSecondY = chartConfig.secondYAxis @@ -211,7 +205,7 @@ const ColumnChart = forwardRef((props, ref) => [onDataPointClick], ); - const onClickInternal = useOnClickInternal(onClick); + const onClickInternal = useOnClickInternal(onClick, dataset, activePayloadsRef); const isBigDataSet = dataset?.length > 30; const primaryDimensionAccessor = primaryDimension?.accessor; @@ -307,18 +301,30 @@ const ColumnChart = forwardRef((props, ref) => )} {isMounted && measures.map((element, index) => { + const color = element.color ?? `var(--sapChart_OrderedColor_${(index % 12) + 1})`; + const dataKey = element.accessor; + const name = element.label ?? element.accessor; + const opacity = element.opacity ?? 1; + activePayloadsRef.current[index].color = color; + activePayloadsRef.current[index].stroke = color; + activePayloadsRef.current[index].dataKey = dataKey; + activePayloadsRef.current[index].hide = element.hide; + activePayloadsRef.current[index].name = name; + activePayloadsRef.current[index].fillOpacity = opacity; + activePayloadsRef.current[index].strokeOpacity = opacity; return ( ((props, ref) => onAnimationEnd={handleBarAnimationEnd} > } /> {dataset.map((data, i) => { diff --git a/packages/charts/src/components/ComposedChart/ComposedChart.cy.tsx b/packages/charts/src/components/ComposedChart/ComposedChart.cy.tsx index e1b0505d481..a4c018c4078 100644 --- a/packages/charts/src/components/ComposedChart/ComposedChart.cy.tsx +++ b/packages/charts/src/components/ComposedChart/ComposedChart.cy.tsx @@ -51,12 +51,13 @@ describe('ComposedChart', () => { measures={measures} onClick={onClick} onLegendClick={onLegendClick} + noAnimation />, ); cy.findByText('January').click(); cy.get('@onClick').should('have.been.called'); - cy.get('[name="January"]').eq(0).click(); + cy.get('[name="January"]').eq(0).realClick({ position: 'topLeft' }); cy.get('@onClick') .should('have.been.calledTwice') .and( @@ -68,7 +69,7 @@ describe('ComposedChart', () => { }), ); - cy.contains('Users').click(); + cy.get('[class="recharts-legend-wrapper"]').findByText('Users').realClick(); cy.get('@onLegendClick').should( 'have.been.calledWith', Cypress.sinon.match({ diff --git a/packages/charts/src/components/ComposedChart/index.tsx b/packages/charts/src/components/ComposedChart/index.tsx index 610e3ca13ef..81e08fd23f1 100644 --- a/packages/charts/src/components/ComposedChart/index.tsx +++ b/packages/charts/src/components/ComposedChart/index.tsx @@ -2,7 +2,7 @@ import { enrichEventWithDetails, ThemingParameters, useIsRTL, useSyncRef } from '@ui5/webcomponents-react-base'; import type { CSSProperties, FC } from 'react'; -import { forwardRef, useCallback } from 'react'; +import { useRef, forwardRef, useCallback } from 'react'; import { Area, Bar, @@ -19,7 +19,6 @@ import { YAxis, } from 'recharts'; import type { YAxisProps } from 'recharts'; -import { getValueByDataKey } from 'recharts/lib/util/ChartUtils.js'; import { useChartMargin } from '../../hooks/useChartMargin.js'; import { useLabelFormatter } from '../../hooks/useLabelFormatter.js'; import { useLegendItemClick } from '../../hooks/useLegendItemClick.js'; @@ -28,7 +27,7 @@ import { useObserveXAxisHeights } from '../../hooks/useObserveXAxisHeights.js'; import { useOnClickInternal } from '../../hooks/useOnClickInternal.js'; import { usePrepareDimensionsAndMeasures } from '../../hooks/usePrepareDimensionsAndMeasures.js'; import { useTooltipFormatter } from '../../hooks/useTooltipFormatter.js'; -import type { IChartBaseProps } from '../../interfaces/IChartBaseProps.js'; +import type { ActivePayload, IChartBaseProps } from '../../interfaces/IChartBaseProps.js'; import type { IChartDimension } from '../../interfaces/IChartDimension.js'; import type { IChartMeasure } from '../../interfaces/IChartMeasure.js'; import { ChartContainer } from '../../internal/ChartContainer.js'; @@ -189,6 +188,7 @@ const ComposedChart = forwardRef((props, ref measureDefaults, ); + const activePayloadsRef = useRef(measures); const tooltipValueFormatter = useTooltipFormatter(measures); const primaryDimension = dimensions[0]; @@ -204,12 +204,6 @@ const ComposedChart = forwardRef((props, ref ? dataKeys.findIndex((key) => key === chartConfig.secondYAxis?.dataKey) : 0; - const valueAccessor = - (attribute) => - ({ payload }) => { - return getValueByDataKey(payload, attribute); - }; - const onDataPointClickInternal = (payload, eventOrIndex, event) => { if (typeof onDataPointClick === 'function') { if (typeof eventOrIndex === 'number') { @@ -245,7 +239,7 @@ const ComposedChart = forwardRef((props, ref }; const onItemLegendClick = useLegendItemClick(onLegendClick); - const onClickInternal = useOnClickInternal(onClick); + const onClickInternal = useOnClickInternal(onClick, dataset, activePayloadsRef); const isBigDataSet = dataset?.length > 30; const primaryDimensionAccessor = primaryDimension?.accessor; @@ -439,11 +433,22 @@ const ComposedChart = forwardRef((props, ref )} {measures?.map((element, index) => { const ChartElement = ChartTypes[element.type] as any as FC; - const chartElementProps: any = { isAnimationActive: !noAnimation, }; let labelPosition = 'top'; + const color = element.color ?? `var(--sapChart_OrderedColor_${(index % 12) + 1})`; + const dataKey = element.accessor; + const name = element.label ?? element.accessor; + const opacity = element.opacity ?? 1; + const hide = element.hide; + activePayloadsRef.current[index].color = color; + activePayloadsRef.current[index].stroke = color; + activePayloadsRef.current[index].dataKey = dataKey; + activePayloadsRef.current[index].hide = hide; + activePayloadsRef.current[index].name = name; + activePayloadsRef.current[index].fillOpacity = opacity; + activePayloadsRef.current[index].strokeOpacity = opacity; switch (element.type) { case 'line': @@ -454,9 +459,12 @@ const ComposedChart = forwardRef((props, ref chartElementProps.strokeOpacity = element.opacity; chartElementProps.dot = element.showDot ?? !isBigDataSet; chartElementProps.hide = element.hide; + + activePayloadsRef.current[index].fillOpacity = opacity; + activePayloadsRef.current[index].strokeOpacity = opacity; break; case 'bar': - chartElementProps.hide = element.hide; + chartElementProps.hide = hide; chartElementProps.fillOpacity = element.opacity; chartElementProps.strokeOpacity = element.opacity; chartElementProps.barSize = element.width; @@ -477,6 +485,9 @@ const ComposedChart = forwardRef((props, ref chartElementProps.activeDot = { onClick: onDataPointClickInternal, }; + + activePayloadsRef.current[index].fillOpacity = 0.3; + activePayloadsRef.current[index].strokeOpacity = opacity; break; } @@ -488,23 +499,22 @@ const ComposedChart = forwardRef((props, ref return ( ) } - stroke={element.color ?? `var(--sapChart_OrderedColor_${(index % 12) + 1})`} - fill={element.color ?? `var(--sapChart_OrderedColor_${(index % 12) + 1})`} + stroke={color} + fill={color} type="monotone" - dataKey={element.accessor} + dataKey={dataKey} {...chartElementProps} > {element.type === 'bar' && ( <> } /> {dataset.map((data, i) => { diff --git a/packages/charts/src/components/DonutChart/DonutChart.cy.tsx b/packages/charts/src/components/DonutChart/DonutChart.cy.tsx index b1cce009f9a..a1a1dd3abb8 100644 --- a/packages/charts/src/components/DonutChart/DonutChart.cy.tsx +++ b/packages/charts/src/components/DonutChart/DonutChart.cy.tsx @@ -43,7 +43,7 @@ describe('DonutChart', () => { }), ); - cy.contains('January').click(); + cy.get('[class="recharts-legend-wrapper"]').findByText('January').realClick(); cy.get('@onLegendClick').should( 'have.been.calledWith', Cypress.sinon.match({ diff --git a/packages/charts/src/components/LineChart/LineChart.tsx b/packages/charts/src/components/LineChart/LineChart.tsx index 1a11e9c3dbd..abaec19b351 100644 --- a/packages/charts/src/components/LineChart/LineChart.tsx +++ b/packages/charts/src/components/LineChart/LineChart.tsx @@ -19,9 +19,10 @@ import { useLabelFormatter } from '../../hooks/useLabelFormatter.js'; import { useLegendItemClick } from '../../hooks/useLegendItemClick.js'; import { useLongestYAxisLabel } from '../../hooks/useLongestYAxisLabel.js'; import { useObserveXAxisHeights } from '../../hooks/useObserveXAxisHeights.js'; +import { useOnClickInternal } from '../../hooks/useOnClickInternal.js'; import { usePrepareDimensionsAndMeasures } from '../../hooks/usePrepareDimensionsAndMeasures.js'; import { useTooltipFormatter } from '../../hooks/useTooltipFormatter.js'; -import type { IChartBaseProps } from '../../interfaces/IChartBaseProps.js'; +import type { ActivePayload, IChartBaseProps } from '../../interfaces/IChartBaseProps.js'; import type { IChartDimension } from '../../interfaces/IChartDimension.js'; import type { IChartMeasure } from '../../interfaces/IChartMeasure.js'; import { ChartContainer } from '../../internal/ChartContainer.js'; @@ -160,7 +161,7 @@ const LineChart = forwardRef((props, ref) => { dimensionDefaults, measureDefaults, ); - + const activePayloadsRef = useRef(measures); const tooltipValueFormatter = useTooltipFormatter(measures); const primaryDimension = dimensions[0]; @@ -180,6 +181,8 @@ const LineChart = forwardRef((props, ref) => { const onItemLegendClick = useLegendItemClick(onLegendClick); const preventOnClickCall = useRef(0); + const handleClick = useOnClickInternal(onClick, dataset, activePayloadsRef); + const onDataPointClickInternal = useCallback( (payload, eventOrIndex) => { if (eventOrIndex.dataKey && typeof onDataPointClick === 'function') { @@ -193,18 +196,13 @@ const LineChart = forwardRef((props, ref) => { }), ); } else if (typeof onClick === 'function' && preventOnClickCall.current === 0) { - onClick( - enrichEventWithDetails(eventOrIndex, { - payload: payload?.activePayload?.[0]?.payload, - activePayloads: payload?.activePayload, - }), - ); + handleClick(payload, eventOrIndex); } if (preventOnClickCall.current > 0) { preventOnClickCall.current -= 1; } }, - [onDataPointClick, preventOnClickCall.current], + [handleClick, onClick, onDataPointClick], ); const isBigDataSet = dataset?.length > 30; @@ -265,6 +263,7 @@ const LineChart = forwardRef((props, ref) => { orientation={isRTL === true ? 'right' : 'left'} axisLine={chartConfig.yAxisVisible} tickLine={tickLineConfig} + // todo: multiple `yAxisId`s cause the Cartesian Grid to break yAxisId="left" interval={0} tick={chartConfig.yAxisTicksVisible && } @@ -296,17 +295,28 @@ const LineChart = forwardRef((props, ref) => { /> )} {measures.map((element, index) => { + const color = element.color ?? `var(--sapChart_OrderedColor_${(index % 12) + 1})`; + const dataKey = element.accessor; + const name = element.label ?? element.accessor; + const opacity = element.opacity ?? 1; + activePayloadsRef.current[index].color = color; + activePayloadsRef.current[index].stroke = color; + activePayloadsRef.current[index].dataKey = dataKey; + activePayloadsRef.current[index].hide = element.hide; + activePayloadsRef.current[index].name = name; + activePayloadsRef.current[index].fillOpacity = opacity; + activePayloadsRef.current[index].strokeOpacity = opacity; return ( } type="monotone" - dataKey={element.accessor} - stroke={element.color ?? `var(--sapChart_OrderedColor_${(index % 12) + 1})`} + dataKey={dataKey} + stroke={color} strokeWidth={element.width} activeDot={{ onClick: onDataPointClickInternal }} isAnimationActive={!noAnimation} @@ -332,7 +342,6 @@ const LineChart = forwardRef((props, ref) => { label={referenceLine?.label} /> )} - {/*ToDo: remove conditional rendering once `active` is working again (https://github.com/recharts/recharts/issues/2703)*/} {tooltipConfig?.active !== false && ( { }), ); - cy.contains('January').click(); + cy.get('[class="recharts-legend-wrapper"]').findByText('January').realClick(); cy.get('@onLegendClick').should( 'have.been.calledWith', Cypress.sinon.match({ diff --git a/packages/charts/src/components/PieChart/PieChart.tsx b/packages/charts/src/components/PieChart/PieChart.tsx index 489efb8a7cd..5cad576a831 100644 --- a/packages/charts/src/components/PieChart/PieChart.tsx +++ b/packages/charts/src/components/PieChart/PieChart.tsx @@ -3,9 +3,10 @@ import { enrichEventWithDetails, useStylesheet, useSyncRef } from '@ui5/webcomponents-react-base'; import { clsx } from 'clsx'; import type { CSSProperties } from 'react'; -import { cloneElement, forwardRef, isValidElement, useCallback, useMemo } from 'react'; +import { useRef, cloneElement, forwardRef, isValidElement, useCallback, useMemo } from 'react'; import { Cell, + Curve, Label as RechartsLabel, Legend, Pie, @@ -17,7 +18,7 @@ import { import { getValueByDataKey } from 'recharts/lib/util/ChartUtils.js'; import { useLegendItemClick } from '../../hooks/useLegendItemClick.js'; import { useOnClickInternal } from '../../hooks/useOnClickInternal.js'; -import type { IChartBaseProps } from '../../interfaces/IChartBaseProps.js'; +import type { ActivePayload, IChartBaseProps } from '../../interfaces/IChartBaseProps.js'; import type { IChartDimension } from '../../interfaces/IChartDimension.js'; import type { IChartMeasure } from '../../interfaces/IChartMeasure.js'; import type { IPolarChartConfig } from '../../interfaces/IPolarChartConfig.js'; @@ -140,6 +141,16 @@ const PieChart = forwardRef((props, ref) => { }), [props.measure], ); + const activePayloadsRef = useRef({ + ...measure, + // these properties must be either set in the component, or in the `useOnClickInternal` hook + dataKey: measure.accessor, + name: measure.accessor, + color: '', + stroke: '', + payload: {}, + value: '', + }); const dataLabel = (props) => { const hideDataLabel = @@ -163,11 +174,12 @@ const PieChart = forwardRef((props, ref) => { ); const onItemLegendClick = useLegendItemClick(onLegendClick, () => measure.accessor); - const onClickInternal = useOnClickInternal(onClick); + const onClickInternal = useOnClickInternal(onClick, dataset, activePayloadsRef); const onDataPointClickInternal = useCallback( (payload, dataIndex, event) => { if (payload && payload && typeof onDataPointClick === 'function') { + //todo check values onDataPointClick( enrichEventWithDetails(event, { value: payload.value, @@ -253,8 +265,10 @@ const PieChart = forwardRef((props, ref) => { (props) => { const hideDataLabel = typeof measure.hideDataLabel === 'function' ? measure.hideDataLabel(props) : measure.hideDataLabel; - if (hideDataLabel || chartConfig.activeSegment === props.index) return null; - return Pie.renderLabelLineItem({}, props, undefined); + if (hideDataLabel || chartConfig.activeSegment === props.index) { + return null; + } + return ; }, [chartConfig.activeSegment, measure.hideDataLabel], ); @@ -299,6 +313,10 @@ const PieChart = forwardRef((props, ref) => { classNames.piechart, )} > + {/*todo: accessibility layer needs active shape?*/} + {/*todo: keyboard nav with active shape doesn't hide the default label of the Cell when active*/} + {/*todo: when clicked while activeShape is set, it takes a lot of time to rerender the component, leading to + strange behavior*/} ((props, ref) => { isAnimationActive={!noAnimation} labelLine={renderLabelLine} label={dataLabel} - activeIndex={chartConfig.activeSegment} activeShape={chartConfig.activeSegment != null && renderActiveShape} - rootTabIndex={-1} + //todo: why do we need this? + // rootTabIndex={-1} > {centerLabel && {centerLabel}} {dataset && - dataset.map((data, index) => ( - - ))} + dataset.map((data, index) => { + const color = measure.colors?.[index] ?? `var(--sapChart_OrderedColor_${(index % 12) + 1})`; + activePayloadsRef.current.color = color; + activePayloadsRef.current.stroke = color; + return ( + + ); + })} {tooltipConfig?.active !== false && ( ((props, ref) => { {...tooltipConfig} /> )} + {chartConfig.activeSegment && ( + // tooltip that only renders the active shape + + )} {!noLegend && ( )} diff --git a/packages/charts/src/components/RadarChart/RadarChart.cy.tsx b/packages/charts/src/components/RadarChart/RadarChart.cy.tsx index f1025f3ed8b..613c5b113c9 100644 --- a/packages/charts/src/components/RadarChart/RadarChart.cy.tsx +++ b/packages/charts/src/components/RadarChart/RadarChart.cy.tsx @@ -52,7 +52,7 @@ describe('RadarChart', () => { cy.get('[name="January"]').eq(0).click({ force: true }); cy.get('@onClick').should('have.been.calledTwice'); - cy.contains('Users').click(); + cy.get('[class="recharts-legend-wrapper"]').findByText('Users').realClick(); cy.get('@onLegendClick').should( 'have.been.calledWith', Cypress.sinon.match({ diff --git a/packages/charts/src/components/RadarChart/RadarChart.mdx b/packages/charts/src/components/RadarChart/RadarChart.mdx index 830c50db85c..bbcfea6ba8b 100644 --- a/packages/charts/src/components/RadarChart/RadarChart.mdx +++ b/packages/charts/src/components/RadarChart/RadarChart.mdx @@ -25,9 +25,9 @@ import LegendStory from '../../resources/LegendConfig.mdx'; -### With Data Labels +### Without Data Labels - + ### Polygon diff --git a/packages/charts/src/components/RadarChart/RadarChart.stories.tsx b/packages/charts/src/components/RadarChart/RadarChart.stories.tsx index bd982be20d1..811d3118b9d 100644 --- a/packages/charts/src/components/RadarChart/RadarChart.stories.tsx +++ b/packages/charts/src/components/RadarChart/RadarChart.stories.tsx @@ -1,5 +1,5 @@ import type { Meta, StoryObj } from '@storybook/react-vite'; -import { complexDataSet, legendConfig, simpleDataSet, tooltipConfig } from '../../resources/DemoProps.js'; +import { legendConfig, radarChartDataset, simpleDataSet, tooltipConfig } from '../../resources/DemoProps.js'; import { RadarChart } from './RadarChart.js'; const meta = { @@ -9,27 +9,23 @@ const meta = { dimensions: [ { accessor: 'name', - formatter: (d) => `${d} 2019`, }, ], measures: [ { - accessor: 'users', - label: 'Users', - formatter: (val) => val.toLocaleString(), + accessor: 'alpha', + label: 'Alpha Series', }, { - accessor: 'sessions', - label: 'Active Sessions', - formatter: (val) => `${val} sessions`, - hideDataLabel: true, + accessor: 'beta', + label: 'Beta Series', }, { - accessor: 'volume', - label: 'Vol.', + accessor: 'gamma', + label: 'Gamma Series', }, ], - dataset: complexDataSet, + dataset: radarChartDataset, }, argTypes: { dataset: { @@ -51,18 +47,23 @@ export const WithCustomColor: Story = { }, }; -export const WithDataLabels: Story = { +export const WithoutDataLabels: Story = { args: { - dimensions: [{ accessor: 'name' }], measures: [ { - accessor: 'users', + accessor: 'alpha', + label: 'Alpha Series', + hideDataLabel: true, }, { - accessor: 'sessions', + accessor: 'beta', + label: 'Beta Series', + hideDataLabel: true, }, { - accessor: 'volume', + accessor: 'gamma', + label: 'Gamma Series', + hideDataLabel: true, }, ], }, @@ -70,20 +71,6 @@ export const WithDataLabels: Story = { export const Polygon: Story = { args: { - dimensions: [{ accessor: 'name', formatter: (element) => element.slice(0, 3) }], - measures: [ - { - accessor: 'users', - formatter: (element) => `${element / 10}`, - label: 'number of users', - }, - { - accessor: 'sessions', - }, - { - accessor: 'volume', - }, - ], chartConfig: { polarGridType: 'polygon' }, }, }; diff --git a/packages/charts/src/components/RadarChart/RadarChart.tsx b/packages/charts/src/components/RadarChart/RadarChart.tsx index dc2c856b46d..0a5b117f045 100644 --- a/packages/charts/src/components/RadarChart/RadarChart.tsx +++ b/packages/charts/src/components/RadarChart/RadarChart.tsx @@ -125,8 +125,10 @@ const RadarChart = forwardRef((props, ref) => { const onItemLegendClick = useLegendItemClick(onLegendClick); const preventOnClickCall = useRef(false); + //todo: check this const onClickInternal = useCallback( (payload, event) => { + console.log(payload); if (typeof onClick === 'function' && !preventOnClickCall.current) { onClick( enrichEventWithDetails(event, { @@ -217,8 +219,6 @@ const RadarChart = forwardRef((props, ref) => { /> )} {!noLegend && ( - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore , Pick { @@ -57,6 +58,7 @@ export interface RadialChartProps */ onClick?: ( event: CustomEvent<{ + // todo: remove payload and activePayloads in next major? payload: unknown; activePayloads: Record[]; dataIndex: number; @@ -126,7 +128,15 @@ const RadialChart = forwardRef((props, ref) => } }; - const onClickInternal = useOnClickInternal(onClick); + const handleOnClick: CategoricalChartFunc = (_, e) => { + onClick( + // @ts-expect-error: enrichEventWithDetails expects a CustomEvent + enrichEventWithDetails(e, { + // @ts-expect-error: detail property exists + activePayloads: [e.detail.payload], + }), + ); + }; return ( ((props, ref) => {...rest} > ((props, ref) => y="50%" textAnchor="middle" dominantBaseline="middle" - className="progress-label" - style={{ ...defaultDisplayValueStyles, ...displayValueStyle }} + //todo: why do we need this? + // className="progress-label" + style={displayValueStyle} > {displayValue} diff --git a/packages/charts/src/components/ScatterChart/ScatterChart.cy.tsx b/packages/charts/src/components/ScatterChart/ScatterChart.cy.tsx index 20de5e21915..f1d7876c5bf 100644 --- a/packages/charts/src/components/ScatterChart/ScatterChart.cy.tsx +++ b/packages/charts/src/components/ScatterChart/ScatterChart.cy.tsx @@ -40,7 +40,8 @@ describe('ScatterChart', () => { />, ); - cy.get('[name="Users"]').eq(0).click(); + //todo: datapoint click is broken + cy.get('[name="APJ"]').eq(0).realClick(); cy.get('@onClick') .should('have.been.calledOnce') .and( @@ -52,12 +53,12 @@ describe('ScatterChart', () => { }), ); - cy.contains('Users').click(); + cy.get('[class="recharts-legend-wrapper"]').findByText('APJ').realClick(); cy.get('@onLegendClick').should( 'have.been.calledWith', Cypress.sinon.match({ detail: Cypress.sinon.match({ - value: 'Users', + value: 'APJ', }), }), ); diff --git a/packages/charts/src/components/ScatterChart/ScatterChart.tsx b/packages/charts/src/components/ScatterChart/ScatterChart.tsx index 0522e63beba..f0b1d1e42ba 100644 --- a/packages/charts/src/components/ScatterChart/ScatterChart.tsx +++ b/packages/charts/src/components/ScatterChart/ScatterChart.tsx @@ -168,8 +168,10 @@ const ScatterChart = forwardRef((props, ref) const [componentRef, chartRef] = useSyncRef(ref); const preventOnClickCall = useRef(false); const onItemLegendClick = useLegendItemClick(onLegendClick); + //todo: error on click const onClickInternal = useCallback( (payload, event) => { + console.log(payload); if (typeof onClick === 'function' && !preventOnClickCall.current) { onClick( enrichEventWithDetails(event, { diff --git a/packages/charts/src/hooks/useLabelFormatter.ts b/packages/charts/src/hooks/useLabelFormatter.ts index f7a1424fc46..459eb6ce0e3 100644 --- a/packages/charts/src/hooks/useLabelFormatter.ts +++ b/packages/charts/src/hooks/useLabelFormatter.ts @@ -1,10 +1,9 @@ import { useCallback } from 'react'; -import type { TooltipProps } from 'recharts'; -import type { NameType, TooltipLabelFormatter, ValueType } from '../interfaces/index.js'; +import type { TooltipLabelFormatter } from '../interfaces/index.js'; export const useLabelFormatter = (formatter: TooltipLabelFormatter) => { return useCallback( - (label: TooltipProps['label'], payload) => { + (label: string | number, payload) => { if (typeof formatter === 'function') { return formatter(label, payload); } diff --git a/packages/charts/src/hooks/useOnClickInternal.ts b/packages/charts/src/hooks/useOnClickInternal.ts index 7c3f4a3cc9c..8d067aeedaa 100644 --- a/packages/charts/src/hooks/useOnClickInternal.ts +++ b/packages/charts/src/hooks/useOnClickInternal.ts @@ -1,17 +1,39 @@ import { enrichEventWithDetails } from '@ui5/webcomponents-react-base'; +import type { MutableRefObject } from 'react'; import { useCallback } from 'react'; - -export const useOnClickInternal = (onClick) => - useCallback( - (payload, event) => { +import type { CategoricalChartFunc } from 'recharts/types/chart/types.js'; +import type { ActivePayload, IChartBaseProps } from '../interfaces/IChartBaseProps.js'; +export const useOnClickInternal = ( + onClick: IChartBaseProps['onClick'], + dataset: IChartBaseProps['dataset'], + activePayloadsRef: MutableRefObject | MutableRefObject, +): CategoricalChartFunc => { + //todo: deprecate payload & activePayloads? + return useCallback( + (nextState, event) => { if (typeof onClick === 'function') { + const payload = nextState.activeIndex != null ? dataset?.[nextState.activeIndex] : undefined; + const activePayloads = ( + Array.isArray(activePayloadsRef.current) ? activePayloadsRef.current : [activePayloadsRef.current] + ).map((item) => ({ + ...item, + payload, + value: item.dataKey ? payload?.[item.dataKey] : undefined, + })); onClick( + //todo: check ts-error + // @ts-expect-error: check enrichEventWithDetails(event, { - payload: payload?.activePayload?.[0]?.payload, - activePayloads: payload?.activePayload, + payload, + activePayloads, + //todo: add nextState? + ...nextState, }), ); } }, - [onClick], + [onClick, dataset, activePayloadsRef], ); +}; + +//todo: integrate LineChart, RadarChart click here as well? diff --git a/packages/charts/src/interfaces/IChartBaseProps.ts b/packages/charts/src/interfaces/IChartBaseProps.ts index 5467c97dfdf..4b7d6c9b898 100644 --- a/packages/charts/src/interfaces/IChartBaseProps.ts +++ b/packages/charts/src/interfaces/IChartBaseProps.ts @@ -1,8 +1,27 @@ import type { CommonProps } from '@ui5/webcomponents-react'; import type { ComponentType, ReactNode } from 'react'; import type { LegendProps, TooltipProps } from 'recharts'; +import type { CategoricalChartFunc } from 'recharts/types/chart/types.js'; import type { ICartesianChartConfig } from './ICartesianChartConfig.js'; +//todo: type measures +// interface ActivePayload extends IChartMeasure{ +// opacity?: +// } + +export interface ActivePayload { + color: string | undefined; + stroke: string | undefined; + dataKey: string; + hide?: boolean | undefined; + name: string; + fillOpacity?: string | number; + strokeOpacity?: string | number; + payload: Record; + value: number | string; + colors?: string[]; +} + export interface IChartBaseProps extends Omit { /** * Flag whether the chart should display a loading indicator. @@ -37,7 +56,14 @@ export interface IChartBaseProps extends Omit[] }>) => void; + onClick?: ( + event: CustomEvent< + Parameters[0] & { + payload: unknown; + activePayloads: ActivePayload[]; + } + >, + ) => void; /** * The `onDataPointClick` event fires whenever the user clicks on e.g. a bar in `BarChart` or a point the `LineChart`. diff --git a/packages/charts/src/internal/ChartDataLabel.tsx b/packages/charts/src/internal/ChartDataLabel.tsx index 757062bf830..76fa1ca5933 100644 --- a/packages/charts/src/internal/ChartDataLabel.tsx +++ b/packages/charts/src/internal/ChartDataLabel.tsx @@ -15,16 +15,17 @@ interface CustomDataLabelProps { export const ChartDataLabel = (props: CustomDataLabelProps) => { const { config, chartType, viewBox } = props; - if (config.hideDataLabel) { + //todo: viewBox is not initially available, check if this is changed in a later version + if (config.hideDataLabel || !viewBox) { return null; } - if (config.DataLabel) { + if (config.DataLabel && !!viewBox) { return createElement(config.DataLabel, props); } const formattedLabel = config.formatter(props.value ?? props.children); - if (chartType === 'bar' || chartType === 'column') { + if ((chartType === 'bar' || chartType === 'column') && !!viewBox) { if (Math.abs(viewBox.width) < getTextWidth(formattedLabel)) { return null; } @@ -37,7 +38,6 @@ export const ChartDataLabel = (props: CustomDataLabelProps) => { if (['area', 'line', 'radar'].includes(chartType)) { fill = ThemingParameters.sapTextColor; // label is displayed outside of the colored element } - return (