Skip to content

Commit

Permalink
[docs] Explain how to clip plots with composition (#12679)
Browse files Browse the repository at this point in the history
Signed-off-by: Alexandre Fauquette <45398769+alexfauquette@users.noreply.github.com>
Co-authored-by: Michel Engelen <32863416+michelengelen@users.noreply.github.com>
Co-authored-by: Jose C Quintas Jr <juniorquintas@gmail.com>
  • Loading branch information
3 people committed Apr 5, 2024
1 parent d69feaf commit 00b0415
Show file tree
Hide file tree
Showing 3 changed files with 245 additions and 0 deletions.
103 changes: 103 additions & 0 deletions docs/data/charts/composition/LimitOverflow.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import * as React from 'react';
import Slider from '@mui/material/Slider';
import Box from '@mui/material/Box';
import FormControlLabel from '@mui/material/FormControlLabel';
import Checkbox from '@mui/material/Checkbox';
import useId from '@mui/utils/useId';

import { ResponsiveChartContainer } from '@mui/x-charts/ResponsiveChartContainer';
import { ScatterPlot } from '@mui/x-charts/ScatterChart';
import { LinePlot, MarkPlot } from '@mui/x-charts/LineChart';
import { ChartsClipPath } from '@mui/x-charts/ChartsClipPath';
import { ChartsXAxis } from '@mui/x-charts/ChartsXAxis';
import { ChartsYAxis } from '@mui/x-charts/ChartsYAxis';
import { ChartsGrid } from '@mui/x-charts/ChartsGrid';

import { Chance } from 'chance';

const chance = new Chance(42);

const data = Array.from({ length: 100 }, () => ({
x: chance.floating({ min: -25, max: 25 }),
y: chance.floating({ min: -25, max: 25 }),
})).map((d, index) => ({ ...d, id: index }));

const minDistance = 10;

export default function LimitOverflow() {
const [isLimited, setIsLimited] = React.useState(false);
const [xLimits, setXLimites] = React.useState([-20, 20]);

const id = useId();
const clipPathId = `${id}-clip-path`;

const handleChange = (event, newValue, activeThumb) => {
if (!Array.isArray(newValue)) {
return;
}

if (newValue[1] - newValue[0] < minDistance) {
if (activeThumb === 0) {
const clamped = Math.min(newValue[0], 100 - minDistance);
setXLimites([clamped, clamped + minDistance]);
} else {
const clamped = Math.max(newValue[1], minDistance);
setXLimites([clamped - minDistance, clamped]);
}
} else {
setXLimites(newValue);
}
};

return (
<Box sx={{ width: '100%', maxWidth: 500 }}>
<FormControlLabel
checked={isLimited}
control={
<Checkbox onChange={(event) => setIsLimited(event.target.checked)} />
}
label="Clip the plot"
labelPlacement="end"
/>
<ResponsiveChartContainer
xAxis={[
{
label: 'x',
min: xLimits[0],
max: xLimits[1],
data: [-30, -25, -20, -15, -10, -5, 0, 5, 10, 15, 20, 25],
},
]}
series={[
{ type: 'scatter', data, markerSize: 8 },
{
type: 'line',
data: [10, 13, 12, 5, -6, -3, 4, 20, 18, 17, 12, 11],
showMark: true,
},
]}
height={300}
margin={{ top: 10 }}
>
<ChartsGrid vertical horizontal />
<g clipPath={`url(#${clipPathId})`}>
<ScatterPlot />
<LinePlot />
</g>
<ChartsXAxis />
<ChartsYAxis />
<MarkPlot />
{isLimited && <ChartsClipPath id={clipPathId} />}
</ResponsiveChartContainer>

<Slider
value={xLimits}
onChange={handleChange}
valueLabelDisplay="auto"
min={-40}
max={40}
sx={{ mt: 2 }}
/>
</Box>
);
}
107 changes: 107 additions & 0 deletions docs/data/charts/composition/LimitOverflow.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import * as React from 'react';
import Slider from '@mui/material/Slider';
import Box from '@mui/material/Box';
import FormControlLabel from '@mui/material/FormControlLabel';
import Checkbox from '@mui/material/Checkbox';
import useId from '@mui/utils/useId';

import { ResponsiveChartContainer } from '@mui/x-charts/ResponsiveChartContainer';
import { ScatterPlot } from '@mui/x-charts/ScatterChart';
import { LinePlot, MarkPlot } from '@mui/x-charts/LineChart';
import { ChartsClipPath } from '@mui/x-charts/ChartsClipPath';
import { ChartsXAxis } from '@mui/x-charts/ChartsXAxis';
import { ChartsYAxis } from '@mui/x-charts/ChartsYAxis';
import { ChartsGrid } from '@mui/x-charts/ChartsGrid';

import { Chance } from 'chance';

const chance = new Chance(42);

const data = Array.from({ length: 100 }, () => ({
x: chance.floating({ min: -25, max: 25 }),
y: chance.floating({ min: -25, max: 25 }),
})).map((d, index) => ({ ...d, id: index }));

const minDistance = 10;

export default function LimitOverflow() {
const [isLimited, setIsLimited] = React.useState(false);
const [xLimits, setXLimites] = React.useState<number[]>([-20, 20]);

const id = useId();
const clipPathId = `${id}-clip-path`;

const handleChange = (
event: Event,
newValue: number | number[],
activeThumb: number,
) => {
if (!Array.isArray(newValue)) {
return;
}

if (newValue[1] - newValue[0] < minDistance) {
if (activeThumb === 0) {
const clamped = Math.min(newValue[0], 100 - minDistance);
setXLimites([clamped, clamped + minDistance]);
} else {
const clamped = Math.max(newValue[1], minDistance);
setXLimites([clamped - minDistance, clamped]);
}
} else {
setXLimites(newValue as number[]);
}
};

return (
<Box sx={{ width: '100%', maxWidth: 500 }}>
<FormControlLabel
checked={isLimited}
control={
<Checkbox onChange={(event) => setIsLimited(event.target.checked)} />
}
label="Clip the plot"
labelPlacement="end"
/>
<ResponsiveChartContainer
xAxis={[
{
label: 'x',
min: xLimits[0],
max: xLimits[1],
data: [-30, -25, -20, -15, -10, -5, 0, 5, 10, 15, 20, 25],
},
]}
series={[
{ type: 'scatter', data, markerSize: 8 },
{
type: 'line',
data: [10, 13, 12, 5, -6, -3, 4, 20, 18, 17, 12, 11],
showMark: true,
},
]}
height={300}
margin={{ top: 10 }}
>
<ChartsGrid vertical horizontal />
<g clipPath={`url(#${clipPathId})`}>
<ScatterPlot />
<LinePlot />
</g>
<ChartsXAxis />
<ChartsYAxis />
<MarkPlot />
{isLimited && <ChartsClipPath id={clipPathId} />}
</ResponsiveChartContainer>

<Slider
value={xLimits}
onChange={handleChange}
valueLabelDisplay="auto"
min={-40}
max={40}
sx={{ mt: 2 }}
/>
</Box>
);
}
35 changes: 35 additions & 0 deletions docs/data/charts/composition/composition.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,41 @@ The order of elements in composition is the only way to define how they overlap.

To display data, you have components named `<XxxPlot />` such as `<LinePlot />`, `<AreaPlot />`, `<MarkPlot />`, `<BarPlot />`, etc.

### Clipping

To ensure chart elements stay confined to the designated drawing area, use the `ChartsClipPath` component.
This component defines a rectangular clip path that acts as a boundary.

1. **Define the Clip Path**: Use `<ChartsClipPath id={clipPathId} />` to establish the clip path for the drawing area. `clipPathId` must be a unique identifier.
2. **Wrap the Chart**: Enclose the chart elements you want to clip within a `<g>` element. Set the `clipPath` attribute to `url(#${clipPathId})` to reference the previously defined clip path. Example: ``<g clipPath={`url(#${clipPathId})`}>``

```jsx
<ChartContainer>
<g clipPath={`url(#${clipPathId})`}>
// The plotting to clip in the drawing area.
<ScatterPlot />
<LinePlot />
</g>
<ChartsClipPath id={clipPathId} /> // Defines the clip path of the drawing area.
</ChartContainer>
```

The following demo allows you to toggle clipping for scatter and line plots.
Observe how line markers extend beyond the clip area, rendering on top of the axes.

{{"demo": "LimitOverflow.js" }}

:::warning
The provided demo is generating a unique ID with `useId()`.

```js
const id = useId();
const clipPathId = `${id}-clip-path`;
```

It's important to generate unique IDs for clip paths, especially when dealing with multiple charts on a page. Assigning a static ID like `"my-id"` would lead to conflicts.
:::

### Axis

To add axes, you can use `<ChartsXAxis />` and `<ChartsYAxis />` as defined in the [axis page](/x/react-charts/axis/#composition).
Expand Down

0 comments on commit 00b0415

Please sign in to comment.