Related
I made a d3.js chart using this code bl.ocks.org/d3noob/8375092
I am trying to change the color of a word. In every instance of a word, I would like to have that color changed. For example, if you look at the chart, there is First Daughter of A and Daughter of A. I would like to change the color of the text "Daughter" to red. And I would like to change all of "Son of Level 2: B" to the color blue. (Ignore the large height and width -in my project I have 50 terms in my collapsible chart - but only included a few in this question).
I tried in CSS to change the color of all text, but it does not appear. When I inspect an element, the color will show as "blue" but the text appears black. I also tried in HTML to use , but I don't know what to do for the variable tree.Data, since it is in JavaScript.
I am a beginner in coding. Appreciate your help!
var treeData = [
{
"name": "Top Level",
"parent": "null",
"children": [
{
"name": "Level 2: A",
"parent": "Top Level",
"children": [
{
"name": "First Daughter of A",
"parent": "Level 2: A"
},
{
"name": "Daughter of A",
"parent": "Level 2: A"
}
]
},
{
"name": "Level 2: B",
"parent": "Top Level",
"children": [
{
"name": "Son of Level 2: B",
"parent": "Level 2: B"
}
]
}
]
}
];
// ************** Generate the tree diagram *****************
var margin = {top: 20, right: 120, bottom: 20, left: 250},
width = 2000 - margin.right - margin.left,
height = 2000 - margin.top - margin.bottom;
var i = 0,
duration = 750,
root;
var tree = d3.layout.tree()
.size([height, width]);
var diagonal = d3.svg.diagonal()
.projection(function(d) { return [d.y, d.x]; });
var svg = d3.select("body").append("svg")
.attr("width", width + margin.right + margin.left)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
root = treeData[0];
root.x0 = height / 2;
root.y0 = 0;
update(root);
d3.select(self.frameElement).style("height", "500px");
function update(source) {
// Compute the new tree layout.
var nodes = tree.nodes(root).reverse(),
links = tree.links(nodes);
// Normalize for fixed-depth.
nodes.forEach(function(d) { d.y = d.depth * 180; });
// Update the nodes…
var node = svg.selectAll("g.node")
.data(nodes, function(d) { return d.id || (d.id = ++i); });
// Enter any new nodes at the parent's previous position.
var nodeEnter = node.enter().append("g")
.attr("class", "node")
.attr("transform", function(d) { return "translate(" + source.y0 + "," + source.x0 + ")"; })
.on("click", click);
nodeEnter.append("circle")
.attr("r", 1e-6)
.style("fill", function(d) { return d._children ? "lightsteelblue" : "#fff"; });
nodeEnter.append("text")
.attr("x", function(d) { return d.children || d._children ? -13 : 13; })
.attr("dy", ".35em")
.attr("text-anchor", function(d) { return d.children || d._children ? "end" : "start"; })
.text(function(d) { return d.name; })
.style("fill-opacity", 1e-6);
// Transition nodes to their new position.
var nodeUpdate = node.transition()
.duration(duration)
.attr("transform", function(d) { return "translate(" + d.y + "," + d.x + ")"; });
nodeUpdate.select("circle")
.attr("r", 10)
.style("fill", function(d) { return d._children ? "lightsteelblue" : "#fff"; });
nodeUpdate.select("text")
.style("fill-opacity", 1);
// Transition exiting nodes to the parent's new position.
var nodeExit = node.exit().transition()
.duration(duration)
.attr("transform", function(d) { return "translate(" + source.y + "," + source.x + ")"; })
.remove();
nodeExit.select("circle")
.attr("r", 1e-6);
nodeExit.select("text")
.style("fill-opacity", 1e-6);
// Update the links…
var link = svg.selectAll("path.link")
.data(links, function(d) { return d.target.id; });
// Enter any new links at the parent's previous position.
link.enter().insert("path", "g")
.attr("class", "link")
.attr("d", function(d) {
var o = {x: source.x0, y: source.y0};
return diagonal({source: o, target: o});
});
// Transition links to their new position.
link.transition()
.duration(duration)
.attr("d", diagonal);
// Transition exiting nodes to the parent's new position.
link.exit().transition()
.duration(duration)
.attr("d", function(d) {
var o = {x: source.x, y: source.y};
return diagonal({source: o, target: o});
})
.remove();
// Stash the old positions for transition.
nodes.forEach(function(d) {
d.x0 = d.x;
d.y0 = d.y;
});
}
// Toggle children on click.
function click(d) {
if (d.children) {
d._children = d.children;
d.children = null;
} else {
d.children = d._children;
d._children = null;
}
update(d);
}
.node {
cursor: pointer;
}
.block{
width:5%;
height:10%;
}
.top{
width: 95%;
margin-left: 5%;
color: black;
}
.no{
color:black;
}
.node circle {
fill: #fff;
stroke: steelblue;
stroke-width: 3px;
}
.node text {
color: Blue;
font-weight: bold;
font: 12px sans-serif;
}
.link {
fill: none;
stroke: #ccc;
stroke-width: 2px;
}
<div class="block"></div>
<div class="top">
<h1>Title</h1>
<h2>Click nodes to expand each level</h2>
<h3>Key</h3>
<div class="no">
<h4>No capability</h4>
</div>
<div class="adhoc">
<h4>Another heading</h4>
</div>
<h4>Another heading4</h4>
<h4>Heading 4</h4>
<h4>Final heading 4</h4>
</div>
<script src="https://d3js.org/d3.v7.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.17/d3.min.js"></script>
<script src="myChart.js"></script>
<link rel= "stylesheet" href="./myStyle.css">
Within an SVG in order to color individual words you'll need to use tspans within text elements. This means searching through text elements, finding matching strings, and replacing them with new child tspan elements containing the matching word.
Highlight (change color) of specified phrase/word in text elements:
One approach could be:
function highlight(selection,word) {
selection.each(function() {
this.innerHTML = this.textContent.replace(new RegExp(word, "ig"),(w)=>"<tspan>"+w+"</tspan>")
})
}
This function takes a selection of (text) elements, and a word to find. It searches the text element's text content to find matching strings and replaces matching strings with a tspan containing the matching string. It is case insensitive in matching text but preserves the case in the original text.
In the snippet below just type into the text box to dynamically highlight text:
var svg = d3.select("svg");
var data = [
"You can't direct the wind, but you can adjust your sails",
"If you chase two rabbits, you will lose them both.",
"If you speak the truth, have a foot in the stirrup.",
"One doesn't discover new lands without losing sight of the shore.",
"The whole is more than the sum of its parts."
]
var textElements = svg.selectAll(null)
.data(data)
.join("text")
.text(d=>d)
.attr("x",20)
.attr("y",(d,i) => i* 30 + 30);
function highlight(selection,word) {
selection.each(function() {
this.innerHTML = this.textContent.replace(new RegExp(word, "ig"),(w)=>"<tspan>"+w+"</tspan>")
})
}
d3.select("#text").on("keyup", function() {
textElements.call(highlight, this.value);
//alternatively: highlight(textElements,this.value);
})
tspan {
fill: orange;
stroke: orange;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.0.0/d3.min.js"></script>
<div><input type="text" id="text"/></div>
<svg width="500" height="250"></svg>
Highlight with a rectangle:
We could also be a bit fancier and use a highlighter stroke, which requires adding rectangles, to do this we could use the tspans as the data for a join to create the rectangles:
var svg = d3.select("svg");
var data = [
"You can't direct the wind, but you can adjust your sails",
"If you chase two rabbits, you will lose them both.",
"If you speak the truth, have a foot in the stirrup.",
"One doesn't discover new lands without losing sight of the shore.",
"The whole is more than the sum of its parts.",
]
var textElements = svg.selectAll(null)
.data(data)
.join("text")
.text(d=>d)
.attr("x",20)
.attr("y",(d,i) => i* 30 + 30);
function highlight(selection,word,rectContainer) {
selection.each(function() {
this.innerHTML = this.textContent.replace(new RegExp(word, "ig"),(w)=>"<tspan>"+w+"</tspan>")
})
// join, color, positoin the rectangles:
rectContainer.selectAll(".highlight")
.data(selection.selectAll("tspan").nodes())
.join("rect")
.attr("class","highlight")
.datum(d=>d.getBBox())
.attr("x", d=>d.x)
.attr("y", d=>d.y)
.attr("width", d=>d.width)
.attr("height", d=>d.height)
.attr("fill","yellow")
.lower();
}
d3.select("#text").on("keyup", function() {
textElements.call(highlight, this.value, svg);
//alternatively: highlight(textElements,this.value);
})
tspan {
fill: orange;
stroke: orange;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.0.0/d3.min.js"></script>
<div><input type="text" id="text"/></div>
<svg width="500" height="250"></svg>
Highlight multiple phrases/words with one highlighting style:
If we want to highlight multiple phrases with the same color, we can modify the regex:
let ex = new RegExp(words.join("|"),"gi");
selection.each(function() {
this.innerHTML = this.textContent.replace(ex, "ig"),(w)=>"<tspan>"+w+"</tspan>")
})
var svg = d3.select("svg");
var data = [
"You can't direct the wind, but you can adjust your sails",
"If you chase two rabbits, you will lose them both.",
"If you speak the truth, have a foot in the stirrup.",
"One doesn't discover new lands without losing sight of the shore.",
"The whole is more than the sum of its parts.",
]
var textElements = svg.selectAll(null)
.data(data)
.join("text")
.text(d=>d)
.attr("x",20)
.attr("y",(d,i) => i* 30 + 30);
function highlight(selection,words,rectContainer) {
let ex = new RegExp(words.join("|"),"gi");
selection.each(function() {
this.innerHTML = this.textContent.replace(ex,(w)=>"<tspan>"+w+"</tspan>")
})
// join, color, positoin the rectangles:
rectContainer.selectAll(".highlight")
.data(selection.selectAll("tspan").nodes())
.join("rect")
.attr("class","highlight")
.datum(d=>d.getBBox())
.attr("x", d=>d.x)
.attr("y", d=>d.y)
.attr("width", d=>d.width)
.attr("height", d=>d.height)
.attr("fill","yellow")
.lower();
}
d3.selectAll("input").on("keyup", function() {
var words = d3.selectAll("input").nodes().map(function(n) { return n.value; });
textElements.call(highlight, words, svg);
})
d3.select("input").dispatch("keyup");
tspan {
fill: orange;
stroke: orange;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.0.0/d3.min.js"></script>
<div><input type="text" value="you" id="text1"/></div>
<div><input type="text" value="two" id="text2"/></div>
<svg width="500" height="250"></svg>
Highlight multiple phrases/words with different styles for each:
The challenge is a bit more difficult when dealing with multiple tspans and rectangles for different phrases. For this we need to modify the highlight function to match multiple strings, to allow flexibility we'll avoid hard coded classes as well. This requires a bit better data management than before, so there a few changes to the highlighting function to accommodate this.
To do accomplish this task we'll iterate through the list of words provided and assign them colors based on specified arrays of colors for rectangle fill and text fill. We'll add tspans and use both the content of the tspan and the bbox of the tspan as data to pass forward to the join used to add the rectangles.
For both rect and text we'll use the highlight function to give elements fills directly, eg:
function highlight(selection,words,rectContainer) {
// create a pool of colors available:
let textColors = ["crimson","steelblue"];
let rectColors = ["yellow","#ccc","orange","#eee"];
// assign colors to words:
let colors = {}
words.forEach((w,i)=>{
colors[w.toLowerCase()] = {
text: textColors[i%textColors.length],
rect: rectColors[i%rectColors.length]
}
})
// create a regex experssion:
let ex = new RegExp(words.join("|"),"gi");
// Create the tspans:
selection.each(function() {
this.innerHTML = this.textContent.replace(ex,(w)=>"<tspan>"+w+"</tspan>")
})
// Select the tspans, bind data to them, color them:
let tspans = selection.selectAll("tspan")
.datum((d,i,n)=>{
return {word:n[i].textContent.toLowerCase()}
})
.attr("fill", d=>colors[d.word].text)
.each((d,i,n)=>{ d.bbox = n[i].getBBox() })
// Conduct a join of rectangles, color them, place them:
rectContainer.selectAll(".highlight")
.data(tspans.data())
.join("rect")
.attr("class","highlight")
.attr("x", d=>d.bbox.x)
.attr("y", d=>d.bbox.y)
.attr("width", d=>d.bbox.width)
.attr("height", d=>d.bbox.height)
.attr("fill", d=>colors[d.word].rect)
.lower();
}
var svg = d3.select("svg");
var data = [
"You can't direct the wind, but you can adjust your sails",
"If you chase two rabbits, you will lose them both.",
"If you speak the truth, have a foot in the stirrup.",
"One doesn't discover new lands without losing sight of the shore.",
"The whole is more than the sum of its parts."
]
var textElements = svg.selectAll(null)
.data(data)
.join("text")
.text(d=>d)
.attr("x",20)
.attr("y",(d,i) => i* 30 + 30);
function highlight(selection,words,rectContainer) {
// create a pool of colors available:
let textColors = ["crimson","steelblue"];
let rectColors = ["yellow","#ccc","orange","#eee"];
// assign colors to words:
let colors = {}
words.forEach((w,i)=>{
colors[w.toLowerCase()] = {
text: textColors[i%textColors.length],
rect: rectColors[i%rectColors.length]
}
})
// create a regex experssion:
let ex = new RegExp(words.join("|"),"gi");
// Create the tspans:
selection.each(function() {
this.innerHTML = this.textContent.replace(ex,(w)=>"<tspan>"+w+"</tspan>")
})
// Select the tspans:
let tspans = selection.selectAll("tspan")
.datum((d,i,n)=>{
return {word:n[i].textContent.toLowerCase()}
})
.attr("fill", d=>colors[d.word].text)
.each((d,i,n)=>{ d.bbox = n[i].getBBox() })
// Conduct a join of rectangles:
rectContainer.selectAll(".highlight")
.data(tspans.data())
.join("rect")
.attr("class","highlight")
.attr("x", d=>d.bbox.x)
.attr("y", d=>d.bbox.y)
.attr("width", d=>d.bbox.width)
.attr("height", d=>d.bbox.height)
.attr("fill", d=>colors[d.word].rect)
.lower();
}
// cycle through some words:
let wordlist = [
["you","the","can"],
["stirrup","chase","discover","whole"],
["if"]
]
let i = 0;
highlight(textElements,wordlist[i++%3],svg)
setInterval(function(){
highlight(textElements,wordlist[i++%3],svg) },
1000);
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.0.0/d3.min.js"></script>
<svg width="500" height="250"></svg>
The above should produce:
I've been using the sample code from this d3 project to learn how to display d3 graphs and I can't seem to get text to show up in the middle of the circles (similar to this example and this example). I've looked at other examples and have tried adding
node.append("title").text("Node Name To Display")
and
node.append("text")
.attr("text-anchor", "middle")
.attr("dy", ".3em").text("Node Name To Display")
right after node is defined but the only results I see is "Node Name To Display" is showing up when I hover over each node. It's not showing up as text inside the circle. Do I have to write my own svg text object and determine the coordinates of that it needs to be placed at based on the coordinates of radius of the circle? From the other two examples, it would seem like d3 already takes cares of this somehow. I just don't know the right attribute to call/set.
There are lots of examples showing how to add labels to graph and tree visualizations, but I'd probably start with this one as the simplest:
http://bl.ocks.org/950642
You haven’t posted a link to your code, but I'm guessing that node refers to a selection of SVG circle elements. You can’t add text elements to circle elements because circle elements are not containers; adding a text element to a circle will be ignored.
Typically you use a G element to group a circle element (or an image element, as above) and a text element for each node. The resulting structure looks like this:
<g class="node" transform="translate(130,492)">
<circle r="4.5"/>
<text dx="12" dy=".35em">Gavroche</text>
</g>
Use a data-join to create the G elements for each node, and then use selection.append to add a circle and a text element for each. Something like this:
var node = svg.selectAll(".node")
.data(nodes)
.enter().append("g")
.attr("class", "node")
.call(force.drag);
node.append("circle")
.attr("r", 4.5);
node.append("text")
.attr("dx", 12)
.attr("dy", ".35em")
.text(function(d) { return d.name });
One downside of this approach is that you may want the labels to be drawn on top of the circles. Since SVG does not yet support z-index, elements are drawn in document order; so, the above approach causes a label to be drawn above its circle, but it may be drawn under other circles. You can fix this by using two data-joins and creating separate groups for circles and labels, like so:
<g class="nodes">
<circle transform="translate(130,492)" r="4.5"/>
<circle transform="translate(110,249)" r="4.5"/>
…
</g>
<g class="labels">
<text transform="translate(130,492)" dx="12" dy=".35em">Gavroche</text>
<text transform="translate(110,249)" dx="12" dy=".35em">Valjean</text>
…
</g>
And the corresponding JavaScript:
var circle = svg.append("g")
.attr("class", "nodes")
.selectAll("circle")
.data(nodes)
.enter().append("circle")
.attr("r", 4.5)
.call(force.drag);
var text = svg.append("g")
.attr("class", "labels")
.selectAll("text")
.data(nodes)
.enter().append("text")
.attr("dx", 12)
.attr("dy", ".35em")
.text(function(d) { return d.name });
This technique is used in the Mobile Patent Suits example (with an additional text element used to create a white shadow).
I found this guide very useful in trying to accomplish something similar :
https://www.dashingd3js.com/svg-text-element
Based on above link this code will generate circle labels :
<!DOCTYPE html>
<html>
<head>
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
<script src="http://d3js.org/d3.v3.min.js" charset="utf-8"></script>
</head>
<body style="overflow: hidden;">
<div id="canvas" style="overflow: hidden;"></div>
<script type="text/javascript">
var graph = {
"nodes": [
{name: "1", "group": 1, x: 100, y: 90, r: 10 , connected : "2"},
{name: "2", "group": 1, x: 200, y: 50, r: 15, connected : "1"},
{name: "3", "group": 2, x: 200, y: 130, r: 25, connected : "1"}
]
}
$( document ).ready(function() {
var width = 2000;
var height = 2000;
var svg = d3.select("#canvas").append("svg")
.attr("width", width)
.attr("height", height)
.append("g");
var lines = svg.attr("class", "line")
.selectAll("line").data(graph.nodes)
.enter().append("line")
.style("stroke", "gray") // <<<<< Add a color
.attr("x1", function (d, i) {
return d.x
})
.attr("y1", function (d) {
return d.y
})
.attr("x2", function (d) {
return findAttribute(d.connected).x
})
.attr("y2", function (d) {
return findAttribute(d.connected).y
})
var circles = svg.selectAll("circle")
.data(graph.nodes)
.enter().append("circle")
.style("stroke", "gray")
.style("fill", "white")
.attr("r", function (d, i) {
return d.r
})
.attr("cx", function (d, i) {
return d.x
})
.attr("cy", function (d, i) {
return d.y
});
var text = svg.selectAll("text")
.data(graph.nodes)
.enter()
.append("text");
var textLabels = text
.attr("x", function(d) { return d.x; })
.attr("y", function(d) { return d.y; })
.text( function (d) { return d.name })
.attr("font-family", "sans-serif")
.attr("font-size", "10px")
.attr("fill", "red");
});
function findAttribute(name) {
for (var i = 0, len = graph.nodes.length; i < len; i++) {
if (graph.nodes[i].name === name)
return graph.nodes[i]; // Return as soon as the object is found
}
return null; // The object was not found
}
</script>
</body>
</html>
If you want to grow the nodes to fit large labels, you can use the getBBox property of an SVG text node after you've drawn it. Here's how I did it, for a list of nodes with fixed coordinates, and two possible shapes:
nodes.forEach(function(v) {
var nd;
var cx = v.coord[0];
var cy = v.coord[1];
switch (v.shape) {
case "circle":
nd = svg.append("circle");
break;
case "rectangle":
nd = svg.append("rect");
break;
}
var w = 10;
var h = 10;
if (v.label != "") {
var lText = svg.append("text");
lText.attr("x", cx)
.attr("y", cy + 5)
.attr("class", "labelText")
.text(v.label);
var bbox = lText.node().getBBox();
w = Math.max(w,bbox.width);
h = Math.max(h,bbox.height);
}
var pad = 4;
switch (v.shape) {
case "circle":
nd.attr("cx", cx)
.attr("cy", cy)
.attr("r", Math.sqrt(w*w + h*h)/2 + pad);
break;
case "rectangle":
nd.attr("x", cx - w/2 - pad)
.attr("y", cy - h/2 - pad)
.attr("width", w + 2*pad)
.attr("height", h + 2*pad);
break;
}
});
Note that the shape is added, the text is added, then the shape is positioned, in order to get the text to show on top.
I've been using the sample code from this d3 project to learn how to display d3 graphs and I can't seem to get text to show up in the middle of the circles (similar to this example and this example). I've looked at other examples and have tried adding
node.append("title").text("Node Name To Display")
and
node.append("text")
.attr("text-anchor", "middle")
.attr("dy", ".3em").text("Node Name To Display")
right after node is defined but the only results I see is "Node Name To Display" is showing up when I hover over each node. It's not showing up as text inside the circle. Do I have to write my own svg text object and determine the coordinates of that it needs to be placed at based on the coordinates of radius of the circle? From the other two examples, it would seem like d3 already takes cares of this somehow. I just don't know the right attribute to call/set.
There are lots of examples showing how to add labels to graph and tree visualizations, but I'd probably start with this one as the simplest:
http://bl.ocks.org/950642
You haven’t posted a link to your code, but I'm guessing that node refers to a selection of SVG circle elements. You can’t add text elements to circle elements because circle elements are not containers; adding a text element to a circle will be ignored.
Typically you use a G element to group a circle element (or an image element, as above) and a text element for each node. The resulting structure looks like this:
<g class="node" transform="translate(130,492)">
<circle r="4.5"/>
<text dx="12" dy=".35em">Gavroche</text>
</g>
Use a data-join to create the G elements for each node, and then use selection.append to add a circle and a text element for each. Something like this:
var node = svg.selectAll(".node")
.data(nodes)
.enter().append("g")
.attr("class", "node")
.call(force.drag);
node.append("circle")
.attr("r", 4.5);
node.append("text")
.attr("dx", 12)
.attr("dy", ".35em")
.text(function(d) { return d.name });
One downside of this approach is that you may want the labels to be drawn on top of the circles. Since SVG does not yet support z-index, elements are drawn in document order; so, the above approach causes a label to be drawn above its circle, but it may be drawn under other circles. You can fix this by using two data-joins and creating separate groups for circles and labels, like so:
<g class="nodes">
<circle transform="translate(130,492)" r="4.5"/>
<circle transform="translate(110,249)" r="4.5"/>
…
</g>
<g class="labels">
<text transform="translate(130,492)" dx="12" dy=".35em">Gavroche</text>
<text transform="translate(110,249)" dx="12" dy=".35em">Valjean</text>
…
</g>
And the corresponding JavaScript:
var circle = svg.append("g")
.attr("class", "nodes")
.selectAll("circle")
.data(nodes)
.enter().append("circle")
.attr("r", 4.5)
.call(force.drag);
var text = svg.append("g")
.attr("class", "labels")
.selectAll("text")
.data(nodes)
.enter().append("text")
.attr("dx", 12)
.attr("dy", ".35em")
.text(function(d) { return d.name });
This technique is used in the Mobile Patent Suits example (with an additional text element used to create a white shadow).
I found this guide very useful in trying to accomplish something similar :
https://www.dashingd3js.com/svg-text-element
Based on above link this code will generate circle labels :
<!DOCTYPE html>
<html>
<head>
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
<script src="http://d3js.org/d3.v3.min.js" charset="utf-8"></script>
</head>
<body style="overflow: hidden;">
<div id="canvas" style="overflow: hidden;"></div>
<script type="text/javascript">
var graph = {
"nodes": [
{name: "1", "group": 1, x: 100, y: 90, r: 10 , connected : "2"},
{name: "2", "group": 1, x: 200, y: 50, r: 15, connected : "1"},
{name: "3", "group": 2, x: 200, y: 130, r: 25, connected : "1"}
]
}
$( document ).ready(function() {
var width = 2000;
var height = 2000;
var svg = d3.select("#canvas").append("svg")
.attr("width", width)
.attr("height", height)
.append("g");
var lines = svg.attr("class", "line")
.selectAll("line").data(graph.nodes)
.enter().append("line")
.style("stroke", "gray") // <<<<< Add a color
.attr("x1", function (d, i) {
return d.x
})
.attr("y1", function (d) {
return d.y
})
.attr("x2", function (d) {
return findAttribute(d.connected).x
})
.attr("y2", function (d) {
return findAttribute(d.connected).y
})
var circles = svg.selectAll("circle")
.data(graph.nodes)
.enter().append("circle")
.style("stroke", "gray")
.style("fill", "white")
.attr("r", function (d, i) {
return d.r
})
.attr("cx", function (d, i) {
return d.x
})
.attr("cy", function (d, i) {
return d.y
});
var text = svg.selectAll("text")
.data(graph.nodes)
.enter()
.append("text");
var textLabels = text
.attr("x", function(d) { return d.x; })
.attr("y", function(d) { return d.y; })
.text( function (d) { return d.name })
.attr("font-family", "sans-serif")
.attr("font-size", "10px")
.attr("fill", "red");
});
function findAttribute(name) {
for (var i = 0, len = graph.nodes.length; i < len; i++) {
if (graph.nodes[i].name === name)
return graph.nodes[i]; // Return as soon as the object is found
}
return null; // The object was not found
}
</script>
</body>
</html>
If you want to grow the nodes to fit large labels, you can use the getBBox property of an SVG text node after you've drawn it. Here's how I did it, for a list of nodes with fixed coordinates, and two possible shapes:
nodes.forEach(function(v) {
var nd;
var cx = v.coord[0];
var cy = v.coord[1];
switch (v.shape) {
case "circle":
nd = svg.append("circle");
break;
case "rectangle":
nd = svg.append("rect");
break;
}
var w = 10;
var h = 10;
if (v.label != "") {
var lText = svg.append("text");
lText.attr("x", cx)
.attr("y", cy + 5)
.attr("class", "labelText")
.text(v.label);
var bbox = lText.node().getBBox();
w = Math.max(w,bbox.width);
h = Math.max(h,bbox.height);
}
var pad = 4;
switch (v.shape) {
case "circle":
nd.attr("cx", cx)
.attr("cy", cy)
.attr("r", Math.sqrt(w*w + h*h)/2 + pad);
break;
case "rectangle":
nd.attr("x", cx - w/2 - pad)
.attr("y", cy - h/2 - pad)
.attr("width", w + 2*pad)
.attr("height", h + 2*pad);
break;
}
});
Note that the shape is added, the text is added, then the shape is positioned, in order to get the text to show on top.
Hey I'm using D3JS as a chart library and I'm really interested in exploiting the cool features in the Bubble Chart. On the main D3JS chart site the following Bubble Chart is used to compare two sets of data:
Bubble Chart.
I was wondering if anybody actually had any idea of how to create a bubble chart like this, I'm struggling to get it work past using a size variable.
I just really want to be able to compare two sets of data for example:
Hostnames (45,955,158) VS Active Sites (21,335,629)
The code I am using is below, I use JSON to retrieve my data, I'm a major newbie when it comes to js and even more so this jQuery library so would appreciate any help.
index.html
<div class="four columns browserstats2003">
<h3>Browser Stats 2003</h3>
</div>
<div class="four columns mobilephonestats">
<h3>Smartphone Sales 2003</h3>
<p>The first smartphone had not been released in 2003.</p>
<div id=""></div>
</div>
mobile.json
{
"name": "flare",
"children": [
{
"name": "analytics",
"children": [
{
"name": "cluster",
"children": [
{"name": "Smartphone Sales", "size": 11111},
{"name": "Smartphone Salesa", "size": 2111}
]
}
]
}
]
}
js/js.js
// JavaScript Document
$(document).ready(function () {
// 2003 bubble chart
var diameter = 360,
format = d3.format(",d"),
color = d3.scale.category20c();
var bubble = d3.layout.pack()
.sort(null)
.size([diameter, diameter])
.padding(1.5);
var svg = d3.select(".mobilephonestats").append("svg")
.attr("width", diameter)
.attr("height", diameter)
.attr("class", "bubble");
d3.json("mobile.json", function(error, root) {
var node = svg.selectAll(".node")
.data(bubble.nodes(classes(root))
.filter(function(d) { return !d.children; }))
.enter().append("g")
.attr("class", "node")
.attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; });
node.append("title")
.text(function(d) { return d.className + ": " + format(d.value); });
node.append("circle")
.attr("r", function(d) { return d.r; })
.style("fill", function(d) { return color(d.packageName); });
node.append("text")
.attr("dy", ".3em")
.style("text-anchor", "middle")
.text(function(d) { return d.className.substring(0, d.r / 3); });
});
// Returns a flattened hierarchy containing all leaf nodes under the root.
function classes(root) {
var classes = [];
function recurse(name, node) {
if (node.children) node.children.forEach(function(child) { recurse(node.name, child); });
else classes.push({packageName: name, className: node.name, value: node.size});
}
recurse(null, root);
return {children: classes};
}
d3.select(self.frameElement).style("height", diameter + "px");
// end bubble year
});
In the example you provided, he's definitely using a force layout, which is a bit more complicated that the Bubble Chart you're using. You'll have to take into consideration things like collisions and animation.
Why not just have a look at the JavaScript he used to generate it.
Jim Vallandingham wrote an extensive tutorial on Bubble Clouds, which should get you started.
To create the split in the middle of the cirlce as a way of doing some form of data comparison, "clip-paths" are the way forward:
Append two circles, one for each data-set.
Append two clip-path, one for each data-set
Append a rectange to each clip-path.
Set the rectangle x attributes and width, which defines the position of the split in the middle. This has to be a function of the data.
Crop the rectangles and circles
Here's the code:
var nodeEnter = node.enter().append("a")
.attr("class", "g-node")
.call(force.drag);
var democratEnter = nodeEnter.append("g")
.attr("class", "g-democrat");
democratEnter.append("clipPath") // clip-path to crop the rectangle
.attr("id", function(d) { return "g-clip-democrat-" + d.id; })
.append("rect");
democratEnter.append("circle");
var republicanEnter = nodeEnter.append("g")
.attr("class", "g-republican");
republicanEnter.append("clipPath") // Clip-path to crop the rectangle
.attr("id", function(d) { return "g-clip-republican-" + d.id; })
.append("rect");
republicanEnter.append("circle");
node.selectAll("rect")
.attr("y", function(d) { return -d.r - clipPadding; })
.attr("height", function(d) { return 2 * d.r + 2 * clipPadding; });
// Defining the x-attr and width of the rectangle, which effectively splits the circle
node.select(".g-democrat rect")
.attr("x", function(d) { return -d.r - clipPadding; })
.attr("width", function(d) { return 2 * d.r * d.k + clipPadding; });
node.select(".g-republican rect")
.attr("x", function(d) { return -d.r + 2 * d.r * d.k; })
.attr("width", function(d) { return 2 * d.r; });
// Setting the clip-path to crop the circles
node.select(".g-democrat circle")
.attr("clip-path", function(d) { return d.k < 1 ? "url(#g-clip-democrat-" + d.id + ")" : null; });
node.select(".g-republican circle")
.attr("clip-path", function(d) { return d.k > 0 ? "url(#g-clip-republican-" + d.id + ")" : null; });
This should generate something like this:
<g class="g-democrat">
<clipPath id="g-clip-democrat-43">
<rect y="-63.36487389363757" height="126.72974778727514" x="-63.36487389363757" width="59.449375597303515">
</rect>
</clipPath>
<circle clip-path="url(#g-clip-democrat-43)" r="59.36487389363757">
</circle></g>
<g class="g-republican">
<clipPath id="g-clip-republican-43">
<rect y="-63.36487389363757" height="126.72974778727514" x="-3.915498296334057" width="118.72974778727514">
</rect>
</clipPath>
<circle clip-path="url(#g-clip-republican-43)" r="59.36487389363757">
</circle></g>
I'm trying to do a multi-series line chart like the example here http://bl.ocks.org/mbostock/3884955
the only difference is i'm trying to do a transition between two sets of data. The transition works absolutely fine but I have no idea how to begin to go about getting the series labels to align with each line as is the case in that example.
Does anyone have any thoughts about how to go about it? My code is set up in a slightly different manner than the above example so I'm not able to follow it strictly. But even when I do, the data labels dont come through...
Here's my code:
<!DOCTYPE html>
<meta charset="utf-8">
<style>
body {
font: 10px serif;
}
.axis path,
.axis line {
fill: none;
stroke: #000;
shape-rendering: crispEdges;
}
.x.axis path {
fill: none;
stroke: #000;
shape-rendering: crispEdges;
}
.line {
fill: none;
stroke-width: 1.5px;
}
</style>
<body>
<script src="http://d3js.org/d3.v3.js"></script>
<script>
var width = 550;
var height = 600;
var padding=60;
var parseDate = d3.time.format("%Y").parse;
var x = d3.time.scale().range([padding, width-padding]);
var y = d3.scale.linear().range([height-padding, padding]);
var z = d3.scale.category10();
var xAxis = d3.svg.axis().scale(x).orient("bottom");
var yAxis = d3.svg.axis().scale(y).orient("left");
var svg = d3.select("body").append("svg").attr("width", width).attr("height", height)
//load CSV
d3.csv("diverge-nonscaled1950.csv", function(error, data) {
data.forEach(function(d) {
d.date = parseDate(d.date);
d.India=+d.India;
d.China=+d.China;
});
//compute column names
var seriesNames = d3.keys(data[0]).filter(function(d) { return d !== "date"; })
.sort();
//this is dataset one: data is scaled to column name INdia
var series = seriesNames.map(function(series) {
return data.map(function(d) {
return {x: d.date, y: ((+d[series]/d.India)*100)};
});
});
//this is dataset two: data is scaled to column name China
var series1 = seriesNames.map(function(series1) {
return data.map(function(d) {
return {x: d.date, y: ((+d[series1]/d.China)*100)};
});
});
var line = d3.svg.line()
.interpolate("basis")
// .defined(function(d) { return d.country>0; })
.x(function(d) { return x(d.x); })
.y(function(d) { return y(d.y); });
//domains
x.domain(d3.extent(data, function(d) { return d.date; }));
y.domain([0,2300]);
z.domain(seriesNames)
//axes
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + (height-padding) + ")")
.call(xAxis);
svg.append("g")
.attr("class", "y axis")
.attr("transform", "translate(" + padding + ",0)")
.call(yAxis)
.append("text")
.attr("transform", "rotate(-90)")
.attr("x",0-(height/2.5))
.attr("y", -50)
.attr("dy", ".71em")
.style("text-anchor", "end")
.style("font-size",11)
.text("Relative per Capita Incomes");;
//draw path
svg.selectAll(".line")
.data(series)
.enter().append("path")
.attr("class", "line")
.attr("d", line)
.attr("stroke",function(d, i) { return z(i); })
//first transition from dataset one to two
d3.select("svg")
.on("click",file1);
function file1(){
svg.selectAll(".line").data(series1)
.transition().duration(750)
.attr("d", line)
.attr("stroke",function(d, i) { return z(i); })
d3.select("svg")
.on("click",file2);
}
//second transition back to one
function file2(){
svg.selectAll(".line").data(series)
.transition().duration(750)
.attr("d", line)
.attr("stroke",function(d, i) { return z(i); })
d3.select("svg")
.on("click",file1);
}
});
</script>
</body>
Thanks very much for any help whatsoever...
Avin
I had trouble with this a while ago as well. As in the example you posted, you have to use d3's selection.datum() method, which snags data for individual elements. You will then select the final data point in each series using thing[thing.length - 1] and use the x and y values of that point to perform a transform/translate on your text. The relevant part of the code is:
city.append("text")
.datum(function(d) { return {name: d.name, value: d.values[d.values.length - 1]}; })
.attr("transform", function(d) {
return "translate(" + x(d.value.date) + "," + y(d.value.temperature) + ")";
})
Then the animation is just a matter of updating the transformation, with something like:
city.selectAll('text').transition()
.attr("transform", function(d) {
return "translate(" + x(d.value.date) + "," + y(d.value.temperature) + ")";
})
You can also view a chart I made a while ago that seems to be similar to what you're trying to do.