可更新的和弦图

时间:2018-11-21 13:09:46

标签: javascript d3.js data-manipulation chord-diagram

我正在制作D3和弦图,并希望具有更新功能。

这是我目前拥有的代码

// 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)
}

<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>

这就是我现在得到的结果: enter image description here

我想在用户单击按钮 Alleen dubbele流派时更新和弦,所以它变成这样:

enter image description here

所以我想自行删除和弦,因此数据看起来像:

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

如果用户点击重置按钮,我想回到原始视图。有谁可以帮助我吗?

1 个答案:

答案 0 :(得分:1)

这里是指向您的示例的简化版本的链接,其中包含以下转换:https://stackblitz.com/edit/q53412789

Gif with animation

如果要删除自身的和弦,则应将对角线上的值替换为零(而不仅仅是删除它们:矩阵必须保持正方形):

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]
]

就像@ rioV8所建议的那样,第一步是将用于绘制和更新图表的代码包装到一个函数中(为了简洁起见,我省略了代码,为了简洁起见,它们的原理是相同的):

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);

我已将代码中的.datum(val).data(fun)的组合替换为单个.data(vals)。这不是必需的,但我认为后者在d3社区中更为常见。

.data的第二个参数是键函数。它可以确保在过渡期间,每个路径在新数据中都映射到自身(和弦数不相等:我们删除了自身的和弦)。

如果不需要动画过渡,则代码甚至更简单:

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();
}