Hiding text on hover of pie chart - javascript

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

Related

d3 change bar chart color according to height

I created a bar chart in a way that I can expand or reduce the height of every bar with mouse drag and it changes the data automatically according to the dragged value. I want to change the color in a way that every bar gets more red when I expand its height and more green when I reduce its height.
How can I achieve it ?
<!DOCTYPE html>
<html>
<style>
.selection {
fill: steelblue;
fill-opacity: 1;
}
body {
width: 80%;
margin: auto;
}
</style>
<body >
<script src="https://d3js.org/d3.v4.min.js"></script>
<script>
var data = [
{ index: 0, value: 18 },
{ index: 1, value: 20 },
{ index: 2, value: 19 },
];
var widthY = 550,
heightY = 600,
delim = 4;
var scaleY = d3.scaleLinear().domain([0, 21]).rangeRound([heightY, 0]);
var x = d3.scaleLinear().domain([0, data.length]).rangeRound([0, widthY]);
var svgY = d3
.select("body")
.append("svg")
.attr("width", widthY)
.attr("height", heightY)
.attr("fill", "green")
.append("g");
var brushY = d3
.brushY()
.extent(function (d, i) {
return [
[x(i) + delim / 2, 0],
[x(i) + x(1) - delim / 2, heightY],
];
})
.on("brush", brushmoveY)
.on("end", brushendY);
var svgbrushY = svgY
.selectAll(".brush")
.data(data)
.enter()
.append("g")
.attr("class", "brush")
.append("g")
.call(brushY)
.call(brushY.move, function (d) {
return [d.value, 0].map(scaleY);
});
svgbrushY
.append("text")
.attr("y", function (d) {
return scaleY(d.value) + 25;
})
.attr("x", function (d, i) {
return x(i) + x(0.5);
})
.attr("dx", "-.60em")
.attr("dy", -5)
.style("fill", "white")
.text(function (d) {
return d3.format(".2")(d.value);
});
function brushendY() {
if (!d3.event.sourceEvent) return;
if (d3.event.sourceEvent.type === "brush") return;
if (!d3.event.selection) {
svgbrushY.call(brushY.move, function (d) {
return [d.value, 0].map(scaleY);
});
}
}
function brushmoveY() {
if (!d3.event.sourceEvent) return;
if (d3.event.sourceEvent.type === "brush") return;
if (!d3.event.selection) return;
var d0 = d3.event.selection.map(scaleY.invert);
var d = d3.select(this).select(".selection");
d.datum().value = d0[0];
update();
}
function update() {
svgbrushY
.call(brushY.move, function (d) {
return [d.value, 0].map(scaleY);
})
.selectAll("text")
.attr("y", function (d) {
return scaleY(d.value) + 25;
})
.text(function (d) {
return d3.format(".2")(d.value);
});
}
</script>
</body>
</html>
First we need a scale:
var color = d3.scaleLinear().domain(scaleY.domain()).range(["lightgreen","crimson"])
Now we need to update the color of the bars on drag in the update function:
svgbrushY.selectAll(".selection").style("fill", function(d) { return color(d.value); })
That gets us almost all the way there - but the colors are not right initially. The creation of the brush breaks the data binding for the .selection rectangle: if you log
svgbrushY.selectAll(".selection").each(function(d) { console.log(d); })
You'll notice that the datum doesn't have the value property before being dragged. So we can add it ourselves initially and style the bar the same as we do during the update function:
svgbrushY.select(".selection")
.datum(d=>d)
.style("fill", function(d) { return color(d.value); })
That should give us something like:
<!DOCTYPE html>
<html>
<style>
.selection {
fill-opacity: 1;
}
body {
width: 80%;
margin: auto;
}
</style>
<body >
<script src="https://d3js.org/d3.v4.min.js"></script>
<script>
var data = [
{ index: 0, value: 18 },
{ index: 1, value: 20 },
{ index: 2, value: 19 },
];
var widthY = 550,
heightY = 200,
delim = 4;
var scaleY = d3.scaleLinear().domain([0, 21]).rangeRound([heightY, 0]);
var x = d3.scaleLinear().domain([0, data.length]).rangeRound([0, widthY]);
var color = d3.scaleLinear().domain(scaleY.domain()).range(["lightgreen","crimson"])
var svgY = d3
.select("body")
.append("svg")
.attr("width", widthY)
.attr("height", heightY)
.attr("fill", "green")
.append("g");
var brushY = d3
.brushY()
.extent(function (d, i) {
return [
[x(i) + delim / 2, 0],
[x(i) + x(1) - delim / 2, heightY],
];
})
.on("brush", brushmoveY)
.on("end", brushendY);
var svgbrushY = svgY
.selectAll(".brush")
.data(data)
.enter()
.append("g")
.attr("class", "brush")
.append("g")
.call(brushY)
.call(brushY.move, function (d) {
return [d.value, 0].map(scaleY);
});
svgbrushY.select(".selection")
.datum(d=>d)
.style("fill", function(d) { return color(d.value); })
svgbrushY
.append("text")
.attr("y", function (d) {
return scaleY(d.value) + 25;
})
.attr("x", function (d, i) {
return x(i) + x(0.5);
})
.attr("dx", "-.60em")
.attr("dy", -5)
.style("fill", "white")
.text(function (d) {
return d3.format(".2")(d.value);
});
function brushendY() {
if (!d3.event.sourceEvent) return;
if (d3.event.sourceEvent.type === "brush") return;
if (!d3.event.selection) {
svgbrushY.call(brushY.move, function (d) {
return [d.value, 0].map(scaleY);
});
}
}
function brushmoveY() {
if (!d3.event.sourceEvent) return;
if (d3.event.sourceEvent.type === "brush") return;
if (!d3.event.selection) return;
var d0 = d3.event.selection.map(scaleY.invert);
var d = d3.select(this).select(".selection");
d.datum().value = d0[0];
update();
}
function update() {
svgbrushY
.call(brushY.move, function (d) {
return [d.value, 0].map(scaleY);
})
.selectAll("text")
.attr("y", function (d) {
return scaleY(d.value) + 25;
})
.text(function (d) {
return d3.format(".2")(d.value);
});
svgbrushY.selectAll(".selection").style("fill", function(d) { return color(d.value); })
}
</script>
</body>
</html>
(scaled down for snippet view)

D3 V4: Updated data is being seen as new data? (Update function)

Currently, I am building a system, and I am having some trouble with the update function.
Essentially, I am trying to add new nodes to a D3 tree. A new child node can be added when the user clicks the "add button" of a node. Each add button can be found on the left side of each node.
I have followed Mike Bostock's general update pattern. Once I click on the button, the only "new" data element should be the newly created child node, but it looks like the entire data is being treat as "new". I came to this conclusion when I looked at the class names for each node and the obvious fact that there is a transition of all the nodes coming to the central node and disappearing. The other original data should be "updated", but it's not. Can anyone please gently point out as to why this is happening?
A working sample of my code can be found in this jfiddle link.
EDIT 06/09
Given Gordon's suggestion, I have found a unique field for both of my nodes and link. So to uniquely identify the data I have made the following change:
NODE
.data(d, d => d.data.name)
LINK
.data(d, d => d.source.data.name)
This change works (mostly), but I see that some weird behaviors are still occurring: (1) Branch 7.2.1 is still being recognized a new node and disappear; (2) links are not being properly aligned with their respective node after the second "add" or so. I think my two small edits are affecting this because when I went back to the original code, the lines are being properly drawn, despite the fact they're transitioning away. Thoughts? Advice?
HTML
<div id="div-mindMap">
CSS
.linkMindMap {
fill: none;
stroke: #555;
stroke-opacity: 0.4;
}
rect {
fill: white;
stroke: #3182bd;
stroke-width: 1.5px;
}
JS
const widthMindMap = 700;
const heightMindMap = 700;
let parsedData;
let parsedList = {
"name": " Stapler",
"children": [{
"name": " Bind",
"children": []
},
{
"name": " Nail",
"children": []
},
{
"name": " String",
"children": []
},
{
"name": " Glue",
"children": [{
"name": "Gum",
"children": []
},
{
"name": "Sticky Gum",
"children": []
}
]
},
{
"name": " Branch 3",
"children": []
},
{
"name": " Branch 4",
"children": [{
"name": " Branch 4.1",
"children": []
},
{
"name": " Branch 4.2",
"children": []
},
{
"name": " Branch 4.1",
"children": []
}
]
},
{
"name": " Branch 5",
"children": []
},
{
"name": " Branch 6",
"children": []
},
{
"name": " Branch 7",
"children": []
},
{
"name": " Branch 7.1",
"children": []
},
{
"name": " Branch 7.2",
"children": [{
"name": " Branch 7.2.1",
"children": []
},
{
"name": " Branch 7.2.1",
"children": []
}
]
}
]
}
let svgMindMap = d3.select('#div-mindMap')
.append("svg")
.attr("id", "svg-mindMap")
.attr("width", widthMindMap)
.attr("height", heightMindMap);
let backgroundLayer = svgMindMap.append('g')
.attr("width", widthMindMap)
.attr("height", heightMindMap)
.attr("class", "background")
let gLeft = backgroundLayer.append("g")
.attr("transform", "translate(" + widthMindMap / 2 + ",0)")
.attr("class", "g-left");
let gLeftLink = gLeft.append('g')
.attr('class', 'g-left-link');
let gLeftNode = gLeft.append('g')
.attr('class', 'g-left-node');
function loadMindMap(parsed) {
var data = parsed;
var split_index = Math.round(data.children.length / 2);
parsedData = {
"name": data.name,
"children": JSON.parse(JSON.stringify(data.children.slice(split_index)))
};
var left = d3.hierarchy(parsedData, d => d.children);
drawLeft(left, "left");
}
// draw single tree
function drawLeft(root, pos) {
var SWITCH_CONST = 1;
if (pos === "left") SWITCH_CONST = -1;
update(root, SWITCH_CONST);
}
function update(source, SWITCH_CONST) {
var tree = d3.tree()
.size([heightMindMap, SWITCH_CONST * (widthMindMap - 150) / 2]);
var root = tree(source);
console.log(root)
var nodes = root.descendants();
var links = root.links();
console.log(nodes)
console.log(links)
// Set both root nodes to be dead center vertically
nodes[0].x = heightMindMap / 2
//JOIN new data with old elements
var link = gLeftLink.selectAll(".link-left")
.data(links, d => d)
.style('stroke-width', 1.5);
var linkEnter = link.enter().append("path")
.attr("class", "linkMindMap link-left")
.attr("d", d3.linkHorizontal()
.x(d => d.y)
.y(d => d.x));
var linkUpdate = linkEnter.merge(link);
linkUpdate.transition()
.duration(750)
var linkExit = link.exit()
.transition()
.duration(750)
.attr('x1', function(d) {
return root.x;
})
.attr('y1', function(d) {
return root.y;
})
.attr('x2', function(d) {
return root.x;
})
.attr('y2', function(d) {
return root.y;
})
.remove();
//JOIN new data with old elements
var node = gLeftNode.selectAll(".nodeMindMap-left")
.data(nodes, d => d);
console.log(nodes);
//ENTER new elements present in new data
var nodeEnter = node.enter().append("g").merge(node)
.attr("class", function(d) {
return "nodeMindMap-left " + "nodeMindMap" + (d.children ? " node--internal" : " node--leaf");
})
.classed("enter", true)
.attr("transform", function(d) {
return "translate(" + d.y + "," + d.x + ")";
})
.attr("id", function(d) {
let str = d.data.name;
str = str.replace(/\s/g, '');
return str;
});
nodeEnter.append("circle")
.attr("r", function(d, i) {
return 2.5
});
var addLeftChild = nodeEnter.append("g")
.attr("class", "addHandler")
.attr("id", d => {
let str = d.data.name;
str = "addHandler-" + str.replace(/\s/g, '');
return str;
})
.style("opacity", "1")
.on("click", (d, i, nodes) => addNewLeftChild(d, i, nodes));
addLeftChild.append("line")
.attr("x1", -74)
.attr("y1", 1)
.attr("x2", -50)
.attr("y2", 1)
.attr("stroke", "#85e0e0")
.style("stroke-width", "2");
addLeftChild.append("rect")
.attr("x", "-77")
.attr("y", "-7")
.attr("height", 15)
.attr("width", 15)
.attr("rx", 5)
.attr("ry", 5)
.style("stroke", "#444")
.style("stroke-width", "1")
.style("fill", "#ccc");
addLeftChild.append("line")
.attr("x1", -74)
.attr("y1", 1)
.attr("x2", -65)
.attr("y2", 1)
.attr("stroke", "#444")
.style("stroke-width", "1.5");
addLeftChild.append("line")
.attr("x1", -69.5)
.attr("y1", -3)
.attr("x2", -69.5)
.attr("y2", 5)
.attr("stroke", "#444")
.style("stroke-width", "1.5");
// .call(d3.drag().on("drag", dragged));;
nodeEnter.append("foreignObject")
.style("fill", "blue")
.attr("x", -50)
.attr("y", -7)
.attr("height", "20px")
.attr("width", "100px")
.append('xhtml:div')
.append('div')
.attr("class", 'clickable-node')
.attr("id", function(d) {
let str = d.data.name;
str = "div-" + str.replace(/\s/g, '');
return str;
})
.attr("ondblclick", "this.contentEditable=true")
.attr("onblur", "this.contentEditable=false")
.attr("contentEditable", "false")
.style("text-align", "center")
.text(d => d.data.name);
//TODO: make it dynamic
nodeEnter.insert("rect", "foreignObject")
.attr("ry", 6)
.attr("rx", 6)
.attr("y", -10)
.attr("height", 20)
.attr("width", 100)
// .filter(function(d) { return d.flipped; })
.attr("x", -50)
.classed("selected", false)
.attr("id", function(d) {
let str = d.data.name;
str = "rect-" + str.replace(/\s/g, '');
return str;
});
var nodeUpdate = nodeEnter.merge(node);
// Transition to the proper position for the node
nodeUpdate.transition()
.duration(750)
.attr("transform", function(d) {
return "translate(" + d.y + "," + d.x + ")";
});
// Remove any exiting nodes
var nodeExit = node.exit()
.transition()
.duration(750)
.attr("transform", function(d) {
return "translate(" + source.y + "," + source.x + ")";
})
.remove();
// On exit reduce the node circles size to 0
nodeExit.select('circle').attr('r', 0);
// node = nodeEnter.merge(node)
}
function addNewLeftChild(d, i, nodes) {
console.log("make new child");
event.stopPropagation();
var newNodeObj = {
// name: new Date().getTime(),
name: "New Child",
children: []
};
console.log("this is ", parsedData)
//Creates new Node
var newNode = d3.hierarchy(newNodeObj);
newNode.depth = d.depth + 1;
newNode.height = d.height - 1;
newNode.parent = d;
newNode.id = Date.now();
console.log(newNode);
console.log(d)
if (d.data.children.length == 0) {
console.log("i have no children")
d.children = []
}
d.children.push(newNode)
d.data.children.push(newNode.data)
console.log(d)
let foo = d3.hierarchy(parsedData, d => d.children)
drawLeft(foo, "left");
}
loadMindMap(parsedList);
There are a few things going on:
Using Unique Keys
Using names isn't the best for keys, because each new node has the same name ("New Child"). Instead it's probably better to use some sort of ID system. Here's a quick function to tag the data for each node with an ID.
let currNodeId = 0;
function idData(node) {
node.nodeId = ++currNodeId;
node.children.forEach(idData);
}
idData(parsedList);
And since you're redefining the data in parsedData, you need to use the id property there too:
parsedData = {
"name": data.name,
"nodeId": data.nodeId,
"children": JSON.parse(JSON.stringify(data.children.slice(split_index)))
};
When adding a new node, you can also set it in the nodeData:
var newNodeObj = {
// name: new Date().getTime(),
name: "New Child",
nodeId: ++currNodeId,
children: []
};
Then to actually use .nodeId as the key for nodes, use it as the key function:
.data(nodes, d => d.data.nodeId);
For links, you should use the target instead of the source, since this is a tree and there is only one link per child (instead of multiple links for one parent).
.data(nodes, d => d.target.data.nodeId);
Prevent multiple node elements from being added
There's also an issue where you're merging your new and old nodes before adding new elements. To prevent this, you should change
node.enter().append("g").merge(node)
to:
node.enter().append("g")
Link transitions
Finally the transitions for your links are not transitioning with the nodes. To make them transition, move:
.attr("d", d3.linkHorizontal()
.x(d => d.y)
.y(d => d.x));
to under
linkUpdate.transition()
.duration(750)
All together it looks like this: https://jsfiddle.net/v9wyb6q4/
Or:
const widthMindMap = 700;
const heightMindMap = 700;
let parsedData;
let parsedList = {
"name": " Stapler",
"children": [{
"name": " Bind",
"children": []
},
{
"name": " Nail",
"children": []
},
{
"name": " String",
"children": []
},
{
"name": " Glue",
"children": [{
"name": "Gum",
"children": []
},
{
"name": "Sticky Gum",
"children": []
}
]
},
{
"name": " Branch 3",
"children": []
},
{
"name": " Branch 4",
"children": [{
"name": " Branch 4.1",
"children": []
},
{
"name": " Branch 4.2",
"children": []
},
{
"name": " Branch 4.1",
"children": []
}
]
},
{
"name": " Branch 5",
"children": []
},
{
"name": " Branch 6",
"children": []
},
{
"name": " Branch 7",
"children": []
},
{
"name": " Branch 7.1",
"children": []
},
{
"name": " Branch 7.2",
"children": [{
"name": " Branch 7.2.1",
"children": []
},
{
"name": " Branch 7.2.1",
"children": []
}
]
}
]
}
let currNodeId = 0;
function idData(node) {
node.nodeId = ++currNodeId;
node.children.forEach(idData);
}
idData(parsedList);
let svgMindMap = d3.select('#div-mindMap')
.append("svg")
.attr("id", "svg-mindMap")
.attr("width", widthMindMap)
.attr("height", heightMindMap);
let backgroundLayer = svgMindMap.append('g')
.attr("width", widthMindMap)
.attr("height", heightMindMap)
.attr("class", "background")
let gLeft = backgroundLayer.append("g")
.attr("transform", "translate(" + widthMindMap / 2 + ",0)")
.attr("class", "g-left");
let gLeftLink = gLeft.append('g')
.attr('class', 'g-left-link');
let gLeftNode = gLeft.append('g')
.attr('class', 'g-left-node');
function loadMindMap(parsed) {
var data = parsed;
var split_index = Math.round(data.children.length / 2);
parsedData = {
"name": data.name,
"nodeId": data.nodeId,
"children": JSON.parse(JSON.stringify(data.children.slice(split_index)))
};
var left = d3.hierarchy(parsedData, d => d.children);
drawLeft(left, "left");
}
// draw single tree
function drawLeft(root, pos) {
var SWITCH_CONST = 1;
if (pos === "left") SWITCH_CONST = -1;
update(root, SWITCH_CONST);
}
function update(source, SWITCH_CONST) {
var tree = d3.tree()
.size([heightMindMap, SWITCH_CONST * (widthMindMap - 150) / 2]);
var root = tree(source);
console.log(root)
var nodes = root.descendants();
var links = root.links();
console.log(nodes)
console.log(links)
// Set both root nodes to be dead center vertically
nodes[0].x = heightMindMap / 2
//JOIN new data with old elements
var link = gLeftLink.selectAll(".link-left")
.data(links, d => d.target.data.nodeId)
.style('stroke-width', 1.5);
var linkEnter = link.enter().append("path")
.attr("class", "linkMindMap link-left");
var linkUpdate = linkEnter.merge(link);
linkUpdate.transition()
.duration(750)
.attr("d", d3.linkHorizontal()
.x(d => d.y)
.y(d => d.x));
var linkExit = link.exit()
.transition()
.duration(750)
.attr('x1', function(d) {
return root.x;
})
.attr('y1', function(d) {
return root.y;
})
.attr('x2', function(d) {
return root.x;
})
.attr('y2', function(d) {
return root.y;
})
.remove();
//JOIN new data with old elements
var node = gLeftNode.selectAll(".nodeMindMap-left")
.data(nodes, d => d.data.nodeId);
console.log(nodes);
//ENTER new elements present in new data
var nodeEnter = node.enter().append("g")
.attr("class", function(d) {
return "nodeMindMap-left " + "nodeMindMap" + (d.children ? " node--internal" : " node--leaf");
})
.classed("enter", true)
.attr("transform", function(d) {
return "translate(" + d.y + "," + d.x + ")";
})
.attr("id", function(d) {
let str = d.data.name;
str = str.replace(/\s/g, '');
return str;
});
nodeEnter.append("circle")
.attr("r", function(d, i) {
return 2.5
});
var addLeftChild = nodeEnter.append("g")
.attr("class", "addHandler")
.attr("id", d => {
let str = d.data.name;
str = "addHandler-" + str.replace(/\s/g, '');
return str;
})
.style("opacity", "1")
.on("click", (d, i, nodes) => addNewLeftChild(d, i, nodes));
addLeftChild.append("line")
.attr("x1", -74)
.attr("y1", 1)
.attr("x2", -50)
.attr("y2", 1)
.attr("stroke", "#85e0e0")
.style("stroke-width", "2");
addLeftChild.append("rect")
.attr("x", "-77")
.attr("y", "-7")
.attr("height", 15)
.attr("width", 15)
.attr("rx", 5)
.attr("ry", 5)
.style("stroke", "#444")
.style("stroke-width", "1")
.style("fill", "#ccc");
addLeftChild.append("line")
.attr("x1", -74)
.attr("y1", 1)
.attr("x2", -65)
.attr("y2", 1)
.attr("stroke", "#444")
.style("stroke-width", "1.5");
addLeftChild.append("line")
.attr("x1", -69.5)
.attr("y1", -3)
.attr("x2", -69.5)
.attr("y2", 5)
.attr("stroke", "#444")
.style("stroke-width", "1.5");
// .call(d3.drag().on("drag", dragged));;
nodeEnter.append("foreignObject")
.style("fill", "blue")
.attr("x", -50)
.attr("y", -7)
.attr("height", "20px")
.attr("width", "100px")
.append('xhtml:div')
.append('div')
.attr("class", 'clickable-node')
.attr("id", function(d) {
let str = d.data.name;
str = "div-" + str.replace(/\s/g, '');
return str;
})
.attr("ondblclick", "this.contentEditable=true")
.attr("onblur", "this.contentEditable=false")
.attr("contentEditable", "false")
.style("text-align", "center")
.text(d => d.data.name);
//TODO: make it dynamic
nodeEnter.insert("rect", "foreignObject")
.attr("ry", 6)
.attr("rx", 6)
.attr("y", -10)
.attr("height", 20)
.attr("width", 100)
// .filter(function(d) { return d.flipped; })
.attr("x", -50)
.classed("selected", false)
.attr("id", function(d) {
let str = d.data.name;
str = "rect-" + str.replace(/\s/g, '');
return str;
});
var nodeUpdate = nodeEnter.merge(node);
// Transition to the proper position for the node
nodeUpdate.transition()
.duration(750)
.attr("transform", function(d) {
return "translate(" + d.y + "," + d.x + ")";
});
// Remove any exiting nodes
var nodeExit = node.exit()
.transition()
.duration(750)
.attr("transform", function(d) {
return "translate(" + source.y + "," + source.x + ")";
})
.remove();
// On exit reduce the node circles size to 0
nodeExit.select('circle').attr('r', 0);
// node = nodeEnter.merge(node)
}
function addNewLeftChild(d, i, nodes) {
console.log("make new child");
event.stopPropagation();
var newNodeObj = {
// name: new Date().getTime(),
name: "New Child",
nodeId: ++currNodeId,
children: []
};
console.log("this is ", parsedData)
//Creates new Node
var newNode = d3.hierarchy(newNodeObj);
newNode.depth = d.depth + 1;
newNode.height = d.height - 1;
newNode.parent = d;
newNode.id = Date.now();
console.log(newNode);
console.log(d)
if (d.data.children.length == 0) {
console.log("i have no children")
d.children = []
}
d.children.push(newNode)
d.data.children.push(newNode.data)
console.log(d)
let foo = d3.hierarchy(parsedData, d => d.children)
drawLeft(foo, "left");
}
loadMindMap(parsedList);
.linkMindMap {
fill: none;
stroke: #555;
stroke-opacity: 0.4;
}
rect {
fill: white;
stroke: #3182bd;
stroke-width: 1.5px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.13.0/d3.min.js"></script>
<div id="div-mindMap">
These sorts of problems usually arise because the key function which you pass as the second parameter to selection.data() is not unique or "too unique".
It should return a simple value which uniquely identifies each datum. In your case
.data(nodes, d => d);
might be better as
.data(nodes, d => d.name);
and similarly for the links.
However you will need to examine the output of d3.tree to see what fields the nodes and links have and which field contains a unique id.

d3 javascript series chart

I am trying to create this particular d3 application where a series of data can be dynamically displayed like this. Each segment contains two pieces of data.
The first step is to print the circles so there is sufficient space between the series but also the largest circle is always under the smaller circle.
//version 3 -- with correct labels and legend--
http://jsfiddle.net/0ht35rpb/33/
//******version 2 fiddle******
http://jsfiddle.net/1oka61mL/10/
-- How to set the diagonal labels properly - same angles, aligned properly?
-- Add legend?
-- Mask the bottom pointers in an opposite color then continue the line in a different color?
//******Latest Jsfiddle******
http://jsfiddle.net/0ht35rpb/26/
var width = 600;
var height = 400;
var svg = d3.select('svg').attr("width", width).attr("height", height);
//Count
//Checkins
//Popularity
var data = [{
"name": "Twitter",
"items": [{
"id": 0,
"label": "Count",
"value": 200
}, {
"id": 1,
"label": "Checkins",
"value": 1000
}, {
"id": 2,
"label": "Popularity",
"value": 30
}]
}, {
"name": "Facebook",
"items": [{
"id": 0,
"label": "Count",
"value": 500
}, {
"id": 1,
"label": "Checkins",
"value": 300
}, {
"id": 2,
"label": "Popularity",
"value": 740
}]
}, {
"name": "Ebay",
"items": [{
"id": 0,
"label": "Count",
"value": 4000
}, {
"id": 1,
"label": "Checkins",
"value": 1000
}, {
"id": 2,
"label": "Popularity",
"value": 40
}]
}, {
"name": "Foursquare",
"items": [{
"id": 0,
"label": "Count",
"value": 2000
}, {
"id": 1,
"label": "Checkins",
"value": 3000
}, {
"id": 2,
"label": "Popularity",
"value": 4500
}]
}];
var outerRadius = [];
// organise the data.
// Insert indices and sort items in each series
// keep a running total of max circle size in each series
// for later positioning
var x = 0;
var totalWidth = d3.sum(
data.map(function(series) {
series.items.forEach(function(item, i) {
item.index = i;
});
series.items.sort(function(a, b) {
return b.value - a.value;
});
var maxr = Math.sqrt(series.items[0].value);
outerRadius.push(maxr);
x += maxr;
series.xcentre = x;
x += maxr;
return maxr * 2;
})
);
// make scales for position and colour
var scale = d3.scale.linear().domain([0, totalWidth]).range([0, width]);
//var colScale = d3.scale.category10();
function colores_google(n) {
var colores_g = ["#f7b363", "#448875", "#c12f39", "#2b2d39", "#f8dd2f"];
return colores_g[n % colores_g.length];
}
// add a group per series, position the group according to the values and position scale we calculated above
var groups = svg.selectAll("g").data(data);
groups.enter().append("g");
groups.attr("transform", function(d) {
return ("translate(" + d.xcentre + ",0)");
});
// then add circles per series, biggest first as items are sorted
// colour according to index (the property we inserted previously so we can
// keep track of their original position in the series)
var circles = groups.selectAll("circle").data(function(d) {
return d.items;
}, function(d) {
return d.index;
});
circles.enter().append("circle").attr("cy", height / 2).attr("cx", 0);
circles
.attr("r", function(d) {
return Math.sqrt(d.value);
})
.style("fill", function(d) {
return colores_google(d.index);
});
var labelsgroups = svg.selectAll("text").data(data);
labelsgroups.enter().append("text");
labelsgroups
.attr("y", function(d, i) {
d.y = 300;
d.cy = 200;
return 300;
})
.attr("x", function(d) {
d.x = d.xcentre;
d.cx = d.xcentre;
return d.xcentre;
})
.text(function(d) {
return d.name;
})
.each(function(d) {
var bbox = this.getBBox();
d.sx = d.x - bbox.width / 2 - 2;
d.ox = d.x + bbox.width / 2 + 2;
d.sy = d.oy = d.y + 5;
})
.attr("text-anchor", "middle");
var pointersgroups = svg.selectAll("path.pointer").data(data);
pointersgroups.enter().append("path");
pointersgroups
.attr("class", "pointer")
.attr("marker-end", "url(#circ)");
pointersgroups
.attr("d", function(d) {
return "M" + (d.xcentre) + "," + (d.oy - 25) + "L" + (d.xcentre) + "," + (d.sy - 25) + " " + d.xcentre + "," + (d.cy);
})
function fetchValue(items, label) {
for (i = 0; i <= items.length; i++) {
if (items[i].label == label) {
return items[i].value;
}
}
}
function fetchRadius(items, label) {
for (i = 0; i <= items.length; i++) {
if (items[i].label == label) {
return Math.sqrt(items[i].value);
}
}
}
/*
var labels1groups = svg.selectAll(".label1").data(data);
labels1groups.enter().append("text");
labels1groups
.attr("class", "label1")
.attr("y", function(d, i) {
d.y = 100;
d.cy = 100;
return 100;
})
.attr("x", function(d) {
d.x = d.xcentre;
d.cx = d.xcentre+50;
return d.xcentre+50;
})
.text(function(d) {
return fetchValue(d.items, "Count");
})
.attr("transform", function(d, i) {
return "translate(" + (15 * i) + "," + (i * 45) + ") rotate(-45)";
})
.each(function(d) {
var bbox = this.getBBox();
d.sx = d.x - bbox.width / 2 - 2;
d.ox = d.x + bbox.width / 2 + 2;
d.sy = d.oy = d.y ;
})
.attr("text-anchor", "left");
*/
var gridSize = 100;
var labels1groups = svg.selectAll(".label2")
.data(data)
.enter().append("text")
.text(function(d) {
return fetchValue(d.items, "Count");
//return d;
})
.attr("x", function(d, i) {
d.x = i * gridSize + 50;
d.cx = i * gridSize + 50;
return i * gridSize;
})
.attr("y", function(d, i) {
d.y = 105;
d.cy = 50;
return 0;
})
.attr("transform", function(d, i) {
return "translate(" + gridSize / 2 + ", -6)" +
"rotate(-45 " + ((i + 0.5) * gridSize) + " " + (-6) + ")";
})
.each(function(d) {
var bbox = this.getBBox();
d.sx = d.x - bbox.width / 2 - 2;
d.ox = d.x + bbox.width / 2 + 2;
d.sy = d.oy = d.y;
})
.style("text-anchor", "end")
.attr("class", function(d, i) {
return ((i >= 8 && i <= 16) ?
"timeLabel mono axis axis-worktime" :
"timeLabel mono axis");
});
var pointers1groups = svg.selectAll("path.pointer1").data(data);
pointers1groups.enter().append("path");
pointers1groups
.attr("class", "pointer1")
.attr("marker-end", "url(#circ)");
pointers1groups
.attr("d", function(d, i) {
//d.y = outerRadius[i];
//d.y = d.oy - d.cy;
//fetchRadius(d.items, "Count");
//(d.xcentre+100)
// + " " + d.cx + "," + d.cy
//return "M "+ (d.xcentre) +" 25 ,L "+ dist +" 75";
return "M" + (d.xcentre) + "," + (d.y + d.oy - fetchRadius(d.items, "Count") - 10) + "L" + (d.xcentre + 80) + "," + d.cy;
})
//Older Jsfiddle
http://jsfiddle.net/59bunh8u/51/
var rawData = [{
"name": "Twitter",
"items" : [
{
"label" : "15 billion",
"unit" : "per day",
"value" : 1500
},
{
"label" : "450 checkins",
"unit" : "per day",
"value" : 450
}
]
},
{
"name": "Facebook",
"items" : [
{
"label" : "5 billion",
"unit" : "per day",
"value" : 5000
},
{
"label" : "2000 checkins",
"unit" : "per day",
"value" : 2000
}
]
}];
$.each(rawData, function(index, value) {
var total = 0;
var layerSet = [];
var ratios = [25, 100];
$.each(value["items"], function(i, v) {
total += v["value"];
});
value["total"] = total;
});
var w = $this.data("width");
var h = $this.data("height");
var el = $this;
var margin = {
top: 65,
right: 90,
bottom: 5,
left: 150
};
var svg = d3.select(el[0]).append("svg")
.attr("class", "series")
.attr("width", w + margin.left + margin.right)
.attr("height", h + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
var defs = svg.append("svg:defs");
$.each(rawData, function(i, v) {
circleDraw(i, v["items"]);
});
//calculates where each element should be placed
function calculateDistance (d, i, items) {
var dcx = 0;
for (var k = 0; k < i; k++) {
dcx += Math.sqrt(items[k].value);
}
return dcx + 10 * i;
}
function getPercentage(value, total) {
return ((value / total) * 100);
}
function circleDraw(index, data){
data.sort(function(a, b) {
return parseFloat(b.value) - parseFloat(a.value);
});
var circlelayer = svg.append("g")
.attr("class", "circlelayer");
var circle = circlelayer.selectAll("circle")
.data(data);
circle.enter().append("circle")
.attr("class", function(d, i) {
if (i == 0) {
return "blue";
}
return "gold";
})
.attr("cy", 60)
.attr("cx", function(d, i) {
return calculateDistance(d, index, data);
})
.attr("r", function(d, i) {
return Math.sqrt(d.value);
});
circle.exit().remove();
}
Here is how you could drawn the lines:
groups.append("line")
.attr("class", "pointer1")
.attr("marker-end", "url(#circ)")
.attr("x1", 0)
.attr("y1", height / 2)
.attr("x2", 0)
.attr("y2",height - 10);
var linesG = svg.selectAll(".slines").data(data)
.enter().append("g")
.attr("transform", function(d) {
return ("translate(" + d.xcentre + "," + height/2 +") rotate(-45)");
});
linesG.append("line")
.attr("class", "pointer1")
.attr("marker-end", "url(#circ)")
.attr("x1", 0)
.attr("y1", 0)
.attr("x2", 150)
.attr("y2", 0);
linesG.append("text")
.text(function(d) {
return fetchValue(d.items, "Count");
})
.attr("text-anchor", "end")
.attr("y", -5)
.attr("x", 150);
Updated jsfiddle
I've managed to get the diagonal markers and pointers in alignment, pointing to the correct circle colors to represent that set. I am keen to fine tune this chart and have more control over the padding and chart width/height parameters. The chart looks stable but would be keen to test it with different values and sized data sets.
/LATEST/
http://jsfiddle.net/0ht35rpb/33/
var width = 760;
var height = 400;
var svg = d3.select('#serieschart')
.append("svg:svg")
.attr("width", width)
.attr("height", height);
//Count
//Checkins
//Popularity
var data = [{
"name": "Twitter",
"items": [{
"id": 0,
"label": "Count",
"value": 200
}, {
"id": 1,
"label": "Checkins",
"value": 1000
}, {
"id": 2,
"label": "Popularity",
"value": 30
}]
}, {
"name": "Facebook",
"items": [{
"id": 0,
"label": "Count",
"value": 500
}, {
"id": 1,
"label": "Checkins",
"value": 300
}, {
"id": 2,
"label": "Popularity",
"value": 740
}]
}, {
"name": "Ebay",
"items": [{
"id": 0,
"label": "Count",
"value": 4000
}, {
"id": 1,
"label": "Checkins",
"value": 1000
}, {
"id": 2,
"label": "Popularity",
"value": 40
}]
}, {
"name": "Foursquare",
"items": [{
"id": 0,
"label": "Count",
"value": 2000
}, {
"id": 1,
"label": "Checkins",
"value": 3000
}, {
"id": 2,
"label": "Popularity",
"value": 4500
}]
}];
var legend_group = svg.append("g")
.attr("class", "legend")
.attr("width", 80)
.attr("height", 100)
.append("svg:g")
.attr("class", "legendsection")
.attr("transform", "translate(0,30)");
var legend = legend_group.selectAll("circle").data(data[0].items);
legend.enter().append("circle")
.attr("cx", 70)
.attr("cy", function(d, i) {
return 15 * i;
})
.attr("r", 7)
.attr("width", 18)
.attr("height", 18)
.style("fill", function(d, i) {
return colores_google(i);
});
legend.exit().remove();
var legendtext = legend_group.selectAll("text").data(data[0].items);
legendtext.enter().append("text")
.attr("class", "labels")
.attr("dy", function(d, i) {
return 15 * i;
})
.attr("text-anchor", function(d) {
return "start";
})
.text(function(d) {
return d.label;
});
legendtext.exit().remove();
var m = [80, 20, 20, 10];
var w =+ width - m[0];
var h =+ height - m[1];
var chart = svg.append("g")
.attr("class", "serieschart")
.attr("width", w)
.attr("height", h);
var outerRadius = [];
// organise the data.
// Insert indices and sort items in each series
// keep a running total of max circle size in each series
// for later positioning
var x = 0;
var totalWidth = d3.sum(
data.map(function(series) {
series.items.forEach(function(item, i) {
item.index = i;
});
series.items.sort(function(a, b) {
return b.value - a.value;
});
var maxr = Math.sqrt(series.items[0].value);
outerRadius.push(maxr);
x += maxr;
series.xcentre = x;
x += maxr;
return maxr * 2;
})
);
// make scales for position and colour
var scale = d3.scale.linear().domain([0, totalWidth]).range([0, w]);
//var colScale = d3.scale.category10();
function colores_google(n) {
var colores_g = ["#f7b363", "#448875", "#c12f39", "#2b2d39", "#f8dd2f"];
return colores_g[n % colores_g.length];
}
function fetchValue(items, label) {
for (i = 0; i <= items.length; i++) {
if (items[i].label == label) {
return items[i].value;
}
}
}
function fetchRadius(items, label) {
for (i = 0; i <= items.length; i++) {
if (items[i].label == label) {
return Math.sqrt(items[i].value);
}
}
}
// add a group per series, position the group according to the values and position scale we calculated above
var groups = chart.selectAll("g.seriesGroup").data(data);
var newGroups = groups.enter().append("g").attr("class", "seriesGroup");
newGroups.append("text")
.attr("class", "seriesName")
.attr("text-anchor", "middle");
newGroups.append("line")
.attr("class", "seriesName")
.attr("y1", h - 40)
.attr("y2", h / 2);
newGroups.append("text")
.attr("class", "datumValue")
.attr("y", 10)
//.attr("transform", "rotate(-45)")
;
newGroups.append("g").attr("class", "circleGroup");
newGroups.append("g").attr("class", "datumLine")
.append("line")
.attr("class", "datumValue")
.attr("y2", 40);
var focus = "Count";
groups.attr("transform", function(d) {
return "translate(" + scale(d.xcentre) + ",0)";
});
groups.select("text.seriesName")
.text(function(d) {
return d.name;
})
.attr("y", h - 20);
groups.select("text.datumValue")
.text(function(d) {
return fetchValue(d.items, focus);
})
.attr("transform", function(d) {
return "translate(" + ((h / 2) - 20 - scale(fetchRadius(d.items, focus))) + ",20) rotate(-45)";
});
groups.select("line.datumValue")
.attr("y1", function(d) {
return (h / 2) - scale(fetchRadius(d.items, focus));
})
.attr("x2", function(d) {
return (h / 2) - scale(fetchRadius(d.items, focus) + 20);
});
// then add circles per series, biggest first as items are sorted
// colour according to index (the property we inserted previously so we can
// keep track of their original position in the series)
var circles = groups
.select(".circleGroup")
.selectAll("circle").data(function(d) {
return d.items;
}, function(d) {
return d.index;
});
circles.enter().append("circle").attr("cy", h / 2).attr("cx", 0);
circles
.attr("r", function(d) {
return scale(Math.sqrt(d.value));
})
.style("fill", function(d) {
return colores_google(d.index);
});

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 Common Trait Chart

http://jsfiddle.net/NYEaX/1791/
In creating a relationship chart - that shows common traits - I am struggling to create the curved arcs that will match the position of the dots.
What is the best way of plotting these arcs so it dips below the horizon?
var data = [{
"userName": "Rihanna",
"userImage": "https://encrypted-tbn1.gstatic.com/images?q=tbn:ANd9GcSTzjaQlkAJswpiRZByvgsb3CVrfNNLLwjFHMrkZ_bzdPOWdxDE2Q",
"userDetails": [{
"Skills & Expertise": [{
"id": 2,
"tag": "Javascript"
}, {
"id": 3,
"tag": "Design"
}],
"Location": [{
"id": 0,
"tag": "London"
}, {
"id": 1,
"tag": "Germany"
}],
"Company": [{
"id": 0,
"tag": "The Old County"
}]
}]
}, {
"userName": "Brad",
"userImage": "https://lh3.googleusercontent.com/-XdASQvEzIzE/AAAAAAAAAAI/AAAAAAAAAls/5vbx7yVLDnc/photo.jpg",
"userDetails": [{
"Skills & Expertise": [{
"id": 0,
"tag": "JAVA"
}, {
"id": 1,
"tag": "PHP"
}, {
"id": 2,
"tag": "Javascript"
}],
"Location": [{
"id": 0,
"tag": "London"
}],
"Company": [{
"id": 0,
"tag": "The Old County"
}, {
"id": 1,
"tag": "Bakerlight"
}]
}]
}]
var viz = d3.select("#viz")
.append("svg")
.attr("width", 600)
.attr("height", 600)
.append("g")
.attr("transform", "translate(40,100)")
var patternsSvg = viz
.append('g')
.attr('class', 'patterns');
function colores_google(n) {
var colores_g = ["#f7b363", "#448875", "#c12f39", "#2b2d39", "#f8dd2f"];
return colores_g[n % colores_g.length];
}
function getRadius(d) {
var count = d.commonTags.split(",").length;
var ratio = count * 2.3;
if (count == 1) {
ratio = 8;
}
return ratio;
}
//create patterns for user images
$.each(data, function(index, value) {
var defs = patternsSvg.append('svg:defs');
defs.append('svg:pattern')
.attr('id', index + "-" + value.userName.toLowerCase())
.attr('width', 1)
.attr('height', 1)
.append('svg:image')
.attr('xlink:href', value.userImage)
.attr('x', 0)
.attr('y', 0)
.attr('width', 75)
.attr('height', 75);
console.log(value.userDetails[0]);
});
//create common data assement
var data1 = [{
"commonLabel": "Groups",
"commonTags": "test1, test2, test3, test4, test5, test6, test7"
}, {
"commonLabel": "Skills & Expertise",
"commonTags": "test1, test2, test3, test1, test2, test3, test1, test2, test3, test1, test2"
}, {
"commonLabel": "Location",
"commonTags": "test1"
}, {
"commonLabel": "Company",
"commonTags": "test1"
}]
//add curved paths
var distanceBetween = 70;
var pathStart = -400;
var path = viz.append("svg:g").selectAll("path")
.data(data1)
path
.enter().append("svg:path")
.attr("class", function(d) {
return "link "
})
path.attr("d", function(d, i) {
var sx = 0;
var tx = 235;
var sy = 120;
var ty = 120;
pathStart += 125;
var dx = 0;
var dy = getRadius(d) + (distanceBetween * i) - pathStart;
var dr = Math.sqrt(dx * dx + dy * dy);
console.log("dy", dy);
return "M" + sx + "," + sy + "A" + dr + "," + dr + " 0 0,1 " + tx + "," + ty;
});
//add curved paths
//create circles to hold the user images
var circle = viz.append("svg:g").selectAll("circle")
.data(data);
//enter
circle
.enter()
.append("svg:circle")
.attr("id", function(d) {
return d.userName;
})
.attr("r", function(d) {
return "30";
})
.attr("cx", function(d, i) {
return "235" * i;
})
.attr("cy", function(d, i) {
return "120";
})
.style("fill", function(d, i) {
return "url(#" + i + "-" + d.userName.toLowerCase() + ")";
})
var distanceBetween = 65;
var circle = viz.append("svg:g").selectAll("circle")
.data(data1);
//enter
circle
.enter()
.append("svg:circle")
.attr("id", function(d) {
return d.commonLabel;
})
.attr("r", function(d) {
return getRadius(d);
})
.attr("cx", function(d, i) {
return 125;
})
.attr("cy", function(d, i) {
return distanceBetween * i;
})
.style("fill", function(d, i) {
return colores_google(i);
});
var text = viz.append("svg:g").selectAll("g")
.data(data1)
text
.enter().append("svg:g");
text.append("svg:text")
.attr("text-anchor", "middle")
.attr("x", "125")
.attr("y", function(d, i) {
return getRadius(d) + 15 + (distanceBetween * i);
})
.text(function(d) {
return d.commonLabel;
})
.attr("id", function(d) {
return "text" + d.commonLabel;
});
var counters = viz.append("svg:g").selectAll("g")
.data(data1)
counters
.enter().append("svg:g");
counters.append("svg:text")
.attr("text-anchor", "middle")
.attr("x", "125")
.attr("y", function(d, i) {
return ((getRadius(d) / 2) + (distanceBetween * i)) - 3;
})
.text(function(d) {
var count = d.commonTags.split(",").length;
if (count > 1) {
return count;
}
})
.attr("id", function(d) {
return "textcount" + d.commonLabel;
});
Live demo:
http://jsfiddle.net/blackmiaool/p58a0w3h/1/
First of all, you should remove all the magic numbers to keep the code clean and portable.
This one, for example:
pathStart += 125;
How to draw arcs correctly is a math problem. The code is as blow:
path.attr("d", function (d, i) {
const sx = 0;
const sy = height/2;
const a=width/2;
const b = ((data1.length-1)/2-i)*distanceBetween;
const c = Math.sqrt(a * a + b * b);
const angle=Math.atan(a/b);
let r;
if(b===0){
r=0;
}else{
r=1/2*c/Math.cos(angle);//also equals c/b*(c/2)
// r=c/b*(c/2);
}
return `M${sx},${sy} A${r},${r} 0 0,${b>0?1:0} ${width},${height/2}`;
});
And the diagram:

Categories