D3 - dragging multiple elements across groups - javascript

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);
}

Related

Append two elements in svg at the same level

I'm using d3.js
Hi, I'm having an issue finding how to append two elements (path and image) to the same g (inside my svg) from the same data. I know how to do this, but the tricky thing is I need to get the BBox values of the "path" elements in order to place the "image" elements in the middle... My goal is actually to place little clouds in the center of cities on a map like this : this is the map I am trying to reproduce
On the map it's not centered but I have to do so. So this is my current code:
// Draw the map
svg.append("g")
.selectAll("path")
.data(mapEPCI.features)
.enter()
.append("path")
.attr("fill", d => d.properties.color)
.attr("d", d3.geoPath().projection(projection))
.style("stroke", "white")
.append("image")
.attr("xlink:href", function(d) {
if (d.properties.plan_air == 1)
return ("data/page8_territoires/cloud.png")
else if (d.properties.plan_air == 2)
return ("data/page8_territoires/cloudgray.png")
})
.attr("width", "20")
.attr("height", "15")
.attr("x", function (d) {
let bbox = d3.select(this.parentNode).node().getBBox();
return bbox.x + 30})
.attr("y", function (d) {
return d3.select(this.parentNode).node().getBBox().y + 30})
This gets the right coordinates for my images but it's because the parent node is actually the path... If I append the image to the g element, is there a way to get the "BrotherNode", or maybe the last child of the "g" element ? I don't know if I'm clear enough but I hope you get my point.
I'm kinda new to js so maybe I'm missing something simple I just don't know yet
Thanks for your help
I would handle your data at the g level and create a group for every map feature (country) which contains the path and a sibling image:
<!doctype html>
<html>
<head>
<script src="https://d3js.org/d3.v6.min.js"></script>
</head>
<body>
<svg width="600" height="600"></svg>
<script>
let svg = d3.select('svg'),
mapEPCI = {
features: [100, 200, 300, 400]
};
let g = svg.selectAll('g')
.data(mapEPCI.features)
// enter selection is collection of g
let ge = g.enter().append("g");
// append a path to each g according to data
ge.append('path')
.attr("d", (d) => "M" + d + ",10L" + d + ",100")
.style("stroke", "black");
// append a sibling image
ge.append("image")
.attr("xlink:href", "https://placeimg.com/20/15/animals")
.attr("width", "20")
.attr("height", "15")
.attr("transform", function(d) {
// find my sibling path to get bbox
let sibling = this.parentNode.firstChild;
let bbox = sibling.getBBox();
return "translate(" + (bbox.x - 20 / 2) + "," + (bbox.y + bbox.height / 2 - 15 / 2) + ")"
});
</script>
</body>
</html>

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.

d3 SVG drag drop plus rotate with button

I'm attempting to create a program where an image can be dragged and dropped but also rotated at the push of a button(after being dragged, and rotated on its own axis). I can successfully make an image drag and drop but cannot get the rotate to work correctly. I think the solution would be to use a "transform","translate" for positioning the image at the start; and for the dragging instead of "d3.event" however I am unsure of how to do this. I'm using SVG with d3 library. I wish to keep my: dragstarted, dragged and dragended separate as will be adding further code later.
var svgWidth = parseInt(document.getElementById("canvas").getAttribute("width")),
selected = null;
canvas = d3.select('#canvas');
canvas.append("image")
.attr('width', 17)
.attr('height', 59)
.attr("x", svgWidth - 40)
.attr("y", 100)
.attr("xlink:href", "https://lorempixel.com/100/100/cats")
.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended));
canvas.append("image")
.attr('width', 80)
.attr('height', 38)
.attr("x", svgWidth - 90)
.attr("y", 200)
.attr("xlink:href", "https://lorempixel.com/100/100/sports")
.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended));
function dragstarted(d){
d3.select(this).raise().classed("active", true);
}
function dragged(d){
d3.select(this)
.attr("x", d3.event.x - parseInt(d3.select('image').attr("width")) / 2)
.attr("y", d3.event.y - parseInt(d3.select('image').attr("height")) / 2);
}
function dragended(d){
d3.select(this).classed("active", false);
selected = this;
}
window.onload=function(){
document.getElementById("rotate").addEventListener("click", function(){
if(selected != null){
var x = selected.getAttribute("x"),
y = selected.getAttribute("y");
selected.setAttribute("transform","translate(" + x / 2 + "," + y / 2 +")" + "rotate(90)");
}
});
}
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>test</title>
<link rel="stylesheet" href="styles/style.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.8.0/d3.min.js">
</script>
</head>
<body>
<svg id="canvas" width="960px" height="500px"></svg>
<button id="rotate">Rotate</button>
</body>
</html>
Nevermind I fixed it. Didn't realise the x and y values didn't have to be set on the images to use d3.event.x and d3.event.y . Meaning I could use those values on the transform when the image was being dragged.

d3.selectAll().data(myDataSet).enter().append("rect") is selecting the previously created rectangles instead of myDataSet

I am new to d3.js, currently I am working in a project which needs d3.js. I am facing a problem need help / guidance to solve this.
The problem is in my svg container I have 2 hard-coded rectangles [created with svg.append("rect")].
Now I have a json dataset which has 2 data to create another 2 rectangles[ there might be more data in json at runtime, this is static example].
Now when I use the following code it only creates 1 rectangles.
Notice The selectAll function on svg selects the other 2 previously created rectangles and only dynamically creates the extra 1 rectangles (3 in json - 2 hardcoded rectangles = 1 rectangles created).
But I want that all the data (3) in json should created a rectangle that is 3 rectangle should be created by Json and the hardcoded rectangles should remain as it is.
But it is not doing that, I have added the html code this post. Here is my javascript and HTML what i have tried so far.
Please help.
<html>
<head>
<meta charset="ISO-8859-1">
<title>D3 Test</title>
<script type="text/javascript" src="d3/d3.v3.min.js"></script>
</head>
<body>
<h1>hello....</h1>
<div id="chartDiv">
</div>
<script type="text/javascript">
var w = 1000;
var h = 400;
var svgContainer = d3.select("#chartDiv").append("svg").attr("width", w).attr("height", h);
var call_result_dataset = [{"x_axis":40, "y_axis":10, "width":5, "height":110, "color":"green"},
{"x_axis":700, "y_axis":10, "width":5, "height":110, "color":"red"},
{"x_axis":760, "y_axis":10, "width":5, "height":110, "color":"red"}];
var call_result_strip = svgContainer.append("rect")
.attr("id", "call_result_strip")
.attr("x", 10)
.attr("y", 10)
.attr("width", 980)
.attr("height", 110)
.attr("fill", "rgb(235,235,235)");
var call_type_strip = svgContainer.append("rect")
.attr("id", "call_type_strip")
.attr("x", 10)
.attr("y", 200)
.attr("width", 980)
.attr("height", 110)
.attr("fill", "rgb(235,235,235)");
var call_result_strip_bars = svgContainer.selectAll("rect")
.data(call_result_dataset)
.enter()
.append("rect");
var all_r_strip = call_result_strip_bars.attr("x", function(d){ return d.x_axis;})
.attr("y", function(d){ return d.y_axis;})
.attr("width", function(d){ return d.width;})
.attr("height", function(d){ return d.height;})
.attr("fill", function(d){ return d.color;})
.attr("id", function(d,i) {return "result_"+i;});
</script>
</body>
</html>
You can use the following snippet:
svgContainer.selectAll(".dynamicRects")
.data(call_result_dataset)
.enter()
.append("rect")
.addClass('dynamicRects')
...
This way, the enter selection will first be empty and then the three dynamic rects will be created.
selecting nothing wit some class name will suit your requirment.
svgContainer.selectAll()
.data(call_result_dataset)
.enter()
.append("rect");
it is recommended to use a class name for the selection and the code goes like..
svgContainer.selectAll(".someClass")
.data(call_result_dataset)
.enter()
.append("rect").classed("someClass",true);

D3.js code in Reveal.js slide

I'm trying to make D3.js work on Reveal.js slides, but I can't get it to run even the most basic snippet:
<section>
<h2>Title</h2>
<div id="placeholder"></div>
<script type="text/javascript">
d3.select("#placeholder").append("p").text("TEST");
</script>
</section>
Doesn't show the "TEST" word. What am I doing wrong?
Okay here we go.
I have made a basic example with Reveal.js & D3.js and it works well.
There are two slides
First slide contains Bar chart
Second slide renders Bubble chart by taking data from a json input file
Your code works fine if it is placed outside of the section at the bottom. I have placed all D3.js code at the end of the html page before body closer.
The folder structure is show below (in snapshot)
I placed my JS inside the HTML (in order to make it easier to read/comprehend)
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Reveal.js with D3 JS</title>
<link rel="stylesheet" href="css/reveal.min.css">
<link rel="stylesheet" href="css/theme/default.css" id="theme">
<style>
.chart rect {
fill: #63b6db;
}
.chart text {
fill: white;
font: 10px sans-serif;
text-anchor: end;
}
text {
font: 10px sans-serif;
}
</style>
</head>
<body>
<div class="reveal">
<div class="slides">
<section>
<h2>Barebones Presentation</h2>
<p>This example contains the bare minimum includes and markup required to run a reveal.js presentation.</p>
<svg class="chart"></svg>
</section>
<section id="sect2">
<h2>No Theme</h2>
<p>There's no theme included, so it will fall back on browser defaults.</p>
<svg class="bubleCharts"></svg>
</section>
</div>
</div>
<script src="js/reveal.min.js"></script>
<script>
Reveal.initialize();
</script>
<script src="js/d3.min.js"></script>
<script type="text/javascript">
//------ code to show D3 Bar Chart on First Slide-------
var data = [44, 28, 15, 16, 23, 5];
var width = 420,
barHeight = 20;
var x = d3.scale.linear()
.domain([0, d3.max(data)])
.range([0, width]);
var chart = d3.select(".chart")
.attr("width", width)
.attr("height", barHeight * data.length);
var bar = chart.selectAll("g")
.data(data)
.enter().append("g")
.attr("transform", function(d, i) { return "translate(0," + i * barHeight + ")"; });
bar.append("rect")
.attr("width", x)
.attr("height", barHeight - 1);
bar.append("text")
.attr("x", function(d) { return x(d) - 3; })
.attr("y", barHeight / 2)
.attr("dy", ".35em")
.text(function(d) { return d; });
//---Code below will show Bubble Charts on the secon Slide -------
var diameter = 560,
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(".bubleCharts")
.attr("width", diameter)
.attr("height", diameter)
.attr("class", "bubble");
d3.json("flare.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");
</script>
</body>
</html>
Output/results
Slide 1
Slide 2
Download complete code https://github.com/aahad/D3.js/tree/master/Reveal JS with D3 JS
To learn more about how Bar Chart or Bubble Chart code works: check followings:
Bubble Chart examples: http://bl.ocks.org/mbostock/4063269
Bar Chart examples: http://bost.ocks.org/mike/bar/
Both existing answers are fine, but I'd like to point out a 3rd approach that worked for me and has some advantages.
Reveal.js has an event system and it also works with the per-slide data states.
This means that you can have a separate javascript block for each slide and have it execute only when that slide is loaded. This lets you do D3-based animations upon load of the slide and has the further advantage of placing the code for the slide closer to the text/markup of it.
For example, you could set your slide like this. Note the data-state attribute:
<section data-state="myslide1">
<h2>Blah Blah</h2>
<div id="slide1d3container"></div>
</section>
And then have an associated script block:
<script type="text/javascript">
Reveal.addEventListener( 'myslide1', function() {
var svg = d3.select("#slide1d3container").append("svg")
// do more d3 stuff
} );
</script>
Here's an example of a presentation that uses this technique:
http://explunit.github.io/d3_cposc_2014.html
I found out by myself. Of course I cannot match against ids that are not loaded yet: it works if I put the d3 javascript code after the Reveal.initialize script block at the end of the index.html file.

Categories