D3 - Multiple wordclouds from the same source file - javascript

I'm having some trouble trying to create multiple wordclouds from a single source json using d3.
Edit: Forgot to mention, I'm basing the wordclouds on Jason Davies example, here.
There are various guides for creating small multiples from the same file, but I can't find anything which involves using a layout model for each chart, as is the case for the word-cloud layout.
I want to make a separate wordcloud for each item in the 'results' object:
sourcedata = {
"results":
[
{
"category":"spain",
"words":[["i", 190], ["the", 189], ["it", 139], ["you", 134], ["to", 133], ["a", 131]]
},
{
"category":"england",
"words":[["lol", 37], ["on", 36], ["can", 35], ["do", 35], ["was", 33], ["mike", 33], ["but", 31], ["get", 30], ["like", 30]]
},
{
"category":"france",
"words":[["ve", 18], ["make", 18], ["nick", 18], ["soph", 18], ["got", 18], ["he", 17], ["work", 17]]
},
{
"category":"germany",
"words":[["about", 13], ["by", 13], ["out", 13], ["probabl", 13], ["how", 13], ["video", 12], ["an", 12]]
}
]
}
Since each wordcloud needs it's own layout model, I'm trying to use a forEach loop to go through each category, creating a model and 'draw' callback method for each one:
d3.json(sourcedata, function(error, data) {
if (error) return console.warn(error);
data = data.results;
var number_of_charts = data.length;
var chart_margin = 10;
var total_width = 800;
var chart_width_plus_two_margin = total_width/number_of_charts;
var chart_width = chart_width_plus_two_margin - (2 * chart_margin);
data.forEach(function(category) {
svg = d3.select("body")
.append("svg")
.attr("id", category.name)
.attr("width", (chart_width + (chart_margin * 2)))
.attr("height", (chart_width + (chart_margin * 2)))
.append("g")
.attr("transform", "translate(" + ((chart_width/2) + chart_margin) + "," + ((chart_width/2) + chart_margin) + ")");
d3.layout.cloud().size([chart_width, chart_width])
.words(category.words.map(function(d) {
return {text: d[0], size: 10 + (d[1] / 10)};
}))
.padding(5)
.rotate(function() { return ~~(Math.random() * 2) * 90; })
.font("Impact")
.fontSize(function(d) { return 10 + (d[1] * 10); })
.on("end", drawInner)
.start();
function drawInner(words) {
svg.selectAll("text").data(words)
.enter().append("text")
.style("font-size", function(d,i) { return d.size + "px"; })
.style("font-family", "Impact")
.style("fill", function(d, i) { return fill(i); })
.attr("text-anchor", "middle")
.attr("transform", function(d) {
return "translate(" + [d.x+(chart_width/2), d.y+(chart_width/2)] + ")rotate(" + d.rotate + ")";
})
.text(function(d) { return d.text; });
}
});
});
However, when I run this, I get one set of svg/g tags per category, but each one only contains a single text tag, with just one of the words from that category, and a size of 0.
Any help much appreciated, thanks!
Edit: See my own reply for fixed code. Thanks to Mark.

Stared at this for too long. The problem is this line:
.fontSize(function(d) { return 10 + (d[1] * 10); })
The d here is constructed object from the layout, not your array entry. This fails silently.
Substitute it with:
.fontSize(function(d) { return d.size; })
Also, check your translate math. It seems to be shifting the g elements out of the svg.
Example here.
Another potential pitfall is that since the drawInner is a callback it appears to be called in an async fashion. Because of this you have a potential for the svg variable to be overwritten with multiple calls to drawInner. I would consider moving the svg creation to inside the call back. One way to do this is:
...
.on("end", function(d, i) {
drawInner(d, category.category);
})
.start();
function drawInner(words, name) {
svg = d3.select("body")
.append("svg")
.attr("id", name)
...
So that you can still pass in the category name.
Updated example.

Thanks to Mark's fix - here's the working code:
d3.json('./resources/whatsappList.json', function(error, data) {
if (error) return console.warn(error);
var fill = d3.scale.category20(); //added missing fill scale
data = data.results;
var number_of_charts = data.length;
var chart_margin = 10;
var total_width = 800;
var chart_width_plus_two_margin = total_width/number_of_charts;
var chart_width = chart_width_plus_two_margin - (2 * chart_margin);
data.forEach(function(category) {
d3.layout.cloud().size([chart_width, chart_width])
.words(category.words.map(function(d) {
return {text: d[0], size: 10 + (d[1] / 10)};
}))
.padding(5)
.rotate(function() { return ~~(Math.random() * 2) * 90; })
.font("Impact")
.fontSize(function(d) { return d.size; }) //fixed error on size
.on("end", function(d, i) {
drawInner(d, category.category);
})
.start();
function drawInner(words, name) {
svg = d3.select("body")
.append("svg")
.attr("id", name)
.attr("width", (chart_width + (chart_margin * 2)))
.attr("height", (chart_width + (chart_margin * 2)))
.append("g")
.attr("transform", "translate(0,0)") //neutralised pointless translation
.selectAll("text").data(words)
.enter().append("text")
.style("font-size", function(d,i) { return d.size + "px"; })
.style("font-family", "Impact")
.style("fill", function(d, i) { return fill(i); })
.attr("text-anchor", "middle")
.attr("transform", function(d) {
return "translate(" + [d.x+(chart_width/2), d.y+(chart_width/2)] + ")rotate(" + d.rotate + ")";
})
.text(function(d) { return d.text; });
}
});
});

Related

Updatable chord diagram

I am working on a D3 chord diagram and want to have an update function.
This is the code I have at the moment
// Genres, check de readme daar staat visueel hoe je dit uitleest.
var data = [
[9962, 1196, 94, 93, 18],
[1196, 9102, 11, 343, 169],
[94, 11, 7143, 138, 32],
[93, 343, 138, 6440, 75],
[18, 169, 32, 75, 4886]
]
var genres = ["Psychologischverhaal", "Thriller", "Detective", "Romantischverhaal", "Sciencefiction"]
var svg = d3.select("svg"),
width = +svg.attr("width"),
height = +svg.attr("height"),
outerRadius = Math.min(width, height) * 0.5 - 40,
innerRadius = outerRadius - 30;
// Zet getallen naar 1K ipv 1000
var formatValue = d3.formatPrefix(",.0", 1e3);
var chord = d3.chord()
.padAngle(0.05)
.sortSubgroups(d3.ascending);
var arc = d3.arc()
.innerRadius(innerRadius)
.outerRadius(outerRadius);
var ribbon = d3.ribbon()
.radius(innerRadius);
var color = d3.scaleOrdinal()
.range(["#ed0b0b", "#03aa24", "#f2ae04", "#1f03f1", "#e1ed04"]);
var g = svg.append("g")
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")")
.datum(chord(data));
var group = g.append("g")
.attr("class", "groups")
.selectAll("g")
.data(function(chords) { return chords.groups; })
.enter().append("g");
group.append("path")
.style("fill", function(d) { return color(d.index); })
.style("stroke", function(d) { return d3.rgb(color(d.index)).darker(); })
.attr("id", function(d, i) { return "group" + d.index; })
.attr("d", arc)
.on("mouseover", fade(.1))
.on("mouseout", fade(1));
group.append("title").text(function(d) {
return groupTip(d);
});
group.append("text")
.attr("x", 6)
.attr("dy", 15)
.append("textPath")
.attr("xlink:href", function(d) { return "#group" + d.index; })
.text(function(chords, i){return genres[i];})
.style("fill", "black");
var groupTick = group.selectAll(".group-tick")
.data(function(d) { return groupTicks(d, 1e3); })
.enter().append("g")
.attr("class", "group-tick")
.attr("transform", function(d) { return "rotate(" + (d.angle * 180 / Math.PI - 90) + ") translate(" + outerRadius + ",0)"; });
groupTick.append("line")
.attr("x1", 1)
.attr("y1", 0)
.attr("x2", 5)
.attr("y2", 0)
.style("stroke", "#000")
groupTick
.filter(function(d) { return d.value % 1e3 === 0; })
.append("text")
.attr("x", 8)
.attr("dy", ".35em")
.attr("transform", function(d) { return d.angle > Math.PI ? "rotate(180) translate(-16)" : null; })
.style("text-anchor", function(d) { return d.angle > Math.PI ? "end" : null; })
.text(function(d) { return formatValue(d.value); });
var ribbons = g.append("g")
.attr("class", "ribbons")
.selectAll("path")
.data(function(chords) { return chords; })
.enter().append("path")
.attr("d", ribbon)
.style("fill", function(d) { return color(d.target.index); })
.style("stroke", function(d) { return d3.rgb(color(d.target.index)).darker(); })
ribbons.append("title").
text(function(d){return chordTip(d);});
// Returns an array of tick angles and values for a given group and step.
function groupTicks(d, step) {
var k = (d.endAngle - d.startAngle) / d.value;
return d3.range(0, d.value, step).map(function(value) {
return {value: value, angle: value * k + d.startAngle};
});
}
function fade(opacity) {
return function(d, i) {
ribbons
.filter(function(d) {
return d.source.index != i && d.target.index != i;
})
.transition()
.style("opacity", opacity);
};
}
function chordTip(d){
var j = d3.formatPrefix(",.0", 1e1)
return "Aantal boeken met genres:\n"
+ genres[d.target.index] + " en " + genres[d.source.index] + ": " + j(d.source.value)
}
function groupTip(d) {
var j = d3.formatPrefix(",.0", 1e1)
return "Totaal aantal boeken met het genre " + genres[d.index] + ":\n" + j(d.value)
}
and
<body>
<h1>Boeken met enkele & dubbele genres</h1>
<button id="doubleGenre">Alleen dubbele genres</button>
<button id="reset">Reset</button>
<svg width="960" height="900"></svg>
<script src="https://d3js.org/d3.v5.min.js"></script>
<script src="assets/index.js"></script>
This is the result what I have now :
I want to update the chord when a user clicks on the button Alleen dubbele genres so it becomes like this :
So I want to delete the chords to it self so the data looks like :
var data = [
[1196, 94, 93, 18],
[1196, 11, 343, 169],
[94, 11, 138, 32],
[93, 343, 138, 75],
[18, 169, 32, 75]
]
And if a user clicks on the button reset I want to go back to the original view. Is there anyone who can help me with this?
Here is a link to the simplified version of your example with transitions: https://stackblitz.com/edit/q53412789
If you want to remove chords to self, you should replace values on diagonal to zeros (not just delete them: matrix must remain square):
const data2 = [
[0, 1196, 94, 93, 18],
[1196, 0, 11, 343, 169],
[94, 11, 0, 138, 32],
[93, 343, 138, 0, 75],
[18, 169, 32, 75, 0]
]
As #rioV8 suggested, the first step is to wrap code for drawing and updating the chart into a function (I've ommited code for ticks and labels for brevity, the principle is the same for them):
var g = svg.append("g")
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")")
var ribbons = g.append("g");
function update(data) {
const chords = chord(data);
const ribbonsUpdate = ribbons.selectAll("path")
.data(chords, ({source, target}) => source.index + '-' + target.index)
const duration = 3000;
ribbonsUpdate
.transition()
.duration(duration)
.attr("d", ribbon)
.style("fill", function(d) { return color(d.target.index); })
.style("stroke", function(d) { return d3.rgb(color(d.target.index)).darker(); })
ribbonsUpdate
.enter()
.append("path")
.attr("opacity", 0)
.attr("d", ribbon)
.style("fill", function(d) { return color(d.target.index); })
.style("stroke", function(d) { return d3.rgb(color(d.target.index)).darker(); })
.transition()
.duration(duration)
.attr('opacity', 1)
ribbonsUpdate
.exit()
.transition()
.duration(duration)
.attr("opacity", 0)
.remove();
}
update(data1);
I've replaced combination of .datum(val) and .data(fun) in your code to single .data(vals). It is not required, but my opinion is that the latter is more common in d3 community.
The second argument to .data is the key function. It ensures, that during transition each path maps to itself in the new data (the number of chords is not equal: we removed chords to itself).
If you do not need animated transitions, the code is even simplier:
function simpleUpdate(data) {
const chords = chord(data);
const ribbonsUpdate = ribbons.selectAll("path")
.data(chords, ({source, target}) => source.index + '-' + target.index)
ribbonsUpdate
.enter().append("path")
.merge(ribbonsUpdate)
.attr("d", ribbon)
.style("fill", function(d) { return color(d.target.index); })
.style("stroke", function(d) { return d3.rgb(color(d.target.index)).darker(); })
ribbonsUpdate.exit().remove();
}

D3.js word cloud does not display without error message

I'm trying to make word cloud with d3.js, and when I try to call page the word cloud does not displayed.
However, there is no error message in console so I can't figure out which part is wrong.
The data set looks like this.
[{word: "happy", freq: 3}, {word: "apple", freq: 4}]
This is my code.
<div id="cloud"></div>
<style>
text:hover {
stroke: black;
}
</style>
<script src="http://d3js.org/d3.v3.min.js" charset="utf-8"></script>
<script src="https://rawgit.com/emeeks/3361332/raw/61cf57523fe8cf314333e5f60cc266351fec2017/d3.layout.cloud.js"></script>
<script type="text/javascript">
var weight = 3,
width = 960,
height = 500;
var fill = d3.scale.category20();
var data = {{ data|js }};
var result = scale(data);
function scale (data) {
var result = [];
for (var k in data){
var value = data[k];
result.push({word:value['word'], weight:+value['freq'] * weight});
}
return result;
}
d3.layout.cloud().size([width, height]).words(result)
.rotate(0)
.font("Impact")
.fontSize(function(data) { return data.size; })
.on("end", draw)
.start();
function draw(words) {
d3.select("#cloud").append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + width/2 + "," + height/2 + ")")
.selectAll("text")
.data(words)
.enter().append("text")
.style("font-size", function(data) { return data.size + "px"; })
.style("font-family", "Impact")
.style("fill", function(data, i) { return fill(i); })
.attr("text-anchor", "middle")
.attr("transform", function(data) {
return "translate(" + [data.x, data.y] + ")rotate(" + data.rotate + ")";
})
.text(function(data) { return data.text; });
}
I noticed two issues in your code.
1) The freq property is missing in the data set which is currently used to calculate the weight of each node. Since font size depends on the weight attribute, it becomes 0.
2) The result array contains objects having key name as word. So either you should override the text method of the cloud layout as shown below OR update the key name to text.
d3.layout.cloud()
..............
..............
.text(function(d) {
return d.word;
})
var weight = 3,
width = 960,
height = 500;
var fill = d3.scale.category20();
var data = [{
word: "happy",
weight: 10,
"freq": 8
}, {
word: "apple",
weight: 4,
"freq": 3
}, {
word: "wishes",
weight: 6,
"freq": 5
}, {
word: "sad",
weight: 5,
"freq": 2
}, {
word: "orange",
weight: 21,
"freq": 3
}, {
word: "condolence",
weight: 3,
"freq": 2
}];
var result = scale(data);
function scale(data) {
var result = [];
for (var k in data) {
var value = data[k];
result.push({
word: value['word'],
weight: +value['freq'] * weight
});
}
return result;
}
//console.log(result);
d3.layout.cloud().size([width, height]).words(result)
.rotate(0)
.font("Impact")
.text(function(d) {
return d.word;
})
.fontSize(function(data) {
return data.weight;
})
.on("end", draw)
.start();
function draw(words) {
d3.select("#cloud").append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")")
.selectAll("text")
.data(words)
.enter().append("text")
.style("font-size", function(data) {
return data.size + "px";
})
.style("font-family", "Impact")
.style("fill", function(data, i) {
return fill(i);
})
.attr("text-anchor", "middle")
.attr("transform", function(data) {
return "translate(" + [data.x, data.y] + ")rotate(" + data.rotate + ")";
})
.text(function(data) {
return data.text;
});
}
text:hover {
stroke: black;
}
<div id="cloud"></div>
<script src="https://d3js.org/d3.v3.min.js" charset="utf-8"></script>
<script src="https://rawgit.com/emeeks/3361332/raw/61cf57523fe8cf314333e5f60cc266351fec2017/d3.layout.cloud.js"></script>

line break in d3 tag Cloud

I am using it to generate a heavy amount of words in my application and I grab data from a json file like below:
var a = [];
for (var i=0; i < a.length; i++){
a.push(a[i].word);
}
And this gives us an array.
a = ["gsad","sagsa","gsag","sagas","gsag","gsagas","yhff","gag"];
I have it display on the screen correctly but since the row is too long it got out from the border and I'd like to give it a link break instead of change the SVG size, How may i do this?
UPDATE:
The code below is how i insert my codes:
var PositiveArr = ["gsad","sagsa","gsag","sagas","gsag","gsagas","yhff","gag"]; //consider the NegativeArr,NeutralArr have the similar contents
var fill = d3.scale.category20();
d3.layout.cloud().size([600, 300])
.words([NegativeArr,NeutralArr,PositiveArr].map(function(d) {
return {text: d, size: 10 + Math.random() * 50};
}))
.rotate(function() { return ~~(Math.random() * 2) * 90; })
.font("Impact")
.fontSize(function(d) { return d.size; })
.on("end", draw)
.start();
function draw(words) {
d3.select("#pre-theme").append("svg")
.attr("width", 600)
.attr("height", 300)
.append("g")
.attr("transform", "translate(300,150)")
.selectAll("text")
.data(words)
.enter().append("text")
.style("font-size", function(d) { return d.size + "px"; })
.style("font-family", "Impact, Arial")
.style("fill", function(d, i) { return fill(i); })
.attr("text-anchor", "middle")
.attr("transform", function(d) {
return "translate(" + [d.x, d.y] + ")rotate(" + d.rotate + ")";
})
.text(function(d) { return d.text; });
}
SOLUTION:
I have found myself a solution, i just merge all my arrays into one using
var allResult = PersonsArr.concat(PlacesArr,PatternsArr,ProductsArr,CompaniesArr);
and insert the to the .map like
.words(entityResult.map(function(d) {
return {text: d, size: 10 + Math.random() * 50};
}))
SOLUTION: I have found myself a solution, i just merge all my arrays into one using
var allResult = PersonsArr.concat(PlacesArr,PatternsArr,ProductsArr,CompaniesArr);
and insert the to the .map like
.words(entityResult.map(function(d) {
return {text: d, size: 10 + Math.random() * 50};
}))

Prevent loop in D3js Sankey chart

I'm trying to create a sankey chart using the d3 sankey plugin with dynamic shipping data. It works great most of the time except when I get a data set like this:
[{"DeparturePort":"CHARLESTON","ArrivalPort":"BREMERHAVEN","volume":5625.74},{"DeparturePort":"CHARLESTON","ArrivalPort":"ITAPOA","volume":2340},{"DeparturePort":"PT EVERGLADES","ArrivalPort":"PT AU PRINCE","volume":41.02},{"DeparturePort":"BREMERHAVEN","ArrivalPort":"CHARLESTON","volume":28}]
The key to my issue is the first and last entry in the data set. It seems that having opposite directions in the same sankey chart sends the javascript into an infinite loop and kills the browser. Any ideas on how to prevent this from happening?
Here's my chart code where raw would be the object above:
var data = raw;
var units = "Volume";
var margin = { top: 100, right: 0, bottom: 30, left: 0 },
width = $("#"+divID).width() - margin.left - margin.right,
height = divID == "enlargeChart" ? 800 - margin.top - margin.bottom : 600 - margin.top - margin.bottom;
var formatNumber = d3.format(",.0f"), // zero decimal places
format = function (d) { return ""; },
color = d3.scale.ordinal()
.range(["#0077c0", "#FF6600"]);
// append the svg canvas to the page
var svg = d3.select("#"+divID).append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.style("font-size", "12px")
.append("g")
.attr("transform",
"translate(" + margin.left + "," + margin.top + ")");
// Set the sankey diagram properties
var sankey = d3.sankey(width)
.nodeWidth(10)
.nodePadding(10)
.size([width, height]);
var path = sankey.link();
// load the data (using the timelyportfolio csv method)
//d3.csv("sankey.csv", function (error, data) {
//set up graph in same style as original example but empty
graph = { "nodes": [], "links": [] };
var checklist = [];
data.forEach(function (d) {
if ($.inArray(d.DeparturePort, checklist) == -1) {
checklist.push(d.DeparturePort)
graph.nodes.push({ "name": d.DeparturePort });
}
if ($.inArray(d.ArrivalPort, checklist) == -1) {
checklist.push(d.ArrivalPort)
graph.nodes.push({ "name": d.ArrivalPort });
}
graph.links.push({
"source": d.DeparturePort,
"target": d.ArrivalPort,
"value": +d.volume
});
});
// return only the distinct / unique nodes
graph.nodes = d3.keys(d3.nest()
.key(function (d) { return d.name; })
.map(graph.nodes));
// 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);
});
//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 };
});
sankey
.nodes(graph.nodes)
.links(graph.links)
.layout(32);
// add in the links
var link = svg.append("g").selectAll(".link")
.data(graph.links)
.enter().append("path")
.attr("class", "link")
.attr("d", path)
.style("stroke-width", function (d) { return Math.max(1, d.dy); })
.sort(function (a, b) { setTimeout(function () { return b.dy - a.dy; }, 10);});
// add the link titles
link.append("title")
.text(function (d) {
return d.source.name + " → " +
d.target.name + "\n" + d.value.toFixed(0) + " TEU";
});
$("#" + divID + " .loading").addClass("hide");
// add in the nodes
var node = svg.append("g").selectAll(".node")
.data(graph.nodes)
.enter().append("g")
.attr("class", "node")
.attr("transform", function (d) {
//setTimeout(function () {
return "translate(" + d.x + "," + d.y + ")";
//}, 10);
})
.call(d3.behavior.drag()
.origin(function (d) { return d; })
.on("dragstart", function () {
this.parentNode.appendChild(this);
})
.on("drag", dragmove));
// add the rectangles for the nodes
node.append("rect")
.attr("height", function (d) { if (d.dy < 0) { d.dy = (d.dy * -1); } return d.dy; })
.attr("width", sankey.nodeWidth())
.style("fill", function (d) {
return d.color = color(d.name);
})
.style("stroke", function (d) {
return d3.rgb(d.color);
})
.append("title")
.text(function (d) {
return d.name + "\n" + format(d.value);
});
// add in the title for the nodes
node.append("text")
.attr("x", -6)
.attr("y", function (d) { return d.dy / 2; })
.attr("dy", ".35em")
.attr("text-anchor", "end")
.style("stroke", function (d) { return "#000000" })
.attr("transform", null)
.text(function (d) { return d.name; })
.filter(function (d) { return d.x < width / 2; })
.attr("x", 6 + sankey.nodeWidth())
.attr("text-anchor", "start");
// the function for moving the nodes
function dragmove(d) {
d3.select(this).attr("transform",
"translate(" + d.x + "," + (
d.y = Math.max(0, Math.min(height - d.dy, d3.event.y))
) + ")");
sankey.relayout();
link.attr("d", path);
}
}, 0)
It's an issue with the sankey.js script.
See this commit (on a fork of sankey.js) which fixed it:
https://github.com/soxofaan/d3-plugin-captain-sankey/commit/0edba18918aac3e9afadffd4a169c47f88a98f81
while (remainingNodes.length) {
becomes:
while (remainingNodes.length && x < nodes.length) {
That should prevent the endless loop.

How to add a list of node's name with tooltip in sankey

I have made a sankey diagram with rChars package in R.
But I want to add a function, when we move to a link or a target node, it will show all the names of source node in a tooltip (or a new box at the right top of the graph).
For example, it will show "ddd.fr, pramana.fr" when we move to the node "target1".
I'm new in d3.js and know little about svg attribute. I have tried to do something with "link.append("title").text" or "node.append("title").text". But what I have done seem to be no use, because the function(d) always return one data but not an array.
Here is my code, hope someone can help, thanks !
<!doctype HTML>
<meta charset = 'utf-8'>
<html>
<head>
<link rel='stylesheet' href='http://timelyportfolio.github.io/rCharts_d3_sankey/css/sankey.css'>
<script src='http://timelyportfolio.github.io/rCharts_d3_sankey/js/d3.v3.js' type='text/javascript'></script>
<script src='http://timelyportfolio.github.io/rCharts_d3_sankey/js/sankey.js' type='text/javascript'></script>
<style>
.rChart {
display: block;
margin-left: auto;
margin-right: auto;
width: 900px;
height: 1000px;
}
</style>
</head>
<body >
<div id = 'chart202c4a213951' class = 'rChart rCharts_d3_sankey'></div>
<!--Attribution:
Mike Bostock https://github.com/d3/d3-plugins/tree/master/sankey
Mike Bostock http://bost.ocks.org/mike/sankey/
-->
<script>
(function(){
var params = {
"dom": "chart202c4a213951",
"width": 800,
"height": 300,
"data": {
"source": [ "A.fr", "B.fr", "C.fr", "ddd.fr", "pramana.fr", "pramana.fr" ],
"target": [ "pramana.fr", "pramana.fr", "ddd.fr", "target1", "target1", "target2" ],
"cat": [ 0, 1, 0, 1, 0, -1 ] ,
"value": [ 1, 1, 1, 1, 1, 1]
},
"nodeWidth": 15,
"nodePadding": 10,
"layout": 32,
"units": "freq",
"title": "Sankey Diagram pramana",
"id": "chart202c4a213951"
};
params.units ? units = " " + params.units : units = "";
//hard code these now but eventually make available
var formatNumber = d3.format("0,.0f"), // zero decimal places
format = function(d) { return formatNumber(d) + units; },
color = d3.scale.category20();
if(params.labelFormat){
formatNumber = d3.format(".2%");
}
var svg = d3.select('#' + params.id).append("svg")
.attr("width", params.width)
.attr("height", params.height);
var sankey = d3.sankey()
.nodeWidth(params.nodeWidth)
.nodePadding(params.nodePadding)
.layout(params.layout)
.size([params.width,params.height]);
var path = sankey.link();
var data = params.data,
links = [],
nodes = [];
//get all source and target into nodes
//will reduce to unique in the next step
//also get links in object form
data.source.forEach(function (d, i) {
nodes.push({ "name": data.source[i] });
nodes.push({ "name": data.target[i] });
links.push({ "source": data.source[i], "target": data.target[i], "value": +data.value[i] });
});
//now get nodes based on links data
//thanks Mike Bostock https://groups.google.com/d/msg/d3-js/pl297cFtIQk/Eso4q_eBu1IJ
//this handy little function returns only the distinct / unique nodes
nodes = d3.keys(d3.nest()
.key(function (d) { return d.name; })
.map(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
links.forEach(function (d, i) {
links[i].source = nodes.indexOf(links[i].source);
links[i].target = nodes.indexOf(links[i].target);
});
//now loop through each nodes to make nodes an array of objects rather than an array of strings
nodes.forEach(function (d, i) {
nodes[i] = { "name": d };
});
sankey
.nodes(nodes)
.links(links)
.layout(params.layout);
var link = svg.append("g").selectAll(".link")
.data(links)
.enter().append("path")
.attr("class", "link")
.attr("d", path)
.style("stroke-width", function (d) { return Math.max(1, d.dy); })
.sort(function (a, b) { return b.dy - a.dy; });
link.append("title")
.text(function (d) { return d.source.name + " → " + d.target.name + "\n" + format(d.value); });
var node = svg.append("g").selectAll(".node")
.data(nodes)
.enter().append("g")
.attr("class", "node")
.attr("transform", function (d) { return "translate(" + d.x + "," + d.y + ")"; })
.call(d3.behavior.drag()
.origin(function (d) { return d; })
.on("dragstart", function () { this.parentNode.appendChild(this); })
.on("drag", dragmove));
node.append("rect")
.attr("height", function (d) { return d.dy; })
.attr("width", sankey.nodeWidth())
.style("fill", function (d) { return d.color = color(d.name.replace(/ .*/, "")); })
.style("stroke", function (d) { return d3.rgb(d.color).darker(2); })
.append("title")
.text(function (d) { return d.name + "\n" + format(d.value); });
node.append("text")
.attr("x", -6)
.attr("y", function (d) { return d.dy / 2; })
.attr("dy", ".35em")
.attr("text-anchor", "end")
.attr("transform", null)
.text(function (d) { return d.name; })
.filter(function (d) { return d.x < params.width / 2; })
.attr("x", 6 + sankey.nodeWidth())
.attr("text-anchor", "start");
// the function for moving the nodes
function dragmove(d) {
d3.select(this).attr("transform",
"translate(" + (
d.x = Math.max(0, Math.min(params.width - d.dx, d3.event.x))
) + "," + (
d.y = Math.max(0, Math.min(params.height - d.dy, d3.event.y))
) + ")");
sankey.relayout();
link.attr("d", path);
}
})();
</script>
</body>
</html>
In your function that is supposed to retrieve names of other nodes that are "source" (or"target", implementation would be the same) to the current node, you can do something like this:
function(d,i){
d.sourceLinks.forEach(function(srcLnk){
// find the name of the other end of the link
});
d.targetLinks.forEach(function(tgtLnk){
// find the name of the other end of the link
});
}
In other words, use d.sourceLinks and d.targetLinks. And you gradually add names one by one, and display wherever you find suitable (tooltip, separate box, etc.).
In turn, each link has property source and target, and you can use something like srcLnk.source.name to obtain the name of one of the nodes that is a "source" for the current node.
I am writing this by hearth, so double check everything, some properties may have differeent name than I said.
Hope this helps.
UPDATE: jsfiddle
Key code:
.append("title")
.text(function (d) {
var titleText = d.name + " - " +
format(d.value) + " total" + "\n" + "\n";
var sourcesText = "";
d.targetLinks.forEach(function(dstLnk){
sourcesText += "from " + dstLnk.source.name + " - " +
format(dstLnk.value) + "\n";
});
return titleText + sourcesText;
});

Categories