unable to format json data as required for forcelayout

First timer, both to Observable and Javascript! so bear with me. i downloaded the sample as shown below and tried to “fetch” data for my JSON from a python call - which i do seem to get but it is never comparable to the “links” data as shown in both the code as well as the console output. Any help here to correct me on how this ‘incoming’ JSON data can be made comparable to the “links” data on the code - would be great.

<script>
var width = 960,
    height = 500;
	
//force = d3.layout.force().nodes(d3.values(ailments)).links(rels).size([width, height]).linkDistance(60).charge(-300).on("tick", tick).start();

var nodes = {};
var ailments = {} ; 
var rels = []; 
//var force = d3.layout.force().size([width, height]).linkDistance(60).charge(-300).on("tick",tick); 
console.log('initiated force!! be with you !!'); 

d3.json("/getA", function(error, dataset){
	console.log('getA the function gets called now ... '); 
	//ailments = JSON.stringify(dataset.nodes); 
	ailments = dataset.nodes; 
	//ailments = dataset.nodes['nodes']; 
	//rels = dataset.links['links'] ; 
	//rels = JSON.stringify(dataset.links); 
	rels = dataset.links; 
	console.log(ailments); 
	//console.log(nodes);
	console.log('relationships coming up..'); 
	console.log(rels); 
	//links = [dataset.links]; 
	
	var links = [
	  {source: "burning eyes", target: "Bel", type: "SOLVED_BY"},
	  {source: "digestive disorders", target: "Bel", type: "SOLVED_BY"},
	  {source: "Piles", target: "Bel", type: "SOLVED_BY"},
	  {source: "Fever", target: "Bel", type: "SOLVED_BY"},
	  {source: "malaria", target: "Bel", type: "SOLVED_BY"}
	];
	console.log(links); 
	
	links.forEach(function(link) {
		link.source = nodes[link.source] || (nodes[link.source] = {name: link.source});
		link.target = nodes[link.target] || (nodes[link.target] = {name: link.target});
		console.log('creating unique nodes gets called now'); 
	});

	var svg = d3.select("body").append("svg")
		.attr("width", width)
		.attr("height", height);
	svg.append("defs").selectAll("marker")
		.data(["suit", "licensing", "resolved"])
	  .enter().append("marker")
		.attr("id", function(d) { return d; })
		.attr("viewBox", "0 -5 10 10")
		.attr("refX", 15)
		.attr("refY", -1.5)
		.attr("markerWidth", 6)
		.attr("markerHeight", 6)
		.attr("orient", "auto")
		.attr("stroke", "black")
	  .append("path")
		.attr("d", "M0,-5L10,0L0,5");	
	
	var force = d3.layout.force().nodes(d3.values(nodes)).links(links).size([width, height]).linkDistance(60).charge(-300).on("tick", tick).start();
	console.log('graph goes up only now'); 
	
	force.on("tick", function(e) {
	  path.attr("d", linkArc);
	  circle.attr("transform", transform);
	  text.attr("transform", transform);
	}); 
	var text = svg.append("g").selectAll("text")
		.data(force.nodes())
	  .enter().append("text")
		.attr("x", 8)
		.attr("y", ".31em")
		.text(function(d) { return d.name; });

	var path = svg.append("g").selectAll("path")
		.data(force.links())
	  .enter().append("path")
		.attr("class", function(d) { return "link " + d.type; })
		.attr("marker-end", function(d) { return "url(#" + d.type + ")"; });
	var circle = svg.append("g").selectAll("circle")
		.data(force.nodes())
	  .enter().append("circle")
		.attr("r", 6)
		.call(force.drag);
	circle.attr("transform", function(d) {
		return "translate(" + d.x + "," + d.y + ")";
	}); 
	
}); 
/*
// http://blog.thomsonreuters.com/index.php/mobile-patent-suits-graphic-of-the-day/
var oldlinks = [
  {source: "Microsoft", target: "Amazon", type: "licensing"},
  {source: "Microsoft", target: "HTC", type: "licensing"},
  {source: "Samsung", target: "Apple", type: "suit"},
  {source: "Motorola", target: "Apple", type: "suit"},
  {source: "Nokia", target: "Apple", type: "resolved"},
  {source: "Nokia", target: "Qualcomm", type: "suit"}
];

*/
function tick() {
  path.attr("d", linkArc);
  circle.attr("transform", transform);
  text.attr("transform", transform);
}
	
function linkArc(d) {
  var dx = d.target.x - d.source.x,
      dy = d.target.y - d.source.y,
      dr = Math.sqrt(dx * dx + dy * dy);
  return "M" + d.source.x + "," + d.source.y + "A" + dr + "," + dr + " 0 0,1 " + d.target.x + "," + d.target.y;
}

function transform(d) {
  return "translate(" + d.x + "," + d.y + ")";
}

</script>

The console output is also shown below … obviously something is wrong with my data construction. The data structure at the top - is what i receive from Python but am unable to use it AS IS - so i’ve pasted the original hand created data structure at the bottom for ease of comparison.

1 Like

Hi Atom,

Welcome to not only the forum, but also Observable (and JavaScript)! We would love to help you with this :slight_smile: It’s a bit tricky to do so with the code snippet, since we don’t have the full context (e.g. what version of d3 you’re using). Would it be possible to share an Observable notebook that exhibits the problem you’re facing?

If it’s the case that you have a working force layout with some hand-made data, but the same code fails to render the data you’re providing from python, then the issue is likely that the “shape” of the data is different, and you will need to modify the data that comes from python (either in python before it gets to JavaScript, or in JavaScript itself) to ensure that the shape is what is expected.

In your console.log statements in the screenshot I see that there is a difference between the two. In the first the shape of each item in the array is {source: string, target: string, type: string} whereas in the second log the shape of each item is {source: {name: string, ...}, target: {name: string, ...}, type: string}. In other words, the source/target in the first is just the name, whereas the source/target in the second is an object that contains a property name.

If you want to transform the first into the second, you could do so like this (though you will probably need to rename the variables to be what you’re using in your code):

const transformedLinks = links.map((link) => ({
  source: {name: link.source},
  target: {name: link.target},
  type: link.type
}));

Or the equivalent with a plain for loop:

const transformedLinks = [];
for (const link of links) {
  transformedLinks.push({
    source: {name: link.source},
    target: {name: link.target},
    type: link.type
  });
}

Let me know if this helps!

Duane

3 Likes

Thanks Duane … i figured the expectation “here” in javascript is an array which became possible when i changed this particular line - to what is called out below.

links = dataset.links[“links”]

i tried tampering this in many other ways (json.strigify/ doing json.dumps in the python code before it was sent :)) before i realized that am just calling dataset.links instead of getting an array out. Once again - huge thanks for the response but this stands fixed right now.

The updated code (i’ve taken to another example for colors and size of circle) that works is pasted below. I also didn’t realize that all my code has to be within the json function (at least the ones that matter).

d3.json(“/getA”, function(error, dataset){
console.log('getA the function gets called now … ');

var nodes = {};

links = dataset.links[“links”];

// Compute the distinct nodes from the links.
links.forEach(function(link) {
link.source = nodes[link.source] ||
(nodes[link.source] = {name: link.source});
link.target = nodes[link.target] ||
(nodes[link.target] = {name: link.target});
link.value = +link.value;
});

var width = 760,
height = 500,
color = d3.scale.category20c();

var force = d3.layout.force()
.nodes(d3.values(nodes))
.links(links)
.size([width, height])
.linkDistance(60)
.charge(-300)
.on(“tick”, tick)
.start();

// Set the range
var v = d3.scale.linear().range([0, 100]);

// Scale the range of the data
v.domain([0, d3.max(links, function(d) { return d.value; })]);

// asign a type per value to encode opacity
/*
links.forEach(function(link) {
if (v(link.value) <= 25) {
link.type = “twofive”;
} else if (v(link.value) <= 50 && v(link.value) > 25) {
link.type = “fivezero”;
} else if (v(link.value) <= 75 && v(link.value) > 50) {
link.type = “sevenfive”;
} else if (v(link.value) <= 100 && v(link.value) > 75) {
link.type = “onezerozero”;
}
});
*/
links.forEach(function(dink) {
if (v(dink.name).toString().includes(“Bel”)) {
dink.type = “searchresult”;
}
else
{
dink.type = “nodes”;
}
console.log(dink.type);
});

var svg = d3.select(“body”).append(“svg”)
.attr(“width”, width)
.attr(“height”, height);

// build the arrow.
svg.append(“svg:defs”).selectAll(“marker”)
.data([“end”]) // Different link/path types can be defined here
.enter().append(“svg:marker”) // This section adds in the arrows
.attr(“id”, String)
.attr(“viewBox”, “0 -5 10 10”)
.attr(“refX”, 15)
.attr(“refY”, -1.5)
.attr(“markerWidth”, 6)
.attr(“markerHeight”, 6)
.attr(“orient”, “auto”)
.append(“svg:path”)
.attr(“d”, “M0,-5L10,0L0,5”);

// add the links and the arrows
var path = svg.append(“svg:g”).selectAll(“path”)
.data(force.links())
.enter().append(“svg:path”)
.attr(“class”, function(d) { return "link " + d.type; })
.attr(“marker-end”, “url(#end)”);

// define the nodes
var node = svg.selectAll(“.node”)
.data(force.nodes())
.enter().append(“g”)
.attr(“class”, “node”)
.on(“click”, click)
.on(“dblclick”, dblclick)
.call(force.drag);

// add the nodes
node.append(“circle”)
.attr(“r”, 5)
.style(“fill”, function(d) { return color(d.name); });

// add the text
node.append(“text”)
.attr(“x”, 12)
.attr(“dy”, “.35em”)
.text(function(d) { return d.name; });

// add the curvy lines
function tick() {
path.attr(“d”, function(d) {
var dx = d.target.x - d.source.x,
dy = d.target.y - d.source.y,
dr = Math.sqrt(dx * dx + dy * dy);
return “M” +
d.source.x + “,” +
d.source.y + “A” +
dr + “,” + dr + " 0 0,1 " +
d.target.x + “,” +
d.target.y;
});

node
    .attr("transform", function(d) { 
      return "translate(" + d.x + "," + d.y + ")"; });

}

// action to take on mouse click
function click() {
d3.select(this).select(“text”).transition()
.duration(750)
.attr(“x”, 22)
.style(“stroke”, “lightsteelblue”)
.style(“stroke-width”, “.5px”)
.style(“font”, “20px sans-serif”);
d3.select(this).select(“circle”).transition()
.duration(750)
.attr(“r”, 16);
}

// action to take on mouse double click
function dblclick() {
d3.select(this).select(“circle”).transition()
.duration(750)
.attr(“r”, 6);
d3.select(this).select(“text”).transition()
.duration(750)
.attr(“x”, 12)
.style(“stroke”, “none”)
.style(“fill”, “black”)
.style(“stroke”, “none”)
.style(“font”, “10px sans-serif”);
}

});

The next question that am working oN right now is how to draw the same graph with Categorical FOCI as i heard in one vimeo video by Mike Bostock … i would like to gravitate the points towards multiple foci that are a result of a search outcome. If you have any observable samples for the same kindly advise.

This is a wonderful library that you guys maintain … hats off.

Also - to give you some context - i happened to NLP / text process some basic plants → cure for common ailments - type data into a graph db. And since the neo4j view does not extend to an end user easily enough i thought it best to show at least the basic level of UI through forcelayout (which seemed almost the best fit for the ask here). For ex. i will have a simple search ability that will return values like this - screen grab from neo4j itself. The only challenge i have with forcelayout is - it has a bit of a mind of it’s own :slight_smile: i need to be able to corner / position / color a few variables appropriately (based on this context). Thanks for reading.

1 Like