-
-
Notifications
You must be signed in to change notification settings - Fork 1.5k
[charts-pro] Export charts as image #17353
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
Changes from all commits
d57f9fd
ee7d7dc
f6fa42c
bf35743
da96b67
7deb126
4ae4758
ef6ecec
488a1ba
7bb41b8
2903719
1866400
eecd30e
37f9a27
886a3b2
ea7328f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,94 @@ | ||
import * as React from 'react'; | ||
import Button from '@mui/material/Button'; | ||
import FormControl from '@mui/material/FormControl'; | ||
import FormControlLabel from '@mui/material/FormControlLabel'; | ||
import FormLabel from '@mui/material/FormLabel'; | ||
import Radio from '@mui/material/Radio'; | ||
import RadioGroup from '@mui/material/RadioGroup'; | ||
import Stack from '@mui/material/Stack'; | ||
import TextField from '@mui/material/TextField'; | ||
import { LineChartPro } from '@mui/x-charts-pro/LineChartPro'; | ||
|
||
function ExportParamsSelector({ apiRef }) { | ||
const [type, setType] = React.useState('image/png'); | ||
const [rawQuality, setRawQuality] = React.useState('0.9'); | ||
const quality = Math.max(0, Math.min(1, Number.parseFloat(rawQuality))); | ||
|
||
return ( | ||
<Stack | ||
direction="row" | ||
justifyContent="space-between" | ||
flexWrap="wrap" | ||
gap={2} | ||
sx={{ width: '100%' }} | ||
> | ||
<FormControl fullWidth> | ||
<FormLabel id="image-format-radio-buttons-group-label"> | ||
Image Format | ||
</FormLabel> | ||
<RadioGroup | ||
row | ||
aria-labelledby="image-format-radio-buttons-group-label" | ||
name="image-format" | ||
value={type} | ||
onChange={(event) => setType(event.target.value)} | ||
> | ||
<FormControlLabel | ||
value="image/png" | ||
control={<Radio />} | ||
label="image/png" | ||
/> | ||
<FormControlLabel | ||
value="image/jpeg" | ||
control={<Radio />} | ||
label="image/jpeg" | ||
/> | ||
<FormControlLabel | ||
value="image/webp" | ||
control={<Radio />} | ||
label="image/webp" | ||
/> | ||
</RadioGroup> | ||
</FormControl> | ||
<FormControl> | ||
<TextField | ||
label="Quality" | ||
value={rawQuality} | ||
onChange={(event) => setRawQuality(event.target.value)} | ||
disabled={type === 'image/png'} | ||
helperText="Only applicable to lossy formats." | ||
/> | ||
</FormControl> | ||
<Button | ||
variant="outlined" | ||
onClick={() => apiRef.current?.exportAsImage({ type, quality })} | ||
> | ||
Export Image | ||
</Button> | ||
</Stack> | ||
); | ||
} | ||
|
||
export default function ExportChartAsImage() { | ||
const apiRef = React.useRef(undefined); | ||
|
||
return ( | ||
<Stack width="100%" gap={2}> | ||
<LineChartPro | ||
apiRef={apiRef} | ||
xAxis={[{ data: [1, 2, 3, 5, 8, 10] }]} | ||
series={[ | ||
{ data: [4, 9, 1, 4, 9, 6], label: 'Series A' }, | ||
{ | ||
data: [2, 5.5, 2, 8.5, 1.5, 5], | ||
area: true, | ||
label: 'Series B', | ||
}, | ||
]} | ||
height={300} | ||
grid={{ vertical: true, horizontal: true }} | ||
/> | ||
<ExportParamsSelector apiRef={apiRef} /> | ||
</Stack> | ||
); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,101 @@ | ||
import * as React from 'react'; | ||
import Button from '@mui/material/Button'; | ||
import FormControl from '@mui/material/FormControl'; | ||
import FormControlLabel from '@mui/material/FormControlLabel'; | ||
import FormLabel from '@mui/material/FormLabel'; | ||
import Radio from '@mui/material/Radio'; | ||
import RadioGroup from '@mui/material/RadioGroup'; | ||
import Stack from '@mui/material/Stack'; | ||
import TextField from '@mui/material/TextField'; | ||
import { LineChartPro } from '@mui/x-charts-pro/LineChartPro'; | ||
import { ChartProApi } from '@mui/x-charts-pro/ChartContainerPro'; | ||
|
||
function ExportParamsSelector({ | ||
apiRef, | ||
}: { | ||
apiRef: React.RefObject<ChartProApi | undefined>; | ||
}) { | ||
const [type, setType] = React.useState('image/png'); | ||
const [rawQuality, setRawQuality] = React.useState('0.9'); | ||
const quality = Math.max(0, Math.min(1, Number.parseFloat(rawQuality))); | ||
|
||
return ( | ||
<Stack | ||
direction="row" | ||
justifyContent="space-between" | ||
flexWrap="wrap" | ||
gap={2} | ||
sx={{ width: '100%' }} | ||
> | ||
<FormControl fullWidth> | ||
<FormLabel id="image-format-radio-buttons-group-label"> | ||
Image Format | ||
</FormLabel> | ||
<RadioGroup | ||
row | ||
aria-labelledby="image-format-radio-buttons-group-label" | ||
name="image-format" | ||
value={type} | ||
onChange={(event) => | ||
setType(event.target.value as 'image/png' | 'image/jpeg' | 'image/webp') | ||
} | ||
> | ||
<FormControlLabel | ||
value="image/png" | ||
control={<Radio />} | ||
label="image/png" | ||
/> | ||
<FormControlLabel | ||
value="image/jpeg" | ||
control={<Radio />} | ||
label="image/jpeg" | ||
/> | ||
<FormControlLabel | ||
value="image/webp" | ||
control={<Radio />} | ||
label="image/webp" | ||
/> | ||
</RadioGroup> | ||
</FormControl> | ||
<FormControl> | ||
<TextField | ||
label="Quality" | ||
value={rawQuality} | ||
onChange={(event) => setRawQuality(event.target.value)} | ||
bernardobelchior marked this conversation as resolved.
Show resolved
Hide resolved
|
||
disabled={type === 'image/png'} | ||
helperText="Only applicable to lossy formats." | ||
/> | ||
</FormControl> | ||
<Button | ||
variant="outlined" | ||
onClick={() => apiRef.current?.exportAsImage({ type, quality })} | ||
> | ||
Export Image | ||
</Button> | ||
</Stack> | ||
); | ||
} | ||
|
||
export default function ExportChartAsImage() { | ||
const apiRef = React.useRef<ChartProApi>(undefined); | ||
|
||
return ( | ||
<Stack width="100%" gap={2}> | ||
<LineChartPro | ||
apiRef={apiRef} | ||
xAxis={[{ data: [1, 2, 3, 5, 8, 10] }]} | ||
series={[ | ||
{ data: [4, 9, 1, 4, 9, 6], label: 'Series A' }, | ||
{ | ||
data: [2, 5.5, 2, 8.5, 1.5, 5], | ||
area: true, | ||
label: 'Series B', | ||
}, | ||
]} | ||
height={300} | ||
grid={{ vertical: true, horizontal: true }} | ||
/> | ||
<ExportParamsSelector apiRef={apiRef} /> | ||
</Stack> | ||
); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
<LineChartPro | ||
apiRef={apiRef} | ||
xAxis={[{ data: [1, 2, 3, 5, 8, 10] }]} | ||
series={[ | ||
{ data: [4, 9, 1, 4, 9, 6], label: 'Series A' }, | ||
{ | ||
data: [2, 5.5, 2, 8.5, 1.5, 5], | ||
area: true, | ||
label: 'Series B', | ||
}, | ||
]} | ||
height={300} | ||
grid={{ vertical: true, horizontal: true }} | ||
/> | ||
<ExportParamsSelector apiRef={apiRef} /> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
import ownerDocument from '@mui/utils/ownerDocument'; | ||
|
||
export function createExportIframe(title?: string): HTMLIFrameElement { | ||
const iframeEl = document.createElement('iframe'); | ||
iframeEl.style.position = 'absolute'; | ||
iframeEl.style.width = '0px'; | ||
iframeEl.style.height = '0px'; | ||
iframeEl.title = title || document.title; | ||
return iframeEl; | ||
} | ||
|
||
export function loadStyleSheets(document: Document, element: HTMLElement | SVGElement) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This looks close to the data grid print logic, maybe something to share at one point. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, it's a direct copy. I'll look into how we can share this. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Here it is: #17548 |
||
const stylesheetLoadPromises: Promise<void>[] = []; | ||
const doc = ownerDocument(element); | ||
|
||
const rootCandidate = element.getRootNode(); | ||
const root = | ||
rootCandidate.constructor.name === 'ShadowRoot' ? (rootCandidate as ShadowRoot) : doc; | ||
const headStyleElements = root!.querySelectorAll("style, link[rel='stylesheet']"); | ||
|
||
for (let i = 0; i < headStyleElements.length; i += 1) { | ||
const node = headStyleElements[i]; | ||
if (node.tagName === 'STYLE') { | ||
const newHeadStyleElements = document.createElement(node.tagName); | ||
const sheet = (node as HTMLStyleElement).sheet; | ||
|
||
if (sheet) { | ||
let styleCSS = ''; | ||
// NOTE: for-of is not supported by IE | ||
for (let j = 0; j < sheet.cssRules.length; j += 1) { | ||
if (typeof sheet.cssRules[j].cssText === 'string') { | ||
styleCSS += `${sheet.cssRules[j].cssText}\r\n`; | ||
} | ||
} | ||
newHeadStyleElements.appendChild(document.createTextNode(styleCSS)); | ||
document.head.appendChild(newHeadStyleElements); | ||
} | ||
} else if (node.getAttribute('href')) { | ||
// If `href` tag is empty, avoid loading these links | ||
|
||
const newHeadStyleElements = document.createElement(node.tagName); | ||
|
||
for (let j = 0; j < node.attributes.length; j += 1) { | ||
const attr = node.attributes[j]; | ||
if (attr) { | ||
newHeadStyleElements.setAttribute(attr.nodeName, attr.nodeValue || ''); | ||
} | ||
} | ||
|
||
stylesheetLoadPromises.push( | ||
new Promise((resolve) => { | ||
newHeadStyleElements.addEventListener('load', () => resolve()); | ||
}), | ||
); | ||
|
||
document.head.appendChild(newHeadStyleElements); | ||
} | ||
} | ||
|
||
return Promise.all(stylesheetLoadPromises); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A slider woudl solve the need for such a validation :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Isn't a slider a bit harder to use when selecting a specific value? For floating point numbers we would have a step of 0.01, which wouldn't be great when selecting a number, right?
I suppose sliders work better for integers.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you consider this as a percentage, it becomes an integer equivalent to a step of 0.01 ;)
But the texfield is ok too 👍