Embed a svg shape in d3tip tooltip - javascript

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.

Related

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

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)
);

How to position the legend in a d3 chart

How do I position the legend above and out of the chart?
I am working in this d3 example Grouped Bar Chart
Here is my PLUNKER but the legend can overlap the graph. Ideally I would like the legend above and out of the chart.
This is my code that I have to change. I don't understand why the 0 refers to the current position.
var legend = svg.selectAll(".legend")
.data(ageNames.slice().reverse())
.enter().append("g")
.attr("class", "legend")
.attr("transform", function(d, i) { return "translate(0," + i * 20 + ")"; });
I can move the legend as follows: PLUNKER
.attr("transform", function(d, i) { return "translate(" + "-500," + i * 20 + ")"; }); which moves imore to the center.
I can then have the legend read from left to right as follows:
.attr("transform", function(d, i) { return "translate(" + (-700+i*100) + "," + 0 + ")"; }); I would be great if I could move this above and outside the chart as it still overlaps some of the graph.
EDIT1 PLUNKER
tks to an answer belwo. This is my attempt, which is above the chart as I would expect, but I would like the different series in the legend to appear closer together (there is too much white space). So how do I have the coloured rect and then the text beside it, but without the whitespace?
## the below is close but I am just guessing
var legendHolder = svg.append('g')
// translate the holder to the right side of the graph
.attr('transform', "translate(" + (-width) + "," + (-margin.top) + ")")
.attr('class','legendHolder')
var legend = legendHolder.selectAll(".legend")
.data(ageNames.slice().reverse())
.enter().append("g")
.attr("class", "legend")
legend.append("rect")
.attr("x", function(d,i){return (width +(150*i))})
.attr("width", 36)
.attr("height", 18)
//.style("text-anchor", "end")
.style("fill", color);
legend.append("text")
//.attr("x", width - 24)
.attr("x", function(d,i){return (width +(140*i))})
.attr("y", 9)
.attr("dy", ".35em")
//.style("text-anchor", "end")
.text(function(d) { return d; });
EDIT2 PLUNKER
This is the best I can do, but I fell I am jsut guessing, maybe I will revisit but in the meant time if anyone can beautifully explain it to me that would be greatly appreciated
var legendHolder = svg.append('g')
// translate the holder to the right side of the graph
.attr('transform', "translate(" + (-width) + "," + (-margin.top) + ")")
.attr('class','legendHolder')
var legend = legendHolder.selectAll(".legend")
.data(ageNames.slice().reverse())
.enter().append("g")
.attr("class", "legend")
.attr('transform', function(d, i) { return "translate(" + -40*i + "," + 0 + ")"; })
.attr("width", 36)
legend.append("rect")
.attr("x", function(d,i){return (width +(150*i))})
.attr("width", 18)
.attr("height", 18)
//.style("text-anchor", "end") //"startOffset"="100%
//.style("startOffset","100%") //"startOffset"="100%
.style("fill", color);
legend.append("text")
//.attr("x", width - 24)
.attr("x", function(d,i){return (width +(150*i)+20)})
.attr("y", 9)
.attr("dy", ".35em")
//.style("text-anchor", "end")
.text(function(d) { return d; });
If you want the legend to be located outside of the graph, you just need to increase the size of the margin where you want it to be placed and translate it into position.
Right now you are positioning the individual parts of your legend based on the size of the <svg>. You can simplify this by creating a <g> that contains all of your legend elements and translating that to its desired position in the graph.
You'll need to play around with the values to get exactly what you want, but below are the values that would allow you to place the legend in the right margin.
var margin = {top: 20, right: 100, bottom: 30, left: 40};
var legendHolder = svg.append('g')
// translate the holder to the right side of the graph
.attr('transform', "translate(" + (margin.left + width) + ",0)")
var legend = legendHolder.selectAll(".legend")
.data(ageNames.slice().reverse())
.enter().append("g")
.attr("class", "legend")
.attr("transform", function(d, i) { return "translate(0," + i * 20 + ")"; });
legend.append("rect")
.attr("x", 0)
.attr("width", 18)
.attr("height", 18)
.style("fill", color);
legend.append("text")
.attr("x", 0)
.attr("y", 9)
.attr("dy", ".35em")
.style("text-anchor", "end")
.text(function(d) { return d; });
The legend in the example appears on the right hand side, despite a transform of zero because the elements in the group have an x attribute of nearly the width of the frame (minus a small offset), pushing them to the right:
legend.append("rect")
.attr("x", width - 18)
.attr("width", 18)
.attr("height", 18)
.style("fill", color);
legend.append("text")
.attr("x", width - 24)
.attr("y", 9)
.attr("dy", ".35em")
.style("text-anchor", "end")
.text(function(d) { return d; });
So an x transform of -500, about half your width, pulls it to the middle, as noted. Using a smaller x attribute for the legend elements might help make it clearer for setting up your legend (this is seen in the other answer), though as your comment notes, it isn't too hard to make it work as it is (just more confusing than needed).

D3 Set labels on Legend

I need help setting the labels on a D3 stacked bar chart. I'm not sure how to map the color in the legend with a reference to the name property in the data object.
I have a JSFiddle here:
http://jsfiddle.net/Lhs3e7xk/1/
The code in particular I need help with is the legend function:
function updateLegend(dt) {
var legend = svg.selectAll(".legend")
.data(color.domain()) // I tried dt as well.
.enter().append("g")
.attr("class", "legend")
.attr("transform", function(d, i) {
return "translate(0," + i * 20 + ")";
});
legend.append("rect")
.attr("x", width - 18)
.attr("width", 18)
.attr("height", 18)
.style("fill", color);
legend.append("text")
.attr("x", width - 24)
.attr("y", 9)
.attr("dy", ".35em")
.style("text-anchor", "end")
.text(function(d, i) {
console.log(d)
return color(d.name)
});
}
The output should be the value for the name property in the data set and the color associated with that group.
Fixed MBS [Color01]
Floating MBS [Color02]
CMO [Color03]
Thank you!
Instead of this
.text(function(d, i) {
console.log(d)
return color(d.name)
})
do the text function like this:
.text(function(d, i) {
if(i==0)
return "Fixed MBS"
if(i==1)
return "Floating MBS"
if(i==2)
return "CMO"
});
Working example here
EDIT
For setting legends using data
//your dynamic legend data set
var legends = ["Fixed MBS", "Floating MBS", "CMO"];
function updateLegend(dt) {
var legend = svg.selectAll(".legend")
.data(legends)//pass the lgend data
.enter().append("g")
.attr("class", "legend")
.attr("transform", function(d, i) {
return "translate(0," + i * 20 + ")";
});
legend.append("rect")
.attr("x", width - 18)
.attr("width", 18)
.attr("height", 18)
.style("fill", function(d, i){return color(i)});//color based on index
legend.append("text")
.attr("x", width - 24)
.attr("y", 9)
.attr("dy", ".35em")
.style("text-anchor", "end")
.text(function(d, i) {
return d;//return the array data
});
}
Working code here

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.

Apply D3 tooltip to Donut Multiples

I have code like this that creates multiple D3 donut multiples.
<!DOCTYPE html>
<meta charset="utf-8">
<style>
body {
font: 10px sans-serif;
}
svg {
padding: 10px 0 0 10px;
}
.arc {
stroke: #fff;
}
</style>
<body>
<script src="http://d3js.org/d3.v3.min.js"></script>
<script>
var radius = 74,
padding = 10;
var color = d3.scale.ordinal()
.range(["#98abc5", "#8a89a6", "#7b6888", "#6b486b", "#a05d56", "#d0743c", "#ff8c00"]);
var arc = d3.svg.arc()
.outerRadius(radius)
.innerRadius(radius - 30);
var pie = d3.layout.pie()
.sort(null)
.value(function(d) { return d.population; });
d3.csv("data.csv", function(error, data) {
color.domain(d3.keys(data[0]).filter(function(key) { return key !== "State"; }));
data.forEach(function(d) {
d.ages = color.domain().map(function(name) {
return {name: name, population: +d[name]};
});
});
var legend = d3.select("body").append("svg")
.attr("class", "legend")
.attr("width", radius * 2)
.attr("height", radius * 2)
.selectAll("g")
.data(color.domain().slice().reverse())
.enter().append("g")
.attr("transform", function(d, i) { return "translate(0," + i * 20 + ")"; });
legend.append("rect")
.attr("width", 18)
.attr("height", 18)
.style("fill", color);
legend.append("text")
.attr("x", 24)
.attr("y", 9)
.attr("dy", ".35em")
.text(function(d) { return d; });
var svg = d3.select("body").selectAll(".pie")
.data(data)
.enter().append("svg")
.attr("class", "pie")
.attr("width", radius * 2)
.attr("height", radius * 2)
.append("g")
.attr("transform", "translate(" + radius + "," + radius + ")");
svg.selectAll(".arc")
.data(function(d) { return pie(d.ages); })
.enter().append("path")
.attr("class", "arc")
.attr("d", arc)
.style("fill", function(d) { return color(d.data.name); });
svg.append("text")
.attr("dy", ".35em")
.style("text-anchor", "middle")
.text(function(d) { return d.State; });
});
</script>
I am looking for a way to implement the D3 tooltip so that I can see the exact data of each chunk of the donut when I put my cursor over it. I understand that there are other examples of the tooltip on here but none of them have worked with the donut multiples example.
Here is some example data
State,Under 5 Years,5 to 13 Years,14 to 17 Years,18 to 24 Years,25 to 44 Years,45 to 64 Years,65 Years and Over
AL,310504,552339,259034,450818,1231572,1215966,641667
AK,52083,85640,42153,74257,198724,183159,50277
AZ,515910,828669,362642,601943,1804762,1523681,862573
AR,202070,343207,157204,264160,754420,727124,407205
CA,2704659,4499890,2159981,3853788,10604510,8819342,4114496
CO,358280,587154,261701,466194,1464939,1290094,511094
CT,211637,403658,196918,325110,916955,968967,478007
DE,59319,99496,47414,84464,230183,230528,121688
DC,36352,50439,25225,75569,193557,140043,70648
FL,1140516,1938695,925060,1607297,4782119,4746856,3187797
GA,740521,1250460,557860,919876,2846985,2389018,981024
HI,87207,134025,64011,124834,356237,331817,190067
ID,121746,201192,89702,147606,406247,375173,182150
IL,894368,1558919,725973,1311479,3596343,3239173,1575308
IN,443089,780199,361393,605863,1724528,1647881,813839
IA,201321,345409,165883,306398,750505,788485,444554
KS,202529,342134,155822,293114,728166,713663,366706
KY,284601,493536,229927,381394,1179637,1134283,565867
LA,310716,542341,254916,471275,1162463,1128771,540314
ME,71459,133656,69752,112682,331809,397911,199187
MD,371787,651923,316873,543470,1556225,1513754,679565
The D3 doc of this can be found at
http://bl.ocks.org/mbostock/3888852
I'm not sure what you're referring to when you say "the D3 tooltip", because d3 doesn't have any built-in tooltip functionality. That said, there are some good third-party plugins out there for doing tooltips in d3.
d3-tip is one that would work well for what you're trying to do.
You can create a tooltip function to display your population data for each arc like this:
var tip = d3.tip()
.attr('class', 'd3-tip')
.html(function(d) { return d.data.population; })
.direction('s');
Then you can call the function on your svg selection:
svg.call(tip);
Finally, you can use mouse event listeners on your arcs to show and hide the tooltip:
svg.selectAll(".arc")
.data(function(d) { return pie(d.ages); })
.enter().append("path")
.attr("class", "arc")
.attr("d", arc)
.style("fill", function(d) { return color(d.data.name); })
.on('mouseover', tip.show)
.on('mouseout', tip.hide);
Here's a PLUNK with a working example.
You can also check up on the d3-tip documentation here.
I too got the same TypeError while debugging my javascript code using d3js while adding tooltip for my barcharts.
bars.append('rect')
.attr('y', maxHeight)
.attr('height', 0)
.attr('width', function (d) { return x.rangeBand(d) - 1; })
.attr('class', 'bar')
.transition().duration(1500)
.attr('y', function (d, i) { return y(d.y); })
.attr('height', function (d, i) { return maxHeight - y(d.y); });
.on('mouseover', tip.show)
.on('mouseout', tip.hide);
In the above code i used ".on('mouseover', tip.show)" on "bars.append()".This is wrong usage.
Later, i found that ".on('mouseover', tip.show)" should be applied on function ".select('rect') or .selectall('rect')".
Now below snippet is working correctly for my application.
bars.append('rect')
.attr('y', maxHeight)
.attr('height', 0)
.attr('width', function (d) { return x.rangeBand(d) - 1; })
.attr('class', 'bar')
.transition().duration(1500)
.attr('y', function (d, i) { return y(d.y); })
.attr('height', function (d, i) { return maxHeight - y(d.y); });
bars.select('rect')
.on('mouseover', tip.show)
.on('mouseout', tip.hide);

Categories

Resources