Chord(弦布局)

134 阅读3分钟

简介

弦图用来展示一组实体之间的关系,通过在不同的弧线之间画出二次贝塞尔曲线,将实体之间的关系表示在一张弦图中。 如:展示五个城市人口互相之间的来源关系,比如北京有2015人来自上海,上海有2060人来自广州。

主程序

代码里面已经标注详细的步骤,这里就不再赘述了。

  var data = Object.assign([
    [11975, 5871, 8916, 2868],
    [1951, 10048, 2060, 6171],
    [8010, 16145, 8090, 8045],
    [1013, 990, 940, 6907]
  ], {
    names: ["black", "blond", "brown", "red"],
    colors: ["#000000", "#ffdd89", "#957244", "#f26223"]
  })
  var width = 640;
  var height = 640;
  var dome = document.getElementById('svgAll');
  var svg = d3.create("svg").attr("width", width).attr("height", height)
    .attr("viewBox", [-width / 2, -height / 2, width, height])
    .attr("style", "max-width: 100%; height: auto; font: 10px sans-serif;");

  const outerRadius = 320 - 30;
  const innerRadius = outerRadius - 20;
  const { names, colors } = data;
  const sum = d3.sum(data.flat());
  const tickStep = d3.tickStep(0, sum, 100);
  const tickStepMajor = d3.tickStep(0, sum, 20);
  const formatValue = d3.formatPrefix(",.0", tickStep);

  const chord = d3.chord()
    .padAngle(20 / innerRadius)
    .sortSubgroups(d3.descending);
  //绘制器、圆弧生成器
  const arc = d3.arc()
    .innerRadius(innerRadius)
    .outerRadius(outerRadius);
  //ribbon通过传入的数据source和target就可以帮我们生成一个path路径,有了路径就可以轻松的绘制弦图了
  const ribbon = d3.ribbon().radius(innerRadius);
  //用d3.chord处理数据
  const chords = chord(data);
  //添加分组
  const group = svg.append("g")
    .selectAll()
    .data(chords.groups)
    .join("g");
    
  //外环形
  group.append("path")
    .attr("fill", d => colors[d.index])
    .attr("d", arc)//绑定路径的属性值
    .append("title")
    .text(d => `${d.value.toLocaleString("en-US")} ${names[d.index]}`);
  
  //环形刻度
  const groupTick = group.append("g")
    .selectAll()
    .data(d => groupTicks(d, tickStep))
    .join("g")
    .attr("transform", d => `rotate(${d.angle * 180 / Math.PI - 90}) translate(${outerRadius},0)`);
  //刻度横线
  groupTick.append("line")
    .attr("stroke", "currentColor")
    .attr("x2", 6);
  //刻度数值
  groupTick
    .filter(d => d.value % tickStepMajor === 0)
    .append("text")
    .attr("x", 8)
    .attr("dy", ".35em")
    .attr("transform", d => d.angle > Math.PI ? "rotate(180) translate(-16)" : null)
    .attr("text-anchor", d => d.angle > Math.PI ? "end" : null)
    .text(d => formatValue(d.value));

  // 重点----
  // 生成丝带图形
  svg.append("g")
    .attr("fill-opacity", 0.7)
    .selectAll()
    .data(chords)
    .join("path")
    .attr("d", ribbon)
    .attr("fill", d => colors[d.target.index])
    .attr("stroke", "white")
    .append("title")
    .text(d => `${d.source.value.toLocaleString("en-US")} ${names[d.source.index]}${names[d.target.index]}${d.source.index !== d.target.index ? `\n${d.target.value.toLocaleString("en-US")} ${names[d.target.index]}${names[d.source.index]}` : ``}`);

  function groupTicks(d, step) {
    const k = (d.endAngle - d.startAngle) / d.value;
    return d3.range(0, d.value, step).map(value => {
      return { value: value, angle: value * k + d.startAngle };
    });
  }
  dome.appendChild(svg.node());

另外一个人口流动示例

  const city_name = ["北京", "上海", "广州", "深圳", "香港"];
  const population = [
    [1000, 3045, 4567, 1234, 3714],
    [3214, 2000, 2060, 124, 3234],
    [8761, 6545, 3000, 8045, 647],
    [3211, 1067, 3214, 4000, 1006],
    [2146, 1034, 6745, 4764, 5000]
  ];

  var dome = document.getElementById('svgAll');
  var svg = d3.create("svg").attr("width", 600).attr("height", 600);

  //创建分组
  const rootG = svg
    .append("g")
    .attr("class", "rootG")
    .attr("transform", "translate(300,300)");

  const groupG = rootG.append("g").attr("class", "group");
  const chordG = rootG.append("g").attr("class", "chords");

  //处理数据得到适合绘图数据
  const chords = d3
    .chord()
    .padAngle(0.1)
    .sortSubgroups(d3.descending)
    .sortChords(d3.descending)(population);
  console.log(chords);

  //构建颜色比例尺
  const color = d3.scaleOrdinal(d3.schemeCategory10);
  //创建绘制器
  const arc = d3.arc().innerRadius(100).outerRadius(130);

  console.log(chords.groups);
  const ribbon = d3.ribbon().radius(100); //创建弦绘制器
  //绘制
  groupG
    .selectAll("path")
    .data(chords.groups)
    .enter()
    .append("path")
    .attr("fill", function (d, i) {
      return color(i);
    })
    .attr("d", arc);

  groupG
    .selectAll(".outerText")
    .data(chords.groups)
    .enter()
    .append("text")
    .each(function (d, i) {
      d.angle = (d.startAngle + d.endAngle) / 2;
      d.name = city_name[i];
    })
    .attr("class", "outerText")
    .attr("dy", ".35em")
    .attr("transform", function (d) {
      console.log(d);
      const jd = (d.angle * 180) / Math.PI;
      // 字体本来是水平的,所以旋转角度需要 - 90
      return (
        "rotate(" +
        (jd - 90) +
        ")" +
        "translate(" +
        (jd >= 180 ? 180 : 140) +
        ")" +
        (d.angle > Math.PI ? "rotate(180)" : "")
      );
    })
    .text(function (d) {
      return d.name;
    });

  chordG
    .selectAll("path")
    .data(chords)
    .enter()
    .append("path")
    .attr("fill", function (d, i) {
      return color(i);
    })
    .attr("d", ribbon);

  dome.appendChild(svg.node());