I'm building a D3 treemap diagram that requires a tooltip. The tooltip is a div element that I'm toggling on and off with the 'mouseover' and 'mouseleave' events attached to each leaf of the treemap. The developer console confirms that the element is being added to the DOM, that it has the correct text to display within the div, that 'display' is set to 'block' when hovering, 'z-index' is 10, and that the 'position' is 'absolute', however, I can't get the tooltip to actually appear as expected.
Here's a link to the Codepen.
Here's my actual code:
HTML:
<script src="https://cdn.freecodecamp.org/testable-projects-fcc/v1/bundle.js"></script>
<div>
<h1 id='title'>JSON Data Treemaps</h1>
<h2 id='description'>Top 100 Most Pledged Kickstarter Campaigns</h2>
<svg id="canvas"></svg>
<svg id="legend"></svg>
</div>
CSS:
#import url('https://fonts.googleapis.com/css?family=Noto+Sans');
body {
background-color: ghostwhite;
display: flex;
flex-direction: column;
font-size: 16px;
font-family: 'Noto Sans', sans-serif;
div#tooltip {
position: absolute;
}
div {
display: flex;
flex-direction: column;
h1 {
margin: 4.6rem auto 0;
font-size: 3.2rem;
}
h2 {
margin: 1rem auto 2rem;
font-size: 1.4rem;
}
svg {
margin: auto;
}
svg#legend {
margin: 2rem auto;
}
}
}
JS:
const margin = {
top: 30,
bottom: 100,
left: 30,
right: 30
},
width = 960 - margin.left - margin.right,
height = 570 - margin.top - margin.bottom;
const KICK_STARTER_DATA_URL = 'https://cdn.rawgit.com/freeCodeCamp/testable-projects-fcc/a80ce8f9/src/data/tree_map/kickstarter-funding-data.json';
const MOVIE_DATA_URL = 'https://cdn.rawgit.com/freeCodeCamp/testable-projects-fcc/a80ce8f9/src/data/tree_map/movie-data.json';
const VIDEO_GAME_DATA_URL = 'https://cdn.rawgit.com/freeCodeCamp/testable-projects-fcc/a80ce8f9/src/data/tree_map/video-game-sales-data.json';
// Load all data and assign to variables.
d3.queue()
.defer(d3.json, KICK_STARTER_DATA_URL)
.defer(d3.json, MOVIE_DATA_URL)
.defer(d3.json, VIDEO_GAME_DATA_URL)
.await(storeDataCategories);
let KICK_STARTER_DATA;
let MOVIE_DATA;
let VIDEO_GAME_DATA;
let currentDataSet;
function storeDataCategories(error, kickStarter, movies, videoGames) {
// console.log(...arguments);
if(error) throw error;
KICK_STARTER_DATA = kickStarter;
MOVIE_DATA = movies;
VIDEO_GAME_DATA = videoGames;
currentDataSet = kickStarter;
buildTreemap();
buildLegend();
}
var svg = d3.select('svg#canvas')
.attr('width', width)
.attr('height', height)
.style('background-color', 'ghostwhite')
.style('box-shadow', '0 6px 26px darkgray');
var fader = (color) => {
return d3.interpolateRgb(color, 'ghostwhite')(0.2);
},
test = d3.schemeCategory20.map(fader),
colorScale = d3.scaleOrdinal(d3.schemeCategory20.map(fader)),
format = d3.format(',d');
var treemap = d3.treemap()
.tile(d3.treemapResquarify)
.size([width, height]);
// Adding tooltip for info on hover
const tooltip = d3.select('svg#canvas')
.append('div')
.attr('id', 'tooltip')
.attr('width', 60 + 'px')
.attr('height', 40 + 'px')
.style('z-index', 10)
.style('display', 'none')
.style('position', 'absolute');
function buildTreemap() {
// console.log(KICK_STARTER_DATA);
// console.log(MOVIE_DATA);
// console.log(VIDEO_GAME_DATA);
// console.log(currentDataSet);
var root = d3.hierarchy(currentDataSet)
.sum(sumValue)
.sort((a, b) => b.height - a.height || b.value - a.value);
treemap(root);
var cell = svg.selectAll('g')
.data(root.leaves())
.enter().append('g')
.attr('transform', d => 'translate(' + [d.x0, d.y0] + ')')
.on('mouseover', d => {
// console.log('mouseover - d:\n', d);
const tooltipText = formatTooltip(d);
tooltip.transition().duration(200)
.style('position', 'absolute')
.style('display', 'block');
tooltip.html(tooltipText)
.attr('data-value', d.value)
.style('top', (d3.event.pageY - 50) + 'px')
.style('left', (d3.event.pageX + 20) + 'px');
})
.on('mouseout', d => {
// console.log('mouseout!');
tooltip.transition().duration(500)
.style('display', 'none');
});
cell.append('rect')
.attr('id', d => d.data.id)
.attr('class', 'tile')
.attr('width', d => d.x1 - d.x0)
.attr('height', d => d.y1 - d.y0)
.attr('fill', d => colorScale(d.parent.data.name))
.attr('data-name', d => d.data.name)
.attr('data-category', d => d.parent.data.name)
.attr('data-value', d => d.data.value);
cell.append('clipPath')
.attr('id', d => `clip-${d.data.name}`)
.append('use')
.attr('xlink:href', d => `#${d.data.name}`);
cell.append('text')
.attr('clip-path', d => `url(#clip-${d.data.name})`)
.selectAll('tspan')
.data(d => d.data.name.split(/(?=[A-Z][^A-Z])/g))
.enter().append('tspan')
.attr('x', 4)
.attr('y', (d, i) => 13 + i * 10)
.text(d => d)
.style('font-size', '10');
}
function buildLegend() {
console.log(`inside 'buildLegend'`);
// console.log(currentDataSet);
const rectWidth = width / 8,
rectHeight = 40;
const legend = d3.select('svg#legend')
.attr('width', width)
.attr('height', rectHeight);
legend.append('g')
.selectAll('g')
.data(colorScale.domain())
.enter()
.append('g')
.attr('class', 'legend')
.append('rect')
.attr('class', 'legend-item')
.attr('width', '40px')
.attr('height', '40px')
.attr('transform', (d, i) => {
return 'translate(' + i * 40 + ',' + 0 + ')';
})
.style('fill', d => colorScale(d))
.style('stroke', d => colorScale(d));
}
function sumValue(d) {
return d.value;
}
function formatTooltip(d) {
const name = d.data.name,
category = d.data.category,
value = d.data.value,
tooltipText = `
<span>Name:</span> ${name} <br>
<span>Category: </span> ${category} <br>
<span>Value:</span> ${value} <br>`;;
console.log('formatted tooltip:\n', tooltipText);
return tooltipText;
}
Minor mistake: You're appending a <div> to a SVG which wouldn't work. Changing the code and appending it to the body, here's a fork:
https://codepen.io/anon/pen/KbVvwR
Code changes:
const tooltip = d3.select('body')
and a couple of minor CSS additions:
div#tooltip {
background: #FFF;
pointer-events: none; // important
padding: 4px;
border: 1px solid #CCC;
border-radius: 3px;
}
Hope this helps.
Related
I am trying to add a hover effect without success. How to show data when hovering over datapoint?
https://plnkr.co/edit/Va1Dw3hg2D5jPoNGKVWp?p=preview&preview
<!DOCTYPE html>
svg {
font: 12px sans-serif;
text-anchor: middle;
}
rect {
stroke: lightgray;
stoke-width: 1px;
fill: none;
}
.y.axis path {
fill: none;
stroke: none;
}
</style>
d3.csv('data.csv', function (error, rows) {
var data = [];
rows.forEach(function (d) {
var x = d[''];
delete d[''];
for (prop in d) {
var y = prop,
value = d[prop];
data.push({
x: x,
y: y,
value: +value,
});
}
});
var margin = {
top: 25,
right: 80,
bottom: 25,
left: 25,
},
width = 500 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom,
domain = d3
.set(
data.map(function (d) {
return d.x;
})
)
.values(),
num = Math.sqrt(data.length),
color = d3.scale
.linear()
.domain([-1, 0, 1])
.range(['#B22222', '#fff', '#000080']);
var x = d3.scale.ordinal().rangePoints([0, width]).domain(domain),
y = d3.scale.ordinal().rangePoints([0, height]).domain(domain),
xSpace = x.range()[1] - x.range()[0],
ySpace = y.range()[1] - y.range()[0];
var svg = d3
.select('body')
.append('svg')
.attr('width', width + margin.left + margin.right)
.attr('height', height + margin.top + margin.bottom)
.append('g')
.attr(
'transform',
'translate(' + margin.left + ',' + margin.top + ')'
);
var cor = svg
.selectAll('.cor')
.data(data)
.enter()
.append('g')
.attr('class', 'cor')
.attr('transform', function (d) {
return 'translate(' + x(d.x) + ',' + y(d.y) + ')';
});
cor
.append('rect')
.attr('width', xSpace)
.attr('height', ySpace)
.attr('x', -xSpace / 2)
.attr('y', -ySpace / 2);
cor
.filter(function (d) {
var ypos = domain.indexOf(d.y);
var xpos = domain.indexOf(d.x);
for (var i = ypos + 1; i < num; i++) {
if (i === xpos) return false;
}
return true;
})
.append('text')
.attr('y', 5)
.text(function (d) {
if (d.x === d.y) {
return d.x;
} else {
return d.value.toFixed(2);
}
})
.style('fill', function (d) {
if (d.value === 1) {
return '#000';
} else {
return color(d.value);
}
});
cor
.filter(function (d) {
var ypos = domain.indexOf(d.y);
var xpos = domain.indexOf(d.x);
for (var i = ypos + 1; i < num; i++) {
if (i === xpos) return true;
}
return false;
})
.append('circle')
.attr('r', function (d) {
return (width / (num * 2)) * (Math.abs(d.value) + 0.1);
})
.style('fill', function (d) {
if (d.value === 1) {
return '#000';
} else {
return color(d.value);
}
});
var aS = d3.scale
.linear()
.range([-margin.top + 5, height + margin.bottom - 5])
.domain([1, -1]);
var yA = d3.svg.axis().orient('right').scale(aS).tickPadding(7);
var aG = svg
.append('g')
.attr('class', 'y axis')
.call(yA)
.attr(
'transform',
'translate(' + (width + margin.right / 2) + ' ,0)'
);
var iR = d3.range(-1, 1.01, 0.01);
var h = height / iR.length + 3;
iR.forEach(function (d) {
aG.append('rect')
.style('fill', color(d))
.style('stroke-width', 0)
.style('stoke', 'none')
.attr('height', h)
.attr('width', 10)
.attr('x', 0)
.attr('y', aS(d));
});
});
</script>
Create a new div and give it some styles. also set the opacity to 0.
var popupDiv = d3
.select("#root")
.append("div")
.attr("class", "tooltip")
.style("opacity", 0);
Next, you need to add a mouseenter and mouseout to your desired elements
d3.append("div") // Change this to your element(s)
.on("mouseenter", function (d, i) {
popupDiv.transition().duration(200).style("opacity", 0.9);
popupDiv
.html(i.Description) // Example of display a data item description
.style("left", d.pageX + "px") // X offset can be added here
.style("top", d.pageY + "px"); // Y offset can be added here
})
.on("mouseout", function (d, i) {
popupDiv.transition().duration(200).style("opacity", 0);
})
Bear in mind the first parameter d to the mouseenter and mouseout functions is the mouse event itself which is being used to find out the x and y position of the mouse. The second parameter i is the data attached to your element if you wanted to use any of the data for specific descriptions or colours etc.
These are the base styles I used when creating a popup
div.tooltip {
position: absolute;
text-align: center;
width: auto;
height: auto;
max-width: 200px;
padding: 8px;
font: 12px sans-serif;
background: lightsteelblue;
border: 0px;
border-radius: 8px;
pointer-events: none;
z-index: 10;
}
Attached is my minimum working example in JS Fiddle. I am able to only show the initial data, but when I change the slider, the data does not update. Where am I going wrong in my code here? Apologies for the poor structure - I am still a beginner in D3.
https://jsfiddle.net/pv02z8em/1/
chartGroup
.selectAll('.line-series')
.data(data, d=> d.x)
.join(
enter => {
enter.append('path')
.attr('class', d => `line-series x_${d.x}`)
.attr("d", drawLine(data))
.style('stroke', 'dodgerblue')
.style('stroke-width', 2)
.style('fill', 'none')
.style('opacity', 1)
},
update => { update.transition().duration(500) },
exit => { exit.remove() }
)
}
You have several issues in that code:
You are not selecting the slider by its ID. It should be d3.select('#slider-x-range');
In the listener, you're not calling buildLine(data);
Remove everything inside buildLine that isn't related to the path itself, otherwise you'll create different SVGs every time the user moves the slider;
Your join structure is currently appending several paths, one above the other. This is certainly not what you want. It could be just:
let path = chartGroup.selectAll('.line-series')
.data([data]);
path = path.enter()
.append('path')
.attr('class', "line-series")
.attr("d", d => drawLine(d))
.style('stroke', 'dodgerblue')
.style('stroke-width', 2)
.style('fill', 'none')
.style('opacity', 1)
.merge(path)
path.transition().duration(500).attr("d", d => drawLine(d))
Here is your code with these and other minor changes:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="css/normalize.css">
<link rel="stylesheet" href="css/skeleton.css">
<link rel="stylesheet" href="css/skeleton_override.css">
<style>
svg {
display: inline-block;
position: relative;
vertical-align: top;
overflow: hidden;
}
.x-axis,
.y-axis {
font: 16px sans-serif;
}
.axis-label {
font: 18px sans-serif;
}
.chart-title {
font: 24px sans-serif;
}
.x-axis .tick:first-of-type text {
fill: none;
}
.body {
display: flex;
}
.chart-group-container {
margin: 10px 20px 20px 20px;
}
.controls-container {
display: flex;
flex-direction: column;
}
.controls-header {
color: black;
padding: 0.5rem 2rem;
text-align: left;
}
.controls-body {
overflow: auto;
font-size: 0.8em;
cursor: default;
}
.slidecontainer {
text-align: left;
margin: 10px;
font-family: sans-serif;
font-size: 14px;
}
#slider-x-range {
vertical-align: bottom;
}
</style>
<title>Document</title>
</head>
<body>
<div class="chart-group-container">
<div class="row">
<div class="six columns">
<div class="chart-container">
<div class="viz">
</div>
</div>
</div>
<div class="six columns"></div>
<div class="controls-container">
<div class="controls-header">UI Controls</div>
<div class="controls-body">
<div class="slider-label">Adjust x axis</div>
<div class="slidecontainer">
<span>10</span>
<input type="range" min="10" max="100" value="1" id="slider-x-range">
<span>100</span>
</div>
</div>
</div>
</div>
<script src="https://d3js.org/d3.v5.min.js"></script>
<script>
//let sinWave = Math.sin(x)
let range = function(start, stop, step) {
step = step || 1;
let arr = []
for (let i = start; i < stop; i += step) {
arr.push(i);
}
return arr;
}
let generateSinWave = function(x) {
let y = []
x.forEach(function(i) {
y.push(Math.sin(i))
});
return y;
}
const generateData = (n) => {
x = range(0, n, 1)
y = generateSinWave(x)
let labels = ['x', 'y']
let data = []
for (let i = 0; i < x.length; i++) {
data.push({
x: x[i],
y: y[i]
})
}
return data;
}
</script>
<script>
let margin = {
top: 50,
right: 30,
bottom: 30,
left: 100
},
width = 800 - margin.left - margin.right
height = 400 - margin.top - margin.bottom;
let xScale = d3.scaleLinear()
.range([0, width])
let yScale = d3.scaleLinear()
.range([height, 0])
.nice()
let drawLine = d3.line()
.x(d => xScale(d.x))
.y(d => yScale(d.y))
.curve(d3.curveBasis);
let svg = d3.select('.viz')
.append('svg')
.attr("viewBox", `0 0 ${width + margin.left + margin.right} ${height + margin.top + margin.bottom}`)
.attr("preserveAspectRatio", "xMinYMin meet")
//.attr('width', `${width + margin.left + margin.right}px`)
//.attr('height', `${height + margin.top + margin.bottom}px`)
//.classed("svg-content", true);
.append('g')
.attr('class', 'line-chart-container')
.attr('transform', `translate(${margin.left}, ${margin.top})`);
/* d3.select(".line-chart-container")
.attr("style", "outline: thin solid black;")
.attr("margin-right", "102px") */
const chartGroup = svg.append('g').attr('class', 'line-chart')
// Draw x axis
const xAxis = d3.axisBottom(xScale).tickSizeOuter(0);
const xAxisDraw = svg
.append('g')
.attr('class', 'x-axis')
//.style('font', '14px sans-serif')
.attr('transform', `translate(0, ${height / 2})`)
.call(xAxis);
const yAxis = d3
.axisLeft(yScale)
.ticks(10)
//.tickSizeInner(-width);
const yAxisDraw = svg
.append('g')
.attr('class', 'y-axis')
.call(yAxis);
// x axis label
svg.append('text')
.attr('class', 'axis-label')
.attr('text-anchor', 'end')
.text('X axis')
.attr('x', width)
.attr('y', height - margin.bottom + 50)
// y axis label
svg.append('text')
.attr('class', 'axis-label')
.attr('text-anchor', 'end')
.attr('transform', 'rotate(-90)')
.attr('x', margin.top + 50 - (height / 2))
.attr('y', margin.left - 160)
.text('Y axis')
// Draw Header
const header = svg
.append('g')
.attr('class', 'chart-title')
.attr('transform', `translate(${width / 2 - 75}, ${margin.top - 75})`)
.append('text')
header.append('tspan').text('Sine wave')
function buildLine(data) {
xScale.domain([d3.min(data, d => d.x), d3.max(data, d => d.x)])
yScale.domain([d3.min(data, d => d.y), d3.max(data, d => d.y)])
let path = chartGroup
.selectAll('.line-series')
.data([data]);
path = path.enter().append('path')
.attr('class', "line-series")
.attr("d", d => drawLine(d))
.style('stroke', 'dodgerblue')
.style('stroke-width', 2)
.style('fill', 'none')
.style('opacity', 1)
.merge(path)
path.transition().duration(500).attr("d", d => drawLine(d))
}
let xRangeSlider = document.getElementById('slider-x-range');
xRangeSlider.min = 10;
xRangeSlider.max = 100;
let data = generateData(xRangeSlider.value)
buildLine(data)
d3.select('#slider-x-range')
.on("change", d => {
data = generateData(xRangeSlider.value)
buildLine(data)
});
</script>
</body>
</html>
As you'll find out, the transition is not what you probably expect: that's an unrelated issue, the interpolation of the d property string (you can find more info here).
I put together the following treemap using d3.js. It's the top 20 states in terms of voter turnout. Link
I'm unsure how to add the 'values' into the hover. Ideally, it should show the unique value for each state. I'm able to do it for one (California in the example), but I'm not sure how to make it flexible so that it generates other values for different states.
let margin = {
top: 0,
right: 0,
bottom: 0,
left: 0
},
width = 1100 - margin.left - margin.right,
height = 900 - margin.top - margin.bottom;
// append the svg object to the body of the page
let svg = d3
.select('#treemap')
.append('svg')
.attr('width', width + margin.left + margin.right)
.attr('height', height + margin.top + margin.bottom)
.append('g')
.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
// Read data
d3.csv('https://raw.githubusercontent.com/oihamza/Interactive-Data-Vis-Fall2020/master/Project%201/stateVoterTurnout.csv', function(data) {
// stratify the data: reformatting for d3.js
var root = d3
.stratify()
.id(function(d) {
return d.name;
}) // Name of the entity (column name is name in csv)
.parentId(function(d) {
return d.parent;
})(
// Name of the parent (column name is parent in csv)
data
);
// data is an object
console.log(data);
let values = data[1].value;
let tooltip = d3
.select('body')
.append('div')
.style('position', 'absolute')
.style('z-index', '10')
.style('visibility', 'hidden')
.style('background-color', 'white')
.style('border', 'solid')
.style('border-width', '2px')
.style('border-radius', '5px')
.style('padding', '5px')
.text(`${values} voters`);
root.sum(function(d) {
return +d.value;
}); // Compute the numeric value for each entity
// Then d3.treemap computes the position of each element of the hierarchy
// The coordinates are added to the root object above
d3.treemap().size([width, height]).padding(3)(root);
console.log(root.leaves());
// use this information to add rectangles:
svg
.selectAll('rect')
.data(root.leaves())
.enter()
.append('rect')
.attr('x', function(d) {
return d.x0;
})
.attr('y', function(d) {
return d.y0;
})
.attr('width', function(d) {
return d.x1 - d.x0;
})
.attr('height', function(d) {
return d.y1 - d.y0;
})
.style('stroke', 'black')
.style('fill', '#945f04')
.on('mouseover', function() {
return tooltip.style('visibility', 'visible');
})
.on('mousemove', function() {
return tooltip
.style('top', d3.event.pageY - 10 + 'px')
.style('left', d3.event.pageX + 10 + 'px');
})
.on('mouseout', function() {
return tooltip.style('visibility', 'hidden');
});
// and to add the text labels
svg
.selectAll('text')
.data(root.leaves())
.enter()
.append('text')
.attr('x', function(d) {
return d.x0 + 10;
}) // +10 to adjust position (more right)
.attr('y', function(d) {
return d.y0 + 20;
}) // +20 to adjust position (lower)
.text(function(d) {
return d.data.name;
})
.attr('font-size', '15px')
.attr('fill', 'white');
});
body,
h1,
h2,
h3,
h4,
h5,
h6 {
font-family: "Montserrat", sans-serif
}
.styling {
transform-origin: center center;
align-content: center;
position: relative;
}
.tooltip {
position: relative;
display: inline-block;
/* border-bottom: 1px dotted black; */
}
.tooltip .tooltiptext {
visibility: hidden;
width: 180px;
background-color: black;
color: #fff;
text-align: center;
border-radius: 6px;
padding: 5px 0;
/* Position the tooltip */
position: absolute;
z-index: 1;
}
.tooltip:hover .tooltiptext {
visibility: visible;
}
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Montserrat">
<span class="styling"><div id="treemap"></div></span>
<!-- Loading this version of d3 -->
<script src="https://d3js.org/d3.v4.js"></script>
When you mouseover or mousemove the rectangle, you can find it's assigned datum with the first argument (often called d) - just like you do when you set attributes with .attr() or styles with .style().
You can set the .text() of the tooltip dynamically, using this d:
let margin = {
top: 0,
right: 0,
bottom: 0,
left: 0
},
width = 1100 - margin.left - margin.right,
height = 900 - margin.top - margin.bottom;
// append the svg object to the body of the page
let svg = d3
.select('#treemap')
.append('svg')
.attr('width', width + margin.left + margin.right)
.attr('height', height + margin.top + margin.bottom)
.append('g')
.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
// Read data
d3.csv('https://raw.githubusercontent.com/oihamza/Interactive-Data-Vis-Fall2020/master/Project%201/stateVoterTurnout.csv', function(data) {
// stratify the data: reformatting for d3.js
var root = d3
.stratify()
.id(function(d) {
return d.name;
}) // Name of the entity (column name is name in csv)
.parentId(function(d) {
return d.parent;
})(
// Name of the parent (column name is parent in csv)
data
);
let tooltip = d3
.select('body')
.append('div')
.style('position', 'absolute')
.style('z-index', '10')
.style('visibility', 'hidden')
.style('background-color', 'white')
.style('border', 'solid')
.style('border-width', '2px')
.style('border-radius', '5px')
.style('padding', '5px');
root.sum(function(d) {
return +d.value;
}); // Compute the numeric value for each entity
// Then d3.treemap computes the position of each element of the hierarchy
// The coordinates are added to the root object above
d3.treemap().size([width, height]).padding(3)(root);
// use this information to add rectangles:
svg
.selectAll('rect')
.data(root.leaves())
.enter()
.append('rect')
.attr('x', function(d) {
return d.x0;
})
.attr('y', function(d) {
return d.y0;
})
.attr('width', function(d) {
return d.x1 - d.x0;
})
.attr('height', function(d) {
return d.y1 - d.y0;
})
.style('stroke', 'black')
.style('fill', '#945f04')
.on('mouseover', function() {
tooltip.style('visibility', 'visible');
})
.on('mousemove', function(d) {
tooltip
.style('top', d3.event.pageY - 10 + 'px')
.style('left', d3.event.pageX + 10 + 'px')
.text(`${d.data.value} voters`);
})
.on('mouseout', function() {
tooltip.style('visibility', 'hidden');
});
// and to add the text labels
svg
.selectAll('text')
.data(root.leaves())
.enter()
.append('text')
.attr('x', function(d) {
return d.x0 + 10;
}) // +10 to adjust position (more right)
.attr('y', function(d) {
return d.y0 + 20;
}) // +20 to adjust position (lower)
.text(function(d) {
return d.data.name;
})
.attr('font-size', '15px')
.attr('fill', 'white');
});
body,
h1,
h2,
h3,
h4,
h5,
h6 {
font-family: "Montserrat", sans-serif
}
.styling {
transform-origin: center center;
align-content: center;
position: relative;
}
.tooltip {
position: relative;
display: inline-block;
/* border-bottom: 1px dotted black; */
}
.tooltip .tooltiptext {
visibility: hidden;
width: 180px;
background-color: black;
color: #fff;
text-align: center;
border-radius: 6px;
padding: 5px 0;
/* Position the tooltip */
position: absolute;
z-index: 1;
}
.tooltip:hover .tooltiptext {
visibility: visible;
}
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Montserrat">
<span class="styling"><div id="treemap"></div></span>
<!-- Loading this version of d3 -->
<script src="https://d3js.org/d3.v4.js"></script>
It's not necessary to return anything inside these .on() functions.
I have a donut chart with lots of labels and need place the labels preferably in a an unorder list so they will resize based on the size of the parent element.
I am going off of this example: http://zeroviscosity.com/d3-js-step-by-step/step-3-adding-a-legend
I still don't fully understand D3 and working quick to use it. Thanks for any suggestions you have!!
Here is my code.
JS
(function(d3) {
'use strict';
var width = 760;
var height = 760;
var radius = Math.min(width, height) / 2;
var donutWidth = 275;
var legendRectSize = 18;
var legendSpacing = 4;
var color = d3.scale.category20b();
var svg = d3.select('#chart')
.append('svg')
.attr('width', width)
.attr('height', height)
.append('g')
.attr('transform', 'translate(' + (width / 2) +
',' + (height / 2) + ')');
var arc = d3.svg.arc()
.innerRadius(radius - donutWidth)
.outerRadius(radius);
var pie = d3.layout.pie()
.value(function(d) { return d.count; })
.sort(null);
var tooltip = d3.select('#chart')
.append('div')
.attr('class', 'tooltip');
tooltip.append('div')
.attr('class', 'label');
tooltip.append('div')
.attr('class', 'count');
tooltip.append('div')
.attr('class', 'percent');
d3.json('http://localhost:8080/product_sales/123/2014.01.01/2014.12.31', function(error, data) {
console.log(dataset);
var dataset = [];
for (var key in data) {
if (data.hasOwnProperty(key)) {
var obj = {
count: data[key],
enabled:true,
label:key
};
dataset.push(obj);
}
}
var path = svg.selectAll('path')
.data(pie(dataset))
.enter()
.append('path')
.attr('d', arc)
.attr('fill', function(d, i) {
return color(d.data.label);
})
.each(function(d) { this._current = d; });
path.on('mouseover', function(d) {
var total = d3.sum(dataset.map(function(d) {
return (d.enabled) ? d.count : 0;
}));
var percent = Math.round(1000 * d.data.count / total) / 10;
tooltip.select('.label').html(d.data.label);
tooltip.select('.count').html("$"+d.data.count);
tooltip.select('.percent').html(percent + '%');
tooltip.style('display', 'block');
});
path.on('mouseout', function() {
tooltip.style('display', 'none');
});
path.on('mousemove', function(d) {
console.log((d3.event.pageX - 100)+', '+(d3.event.pageY + 10));
tooltip.style('top', (d3.event.offsetY+10) + 'px')
.style('left', (d3.event.offsetX+10) + 'px');
});
var legend = svg.selectAll('.legend')
.data(color.domain())
.enter()
.append('g')
.attr('class', 'legend')
.attr('transform', function(d, i) {
var height = legendRectSize + legendSpacing;
var offset = height * color.domain().length / 2;
var horz = -2 * legendRectSize;
var vert = i * height - offset;
return 'translate(' + horz + ',' + vert + ')';
});
legend.append('rect')
.attr('width', legendRectSize)
.attr('height', legendRectSize)
.style('fill', color)
.style('stroke', color)
.on('click', function(label) {
var rect = d3.select(this);
var enabled = true;
var totalEnabled = d3.sum(dataset.map(function(d) {
return (d.enabled) ? 1 : 0;
}));
if (rect.attr('class') === 'disabled') {
rect.attr('class', '');
} else {
if (totalEnabled < 2) return;
rect.attr('class', 'disabled');
enabled = false;
}
pie.value(function(d) {
if (d.label === label) d.enabled = enabled;
return (d.enabled) ? d.count : 0;
});
path = path.data(pie(dataset));
path.transition()
.duration(750)
.attrTween('d', function(d) {
var interpolate = d3.interpolate(this._current, d);
this._current = interpolate(0);
return function(t) {
return arc(interpolate(t));
};
});
});
legend.append('text')
.attr('x', legendRectSize + legendSpacing)
.attr('y', legendRectSize - legendSpacing)
.text(function(d) { return d; });
});
})(window.d3);
HTML
<!doctype html>
<html>
<head>
<title>Pie</title>
<style>
h1{
font-size: 14px;
text-align: center;
}
#chart {
height: 760px;
margin: 0 auto;
position: relative;
width: 760px;
}
.tooltip {
background: #eee;
box-shadow: 0 0 5px #999999;
color: #333;
display: none;
font-family: sans-serif;
font-size: 14px;
left: 0;
padding: 10px;
position: absolute;
text-align: center;
top: 95px;
width: 80px;
z-index: 10;
}
.legend {
font-size: 14px;
font-family: sans-serif;
float:left;
}
rect {
cursor: pointer;
stroke-width: 2;
}
rect.disabled {
fill: transparent !important;
}
</style>
</head>
<body>
<div id="chart"></div>
<script src="../bower_components/d3/d3.min.js"></script>
<script src="/js/tip/new.js"></script>
</body>
</html>
I am finishing off a simple scatterplot graph project using D3. I am using D3 tip to show more information about the subject on the graph upon mouseover.
Currently, the tip shows up position to wherever the subject is on the graph.
However, I want to position my tip in a fixed position in the bottom right corner of the graph whenever a tip is triggered.
I have tried to amend the d3-tip class in CSS but no matter what I try, it seems to get overridden, and the tip's top and left positions are always wherever the mouse happens to be.
Here is a Codepen of my project, where you will see the tip appear wherever the subject is positioned: http://codepen.io/alanbuchanan/pen/RryBVQ
How can I ensure my tip shows up in a fixed position at the bottom right of the graph?
Javascript:
var w = 600;
var h = 700;
var margin = {top: 70, bottom: 90, left: 110, right: 70};
var nameMargin = 2;
var chartWidth = w - margin.left - margin.right;
var chartHeight = h - margin.top - margin.bottom;
var url = 'https://raw.githubusercontent.com/FreeCodeCamp/ProjectReferenceData/master/cyclist-data.json';
var svg = d3.select('body')
.append('svg')
.attr('width', w + margin.right)
.attr('height', h)
.style('background-color', '#eeeeee')
.style('border-radius', '10px')
.append('g')
.attr('transform', `translate(${margin.left}, ${margin.top})`);
var formatMinutes = d => {
d -= 2210
var mins = Math.floor(d / 60)
var secs = d - mins * 60
return mins + ':' + secs
};
// Scales
var x = d3.scale.linear()
.domain([
2390 + 5,
2210
])
.range([0, chartWidth]);
var y = d3.scale.linear()
.domain([1, 35 + 1])
.range([0, chartHeight]);
// Axes
var xAxis = d3.svg.axis()
.scale(x)
.orient('bottom')
.ticks(7)
.tickFormat(formatMinutes);
var yAxis = d3.svg.axis()
.scale(y)
.orient('left')
.ticks(10);
var getFlag = nat => {
switch (nat) {
// Deal with odd cases
case 'GER': nat = "DE"; break;
case 'SUI': nat = "CH"; break;
case 'UKR': nat = "UA"; break;
case 'POR': nat = "PT"; break;
default: break;
}
return nat.substr(0, 2).toLowerCase();
};
$.getJSON(url, data => {
console.log(data)
var tip = d3.tip()
.attr('class', 'd3-tip')
.html(d => {
return `<h4><span class="flag-icon flag-icon-` + getFlag(d.Nationality) + `"></span> ${d.Name}</h4>
<h5>Time: ${d.Time} in ${d.Year}</h5>
<p>${d.Doping || 'No allegations'}</p>`
});
// Add data from JSON
svg.selectAll('circle')
.data(data)
.enter()
.append('circle')
.attr('cx', d => x(d.Seconds))
.attr('cy', d => y(d.Place))
.attr('r', '2')
.style('width', '4px')
.style('height', '10px');
svg.call(tip);
var createLink = d => {
return `window.location='${d.URL}`
};
var text = svg.selectAll('text')
.data(data)
.enter()
.append('text')
.attr('x', d => x(d.Seconds - nameMargin))
.attr('y', d => y(d.Place))
.attr("dy", ".35em")
.text(d => d.Name)
.attr('class', 'rider-name')
.attr('class', d => d.Doping.length === 0 ? 'noAllegations' : 'hasAllegations')
.on('click', (d) => location.href = d.URL)
.on('mouseover', tip.show)
.on('mouseout', tip.hide);
svg.append('g')
.attr('class', 'axis')
.attr('transform', `translate(0, ${chartHeight})`)
.call(xAxis);
svg.append('g')
.attr('class', 'axis')
.call(yAxis);
// Axis labels
svg.append('text')
.attr('class', 'label')
.attr('text-anchor', 'middle')
.attr('x', chartWidth / 2)
.attr('y', chartHeight + 50)
.text('Mins and secs behind best time');
svg.append('text')
.attr('class', 'label')
.attr('text-anchor', 'middle')
.attr('y', 0 - margin.left)
.attr('x', 0 - (chartHeight / 2))
.attr('dy', '3.9em')
.attr('transform', 'rotate(-90)')
.text('Place');
// Title
var title = svg.append('text')
.attr('class', 'title')
.attr('x', chartWidth / 2)
.attr('y', 0 - (margin.top / 2))
.attr('text-anchor', 'middle')
.text('Doping in Professional Bicycle Racing: 35 Fastest times up Alpe d\'Huez')
.style('font-size', '16px')
.style('font-weight', 'bold');
});
CSS:
#import url(https://fonts.googleapis.com/css?family=Raleway:400,100);
$font: 'Raleway', sans-serif;
$font-light: 100;
$font-med: 400;
$green: #416600;
$red: #b40000;
body {
background-color: #DADADA;
position: relative;
font-family: $font;
}
.axis path,
.axis line {
fill: none;
stroke: gray;
shape-rendering: crispEdges;
}
.d3-tip {
width: 200px;
}
svg {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, 10%);
border: 1px solid $red;
box-shadow: 3px 3px 15px #888888;
}
h4, h5 {
margin: 0;
}
text {
cursor: pointer;
}
.noAllegations {
fill: $green;
}
.hasAllegations {
fill: $red;
}
.title {
cursor: initial;
fill: $red;
}
.noAllegations, .hasAllegations {
text-transform: uppercase;
font-weight: $font-med;
font-size: 12px;
}
circle {
fill: black;
}
.label {
cursor: initial;
}