Skip to content

Instantly share code, notes, and snippets.

@shimizu
Last active June 1, 2021 08:12
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save shimizu/c5c8ea2add273c83995983c540a53f2d to your computer and use it in GitHub Desktop.
Save shimizu/c5c8ea2add273c83995983c540a53f2d to your computer and use it in GitHub Desktop.
Chart Downloader α
license: gpl-3.0

D3.jsで描画したチャートをダウンロードできるようにするモジュール。 cssで適用したカラーなども反映されるようになっている。

  • iframe越しでは動かないので「open」を押した先で実行してください。

未完成。

サーバー側でレンタリングする方が楽かも。

ちょっと後悔している。

動作

downloadSVG

  • IE 11 ○
  • Chrome ○
  • Firefox ○
  • Safari △

downloadPNG

  • IE ×
  • Chrome ○
  • Firefox ○
  • Safari △

問題点

以下、勘違いして書いているかもしれないので要注意。


  • safari

a要素のDownload属性が使えないので、ダウンロードされずブラウザ上でファイルが開いてしまう。なんかいい方法があったら教えてください。

  • IE 11

image要素のsrcにデータURIスキームとしてsvgを読み込ませるにはbase64にエンコードする必要がある。window.btoaはユニコードに対応していないので、svgに日本語が含まれている場合は別途base64エンコード処理を実装する必要がある。めんどい 頑張ってbase64に変換してimgタグに読みこませても、canvasのdrawImageに渡すとセキュリティエラーがでる。

(今回はcanvgを使って上記問題を回避した。でもあんまり綺麗では無い)

その他、img.onloadの発火がsvgの読み込みが終わる前に発火してたり、そもそも発火しなかったりするので訳がわからないよ。

Built with blockbuilder.org

/**
*
* @module createVBarChart
* @desc セレクター上に棒グラフを描画します。
*/
function createVBarChart(){
"use strict"
var _chartWidth,_chartHeight
var _margin = {top:0, left:0, bottom:0, right:0};
var _x = function(){ return d },
_y = function(){ return d }
var _xScale = d3.scaleBand(),
_yScale = d3.scaleLinear()
var _xScaleDomain, _yScaleDomain,
_xScaleRange, _yScaleRange
var _xScalePaddingInner = 0.1,
_xScalePaddingOuter = 0.5
var _xAxisLabel,_yAxisLabel
var _xAxisGridVisible = false,
_yAxisGridVisible = false
var _xAxisLabelOption = {x:0, y:0, "text-anchor":"middle", "dominant-baseline":"auto"},
_yAxisLabelOption = {x:0, y:0, "text-anchor":"start", "dominant-baseline":"auto"}
var _yTickValues, _xTickValues
var _transitionObject = d3.transition().duration(0)
var _responsive = true
var _dispatch = d3.dispatch("mouseover","mousemove", "mouseout", "click");
function exports(_selection) {
_selection.each(function(_data){
var isHash = function(value) {
return value.toString() === '[object Object]';
}
var isArray = Array.isArray || function(value) {
return value.toString() === '[object Array]';
}
var parentNode = _selection.node()
var selectedSVG = _selection.selectAll("svg")
.data(["dummy"])
var newSVG = selectedSVG.enter().append("svg")
var svg = selectedSVG.merge(newSVG)
svg.attr("class", "vbarChart")
var axisLayer = svg.append("g").classed("axisLayer", true)
var chartLayer = svg.append("g").classed("chartLayer", true)
var parentWidth, parentHeight
main(_data)
if(_responsive) setReSizeEvent()
function main(data) {
setSize()
if(isHash(data)){
var tmp = []
Object.keys(data).forEach(function(key){
tmp.push(data[key])
})
setScale(Array.prototype.concat.apply([], tmp))
} else if (isArray(data)){
setScale(data)
}
if(_yAxisGridVisible) renderYAxisGrid()
if(_xAxisGridVisible) renderXAxisGrid()
renderYAxis()
renderXAxis()
renderYAxisLabel()
renderXAxisLabel()
renderBarChart(data)
}
function setReSizeEvent() {
var resizeTimer;
var interval = Math.floor(1000 / 60 * 10);
window.addEventListener('resize', function (event) {
if (resizeTimer !== false) {
clearTimeout(resizeTimer);
}
resizeTimer = setTimeout(function () {
main(_data)
}, interval);
});
}
function setSize(args) {
parentWidth = parentNode.clientWidth
parentHeight = parentNode.clientHeight
_chartWidth = parentWidth - (_margin.left + _margin.right)
_chartHeight = parentHeight - (_margin.top + _margin.bottom)
svg
.attr("width", parentWidth)
.attr("height", parentHeight)
axisLayer
.attr("width", parentWidth)
.attr("height", parentHeight)
chartLayer
.attr("width", _chartWidth)
.attr("height", _chartHeight)
.attr("transform", "translate("+[_margin.left, _margin.top]+")")
}
function setScale(data){
var xMap = data.map(function(d){ return _x(d) }).sort(function(a, b){ return a -b })
var yMax = d3.max(data, function(d){ return _y(d) })
var yMin = d3.min(data, function(d){ return _y(d) })
if (yMin < 0){
var yExtent = [yMin, yMax]
}else{
var yExtent = [0, yMax]
}
if (!_xScaleDomain) _xScaleDomain = xMap
if (!_yScaleDomain) _yScaleDomain = yExtent
_xScaleRange = [0, _chartWidth]
_yScaleRange = [_chartHeight, 0]
_xScale.domain(_xScaleDomain).paddingInner(_xScalePaddingInner).paddingOuter(_xScalePaddingOuter)
_yScale.domain(_yScaleDomain)
_xScale.range(_xScaleRange)
_yScale.range(_yScaleRange)
}
function renderYAxis() {
var yAxisCall = d3.axisLeft(_yScale)
.tickSizeOuter(0)
if (_yTickValues) yAxisCall.tickValues(_yTickValues)
var yAxis = axisLayer.selectAll(".axis.y")
.data(["dummy"])
var newYAxis = yAxis.enter().append("g")
newYAxis.merge(yAxis)
.transition(_transitionObject)
.attr("transform", "translate("+[_margin.left, _margin.top]+")")
.attr("class", "axis y")
.call(yAxisCall);
}
function renderYAxisGrid() {
var yAxisCall = d3.axisLeft(_yScale)
.tickSizeOuter(0)
.tickSizeInner(-_chartWidth)
.tickFormat(function(d){ return null })
if (_yTickValues) yAxisCall.tickValues(_yTickValues)
var yAxis = axisLayer.selectAll(".grid.y")
.data(["dummy"])
var newYAxis = yAxis.enter().append("g")
.attr("class", "grid y")
newYAxis.merge(yAxis)
.transition(_transitionObject)
.attr("transform", "translate("+[_margin.left, _margin.top]+")")
.call(yAxisCall);
}
function renderXAxis() {
var xAxisCall = d3.axisBottom(_xScale)
.tickSizeOuter(0)
if (_xTickValues) xAxisCall.tickValues(_xTickValues)
var xAxis = axisLayer.selectAll(".axis.x")
.data(["dummy"])
var newXAxis = xAxis.enter().append("g")
newXAxis.merge(xAxis)
.transition(_transitionObject)
.attr("transform", "translate("+[_margin.left, _chartHeight+_margin.top]+")")
.attr("class", "axis x")
.call(xAxisCall)
.each(function(){
d3.select(this).select(".domain")
.attr("transform", "translate("+[0,-_chartHeight + _yScale(0)]+")")
})
}
function renderXAxisGrid() {
var xAxisCall = d3.axisBottom(_xScale)
.tickSizeOuter(0)
.tickSizeInner(-_chartHeight)
.tickFormat(function(d){ return null })
if (_xTickValues) xAxisCall.tickValues(_xTickValues)
var xAxis = axisLayer.selectAll(".grid.x")
.data(["dummy"])
var newXAxis = xAxis.enter().append("g")
newXAxis.merge(xAxis)
.transition(_transitionObject)
.attr("transform", "translate("+[_margin.left, _chartHeight+_margin.top]+")")
.attr("class", "grid x")
.call(xAxisCall);
}
function renderYAxisLabel() {
var yAxisLabel = axisLayer.selectAll(".label.y")
.data(["dummy"])
var newYAxisLabel = yAxisLabel.enter().append("text").attr("class", "label y")
yAxisLabel.merge(newYAxisLabel)
.text(function(d){ return _yAxisLabel })
.attr("x", _yAxisLabelOption.x)
.attr("y", _yAxisLabelOption.y)
.attr("text-anchor", _yAxisLabelOption["text-anchor"])
.attr("dominant-baseline", _yAxisLabelOption["dominant-baseline"])
.attr("transform", "translate("+[_margin.left, _margin.top]+")")
}
function renderXAxisLabel() {
var xAxisLabel = axisLayer.selectAll(".label.x")
.data(["dummy"])
var newXAxisLabel = xAxisLabel.enter().append("text").attr("class", "label x")
xAxisLabel.merge(newXAxisLabel)
.text(function(d){ return _xAxisLabel })
.attr("x", _xAxisLabelOption.x)
.attr("y", _xAxisLabelOption.y)
.attr("text-anchor", _xAxisLabelOption["text-anchor"])
.attr("dominant-baseline", _xAxisLabelOption["dominant-baseline"])
.attr("transform", "translate("+[_chartWidth+_margin.left, _chartHeight+_margin.top]+")")
}
function renderBarChart(data) {
var bar = chartLayer.selectAll(".bar").data(data)
bar.exit().remove()
var newBar = bar.enter().append("rect")
.attr("class", function(d){ return "bar " + _x(d) })
bar.merge(newBar) //選択済みセレクションをenterで追加されるセレクションにマージする
.attr("width", _xScale.bandwidth())
.attr("height", function(d){
var height = Math.abs( _yScale(_y(d)) - _yScale(0) )
return height
})
.attr("transform", function(d){
var y = _yScale(Math.max(0, _y(d)))
return "translate("+[_xScale(_x(d)), y]+")"
})
}
//セレクションにモジュールへのショートカットをつける
_selection._module = exports
})
}
exports.margin = function(_arg) {
if (!arguments.length) return _margin;
Object.keys(_arg).forEach(function(key){
_margin[key] = _arg[key]
})
return this;
}
exports.x = function(_arg) {
if (!arguments.length) return _x;
_x = _arg;
return this;
}
exports.y = function(_arg) {
if (!arguments.length) return _y;
_y = _arg;
return this;
}
exports.xScale = function(_arg) {
if (!arguments.length) return _xScale;
_xScale = _arg;
return this;
}
exports.yScale = function(_arg) {
if (!arguments.length) return _yScale;
_yScale = _arg;
return this;
}
exports.xScaleDomain = function(_arg) {
if (!arguments.length) return _xScaleDomain;
_xScaleDomain = _arg;
return this;
}
exports.yScaleDomain = function(_arg) {
if (!arguments.length) return _yScaleDomain;
_yScaleDomain = _arg;
return this;
}
exports.xScalePaddingInner = function(_arg) {
if (!arguments.length) return _xScalePaddingInner;
_xScalePaddingInner = _arg;
return this;
}
exports.xScalePaddingOuter = function(_arg) {
if (!arguments.length) return _xScalePaddingOuter;
_xScalePaddingOuter = _arg;
return this;
}
exports.xTickValues = function(_arg) {
if (!arguments.length) return _xTickValues;
_xTickValues = _arg;
return this;
}
exports.yTickValues = function(_arg) {
if (!arguments.length) return _yTickValues;
_yTickValues = _arg;
return this;
}
exports.xAxisGridVisible = function(_arg) {
if (!arguments.length) return _xAxisGridVisible;
_xAxisGridVisible = _arg;
return this;
}
exports.yAxisGridVisible = function(_arg) {
if (!arguments.length) return _yAxisGridVisible;
_yAxisGridVisible = _arg;
return this;
}
exports.xAxisLabel = function(_arg) {
if (!arguments.length) return _xAxisLabel;
_xAxisLabel = _arg;
return this;
}
exports.yAxisLabel = function(_arg) {
if (!arguments.length) return _yAxisLabel;
_yAxisLabel = _arg;
return this;
}
exports.xAxisLabelOption = function(_arg) {
if (!arguments.length) return _xAxisLabelOption;
Object.keys(_arg).forEach(function(key){
_xAxisLabelOption[key] = _arg[key]
})
return this;
}
exports.yAxisLabelOption = function(_arg) {
if (!arguments.length) return _yAxisLabelOption;
Object.keys(_arg).forEach(function(key){
_yAxisLabelOption[key] = _arg[key]
})
return this;
}
exports.responsive = function(_arg) {
if (!arguments.length) return _responsive;
_responsive = _arg;
return this;
}
return exports
}
/**
*
* @module createDownloader
* @desc セレクターにダウンロード機能を付加します。
*/
function createDownloader(){
var doctype = '<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">';
var prefix = {
xmlns: "http://www.w3.org/2000/xmlns/",
xlink: "http://www.w3.org/1999/xlink",
svg: "http://www.w3.org/2000/svg"
}
function exports(_selection) {
var svg = _selection.node()
var w = svg.clientWidth, h = svg.clientHeight
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]
var value=cSSStyleDeclarationComputed.getPropertyValue(key)
if(!attributes.some(function(k){ return k === key}) && value!==_emptySvgDeclarationComputed.getPropertyValue(key)) {
computedStyleStr+=key+":"+value+";"
}
}
element.setAttribute('style', computedStyleStr);
}
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){
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);
}
}
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){
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 hack
d3.select(img).attr("onload", download)
}else{
img.addEventListener('load', download, false)
}
}
_selection.downloadSVG = function(){
createEmptySVG()
createCopySVG()
var allElements = traverse(_copyChart)
var i = allElements.length;
while (i--){
explicitlySetStyle(allElements[i]);
}
downloadSVG(_copyChart)
d3.select(_copyChart).remove()
d3.select(_emptySvg).remove()
}
_selection.downloadPNG = function(){
createEmptySVG()
createCopySVG()
var allElements = traverse(_copyChart)
var i = allElements.length;
while (i--){
explicitlySetStyle(allElements[i]);
}
downloadPNG(_copyChart)
d3.select(_copyChart).remove()
d3.select(_emptySvg).remove()
}
}
return exports
}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge"/>
<title></title>
<style>
html, body {
width: 100%;
height: 100%;
padding: 0px;
margin: 0px;
}
#chart {
width: 900px;
height: 450px;
}
.vbarChart .bar {
fill:blue;
}
/* axis */
.vbarChart .axis {
}
.vbarChart .axis .domain {
stroke: #333333;
}
.vbarChart .tick line {
stroke: #333333;
stroke-width: 1px;
}
.vbarChart .tick text {
fill: #333333;
font-size: 14px;
letter-spacing: .05em;
}
/* grid */
.vbarChart .grid line {
stroke: #cccccc;
stroke-dasharray: 3,3;
}
/* label */
.vbarChart .label {
font-size: 12px;
font-weight: normal;
letter-spacing: .05em;
}
</style>
</head>
<body>
<div id="chart"></div>
<button id="downloadSVG">download SVG</button>
<button id="downloadPNG">download PNG</button>
<script src="http://gabelerner.github.io/canvg/canvg.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/d3/4.1.1/d3.min.js"></script>
<script src="barchart.js"></script>
<script src="downloader.js"></script>
<script>
!(function(){
"use strict"
//ランダムデータセット
var data = ["アメリカ", "日本", "フランス", "イギリス", "スイス"].map(function(d){
var r = ~~(Math.random() * 100)
if (Math.random() > 0.2){
var cos = Math.sin(r * Math.PI / 180)
}else{
var cos = -Math.sin(r * Math.PI / 180)
}
var value = cos*100
return {"国名":d, "値":value}
})
//barChartモジュール初期設定
var BarChart = createVBarChart()
.margin({top:40, left:100, bottom:40, right:100})
.x(function(d){ return d["国名"] })
.y(function(d){ return d["値"] })
.xAxisLabel("国名")
.xAxisLabelOption({y:"-0.5em"})
.yAxisGridVisible(true)
.yAxisLabel("値")
.yAxisLabelOption({y:"-0.5em", "text-anchor":"middle"})
//downloaderモジュールのインスタンス生成
var downloader = createDownloader()
//セレクターにbarChartモジュールとdownloaderモジュールを適用
var selector = d3.selectAll("#chart")
.datum(data)
.call(BarChart)
.call(downloader)
d3.select("#downloadSVG").on("click", selector.downloadSVG )
d3.select("#downloadPNG").on("click", selector.downloadPNG )
}())
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment