block by shimizu c5c8ea2add273c83995983c540a53f2d

Chart Downloader α

Full Screen

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

未完成。

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

ちょっと後悔している。

動作

downloadSVG

downloadPNG

問題点

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


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

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

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

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

Built with blockbuilder.org

index.html

<!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="//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>

barchart.js

/**
 *
 * @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
}

downloader.js

/**
 *
 * @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
    
}