d3-zoom breaks when cursor is over an inner svg element - javascript

I have implemented d3-zoom by following this brief tutorial.
I'm using https://d3js.org/d3.v5.min.js. This is my first project with d3.
My goal is to have a kind of floor plan showing booth tables at a venue. Similar to the tutorial, I've drawn shape elements from an array. In my case I've entered an array of booth information into a grid of elements.
The zoom functionality works just fine, except when my cursor is over the border or fill of one of my rectangles, or on the text of a element. If the point of my cursor is touching any of these elements, the zooming behavior stops working.
Try to zoom with the mousewheel with your cursor in blank space versus touching a shape or text.
I've tried to fit a console.log in somewhere to see what's not getting passed in the event, but have had trouble even finding where I can get the event argument.
Any help greatly appreciated! Here is my code:
var svg = d3.select("#venue-svg"); // this is my svg element
// the zoom rectangle. from the tutorial: '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.'
svg.append("rect")
.attr("width", "100%")
.attr("height", "100%")
.style("fill", "none")
.style("pointer-events", "all")
.call(
d3
.zoom()
.scaleExtent([1 / 2, 4])
.on("zoom", zoomed)
);
function zoomed() {
g.attr("transform", d3.event.transform);
}
// a parent <g> that holds everything else and is targeted
// for the transform (from the tutorial).
var g = svg.append("g");
// the groups that hold each booth table, associated org name, etc.
var tables = g
.selectAll("g")
.data(venueBooths)
.enter()
.append("g")
.attr("transform", function(d) {
return "translate(" + d.x + " " + d.y + ")";
});
var tableRects = tables
.append("rect")
.attr("stroke", "steelblue")
.attr("stroke-width", "2px")
.attr("width", function(d) {
return d.w;
})
.attr("height", function(d) {
return d.h;
})
.attr("fill", "none")
.attr("fill", function(d) {
return $.isEmptyObject(d.reservation) ? "none" : "#FF5733";
})
.attr("id", function(d) {
return "table-" + d.id;
});
tables
.append("text")
.text(function(d) {
return "Booth " + d.id;
})
.attr("dx", 5)
.attr("dy", 60)
.attr("font-size", "8px");
tables
.append("text")
.text(function(d) {
return d.reservation.orgName ? d.reservation.orgName : "Available";
})
.attr("dy", 15)
.attr("dx", 5)
.attr("font-size", "9px")
.attr("font-weight", "bold");

Try creating the rect in the end such that the DOM looks like this:
<svg>
<g></g>
<rect></rect>
</svg>
Since the zoom function is attached to the large rectangle, creating the smaller boxes above it prevents a zoom event from propagating to the large rectangle below them. It works for the boxes with a fill: none; since it behaves like a hollow box.
Try modifying the code to something like:
var svg = d3.select("#venue-svg"); // this is my svg element
// the zoom rectangle. from the tutorial: '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.'
function zoomed() {
g.attr("transform", d3.event.transform);
}
// a parent <g> that holds everything else and is targeted
// for the transform (from the tutorial).
var g = svg.append("g");
// the groups that hold each booth table, associated org name, etc.
var tables = g
.selectAll("g")
.data(venueBooths)
.enter()
.append("g")
.attr("transform", function(d) {
return "translate(" + d.x + " " + d.y + ")";
});
var tableRects = tables
.append("rect")
.attr("stroke", "steelblue")
.attr("stroke-width", "2px")
.attr("width", function(d) {
return d.w;
})
.attr("height", function(d) {
return d.h;
})
.attr("fill", "none")
.attr("fill", function(d) {
return $.isEmptyObject(d.reservation) ? "none" : "#FF5733";
})
.attr("id", function(d) {
return "table-" + d.id;
});
tables
.append("text")
.text(function(d) {
return "Booth " + d.id;
})
.attr("dx", 5)
.attr("dy", 60)
.attr("font-size", "8px");
tables
.append("text")
.text(function(d) {
return d.reservation.orgName ? d.reservation.orgName : "Available";
})
.attr("dy", 15)
.attr("dx", 5)
.attr("font-size", "9px")
.attr("font-weight", "bold");
svg.append("rect")
.attr("width", "100%")
.attr("height", "100%")
.style("fill", "none")
.style("pointer-events", "all")
.call(
d3
.zoom()
.scaleExtent([1 / 2, 4])
.on("zoom", zoomed)
);

Related

How to append multiple items to a force simulation node?

I'm using D3 v4 and can't seem to get multiple items to append to a node. In the code below I'm trying to get text to appear with the image as part of my force simulation. Both the image and text need to move together around the screen. It works perfectly if I only append either the image or the text but I can't get it to group both. When I run this it just shows 1 node in the corner.
this.node = this.d3Graph.append("g")
.attr("class", "nodes")
.selectAll("circle")
.data(Nodes)
.enter()
.append("svg:image")
.attr("xlink:href", 'https://seeklogo.com/images/T/twitter-2012-negative-logo-5C6C1F1521-seeklogo.com.png')
.attr("height", 50)
.attr("width", 50)
.append("text")
.attr("x", 20)
.attr("y", 20)
.attr("fill", "black")
.text("test text");
this.force.on('tick', this.tickActions);
tickActions() {
this.node
.attr("transform", function(d) {
return "translate(" + d.x + "," + d.y + ")";
})
this.force
.restart()
}
You cannot append a <text> element to an <image> element. You have to append the <text> to the <g>.
The easiest solution is breaking your selection:
this.node = this.d3Graph.selectAll(null)
.data(Nodes)
.enter()
.append("g")
.attr("class", "nodes");
this.node.append("svg:image")
.attr("xlink:href", 'https://seeklogo.com/images/T/twitter-2012-negative-logo-5C6C1F1521-seeklogo.com.png')
.attr("height", 50)
.attr("width", 50);
this.node.append("text")
.attr("x", 20)
.attr("y", 20)
.attr("fill", "black")
.text("test text");
Here we use the data to create <g> elements in the enter selection. Then, to each <g> element, we append an <image> and a <text> as children.

Embed a svg shape in d3tip tooltip

I'm working with the popular tip library d3-tip.js, an example of it can be found here. Typically, the tip contains text that is defined dynamically like this:
var tip = d3.tip()
.attr('class', 'd3-tip')
.offset([-10, 0])
.html(function(d) {
html = "";
html += "<strong>Frequency:</strong> <span style='color:red'>" + d.frequency + "</span>";
return html;
})
However, lets say I have a legend like this:
var legend = g.append("g")
.attr("font-family", "sans-serif")
.attr("font-size", 10)
.attr("text-anchor", "end")
.selectAll("g")
.data(keys.slice().reverse())
.enter().append("g")
.attr("transform", function(d, i) { return "translate(0," + i * 20 + ")"; });
legend.append("rect")
.attr("x", width - 19)
.attr("width", 19)
.attr("height", 19)
.attr("fill", z);
legend.append("text")
.attr("x", width - 24)
.attr("y", 9.5)
.attr("dy", "0.32em")
.text(function(d) { return d; });
I would like to somehow append a small svg rect inside the d3 toolip. This way when you hover over a graph with different classes (i.e. grouped bar chart) the tooltip will have a svg rect of matching color in addition to the html text. Ideally by using an existing legend variable, as seen above.
If it's not possible, then just explain why and I can accept that as an answer as well.
For clarity, here is a rough idea of what I'm going for visually:
It's easy to create an SVG inside a d3.tip tooltip. Actually, you just have to use the same logic of any other D3 created SVG: select the container and append the SVG to it.
In the following demo, in your var tip, I'll create an empty div with a given ID. In this case, the div has an ID named mySVGtooltip:
var tool_tip = d3.tip()
.attr("class", "d3-tip")
.offset([20, 40])
.html("<div id='mySVGtooltip'></div>");
After that, it's just a matter of, inside the mouseover event, selecting that div by ID and appending the SVG to it:
var legendSVG = d3.select("#mySVGtooltip")
.append("svg")
.attr("width", 160)
.attr("height", 50);
Here is the demo, hover over the circles:
var svg = d3.select("body")
.append("svg")
.attr("width", 300)
.attr("height", 300);
var tool_tip = d3.tip()
.attr("class", "d3-tip")
.offset([20, 40])
.html("<div id='mySVGtooltip'></div>");
svg.call(tool_tip);
var data = [20, 10, 30, 15, 35];
var circles = svg.selectAll(null)
.data(data)
.enter()
.append("circle");
circles.attr("cy", 50)
.attr("cx", function(d, i) {
return 30 + 55 * i
})
.attr("r", function(d) {
return d
})
.attr("fill", "lightgreen")
.attr("stroke", "dimgray")
.on('mouseover', function(d) {
tool_tip.show();
var legendSVG = d3.select("#mySVGtooltip")
.append("svg")
.attr("width", 160)
.attr("height", 50);
var legend = legendSVG.append("g")
.attr("font-family", "sans-serif")
.attr("font-size", 10);
legend.append("text")
.attr("x", 80)
.attr("text-anchor", "middle")
.attr("y", 16)
.attr("font-size", 14)
.text("Age Group:");
legend.append("rect")
.attr("y", 25)
.attr("x", 10)
.attr("width", 19)
.attr("height", 19)
.attr("fill", "goldenrod");
legend.append("text")
.attr("x", 35)
.attr("y", 40)
.text(function() {
return d + " years and over";
});
})
.on('mouseout', tool_tip.hide);
.d3-tip {
line-height: 1;
background: gainsboro;
border: 1px solid black;
font-size: 12px;
}
p {
font-family: Helvetica;
}
<script src="https://d3js.org/d3.v4.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3-tip/0.7.1/d3-tip.min.js"></script>
Notice that, in this very simple demo, I'm using the datum (d) passed to the anonymous function by the mouseover event. I'm seeing in your question that you have your own data. Thus, change the code in my demo accordingly.

D3.js IE vs Chrome SVG not showing

I have a simple D3 donut diagram with a .mouseover() event that updates an SVG:text element at the center of the donut hole. It works great...
Until I encounter users with IE 9, 10 and 11. These browsers won't render the center label. Is there a way to accommodate IE and show the center label in both browsers?
The HTML page is based on HTML5BoilerPlate with the various shims to detect old browsers.
The D3 script seems pretty straight forward.
d3.json("data/census.php", function(error, dataset) {
var h = 220, w = 295;
var outerRadius = h / 2, innerRadius = w / 4;
var color = d3.scale.category20b();
var svg= d3.select("#dailycensus")
.append("svg")
.data([dataset])
.attr("width", w)
.attr("height", h)
.append("svg:g")
.attr("transform", "translate(" + outerRadius + "," + outerRadius + ")");
var arc = d3.svg.arc()
.innerRadius(innerRadius)
.outerRadius(outerRadius);
var pie = d3.layout.pie()
.value(function(d,i) { return +dataset[i].Census; });
var arcs = svg.selectAll("g.slice")
.data(pie)
.enter()
.append("svg:g")
.attr("class", "slice");
var syssum = d3.sum(dataset, function(d,i) { return +dataset[i].Census; });
var tip = d3.tip()
.attr("class", "d3-tip")
.html(String);
var formatter = d3.format(".1%");
svg.append("text")
.attr("id", "hospital")
.attr("class", "label")
.attr("y", -10)
.attr("x", 0)
.html("Health System Census"); // Default label text
svg.append("text")
.attr("id", "census")
.attr("class", "census")
.attr("y", 40)
.attr("x", 0)
.html(syssum); // Default label value
arcs.append("svg:path")
.call(tip) // Initialize the tooltip in the arc context
.attr("fill", function(d,i) { return color(i); }) // Color the arc
.attr("d", arc)
.on("mouseover", function(d,i) {
tip.show( formatter(dataset[i].Census/syssum) );
// Update the doughnut hole label with slice meta data
svg.select("#hospital").remove();
svg.select("#census").remove();
svg.append("text")
.attr("id", "hospital")
.attr("class", "label")
.attr("y", -10)
.attr("x", 0)
.html(dataset[i].Facility);
svg.append("text")
.attr("id", "census")
.attr("class", "census")
.attr("y", 40)
.attr("x", 0)
.html(+dataset[i].Census);
})
.on("mouseout", function(d) {
tip.hide();
// Return the doughnut hole label to the default label
svg.select("#hospital").remove();
svg.select("#census").remove();
svg.append("text")
.attr("id", "hospital")
.attr("class", "label")
.attr("y", -10)
.attr("x", 0)
.html("Health System Census");
svg.append("text")
.attr("id", "census")
.attr("class", "census")
.attr("y", 40)
.attr("x", 0)
.html(syssum);
})
Replace all the .html calls with .text calls. Generally innerHTML is for HTML things although browsers are giving it SVG support as everybody keeps expecting it to work.
It's not immediately clear what is causing the issue, however setting the .text property instead resolves the issue after testing with Fiddler:
svg.append("text")
.attr("id", "hospital")
.attr("class", "label")
.attr("y", -10)
.attr("x", 0)
.text(dataset[i].Facility);
svg.append("text")
.attr("id", "census")
.attr("class", "census")
.attr("y", 40)
.attr("x", 0)
.text(+dataset[i].Census);
})
After investigating the <text /> elements directly in the Developer Tools you can see that setting the .innerHTML property doesn't render the results you'd expect, however .textContent does.
If this is working as expected in both Chrome and Firefox, I'll gladly open up an interop bug for the IE team to look into. We've been doing some SVG work lately, so I may find that this has already been discussed.
I had the same issue and innerSvg polyfill helps me. Now html() in SVG works in IE.

D3js: Dragging a group by using one of it's children

Jsfiddle: http://jsfiddle.net/6NBy2/
Code:
var in_editor_drag = d3.behavior.drag()
.origin(function() {
var g = this.parentNode;
return {x: d3.transform(g.getAttribute("transform")).translate[0],
y: d3.transform(g.getAttribute("transform")).translate[1]};
})
.on("drag", function(d,i) {
g = this.parentNode;
translate = d3.transform(g.getAttribute("transform")).translate;
x = d3.event.dx + translate[0],
y = d3.event.dy + translate[1];
d3.select(g).attr("transform", "translate(" + x + "," + y + ")");
d3.event.sourceEvent.stopPropagation();
});
svg = d3.select("svg");
d = {x: 20, y: 20 };
groups = svg
.append("g")
.attr("transform", "translate(20, 20)");
groups
.append("rect")
.attr("x", 0)
.attr("y", 0)
.attr("width", 100)
.attr("height", 100)
.style("fill", "green")
.call(in_editor_drag)
.style("opacity", 0.4);
I'm trying to drag a group by using one of it's children as a handle. Simply, what i'm trying to do is, when a groups child is dragged:
Get translation transformation of group
Get drag distance from d3.event.dx, d3.event.dy
Apply difference to group's transform attribute
When child dragged, group does not move as expected. It moves less than the dragged distance, and it begins to jump here and there.
What am I doing wrong here?
Edit:
Updated jsfiddle: http://jsfiddle.net/6NBy2/2/
I'm trying to drag the whole group by using one or more of it's children as dragging handles.
This is an old question, but not really answered. I had exactly the same problem and wanted to drag the group by only one child (not all child elements of the <g>).
The problem is, that the d3.event.dx/y is calculated relatively to the position of the <g>. And as soon as the <g> is moved by .attr(“transform”, “translate(x, y)”), the d3.event.dx/dy is adjusted to the new (smaller) value. This results in a jerky movement with approx. the half of the speed of the cursor. I found two possible solutions for this:
First (finally I ended up with this approach):
Append the drag handle rect directly to the svg and not to the <g>. So it is positioned relatively to the <svg> and not to the <g>. Then move both (the <rect> and the <g>) within the on drag function.
var svg = d3.select("svg");
var group = svg
.append("g").attr("id", "group")
.attr("transform", "translate(0, 0)");
group
.append("rect")
.attr("x", 0)
.attr("y", 0)
.attr("width", 100)
.attr("height", 100)
.style("fill", "green")
.style("opacity", 0.4);
group
.append("text")
.attr("x", 10)
.attr("y", 5)
.attr("dominant-baseline", "hanging")
.text("drag me");
handle = svg
.append("rect")
.data([{
// Position of the rectangle
x: 0,
y: 0
}])
.attr("class", "draghandle")
.attr("x", 0)
.attr("y", 0)
.attr("width", 100)
.attr("height", 20)
.style("fill", "blue")
.style("opacity", 0.4)
.attr("cursor", "move")
.call(d3.drag().on("drag", function (d) {
console.log("yep");
d.x += d3.event.dx;
d.y += d3.event.dy;
// Move handle rect
d3.select(this)
.attr("x", function (d) {
return d.x;
})
.attr("y", function (d) {
return d.y;
});
// Move Group
d3.select("#group").attr("transform", "translate(" + [d.x, d.y] + ")");
}));
<body>
<svg width="400" height="400"></svg>
<script src="https://d3js.org/d3.v4.min.js"></script>
</body>
Second:
Check on which element the cursor was during the drag event with d3.event.sourceEvent.path[0] and run the drag function only if the handle <rect> was clicked. With this approach, all elements can be grouped within one <g> (no need for an additional <rect> outside the group). The downside of this method is, that the drag is also executed, if the cursor is moved over the drag handle with mouse down.
var svg = d3.select("svg");
var group = svg
.append("g")
.data([{
// Position of the rectangle
x: 0,
y: 0
}])
.attr("id", "group")
.attr("transform", function (d) {
return "translate(" + d.x + ", " + d.y + ")"
})
.call(d3.drag().on("drag", function (d) {
if (d3.event.sourceEvent.target.classList.value === "draghandle") {
console.log("yep");
d.x += d3.event.dx;
d.y += d3.event.dy;
d3.select(this).attr("transform", function (d) {
return "translate(" + [d.x, d.y] + ")"
})
} else {
console.log("nope");
return;
}
}));
group
.append("rect")
.attr("x", 0)
.attr("y", 0)
.attr("width", 100)
.attr("height", 100)
.style("fill", "green")
.style("opacity", 0.4);
group
.append("text")
.attr("x", 10)
.attr("y", 5)
.attr("dominant-baseline", "hanging")
.text("drag me");
handle = group
.append("rect")
.attr("class", "draghandle")
.attr("x", 0)
.attr("y", 0)
.attr("width", 100)
.attr("height", 20)
.style("fill", "blue")
.style("opacity", 0.4)
.attr("cursor", "move");
<body>
<svg width="400" height="400"></svg>
<script src="https://d3js.org/d3.v4.min.js"></script>
</body>
use g = this; instead of g = this.parentNode;
Use drag.container() to set the container accessor.
See the D3 docs.

Used Arc.centroid to plot circles - but text do no go along with it why?

I've used arc.Centroid to try to plot my circles on the arcs with labels. However, the labels do not stay with it?
force.on("tick", function() {
text.attr("x", function(d) { return d.x + 6; })
.attr("y", function(d) { return d.y + 4; });
node.attr("transform", function(d,i) {
return "translate(" + arc[i].centroid(d) + ")"; })
});
I have attempted to put centroid & arc[i] instead of the x & y. How can I put my circles with text? http://jsfiddle.net/xwZjN/20/
Also say if I were to have more json data, would I be able to restrict the plots only going into each section e.g. each section being a category?
Any help would be great. I think the solution may be similar to this - http://jsfiddle.net/nrabinowitz/GQDUS/
It seems that the force layout is not the right choice for your application. Try to group your symbol and text in a g element and place them at the calculated coordinates. See updated fiddle without force layout: http://jsfiddle.net/xwZjN/26/
var node = svg.selectAll("g.node")
.data(nodes)
.enter().append("g")
.attr("class", "node")
.attr("transform", function(d,i) {
return "translate(" + arc[i].centroid() + ")";
});
node.append("path")
.attr("d", d3.svg.symbol().type(function(d) { return d.type; }))
// change (0,0) for exact symbol placement
.attr("transform", "translate(0,0)")
.style("fill", "blue" );
node.append("text")
.text(function(d) { return d.Name; })
// shift text in nice position
.attr("x", 10)
.attr("y", 5);

Categories

Resources