Search code examples
d3.jsdc.jscrossfilter

How to add icons in dc.js piechart slices instead texts


I have been working with dc.js for a year now. Recently I have been tasked to implement a pie chart as below:

user requirement image

I want to replace the text labels in the pie chart slices with appropriate images.

I saw this implemented in pure d3.js. Can someone help me translate the implementation to dc.js?

http://jsfiddle.net/LLwr4q7s/

pie
    .width(600)
    .height(500)
    .radius(200)
    .innerRadius(120)
    .dimension(disastersDimension)
    .group(disastersGroup)
    .on("filtered", function (chart, filter) {
      var sel = filter;
      let percentage = 0,
        value = 0;
      let disastersBuffer = [];
      totalAmount = 0;

      pie.selectAll("text.pie-slice").text((d) => {
        percentage = dc.utils.printSingleValue(
          ((d.endAngle - d.startAngle) / (2 * Math.PI)) * 100
        );
        disastersBuffer.push({ ...d.data, percentage });
        totalAmount += parseFloat(d.data.value);
      });

      filterPiechart(sel, percentage, totalAmount, disastersBuffer, value);
    })
    .on("renderlet", (chart) => {
      if (!chart.selectAll("g.selected")._groups[0].length) {
        chart.filter(null);
        filterPiechart("", 100, totalAmount, [], 0);
      }
      var arc = chart.radius(250).innerRadius(100);
      console.log(arc);
      var g = chart.selectAll(".pie-slice");

      chart
        .selectAll(".pie-slice")
        .append("image")
        .attr("xlink:href", "img/disasters/Floods.png")
        .attr("transform", "translate(-10,10) rotate(315)")
        .attr("width", "26px")
        .attr("hight", "26px")
        .style("background-color", "white")
        .attr("x", function (d) {
          var bbox = this.parentNode.getBBox();
          return bbox.x;
        })
        .attr("y", function () {
          var bbox = this.parentNode.getBBox();
          return bbox.y;
        });

      g.append("g")
        .append("svg:image")
        .attr("xlink:href", function (d) {
          let filteredImage = self.piedata.find(
            (i) => i.label == d.data.key
          );
          let image = filteredImage ? filteredImage.image : "";
          return image;
        })
        .attr("width", 30)
        .attr("height", 40)
        .attr("x", function (d) {
          var bbox = this.parentNode.getBBox();
          return bbox.x;
        })
        .attr("y", function (d) {
          var bbox = this.parentNode.getBBox();
          return bbox.y;
        });
    })
    .addFilterHandler(function (filters, filter) {
      filters.length = 0; // empty the array
      filters.push(filter);
      return filters;
    });

Solution

  • First, apologies for an incomplete example, but I ran out of time and I think this shows the principles.

    1. I agree with @MichaelRovinsky that SVG icons would be better than images, but I couldn't find a CDN for SVG icons that would be suitable for the example, and I think the principles are exactly the same, since you could embed SVGs as image just as well.

    2. Using placeimg.com for this purpose leads to weird results because the same URL will yield different results when read twice, so e.g. two slices may end up with the same image, and images change when the chart redraws.

    Luckily these are both beside the point of customizing dc.js!

    Adding things to dc.js pie slices

    It would be nice if dc.js used an svg g group element to put the text in. Then we could just add to it and the position would be correct.

    Instead, we have to add our image element and read the corresponding data from the pie label to get the placement:

      chart.on('pretransition', chart => {
        let labelg = chart.select('g.pie-label-group');
        let data = labelg.selectAll('text.pie-label').data();
        console.log('data', data);
    

    Then we can add image elements in the same layer/g:

        let pieImage = labelg.selectAll('image.pie-image');
        let arcs = chart._buildArcs();
        pieImage.data(data)
          .join(
            enter => enter.append('image')
                        .attr('class', 'pie-image')
                        .attr('x', -19) 
                        .attr('y', -19))
           .attr('href', d => images[d.data.key === 'Others' ? 'Others' : d.data.key.slice(4)])
           .attr('transform', d => chart._labelPosition(d, arcs));
      });
    

    Notice that the attributes which only need to be set once (on enter) are inside the join call, and the attributes which need to be set every redraw (on update) are outside the join call.

    x and y are negative one half the image size to center the images.

    I used an object to store the URLs but you could use whatever.

    pie with images

    Demo fiddle

    Limitation

    As with any customization of the pie chart, this doesn't account for animations well. The images will move before the animation is complete. If you care, I think I wrote an answer some years ago which dealt with this properly. I can probably dig it up but it was quite complicated and IMHO not worth it.