Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[charts] Allow zoom on Y axis and add zoom options to configure zooming behaviour #13726

Merged
merged 40 commits into from
Jul 11, 2024
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
0eba786
Initial idea
JCQuintas Jul 4, 2024
2bdb126
Add panning
JCQuintas Jul 4, 2024
42145f9
separate min max
JCQuintas Jul 5, 2024
30b6224
create defaultize axis helper
JCQuintas Jul 5, 2024
5cf684d
create isdefined function
JCQuintas Jul 5, 2024
c0d62ba
Accept zoom options in axis config
JCQuintas Jul 5, 2024
07a1b2e
Multi axis zoom setup
JCQuintas Jul 8, 2024
004bd53
Pass zoomdata to computeValue
JCQuintas Jul 8, 2024
e71b432
Allow zooming on multiple axis
JCQuintas Jul 8, 2024
7adb9fc
Allow panning on y axis
JCQuintas Jul 8, 2024
9b53292
Fix overflowing dots on Y axis
JCQuintas Jul 8, 2024
4670f47
Adapt y axis to scale
JCQuintas Jul 8, 2024
959438e
fix issues with min/max props
JCQuintas Jul 8, 2024
7bb99cd
Merge commit '0f7c7c5765f16d14b3b48f2368fa0468f4396de0' into zoom-props
JCQuintas Jul 9, 2024
6da4127
use start/end instead of min/max
JCQuintas Jul 9, 2024
bac62f9
update typedoc
JCQuintas Jul 9, 2024
6500523
Throw on errors
JCQuintas Jul 9, 2024
b29939f
Add error tests
JCQuintas Jul 9, 2024
ad8e959
Export props
JCQuintas Jul 9, 2024
4a92b23
Update examples
JCQuintas Jul 9, 2024
3868cd2
Update previews
JCQuintas Jul 9, 2024
0a63a7a
Fix behaviors
JCQuintas Jul 9, 2024
fe2bcea
Fix zoomoptions name
JCQuintas Jul 9, 2024
5af0827
Fix code example
JCQuintas Jul 9, 2024
a73ab82
Fix demo using wrong scaleType
JCQuintas Jul 9, 2024
098b4ec
Fix tests
JCQuintas Jul 9, 2024
3abedd8
Remove error on min/max span
JCQuintas Jul 10, 2024
6a7d1bf
Update docs/data/charts/zoom-and-pan/zoom-and-pan.md
JCQuintas Jul 10, 2024
bcc6dfd
Update packages/x-charts-pro/src/context/ZoomProvider/Zoom.types.ts
JCQuintas Jul 10, 2024
01d02ad
Update packages/x-charts/src/ChartsYAxis/ChartsYAxis.tsx
JCQuintas Jul 10, 2024
7c27d6e
Improve defaultizeZoom
JCQuintas Jul 10, 2024
0fdb3f6
Props instead of restProps
JCQuintas Jul 10, 2024
ca9cf7f
Improve typing
JCQuintas Jul 10, 2024
3442bc6
Remove error handling
JCQuintas Jul 10, 2024
c58b5c2
Fix panning
JCQuintas Jul 10, 2024
415d850
Fix yaxis offset with linecap
JCQuintas Jul 11, 2024
85eb8c9
rename minStart/maxEnd props
JCQuintas Jul 11, 2024
d3789b5
Introduce AxisConfigExtension interface
JCQuintas Jul 11, 2024
cdc57b1
Fix example type
JCQuintas Jul 11, 2024
123b066
Merge commit '55aee56a7ed8e893a395bf36e715e662f9c1798e' into zoom-props
JCQuintas Jul 11, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 3 additions & 8 deletions packages/x-charts-pro/src/BarChartPro/BarChartPro.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,7 @@ import { ResponsiveChartContainerPro } from '../ResponsiveChartContainerPro';
import { ZoomSetup } from '../context/ZoomProvider/ZoomSetup';
import { useZoom } from '../context/ZoomProvider/useZoom';

export interface BarChartProProps extends BarChartProps {
/**
* If `true`, the chart will be zoomable.
*/
zoom?: boolean;
}
export interface BarChartProProps extends BarChartProps {}

/**
* Demos:
Expand All @@ -33,7 +28,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, ...restProps } = props;
const { ...restProps } = props;
const {
chartContainerProps,
barPlotProps,
Expand Down Expand Up @@ -62,7 +57,7 @@ const BarChartPro = React.forwardRef(function BarChartPro(props: BarChartProProp
<ChartsAxisHighlight {...axisHighlightProps} />
{!props.loading && <ChartsTooltip {...tooltipProps} />}
<ChartsClipPath {...clipPathProps} />
{zoom && <ZoomSetup />}
<ZoomSetup />
{children}
</ResponsiveChartContainerPro>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ const ChartContainerPro = React.forwardRef(function ChartContainer(
dataset={dataset}
seriesFormatters={seriesFormatters}
>
<ZoomProvider>
<ZoomProvider xAxis={xAxis} yAxis={yAxis}>
<CartesianContextProviderPro
xAxis={xAxis}
yAxis={yAxis}
Expand Down
11 changes: 3 additions & 8 deletions packages/x-charts-pro/src/LineChartPro/LineChartPro.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,7 @@ import { ResponsiveChartContainerPro } from '../ResponsiveChartContainerPro';
import { ZoomSetup } from '../context/ZoomProvider/ZoomSetup';
import { useZoom } from '../context/ZoomProvider/useZoom';

export interface LineChartProProps extends LineChartProps {
/**
* If `true`, the chart will be zoomable.
*/
zoom?: boolean;
}
export interface LineChartProProps extends LineChartProps {}

/**
* Demos:
Expand All @@ -38,7 +33,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, ...restProps } = props;
const { ...restProps } = props;
JCQuintas marked this conversation as resolved.
Show resolved Hide resolved
const {
chartContainerProps,
axisClickHandlerProps,
Expand Down Expand Up @@ -73,7 +68,7 @@ const LineChartPro = React.forwardRef(function LineChartPro(props: LineChartProP
<ChartsLegend {...legendProps} />
{!props.loading && <ChartsTooltip {...tooltipProps} />}
<ChartsClipPath {...clipPathProps} />
{zoom && <ZoomSetup />}
<ZoomSetup />
{children}
</ResponsiveChartContainerPro>
);
Expand Down
11 changes: 3 additions & 8 deletions packages/x-charts-pro/src/ScatterChartPro/ScatterChartPro.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,7 @@ import { useScatterChartProps } from '@mui/x-charts/internals';
import { ResponsiveChartContainerPro } from '../ResponsiveChartContainerPro';
import { ZoomSetup } from '../context/ZoomProvider/ZoomSetup';

export interface ScatterChartProProps extends ScatterChartProps {
/**
* If `true`, the chart will be zoomable.
*/
zoom?: boolean;
}
export interface ScatterChartProProps extends ScatterChartProps {}

/**
* Demos:
Expand All @@ -33,7 +28,7 @@ const ScatterChartPro = React.forwardRef(function ScatterChartPro(
props: ScatterChartProProps,
ref,
) {
const { zoom, ...restProps } = props;
const { ...restProps } = props;
const {
chartContainerProps,
zAxisProps,
Expand All @@ -58,7 +53,7 @@ const ScatterChartPro = React.forwardRef(function ScatterChartPro(
<ChartsLegend {...legendProps} />
<ChartsAxisHighlight {...axisHighlightProps} />
{!props.loading && <ChartsTooltip {...tooltipProps} />}
{zoom && <ZoomSetup />}
<ZoomSetup />
{children}
</ZAxisContextProvider>
</ResponsiveChartContainerPro>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,20 +24,20 @@ function CartesianContextProviderPro(props: CartesianContextProviderProProps) {

const formattedSeries = useSeries();
const drawingArea = useDrawingArea();
const { zoomRange } = useZoom();
const { zoomData } = useZoom();

const xAxis = React.useMemo(() => normalizeAxis(inXAxis, dataset, 'x'), [inXAxis, dataset]);

const yAxis = React.useMemo(() => normalizeAxis(inYAxis, dataset, 'y'), [inYAxis, dataset]);

const xValues = React.useMemo(
() => computeValue(drawingArea, formattedSeries, xAxis, xExtremumGetters, 'x', zoomRange),
[drawingArea, formattedSeries, xAxis, xExtremumGetters, zoomRange],
() => computeValue(drawingArea, formattedSeries, xAxis, xExtremumGetters, 'x', zoomData),
[drawingArea, formattedSeries, xAxis, xExtremumGetters, zoomData],
);

const yValues = React.useMemo(
() => computeValue(drawingArea, formattedSeries, yAxis, yExtremumGetters, 'y'),
[drawingArea, formattedSeries, yAxis, yExtremumGetters],
() => computeValue(drawingArea, formattedSeries, yAxis, yExtremumGetters, 'y', zoomData),
[drawingArea, formattedSeries, yAxis, yExtremumGetters, zoomData],
);

const value = React.useMemo(
Expand Down
71 changes: 71 additions & 0 deletions packages/x-charts-pro/src/context/ZoomProvider/Zoom.types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { AxisId } from '@mui/x-charts/internals';

export type ZoomOptions = {
/**
* The starting percentage of the zoom range. In the range of 0 to 100.
*
* @default 0
*/
start?: number;
JCQuintas marked this conversation as resolved.
Show resolved Hide resolved
/**
* The ending percentage of the zoom range. In the range of 0 to 100.
*
* @default 100
*/
end?: number;
/**
* The step size of the zoom range. Defines the granularity of the zoom.
*
* @default 5
*/
step?: number;
/**
* Restricts the minimal window size in a percentage. In the range of 0 to 100.
*
* If the window size is smaller than the minSpan, the window will be resized to the minSpan.
*
* @default 10
*/
minSpan?: number;
/**
* Restricts the maximal window size in a percentage. In the range of 0 to 100.
*
* If the window size is larger than the maxSpan, the window will be resized to the maxSpan.
*
* @default 100
*/
maxSpan?: number;
/**
* Set to `false` to disable panning. Useful when you want to pan programmatically,
* or to show only a specific section of the chart.
*
* @default true
*/
panning?: boolean;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We might want to make this more complex. Eg: panOnDrag|zoomOnWheel|zoomOnPinch: bool | 'shift' | 'ctrl' | 'alt' similar to what echarts does. https://echarts.apache.org/en/option.html#dataZoom-inside.moveOnMouseMove

};

export type ZoomData = {
/**
* The starting percentage of the zoom range. In the range of 0 to 100.
*
* @default 0
*/
start: number;
/**
* The ending percentage of the zoom range. In the range of 0 to 100.
*
* @default 100
*/
end: number;
/**
* The axis id that the zoom data belongs to.
*/
axisId: AxisId;
};

export type ZoomProps = {
zoom: ZoomData[];
onZoomChange: (zoom: ZoomData[]) => void;
};
JCQuintas marked this conversation as resolved.
Show resolved Hide resolved

export type DefaultizedZoomOptions = Required<ZoomOptions> & { axisId: AxisId; axis: 'x' | 'y' };
17 changes: 12 additions & 5 deletions packages/x-charts-pro/src/context/ZoomProvider/ZoomContext.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,25 @@
import * as React from 'react';
import { Initializable } from '@mui/x-charts/internals';
import { AxisId, Initializable } from '@mui/x-charts/internals';
import { DefaultizedZoomOptions, ZoomData } from './Zoom.types';

export type ZoomState = {
zoomRange: [number, number];
setZoomRange: (range: [number, number]) => void;
isZoomEnabled: boolean;
isPanEnabled: boolean;
options: Record<AxisId, DefaultizedZoomOptions>;
zoomData: ZoomData[];
setZoomData: (zoomData: ZoomData[]) => void;
isInteracting: boolean;
setIsInteracting: (isInteracting: boolean) => void;
};

export const ZoomContext = React.createContext<Initializable<ZoomState>>({
isInitialized: false,
data: {
zoomRange: [0, 100],
setZoomRange: () => {},
isZoomEnabled: false,
isPanEnabled: false,
options: {},
zoomData: [],
setZoomData: () => {},
isInteracting: false,
setIsInteracting: () => {},
},
Expand Down
55 changes: 49 additions & 6 deletions packages/x-charts-pro/src/context/ZoomProvider/ZoomProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,69 @@
import * as React from 'react';
import { ZoomContext } from './ZoomContext';
import { AxisConfig, ScaleName, ChartsXAxisProps, ChartsYAxisProps } from '@mui/x-charts';
import { cartesianProviderUtils } from '@mui/x-charts/internals';
import { ZoomContext, ZoomState } from './ZoomContext';
import { defaultizeZoom } from './defaultizeZoom';
import { ZoomData } from './Zoom.types';

const { defaultizeAxis } = cartesianProviderUtils;

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?: Partial<Pick<AxisConfig<ScaleName, any, ChartsXAxisProps>, 'id' | 'zoom'>>[];
JCQuintas marked this conversation as resolved.
Show resolved Hide resolved
/**
* The configuration of the y-axes.
* If not provided, a default axis config is used.
* An array of [[AxisConfig]] objects.
*/
yAxis?: Partial<Pick<AxisConfig<ScaleName, any, ChartsYAxisProps>, 'id' | 'zoom'>>[];
};

export function ZoomProvider({ children }: ZoomProviderProps) {
const [zoomRange, setZoomRange] = React.useState<[number, number]>([0, 100]);
export function ZoomProvider({ children, xAxis: inXAxis, yAxis: inYAxis }: ZoomProviderProps) {
const [isInteracting, setIsInteracting] = React.useState<boolean>(false);

const options = React.useMemo(
() =>
[
...(defaultizeZoom(defaultizeAxis(inXAxis, 'x'), 'x') ?? []),
...(defaultizeZoom(defaultizeAxis(inYAxis, 'y'), 'y') ?? []),
JCQuintas marked this conversation as resolved.
Show resolved Hide resolved
].reduce(
(acc, v) => {
acc[v.axisId] = v;
return acc;
},
{} as ZoomState['options'],
),
[inXAxis, inYAxis],
);

const [zoomData, setZoomData] = React.useState<ZoomData[]>(() =>
Object.values(options).map(({ axisId, start, end }) => ({ axisId, start, end })),
);
JCQuintas marked this conversation as resolved.
Show resolved Hide resolved

const value = React.useMemo(
() => ({
isInitialized: true,
data: {
zoomRange,
setZoomRange,
isZoomEnabled: zoomData.length > 0,
isPanEnabled: isPanEnabled(options),
options,
zoomData,
setZoomData,
isInteracting,
setIsInteracting,
},
}),
[zoomRange, setZoomRange, isInteracting, setIsInteracting],
[zoomData, setZoomData, isInteracting, setIsInteracting, options],
);

return <ZoomContext.Provider value={value}>{children}</ZoomContext.Provider>;
}

function isPanEnabled(options: Record<any, { panning?: boolean }>): boolean {
return Object.values(options).some((v) => v.panning) || false;
}
Loading