Пчелиный участок с силой - добавить ссылки на узлы

Я создал график Beeswarm с d3v4 и d3.forceSimulation, и точки находятся там, где я хочу, чтобы они были:

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 показывает количество. Каждая точка – это страна.

Я хотел бы добавить соединительные линии между точками, которые относятся к одной и той же стране в разные годы. Я выделил их цветом для ясности.

изображение, показывающее линии, которые не совсем соединяются с точками

Проблема в том, что я не могу заставить x/y совпадать с точками, основанными на силе. Я прокомментировал то, что, по моему мнению, сработает. Любые идеи?


person sprucegoose    schedule 13.06.2018    source источник


Ответы (1)


После создания кругов можно получить их точные координаты. С этими координатами круга становится очень легко установить концы линий:

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)

на котором мы можем получить атрибуты cx и cy (положение 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"));

Боковые примечания:

Обратите внимание, как я изменил классы, которые вы использовали для именования кругов. Поскольку имена классов содержали пробелы, мы не могли их выбрать; Вместо этого я использовал -. Кроме того, мы, по-видимому, не можем выбрать класс, который начинается с цифры, поэтому я изменил имена классов, чтобы они начинались со страны, а не с суммы.

Поскольку здесь классы используются для однозначного определения кругов и линий, вероятно, было бы более разумно установить ids, а не classes.

Как заметил @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)...
person Xavier Guihot    schedule 13.06.2018
comment
Вау, большое спасибо за подробный ответ. Я очень ценю ваше объяснение! Это невероятно полезно. - person sprucegoose; 14.06.2018
comment
Хороший ответ, +1, но вот совет: вы добавляете как минимум 2 пути (иногда 3) в одну и ту же позицию. - person Gerardo Furtado; 14.06.2018