Render text with nested data in D3 js version 5

Viewed 219

I'm trying to render a concentric pie chart split into four departments from a department store. Each department has a nested products array of product objects. There are three circles, inner, middle, outer which represent high margin, middle margin, low margin. I can create the graph see below, however, I cannot get the product names to render into the correct department because the data is nested. Below is the data and the method to render the high margin circle as well as a screen shot of what I have right now. I am using React which is the .ref in the renderMiddleCircle method

this.data = [
  { 
    id: 'department1',
    name: 'Electronics',
    value: 100,
    products: [
      {
        description: 'latptop computers',
        id: 'product1',
        name: 'Fast Laptop computers',
        margin: 'high',
      },
      {
        description: 'Coaxial Cable',
        id: 'product2',
        name: 'Gold Coaxial Cable',
        margin: 'high',
      },
    ]
  }, 
  { 
    id: 'department2', 
    value: 100,
    name: 'Hardware',
    products: [
      {
        description: 'Power Drill',
        id: 'product1',
        name: '24 volt power drill',
        margin: 'high'
      },
      {
        description: 'Air tool Wrench',
        id: 'product2',
        name: 'Air Compressor Wrench',
        margin: 'high',
      },
    ]
  },
  { 
    id: 'department3', 
    value: 100,
    name: 'Lawn and Garden',
    products: [
      {
        description: '24 Volt Weed eater'
        id: 'product1',
        name: 'Ryobi Weed Eater',
        margin: 'high',
      },
    ]
  },
  { 
    id: 'department4', 
    value: 100,
    name: 'Grocery',
    products: [
      {
        description: 'Milk',
        id: 'product1',
        name: 'Whole Milk',
        margin: 'high',
      },
    ]
  },
];

renderCenterCircle = (productData) => {
  const highMarginData = productData.map(({ data }) => {
    return {
      ...data,
      products: data.products.filter(product => product.margin === 'high')
    }
  });
  const prepHighMarginData = this.createPie(highMarginData);

  const svg = d3
    .select(this.departmentsRef.current)
    .append('svg')
    .attr('width', WIDTH * 2)
    .attr('height', HEIGHT * 2)
    .append('g')
    .attr('transform', `translate(${WIDTH}, ${HEIGHT})`);

  const g = svg
    .selectAll('.arc')
    .data(prepHighMarginData)
    .enter();

  g.append('g')
    .append('path')
    .attr('d', this.createArc(1.45, 0))
    .style('fill', (d, i) => BRAND_COLORS.steel)
    .style('border-color', (d, i) => BRAND_COLORS.steel);

  g.append('g')
    .attr('transform', (d) => {
      console.log(d);
      return `translate(${this.topicArc.centroid(d)})`
  })
    .append('g')
    .datum(d => d.data.products)
    .append('text')
    .text((d, i) => {
      console.log(d);
      return 'd.name'
    })
    .style('fill', '#fff');
}

enter image description here

2 Answers

For each g the datum is a department object (after passing it through d3.pie), if you want each child text object to have the datum department.products you can do so as you are:

.datum(data.products)

However, now the datum is an array, so you can't access d.name, instead you could do something like:

.append('g')
.datum(d => d.data.products)
.append('text')
.text((d, i) => {
  return d.filter(function(product) {
    return product.margin == 'high';
  })
  .map(function(product) {
    return product.name;
  })
})

As seen below:

var WIDTH = 200;
var HEIGHT = 200;
var data = [
  { 
    id: 'department1',
    name: 'Electronics',
    value: 100,
    products: [
      {
        description: 'latptop computers',
        id: 'product1',
        name: 'Fast Laptop computers',
        margin: 'high',
      },
      {
        description: 'Coaxial Cable',
        id: 'product2',
        name: 'Gold Coaxial Cable',
        margin: 'high',
      },
    ]
  }, 
  { 
    id: 'department2', 
    value: 100,
    name: 'Hardware',
    products: [
      {
        description: 'Power Drill',
        id: 'product1',
        name: '24 volt power drill',
        margin: 'high'
      },
      {
        description: 'Air tool Wrench',
        id: 'product2',
        name: 'Air Compressor Wrench',
        margin: 'high',
      },
    ]
  },
  { 
    id: 'department3', 
    value: 100,
    name: 'Lawn and Garden',
    products: [
      {
        description: '24 Volt Weed eater',
        id: 'product1',
        name: 'Ryobi Weed Eater',
        margin: 'high',
      },
    ]
  },
  { 
    id: 'department4', 
    value: 100,
    name: 'Grocery',
    products: [
      {
        description: 'Milk',
        id: 'product1',
        name: 'Whole Milk',
        margin: 'high',
      },
    ]
  },
];

  // Set up pie data:
  var prepHighMarginData = d3.pie().value(function(d) { return d.value; })(data);
  
  // Create arc generator:
  var createArc = d3.arc()
    .innerRadius(0)
    .outerRadius(200);

  const svg = d3
    .select("body")
    .append('svg')
    .attr('width', WIDTH * 2)
    .attr('height', HEIGHT * 2)
    .append('g')
    .attr('transform', `translate(${WIDTH}, ${HEIGHT})`);

  const g = svg
    .selectAll('.arc')
    .data(prepHighMarginData)
    .enter();

  g.append('g')
    .append('path')
    .attr('d', createArc)
    .style('fill', "#ccc")
    .style('border-color', "#aaa");
  g.append('g')
    .attr('transform', (d) => {
      return `translate(${createArc.centroid(d)})`
    })
    .append('g')
    .datum(d => d.data.products)
    .append('text')
    .text((d, i) => {
      return d.filter(function(product) {
        return product.margin == 'high';
      })
      .map(function(product) {
        return product.name;
      })
    })
    .style('fill', '#fff');
text {
  text-anchor: middle;
  font-size: 10px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>

However, you could simply skip setting the datum because the department already has this data as part of it:

.append('g')
//.datum(d => d.data.products)
.append('text')
.text((d, i) => {
  // updating to account for the filtered array's location:
  return d.data.products.filter(function(product) { 
    return product.margin == 'high';
  })
  .map(function(product) {
    return product.name;
  })
})

Both of these approaches work if you want only one text element per pie wedge. However, the first one is a bit odd in the d3 idiom as it contains a datum that is in fact a data array.

A third option, which would be to append one child text element per high margin product would be to use something more along the lines of:

g.append('g')
  .attr('transform', (d) => {
    return `translate(${createArc.centroid(d)})`
  })
  .append('g')
  .selectAll('text')
  .data((d) => d.data.products)
  .enter()
  .append('text')
  .attr("y", (d,i,n) => {
     return (i-(n.length/2))*20;
  })
  .text((d) => d.name)
  .style('fill', '#fff');

as seen below. This gives a bit more flexibility in placing multiple text elements as line breaks aren't always an easy solution in SVG. This is also the idiomatic method for appending (multiple) children to a parent. The pattern is unnecessary if parent and child have a 1:1 relationship.

var WIDTH = 200;
var HEIGHT = 200;
var data = [
  { 
    id: 'department1',
    name: 'Electronics',
    value: 100,
    products: [
      {
        description: 'latptop computers',
        id: 'product1',
        name: 'Fast Laptop computers',
        margin: 'high',
      },
      {
        description: 'Coaxial Cable',
        id: 'product2',
        name: 'Gold Coaxial Cable',
        margin: 'high',
      },
    ]
  }, 
  { 
    id: 'department2', 
    value: 100,
    name: 'Hardware',
    products: [
      {
        description: 'Power Drill',
        id: 'product1',
        name: '24 volt power drill',
        margin: 'high'
      },
      {
        description: 'Air tool Wrench',
        id: 'product2',
        name: 'Air Compressor Wrench',
        margin: 'high',
      },
    ]
  },
  { 
    id: 'department3', 
    value: 100,
    name: 'Lawn and Garden',
    products: [
      {
        description: '24 Volt Weed eater',
        id: 'product1',
        name: 'Ryobi Weed Eater',
        margin: 'high',
      },
    ]
  },
  { 
    id: 'department4', 
    value: 100,
    name: 'Grocery',
    products: [
      {
        description: 'Milk',
        id: 'product1',
        name: 'Whole Milk',
        margin: 'high',
      },
    ]
  },
];

  // Set up pie data:
  var prepHighMarginData = d3.pie().value(function(d) { return d.value; })(data);
  
  // Create arc generator:
  var createArc = d3.arc()
    .innerRadius(0)
    .outerRadius(200);

  const svg = d3
    .select("body")
    .append('svg')
    .attr('width', WIDTH * 2)
    .attr('height', HEIGHT * 2)
    .append('g')
    .attr('transform', `translate(${WIDTH}, ${HEIGHT})`);

  const g = svg
    .selectAll('.arc')
    .data(prepHighMarginData)
    .enter();

  g.append('g')
    .append('path')
    .attr('d', createArc)
    .style('fill', "#ccc")
    .style('border-color', "#aaa");
    
  g.append('g')
    .attr('transform', (d) => {
      return `translate(${createArc.centroid(d)})`
    })
    .append('g')
    .selectAll('text')
    .data((d) => d.data.products)
    .enter()
    .append('text')
    .attr("y", (d,i,n) => {
      return (i-(n.length/2))*20;
    })
    .text((d) => d.name)
    .style('fill', '#fff');
text {
  text-anchor: middle;
  font-size: 14px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>

seems like you are just returning the string of 'd.name'

.text((d, i) => {
  console.log(d);
  return 'd.name'
})

to return the value, it should be without quotes?

.text((d, i) => {
  console.log(d);
  return d.name
})

assuming d is returning the product object

Related