diff --git a/packages/x-charts-pro/src/Heatmap/HeatmapPlot.tsx b/packages/x-charts-pro/src/Heatmap/HeatmapPlot.tsx index 171ce56b3cffa..daea4f3d8aafc 100644 --- a/packages/x-charts-pro/src/Heatmap/HeatmapPlot.tsx +++ b/packages/x-charts-pro/src/Heatmap/HeatmapPlot.tsx @@ -2,7 +2,7 @@ import * as React from 'react'; import PropTypes from 'prop-types'; import { useXScale, useYScale, useZColorScale } from '@mui/x-charts/hooks'; -import { useHeatmapSeries } from '../hooks/useSeries'; +import { useHeatmapSeries } from '../hooks/useHeatmapSeries'; import { HeatmapItem, HeatmapItemProps } from './HeatmapItem'; export interface HeatmapPlotProps extends Pick {} diff --git a/packages/x-charts-pro/src/Heatmap/HeatmapTooltip.tsx b/packages/x-charts-pro/src/Heatmap/HeatmapTooltip.tsx index bce80b58c67ae..fb3fe1f0ae7d2 100644 --- a/packages/x-charts-pro/src/Heatmap/HeatmapTooltip.tsx +++ b/packages/x-charts-pro/src/Heatmap/HeatmapTooltip.tsx @@ -16,7 +16,7 @@ import { } from '@mui/x-charts/ChartsTooltip'; import { useXAxis, useYAxis } from '@mui/x-charts/hooks'; import { getLabel, ChartsLabelMark } from '@mui/x-charts/internals'; -import { useHeatmapSeries } from '../hooks/useSeries'; +import { useHeatmapSeries } from '../hooks/useHeatmapSeries'; export interface HeatmapTooltipProps extends Omit {} diff --git a/packages/x-charts-pro/src/hooks/index.ts b/packages/x-charts-pro/src/hooks/index.ts index f9be207805602..60ff5de53db1b 100644 --- a/packages/x-charts-pro/src/hooks/index.ts +++ b/packages/x-charts-pro/src/hooks/index.ts @@ -1,2 +1,2 @@ -export { useHeatmapSeries } from './useSeries'; +export { useHeatmapSeries } from './useHeatmapSeries'; export * from './zoom'; diff --git a/packages/x-charts-pro/src/hooks/useHeatmapSeries.test.tsx b/packages/x-charts-pro/src/hooks/useHeatmapSeries.test.tsx new file mode 100644 index 0000000000000..b957a16a1d393 --- /dev/null +++ b/packages/x-charts-pro/src/hooks/useHeatmapSeries.test.tsx @@ -0,0 +1,64 @@ +import { renderHook } from '@mui/internal-test-utils'; +import { expect } from 'chai'; +import * as React from 'react'; +import { useHeatmapSeries } from './useHeatmapSeries'; +import { Heatmap } from '../Heatmap'; +import { HeatmapSeriesType } from '../models'; + +describe('useHeatmapSeries', () => { + const mockSeries: HeatmapSeriesType[] = [ + { + type: 'heatmap', + id: '1', + data: [ + [0, 0, 10], + [0, 1, 20], + [0, 2, 40], + ], + }, + { + type: 'heatmap', + id: '2', + data: [ + [3, 2, 20], + [3, 3, 70], + [3, 4, 90], + ], + }, + ]; + + const defaultProps = { + series: mockSeries, + height: 400, + width: 400, + xAxis: [{ data: [1, 2, 3, 4] }], + yAxis: [{ data: ['A', 'B', 'C', 'D', 'E'] }], + }; + + const options: any = { + wrapper: ({ children }: { children: React.ReactElement }) => { + return {children}; + }, + }; + + it('should return all heatmap series when no seriesIds are provided', () => { + const { result } = renderHook(() => useHeatmapSeries(), options); + expect(result.current?.seriesOrder).to.deep.equal(['1', '2']); + expect(Object.keys(result.current?.series ?? {})).to.deep.equal(['1', '2']); + }); + + it('should return the specific heatmap series when a single seriesId is provided', () => { + const { result } = renderHook(() => useHeatmapSeries('1'), options); + expect(result.current?.id).to.deep.equal(mockSeries[0].id); + }); + + it('should return the specific heatmap series when multiple seriesIds are provided', () => { + const { result } = renderHook(() => useHeatmapSeries(['2', '1']), options); + expect(result.current?.map((v) => v?.id)).to.deep.equal([mockSeries[1].id, mockSeries[0].id]); + }); + + it('should return undefined series when invalid seriesIds are provided', () => { + const { result } = renderHook(() => useHeatmapSeries(['1', '3']), options); + expect(result.current?.map((v) => v?.id)).to.deep.equal([mockSeries[0].id, undefined]); + }); +}); diff --git a/packages/x-charts-pro/src/hooks/useHeatmapSeries.ts b/packages/x-charts-pro/src/hooks/useHeatmapSeries.ts new file mode 100644 index 0000000000000..01ff32d14b598 --- /dev/null +++ b/packages/x-charts-pro/src/hooks/useHeatmapSeries.ts @@ -0,0 +1,37 @@ +'use client'; +import { + createSeriesSelectorsOfType, + ProcessedSeries, + SeriesId, + ChartSeriesDefaultized, +} from '@mui/x-charts/internals'; + +const selectorSeries = createSeriesSelectorsOfType('heatmap'); + +/** + * Get access to the internal state of heatmap series. + * The returned object contains: + * - series: a mapping from ids to series attributes. + * - seriesOrder: the array of series ids. + * @returns {{ series: Record; seriesOrder: SeriesId[]; } | undefined} heatmapSeries + */ +export function useHeatmapSeries(): ProcessedSeries['heatmap']; +/** + * Get access to the internal state of heatmap series. + * + * @param {SeriesId} seriesId The id of the series to get. + * @returns {ChartSeriesDefaultized<'heatmap'> | undefined} heatmapSeries + */ +export function useHeatmapSeries(seriesId: SeriesId): ChartSeriesDefaultized<'heatmap'> | undefined; +/** + * Get access to the internal state of heatmap series. + * + * @param {SeriesId[]} seriesIds The ids of the series to get. Order is preserved. + * @returns {ChartSeriesDefaultized<'heatmap'>[] | undefined} heatmapSeries + */ +export function useHeatmapSeries( + seriesIds: SeriesId[], +): (ChartSeriesDefaultized<'heatmap'> | undefined)[]; +export function useHeatmapSeries(seriesIds?: SeriesId | SeriesId[]) { + return selectorSeries(seriesIds); +} diff --git a/packages/x-charts-pro/src/hooks/useSeries.ts b/packages/x-charts-pro/src/hooks/useSeries.ts deleted file mode 100644 index 6a150b390d983..0000000000000 --- a/packages/x-charts-pro/src/hooks/useSeries.ts +++ /dev/null @@ -1,16 +0,0 @@ -'use client'; -import * as React from 'react'; -import { useSeries, ProcessedSeries } from '@mui/x-charts/internals'; - -/** - * Get access to the internal state of heatmap series. - * The returned object contains: - * - series: a mapping from ids to series attributes. - * - seriesOrder: the array of series ids. - * @returns { series: Record; seriesOrder: SeriesId[]; } | undefined heatmapSeries - */ -export function useHeatmapSeries(): ProcessedSeries['heatmap'] { - const series = useSeries(); - - return React.useMemo(() => series.heatmap, [series.heatmap]); -} diff --git a/packages/x-charts/src/BarChart/BarPlot.tsx b/packages/x-charts/src/BarChart/BarPlot.tsx index 7549326221ff4..273f4f21402de 100644 --- a/packages/x-charts/src/BarChart/BarPlot.tsx +++ b/packages/x-charts/src/BarChart/BarPlot.tsx @@ -13,7 +13,7 @@ import { BarClipPath } from './BarClipPath'; import { BarLabelItemProps, BarLabelSlotProps, BarLabelSlots } from './BarLabel/BarLabelItem'; import { BarLabelPlot } from './BarLabel/BarLabelPlot'; import { checkScaleErrors } from './checkScaleErrors'; -import { useBarSeries } from '../hooks/useSeries'; +import { useBarSeries } from '../hooks/useBarSeries'; import { useSkipAnimation } from '../context/AnimationProvider'; import { SeriesProcessorResult } from '../internals/plugins/models/seriesConfig/seriesProcessor.types'; diff --git a/packages/x-charts/src/LineChart/AreaPlot.tsx b/packages/x-charts/src/LineChart/AreaPlot.tsx index ad7eea30bf346..161aaedd7e58f 100644 --- a/packages/x-charts/src/LineChart/AreaPlot.tsx +++ b/packages/x-charts/src/LineChart/AreaPlot.tsx @@ -15,7 +15,7 @@ import { getCurveFactory } from '../internals/getCurve'; import { isBandScale } from '../internals/isBandScale'; import { DEFAULT_X_AXIS_KEY } from '../constants'; import { LineItemIdentifier } from '../models/seriesType/line'; -import { useLineSeries } from '../hooks/useSeries'; +import { useLineSeries } from '../hooks/useLineSeries'; import { useSkipAnimation } from '../context/AnimationProvider'; import { useChartGradientIdBuilder } from '../hooks/useChartGradientId'; import { useXAxes, useYAxes } from '../hooks/useAxis'; diff --git a/packages/x-charts/src/LineChart/LineHighlightPlot.tsx b/packages/x-charts/src/LineChart/LineHighlightPlot.tsx index c4f4ad2074f76..c354d6f0f6ef0 100644 --- a/packages/x-charts/src/LineChart/LineHighlightPlot.tsx +++ b/packages/x-charts/src/LineChart/LineHighlightPlot.tsx @@ -7,8 +7,8 @@ import { useSelector } from '../internals/store/useSelector'; import { LineHighlightElement, LineHighlightElementProps } from './LineHighlightElement'; import { getValueToPositionMapper } from '../hooks/useScale'; import { DEFAULT_X_AXIS_KEY } from '../constants'; +import { useLineSeries } from '../hooks/useLineSeries'; import getColor from './seriesConfig/getColor'; -import { useLineSeries } from '../hooks/useSeries'; import { useChartContext } from '../context/ChartProvider'; import { selectorChartsInteractionXAxis } from '../internals/plugins/featurePlugins/useChartInteraction'; import { useXAxes, useYAxes } from '../hooks/useAxis'; diff --git a/packages/x-charts/src/LineChart/LinePlot.tsx b/packages/x-charts/src/LineChart/LinePlot.tsx index e9c588a6e03ac..d25b35a63cbe1 100644 --- a/packages/x-charts/src/LineChart/LinePlot.tsx +++ b/packages/x-charts/src/LineChart/LinePlot.tsx @@ -15,7 +15,7 @@ import { getCurveFactory } from '../internals/getCurve'; import { isBandScale } from '../internals/isBandScale'; import { DEFAULT_X_AXIS_KEY } from '../constants'; import { LineItemIdentifier } from '../models/seriesType/line'; -import { useLineSeries } from '../hooks/useSeries'; +import { useLineSeries } from '../hooks/useLineSeries'; import { useSkipAnimation } from '../context/AnimationProvider'; import { useChartGradientIdBuilder } from '../hooks/useChartGradientId'; import { useXAxes, useYAxes } from '../hooks'; diff --git a/packages/x-charts/src/LineChart/MarkPlot.tsx b/packages/x-charts/src/LineChart/MarkPlot.tsx index 9abf4579ca084..e109828312a50 100644 --- a/packages/x-charts/src/LineChart/MarkPlot.tsx +++ b/packages/x-charts/src/LineChart/MarkPlot.tsx @@ -5,7 +5,7 @@ import { DEFAULT_X_AXIS_KEY } from '../constants'; import { useSkipAnimation } from '../context/AnimationProvider'; import { useChartId } from '../hooks/useChartId'; import { getValueToPositionMapper } from '../hooks/useScale'; -import { useLineSeries } from '../hooks/useSeries'; +import { useLineSeries } from '../hooks/useLineSeries'; import { cleanId } from '../internals/cleanId'; import { LineItemIdentifier } from '../models/seriesType/line'; import { CircleMarkElement } from './CircleMarkElement'; diff --git a/packages/x-charts/src/PieChart/PiePlot.tsx b/packages/x-charts/src/PieChart/PiePlot.tsx index 9cce72cacefe3..6ae797aa7f981 100644 --- a/packages/x-charts/src/PieChart/PiePlot.tsx +++ b/packages/x-charts/src/PieChart/PiePlot.tsx @@ -5,7 +5,7 @@ import { PieArcPlot, PieArcPlotProps, PieArcPlotSlotProps, PieArcPlotSlots } fro import { PieArcLabelPlotSlots, PieArcLabelPlotSlotProps, PieArcLabelPlot } from './PieArcLabelPlot'; import { getPercentageValue } from '../internals/getPercentageValue'; import { getPieCoordinates } from './getPieCoordinates'; -import { usePieSeries } from '../hooks/useSeries'; +import { usePieSeries } from '../hooks/usePieSeries'; import { useSkipAnimation } from '../context/AnimationProvider'; import { useDrawingArea } from '../hooks'; diff --git a/packages/x-charts/src/ScatterChart/ScatterPlot.tsx b/packages/x-charts/src/ScatterChart/ScatterPlot.tsx index efe2967bb19e9..737525d5b3421 100644 --- a/packages/x-charts/src/ScatterChart/ScatterPlot.tsx +++ b/packages/x-charts/src/ScatterChart/ScatterPlot.tsx @@ -2,8 +2,8 @@ import * as React from 'react'; import PropTypes from 'prop-types'; import { Scatter, ScatterProps } from './Scatter'; +import { useScatterSeries } from '../hooks/useScatterSeries'; import getColor from './seriesConfig/getColor'; -import { useScatterSeries } from '../hooks/useSeries'; import { useXAxes, useYAxes } from '../hooks'; import { useZAxes } from '../hooks/useZAxis'; diff --git a/packages/x-charts/src/hooks/index.ts b/packages/x-charts/src/hooks/index.ts index 8aec879341109..2b60e7ce7b78e 100644 --- a/packages/x-charts/src/hooks/index.ts +++ b/packages/x-charts/src/hooks/index.ts @@ -5,14 +5,12 @@ export * from './useAxis'; export * from './useZAxis'; export * from './useColorScale'; export * from './useSvgRef'; +export * from './useSeries'; +export * from './useScatterSeries'; +export * from './usePieSeries'; +export * from './useBarSeries'; +export * from './useLineSeries'; export * from './useItemHighlighted'; export * from './useItemHighlightedGetter'; -export { - useSeries, - usePieSeries, - useLineSeries, - useBarSeries, - useScatterSeries, -} from './useSeries'; export * from './useLegend'; export { useChartGradientId, useChartGradientIdObjectBound } from './useChartGradientId'; diff --git a/packages/x-charts/src/hooks/useAxis.ts b/packages/x-charts/src/hooks/useAxis.ts index a9536e0f69b59..6ff78f947d68e 100644 --- a/packages/x-charts/src/hooks/useAxis.ts +++ b/packages/x-charts/src/hooks/useAxis.ts @@ -6,7 +6,18 @@ import { } from '../internals/plugins/featurePlugins/useChartCartesianAxis/useChartCartesianAxis.selectors'; import { useSelector } from '../internals/store/useSelector'; import { useStore } from '../internals/store/useStore'; +import { AxisId } from '../models/axis'; +/** + * Get all the x-axes. + * + * - `xAxis` is an object with the shape `{ [axisId]: axis }`. + * - `xAxisIds` is an array of axis IDs. + * + * If access to a specific X axis is needed, use the `useXAxis` hook instead. + * + * @returns `{ xAxis, xAxisIds }` - The x-axes and their IDs. + */ export function useXAxes() { const store = useStore<[UseChartCartesianAxisSignature]>(); const { axis: xAxis, axisIds: xAxisIds } = useSelector(store, selectorChartXAxis); @@ -14,6 +25,16 @@ export function useXAxes() { return { xAxis, xAxisIds }; } +/** + * Get all the y-axes. + * + * - `yAxis` is an object with the shape `{ [axisId]: axis }`. + * - `yAxisIds` is an array of axis IDs. + * + * If access to a specific Y axis is needed, use the `useYAxis` hook instead. + * + * @returns `{ yAxis, yAxisIds }` - The y-axes and their IDs. + */ export function useYAxes() { const store = useStore<[UseChartCartesianAxisSignature]>(); const { axis: yAxis, axisIds: yAxisIds } = useSelector(store, selectorChartYAxis); @@ -21,20 +42,30 @@ export function useYAxes() { return { yAxis, yAxisIds }; } -export function useXAxis(identifier?: number | string) { +/** + * Get the X axis. + * @param {AxisId | undefined} axisId - If provided returns the x axis with axisId, else returns the values for the default x axis. + * @returns The X axis. + */ +export function useXAxis(axisId?: AxisId) { const store = useStore<[UseChartCartesianAxisSignature]>(); const { axis: xAxis, axisIds: xAxisIds } = useSelector(store, selectorChartXAxis); - const id = typeof identifier === 'string' ? identifier : xAxisIds[identifier ?? 0]; + const id = axisId ?? xAxisIds[0]; return xAxis[id]; } -export function useYAxis(identifier?: number | string) { +/** + * Get the Y axis. + * @param {AxisId | undefined} axisId - If provided returns the y axis with axisId, else returns the values for the default y axis. + * @returns The Y axis. + */ +export function useYAxis(axisId?: AxisId) { const store = useStore<[UseChartCartesianAxisSignature]>(); const { axis: yAxis, axisIds: yAxisIds } = useSelector(store, selectorChartYAxis); - const id = typeof identifier === 'string' ? identifier : yAxisIds[identifier ?? 0]; + const id = axisId ?? yAxisIds[0]; return yAxis[id]; } diff --git a/packages/x-charts/src/hooks/useBarSeries.test.tsx b/packages/x-charts/src/hooks/useBarSeries.test.tsx new file mode 100644 index 0000000000000..f567986ca6aac --- /dev/null +++ b/packages/x-charts/src/hooks/useBarSeries.test.tsx @@ -0,0 +1,54 @@ +import { renderHook } from '@mui/internal-test-utils'; +import { expect } from 'chai'; +import * as React from 'react'; +import { useBarSeries } from './useBarSeries'; +import { BarSeriesType } from '../models'; +import { BarChart } from '../BarChart'; + +describe('useBarSeries', () => { + const mockSeries: BarSeriesType[] = [ + { + type: 'bar', + id: '1', + data: [1, 2, 3], + }, + { + type: 'bar', + id: '2', + data: [4, 5, 6], + }, + ]; + + const defaultProps = { + series: mockSeries, + height: 400, + width: 400, + }; + + const options: any = { + wrapper: ({ children }: { children: React.ReactElement }) => { + return {children}; + }, + }; + + it('should return all bar series when no seriesIds are provided', () => { + const { result } = renderHook(() => useBarSeries(), options); + expect(result.current?.seriesOrder).to.deep.equal(['1', '2']); + expect(Object.keys(result.current?.series ?? {})).to.deep.equal(['1', '2']); + }); + + it('should return the specific bar series when a single seriesId is provided', () => { + const { result } = renderHook(() => useBarSeries('1'), options); + expect(result.current?.id).to.deep.equal(mockSeries[0].id); + }); + + it('should return the specific bar series when multiple seriesIds are provided', () => { + const { result } = renderHook(() => useBarSeries(['2', '1']), options); + expect(result.current?.map((v) => v?.id)).to.deep.equal([mockSeries[1].id, mockSeries[0].id]); + }); + + it('should return undefined series when invalid seriesIds are provided', () => { + const { result } = renderHook(() => useBarSeries(['1', '3']), options); + expect(result.current?.map((v) => v?.id)).to.deep.equal([mockSeries[0].id, undefined]); + }); +}); diff --git a/packages/x-charts/src/hooks/useBarSeries.ts b/packages/x-charts/src/hooks/useBarSeries.ts new file mode 100644 index 0000000000000..cda1d6df429e8 --- /dev/null +++ b/packages/x-charts/src/hooks/useBarSeries.ts @@ -0,0 +1,33 @@ +'use client'; +import { ProcessedSeries } from '../internals/plugins/corePlugins/useChartSeries/useChartSeries.types'; +import { SeriesId } from '../models/seriesType/common'; +import { ChartSeriesDefaultized } from '../models/seriesType/config'; +import { createSeriesSelectorsOfType } from '../internals/createSeriesSelectorOfType'; + +const selectorSeries = createSeriesSelectorsOfType('bar'); + +/** + * Get access to the internal state of bar series. + * The returned object contains: + * - series: a mapping from ids to series attributes. + * - seriesOrder: the array of series ids. + * @returns {{ series: Record; seriesOrder: SeriesId[]; } | undefined} barSeries + */ +export function useBarSeries(): ProcessedSeries['bar']; +/** + * Get access to the internal state of bar series. + * + * @param {SeriesId} seriesId The id of the series to get. + * @returns {ChartSeriesDefaultized<'bar'> | undefined} barSeries + */ +export function useBarSeries(seriesId: SeriesId): ChartSeriesDefaultized<'bar'> | undefined; +/** + * Get access to the internal state of bar series. + * + * @param {SeriesId[]} seriesIds The ids of the series to get. Order is preserved. + * @returns {ChartSeriesDefaultized<'bar'>[] | undefined} barSeries + */ +export function useBarSeries(seriesIds: SeriesId[]): (ChartSeriesDefaultized<'bar'> | undefined)[]; +export function useBarSeries(seriesIds?: SeriesId | SeriesId[]) { + return selectorSeries(seriesIds); +} diff --git a/packages/x-charts/src/hooks/useChartId.ts b/packages/x-charts/src/hooks/useChartId.ts index f51899a80ef61..383f5d1ab17df 100644 --- a/packages/x-charts/src/hooks/useChartId.ts +++ b/packages/x-charts/src/hooks/useChartId.ts @@ -3,7 +3,11 @@ import { useStore } from '../internals/store/useStore'; import { useSelector } from '../internals/store/useSelector'; import { selectorChartId } from '../internals/plugins/corePlugins/useChartId/useChartId.selectors'; -export function useChartId() { +/** + * Get the unique identifier of the chart. + * @returns chartId if it exists. + */ +export function useChartId(): string | undefined { const store = useStore(); return useSelector(store, selectorChartId); } diff --git a/packages/x-charts/src/hooks/useColorScale.ts b/packages/x-charts/src/hooks/useColorScale.ts index 76406894f13ce..7546f63eb0d8e 100644 --- a/packages/x-charts/src/hooks/useColorScale.ts +++ b/packages/x-charts/src/hooks/useColorScale.ts @@ -1,34 +1,46 @@ 'use client'; -import { AxisScaleComputedConfig, ScaleName } from '../models/axis'; -import { useXAxes, useYAxes } from './useAxis'; -import { useZAxes } from './useZAxis'; - +import { AxisId, AxisScaleComputedConfig, ScaleName } from '../models/axis'; +import { useXAxis, useYAxis } from './useAxis'; +import { useZAxis } from './useZAxis'; + +/** + * Get the X axis color scale. + * + * @param {AxisId | undefined} axisId - If provided returns color scale for axisId, else returns the values for the default axis. + * @returns {AxisScaleComputedConfig[S]['colorScale'] | undefined} The color scale for the specified X axis, or undefined if not found. + */ export function useXColorScale( - identifier?: number | string, + axisId?: AxisId, ): AxisScaleComputedConfig[S]['colorScale'] | undefined { - const { xAxis, xAxisIds } = useXAxes(); - - const id = typeof identifier === 'string' ? identifier : xAxisIds[identifier ?? 0]; + const axis = useXAxis(axisId); - return xAxis[id].colorScale; + return axis.colorScale; } +/** + * Get the Y axis color scale. + * + * @param {AxisId | undefined} axisId - If provided returns color scale for axisId, else returns the values for the default axis. + * @returns {AxisScaleComputedConfig[S]['colorScale'] | undefined} The color scale for the specified Y axis, or undefined if not found. + */ export function useYColorScale( - identifier?: number | string, + axisId?: AxisId, ): AxisScaleComputedConfig[S]['colorScale'] | undefined { - const { yAxis, yAxisIds } = useYAxes(); + const axis = useYAxis(axisId); - const id = typeof identifier === 'string' ? identifier : yAxisIds[identifier ?? 0]; - - return yAxis[id].colorScale; + return axis.colorScale; } +/** + * Get the Z axis color scale. + * + * @param {AxisId | undefined} axisId - If provided returns color scale for axisId, else returns the values for the default axis. + * @returns {AxisScaleComputedConfig[S]['colorScale'] | undefined} The color scale for the specified Z axis, or undefined if not found. + */ export function useZColorScale( - identifier?: number | string, + axisId?: AxisId, ): AxisScaleComputedConfig[S]['colorScale'] | undefined { - const { zAxis, zAxisIds } = useZAxes(); - - const id = typeof identifier === 'string' ? identifier : zAxisIds[identifier ?? 0]; + const axis = useZAxis(axisId); - return zAxis[id]?.colorScale; + return axis.colorScale; } diff --git a/packages/x-charts/src/hooks/useDrawingArea.ts b/packages/x-charts/src/hooks/useDrawingArea.ts index 0519e645a4faf..63710546bdc78 100644 --- a/packages/x-charts/src/hooks/useDrawingArea.ts +++ b/packages/x-charts/src/hooks/useDrawingArea.ts @@ -11,7 +11,15 @@ export type ChartDrawingArea = { width: number; height: number; }; -export function useDrawingArea() { + +/** + * Get the drawing area dimensions and coordinates. The drawing area is the area where the chart is rendered. + * + * It includes the left, top, width, height, bottom, and right dimensions. + * + * @returns The drawing area dimensions. + */ +export function useDrawingArea(): ChartDrawingArea { const store = useStore(); return useSelector(store, selectorChartDrawingArea); } diff --git a/packages/x-charts/src/hooks/useLineSeries.test.tsx b/packages/x-charts/src/hooks/useLineSeries.test.tsx new file mode 100644 index 0000000000000..21acb1ac116d7 --- /dev/null +++ b/packages/x-charts/src/hooks/useLineSeries.test.tsx @@ -0,0 +1,54 @@ +import { renderHook } from '@mui/internal-test-utils'; +import { expect } from 'chai'; +import * as React from 'react'; +import { useLineSeries } from './useLineSeries'; +import { LineSeriesType } from '../models'; +import { LineChart } from '../LineChart'; + +describe('useLineSeries', () => { + const mockSeries: LineSeriesType[] = [ + { + type: 'line', + id: '1', + data: [1, 2, 3], + }, + { + type: 'line', + id: '2', + data: [4, 5, 6], + }, + ]; + + const defaultProps = { + series: mockSeries, + height: 400, + width: 400, + }; + + const options: any = { + wrapper: ({ children }: { children: React.ReactElement }) => { + return {children}; + }, + }; + + it('should return all line series when no seriesIds are provided', () => { + const { result } = renderHook(() => useLineSeries(), options); + expect(result.current?.seriesOrder).to.deep.equal(['1', '2']); + expect(Object.keys(result.current?.series ?? {})).to.deep.equal(['1', '2']); + }); + + it('should return the specific line series when a single seriesId is provided', () => { + const { result } = renderHook(() => useLineSeries('1'), options); + expect(result.current?.id).to.deep.equal(mockSeries[0].id); + }); + + it('should return the specific line series when multiple seriesIds are provided', () => { + const { result } = renderHook(() => useLineSeries(['2', '1']), options); + expect(result.current?.map((v) => v?.id)).to.deep.equal([mockSeries[1].id, mockSeries[0].id]); + }); + + it('should return undefined series when invalid seriesIds are provided', () => { + const { result } = renderHook(() => useLineSeries(['1', '3']), options); + expect(result.current?.map((v) => v?.id)).to.deep.equal([mockSeries[0].id, undefined]); + }); +}); diff --git a/packages/x-charts/src/hooks/useLineSeries.ts b/packages/x-charts/src/hooks/useLineSeries.ts new file mode 100644 index 0000000000000..bcadf79f29c05 --- /dev/null +++ b/packages/x-charts/src/hooks/useLineSeries.ts @@ -0,0 +1,35 @@ +'use client'; +import { ProcessedSeries } from '../internals/plugins/corePlugins/useChartSeries/useChartSeries.types'; +import { SeriesId } from '../models/seriesType/common'; +import { ChartSeriesDefaultized } from '../models/seriesType/config'; +import { createSeriesSelectorsOfType } from '../internals/createSeriesSelectorOfType'; + +const selectorSeries = createSeriesSelectorsOfType('line'); + +/** + * Get access to the internal state of line series. + * The returned object contains: + * - series: a mapping from ids to series attributes. + * - seriesOrder: the array of series ids. + * @returns {{ series: Record; seriesOrder: SeriesId[]; } | undefined} lineSeries + */ +export function useLineSeries(): ProcessedSeries['line']; +/** + * Get access to the internal state of line series. + * + * @param {SeriesId} seriesId The id of the series to get. + * @returns {ChartSeriesDefaultized<'line'> | undefined} lineSeries + */ +export function useLineSeries(seriesId: SeriesId): ChartSeriesDefaultized<'line'> | undefined; +/** + * Get access to the internal state of line series. + * + * @param {SeriesId[]} seriesIds The ids of the series to get. Order is preserved. + * @returns {ChartSeriesDefaultized<'line'>[] | undefined} lineSeries + */ +export function useLineSeries( + seriesIds: SeriesId[], +): (ChartSeriesDefaultized<'line'> | undefined)[]; +export function useLineSeries(seriesIds?: SeriesId | SeriesId[]) { + return selectorSeries(seriesIds); +} diff --git a/packages/x-charts/src/hooks/usePieSeries.test.tsx b/packages/x-charts/src/hooks/usePieSeries.test.tsx new file mode 100644 index 0000000000000..c82979892e1c3 --- /dev/null +++ b/packages/x-charts/src/hooks/usePieSeries.test.tsx @@ -0,0 +1,54 @@ +import { renderHook } from '@mui/internal-test-utils'; +import { expect } from 'chai'; +import * as React from 'react'; +import { usePieSeries } from './usePieSeries'; +import { PieSeriesType } from '../models'; +import { PieChart } from '../PieChart'; + +describe('usePieSeries', () => { + const mockSeries: PieSeriesType[] = [ + { + type: 'pie', + id: '1', + data: [{ value: 1 }], + }, + { + type: 'pie', + id: '2', + data: [{ value: 1 }], + }, + ]; + + const defaultProps = { + series: mockSeries, + height: 400, + width: 400, + }; + + const options: any = { + wrapper: ({ children }: { children: React.ReactElement }) => { + return {children}; + }, + }; + + it('should return all pie series when no seriesIds are provided', () => { + const { result } = renderHook(() => usePieSeries(), options); + expect(result.current?.seriesOrder).to.deep.equal(['1', '2']); + expect(Object.keys(result.current?.series ?? {})).to.deep.equal(['1', '2']); + }); + + it('should return the specific pie series when a single seriesId is provided', () => { + const { result } = renderHook(() => usePieSeries('1'), options); + expect(result.current?.id).to.deep.equal(mockSeries[0].id); + }); + + it('should return the specific pie series when multiple seriesIds are provided', () => { + const { result } = renderHook(() => usePieSeries(['2', '1']), options); + expect(result.current?.map((v) => v?.id)).to.deep.equal([mockSeries[1].id, mockSeries[0].id]); + }); + + it('should return undefined series when invalid seriesIds are provided', () => { + const { result } = renderHook(() => usePieSeries(['1', '3']), options); + expect(result.current?.map((v) => v?.id)).to.deep.equal([mockSeries[0].id, undefined]); + }); +}); diff --git a/packages/x-charts/src/hooks/usePieSeries.ts b/packages/x-charts/src/hooks/usePieSeries.ts new file mode 100644 index 0000000000000..16011db4fd9f6 --- /dev/null +++ b/packages/x-charts/src/hooks/usePieSeries.ts @@ -0,0 +1,33 @@ +'use client'; +import { ProcessedSeries } from '../internals/plugins/corePlugins/useChartSeries/useChartSeries.types'; +import { SeriesId } from '../models/seriesType/common'; +import { ChartSeriesDefaultized } from '../models/seriesType/config'; +import { createSeriesSelectorsOfType } from '../internals/createSeriesSelectorOfType'; + +const selectorSeries = createSeriesSelectorsOfType('pie'); + +/** + * Get access to the internal state of pie series. + * The returned object contains: + * - series: a mapping from ids to series attributes. + * - seriesOrder: the array of series ids. + * @returns {{ series: Record; seriesOrder: SeriesId[]; } | undefined} pieSeries + */ +export function usePieSeries(): ProcessedSeries['pie']; +/** + * Get access to the internal state of pie series. + * + * @param {SeriesId} seriesId The id of the series to get. + * @returns {ChartSeriesDefaultized<'pie'> | undefined} pieSeries + */ +export function usePieSeries(seriesId: SeriesId): ChartSeriesDefaultized<'pie'> | undefined; +/** + * Get access to the internal state of pie series. + * + * @param {SeriesId[]} seriesIds The ids of the series to get. Order is preserved. + * @returns {ChartSeriesDefaultized<'pie'>[] | undefined} pieSeries + */ +export function usePieSeries(seriesIds: SeriesId[]): (ChartSeriesDefaultized<'pie'> | undefined)[]; +export function usePieSeries(seriesIds?: SeriesId | SeriesId[]) { + return selectorSeries(seriesIds); +} diff --git a/packages/x-charts/src/hooks/useScale.ts b/packages/x-charts/src/hooks/useScale.ts index 6081559d0a685..58caa3c8d53b1 100644 --- a/packages/x-charts/src/hooks/useScale.ts +++ b/packages/x-charts/src/hooks/useScale.ts @@ -1,33 +1,41 @@ 'use client'; import { isBandScale } from '../internals/isBandScale'; -import { AxisScaleConfig, D3Scale, ScaleName } from '../models/axis'; +import { AxisId, AxisScaleConfig, D3Scale, ScaleName } from '../models/axis'; import { useXAxis, useYAxis } from './useAxis'; /** * For a given scale return a function that map value to their position. * Useful when dealing with specific scale such as band. - * @param scale The scale to use - * @returns (value: any) => number + * @param {D3Scale} scale The scale to use + * @returns {(value: any) => number} A function that map value to their position */ -export function getValueToPositionMapper(scale: D3Scale) { +export function getValueToPositionMapper(scale: D3Scale): (value: any) => number { if (isBandScale(scale)) { return (value: any) => (scale(value) ?? 0) + scale.bandwidth() / 2; } return (value: any) => scale(value) as number; } -export function useXScale( - identifier?: number | string, -): AxisScaleConfig[S]['scale'] { - const axis = useXAxis(identifier); +/** + * Get the X scale. + * + * @param {AxisId | undefined} axisId - If provided returns the scale for the x axis with axisId, else returns the values for the default x axis. + * @returns {AxisScaleConfig[S]['scale']} The scale for the specified X axis. + */ +export function useXScale(axisId?: AxisId): AxisScaleConfig[S]['scale'] { + const axis = useXAxis(axisId); return axis.scale; } -export function useYScale( - identifier?: number | string, -): AxisScaleConfig[S]['scale'] { - const axis = useYAxis(identifier); +/** + * Get the Y scale. + * + * @param {AxisId | undefined} axisId - If provided returns the scale for the y axis with axisId, else returns the values for the default y axis. + * @returns {AxisScaleConfig[S]['scale']} The scale for the specified Y axis. + */ +export function useYScale(axisId?: AxisId): AxisScaleConfig[S]['scale'] { + const axis = useYAxis(axisId); return axis.scale; } diff --git a/packages/x-charts/src/hooks/useScatterSeries.test.tsx b/packages/x-charts/src/hooks/useScatterSeries.test.tsx new file mode 100644 index 0000000000000..3bfbd25ee8165 --- /dev/null +++ b/packages/x-charts/src/hooks/useScatterSeries.test.tsx @@ -0,0 +1,60 @@ +import { renderHook } from '@mui/internal-test-utils'; +import { expect } from 'chai'; +import * as React from 'react'; +import { useScatterSeries } from './useScatterSeries'; +import { ScatterSeriesType } from '../models'; +import { ScatterChart } from '../ScatterChart'; + +describe('useScatterSeries', () => { + const mockSeries: ScatterSeriesType[] = [ + { + type: 'scatter', + id: '1', + data: [ + { id: 1, x: 1, y: 1 }, + { id: 2, x: 2, y: 2 }, + ], + }, + { + type: 'scatter', + id: '2', + data: [ + { id: 3, x: 3, y: 3 }, + { id: 4, x: 4, y: 4 }, + ], + }, + ]; + + const defaultProps = { + series: mockSeries, + height: 400, + width: 400, + }; + + const options: any = { + wrapper: ({ children }: { children: React.ReactElement }) => { + return {children}; + }, + }; + + it('should return all scatter series when no seriesIds are provided', () => { + const { result } = renderHook(() => useScatterSeries(), options); + expect(result.current?.seriesOrder).to.deep.equal(['1', '2']); + expect(Object.keys(result.current?.series ?? {})).to.deep.equal(['1', '2']); + }); + + it('should return the specific scatter series when a single seriesId is provided', () => { + const { result } = renderHook(() => useScatterSeries('1'), options); + expect(result.current?.id).to.deep.equal(mockSeries[0].id); + }); + + it('should return the specific scatter series when multiple seriesIds are provided', () => { + const { result } = renderHook(() => useScatterSeries(['2', '1']), options); + expect(result.current?.map((v) => v?.id)).to.deep.equal([mockSeries[1].id, mockSeries[0].id]); + }); + + it('should return undefined series when invalid seriesIds are provided', () => { + const { result } = renderHook(() => useScatterSeries(['1', '3']), options); + expect(result.current?.map((v) => v?.id)).to.deep.equal([mockSeries[0].id, undefined]); + }); +}); diff --git a/packages/x-charts/src/hooks/useScatterSeries.ts b/packages/x-charts/src/hooks/useScatterSeries.ts new file mode 100644 index 0000000000000..d41a2c22a4fd8 --- /dev/null +++ b/packages/x-charts/src/hooks/useScatterSeries.ts @@ -0,0 +1,35 @@ +'use client'; +import { ProcessedSeries } from '../internals/plugins/corePlugins/useChartSeries/useChartSeries.types'; +import { SeriesId } from '../models/seriesType/common'; +import { ChartSeriesDefaultized } from '../models/seriesType/config'; +import { createSeriesSelectorsOfType } from '../internals/createSeriesSelectorOfType'; + +const selectorSeries = createSeriesSelectorsOfType('scatter'); + +/** + * Get access to the internal state of scatter series. + * The returned object contains: + * - series: a mapping from ids to series attributes. + * - seriesOrder: the array of series ids. + * @returns {{ series: Record; seriesOrder: SeriesId[]; } | undefined} scatterSeries + */ +export function useScatterSeries(): ProcessedSeries['scatter']; +/** + * Get access to the internal state of scatter series. + * + * @param {SeriesId} seriesId The id of the series to get. + * @returns {ChartSeriesDefaultized<'scatter'> | undefined} scatterSeries + */ +export function useScatterSeries(seriesId: SeriesId): ChartSeriesDefaultized<'scatter'> | undefined; +/** + * Get access to the internal state of scatter series. + * + * @param {SeriesId[]} seriesIds The ids of the series to get. Order is preserved. + * @returns {ChartSeriesDefaultized<'scatter'>[] | undefined} scatterSeries + */ +export function useScatterSeries( + seriesIds: SeriesId[], +): (ChartSeriesDefaultized<'scatter'> | undefined)[]; +export function useScatterSeries(seriesIds?: SeriesId | SeriesId[]) { + return selectorSeries(seriesIds); +} diff --git a/packages/x-charts/src/hooks/useSeries.ts b/packages/x-charts/src/hooks/useSeries.ts index ed1e54bf26fe7..d45c7ef497cdd 100644 --- a/packages/x-charts/src/hooks/useSeries.ts +++ b/packages/x-charts/src/hooks/useSeries.ts @@ -1,5 +1,4 @@ 'use client'; -import * as React from 'react'; import { useStore } from '../internals/store/useStore'; import { useSelector } from '../internals/store/useSelector'; import { selectorChartSeriesProcessed } from '../internals/plugins/corePlugins/useChartSeries/useChartSeries.selectors'; @@ -15,55 +14,3 @@ export function useSeries() { const store = useStore<[UseChartSeriesSignature]>(); return useSelector(store, selectorChartSeriesProcessed); } - -/** - * Get access to the internal state of pie series. - * The returned object contains: - * - series: a mapping from ids to series attributes. - * - seriesOrder: the array of series ids. - * @returns {{ series: Record; seriesOrder: SeriesId[]; } | undefined} pieSeries - */ -export function usePieSeries() { - const series = useSeries(); - - return React.useMemo(() => series.pie, [series.pie]); -} - -/** - * Get access to the internal state of line series. - * The returned object contains: - * - series: a mapping from ids to series attributes. - * - seriesOrder: the array of series ids. - * @returns {{ series: Record; seriesOrder: SeriesId[]; } | undefined} lineSeries - */ -export function useLineSeries() { - const series = useSeries(); - - return React.useMemo(() => series.line, [series.line]); -} - -/** - * Get access to the internal state of bar series. - * The returned object contains: - * - series: a mapping from ids to series attributes. - * - seriesOrder: the array of series ids. - * @returns {{ series: Record; seriesOrder: SeriesId[]; } | undefined} barSeries - */ -export function useBarSeries() { - const series = useSeries(); - - return React.useMemo(() => series.bar, [series.bar]); -} - -/** - * Get access to the internal state of scatter series. - * The returned object contains: - * - series: a mapping from ids to series attributes. - * - seriesOrder: the array of series ids. - * @returns {{ series: Record; seriesOrder: SeriesId[]; } | undefined} scatterSeries - */ -export function useScatterSeries() { - const series = useSeries(); - - return React.useMemo(() => series.scatter, [series.scatter]); -} diff --git a/packages/x-charts/src/hooks/useSvgRef.ts b/packages/x-charts/src/hooks/useSvgRef.ts index 2da48b2992733..3e8dea2665919 100644 --- a/packages/x-charts/src/hooks/useSvgRef.ts +++ b/packages/x-charts/src/hooks/useSvgRef.ts @@ -2,6 +2,10 @@ import * as React from 'react'; import { useChartContext } from '../context/ChartProvider'; +/** + * Get the ref for the SVG element. + * @returns The SVG ref. + */ export function useSvgRef(): React.RefObject { const context = useChartContext(); diff --git a/packages/x-charts/src/internals/createSeriesSelectorOfType.ts b/packages/x-charts/src/internals/createSeriesSelectorOfType.ts new file mode 100644 index 0000000000000..9f49a7fb3e680 --- /dev/null +++ b/packages/x-charts/src/internals/createSeriesSelectorOfType.ts @@ -0,0 +1,29 @@ +import { ChartsSeriesConfig } from '../models/seriesType/config'; +import { SeriesId } from '../models/seriesType/common'; +import { createSelector } from './plugins/utils/selectors'; +import { selectorChartSeriesProcessed } from './plugins/corePlugins/useChartSeries/useChartSeries.selectors'; +import { useStore } from './store/useStore'; +import { useSelector } from './store/useSelector'; + +export function createSeriesSelectorsOfType(seriesType: T) { + const selectorSeriesWithIds = createSelector( + [selectorChartSeriesProcessed, (_, ids?: SeriesId | SeriesId[]) => ids], + (processedSeries, ids) => { + if (!ids || (Array.isArray(ids) && ids.length === 0)) { + return processedSeries[seriesType]; + } + + if (!Array.isArray(ids)) { + return processedSeries[seriesType]?.series?.[ids]; + } + + return ids.map((id) => processedSeries[seriesType]?.series?.[id]); + }, + ); + + return (ids?: SeriesId | SeriesId[]) => { + const store = useStore(); + + return useSelector(store, selectorSeriesWithIds, ids); + }; +} diff --git a/packages/x-charts/src/internals/index.ts b/packages/x-charts/src/internals/index.ts index 107d9d25d524b..8fd7d9f3305e4 100644 --- a/packages/x-charts/src/internals/index.ts +++ b/packages/x-charts/src/internals/index.ts @@ -12,6 +12,7 @@ export { useLineChartProps } from '../LineChart/useLineChartProps'; export { useBarChartProps } from '../BarChart/useBarChartProps'; export * from '../ChartContainer/useChartContainerProps'; export * from '../ChartDataProvider/useChartDataProviderProps'; +export * from './createSeriesSelectorOfType'; // plugins export * from './plugins/corePlugins/useChartId'; diff --git a/packages/x-internals/src/fastArrayCompare/fastArrayCompare.test.ts b/packages/x-internals/src/fastArrayCompare/fastArrayCompare.test.ts new file mode 100644 index 0000000000000..ae0ea91046702 --- /dev/null +++ b/packages/x-internals/src/fastArrayCompare/fastArrayCompare.test.ts @@ -0,0 +1,35 @@ +import { expect } from 'chai'; +import { fastArrayCompare } from './fastArrayCompare'; + +describe('fastArrayCompare', () => { + it('should return true if arrays are equal', () => { + expect(fastArrayCompare([1, 2, 3], [1, 2, 3])).to.equal(true); + }); + + it('should return false if arrays are not equal', () => { + expect(fastArrayCompare([1, 2, 3], [1, 2, 4])).to.equal(false); + }); + + it('should return false if arrays have different lengths', () => { + expect(fastArrayCompare([1, 2, 3], [1, 2])).to.equal(false); + }); + + it('should return false if one of the arguments is not an array', () => { + // @ts-expect-error + expect(fastArrayCompare([1, 2, 3], 1)).to.equal(false); + }); + + it('should return false if both arguments are not an array', () => { + // @ts-expect-error + expect(fastArrayCompare(1, 2)).to.equal(false); + }); + + it('should return true if both arguments are the same array', () => { + const arr = [1, 2, 3]; + expect(fastArrayCompare(arr, arr)).to.equal(true); + }); + + it('should return true if both arguments are empty arrays', () => { + expect(fastArrayCompare([], [])).to.equal(true); + }); +}); diff --git a/packages/x-internals/src/fastArrayCompare/fastArrayCompare.ts b/packages/x-internals/src/fastArrayCompare/fastArrayCompare.ts new file mode 100644 index 0000000000000..b82b0729465bd --- /dev/null +++ b/packages/x-internals/src/fastArrayCompare/fastArrayCompare.ts @@ -0,0 +1,32 @@ +/** + * A fast array comparison function that compares two arrays for equality. + * + * Assumes that the arrays are ordered and contain only primitive values. + * + * It is faster than `fastObjectShallowCompare` for arrays. + * + * @returns true if arrays contain the same elements in the same order, false otherwise. + */ +export function fastArrayCompare(a: T, b: T): boolean { + if (a === b) { + return true; + } + + if (!Array.isArray(a) || !Array.isArray(b)) { + return false; + } + + let i = a.length; + if (i !== b.length) { + return false; + } + + // eslint-disable-next-line no-plusplus + while (i--) { + if (a[i] !== b[i]) { + return false; + } + } + + return true; +} diff --git a/packages/x-internals/src/fastArrayCompare/index.ts b/packages/x-internals/src/fastArrayCompare/index.ts new file mode 100644 index 0000000000000..8c93a145fda3b --- /dev/null +++ b/packages/x-internals/src/fastArrayCompare/index.ts @@ -0,0 +1 @@ +export { fastArrayCompare } from './fastArrayCompare';