Mouseout on element grouping fires before leaving bounding area in D3 - javascript

I'm trying to add a click and mouseout handler to a group of circles and text. The click handler works fine, but the mouse out seems to fire even if I haven't left the circleGroup area (shown below).
Note: there are multiple of these circle groups which are added in a grid.
Here is the SVG as it appears in the browser:
The code to produce the circleGroup, containing an outer green-ish circle, an inner white circle, and a text element, is as follows:
let circleGroup = leftPanelGroup.append('g');
let outerCircle = circleGroup.append("circle")
.attr("cx", x)
.attr("cy", y)
.style('fill', color)
.attr("r", 15);
let innerCircle = circleGroup.append("circle")
.attr("cx", x)
.attr("cy", y)
.style('fill', color)
.style('stroke', '#fff')
.attr("r", 7);
let text = circleGroup.append('text')
.style('color', '#fff')
.attr("x", x)
.attr("y", y - 25)
.style('fill', '#fff')
.attr("font-size", 12)
.attr('font-weight', 'bold')
.attr("font-family", "sans-serif")
.attr('id', 'circle-text')
.style("text-anchor", "middle")
.text('Thank you');
...
On click anywhere within the circleGroup, the circleClick should fire. This works fine. The issue is, the circleMouseout function seems to randomly fire even if I haven't yet left the bounding area of the circleGroup:
circleGroup.on('click', circleClick).on('mouseout', circleMouseout);
function circleClick() {
// Do something
}
function circleMouseout() {
// Do something else
}
The output HTML in console shows the area of the <g> svg group element. I'd expect that click anywhere in this highlighted area would fire the click event, and only when I mouse out of this highlighted area would the mouseout event fire. Again, the click works fine, the mouseout does not.
<g>
<circle cx="252.99037499999997" cy="340.938" r="15" style="fill: rgb(108, 160, 123);">
</circle>
<circle cx="252.99037499999997" cy="340.938" r="7" style="fill: rgb(108, 160, 123); stroke: rgb(255, 255, 255);">
</circle>
<text x="252.99037499999997" y="315.938" font-size="12" font-weight="bold" font-family="sans-serif" id="circle-text" style="color: rgb(255, 255, 255); fill: rgb(255, 255, 255); text-anchor: middle;">Thank you
</text>
</g>

The bounding box of a g doesn't affect where a mouse interacts with it.The mouse only interacts with the "interaction area" of an element. This is generally the stroke or fill of rendered elements. So, whenever your mouse leaves the circles or the text, you trigger the mouseout event, not when you leave the bounding box of the parent g.
If you want the bounding box of the parent g to interact with the mouse, then we need to add a new rectangle. We can extract the g's bbox and use this to draw a new rectangle over the bounding box of the g. This rectangle can be given the fill of none to make it invisible, but also given a pointer-events property of all to ensure it interacts with the mouse:
let boundingBox = circleGroup.append('rect')
.each(function() {
var bbox = this.parentNode.getBBox(); // get parent `g` bounding box
d3.select(this)
.attr("width", bbox.width) // size rect based on bounding box.
.attr("height", bbox.height)
.attr("x", bbox.x)
.attr("y", bbox.y)
.attr("fill","none")
.attr("pointer-events","all")
})
Now we can assign event listeners to the g, or the rect for that matter, and the entire g bounding box will respond to mouse events:
let circleGroup = d3.select("body")
.append("svg")
.append("g");
let x = 50;
let y = 50;
let color = "steelblue";
let outerCircle = circleGroup.append("circle")
.attr("cx", x)
.attr("cy", y)
.style('fill', color)
.attr("r", 15);
let innerCircle = circleGroup.append("circle")
.attr("cx", x)
.attr("cy", y)
.style('fill', color)
.style('stroke', '#fff')
.attr("r", 7);
let text = circleGroup.append('text')
.attr("x", x)
.attr("y", y - 25)
.style('fill', color)
.attr("font-size", 12)
.attr('font-weight', 'bold')
.attr("font-family", "sans-serif")
.attr('id', 'circle-text')
.style("text-anchor", "middle")
.text('Thank you');
let boundingBox = circleGroup.append('rect')
.each(function() {
var bbox = this.parentNode.getBBox();
d3.select(this)
.attr("width", bbox.width)
.attr("height", bbox.height)
.attr("x", bbox.x)
.attr("y", bbox.y)
.attr("fill","none")
.attr("pointer-events","all")
})
circleGroup.on("mouseover", function() {
console.log("mouseover");
}).on("mouseout", function() {
console.log("mouseout");
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>

Related

ClipPath hides the original shape - d3 ReactJS

I'm a newbie to D3 and ReactJS, and trying to clip circle with a straight line, but could not get how it works. I have this HTML code to draw image;
<div >
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" className="clip-path" ref={this.attachCircle.bind(this)}>
</svg>
</div>
here is my function to draw image;
attachCircle = (svgRef:SVGElement) => {
const svg = d3.select(svgRef);
// draw a circle
svg.append("circle") // shape it as an ellipse
.attr("cx", 100) // position the x-centre
.attr("cy", 80) // position the y-centre
.attr("r", 80) // set the x radius
.attr("fill", "SteelBlue")
}
and it results in this way;
but as soon as I add clipPath to circle;
svg.append("clipPath") // define a clip path
.attr("id", "clip") // give the clipPath an ID
.append("circle") // shape it as an ellipse
.attr("cx", 100) // position the x-centre
.attr("cy", 80) // position the y-centre
.attr("r", 80) // set the x radius
.attr("fill", "SteelBlue")
it shows nothing on the screen. It is not drawing any circle or anything.
Can anyone explain why is it so?? or What am I missing?
You need to use use
<use clip-path="url(#clip)" xlink:href="#clip" fill="red" />
https://developer.mozilla.org/en-US/docs/Web/SVG/Element/clipPath

Draw circles with text in the middle using Javascript/CSS [duplicate]

I've been using the sample code from this d3 project to learn how to display d3 graphs and I can't seem to get text to show up in the middle of the circles (similar to this example and this example). I've looked at other examples and have tried adding
node.append("title").text("Node Name To Display")
and
node.append("text")
.attr("text-anchor", "middle")
.attr("dy", ".3em").text("Node Name To Display")
right after node is defined but the only results I see is "Node Name To Display" is showing up when I hover over each node. It's not showing up as text inside the circle. Do I have to write my own svg text object and determine the coordinates of that it needs to be placed at based on the coordinates of radius of the circle? From the other two examples, it would seem like d3 already takes cares of this somehow. I just don't know the right attribute to call/set.
There are lots of examples showing how to add labels to graph and tree visualizations, but I'd probably start with this one as the simplest:
http://bl.ocks.org/950642
You haven’t posted a link to your code, but I'm guessing that node refers to a selection of SVG circle elements. You can’t add text elements to circle elements because circle elements are not containers; adding a text element to a circle will be ignored.
Typically you use a G element to group a circle element (or an image element, as above) and a text element for each node. The resulting structure looks like this:
<g class="node" transform="translate(130,492)">
<circle r="4.5"/>
<text dx="12" dy=".35em">Gavroche</text>
</g>
Use a data-join to create the G elements for each node, and then use selection.append to add a circle and a text element for each. Something like this:
var node = svg.selectAll(".node")
.data(nodes)
.enter().append("g")
.attr("class", "node")
.call(force.drag);
node.append("circle")
.attr("r", 4.5);
node.append("text")
.attr("dx", 12)
.attr("dy", ".35em")
.text(function(d) { return d.name });
One downside of this approach is that you may want the labels to be drawn on top of the circles. Since SVG does not yet support z-index, elements are drawn in document order; so, the above approach causes a label to be drawn above its circle, but it may be drawn under other circles. You can fix this by using two data-joins and creating separate groups for circles and labels, like so:
<g class="nodes">
<circle transform="translate(130,492)" r="4.5"/>
<circle transform="translate(110,249)" r="4.5"/>
…
</g>
<g class="labels">
<text transform="translate(130,492)" dx="12" dy=".35em">Gavroche</text>
<text transform="translate(110,249)" dx="12" dy=".35em">Valjean</text>
…
</g>
And the corresponding JavaScript:
var circle = svg.append("g")
.attr("class", "nodes")
.selectAll("circle")
.data(nodes)
.enter().append("circle")
.attr("r", 4.5)
.call(force.drag);
var text = svg.append("g")
.attr("class", "labels")
.selectAll("text")
.data(nodes)
.enter().append("text")
.attr("dx", 12)
.attr("dy", ".35em")
.text(function(d) { return d.name });
This technique is used in the Mobile Patent Suits example (with an additional text element used to create a white shadow).
I found this guide very useful in trying to accomplish something similar :
https://www.dashingd3js.com/svg-text-element
Based on above link this code will generate circle labels :
<!DOCTYPE html>
<html>
<head>
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
<script src="http://d3js.org/d3.v3.min.js" charset="utf-8"></script>
</head>
<body style="overflow: hidden;">
<div id="canvas" style="overflow: hidden;"></div>
<script type="text/javascript">
var graph = {
"nodes": [
{name: "1", "group": 1, x: 100, y: 90, r: 10 , connected : "2"},
{name: "2", "group": 1, x: 200, y: 50, r: 15, connected : "1"},
{name: "3", "group": 2, x: 200, y: 130, r: 25, connected : "1"}
]
}
$( document ).ready(function() {
var width = 2000;
var height = 2000;
var svg = d3.select("#canvas").append("svg")
.attr("width", width)
.attr("height", height)
.append("g");
var lines = svg.attr("class", "line")
.selectAll("line").data(graph.nodes)
.enter().append("line")
.style("stroke", "gray") // <<<<< Add a color
.attr("x1", function (d, i) {
return d.x
})
.attr("y1", function (d) {
return d.y
})
.attr("x2", function (d) {
return findAttribute(d.connected).x
})
.attr("y2", function (d) {
return findAttribute(d.connected).y
})
var circles = svg.selectAll("circle")
.data(graph.nodes)
.enter().append("circle")
.style("stroke", "gray")
.style("fill", "white")
.attr("r", function (d, i) {
return d.r
})
.attr("cx", function (d, i) {
return d.x
})
.attr("cy", function (d, i) {
return d.y
});
var text = svg.selectAll("text")
.data(graph.nodes)
.enter()
.append("text");
var textLabels = text
.attr("x", function(d) { return d.x; })
.attr("y", function(d) { return d.y; })
.text( function (d) { return d.name })
.attr("font-family", "sans-serif")
.attr("font-size", "10px")
.attr("fill", "red");
});
function findAttribute(name) {
for (var i = 0, len = graph.nodes.length; i < len; i++) {
if (graph.nodes[i].name === name)
return graph.nodes[i]; // Return as soon as the object is found
}
return null; // The object was not found
}
</script>
</body>
</html>
If you want to grow the nodes to fit large labels, you can use the getBBox property of an SVG text node after you've drawn it. Here's how I did it, for a list of nodes with fixed coordinates, and two possible shapes:
nodes.forEach(function(v) {
var nd;
var cx = v.coord[0];
var cy = v.coord[1];
switch (v.shape) {
case "circle":
nd = svg.append("circle");
break;
case "rectangle":
nd = svg.append("rect");
break;
}
var w = 10;
var h = 10;
if (v.label != "") {
var lText = svg.append("text");
lText.attr("x", cx)
.attr("y", cy + 5)
.attr("class", "labelText")
.text(v.label);
var bbox = lText.node().getBBox();
w = Math.max(w,bbox.width);
h = Math.max(h,bbox.height);
}
var pad = 4;
switch (v.shape) {
case "circle":
nd.attr("cx", cx)
.attr("cy", cy)
.attr("r", Math.sqrt(w*w + h*h)/2 + pad);
break;
case "rectangle":
nd.attr("x", cx - w/2 - pad)
.attr("y", cy - h/2 - pad)
.attr("width", w + 2*pad)
.attr("height", h + 2*pad);
break;
}
});
Note that the shape is added, the text is added, then the shape is positioned, in order to get the text to show on top.

Text label hides behind the circle node in d3.js [duplicate]

I've been using the sample code from this d3 project to learn how to display d3 graphs and I can't seem to get text to show up in the middle of the circles (similar to this example and this example). I've looked at other examples and have tried adding
node.append("title").text("Node Name To Display")
and
node.append("text")
.attr("text-anchor", "middle")
.attr("dy", ".3em").text("Node Name To Display")
right after node is defined but the only results I see is "Node Name To Display" is showing up when I hover over each node. It's not showing up as text inside the circle. Do I have to write my own svg text object and determine the coordinates of that it needs to be placed at based on the coordinates of radius of the circle? From the other two examples, it would seem like d3 already takes cares of this somehow. I just don't know the right attribute to call/set.
There are lots of examples showing how to add labels to graph and tree visualizations, but I'd probably start with this one as the simplest:
http://bl.ocks.org/950642
You haven’t posted a link to your code, but I'm guessing that node refers to a selection of SVG circle elements. You can’t add text elements to circle elements because circle elements are not containers; adding a text element to a circle will be ignored.
Typically you use a G element to group a circle element (or an image element, as above) and a text element for each node. The resulting structure looks like this:
<g class="node" transform="translate(130,492)">
<circle r="4.5"/>
<text dx="12" dy=".35em">Gavroche</text>
</g>
Use a data-join to create the G elements for each node, and then use selection.append to add a circle and a text element for each. Something like this:
var node = svg.selectAll(".node")
.data(nodes)
.enter().append("g")
.attr("class", "node")
.call(force.drag);
node.append("circle")
.attr("r", 4.5);
node.append("text")
.attr("dx", 12)
.attr("dy", ".35em")
.text(function(d) { return d.name });
One downside of this approach is that you may want the labels to be drawn on top of the circles. Since SVG does not yet support z-index, elements are drawn in document order; so, the above approach causes a label to be drawn above its circle, but it may be drawn under other circles. You can fix this by using two data-joins and creating separate groups for circles and labels, like so:
<g class="nodes">
<circle transform="translate(130,492)" r="4.5"/>
<circle transform="translate(110,249)" r="4.5"/>
…
</g>
<g class="labels">
<text transform="translate(130,492)" dx="12" dy=".35em">Gavroche</text>
<text transform="translate(110,249)" dx="12" dy=".35em">Valjean</text>
…
</g>
And the corresponding JavaScript:
var circle = svg.append("g")
.attr("class", "nodes")
.selectAll("circle")
.data(nodes)
.enter().append("circle")
.attr("r", 4.5)
.call(force.drag);
var text = svg.append("g")
.attr("class", "labels")
.selectAll("text")
.data(nodes)
.enter().append("text")
.attr("dx", 12)
.attr("dy", ".35em")
.text(function(d) { return d.name });
This technique is used in the Mobile Patent Suits example (with an additional text element used to create a white shadow).
I found this guide very useful in trying to accomplish something similar :
https://www.dashingd3js.com/svg-text-element
Based on above link this code will generate circle labels :
<!DOCTYPE html>
<html>
<head>
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
<script src="http://d3js.org/d3.v3.min.js" charset="utf-8"></script>
</head>
<body style="overflow: hidden;">
<div id="canvas" style="overflow: hidden;"></div>
<script type="text/javascript">
var graph = {
"nodes": [
{name: "1", "group": 1, x: 100, y: 90, r: 10 , connected : "2"},
{name: "2", "group": 1, x: 200, y: 50, r: 15, connected : "1"},
{name: "3", "group": 2, x: 200, y: 130, r: 25, connected : "1"}
]
}
$( document ).ready(function() {
var width = 2000;
var height = 2000;
var svg = d3.select("#canvas").append("svg")
.attr("width", width)
.attr("height", height)
.append("g");
var lines = svg.attr("class", "line")
.selectAll("line").data(graph.nodes)
.enter().append("line")
.style("stroke", "gray") // <<<<< Add a color
.attr("x1", function (d, i) {
return d.x
})
.attr("y1", function (d) {
return d.y
})
.attr("x2", function (d) {
return findAttribute(d.connected).x
})
.attr("y2", function (d) {
return findAttribute(d.connected).y
})
var circles = svg.selectAll("circle")
.data(graph.nodes)
.enter().append("circle")
.style("stroke", "gray")
.style("fill", "white")
.attr("r", function (d, i) {
return d.r
})
.attr("cx", function (d, i) {
return d.x
})
.attr("cy", function (d, i) {
return d.y
});
var text = svg.selectAll("text")
.data(graph.nodes)
.enter()
.append("text");
var textLabels = text
.attr("x", function(d) { return d.x; })
.attr("y", function(d) { return d.y; })
.text( function (d) { return d.name })
.attr("font-family", "sans-serif")
.attr("font-size", "10px")
.attr("fill", "red");
});
function findAttribute(name) {
for (var i = 0, len = graph.nodes.length; i < len; i++) {
if (graph.nodes[i].name === name)
return graph.nodes[i]; // Return as soon as the object is found
}
return null; // The object was not found
}
</script>
</body>
</html>
If you want to grow the nodes to fit large labels, you can use the getBBox property of an SVG text node after you've drawn it. Here's how I did it, for a list of nodes with fixed coordinates, and two possible shapes:
nodes.forEach(function(v) {
var nd;
var cx = v.coord[0];
var cy = v.coord[1];
switch (v.shape) {
case "circle":
nd = svg.append("circle");
break;
case "rectangle":
nd = svg.append("rect");
break;
}
var w = 10;
var h = 10;
if (v.label != "") {
var lText = svg.append("text");
lText.attr("x", cx)
.attr("y", cy + 5)
.attr("class", "labelText")
.text(v.label);
var bbox = lText.node().getBBox();
w = Math.max(w,bbox.width);
h = Math.max(h,bbox.height);
}
var pad = 4;
switch (v.shape) {
case "circle":
nd.attr("cx", cx)
.attr("cy", cy)
.attr("r", Math.sqrt(w*w + h*h)/2 + pad);
break;
case "rectangle":
nd.attr("x", cx - w/2 - pad)
.attr("y", cy - h/2 - pad)
.attr("width", w + 2*pad)
.attr("height", h + 2*pad);
break;
}
});
Note that the shape is added, the text is added, then the shape is positioned, in order to get the text to show on top.

Javascript's getBBox() provides a box that is taller than its content text. Why?

I'm using D3.js to draw some text ("100%") on an SVG Container.
Now I'm trying to figure out the dimensions of the bounding box around that text.
This is how I do it:
var label = svgContainer.append("text")
.attr("x", 200)
.attr("y", 200)
.text("100%")
.attr("font-family", "Courier New")
.attr("font-weight", "bold")
.attr("font-size", "10px")
.attr("fill", "black");
.attr("transform", function(d){
var bb = this.getBBox();
console.log("bb.width = ", bb.width);
console.log("bb.height = ", bb.height);
return "translate(0, 0)";
}
);
Not only does this draw the string "100%" on my screen, but in the console.log, it also outputs the width and height of the bounding box (24 & 11.5 respectively!);
Now I want to visualize the aforementioned bounding box on the screen. So before the code above, I prepend the following code:
var rect = svgContainer.append("rect")
.attr("x", 200)
.attr("y", 200-11.5)
.attr("width", 24)
.attr("height", 11.5)
.attr("fill", "pink");
When I run this altered code, I expect to see a pink bounding box around the "100%" string. But I see the following instead!
Why is the pink bounding box taller than the text? I need to get the dimensions of the actual bounding-box of the text -- not something taller than it. How do I do that?
Here is the Plunker: https://plnkr.co/edit/XoVSZwTBNXhdKKJKkYWn
There are two things going on here:
You are only using the height and width fields of the bounding box. You should be also taking into account the x and y attributes of the bounding box - as #Gilsha correctly pointed out. When you do adjust for the x and y, you'll see the box sits a little lower:
var svg = document.getElementById("my_svg_widget");
var bbox = svg.getElementById("test").getBBox();
var rect = document.createElementNS("http://www.w3.org/2000/svg", "rect");
rect.setAttribute("x", bbox.x);
rect.setAttribute("y", bbox.y);
rect.setAttribute("width", bbox.width);
rect.setAttribute("height", bbox.height);
rect.setAttribute("fill", "rgba(255, 0, 0, 0.5)");
svg.appendChild(rect);
<svg id="my_svg_widget" width="300" height="300" style="border-style:solid;border-color:purple;border-width: 2px;">
<text id="test" x="100" y="100" font-family="Courier New" font-weight="bold" font-size="50px">100%</text>
</svg>
This brings us to the second reason. The bounding box returned by getBBox() is not the tight bounding box around the glyphs. It includes the full em-box height of the text - including allowance for ascenders (tallest characters in font) and descenders (lowest below baseline).
You can see what I mean if we include some of those characters in the text element.
var svg = document.getElementById("my_svg_widget");
var bbox = svg.getElementById("test").getBBox();
var rect = document.createElementNS("http://www.w3.org/2000/svg", "rect");
rect.setAttribute("x", bbox.x);
rect.setAttribute("y", bbox.y);
rect.setAttribute("width", bbox.width);
rect.setAttribute("height", bbox.height);
rect.setAttribute("fill", "rgba(255, 0, 0, 0.5)");
svg.appendChild(rect);
<svg id="my_svg_widget" width="300" height="300" style="border-style:solid;border-color:purple;border-width: 2px;">
<text id="test" x="100" y="100" font-family="Courier New" font-weight="bold" font-size="50px">Á100%╣</text>
</svg>
You can directly use bounding box x and y attributes. Use below code.
var label = self.svgContainer.append("text")
.attr("x", 200)
.attr("y", 200)
.text("100%")
.attr("font-family", "Courier New")
.attr("font-weight", "bold")
.attr("font-size", "10px")
.attr("fill", "black")
.attr("transform", function(d){
var bb = this.getBBox();
self.svgContainer.insert("rect","text")
.attr("x", bb.x)
.attr("y", bb.y)
.attr("width", bb.width)
.attr("height", bb.height)
.attr("fill", "pink");
return "translate(0, 0)";
});
var myApp = angular.module('myApp', []);
myApp.controller('MyController', function() {
var self = this;
self.svgContainer = d3.select("#my_svg_widget");
console.log("Hello!");
var label = self.svgContainer.append("text")
.attr("x", 200)
.attr("y", 200)
.text("100%")
.attr("font-family", "Courier New")
.attr("font-weight", "bold")
.attr("font-size", "10px")
.attr("fill", "black")
.attr("transform", function(d){
var bb = this.getBBox();
self.svgContainer.insert("rect","text")
.attr("x", bb.x)
.attr("y", bb.y)
.attr("width", bb.width)
.attr("height", bb.height)
.attr("fill", "pink");
return "translate(0, 0)";
});
}
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<html ng-app="myApp">
<body ng-controller="MyController as mycontroller">
<svg id="my_svg_widget" width="300" height="300" style="border-style:solid;border-color:purple;border-width: 2px;">
</svg>
</body>
</html>

d3 How to trigger event on the object behind text

I draw A rect and text on svg.
In order to show text, I render rect first.
I add mouse click event to rect
when I click the text, it seems the rect is not selected, because rect is behind text, so text is selected frist.
I need to select trigger the event when mouse click inside the rect.
How should I do?
Thanks!
Fiddle
You can see, when you mouse click on the text, the rect is not clicked.
var data = ["TEXT SAMPLE TEXT SAMPLE TEXT SAMPLE"];
var svg = d3.select("svg");
var bar = svg.selectAll("g")
.data(data)
.enter().append("g")
.attr("transform", function(d, i) { return "translate(100,100)"; });
bar.append("rect")
.attr("width", 200)
.attr("height", 50)
.style("fill", "#f00")
.on("click", function (d) {
alert("text");
});
bar.append("text")
.attr("x", 10)
.attr("y", 25)
.attr("dy", "-.35em")
.text(function(d) { return d; });
You can attach a style to the text to ignore mouse events. Here is the example:
Style:
.bar-text {
pointer-events: none;
}
Modified code:
bar.append("text")
.attr("x", 10)
.attr("y", 25)
.attr("class", "bar-text") // add class
.attr("dy", "-.35em")
.text(function(d) { return d; });
Attach the handler to the g element. Complete demo here.

Categories

Resources