-
-
Notifications
You must be signed in to change notification settings - Fork 1.9k
Printing a HTML with a chart #7403
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
Comments
NOTES: I'm trying also resizing, the graph, but the resizing seems to be done by height, where the inner container uses 100%, so result is odd: For those doing exp., I'm using extra scripts: <script>
window.addEventListener('beforeprint', () => {
document.querySelectorAll('.js-plotly-plot').forEach(plot => {
console.log('resizing', plot);
Plotly.Plots.resize(plot);
});
});
</script> |
Intermediate solution, good to play with - final should include auto patching all plotly graphs. <html>
<head>
<script src='https://cdn.plot.ly/plotly-3.0.1.min.js'></script>
<style>
#myDiv {
width: 50% !important;
border: 2px solid red;
/*
FLEX,
OVERFLOW HIDDEN,
FIXED HEIGHT (via VH),
...
*/
/* must set height or use display flex, otherwise "plot-container plotly"'s 100% height will expand the container to stretch over multiple pages... */
display: flex;
}
#myDiv .main-svg {
border: 2px dashed red;
}
#myW {
background-color: red;
width: 0;
height: 10px;
margin-bottom: 10px;
}
</style>
</head>
<body>
<div id="myW"></div>
<div id='myDiv'>
<!-- Plotly chart will be drawn inside this DIV -->
</div>
<img id="img" style="width: 25%" />
<script>
var trace1 = {
x: ['giraffes', 'orangutans', 'monkeys'],
y: [20, 14, 23],
name: 'SF Zoo',
type: 'bar'
};
var trace2 = {
x: ['giraffes', 'orangutans', 'monkeys'],
y: [12, 18, 29],
name: 'LA Zoo',
type: 'bar'
};
var data = [trace1, trace2];
var layout = { barmode: 'stack' };
Plotly.newPlot('myDiv', data, layout, { responsive: false });
</script>
<script>
const resizeFn = () => {
var container = document.querySelector('#myDiv');
var containerBox = container.getBoundingClientRect();
var svg = document.querySelector("#myDiv .svg-container");
var svgBox = svg.getBoundingClientRect();
var ratio = svgBox.width / svgBox.height;
// overriden with a more reliable offsetWidth as getBoundingClientRect().width is affected by zooming, scaling, or print mode!
ratio = svg.offsetWidth / svg.offsetHeight;
var w = containerBox.width;
// overriden with a more reliable offsetWidth as getBoundingClientRect().width is affected by zooming, scaling, or print mode!
w = container.offsetWidth;
var h = w / ratio;
console.log('container size', containerBox.width, containerBox.height);
console.log('svg size', svgBox.width, svgBox.height);
console.log('svg w/h ratio', ratio);
console.log('new size', w, h);
var visualizer = document.querySelector('#myW');
visualizer.style.width = w;
Plotly.relayout(container, { width: w, height: h, autosize: true })
}
window.onresize = () => {
console.log('on resize');
resizeFn();
}
window.matchMedia('print').onchange = (print) => {
console.log('media change');
if (print.matches) {
console.log('- match -> resize');
resizeFn();
} else {
console.log('- no match');
resizeFn();
}
}
/*
window.onbeforeprint = () => {
console.log('on before print');
}
window.onafterprint = () => {
console.log('on after print');
}
*/
</script>
</body>
</html> |
Would love to have this fix. Although I'm using plotly.py. I can't print any pages with charts and get them to resize properly |
@rodriguesfred i was also experimenting (AI helped here a lot) to just store all the data and setup in a JS variable and then use Plotly to re-draw all the charts. What I've have found out is this:
Let me help you a bit to get you started, here's the HTML, try it out, play with it a bit... Anyhow, with an extra script at the end, you should now be able to code around the issue! <html>
<head>
<script src='https://cdn.plot.ly/plotly-3.0.1.min.js'></script>
<style>
:root {
font-family: Helvetica, Arial, sans-serif;
}
@media print {
.plotly {
page-break-inside: avoid;
break-inside: avoid;
}
}
.myPlotly {
width: 50% !important;
border: 2px solid red;
/*
FLEX,
OVERFLOW HIDDEN,
FIXED HEIGHT (via VH),
...
*/
/* must set height or use display flex, otherwise "plot-container plotly"'s 100% height will expand the container to stretch over multiple pages... */
display: flex;
}
#myDiv .main-svg {
border: 2px dashed red;
}
@media print {
#myDiv {
display: none !important;
}
}
#myW {
background-color: red;
width: 0;
height: 10px;
margin-bottom: 10px;
}
</style>
</head>
<body>
<p>Check font family</p>
<div id="myW"></div>
<div id='myDiv' class="myPlotly">
<!-- Plotly chart will be drawn inside this DIV -->
</div>
<div id='myPie' class="myPlotly">
<!-- Plotly chart will be drawn inside this DIV -->
</div>
<div id='myStacked' class="myPlotly">
<!-- Plotly chart will be drawn inside this DIV -->
</div>
<img id="img" style="width: 25%" />
<script>
const remToPx = rem => rem * parseFloat(getComputedStyle(document.documentElement).fontSize);
const plotlyResizeConst = .8;
const plotlyLayoutDefaults = {
font: {
family: "Helvetica, Arial, sans-serif",
size: remToPx(.8) * plotlyResizeConst
},
// NOTE:
// Margins behave weirdly, trimming x/y axis, legend, etc.!
// So adjusted it with some "best" working values
margin: {
l: remToPx(5),
r: remToPx(5),
t: remToPx(4),
b: remToPx(1.5),
autoexpand: true
},
title: {
font: {
size: remToPx(1.4) * plotlyResizeConst
}
},
legend: {
font: {
size: remToPx(.8) * plotlyResizeConst
}
},
textfont: {
size: remToPx(1) * plotlyResizeConst
},
marker: {
},
annotations: [{
/* inside layout annotation is an array, we need to apply settings for each */
font: {
size: remToPx(1.4) * plotlyResizeConst
}
}]
}
const defaultUiColors = ['#BCC5CD', '#96A6B4', '#5C7D95', '#135B7C', '#10516E', '#0C445E'];
function debounce(fn, delay) {
let timeout;
return function(...args) {
clearTimeout(timeout);
timeout = setTimeout(() => fn.apply(this, args), delay);
};
}
function deepMerge(target, source, skipPaths = [], currentPath = "") {
for (let key in source) {
if (source.hasOwnProperty(key)) {
const path = currentPath ? `${currentPath}.${key}` : key;
// Skip merging if the path is in skipPaths
if (skipPaths.some(skipPath => path === skipPath || path.startsWith(skipPath + "."))) {
continue;
}
if (typeof source[key] === 'object' && source[key] !== null && typeof target[key] === 'object' && target[key] !== null) {
deepMerge(target[key], source[key], skipPaths, path);
} else {
target[key] = source[key];
}
}
}
return target;
}
function applyDefaultsToAnnotations(target, defaults) {
if (target.annotations && Array.isArray(target.annotations)) {
target.annotations.forEach(annotation => {
if (defaults.annotations[0])
deepMerge(annotation, defaults.annotations[0]);
});
}
}
function getLayoutWithDefaults(target) {
deepMerge(target, plotlyLayoutDefaults, [ "annotations" ]);
applyDefaultsToAnnotations(target, plotlyLayoutDefaults);
return target;
}
const pieData = {
CategoryNames: [
"Cat A",
"Cat B",
"Cat C",
"Cat D"
],
Series: {
"Pie": [
482571455.57999998,
524544.42000000004,
369163000,
2556778000
]
},
ValueFormatCode: "$#,##0"
};
function toPlotlyPieData(data, { title, colors /* array */, hole /* num: .4 */, annotation /* html, use <br> */} = {}) {
const defaultPieChartColors = defaultUiColors;
const seriesName = Object.keys(data.Series)[0];
const seriesValues = data.Series[seriesName];
return {
data: [{
// our data
labels: data.CategoryNames,
values: seriesValues,
// plotly tweaks
type: 'pie',
sort: false,
direction: 'clockwise',
automargin: true,
hole: hole != undefined ? hole : 0,
marker: {
colors: colors ? colors : defaultPieChartColors
}
}],
layout: getLayoutWithDefaults({
// additional plotly layout
title: {
text: title ? title : seriesName
},
annotations: [{
text: annotation ? annotation : '',
showarrow: false,
}]
})
}
}
const stackedData = {
CategoryNames: ["A", "B", "C", "D", "E", "Other"],
Series: {
"Used": [
0.040082114696816558,
0.026988317172442423,
0.02650146962197153,
0.012419983509355888 * 1, // * 1000 * 1000 * 1000 * 1000,
0.007896725890937935,
0.030119969131519604
],
"Remaining": [
0.10991788530318346,
0.12301168282755758,
0.12349853037802846,
0.13758001649064414,
0.14210327410906207,
0.11988003086848041
]
}
};
function toPlotlyStackedBarData(data, { title, colors, texttemplate /* '%{y:.2%}', '$%{y:,.0f}' */, textposition /* auto, none */, hovertemplate /* '%{x}-%{data.name}: $%{y:,.0f}' */, yaxisformat /* '.0%', '$,.2f' */ } = {}) {
const currentUiColors = ['#2c7abb', '#8cb554'];
const defaultStackedBarChartColors = [ defaultUiColors[3], defaultUiColors[0] ];
const categoryNames = data.CategoryNames;
const series = data.Series;
const traces = [];
Object.entries(series).forEach(([seriesName, seriesData], i) => {
traces.push({
x: categoryNames,
y: seriesData,
name: seriesName,
type: 'bar',
texttemplate: texttemplate || '%{y:.2%}',
textposition: textposition || 'auto',
hovertemplate: hovertemplate || '' /* default */,
marker: {
color: (colors || defaultStackedBarChartColors)[i]
}
});
});
return {
data: traces,
layout: getLayoutWithDefaults({
// additional plotly layout
barmode: 'stack',
title: {
text: title
},
yaxis: {
tickformat: yaxisformat || '.0%'
},
})
};
}
function toPlotlyStackedBarData_Percentual(data, { title, textposition, decimals = 2, axisdecimals = 0 }) {
return toPlotlyStackedBarData(data, { title, textposition, texttemplate: `%{y:,.${decimals}%}`, hovertemplate: `%{x}-%{data.name}: %{y:,.${decimals}%}`, yaxisformat: `,.${axisdecimals}%` })
}
function toPlotlyStackedBarData_Currency(data, { title, textposition, decimals = 0, axisdecimals = 0 }) {
return toPlotlyStackedBarData(data, { title, textposition, texttemplate: `$%{y:,.${decimals}f}`, hovertemplate: `%{x}-%{data.name}: $%{y:,.${decimals}f}`, yaxisformat: `$,.${axisdecimals}f` })
}
</script>
<script>
/*
* Resize - sometimes odd output...
*/
const resizeFn = (container) => {
var containerBox = container.getBoundingClientRect();
var svg = container.querySelector(".svg-container");
var svgBox = svg.getBoundingClientRect();
var ratio = svgBox.width / svgBox.height;
// overriden with a more reliable offsetWidth as getBoundingClientRect().width is affected by zooming, scaling, or print mode!
ratio = svg.offsetWidth / svg.offsetHeight;
// skip elements that won't be displayed (media print, diplay none)...
if (Number.isNaN(ratio))
return;
var w = containerBox.width;
// overriden with a more reliable offsetWidth as getBoundingClientRect().width is affected by zooming, scaling, or print mode!
w = container.offsetWidth;
var h = w / ratio;
console.log('container size', containerBox.width, containerBox.height);
console.log('svg size', svgBox.width, svgBox.height);
console.log('svg w/h ratio', ratio);
console.log('new size', w, h);
Plotly.relayout(container, { width: w, height: h, autosize: false })
}
const resizeAllChartsFn = () => {
document.querySelectorAll('.js-plotly-plot').forEach(resizeFn);
};
/*
* REDRAW - store data
*/
const chartStore = new Map();
const setChartData = (idSelector, data, layout) => {
chartStore.set(idSelector, { data, layout });
};
const drawChart = (idSelector) => {
const entry = chartStore.get(idSelector);
if (!entry) return;
const container = document.querySelector(idSelector);
if (!container) return;
Plotly.newPlot(container, entry.data, entry.layout);
};
const redrawCharts = () => {
// Iterate over each entry in chartStore
chartStore.forEach((chartData, id) => {
const container = document.querySelector("#" + id);
if (!container) return;
const { data: chart, layout } = chartData;
/*
// SVG is only on re-draws!
const svg = container.querySelector('.svg-container');
if (!svg) return;
const ratio = svg.offsetWidth / svg.offsetHeight;
const width = container.offsetWidth;
const height = width / ratio;
const newLayout = { ...layout, width, height, autosize: true };
*/
const newLayout = { ...chart.layout, ...layout, autosize: true };
Plotly.newPlot(container, chart.data, newLayout);
});
};
/*
* Setup (if needed)
*/
Plotly.newPlot(document.querySelector("#myDiv"), toPlotlyStackedBarData(stackedData).data, { barmode: 'stack' });
var data = [{
values: [19, 26, 55],
labels: ['Residential', 'Non-Residential', 'Utility'],
type: 'pie'
}];
Plotly.newPlot('myDiv', data, { height: 400, width: 500 });
setChartData("myPie", toPlotlyPieData(pieData, { hole: .4 }));
setChartData("myStacked", toPlotlyStackedBarData_Percentual(stackedData, { title: 'Stacked' }));
redrawCharts();
/*
* Events
*/
window.matchMedia('print').onchange = (print) => {
// Do NOT use debounce here!
// NOTE: seems redraw works nicer - keeps the ratio's that we have on direct HTML output...
console.log('media change');
if (print.matches) {
console.log('- match -> resize');
resizeAllChartsFn();
} else {
console.log('- no match');
resizeAllChartsFn();
}
}
const resizeAllDebounced = debounce(resizeAllChartsFn, 1000);
const refreshAllDebounced = debounce(redrawCharts, 100);
window.onresize = () => resizeAllDebounced();
/*
window.onbeforeprint = () => {
console.log('on before print');
}
window.onafterprint = () => {
console.log('on after print');
}
*/
</script>
</body>
</html> |
Hi, and thanks for your work on Plotly.js. We're experiencing issues when printing HTML reports with embedded Plotly graphs using the browser's print function (e.g. Save as PDF). Everything displays correctly on screen, but:
Interestingly, printing the same HTML in A3 landscape works fine. We’ve tried CSS Use case: We generate scientific/engineering reports from Jupyter notebooks as standalone HTML and want users to print them directly (without exporting each figure to PNG). In our case, exporting via LaTeX is not a practical or convenient option. We’ve attached a ZIP file containing a simplified HTML example (esempio_progetto.html) that reproduces the issue. Could this be improved or officially supported? Any recommended patterns to ensure consistent PDF output? Thanks! |
@hidegh thank you so much for you code! It really helped. Made some modifications and works a treat! I don't actually know javascript so the code could do with some tiding up. I'm working in plotly.py so I put some modification of your code (see below) in a print.js in assets folder. I then had to load the script in the app by having the following element somewhere in my app. print.js
|
The HTML renders well.
The container "myDiv" has a width of 50% - PIC 1.
Now, when I try to print the HTML to A4, the chart is now overflowing, is outside the parent container, not re-sized.
I've added a rectange on the container for clarity.
PIC 1:

PIC 2:

The text was updated successfully, but these errors were encountered: