0

I would like to implement a simple download/export function that will convert and save a rendered svg word cloud as a png in my Angular app.

I am making use of d3 word cloud generator done by Jason Davies and a simplified script by Julien Renaux.

I am trying to add a simple export feature, using iweczek's save-svg-as-an-image export function, but somewhere I am missing something.

This is my Angular WordCloud Directive, including the export function at the bottom (scope.exportToPNG):

'use strict';

/**
 * @ngdoc: function
 * @name: portalDashboardApp.directive:ogWordCloud
 * @description: Word Cloud Generator based on the d3 word cloud generator done by Jason Davies and a simplified script by Julien Renaux
 * Directive of the portalDashboardApp
 */

angular.module('portalDashboardApp')
  .directive('ogWordCloud', function () {
      return {
          restrict: 'E',
          replace: true,
          templateUrl: './socialMedia.module/socialMedia.templates/WordCloudTemplate.html',
          scope: {
              words: '='
          },
          link: function (scope) {

              var fill = d3.scale.category20b();

              var w = window.innerWidth - 238,
                h = 400;

              var max,
                fontSize;

              var layout = d3.layout.cloud()
                .timeInterval(Infinity)
                .size([w, h])
                .fontSize(function (d) {
                    return fontSize(+d.value);
                })
                .text(function (d) {
                    return d.key;
                })
                .on("end", draw);

              var svg = d3.select("#wordCloudVisualisation").append("svg")
                .attr("width", w)
                .attr("height", h)
                .attr("xmlns", 'http://www.w3.org/2000/svg')
                .attr("xmlns:xlink", 'http://www.w3.org/1999/xlink')
                .attr("version", '1.1')
                .attr("id", "wordCloudSVG");

              var wordCloudVisualisation = svg.append("g").attr("transform", "translate(" + [w >> 1, h >> 1] + ")");

              update();

              window.onresize = function (event) {
                  update();
              };

              var tags = [];

              scope.$watch('words', function () {
                  tags = scope.words;
              }, true);

              function draw(data, bounds) {
                  var w = window.innerWidth - 238,
                    h = 400;

                  svg.attr("width", w).attr("height", h);

                  var scale = bounds ? Math.min(
                    w / Math.abs(bounds[1].x - w / 2),
                    w / Math.abs(bounds[0].x - w / 2),
                    h / Math.abs(bounds[1].y - h / 2),
                    h / Math.abs(bounds[0].y - h / 2)) / 2 : 1;

                  var text = wordCloudVisualisation.selectAll("text")
                    .data(data, function (d) {
                        return d.text.toLowerCase();
                    });
                  text.transition()
                    .duration(1000)
                    .attr("transform", function (d) {
                        return "translate(" + [d.x, d.y] + ")rotate(" + d.rotate + ")";
                    })
                    .style("font-size", function (d) {
                        return d.size + "px";
                    });
                  text.enter().append("text")
                    .attr("text-anchor", "middle")
                    .attr("transform", function (d) {
                        return "translate(" + [d.x, d.y] + ")rotate(" + d.rotate + ")";
                    })
                    .style("font-size", function (d) {
                        return d.size + "px";
                    })
                    .style("opacity", 1e-6)
                    .transition()
                    .duration(1000)
                    .style("opacity", 1);
                  text.style("font-family", function (d) {
                      return d.font;
                  })
                    .style("fill", function (d) {
                        return fill(d.text.toLowerCase());
                    })
                    .text(function (d) {
                        return d.text;
                    });

                  wordCloudVisualisation.transition().attr("transform", "translate(" + [w >> 1, h >> 1] + ")scale(" + scale + ")");
              }

              function update() {
                  layout.font('impact').spiral('archimedean');
                  fontSize = d3.scale['sqrt']().range([10, 100]);
                  if (scope.words.length) {
                      fontSize.domain([+scope.words[scope.words.length - 1].value || 1, +scope.words[0].value]);
                  }
                  layout.stop().words(scope.words).start();
              }

//////////////////////////////////////////////////////////////////

              scope.exportToPNG = function () {

                  var html = d3.select("svg") //svg
                        .attr("version", 1.1)
                        .attr("xmlns", "http://www.w3.org/2000/svg")
                        .node().parentNode.innerHTML;

                  var imgsrc = 'data:image/svg+xml;base64,' + btoa(html);
                  var img = '<img src="' + imgsrc + '">';
                  d3.select("#svgdataurl").html(img);


                  var canvas = document.querySelector("canvas"),
                      context = canvas.getContext("2d");

                  var image = new Image;
                  image.src = imgsrc;
                  image.onload = function () {
                      context.drawImage(image, 0, 0);

                      var canvasdata = canvas.toDataURL("image/png");

                      var pngimg = '<img src="' + canvasdata + '">';
                      d3.select("#pngdataurl").html(pngimg);

                      var a = document.createElement("a");
                      a.download = "sample.png";
                      a.href = canvasdata;
                      a.click();
                  };
              }
          }
      };
  });

This is my directive template:

<div id="wordCloud">
    <button class="basicButton" ng-click="exportToPNG()">Export to .PNG</button>
    <div id="wordCloudVisualisation"></div>
    <canvas id="canvas"></canvas>
    <h2>svgdataurl</h2>
    <div id="svgdataurl"></div>
    <h2>pngdataurl</h2>
    <div id="pngdataurl"></div>
</div>

As is, the code generates an broken image (like when an image is missing from a placeholder on a website) in Chrome. In IE it creates an image in my "svgdataurl" but bombs out. I believe it is not compatible with IE.

MERose
  • 4,048
  • 7
  • 53
  • 79
onmyway
  • 1,435
  • 3
  • 29
  • 53

2 Answers2

2

I was able to fix solve my issues, thanks to the following posts:

Save inline SVG as JPEG/PNG/SVG

https://gist.github.com/gustavohenke/9073132

Here is my complete working solution:

My Angular directive:

'use strict';

/**
 * @ngdoc: function
 * @name: portalDashboardApp.directive:ogWordCloud
 * @description: Word Cloud Generator based on the d3 word cloud generator done by Jason Davies and a simplified script by Julien Renaux
 * Directive of the portalDashboardApp
 */

angular.module('portalDashboardApp')
  .directive('ogWordCloud', function () {
      return {
          restrict: 'E',
          replace: true,
          templateUrl: './socialMedia.module/socialMedia.templates/WordCloudTemplate.html',
          scope: {
              words: '='
          },
          link: function (scope) {

              var fill = d3.scale.category20b();

              var w = window.innerWidth - 238,
                h = 400;

              var max,
                fontSize;

              var layout = d3.layout.cloud()
                .timeInterval(Infinity)
                .size([w, h])
                .fontSize(function (d) {
                    return fontSize(+d.value);
                })
                .text(function (d) {
                    return d.key;
                })
                .on("end", draw);

              var svg = d3.select("#wordCloudVisualisation").append("svg")
                .attr("width", w)
                .attr("height", h)
                .attr("xmlns", 'http://www.w3.org/2000/svg')
                .attr("xmlns:xlink", 'http://www.w3.org/1999/xlink')
                .attr("version", '1.1')
                .attr("id", "wordCloudSVG");

              var wordCloudVisualisation = svg.append("g").attr("transform", "translate(" + [w >> 1, h >> 1] + ")");

              update();

              window.onresize = function (event) {
                  update();
              };

              var tags = [];

              scope.$watch('words', function () {
                  tags = scope.words;
              }, true);

              function draw(data, bounds) {
                  var w = window.innerWidth - 238,
                    h = 400;

                  svg.attr("width", w).attr("height", h);

                  var scale = bounds ? Math.min(
                    w / Math.abs(bounds[1].x - w / 2),
                    w / Math.abs(bounds[0].x - w / 2),
                    h / Math.abs(bounds[1].y - h / 2),
                    h / Math.abs(bounds[0].y - h / 2)) / 2 : 1;

                  var text = wordCloudVisualisation.selectAll("text")
                    .data(data, function (d) {
                        return d.text.toLowerCase();
                    });
                  text.transition()
                    .duration(1000)
                    .attr("transform", function (d) {
                        return "translate(" + [d.x, d.y] + ")rotate(" + d.rotate + ")";
                    })
                    .style("font-size", function (d) {
                        return d.size + "px";
                    });
                  text.enter().append("text")
                    .attr("text-anchor", "middle")
                    .attr("transform", function (d) {
                        return "translate(" + [d.x, d.y] + ")rotate(" + d.rotate + ")";
                    })
                    .style("font-size", function (d) {
                        return d.size + "px";
                    })
                    .style("opacity", 1e-6)
                    .transition()
                    .duration(1000)
                    .style("opacity", 1);
                  text.style("font-family", function (d) {
                      return d.font;
                  })
                    .style("fill", function (d) {
                        return fill(d.text.toLowerCase());
                    })
                    .text(function (d) {
                        return d.text;
                    });

                  wordCloudVisualisation.transition().attr("transform", "translate(" + [w >> 1, h >> 1] + ")scale(" + scale + ")");
              }

              function update() {
                  layout.font('impact').spiral('archimedean');
                  fontSize = d3.scale['sqrt']().range([10, 100]);
                  if (scope.words.length) {
                      fontSize.domain([+scope.words[scope.words.length - 1].value || 1, +scope.words[0].value]);
                  }
                  layout.stop().words(scope.words).start();
              }

              scope.exportToPNG2 = function () {

                  var svg = document.querySelector('#wordCloudSVG'); //svg
                  var canvas = document.createElement("canvas");

                  var svgSize = svg.getBoundingClientRect();
                  canvas.width = svgSize.width;
                  canvas.height = svgSize.height;

                  var ctx = canvas.getContext('2d');
                  var data = new XMLSerializer().serializeToString(svg);

                  var DOMURL = window.URL || window.webkitURL || window;

                  var img = new Image();
                  var svgBlob = new Blob([data], { type: 'image/svg+xml;charset=utf-8' });
                  var url = DOMURL.createObjectURL(svgBlob);

                  img.onload = function () {
                      ctx.drawImage(img, 0, 0);
                      DOMURL.revokeObjectURL(url);

                      var imgURI = canvas
                          .toDataURL('image/png')
                          .replace('image/png', 'image/octet-stream');

                      triggerDownload(imgURI);
                  };

                  img.src = url;
              }

              function triggerDownload(imgURI) {
                  var evt = new MouseEvent('click', {
                      view: window,
                      bubbles: false,
                      cancelable: true
                  });

                  var a = document.createElement('a');
                  a.setAttribute('download', 'MY_COOL_IMAGE.png');
                  a.setAttribute('href', imgURI);
                  a.setAttribute('target', '_blank');

                  a.dispatchEvent(evt);
              }

          }
      };
  });

My directive template:

 <div id="wordCloud">
    <button class="basicButton" ng-click="exportToPNG2()">Export to .PNG</button>
    <div id="wordCloudVisualisation"></div>
    <canvas id="WordCloudCanvas"></canvas>
</div>

I hope this helps someone else!

Community
  • 1
  • 1
onmyway
  • 1,435
  • 3
  • 29
  • 53
0

Do the divs #svgdataurl and #pngdataurl already exist outside your directive? If not, you should include them in your directive template. In the example you're following, they are already present on the page, not created by the click function.

You could also look at a similar tool Mike Bostock just released, which uses canvas.toBlob(): https://github.com/mbostock/svjimmy/blob/master/index.js.

anbnyc
  • 911
  • 6
  • 13
  • Thank you for the response! I appreciate the help. You will see my directive template is actually part of my directive. I don't have an external template file. `template: '
    '` Would you give me aome suggestions on the adding of the missing tags?
    – onmyway Feb 10 '17 at 04:21
  • I have created an external template for my directive. Please see updated question. – onmyway Feb 10 '17 at 05:03