SVG to Image export performance issues (using canvg / XMLSerializer / getComputedStyle) - javascript

I am using canvg to convert a chart svg to an image. We have the issue that by default not all CSS attributes are applied to the image so we ended up using getComputedStyle in a loop.
This is a total mess under performance aspects if we have 10 or even 20 charts to be exported at once.
var labels = ['2018-10-01', '2018-10-02', '2018-10-03', '2018-10-04', '2018-10-05', '2018-10-06', '2018-10-07', '2018-10-08', '2018-10-09', '2018-10-10', '2018-10-11', '2018-10-12', '2018-10-13', '2018-10-14', '2018-10-15', '2018-10-16', '2018-10-17', '2018-10-18', '2018-10-19', '2018-10-20', '2018-10-21', '2018-10-22', '2018-10-23', '2018-10-24', '2018-10-25', '2018-10-26', '2018-10-27', '2018-10-28', '2018-10-29', '2018-10-30', '2018-10-31', '2018-11-01', '2018-11-02', '2018-11-03', '2018-11-04', '2018-11-05', '2018-11-06', '2018-11-07', '2018-11-08', '2018-11-09', '2018-11-10', '2018-11-11', '2018-11-12', '2018-11-13', '2018-11-14', '2018-11-15', '2018-11-16', '2018-11-17', '2018-11-18', '2018-11-19', '2018-11-20', '2018-11-21', '2018-11-22', '2018-11-23', '2018-11-24', '2018-11-25', '2018-11-26', '2018-11-27', '2018-11-28', '2018-11-29', '2018-11-30', '2018-12-01', '2018-12-02', '2018-12-03', '2018-12-04', '2018-12-05', '2018-12-06', '2018-12-07', '2018-12-08', '2018-12-09', '2018-12-10', '2018-12-11', '2018-12-12', '2018-12-13', '2018-12-14', '2018-12-15', '2018-12-16', '2018-12-17', '2018-12-18', '2018-12-19', '2018-12-20', '2018-12-21', '2018-12-22', '2018-12-23', '2018-12-24', '2018-12-25', '2018-12-26', '2018-12-27', '2018-12-28', '2018-12-29', '2018-12-30', '2018-12-31', '2019-01-01', '2019-01-02', '2019-01-03', '2019-01-04', '2019-01-05', '2019-01-06', '2019-01-07', '2019-01-08', '2019-01-09', '2019-01-10', '2019-01-11', '2019-01-12', '2019-01-13', '2019-01-14', '2019-01-15', '2019-01-16', '2019-01-17', '2019-01-18', '2019-01-19', '2019-01-20', '2019-01-21', '2019-01-22', '2019-01-23', '2019-01-24', '2019-01-25', '2019-01-26', '2019-01-27', '2019-01-28', '2019-01-29', '2019-01-30', '2019-01-31', '2019-02-01', '2019-02-02', '2019-02-03', '2019-02-04', '2019-02-05', '2019-02-06', '2019-02-07', '2019-02-08', '2019-02-09', '2019-02-10', '2019-02-11', '2019-02-12', '2019-02-13', '2019-02-14', '2019-02-15', '2019-02-16', '2019-02-17', '2019-02-18', '2019-02-19', '2019-02-20', '2019-02-21', '2019-02-22', '2019-02-23', '2019-02-24', '2019-02-25', '2019-02-26', '2019-02-27', '2019-02-28'];
var columns = ['data101', 'data2', 'data347'];
var data = [
[0, 0, 2, 2, 1, 2, 7, 3, 1, 7, 5, 5, 5, 5, 6, 6, 11, 7, 2, 7, 16, 7, 3, 5, 10, 9, 11, 7, 3, 7, 7, 10, 10, 9, 18, 10, 20, 13, 9, 19, 16, 13, 20, 18, 14, 15, 18, 20, 19, 11, 13, 13, 12, 16, 11, 12, 21, 20, 23, 19, 19, 23, 23, 24, 23, 25, 21, 23, 20, 22, 21, 23, 24, 25, 27, 29, 28, 25, 24, 17, 20, 24, 22, 27, 21, 27, 19, 26, 31, 27, 28, 27, 21, 20, 27, 22, 22, 19, 17, 21, 23, 19, 22, 20, 21, 25, 15, 19, 20, 19, 21, 28, 17, 20, 14, 18, 17, 20, 27, 21, 18, 18, 20, 16, 27, 16, 16, 9, 18, 8, 19, 13, 8, 16, 15, 16, 9, 15, 10, 13, 10, 11, 10, 13, 12, 7, 14, 16, 13, 14, 8],
[0, 0, 338, 1201, 1268, 1371, 1286, 1148, 446, 288, 228, 253, 193, 201, 283, 393, 436, 379, 421, 444, 444, 417, 513, 353, 364, 399, 238, 191, 305, 337, 365, 349, 365, 244, 101, 39, 55, 72, 151, 98, 31, 127, 114, 92, 104, 196, 307, 245, 84, 168, 41, 38, 292, 488, 536, 569, 495, 448, 408, 358, 344, 380, 328, 334, 332, 330, 345, 312, 369, 377, 356, 301, 226, 273, 237, 116, 178, 133, 114, 138, 95, 143, 74, 74, 83, 47, 75, 101, 96, 59, 46, 128, 70, 57, 93, 80, 94, 93, 63, 86, 81, 63, 70, 102, 91, 67, 69, 68, 88, 76, 79, 70, 119, 88, 74, 94, 76, 54, 82, 90, 75, 130, 67, 78, 106, 91, 81, 27, 77, 21, 104, 83, 55, 60, 62, 304, 393, 191, 292, 77, 76, 55, 125, 89, 99, 127, 60, 75, 99, 120, 56],
[0, 0, 0, 1419, 7454, 12638, 10944, 7652, 4272, 11219, 9071, 7207, 7929, 8373, 9566, 6310, 7406, 9286, 8415, 7659, 6457, 3380, 10902, 10952, 10508, 7219, 4625, 4484, 4396, 5178, 5991, 7927, 14132, 14307, 5094, 10011, 6257, 9184, 18574, 12597, 11415, 7118, 9991, 10225, 14337, 4417, 12701, 17833, 23553, 10037, 4833, 5894, 19421, 14735, 12597, 8730, 5888, 11836, 13143, 17219, 10492, 10528, 8649, 11868, 10502, 6758, 7672, 8479, 11142, 22330, 26595, 4423, 17434, 8709, 9657, 7823, 9135, 19765, 18016, 16010, 8419, 7300, 8877, 9611, 9050, 8680, 8211, 6635, 3069, 10739, 6288, 6761, 7807, 16243, 20415, 23051, 19727, 8721, 6445, 8585, 13688, 14728, 17113, 16255, 3898, 4622, 3869, 3774, 4190, 3461, 4824, 4608, 4613, 3677, 3648, 3575, 3556, 4036, 3732, 2517, 4676, 4129, 3250, 4142, 3987, 4396, 3362, 2964, 1849, 2609, 2851, 3003, 3583, 3473, 3190, 2658, 4363, 3959, 4588, 3771, 4315, 3178, 3354, 3159, 2695, 4114, 4292, 3322, 1218, 3526, 3717]
];
var colors = ['#0065A3', '#767670', '#D73648', '#7FB2CE', '#00345B'];
var padding = 5;
//prepare chart data
var columnData = [];
var chartDataColumns = [];
var chartData = [];
chartData.push([columns[0]].concat(data[0]));
chartDataColumns = [
['x'].concat(labels)
].concat(chartData);
var chart1 = c3.generate({
bindto: d3.select('#chart1'),
data: {
x: 'x',
columns: [['x'].concat(labels)].concat(chartData),
type: 'line',
onmouseover: function(d) {
chart1.focus(d.id);
chart2.focus(d.id);
},
onmouseout: function() {
chart1.revert();
chart2.revert();
}
},
legend: {
position: 'right',
show: true,
item: {
onclick: function(id) {
if (chart1) chart1.toggle(id);
if (chart2) chart2.toogle(id);
},
onmouseover: function(id) {
if (chart1) chart1.focus(id);
if (chart2) chart2.focus(id);
},
onmouseout: function(id) {
if (chart1) chart1.revert();
if (chart2) chart2.revert();
}
}
},
tooltip: {
show: true,
format: {
value: function(value) {
return d3.format(",.0f")(value);
}
}
},
zoom: {
enabled: true
},
axis: {
x: {
type: 'timeseries',
tick: {
rotate: 90,
format: '%Y-%m-%d'
}
},
y: {
label: 'sample-data',
tick: {
format: d3.format(",")
}
}
},
color: {
pattern: colors
}
});
var chart2 = c3.generate({
bindto: d3.select('#chart2'),
data: {
columns: [[columns[0]].concat(data[0])],
type: 'pie',
onmouseover: function(id) {
if (chart1) chart1.focus(id);
if (chart2) chart2.focus(id);
},
onmouseout: function(id) {
if (chart1) chart1.revert();
if (chart2) chart2.revert();
}
},
legend: {
position: 'right',
show: true,
item: {
onclick: function(id) {
if (chart1) chart1.toggle(id);
if (chart2) chart2.toogle(id);
},
onmouseover: function(id) {
if (chart1) chart1.focus(id);
if (chart2) chart2.focus(id);
},
onmouseout: function(id) {
if (chart1) chart1.revert();
if (chart2) chart2.revert();
}
}
},
color: {
pattern: colors
},
});
for (var i = 1; i < columns.length; i++) {
setTimeout(function(column) {
chart1.load({
columns: [
[columns[column]].concat(data[column])
]
});
chart2.load({
columns: [[columns[column]].concat(data[column])]
});
}, (i * 5000 / columns.length), i);
}
document.getElementById("exportButton").onclick = function() {
exportChartToImage();
};
function exportChartToImage() {
var createImagePromise = new Promise(function(resolve, reject) {
var images = [];
d3.selectAll('svg').each(function() {
if (this.parentNode) {
images.push(getSvgImage(this.parentNode, true));
}
});
if (images.length > 0)
resolve(images);
else
reject(images);
});
createImagePromise.then(function(images) {
images.forEach(function(img, n) {
img.toBlob(function(blob) {
saveAs(blob, "image_" + (n + 1) + ".png");
});
});
})
.catch(function(error) {
throw error;
});
};
/**
* Converts a SVG-Chart to a canvas and returns it.
*/
function getSvgImage(svgContainer, png) {
var svgEl = d3.select(svgContainer).select('svg').node();
var svgCopyEl = svgEl.cloneNode(true);
if (!svgCopyEl)
return;
//remove elements not for printing
lensObject = d3.selectAll(".hidden-print").remove().exit();
//add temp document objects
var emptySvgEl = d3.select(document.createElementNS("http://www.w3.org/2000/svg", "svg")).attr("id", "emptysvg")
.attr("version", 1.1)
.attr("height", 2)
.node();
var canvasComputed = d3.select(document.createElement("canvas")).attr("id", "canvasComputed").node();
var container = d3.select(document.createElement("div")).attr("style", "display: none;")
.attr("class", "c3").node();
svgContainer.append(container);
container.append(svgCopyEl);
container.append(emptySvgEl);
container.append(canvasComputed);
//apply all CSS styles to SVG
exportStyles(svgCopyEl, emptySvgEl);
// transform SVG to canvas using external canvg
canvg(canvasComputed, new XMLSerializer().serializeToString(svgCopyEl));
//remove temp document objects
canvasComputed.remove();
emptySvgEl.remove();
svgCopyEl.remove();
container.remove();
return canvasComputed;
}
function exportStyles(svg, emptySvg) {
var tree = [];
var emptySvgDeclarationComputed = getComputedStyle(emptySvg);
//d3.select(svg).selectAll().each(function() {
$(svg).find("*").each(function() {
explicitlySetStyle(this, emptySvgDeclarationComputed);
});
}
function traverse(obj, tree) {
tree.push(obj);
if (obj.hasChildNodes()) {
var child = obj.firstChild;
while (child) {
if (child.nodeType === 1 && child.nodeName != 'SCRIPT') {
traverse(child, tree);
}
child = child.nextSibling;
}
}
return tree;
}
function explicitlySetStyle(element, emptySvgDeclarationComputed) {
var cSSStyleDeclarationComputed = getComputedStyle(element);
var i, len, key, value;
var computedStyleStr = "";
for (i = 0, len = cSSStyleDeclarationComputed.length; i < len; i++) {
key = cSSStyleDeclarationComputed[i];
value = cSSStyleDeclarationComputed.getPropertyValue(key);
if (value !== emptySvgDeclarationComputed.getPropertyValue(key)) {
if (key == 'visibility' && value == 'hidden') {
computedStyleStr += 'display: none;';
} else {
computedStyleStr += key + ":" + value + ";";
}
}
}
element.setAttribute('style', computedStyleStr);
}
<link href="https://cdnjs.cloudflare.com/ajax/libs/c3/0.6.12/c3.min.css" rel="stylesheet" />
<script src="https://d3js.org/d3.v5.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/c3/0.6.12/c3.min.js"></script>
<!-- Required to convert named colors to RGB -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/canvg/1.4/rgbcolor.min.js"></script>
<!-- Optional if you want blur -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/stackblur-canvas/1.4.1/stackblur.min.js"></script>
<!-- Main canvg code -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/canvg/1.5/canvg.js"></script>
<script src="https://fastcdn.org/FileSaver.js/1.1.20151003/FileSaver.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div id="chart1" class "c3">
</div>
<div id="chart2" class="c3">
</div>
<button type="button" id="exportButton">
export to SVG
</button>
=> http://jsfiddle.net/gothmogg/jLt3yq75/
This sample shows exemplary how we apply the CSS styles to the SVG. If we use normal canvg without getComputedStyle the x and y axes of the c3 chart look like a total mess...
Do you know faster way to get valid images?
Is there a way to filter the CSS-styles? Maybe use "c3" styles only?

I have managed to solve the performance problems of exporting C3 SVG charts to PNG by avoiding to use getComputedStyle.
Browsing the issues in C3 I found issue #313
https://github.com/c3js/c3/issues/313
For others http://www.nihilogic.dk/labs/canvas2image/ might also be a good place to look at but I found a solution at https://gist.github.com/aendrew/1ad2eed6afa29e30d52e#file-exportchart-js-L29.
I have changed the code from using angular to d3 and now it works (for me).
Hopefully this might help others having the same issue.
Here the working code. Please note: the css styles are only examined and inlined once.
var labels = ['2018-10-01', '2018-10-02', '2018-10-03', '2018-10-04', '2018-10-05', '2018-10-06', '2018-10-07', '2018-10-08', '2018-10-09', '2018-10-10', '2018-10-11', '2018-10-12', '2018-10-13', '2018-10-14', '2018-10-15', '2018-10-16', '2018-10-17', '2018-10-18', '2018-10-19', '2018-10-20', '2018-10-21', '2018-10-22', '2018-10-23', '2018-10-24', '2018-10-25', '2018-10-26', '2018-10-27', '2018-10-28', '2018-10-29', '2018-10-30', '2018-10-31', '2018-11-01', '2018-11-02', '2018-11-03', '2018-11-04', '2018-11-05', '2018-11-06', '2018-11-07', '2018-11-08', '2018-11-09', '2018-11-10', '2018-11-11', '2018-11-12', '2018-11-13', '2018-11-14', '2018-11-15', '2018-11-16', '2018-11-17', '2018-11-18', '2018-11-19', '2018-11-20', '2018-11-21', '2018-11-22', '2018-11-23', '2018-11-24', '2018-11-25', '2018-11-26', '2018-11-27', '2018-11-28', '2018-11-29', '2018-11-30', '2018-12-01', '2018-12-02', '2018-12-03', '2018-12-04', '2018-12-05', '2018-12-06', '2018-12-07', '2018-12-08', '2018-12-09', '2018-12-10', '2018-12-11', '2018-12-12', '2018-12-13', '2018-12-14', '2018-12-15', '2018-12-16', '2018-12-17', '2018-12-18', '2018-12-19', '2018-12-20', '2018-12-21', '2018-12-22', '2018-12-23', '2018-12-24', '2018-12-25', '2018-12-26', '2018-12-27', '2018-12-28', '2018-12-29', '2018-12-30', '2018-12-31', '2019-01-01', '2019-01-02', '2019-01-03', '2019-01-04', '2019-01-05', '2019-01-06', '2019-01-07', '2019-01-08', '2019-01-09', '2019-01-10', '2019-01-11', '2019-01-12', '2019-01-13', '2019-01-14', '2019-01-15', '2019-01-16', '2019-01-17', '2019-01-18', '2019-01-19', '2019-01-20', '2019-01-21', '2019-01-22', '2019-01-23', '2019-01-24', '2019-01-25', '2019-01-26', '2019-01-27', '2019-01-28', '2019-01-29', '2019-01-30', '2019-01-31', '2019-02-01', '2019-02-02', '2019-02-03', '2019-02-04', '2019-02-05', '2019-02-06', '2019-02-07', '2019-02-08', '2019-02-09', '2019-02-10', '2019-02-11', '2019-02-12', '2019-02-13', '2019-02-14', '2019-02-15', '2019-02-16', '2019-02-17', '2019-02-18', '2019-02-19', '2019-02-20', '2019-02-21', '2019-02-22', '2019-02-23', '2019-02-24', '2019-02-25', '2019-02-26', '2019-02-27', '2019-02-28'];
var columns = ['data101', 'data2', 'data347'];
var data = [
[0, 0, 2, 2, 1, 2, 7, 3, 1, 7, 5, 5, 5, 5, 6, 6, 11, 7, 2, 7, 16, 7, 3, 5, 10, 9, 11, 7, 3, 7, 7, 10, 10, 9, 18, 10, 20, 13, 9, 19, 16, 13, 20, 18, 14, 15, 18, 20, 19, 11, 13, 13, 12, 16, 11, 12, 21, 20, 23, 19, 19, 23, 23, 24, 23, 25, 21, 23, 20, 22, 21, 23, 24, 25, 27, 29, 28, 25, 24, 17, 20, 24, 22, 27, 21, 27, 19, 26, 31, 27, 28, 27, 21, 20, 27, 22, 22, 19, 17, 21, 23, 19, 22, 20, 21, 25, 15, 19, 20, 19, 21, 28, 17, 20, 14, 18, 17, 20, 27, 21, 18, 18, 20, 16, 27, 16, 16, 9, 18, 8, 19, 13, 8, 16, 15, 16, 9, 15, 10, 13, 10, 11, 10, 13, 12, 7, 14, 16, 13, 14, 8],
[0, 0, 338, 1201, 1268, 1371, 1286, 1148, 446, 288, 228, 253, 193, 201, 283, 393, 436, 379, 421, 444, 444, 417, 513, 353, 364, 399, 238, 191, 305, 337, 365, 349, 365, 244, 101, 39, 55, 72, 151, 98, 31, 127, 114, 92, 104, 196, 307, 245, 84, 168, 41, 38, 292, 488, 536, 569, 495, 448, 408, 358, 344, 380, 328, 334, 332, 330, 345, 312, 369, 377, 356, 301, 226, 273, 237, 116, 178, 133, 114, 138, 95, 143, 74, 74, 83, 47, 75, 101, 96, 59, 46, 128, 70, 57, 93, 80, 94, 93, 63, 86, 81, 63, 70, 102, 91, 67, 69, 68, 88, 76, 79, 70, 119, 88, 74, 94, 76, 54, 82, 90, 75, 130, 67, 78, 106, 91, 81, 27, 77, 21, 104, 83, 55, 60, 62, 304, 393, 191, 292, 77, 76, 55, 125, 89, 99, 127, 60, 75, 99, 120, 56],
[0, 0, 0, 1419, 7454, 12638, 10944, 7652, 4272, 11219, 9071, 7207, 7929, 8373, 9566, 6310, 7406, 9286, 8415, 7659, 6457, 3380, 10902, 10952, 10508, 7219, 4625, 4484, 4396, 5178, 5991, 7927, 14132, 14307, 5094, 10011, 6257, 9184, 18574, 12597, 11415, 7118, 9991, 10225, 14337, 4417, 12701, 17833, 23553, 10037, 4833, 5894, 19421, 14735, 12597, 8730, 5888, 11836, 13143, 17219, 10492, 10528, 8649, 11868, 10502, 6758, 7672, 8479, 11142, 22330, 26595, 4423, 17434, 8709, 9657, 7823, 9135, 19765, 18016, 16010, 8419, 7300, 8877, 9611, 9050, 8680, 8211, 6635, 3069, 10739, 6288, 6761, 7807, 16243, 20415, 23051, 19727, 8721, 6445, 8585, 13688, 14728, 17113, 16255, 3898, 4622, 3869, 3774, 4190, 3461, 4824, 4608, 4613, 3677, 3648, 3575, 3556, 4036, 3732, 2517, 4676, 4129, 3250, 4142, 3987, 4396, 3362, 2964, 1849, 2609, 2851, 3003, 3583, 3473, 3190, 2658, 4363, 3959, 4588, 3771, 4315, 3178, 3354, 3159, 2695, 4114, 4292, 3322, 1218, 3526, 3717]
];
var colors = ['#0065A3', '#767670', '#D73648', '#7FB2CE', '#00345B'];
var padding = 5;
//prepare chart data
var columnData = [];
var chartDataColumns = [];
var chartData = [];
var C3Styles = null;
chartData.push([columns[0]].concat(data[0]));
chartDataColumns = [
['x'].concat(labels)
].concat(chartData);
var chart1 = c3.generate({
bindto: d3.select('#chart1'),
data: {
x: 'x',
columns: [
['x'].concat(labels)
].concat(chartData),
type: 'line',
onmouseover: function(d) {
chart1.focus(d.id);
chart2.focus(d.id);
},
onmouseout: function() {
chart1.revert();
chart2.revert();
}
},
legend: {
position: 'right',
show: true,
item: {
onclick: function(id) {
if (chart1) chart1.toggle(id);
if (chart2) chart2.toogle(id);
},
onmouseover: function(id) {
if (chart1) chart1.focus(id);
if (chart2) chart2.focus(id);
},
onmouseout: function(id) {
if (chart1) chart1.revert();
if (chart2) chart2.revert();
}
}
},
tooltip: {
show: true,
format: {
value: function(value) {
return d3.format(",.0f")(value);
}
}
},
zoom: {
enabled: true
},
axis: {
x: {
type: 'timeseries',
tick: {
rotate: 90,
format: '%Y-%m-%d'
}
},
y: {
label: 'sample-data',
tick: {
format: d3.format(",")
}
}
},
color: {
pattern: colors
}
});
var chart2 = c3.generate({
bindto: d3.select('#chart2'),
data: {
columns: [
[columns[0]].concat(data[0])
],
type: 'pie',
onmouseover: function(id) {
if (chart1) chart1.focus(id);
if (chart2) chart2.focus(id);
},
onmouseout: function(id) {
if (chart1) chart1.revert();
if (chart2) chart2.revert();
}
},
legend: {
position: 'right',
show: true,
item: {
onclick: function(id) {
if (chart1) chart1.toggle(id);
if (chart2) chart2.toogle(id);
},
onmouseover: function(id) {
if (chart1) chart1.focus(id);
if (chart2) chart2.focus(id);
},
onmouseout: function(id) {
if (chart1) chart1.revert();
if (chart2) chart2.revert();
}
}
},
color: {
pattern: colors
},
});
for (var i = 1; i < columns.length; i++) {
setTimeout(function(column) {
chart1.load({
columns: [
[columns[column]].concat(data[column])
]
});
chart2.load({
columns: [
[columns[column]].concat(data[column])
]
});
}, (i * 5000 / columns.length), i);
}
document.getElementById("exportButton").onclick = function() {
exportChartToImage();
};
function exportChartToImage() {
var createImagePromise = new Promise(function(resolve, reject) {
var images = [];
d3.selectAll('svg').each(function() {
if (this.parentNode) {
images.push(getSvgImage(this.parentNode, true));
}
});
if (images.length > 0)
resolve(images);
else
reject(images);
});
createImagePromise.then(function(images) {
images.forEach(function(img, n) {
img.toBlob(function(blob) {
saveAs(blob, "image_" + (n + 1) + ".png");
});
});
})
.catch(function(error) {
throw error;
});
};
/**
* Converts a SVG-Chart to a canvas and returns it.
*/
function getSvgImage(svgContainer, png) {
var svgEl = d3.select(svgContainer).select('svg').node();
var svgCopyEl = svgEl.cloneNode(true);
if (!svgCopyEl)
return;
//remove elements not for printing
lensObject = d3.selectAll(".hidden-print").remove().exit();
//add temp document objects
var canvasComputed = d3.select(document.createElement("canvas")).attr("id", "canvasComputed").node();
var container = d3.select(document.createElement("div")).attr("style", "display: none;")
.attr("class", "c3").node();
svgContainer.append(container);
container.append(svgCopyEl);
container.append(canvasComputed);
/* taken from https://gist.github.com/aendrew/1ad2eed6afa29e30d52e#file-exportchart-js
and changed from, angular to D3 functions
*/
/* Take styles from CSS and put as inline SVG attributes so that Canvg
can properly parse them. */
var chartStyle;
if (!C3Styles) {
// Get rules from c3.css
var styleSheets = document.styleSheets;
for (var i = 0; i <= styleSheets.length - 1; i++) {
if (styleSheets[i].href && (styleSheets[i].href.indexOf('c3.min.css') !== -1 || styleSheets[i].href.indexOf('c3.css') !== -1)) {
try {
if (styleSheets[i].rules !== undefined) {
chartStyle = styleSheets[i].rules;
} else {
chartStyle = styleSheets[i].cssRules;
}
break;
}
//Note that SecurityError exception is specific to Firefox.
catch (e) {
if (e.name == 'SecurityError') {
console.log("SecurityError. Cant read: " + styleSheets[i].href);
continue;
}
}
}
if (chartStyle !== null && chartStyle !== undefined) {
C3Styles = {};
var selector;
// Inline apply all the CSS rules as inline
for (i = 0; i < chartStyle.length; i++) {
if (chartStyle[i].type === 1) {
selector = chartStyle[i].selectorText;
var styleDec = chartStyle[i].style;
for (var s = 0; s < styleDec.length; s++) {
C3Styles[styleDec[s]] = styleDec[styleDec[s]];
}
}
}
}
}
}
if (C3Styles)
d3.select(svgCopyEl).selectAll('.c3:not(.c3-chart):not(path)').style(C3Styles);
// SVG doesn't use CSS visibility and opacity is an attribute, not a style property. Change hidden stuff to "display: none"
d3.select(svgCopyEl).selectAll('*')
.filter(function(d) {
return d && d.style && (d.style('visibility') === 'hidden' || d.style('opacity') === '0');
})
.style('display', 'none');
//fix weird back fill
d3.select(svgCopyEl).selectAll("path").attr("fill", "none");
//fix no axes
d3.select(svgCopyEl).selectAll("path.domain").attr("stroke", "black");
//fix no tick
d3.select(svgCopyEl).selectAll(".tick line").attr("stroke", "black");
// transform SVG to canvas using external canvg
canvg(canvasComputed, new XMLSerializer().serializeToString(svgCopyEl));
//remove temp document objects
canvasComputed.remove();
svgCopyEl.remove();
container.remove();
return canvasComputed;
}
<link href="https://cdnjs.cloudflare.com/ajax/libs/c3/0.6.12/c3.min.css" rel="stylesheet" />
<script src="https://d3js.org/d3.v5.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/c3/0.6.12/c3.min.js"></script>
<!-- Required to convert named colors to RGB -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/canvg/1.4/rgbcolor.min.js"></script>
<!-- Optional if you want blur -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/stackblur-canvas/1.4.1/stackblur.min.js"></script>
<!-- Main canvg code -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/canvg/1.5/canvg.js"></script>
<script src="https://fastcdn.org/FileSaver.js/1.1.20151003/FileSaver.min.js"></script>
<div id="chart1" class "c3">
</div>
<div id="chart2" class="c3">
</div>
<button type="button" id="exportButton">
export to SVG
</button>
Unfortunately
document.styleSheets[i].rules
can't be accessed in the script here. It worked in my environment though.
Any idea why this does not work here?

Related

chart.js bar chart datalabel vanishes on hovering over chart

I have a bar chart that has a label over each bar.
The problem is that when I hover on the chart, all of the labels disappear.
Before hovering:
After hovering:
and here's my code:
let barChartData = {
labels:[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29],
datasets: [{
label: 'Sessions',
backgroundColor: 'rgba(0,163,224, 0.7)',
borderColor: 'rgba(0,163,224, 1)',
borderWidth: 1,
data:[81, 69, 60, 51, 37, 35, 45, 65, 86, 58, 64, 39, 48, 29, 69, 80, 52, 61, 56, 40, 51, 31, 70, 51, 32, 51, 27, 30, 44, 59, 46]
}]
};
let ctx = document.getElementById(id).getContext("2d");
let chart = new Chart(ctx, {
type: 'bar',
data: barChartData,
options: {
responsive: true,
"animation": {
"duration": 1,
"onComplete": function () {
let chartInstance = this.chart, ctx = chartInstance.ctx;
ctx.font = Chart.helpers.fontString(Chart.defaults.global.defaultFontSize, Chart.defaults.global.defaultFontStyle, Chart.defaults.global.defaultFontFamily);
ctx.textAlign = 'center';
ctx.textBaseline = 'bottom';
ctx.fontColor = 'black'
this.data.datasets.forEach(function (dataset, i) {
let meta = chartInstance.controller.getDatasetMeta(i);
meta.data.forEach(function (bar, index) {
let data = dataset.data[index];
ctx.fillText(data, bar._model.x, bar._model.y - 5);
});
});
}
},
plugins: {
datalabels: {
color: '#000000'
}
},
legend: {
display: false
},
tooltips: {
enabled: true,
mode: 'label'
},
}
});
I removed the animation field but didn't work.
the chartjs version I'm using is 2.7.3
You can fix the problem with an update to Chart.js 2.9.3 and passing an empty hover function:
<html>
<head>
</head>
<body>
<canvas id="container"></canvas>
<script src="https://pagecdn.io/lib/chart/2.9.3/Chart.bundle.min.js"></script>
</body>
</html>
let barChartData = {
labels:[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29],
datasets: [{
label: 'Sessions',
backgroundColor: 'rgba(0,163,224, 0.7)',
borderColor: 'rgba(0,163,224, 1)',
borderWidth: 1,
data:[81, 69, 60, 51, 37, 35, 45, 65, 86, 58, 64, 39, 48, 29, 69, 80, 52, 61, 56, 40, 51, 31, 70, 51, 32, 51, 27, 30, 44, 59, 46]
}]
};
const id = 'container';
let ctx = document.getElementById(id).getContext("2d");
let chart = new Chart(ctx, {
type: 'bar',
data: barChartData,
options: {
responsive: true,
"animation": {
"duration": 1,
"onComplete": function () {
let chartInstance = this.chart, ctx = chartInstance.ctx;
ctx.font = Chart.helpers.fontString(Chart.defaults.global.defaultFontSize, Chart.defaults.global.defaultFontStyle, Chart.defaults.global.defaultFontFamily);
ctx.textAlign = 'center';
ctx.textBaseline = 'bottom';
ctx.fontColor = 'black'
this.data.datasets.forEach(function (dataset, i) {
let meta = chartInstance.controller.getDatasetMeta(i);
meta.data.forEach(function (bar, index) {
let data = dataset.data[index];
ctx.fillText(data, bar._model.x, bar._model.y - 5);
});
});
}
},
hover: () => {},
plugins: {
datalabels: {
color: '#000000'
}
},
legend: {
display: false
},
tooltips: {
enabled: true,
mode: 'label'
},
}
});
This fix doesn't work with Chart.js 2.7.3. I don't know if it's a bug or a different behavior on purpose.
Live example

How to map json array elements to option selected in drop down list?

I have a json array like this
{Id:[1,2,4,5,8,9,14,22,10,11,12,13,20,21,28,30,31,15,23,32,33,34,35,36,37,38,41,16,24,42,43,48,49,17,25,58,59,61,62,63,64,66,67,68,18,26,69,70,74,75,19,27,82,83,85,86,87,88,89,90,91,108,109,110,111,120]}
and want to select corresponding id when an option is selected from select tag which is also dynamic i.e also a json array which is displayed in select tag option as
<select id="list">
<option>--Select--</option>`enter code here`
<option>List gets updated by the json array</option>
</select>
and I have to do it using jQuery/javascript..any help will be appreciated.
Use Array#map method to iterate and generate option element using jQuery. Attach change() event handler to listen the event.
var data = {
Id: [1, 2, 4, 5, 8, 9, 14, 22, 10, 11, 12, 13, 20, 21, 28, 30, 31, 15, 23, 32, 33, 34, 35, 36, 37, 38, 41, 16, 24, 42, 43, 48, 49, 17, 25, 58, 59, 61, 62, 63, 64, 66, 67, 68, 18, 26, 69, 70, 74, 75, 19, 27, 82, 83, 85, 86, 87, 88, 89, 90, 91, 108, 109, 110, 111, 120]
};
$('#list').append(
data.Id.map(function(v) {
return $('<option/>', {
value: v,
text: v
})
})
).change(function() {
console.log(this.value);
});
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<select id="list">
<option>--Select--</option>
</select>
UPDATE : Since you need to get id based on the option selected update your code as follows. Where you can use Array#indexOf method to get the index of the selected element.
var data = {
"StoreName": ["10001 Main ST", "10002 Part1", "10004 MyStore1", "10005 M STR", "10008 Centro", "10009 MyStore 02", "1001 G", "1001 H", "10010 Store main ROAD", "10011 Central M Store", "10012 En Department", "10013 M Station", "10014 Test Center", "10015 SubStore1", "10016 AA", "10018 M part #", "10019 Test A - 26032016", "1002 B", "1002 I", "10020 Test Central B "],
Id: [1, 2, 4, 5, 8, 9, 14, 22, 10, 11, 12, 13, 20, 21, 28, 30, 31, 15, 23, 32, 3, 34, 35, 36, 37, 38, 41, 16, 24, 42, 43, 48, 49, 1, 25, 58, 59, 61, 62, 63, 4, 66, 67, 68, 18, 26, 69, 70, 74, 75, 19, 27, 82, 8, 85, 86, 87, 88, 89, 90, 1, 108, 109, 110, 111, 10]
};
$('#storenm').append(data.StoreName.map(function(v) {
// generate option with value and text content
return $('<option>', {
text: v,
value: v
})
})).change(function() {
console.log(data.Id[data.StoreName.indexOf(this.value)])
})
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<body>
<select id="storenm" name="name">
<option>--Select--</option>
</select>
</body>

How to print array value to variable?

Here, i have used chartist js for area chart. Each currency having the different value. I have stored each currency value in each variable. After ajax call i added value like that var arrVal = $('#changeCurrency a').attr('id',' ');. But its not pointing currectly. When i given like that var arrVal = dataCur11 means it coming fine. What is the solution for this problem?
HTML
<div id="changeCurrency">
UAE
USD
SGD
INR
POUND
</div>
<div id="graphContainer"></div>
JS
$(document).on('click', '#changeCurrency a', function(){
$.ajax({
url: 'partials/graph.html',
method: 'GET',
success: function (data, supportsForeignObject) {
$('#graphContainer').html('');
$('#graphContainer').append(data);
$('#chart-loader').fadeOut();
var dataCur11 = [50, 53, 54, 52, 56, 55, 53, 50],
dataCur21 = [20, 53, 34, 42, 26, 55, 53, 50],
dataCur31 = [40, 53, 24, 32, 56, 35, 53, 56],
dataCur41 = [50, 33, 54, 30, 22, 20, 30, 52],
dataCur51 = [20, 33, 54, 30, 22, 20, 30, 52];
var arrVal = $('#changeCurrency a').attr('id','');
// Support
var supportsForeignObject = document.implementation.hasFeature("www.http://w3.org/TR/SVG11/feature#Extensibility","1.1");
if(supportsForeignObject) {
var labelsVal = [22+" <span>Dec'14</span>", 24+" <span>Dec'14</span>", 26+" <span>Dec'14</span>", 28+" <span>Dec'14</span>", 30+" <span>Dec'14</span>", 1+" <span>Jan'15</span>", 3+" <span>Jan'15</span>", 5+" <span>Jan'15</span>"]
} else {
var labelsVal = [22+" Dec'14", 24+" Dec'14", 26+" Dec'14", 28+" Dec'14", 30+" Dec'14", 1+" Jan'15", 3+" Jan'15", 5+" Jan'15"];
}
setTimeout(function(){
$('.val-pos').addClass('current-pos');
new Chartist.Line('.ct-chart', {
labels: labelsVal,
series:[arrVal]
}, {
axisY: {
offset: 35,
scaleMinSpace: 60
},
low: 0,
showArea: true
});
}, 1000);
}
});
});
please check the below code ,hope this will help you
$(document).on('click', '#changeCurrency a', function () {
var dataCur11 = [50, 53, 54, 52, 56, 55, 53, 50],
dataCur21 = [20, 53, 34, 42, 26, 55, 53, 50],
dataCur31 = [40, 53, 24, 32, 56, 35, 53, 56],
dataCur41 = [50, 33, 54, 30, 22, 20, 30, 52],
dataCur51 = [20, 33, 54, 30, 22, 20, 30, 52];
var arrVal = eval($(this).attr('id'));
console.log(arrVal);
});
fiddle :http://jsfiddle.net/cdq95qLt/
so your code will be like this
$(document).on('click', '#changeCurrency a', function(){
$.ajax({
url: 'partials/graph.html',
method: 'GET',
success: function (data, supportsForeignObject) {
$('#graphContainer').html('');
$('#graphContainer').append(data);
$('#chart-loader').fadeOut();
var dataCur11 = [50, 53, 54, 52, 56, 55, 53, 50],
dataCur21 = [20, 53, 34, 42, 26, 55, 53, 50],
dataCur31 = [40, 53, 24, 32, 56, 35, 53, 56],
dataCur41 = [50, 33, 54, 30, 22, 20, 30, 52],
dataCur51 = [20, 33, 54, 30, 22, 20, 30, 52];
var arrVal = eval($(this).attr('id'));
// Support
var supportsForeignObject = document.implementation.hasFeature("www.http://w3.org/TR/SVG11/feature#Extensibility","1.1");
if(supportsForeignObject) {
var labelsVal = [22+" <span>Dec'14</span>", 24+" <span>Dec'14</span>", 26+" <span>Dec'14</span>", 28+" <span>Dec'14</span>", 30+" <span>Dec'14</span>", 1+" <span>Jan'15</span>", 3+" <span>Jan'15</span>", 5+" <span>Jan'15</span>"]
} else {
var labelsVal = [22+" Dec'14", 24+" Dec'14", 26+" Dec'14", 28+" Dec'14", 30+" Dec'14", 1+" Jan'15", 3+" Jan'15", 5+" Jan'15"];
}
setTimeout(function(){
$('.val-pos').addClass('current-pos');
new Chartist.Line('.ct-chart', {
labels: labelsVal,
series:arrVal
}, {
axisY: {
offset: 35,
scaleMinSpace: 60
},
low: 0,
showArea: true
});
}, 1000);
}
});
});

Convert buffer to array

I am setting memcached with
$memcached->set("item" , ["1" => "hello"]);
anything work in PHP ,
In Node.js with memcached plugin , I get a buffer instead of array in result
<Buffer 61 3a 25 61 34 3a>
I can not convert such buffer to array
In Node.js :
memcached.get("item" , function(err, data) {
console.log(data);
}
Do you have any way ?
arr = [...buffer]
ES6 introduced a lot of other features, besides buffers.
You can even easily append like this:
arr.push(...buffer)
The ... operator expands enumerables such as arrays and buffers when used in array. It also expands them into separate function arguments.
Yes, it's also faster:
... : x100000: 835.850ms
Slice call from prototype : x100000: 2118.513ms
var array,
buffer = new Buffer([1, 4, 4, 5, 6, 7, 5, 3, 5, 67, 7, 4, 3, 5, 76, 234, 24, 235, 24, 4, 234, 234, 234, 325, 32, 6246, 8, 89, 689, 7687, 56, 54, 643, 32, 213, 2134, 235, 346, 45756, 857, 987, 0790, 89, 57, 5, 32, 423, 54, 6, 765, 65, 745, 4, 34, 543, 43, 3, 3, 3, 34, 3, 63, 63, 35, 7, 537, 35, 75, 754, 7, 23, 234, 43, 6, 247, 35, 54, 745, 767, 5, 3, 2, 2, 6, 7, 32, 3, 56, 346, 4, 32, 32, 3, 4, 45, 5, 34, 45, 43, 43]),
iter = 100000;
array = buffer;
console.time("... : x" + iter);
for (var i = iter; i--;) array = [...buffer]
console.timeEnd("... : x" + iter);
console.time("Apply/call/etc : x" + iter);
for (var i = iter; i--;) array = Array.prototype.slice.call(buffer, 0)
console.timeEnd("Apply/call/etc : x" + iter);
There is another way to convert to array of integers
Using toJSON()
Buffer.from('Text of example').toJSON()
{ type: 'Buffer',data: [ 84, 101, 120, 116, 32, 111, 102, 32, 101, 120, 97, 109, 112, 108, 101 ] }
// simple get data
Buffer.from('Text of example').toJSON().data
[ 84, 101, 120, 116, 32, 111, 102, 32, 101, 120, 97, 109, 112, 108, 101 ]
Example of benchmark
// I took this from #user4584267's answer
const buffer = new Buffer([1, 4, 4, 5, 6, 7, 5, 3, 5, 67, 7, 4, 3, 5, 76, 234, 24, 235, 24, 4, 234, 234, 234, 325, 32, 6246, 8, 89, 689, 7687, 56, 54, 643, 32, 213, 2134, 235, 346, 45756, 857, 987, 0790, 89, 57, 5, 32, 423, 54, 6, 765, 65, 745, 4, 34, 543, 43, 3, 3, 3, 34, 3, 63, 63, 35, 7, 537, 35, 75, 754, 7, 23, 234, 43, 6, 247, 35, 54, 745, 767, 5, 3, 2, 2, 6, 7, 32, 3, 56, 346, 4, 32, 32, 3, 4, 45, 5, 34, 45, 43, 43]);
let array = null;
const iterations = 100000;
console.time("...buffer");
for (let i = iterations; i=i-1;) array = [...buffer]
console.timeEnd("...buffer");
console.time("array.prototype.slice.call");
for (let i = iterations; i=i-1;) array = Array.prototype.slice.call(buffer, 0)
console.timeEnd("array.prototype.slice.call");
console.time("toJSON().data");
for (let i = iterations; i=i-1;) array = buffer.toJSON().data
console.timeEnd("toJSON().data");
OUTPUT
...buffer: 559.932ms
array.prototype.slice.call: 1176.535ms
toJSON().data: 30.571ms
or if you want more profesional and custom function in Buffer use this:
Buffer.prototype.toArrayInteger = function(){
if (this.length > 0) {
const data = new Array(this.length);
for (let i = 0; i < this.length; i=i+1)
data[i] = this[i];
return data;
}
return [];
}
Example of benchmark:
const buffer = new Buffer([1, 4, 4, 5, 6, 7, 5, 3, 5, 67, 7, 4, 3, 5, 76, 234, 24, 235, 24, 4, 234, 234, 234, 325, 32, 6246, 8, 89, 689, 7687, 56, 54, 643, 32, 213, 2134, 235, 346, 45756, 857, 987, 0790, 89, 57, 5, 32, 423, 54, 6, 765, 65, 745, 4, 34, 543, 43, 3, 3, 3, 34, 3, 63, 63, 35, 7, 537, 35, 75, 754, 7, 23, 234, 43, 6, 247, 35, 54, 745, 767, 5, 3, 2, 2, 6, 7, 32, 3, 56, 346, 4, 32, 32, 3, 4, 45, 5, 34, 45, 43, 43]);
let array = null;
const iterations = 100000;
console.time("toArrayInteger");
for (let i = iterations; i=i-1;) buffer.toArrayInteger();
console.timeEnd("toArrayInteger");
Ouput:
toArrayInteger: 28.714ms
Note: In the last example I copied a function from Buffer.toJSON and custom it a lite
Here you go:
var buffer = new Buffer([1,2,3])
var arr = Array.prototype.slice.call(buffer, 0)
console.log(arr)
I haven't used memcached so I am not sure just what this buffer represents or what you want to have instead. Sorry. Here is a function to split a buffer up into an array of bytes. More at node.js Buffer docs, hope it helps!
var hex = new Buffer("613a2561343a", "hex");
var l = hex.length; // in bytes
var output = [];
for(var i = 0; i < l; i++){
var char = hex.toString('hex',i,i+1); // i is byte index of hex
output.push(char);
};
console.log(output);
// output: [ '61', '3a', '25', '61', '34', '3a' ]
You can also use Array.from:
memcached.get("item" , function(err, data) {
console.log(Array.from(data));
}
I have a solution, although I am currently trying to find a better one:
function bufToArray(buffer) {
let array = new Array();
for (data of buffer.values()) array.push(data);
return array;
}
EDIT : I found a simpler way:
var buffer = Buffer.from('NodeJS rocks!')
var array = new Function(`return [${Array.prototype.slice.call(buffer, 0)}]`)
But, like someone already said, [...buffer] is faster (and more code efficient).
You can also use new Uint8Array(buffer [, byteOffset [, length]]);
In interent , there was no information about that , but I have found the convert way
In nodejs , I have to use :
var arrayobject = phpjs.unserialize(data.toString());
but , it is very stupid way for getting array , it seem that php serilzie the data when setting memcache .

javascript functions arguments in an array

what I'm trying to do is this
$.each($(".canvas"), function(n) {
var canvas = $(this)[0].getContext("2d");
canvas.drawImage(options[n]);
});
where options would be an array of arguments, but can't figure out how should the array be formatted for it to work
obviously not
['img, 0, 0, 70, 17, 21, 16, 70, 17', 'img, 0, 0, 80, 12, 21, 16, 70, 17']
this neither
[''+img+', 0, 0, 70, 17, 21, 16, 70, 17', ''+img+', 0, 0, 80, 12, 21, 16, 70, 17']
nor this
{0: [img, 0, 0, 70, 17, 21, 16, 70, 17]}
this would not work either
var options = [[img, 0, 0, 70, 17, 21, 16, 70, 17],
[img, 0, 16, 70, 17, 21, 16, 70, 17],
[img, 0, 32, 70, 17, 21, 16, 70, 17],
[img, 0, 48, 70, 17, 21, 16, 70, 17]];
img.onload = function() {
$.each($(".canvas"), function(n) {
var canvas = $(this)[0].getContext("2d");
canvas.drawImage(options[n].join(', '));
});
};
the error is alway Uncaught TypeError: Type error or Uncaught TypeError: Illegal invocation
Use Function.apply.
canvas.drawImage.apply(canvas, options[n]);
Where options[n] looks something like
[img, 0, 0, 70, 17, 21, 16, 70, 17]
Full example:
var options = [[img, 0, 0, 70, 17, 21, 16, 70, 17],
[img, 0, 16, 70, 17, 21, 16, 70, 17],
[img, 0, 32, 70, 17, 21, 16, 70, 17],
[img, 0, 48, 70, 17, 21, 16, 70, 17]];
$(img).load(function()
{
$(".canvas").each(function(n)
{
var canvas = this.getContext('2d');
canvas.drawImage.apply(canvas, options[n]);
});
});
Demo: http://jsfiddle.net/mattball/5EQbC/

Categories