d3 skips first index in an array when appending circles - javascript

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.

Related

D3 - dragging multiple elements across groups

I'm trying to achieve a D3 drag behavior, for which I'm not finding an example.
The below code creates a couple of rows with a rectangle, circle and ellipse each, and the drag feature is enabled for the circle.
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Drag many</title>
<style media="screen">
.active {
stroke: #000;
stroke-width: 2px;
}
</style>
</head>
<body>
<svg width=1000 height=500></svg>
<script src="https://d3js.org/d3.v4.min.js" charset="utf-8"></script>
<script type="text/javascript">
var svg=d3.select("svg"),
mainGroup = svg.append("g");
var squareData = [
{x:25, y:23, width:15, height:15},
{x:25, y:63, width:15, height:15}
];
var circleData = [
{cx:60, cy:30, r:10},
{cx:60, cy:70, r:10}
];
var ellipseData = [
{cx:90, cy:30, rx:10, ry:15},
{cx:90, cy:70, rx:10, ry:15}
];
var squareGroup = mainGroup.append("g");
squareGroup.selectAll(null)
.data(squareData)
.enter().append("rect")
.attr("class", (d,i)=>`rectGroup row_${i+1}`)
.attr("x", d=>d.x)
.attr("y", d=>d.y)
.attr("width", d=>d.width)
.attr("height", d=>d.height)
.attr("fill", "blue");
circleGroup = mainGroup.append("g");
circleGroup.selectAll(null)
.data(circleData)
.enter().append("circle")
.attr("class", (d,i)=>`circleGroup row_${i+1}`)
.attr("cx", d=>d.cx)
.attr("cy", d=>d.cy)
.attr("r", d=>d.r)
.attr("fill", "red")
.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended));
ellipseGroup = mainGroup.append("g");
ellipseGroup.selectAll(null)
.data(ellipseData)
.enter().append("ellipse")
.attr("class", (d,i)=>`ellipseGroup row_${i+1}`)
.attr("cx", d=>d.cx)
.attr("cy", d=>d.cy)
.attr("rx", d=>d.rx)
.attr("ry", d=>d.ry)
.attr("fill", "green");
function dragstarted(d){
d3.select(this).classed("active", true);
}
function dragended(d){
d3.select(this).classed("active", false);
}
function dragged(d){
//var dx = d3.event.x - d3.select(this).attr("cx")
d3.select(this).attr("cx", d.x = d3.event.x);
// I really need to move all the elements in this row here,
// with the circle being the pivot...
}
</script>
</body>
</html>
I would like to modify the drag behavior of the circle to pull the entire row with it rather than move alone.
How can I achieve this?
It looks like you give each row a class so instead of:
function dragged(d){d3.select(this).attr("cx", d.x = d3.event.x);}
get the class of that row (from the looks of your code it should be the second class) with:var thisRow = d3.select(this).attr("class").split()[1];
.split() will split on spaces which is how getting the class list will spit out its result.
After getting the class move all of that class:
d3.selectAll("." + thisRow).attr("cx", d.x = d3.event.x);
I'm not really sure if there's a better way of finding the row, but here's how I solved it using the concept from #pmkroeker
function dragged(d){
var deltaX = d3.event.x - d3.select(this).attr("cx");
d3.select(this).attr("cx", d.x = d3.event.x); // move the circle
// find the row
var row = d3.select(this).attr("class").split(/\s+/).filter(s=>s.startsWith("row_"))[0];
// grab other elements of the row and move them as well
d3.selectAll("."+row).filter(".rectGroup").attr("x", d=>d.x = d.x+deltaX);
d3.selectAll("."+row).filter(".ellipseGroup").attr("cx", d=>d.cx = d.cx+deltaX);
}

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

Proportional circles and labels in d3

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>

d3 mouse move cross hair boundary

[Update]
Click jsfiddle here.
[Original Post]
How can I limit the coordinates of cross hair when moving the mouse? Notice when I move my mouse to the left of x-axis or bottom of y-axis, the cross hair and text still shows. I want the cross hair and text to stop showing when either mouse is moved to the left of x-axis or bottom of y-axis.
I have tried to add if else to limit the cross hair, but it didn't work. For instance, I tried something like .style("display", (xCoord>=minX & xCoord<=maxX & yCoord>=minY & yCoord<=maxY) ? "block" : "none") in the addCrossHair() function.
<!DOCTYPE html>
<meta charset="utf-8">
<head>
<style>
.axis path,
.axis line
{
fill:none;
rendering:crispEdges;
stroke:black;
width:2.5;
}
</style>
</head>
<body>
<script src="d3.min.js"></script>
<script>
var width = 800,
height = 600;
var randomX = [],
randomY = [];
for (var i = 0; i <= 500; i++) {
randomX[i] = Math.random() * 400;
randomY[i] = Math.random() * 400;
}
var minX = d3.min(randomX),
maxX = d3.max(randomX),
minY = d3.min(randomY),
maxY = d3.max(randomY);
var xScale = d3.scale.linear().domain([minX, maxX]).range([0, width]);
var xAxis = d3.svg.axis().scale(xScale).orient("bottom");
var yScale = d3.scale.linear().domain([minY, maxY]).range([height, 0]);
var yAxis = d3.svg.axis().scale(yScale).orient("left");
var svgContainer = d3.select("body").append("div").append("svg").attr("width", width).attr("height", height);
var svg = svgContainer.append("g").attr("transform", "translate(50, 50)");
svg.append("g").attr("class", "axis").attr("transform", "translate(0,530)").call(xAxis);
svg.append("g").attr("class", "axis").call(yAxis);
var crossHair = svg.append("g").attr("class", "crosshair");
crossHair.append("line").attr("id", "h_crosshair") // horizontal cross hair
.attr("x1", 0)
.attr("y1", 0)
.attr("x2", 0)
.attr("y2", 0)
.style("stroke", "gray")
.style("stroke-width", "1px")
.style("stroke-dasharray", "5,5")
.style("display", "none");
crossHair.append("line").attr("id", "v_crosshair") // vertical cross hair
.attr("x1", 0)
.attr("y1", 0)
.attr("x2", 0)
.attr("y2", 0)
.style("stroke", "gray")
.style("stroke-width", "1px")
.style("stroke-dasharray", "5,5")
.style("display", "none");
crossHair.append("text").attr("id", "crosshair_text") // text label for cross hair
.style("font-size", "10px")
.style("stroke", "gray")
.style("stroke-width", "0.5px");
svgContainer.on("mousemove", function () {
var xCoord = d3.mouse(this)[0] - 50,
yCoord = d3.mouse(this)[1] - 50;
addCrossHair(xCoord, yCoord);
})
.on("mouseover", function () {d3.selectAll(".crosshair").style("display", "block");})
.on("mouseout", function () {d3.selectAll(".crosshair").style("display", "none");});
function addCrossHair(xCoord, yCoord) {
// Update horizontal cross hair
d3.select("#h_crosshair")
.attr("x1", xScale(minX))
.attr("y1", yCoord)
.attr("x2", xScale(maxX))
.attr("y2", yCoord)
.style("display", "block");
// Update vertical cross hair
d3.select("#v_crosshair")
.attr("x1", xCoord)
.attr("y1", yScale(minY))
.attr("x2", xCoord)
.attr("y2", yScale(maxY))
.style("display", "block");
// Update text label
d3.select("#crosshair_text")
.attr("transform", "translate(" + (xCoord + 5) + "," + (yCoord - 5) + ")")
.text("(" + xScale.invert(xCoord) + " , " + yScale.invert(yCoord) + ")");
}
svg.selectAll("scatter-dots")
.data(randomY)
.enter().append("svg:circle")
.attr("cy", function (d) {
return yScale(d);
})
.attr("cx", function (d, i) {
return xScale(randomX[i]);
})
.style("fill", "brown")
.attr("r", 3)
</script>
</body>
The way to do this is to make the actual canvas (i.e. where you're drawing the dots) a separate g element from the ones that the axes are rendered into. Then the canvas can be translated such that it sits to the right and above the axes. The crosshair handler would be attached to this canvas g element (which is not quite straightforward, see this question) and the crosshairs or dots won't appear outside of the canvas.
Complete demo here.

D3: adding a line when clicking on a circle in scatter plot

I have a scatter plot. Now if I click on one of the points, how can I generate a line passing through that point?
I am stuck at two places:
With the following code, why is my line now showing?
<!DOCTYPE html>
<meta charset="utf-8">
<body>
<script src="d3.min.js"></script>
<script>
var width = 500, height = 500;
var randomX=[], randomY=[];
for (var i=0; i<=50; i++) {
randomX[i] = Math.random()*400;
randomY[i] = Math.random()*400;}
var data = randomX.concat(randomY);
var x = d3.scale.linear()
.domain([0, d3.max(randomX)])
.range([0, width]);
var y = d3.scale.linear()
.domain([0, d3.max(randomY)])
.range([height, 0]);
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height)
.append("g");
svg.selectAll("scatter-dots")
.data(randomY)
.enter().append("svg:circle")
.attr("cy", function(d) {return y(d); } )
.attr("cx", function(d,i) {return x(randomX[i]); } )
.style("fill", "brown")
.attr("r", 5)
.on("click", function(d,i) {
d3.select(this)
.append("svg:line")
.attr("x1", 300).attr("y1", 300)
.attr("x2", 50).attr("y2", 50)
.style("stroke", "steelblue")
.style("stroke-width", 3);
});
</script>
Where is the coordinates of my clicked point stored? I tried this.cx and this.cy, but none of them gave me the actual coordinates.
First, you need to append the line element to the top-level SVG or a g element, not a circle element, otherwise it won't be shown. So in your click handler, you would need to do this:
.on("click", function(d,i) {
svg.append("svg:line")
.attr("x1", 300).attr("y1", 300)
.attr("x2", 50).attr("y2", 50)
.style("stroke", "steelblue")
.style("stroke-width", 3);
});
You can get the coordinates of the click either through d3.event or the coordinates of the circle itself, i.e.
.on("click", function(d,i) {
var x = x(randomX[i]),
y = y(d);
});
or even
.on("click", function(d,i) {
var x = d3.select(this).attr("cx"),
y = d3.select(this).attr("cy");
});

Categories