I have tried to follow the advice here:
as well as the this npm module to export my c3 (which is based on d3) plots to an image file - .png for now.
In the browser, the image file looks like this:
The save-svg-as-png
node module, however, produces this:
The script from the above mentioned SO post produces the following in a new tab:
As you can see, the paths are closed and then filled in, as though the .css is being ignored.
Here is the code to produce the plots:
# plotMultiline.js
import * as c3 from 'c3';
import * as d3 from 'd3';
import { saveSvgAsPng } from 'save-svg-as-png';
import createImageFromSVG from './createImageFromSVG';
const plotMultiline = (
chartID, dataToPlot,
headingsAndLabels,
xHeading,
nTicks,
xRange,
xLines = [],
title,
xAxisTitle,
note,
yTitle = 'Frequency of occurrence',
inset = null,
) => {
d3.select('body').append('div')
.attr('id', chartID);
const yDatas = Object.entries(headingsAndLabels).map(([columnName, newLabel]) =>
[newLabel, ...dataToPlot.map(d => d[columnName])]);
const firstKey = Object.keys(dataToPlot[0])[0];
const secondKey = Object.keys(dataToPlot[0])[1];
const xMin = +dataToPlot[0][firstKey];
const xMax = +[...dataToPlot].pop()[secondKey];
const xTickValuesAll = [...dataToPlot.map(d => +d[firstKey])];
const nXTickValuesAll = xTickValuesAll.length;
const xTickValuesIndices =
[...Array(nTicks).keys()].map(d => d * Math.ceil(nXTickValuesAll / nTicks))
.filter(d => d <= nXTickValuesAll - 1);
let xTickValues = [];
if (nTicks) {
if (typeof nTicks === 'number') {
xTickValues = [...xTickValuesIndices.map(i => +xTickValuesAll[i]), xMax];
} else if (nTicks === 'integer') {
xTickValues = [...xTickValuesAll, xMax].filter(d => Math.round(d) === d);
}
} else {
xTickValues = [...xTickValuesAll, xMax];
}
const rightPadding = (xTickValues[1] - xTickValues[0]) / 5;
const chart = c3.generate({
bindto: `#${chartID}`,
title: {
text: title,
},
point: {
show: false,
},
size: {
width: 960,
height: 500,
},
padding: {
bottom: 20,
top: 20,
},
data: {
x: xHeading,
columns: yDatas,
},
legend: {
position: 'inset',
inset,
},
axis: {
x: {
tick: {
outer: false,
values: xTickValues,
},
min: xMin,
max: xMax,
padding: { left: 0, right: rightPadding },
label: {
text: xAxisTitle || xHeading,
position: 'outer-center',
},
height: 50,
},
y: {
padding: { top: 0, bottom: 0 },
label: {
text: yTitle,
position: 'outer-middle',
},
},
},
grid: {
x: {
show: true,
lines: xLines,
},
y: {
show: true,
},
},
});
d3.select(`#${chartID} svg`).attr('id', `svg-${chartID}`);
if (note) {
d3.select(`#${chartID} svg`).append('text')
.attr('x', 630)
.attr('y', 485)
.classed('note', true)
.text(note);
}
if (xRange) {
const xRangeMin = xRange[0];
const xRangeMax = xRange[1];
chart.axis.range({
min: {
x: xRangeMin,
},
max: {
x: xRangeMax,
},
});
}
setTimeout(() => {
d3.select(`#${chartID}`)
.append('button')
.on('click', () => saveSvgAsPng(d3.select(`#svg-${chartID}`)[0]['0'], `#svg-${chartID}.png`))
.classed('btn btn-success', true)
.attr('id', 'button-library');
d3.select(`#${chartID}`)
.append('button')
.on('click', () => createImageFromSVG(`#svg-${chartID}`))
.classed('btn btn-success', true)
.attr('id', 'button-so-script');
}, 1000);
};
export default plotMultiline;
and (copied from the above-mentioned SO post):
# createImageFromSVG
import * as d3 from 'd3';
const createImageFromSVG = (selectorForSVG) => {
// get styles from all required stylesheets
// http://www.coffeegnome.net/converting-svg-to-png-with-canvg/
let style = '\n';
for (let i = 0; i < document.styleSheets.length; i++) {
const sheet = document.styleSheets[i];
if (sheet.href) {
const { rules } = sheet;
if (rules) {
for (let j = 0; j < rules.length; j++) {
style += (`${rules[j].cssText}\n`);
}
}
}
}
const svg = d3.select(selectorForSVG);
const img = new Image();
const serializer = new XMLSerializer();
// prepend style to svg
svg.insert('defs', ':first-child');
d3.select('svg defs')
.append('style')
.attr('type', 'text/css')
.html(style);
// generate IMG in new tab
const svgStr = serializer.serializeToString(svg.node());
img.src = `data:image/svg+xml;base64,${window.btoa(unescape(encodeURIComponent(svgStr)))}`;
const popUp = window.open();
if (popUp) {
popUp.document.write(`<img src="${img.src}"/>`);
}
};
export default createImageFromSVG;
I have also uploaded a sample project with this code to github:
https://github.com/shafiquejamal/export-svg-to-png
Could someone advise on how to make the exported file look like what is rendered in the browser? Thanks!
UPDATE #1: Following https://stackoverflow.com/users/3702797/kaiido's suggestion below, in plotMultiline.js
I changed
d3.select(`#${chartID} svg`).attr('id', `svg-${chartID}`)
to
d3.select(`#${chartID} svg`).attr('id', `svg-${chartID}`)
.classed('c3', true);
and then saveSvgAsPng
generates the following image file:
This fixes the closed-shape fill issue, but the background is actually transparent, not white, as you can see from the following screenshot:
But this is actually good enough for my purposes.