javascriptnode.jsd3.jssvgdatejs

d3js with node, how do I add a lines to pie chart?


d3js newbie here. I am using d3js to render an svg on the server. After going over some tutorials I was able to come up with this:

var fs = require('fs');
var d3 = require('d3');
var jsdom = require('jsdom');
var _ = require("underscore");
var chartWidth = 500, chartHeight = 500;

var arc = d3.svg.arc()
    .outerRadius(chartWidth/2 - 10)
    .innerRadius(0);

var colours = ['#98abc5','#8a89a6','#7b6888','#6b486b','#a05d56',"#d0743c", "#ff8c00"];

var pieData = [ 
    {
        "category" : "Category A",
        "amount" : 300
    },
    {
        "category" : "Category B",
        "amount" : 300
    }
];

makePie( pieData );

function makePie( pieData, outputLocation ){
    var amount = _.pluck( pieData, "amount" ); 
    var category = _.pluck( pieData, "category" );

    jsdom.env({
        html:'',
        features:{ QuerySelector:true }, //you need query selector for D3 to work
        done:function(errors, window){
            window.d3 = d3.select(window.document); //get d3 into the dom

            //do yr normal d3 stuff
            var svg = window.d3.select('body')
                .append('div')
                    .attr('class','container') //make a container div to ease the saving process
                .append('svg')
                    .attr({
                        xmlns:'http://www.w3.org/2000/svg',
                        width:chartWidth,
                        height:chartHeight
                    })
                .append('g')
                    .attr('transform','translate(' + chartWidth/2 + ',' + chartWidth/2 + ')');

            svg.selectAll('.arc')
                .data( d3.layout.pie()(amount) )
                    .enter()
                .append('path')
                    .attr({
                        'class':'arc',
                        'd':arc,
                        'fill':function(d,i){
                            return colours[i];
                        },
                        'stroke':'#fff'
                    });
            //write out the children of the container div
            fs.writeFileSync("test.svg", window.d3.select('.container').html()) //using sync to keep the code simple

        }
    });
}  

Although this renders the SVG chart, I do not know how to add lines to the chart which will show which slice belongs to which category.

How do I do that?


Solution

  • Edit/Update (note: written by 'thudfactor') - this is how to add the labels/lines

    var data = [{
        label: 'Star Wars',
        instances: 207
    }, {
        label: 'Lost In Space',
        instances: 3
    }, {
        label: 'the Boston Pops',
        instances: 20
    }, {
        label: 'Indiana Jones',
        instances: 150
    }, {
        label: 'Harry Potter',
        instances: 75
    }, {
        label: 'Jaws',
        instances: 5
    }, {
        label: 'Lincoln',
        instances: 1
    }];
    
    svg = d3.select("svg");
    canvas = d3.select("#canvas");
    art = d3.select("#art");
    labels = d3.select("#labels");
    
    // Create the pie layout function.
    // This function will add convenience
    // data to our existing data, like 
    // the start angle and end angle
    // for each data element.
    jhw_pie = d3.layout.pie()
    jhw_pie.value(function (d, i) {
        // Tells the layout function what
        // property of our data object to
        // use as the value.
        return d.instances;
    });
    
    // Store our chart dimensions
    cDim = {
        height: 500,
        width: 500,
        innerRadius: 50,
        outerRadius: 150,
        labelRadius: 175
    }
    
    // Set the size of our SVG element
    svg.attr({
        height: cDim.height,
        width: cDim.width
    });
    
    // This translate property moves the origin of the group's coordinate
    // space to the center of the SVG element, saving us translating every
    // coordinate individually. 
    canvas.attr("transform", "translate(" + (cDim.width / 2) + "," + (cDim.width / 2) + ")");
    
    pied_data = jhw_pie(data);
    
    // The pied_arc function we make here will calculate the path
    // information for each wedge based on the data set. This is 
    // used in the "d" attribute.
    pied_arc = d3.svg.arc()
        .innerRadius(50)
        .outerRadius(150);
    
    // This is an ordinal scale that returns 10 predefined colors.
    // It is part of d3 core.
    pied_colors = d3.scale.category10();
    
    // Let's start drawing the arcs.
    enteringArcs = art.selectAll(".wedge").data(pied_data).enter();
    
    enteringArcs.append("path")
        .attr("class", "wedge")
        .attr("d", pied_arc)
        .style("fill", function (d, i) {
        return pied_colors(i);
    });
    
    // Now we'll draw our label lines, etc.
    enteringLabels = labels.selectAll(".label").data(pied_data).enter();
    labelGroups = enteringLabels.append("g").attr("class", "label");
    labelGroups.append("circle").attr({
        x: 0,
        y: 0,
        r: 2,
        fill: "#000",
        transform: function (d, i) {
            centroid = pied_arc.centroid(d);
            return "translate(" + pied_arc.centroid(d) + ")";
        },
            'class': "label-circle"
    });
    
    // "When am I ever going to use this?" I said in 
    // 10th grade trig.
    textLines = labelGroups.append("line").attr({
        x1: function (d, i) {
            return pied_arc.centroid(d)[0];
        },
        y1: function (d, i) {
            return pied_arc.centroid(d)[1];
        },
        x2: function (d, i) {
            centroid = pied_arc.centroid(d);
            midAngle = Math.atan2(centroid[1], centroid[0]);
            x = Math.cos(midAngle) * cDim.labelRadius;
            return x;
        },
        y2: function (d, i) {
            centroid = pied_arc.centroid(d);
            midAngle = Math.atan2(centroid[1], centroid[0]);
            y = Math.sin(midAngle) * cDim.labelRadius;
            return y;
        },
            'class': "label-line"
    });
    
    textLabels = labelGroups.append("text").attr({
        x: function (d, i) {
            centroid = pied_arc.centroid(d);
            midAngle = Math.atan2(centroid[1], centroid[0]);
            x = Math.cos(midAngle) * cDim.labelRadius;
            sign = (x > 0) ? 1 : -1
            labelX = x + (5 * sign)
            return labelX;
        },
        y: function (d, i) {
            centroid = pied_arc.centroid(d);
            midAngle = Math.atan2(centroid[1], centroid[0]);
            y = Math.sin(midAngle) * cDim.labelRadius;
            return y;
        },
            'text-anchor': function (d, i) {
            centroid = pied_arc.centroid(d);
            midAngle = Math.atan2(centroid[1], centroid[0]);
            x = Math.cos(midAngle) * cDim.labelRadius;
            return (x > 0) ? "start" : "end";
        },
            'class': 'label-text'
    }).text(function (d) {
        return d.data.label
    });
    
    alpha = 0.5;
    spacing = 12;
    
    function relax() {
        again = false;
        textLabels.each(function (d, i) {
            a = this;
            da = d3.select(a);
            y1 = da.attr("y");
            textLabels.each(function (d, j) {
                b = this;
                // a & b are the same element and don't collide.
                if (a == b) return;
                db = d3.select(b);
                // a & b are on opposite sides of the chart and
                // don't collide
                if (da.attr("text-anchor") != db.attr("text-anchor")) return;
                // Now let's calculate the distance between
                // these elements. 
                y2 = db.attr("y");
                deltaY = y1 - y2;
                
                // Our spacing is greater than our specified spacing,
                // so they don't collide.
                if (Math.abs(deltaY) > spacing) return;
                
                // If the labels collide, we'll push each 
                // of the two labels up and down a little bit.
                again = true;
                sign = deltaY > 0 ? 1 : -1;
                adjust = sign * alpha;
                da.attr("y",+y1 + adjust);
                db.attr("y",+y2 - adjust);
            });
        });
        // Adjust our line leaders here
        // so that they follow the labels. 
        if(again) {
            labelElements = textLabels[0];
            textLines.attr("y2",function(d,i) {
                labelForLine = d3.select(labelElements[i]);
                return labelForLine.attr("y");
            });
            setTimeout(relax,20)
        }
    }
    
    relax();
    .label-text {
        alignment-baseline: middle;
        font-size: 12px;
        font-family: arial,helvetica,"sans-serif";
        fill: #393939;
    }
    .label-line {
        stroke-width: 1;
        stroke: #393939;
    }
    .label-circle {
        fill: #393939;
    }
    <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
    <svg>
        <g id="canvas">
            <g id="art" />
            <g id="labels" /></g>
    </svg>

    Do you mean something like this?

    var w = 400;
    var h = 400;
    var r = h / 2;
    var color = d3.scale.category20c();
    
    var data = [{
      "label": "Category A",
      "value": 20
    }, {
      "label": "Category B",
      "value": 50
    }, {
      "label": "Category C",
      "value": 30
    }];
    
    
    var vis = d3.select('#pie').append("svg:svg").data([data]).attr("width", w).attr("height", h).append("svg:g").attr("transform", "translate(" + r + "," + r + ")");
    var pie = d3.layout.pie().value(function(d) {
      return d.value;
    });
    var arc = d3.svg.arc().outerRadius(r);
    var arcs = vis.selectAll("g.slice").data(pie).enter().append("svg:g").attr("class", "slice");
    arcs.append("svg:path")
      .attr("fill", function(d, i) {
        return color(i);
      })
      .attr("d", function(d) {
        return arc(d);
      });
    
    // add the text
    arcs.append("svg:text").attr("transform", function(d) {
      d.innerRadius = 0;
      d.outerRadius = r;
      return "translate(" + arc.centroid(d) + ")";
    }).attr("text-anchor", "middle").text(function(d, i) {
      return data[i].label;
    });
    <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
    <div id="pie">
    
    </div>