带有力量的怪异情节 - 添加节点链接

时间:2018-06-13 21:38:13

标签: 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与基于力的点匹配。我评论了我认为会起作用的东西。有什么想法吗?

1 个答案:

答案 0 :(得分:3)

创建圆圈后,可以检索其精确坐标。使用这些圆坐标,可以很容易地设置线的末端:

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