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

[docs] Explain how to clip plots with composition #12679

Merged
merged 5 commits into from
Apr 5, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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>
);
}
34 changes: 34 additions & 0 deletions docs/data/charts/composition/composition.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,40 @@ 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 force components to strictly stay inside the drawing area, use the `<ChartsClipPath id={clipPathId} />`.
This component defines the rectangle of the drawing area.

Then you can clip any part of the chart by wrapping it into ``<g clipPath={`url(#${clipPathId})`}>``.
alexfauquette marked this conversation as resolved.
Show resolved Hide resolved

```jsx
<ChartContainer>
<g clipPath={`url(#${clipPathId})`}>
// The ploting to clip in the drawing area.
alexfauquette marked this conversation as resolved.
Show resolved Hide resolved
<ScatterPlot />
<LinePlot />
</g>
<ChartsClipPath id={clipPathId} /> // Defines the clip path of the drawing area.
</ChartContainer>
```

In the following demo you can clip or not scatter and line plots.
Notice that the mark elements of the line are placed outside of the clip, and are rendered on top of the axes.
alexfauquette marked this conversation as resolved.
Show resolved Hide resolved

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

:::warning
Notice how the id is generated in the demo.

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

If the id was simply `const clipPathId = "my-id";` a page with two charts would have two components with the same `id`, leading to unexpected behavior.
alexfauquette marked this conversation as resolved.
Show resolved Hide resolved
:::

### Axis

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