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.
Related
I'm trying to avoid repeating code for the enter and update selections in d3. I have successfully used merge to get the right selection of all items that are present with the new data (after I have used append to add the new ones). Now I want to set some attributes such as r, cy and cx. I can do that on the merged selection but if I put them behind a transition the items that are just appearing animate in from 0,0 and I want them to just appear in the right place.
What I'm hoping for is some way to write all the attribute setting lines into one function and one time pass in the enter() selection and the next time pass in the transitioned merge so that I only have to write the lines for setting cx once. Here's what I have already followed by my guess at pseudo code
const valueCircles = this.chartGroup.selectAll('.valueCircle')
.data(values);
valueCircles.enter()
.append('circle')
.attr('stroke-width', '1px')
.attr('stroke', '#ffffff')
.attr('opacity', '1')
.attr('class', 'dots valueCircle')
.merge(valueCircles)
.transition()
.duration(100)
.attr('r', (d) => {
if (d.y < 0.5 ) {
return 5;
} else {
return 15;
}
})
.attr('cx', (d) => {
return timescale(new Date(d.x));
})
.attr('cy', (d) => {
return valueScale(d.y)
})
This is what I think I want to achieve
function setTheDynamicValues(aSelection) {
aSelection
.attr('cx', (d) => {
return timescale(new Date(d.x));
})
.attr('cy', (d) => {
return valueScale(d.y)
})
}
function updateGraph(newData) {
const valueCircles = this.chartGroup.selectAll('.valueCircle')
.data(newData);
setTheDynamicValues(valueCircles.enter());
setTheDynamicValues(valueCircles.transition().duration(100));
}
Another way of describing this would be to make the duration of the transition 0 for the entering elements and 100 for existing elements so that new ones appear correct and existing ones have a visible transition.
First of all, D3 selections are immutable: that merge of yours is not doing anything. This would be the proper pattern:
//here, pay attention to "let" instead of const
let valueCircles = this.chartGroup.selectAll('.valueCircle')
.data(values);
//our enter selection is called "valuesCirclesEnter"
const valueCirclesEnter = valueCircles.enter()
.append('circle')
etc...
//now, you merge the selections
valueCircles = valueCirclesEnter.merge(valueCircles);
From that point on, valueCircles contains both your updating and entering elements.
Now, back to your question:
An idiomatic D3 indeed has a lot of repetition, and that's a point lots of people complain about D3. But we can avoid some repetition with methods like selection.call. Also, since you want a transition for the update selection but none for the enter selection, you can simply drop the merge.
Here is a basic exemple, using a bit of your code:
const data1 = [{
x: 100,
y: 100
}, {
x: 230,
y: 20
}];
const data2 = [{
x: 10,
y: 10
}, {
x: 50,
y: 120
}, {
x: 190,
y: 100
}, {
x: 140,
y: 30
}, {
x: 270,
y: 140
}];
const svg = d3.select("svg");
draw(data1);
setTimeout(() => draw(data2), 1000);
function draw(values) {
const valueCircles = svg.selectAll('.valueCircle')
.data(values);
valueCircles.enter()
.append('circle')
.attr("class", "valueCircle")
.attr("r", 10)
.call(positionCircles);
valueCircles.transition()
.duration(1000)
.call(positionCircles)
};
function positionCircles(selection) {
selection.attr("cx", d => d.x)
.attr("cy", d => d.y)
}
<script src="https://d3js.org/d3.v7.min.js"></script>
<svg></svg>
In the snippet above, I'm positioning both the enter and update selections using a function called positionCircles; however, while for the enter selection I'm passing a simple selection, for the update selection I'm passing a transitioning selection. But the function is the same:
function positionCircles(selection) {
selection.attr("cx", d => d.x)
.attr("cy", d => d.y)
}
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
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.
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.