I am doing a visualization web application that presents data using a d3 tree map.
When the user is hovering on the treemap, there is a tooltip displaying data as table with many rows and column like 5x4 for example.
I can display the data in every row correctly, but I have difficulty to display a line graph into one column of each row.
Below is the code that I use:
// Tooltip
leaf.on("mousemove", function (d) {
if (d.depth < 3) return;
var name1 = data.name;
var name2 = d.parent.data.name;
var name3 = d.data.name;
var category = d.data.category;
var change = d.data.change;
var filtered = items.filter(function (item) { return item.level2 == d.parent.data.name });
var tooltip_template = '\
<h3 class="tooltip-header"> ' + name2 + ' / ' + name3 + '</h3> \
<div class="tooltip-body"> \
<table> \
';
filtered.forEach(function (item) {
tooltip_template += item.name == d.data.name
? '<tr style="color: #fff; background: ' + getColor(d.data.change) + '"> \
<td> ' + item.name + '</td> \
<td> \
<svg></svg> \
</td> \
<td class="txtright"> ' + item.value + ' 건</td> \
<td class="txtright"> ' + (item.change > 0 ? '+' : '') + item.change + ' 건</td> \
</tr>'
: '<tr> \
<td> ' + item.name + '</td> \
<td> \
<svg></svg> \
</td> \
<td class="txtright"> ' + item.value + ' 건</td> \
<td class="txtright"> ' + (item.change > 0 ? '+' : '') + item.change + ' 건</td> \
</tr>';
// set the dimensions and margins of the line graph
var linemargin = {
top: d3.event.pageY + 15,
right: d3.event.pageX + 5,
bottom: d3.event.pageY + 15,
left: d3.event.pageX + 5
}, linewidth = 100 - linemargin.left - linemargin.right, lineheight = 20 - linemargin.top - linemargin.bottom;
// Set the data for the chart
const linedata = [
{ x: 0, y: 5 },
{ x: 1, y: 9 },
{ x: 2, y: 7 },
{ x: 3, y: 5 },
{ x: 4, y: 3 },
{ x: 5, y: 4 },
{ x: 6, y: 8 },
{ x: 7, y: 6 },
{ x: 8, y: 3 },
{ x: 9, y: 2 },
];
const linechart = tooltip.selectAll("svg").append("g").enter().attr("class", "linechart").attr("transform", `translate(${linemargin.left}, ${linemargin.top})`);
const x = d3.scaleLinear().domain([0, 9]).range([0, linewidth]);
const y = d3.scaleLinear().domain([0, 10]).range([lineheight, 0]);
// Create the line generator
const line = d3
.line()
.x((d) => x(d.x))
.y((d) => y(d.y));
// Add the line to the chart
linechart.append("path").datum(linedata).attr("class", "line").attr("d", line);
})
tooltip_template += ' \
</tbody> \
</table> \
</div> \
'
tooltip
.attr('id', 'tooltip')
.attr('data-change', change)
.style("", "#")
tooltip.transition()
.style("opacity", 1);
tooltip.html(tooltip_template)
.style("left", (d3.event.pageX + 5) + "px")
.style("top", (d3.event.pageY + 15) + "px")
});
Please help!
Potentially the core problem is that you create an <svg>
in tooltip_template
but then you never assign any dimensions to it (e.g. height
and width
).
There are some additional adjustments you can make:
<div>
and re-use it per node of the treemap<template>
(see here) for each <tr>
of the table to be shown on hover<tr>
has an embedded <svg>
that takes the line chart and has set dimensions per a class
<table>
is on mouseenter
; update to position per mouse on mousemove
; and hiding of the table on mouseout
With these adjustments you can rely on HTML rather than strings and move the computation of the table to an event handler called once rather than many times.
Working example below with randomised content:
const leaf = d3.selectAll(".leaf");
leaf.on("mouseenter", function(d) {
const table = d3.select("#custom-tooltip table")
// remove any rows
table.selectAll("*").remove();
// add random 2-5 data rows with content
const rowCount = Math.floor(Math.random() * 4) + 2;
for (let r=0; r<rowCount; ++r) {
// add row
const templateRow = document.querySelector("#tooltip-datarow").content.cloneNode(true);
table.node().appendChild(templateRow);
// add line chart
const d3row = table.select("tr#datarow_x")
d3row.attr("id", `datarow_${r}`);
const linechart = d3row.select("svg.chartarea");
linechart
.append("path")
.datum(linedata()) // randomised - replace with your data
.attr("class", "line")
.attr("d", line);
// add other details
d3row.select(".label1").html(`Rand: ${Math.random().toFixed(2)}`);
d3row.select(".label2").html(`Rand: ${Math.random().toFixed(2)}`);
d3row.select(".label3").html(`Rand: ${Math.random().toFixed(2)}`);
// show tooltip
d3.select("#custom-tooltip")
.style("visibility", "visible");
}
});
leaf.on("mousemove", function(d) {
// update tooltip with mouse
d3.select("#custom-tooltip")
.style("top", (d3.event.pageY + 5) + "px")
.style("left", (d3.event.pageX + 5) + "px")
});
leaf.on("mouseout", function(d) {
// hide tooltip
d3.selectAll("#custom-tooltip")
.style("visibility", "hidden")
});
.leaf { width: 80px; height: 80px; fill: blue; }
.chartarea { width: 100px; height: 20px; }
.line { stroke: red; stroke-width: 1; fill: none; }
#custom-tooltip { position: absolute; z-index: 10; visibility: hidden; }
#custom-tooltip tr { background-color: #d6eeee; }
#tooltip-header { background-color: #eeD600; }
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<svg class="container">
<rect class="leaf"></rect>
</svg>
<div id="custom-tooltip">
<h3 id="tooltip-header">Header</h3>
<div>
<table>
</table>
</div>
</div>
<template id="tooltip-datarow">
<tr id="datarow_x">
<td class="label1">Label</td>
<td>
<svg class="chartarea"></svg>
</td>
<td class="txtright label2">123</td>
<td class="txtright label3">456</td>
</tr>
</template>
<script>
// random data for line
function linedata() {
return d3.range(10).map((_, i) => ({x: i, y: Math.floor(Math.random() * 10) + 1}));
}
// scales
const x = d3.scaleLinear().range([0, 100]).domain([0, 9]);
const y = d3.scaleLinear().range([0, 15]).domain([0, 9]);
// line generator
const line = d3
.line()
.x((d) => x(d.x))
.y((d) => y(d.y));
</script>