Skip to content

Commit

Permalink
[charts] Allow the creation of custom HTML components using charts da…
Browse files Browse the repository at this point in the history
…ta (mui#15511)

Signed-off-by: Jose C Quintas Jr <juniorquintas@gmail.com>
Signed-off-by: Alexandre Fauquette <45398769+alexfauquette@users.noreply.github.com>
Co-authored-by: Alexandre Fauquette <45398769+alexfauquette@users.noreply.github.com>
  • Loading branch information
2 people authored and LukasTy committed Dec 19, 2024
1 parent 467dd66 commit d5ae6f3
Show file tree
Hide file tree
Showing 26 changed files with 503 additions and 143 deletions.
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>
19 changes: 19 additions & 0 deletions docs/data/charts/components/components.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,3 +76,22 @@ By using `invert`, the value associated with the current mouse coordinate `y` ca
```

{{"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.
This creates an html `table` element, which handles long series names better than the default legend.

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

:::warning
Note that the HTML components are not part of the SVG hierarchy.
Hence, they should be:

- Outside the `<ChartsSurface />` component to avoid mixing HTAM and SVG.
- Inside the `<ChartDataProvider />` component to get access to the data.

:::
74 changes: 69 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,75 @@ packageName: '@mui/x-charts'
## 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 they won't all be listed here.
You can even [create your own components](/x/react-charts/components/).

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

0 comments on commit d5ae6f3

Please sign in to comment.