Dinosaurius
Dinosaurius

Reputation: 8628

How to add titles to multiple pie charts, while having a single legend?

I have this data:

var data = [
  [
    {"piece_value":76.34,name:"AG",group:"G1"},
    {"piece_value":69.05,name:"A3",group:"G1"},
    {"piece_value":275.19,name:"A4",group:"G1"}
  ],
  [
    {"piece_value":69.93,name:"AG",group:"G2"},
    {"piece_value":61.50,name:"A3",group:"G2"},
    {"piece_value":153.31,name:"A4",group:"G2"}
  ],
  [
    {"piece_value":67.48,name:"AG",group:"G3"},
    {"piece_value":58.03,name:"A3",group:"G3"},
    {"piece_value":145.93,name:"A4",group:"G3"}
  ]
];

and I have 3 pie charts (see the code snippet). I want to put the corresponding group values on top of each pie chart, and move a legend to the right side (a vertical legend). The problem is that now I have 3 legends (one per pie chart), but in fact it's always the same legend. Therefore instead of duplicating it 3 times, I want to have a single legend on the right side, while putting group values as titles. How to do this?

var data = [
  [
    {"piece_value":76.34,name:"AG",group:"G1"},
    {"piece_value":69.05,name:"A3",group:"G1"},
    {"piece_value":275.19,name:"A4",group:"G1"}
  ],
  [
    {"piece_value":69.93,name:"AG",group:"G2"},
    {"piece_value":61.50,name:"A3",group:"G2"},
    {"piece_value":153.31,name:"A4",group:"G2"}
  ],
  [
    {"piece_value":67.48,name:"AG",group:"G3"},
    {"piece_value":58.03,name:"A3",group:"G3"},
    {"piece_value":145.93,name:"A4",group:"G3"}
  ]
];

var m = 10,
    r = 90,
    z = d3.scale.category20c();

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

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

var arc = d3.svg.arc()
    .innerRadius(r / 2.2)
    .outerRadius(r/1.2)

svg.selectAll("path")
    .data(pie)
    .enter().append("path")
    .attr("d", arc)
    .style("fill", function(d, i) {
        return z(i);
    });

svg.selectAll("foo")
    .data(pie)
    .enter()
    .append("text")
    .attr("text-anchor", "middle")
    .attr("transform", d => "translate(" + arc.centroid(d) + ")")
    .text(d => d.data.piece_value);
		
svg.selectAll("foo")
    .data(d=>d)
    .enter()
    .append("text")
		.attr("transform", (d,i)=>"translate(" + (-r + 2.5*m + (i * 70)) + "," +  (-r + m) + ")")
		.text(d=>d.name);
		
svg.selectAll("foo")
    .data(d=>d)
    .enter()
    .append("rect")
		.attr("transform", (d,i)=>"translate(" + (-r + m + (i * 70)) + "," +  (-r) + ")")
		.attr("width", 10)
		.attr("height", 10)
		.style("fill", (d, i) => z(i));
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>

Upvotes: 2

Views: 608

Answers (2)

Gerardo Furtado
Gerardo Furtado

Reputation: 102174

The idea behind of having one legend for each donut is that it allows having different data (regarding the categorical variables). However, since you said you want only one legend, I'm assuming that the categorical variables are the same for each chart. That being the case, we can create a new data array...

var legendData = data[0].map(d=>d.name);

... using any index (since, again, the arrays are the same regading the categorical variable), and instead of drawing another SVG, we can simply create the legend with HTML:

var legendDiv = d3.select("#legend");

var legendRow = legendDiv.selectAll("foo")
    .data(legendData)
    .enter()
    .append("div")
    .style("margin-bottom", "2px");

legendRow.append("div")
    .html("&nbsp")
    .attr("class", "rect")
    .style("background-color", (d, i) => z(i));

legendRow.append("div")
    .html(d => d);

Here is a demo with the vertical legend:

var data = [
  [
    {"piece_value":76.34,name:"AG",group:"G1"},
    {"piece_value":69.05,name:"A3",group:"G1"},
    {"piece_value":275.19,name:"A4",group:"G1"}
  ],
  [
    {"piece_value":69.93,name:"AG",group:"G2"},
    {"piece_value":61.50,name:"A3",group:"G2"},
    {"piece_value":153.31,name:"A4",group:"G2"}
  ],
  [
    {"piece_value":67.48,name:"AG",group:"G3"},
    {"piece_value":58.03,name:"A3",group:"G3"},
    {"piece_value":145.93,name:"A4",group:"G3"}
  ]
];

var m = 10,
    r = 90,
    z = d3.scale.category20c();

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

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

var arc = d3.svg.arc()
    .innerRadius(r / 2)
    .outerRadius(r/1)

svg.selectAll("path")
    .data(pie)
    .enter().append("path")
    .attr("d", arc)
    .style("fill", function(d, i) {
        return z(i);
    });

svg.selectAll("foo")
    .data(pie)
    .enter()
    .append("text")
    .attr("text-anchor", "middle")
    .attr("transform", d => "translate(" + arc.centroid(d) + ")")
    .text(d => d.data.piece_value);
		
var legendData = data[0].map(d=>d.name);

var legendDiv = d3.select("#legend");

var legendRow = legendDiv.selectAll("foo")
	  .data(legendData)
    .enter()
    .append("div")
		.style("margin-bottom", "2px");

legendRow.append("div")
		.html("&nbsp")
		.attr("class", "rect")
		.style("background-color", (d, i) => z(i));
		
legendRow.append("div")
		.html(d=>d);
		
.rect {
	width: 20px;
	float: left;
	margin-right: 10px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<div id="legend">Legend:</div>

PS: you said that you want the legend at the right hand side. To do that, simply create 2 divs, side by side: on the left one you append the SVGs, and on the right one you append the legend. I'll leave this as a homework for you.

PS2: pay attention to the fact that scale.category20 works on a first-come, first-served basis. So, for the legends to be accurate, the order of the categorical variables in the inner arrays has to be the same.

Upvotes: 1

Hugues Stefanski
Hugues Stefanski

Reputation: 1182

I would simply add a totally separate svg for the legend. Also, unless you need different svg, you might consider appending one group per pie, and one group for the legend (but that's more work with regard to translation). For the sake of performances, I would also stick to a single binding for enter, and store it in local variable. Finally, I would create one group per legend item to avoid duplication of translate code:

var data = [
  [
    {"piece_value":76.34,name:"AG",group:"G1"},
    {"piece_value":69.05,name:"A3",group:"G1"},
    {"piece_value":275.19,name:"A4",group:"G1"}
  ],
  [
    {"piece_value":69.93,name:"AG",group:"G2"},
    {"piece_value":61.50,name:"A3",group:"G2"},
    {"piece_value":153.31,name:"A4",group:"G2"}
  ],
  [
    {"piece_value":67.48,name:"AG",group:"G3"},
    {"piece_value":58.03,name:"A3",group:"G3"},
    {"piece_value":145.93,name:"A4",group:"G3"}
  ]
];

var m = 10,
    r = 90,
    z = d3.scale.category20c();

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

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

var arc = d3.svg.arc()
    .innerRadius(r / 2.2)
    .outerRadius(r/1.2)

svg.selectAll("path")
    .data(pie)
    .enter()
    .append("path")
    .attr("d", arc)
    .style("fill", function(d, i) {
        return z(i);
    });

 var foo = svg.selectAll("foo")
    .data(pie)
    .enter();
 foo.append("text")
    .attr("text-anchor", "middle")
    .attr("transform", d => "translate(" + arc.centroid(d) + ")")
    .text(d => d.data.piece_value);
	//the legend
 var legendGroup =    d3.select("body")
      .append('svg')
      .append('g')
      .classed('legend',true);
 var enterLegend = legendGroup
    .selectAll('.legend-item')
    .data(pie)
    .enter();
 var legendItem = enterLegend
    .append('g')
    .classed('legend-item',true)
    .attr("transform", (d,i)=>"translate(" + (-r + (i * 70)) + "," +  (-r + m) + ")");
 legendItem.append("text")
         .text(d=>d.name);
		
 legendItem.append("rect")
		.attr('x',10)
		.attr("width", 10)
		.attr("height", 10)
		.style("fill", (d, i) => z(i));
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>

I did not run the code, but I hope you get the idea.

Upvotes: 0

Related Questions