2系列の棒グラフを作る

以前棒グラフを SVG で保存したやり方で、2系列の棒グラフをつくってみます。東京都の年齢区分別の人口を男女別で2系列の棒グラフにします。使用するデータは下のようなものです。

年齢5歳階級
0~4歳275264
5~9歳264252
10~14歳253242
15~19歳282272
20~24歳438430
25~29歳464447
30~34歳489472
35~39歳517495
40~44歳566543
45~49歳591576
50~54歳518487
55~59歳417393
60~64歳347340
65~69歳391407
70~74歳350400
75~79歳282365
80~84歳201299
85歳以上156338

javascript のコードもほぼそのままで、各年齢区分ごとに2本ずつ棒を描画するため、幅を半分にしたりx座標値を調節し、凡例を加えています。このやりかただと、ただ2回同じことを繰り返しているだけなので、系列が増減するとその都度やりなおしになります。ほんとうはもっといいやり方があるかもしれません。

プロジェクトフォルダの直下に、javascriptファイル、data フォルダ、svg フォルダを配置し、data フォルダから csv ファイルを読み込み、svg フォルダにファイルを保存するものとしています。npm で d3、jsdom、csvtojson のライブラリを導入しておきます。

//ライブラリの読み込み
const d3 = require('d3');
const { JSDOM } = require('jsdom');
const csv=require('csvtojson');

const fs = require('fs');
const path = require('path');


//カレントディレクトリを取得
const dir = process.cwd();
//データディレクトリ
const datadir = path.join(dir, "data");
//データファイル名
const fullPath = path.join(datadir, "tokyo.csv");
//保存先ファイル名
const targetPath = path.format({
                dir: path.join(dir, "svg"),
                name: path.basename(fullPath, path.extname(fullPath)),
                ext: ".svg"
            });

//読み込み元ファイル名と出力先ファイル名を表示させます
console.log(fullPath + "から" + targetPath+ "を作成します");

//マージン設定
const margin = { left:40, right:0, top:25, bottom:40 };

//SVGのサイズ設定
const svgWidth = 262;
const svgHeight = 184;

//グラフのサイズ設定
const chartWidth = svgWidth - margin.left - margin.right;
const chartHeight = svgHeight - margin.top - margin.bottom;


//新規ドキュメント
const document = new JSDOM().window.document;
//SVG要素を追加
const svg = d3.select(document.body)
              .append('svg')
              .attr("xmlns",'http://www.w3.org/2000/svg')
              .attr('width', svgWidth)
              .attr('height', svgHeight);

//タイトルを追加
svg.append("text")
    .attr("x", 0)
    .attr("y", 10)
    .attr("font-size", "10px")
    .attr("text-anchor", "top")
    .attr("font-family", "Tazugane Info Std N")
    .attr("font-weight", 700)
    .text("年齢5歳階級別人口[男女別・東京都]");

svg.append("line")
    .attr("x1", 0)
    .attr("x2", svgWidth)
    .attr("y1", 15)
    .attr("y2", 15)
    .attr("stroke", "#000000")
    .attr("stroke-width", "0.1mm");


//グループ要素の追加
const g = svg.append("g")
        .attr("transform", "translate(" + margin.left + ", " + margin.top + ")");

//背景グレーを追加
g.append("rect")
 .attr("class", "background")
 .attr("x", 0)
 .attr("y", 0)
 .attr("width", chartWidth)
 .attr("height", chartHeight)
 .attr("fill", "#e5e5e5");


//軸ラベルを追加
// X 軸
g.append("text")
    .attr("y", chartHeight + 40)
    .attr("x", chartWidth / 2)
    .attr("font-size", "10px")
    .attr("text-anchor", "middle")
    .attr("font-family", "Tazugane Info Std N")
    .attr("font-weight", 300)
    .text("年齢");

// Y 軸
g.append("text")
    .attr("y", chartHeight/2)
    .attr("x", -30)
    .attr("font-size", "10px")
    .attr("text-anchor", "middle")
    .attr("font-family", "Tazugane Info Std N")
    .attr("font-weight", 300)
    .attr("writing-mode", "tb")
    .text("人口【千人】");


csv()
    .fromFile(fullPath)
    .then((data)=>{
    
        //男女の列の値を数値に変換する
        data.forEach(function(d) {
            d["男"] = +d["男"];
            d["女"] = +d["女"];
        });
    
        console.log(data)
    
        //読み込んだデータをもとにグラフを描画する
        // Xスケール
        const xScale = d3.scaleBand()
            .domain(data.map(function(d){ return d["年齢5歳階級"] }))
            .range([0, chartWidth])
            .padding(0.2);

        // Yスケール
        const yScale = d3.scaleLinear()
            .domain([0, 600])
            .range([chartHeight, 0]);

        // 横軸
        const xAxisCall = d3.axisBottom(xScale)
                            .tickSize(0);
        const xAxis = g.append("g")
            .attr("class", "x axis")
            .attr("transform", "translate(0," + chartHeight +")")
            .call(xAxisCall);

        xAxis.selectAll("path")
                .attr("stroke", "none");
        xAxis.selectAll("text")
            .attr("x", "0")
            .attr("y", "1")
            .attr("text-anchor", "end")
            .attr("font-family", "Tazugane Info Std N")
            .attr("font-weight", 300)
            .attr("font-size", "7px")
            .attr("x", "-1")
            .attr("y", "1")
            .attr("transform", "rotate(-45)");

        // 縦軸
        const yAxisCall = d3.axisLeft(yScale)
                            .tickSize(-chartWidth);
        const yAxis = g.append("g")
            .attr("class", "y axis")
            .call(yAxisCall);

        yAxis.selectAll("line")
                .attr("stroke", "#ffffff")
                .attr("stroke-width", "0.1mm");
        yAxis.selectAll("path")
                .attr("stroke", "none");
        yAxis.selectAll("text")
            .attr("x", "-3")
            .attr("y", "0")
            .attr("text-anchor", "end")
            .attr("font-family", "Tazugane Info Std N")
            .attr("font-weight", 300)
            .attr("font-size", "7px");

        // 1系列めの棒を描く
        const male = g.selectAll("rect.male").data(data);                    

        male.enter()
            .append("rect")
                .attr("class", "male")
                .attr("y", function(d){ return yScale(d["男"]); })
                .attr("x", function(d){ return xScale(d["年齢5歳階級"]); })
                .attr("height", function(d){ return chartHeight - yScale(d["男"]); })
                .attr("width", xScale.bandwidth()/2)
                .attr("fill", "#666666");

        //値のラベルをつける
        const maleValueLabel = g.selectAll("text.maleValue")
            .data(data);

        maleValueLabel.enter()
            .append("text")
            .attr("class", "maleValue")
            .attr("fill", "#ffffff")
            .attr("transform", "rotate(90)")
            .attr("y", function(d){ return -xScale(d["年齢5歳階級"])-0.6; })
            .attr("x", function(d){ return yScale(d["男"])+1; })
            .attr("text-anchor", "top")
            .attr("font-family", "Tazugane Info Std N")
            .attr("font-weight", 500)
            .attr("font-size", "5px")
            .text(function(d){ return d["男"]; });
    
        // 2系列めの棒を描く
        const female = g.selectAll("rect.female").data(data);                    
    
        female.enter()
            .append("rect")
                .attr("class", "female")
                .attr("y", function(d){ return yScale(d["女"]); })
                .attr("x", function(d){ return xScale(d["年齢5歳階級"])+xScale.bandwidth()/2; })
                .attr("height", function(d){ return chartHeight - yScale(d["女"]); })
                .attr("width", xScale.bandwidth()/2)
                .attr("fill", "#999999");
    
        //値のラベルをつける
        const femaleValueLabel = g.selectAll("text.femaleValue")
            .data(data);

        femaleValueLabel.enter()
            .append("text")
            .attr("class", "femaleValue")
            .attr("fill", "#ffffff")
            .attr("transform", "rotate(90)")
            .attr("y", function(d){ return -xScale(d["年齢5歳階級"])-0.6-xScale.bandwidth()/2; })
            .attr("x", function(d){ return yScale(d["女"])+1; })
            .attr("text-anchor", "top")
            .attr("font-family", "Tazugane Info Std N")
            .attr("font-weight", 500)
            .attr("font-size", "5px")
            .text(function(d){ return d["女"]; });
    
        //凡例
       const legend = g.append("g")
                        .attr("class", "legend")
                        .attr("transform", "translate(5, 5)");
        
        const legendRow = legend.append("g");
    
        legendRow.append("rect")
                    .attr("x", 0)
                    .attr("y", 0)
                    .attr("width", 10)
                    .attr("height", 6)
                    .attr("fill", "#666666");
        legendRow.append("text")
                    .attr("x", 11)
                    .attr("y", 5)
                    .attr("fill", "#000000")
                    .attr("text-anchor", "top")
                    .attr("font-family", "Tazugane Info Std N")
                    .attr("font-weight", 500)
                    .attr("font-size", "6px")
                    .text("男");
    
        legendRow.append("rect")
                    .attr("x", 0)
                    .attr("y", 8)
                    .attr("width", 10)
                    .attr("height", 6)
                    .attr("fill", "#999999");
        legendRow.append("text")
                    .attr("x", 11)
                    .attr("y", 13)
                    .attr("fill", "#000000")
                    .attr("text-anchor", "top")
                    .attr("font-family", "Tazugane Info Std N")
                    .attr("font-weight", 500)
                    .attr("font-size", "6px")
                    .text("女");
    
        //グラフの上下をケイ線で区切る
        g.append("line")
            .attr("x1", 0)
            .attr("x2", chartWidth)
            .attr("y1", 0)
            .attr("y2", 0)
            .attr("stroke", "#000000")
            .attr("stroke-width", "0.3mm");
        g.append("line")
            .attr("x1", 0)
            .attr("x2", chartWidth)
            .attr("y1", chartHeight)
            .attr("y2", chartHeight)
            .attr("stroke", "#000000")
            .attr("stroke-width", "0.3mm");
        //描画したグラフをファイルに保存する
        fs.writeFile(targetPath, document.body.innerHTML, (err) => {
            if(err){
                console.log("エラーが発生しました。" + err);
                throw err
            } else {
                console.log(targetPath + "を保存しました");
            }
        });
    
    
    });

下のようなグラフが出来ます。