d3.js - 强制蜂群图 - 添加节点链接

标签 d3.js force-layout beeswarm

我使用 d3v4d3.forceSimulation 创建了 Beeswarm 图,这些点位于我想要的位置:

var data = [
  { country: "Algeria", amount: 22, year: 2000 },
  { country: "Argentina", amount: 49, year: 1990 },
  { country: "Armenia", amount: 3, year: 1990 },
  { country: "Australia", amount: 9, year: 2010 },
  { country: "Austria", amount: 1, year: 2010 },
  { country: "Bahamas", amount: 5, year: 2018 },
  { country: "Bahrain", amount: 22, year: 2018 },
  { country: "Belarus", amount: 9, year: 2010 },
  { country: "Belgium", amount: 46, year: 2018 },
  { country: "Brazil", amount: 79, year: 1990 },
  { country: "Canada", amount: 12, year: 2000 },
  { country: "China", amount: 26, year: 2018 },
  { country: "Colombia", amount: 9, year: 2010 },
  { country: "Croatia", amount: 8, year: 2000 },
  { country: "Cuba", amount: 14, year: 1990 },
  { country: "Czech Republic", amount: 11, year: 2018 },
  { country: "Denmark", amount: 125, year: 2010 },
  { country: "Canada", amount: 124, year: 2018 },
  { country: "Bahrain", amount: 39, year: 2010 },
  { country: "Estonia", amount: 141, year: 2018 },
  { country: "Ethiopia", amount: 38, year: 1990 },
  { country: "France", amount: 4, year: 2018 },
  { country: "Germany", amount: 15, year: 2000 },
  { country: "Greece", amount: 16, year: 2010 },
  { country: "Grenada", amount: 241, year: 2010 },
  { country: "Hungary", amount: 135, year: 1990 },
  { country: "India", amount: 22, year: 1990 },
  { country: "Indonesia", amount: 31, year: 1990 },
  { country: "Iran", amount: 88, year: 2010 },
  { country: "Ireland", amount: 12, year: 2018 },
  { country: "Italy", amount: 128, year: 2000 },
  { country: "Jamaica", amount: 1, year: 2018 },
  { country: "Japan", amount: 41, year: 1990 },
  { country: "Jordan", amount: 137, year: 2010 },
  { country: "Iran", amount: 13, year: 1990 },
  { country: "Malaysia", amount: 25, year: 2018 },
  { country: "Mexico", amount: 59, year: 2010 },
  { country: "Moldova", amount: 71, year: 2000 },
  { country: "Mongolia", amount: 22, year: 2018 },
  { country: "Morocco", amount: 131, year: 1990 },
  { country: "Netherlands", amount: 129, year: 2018 },
  { country: "New Zealand", amount: 148, year: 2018 },
  { country: "Niger", amount: 1, year: 2010 },
  { country: "Nigeria", amount: 41, year: 1990 },
  { country: "Norway", amount: 14, year: 2010 },
  { country: "Philippines", amount: 15, year: 2018 },
  { country: "Poland", amount: 12, year: 2010 },
  { country: "Portugal", amount: 31, year: 2000 },
  { country: "Puerto Rico", amount: 51, year: 2000 },
  { country: "Romania", amount: 15, year: 2000 },
  { country: "Serbia", amount: 18, year: 2000 },
  { country: "South Africa", amount: 14, year: 2010 },
  { country: "Sweden", amount: 11, year: 2018 },
  { country: "Switzerland", amount: 7, year: 2010 },
  { country: "Thailand", amount: 61, year: 2018 },
  { country: "Trinidad and Tobago", amount: 12, year: 2018 },
  { country: "Tunisia", amount: 34, year: 2010 },
  { country: "Turkey", amount: 28, year: 2010 },
  { country: "Ukraine", amount: 11, year: 2010 },
  { country: "Uzbekistan", amount: 123, year: 2018 },
  { country: "Venezuela", amount: 23, year: 2018 },
  { country: "Iran", amount: 13, year: 2018 }
];

var width = 1000,
  height = 500;

var svg = d3.select("#chart")
  .append("svg")
  .attr("width", width)
  .attr("height", height);

var x = d3.scaleLinear()
  .range([95, 650]);

var y = d3.scaleLinear()
  .range([100, 450]);

  data.forEach(d => {
    d.amount = +d.amount;
  });

  var sort = data.sort((a, b) => d3.descending(a, b));

  y.domain(d3.extent(data, function(d) {
    return d.amount;
  }));
  x.domain(d3.extent(data, function(d) {
    return d.year;
  }));

  var simulation = d3.forceSimulation(data)
    .force("x", d3.forceX(function(d) {
      return x(d.year);
    }).strength(3))
    .force("y", d3.forceY(function(d) {
      return y(d.amount)
    }).strength(2))
    .force("collide", d3.forceCollide(7).strength(7))
    .stop();

  for (var i = 0; i < data.length * 2; ++i) simulation.tick();

  var circles = svg.selectAll(".circles")
    .data(data);

  var circlesEnter = circles.enter()
    .append("circle");

  circlesEnter.attr("r", 4)
    .attr("cx", function(d) {
      return d.x
    })
    .attr("cy", function(d) {
      return d.y
    })
    .attr("fill", function(d) {
      if (d.country == "Iran") {
        return "#FF0044"
      } else if (d.country == "Canada") {
        return "#00A9E9"
      } else if (d.country == "Bahrain") {
        return "#6BF4C6"
      } else {
        return '#333'
      }
    })
    .attr('class', function(d) {
      return d.amount + ' ' + d.year + ' ' + d.country
    })

  // connector lines

  var byCountry = d3.nest()
    .key(function(d) {
      return d.country;
    })
    .entries(data);

  var countryNames = d3.values(byCountry).map(function(d) {
    return d.values.map(function(v) {
      return v.country;
    }).join(', ');
  });

  for (i = 0; i < countryNames.length; i++) {
    eaco = countryNames[i].split(',')[0]

    const filterByCountry = (country, data) => item => item.country === country
    connectData = data.filter(filterByCountry(eaco))

    var linesGroup = svg.append("g")
      .attr("class", "connectors");

    var linec = d3.line()
      .x(function(d) {
        return x(d.year)
      })
      .y(function(d) {
        return y(d.amount)
      })

    // using below as the points does not work
    // .x(function(d) { return x(d.x)})
    // .y(function(d) { return y(d.y)})

    var lineGraph = linesGroup.selectAll('.connect')
      .data(connectData)
      .enter()
      .append("path")
      .attr('class', function(d) {
        return d.amount + ' ' + d.year + ' ' + d.country
      })
      .attr("d", linec(connectData))
      .attr("stroke", function(d) {
        if (d.country == "Iran") {
          return "#FF0044"
        } else if (d.country == "Canada") {
          return "#00A9E9"
        } else if (d.country == "Bahrain") {
          return "#6BF4C6"
        } else {
          return '#333'
        }
      })
      .attr("stroke-width", 1)
      .attr("fill", "none")
  }
<script src="https://d3js.org/d3.v4.min.js"></script>
<body><div id="chart"></div></body>

x 轴显示年份。 y 轴显示数量。 每个点都是一个国家。

我想在不同年份的同一国家/地区的点之间添加连接线。为了清楚起见,我对它们进行了颜色编码。

image showing lines that don't quite connect to the dots

问题是,我无法让 x/y 与基于力的点相匹配。我评论了我认为可行的内容。有什么想法吗?

最佳答案

创建圆圈后,就可以检索其精确坐标。有了这些圆坐标,设置线的末端就变得非常容易:

var data = [
  { country: "Algeria", amount: 22, year: 2000 },
  { country: "Argentina", amount: 49, year: 1990 },
  { country: "Armenia", amount: 3, year: 1990 },
  { country: "Australia", amount: 9, year: 2010 },
  { country: "Austria", amount: 1, year: 2010 },
  { country: "Bahamas", amount: 5, year: 2018 },
  { country: "Bahrain", amount: 22, year: 2018 },
  { country: "Belarus", amount: 9, year: 2010 },
  { country: "Belgium", amount: 46, year: 2018 },
  { country: "Brazil", amount: 79, year: 1990 },
  { country: "Canada", amount: 12, year: 2000 },
  { country: "China", amount: 26, year: 2018 },
  { country: "Colombia", amount: 9, year: 2010 },
  { country: "Croatia", amount: 8, year: 2000 },
  { country: "Cuba", amount: 14, year: 1990 },
  { country: "Czech Republic", amount: 11, year: 2018 },
  { country: "Denmark", amount: 125, year: 2010 },
  { country: "Canada", amount: 124, year: 2018 },
  { country: "Bahrain", amount: 39, year: 2010 },
  { country: "Estonia", amount: 141, year: 2018 },
  { country: "Ethiopia", amount: 38, year: 1990 },
  { country: "France", amount: 4, year: 2018 },
  { country: "Germany", amount: 15, year: 2000 },
  { country: "Greece", amount: 16, year: 2010 },
  { country: "Grenada", amount: 241, year: 2010 },
  { country: "Hungary", amount: 135, year: 1990 },
  { country: "India", amount: 22, year: 1990 },
  { country: "Indonesia", amount: 31, year: 1990 },
  { country: "Iran", amount: 88, year: 2010 },
  { country: "Ireland", amount: 12, year: 2018 },
  { country: "Italy", amount: 128, year: 2000 },
  { country: "Jamaica", amount: 1, year: 2018 },
  { country: "Japan", amount: 41, year: 1990 },
  { country: "Jordan", amount: 137, year: 2010 },
  { country: "Iran", amount: 13, year: 1990 },
  { country: "Malaysia", amount: 25, year: 2018 },
  { country: "Mexico", amount: 59, year: 2010 },
  { country: "Moldova", amount: 71, year: 2000 },
  { country: "Mongolia", amount: 22, year: 2018 },
  { country: "Morocco", amount: 131, year: 1990 },
  { country: "Netherlands", amount: 129, year: 2018 },
  { country: "New Zealand", amount: 148, year: 2018 },
  { country: "Niger", amount: 1, year: 2010 },
  { country: "Nigeria", amount: 41, year: 1990 },
  { country: "Norway", amount: 14, year: 2010 },
  { country: "Philippines", amount: 15, year: 2018 },
  { country: "Poland", amount: 12, year: 2010 },
  { country: "Portugal", amount: 31, year: 2000 },
  { country: "Puerto Rico", amount: 51, year: 2000 },
  { country: "Romania", amount: 15, year: 2000 },
  { country: "Serbia", amount: 18, year: 2000 },
  { country: "South Africa", amount: 14, year: 2010 },
  { country: "Sweden", amount: 11, year: 2018 },
  { country: "Switzerland", amount: 7, year: 2010 },
  { country: "Thailand", amount: 61, year: 2018 },
  { country: "Trinidad and Tobago", amount: 12, year: 2018 },
  { country: "Tunisia", amount: 34, year: 2010 },
  { country: "Turkey", amount: 28, year: 2010 },
  { country: "Ukraine", amount: 11, year: 2010 },
  { country: "Uzbekistan", amount: 123, year: 2018 },
  { country: "Venezuela", amount: 23, year: 2018 },
  { country: "Iran", amount: 13, year: 2018 }
];

var width = 1000,
  height = 500;

var svg = d3.select("#chart")
  .append("svg")
  .attr("width", width)
  .attr("height", height);

var x = d3.scaleLinear()
  .range([95, 650]);

var y = d3.scaleLinear()
  .range([100, 450]);

data.forEach(d => {
  d.amount = +d.amount;
});

var sort = data.sort((a, b) => d3.descending(a, b));

y.domain(d3.extent(data, function(d) {
  return d.amount;
}));
x.domain(d3.extent(data, function(d) {
  return d.year;
}));

var simulation = d3.forceSimulation(data)
  .force("x", d3.forceX(function(d) {
    return x(d.year);
  }).strength(3))
  .force("y", d3.forceY(function(d) {
    return y(d.amount)
  }).strength(2))
  .force("collide", d3.forceCollide(7).strength(7))
  .stop();

for (var i = 0; i < data.length * 2; ++i) simulation.tick();

var circles = svg.selectAll(".circles").data(data);

var circlesEnter = circles.enter().append("circle");

circlesEnter.attr("r", 4)
  .attr("cx", function(d) { return d.x; })
  .attr("cy", function(d) { return d.y; })
  .attr("fill", function(d) {
    if (d.country == "Iran") {
      return "#FF0044"
    } else if (d.country == "Canada") {
      return "#00A9E9"
    } else if (d.country == "Bahrain") {
      return "#6BF4C6"
    } else {
      return '#333'
    }
  })
  .attr('class', function(d) {
    return d.country + '-' + d.year + '-' + d.amount
  });

// connector lines

var byCountry = d3.nest().key(function(d) { return d.country; }).entries(data);

var countryNames = d3.values(byCountry).map(function(d) {
  return d.values.map(function(v) {
    return v.country;
  }).join(', ');
});

for (i = 0; i < countryNames.length; i++) {
  eaco = countryNames[i].split(',')[0]

  const filterByCountry = (country, data) => item => item.country === country
  connectData = data.filter(filterByCountry(eaco))

  if (connectData.length >= 2) {

    var linesGroup = svg.append("g")
      .attr("class", "connectors");

    var linec = d3.line()
      .x(d => d3.select("circle." + d.country + "-" + d.year + "-" + d.amount).attr("cx"))
      .y(d => d3.select("circle." + d.country + "-" + d.year + "-" + d.amount).attr("cy"));

    linesGroup
      .datum(connectData)
      .append("path")
      .attr('class', d => d[0].amount + '-' + d[0].year + '-' + d[0].country)
      .attr("d", linec)
      .attr("stroke", function(d) {
        if (d[0].country == "Iran") return "#FF0044";
        else if (d[0].country == "Canada") return "#00A9E9";
        else if (d[0].country == "Bahrain") return "#6BF4C6";
        else return '#333';
      })
      .attr("stroke-width", 1)
      .attr("fill", "none");
  }
}
<script src="https://d3js.org/d3.v5.min.js"></script>
<body><div id="chart"></div></body>

这样,线的末端与圆的坐标完全匹配。

为此,我们可以使用以下方法选择与相关线的末端相对应的圆圈:

d3.select("circle." + d.country + "-" + d.year + "-" + d.amount)

我们可以在其上检索 cxcy 属性(圆的 x 和 y 位置):

d3.select("circle." + d.country + "-" + d.year + "-" + d.amount).attr("cx")

最终使用这些坐标创建线条:

var linec = d3.line()
  .x(d => d3.select("circle." + d.country + "-" + d.year + "-" + d.amount).attr("cx"))
  .y(d => d3.select("circle." + d.country + "-" + d.year + "-" + d.amount).attr("cy"));

旁注:

请注意我如何修改您用来命名圆圈的类。由于类名包含空格,我们无法选择它们;我用 - 代替。此外,我们显然无法选择以数字开头的类别,因此我将类别名称更改为以国家/地区而不是金额开头。

由于此处使用类来唯一定义圆和线,因此设置 id 可能比设置 class 更有意义。

正如 @Gerardo 所注意到的,每条线实际上都会创建多次(每个圆一次;这样,连接 3 个圆的线就会创建 3 次)。这是解决该问题的一种可能方法:

var linec = d3.line()
  .x(d => d3.select("circle." + d.country + "-" + d.year + "-" + d.amount).attr("cx"))
  .y(d => d3.select("circle." + d.country + "-" + d.year + "-" + d.amount).attr("cy"));

linesGroup.datum(connectData).append("path").attr("d", linec)...

关于d3.js - 强制蜂群图 - 添加节点链接,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/50846650/

相关文章:

javascript - D3.js 在线性刻度上使用序数刻度

javascript - 如何在 D3 节点中放置图像?

d3.js - 如何使用 voronoi 多边形修改 d3 力布局以触发分组元素上的事件?

R可视化: sensible repel points on map (beeswarm?)

javascript - 如何在放大时在 D3.js 上绘制更多数据点?

d3.js - 有没有办法让D3的力布局不断移动?

algorithm - 是否有用于绘制力导向图的简单(-ish)算法?

python - "TypeError: ' 模块 ' object is not callable"使用 beeswarm 时出错

javascript - 重置 d3 forceSimulation 中的所有隔离力

javascript - d3.js 的正式分类是什么?框架?图书馆?包裹?