I'm trying to use d3 to make a circle that is draggable, but leaves a copy of the original in place. Here is the circle:
g.selectAll('circle')
.data([{
cx: 90,
cy: 80,
r: 30 }])
.enter().append('circle')
.attr('cx', function (d) {return d.cx})
.attr('cy', function (d) {return d.cy})
.attr('r', function(d) {return d.r})
.attr('class','original')
.call(dragOriginal);
Here is the drag behavior:
var dragOriginal = d3.behavior.drag()
.on('dragstart', cloneSpeciesMaker)
.on('drag', function (d, i) {
d.cx += d3.event.dx;
d.cy += d3.event.dy;
d3.select(this).attr('cx', d.cx).attr('cy', d.cy)
});
And here is the dragstart function:
function cloneSpeciesMaker(d) {
var svg = d3.select('svg');
//original becomes copy
d3.select(this)
.classed('original',false)
.attr('class','copy');
// creates new 'original' in place
var data = [{cx:d.cx,cy:d.cy,r:d.r}];
svg.append('circle')
.data(data)
.attr('class','original')
.attr("cx",function(d) {return d.x})
.attr("cy",function(d) {return d.y})
.attr("r",function(d) {return d.r})
.style("fill","purple")
.attr("class","original")
.call(dragOriginal);
}
Right now, I'm succeeding in making the original circle become a 'copy' and dragging it around, but it the part where I append a new circle in its old place isn't working, can anyone explain why?
One problem I can see from the code is in this section:
function cloneSpeciesMaker(d) {
var svg = d3.select('svg');
//original becomes copy
d3.select(this)
.classed('original',false)
.attr('class','copy');
// creates new 'original' in place
var data = [{cx:d.cx,cy:d.cy,r:d.r}];
svg.append('circle')
.data(data)
.attr('class','original')
.attr("cx",function(d) {return d.x})
.attr("cy",function(d) {return d.y})
.attr("r",function(d) {return d.r})
.style("fill","purple")
.attr("class","original")
.call(dragOriginal);
}
You are setting the data like this
var data = [{cx:d.cx,cy:d.cy,r:d.r}];
But you are doing which is incorrect d.x and d.y is not defined by you in data.
.attr("cx",function(d) {return d.x})
.attr("cy",function(d) {return d.y})
This should have been:
.attr("cx",function(d) {return d.cx})
.attr("cy",function(d) {return d.cy})
Related
I'm attempting my first foray into D3.js - the aim is a grouped multiline chart with draggable points, in which dragging a point results in the connecting line being updated too. Eventually, the updated data should be passed back to r (via r2d3() ). So far I managed to get the base plot and to make the points draggable... but when it comes to updating the line (and passing back the data?) I have been hitting a wall for hours now. Hoping someone might help me out?
I'm pasting the full script because I don't trust myself to not have done something truly unexpected anywhere. The code to do with dragging is all positioned at the bottom. In it's current form, dragging a circle makes the first of the lines (the lightblue one) disappear - regardless of which of the circles is dragged. On draggend the lines are drawn again with the default (smaller) stroke, which is of course not how it should work in the end. Ideally the line moves with the drag movement, althoug I'd also be happy if an updated line was drawn back again only after the drag ended.
I think that what I need to know is how to get the identifying info from the dragged circle, use it to update the corresponding variable in data (data is in wide format, btw), and update the corresponding path.
bonus question: drag doesn't work when making x scaleOrdinal (as intended). Is there a workaround for this?
// !preview r2d3 data= data.frame(id = c(1,1,2,2,3,3,4,4,5,5), tt = c(1, 2, 1, 2, 1, 2, 1, 2, 1, 2), val = c(14.4, 19.3, 22.0, 27.0, 20.7, 25.74, 16.9, 21.9, 18.6, 23.6))
var dById = d3.nest()
.key(function(d) {
return d.id;
})
.entries(data);
var margin = {
top: 40,
right: 40,
bottom: 40,
left: 40
},
width = 450 - margin.left - margin.right,
height = 300 - margin.top - margin.bottom;
var color = d3.scaleOrdinal()
.range(["#a6cee3", "#1f78b4", "#b2df8a", "#33a02c", "#fb9a99"]);
var x = d3.scaleLinear()
.range([0.25 * width, 0.75 * width])
.domain([1, 2]);
var y = d3.scaleLinear()
.rangeRound([height, 0])
.domain([0, d3.max(data, function(d) {
return d.val * 1.1;
})]);
var xAxis = d3.axisBottom(x),
yAxis = d3.axisLeft(y);
// Define the line by data variables
var connectLine = d3.line()
.x(function(d) {
return x(d.tt);
})
.y(function(d) {
return y(d.val);
});
svg.append('rect')
.attr('class', 'zoom')
.attr('cursor', 'move')
.attr('fill', 'none')
.attr('pointer-events', 'all')
.attr('width', width)
.attr('height', height)
.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
var focus = svg.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
focus.selectAll('lines')
.data(dById)
.enter().append("path")
.attr("class", "line")
.attr("d", function(d) {
return connectLine(d.values);
})
.attr("stroke", function(d) {
return color(d.key);
})
.attr('stroke-width', 4);
focus.selectAll('circles')
.data(dById)
.enter().append("g")
.attr("class", "dots")
.selectAll("circle")
.data(function(d) {
return d.values;
})
.enter().append("circle")
.attr("cx", function(d) {
return x(d.tt);
})
.attr("cy", function(d) {
return y(d.val);
})
.attr("r", 6)
.style('cursor', 'pointer')
.attr("fill", function(d) {
return color(d.id);
})
.attr("stroke", function(d) {
return color(d.id);
});
focus.append('g')
.attr('class', 'axis axis--x')
.attr('transform', 'translate(0,' + height + ')')
.call(xAxis);
focus.append('g')
.attr('class', 'axis axis--y')
.call(yAxis);
/// drag stuff:
let drag = d3.drag()
.on('start', dragstarted)
.on('drag', dragged)
.on('end', dragended);
focus.selectAll('circle')
.call(drag);
// focus.selectAll('line')
// .call(drag);
function dragstarted(d) {
d3.select(this).raise().classed('active', true);
dragID = Math.round(x.invert(d3.event.x));
// get x at start in order to force the dragged circle to stay at this x-value (i.e. allow it to vertically only)
}
function dragged(d) {
dragNewY = y.invert(d3.event.y);
d3.select(this)
.attr('cx', x(dragID))
.attr('cy', y(dragNewY));
// focus.selectAll('path')
// .attr("d", function(d) { return connectLine(d); }); // removes all lines (to be redrawn at dragended with a smaller stroke)
focus.select('path').attr("d", function(d) {
return connectLine(d);
}); // removes first lines (to be redrawn at dragended with a smaller stroke)
// How do I select only the line associated with the dragged circle?
}
function dragended(d) {
d3.select(this).classed('active', false);
focus.selectAll('lines')
.data(dById)
.enter().append("path")
.attr("class", "line")
.attr("d", function(d) {
return connectLine(d.values);
})
.attr("stroke", function(d) {
return color(d.key);
});
}
Update the data point associated with the circle and then update the circle and all the lines.
Do not add new lines in the dragend()
function dragged(d) {
dragNewY = y.invert(d3.event.y);
d.val = dragNewY;
d3.select(this)
.attr('cx', d => x(d.tt))
.attr('cy', d => y(d.val));
// focus.selectAll('path')
// .attr("d", function(d) { return connectLine(d); }); // removes all lines (to be redrawn at dragended with a smaller stroke)
focus.selectAll('path').attr("d", function(d) {
return connectLine(d.values);
}); // removes first lines (to be redrawn at dragended with a smaller stroke)
// How do I select only the line associated with the dragged circle?
}
function dragended(d) {
d3.select(this).classed('active', false);
// focus.selectAll('lines')
// .data(dById)
// .enter().append("path")
// .attr("class", "line")
// .attr("d", function(d) { return connectLine(d.values); })
// .attr("stroke", function(d) { return color(d.key); });
}
I have a graph with a line, two areas and a few circles in it:
I wrote an update method, which works just fine for everything except the circles:
function updateGraph() {
xScale.domain([startDate.getTime(), endDate.getTime()]);
addTimedTickPadding(xAxis);
yScale.domain([0, d3.max(interpolatedData, function (d) {
return d.y0 + d.y1;
})]);
var updateGroup = parentGroup.transition();
updateGroup.select('.xaxis')
.duration(750)
.call(xAxis);
updateGroup.select('.yxaxis')
.duration(750)
.call(yAxis);
updateGroup.select('.xgrid')
.duration(750)
.call(verticalGridLine());
updateGroup.select('.area-top')
.duration(750)
.attr('d', areaTop(interpolatedData));
updateGroup.select('.area-bottom')
.duration(750)
.attr('d', areaBottom(interpolatedData));
updateGroup.select('.line')
.duration(750)
.attr('d', line(interpolatedData));
updateGroup.select('.post')
.duration(750)
.data(interpolatedData)
.attr('cx', function (d) {
return xScale(dateParser.parse(d.x))
})
.attr('cy', function (d) {
return yScale(d.y0 + d.y1)
});
}
Everything transitions as expected but the circles. They just stay in place. My thought was: I append the updated data to each element and alter the cx and cy attributes accordingly, so the circles transition to their new place. But they don't move :( What am I doing wrong here?
Here is the initial code for the circles:
dataGroup
.selectAll('.post')
.data(interpolatedData)
.enter()
.append('circle')
.classed('post', true)
.attr({
'r': circleRadius,
'fill': circleFillColor,
'stroke-width': circleStrokeWidth,
'stroke': circleStrokeColor,
'cx': (function (d) {
return xScale(dateParser.parse(d.x))
}),
'cy': (function (d) {
return yScale(d.y0 + d.y1)
})
});
PS: I read abaout enter() and exit() here but don't really know, how they could be useful for updating the data which is appended on the elements.
PPS: I tried selectAll() too, but it doesn't seem to have a data() function.
I have a set of circles defined as
nodes = [{
x: xRange(xvalue),
y: yRange(getY(xvalue)),
...
}]
vis.selectAll(".nodes")
.data(nodes)
.enter().append("circle")
.attr("class", "nodes")
.attr("cx", function (d) {
return d.x;
(coordinate display)
})
.attr("cy", function (d) {
return d.y;
})
.attr("r", "7px")
.attr("fill", "black")
.attr("transform", function (p) {
return "translate(" + p.x + "," + p.y + ")";
})
The problem that I am having with these circles is that the coordinates taken from nodes are undefined when the circles are being defined, despite being defined everywhere else. Here is a test case to represent this problem, where the coordinates of one of the dots should be displayed, but isn't, since it seems to be undefined. To prove that the axes work, I have placed a dot in the first quadrant of the graph. Is there any reason for why this is happening?
You're returning before you run the logic :
.attr("cx", function (d) {
return d.x;
(coordinate display)
})
change to :
.attr("cx", function (d) {
(coordinate display)
return d.x;
})
I have modified this sunburst diagram in D3 and would like to add text labels and some other effects. I have tried to adopt every example I could find but without luck. Looks like I'm not quite there yet with D3 :(
For labels, I would like to only use names of top/parent nodes and that they appear outside of the diagram (as per the image below). This doesn't quite work:
var label = svg.datum(root)
.selectAll("text")
.data(partition.nodes(root).slice(0,3)) // just top/parent nodes?
.enter().append("text")
.attr("class", "label")
.attr("x", 0) // middle of arc
.attr("dy", -10) // outside last children arcs
/*
.attr("transform", function(d) {
var angle = (d.x + d.dx / 2) * 180 / Math.PI - 90;
console.log(d, angle);
if (Math.floor(angle) == 119) {
console.log("Flip", d)
return ""
} else {
//return "scale(-1 -1)"
}
})
*/
.append("textPath")
.attr("xlink:href", function(d, i) { return "#path_" + i; })
.text(function(d) { return d.name + " X%"; });
I would also like to modify a whole tree branch on hover so that it 'shifts' outwards. How would I accomplish that?
function mouseover(d) {
d3.select(this) // current element and all its children
.transition()
.duration(250)
.style("fill", function(d) { return color((d.children ? d : d.parent).name); });
// shift arcs outwards
}
function mouseout(d) {
d3.selectAll("path")
.transition()
.duration(250)
.style("fill", "#fff");
// bring arcs back
}
Next, I'd like to add extra lines/ticks on the outside of the diagram that correspond to boundaries of top/parent nodes, highlighting them. Something along these lines:
var ticks = svg.datum(root).selectAll("line")
.data(partition.nodes) // just top/parent nodes?
.enter().append("svg:line")
.style("fill", "none")
.style("stroke", "#f00");
ticks
.transition()
.ease("elastic")
.duration(750)
.attr("x1", function(d) { return Math.max(0, Math.min(2 * Math.PI, x(d.x))); })
.attr("y1", function(d) { return Math.max(0, y(d.y + d.dy)); })
.attr("x2", function(d) { return Math.max(0, Math.min(2 * Math.PI, x(d.x))); })
.attr("y2", function(d) { return Math.max(0, y(d.y + d.dy) + radius/10); });
Finally, I would like to limit zoom level so the last nodes in the tree do not fire zoom but instead launch a URL (which will be added in JSON file). How would I modify the below?
function click(d) {
node = d;
path.transition()
.duration(750)
.attrTween("d", arcTweenZoom(d));
}
My full pen here.
Any help with this would be much appreciated.
I am trying to make tooltip like: http://jsfiddle.net/6cJ5c/10/ for my graph and that is the result on my realtime graph: http://jsfiddle.net/QBDGB/52/ I am wondering why there is a gap between the circles and the graph and why at the beginning there is a vertical line of circles? When it starts the circles are close to the curve but suddendly they start to jump up and down !! I want the circles to move smooothly and stick on the surface of the curve. I think the problem is that they are not moving with the "path1" and so it does not recognize the circles and thats why they are moving separetly or maybe the value of tooltipis are different of the value of the curve so they do not overlap!. That is how the data is generated ( value and time) and the tooltip:
var data1 = initialise();
var data1s = data1;
function initialise() {
var arr = [];
for (var i = 0; i < n; i++) {
var obj = {
time: Date.now(),
value: Math.floor(Math.random() * 90)
};
arr.push(obj);
}
return arr;
}
// push a new element on to the given array
function updateData(a) {
var obj = {
time: Date.now(),
value: Math.floor(Math.random() * 90)
};
a.push(obj);
}
var formatTime = d3.time.format("%H:%M:%S");
//tooltip
var div = d3.select("body").append("div")
.attr("class", "tooltip")
.style("opacity", 0);
var blueCircles = svg.selectAll("dot")
.data(data1s)
.enter().append("circle")
.attr("r", 3)
.attr("cx", function(d) { return x(d.time); })
.attr("cy", function(d) { return y(d.value); })
.style("fill", "white")
.style("stroke", "red")
.style("stroke-width", "2px")
.on("mousemove", function(d ,i) {
div.transition()
.duration(650)
.style("opacity", .9);
div.html(formatTime(new Date(d.time)) + "<br/>" + d.value)
.style("left", (d3.event.pageX) + "px")
.style("top", (d3.event.pageY - 28) + "px");
})
.on("mouseout", function(d ,i ) {
div.transition()
.duration(650)
.style("opacity", 0);
});
blueCircles.data(data1s)
.transition()
.duration(650)
.attr("cx", function(d) { return x(d.time); })
.attr("cy", function(d) { return y(d.value); });
Please kindly tell me your opinions since I really need it :(
As I said maybe I should add "mouseover and mouse move functions" to the "path" to make it recognize the tooltip. something like following. but I am nor really sure :(
var path1 = svg.append("g")
.attr("clip-path", "url(#clip)")
.append("path")
.data([data1])
.attr("class", "line1")
.on("mouseover", mouseover)
.on("mousemove", mousemove)
.on("mouseout", mouseout);
I think your problem lies in the interpolation of your paths. You set the interpolation between points on your var area to "basis", which I found is a B-spline interpolation. This means the area drawn does not go through the points in your dataset, as shown in this example:
The path your points move over, though, are just straight lines between the points in your dataset. I updated and changed the interpolation from basic to linear, to demonstrate that it will work that way. I also set the ease() for the movement to linear, which makes it less 'jumpy'. http://jsfiddle.net/QBDGB/53/