Avery
Avery

Reputation: 57

how to update reusable nested donut chart?

I have a reusable chart with a data update function--following the suggestions from this article by Rob Moore: https://www.toptal.com/d3-js/towards-reusable-d3-js-charts --which I am calling pizza.

Pizza is a nested donut chart. It looks like a pie chart with concentric rings. I would like to add an interaction whereby a user can click on a slice of the pie and the selected slice will expand to fill the whole pie.

The chart has two setter functions: .data() and .numberOfRings().

Pizza.data(dataArray).numberOfRings(n) results in a chart with n concentric rings and dataArray.length many slices, where dataArray is an array of integers.

n copies of dataArray are passed to d3.pie() to determine the startAngle and endAngle for each eventual arc in each of the n slices. Then the objects returned by pie are assigned outerRadius, innerRadius and some other useful properties, based on their indexes:

const ringSet = [...Array(numberOfRings).keys()].reverse()

const sliceGenorator = Object.fromEntries(ringSet.map((x, i) => [i, 
data]))

const pie = d3.pie().sort(null)
const arc = d3.arc()

let arcs = d3.values(sliceGenorator).map((d, i) => 
                              pie(d).map((x, v) => 
        ({
            ...x,
            arcSlice: 'slice_' + v,
            arcRing: 'ring_' + i,
            innerRadius: cWidth * i,
            outerRadius: cWidth * (i + 1),
            id: 'ring' + i + '_' + 'slice' + v
        })));

these objects then get passed to an arc generator and appended to the svg group.

const flatArcs = arcs.flat()
const paths = svg.selectAll('path.arc')
    .data(flatArcs)
    .enter()
            .append('path')
    .attr('class', 'arc')
            .attr('d',d => arc(d))
    .attr('id', d => d.id)
    .attr('fill', d => function that colors slices)

My idea is to update the chart data with an index array, as shown below:

let data = [30, 22, 8, 10]

//create an instance of the chart. call it 'pizza'
const pizza = pizzaChart()
  .numberOfRings(5)
  .data(data); 

//render the pizza into the div
d3.select('#container')
  .call(pizza)

//returns an index array
function selected(d) {
  let zeros = new Array(data.length).fill(0);
  zeros[d.index] = 1;
    return zeros}

//pass an index array to pizza as the data update
d3.selectAll('path.arc')
  .on('click', d => pizza.data(selected(d)))

Say the user selects slice 1, then indexArray = [0, 1, 0, 0] is passed to pizza's updateData function, which runs the same processes described above, producing a new array of arc objects whose start angles and end angles have been calculated from the values in indexArray.

The paths are then updating the chart with the arc id's as a key, to proved object constancy.

// update the data and pass in the arc id's as a key
const updatePaths = d3.selectAll('path.arc')
             .data(updateArcs.flat(), d => d.id)

updatePaths.enter()
     .append('path')
     .attr('class', 'arc')
     .attr('d',d => arc(d))
     .attr('stroke', '#334646')
     .attr('id', d => d.id)
 .attr('fill', d => function to color code by slice)

updatePaths.exit().remove();

I expect that the arcs in the selected slice will be re-rendered with startAngle = 0 and endAngle = 2*PI, while the other arcs will be re-rendered with StartAngle = 0 and endAngle = 0. Thus the selected slice will fill the chart while the other slices will collapse. But nothing happens.

I will eventually add an animated transition, but I only want to do that once I get the expected behavior out of the chart.

if I console.log(updatePaths.data()) see the expected data.

Am I on the wrong track? Am I missing something obvious?

Any help would be deeply appreciated

here is a link to my full code:

https://bl.ocks.org/AveryBurke/d366090ccc19c41c8ee7be2958b3f425/46540a46380d7f7eb8ced458e6eda06c2c05f2d2

Upvotes: 0

Views: 189

Answers (1)

Coderino Javarino
Coderino Javarino

Reputation: 2881

You were not updating existing items at all -- all your actions were upon new items selection updatePaths.enter(), and none upon updatePaths.

I added a merge, and it started doing what I think you wanted it to

updatePaths.enter()
    .append('path')
    //actions for only new items; things that never change
    .attr('class', 'arc')
    .attr('stroke', '#334646')
    .attr('id', d => d.id)
.merge(updatePaths)
    //actions for both new and existing items
    .attr('d',d => arc(d))
    .attr('fill', d => sliceColor.range(pallet[d.index])(arcRingValue(d)))

Upvotes: 1

Related Questions