d3 - "Cannot create property 'vx' on number '65'"
Asked Answered
C

1

7

So I'm trying to purpose this great example Force-Directed Graph for some very simple json: https://raw.githubusercontent.com/DealPete/forceDirected/master/countries.json

My work is here: codepen

I'm getting a never-ending feed of errors from d3 with no error at the start to suggest something wrong with my code. It starts like this:

XHR finished loading: GET "https://raw.githubusercontent.com/DealPete/forceDirected/master/countries.json".
[...]
d3.min.js:2 Uncaught Error: missing: 0
    at ar (d3.min.js:2)
    at r (d3.min.js:5)
    at Function.e.links (d3.min.js:5)
    at pen.js:46
    at Object.<anonymous> (d3.min.js:7)
    at d.call (d3.min.js:4)
    at XMLHttpRequest.e (d3.min.js:7)
ar @ d3.min.js:2
r @ d3.min.js:5
e.links @ d3.min.js:5
(anonymous) @ pen.js:46
(anonymous) @ d3.min.js:7
call @ d3.min.js:4
e @ d3.min.js:7
d3.min.js:5 Uncaught TypeError: Cannot create property 'vx' on number '66'
    at e (d3.min.js:5)
    at d3.min.js:5
    at Fe.each (d3.min.js:5)
    at e (d3.min.js:5)
    at n (d3.min.js:5)
    at yn (d3.min.js:2)
    at gn (d3.min.js:2)
e @ d3.min.js:5
(anonymous) @ d3.min.js:5
each @ d3.min.js:5
e @ d3.min.js:5
n @ d3.min.js:5
yn @ d3.min.js:2
gn @ d3.min.js:2
d3.min.js:5 Uncaught TypeError: Cannot create property 'vx' on number '66'
    at e (d3.min.js:5)
    at d3.min.js:5
    at Fe.each (d3.min.js:5)
    at e (d3.min.js:5)
    at n (d3.min.js:5)
    at yn (d3.min.js:2)
    at gn (d3.min.js:2)
e @ d3.min.js:5
(anonymous) @ d3.min.js:5
each @ d3.min.js:5
e @ d3.min.js:5
n @ d3.min.js:5
yn @ d3.min.js:2
gn @ d3.min.js:2

I can't actually find a good introductory resource on force graphs in d3 v4+, so I must just hack at it.

html

<main>
  <section class="d3">
  </section>
</main>

code

const api = 'https://raw.githubusercontent.com/DealPete/forceDirected/master/countries.json' 

let root = d3.select(".d3"),
    width = +root.attr("width"),
    height = +root.attr("height")

let svg = root.append('svg')
    .attr("width", width)
    .attr("height", height)

let color = d3.scaleOrdinal(d3.schemeCategory20);

let simulation = d3.forceSimulation()
    .force("link", d3.forceLink().id((d) => d.country))
    .force("charge", d3.forceManyBody())
    .force("center", d3.forceCenter(width / 2, height / 2));

d3.json(api, function(error, graph) {
  if (error) 
    throw error

  let link = svg.append("g")
    .attr("class", "links")
    .selectAll("line")
    .data(graph.links)
    .enter().append("line")
    .attr("stroke-width",  () => 4);

  let node = svg.append("g")
    .attr("class", "nodes")
    .selectAll("circle")
    .data(graph.nodes)
    .enter().append("circle")
    .attr("r", 5)
    .attr("fill", d => color(1))
    .call(d3.drag()
      .on("start", dragstarted)
      .on("drag", dragged)
      .on("end", dragended))

  simulation
    .nodes(graph.nodes)
    .on("tick", ticked)

  simulation.force("link")
    .links(graph.links)

  function ticked() {
    link
        .attr("x1", function(d) { return d.source.x; })
        .attr("y1", function(d) { return d.source.y; })
        .attr("x2", function(d) { return d.target.x; })
        .attr("y2", function(d) { return d.target.y; });

    node
        .attr("cx", function(d) { return d.x; })
        .attr("cy", function(d) { return d.y; });
  }
})

function dragstarted(d) {
  if (!d3.event.active) simulation.alphaTarget(0.3).restart();
  d.fx = d.x;
  d.fy = d.y;
}

function dragged(d) {
  d.fx = d3.event.x;
  d.fy = d3.event.y;
}

function dragended(d) {
  if (!d3.event.active) simulation.alphaTarget(0);
  d.fx = null;
  d.fy = null;
}
Chipboard answered 3/5, 2017 at 12:37 Comment(2)
The issue seems to be with your links and how they are parsed. Specifically, when inserting a return; before simulation.force("link") .links(graph.links); the nodes appear. Change .force("link", d3.forceLink().id((d, i) => d.country)) to .force("link", d3.forceLink().id((d, i) => d.index)) and : codepen.io/mkaranasou/pen/VbzvooMoussaka
(With some re-arrangement of your code also)Moussaka
H
20

Have a look at your links array:

[
    { "target": 66, "source": 0 },
    { "target": 3, "source": 1 },
    { "target": 100, "source": 2 },
    ...
]

Now have a look at your id function:

.id((d) => d.country)

As you can see, there is no country in your links array.

Therefore, since you are using the numeric index for the links, simply drop the id() function. According to the API:

If id is specified, sets the node id accessor to the specified function and returns this force. If id is not specified, returns the current node id accessor, which defaults to the numeric node.index

Here is your working code:

const api = 'https://raw.githubusercontent.com/DealPete/forceDirected/master/countries.json'

var width = 500,
  height = 500;

let svg = d3.select("body").append('svg')
  .attr("width", width)
  .attr("height", height)

let color = d3.scaleOrdinal(d3.schemeCategory20);

let simulation = d3.forceSimulation()
  .force("link", d3.forceLink())
  .force("charge", d3.forceManyBody())
  .force("center", d3.forceCenter(width / 2, height / 2));

d3.json(api, function(error, graph) {
  if (error)
    throw error

  let link = svg.append("g")
    .attr("class", "links")
    .selectAll("line")
    .data(graph.links)
    .enter().append("line")
    .attr("stroke", "black")
    .attr("stroke-width", 4);

  let node = svg.append("g")
    .attr("class", "nodes")
    .selectAll("circle")
    .data(graph.nodes)
    .enter().append("circle")
    .attr("r", 5)
    .attr("fill", d => color(1))
    .call(d3.drag()
      .on("start", dragstarted)
      .on("drag", dragged)
      .on("end", dragended))

  simulation
    .nodes(graph.nodes)
    .on("tick", ticked)

  simulation.force("link")
    .links(graph.links)

  function ticked() {
    link
      .attr("x1", function(d) {
        return d.source.x;
      })
      .attr("y1", function(d) {
        return d.source.y;
      })
      .attr("x2", function(d) {
        return d.target.x;
      })
      .attr("y2", function(d) {
        return d.target.y;
      });

    node
      .attr("cx", function(d) {
        return d.x;
      })
      .attr("cy", function(d) {
        return d.y;
      });
  }
})

function dragstarted(d) {
  if (!d3.event.active) simulation.alphaTarget(0.3).restart();
  d.fx = d.x;
  d.fy = d.y;
}

function dragged(d) {
  d.fx = d3.event.x;
  d.fy = d3.event.y;
}

function dragended(d) {
  if (!d3.event.active) simulation.alphaTarget(0);
  d.fx = null;
  d.fy = null;
}
<script src="https://d3js.org/d3.v4.min.js"></script>
Hereto answered 3/5, 2017 at 13:27 Comment(4)
Thank you.. voting you up. On my own, I had found that removing the .id call worked. But I don't feel sure about your answer. Why would this function return an id for the links themselves? That makes no sense to me. It should be providing a function to map the data to the links. Also, this discussion particularly seems to suggest that the original should be valid: github.com/d3/d3-force/issues/32 .. and note that the code in the example works, too.Krug
You didn't vote, but thanks anyway. Nope, the original is not valid, and that discussion talks about another issue. Please read this section: github.com/d3/d3-force/blob/master/README.md#link_idHereto
actually I did, I swear it. but it must've been double clicked because it was empty when I revised just now. Thanks again.Krug
One vote up is not enough for this answer ! Best explanation in the web !Brunson

© 2022 - 2024 — McMap. All rights reserved.