Rooney
Rooney

Reputation: 1772

Updatable chord diagram

I am working on a D3 chord diagram and want to have an update function.

This is the code I have at the moment

// Genres, check de readme daar staat visueel hoe je dit uitleest. 
var data = [
  [9962, 1196, 94, 93, 18],
  [1196, 9102, 11, 343, 169],
  [94, 11, 7143, 138, 32],
  [93, 343, 138, 6440, 75],
  [18, 169, 32, 75, 4886]
]

var genres = ["Psychologischverhaal", "Thriller", "Detective", "Romantischverhaal", "Sciencefiction"]

var svg = d3.select("svg"),
    width = +svg.attr("width"),
    height = +svg.attr("height"),
    outerRadius = Math.min(width, height) * 0.5 - 40,
    innerRadius = outerRadius - 30;

// Zet getallen naar 1K ipv 1000
var formatValue = d3.formatPrefix(",.0", 1e3);

var chord = d3.chord()
    .padAngle(0.05)
    .sortSubgroups(d3.ascending);

var arc = d3.arc()
    .innerRadius(innerRadius)
    .outerRadius(outerRadius);

var ribbon = d3.ribbon()
    .radius(innerRadius);

var color = d3.scaleOrdinal()
    .range(["#ed0b0b", "#03aa24", "#f2ae04", "#1f03f1", "#e1ed04"]);

var g = svg.append("g")
    .attr("transform", "translate(" + width / 2 + "," + height / 2 + ")")
    .datum(chord(data));

var group = g.append("g")
    .attr("class", "groups")
  .selectAll("g")
  .data(function(chords) { return chords.groups; })
  .enter().append("g");

group.append("path")
    .style("fill", function(d) { return color(d.index); })
    .style("stroke", function(d) { return d3.rgb(color(d.index)).darker(); })
    .attr("id", function(d, i) { return "group" + d.index; })
    .attr("d", arc)
    .on("mouseover", fade(.1)) 
    .on("mouseout", fade(1));

group.append("title").text(function(d) {
        return groupTip(d);
});

group.append("text")
        .attr("x", 6)
        .attr("dy", 15)
      .append("textPath")
        .attr("xlink:href", function(d) { return "#group" + d.index; })
        .text(function(chords, i){return genres[i];})
        .style("fill", "black");

var groupTick = group.selectAll(".group-tick")
  .data(function(d) { return groupTicks(d, 1e3); })
  .enter().append("g")
    .attr("class", "group-tick")
    .attr("transform", function(d) { return "rotate(" + (d.angle * 180 / Math.PI - 90) + ") translate(" + outerRadius + ",0)"; });

groupTick.append("line")
    .attr("x1", 1)
    .attr("y1", 0)
    .attr("x2", 5)
    .attr("y2", 0)
    .style("stroke", "#000")

groupTick
  .filter(function(d) { return d.value % 1e3 === 0; })
  .append("text")
    .attr("x", 8)
    .attr("dy", ".35em")
    .attr("transform", function(d) { return d.angle > Math.PI ? "rotate(180) translate(-16)" : null; })
    .style("text-anchor", function(d) { return d.angle > Math.PI ? "end" : null; })
    .text(function(d) { return formatValue(d.value); });

var ribbons = g.append("g")
    .attr("class", "ribbons")
  .selectAll("path")
  .data(function(chords) { return chords; })
  .enter().append("path")
    .attr("d", ribbon)
    .style("fill", function(d) { return color(d.target.index); })
    .style("stroke", function(d) { return d3.rgb(color(d.target.index)).darker(); })

ribbons.append("title").
    text(function(d){return chordTip(d);});


// Returns an array of tick angles and values for a given group and step.
function groupTicks(d, step) {
  var k = (d.endAngle - d.startAngle) / d.value;
  return d3.range(0, d.value, step).map(function(value) {
    return {value: value, angle: value * k + d.startAngle};
  });
}

function fade(opacity) {
  return function(d, i) {
    ribbons
        .filter(function(d) {
          return d.source.index != i && d.target.index != i;
        })
      .transition()
        .style("opacity", opacity);
  };
}

function chordTip(d){
  var j = d3.formatPrefix(",.0", 1e1)
     return "Aantal boeken met genres:\n"
        + genres[d.target.index] + " en " + genres[d.source.index] + ": " + j(d.source.value)
}

function groupTip(d) {
        var j = d3.formatPrefix(",.0", 1e1)
        return "Totaal aantal boeken met het genre " + genres[d.index] + ":\n" + j(d.value)
}

and

<body>
<h1>Boeken met enkele & dubbele genres</h1>
<button id="doubleGenre">Alleen dubbele genres</button>
<button id="reset">Reset</button>

<svg width="960" height="900"></svg>

<script src="https://d3js.org/d3.v5.min.js"></script>
<script src="assets/index.js"></script>

This is the result what I have now : enter image description here

I want to update the chord when a user clicks on the button Alleen dubbele genres so it becomes like this :

enter image description here

So I want to delete the chords to it self so the data looks like :

var data = [
  [1196, 94, 93, 18],
  [1196, 11, 343, 169],
  [94, 11, 138, 32],
  [93, 343, 138, 75],
  [18, 169, 32, 75]
]

And if a user clicks on the button reset I want to go back to the original view. Is there anyone who can help me with this?

Upvotes: 0

Views: 493

Answers (1)

Yaroslav Sergienko
Yaroslav Sergienko

Reputation: 720

Here is a link to the simplified version of your example with transitions: https://stackblitz.com/edit/q53412789

Gif with animation

If you want to remove chords to self, you should replace values on diagonal to zeros (not just delete them: matrix must remain square):

const data2 = [
  [0, 1196, 94, 93, 18],
  [1196, 0, 11, 343, 169],
  [94, 11, 0, 138, 32],
  [93, 343, 138, 0, 75],
  [18, 169, 32, 75, 0]
]

As @rioV8 suggested, the first step is to wrap code for drawing and updating the chart into a function (I've ommited code for ticks and labels for brevity, the principle is the same for them):

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

var ribbons = g.append("g");

function update(data) {
  const chords = chord(data);

  const ribbonsUpdate = ribbons.selectAll("path")
    .data(chords, ({source, target}) => source.index + '-' + target.index)

  const duration = 3000;

  ribbonsUpdate
    .transition()
      .duration(duration)
      .attr("d", ribbon)
      .style("fill", function(d) { return color(d.target.index); })
      .style("stroke", function(d) { return d3.rgb(color(d.target.index)).darker(); })

  ribbonsUpdate
    .enter()
      .append("path")
      .attr("opacity", 0)
      .attr("d", ribbon)
      .style("fill", function(d) { return color(d.target.index); })
      .style("stroke", function(d) { return d3.rgb(color(d.target.index)).darker(); })
      .transition()
        .duration(duration)
        .attr('opacity', 1)

  ribbonsUpdate
    .exit()
      .transition()
        .duration(duration)
        .attr("opacity", 0)
        .remove();
}

update(data1);

I've replaced combination of .datum(val) and .data(fun) in your code to single .data(vals). It is not required, but my opinion is that the latter is more common in d3 community.

The second argument to .data is the key function. It ensures, that during transition each path maps to itself in the new data (the number of chords is not equal: we removed chords to itself).

If you do not need animated transitions, the code is even simplier:

function simpleUpdate(data) {
  const chords = chord(data);

  const ribbonsUpdate = ribbons.selectAll("path")
    .data(chords, ({source, target}) => source.index + '-' + target.index)

  ribbonsUpdate
    .enter().append("path")
    .merge(ribbonsUpdate)
      .attr("d", ribbon)
      .style("fill", function(d) { return color(d.target.index); })
      .style("stroke", function(d) { return d3.rgb(color(d.target.index)).darker(); })

  ribbonsUpdate.exit().remove();
}

Upvotes: 1

Related Questions