Skip to content

[charts-pro] Allow exporting a heatmap chart #17916

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

Merged
merged 8 commits into from
May 23, 2025
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
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
2 changes: 1 addition & 1 deletion docs/data/charts/export/ExportChartAsImage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ function ExportParamsSelector({
}

export default function ExportChartAsImage() {
const apiRef = React.useRef<ChartProApi>(undefined);
const apiRef = React.useRef<ChartProApi<'line'>>(undefined);
Copy link
Member Author

Choose a reason for hiding this comment

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

Unfortunately, I don't think we can keep the current API for ChartProApi. Either we create a new type, or we need to accept this breaking change as a bug fix.

It is a bug because invalid types are accepted. For example, the heatmap's apiRef doesn't contain a setZoomData property, but the type says it does.

It's unlikely a lot of people are using it as it was released in v8.1.0, but it's technically a breaking change if we don't flag it as a bug.

Another option would be to create another type to avoid breaking users and deprecate this one. What are your thoughts?

Copy link
Member

Choose a reason for hiding this comment

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

We could just go with the generic one but still allow users to narrow the types with the <'seriesType'> notation.

Copy link
Member Author

Choose a reason for hiding this comment

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

That doesn't work either. I suppose it's the same issue as trying to pass a RefObject<HTMLElement> to a HTMLDivElement.

Copy link
Member

Choose a reason for hiding this comment

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

I don't get it, if the issue is that you are passing a wide type but it expects a narrow type, just make the expectation wide, so it can accept a narrow one.

Copy link
Member Author

Choose a reason for hiding this comment

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

I think I managed to make it work.

I thought it was another case of this issue with HTMLDivElement and HTMLElement:

image

I've kept the ChartProApi<'line'> here because we should encourage users to use it, but it's usable without it as well:

image

Copy link
Member

Choose a reason for hiding this comment

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

nice


return (
<Stack width="100%" gap={2}>
Expand Down
4 changes: 2 additions & 2 deletions docs/data/charts/export/ExportCompositionNoSnap.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ function CustomChartWrapper({ children }: React.PropsWithChildren) {

return (
<div
ref={chartRootRef}
ref={chartRootRef as React.RefObject<HTMLDivElement>}
style={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}
>
{children}
Expand All @@ -25,7 +25,7 @@ function CustomChartWrapper({ children }: React.PropsWithChildren) {
}

export default function ExportCompositionNoSnap() {
const apiRef = React.useRef<ChartProApi>(undefined);
const apiRef = React.useRef<ChartProApi<'composition'>>(undefined);

return (
<Stack width="100%" sx={{ display: 'block' }}>
Expand Down
93 changes: 86 additions & 7 deletions docs/data/charts/export/PrintChart.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,19 @@
import * as React from 'react';
import Stack from '@mui/material/Stack';
import Button from '@mui/material/Button';
import Select from '@mui/material/Select';
import InputLabel from '@mui/material/InputLabel';
import FormControl from '@mui/material/FormControl';
import MenuItem from '@mui/material/MenuItem';
import { ScatterChartPro } from '@mui/x-charts-pro/ScatterChartPro';

import { BarChartPro } from '@mui/x-charts-pro/BarChartPro';
import { LineChartPro } from '@mui/x-charts-pro/LineChartPro';
import { Heatmap } from '@mui/x-charts-pro/Heatmap';
import { data } from './randomData';
import { heatmapData } from './heatmapData';

const series = [
const scatterSeries = [
{
label: 'Series A',
data: data.map((v) => ({ x: v.x1, y: v.y1, id: v.id })),
Expand All @@ -16,19 +24,90 @@ const series = [
},
];

const series = [
{ label: 'Series A', data: data.map((p) => p.y1) },
{ label: 'Series B', data: data.map((p) => p.y2) },
];

export default function PrintChart() {
const [chartType, setChartType] = React.useState('scatter');
const apiRef = React.useRef(undefined);

const handleChange = (event) => setChartType(event.target.value);

return (
<Stack width="100%" sx={{ display: 'block' }}>
<Button
onClick={() => apiRef.current.exportAsPrint()}
variant="contained"
<Stack
width="100%"
direction="row"
gap={2}
justifyContent="center"
sx={{ mb: 1 }}
>
Print
</Button>
<ScatterChartPro apiRef={apiRef} height={300} series={series} />
<FormControl sx={{ minWidth: 200 }}>
<InputLabel id="chart-type-label">Chart Type</InputLabel>
<Select
labelId="chart-type-label"
id="chart-type-select"
value={chartType}
label="Chart Type"
onChange={handleChange}
>
<MenuItem value="scatter">Scatter</MenuItem>
<MenuItem value="line">Line</MenuItem>
<MenuItem value="bar">Bar</MenuItem>
<MenuItem value="heatmap">Heatmap</MenuItem>
</Select>
</FormControl>
<Button onClick={() => apiRef.current.exportAsPrint()} variant="contained">
Print
</Button>
</Stack>
<Chart apiRef={apiRef} type={chartType} />
</Stack>
);
}

function Chart({ apiRef, type }) {
switch (type) {
case 'scatter':
return <ScatterChartPro apiRef={apiRef} height={300} series={scatterSeries} />;

case 'line':
return (
<LineChartPro
apiRef={apiRef}
height={300}
xAxis={[{ data: data.map((p) => p.x1).toSorted((a, b) => a - b) }]}
series={series}
/>
);

case 'bar':
return (
<BarChartPro
apiRef={apiRef}
height={300}
xAxis={[
{ data: data.map((p) => Math.round(p.x1)).toSorted((a, b) => a - b) },
]}
series={series}
/>
);

case 'heatmap':
return (
<Heatmap
apiRef={apiRef}
xAxis={[{ data: [1, 2, 3, 4] }]}
yAxis={[{ data: ['A', 'B', 'C', 'D', 'E'] }]}
series={[{ data: heatmapData }]}
height={300}
hideLegend={false}
/>
);

default:
throw new Error(`Unknown chart type: ${type}`);
}
}
103 changes: 96 additions & 7 deletions docs/data/charts/export/PrintChart.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,19 @@
import * as React from 'react';
import Stack from '@mui/material/Stack';
import Button from '@mui/material/Button';
import Select, { SelectChangeEvent } from '@mui/material/Select';
import InputLabel from '@mui/material/InputLabel';
import FormControl from '@mui/material/FormControl';
import MenuItem from '@mui/material/MenuItem';
import { ScatterChartPro } from '@mui/x-charts-pro/ScatterChartPro';
import { ChartProApi } from '@mui/x-charts-pro/ChartContainerPro';
import { BarChartPro } from '@mui/x-charts-pro/BarChartPro';
import { LineChartPro } from '@mui/x-charts-pro/LineChartPro';
import { Heatmap } from '@mui/x-charts-pro/Heatmap';
import { data } from './randomData';
import { heatmapData } from './heatmapData';

const series = [
const scatterSeries = [
{
label: 'Series A',
data: data.map((v) => ({ x: v.x1, y: v.y1, id: v.id })),
Expand All @@ -15,20 +23,101 @@ const series = [
data: data.map((v) => ({ x: v.x1, y: v.y2, id: v.id })),
},
];
const series = [
{ label: 'Series A', data: data.map((p) => p.y1) },
{ label: 'Series B', data: data.map((p) => p.y2) },
];

type ChartType = 'scatter' | 'line' | 'bar' | 'heatmap';

export default function PrintChart() {
const [chartType, setChartType] = React.useState<ChartType>('scatter');
const apiRef = React.useRef<ChartProApi>(undefined);

const handleChange = (event: SelectChangeEvent) =>
setChartType(event.target.value as ChartType);

return (
<Stack width="100%" sx={{ display: 'block' }}>
<Button
onClick={() => apiRef.current!.exportAsPrint()}
variant="contained"
<Stack
width="100%"
direction="row"
gap={2}
justifyContent="center"
sx={{ mb: 1 }}
>
Print
</Button>
<ScatterChartPro apiRef={apiRef} height={300} series={series} />
<FormControl sx={{ minWidth: 200 }}>
<InputLabel id="chart-type-label">Chart Type</InputLabel>
<Select
labelId="chart-type-label"
id="chart-type-select"
value={chartType}
label="Chart Type"
onChange={handleChange}
>
<MenuItem value="scatter">Scatter</MenuItem>
<MenuItem value="line">Line</MenuItem>
<MenuItem value="bar">Bar</MenuItem>
<MenuItem value="heatmap">Heatmap</MenuItem>
</Select>
</FormControl>
<Button onClick={() => apiRef.current!.exportAsPrint()} variant="contained">
Print
</Button>
</Stack>
<Chart apiRef={apiRef} type={chartType} />
</Stack>
);
}

function Chart<T extends ChartType = ChartType>({
apiRef,
type,
}: {
apiRef: React.RefObject<ChartProApi<T> | undefined>;
type: T;
}) {
switch (type) {
case 'scatter':
return (
<ScatterChartPro
apiRef={apiRef as React.RefObject<ChartProApi<'scatter'> | undefined>}
height={300}
series={scatterSeries}
/>
);
case 'line':
return (
<LineChartPro
apiRef={apiRef as React.RefObject<ChartProApi<'line'> | undefined>}
height={300}
xAxis={[{ data: data.map((p) => p.x1).toSorted((a, b) => a - b) }]}
series={series}
/>
);
case 'bar':
return (
<BarChartPro
apiRef={apiRef as React.RefObject<ChartProApi<'bar'> | undefined>}
height={300}
xAxis={[
{ data: data.map((p) => Math.round(p.x1)).toSorted((a, b) => a - b) },
]}
series={series}
/>
);
case 'heatmap':
return (
<Heatmap
apiRef={apiRef as React.RefObject<ChartProApi<'heatmap'> | undefined>}
xAxis={[{ data: [1, 2, 3, 4] }]}
yAxis={[{ data: ['A', 'B', 'C', 'D', 'E'] }]}
series={[{ data: heatmapData }]}
height={300}
hideLegend={false}
/>
);
default:
throw new Error(`Unknown chart type: ${type}`);
}
}
8 changes: 0 additions & 8 deletions docs/data/charts/export/PrintChart.tsx.preview

This file was deleted.

2 changes: 1 addition & 1 deletion docs/data/charts/export/export.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ components: ScatterChartPro, BarChartPro, LineChartPro

<p class="description">Charts can be printed and exported as PDF.</p>

Export is available on the Pro version of the charts: `<LineChartPro />`, `<BarChartPro />`, `<ScatterChartPro />`.
Export is available for the following charts: `<LineChartPro />`, `<BarChartPro />`, `<ScatterChartPro />`, `<Heatmap />`.

## Print/Export as PDF

Expand Down
22 changes: 22 additions & 0 deletions docs/data/charts/export/heatmapData.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
export const heatmapData = [
[0, 0, 10],
[0, 1, 20],
[0, 2, 40],
[0, 3, 90],
[0, 4, 70],
[1, 0, 30],
[1, 1, 50],
[1, 2, 10],
[1, 3, 70],
[1, 4, 40],
[2, 0, 50],
[2, 1, 20],
[2, 2, 90],
[2, 3, 20],
[2, 4, 70],
[3, 0, 40],
[3, 1, 50],
[3, 2, 20],
[3, 3, 70],
[3, 4, 90],
];
24 changes: 24 additions & 0 deletions docs/data/charts/export/heatmapData.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { HeatmapValueType } from '@mui/x-charts-pro/models';

export const heatmapData: HeatmapValueType[] = [
[0, 0, 10],
[0, 1, 20],
[0, 2, 40],
[0, 3, 90],
[0, 4, 70],
[1, 0, 30],
[1, 1, 50],
[1, 2, 10],
[1, 3, 70],
[1, 4, 40],
[2, 0, 50],
[2, 1, 20],
[2, 2, 90],
[2, 3, 20],
[2, 4, 70],
[3, 0, 40],
[3, 1, 50],
[3, 2, 20],
[3, 3, 70],
[3, 4, 90],
];
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,6 @@ type ChartContainerProComponent = <
props: ChartContainerProProps<TSeries, TSignatures> & { ref?: React.ForwardedRef<SVGSVGElement> },
) => React.JSX.Element;

export type ChartProApi = NonNullable<NonNullable<ChartContainerProProps['apiRef']>['current']>;

/**
* It sets up the data providers as well as the `<svg>` for the chart.
*
Expand Down
28 changes: 28 additions & 0 deletions packages/x-charts-pro/src/ChartContainerPro/ChartProApi.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { ChartAnyPluginSignature, ChartPublicAPI } from '@mui/x-charts/internals';
import { HeatmapPluginsSignatures } from '../Heatmap/Heatmap.plugins';
import { LineChartProPluginsSignatures } from '../LineChartPro/LineChartPro.plugins';
import { ScatterChartProPluginsSignatures } from '../ScatterChartPro/ScatterChartPro.plugins';
import { BarChartProPluginsSignatures } from '../BarChartPro/BarChartPro.plugins';
import { AllPluginSignatures, DefaultPluginSignatures } from '../internals/plugins/allPlugins';

type PluginsPerSeriesType = {
heatmap: HeatmapPluginsSignatures;
line: LineChartProPluginsSignatures;
scatter: ScatterChartProPluginsSignatures;
bar: BarChartProPluginsSignatures;
/* Special value when creating a chart using composition. */
composition: DefaultPluginSignatures;
};

/**
* The API of the chart `apiRef` object.
* The chart type can be passed as the first generic parameter to narrow down the API to the specific chart type.
* @example ChartProApi<'bar'>
*/
export type ChartProApi<
TSeries extends keyof PluginsPerSeriesType | undefined = undefined,
TSignatures extends
readonly ChartAnyPluginSignature[] = TSeries extends keyof PluginsPerSeriesType
? PluginsPerSeriesType[TSeries]
: AllPluginSignatures,
> = ChartPublicAPI<TSignatures>;
1 change: 1 addition & 0 deletions packages/x-charts-pro/src/ChartContainerPro/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import '../typeOverloads';

export * from './ChartContainerPro';
export * from './ChartProApi';
Loading