I'm drawing out text labels to my svg in multiple lines. The solution I have is working and ok, but it has a limitation that it will not draw any more lines than what I hard-code, and also there is a bit of inefficient processing. Is there a better way to make this happen, without having to re-parse the name string every time, and appending the tspan just the right amount of times?
node.append("text")
.attr("id", function(d){ return "contact-node-label-"+d.id })
.style("text-anchor", "middle")
.attr("dy", function(d)
{
// split name by space and -
var n = d.name.replace("-","- ").split(" ") // this expression is repeated
return n.length/3-(n.length-1)*0.9+'em'
})
.text(function(d)
{
var n = d.name.replace("-","- ").split(" ")
// return first part of name
return n[0]
})
// some kind of loop would start here
.append("tspan").attr('x',0).attr('dy','1em').text(function(d)
{
var n = d.name.replace("-","- ").split(" ");
if (n.length > 1) return n[1];
})
// second round of loop would be this
.append("tspan").attr('x',0).attr('dy','1em').text(function(d)
{
var n = d.name.replace("-","- ").split(" ");
if (n.length > 2) return n[2];
})
Perhaps I could use the following code. The problem is that n is created (and recreated) inside the method, and if I save it outside it will reference the wrong data. The solution would be to be able to put this code inside one of the methods, but I couldn't make it work (neither in the text method, nor in the tspan append method):
d3.select(this).append("tspan").attr('x',0).attr('dy','1em').text( n[i] )
Looks like a job for .each:
node.append("text")
.each(function(d) {
// split name by space and -
var n = d.name.replace("-","- ").split(" ");
// get the current element
var text = d3.select(this)
.attr("dy", n.length / 3 - (n.length-1) * 0.9 + 'em')
.text(n[0]);
// now loop
for (var i = 1; i < n.length; i++) {
text.append("tspan")
.attr('x', 0)
.attr('dy', '1em')
.text(n[i])
}
});
One big advantage of .each, as shown here, is that it gives you a per-element scope to work with, making it easy to avoid repeated calculations like this.
Related
I've been playing around with this example here for a little while. What I'm trying to do is highlight a single node/circle in the plot (by making it larger with a border; later I want to add text or a letter inside it too).
Currently, I've made the circle for Bhutan larger in the plot like the following:
.attr("r",
function(d){return ( d.countryName === "Bhutan" ? r + 4 : r);})
.attr("stroke", function(d){if (d.countryName==="Bhutan"){return "black"}})
However, it overlaps with the other circles. What would be the best approach to avoid these collisions/overlaps? Thanks in advance.
Link to Plunkr - https://plnkr.co/edit/rG6X07Kzkg9LeVVuL0PH?p=preview
I tried the following to add a letter inside the bhutan circle
//find bhutan circle and add a "B" to it
countriesCircles
.data(data)
.enter().append("text")
.filter(function(d) { return d.countryName === "Bhutan"; })
.text("B");
Updated Plunkr - https://plnkr.co/edit/Bza5AMxqUr2HW9CYdpC6?p=preview
This is a slightly different problem than in this question here: How to change the size of dots in beeswarm plots in D3.js
You have a few options that I can think of:
Set the forceCollide to be your largest possible radius * 1.33, e.g. (r + 4) * 1.33. This will prevent overlapping, but spread things out a lot and doesn't look that great.
Add the radius property to each entry in your array and make the collide work based off that, which will look a bit better but not perform as awesomely for large sets.
Here's an example of how to do that:
...
d3.csv("co2bee.csv", function(d) {
if (d.countryName === "Bhutan") {
d.r = r + 4;
} else {
d.r = r;
}
return d;
}, function(error, data) {
if (error) throw error;
var dataSet = data;
...
var simulation = d3.forceSimulation(dataSet)
...
.force("collide", d3.forceCollide(function(d) { return d.r * 1.33; }))
...
countriesCircles.enter()
.append("circle")
.attr("class", "countries")
.attr("cx", 0)
.attr("cy", (h / 2)-padding[2]/2)
.attr("r", function(d){ return d.r; })
....
Use the row function in d3.csv to add a property to each member of the array called r, and check the country name to determine which one gets the larger value. Then use that value wherever you need to mess with the radius.
I guess it would've been possible to check the country name everywhere the radius was impacted (e.g. .force("collide", d3.forceCollide(function(d) { return d.countryName === "Bhutan" ? (r + 4) * 1.33 : r * 1.33; }), etc.). This feels a bit cleaner to me, but it might be cleaner still by abstracting out the radius from the data entries themselves...
Forked your plunk here: https://plnkr.co/edit/Tet1DVvHtC7mHz91eAYW?p=preview
Now i got this second problem using d3.js that i cannot solve by my own. I got a dynamic array "path" which length always changes when i click on. Then i got a "text" variable at a starting position on the svg (112, 490).
With the help of you guys, i now use the for-loop to show the names of the "path" array based on it's switching length on the console and it worked. But now i want the names to appear on the screen. But my
textNode.attr("dx", 112 + i*2);
line does not work. I want the text shift right on the x-scale starting at the point 112 a little bit with each node.
This is the text
var text = svg.append("text")
.attr("dx", 112)
.attr("dy", 490)
.text("1. Node: " )
and this is the for loop
for (var i=0;i<path.length;i++) {
text.attr("dx", 112 + i*2);
text.text( +i+1+". Node: " + path[i].name);
A very basic principle in D3: don't use loops, such as a for loop, to show or display the data (sometimes we do use loops, but in very specific and complex situations, not this one). In your other question, the proposed solution used loops because there was no d3.js tag in the question. But using a loop like this makes little sense if you're using D3. It's an entire library created to manipulate data, and you're ignoring its most important aspect.
Instead of that, bind your data to a selection. In your case, as your array is constantly changing, you're gonna need an "enter", "update" and "exit" selections.
First, bind your data:
var texts = svg.selectAll(".texts")
.data(data);
Then, set the selections:
textsExit = texts.exit().remove();
textsEnter = texts.enter()
.append("text")
.attr("class", "texts");
textsUpdate = texts.merge(textsEnter)
.attr("x", 10)
.attr("y", (d, i) => i * 16)
.text(d => d.name);
Here is a demo to show you how it works. I have an data array, which length changes every second:
var svg = d3.select("body").append("svg");
var dataset = [{
name: "foo"
}, {
name: "bar"
}, {
name: "baz"
}, {
name: "lorem"
}, {
name: "ipsum"
}, {
name: "dolot"
}, {
name: "amet"
}];
print(dataset);
setInterval(()=> {
var data = dataset.slice(Math.random() * 6);
print(data);
}, 1000);
function print(data) {
var texts = svg.selectAll(".texts")
.data(data);
textsExit = texts.exit().remove();
textsEnter = texts.enter()
.append("text")
.attr("class", "texts");
textsUpdate = texts.merge(textsEnter).attr("x", 10)
.attr("y", (d, i) => 20 + i * 16)
.text((d,i) => "Node " + (i+1) + ", name: " + d.name);
}
<script src="https://d3js.org/d3.v4.min.js"></script>
Not sure if this is incorrect for your specific use case, but I think you want to do "x" instead of "dx". That's how I've moved objects to the left / right in d3.js:
textNode.attr("x", 112 + i*2);
EDIT: here's an example that adds text objects and moves them to the right based on the index:
for (var i = 0; i < path.length; i++) {
var text = svg.append("text")
.attr("x", 112 + i*2)
.attr("y", 490)
.text("1. Node: " )
}
EDIT2: See Gerardo's answer. D3js has its own methods for binding and looping through data.
I want to reimplement the following Processing sketch with d3js.
Recursion Thing
This wonderful sketch recursively builds up the graph, using a complex pushMatrix, popMatrix hierarchy.
How could this be implemented in d3.js as we there always work on the DOM immediately when appending a shape or transformation. But in a logic like in this sketch the appending part seems to have to be held back for the respective popMatrix to come. It feels like I have to implement my own transform and shape stack to temporary remember the transformation and shapes to be added until the popMatrix comes but that seems so not d3.js.
Any suggestion highly appreciated
ps:
i dont want to use processing.js as i want to work with svg, not canvas.
Interesting problem! Here's my take on it: http://jsfiddle.net/Y48BL/
This is more a proof of concept; I didn't do all the different colours and such. Nevertheless, it demonstrates the general approach. The general idea is to use g elements instead of the matrices that processing uses. Both are used for local transformations of the coordinate system; in the case of the g elements by setting transform accordingly. New gs (matrices) are created inside the recursive function and then passed on to the next level of the recursion. This would correspond to pushMatrix(). Coming back up, we continue to use the original g, corresponding to popMatrix().
The translation of the drawing of the circles and lines is fairly straightforward -- I find the D3 code much easier to read.
So I came up with this helper "class" to get this done, maybe a bit of a overkill but I will have more use cases for this.
var TransformStack = (function () {
function TransformStack() {
this.stack = [];
}
TransformStack.prototype.getCurrentElement = function () {
return this.stack[this.stack.length - 1];
};
TransformStack.prototype.setCurrentElement = function (element) {
this.stack[this.stack.length - 1] = element;
};
TransformStack.prototype.push = function (transformElement) {
this.stack.push(transformElement);
};
TransformStack.prototype.pushAndTransform = function (transformAttr) {
this.push(this.getCurrentElement().append("g").attr("transform", transformAttr));
};
TransformStack.prototype.transform = function (transformAttr) {
this.setCurrentElement(this.getCurrentElement().append("g").attr("transform", transformAttr));
};
TransformStack.prototype.pop = function () {
return this.stack.pop();
};
return TransformStack;
})();
Basically a stack to push/pop g elements which replaces the matrices approach in processing as Lars already pointed out. With this the main routine looks something like
var svg = d3.select("body").append("svg").attr("width", width).attr("height", height)
.append("g").attr("transform", "translate(" + width / 2 + "," + height / 2 + ")scale(" + gScale + ")");
var tstack = new TransformStack();
tstack.push(svg);
doIt(nRecursions);
function doIt(n) {
// Circle
tstack.getCurrentElement()
.append("circle")
.attr("r", theSize)
.style("fill", "#fe6b0c")
.style("stroke", "0")
.style("stroke-width", "2")
.style("opacity", 0.3);
if (n != nRecursions) {
for (var i = 0; i < 4; i++) {
tstack.getCurrentElement().append("line")
.style("stroke", "red")
.style("opacity", 0.16)
.attr("x1", Math.random() * 4 - 2)
.attr("x2", Math.random() * 4 - 2)
.attr("y1", theSize / 2.0 + Math.random() * 4 - 2)
.attr("y2", distance - (theSize * theScale / 2.0) - 8.0 + Math.random() * 4 - 2);
}
}
var rot = 0;
tstack.pushAndTransform("scale(" + theScale + ")");
for (var i = 0; i < n; i++) {
if (n > 0) {
tstack.pushAndTransform("translate(0," + distance + ")");
doIt(n - 1);
tstack.pop();
rot = 360 / n;
tstack.transform('rotate(' + rot + ')');
}
}
tstack.pop();
}
}
Just wanted to share this, maybe of some use for some. The main point was given by Lars already.
I am starting with d3.js, and am trying to create a network graph each circle of which contains a label.
What I want is a line break an svg text.
What I am trying to do is to break the text into multiple <tspan>s, each with x="0" and variable "y" to simulate actual lines of text. The code I have written gives some unexpected result.
var text = svg.selectAll("text").data(force.nodes()).enter().append("text");
text
.text(function (d) {
arr = d.name.split(" ");
var arr = d.name.split(" ");
if (arr != undefined) {
for (i = 0; i < arr.length; i++) {
text.append("tspan")
.text(arr[i])
.attr("class", "tspan" + i);
}
}
});
In this code am splitting the text string by white space and appending the each splitted string to tspan. But the text belonging to other circle is also showing in each circle. How to overcome this issue?
Here is a JSFIDDLE http://jsfiddle.net/xhNXS/ with only svg text
Here is a JSFIDDLE http://jsfiddle.net/2NJ25/16/ showing my problem with tspan.
You need to specify the position (or offset) of each tspan element to give the impression of a line break -- they are really just text containers that you can position arbitrarily. This is going to be much easier if you wrap the text elements in g elements because then you can specify "absolute" coordinates (i.e. x and y) for the elements within. This will make moving the tspan elements to the start of the line easier.
The main code to add the elements would look like this.
text.append("text")
.each(function (d) {
var arr = d.name.split(" ");
for (i = 0; i < arr.length; i++) {
d3.select(this).append("tspan")
.text(arr[i])
.attr("dy", i ? "1.2em" : 0)
.attr("x", 0)
.attr("text-anchor", "middle")
.attr("class", "tspan" + i);
}
});
I'm using .each(), which will call the function for each element and not expect a return value instead of the .text() you were using. The dy setting designates the line height and x set to 0 means that every new line will start at the beginning of the block.
Modified jsfiddle here, along with some other minor cleanups.
I am trying to complete the last bit of a d3 project which dynamically creates these blue arcs, over which I need to place arc text, as shown in this image:
The image above is something I've done by placing the arc text statically, through trial and error, but I want to place it dynamically, based on the blue arcs which sit beneath the text. This is the code that dynamically creates the arcs:
var groupData = data_group.selectAll("g.group")
.data(nodes.filter(function(d) { console.log(d.__data__.key); return (d.key=='Employers' ||{exp:channel:entries category="13" backspace="2"} d.key == '{url_title}' ||{/exp:channel:entries}) && d.children; }))
.enter().append("group")
.attr("class", "group");
arc_group.selectAll("g.arc")
.data(groupData[0])
.enter().append("svg:path")
.attr("d", groupArc)
.attr("class", "groupArc")
.style("fill", "#1f77b4")
.style("fill-opacity", 0.5);
The {exp:} content is preparsed data I'm pulling from my content management system in expression engine if it looks confusing.
So, I have my arcs. Now you'll notice in the groupData code block I have a console.log statement, that will give me the names I want to appear in the arc text:
console.log(d.__data__.key);
Now, the code I was using to place the arc text statically was this:
var arcData = [
{aS: 0, aE: 45,rI:radius - chartConfig.linePadding + chartConfig.arcPadding,rO:radius - chartConfig.linePadding + chartConfig.textPadding-chartConfig.arcPadding}
];
var arcJobsData = d3.svg.arc().innerRadius(arcData[0].rI).outerRadius(arcData[0].rO).startAngle(degToRad(1)).endAngle(degToRad(15));
var g = d3.select(".chart").append("svg:g").attr("class","arcs");
var arcJobs = d3.select(".arcs").append("svg:path").attr("d",arcJobsData).attr("id","arcJobs").attr("class","arc");
g.append("svg:text").attr("x",3).attr("dy",15).append("svg:textPath").attr("xlink:href","#arcJobs").text("JOBS").attr("class","arcText"); //x shifts x pixels from the starting point of the arc. dy shifts the text y units from the top of the arc
And in this above code, the only thing left that I should need to do is dynamically assign an ID to the arcs, and then reference that ID in the xlink:href attribute, as well as replace the text("JOBS") with text that pulls from d.data__key. Given the code above which dynamically creates the arcs, and given that I know how to dynamically create and retrieve the text I want to place in the arcs using d.__data.key, I should be able to finish this thing off, but I can't figure out how write code in d3 that will take the data and place it in the arcs. Can anybody help with this?
You should give this blog post on nested selections a read; I believe it'll explain what you're trying to do.
Here's the gist. When you add data to your selection, assign the selection to a variable:
var g = data_group.selectAll("g.group")
.data(nodes.filter(function(d) { /* stuff */ }));
That way, you can perform subselections on it, which will receive a single element of the data bound to your g selection. You can use this to add your arcs and text:
g.enter().append('group') // Question: Are you sure you mean 'group' here?
.attr('class', 'group')
g.selectAll('g.arc')
.data(function(d, i) { return d; })
enter().append('path')
// Setup the path here
g.selectAll('text')
.data(function(d, i) { return d; })
.enter().append('text')
.attr('text', function(d) { return d.__data__.key })
The functions that are being used to do data binding in the nested selections (i.e., the g.selectAll()s) are being passed a single element of the data attached to g as d, and i is its index.
Figured this out. Changed the structure of things a bit so it made a little more sense, but essentially what I did is this:
var groupData = data_group.selectAll("g.group")
.data(nodes.filter(function(d) { return (d.key=='Employers' ||{exp:channel:entries category="13" backspace="2"} d.key == '{url_title}' ||{/exp:channel:entries}) && d.children; }))
.enter().append("group")
.attr("class", "group"); //MH - why do we need this group - these elements are empty. Shouldn't this just be an array? Find out how to delete the svg elements without getting rid of the data, which is needed below.
var groupArc = d3.svg.arc()
.innerRadius(ry - 177)
.outerRadius(ry - 157)
.startAngle(function(d) { return (findStartAngle(d.__data__.children)-2) * pi / 180;})
.endAngle(function(d) { console.log(d.__data__.key); return (findEndAngle(d.__data__.children)+2) * pi / 180});
var arc_and_text = arc_group.selectAll("g.arc")
.data(groupData[0])
.enter().append("svg:g")
.attr("class","arc_and_text");
var arc_path = arc_and_text.append("svg:path")
.attr("d", groupArc)
.attr("class", "groupArc")
.attr("id", function(d, i) { return "arc" + i; })
.style("fill", "#1f77b4")
.style("fill-opacity", 0.5); //MH: (d.__data__.key) gives names of groupings
var arc_text = arc_and_text.append("text")
.attr("class","arc_text")
.attr("x", 3)
.attr("dy", 15);
arc_text.append("textPath")
.attr("xlink:href", function(d, i) { return "#arc" + i; })
.attr("class","arc_text_path")
.style("fill","#ffffff")
.text(function(d, i) { return d.__data__.key; });
D3 still mystifies me a bit, and I'm sure this code could be much improved, but it works.