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 the creation of custom HTML components using charts data #15511

Merged
merged 29 commits into from
Nov 27, 2024
Merged
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
0c9634b
direct svg
JCQuintas Nov 20, 2024
468c05e
fix svgref
JCQuintas Nov 20, 2024
4cf057d
Revert "fix svgref"
JCQuintas Nov 20, 2024
db2de3c
Remove SvgRefProvider
JCQuintas Nov 21, 2024
7de7919
Merge commit 'aff210aaee26b66723a538c13cf2945b7c1ba219' into remove-w…
JCQuintas Nov 21, 2024
f00e50a
formatting
JCQuintas Nov 21, 2024
42591eb
fix test
JCQuintas Nov 21, 2024
5c89ab6
force mutable ref
JCQuintas Nov 21, 2024
f6c3fc8
fix test
JCQuintas Nov 21, 2024
377ef35
Merge branch 'master' into remove-wrapper-div
JCQuintas Nov 21, 2024
ba2f70e
Improve component documentation
JCQuintas Nov 22, 2024
0eb3494
add docs and demo
JCQuintas Nov 22, 2024
e29d6b7
Merge branch 'master' into remove-wrapper-div
JCQuintas Nov 22, 2024
23aee93
add more text for visual testing
JCQuintas Nov 22, 2024
7e03c81
fix scripts
JCQuintas Nov 22, 2024
a210c25
Apply suggestions from code review
JCQuintas Nov 25, 2024
8d77b27
Merge commit '80a1e0310ac5729fc08eb0d0ec1c2ec3adf8c96f' into remove-w…
JCQuintas Nov 26, 2024
a04bf05
fix tooltips
JCQuintas Nov 26, 2024
aa43b39
return outside-chart
JCQuintas Nov 26, 2024
d730a1d
docs suggestions
JCQuintas Nov 26, 2024
e8abd00
remove useless wrapper
JCQuintas Nov 26, 2024
5fe5752
Add error in dev mode
JCQuintas Nov 26, 2024
6d8c786
revert voronoi handler changes
JCQuintas Nov 27, 2024
77f988e
Apply suggestions from code review
JCQuintas Nov 27, 2024
42fc45e
Apply suggestions from code review
JCQuintas Nov 27, 2024
4f32d59
fix linting
JCQuintas Nov 27, 2024
9d4190f
Merge branch 'master' into remove-wrapper-div
JCQuintas Nov 27, 2024
dc57646
Update docs/data/charts/components/components.md
alexfauquette Nov 27, 2024
e55b66e
suggestion
JCQuintas Nov 27, 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
66 changes: 66 additions & 0 deletions docs/data/charts/components/HtmlLegend.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import * as React from 'react';
import Box from '@mui/material/Box';
import { unstable_useBarSeries } from '@mui/x-charts/hooks';
import { ChartDataProvider } from '@mui/x-charts/context';
import { ChartsSurface } from '@mui/x-charts/ChartsSurface';
import { BarPlot } from '@mui/x-charts/BarChart';
import { ChartsXAxis } from '@mui/x-charts/ChartsXAxis';
import { ChartsYAxis } from '@mui/x-charts/ChartsYAxis';

function MyCustomLegend() {
const s = unstable_useBarSeries();
return (
<table
style={{
marginLeft: 40,
marginRight: 40,
}}
>
<tbody>
{Object.values(s?.series ?? []).map((v) => {
return (
<tr key={v.id}>
<td aria-hidden>
<div
style={{
background: v.color,
height: 10,
width: 10,
marginRight: 10,
flexShrink: 0,
borderRadius: 5,
}}
/>
</td>
<td>{`${v.label}`}</td>
</tr>
);
})}
</tbody>
</table>
);
}

const veryLongText =
"Second Series. You should always try to avoid long sentences. But oftentimes, it's not possible. So, we need to handle them gracefully. This is a very long sentence that should be fully readable.";

export default function HtmlLegend() {
return (
<Box sx={{ height: 400, display: 'flex', flexDirection: 'column' }}>
<ChartDataProvider
series={[
{ label: 'First Series', type: 'bar', data: [100, 200] },
{ label: veryLongText, type: 'bar', data: [45, 333] },
]}
xAxis={[{ data: ['A', 'B'], scaleType: 'band', id: 'x-axis' }]}
>
<ChartsSurface>
<BarPlot />
<ChartsXAxis position="bottom" axisId="x-axis" />
<ChartsYAxis position="left" />
</ChartsSurface>
<MyCustomLegend />
</ChartDataProvider>
</Box>
);
}
66 changes: 66 additions & 0 deletions docs/data/charts/components/HtmlLegend.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import * as React from 'react';
import Box from '@mui/material/Box';
import { unstable_useBarSeries } from '@mui/x-charts/hooks';
import { ChartDataProvider } from '@mui/x-charts/context';
import { ChartsSurface } from '@mui/x-charts/ChartsSurface';
import { BarPlot } from '@mui/x-charts/BarChart';
import { ChartsXAxis } from '@mui/x-charts/ChartsXAxis';
import { ChartsYAxis } from '@mui/x-charts/ChartsYAxis';

function MyCustomLegend() {
const s = unstable_useBarSeries();
return (
<table
style={{
marginLeft: 40,
marginRight: 40,
}}
>
<tbody>
{Object.values(s?.series ?? []).map((v) => {
return (
<tr key={v.id}>
<td aria-hidden>
<div
style={{
background: v.color,
height: 10,
width: 10,
marginRight: 10,
flexShrink: 0,
borderRadius: 5,
}}
/>
</td>
<td>{`${v.label}`}</td>
</tr>
);
})}
</tbody>
</table>
);
}

const veryLongText =
"Second Series. You should always try to avoid long sentences. But oftentimes, it's not possible. So, we need to handle them gracefully. This is a very long sentence that should be fully readable.";

export default function HtmlLegend() {
return (
<Box sx={{ height: 400, display: 'flex', flexDirection: 'column' }}>
<ChartDataProvider
series={[
{ label: 'First Series', type: 'bar', data: [100, 200] },
{ label: veryLongText, type: 'bar', data: [45, 333] },
]}
xAxis={[{ data: ['A', 'B'], scaleType: 'band', id: 'x-axis' }]}
>
<ChartsSurface>
<BarPlot />
<ChartsXAxis position="bottom" axisId="x-axis" />
<ChartsYAxis position="left" />
</ChartsSurface>
<MyCustomLegend />
</ChartDataProvider>
</Box>
);
}
14 changes: 14 additions & 0 deletions docs/data/charts/components/HtmlLegend.tsx.preview
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<ChartDataProvider
series={[
{ label: 'First Series', type: 'bar', data: [100, 200] },
{ label: veryLongText, type: 'bar', data: [45, 333] },
]}
xAxis={[{ data: ['A', 'B'], scaleType: 'band', id: 'x-axis' }]}
>
<ChartsSurface>
<BarPlot />
<ChartsXAxis position="bottom" axisId="x-axis" />
<ChartsYAxis position="left" />
</ChartsSurface>
<MyCustomLegend />
</ChartDataProvider>
17 changes: 17 additions & 0 deletions docs/data/charts/components/components.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,3 +76,20 @@
```

{{"demo": "ScaleDemo.js"}}

## HTML components

With the introduction of the `ChartDataProvider` in v8, the chart data can be accessed from any component.
This allows you to create HTML components that interact with the charts data.

In the next example, notice that `MyCustomLegend` component displays the series names and colors. You can expand the code to see how it works.
This creates an html `table` element, which handles long series names better than the default legend.
JCQuintas marked this conversation as resolved.
Show resolved Hide resolved

{{"demo": "HtmlLegend.js"}}

:::warning
Note that the HTML components are not part of the SVG hierarchy. Hence, they should be placed outside the `<ChartsSurface />` component,
but inside the `<ChartDataProvider />` component. Else they will not be visible.

Check warning on line 92 in docs/data/charts/components/components.md

View workflow job for this annotation

GitHub Actions / runner / vale

[vale] reported by reviewdog 🐶 [Google.Will] Avoid using 'will'. Raw Output: {"message": "[Google.Will] Avoid using 'will'.", "location": {"path": "docs/data/charts/components/components.md", "range": {"start": {"line": 92, "column": 61}}}, "severity": "WARNING"}
JCQuintas marked this conversation as resolved.
Show resolved Hide resolved

If you want to position an HTML component relative to the SVG, you can use the `useDrawingArea()` hook to get the SVG's position and size.
JCQuintas marked this conversation as resolved.
Show resolved Hide resolved
JCQuintas marked this conversation as resolved.
Show resolved Hide resolved
:::
73 changes: 68 additions & 5 deletions docs/data/charts/composition/composition.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
title: React Chart composition
productId: x-charts
githubLabel: 'component: charts'
components: ChartContainer, ChartContainerPro, ChartsGrid
components: ChartContainer, ChartContainerPro, ChartsGrid, ChartDataProvider, ChartsSurface
packageName: '@mui/x-charts'
---

Expand All @@ -13,11 +13,74 @@
## Overview

The `@mui/x-charts` follows an architecture based on context providers.
The overall idea is to pass your series and axes definitions to a single component: the `<ChartContainer />`.
This component transforms the data and makes it available to its children.
The overall idea is to pass your series and axes definitions to special components.
This component transforms the data and makes it available to its children, which can be composed.

Based on the data provided by the container, you can render some graphical elements with provided subcomponents, such as `<LinePlot />` or `<ChartsYAxis />`.
Or you can [create your own components](/x/react-charts/components/).
There are two main classes of components, which are used to create a chart.

### Structural components

These are used to define the chart's structure and data.

#### The Data Provider and Surface components

As the name suggests, the `ChartDataProvider` provides the data to the children components.
While the `ChartsSurface` renders the SVG elements.

```jsx
<ChartDataProvider
// The configuration of the chart
series={[{ type: 'bar', data: [100, 200] }]}
xAxis={[{ scaleType: 'band', data: ['A', 'B'] }]}
width={500}
height={300}
>
<ChartsSurface
// Ref needs to be directly on the ChartsSurface
ref={mySvgRef}
>
{children}
</ChartsSurface>
</ChartDataProvider>
```

:::info
The demos here are using the `ChartContainer` component.
To see demos using the separate `ChartDataProvider` and `ChartsSurface` components, check the [HTML components documentation](/x/react-charts/components/#html-components).
:::

#### The `ChartContainer` helper

This component is a composition of the two previous components.
It can be used instead of them when there is no need to customize anything outside the chart's graphical elements.

```jsx
<ChartContainer
// The configuration of the chart
series={[{ type: 'bar', data: [100, 200] }]}
xAxis={[{ scaleType: 'band', data: ['A', 'B'] }]}
width={500}
height={300}
// Ref is forwarded internally to the ChartsSurface
ref={mySvgRef}
>
{children}
</ChartContainer>
```

### Graphical components

These are any component that render the graphical elements of the chart.
They are the children of the **Structural components** shown above.
There are many of them, so we will not list them all here. You can even [create your own components](/x/react-charts/components/).

Check warning on line 75 in docs/data/charts/composition/composition.md

View workflow job for this annotation

GitHub Actions / runner / vale

[vale] reported by reviewdog 🐶 [Google.We] Try to avoid using first-person plural like 'we'. Raw Output: {"message": "[Google.We] Try to avoid using first-person plural like 'we'.", "location": {"path": "docs/data/charts/composition/composition.md", "range": {"start": {"line": 75, "column": 28}}}, "severity": "WARNING"}

Check warning on line 75 in docs/data/charts/composition/composition.md

View workflow job for this annotation

GitHub Actions / runner / vale

[vale] reported by reviewdog 🐶 [Google.Will] Avoid using 'will'. Raw Output: {"message": "[Google.Will] Avoid using 'will'.", "location": {"path": "docs/data/charts/composition/composition.md", "range": {"start": {"line": 75, "column": 31}}}, "severity": "WARNING"}
JCQuintas marked this conversation as resolved.
Show resolved Hide resolved
JCQuintas marked this conversation as resolved.
Show resolved Hide resolved

Some examples of graphical components are:

- `LinePlot`
- `BarPlot`
- `ChartsXAxis`
- `ChartsLegend`
- `ChartsTooltip`

## Container options

Expand Down
4 changes: 4 additions & 0 deletions docs/data/chartsApiPages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,10 @@ const chartsApiPages: MuiPage[] = [
title: 'ChartContainerPro',
plan: 'pro',
},
{
pathname: '/x/api/charts/chart-data-provider',
title: 'ChartDataProvider',
},
{
pathname: '/x/api/charts/charts-axis',
title: 'ChartsAxis',
Expand Down
23 changes: 23 additions & 0 deletions docs/pages/x/api/charts/chart-data-provider.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import * as React from 'react';
import ApiPage from 'docs/src/modules/components/ApiPage';
import mapApiPageTranslations from 'docs/src/modules/utils/mapApiPageTranslations';
import jsonPageContent from './chart-data-provider.json';

export default function Page(props) {
const { descriptions, pageContent } = props;
return <ApiPage descriptions={descriptions} pageContent={pageContent} />;
}

Page.getInitialProps = () => {
const req = require.context(
'docsx/translations/api-docs/charts/chart-data-provider',
false,
/\.\/chart-data-provider.*.json$/,
);
const descriptions = mapApiPageTranslations(req);

return {
descriptions,
pageContent: jsonPageContent,
};
};
70 changes: 70 additions & 0 deletions docs/pages/x/api/charts/chart-data-provider.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
{
"props": {
"series": {
"type": { "name": "arrayOf", "description": "Array&lt;object&gt;" },
"required": true
},
"colors": {
"type": { "name": "union", "description": "Array&lt;string&gt;<br>&#124;&nbsp;func" },
"default": "blueberryTwilightPalette"
},
"dataset": { "type": { "name": "arrayOf", "description": "Array&lt;object&gt;" } },
"height": { "type": { "name": "number" } },
"highlightedItem": {
"type": {
"name": "shape",
"description": "{ dataIndex?: number, seriesId?: number<br>&#124;&nbsp;string }"
}
},
"margin": {
"type": {
"name": "shape",
"description": "{ bottom?: number, left?: number, right?: number, top?: number }"
},
"default": "object Depends on the charts type."
},
"onHighlightChange": {
"type": { "name": "func" },
"signature": {
"type": "function(highlightedItem: HighlightItemData | null) => void",
"describedArgs": ["highlightedItem"]
}
},
"plugins": { "type": { "name": "arrayOf", "description": "Array&lt;object&gt;" } },
"skipAnimation": { "type": { "name": "bool" } },
"width": { "type": { "name": "number" } },
"xAxis": {
"type": {
"name": "arrayOf",
"description": "Array&lt;{ classes?: object, colorMap?: { colors: Array&lt;string&gt;, type: 'ordinal', unknownColor?: string, values?: Array&lt;Date<br>&#124;&nbsp;number<br>&#124;&nbsp;string&gt; }<br>&#124;&nbsp;{ color: Array&lt;string&gt;<br>&#124;&nbsp;func, max?: Date<br>&#124;&nbsp;number, min?: Date<br>&#124;&nbsp;number, type: 'continuous' }<br>&#124;&nbsp;{ colors: Array&lt;string&gt;, thresholds: Array&lt;Date<br>&#124;&nbsp;number&gt;, type: 'piecewise' }, data?: array, dataKey?: string, disableLine?: bool, disableTicks?: bool, domainLimit?: 'nice'<br>&#124;&nbsp;'strict'<br>&#124;&nbsp;func, fill?: string, hideTooltip?: bool, id?: number<br>&#124;&nbsp;string, label?: string, labelFontSize?: number, labelStyle?: object, max?: Date<br>&#124;&nbsp;number, min?: Date<br>&#124;&nbsp;number, position?: 'bottom'<br>&#124;&nbsp;'top', reverse?: bool, scaleType?: 'band'<br>&#124;&nbsp;'linear'<br>&#124;&nbsp;'log'<br>&#124;&nbsp;'point'<br>&#124;&nbsp;'pow'<br>&#124;&nbsp;'sqrt'<br>&#124;&nbsp;'time'<br>&#124;&nbsp;'utc', slotProps?: object, slots?: object, stroke?: string, sx?: Array&lt;func<br>&#124;&nbsp;object<br>&#124;&nbsp;bool&gt;<br>&#124;&nbsp;func<br>&#124;&nbsp;object, tickFontSize?: number, tickInterval?: 'auto'<br>&#124;&nbsp;array<br>&#124;&nbsp;func, tickLabelInterval?: 'auto'<br>&#124;&nbsp;func, tickLabelPlacement?: 'middle'<br>&#124;&nbsp;'tick', tickLabelStyle?: object, tickMaxStep?: number, tickMinStep?: number, tickNumber?: number, tickPlacement?: 'end'<br>&#124;&nbsp;'extremities'<br>&#124;&nbsp;'middle'<br>&#124;&nbsp;'start', tickSize?: number, valueFormatter?: func }&gt;"
}
},
"yAxis": {
"type": {
"name": "arrayOf",
"description": "Array&lt;{ classes?: object, colorMap?: { colors: Array&lt;string&gt;, type: 'ordinal', unknownColor?: string, values?: Array&lt;Date<br>&#124;&nbsp;number<br>&#124;&nbsp;string&gt; }<br>&#124;&nbsp;{ color: Array&lt;string&gt;<br>&#124;&nbsp;func, max?: Date<br>&#124;&nbsp;number, min?: Date<br>&#124;&nbsp;number, type: 'continuous' }<br>&#124;&nbsp;{ colors: Array&lt;string&gt;, thresholds: Array&lt;Date<br>&#124;&nbsp;number&gt;, type: 'piecewise' }, data?: array, dataKey?: string, disableLine?: bool, disableTicks?: bool, domainLimit?: 'nice'<br>&#124;&nbsp;'strict'<br>&#124;&nbsp;func, fill?: string, hideTooltip?: bool, id?: number<br>&#124;&nbsp;string, label?: string, labelFontSize?: number, labelStyle?: object, max?: Date<br>&#124;&nbsp;number, min?: Date<br>&#124;&nbsp;number, position?: 'left'<br>&#124;&nbsp;'right', reverse?: bool, scaleType?: 'band'<br>&#124;&nbsp;'linear'<br>&#124;&nbsp;'log'<br>&#124;&nbsp;'point'<br>&#124;&nbsp;'pow'<br>&#124;&nbsp;'sqrt'<br>&#124;&nbsp;'time'<br>&#124;&nbsp;'utc', slotProps?: object, slots?: object, stroke?: string, sx?: Array&lt;func<br>&#124;&nbsp;object<br>&#124;&nbsp;bool&gt;<br>&#124;&nbsp;func<br>&#124;&nbsp;object, tickFontSize?: number, tickInterval?: 'auto'<br>&#124;&nbsp;array<br>&#124;&nbsp;func, tickLabelInterval?: 'auto'<br>&#124;&nbsp;func, tickLabelPlacement?: 'middle'<br>&#124;&nbsp;'tick', tickLabelStyle?: object, tickMaxStep?: number, tickMinStep?: number, tickNumber?: number, tickPlacement?: 'end'<br>&#124;&nbsp;'extremities'<br>&#124;&nbsp;'middle'<br>&#124;&nbsp;'start', tickSize?: number, valueFormatter?: func }&gt;"
}
},
"zAxis": {
"type": {
"name": "arrayOf",
"description": "Array&lt;{ colorMap?: { colors: Array&lt;string&gt;, type: 'ordinal', unknownColor?: string, values?: Array&lt;Date<br>&#124;&nbsp;number<br>&#124;&nbsp;string&gt; }<br>&#124;&nbsp;{ color: Array&lt;string&gt;<br>&#124;&nbsp;func, max?: Date<br>&#124;&nbsp;number, min?: Date<br>&#124;&nbsp;number, type: 'continuous' }<br>&#124;&nbsp;{ colors: Array&lt;string&gt;, thresholds: Array&lt;Date<br>&#124;&nbsp;number&gt;, type: 'piecewise' }, data?: array, dataKey?: string, id?: string, max?: number, min?: number }&gt;"
}
}
},
"name": "ChartDataProvider",
"imports": [
"import { ChartDataProvider } from '@mui/x-charts/context';",
"import { ChartDataProvider } from '@mui/x-charts';",
"import { ChartDataProvider } from '@mui/x-charts-pro';"
],
"classes": [],
"spread": false,
"themeDefaultProps": false,
"muiName": "MuiChartDataProvider",
"forwardsRefTo": "SVGSVGElement",
"filename": "/packages/x-charts/src/context/ChartDataProvider/ChartDataProvider.tsx",
"inheritance": null,
"demos": "<ul><li><a href=\"/x/react-charts/composition/\">Chart composition</a></li></ul>",
"cssComponent": false
}
2 changes: 1 addition & 1 deletion docs/pages/x/api/charts/charts-surface.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,6 @@
"muiName": "MuiChartsSurface",
"filename": "/packages/x-charts/src/ChartsSurface/ChartsSurface.tsx",
"inheritance": null,
"demos": "<ul><li><a href=\"/x/react-charts/components/\">Charts - Custom components</a></li></ul>",
"demos": "<ul><li><a href=\"/x/react-charts/components/\">Charts - Custom components</a></li>\n<li><a href=\"/x/react-charts/composition/\">Chart composition</a></li></ul>",
"cssComponent": false
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"componentDescription": "",
"componentDescription": "It sets up the data providers as well as the `<svg>` for the chart.\n\nThis is a combination of both the `ChartDataProvider` and `ChartsSurface` components.",
"propDescriptions": {
"colors": { "description": "Color palette used to colorize multiple series." },
"dataset": {
Expand Down
Loading
Loading