I have been struggling with this issue for the past couple days: I have a force directed graph that labels its edges just like this example does it. The problem I am facing is that when the graph updates (ie: a node on the graph is added upon a user's click) it updates the graph but it leaves the old edge labels that I wrote previously behind:
BEFORE & AFTER A NEW GRAPH IS APPENDED:
As you can see, my edge labels are hanging around after an update. I have a function that is called everytime new data comes in, and in this function I have the following code that draws the labels:
path_text = svg.selectAll(".path")
.data(force.links(), function(d){ return d.name;})
.enter().append("svg:g");
path_text.append("svg:text")
.attr("class","path-text")
.text(function(d) { return d.data.label; });
The svg variable is declared once at a top level closure like so:
var svg = d3.select("body").append("svg:svg")
.attr("viewBox", "0 0 " + width + " " + height)
.attr("preserveAspectRatio", "xMidYMid meet");
My graph has a tick() function that calculates the location of each label like so:
function tick()
{
// Line label
path_text.attr("transform", function(d)
{
var dx = (d.target.x - d.source.x),
dy = (d.target.y - d.source.y);
var dr = Math.sqrt(dx * dx + dy * dy);
var sinus = dy/dr;
var cosinus = dx/dr;
var l = d.data.label.length * 6;
var offset = (1 - (l / dr )) / 2;
var x=(d.source.x + dx*offset);
var y=(d.source.y + dy*offset);
return "translate(" + x + "," + y + ") matrix("+cosinus+", "+sinus+",
"+-sinus+", "+cosinus+", 0 , 0)";
});
.
.
.
I have tried moving this svg declaration down into the update function, so that this is instantiated each time there is a graph change. This actually works - but it makes an entire duplicate of the entire graph. The first, original copy still keeps the old labels - but the second copy acts exactly how I want it to. Is there a way, perhaps, instead of appending svg, there is a way of replacing? I have also tried calling exit().remove() without any luck as well.
Thank you so much for your time. This has been killing me as to how I'm supposed to do this.
I placed the svg declaration inside my graph update function, attached it to a div, and clear the div before appending it again:
jQuery('#v').empty();
var svg = d3.select("#v").append("svg:svg")
.attr("viewBox", "0 0 " + width + " " + height)
.attr("preserveAspectRatio", "xMidYMid meet");
Not the cleanest solution in my opinion, but will go with this unless you all have a better solution!
Related
I want to display a specific icon in different boxes using d3.
I have a data as an array of [x0, y0, x1, y1, vx, vy] where:
x0, y0 is the first corner of a box,
x1, y1 the second corner and
vx, vy two parameters (velocity)
that will be used to generate a SVG path.
I am using:
var boxes = nodeSVGGroup.selectAll("rect").data(nodes).enter().append("rect");
to generate my boxes and this is working well
My problem comes when I want to create the SVG path (icon) and properly render it in each box (it needs to be generated, translated and rotated to fit the center of each box.
I am using a similar pattern i.e.
var selection = nodeSVGGroup.selectAll(".barb").data(nodes);
selection.enter()
.append('g')
.each(function(node, i) {
var vx = node[4];
var vy = node[5];
var speed = Math.sqrt(vx*vx + vy*vy);
var angle = Math.atan2(vx, vy);
// generate a path based on speed. it looks like
// M2 L8 2 M0 0 L1 2 Z
var path = ...
var scale = 0.5*(node[1]-node[0])/8;
var g = d3.select(this).data([path,angle,scale]).enter().append('g');
// still need to add following transforms
// translate(' + node[2] + ', ' + node[3] + ')
// scale(' + scale + ')
// rotate(' + angle + ' ' + 0 + ' ' + 0 + ')
// translate(-8, -2)',
g.append('path').attr('d', function(d){console.log(d); return d[0];})
.attr('vector-effect', 'non-scaling-stroke');
})
.attr('class', 'wind-arrow');
I get the error Uncaught TypeError: Cannot read property 'ownerDocument' of null(…) which seems to be related to this line
.each(function(node, i) {
What am I doing wrong?
The full code is here
It's not entirely clear why are you trying to create an "enter" selection inside an each. I'm not sure if I understand your goals, but you can simply use d3.select(this) to append the path to each group:
d3.select(this).append('path')
.attr('d', path)
Here is the updated fiddle: https://jsfiddle.net/9x169eL1/
Drag and Drop Example
I am trying to rewrite part of this example above to use in my code, specifically this piece:
function centerNode(source) {
scale = zoomListener.scale();
x = -source.y0;
y = -source.x0;
x = x * scale + viewerWidth / 2;
y = y * scale + viewerHeight / 2;
d3.select('g').transition()
.duration(duration)
.attr("transform", "translate(" + x + "," + y + ")scale(" + scale + ")");
zoomListener.scale(scale);
zoomListener.translate([x, y]);
}
However I am getting stuck since the v4 package has changed quite a bit. I wrote my zoomListener function to be
var zoomListener = d3.zoom()
.scaleExtent([0.3,2])
.on("zoom", zoomed);
function zoomed() {
transform = d3.event.transform;
console.log(d3.event);
svg.attr("transform", transform);
}
function centerNode(source){
t = transform;
console.log(t);
x = t.x*t.k; //I only want things to be centered vertically
y = (t.y + -source.x0)*t.k + (viewerHeight)/2 ;
svg.transition()
.duration(duration)
.attr("transform","translate(" + x + "," + y +")scale(" + t.k + ")");
transform.scale(t.k); //DOES NOT WORK
transform.translate([x, y]); //DOES NOT WORK
}
and I know that according to the doc things have changed and info are no longer are stored on what would be my zoomListener
D3 V4 release note on zoom I guess I am just confused on how I am suppose to do it with the new version. The last few lines of my centerNode function don't work which has for effect that when I center the node the zooming and panning reset...
Any suggestion?
So after much digging and trial and error I cam up with an answer that works pretty well for my purposes. Note that this code below is only the relevant part of my code not the whole code, certain variable were self explanatory so did not include them. ALSO THIS IS IN VERSION 4 of d3.js.
var zoom = d3.zoom()
.scaleExtent([0.3,2])
.on("zoom", zoomed);
var svg = d3.select("body")
.append("svg")
.attr("width", viewerWidth)
.attr("height", viewerHeight);
var zoomer = svg.append("rect")
.attr("width", viewerWidth)
.attr("height", viewerHeight)
.style("fill", "none")
.style("pointer-events", "all")
.call(zoom);
var g = svg.append("g");
zoomer.call(zoom.transform, d3.zoomIdentity.translate(150,0)); //This is to pad my svg by a 150px on the left hand side
function zoomed() {
g.attr("transform", d3.event.transform);//The zoom and panning is affecting my G element which is a child of SVG
}
function centerNode(source){
t = d3.zoomTransform(zoomer.node());
console.log(t);
x = t.x;
y = source.x0;
y = -y *t.k + viewerHeight / 2;
g.transition()
.duration(duration)
.attr("transform", "translate(" + x + "," + y + ")scale(" + t.k + ")")
.on("end", function(){ zoomer.call(zoom.transform, d3.zoomIdentity.translate(x,y).scale(t.k))});
}
As per the examples for v4 on the d3.js page, I used a rectangle to apply the zoom to
The zoom behavior is applied to an invisible rect overlaying the SVG
element; this ensures that it receives input, and that the pointer
coordinates are not affected by the zoom behavior’s transform. Pan & Zoom Example
In the Center node function I am using d3.zoomTransform(zoomer.node()); to get the current transform applied to the page.
The purpose of this function is only to center the collapsible tree vertically not horizontally, so I am keeping the current transform.x (here t.x) the same.
The coordinate in my svg are flip hence why y= source.x0, source is a what node was clicked in my collapsible tree. ("Look to the example referenced to the top of this thread to understand what I am trying to convert to version 4)
I am apply the transformation to my G element and then I want to commit those changes to the zoom transform, to do so I use the .on("end", function(){}) otherwise it was doing weird behavior with the transition, by doing that all it does is setting the current state of the transform.
zoomer.call(zoom.transform, d3.zoomIdentity.translate(x,y).scale(t.k))
This line above is applying a translation of x and y and a scale -- that is equal to what the current state -- to the identiy matrix has to get a new transform for G, i then apply it to zoomer which is the element I called zoom on earlier.
This worked like a charm for me!
Calling transform.scale and transform.translate returns a new transform, and modifies nothing. Therefore:
transform = transform.translate([x, y]).scale(k)
svg.call(zoomListener.transform, newTransform)
(At this point zoomListener is a pretty inaccurate name for this, but regardless...)
k, x, and y can be derived from source, maybe as you show, but I'm not sure, because I don't know what source is. But to me, t.x*t.k looks suspicious, because it's multiplying the existing transforms x by its scale. Seems like it would cause a feedback loop.
For more into about the zoom in v4, check out this related StackOverflow post, or this example by mbostock demonstrating programmatic control over the zoom transform of an element (canvas in this case) and includes transitions.
Given the following speed dial, which is constructed using arcs in D3:
segmentArc = d3.svg.arc().outerRadius(radius - chartInset).innerRadius(radius - chartInset - barWidth).startAngle(arcStartRad + startPadRad).endAngle(arcEndRad - endPadRad);
How do I move the labels in each segment so that it appears right justified (at the end of each segment opposed to center)?
the labels are currently added likes this:
chart.append('text')
.attr('transform', () => {
var x = Math.round(segmentArc.centroid()[0]);
var y = Math.round(segmentArc.centroid()[1]);
return 'translate(' + x + ',' + y + ')';
})
.style("text-anchor", "middle")
.text(sectionLabel);
I solved this problem by replicating each segment arc, giving it a transparent fill and making it exactly twice as long. From there it is as simple as working out the centroid for the transparent arc.
I am trying to implement a D3 chart with multiple lines that shows the sample size at every point as a tooltip when hovering over the chart. I have the data as two arrays but I don't know how to access the second array relative to the position of the mouse on the chart.
How can I access the values in the second array with the correct index?
Here is a jsfiddle showing what I need to fix: http://jsfiddle.net/fMXvv/1/ on line 60 of the js, or the div.html method shown below:
graph.append("svg:path").attr("d", line(data[0])).style("stroke", "black").style("stroke-width", 2)
.on("mouseover", function (d, i) {
div.transition()
.duration(200)
.style("opacity", .9);
div.html("n = " + data[0][i])
.style("left", (d3.event.pageX) + "px")
.style("top", (d3.event.pageY - 28) + "px");
The difficulty comes from the fact that the chart has more than one series (3 lines), so knowing the x coordinate is not enough.
I used d3.event.pageY to map mouse coordinates using scale:
var toolTipScale = d3.scale.linear().domain([h + 80, 80]).range([0, max_value]);
and then you use:
div.html("n = " + Math.ceil(toolTipScale( d3.event.pageY)) )
To map mouse position correctly it is necessary to add css
body {
margin: 0;
padding: 0;
}
Here is jsfiddle of this - http://jsfiddle.net/cuckovic/sDnC8/
To access elements from second array you can use d3.event.pageX:
var iScale = d3.scale.linear().domain([w + 80, 80]).range([data[0].length, 0]);
and then:
div.html("n = " + data[1][Math.floor(iScale( d3.event.pageX))] )
jsfiddle: http://jsfiddle.net/cuckovic/sDnC8/5/
I had a problem that might have been similar to yours, I needed to select a time based on the mouse position, this is what I did (might give you some ideas):
var timeScale = d3.scale.linear()
.domain([startTime.getTime(), endTime.getTime()])
.range([30, r + 10])
.clamp(true);
axisOverlay.on("mousemove", mousemove)
.on("touchmove", mousemove);
function mousemove() {
selectedTime = new Date(timeScale.invert(d3.mouse(this)[1]));
}
I have a test site where I am building a d3-based force-directed network graph from my own data.
If I pick about five or six genes, the nodes in my graph start to get drawn outside the canvas.
What parts of the d3 API do I need to call to control zoom level, so that nodes do not disappear off the edge of the canvas?
If available, I would definitely appreciate any code snippets that briefly explain the concept, unless the implementation is fairly simple. Thanks for your advice.
D3 allows to use zoom and it's fairly easy to implement. You'll only need to wrap your graph inside a "g" element that I'll call "viewport". Then you'll assign to the zoom event of the svg element:
svg.call(d3.behavior.zoom().on("zoom", redraw))
the following function:
function redraw() {
d3.select("#viewport").attr("transform",
"translate(" + d3.event.translate + ")"
+ " scale(" + d3.event.scale + ")");
}
var x, y, k;
if (d && centered !== d) {
var centroid = path.centroid(d);
x = centroid[0];
y = centroid[1];
k = 4;
centered = d;
} else {
x = w / 2;
y = h / 2;
k = 1;
centered = null;
}
write the following code in zoom function
svg.attr("transform", "translate(" + w / 2 + "," + h / 2 + ")scale(" + k + ")translate(" + -x + "," + -y + ")");
for absolute scale you cann use this code
this will be helpfull for maps zooming and panning