【D3.js】チャートをSVGやPNGでダウンロードできるようにするまでの長い道のり
結論
先に結論だけ書いておきます。
画像にしてダウンロードさせるのは、サーバーサイドでSVGをラスタに変換する方がずーと楽。
目標
・D3.jsで出力したチャートをSVGやPNGでダウンロードできるようにする。
・ダウロードしたチャートにはCSSの内容も反映されていること。
SVG・PNG共通
チャートに適用されているCSSの内容がダウンロードしたファイルにも反映されるように、すべてのパラメータをSVGの属性(atribute)に変換します。
その際、元のチャートには手をつけたくないので、ダウンロード実行時にいったんチャートエレメントをすべてコピーしgetComputedStyleメソッドを使ってチャートを構成する要素の算出スタイルをatributeに変換します。
そのままでは数が多すぎるので、空のSVGを作成して必要なスタイルを抽出するためのフィルターとして使います。
ぶっちゃけこの辺はSVG Crowbarのコードを丸パクリしてます。
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 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
var _emptySvg,_emptySvgDeclarationComputed var _copyChart function createEmptySVG() { _emptySvg = window.document.createElementNS(prefix.svg, 'svg') window.document.body.appendChild(_emptySvg) _emptySvgDeclarationComputed = getComputedStyle(_emptySvg) } function createCopySVG() { _copyChart = d3.select("body") .append("div") .html(svg.innerHTML) .node() } function traverse(obj){ var tree = [] tree.push(obj) visit(obj) function visit(node) { if (node && node.hasChildNodes()) { var child = node.firstChild while (child) { if (child.nodeType === 1 && child.nodeName != 'SCRIPT'){ tree.push(child) visit(child) } child = child.nextSibling } } } return tree } function explicitlySetStyle(element) { var cSSStyleDeclarationComputed = getComputedStyle(element) var attributes = Object.keys(element.attributes).map(function(i){ return element.attributes[i].name } ) var i, len var computedStyleStr = "" for (i=0, len=cSSStyleDeclarationComputed.length; i<len; i++)="" {="" var="" key="cSSStyleDeclarationComputed[i]" value="cSSStyleDeclarationComputed.getPropertyValue(key)" if(!attributes.some(function(k){="" return="" k="==" key})="" &&="" value!="=_emptySvgDeclarationComputed.getPropertyValue(key))" computedstylestr+="key+":"+value+";"" }="" element.setattribute('style',="" computedstylestr)="" createemptysvg()="" createcopysvg()="" allelements="traverse(_copyChart)" コピーしたsvgから全てのエレメントを取り出す="" i="allElements.length" while="" (i--){="" explicitlysetstyle(allelements[i])="" エレメントにcss="" -=""> atributeの変換を適用する } </len;> |
また、svgの中に画像などをimageタグで挿入している場合、data URI スキーム化してsvg内に埋め込む必要があります。
download SVG
SVG要素をXMLSerializerを使ってシリアライズし、さらにblobオブジェクトに変換してダウンロードできる形にしています。
IEの場合はmsSaveBlob関数を使い、それ以外のブラウザの場合はdownload属性を付けたa要素にblobオブジェクトを渡しています。
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 30 |
function downloadSVG(source) { var filename = "chart.svg"; var svg = d3.select(source).select("svg") .attr("xmlns", prefix.svg) .attr("version", "1.1") .node() var blobObject = new Blob([doctype + (new XMLSerializer()).serializeToString(svg)], { "type" : "text/xml" }) if (navigator.appVersion.toString().indexOf('.NET') > 0){ //IE hack window.navigator.msSaveBlob(blobObject, filename) }else { var url = window.URL.createObjectURL(blobObject) var a = d3.select("body").append("a") a.attr("class", "downloadLink") .attr("download", "chart.svg") .attr("href", url) .text("test") .style("display", "none") a.node().click() setTimeout(function() { window.URL.revokeObjectURL(url) a.remove() }, 10) } } |
・問題点
safariでは、a要素のdownload属性が実装されていないため、この方法だとページ遷移が発生してダウンロードされません。(ファイルがブラウザ上で開かれるます)
downloadPNG
SVGとしてダウンロードさせるより一手間かかります。
まず、シリアライズしたSVGをData URI schemeに変換しimgオブジェクトに読み込ませ、それをcanvasにdrawImage使って転写することでラスタライズします。
その後、canvasのtoDataURLメソッドを使って再度Data URI schemeに変換し、画像としてダウンロードさせます。
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 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
function downloadPNG(source) { var filename = "chart.png"; var svg = d3.select(source).select("svg") .attr("xmlns", prefix.svg) .attr("version", "1.1") .node() var data_uri = "data:image/svg+xml;utf8," + encodeURIComponent( (new XMLSerializer()).serializeToString(svg) ) var canvas = d3.select("body").append("canvas") .attr("width", w) .attr("height", h) .style("display", "none") var context = canvas.node().getContext("2d") var download = function() { context.drawImage(img, 0, 0) //IE error var url = canvas.node().toDataURL("image/png") var a = d3.select("body").append("a").attr("id", "downloadLink") a.attr("class", "downloadLink") .attr("download", filename) .attr("href", url) .text("test") .style("display", "none") a.node().click() setTimeout(function() { window.URL.revokeObjectURL(url) canvas.remove() a.remove() }, 10) } var img = new Image() img.src = data_uri img.addEventListener('load', download, false) } |
・問題点 safari
downloadSVGと同じく、ダウンロードされずブラウザ上で開かれてしまう。
・問題点 IE
svgをData URI schemeに変換しimgオブジェクトに読み込ませる際、base64に変換する必要があります。
(他ブラウザではそのまま渡せる)
base64に変換するwindow.btoaは、ユニコードに対応していないため元svgに日本語が含まれているとエラーとなります。従って、別途base64 encoderを実装する必要があります。
さらにimgオブジェクトに読み込ませることに成功したとしても、canvasのdrawImageメソッドを使ってimgオブジェクトを転写する際にSecurity Errorがでます。
これについては回避策が無いため、IEのみ別のアプローチが必要となります。
今回はサードパーティライブラリのcanvg.jsを使いIEの時だけSVGをパースしてcamvasに一から描画するという方法を使いました。
以下はIE対策を施したdownloadPNG
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 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 |
function downloadPNG(source) { var filename = "chart.png"; var svg = d3.select(source).select("svg") .attr("xmlns", prefix.svg) .attr("version", "1.1") .node() var data_uri = "data:image/svg+xml;utf8," + encodeURIComponent( (new XMLSerializer()).serializeToString(svg) ) var canvas = d3.select("body").append("canvas") .attr("id", "drawingArea") .attr("width", w) .attr("height", h) .style("display", "none") var context = canvas.node().getContext("2d") var download = function() { if (navigator.appVersion.toString().indexOf('.NET') > 0){ //IE対策 canvg(document.getElementById('drawingArea'), (new XMLSerializer()).serializeToString(svg)) var dataURI2Blob = function(dataURI, dataTYPE) { var binary = atob(dataURI.split(',')[1]), array = []; for(var i = 0; i < binary.length; i++) array.push(binary.charCodeAt(i)); return new Blob([new Uint8Array(array)], {type: dataTYPE}); } var data_uri = canvas.node().toDataURL("image/png") var blobObject = dataURI2Blob(data_uri, "image/png") window.navigator.msSaveBlob(blobObject, filename) }else { context.drawImage(img, 0, 0) var url = canvas.node().toDataURL("image/png") var a = d3.select("body").append("a").attr("id", "downloadLink") a.attr("class", "downloadLink") .attr("download", filename) .attr("href", url) .text("test") .style("display", "none") a.node().click() setTimeout(function() { window.URL.revokeObjectURL(url) canvas.remove() a.remove() }, 10); } } var img = new Image() img.src = data_uri if (navigator.appVersion.toString().indexOf('.NET') > 0){ //IEでonloadが発火しない問題を回避するための処理 d3.select(img).attr("onload", download) }else{ img.addEventListener('load', download, false) } } |
また、imgオブジェクトのonloadイベントが発火しなかったり、データの読み込みが終わる前に発火してしまったりと不安定な部分も。
サンプル
bl.ocks.orgにサンプルを掲載してあります。