I currently am working on a GeoJSON of the United States with a mouseover event on every state (for example, one thing that occurs is that it changes the border of the states to bright red to show which one is being highlighted).
Some states are fairly small, especially in New England. I was trying to think of a way to get around this and decided to implement additional buttons on the side that would have the same mouseover event tied to specific states that I have deemed were small.
My question is: how would I go about getting something like highlighting the states borders to happen when mousing over the additional buttons.
My current implementation of the original states themselves is below (I haven't begun work on the additional buttons):
.on("mouseover", function(d) {
d3.select(event.target).attr("stroke", "red").attr("stroke-width", "3");
displayData(d);
});
.on("mouseout", function(d) {
d3.select(event.target).attr("stroke", "black").attr("stroke-width", "1");
hideData(d);
});
displayData(d) and hideData(d) are used for a tooltip's display. As you can see, the way the mouseover works right now is by capturing the event.target. How would I replicate this for a separate button? Is it possible to somehow tie that button's event.target to the corresponding state?
Just use selection.dispatch, which:
Dispatches a custom event of the specified type to each selected element, in order.
Here is a basic example, hovering over the circles will call a given function. Click on the button to dispatch a "mouseover" to the second circle.
const svg = d3.select("svg");
const circles = svg.selectAll(null)
.data(["foo", "bar", "baz"])
.enter()
.append("circle")
.attr("id", (_, i) => `id${i}`)
.attr("cy", 70)
.attr("cx", (_, i) => 30 + 100 * i)
.attr("r", 30)
.style("fill", "teal")
.on("mouseover", (event, d) => {
console.log(`My name is ${d}`);
});
d3.select("button").on("click", () => d3.select("#id1").dispatch("mouseover"));
.as-console-wrapper { max-height: 15% !important;}
<script src="https://d3js.org/d3.v7.min.js"></script>
<button>Click me</button>
<svg></svg>
I have created a worldmap using the d3 and now able to create the specific countries to have hover effect , however I have also created the tooltip what I want to do now is to get the country map in the tooltip (the country which is hovered) i have used d3 v4 to do all this.
I have made changes suggested by CodeSmit but it seems I'm missing a lot of things.
<!DOCTYPE html>
<head>
<meta charset="utf-8">
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="https://d3js.org/topojson.v2.min.js"></script>
<script src="https://d3js.org/d3-queue.v3.min.js"></script>
<style>
#country_name{
border:none;
}
.bigmap {
width: 100%;
height: 100%;
position: center;
background-color: #080e1e;
float:right;
}
.hidden {
display: none;
}
div.tooltip {
color: #222;
background: #19202d;
border-radius: 3px;
box-shadow: 0px 0px 2px 0px #a6a6a6;
padding: .2em;
text-shadow: #f5f5f5 0 1px 0;
opacity: 0.9;
position: absolute;
}
div {
color: #fff;
font-family: Tahoma, Verdana, Segoe, sans-serif;
padding: 5px;
}
.container {
display:flex;
}
.fixed {
text-align:center;
border-width: 1px;
vertical-align:middle;
border-style: solid;
border-color:#55a4bf ;
width: 80px;
margin-right:10px;
}
.flex-item {
border-width: 1px;
text-align:center;
vertical-align:middle;
border-style: solid;
border-color:#55a4bf ;
//background-color:#7887AB;
width: 120px;
}
</style>
</head>
<svg class= "bigmap"width="760" height="340"></svg>
<div class="tooltip"></div>
<body>
<script>
var margin = {top: 0, right: 10, bottom: 10, left: 10};
var width = 760 ;
var height = 400 ;
var projection = d3.geoNaturalEarth1()
.center([0, 15])
.rotate([-11,0])
.scale([150])
.translate([750,350]);
var path = d3.geoPath()
.projection(projection);;
var svg = d3.select(".bigmap")
.append("g")
.attr("width", width)
.attr("height", height);
var tooltip = d3.select("div.tooltip");
d3.queue()
.defer(d3.json, "https://cdn.rawgit.com/mbostock/4090846/raw/d534aba169207548a8a3d670c9c2cc719ff05c47/world-110m.json")
.defer(d3.tsv, "https://cdn.rawgit.com/mbostock/4090846/raw/d534aba169207548a8a3d670c9c2cc719ff05c47/world-country-names.tsv")
.await(ready);
function ready(error, world, names) {
if (error) throw error;
var countries1 = topojson.feature(world, world.objects.countries).features;
countries = countries1.filter(function(d) {
return names.some(function(n) {
if (d.id == n.id) return d.name = n.name;
})});
console.log("countries",countries);
var arr = ["India","Sri Lanka","Afghanistan","Russian Federation"];
svg.selectAll("path")
.data(countries.filter(d => d.name !== "Antarctica"))
.enter()
.append("path")
.attr("stroke","#080e1e")
.attr("stroke-width",1)
.attr("fill", "#0d2331")
.attr("d", path )
.on("mouseover",function(d,i){
if (arr.includes(d.name)){
var tipSVG = tooltip.append("svg")
.attr("width", 220)
.attr("height", 55);
var bbox =tipSVG.append("path")
.attr("stroke","#080e1e")
.attr("stroke-width",1)
.attr("fill", "#0d2331")
.attr("d", path(d) )
.node().getBBox()
tipSVG.attr("viewBox", `${bbox.x} ${bbox.y} ${bbox.width} ${bbox.height}`);
tooltip.classed("hidden", false)
.attr("fill","#19202d")
.style("top", 150+ "px")
.style("left", 20 + "px")
.html('<div class="container"><div class="fixed" id="country_name">'+d.name+'</div><div class="flex-item">Fixed 2</div></div><div class="container"><div class="fixed">Fixed 3</div><div class="flex-item">Fixed 4</div></div><div class="container"><div class="fixed">Fixed 5</div><div class="flex-item">Fixed 6</div></div><div class="container"><div class="fixed">Fixed 7</div><div class="flex-item">Fixed 8</div></div>');
d3.select(this).attr("fill","#0d2331").attr("stroke-width",2).style("stroke","#b72c2f")
return tooltip.style("hidden", false).html(d.name);
}
})
.on("mousemove",function(d){
})
.on("mouseout",function(d,i){
if (arr.includes(d.name)){
d3.select(this).attr("fill","#0d2331").attr("stroke-width",1).style("stroke","#080e1e")
tooltip.classed("hidden", true);
}
});
var g = svg.append('g');
g.selectAll('circle')
.data(countries)
.enter().append('circle')
.attr("fill","white")
.attr('transform', function(d) { return 'translate(' + path.centroid(d) + ')'; })
.attr('r',function(d){
if (arr.includes(d.name)){
return "3"
}
return "0";
}
);
};
</script>
</body>
Any guidance or help is greatly appreciated and thanks in advance
TL;DR:
The .html method on D3 selections first deletes anything that's already inside those elements before setting the new contents. Thus, to initiate an element's base HTML content with .html, be sure to call it first before adding anything else to the element, and also do not call .html later on, or risk it overwriting anything that was added to it.
You're close. You've got a number of issues though.
1. d3-tip Not Used
You're including the d3-tip library, but you're not making real use of it at all. Because of this, it's adding to the confusion. You have your own <div class="tooltip"></div> which is what actually appears. If you don't need the tooltip to float where the cursor is (which is what d3-tip is for), then I'd highly recommend starting by stripping out all your code making use of this library.
2. <svg> Doesn't Make It Into Tooltip
Your "mouseover" event fails to add the country SVG element for two reasons:
First, because you're selecting the #tipDiv element which never appears since it's part of the d3-tip code that doesn't get used. To fix this, I think you want to select the div.tooltip element instead. Since you already have the 'tooltip' variable set to this, you don't need d3.select; you can simply do:
var tipSVG = tooltip.append("svg")
.attr("width", 220)
.attr("height", 55);
At this point, it will add the <svg>, but the issue is that, following this, it is immediately written over. This happens in the "mousemove" event:
tooltip.classed("hidden", false)
...
.html('<div class="container">...</div>');
As soon as the mouse moves over the element, the .html call overwrites the newly added svg, deleting it. I was able to fix this by moving this block of code out of the "mousemove" event and to the start of the "mouseover" event.
After this, the SVG successfully appears in the tooltip.
3. Tooltip SVG Contains Entire Map (But Looks Empty)
Before you're done, though, at this point the newly appearing SVG looks blank. It's actually not blank, but all the contents are appearing outside the SVG's rendering region. Before fixing this, though, first note you're loading the entire map into the SVG in your "mouseover" event:
tipSVG.selectAll("path")
.data(countries.filter(d => d.name !== "Antarctica"))
.enter()
.append("path")
.attr("stroke","#080e1e")
.attr("stroke-width",1)
.attr("fill", "#0d2331")
.attr("d", path )
I'm guessing you want to load just the country being hovered over? Since you already have that data with 'd' in the function, you can do that with something like:
tipSVG.append("path")
.attr("stroke","#080e1e")
.attr("stroke-width",1)
.attr("fill", "#0d2331")
.attr("d", path(d) )
Note now I'm calling 'path' right on the data.
4. Country Path Way Out Of SVG View
So now the SVG only contains the country being hovered over, which makes fixing the last issue much easier: it's way outside the bounding box. Because it's a single path now, a simple fix is to set the SVG's 'viewBox' to the path's bounding box, which you can get by calling getBBox right on the path element (not the d3 selection of it though).
You can do this like so:
var bbox = tipSVG.append("path")
...
.attr("d", path(d) )
.node().getBBox(); // get the bbox from the path we just added
tipSVG.attr("viewBox", `${bbox.x} ${bbox.y} ${bbox.width} ${bbox.height}`);
This effectively copies the bbox into the viewBox, and fortunately for us, due to the default value of the preserveAspectRatio <svg> attribute, this nicely centers the country in the SVG box:
EDIT:
So, now you're very close! You've just got a couple things wrong:
1. .html() Called After SVG Insert
So, the code you moved out of the "mousemove" should be placed at the beginning of the "mouseover" event. Here's what's happening:
// code appends the new SVG element
var tipSVG = tooltip.append("svg")
.attr("width", 220)
.attr("height", 55);
// creates the path in the SVG and gets the bounding box
var bbox = tipSVG.append("path")
// ...
.node().getBBox()
// makes the SVG view the path
tipSVG.attr("viewBox", `${bbox.x} ${bbox.y} ${bbox.width} ${bbox.height}`);
// DELETES the SVG once .html is called
tooltip.classed("hidden", false)
// ...
.html('<div class="container">...</div>');
Again, .html calls replace everything in the element. Generally you want to use the method sparingly. In this case, it's replacing the content and the newly added SVG with content that doesn't have the SVG. You can still keep the .html call, but it must be moved above the SVG append call so it doesn't delete the SVG:
// replaces all contents with tooltip html
tooltip.classed("hidden", false)
// ...
.html('<div class="container">...</div>');
// code appends the new SVG element
var tipSVG = tooltip.append("svg")
.attr("width", 220)
.attr("height", 55);
// creates the path in the SVG and gets the bounding box
var bbox = tipSVG.append("path")
// ...
.node().getBBox()
// makes the SVG view the path
tipSVG.attr("viewBox", `${bbox.x} ${bbox.y} ${bbox.width} ${bbox.height}`);
// now SVG exists
2. .html Called After SVG Insert
So, the second issue is, yes, the same issue:
return tooltip.style("hidden", false).html(d.name);
This line, at the end, re-calls .html on your tooltip, which has the same effect. In this case, though, it replaces everything with just the country name.
To be honest, I'm not exactly sure of the intent of this line.
Unless I'm mistaken, returning a value in an event listener in D3 has no specific effect.
.style is used for applying CSS styles, but "hidden" is not a CSS style. Perhaps you meant .classed?
The .html call clears the well-setup tooltip with just the name. If you were looking to replace just the country name, remember it's under the subelement #country_name. So, you could do so with, e.g., tooltip.select("#country_name").text(d.name). Note, I used .text which safely escapes HTML code.
Also note you're already embedding the country name in the original .html call, making it unnecessary. The safest option, though, would be to actually remove the d.name from the other .html call, just leaving it <div class="fixed" id="country_name"></div>, and then right after the .html call, include the .text code I have above. You could even link it in D3 style like so,
tooltip.classed("hidden", false)
.attr("fill","#19202d")
.style("top", 150+ "px")
.style("left", 20 + "px")
// .html line does not contain d.name embed:
.html('<div class="container">...</div>')
.select("#country_name")
.text(d.name);
Using your updated code, I was able to get this to work for me.
Keep up the good work!
I have a D3 generated map which needs to be able to dynamically change the fill element of drawn paths. The original paths are generated and assigned a class of 'boundaries'. The hover behaviour is set to turn the country yellow when the user hovers the cursor over the country. However, if I then go and dynamically change the fill color of the country, for example by using d3.selectAll- (I have simplified the below example so that this behaviour is simulated by uncommenting the last section), the hover behaviour stops working. The class has not changed, so why is the hover behaviour now not occurring.. and is there a workaround for this?
CSS
.countryMap{
width: 500px;
height: 500px;
position: relative;
}
.boundaries{
fill:green;
}
.boundaries:hover{
fill:yellow;
}
Javascript
const countryMap = {};
const FILE = `aus.geojson`; // the file we will use
var svg = d3
.select('div.country__map')
.append('svg')
.attr('width',200)
.attr('height',200)
.attr('preserveAspectRatio', 'xMinYMin meet')
.attr('viewBox','770 270 200 150')
d3.json(FILE).then(function (outline) {
countryMap.features = outline.features;
// choose a projection
const projection = d3.geoMercator();
// create a path generator function initialized with the projection
const geoPath = d3.geoPath().projection(projection);
drawOutline();
function drawOutline() {
svg
.selectAll('path.boundaries') // CSS styles defined above
.data(countryMap.features)
.enter()
.append('path')
.attr('class', 'boundaries')
.attr('d', geoPath)
// .style('fill', d => {
// return 'green';
// })
}
})
As #Michael mentioned it will be better to manually add or remove class using js.
D3 provides us mouseover and mouseout events which can be used to add and remove class.
Here on hover, we are applying the 'active' class on the element.
svg
.selectAll('path.boundaries')
.data(countryMap.features)
.enter()
.append('path')
.attr('class', 'boundaries')
.attr('d', geoPath)
.on('mouseover', function () {
d3.select(this).classed("active", true)
})
.on('mouseout', function () {
d3.select(this).classed("active", false)
})
We also need to update the CSS according to these changes.
You can update the CSS to:
.boundaries{
fill:green;
}
.boundaries.active{
fill:yellow;
}
I am currently trying to implement a zooming feature that is slightly different from the one that already exists.
Actually, I would like that if a user clicks and drags on the graph it zooms on the so defined domain. I would like to do it that way because with the mouse wheel it prevents the user from the page up/down.
As it doesn't seem to be possible with the C3.js API, I tried to implement the drag event by following this little walkthrough on D3.js Drag Behaviour.
However, I didn't understand it well as it is not working when I try it on the graph.
Here's a sample of my code :
function setHandlers() {
/**
* A custom drag event to zoom on the graph
*/
var drag = d3.behavior.drag();
d3.selectAll('.c3-event-rects').on(".drag", null);
drag
.on('dragstart', function (d) {
d3.event.sourceEvent.stopPropagation();
console.log("start");
console.log(d)
})
.on('drag', function (d) {
console.log("on bouge :)")
})
.on('dragend', function (d) {
console.log("end");
console.log(d)
})
}
I call this function whenever I refresh my graph and I have already coded a custom handler for a double click (in the same function but I have it off to be more clear). I would like to know how to successfully trigger a drag event in a C3.js graph, especially the dragstart and dragend events?
c3-event-rects marks the layer that drives the events (tooltips, clicks, etc.). You don't usually want to move this unless you want the event reactions to be offset (like you get the bar 1 tooltips when you hover over someplace else than over bar 1)
Also, the layer has it's opacity set to 0, so you can't actually see it move unless you override it's opacity. Something like
.c3-event-rects {
fill: red;
fill-opacity: 0.2 !important;
}
That said, here is how you do this
var drag = d3.behavior.drag()
.origin(function () {
var t = d3.select(this);
var translate = d3.transform(t.attr('transform')).translate;
return { x: translate[0], y: translate[1]};
})
.on("drag", function () {
d3.select(this)
.attr("transform", "translate(" + d3.event.x + " " + d3.event.y + ")")
})
d3.selectAll('.c3-event-rects').call(drag);
Fiddle - http://jsfiddle.net/d5bhwwLh/
Here's how it looks - the red spot is where I had my mouse over to make the tooltip appear for the first set of bars
I am trying to create a simple shape, let's say a circle, in d3.js using drag and drop of a DOM element, let's say a div. So here is what I did:
<!DOCTYPE html>
<html>
<head>
<title>d3 tree with drag and drop</title>
<style type="text/css">
#dropInSVG {
width:200px;
height:200px;
margin-left:20px;
background-color:#F8F8F8 ;
}
#dropInSVG svg {
width: 200px;
height:200px;
background-color:yellow;
}
#tobeDropped{
width:50px;
height:15px;
background-color:pink;
float:left;
}
#mainContainer {
width: 250px;
height: 250px;
background-color:orange;
cursor:pointer;
}
</style>
</head>
<body>
<div id="mainContainer">
<div id="dropInSVG"></div>
<div id="tobeDropped"></div>
</div>
</body>
<script type="text/javascript" src="./lib/jquery.js"></script>
<script type="text/javascript" src="./lib/jquery-ui.js"></script>
<script type="text/javascript" src="./lib/d3.js"></script>
<script type="text/javascript" src="./lib/d3.layout.js"></script>
<script type="text/javascript" src="d3appDrag.js"></script>
</html>
JavaScript code:
var treeCreator = function(){};
treeCreator.prototype.callbacktest = function(svgContainer){
alert('the element has been dropped');
};
treeCreator.prototype.createTreeNode = function(theSVG){
$('#tobeDropped').remove();
theSVG.append("circle")
.style("stroke","green")
.style("fill","white")
.attr("r",40)
.attr("cx", 100)
.attr("cy", 100)
.on("mouseover", function () {
d3.select(this).style("fill", "aliceblue");
})
.on("mouseout", function () {
d3.select(this).style("fill", "red");
});
};
$(document).ready(function(){
$('#tobeDropped').draggable({containment:'#mainContainer'});
var theSVG = d3.select('#dropInSVG')
.append("svg")
.attr("width",200)
.attr("height",200);
var tc = new treeCreator();
$('#dropInSVG').droppable({
drop: function(){
tc.createTreeNode(theSVG);
}
});
});
The problem is that the circle is not showing up. Could you please see what's wrong?
Thanks
Mohamed Ali
I resolved this issue by using
.append("svg:svg")
and
.append("svg:circle")
instead of
.append("svg")
and
.append("circle");
however I don't know why I should do that, for instance the following example works with the first type of selectors in jsFiddle however it didn't work when I tried locally in my browser!
I have just checked your own code and it works well. The circle is displayed when I drop the bar into the container. Please see the demo: http://jsfiddle.net/af7zk/
var treeCreator = function(){};
...
You need the SVG namespace defined on the web page to add SVG elements to the embedded SVG. The reason why this is not needed on jsFiddle is because they have their own SVG images used on the page with at least one defined with namespace (xmlns="http://www.w3.org/2000/svg" attribute of the SVG root element).
If you don't have that namespace defined on your SVG element in your own hosted web page the web browser cannot add elements to it as it expects them to be in the XHTML namespace. That of course doesn't know have SVG and CIRCLE tags defined in it.
UPDATE: I've added the SVG namespace attribute in the code below (xmlns attribute). But since web browsers are smart these days and can have SVG namespace already included the example might work for you locally. The real problem was omitting the link to main jQuery library on CDN since the original example won't work outside jsFiddle. So add a link
<script src="https://code.jquery.com/jquery-3.3.1.min.js"></script>
above two other references for jQuery resources.
And other note: for links to work in SVG you have to add XLink namespace as well (in the markup or dynamically via javascript similarly to SVG namespace). Follow SVG specifications though as xlink:href attribute will be changed to just href in SVG 2.0 when this is widely available and supported in web browsers.
http://jsfiddle.net/q1bwzhpc/7
var treeCreator = function(){};
treeCreator.prototype.callbacktest = function(svgContainer){
alert('the element has been dropped');
};
treeCreator.prototype.createTreeNode = function(theSVG){
$('#tobeDropped').remove();
theSVG.append("circle")
.style("stroke","green")
.style("fill","white")
.attr("r",40)
.attr("cx", 100)
.attr("cy", 100)
.on("mouseover", function () {
d3.select(this).style("fill", "aliceblue");
})
.on("mouseout", function () {
d3.select(this).style("fill", "red");
});
};
$(document).ready(function(){
$('#tobeDropped').draggable({containment:'#mainContainer'});
var theSVG = d3.select('#dropInSVG')
.append("svg")
.attr("xmlns", "https://www.w3.org/2000/svg")
.attr("width",200)
.attr("height",200);
var tc = new treeCreator();
$('#dropInSVG').droppable({
drop: function(){
tc.createTreeNode(theSVG);
}
});
});