Sankey diagram transition - json

I would like to know if there is an easy way to modify the Sankey diagram example so that there is smooth transition to new data. For example, imagine I have different datafiles (energy1.json, energy2.json ...) how could d3 plot a Sankey diagram for the first dataset, then waits and later on rearanges the boxes disposition to represent the second dataset?

This is possible. Here's one approach using a csv file. Working sankey here: https://www.betterment.com/resources/investment-strategy/portfolio-management/portfolio-diversification/
Define a global array outside of your d3.csv call.
var portfolioValues = [];
When parsing the csv to create the node/link structure, push values to your global array.
d3.csv("etf-geo.csv", function(error, data) {
graph = {"nodes" : [], "links" : []};
data.forEach(function (d, i) {
var item = { source: d.source, target: d.target, values: [] };
for (var j=0; j < 101; j++) {
item.values.push(d['value'+j.toString()]);
}
portfolioValues.push(item);
graph.nodes.push({ "name": d.source });
graph.nodes.push({ "name": d.target });
graph.links.push({
source: portfolioValues[i].source,
target: portfolioValues[i].target,
value: portfolioValues[i].values[startingAllocation]
});
});
//this handy little function returns only the distinct / unique nodes
graph.nodes = d3.keys(
d3.nest()
.key(function (d) { return d.name; })
.map(graph.nodes)
);
// it appears d3 with force layout wants a numeric source and target
// so loop through each link replacing the text with its index from node
graph.links.forEach(function (d, i) {
graph.links[i].source = graph.nodes.indexOf(graph.links[i].source);
graph.links[i].target = graph.nodes.indexOf(graph.links[i].target);
portfolioValues[i].source = graph.links[i].source;
portfolioValues[i].target = graph.links[i].target;
});
// now loop through each nodes to make nodes an array of objects
// rather than an array of strings
graph.nodes.forEach(function (d, i) {
graph.nodes[i] = { "name": d };
});
// construct sankey
sankey
.nodes(graph.nodes)
.links(graph.links)
.layout();
Listen for a change and pass user input to your update function.
$(".sankey-slider").bind("slider:changed", function (event, data) {
slideValue = data.value;
updateData(parseInt(slideValue));
});
Create a temporary array and retrieve the correct values from the global array. Call the sankey functions to recalculate the layout.
var newLinks = [];
portfolioValues.forEach(function(p, i) {
newLinks.push({
source: p.source,
target: p.target,
value: p.values[allocation]
});
});
graph.links = newLinks;
sankey
.nodes(graph.nodes)
.links(graph.links)
.size([width, height])
.layout();
Select each element that needs to be changed and pass the new data values.
d3.selectAll(".link")
.data(graph.links)
.attr("d", path)
.attr("id", function(d,i){
d.id = i;
return "link-"+i;
})
.style("stroke-width", function(d) { return Math.max(1, d.dy); })
.sort(function(a, b) { return b.dy - a.dy; });
d3.selectAll(".node").attr("transform", function(d) {
return "translate(" + d.x + "," + d.y + ")"; });
d3.selectAll("rect")
.attr("height", function(d) { return d.dy; })
.on("mouseover",highlight_node_links)
.on("mouseout",onNodeMouseout);
Working sankey here:
https://www.betterment.com/resources/investment-strategy/portfolio-management/portfolio-diversification/

Since the automatic positioning of nodes includes a part which tries to minimize link distance in a connected graph which is an np optimization problem, any kind of optimizer can potentially jump from one minimum to another leading to a jump in layout. So a guaranteed smooth transition wont be possible.
The closest possible solution would probably be to linearly interpolate between the two input data sets and thereby generate a series of graphs which (depending on the data) more or less smoothly transition from one two the other.
Hope this helps.

Related

How to reset coordinates into an Ajax call after having initialized a map?

I inserted a map on my webpage by using the Leaflet library. What I want to do is to show a map zoomed on a specific region according to which city the user types into a text field.
I firstly initialized my map on my JS file:
function initMaps(){
map = L.map('leaflet').setView([0, 0], 13);
L.tileLayer('http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
maxZoom: 18,
'attribution': 'Map data © OpenStreetMap contributors'
}).addTo(map);
}
My javascript code also has an Ajax call.
What I want to do now is to reset the coordinates on the Ajax call.
I wrote the following:
var readCoordinates = function(){
$.ajax({
url: "https://nominatim.openstreetmap.org/search?q=" + encodeURIComponent($("#inlineFormInputCitta").val()) + "+Italy&format=geocodejson",
dataType: "json",
success: function (data) {
setTimeout(function () {
for (let i = 0; i < data.features.length; i++) {
let coordinate = data.features[i].geometry.coordinates;
console.log(coordinate);
map.setView(coordinate, 13);
console.log("ajax and for loop have been activated");
console.log(coordinate.geometry.coordinates);
};
$("#ristoranti").prop("disabled", false);
}, 1000);
}
});
};
The API I'm referring to in the URL is the following: https://nominatim.openstreetmap.org/search?q=Roma%20Italy&format=geocodejson
What I did is trying to reset the coordinates here: map.setView(coordinate, 13);
after having cycled the elements in the JSON object, see the following:
for (let i = 0; i < data.features.length; i++) {
let coordinate = data.features[i].geometry.coordinates;
I may display several coordinates in the console, see the following:
That's because in the JSON file I get through the API request there are several:
The result of this is the following map, which isn't zoomed anywhere:
Which coordinates should I take in order to display that specific region?
EDIT - - -
I changed the code because I'm trying to get a specific subobject, i.e. the one in the screen below (which has "type" = "city"):
The new snippet is the one below, where I add an if statement:
var readCoordinates = function(){
$.ajax({
url: "https://nominatim.openstreetmap.org/search?q=" + encodeURIComponent($("#inlineFormInputCitta").val()) + "+Italy&format=geocodejson",
dataType: "json",
success: function (data) {
setTimeout(function() {
for (let i = 0; i < data.features.length; i++) {
debugger;
let type = data.features[i].properties.geocoding.type;
if( $(type).val() === "city") {
let coordinate = data.features[i].geometry.coordinates;
let lng = coordinate[0];
let lat = coordinate[1];
map.setView([lng, lat], 13);
console.log("ajax and for loop have been activated");
console.log(coordinate);}
};
$("#ristoranti").prop("disabled", false);
}, 1000);
}
});
};
I'm doing the debugger and get many undefined values:
I would do something like that:
if (typeof data.features[0] !== 'undefined') {
let coordinate = data.features[0].geometry.coordinates;
var latlng = L.latLng(coordinate.reverse());
map.flyTo(latlng, 12)
}
Be sure to have something in your array
Get data from the first item since it should be the correct one in most case
Create a latlng with those coordinates. Be careful, sometime you need to reverse the array to have the correct position.
Use flyTo to have a smooth transition to your coordinates. 12 is the zoom level
You don't need to loop over the data since you need only one position. You can replace the for with that.
You're having two problems here:
The response from the Nominatim API is returning several search results, each of them in one GeoJSON Feature inside the response FeatureCollection. It's up to you to choose which search result you want to focus in the map (the first?), or let the user do so.
You're not aware that GeoJSON uses longitude-latitude (or x-y) coordinates, whereas Leaflet uses latitude-longitude (or y-x)

Reading nodes data from local variable in d3

How to create a d3 graph from local variable jsonNodes (JSON data) rather than reading from a file, the example I am using Andrew-Reid’s online example on bl.ocks.
Following is the code I am trying, the button 'Go' is calling the refresh function to refresh the graph by using the jsonNodes variable. ( I am actually calling a WebSocket service to get the jsonNodes populated but as of now i have just hardcoded it in the function).
when I click the 'Go' button, it does nothing.
<canvas width="960" height="500"></canvas>
<body>
<button id="refresh" onclick="refresh();">Go</button>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script>
var canvas = document.querySelector("canvas"),
context = canvas.getContext("2d"),
width = canvas.width,
height = canvas.height;
var jsonNodes;
function refresh(){
jsonNodes = {
"nodes": [
{"id": "Myriel", "group": 1},
{"id": "Napoleon", "group": 1},
{"id": "Mlle.Baptistine", "group": 1} ],
"links": [
{"source": "Napoleon", "target": "Myriel", "value": 1} ]
} ;
simulation
.nodes(jsonNodes.nodes)
.force("link")
.links(jsonNodes.links);
};
var simulation = d3.forceSimulation()
.force("link", d3.forceLink().id(function(d) { return d.id; }))
.force("charge", d3.forceManyBody())
.force("center", d3.forceCenter(width / 2, height / 2));
d3.json("../data/miserables.json", function(error, graph) {
if (error) throw error;
simulation
.nodes(graph.nodes)
.on("tick", ticked);
simulation.force("link")
.links(graph.links);
d3.select(canvas)
.call(d3.drag()
.container(canvas)
.subject(dragsubject));
function ticked() {
var margin = 20;
graph.nodes.forEach(function(d) {
d.x = Math.max(margin, Math.min(width - margin, d.x))
d.y = Math.max(margin, Math.min(height - margin, d.y))
})
context.clearRect(0, 0, width, height);
context.beginPath();
graph.links.forEach(drawLink);
context.strokeStyle = "#aaa";
context.stroke();
context.beginPath();
graph.nodes.forEach(drawNode);
context.fill();
context.strokeStyle = "#fff";
context.stroke();
}
function dragsubject() {
return simulation.find(d3.event.x, d3.event.y);
}
});
function drawLink(d) {
context.moveTo(d.source.x, d.source.y);
context.lineTo(d.target.x, d.target.y);
}
function drawNode(d) {
context.moveTo(d.x + 3, d.y);
context.arc(d.x, d.y, 3, 0, 2 * Math.PI);
}
</script>
</body>
Based on your updated post with the sample code, there are few things which may be causing failure to achieve desired results.
Firstly, I can see that the jsonNodes is an object in the desired format already.
Secondly, since the data is already in the desired format in a variable, you do not need to parse it in using d3.json("../data/miserables.json", function(error, graph) {
if (error) throw error;.
Also in the example by Andrew Reid, he uses the object graph, which in your case would jsonNodes. So where ever, you see graph.nodes or graph.links replace that with jsonNodes.nodes and jsonNodes.links. A simpler alternative to changing all individual locations of this is to transfer the data from jsonNodes to a variable called graph, and use the graph variable as in Andrew Reid's example.
Lastly, the order of you declaring simulation is wrong. You are calling
simulation
.nodes(jsonNodes.nodes)
.force("link")
.links(jsonNodes.links);
};
before
var simulation = d3.forceSimulation()
.force("link", d3.forceLink().id(function(d) { return d.id; }))
.force("charge", d3.forceManyBody())
.force("center", d3.forceCenter(width / 2, height / 2));
This is opposite.
Here is a plunker with everything fixed for you:
https://plnkr.co/edit/LcsGes?p=preview
Some other examples: http://jsfiddle.net/DavidHoksza/0d7en4po/ which directly uses data in a variable. However, keep in mind that this example uses d3 v3 and the syntax for the force simulation has changed. There are many more examples which use variable directly in force simulations: http://blockbuilder.org/kenpenn/268878b410ddb1201277be3f0305d566
You can search for more examples using http://blockbuilder.org/search
I hope this helps. If not please create plunker / block for me to check what you have going exactly. And populate more data into the jsonNodes variable.

Trouble Binding JSON subarrays in d3.js

I guess anyone who monitors the d3 questions is getting used to seeing me this week (this is my first time using d3) so here goes another question. I am fetching data via php/ajax but I have placed a sample as hard code below. I wish to bind the fineBinSummary data to a bar chart but can't seem to figure out how. When I inspect the html it makes it as far as setting up the svg and then stops when I start to try to bind data to it.
I successfully bound the detail data (code not shown, just the JSON) but can't get the fineBinSummary data to bind in the same manner. I also will need to extract the length of the fineBinSummary array (you can see that I've just made it the width / 5 but there won't always be 5 bars in the chart. Any assistance is much appreciated.
(document).ready(function() {
var data = [
{"Id":"93439440411",
"detail":[{"fineBin":"UNCLASSIFIED","x":"-142000.0","y":"-21000.0","imageId":null,"serverId":null,"fileSpec":null,"color":"HotPink"},
{"fineBin":"UNCLASSIFIED","x":"-142000.0","y":"16000.0","imageId":null,"serverId":null,"fileSpec":null,"color":"HotPink"},
{"fineBin":"UNCLASSIFIED","x":"-141000.0","y":"-15000.0","imageId":null,"serverId":null,"fileSpec":null,"color":"HotPink"},
{"fineBin":"UNCLASSIFIED","x":"-141000.0","y":"24000.0","imageId":null,"serverId":null,"fileSpec":null,"color":"HotPink"},
{"fineBin":"UNCLASSIFIED","x":"-141000.0","y":"27000.0","imageId":null,"serverId":null,"fileSpec":null,"color":"HotPink"},
{"fineBin":"UNCLASSIFIED","x":"-140000.0","y":"-15000.0","imageId":null,"serverId":null,"fileSpec":null,"color":"HotPink"}],
"fineBinSummary":[{"fineBin":"UNCLASSIFIED","count":8212},{"fineBin":"SMALL_PARTICLE","count":104},{"fineBin":"RESIDUE","count":68},
{"fineBin":"FALSE","count":16},{"fineBin":"258","count":12}]},
{"Id":"93439440419",
"detail":[{"fineBin":"UNCLASSIFIED","x":"-142839.40900000001","y":"20448.394","imageId":null,"serverId":null,"fileSpec":null,"color":"HotPink"},
{"fineBin":"UNCLASSIFIED","x":"-142546.65599999999","y":"26731.720000000001","imageId":null,"serverId":null,"fileSpec":null,"color":"HotPink"},
{"fineBin":"UNCLASSIFIED","x":"-142499.136","y":"-24443.516","imageId":null,"serverId":null,"fileSpec":null,"color":"HotPink"},
{"fineBin":"UNCLASSIFIED","x":"-142267.68799999999","y":"32411.870999999999","imageId":null,"serverId":null,"fileSpec":null,"color":"HotPink"},
{"fineBin":"UNCLASSIFIED","x":"-142000.0","y":"-33000.0","imageId":null,"serverId":null,"fileSpec":null,"color":"HotPink"}],
"fineBinSummary":[{"fineBin":"UNCLASSIFIED","count":8212},{"fineBin":"SMALL_PARTICLE","count":104},{"fineBin":"RESIDUE","count":68},
{"fineBin":"FALSE","count":16},{"fineBin":"258","count":12}]}]
//Constants
var squareSide = 400;
var height = squareSide / 2.0;
var barWidth = squareSide / 5;
//begin building bootstrap courousel stack
var slides = d3.select(".carousel-inner").selectAll('div')
.data(data)
.enter()
.append("div")
.attr("class","item")
.append("div")
.attr("class","container-fluid")
.append("div")
.attr("class","row");
//Make the first slide the active slide
d3.selectAll(".item").classed('active', function(d, i) { return i == 0; });
//Build the image side of the slide
var imageSide = slides.append("div")
.attr("class","col-lg-6 wafer-and-pareto");
//build final location for div that will hold the histogram
var paretoBox = imageSide.append("div")
.attr("class","row")
.append("div")
.attr("class","col-lg-12")
.append("div")
.attr("class","pareto-box")
.append("svg")
.attr("class","chart")
.attr("width",squareSide)
.attr("height",squareSide / 2.0);
Seems to be working up to this point based on reviewing the html but does not successfully execute the code below:
var bar = paretoBox.selectAll("g")
.data(function(d) {return data.fineBinSummary})
.enter()
.append("g")
.attr("transform",function(d,i) {
return "translate(" + i * barWidth + ",0)";
});
bar.append("rect")
.attr("y",function(d) {
return y(d.count);
})
.attr("height",function(d) {
return height - y(d.count)
})
.attr("width",barWidth - 1);
var y = d3.scale.linear()
.range([height,0])
.domain([0,d3.max(data.fineBinSummary,function(d) {
return d.count;
})]);
})
There are just two small problems with your code. First, in your nested selection, you are referencing the original data instead of the part bound to the current element. Instead of
var bar = paretoBox.selectAll("g")
.data(function(d) {return data.fineBinSummary})
use
var bar = paretoBox.selectAll("g")
.data(function(d) {return d.fineBinSummary})
Second, the max element for your scale is determined incorrectly -- data doesn't have a fineBinSummary member. The code should look like this:
var y = d3.scale.linear()
.range([height,0])
.domain([0, d3.max(data, function(d) {
return d3.max(d.fineBinSummary, function(e) { return e.count; });
})]);

Changing JSON files in d3js force visualizaition

Ok I have found a couple of resources that have tried to load two datasources using d3js. Unfortunately they don't seem to solve the problem I having.
Essentially what I am trying to do is create a force layout with nodes and links and have the ability to change datasources via a selection from a drop down menu.
Here are the basics...I retrieve all nodes and links from and external file using d3.json. Everything works perfectly and I have even animated the visualization to cycle through the different dates for each node. My problem is, now I am trying to expand on the functionality of this visualization...I want to be able to select a .json file from a dropdown menu and then reload the visualization with the new data. Basically providing the user to view several different visualizations of completely different data.
All the concepts appear to be working except for when I redraw the visualization using the new data file, some nodes appear and float off on their own not linking to the the original node. I forgot the first 7 nodes I have hard coded their x, y coordinates to be in the middle of the visualization and every node after that uses the force functionality to place themselves depending on what they are linked to.
Here is some code
$("#customer").change(function(){ // this is the dropdown menu
$("#customer option:selected").each(function () {
var line = d3.selectAll("line").remove(); // remove all links on screen
var g = d3.selectAll("g").remove(); // remove all nodes on screen
dataFile = this.innerHTML; // changes variable used in d3.json
change(); // calls function wrapper that contains d3.json
});
function change(){
d3.json(dataFile, function(fileData){
rtm = new Object(); // initialize dictionary object
dates = []; // initialize dates[] array to contain all dates in data
var node = {}; // node object
// traverse through data contained in file
for(var i = 0; i < fileData.nodes.length; i++){
node = fileData.nodes[i]; // assign current node to node object
node.date = new Date(node.date);
rtm[node.id] = node; // add node object to dictionary object
// check if date exists in dateArray
if(findDate(node.date, dates)){
dates.push(node.date); // add dates that don't exist
}
}
getNodes(fileData.nodes[0].date); // finds the nodes to be visualized and updates the array passed to force.nodes()
getLinks(fileData.nodes[0].date); // finds the nodes to be visualized and updates the array passed to force.links()
start(); // initiates visualization after d3.json() finishes
});
}
// function controls visualization. Uses d3.js framework to control visualization
// must be called from within d3.json for first initialization or nothing will render
function start(){
// stop force functionality to allow for removal and adding of new objects
force.stop();
// remove all elements inside visualization
var line = d3.selectAll("line").remove(); // remove all links on screen
var g = d3.selectAll("g").remove(); // remove all nodes on screen
// create any links needed, dependent on links contained links[] array
// selects links already created or adds new links.
link = svg.selectAll("line.link")
.data(force.links(), function(d){ return d.id;}) // 2nd parameter can be added to use as a comaparison for pairing existing links with identical same data
link.enter().append("line") // new data will be bound to a new line. 2nd parameter in .data() allows for element reuse
// instead of creation, otherwise data and line elements are matched on index. If those
// two don't match then a new element is created.
.attr("class", "link") // assigns .link class to link element to allow for css styling
//.style("stroke-opacity", .5)
.style("stroke-width", 8)
.style("stroke", function(d){
return d.target.id > 7 ? (d.target.fraud ? "Red" : "#35586C") : "Grey";
});
link.exit().remove(); // removes any links removed from links[] array
// create any nodes needed, dependent on data contained in .data()
// selects nodes already created or adds new nodes. matches nodes to data
node = svg.selectAll(".node")
.data(force.nodes(), function(d){ return d.id;}) // 2nd parameter can be added to use as a comaparison for pairing existing node with identical same data
node.enter().append("g") // new data will be bound to a new node. 2nd parameter in .data() allows for element reuse
// instead of creation, otherwise data and node elements are matched on index. If those
// two don't match, a new element is created.
.attr("class", "node") // assigns .node class to node element to allow for css styling
.call(force.drag); // allows for nodes to be dragged and moved. x,y coordinates will adjust to movement
//.transition().duration(10000).delay(10000); // transitions any new nodes into the visualization
node.exit().remove(); // removes any nodes removed from nodes[] array
node.append("image")
.attr("xlink:href", function(d){
// add path for image of each node
var path = "images/";
if(!d.fraud){ // check if fraudulent node
if(d.name == "Record"){
path += "record-G.gif";
return path;
}
if(d.id <= 7){
path += "Record/";
}else {
path += "Linked/";
}
if(d.name == "SSN"){
path += "SSN.png";
}else if(d.name == "Name"){
path += "Name.png";
}else if(d.name == "Address"){
path += "Address.png";
}else if(d.name == "Phone"){
path += "Phone.png";
}else if(d.name == "DOB"){
path += "DOB.png";
}else if(d.name == "Email"){
path += "Email.png";
}else if(d.name == "Record"){
path += "record-G.gif";
}else if(d.name == "App"){
path += "App.png";
}else {
path += "nan.gif";
}
}else {
path += "FraudApp.png"
}
return path;
})
.attr("class", "image")
.attr("x", function(d){ return d.name == "Record" ? -30 : -20})
.attr("y", function(d){ return d.name == "Record" ? -30 : -20})
.attr("height", function(d){ return d.name == "Record" ? 60 : 40})
.attr("width", function(d){ return d.name == "Record" ? 60 : 40});
force.start(); // restart force functionality
}
I have no idea if any of this makes sense. A quick synopsis: I want to provide functionality to change .json files and redraw(start over) the visualization with the data contained in the that .json file.

Is d3 edge labeling possible?

Is it possible to have edge labels in a graph using d3? I've looked over the examples and some of the documentation, and I can see where node labeling is possible, but edge labeling is not explicitly mentioned anywhere (that I can find).
Following other peoples examples I
add a path alongside every edge,
add text to the edges
bind that text to a textpath which is referencing the path along the edge
This example is using the above ideas: http://bl.ocks.org/jhb/5955887
The short answer is "yes", but there's no explicit support for it. You'll have to determine the position of the label yourself. One way of doing this would be to attach the label to the start node and translate it by half the distance to the target node, plus some offset to prevent it overlapping the line. For more elaborate edges (e.g. curves) it would be more difficult.
I tried the text on a path option, but it is quite involved and didn't result in the appearance I wanted. Unfortunately, I didn't check it into the local git repository. This comes from my Eclipse history (thanks Eclipse developers). You'll have to change this code snippet to your own data structure, but I'll hope it is of some help. In particular, notice the adding of the id value to the lines selection and then reusing it in the percentages selection by using the xlink:href attribute. Also, I appended a tspan in the textpath to move the text down by a couple of pixels so it appears over the path instead of on it.
function drawLines(links) {
var diagonal = d3.svg.diagonal();
var format = d3.format(".1%");
var linkKey = function(l) {return l.target.key; };
var lines = linesGroup.selectAll("path").data(links, linkKey);
lines.enter()
.append("path")
.on("mouseover", select)
.on("mouseout", unselect)
.attr("d", diagonal)
.attr("id", function (l) { return "interaction-path-target-" + l.target.key; })
.style("stroke-width", 0.000001);
lines.exit()
.transition().duration(500)
.style("stroke-width", 0.000001)
.remove();
lines.transition()
.delay( function(d, i) { return i * 100; })
.duration(500)
.style("stroke-width", function(d) { return d.weight == 0 ? 0.000001 : d.weight / 1000; })
.attr("d", diagonal);
var percentages = linesGroup.selectAll("text").data(links, linkKey);
percentages.enter()
.append("text")
.attr("opacity", 1)
.append("svg:textPath")
.attr("startOffset", "70%")
.attr("xlink:href",
function(l) {
return "#interaction-path-target-" + l.target.key;
})
.append("svg:tspan")
.attr("dy", 3)
.attr("class", "percentageText")
percentages.exit()
.transition().duration(500)
.attr("opacity", 0)
.remove();
percentages
.transition()
.delay( function(d, i) { return i * 100; })
.duration(500)
.attr("opacity", 1);
percentages.select(".percentageText").text(function(d) {
var newvalue = d.weight ?
d.incomming ? percentageIn(d.weight) : percentageOut(d.weight) : 0;
return format(newvalue);
});
}