Loop over JSON data to create d3 pie charts

2019-07-16 04:01发布

问题:

I've succeeded in creating my first pie chart with d3 and JSON, but I'm struggling to get more than one pie chart to appear. I've reviewed numerous examples, including Mike Bostock's donut-multiples, and it appears I need a for-each loop in my code.

Here's a simplified version of my program which produces one pie chart instead of two:

<!doctype html>
<html>
<head>
    <title>Pie Chart Test</title>
    <meta charset="UTF-8">
    <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.5/d3.min.js"></script>
</head>

<style>
.arc path {
    stroke: white;
}
</style>

<body>
    <script>
        var width = 300,
            height = 300,
            radius = Math.min(width, height) / 2;

        var arc = d3.svg.arc()
            .outerRadius(radius)
            .innerRadius(radius - (radius / 2));

        var pie = d3.layout.pie()
            .sort(null)
            .value(function(d) { return d.ratio; });

        var svg = d3.select("body").append("svg")
            .attr("width", width)
            .attr("height", height)
            .append("g")
            .attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");

        d3.json("data.json", function(error, data) {
            if (error) throw error;

            node = data.data[0].pie1.results;

            var g = svg.selectAll(".arc")
                .data(pie(node))
                .enter().append("g")
                .attr("class", "arc")

                g.append("path")
                    .attr("d", arc)
                    .style("fill", function(d) { 
                        if (d.data.failures > 0) {
                            return "#d63535";
                        } else {
                            return "#22c12d";
                        }
                    });
        });
    </script>
</body>
</html>

and here is the JSON that populates the chart:

    {"data":[
    {"pie1": {
        "results": [
            {"ratio": 0.04, "total": 7, "failures": 1},
            {"ratio": 0.04, "total": 8, "failures": 0},
            {"ratio": 0.04, "total": 9001, "failures": 0}
        ]}
    },
    {"pie2": {
        "results": [
            {"ratio": 0.04, "total": 10, "failures": 0},
            {"ratio": 0.04, "total": 11, "failures": 1},
            {"ratio": 0.04, "total": 12, "failures": 1}
        ]}
    }
]}

回答1:

it appears I need a for-each loop in my code

Generally speaking, for each loops can be avoided in d3. Most cannonical examples of d3 avoid use of them.

Instead of a for-each loop, you can use a standard d3 enter selection to create an svg element for each item in your data array (ie one svg for each pie chart). This binds the data for each pie chart to its respective svg:

    var svg = d3.select("body").selectAll("svg")
       .data(data.data)
       .enter()
       .append("svg")
       .attr("width", width)
       .attr("height", height)
       .append("g")
       .attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");

Now you can create pie charts for each svg using the bound data:

    var g = svg.selectAll(".arc")
        .data(function(d,i) { return pie(d["pie"+(i+1)].results) })
        .enter().append("g")
        .attr("class", "arc")

        g.append("path")
         .attr("d", arc)
         .style("fill", function(d) { return (d.data.failures > 0) ? "#d63535" : "#22c12d"; });

All together this looks like:

var data  = {"data":[
    {"pie1": {
        "results": [
            {"ratio": 0.04, "total": 7, "failures": 1},
            {"ratio": 0.04, "total": 8, "failures": 0},
            {"ratio": 0.04, "total": 9001, "failures": 0}
        ]}
    },
    {"pie2": {
        "results": [
            {"ratio": 0.04, "total": 10, "failures": 0},
            {"ratio": 0.04, "total": 11, "failures": 1},
            {"ratio": 0.04, "total": 12, "failures": 1}
        ]}
    }
]};
	
	     var width = 300,
            height = 300,
            radius = Math.min(width, height) / 2;

        var arc = d3.svg.arc()
            .outerRadius(radius)
            .innerRadius(radius - (radius / 2));

        var pie = d3.layout.pie()
            .sort(null)
            .value(function(d) { return d.ratio; });

        var svg = d3.select("body").selectAll("svg")
		   .data(data.data)
		   .enter()
		   .append("svg")
           .attr("width", width)
           .attr("height", height)
           .append("g")
           .attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");

	    var g = svg.selectAll(".arc")
            .data(function(d,i) { return pie(d["pie"+(i+1)].results) })
            .enter().append("g")
            .attr("class", "arc")

            g.append("path")
             .attr("d", arc)
             .style("fill", function(d) { return (d.data.failures > 0) ? "#d63535" : "#22c12d"; });
.arc path {
    stroke: white;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.5/d3.min.js"></script>

Of course, your data structure does complicate things a little. I've kept it in the snippet above, but changing it a little would help simplify the code:

var data = [
       {
	    "name":"pie1",
        "results": [
            {"ratio": 0.04, "total": 7, "failures": 1},
            {"ratio": 0.04, "total": 8, "failures": 0},
            {"ratio": 0.04, "total": 9001, "failures": 0}
			]
		},
       {
	    "name":"pie2",
        "results": [
            {"ratio": 0.04, "total": 10, "failures": 0},
            {"ratio": 0.04, "total": 11, "failures": 1},
            {"ratio": 0.04, "total": 12, "failures": 1}
        ]
		}
       ]
	
	     var width = 300,
            height = 300,
            radius = Math.min(width, height) / 2;

        var arc = d3.svg.arc()
            .outerRadius(radius)
            .innerRadius(radius - (radius / 2));

        var pie = d3.layout.pie()
            .sort(null)
            .value(function(d) { return d.ratio; });

        var svg = d3.select("body").selectAll("svg")
		   .data(data)
		   .enter()
		   .append("svg")
           .attr("width", width)
           .attr("height", height)
           .append("g")
           .attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");

	    var g = svg.selectAll(".arc")
            .data(function(d,i) { return pie(d.results) })
            .enter().append("g")
            .attr("class", "arc")

            g.append("path")
             .attr("d", arc)
             .style("fill", function(d) { return (d.data.failures > 0) ? "#d63535" : "#22c12d"; });
.arc path {
    stroke: white;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.5/d3.min.js"></script>

This second snippet doesn't require accessing a unique property in each element in your data array. If you have a unique identifier for each data element, just assign it to a commonly named property for each item in the data array.

You don't have to bind the data to multiple svg elements, you could use multiple g elements too (for example).



回答2:

You have code node = data.data[0].pie1.results;

but you do nothing with pie2.

If you want it hardcoded, you could have a node2 = data.data[0].pie2.results;, but it would be better to try and do it programatically.

Something like

for( pie in data.data[0] ){
    node = pie.results;

    var g = svg.selectAll(".arc")
        .data(pie(node))
        .enter().append("g")
        .attr("class", "arc")
    g.append("path")
        .attr("d", arc)
        .style("fill", function(d) { 
            if (d.data.failures > 0) {
                return "#d63535";
            } else {
                return "#22c12d";
        }
    });
}