D3.js word cloud does not display without error message - javascript

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>

Related

Filtering data based on input D3

I'm trying to filter down data to one value based on input. If the user enters a value, I want it to be able to display only that value on the graph. However, it's not working. I believe the code is in all the right places, just the logic isn't there.
If I remove the functionallity that attempts this, everything is fine - meaning that if I click on A, only data with type A is returned, and the same with B. But this doesn't work when I implement my code that tries to fitler down to one value.
Question:
How can I filter down to one value along the x axis based on user input, but still retain the funcationality that allows the user to switch between data types.
$(document).ready(function() {
$("#input").attr("value", 0);
});
const margin = {
top: 10,
right: 30,
bottom: 70,
left: 65
},
width = 520,
height = 480;
function SVGmaker(target) {
const svg = d3.selectAll(target)
.append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")")
svg.append("rect")
.attr("x", 0)
.attr("y", 0)
.attr("height", height)
.attr("width", width)
.attr("fill", "white")
return svg;
}
const svgOne = SVGmaker("#svg");
const data = [{
x: "1",
y: "5",
type: "A"
},
{
x: "2",
y: "4",
type: "B"
},
{
x: "3",
y: "3",
type: "B"
},
{
x: "4",
y: "2",
type: "A"
},
{
x: "5",
y: "1",
type: "A"
}
]
let x = d3.scaleLinear()
.domain([1, 5])
.range([0, width]);
// DC SVG Settings
const xAxis = d3.axisBottom(x);
svgOne.append("g")
.attr("transform", "translate(0," + height + ")")
.call(xAxis)
const yAxis = d3.scaleLinear()
.domain([0, 6])
.range([height, 0])
svgOne.append("g")
.call(d3.axisLeft(yAxis));
const colors = d3.scaleOrdinal()
.domain(["A", "B"])
.range(["red", "blue"]);
function updateData(data, type, theSVG, xAxis, yAxis, colors, value) {
// Filters through the data
const changeData = data.filter(function(d) {
if (type === "A") {
return d.type === "A"
} else if (type === "B") {
return d.type === "B"
} else {
return true;
}
});
var min = d3.min(data, function(d) {
return d.x;
});
const gra = theSVG
.selectAll("circle")
.data(changeData);
gra.enter()
.append("circle")
.filter(function(d) {
if (value <= min) {
return d.x
} else {
return d.x >= value && d.x <= value
}
})
.attr("cx", function(d) {
return xAxis(d.x);
})
.attr("cy", function(d) {
return yAxis(d.y);
})
.style("fill", function(d) {
return colors(d.type)
})
.merge(gra)
.attr("r", 11)
.filter(function(d) {
if (value <= min) {
return d.x
} else {
return d.x >= value && d.x <= value
}
})
.attr("cx", function(d) {
return xAxis(d.x);
})
.attr("cy", function(d) {
return yAxis(d.y);
})
.style("fill", function(d) {
return colors(d.type)
})
gra.exit()
.remove();
}
updateData(data, "", svgOne, x, yAxis, colors, 0);
let value;
function getMessage() {
value = document.getElementById("#input").value;
}
d3.select("#a")
.on("click", function() {
updateData(data, "A", svgOne, x, yAxis, colors, value);
});
d3.select("#b")
.on("click", function() {
updateData(data, "B", svgOne, x, yAxis, colors, value);
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<!DOCTYPE html>
<div id="svg"></div>
<button id="a">a</button>
<button id="b">b</button>
<input type="number" id="input" value=0>
<button id="submit">submit</button>
<script type="text/javascript" src="scripts.js"></script>
I was a bit guessing what you want to do, but really the problem was that you never set'value' variable, and thus you were always passing undefined.
When you call getElementById, do not include the #.
Generally if you're not generating the tag/element with d3 or js, I'd personally attach the click listener with onclick attribute straight inside html instead of d3.on.
$(document).ready(function() {
$("#input").attr("value", 0);
});
const margin = {
top: 10,
right: 30,
bottom: 70,
left: 65
},
width = 520,
height = 480;
function SVGmaker(target) {
const svg = d3.selectAll(target)
.append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")")
svg.append("rect")
.attr("x", 0)
.attr("y", 0)
.attr("height", height)
.attr("width", width)
.attr("fill", "white")
return svg;
}
const svgOne = SVGmaker("#svg");
const data = [{
x: "1",
y: "5",
type: "A"
},
{
x: "2",
y: "4",
type: "B"
},
{
x: "3",
y: "3",
type: "B"
},
{
x: "4",
y: "2",
type: "A"
},
{
x: "5",
y: "1",
type: "A"
}
]
let x = d3.scaleLinear()
.domain([1, 5])
.range([0, width]);
// DC SVG Settings
const xAxis = d3.axisBottom(x);
svgOne.append("g")
.attr("transform", "translate(0," + height + ")")
.call(xAxis)
const yAxis = d3.scaleLinear()
.domain([0, 6])
.range([height, 0])
svgOne.append("g")
.call(d3.axisLeft(yAxis));
const colors = d3.scaleOrdinal()
.domain(["A", "B"])
.range(["red", "blue"]);
function updateData(data, type, theSVG, xAxis, yAxis, colors, value) {
// Filters through the data
var min = d3.min(data, function(d) {
return d.x;
});
const changeData = data
.filter(function(d) {
if (value >= min) {
return d.x == value;
} else {
return true;
}
})
.filter(function(d) {
if (type === "A") {
return d.type === "A"
} else if (type === "B") {
return d.type === "B"
} else {
return true;
}
});
const gra = theSVG
.selectAll("circle")
.data(changeData);
gra.enter()
.append("circle")
.attr("r", 11)
.style("fill", function(d) {
return colors(d.type)
})
.merge(gra)
.attr("cx", function(d) {
return xAxis(d.x);
})
.attr("cy", function(d) {
return yAxis(d.y);
})
.style("fill", function(d) {
return colors(d.type)
})
gra.exit()
.remove();
}
updateData(data, "", svgOne, x, yAxis, colors, 0);
d3.select("#submit")
.on("click", function() {
var value = document.getElementById("input").value;
updateData(data, "", svgOne, x, yAxis, colors, value);
});
d3.select("#a")
.on("click", function() {
updateData(data, "A", svgOne, x, yAxis, colors, 0);
});
d3.select("#b")
.on("click", function() {
updateData(data, "B", svgOne, x, yAxis, colors, 0);
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<!DOCTYPE html>
<div id="svg"></div>
<button id="a">a</button>
<button id="b">b</button>
<input type="number" id="input" value=0>
<button id="submit">submit</button>
<script type="text/javascript" src="scripts.js"></script>

Hiding text on hover of pie chart

I found out that the data I'm trying to show with a chart gets a little messy with all the labels, so I thought I'd add a method that hides the <text> tags of all the pie slices except for the one you are hovering over. The <text> tags have classes corresponding to which child they are:
function visibilityShow(dataSetSize) {
for (var i = 0; i < dataSetSize; i++) {
$("#" + i).show();
}
}
function visibilityHide(index, dataSetSize) {
for (var i = 0; i < dataSetSize; i++) {
if (i === index) {
$("#" + i).show();
} else {
$("#" + i).hide();
}
}
}
Then pass it in on hover:
.on("mouseover.arcExpand", arcTween(outerRadius, 0))
.on("mouseover.textHide", function (d, i) {
visibilityHide(i, dataSet.length);
})
.on("mouseout.arcRetract", arcTween(outerRadius - 20, 150))
.on("mouseout.textShow", function (d, i) {
visibilityShow(dataSet.length);
});
Now it turns out that I'm passing in 0 for i because of the way I pass in data:
newSlices.select("path")
.each(function (d) { d.outerRadius = outerRadius - 20; })
//.transition()
.attr("d", arc)
.attr("fill", function (d, i) { return newColor(i); })
.attr("title", function (d) { return d["value"]; });
newSlices.selectAll("path")
.on("click", function (d) {
checkForChild(d["data"]["label"], d["data"]);
})
.on("mouseover.arcExpand", arcTween(outerRadius, 0))
.on("mouseover.textHide", function (d, i) {
visibilityHide(i, dataSet.length);
})
.on("mouseout.arcRetract", arcTween(outerRadius - 20, 150))
.on("mouseout.textShow", function (d, i) {
visibilityShow(dataSet.length);
});
If you run the code snippet you see that the issue that the code only ever hides the text tag with class "0." How do I set up the on mouseover event so that I can try to hide the correct tags?
// This data will be gathered from API calls eventually
dataDefault = [];
dataController = [{ "label": "Example 1", "value": 1, "child": [{ "label": "Child 1", "value": 1 }] },
{ "label": "Example 2", "value": 1, "child": [{ "label": "Child 1", "value": 1 }] },
{ "label": "Example 3", "value": 1, "child": [{ "label": "Child 1", "value": 1 }] },
{ "label": "Example 4", "value": 1, "child": [{ "label": "Child 1", "value": 1 }] },
{ "label": "Example 5", "value": 1, "child": [{ "label": "Child 1", "value": 1 }] }];
var displaySize = 20;
// This is used to keep track of what data is showing
var mode = "Default";
// The amount of pixels the SVG will take up
var width = 600,
height = 675;
// It's a donut, so it has an outer radius and an inner radius. 2r = width so r = width/2
var outerRadius = width / 2,
innerRadius = outerRadius / 3;
// Default color function for deciding the colros of the donut slices
var color = d3.scale.category10();
// The pie function for deciding the size of the donut slices
var pie = d3.layout.pie()
.value(function (d) { return d["value"]; });
// At first we use the default data to create the pie
var pieData = pie(dataDefault);
// Create an arc
var arc = d3.svg.arc()
.innerRadius(innerRadius);
// Add an SVG tag to the document
var svg = d3.select("#graphs").append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + outerRadius + "," + (outerRadius + 50) + ")");
// Append an link tag for each point of the data, then add an path tag inside each a tag
svg.selectAll("a")
.data(pieData)
.enter().append("a")
.append("path")
.each(function (d) { d.outerRadius = outerRadius - 20; })
.attr("d", arc)
.attr("fill", function (d, i) { return color(i); })
.on("mouseover", arcTween(outerRadius, 0, 0))
.on("mouseout", arcTween(outerRadius - 20, 150))
.append("title")
.text(function (d) { return d["value"] + " hits"; });
// Change the default data to the Apps data so it animates on load
changeToAPI("Controller", dataController);
// Function used to increase slice size on hover
function arcTween(outerRadius, delay) {
return function () {
d3.select(this).transition().delay(delay).attrTween("d", function (d) {
var i = d3.interpolate(d.outerRadius, outerRadius);
return function (t) { d.outerRadius = i(t); return arc(d); };
});
};
}
// Passes the color scale into the change function
function getColor(name) {
// Get the remainder when / 3
var bucket = hashify(name) % 4;
// Setup the array of color functions
var colors = [d3.scale.category10(), d3.scale.category20(), d3.scale.category20b(), d3.scale.category20c()];
// Return the correct bucket
return colors[bucket];
}
// Function used to swap the data being shown
function changeToAPI(name, dataSet) {
// Don't update if the data is already showing
// JavaScript doesn't short circuit?
if (dataSet === null) {
dataSet = [{ "label": "No data...", "value": 1 }];
changeTo(name, dataSet);
} else if (dataSet.length === 0) {
dataSet = [{ "label": "No data...", "value": 1 }];
changeTo(name, dataSet);
} else {
mode = name;
// Get the new pie and color functions
var newData = pie(dataSet);
var newColor = getColor(name);
// Remove the labels, titles, and tooltips
svg.selectAll("text").remove();
svg.selectAll("title").remove();
// Line below fixes an error that doesn't cause issues, but makes the graph ugly :(
svg.selectAll("a").remove();
// Add the new slices if there are any
var newSlices = svg.selectAll("a")
.data(newData);
newSlices.enter()
.append("a")
.append("path")
.style("cursor", "pointer");
// Update the attributes of those slices and animate the transition
newSlices.select("path")
.each(function (d) { d.outerRadius = outerRadius - 20; })
.transition()
.attr("d", arc)
.attr("fill", function (d, i) { return newColor(i); })
.attr("title", function (d) { return d["value"]; });
newSlices.selectAll("path")
.on("click", function (d) {
checkForChild(d["data"]["label"], d["data"]);
})
.on("mouseover.arcExpand", arcTween(outerRadius, 0))
.on("mouseover.textHide", function (d, i) {
visibilityHide(i, dataSet.length);
})
.on("mouseout.arcRetract", arcTween(outerRadius - 20, 150))
.on("mouseout.textShow", function (d, i) {
visibilityShow(dataSet.length);
});
// Remove excess slices
newSlices.exit().remove();
// Add a title
var title = svg.append("text")
.attr("x", 0)
.attr("y", -(outerRadius + 10))
.style("text-anchor", "middle")
.text("Distrubution of " + name + " Usage");
// Add labels
var labels = svg.selectAll(null)
.data(newData)
.enter()
.append("text")
.attr("fill", "white")
.attr("id", function (d, i) { return i })
.attr("transform", function (d) {
d.innerRadius = 0;
d.outerRadius = outerRadius;
return "translate(" + arc.centroid(d) + ")";
})
.attr("text-anchor", "middle")
.text(function (d, i) {
return dataSet[i]["label"];
});
// Add tooltips
svg.selectAll("path").data(newData).append("title").text(function (d) { return d["value"] + " hits"; });
svg.append("circle")
.attr("cx", 0)
.attr("cy", 0)
.attr("r", innerRadius)
.style("fill", "white")
.style("cursor", "pointer")
.on("click", function () {
changeToAPI("Controller", dataController);
});
// Adds back button if not at controller level
if (dataSet !== dataController) {
svg.append("text")
.attr("x", 0)
.attr("y", 12)
.style("text-anchor", "middle")
.style("color", "#efefef")
.style("font-size", "40px")
.text("Back");
}
}
}
function changeTo(name, dataSet) {
// Don't update if the data is already showing
// JavaScript doesn't short circuit?
if (dataSet === null) {
dataSet = [{ "label": "No data...", "value": 1 }];
} else if (dataSet.length === 0) {
dataSet = [{ "label": "No data...", "value": 1 }];
}
mode = name;
// Get the new pie and color functions
var newData = pie(dataSet);
var newColor = getColor(name);
// Remove the labels, titles, and tooltips
svg.selectAll("text").remove();
svg.selectAll("title").remove();
// Line below fixes an error that doesn't cause issues, but makes the graph ugly :(
//svg.selectAll("a").remove();
// Add the new slices if there are any
var newSlices = svg.selectAll("a")
.data(newData);
newSlices.enter()
.append("a")
.append("path")
.style("cursor", "pointer");
// Update the attributes of those slices and animate the transition
newSlices.select("path")
.each(function (d) { d.outerRadius = outerRadius - 20; })
.transition()
.attr("fill", function (d, i) { return newColor(i); })
.attr("d", arc)
.attr("title", function (d) { return d["value"]; });
newSlices.selectAll("path")
.on("mouseover.arc", arcTween(outerRadius, 0))
.on("mouseover.text", function (d, i) {
visibilityHide(i, dataSet.length);
})
.on("mouseout.arc", arcTween(outerRadius - 20, 150))
.on("mouseout.text", function (d, i) {
visibilityShow(dataSet.length);
});
// Remove excess slices
newSlices.exit().remove();
// Add a title
svg.append("text")
.attr("x", 0)
.attr("y", -(outerRadius + 10))
.style("text-anchor", "middle")
.text(function (e) {
var title = "Distrubution of " + name + " Usage";
if (name === "Defualt") {
title = "Loading..."
}
return title;
});
// Add labels
svg.selectAll(null)
.data(newData)
.enter()
.append("text")
.attr("fill", "white")
.attr("id", function (d, i) { return i })
.attr("transform", function (d) {
d.innerRadius = 0;
d.outerRadius = outerRadius;
return "translate(" + arc.centroid(d) + ")";
})
.attr("text-anchor", "middle")
.text(function (d, i) {
return dataSet[i]["label"];
});
// Add tooltips
svg.selectAll("path").data(newData).append("title").text(function (d) { return d["value"] + " hits"; });
}
function checkForChild(name, dataSet) {
if (dataSet.hasOwnProperty("child")) {
if (dataSet["child"] !== null) {
if (dataSet["child"].length !== 0) {
changeToAPI(name, dataSet["child"]);
}
}
}
}
// Hashcode generator for strings
function hashify(string) {
var hash = 0;
// Add the value of each char to the hash value
for (var i = 0; i < string.length; i++) {
hash += string.charCodeAt(i);
}
return hash;
}
function visibilityShow(dataSetSize) {
for (var i = 0; i < dataSetSize; i++) {
$("#" + i).show();
}
}
function visibilityHide(index, dataSetSize) {
for (var i = 0; i < dataSetSize; i++) {
if (i === index) {
$("#" + i).show();
} else {
$("#" + i).hide();
}
}
}
body {
font-family: Arial;
transition: all ease .5s;
text-align: center;
color: rgb(58,58,58);
}
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
<script src="https://d3js.org/d3.v3.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
<title>General Statistics</title>
</head>
<body>
<div id="graphs">
</div>
</body>
</html>
Thanks!
The reason is, as you stated, that i is giving you 0 whenever you hover the path. What you can do is simple: when you are iterating through your data using .each(), you can simply bind the index to a property, e.g.:
newSlices.select("path")
.each(function (d, i) {
d.index = i;
d.outerRadius = outerRadius - 20;
})
By doing that, whenever you hover over each <path> again, its index will be accessible via the d.index property:
newSlices.selectAll("path")
.on("mouseover.textHide", function (d) {
// Index was stored in the `index` key
visibilityHide(d.index, dataSet.length);
})
// This data will be gathered from API calls eventually
dataDefault = [];
dataController = [{ "label": "Example 1", "value": 1, "child": [{ "label": "Child 1", "value": 1 }] },
{ "label": "Example 2", "value": 1, "child": [{ "label": "Child 1", "value": 1 }] },
{ "label": "Example 3", "value": 1, "child": [{ "label": "Child 1", "value": 1 }] },
{ "label": "Example 4", "value": 1, "child": [{ "label": "Child 1", "value": 1 }] },
{ "label": "Example 5", "value": 1, "child": [{ "label": "Child 1", "value": 1 }] }];
var displaySize = 20;
// This is used to keep track of what data is showing
var mode = "Default";
// The amount of pixels the SVG will take up
var width = 600,
height = 675;
// It's a donut, so it has an outer radius and an inner radius. 2r = width so r = width/2
var outerRadius = width / 2,
innerRadius = outerRadius / 3;
// Default color function for deciding the colros of the donut slices
var color = d3.scale.category10();
// The pie function for deciding the size of the donut slices
var pie = d3.layout.pie()
.value(function (d) { return d["value"]; });
// At first we use the default data to create the pie
var pieData = pie(dataDefault);
// Create an arc
var arc = d3.svg.arc()
.innerRadius(innerRadius);
// Add an SVG tag to the document
var svg = d3.select("#graphs").append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + outerRadius + "," + (outerRadius + 50) + ")");
// Append an link tag for each point of the data, then add an path tag inside each a tag
svg.selectAll("a")
.data(pieData)
.enter().append("a")
.append("path")
.each(function (d) { d.outerRadius = outerRadius - 20; })
.attr("d", arc)
.attr("fill", function (d, i) { return color(i); })
.on("mouseover", arcTween(outerRadius, 0, 0))
.on("mouseout", arcTween(outerRadius - 20, 150))
.append("title")
.text(function (d) { return d["value"] + " hits"; });
// Change the default data to the Apps data so it animates on load
changeToAPI("Controller", dataController);
// Function used to increase slice size on hover
function arcTween(outerRadius, delay) {
return function () {
d3.select(this).transition().delay(delay).attrTween("d", function (d) {
var i = d3.interpolate(d.outerRadius, outerRadius);
return function (t) { d.outerRadius = i(t); return arc(d); };
});
};
}
// Passes the color scale into the change function
function getColor(name) {
// Get the remainder when / 3
var bucket = hashify(name) % 4;
// Setup the array of color functions
var colors = [d3.scale.category10(), d3.scale.category20(), d3.scale.category20b(), d3.scale.category20c()];
// Return the correct bucket
return colors[bucket];
}
// Function used to swap the data being shown
function changeToAPI(name, dataSet) {
// Don't update if the data is already showing
// JavaScript doesn't short circuit?
if (dataSet === null) {
dataSet = [{ "label": "No data...", "value": 1 }];
changeTo(name, dataSet);
} else if (dataSet.length === 0) {
dataSet = [{ "label": "No data...", "value": 1 }];
changeTo(name, dataSet);
} else {
mode = name;
// Get the new pie and color functions
var newData = pie(dataSet);
var newColor = getColor(name);
// Remove the labels, titles, and tooltips
svg.selectAll("text").remove();
svg.selectAll("title").remove();
// Line below fixes an error that doesn't cause issues, but makes the graph ugly :(
svg.selectAll("a").remove();
// Add the new slices if there are any
var newSlices = svg.selectAll("a")
.data(newData);
newSlices.enter()
.append("a")
.append("path")
.style("cursor", "pointer");
// Update the attributes of those slices and animate the transition
newSlices.select("path")
.each(function (d, i) {
d.index = i;
d.outerRadius = outerRadius - 20; })
.transition()
.attr("d", arc)
.attr("fill", function (d, i) { return newColor(i); })
.attr("title", function (d) { return d["value"]; });
newSlices.selectAll("path")
.on("click", function (d) {
checkForChild(d["data"]["label"], d["data"]);
})
.on("mouseover.arcExpand", arcTween(outerRadius, 0))
.on("mouseover.textHide", function (d) {
visibilityHide(d.index, dataSet.length);
})
.on("mouseout.arcRetract", arcTween(outerRadius - 20, 150))
.on("mouseout.textShow", function (d) {
visibilityShow(dataSet.length);
});
// Remove excess slices
newSlices.exit().remove();
// Add a title
var title = svg.append("text")
.attr("x", 0)
.attr("y", -(outerRadius + 10))
.style("text-anchor", "middle")
.text("Distrubution of " + name + " Usage");
// Add labels
var labels = svg.selectAll(null)
.data(newData)
.enter()
.append("text")
.attr("fill", "white")
.attr("id", function (d, i) { return i })
.attr("transform", function (d) {
d.innerRadius = 0;
d.outerRadius = outerRadius;
return "translate(" + arc.centroid(d) + ")";
})
.attr("text-anchor", "middle")
.text(function (d, i) {
return dataSet[i]["label"];
});
// Add tooltips
svg.selectAll("path").data(newData).append("title").text(function (d) { return d["value"] + " hits"; });
svg.append("circle")
.attr("cx", 0)
.attr("cy", 0)
.attr("r", innerRadius)
.style("fill", "white")
.style("cursor", "pointer")
.on("click", function () {
changeToAPI("Controller", dataController);
});
// Adds back button if not at controller level
if (dataSet !== dataController) {
svg.append("text")
.attr("x", 0)
.attr("y", 12)
.style("text-anchor", "middle")
.style("color", "#efefef")
.style("font-size", "40px")
.text("Back");
}
}
}
function changeTo(name, dataSet) {
// Don't update if the data is already showing
// JavaScript doesn't short circuit?
if (dataSet === null) {
dataSet = [{ "label": "No data...", "value": 1 }];
} else if (dataSet.length === 0) {
dataSet = [{ "label": "No data...", "value": 1 }];
}
mode = name;
// Get the new pie and color functions
var newData = pie(dataSet);
var newColor = getColor(name);
// Remove the labels, titles, and tooltips
svg.selectAll("text").remove();
svg.selectAll("title").remove();
// Line below fixes an error that doesn't cause issues, but makes the graph ugly :(
//svg.selectAll("a").remove();
// Add the new slices if there are any
var newSlices = svg.selectAll("a")
.data(newData);
newSlices.enter()
.append("a")
.append("path")
.style("cursor", "pointer");
// Update the attributes of those slices and animate the transition
newSlices.select("path")
.each(function (d) { d.outerRadius = outerRadius - 20; })
.transition()
.attr("fill", function (d, i) { return newColor(i); })
.attr("d", arc)
.attr("title", function (d) { return d["value"]; });
newSlices.selectAll("path")
.on("mouseover.arc", arcTween(outerRadius, 0))
.on("mouseover.text", function (d, i) {
visibilityHide(i, dataSet.length);
})
.on("mouseout.arc", arcTween(outerRadius - 20, 150))
.on("mouseout.text", function (d, i) {
visibilityShow(dataSet.length);
});
// Remove excess slices
newSlices.exit().remove();
// Add a title
svg.append("text")
.attr("x", 0)
.attr("y", -(outerRadius + 10))
.style("text-anchor", "middle")
.text(function (e) {
var title = "Distrubution of " + name + " Usage";
if (name === "Defualt") {
title = "Loading..."
}
return title;
});
// Add labels
svg.selectAll(null)
.data(newData)
.enter()
.append("text")
.attr("fill", "white")
.attr("id", function (d, i) { return i })
.attr("transform", function (d) {
d.innerRadius = 0;
d.outerRadius = outerRadius;
return "translate(" + arc.centroid(d) + ")";
})
.attr("text-anchor", "middle")
.text(function (d, i) {
return dataSet[i]["label"];
});
// Add tooltips
svg.selectAll("path").data(newData).append("title").text(function (d) { return d["value"] + " hits"; });
}
function checkForChild(name, dataSet) {
if (dataSet.hasOwnProperty("child")) {
if (dataSet["child"] !== null) {
if (dataSet["child"].length !== 0) {
changeToAPI(name, dataSet["child"]);
}
}
}
}
// Hashcode generator for strings
function hashify(string) {
var hash = 0;
// Add the value of each char to the hash value
for (var i = 0; i < string.length; i++) {
hash += string.charCodeAt(i);
}
return hash;
}
function visibilityShow(dataSetSize) {
for (var i = 0; i < dataSetSize; i++) {
$("#" + i).show();
}
}
function visibilityHide(index, dataSetSize) {
for (var i = 0; i < dataSetSize; i++) {
if (i === index) {
$("#" + i).show();
} else {
$("#" + i).hide();
}
}
}
body {
font-family: Arial;
transition: all ease .5s;
text-align: center;
color: rgb(58,58,58);
}
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
<script src="https://d3js.org/d3.v3.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
<title>General Statistics</title>
</head>
<body>
<div id="graphs">
</div>
</body>
</html>
This simple css should do it:
#text:hover {
color: transparent;
}

D3 - Multiple wordclouds from the same source file

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; });
}
});
});

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;
});

d3.js Maximum call stack size exceeded error

When I try to layout my force-directed graph, the following is the error I receive. I read about this issue in Mike Bostock's github page and found that it might be due to NaN values of coordinates or all the points drawn at the same point. I checked into the console and I found that all of my points are getting drawn at the same X and Y values.
On an example data, the code worked perfectly well but no more now. My data goes like 45-50 levels away from the center node. I have successfully made a tree layout with this data. Wanted to try the force directed layout but it did not work.
Any help regarding how to draw the nodes on separate coordinates would be much appreciated.
<!DOCTYPE html>
<meta charset="utf-8">
<style>
.node {
stroke: #fff;
stroke-width: 1.5px;
}
.link {
stroke: #999;
stroke-opacity: .6;
}
</style>
<body>
<script src="http://d3js.org/d3.v3.min.js"></script>
<script>
var margin = {top: 10, right: 10, bottom: 20, left: 40},
width = 1300 - margin.left - margin.right,
height = 800 - margin.top - margin.bottom;
var color = d3.scale.category20();
var force = d3.layout.force().charge(-120)
.linkDistance(30)
.size([width, height]);
var svg = d3.select("body").append("svg:svg")
.attr("width", width)
.attr("height", height)
//.attr("pointer-events","all")
.append('svg:g')
//.call(d3.behavior.zoom().translate([100,50]).scale(.5).on("zoom",redraw))
.append('svg:g')
.attr("transform","translate(100,50)scale(.5,.5)");
svg.append('svg:rect')
.attr('width', width)
.attr('height', height)
.attr('fill','white')
.attr('opacity',0);
function redraw() {
var trans = d3.event.translate;
var scale = d3.event.scale;
svg.attr("transform",
"translate(" + trans + ")"
+ " scale(" + scale + ")");
};
d3.json("test_data.json", function(error, graph) {
var nodeMap = {};
graph.nodes.forEach(function(x) { nodeMap[x.name] = x; });
graph.links = graph.links.map(
function(x)
{
return {
source: nodeMap[x.source],
target: nodeMap[x.target],
value: x.value
};
});
console.log(graph.nodes);
console.log(graph.links);
force
.nodes(graph.nodes)
.links(graph.links)
.start();
var link = svg.selectAll(".link")
.data(graph.links)
.enter().append("line")
.attr("class", "link")
.style("stroke", function(d) { return color(d.value); })
.style("stroke-width", function(d) {
//console.log(d.value);
return Math.sqrt(d.value); });
var node = svg.selectAll(".node")
.data(graph.nodes)
.enter().append("circle")
.attr("class", "node")
.attr("r", 5)
.style("fill", function(d) { return color(d.value); })
.call(force.drag)
.on("mousedown",
function(d) {
d.fixed = true;
d3.select(this).classed("sticky", true);
}
)
.on("mouseover",fade(0.1))
.on("mouseout",fade(1));
var linkedByIndex = {};
graph.links.forEach(function(d) {
linkedByIndex[d.source.index + "," + d.target.index] = 1;
});
function isConnected(a, b) {
return linkedByIndex[a.index + "," + b.index] || linkedByIndex[b.index + "," + a.index] || a.index == b.index;
}
node.append("title")
.text(function(d) {
return "Name : "+d.name+"\n"+"Parent: "+d.parent +"\n"+"Relationship: "+ d.relationship +"\n"+ "Creation Date: "+ d.cod +"\n"; });
function fade(opacity)
{
return function(d) {
node.style("stroke-opacity", function(o) {
thisOpacity = isConnected(d, o) ? 1 : opacity;
this.setAttribute('fill-opacity', thisOpacity);
return thisOpacity;
});
link.style("stroke-opacity", opacity).style("stroke-opacity", function(o) {
return o.source === d || o.target === d ? 1 : opacity;
});
};
}
force.on("tick", function() {
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("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; });
/*node.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; });*/
});
});
</script>
The JSON looks like:
{
"links": [
{
"source": "Me",
"target": "Adam",
"value": 10
},
{
"source": "Me",
"target": "You",
"value": 10
}
], "nodes": [
{
"ancestor": "Adam",
"cod": 19061964,
"distance": 0,
"name": "Adam",
"parent": null,
"relationship": null,
"value": 10
},
{
"ancestor": "Adam",
"cod": 13032003,
"distance": 1,
"name": "Me",
"parent": "You",
"relationship": "Father",
"value": 10
}
]
}
EDIT: I get the error in the following statement:
force
.nodes(graph.nodes)
.links(graph.links)
.start();
highlighting:
"start();"
In your data your links are to names as opposed to indices. Check out the example data from http://bl.ocks.org/mbostock/4062045.
Links have the form: {"source":1,"target":0,"value":1}, which points to the index of the node.

Categories