diff --git a/docs/data/charts/zoom-and-pan/ZoomControlled.js b/docs/data/charts/zoom-and-pan/ZoomControlled.js new file mode 100644 index 0000000000000..82a17f60a3262 --- /dev/null +++ b/docs/data/charts/zoom-and-pan/ZoomControlled.js @@ -0,0 +1,145 @@ +import * as React from 'react'; +import { LineChartPro } from '@mui/x-charts-pro/LineChartPro'; + +import { Button } from '@mui/base'; + +export default function ZoomControlled() { + const [zoom, setZoom] = React.useState([ + { + axisId: 'my-x-axis', + start: 20, + end: 40, + }, + ]); + + return ( +
+ + i), + }, + ]} + /> +
+ ); +} + +const data = [ + { + y1: 443.28, + y2: 153.9, + }, + { + y1: 110.5, + y2: 217.8, + }, + { + y1: 175.23, + y2: 286.32, + }, + { + y1: 195.97, + y2: 325.12, + }, + { + y1: 351.77, + y2: 144.58, + }, + { + y1: 43.253, + y2: 146.51, + }, + { + y1: 376.34, + y2: 309.69, + }, + { + y1: 31.514, + y2: 236.38, + }, + { + y1: 231.31, + y2: 440.72, + }, + { + y1: 108.04, + y2: 20.29, + }, + { + y1: 321.77, + y2: 484.17, + }, + { + y1: 120.18, + y2: 54.962, + }, + { + y1: 366.2, + y2: 418.5, + }, + { + y1: 451.45, + y2: 181.32, + }, + { + y1: 294.8, + y2: 440.9, + }, + { + y1: 121.83, + y2: 273.52, + }, + { + y1: 287.7, + y2: 346.7, + }, + { + y1: 134.06, + y2: 74.528, + }, + { + y1: 104.5, + y2: 150.9, + }, + { + y1: 413.07, + y2: 26.483, + }, + { + y1: 74.68, + y2: 333.2, + }, + { + y1: 360.6, + y2: 422.0, + }, + { + y1: 330.72, + y2: 488.06, + }, +]; + +const chartProps = { + width: 600, + height: 300, + series: [ + { + label: 'Series A', + data: data.map((v) => v.y1), + }, + { + label: 'Series B', + data: data.map((v) => v.y2), + }, + ], +}; diff --git a/docs/data/charts/zoom-and-pan/ZoomControlled.tsx b/docs/data/charts/zoom-and-pan/ZoomControlled.tsx new file mode 100644 index 0000000000000..643f2fd65a7a7 --- /dev/null +++ b/docs/data/charts/zoom-and-pan/ZoomControlled.tsx @@ -0,0 +1,145 @@ +import * as React from 'react'; +import { LineChartPro } from '@mui/x-charts-pro/LineChartPro'; +import { ZoomData } from '@mui/x-charts-pro/context'; +import { Button } from '@mui/base'; + +export default function ZoomControlled() { + const [zoom, setZoom] = React.useState([ + { + axisId: 'my-x-axis', + start: 20, + end: 40, + }, + ]); + + return ( +
+ + i), + }, + ]} + /> +
+ ); +} + +const data = [ + { + y1: 443.28, + y2: 153.9, + }, + { + y1: 110.5, + y2: 217.8, + }, + { + y1: 175.23, + y2: 286.32, + }, + { + y1: 195.97, + y2: 325.12, + }, + { + y1: 351.77, + y2: 144.58, + }, + { + y1: 43.253, + y2: 146.51, + }, + { + y1: 376.34, + y2: 309.69, + }, + { + y1: 31.514, + y2: 236.38, + }, + { + y1: 231.31, + y2: 440.72, + }, + { + y1: 108.04, + y2: 20.29, + }, + { + y1: 321.77, + y2: 484.17, + }, + { + y1: 120.18, + y2: 54.962, + }, + { + y1: 366.2, + y2: 418.5, + }, + { + y1: 451.45, + y2: 181.32, + }, + { + y1: 294.8, + y2: 440.9, + }, + { + y1: 121.83, + y2: 273.52, + }, + { + y1: 287.7, + y2: 346.7, + }, + { + y1: 134.06, + y2: 74.528, + }, + { + y1: 104.5, + y2: 150.9, + }, + { + y1: 413.07, + y2: 26.483, + }, + { + y1: 74.68, + y2: 333.2, + }, + { + y1: 360.6, + y2: 422.0, + }, + { + y1: 330.72, + y2: 488.06, + }, +]; + +const chartProps = { + width: 600, + height: 300, + series: [ + { + label: 'Series A', + data: data.map((v) => v.y1), + }, + { + label: 'Series B', + data: data.map((v) => v.y2), + }, + ], +}; diff --git a/docs/data/charts/zoom-and-pan/ZoomControlled.tsx.preview b/docs/data/charts/zoom-and-pan/ZoomControlled.tsx.preview new file mode 100644 index 0000000000000..443aef289aa8f --- /dev/null +++ b/docs/data/charts/zoom-and-pan/ZoomControlled.tsx.preview @@ -0,0 +1,16 @@ + + i), + }, + ]} +/> \ No newline at end of file diff --git a/docs/data/charts/zoom-and-pan/zoom-and-pan.md b/docs/data/charts/zoom-and-pan/zoom-and-pan.md index 293d55e2bf919..389b055de0999 100644 --- a/docs/data/charts/zoom-and-pan/zoom-and-pan.md +++ b/docs/data/charts/zoom-and-pan/zoom-and-pan.md @@ -47,3 +47,18 @@ The following options are available: - **panning**: Enables or disables panning. {{"demo": "ZoomOptionsNoSnap.js", "hideToolbar": true, "bg": "playground"}} + +## Controlled zoom + +You can control the zoom state by setting the `zoom` and `onZoomChange` props. +This way, you can control the zoom state from outside the chart. + +The `onZoomChange` prop is a function that receives the new zoom state. + +While the `zoom` prop is an array of objects that define the zoom state for each axis with zoom enabled. + +- **axisId**: The id of the axis to control. +- **start**: The starting percentage of the axis range. +- **end**: The ending percentage of the zoom range. + +{{"demo": "ZoomControlled.js"}} diff --git a/docs/pages/x/api/charts/chart-container-pro.json b/docs/pages/x/api/charts/chart-container-pro.json index 85cd769e2e54a..a26642badf560 100644 --- a/docs/pages/x/api/charts/chart-container-pro.json +++ b/docs/pages/x/api/charts/chart-container-pro.json @@ -32,6 +32,13 @@ "describedArgs": ["highlightedItem"] } }, + "onZoomChange": { + "type": { "name": "func" }, + "signature": { + "type": "function(zoomData: Array) => void", + "describedArgs": ["zoomData"] + } + }, "plugins": { "type": { "name": "arrayOf", "description": "Array<object>" } }, "xAxis": { "type": { @@ -50,6 +57,12 @@ "name": "arrayOf", "description": "Array<{ colorMap?: { colors: Array<string>, type: 'ordinal', unknownColor?: string, values?: Array<Date
| number
| string> }
| { color: Array<string>
| func, max?: Date
| number, min?: Date
| number, type: 'continuous' }
| { colors: Array<string>, thresholds: Array<Date
| number>, type: 'piecewise' }, data?: array, dataKey?: string, id?: string, max?: number, min?: number }>" } + }, + "zoom": { + "type": { + "name": "arrayOf", + "description": "Array<{ axisId: number
| string, end: number, start: number }>" + } } }, "name": "ChartContainerPro", diff --git a/docs/pages/x/api/charts/responsive-chart-container-pro.json b/docs/pages/x/api/charts/responsive-chart-container-pro.json index c404bc2fd96b1..8e58fe0bbfd67 100644 --- a/docs/pages/x/api/charts/responsive-chart-container-pro.json +++ b/docs/pages/x/api/charts/responsive-chart-container-pro.json @@ -31,6 +31,13 @@ "describedArgs": ["highlightedItem"] } }, + "onZoomChange": { + "type": { "name": "func" }, + "signature": { + "type": "function(zoomData: Array) => void", + "describedArgs": ["zoomData"] + } + }, "plugins": { "type": { "name": "arrayOf", "description": "Array<object>" } }, "width": { "type": { "name": "number" } }, "xAxis": { @@ -50,6 +57,12 @@ "name": "arrayOf", "description": "Array<{ colorMap?: { colors: Array<string>, type: 'ordinal', unknownColor?: string, values?: Array<Date
| number
| string> }
| { color: Array<string>
| func, max?: Date
| number, min?: Date
| number, type: 'continuous' }
| { colors: Array<string>, thresholds: Array<Date
| number>, type: 'piecewise' }, data?: array, dataKey?: string, id?: string, max?: number, min?: number }>" } + }, + "zoom": { + "type": { + "name": "arrayOf", + "description": "Array<{ axisId: number
| string, end: number, start: number }>" + } } }, "name": "ResponsiveChartContainerPro", diff --git a/docs/pages/x/api/charts/scatter-chart-pro.json b/docs/pages/x/api/charts/scatter-chart-pro.json index dbe112e7cf9b6..570d57a2c3d75 100644 --- a/docs/pages/x/api/charts/scatter-chart-pro.json +++ b/docs/pages/x/api/charts/scatter-chart-pro.json @@ -62,6 +62,13 @@ "describedArgs": ["event", "scatterItemIdentifier"] } }, + "onZoomChange": { + "type": { "name": "func" }, + "signature": { + "type": "function(zoomData: Array) => void", + "describedArgs": ["zoomData"] + } + }, "rightAxis": { "type": { "name": "union", "description": "object
| string" }, "default": "null" @@ -103,6 +110,12 @@ "name": "arrayOf", "description": "Array<{ colorMap?: { colors: Array<string>, type: 'ordinal', unknownColor?: string, values?: Array<Date
| number
| string> }
| { color: Array<string>
| func, max?: Date
| number, min?: Date
| number, type: 'continuous' }
| { colors: Array<string>, thresholds: Array<Date
| number>, type: 'piecewise' }, data?: array, dataKey?: string, id?: string, max?: number, min?: number }>" } + }, + "zoom": { + "type": { + "name": "arrayOf", + "description": "Array<{ axisId: number
| string, end: number, start: number }>" + } } }, "name": "ScatterChartPro", diff --git a/docs/translations/api-docs/charts/chart-container-pro/chart-container-pro.json b/docs/translations/api-docs/charts/chart-container-pro/chart-container-pro.json index 5a13d2067ae7c..20c5b4a301057 100644 --- a/docs/translations/api-docs/charts/chart-container-pro/chart-container-pro.json +++ b/docs/translations/api-docs/charts/chart-container-pro/chart-container-pro.json @@ -19,6 +19,10 @@ "description": "The callback fired when the highlighted item changes.", "typeDescriptions": { "highlightedItem": "The newly highlighted item." } }, + "onZoomChange": { + "description": "Callback fired when the zoom has changed.", + "typeDescriptions": { "zoomData": "Updated zoom data." } + }, "plugins": { "description": "An array of plugins defining how to preprocess data. If not provided, the container supports line, bar, scatter and pie charts." }, @@ -32,7 +36,8 @@ "yAxis": { "description": "The configuration of the y-axes. If not provided, a default axis config is used. An array of AxisConfig objects." }, - "zAxis": { "description": "The configuration of the z-axes." } + "zAxis": { "description": "The configuration of the z-axes." }, + "zoom": { "description": "The list of zoom data related to each axis." } }, "classDescriptions": {} } diff --git a/docs/translations/api-docs/charts/responsive-chart-container-pro/responsive-chart-container-pro.json b/docs/translations/api-docs/charts/responsive-chart-container-pro/responsive-chart-container-pro.json index 1623c12c44cda..469366dd9aad7 100644 --- a/docs/translations/api-docs/charts/responsive-chart-container-pro/responsive-chart-container-pro.json +++ b/docs/translations/api-docs/charts/responsive-chart-container-pro/responsive-chart-container-pro.json @@ -21,6 +21,10 @@ "description": "The callback fired when the highlighted item changes.", "typeDescriptions": { "highlightedItem": "The newly highlighted item." } }, + "onZoomChange": { + "description": "Callback fired when the zoom has changed.", + "typeDescriptions": { "zoomData": "Updated zoom data." } + }, "plugins": { "description": "An array of plugins defining how to preprocess data. If not provided, the container supports line, bar, scatter and pie charts." }, @@ -36,7 +40,8 @@ "yAxis": { "description": "The configuration of the y-axes. If not provided, a default axis config is used. An array of AxisConfig objects." }, - "zAxis": { "description": "The configuration of the z-axes." } + "zAxis": { "description": "The configuration of the z-axes." }, + "zoom": { "description": "The list of zoom data related to each axis." } }, "classDescriptions": {} } diff --git a/docs/translations/api-docs/charts/scatter-chart-pro/scatter-chart-pro.json b/docs/translations/api-docs/charts/scatter-chart-pro/scatter-chart-pro.json index 28a931b143ccb..6a6c1e0b1bf6a 100644 --- a/docs/translations/api-docs/charts/scatter-chart-pro/scatter-chart-pro.json +++ b/docs/translations/api-docs/charts/scatter-chart-pro/scatter-chart-pro.json @@ -43,6 +43,10 @@ "scatterItemIdentifier": "The scatter item identifier." } }, + "onZoomChange": { + "description": "Callback fired when the zoom has changed.", + "typeDescriptions": { "zoomData": "Updated zoom data." } + }, "rightAxis": { "description": "Indicate which axis to display the right of the charts. Can be a string (the id of the axis) or an object ChartsYAxisProps." }, @@ -70,7 +74,8 @@ "yAxis": { "description": "The configuration of the y-axes. If not provided, a default axis config is used. An array of AxisConfig objects." }, - "zAxis": { "description": "The configuration of the z-axes." } + "zAxis": { "description": "The configuration of the z-axes." }, + "zoom": { "description": "The list of zoom data related to each axis." } }, "classDescriptions": {} } diff --git a/packages/x-charts-pro/src/BarChartPro/BarChartPro.tsx b/packages/x-charts-pro/src/BarChartPro/BarChartPro.tsx index 6db9610abedf8..9f03659068b1b 100644 --- a/packages/x-charts-pro/src/BarChartPro/BarChartPro.tsx +++ b/packages/x-charts-pro/src/BarChartPro/BarChartPro.tsx @@ -14,8 +14,9 @@ import { BarPlotProps } from '@mui/x-charts'; import { ResponsiveChartContainerPro } from '../ResponsiveChartContainerPro'; import { ZoomSetup } from '../context/ZoomProvider/ZoomSetup'; import { useZoom } from '../context/ZoomProvider/useZoom'; +import { ZoomProps } from '../context/ZoomProvider'; -export interface BarChartProProps extends BarChartProps {} +export interface BarChartProProps extends BarChartProps, ZoomProps {} /** * Demos: @@ -29,6 +30,7 @@ export interface BarChartProProps extends BarChartProps {} * - [BarChart API](https://mui.com/x/api/charts/bar-chart/) */ const BarChartPro = React.forwardRef(function BarChartPro(props: BarChartProProps, ref) { + const { zoom, onZoomChange, ...other } = props; const { chartContainerProps, barPlotProps, @@ -42,10 +44,15 @@ const BarChartPro = React.forwardRef(function BarChartPro(props: BarChartProProp legendProps, tooltipProps, children, - } = useBarChartProps(props); + } = useBarChartProps(other); return ( - + {props.onAxisClick && } {props.grid && } diff --git a/packages/x-charts-pro/src/ChartContainerPro/ChartContainerPro.tsx b/packages/x-charts-pro/src/ChartContainerPro/ChartContainerPro.tsx index 09dc026abacc0..eac5d772c89db 100644 --- a/packages/x-charts-pro/src/ChartContainerPro/ChartContainerPro.tsx +++ b/packages/x-charts-pro/src/ChartContainerPro/ChartContainerPro.tsx @@ -9,33 +9,32 @@ import { DrawingProvider, InteractionProvider, SeriesContextProvider, - useChartContainerProps, } from '@mui/x-charts/internals'; import { useLicenseVerifier } from '@mui/x-license/useLicenseVerifier'; import { getReleaseInfo } from '../internals/utils/releaseInfo'; import { CartesianContextProviderPro } from '../context/CartesianProviderPro'; -import { ZoomProvider } from '../context/ZoomProvider'; +import { ZoomProps, ZoomProvider } from '../context/ZoomProvider'; +import { useChartContainerProProps } from './useChartContainerProProps'; const releaseInfo = getReleaseInfo(); -export interface ChartContainerProProps extends ChartContainerProps {} +export interface ChartContainerProProps extends ChartContainerProps, ZoomProps {} const ChartContainerPro = React.forwardRef(function ChartContainer( props: ChartContainerProProps, ref, ) { const { - children, + zoomProviderProps, drawingProviderProps, colorProviderProps, seriesContextProps, - cartesianContextProps, zAxisContextProps, highlightedProviderProps, + cartesianContextProps, chartsSurfaceProps, - xAxis, - yAxis, - } = useChartContainerProps(props, ref); + children, + } = useChartContainerProProps(props, ref); useLicenseVerifier('x-charts-pro', releaseInfo); @@ -43,7 +42,7 @@ const ChartContainerPro = React.forwardRef(function ChartContainer( - + @@ -115,6 +114,12 @@ ChartContainerPro.propTypes = { * @param {HighlightItemData | null} highlightedItem The newly highlighted item. */ onHighlightChange: PropTypes.func, + /** + * Callback fired when the zoom has changed. + * + * @param {ZoomData[]} zoomData Updated zoom data. + */ + onZoomChange: PropTypes.func, /** * An array of plugins defining how to preprocess data. * If not provided, the container supports line, bar, scatter and pie charts. @@ -345,6 +350,16 @@ ChartContainerPro.propTypes = { min: PropTypes.number, }), ), + /** + * The list of zoom data related to each axis. + */ + zoom: PropTypes.arrayOf( + PropTypes.shape({ + axisId: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired, + end: PropTypes.number.isRequired, + start: PropTypes.number.isRequired, + }), + ), } as any; export { ChartContainerPro }; diff --git a/packages/x-charts-pro/src/ChartContainerPro/useChartContainerProProps.ts b/packages/x-charts-pro/src/ChartContainerPro/useChartContainerProProps.ts new file mode 100644 index 0000000000000..fa2c8e6d0ba4d --- /dev/null +++ b/packages/x-charts-pro/src/ChartContainerPro/useChartContainerProProps.ts @@ -0,0 +1,42 @@ +import { useChartContainerProps } from '@mui/x-charts/internals'; +import { ZoomProviderProps } from '../context/ZoomProvider'; +import type { ChartContainerProProps } from './ChartContainerPro'; + +export const useChartContainerProProps = ( + props: ChartContainerProProps, + ref: React.ForwardedRef, +) => { + const { zoom, onZoomChange, ...baseProps } = props; + + const { + children, + drawingProviderProps, + colorProviderProps, + seriesContextProps, + cartesianContextProps, + zAxisContextProps, + highlightedProviderProps, + chartsSurfaceProps, + xAxis, + yAxis, + } = useChartContainerProps(baseProps, ref); + + const zoomProviderProps: Omit = { + zoom, + onZoomChange, + xAxis, + yAxis, + }; + + return { + zoomProviderProps, + children, + drawingProviderProps, + colorProviderProps, + seriesContextProps, + cartesianContextProps, + zAxisContextProps, + highlightedProviderProps, + chartsSurfaceProps, + }; +}; diff --git a/packages/x-charts-pro/src/Heatmap/Heatmap.tsx b/packages/x-charts-pro/src/Heatmap/Heatmap.tsx index a7936aa4847dd..42f67ece5b60d 100644 --- a/packages/x-charts-pro/src/Heatmap/Heatmap.tsx +++ b/packages/x-charts-pro/src/Heatmap/Heatmap.tsx @@ -50,7 +50,10 @@ export interface HeatmapSlotProps HeatmapItemSlotProps {} export interface HeatmapProps - extends Omit, + extends Omit< + ResponsiveChartContainerProProps, + 'series' | 'plugins' | 'xAxis' | 'yAxis' | 'zoom' | 'onZoomChange' + >, Omit, Omit, ChartsOnAxisClickHandlerProps { diff --git a/packages/x-charts-pro/src/LineChartPro/LineChartPro.tsx b/packages/x-charts-pro/src/LineChartPro/LineChartPro.tsx index a96716f8b22a9..323d37fb05d53 100644 --- a/packages/x-charts-pro/src/LineChartPro/LineChartPro.tsx +++ b/packages/x-charts-pro/src/LineChartPro/LineChartPro.tsx @@ -22,8 +22,9 @@ import { MarkPlotProps } from '@mui/x-charts'; import { ResponsiveChartContainerPro } from '../ResponsiveChartContainerPro'; import { ZoomSetup } from '../context/ZoomProvider/ZoomSetup'; import { useZoom } from '../context/ZoomProvider/useZoom'; +import { ZoomProps } from '../context/ZoomProvider'; -export interface LineChartProProps extends LineChartProps {} +export interface LineChartProProps extends LineChartProps, ZoomProps {} /** * Demos: @@ -36,6 +37,7 @@ export interface LineChartProProps extends LineChartProps {} * - [LineChart API](https://mui.com/x/api/charts/line-chart/) */ const LineChartPro = React.forwardRef(function LineChartPro(props: LineChartProProps, ref) { + const { zoom, onZoomChange, ...other } = props; const { chartContainerProps, axisClickHandlerProps, @@ -52,10 +54,15 @@ const LineChartPro = React.forwardRef(function LineChartPro(props: LineChartProP legendProps, tooltipProps, children, - } = useLineChartProps(props); + } = useLineChartProps(other); return ( - + {props.onAxisClick && } {props.grid && } diff --git a/packages/x-charts-pro/src/ResponsiveChartContainerPro/ResponsiveChartContainerPro.tsx b/packages/x-charts-pro/src/ResponsiveChartContainerPro/ResponsiveChartContainerPro.tsx index c856012e26b7b..13251cc66229e 100644 --- a/packages/x-charts-pro/src/ResponsiveChartContainerPro/ResponsiveChartContainerPro.tsx +++ b/packages/x-charts-pro/src/ResponsiveChartContainerPro/ResponsiveChartContainerPro.tsx @@ -2,11 +2,15 @@ import * as React from 'react'; import PropTypes from 'prop-types'; import { Watermark } from '@mui/x-license/Watermark'; import { ResponsiveChartContainerProps } from '@mui/x-charts/ResponsiveChartContainer'; -import { ResizableContainer, useResponsiveChartContainerProps } from '@mui/x-charts/internals'; +import { ResizableContainer } from '@mui/x-charts/internals'; import { getReleaseInfo } from '../internals/utils/releaseInfo'; import { ChartContainerPro } from '../ChartContainerPro'; +import { ZoomProps } from '../context/ZoomProvider'; +import { useResponsiveChartContainerProProps } from './useResponsiveChartContainerProProps'; -export interface ResponsiveChartContainerProProps extends ResponsiveChartContainerProps {} +export interface ResponsiveChartContainerProProps + extends ResponsiveChartContainerProps, + ZoomProps {} const releaseInfo = getReleaseInfo(); @@ -14,12 +18,12 @@ const ResponsiveChartContainerPro = React.forwardRef(function ResponsiveChartCon props: ResponsiveChartContainerProProps, ref, ) { - const { chartContainerProps, resizableChartContainerProps, hasIntrinsicSize } = - useResponsiveChartContainerProps(props, ref); + const { chartContainerProProps, resizableChartContainerProps, hasIntrinsicSize } = + useResponsiveChartContainerProProps(props, ref); return ( - {hasIntrinsicSize ? : null} + {hasIntrinsicSize ? : null} ); @@ -77,6 +81,12 @@ ResponsiveChartContainerPro.propTypes = { * @param {HighlightItemData | null} highlightedItem The newly highlighted item. */ onHighlightChange: PropTypes.func, + /** + * Callback fired when the zoom has changed. + * + * @param {ZoomData[]} zoomData Updated zoom data. + */ + onZoomChange: PropTypes.func, /** * An array of plugins defining how to preprocess data. * If not provided, the container supports line, bar, scatter and pie charts. @@ -307,6 +317,16 @@ ResponsiveChartContainerPro.propTypes = { min: PropTypes.number, }), ), + /** + * The list of zoom data related to each axis. + */ + zoom: PropTypes.arrayOf( + PropTypes.shape({ + axisId: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired, + end: PropTypes.number.isRequired, + start: PropTypes.number.isRequired, + }), + ), } as any; export { ResponsiveChartContainerPro }; diff --git a/packages/x-charts-pro/src/ResponsiveChartContainerPro/useResponsiveChartContainerProProps.ts b/packages/x-charts-pro/src/ResponsiveChartContainerPro/useResponsiveChartContainerProProps.ts new file mode 100644 index 0000000000000..363e4f9c9fc3f --- /dev/null +++ b/packages/x-charts-pro/src/ResponsiveChartContainerPro/useResponsiveChartContainerProProps.ts @@ -0,0 +1,27 @@ +import { useResponsiveChartContainerProps } from '@mui/x-charts/internals'; +import type { ChartContainerProProps } from '../ChartContainerPro'; +import type { ResponsiveChartContainerProProps } from './ResponsiveChartContainerPro'; + +export const useResponsiveChartContainerProProps = ( + props: ResponsiveChartContainerProProps, + ref: React.ForwardedRef, +) => { + const { zoom, onZoomChange, ...baseProps } = props; + + const chartContainerProProps: Pick = { + zoom, + onZoomChange, + }; + + const { chartContainerProps, resizableChartContainerProps, hasIntrinsicSize } = + useResponsiveChartContainerProps(baseProps, ref); + + return { + chartContainerProProps: { + ...chartContainerProps, + ...chartContainerProProps, + }, + resizableChartContainerProps, + hasIntrinsicSize, + }; +}; diff --git a/packages/x-charts-pro/src/ScatterChartPro/ScatterChartPro.tsx b/packages/x-charts-pro/src/ScatterChartPro/ScatterChartPro.tsx index 59fbed96eb847..57e5168a52472 100644 --- a/packages/x-charts-pro/src/ScatterChartPro/ScatterChartPro.tsx +++ b/packages/x-charts-pro/src/ScatterChartPro/ScatterChartPro.tsx @@ -12,8 +12,9 @@ import { ChartsTooltip } from '@mui/x-charts/ChartsTooltip'; import { useScatterChartProps } from '@mui/x-charts/internals'; import { ResponsiveChartContainerPro } from '../ResponsiveChartContainerPro'; import { ZoomSetup } from '../context/ZoomProvider/ZoomSetup'; +import { ZoomProps } from '../context/ZoomProvider'; -export interface ScatterChartProProps extends ScatterChartProps {} +export interface ScatterChartProProps extends ScatterChartProps, ZoomProps {} /** * Demos: @@ -29,6 +30,7 @@ const ScatterChartPro = React.forwardRef(function ScatterChartPro( props: ScatterChartProProps, ref, ) { + const { zoom, onZoomChange, ...other } = props; const { chartContainerProps, zAxisProps, @@ -41,9 +43,15 @@ const ScatterChartPro = React.forwardRef(function ScatterChartPro( axisHighlightProps, tooltipProps, children, - } = useScatterChartProps(props); + } = useScatterChartProps(other); + return ( - + {!props.disableVoronoi && } @@ -173,6 +181,12 @@ ScatterChartPro.propTypes = { * @param {ScatterItemIdentifier} scatterItemIdentifier The scatter item identifier. */ onItemClick: PropTypes.func, + /** + * Callback fired when the zoom has changed. + * + * @param {ZoomData[]} zoomData Updated zoom data. + */ + onZoomChange: PropTypes.func, /** * Indicate which axis to display the right of the charts. * Can be a string (the id of the axis) or an object `ChartsYAxisProps`. @@ -437,6 +451,16 @@ ScatterChartPro.propTypes = { min: PropTypes.number, }), ), + /** + * The list of zoom data related to each axis. + */ + zoom: PropTypes.arrayOf( + PropTypes.shape({ + axisId: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired, + end: PropTypes.number.isRequired, + start: PropTypes.number.isRequired, + }), + ), } as any; export { ScatterChartPro }; diff --git a/packages/x-charts-pro/src/context/ZoomProvider/Zoom.types.ts b/packages/x-charts-pro/src/context/ZoomProvider/Zoom.types.ts index 2cc44f9c06205..ac3f9990fc69a 100644 --- a/packages/x-charts-pro/src/context/ZoomProvider/Zoom.types.ts +++ b/packages/x-charts-pro/src/context/ZoomProvider/Zoom.types.ts @@ -1,5 +1,59 @@ import { AxisId } from '@mui/x-charts/internals'; +export type ZoomProviderProps = { + children: React.ReactNode; + /** + * The configuration of the x-axes. + * If not provided, a default axis config is used. + * An array of [[AxisConfig]] objects. + */ + xAxis?: AxisConfigForZoom[]; + /** + * The configuration of the y-axes. + * If not provided, a default axis config is used. + * An array of [[AxisConfig]] objects. + */ + yAxis?: AxisConfigForZoom[]; +} & ZoomProps; + +/** + * Represents the state of the ZoomProvider. + */ +export type ZoomState = { + /** + * Whether zooming is enabled. + */ + isZoomEnabled: boolean; + /** + * Whether panning is enabled. + */ + isPanEnabled: boolean; + /** + * The zoom options for each axis. + */ + options: Record; + /** + * The zoom data for each axis + * @default [] + */ + zoomData: ZoomData[]; + /** + * Set the zoom data for each axis. + * @param {ZoomData[]} zoomData The new zoom data. + */ + setZoomData: (zoomData: ZoomData[] | ((zoomData: ZoomData[]) => ZoomData[])) => void; + /** + * Whether the user is currently interacting with the chart. + * This is useful to prevent animations from running while the user is interacting. + */ + isInteracting: boolean; + /** + * Set the interaction state of the chart. + * @param {boolean} isInteracting The new interaction state. + */ + setIsInteracting: (isInteracting: boolean) => void; +}; + export type ZoomOptions = { /** * The starting percentage of the zoom range. In the range of 0 to 100. @@ -64,8 +118,16 @@ export type ZoomData = { }; export type ZoomProps = { + /** + * The list of zoom data related to each axis. + */ zoom?: ZoomData[]; - onZoomChange?: (zoom: ZoomData[]) => void; + /** + * Callback fired when the zoom has changed. + * + * @param {ZoomData[]} zoomData Updated zoom data. + */ + onZoomChange?: (zoomData: ZoomData[] | ((zoomData: ZoomData[]) => ZoomData[])) => void; }; export type DefaultizedZoomOptions = Required & { diff --git a/packages/x-charts-pro/src/context/ZoomProvider/ZoomContext.ts b/packages/x-charts-pro/src/context/ZoomProvider/ZoomContext.ts index 88edfe25d42ce..a4cecd94d2286 100644 --- a/packages/x-charts-pro/src/context/ZoomProvider/ZoomContext.ts +++ b/packages/x-charts-pro/src/context/ZoomProvider/ZoomContext.ts @@ -1,16 +1,6 @@ import * as React from 'react'; -import { AxisId, Initializable } from '@mui/x-charts/internals'; -import { DefaultizedZoomOptions, ZoomData } from './Zoom.types'; - -export type ZoomState = { - isZoomEnabled: boolean; - isPanEnabled: boolean; - options: Record; - zoomData: ZoomData[]; - setZoomData: (zoomData: ZoomData[]) => void; - isInteracting: boolean; - setIsInteracting: (isInteracting: boolean) => void; -}; +import { Initializable } from '@mui/x-charts/internals'; +import { ZoomState } from './Zoom.types'; export const ZoomContext = React.createContext>({ isInitialized: false, diff --git a/packages/x-charts-pro/src/context/ZoomProvider/ZoomProvider.tsx b/packages/x-charts-pro/src/context/ZoomProvider/ZoomProvider.tsx index 9e3cff88622d1..6c55b67693187 100644 --- a/packages/x-charts-pro/src/context/ZoomProvider/ZoomProvider.tsx +++ b/packages/x-charts-pro/src/context/ZoomProvider/ZoomProvider.tsx @@ -1,25 +1,12 @@ import * as React from 'react'; -import { ZoomContext, ZoomState } from './ZoomContext'; +import useControlled from '@mui/utils/useControlled'; +import { Initializable } from '@mui/x-charts/internals'; +import { ZoomContext } from './ZoomContext'; import { defaultizeZoom } from './defaultizeZoom'; -import { AxisConfigForZoom, ZoomData } from './Zoom.types'; +import { ZoomData, ZoomProviderProps, ZoomState } from './Zoom.types'; +import { initializeZoomData } from './initializeZoomData'; -type ZoomProviderProps = { - children: React.ReactNode; - /** - * The configuration of the x-axes. - * If not provided, a default axis config is used. - * An array of [[AxisConfig]] objects. - */ - xAxis?: AxisConfigForZoom[]; - /** - * The configuration of the y-axes. - * If not provided, a default axis config is used. - * An array of [[AxisConfig]] objects. - */ - yAxis?: AxisConfigForZoom[]; -}; - -export function ZoomProvider({ children, xAxis, yAxis }: ZoomProviderProps) { +export function ZoomProvider({ children, xAxis, yAxis, zoom, onZoomChange }: ZoomProviderProps) { const [isInteracting, setIsInteracting] = React.useState(false); const options = React.useMemo( @@ -34,28 +21,40 @@ export function ZoomProvider({ children, xAxis, yAxis }: ZoomProviderProps) { [xAxis, yAxis], ); - const [zoomData, setZoomData] = React.useState(() => - Object.values(options).map(({ axisId, minStart: start, maxEnd: end }) => ({ - axisId, - start, - end, - })), + // Default zoom data is initialized only once when uncontrolled. If the user changes the options + // after the initial render, the zoom data will not be updated until the next zoom interaction. + // This is required to avoid warnings about controlled/uncontrolled components. + const defaultZoomData = React.useRef(initializeZoomData(options)); + + const [zoomData, setZoomData] = useControlled({ + controlled: zoom, + default: defaultZoomData.current, + name: 'ZoomProvider', + state: 'zoom', + }); + + const setZoomDataCallback = React.useCallback( + (newZoomData) => { + setZoomData(newZoomData); + onZoomChange?.(newZoomData); + }, + [setZoomData, onZoomChange], ); - const value = React.useMemo( + const value = React.useMemo>( () => ({ isInitialized: true, data: { - isZoomEnabled: zoomData.length > 0, + isZoomEnabled: Object.keys(options).length > 0, isPanEnabled: isPanEnabled(options), options, zoomData, - setZoomData, + setZoomData: setZoomDataCallback, isInteracting, setIsInteracting, }, }), - [zoomData, setZoomData, isInteracting, setIsInteracting, options], + [zoomData, isInteracting, setIsInteracting, options, setZoomDataCallback], ); return {children}; diff --git a/packages/x-charts-pro/src/context/ZoomProvider/ZoomSetup.ts b/packages/x-charts-pro/src/context/ZoomProvider/ZoomSetup.ts index 072baa872d421..775b173dabdf1 100644 --- a/packages/x-charts-pro/src/context/ZoomProvider/ZoomSetup.ts +++ b/packages/x-charts-pro/src/context/ZoomProvider/ZoomSetup.ts @@ -1,6 +1,13 @@ import { useSetupPan } from './useSetupPan'; import { useSetupZoom } from './useSetupZoom'; +/** + * Sets up the zoom functionality if using composition or a custom chart. + * + * Simply add this component at the same level as the chart component to enable zooming and panning. + * + * See: [Composition](https://mui.com/x/react-charts/composition/) + */ function ZoomSetup() { useSetupZoom(); useSetupPan(); diff --git a/packages/x-charts-pro/src/context/ZoomProvider/initializeZoomData.ts b/packages/x-charts-pro/src/context/ZoomProvider/initializeZoomData.ts new file mode 100644 index 0000000000000..454865c586486 --- /dev/null +++ b/packages/x-charts-pro/src/context/ZoomProvider/initializeZoomData.ts @@ -0,0 +1,11 @@ +import { ZoomState } from './Zoom.types'; + +// This function is used to initialize the zoom data when it is not provided by the user. +// It is helpful to avoid the need to provide the possibly auto-generated id for each axis. +export const initializeZoomData = (options: ZoomState['options']) => { + return Object.values(options).map(({ axisId, minStart: start, maxEnd: end }) => ({ + axisId, + start, + end, + })); +}; diff --git a/packages/x-charts-pro/src/context/ZoomProvider/useSetupZoom.ts b/packages/x-charts-pro/src/context/ZoomProvider/useSetupZoom.ts index 876f6bd873a2a..c73900cd34bab 100644 --- a/packages/x-charts-pro/src/context/ZoomProvider/useSetupZoom.ts +++ b/packages/x-charts-pro/src/context/ZoomProvider/useSetupZoom.ts @@ -55,7 +55,7 @@ const zoomAtPoint = ( }; export const useSetupZoom = () => { - const { zoomData, setZoomData, isZoomEnabled, options, setIsInteracting } = useZoom(); + const { setZoomData, isZoomEnabled, options, setIsInteracting } = useZoom(); const drawingArea = useDrawingArea(); const svgRef = useSvgRef(); @@ -91,24 +91,28 @@ export const useSetupZoom = () => { setIsInteracting(false); }, 166); - const newZoomData = zoomData.map((zoom) => { - const option = options[zoom.axisId]; - const centerRatio = - option.axisDirection === 'x' - ? getHorizontalCenterRatio(point, drawingArea) - : getVerticalCenterRatio(point, drawingArea); + setZoomData((prevZoomData) => { + return prevZoomData.map((zoom) => { + const option = options[zoom.axisId]; + if (!option) { + return zoom; + } - const { scaleRatio, isZoomIn } = getWheelScaleRatio(event, option.step); - const [newMinRange, newMaxRange] = zoomAtPoint(centerRatio, scaleRatio, zoom, option); + const centerRatio = + option.axisDirection === 'x' + ? getHorizontalCenterRatio(point, drawingArea) + : getVerticalCenterRatio(point, drawingArea); - if (!isSpanValid(newMinRange, newMaxRange, isZoomIn, option)) { - return zoom; - } + const { scaleRatio, isZoomIn } = getWheelScaleRatio(event, option.step); + const [newMinRange, newMaxRange] = zoomAtPoint(centerRatio, scaleRatio, zoom, option); - return { axisId: zoom.axisId, start: newMinRange, end: newMaxRange }; - }); + if (!isSpanValid(newMinRange, newMaxRange, isZoomIn, option)) { + return zoom; + } - setZoomData(newZoomData); + return { axisId: zoom.axisId, start: newMinRange, end: newMaxRange }; + }); + }); }; function pointerDownHandler(event: PointerEvent) { @@ -134,39 +138,41 @@ export const useSetupZoom = () => { const firstEvent = eventCacheRef.current[0]; const curDiff = getDiff(eventCacheRef.current); - const newZoomData = zoomData.map((zoom) => { - const option = options[zoom.axisId]; - - const { scaleRatio, isZoomIn } = getPinchScaleRatio( - curDiff, - eventPrevDiff.current, - option.step, - ); - - // If the scale ratio is 0, it means the pinch gesture is not valid. - if (scaleRatio === 0) { - eventPrevDiff.current = curDiff; - return zoom; - } - - const point = getSVGPoint(element, firstEvent); - - const centerRatio = - option.axisDirection === 'x' - ? getHorizontalCenterRatio(point, drawingArea) - : getVerticalCenterRatio(point, drawingArea); - - const [newMinRange, newMaxRange] = zoomAtPoint(centerRatio, scaleRatio, zoom, option); - - if (!isSpanValid(newMinRange, newMaxRange, isZoomIn, option)) { - return zoom; - } - - return { axisId: zoom.axisId, start: newMinRange, end: newMaxRange }; + setZoomData((prevZoomData) => { + const newZoomData = prevZoomData.map((zoom) => { + const option = options[zoom.axisId]; + if (!option) { + return zoom; + } + + const { scaleRatio, isZoomIn } = getPinchScaleRatio( + curDiff, + eventPrevDiff.current, + option.step, + ); + + // If the scale ratio is 0, it means the pinch gesture is not valid. + if (scaleRatio === 0) { + return zoom; + } + + const point = getSVGPoint(element, firstEvent); + + const centerRatio = + option.axisDirection === 'x' + ? getHorizontalCenterRatio(point, drawingArea) + : getVerticalCenterRatio(point, drawingArea); + + const [newMinRange, newMaxRange] = zoomAtPoint(centerRatio, scaleRatio, zoom, option); + + if (!isSpanValid(newMinRange, newMaxRange, isZoomIn, option)) { + return zoom; + } + return { axisId: zoom.axisId, start: newMinRange, end: newMaxRange }; + }); + eventPrevDiff.current = curDiff; + return newZoomData; }); - - eventPrevDiff.current = curDiff; - setZoomData(newZoomData); } function pointerUpHandler(event: PointerEvent) { @@ -210,7 +216,7 @@ export const useSetupZoom = () => { clearTimeout(interactionTimeoutRef.current); } }; - }, [svgRef, setZoomData, zoomData, drawingArea, isZoomEnabled, options, setIsInteracting]); + }, [svgRef, setZoomData, drawingArea, isZoomEnabled, options, setIsInteracting]); }; /** diff --git a/packages/x-charts-pro/src/context/ZoomProvider/useZoom.ts b/packages/x-charts-pro/src/context/ZoomProvider/useZoom.ts index 59bda96ca395a..af53a694c4482 100644 --- a/packages/x-charts-pro/src/context/ZoomProvider/useZoom.ts +++ b/packages/x-charts-pro/src/context/ZoomProvider/useZoom.ts @@ -1,7 +1,23 @@ import * as React from 'react'; import { ZoomContext } from './ZoomContext'; +import { ZoomState } from './Zoom.types'; + +/** + * Get access to the zoom state. + * + * @returns {ZoomState} The zoom state. + */ +export function useZoom(): ZoomState { + const { data, isInitialized } = React.useContext(ZoomContext); + + if (!isInitialized) { + throw new Error( + [ + 'MUI X: Could not find the zoom context.', + 'It looks like you rendered your component outside of a ChartsContainer parent component.', + ].join('\n'), + ); + } -export const useZoom = () => { - const { data } = React.useContext(ZoomContext); return data; -}; +} diff --git a/packages/x-charts-pro/src/context/index.ts b/packages/x-charts-pro/src/context/index.ts new file mode 100644 index 0000000000000..d0634736be8a8 --- /dev/null +++ b/packages/x-charts-pro/src/context/index.ts @@ -0,0 +1,4 @@ +// # Zoom & Pan +export type { ZoomOptions, ZoomData, ZoomProps, ZoomState } from './ZoomProvider/Zoom.types'; +export * from './ZoomProvider/useZoom'; +export * from './ZoomProvider/ZoomSetup'; diff --git a/packages/x-charts-pro/src/index.ts b/packages/x-charts-pro/src/index.ts index eade0525b9407..34c2369623813 100644 --- a/packages/x-charts-pro/src/index.ts +++ b/packages/x-charts-pro/src/index.ts @@ -33,3 +33,6 @@ export * from './ChartContainerPro'; export * from './ScatterChartPro'; export * from './BarChartPro'; export * from './LineChartPro'; + +// Pro context +export * from './context'; diff --git a/packages/x-charts-pro/src/typeOverloads/modules.ts b/packages/x-charts-pro/src/typeOverloads/modules.ts index 5dae3f83d41ed..78c705633fce2 100644 --- a/packages/x-charts-pro/src/typeOverloads/modules.ts +++ b/packages/x-charts-pro/src/typeOverloads/modules.ts @@ -4,7 +4,7 @@ import { HeatmapSeriesType, DefaultizedHeatmapSeriesType, } from '../models/seriesType/heatmap'; -import { ZoomOptions } from '../context/ZoomProvider/Zoom.types'; +import { ZoomOptions } from '../context/ZoomProvider'; declare module '@mui/x-charts/internals' { interface ChartsSeriesConfig { diff --git a/scripts/x-charts-pro.exports.json b/scripts/x-charts-pro.exports.json index bec72c5ad30ab..7da58b547b024 100644 --- a/scripts/x-charts-pro.exports.json +++ b/scripts/x-charts-pro.exports.json @@ -321,6 +321,12 @@ { "name": "useYColorScale", "kind": "Function" }, { "name": "useYScale", "kind": "Function" }, { "name": "useZColorScale", "kind": "Function" }, + { "name": "useZoom", "kind": "Function" }, { "name": "ZAxisContextProvider", "kind": "Function" }, - { "name": "ZAxisContextProviderProps", "kind": "TypeAlias" } + { "name": "ZAxisContextProviderProps", "kind": "TypeAlias" }, + { "name": "ZoomData", "kind": "TypeAlias" }, + { "name": "ZoomOptions", "kind": "TypeAlias" }, + { "name": "ZoomProps", "kind": "TypeAlias" }, + { "name": "ZoomSetup", "kind": "Function" }, + { "name": "ZoomState", "kind": "TypeAlias" } ]