Proportional circles and labels in d3 - javascript

I am relatively new to d3 and am attempting to create a vertical column of proportional circles and their labels.
At this point I have a horizontal column, but the entire array of labels (texts) are repeated for each circle. Why does this happen and how can I avoid this? How can the circles below be inverted so they are stacked vertically with labels to the right as opposed to horizontal with labels on top?
Any suggestions and explanation would be appreciated!
Here is the example in JS Fiddle
<!DOCTYPE html>
<meta charset="utf-8">
<style>
.radSol{
background:white;
}
.circles{
color:blue;
font-size:29
}
</style>
<body>
<script src="http://d3js.org/d3.v3.js"></script>
<script>
var h = 100
var w = 595
var xspa = 30
var svg = d3.select('body').append('svg').attr('width', w).attr('height', h).attr('class','radSol');
// look at chp 7 of vizualizing data
var circs = [0,2,4,6,8,10,12,14,16,18,20]
var texts = [0,10,20,30,40,50,60,70,80,90,100]
var rscale = d3.scale.linear()
svg.selectAll('.circles').data(circs)
.enter()
.append('circle')
.attr('r', function(d){
return d
})
.attr('class','circles')
.attr('cy', h/2)
.attr('cx',function(d,i){
// console.log(i)
return 20+ 40 *i
})
svg.selectAll('text')
.data(circs).enter().append('text')
.text(texts)
.attr('font-size', 10)
.attr('fill', 'black')
.attr('x', function(d,i){
return 20 +40*i
})
.attr('y', h/5)
.attr('text-anchor', 'middle')
d3.select('body').append('html:br')
</script>
</body>

For the first issue regarding reprinting all the text each time. As with the circles, you are appending n elements where n is the length of your data (circs) array. So for these lines:
svg.selectAll('text')
.data(circs).enter().append('text')
.text(texts)
You are appending the array texts n times. Instead you could do something like:
svg.selectAll('text')
.data(circs).enter().append('text')
.text(function(d,i) { return texts[i]; })
The i represents the increment of the element being appended, so on the nth circle, you want the nth text.
To rearrange the circles and text vertically, you can position on a fixed x element and apply functions similar to what you have now (to position on the x axis) to position elements on the y axis:
var h = 595
var w = 595
var xspa = 30
var svg = d3.select('body').append('svg').attr('width', w).attr('height', h).attr('class','radSol');
// look at chp 7 of vizualizing data
var circs = [0,2,4,6,8,10,12,14,16,18,20]
var texts = [0,10,20,30,40,50,60,70,80,90,100]
var rscale = d3.scale.linear()
svg.selectAll('.circles').data(circs)
.enter()
.append('circle')
.attr('r', function(d){
return d
})
.attr('class','circles')
.attr('cy', function(d,i) { return 20+ 40 *i; })
.attr('cx', 50)
svg.selectAll('text')
.data(circs).enter().append('text')
.text(function(d,i) { return texts[i]; })
.attr('font-size', 10)
.attr('fill', 'black')
.attr('x', 150)
.attr('y', function(d,i){
return 20 +40*i;
})
.attr('text-anchor', 'middle')
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>

Related

d3 raise() not working on specific selection

I have a linechart made with d3, but due to the shape of the data, the lines and dots (I'm using dot's over the lines for each specific data point) usually end up being in top of each other.
To counter this problem, I ended giving opacity 0.4 to the lines and dots, and when you hover over a line, the lines and dots of this specific line of data pops out, and sets it's opacity to 1.
My problem is: I'm using the .raise() funcion to make them pop out and stand over the rest of the lines and dots, the function is working only with my lines selection and not with my dots selection, and I don't know why.
My code:
// draw the data lines
const lines = svg.selectAll('.line')
.data(this.data)
.enter()
.append('path')
.attr('class', 'data.line')
.attr("fill", "none")
.attr("stroke", d => colors(d.key))
.attr("stroke-linejoin", "round")
.attr("stroke-linecap", "round")
.attr("stroke-width", 2.5)
.attr('stroke-opacity', 0.4)
.attr('d', d => line(d.values))
.on('mouseenter', d => {
// Highlight them
let myCircles = circles.selectAll('.circle');
lines.attr('stroke-opacity', b => {
return b.key === d.key ? 1 : 0.4;
});
myCircles.attr('fill-opacity', b => {
return b[this.typeIdentifier] === d.key ? 1 : 0.4;
});
// Bring them to the front
myCircles = circles.selectAll('.circle')
.filter(b => b[this.typeIdentifier] === d.key);
const myLines = lines.filter(b => b.key === d.key);
myLines.raise();
myCircles.raise();
});
// draw the circles
const circles = svg.selectAll('.circle')
.data(this.data)
.enter()
.append('g');
circles.selectAll('.circle')
.data(d => d.values)
.enter()
.append('circle')
.attr('class', 'circle')
.attr('stroke', 'white')
.attr('stroke-width', 1)
.attr('r', 6)
.attr('fill', d => colors(d[this.typeIdentifier]))
.attr('fill-opacity', 0.4)
.attr('cx', d => x(d[this.xAxisValue]) + x.bandwidth() / 2)
.attr('cy', d => y(d[this.yAxisValue]))
.on('mouseenter', (d, b, j) => {
tooltip.raise();
tooltip.style("display", null);
tooltip.select("#text1").text(d[this.typeIdentifier])
.attr('fill', colors(d[this.typeIdentifier]));
tooltip.select('#text4').text(d[this.yAxisValue]);
tooltip.select('#text5').text(d[this.xAxisValue]);
const tWidth = tooltip.select('#text1').node().getComputedTextLength() > 60 ? tooltip.select('#text1').node().getComputedTextLength() + 20 : 80;
tooltipRect.attr('width', tWidth);
const xPosition = d3.mouse(j[b])[0];
const yPosition = d3.mouse(j[b])[1];
if (xPosition + tWidth + 35 < this.xWIDTH) { // display on the right
tooltip.attr("transform", `translate(${xPosition + 15}, ${yPosition - 25})`);
} else { // display on the left
tooltip.attr("transform", `translate(${xPosition - tWidth - 15}, ${yPosition - 25})`);
}
})
.on('mouseleave', d => {
tooltip.style("display", "none");
})
So, when you hover the mouse over a line, this should bring the line and dots associated to it to the front, with opacity 1, but for some reason, it's only working on the lines selection, and not on the myCircles selection. The selection is not empty, and I've been printing them all along to test it out. Also, I've tried to bring the circles one by one (with singular selections, and with raw elements) to the front using the .raise() method, and it's not working eiter.
Why is it not working? Could it have to do with the tooltip on hover over the circles? Am I doing something wrong and not seeing it?
Actually, selection.raise() is working. The problem here is just the tree structure of your SVG: all the circles for a given line belong to a <g> element.
If you look at the docs, you'll see that selection.raise():
Re-inserts each selected element, in order, as the last child of its parent.
The emphasis above is mine: the key work here is parent. So, what you want is to raise the <g> element that contains the selected circles above the other <g> elements for the other circles, not the circles inside their <g> parent.
In your case, it's as simple as changing...
myCircles = circles.selectAll('.circle').filter(etc...)
...to:
myCircles = circles.filter(etc...)
Now, myCircles is the selection with the <g> element, which you can raise. Pay attention to the filter function: as you didn't share your data structure I don't know if the data array for the <g> elements (that is, this.data) contains the key property. Change it accordingly.
Here is a demo:
We have a set of circles for each line, each set inside their own <g> parent. Only the left circles are separated, all other circles are draw one over the other on purpose. When you hover over a circle (use the ones on the left) its <g> container is raised, in this case using...
d3.select(this.parentNode).raise()
..., so all circles are visible:
const svg = d3.select("svg");
const scale = d3.scaleOrdinal(d3.schemeSet1);
const lineGenerator = d3.line()
.x(function(d) {
return d.x
})
.y(function(d) {
return d.y
})
const data = d3.range(5).map(function(d) {
return {
key: d,
values: d3.range(5).map(function(e) {
return {
x: 50 + 100 * e,
y: e ? 150 : 50 + 50 * d
}
})
}
});
const lines = svg.selectAll(null)
.data(data)
.enter()
.append("path")
.attr("d", function(d) {
return lineGenerator(d.values);
})
.style("fill", "none")
.style("stroke-width", "3px")
.style("stroke", function(d) {
return scale(d.key)
});
const circleGroups = svg.selectAll(null)
.data(data)
.enter()
.append("g");
const circles = circleGroups.selectAll(null)
.data(function(d) {
return d.values
})
.enter()
.append("circle")
.attr("r", 20)
.attr("cx", function(d) {
return d.x
})
.attr("cy", function(d) {
return d.y
})
.style("fill", function(d) {
return scale(d3.select(this.parentNode).datum().key)
});
circles.on("mouseover", function(d) {
const thisKey = d3.select(this.parentNode).datum().key;
lines.filter(function(e) {
return e.key === thisKey;
}).raise();
d3.select(this.parentNode).raise();
})
<script src="https://d3js.org/d3.v5.min.js"></script>
<svg width="500" height="300"></svg>

Force simulation is jittery when using svg transforms to update position

JSFiddle example
I've noticed that when updating positions of svg elements in a d3-force diagram, updating the positions of elements using (in the case of circles) the cx and cy attributes is much smoother than using the transform attribute.
In the example JSFiddle, there are two separate force simulations side-by-side. The one on the left updates positions using the transform attribute:
sim_transform.on('tick', function () {
circles_transform.attr('transform', function (d) {
return 'translate(' + d.x + ',' + d.y + ')';
});
});
The one on the right updates positions using the cx and cy attributes of a circle:
sim_position.on('tick', function () {
circles_position
.attr('cx', function (d) {
return d.x;
})
.attr('cy', function (d) {
return d.y;
})
});
The simulations appear identical until they're just about to become static, at which point the one using transforms starts to jitter quite a bit. Any ideas what is causing this? Can it be fixed so that the animation remains smooth using transforms?
It seems to me that the issue you're observing (only reproducible in FireFox, as #altocumulus noted) has something to do with the way FF uses floating numbers for the translate of the transform attribute.
We can see this if we set both simulations to use integers, doing ~~(d.x) and ~~(d.y). Have a look, both will jitter:
var svg = d3.select('svg');
var graph_transform = gen_data();
var graph_position = gen_data();
var force_left = d3.forceCenter(
parseInt(svg.style('width')) / 3,
parseInt(svg.style('height')) / 2
)
var force_right = d3.forceCenter(
2 * parseInt(svg.style('width')) / 3,
parseInt(svg.style('height')) / 2
)
var sim_transform = d3.forceSimulation()
.force('left', force_left)
.force('collide', d3.forceCollide(65))
.force('link', d3.forceLink().id(id));
var sim_position = d3.forceSimulation()
.force('right', force_right)
.force('collide', d3.forceCollide(65))
.force('link', d3.forceLink().id(id));
var g_transform = svg.append('g');
var g_position = svg.append('g');
var circles_transform = g_transform.selectAll('circle')
.data(graph_transform.nodes)
.enter()
.append('circle')
.attr('r', 40);
var circles_position = g_position.selectAll('circle')
.data(graph_position.nodes)
.enter()
.append('circle')
.attr('r', 40);
sim_transform
.nodes(graph_transform.nodes)
.force('link')
.links(graph_transform.links);
sim_position
.nodes(graph_position.nodes)
.force('link')
.links(graph_position.links);
sim_transform.on('tick', function() {
circles_transform.attr('transform', function(d) {
return 'translate(' + (~~(d.x)) + ',' + (~~(d.y)) + ')';
});
});
sim_position.on('tick', function() {
circles_position
.attr('cx', function(d) {
return ~~d.x;
})
.attr('cy', function(d) {
return ~~d.y;
})
});
function id(d) {
return d.id;
}
function gen_data() {
var nodes = [{
id: 'a'
},
{
id: 'b'
},
{
id: 'c'
},
{
id: 'd'
}
]
var links = [{
source: 'a',
target: 'b'
},
{
source: 'b',
target: 'c'
},
{
source: 'c',
target: 'd'
},
{
source: 'd',
target: 'a'
}
];
return {
nodes: nodes,
links: links
}
}
svg {
width: 100%;
height: 500px;
}
<script src="https://d3js.org/d3.v5.min.js"></script>
<svg></svg>
So, in your original code, it seems like the circles move correctly when using cx and cy, but they jump from integer to integer when using translate (or maybe half pixel, see the last demo). If the hypothesis here is correct, the reason that you just see the effect when the simulation is cooling down is because, at that moment, the movements are smaller.
Demos
Now, if we get rid of the simulation, we can see that this strange behaviour also happens with a very basic transform. To check this, I created a transition for a big black circle, using a linear ease and a very long time (to facilitate seeing the issue). The circle will move 30px to the right. I also put a gridline to make the jumps more noticeable.
(Warning: the demos below are only reproducible in FireFox, you won't see any difference in Chrome/Safari)
If we use cx, the transition is smooth:
var svg = d3.select("svg");
var gridlines = svg.selectAll(null)
.data(d3.range(10))
.enter()
.append("line")
.attr("y1", 0)
.attr("y2", 200)
.attr("x1", function(d) {
return 300 + d * 3
})
.attr("x2", function(d) {
return 300 + d * 3
})
.style("stroke", "lightgray")
.style("stroke-width", "1px");
var circle = svg.append("circle")
.attr("cx", 200)
.attr("cy", 100)
.attr("r", 98)
.transition()
.duration(10000)
.ease(d3.easeLinear)
.attr("cx", "230")
<script src="https://d3js.org/d3.v5.min.js"></script>
<svg width="500" height="200"></svg>
However, if we use translate, you can see the circle jumping 1px at every move:
var svg = d3.select("svg");
var gridlines = svg.selectAll(null)
.data(d3.range(10))
.enter()
.append("line")
.attr("y1", 0)
.attr("y2", 200)
.attr("x1", function(d) {
return 300 + d * 3
})
.attr("x2", function(d) {
return 300 + d * 3
})
.style("stroke", "lightgray")
.style("stroke-width", "1px");
var circle = svg.append("circle")
.attr("cx", 200)
.attr("cy", 100)
.attr("r", 98)
.transition()
.duration(10000)
.ease(d3.easeLinear)
.attr("transform", "translate(30,0)")
<script src="https://d3js.org/d3.v5.min.js"></script>
<svg width="500" height="200"></svg>
For you people running this in Chrome/Safari, this is how the last snippet looks like in Firefox. It's like the circle is being moved half a pixel at every change... definitely not as smooth as changing cx:
var svg = d3.select("svg");
var gridlines = svg.selectAll(null)
.data(d3.range(10))
.enter()
.append("line")
.attr("y1", 0)
.attr("y2", 200)
.attr("x1", function(d) {
return 300 + d * 3
})
.attr("x2", function(d) {
return 300 + d * 3
})
.style("stroke", "lightgray")
.style("stroke-width", "1px");
var circle = svg.append("circle")
.attr("cx", 200)
.attr("cy", 100)
.attr("r", 98);
var timer = d3.timer(function(t){
if(t>10000) timer.stop();
circle.attr("cx", 200 + (~~(60/(10000/t))/2));
})
<script src="https://d3js.org/d3.v5.min.js"></script>
<svg width="500" height="200"></svg>
As this is an implementation issue only visible in FF, it may be worth reporting a bug.

d3 skips first index in an array when appending circles

I'm new to using D3 and I'm trying to evenly place circles around another circle by dividing into n pieces. The problem is that it does not draw the first circle even though coords[0] exists. Instead it places starts at coords[1] and continues. Any reason why this is and how to fix it?
main.html
<!doctype html>
<html>
<head>
<title>A Circle</title>
<meta charset="utf-8" />
<script type="text/javascript" src="https://d3js.org/d3.v4.min.js"></script>
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/mathjs/3.16.1/math.min.js"></script>
</head>
<body>
<script type="text/javascript" src="circle.js"></script>
</body>
</html>
circle.js
var w = 500;
var h = 500;
var n = 10;
var r = h/2-20;
var coords = [];
for (var i = 0; i<n; i++)
{
var p_i = {};
p_i.x = w/2+r*math.cos((2*math.pi/n)*i);
p_i.y = h/2-r*math.sin((2*math.pi/n)*i);
coords.push(p_i);
}
var svg = d3.select('body') //SVG Canvas
.append('svg')
.attr('width', w)
.attr('height', h);
var circle = svg.append('circle') //Draw Big Circle
.attr('cx', w/2)
.attr('cy', h/2)
.attr('r', r)
.attr('fill', 'teal')
.attr('stroke', 'black')
.attr('stroke-width', w/100);
var center = svg.append('circle') //Construct Center
.attr('cx', w/2)
.attr('cy', h/2)
.attr('r', r/50)
.attr('fill', 'red')
.attr('stroke', 'black')
.attr('stroke-width', w/100);
var approx_pts = svg.selectAll('circle')
.data(coords, function(d)
{
return this.id;
})
.enter()
.append('circle')
.attr('cx', function(d)
{
return d.x;
})
.attr('cy', function(d)
{
return d.y;
})
.attr('r', w/100)
.attr('fill', 'black');
You already have circles in the SVG when you do this:
var approx_pts = svg.selectAll('circle')
Therefore, your "enter" selection has less elements than your data array.
Solution: just use selectAll(null):
var approx_pts = svg.selectAll(null)
That way you can be sure that your enter selection has all the elements in your data array. Of course, this approach avoids creating an "update" and "exit" selections in the future.
If you do plan to use an "update" and "exit" selections, you can select by class:
var approx_pts = svg.selectAll(".myCircles")
Don't forget to set that class when you append those circles in the enter selection, of course.

concentric emanating circles d3

I have an array of equally spaced values which I am using to draw concentric circles. I want to use an emanating effect, in essence, remove the outermost circle once its value exceeds the maximum value and add a new one at the center to compensate. I am unsure about manipulation on data set to remove and add new circle.
<script src="https://d3js.org/d3.v4.min.js"></script>
<script>
var svg = d3.select("body").append("svg");
var w = window.innerWidth;
var h = window.innerHeight;
var separation = Math.min(50, w/12);
var n=Math.floor((w/2)/separation);
var ellipse=new Array(n);
for(var i=1;i<=n;i++){
ellipse[i-1]=(i*separation);
}
svg.attr("width", w).attr("height", h);
var g = svg.append("g");
var e=g.selectAll("ellipse")
.data(ellipse)
.enter()
.append("ellipse")
.attr("cx", w/2)
.attr("cy", h/2)
.attr("rx",0)
.attr("ry",0)
.transition()
.duration(5000)
.attr("rx", function(d,i){return ellipse[i];})
.attr("ry", function(d,i){return ellipse[i]/5;});
loop();
function loop(){
e.transition()
.attr("rx", function(d,i){
return ellipse[i]+separation;
})
.attr("ry", function(d,i){
return (ellipse[i]+separation)/5;
})
.on("end",loop());
}
</script>
You could approach it with a remove().exit() and enter().append() selection for each ring - but essentially you always have the same number of rings on the screen. Why not just recycle the same elements? When the size hits a threshold, reset it to zero, or some other starting value?
Something along the lines of:
var scale = d3.scaleLinear()
.range(["orange","steelblue","purple"])
.domain([0,60]);
var data = [0,10,20,30,40,50,60];
var width = 200;
var height = 200;
var svg = d3.select("body")
.append("svg")
.attr("width",width)
.attr("height",height);
var circles = svg.selectAll("circle")
.data(data)
.enter()
.append("circle")
.attr("r",function(d) { return d; })
.attr("transform","translate(80,80)")
.attr("fill","none")
.style("stroke-width","4")
.style("stroke",function(d) { return scale(d) });
function transition() {
// Update data, max d is 60:
data = data.map(function(d) { return d == 60 ? 0 : d + 10});
var i = 0;
// Grow circles
circles
.data(data)
.filter(function(d) { return d > 0 })
.transition()
.ease(d3.easeLinear)
.attr("r", function(d) { return d; })
.style("stroke", function(d) { return scale(d) })
.style("opacity",function(d) { return d == 60 ? 0 : 1 })
.duration(1000)
.on("end",function(){if(++i==circles.size()-1) { transition(); } });
// Reset circles where r == 0
circles
.filter(function(d) { return d == 0 })
.attr("r", 0)
.style("opacity",1)
.style("stroke",function(d) { return scale(d); });
}
transition();
<script src="https://d3js.org/d3.v4.min.js"></script>
Note that .on("end",... triggers on each element's transition end - this is why I count to see if all elements are done transitioning before running the transition function again.

D3js dynamically attach circles into a line with drag and drop

I am very new to d3js v3 and I was trying out a new program where there are lines and the according to the data, circles get embedded into them.
This is what I have so far.
var width = 500,
height = 500;
var animals = ['dog', 'cat', 'bat'];
var fruits = ['apple', 'banana'];
var svg = d3.select("body")
.append("svg")
.attr("width", width)
.attr("height", height);
var line1 = svg.append("line")
.attr("x1", 350)
.attr("y1", 5)
.attr("x2", 350)
.attr("y2", 350)
.attr("stroke-width", 2)
.attr("stroke", "black");
var line2 = svg.append("line")
.attr("x1", 80)
.attr("y1", 5)
.attr("x2", 100)
.attr("y2", 350)
.attr("stroke-width", 2)
.attr("stroke", "black");
var animal_scale = d3.scale.ordinal()
.domain(animals)
.rangePoints([5, 350],.2);
var fruit_scale = d3.scale.ordinal()
.domain(fruits)
.rangePoints([5, 350],.2);
var animal_circles = svg.selectAll('circle')
.data(animals)
.enter()
.append('circle')
.attr('cx', function(d) {
// is there a way to calc it automatically according to line 1
})
.attr('cy', function(d) {
return animal_scale(d);
})
.attr('id', function(d) {
return d;
})
.attr('r', 20);
var fruits_circles = svg.selectAll('circle')
.data(fruits)
.enter()
.append('circle')
.attr('cx', function(d) {
// is there a way to calc it automatically according to line 2
})
.attr('cy', function(d) {
return fruit_scale(d);
})
.attr('id', function(d) {
return d;
})
.attr('r', 20);
<!DOCTYPE html>
<html>
<meta charset=utf-8>
<head>
<title></title>
</head>
<body>
<script src="https://d3js.org/d3.v3.min.js"></script>
</body>
</html>
I looked at some sources and being new, its kinda hard to understand most of it. I eventually want to be able to move and drag the circles between lines at the end of the project.There are some issues with the current code, as it does not display the second set of circles too.
Could someone please help me understand further how to do this. It would be a great way for me to learn.
You can select objects by class name and set data. Here is my fast solution for drag-n-drop: jsFiddle. You can modify drag function to add limits to cx position

Categories