Commit 02130a912f5df7bc05660672d788693993c79a53

Authored by hitier
2 parents ca2f26ea b54c4edc

Export chart to PNG

CHANGELOG.md
... ... @@ -24,6 +24,10 @@ or major refactoring improvments.
24 24  
25 25 ## Unreleased
26 26  
  27 +## [0.3.pre-4] - 2021-04-26 - Export chart
  28 +### New
  29 +Export chart as png
  30 +
27 31 ## [0.3.pre-3] - 2021-04-20 - Nicer display
28 32 ### New
29 33 Left menu shows active page
... ...
VERSION.txt
1   -0.3.pre-3
  1 +0.3.pre-4
... ...
app/main/static/css/charges.css
... ... @@ -45,7 +45,7 @@
45 45 padding: 0.8em;
46 46 }
47 47  
48   -.charge_chart {
  48 +.svg_chart {
49 49 background-color: #fAfAfA;
50 50 border: 1pt solid black;
51 51 display: inline-block;
... ... @@ -53,10 +53,11 @@
53 53  
54 54 text.legend {
55 55 font-size: 12px;
  56 + font-family: sans-serif;
56 57 }
57 58  
58 59 rect.legend {
59   - stroke: white;
  60 + stroke: grey;
60 61 stroke-width: 0.5pt;
61 62 }
62 63  
... ... @@ -87,3 +88,8 @@ rect.bar:hover {
87 88 max-width: 300px;
88 89 pointer-events: none;
89 90 }
  91 +
  92 +button.export {
  93 + float: right;
  94 + margin-right: 0;
  95 +}
90 96 \ No newline at end of file
... ...
app/main/static/js/charges.js
  1 +/* Round float to two decimals only */
1 2 function roundToTwo(num) {
2 3 return +(Math.round(num + "e+2") + "e-2");
3 4 }
... ... @@ -64,8 +65,14 @@ function build_chart(div_selector, data_url, entity_name, category_type) {
64 65 alert("ALERT ! Every body shall quit the boat, we are sinking ! ALERT !")
65 66 }
66 67  
  68 + // Create a download button inside the div contaning svg chart
  69 + var dl_btn = d3.select(div_selector).append("button")
  70 + .text('png')
  71 + .attr('class', 'export')
  72 + .on('click', download_png)
  73 +
67 74 const svg = d3.select(div_selector).append("svg")
68   - .attr("id", "svg")
  75 + .attr("class", "svg_chart")
69 76 .attr("width", width + margin.left + margin.right)
70 77 .attr("height", height + margin.top + margin.bottom)
71 78 .append("g")
... ... @@ -135,7 +142,7 @@ function build_chart(div_selector, data_url, entity_name, category_type) {
135 142 var addlegend = function (color_scale) {
136 143  
137 144 // add horizontal legend
138   - let reverse_keys = color_scale.domain().reverse();
  145 + let legend_keys = color_scale.domain();
139 146 var legendSpacing = 5;
140 147  
141 148 var legendWrap = svg.append('g')
... ... @@ -143,7 +150,7 @@ function build_chart(div_selector, data_url, entity_name, category_type) {
143 150 .attr('class', 'legendwrap');
144 151  
145 152 var legend = svg.select('.legendwrap').selectAll('.legend')
146   - .data(reverse_keys)
  153 + .data(legend_keys)
147 154 .enter()
148 155 .append('g')
149 156 .attr('class', 'legend');
... ... @@ -152,7 +159,7 @@ function build_chart(div_selector, data_url, entity_name, category_type) {
152 159 .attr('width', legendRectSize)
153 160 .attr('height', legendRectSize)
154 161 .style('fill', color_scale)
155   - .style('stroke', color_scale);
  162 + .attr('class', 'legend');
156 163  
157 164 legend.append('text')
158 165 .attr('class', 'legend')
... ... @@ -387,7 +394,7 @@ function build_chart(div_selector, data_url, entity_name, category_type) {
387 394  
388 395 // Draw Xaxis
389 396 svg.append("g")
390   - .attr("class", "x axis")
  397 + .attr("class", "x_axis")
391 398 .attr("transform", "translate(0," + height + ")")
392 399 .call(xAxis)
393 400 .selectAll("text")
... ... @@ -403,7 +410,7 @@ function build_chart(div_selector, data_url, entity_name, category_type) {
403 410  
404 411 // Draw Yaxis
405 412 svg.append("g")
406   - .attr("class", "y axis")
  413 + .attr("class", "y_axis")
407 414 .call(yAxis)
408 415  
409 416 // Draw horizontal lines
... ...
app/main/static/js/svg_to_png.js 0 → 100644
... ... @@ -0,0 +1,128 @@
  1 +var download_png = function () {
  2 + // This callback is supposed to be called on the click event of a button child of the div containing the svg.
  3 + // We then get the parent of this btn to guess the chart's title, width and heigth
  4 + var chart_div = this.parentNode;
  5 + var chart_title = chart_div.id;
  6 + width = chart_div.offsetWidth;
  7 + height = chart_div.offsetHeight;
  8 +
  9 + // Then we can access the svg contained in that parent
  10 + svg = chart_div.getElementsByTagName('svg')[0]
  11 +
  12 + // Export to string and save
  13 + var svgString = getSVGString(svg);
  14 + svgString2Image(svgString, 2 * width, 2 * height, 'png', save); // passes Blob and filesize String to the callback
  15 +
  16 + function save(dataBlob, filesize) {
  17 + saveAs(dataBlob, chart_title); // FileSaver.js function
  18 + }
  19 +}
  20 +
  21 +
  22 +// Below are the functions that handle actual exporting:
  23 +// getSVGString ( svgNode ) and svgString2Image( svgString, width, height, format, callback )
  24 +function getSVGString(svgNode) {
  25 + svgNode.setAttribute('xlink', 'http://www.w3.org/1999/xlink');
  26 + var cssStyleText = getCSSStyles(svgNode);
  27 + appendCSS(cssStyleText, svgNode);
  28 +
  29 + var serializer = new XMLSerializer();
  30 + var svgString = serializer.serializeToString(svgNode);
  31 + svgString = svgString.replace(/(\w+)?:?xlink=/g, 'xmlns:xlink='); // Fix root xlink without namespace
  32 + svgString = svgString.replace(/NS\d+:href/g, 'xlink:href'); // Safari NS namespace fix
  33 +
  34 + return svgString;
  35 +
  36 + function getCSSStyles(parentElement) {
  37 + var selectorTextArr = [];
  38 +
  39 + // Add Parent element Id and Classes to the list
  40 + selectorTextArr.push('#' + parentElement.id);
  41 + for (var c = 0; c < parentElement.classList.length; c++){
  42 + if (!contains('.' + parentElement.classList[c], selectorTextArr)){
  43 + selectorTextArr.push('.' + parentElement.classList[c]);
  44 + }
  45 + }
  46 +
  47 + // Add Children element Ids and Classes to the list
  48 + var nodes = parentElement.getElementsByTagName("*");
  49 + for (var i = 0; i < nodes.length; i++) {
  50 + var id = nodes[i].id;
  51 + if (!contains('#' + id, selectorTextArr))
  52 + selectorTextArr.push('#' + id);
  53 +
  54 + var classes = nodes[i].classList;
  55 + for (var c = 0; c < classes.length; c++){
  56 + if (!contains('.' + classes[c], selectorTextArr))
  57 + selectorTextArr.push('.' + classes[c]);
  58 + }
  59 + }
  60 +
  61 + // Extract CSS Rules
  62 + var extractedCSSText = "";
  63 + for (var i = 0; i < document.styleSheets.length; i++) {
  64 + var s = document.styleSheets[i];
  65 +
  66 + try {
  67 + if (!s.cssRules) continue;
  68 + } catch (e) {
  69 + if (e.name !== 'SecurityError') throw e; // for Firefox
  70 + continue;
  71 + }
  72 +
  73 + var cssRules = s.cssRules;
  74 + for (var r = 0; r < cssRules.length; r++) {
  75 + var cssRule = cssRules[r]
  76 + if (typeof cssRule.selectorText === 'undefined') {
  77 + continue;
  78 + }
  79 + var classFromSelector = '.'+cssRule.selectorText.split('.')[1]
  80 + if (contains(classFromSelector, selectorTextArr))
  81 + extractedCSSText += cssRule.cssText;
  82 + }
  83 + }
  84 +
  85 +
  86 + return extractedCSSText;
  87 +
  88 + function contains(str, arr) {
  89 + return arr.indexOf(str) === -1 ? false : true;
  90 + }
  91 +
  92 + }
  93 +
  94 + function appendCSS(cssText, element) {
  95 + var styleElement = document.createElement("style");
  96 + styleElement.setAttribute("type", "text/css");
  97 + styleElement.innerHTML = cssText;
  98 + var refNode = element.hasChildNodes() ? element.children[0] : null;
  99 + element.insertBefore(styleElement, refNode);
  100 + }
  101 +}
  102 +
  103 +function svgString2Image(svgString, width, height, format, callback) {
  104 + var format = format ? format : 'png';
  105 +
  106 + var imgsrc = 'data:image/svg+xml;base64,' + btoa(unescape(encodeURIComponent(svgString))); // Convert SVG string to data URL
  107 +
  108 + var canvas = document.createElement("canvas");
  109 + var context = canvas.getContext("2d");
  110 +
  111 + canvas.width = width;
  112 + canvas.height = height;
  113 +
  114 + var image = new Image();
  115 + image.onload = function () {
  116 + context.clearRect(0, 0, width, height);
  117 + context.drawImage(image, 0, 0, width, height);
  118 +
  119 + canvas.toBlob(function (blob) {
  120 + var filesize = Math.round(blob.length / 1024) + ' KB';
  121 + if (callback) callback(blob, filesize);
  122 + });
  123 +
  124 +
  125 + };
  126 +
  127 + image.src = imgsrc;
  128 +}
... ...
app/main/templates/agent.html
... ... @@ -34,7 +34,7 @@
34 34  
35 35 {% block more_scripts %}
36 36 {% include 'd3js-includes.html' %}
37   -<script src="{{ url_for('main.static', filename='js/charges.js', version=config.VERSION) }}" type="text/javascript"></script>
  37 +{% include 'charges-includes.html' %}
38 38 <script>
39 39 build_chart("#projects_chart",
40 40 "{{url_for('main.charge_agent_csv', agent_id=agent.id)}}",
... ...
app/main/templates/project.html
... ... @@ -31,7 +31,7 @@
31 31  
32 32 {% block more_scripts %}
33 33 {% include 'd3js-includes.html' %}
34   -<script src="{{ url_for('main.static', filename='js/charges.js', version=config.VERSION) }}" type="text/javascript"></script>
  34 +{% include 'charges-includes.html' %}
35 35 <script>
36 36 build_chart("#project_services_chart",
37 37 "{{url_for('main.charge_project_csv', project_id=project.id, category='service')}}",
... ...
app/templates/charges-includes.html 0 → 100644
... ... @@ -0,0 +1,7 @@
  1 +<script
  2 + src="https://cdn.rawgit.com/eligrey/canvas-toBlob.js/f1a01896135ab378aa5c0118eadd81da55e698d8/canvas-toBlob.js"></script>
  3 +<script
  4 + src="https://cdn.rawgit.com/eligrey/FileSaver.js/e9d941381475b5df8b7d7691013401e171014e89/FileSaver.min.js"></script>
  5 +<script src="https://d3js.org/d3.v6.min.js"></script>
  6 +<script src="{{ url_for('main.static', filename='js/svg_to_png.js', version=config.VERSION) }}" type="text/javascript"></script>
  7 +<script src="{{ url_for('main.static', filename='js/charges.js', version=config.VERSION) }}" type="text/javascript"></script>
... ...