After appending or creating a new circle on drag, I want to able to drag the circle around. I tried with the following code using .call(d3.behavior.drag()...) but don't know why it isn't working
Preview: http://jsbin.com/vabowofiqe/1/edit?html,output
code:
var svgContainer = d3.select("body").append("svg")
.attr("width", 800)
.attr("height", 803);
//Draw the Circle
var circle = svgContainer.append("circle")
.attr("cx", 35)
.attr("cy", 145)
.attr("r", 25)
.style("stroke-opacity", .9)
.style("stroke", "green")
.style("stroke-width", 2)
.style('cursor', 'move')
.style("fill", "white");
function move() {
d3.select(this)
.attr('x', d3.event.x)
.attr('y', d3.event.y);
};
var drag = d3.behavior.drag()
.origin(function ()
{
var t = d3.select(this);
return {x: t.attr("x"), y: t.attr("y")};
})
.on('dragend', function (d) {
var mouseCoordinates = d3.mouse(this);
if (mouseCoordinates[0] > 170) {
//Append new element
var newCircle = d3.select("svg").append("circle")
.classed("drg", true)
.attr("cx", 100)
.attr("cy", 100)
.attr("r", 20)
.attr("cx", mouseCoordinates[0])
.attr("cy", mouseCoordinates[1])
.style("fill", "white")
.style("stroke-width", 2)
.style("stroke", "#CDB483")
//Calling the drag behavior after clonning .call(
d3.behavior.drag()
.on('drag', move).origin(function () {
var t = d3.select(this);
return {x: t.attr("cx"), y: t.attr("cy")};
}));
;
}
});
circle.call(drag);
A SVG circle element doesn't have x and y properties, but cx and cy properties that can be used to position them. You should change your move function accordingly.
function move() {
d3.select(this)
.attr('cx', d3.event.x)
.attr('cy', d3.event.y);
};
Related
i can only generate one rect for the text i have. I dont know how to apply getBBox to multiple text elements so they can all have their own backgrounds. Does anyone know how i can accomplish this?
function jobsCreation() {
let enteringText = dataJobs
.enter()
.append("text")
.attr("x", function (d) { return d.posX })
.attr("y", function (d) { return d.posY })
.attr("id", "jobText")
.text(function (d) { return d.name });
let texts = d3.selectAll("#jobText");
let bbox = enteringText.node().getBBox();
console.log(bbox);
let rect = svg.append("rect")
.attr("x", bbox.x)
.attr("y", bbox.y)
.attr("width", bbox.width)
.attr("height", bbox.height)
.attr("id", "nodeBox")
.style("fill", "white")
.style("stroke", "black")
.style("stroke-width", "1.5px");
enteringText.raise();
}
svg.append() will create a single element and selection.node() will only return a single node.
While selection.nodes() will return an array all the nodes in a selection, we still need to append one rectangle for each node. To do this, we can use the array returned by selection.nodes() as a data array for a enter cycle:
let enteringRects = svg.selectAll("rect")
.data(enteringText.nodes())
.enter()
.append("rect")
.attr("x", function(node) { return node.getBBox().x })
.attr("y", function(node) { return node.getBBox().y })
.attr("width", function(node) { return node.getBBox().width })
.attr("height", function(node) { return node.getBBox().height })
.style("fill", "white")
.style("stroke", "black")
.style("stroke-width", "1.5px")
.lower();
Now we create one rectangle for every text element we create. As the bound datum in this selection is a DOM element we can access getBBox() easily as it is now a property of the datum itself.
let data = d3.range(15).map(function(d) { return Math.floor(Math.random() * 150) });
let width = 500;
let height = 300;
let svg = d3.select("body")
.append("svg")
.attr("width", width)
.attr("height", height);
let enteringText = svg.selectAll("text")
.data(data)
.enter()
.append("text")
.attr("x", function (d) { return Math.random() * width })
.attr("y", function (d) { return Math.random() * height })
.text(function (d) { return d });
let enteringRects = svg.selectAll("rect")
.data(enteringText.nodes())
.enter()
.append("rect")
.attr("x", function(node) { return node.getBBox().x })
.attr("y", function(node) { return node.getBBox().y })
.attr("width", function(node) { return node.getBBox().width })
.attr("height", function(node) { return node.getBBox().height })
.style("fill", "white")
.style("stroke", "black")
.style("stroke-width", "1.5px")
.lower();
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
I have been trying to get the basics of how I make a pannable zoomable, and click to center zoom on element d3 work. This example is what I want to do but I am having trouble translating it outside of the geo context: https://bl.ocks.org/mbostock/2206340
What I have accomplished is the first two parts pan and zoom, see a basic fiddle here https://jsfiddle.net/e9fbn2xp/
How can I accomplish centering the the circle in the center of the viewable window, so it looks like the circle is zoomed to? Note that although this is a fixed position circle I will eventually have dynamic data, so ideally I could reference the circles position dynamically.
Here is my code:
HTML (note that this is React JSX syntax but that should be irrelevant to question)
<div style={{width: 800}}>
<svg style={{border: '1px solid black'}} id="viz" width="800" height="800">
</svg>
</div>
JAVASCRIPT
var svg = d3.select("#viz")
var width = svg.attr("width");
var height = svg.attr("height");
var testLayer = svg.append('g');
var aRect = testLayer.append("rect")
.attr("x", 0)
.attr("y", 0)
.attr("height", 800)
.attr("width", 800)
.attr("fill", 'green');
var aCircle = testLayer.append("circle")
.style("stroke", "gray")
.style("fill", "white")
.attr("r", 40)
.attr("cx", 200)
.attr("cy", 200)
.on("mousedown", zoomToMe);
function zoomToMe(){
console.log("do the zoom")
}
var zoom = d3.zoom()
.scaleExtent([.5, 40])
.translateExtent([[0, 0], [width, height]])
.on("zoom", zoomed);
svg.call(zoom);
function zoomed() {
testLayer.attr("transform", d3.event.transform);
}
svg.on("click", function() {
var coords = d3.mouse(this);
})
I got a working solution and thought I would share the code in case others find it useful. It is a fairly different approach then my original but accomplishes the three goals, pan, mouse zoom, zoom to element. While these are three simple static circles the same concept should work with a dynamic dataset.
fiddle: https://jsfiddle.net/uc7oprx3/5/
HTML
<svg id="viz" width="400" height="400" />
JAVASCRIPT
var zoom = d3.zoom()
.scaleExtent([0.3,2])
.on("zoom", zoomed);
var svg = d3.select("#viz")
var width = svg.attr("width");
var height = svg.attr("height");
var zoomer = svg.append("rect")
.attr("width", width)
.attr("height", height)
.style("fill", "none")
.style("pointer-events", "all")
.call(zoom);
var g = svg.append("g");
var aCircle = g.append("circle")
.style("stroke", "gray")
.style("fill", "white")
.attr("r", 40)
.attr("cx", 200)
.attr("cy", 200)
.on("mousedown", () => centerNode(200, 200));
var bCircle = g.append("circle")
.style("stroke", "gray")
.style("fill", "white")
.attr("r", 40)
.attr("cx", 400)
.attr("cy", 400)
.on("mousedown", () => centerNode(400, 400));
var cCircle = g.append("circle")
.style("stroke", "gray")
.style("fill", "white")
.attr("r", 40)
.attr("cx", 600)
.attr("cy", 600)
.on("mousedown", () => centerNode(600, 600));
function zoomed() {
g.attr("transform", d3.event.transform);
}
function centerNode(xx, yy){
g.transition()
.duration(500)
.attr("transform", "translate(" + (width/2 - xx) + "," + (height/2 - yy) + ")scale(" + 1 + ")")
.on("end", function(){ zoomer.call(zoom.transform, d3.zoomIdentity.translate((width/2 - xx),(height/2 - yy)).scale(1))});
}
I am working on a population pyramid that has an updating function.
http://bricbracs.com/hh/
As you can see the bars expand and contract in a horizontal line when you update it with new data. I want to modify the transition effect so that the bars enter and exit vertically like this:
http://vis.stanford.edu/jheer/d3/pyramid/shift.html
I have been following this tutorial and modifying the code but so far no luck.
https://strongriley.github.io/d3/tutorial/bar-2.html
Here is the code that first draws the bars on loading. (this is the male bar group, the female bar group is the same)
leftBarGroup.selectAll('.bar.left')
.data(data)
.enter()
.append('rect')
.attr('class', 'bar left')
.attr('y', function(d) { return yScale(d.group); })
.attr("width", 0)
.attr("opacity", 0)
.transition()
.duration(500)
.attr('width', function(d) { return xScale(d.male); })
.attr('height', yScale.rangeBand())
.attr("opacity", 1)
And here is the corresponding part of the code in the updating function which changes the bars.
var sel = leftBarGroup.selectAll('.bar.left')
.attr('class', 'bar left')
.data(data)
.data(data, function(d) { return d.male; })
.transition()
.attr('y',0)
.duration(500)
.attr('y', function(d) { return yScale(d.group); })
.attr('height', yScale.rangeBand())
.attr('width', function(d) { return xScale(d.male); })
.attr('height', yScale.rangeBand())
Thanks in advance.
Here's one way to reproduce the effect in your linked example. I offset the bars and then slide them back into place. You then handle the top and bottom bars slightly different.
Note, I only did the slide down on the male side of the pyramid, if you need help going the rest of the way just leave me a comment.
var sel = leftBarGroup.selectAll('.bar.left')
.attr('class', 'bar left')
.data(data)
.data(data, function(d) {
return d.male;
})
// offset y to slide down
.attr('y', function(d){
var self = d3.select(this);
return +self.attr('y') - yScale.rangeBand();
})
.transition()
.duration(500)
// slide it back into place
.attr('y', function(d) {
return yScale(d.group);
})
// and set new width
.attr('width', function(d) {
return xScale(d.male);
});
// for the very top bar
// not only slide it but "fade it in"
leftBarGroup.select(':last-child')
.style('opacity', 0)
.transition()
.duration(500)
.style('opacity', 1)
.attr('y', function(d) {
return yScale(d.group);
});
// append a fake bar on the bottom
// to slide and fade out
leftBarGroup.append('rect')
.attr('y', function(d) {
return yScale('0-4');
})
.attr('height', yScale.rangeBand())
.attr('width', function(){
return leftBarGroup.select(':first-child').attr('width');
})
.style('fill', 'steelblue')
.style('opacity', 0.6)
.transition()
.duration(500)
.attr('y', function(d) {
return yScale('0-4') + yScale.rangeBand();
})
.style('opacity', 0)
.remove();
EDITS
Going up is just a matter of reversing the logic:
var sel = leftBarGroup.selectAll('.bar.left')
.attr('class', 'bar left')
.data(data)
.data(data, function(d) {
return d.male;
})
// offset y to slide up
.attr('y', function(d){
var self = d3.select(this);
return +self.attr('y') + yScale.rangeBand()
})
.transition()
.duration(500)
// slide it back into place
.attr('y', function(d) {
return yScale(d.group);
})
// and set new width
.attr('width', function(d) {
return xScale(d.male);
});
// for the very bottom bar
// not only slide it but "fade it in"
leftBarGroup.select(':first-child')
.style('opacity', 0)
.transition()
.duration(500)
.style('opacity', 1)
.attr('y', function(d) {
return yScale(d.group);
});
// append a fake bar on the top
// to slide and fade out
var w = leftBarGroup.select(':last-child').attr('width');
leftBarGroup.append('rect')
.attr('class','fake')
.attr('y', function(d) {
return yScale('90+');
})
.attr('height', yScale.rangeBand())
.attr('width', w)
.style('fill', 'steelblue')
.style('opacity', 0.6)
.transition()
.duration(500)
.attr('y', function(d) {
return yScale('90+') - yScale.rangeBand();
})
.style('opacity', 0)
.remove();
Updated code sample here.
I have a chart with 2 lines using d3.js.Both the line comes from different dataset but y axis range is same for both the lines. I am displaying the nodes for both the line.Nodes for first line appears fine.But nodes for second line doesn't appear.Can anybody please tell me what is the problem?? Here is my code for nodes and line.
var vis = d3.select('#visualisation'),
WIDTH = 400,
HEIGHT = 400,
MARGINS = { top: 20, right: 20, bottom: 20, left: 50 },
xRange = d3.scale.ordinal().rangeBands([MARGINS.left, WIDTH - MARGINS.right], 0.4).domain(barData.map(function (d) { return d.x; })),
y1Range = d3.scale.linear().range([HEIGHT - MARGINS.top, MARGINS.bottom]).domain([0, d3.max(barData1, function (d) { return d.y1; })]),
xAxis = d3.svg.axis().scale(xRange).tickSize(5);
y1Axis = d3.svg.axis().scale(y1Range).tickSize(5).orient("right").tickSubdivide(true);
/*Draw X Axis values*/
vis.append('svg:g')
.attr('class', 'x axis')
.attr('transform', 'translate(0,' + (HEIGHT-MARGINS.bottom) + ')')
.call(xAxis);
/*Draw Y1 Axis values*/
vis.append('svg:g')
.attr('class', 'y axis')
.attr('transform', 'translate(' + (HEIGHT - MARGINS.bottom) + ',0)')
.call(y1Axis);
/*Draw the First Line*/
var lineFunc = d3.svg.line()
.x(function (d) {
return (xRange(d.x))+MARGINS.right;
})
.y(function (d) {
return y1Range(d.y1);
})
.interpolate('linear');
/*Animate the line*/
var path1 = vis.append('svg:path')
.attr("d", lineFunc(barData1))
.attr("stroke", "#00CC66")
.attr("stroke-width", 2)
.attr("fill", "none");
var totalLength = path1.node().getTotalLength();
path1.attr("stroke-dasharray", totalLength + " " + totalLength)
.attr("stroke-dashoffset", totalLength)
.transition()
.duration(1000)
.ease("linear")
.attr("stroke-dashoffset", 0);
/*Draw the circles*/
var circles = vis.selectAll("circle").data(barData1);
circles.enter()
.insert("circle")
.attr("cx", function (d) { return (xRange(d.x))+MARGINS.right; })
.attr("cy", function (d) { return y1Range(d.y1); })
.attr("r", 3)
.style("fill", "#00CC66");
/*Draw the Second Line*/
var lineFunc1 = d3.svg.line()
.x(function (d) {
return (xRange(d.x))+MARGINS.right;
})
.y(function (d) {
return y1Range(d.y2);
})
.interpolate('linear');
var path2= vis.append('svg:path')
.attr("d", lineFunc1(barData2))
.attr("stroke", "#CB347D")
.attr("stroke-width", 2)
.attr("fill", "none")
.attr('class', 'line');
var totalLength = path1.node().getTotalLength();
path2.attr("stroke-dasharray", totalLength + " " +totalLength)
.attr("stroke-dashoffset", totalLength)
.transition()
.duration(1000)
.ease("linear")
.attr("stroke-dashoffset", 0);
/*Draw the circles for second line*/
var circles = vis.selectAll("circle").data(barData2);
circles.enter()
.insert("circle")
.attr("cx1", function (d) { return (xRange(d.x)) + MARGINS.right; })
.attr("cy2", function (d) { return y1Range(d.y2); })
.attr("r", 3)
.style("fill", "#CB347D");
The problem is that when adding the second set of circles, you're selecting the first set that has just been created:
vis.selectAll("circle").data(barData2)
This selection will contain all the circles you've just added. Then you're matching the data to it, which is fine, but the enter selection will be empty (all data items matched to existing circles). Therefore, the following code, which operates only on the enter selection, does nothing.
The easiest way to fix this is to add a distinguishing class to the second set of circles (and ideally the first one as well):
var circles = vis.selectAll("circle.second").data(barData2);
circles.enter()
.insert("circle")
.attr("class", "second")
// ...
Here is a plunker modified from mbostock
I want to make the text labels drag-able and attach a line to the circle when dragged.
.call(drag) works on the dots but not the labels
label = container.append("g")
.attr("class", "label")
.selectAll(".label")
.data(dots)
.enter().append("text")
.text(function(d) {return d.x + d.y; })
.attr("x", function(d) {return d.x; })
.attr("y", function(d) {return d.y; })
.attr("text-anchor", "middle")
.call(drag)
Here's a JSFiddle I made to demonstrate draggable text labels in D3.js
https://jsfiddle.net/h1n6fuwr/
Essentially you want to define the following variables/functions:
const drag = d3.behavior.drag()
.origin(function(d) { return d; })
.on("dragstart", dragstarted)
.on("drag", dragged)
.on("dragend", dragended)
function dragstarted(d) {
d3.event.sourceEvent.stopPropagation();
}
function dragged(d) {
const elem = d3.select(this)
elem.attr('x', +elem.attr('x') + d3.event.dx)
elem.attr('y', +elem.attr('y') + d3.event.dy)
}
function dragended(d) {}
And then call .call(drag) on your text labels.
const labels = ['Drag Me1', 'Drag Me2', 'Drag Me3']
d3.select('svg')
.selectAll('text')
.data(labels)
.enter()
.append('text')
.text(d => d)
.attr('fill', 'green')
.attr('x', (d, i) => 10 + i*30)
.attr('y', (d, i) => 15 + i*30)
.call(drag)
Append a rect behind the text, then .call(drag) on your rect. To get a suitable rect, you can use text.getBBox().