i'm currently working on an interactive D3.js Treemap, strongly inspired from https://observablehq.com/@d3/zoomable-treemap. However I got a problem with the size of inner rectangle on the composing part of "Maxillopoda" group as you can see in https://jsfiddle.net/CharlotteAndre/rjy2pb4x/4/ the last "Halopitilus" group is larger than many groups even if I got a value of 1. The problem might be coming from this function
function position(group, root) {
group.selectAll("g")
.attr("transform", d => d === root ? `translate(0,-30)` : `translate(${x(d.x0)},${y(d.y0)})`)
.select("rect")
.attr("width", d => d === root ? width : x(d.x1) - x(d.x0))
.attr("height", d => d === root ? 30 : y(d.y1) - y(d.y0));
}
CodePudding user response:
I found issue with your treemap building using value of nodes and sort function
let treemap = data => d3.treemap() .tile(tile)(d3.hierarchy(data) .sum(d => d.value) .sort((a, b) => b.value - a.value))
When you are sorting based on sum of values, dont assign 0 value to parent nodes. I changed your data to remove the values associated with parents. Either you assign sum of children values or dont assign. This logic will mess up your hierarchy. Run the code below and let me know if this suffice on what you want. I have changed the width and height for better viewing.
let data = {
"name": "Crustacea",
"children": [
{
"name": "Maxillopoda",
"children": [
{
"name": "Maxillopoda_X",
"children": [
{
"name": "Maxillopoda_X_sp.",
"value": 448
}
]
},
{
"name": "Acartia",
"children": [
{
"name": "Acartia_longiremis",
"value": 6
},
{
"name": "Acartia_negligens",
"value": 6
},
{
"name": "Acartia_danae",
"value": 2
},
{
"name": "Acartia_pacifica",
"value": 1
}
]
},
{
"name": "Pleuromamma",
"children": [
{
"name": "Pleuromamma_scutullata",
"value": 10
},
{
"name": "Pleuromamma_borealis",
"value": 8
}
]
},
{
"name": "Calocalanus",
"children": [
{
"name": "Calocalanus_minutus",
"value": 143
},
{
"name": "Calocalanus_sp.",
"value": 61
},
{
"name": "Calocalanus_plumulosus",
"value": 12
},
{
"name": "Calocalanus_pavo",
"value": 19
}
]
},
{
"name": "Mecynocera",
"children": [
{
"name": "Mecynocera_clausi",
"value": 11
}
]
},
{
"name": "Oithona",
"children": [
{
"name": "Oithona_sp.",
"value": 18
}
]
},
{
"name": "Corycaeus",
"children": [
{
"name": "Corycaeus_speciosus",
"value": 9
}
]
},
{
"name": "Acrocalanus",
"children": [
{
"name": "Acrocalanus_monachus",
"value": 1
}
]
},
{
"name": "Subeucalanus",
"children": [
{
"name": "Subeucalanus_crassus",
"value": 5
}
]
},
{
"name": "Sapphirina",
"children": [
{
"name": "Sapphirina_sp.",
"value": 9
},
{
"name": "Sapphirina_scarlata",
"value": 3
},
{
"name": "Sapphirina_darwinii",
"value": 1
}
]
},
{
"name": "Paracalanus",
"children": [
{
"name": "Paracalanus_aculeatus",
"value": 1
}
]
},
{
"name": "Canthocalanus",
"children": [
{
"name": "Canthocalanus_pauper",
"value": 1
}
]
},
{
"name": "Temoropia",
"children": [
{
"name": "Temoropia_mayumbaensis",
"value": 5
}
]
},
{
"name": "Cosmocalanus",
"children": [
{
"name": "Cosmocalanus_darwinii",
"value": 2
}
]
},
{
"name": "Haloptilus",
"children": [
{
"name": "Haloptilus_longicornis",
"value": 1
}
]
},
{
"name": "Undinula",
"children": [
{
"name": "Undinula_vulgaris",
"value": 2
}
]
},
{
"name": "Centropages",
"children": [
{
"name": "Centropages_violaceus",
"value": 3
}
]
},
{
"name": "Euchaeta",
"children": [
{
"name": "Euchaeta_indica",
"value": 2
}
]
}
]
}
]
}
let svg_element = document.getElementById('treemap');
this.svg = d3.select(svg_element);
let margin = {top: 20, right: 20, bottom: 30, left: 40},
width = 550 - margin.left - margin.right,
height = 550 - margin.top - margin.bottom;
// let height = 400;
// let width = 500;
let format = d3.format("");
let uids = new Map();
let uid = title => {
let counter = uids.has(title) ? uids.get(title) 1 : 0;
uids.set(title, counter);
return `${title}-${counter}`;
}
let name = d => d.ancestors().reverse().map(d => d.data.name).join("/");
function tile(node, x0, y0, x1, y1) {
d3.treemapBinary(node, 0, 0, width, height);
for (const child of node.children) {
child.x0 = x0 child.x0 / width * (x1 - x0);
child.x1 = x0 child.x1 / width * (x1 - x0);
child.y0 = y0 child.y0 / height * (y1 - y0);
child.y1 = y0 child.y1 / height * (y1 - y0);
}
}
let treemap = data => d3.treemap()
.tile(tile)(d3.hierarchy(data)
.sum(d => d.value)
.sort((a, b) => b.value - a.value))
const x = d3.scaleLinear().rangeRound([0, width]);
const y = d3.scaleLinear().rangeRound([0, height]);
const svg = this.svg
.attr("viewBox", [0.5, -30.5, width, height 30])
.style("font", "8px sans-serif");
let group = svg.append("g")
.call(render, treemap(data));
function render(group, root) {
const node = group
.selectAll("g")
.data(root.children.concat(root))
.join("g");
node.filter(d => d === root ? d.parent : d.children)
.attr("cursor", "pointer")
.on("click", (event, d) => d === root ? zoomout(root) : zoomin(d));
node.append("title")
.text(d => `${name(d)}\n${format(d.value)}`)
.style("font-size", "70%");
node.append("rect")
.attr("id", d => (d.leafUid = uid("leaf")))
.attr("fill", d => d === root ? "#edf3f7" : d.children ? "#0b9ba3" : "#0b9ba3")
.attr("stroke", "#edf3f7");
node.append("clipPath")
.attr("id", d => (d.clipUid = uid("clip")))
.append("use")
.attr("xlink:href", d => d.leafUid.href);
node.append("text")
.attr("clip-path", d => d.clipUid)
.attr("font-weight", d => d === root ? "bold" : null)
.selectAll("tspan")
.data(d => (d.data.name).split(/(?=[A-Z][^A-Z])/g).concat(format(d.value)))
.join("tspan")
.attr("x", 3)
.attr("y", (d, i, nodes) => `${(i === nodes.length - 1) * 0.3 1.1 i * 0.9}em`)
.attr("fill-opacity", (d, i, nodes) => i === nodes.length - 1 ? 0.7 : null)
.attr("font-weight", (d, i, nodes) => i === nodes.length - 1 ? "normal" : null)
.text(d => d);
group.call(position, root);
}
function position(group, root) {
group.selectAll("g")
.attr("transform", d => d === root ? `translate(0,-30)` : `translate(${x(d.x0)},${y(d.y0)})`)
.select("rect")
.attr("width", d => d === root ? width : x(d.x1) - x(d.x0))
.attr("height", d => d === root ? 20 : y(d.y1) - y(d.y0));
}
// When zooming in, draw the new nodes on top, and fade them in.
function zoomin(d) {
const group0 = group.attr("pointer-events", "none");
const group1 = group = svg.append("g").call(render, d);
x.domain([d.x0, d.x1]);
y.domain([d.y0, d.y1]);
svg.transition()
.duration(750)
.call(t => group0.transition(t).remove()
.call(position, d.parent))
.call(t => group1.transition(t)
.attrTween("opacity", () => d3.interpolate(0, 1))
.call(position, d));
}
// When zooming out, draw the old nodes on top, and fade them out.
function zoomout(d) {
const group0 = group.attr("pointer-events", "none");
const group1 = group = svg.insert("g", "*").call(render, d.parent);
x.domain([d.parent.x0, d.parent.x1]);
y.domain([d.parent.y0, d.parent.y1]);
svg.transition()
.duration(750)
.call(t => group0.transition(t).remove()
.attrTween("opacity", () => d3.interpolate(1, 0))
.call(position, d))
.call(t => group1.transition(t)
.call(position, d.parent));
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.4.4/d3.min.js"></script>
<svg id="treemap"></svg>
UPDATE without changing data
let data = {
"name": "Crustacea",
"value": 0,
"children": [
{
"name": "Maxillopoda",
"value": 195,
"children": [
{
"name": "Maxillopoda_X",
"value": 0,
"children": [
{
"name": "Maxillopoda_X_sp.",
"value": 448
}
]
},
{
"name": "Acartia",
"value": 0,
"children": [
{
"name": "Acartia_longiremis",
"value": 6
},
{
"name": "Acartia_negligens",
"value": 6
},
{
"name": "Acartia_danae",
"value": 2
},
{
"name": "Acartia_pacifica",
"value": 1
}
]
},
{
"name": "Pleuromamma",
"value": 0,
"children": [
{
"name": "Pleuromamma_scutullata",
"value": 10
},
{
"name": "Pleuromamma_borealis",
"value": 8
}
]
},
{
"name": "Calocalanus",
"value": 9,
"children": [
{
"name": "Calocalanus_minutus",
"value": 43
},
{
"name": "Calocalanus_sp.",
"value": 61
},
{
"name": "Calocalanus_plumulosus",
"value": 12
},
{
"name": "Calocalanus_pavo",
"value": 19
}
]
},
{
"name": "Mecynocera",
"value": 0,
"children": [
{
"name": "Mecynocera_clausi",
"value": 11
}
]
},
{
"name": "Oithona",
"value": 0,
"children": [
{
"name": "Oithona_sp.",
"value": 18
}
]
},
{
"name": "Corycaeus",
"value": 0,
"children": [
{
"name": "Corycaeus_speciosus",
"value": 9
}
]
},
{
"name": "Acrocalanus",
"value": 0,
"children": [
{
"name": "Acrocalanus_monachus",
"value": 1
}
]
},
{
"name": "Subeucalanus",
"value": 0,
"children": [
{
"name": "Subeucalanus_crassus",
"value": 5
}
]
},
{
"name": "Sapphirina",
"value": 0,
"children": [
{
"name": "Sapphirina_sp.",
"value": 9
},
{
"name": "Sapphirina_scarlata",
"value": 3
},
{
"name": "Sapphirina_darwinii",
"value": 1
}
]
},
{
"name": "Paracalanus",
"value": 0,
"children": [
{
"name": "Paracalanus_aculeatus",
"value": 1
}
]
},
{
"name": "Canthocalanus",
"value": 0,
"children": [
{
"name": "Canthocalanus_pauper",
"value": 1
}
]
},
{
"name": "Temoropia",
"value": 0,
"children": [
{
"name": "Temoropia_mayumbaensis",
"value": 5
}
]
},
{
"name": "Cosmocalanus",
"value": 0,
"children": [
{
"name": "Cosmocalanus_darwinii",
"value": 2
}
]
},
{
"name": "Haloptilus",
"value": 0,
"children": [
{
"name": "Haloptilus_longicornis",
"value": 1
}
]
},
{
"name": "Undinula",
"value": 0,
"children": [
{
"name": "Undinula_vulgaris",
"value": 2
}
]
},
{
"name": "Centropages",
"value": 0,
"children": [
{
"name": "Centropages_violaceus",
"value": 3
}
]
},
{
"name": "Euchaeta",
"value": 0,
"children": [
{
"name": "Euchaeta_indica",
"value": 2
}
]
}
]
}
]
}
let svg_element = document.getElementById('treemap');
this.svg = d3.select(svg_element);
let margin = {top: 20, right: 20, bottom: 30, left: 40},
width = 350 - margin.left - margin.right,
height = 198 - margin.top - margin.bottom;
// let height = 400;
// let width = 500;
let format = d3.format("");
let uids = new Map();
let uid = title => {
let counter = uids.has(title) ? uids.get(title) 1 : 0;
uids.set(title, counter);
return `${title}-${counter}`;
}
let name = d => d.ancestors().reverse().map(d => d.data.name).join("/");
function tile(node, x0, y0, x1, y1) {
d3.treemapBinary(node, 0, 0, width, height);
for (const child of node.children) {
child.x0 = x0 child.x0 / width * (x1 - x0);
child.x1 = x0 child.x1 / width * (x1 - x0);
child.y0 = y0 child.y0 / height * (y1 - y0);
child.y1 = y0 child.y1 / height * (y1 - y0);
}
}
function identifyLeaves(obj) {
var hasNoChildren = (obj.children) ? false : true;
if(hasNoChildren)
return obj.value
}
let treemap = data => d3.treemap()
.tile(tile)(d3.hierarchy(data)
.sum((d) => identifyLeaves(d))
.sort((a, b) => identifyLeaves(b) - identifyLeaves(a)))
const x = d3.scaleLinear().rangeRound([0, width]);
const y = d3.scaleLinear().rangeRound([0, height]);
const svg = this.svg
.attr("viewBox", [0.5, -30.5, width, height 30])
.style("font", "8px sans-serif");
let group = svg.append("g")
.call(render, treemap(data));
function render(group, root) {
const node = group
.selectAll("g")
.data(root.children.concat(root))
.data(root.children.concat(root))
.join("g");
node.filter(d => d === root ? d.parent : d.children)
.attr("cursor", "pointer")
.on("click", (event, d) => d === root ? zoomout(root) : zoomin(d));
node.append("title")
.text(d => `${name(d)}\n${format(d.value)}`)
.style("font-size", "70%");
node.append("rect")
.attr("id", d => (d.leafUid = uid("leaf")))
.attr("fill", d => d === root ? "#edf3f7" : d.children ? "#0b9ba3" : "#0b9ba3")
.attr("stroke", "#edf3f7");
node.append("clipPath")
.attr("id", d => (d.clipUid = uid("clip")))
.append("use")
.attr("xlink:href", d => d.leafUid.href);
node.append("text")
.attr("clip-path", d => d.clipUid)
.attr("font-weight", d => d === root ? "bold" : null)
.selectAll("tspan")
.data(d => (d.data.name).split(/(?=[A-Z][^A-Z])/g).concat(format(d.value)))
.join("tspan")
.attr("x", 3)
.attr("y", (d, i, nodes) => `${(i === nodes.length - 1) * 0.3 1.1 i * 0.9}em`)
.attr("fill-opacity", (d, i, nodes) => i === nodes.length - 1 ? 0.7 : null)
.attr("font-weight", (d, i, nodes) => i === nodes.length - 1 ? "normal" : null)
.text(d => d);
group.call(position, root);
}
function position(group, root) {
group.selectAll("g")
.attr("transform", d => d === root ? `translate(0,-30)` : `translate(${x(d.x0)},${y(d.y0)})`)
.select("rect")
.attr("width", d => d === root ? width : x(d.x1) - x(d.x0))
.attr("height", d => d === root ? 30 : y(d.y1) - y(d.y0));
}
// When zooming in, draw the new nodes on top, and fade them in.
function zoomin(d) {
const group0 = group.attr("pointer-events", "none");
const group1 = group = svg.append("g").call(render, d);
x.domain([d.x0, d.x1]);
y.domain([d.y0, d.y1]);
svg.transition()
.duration(750)
.call(t => group0.transition(t).remove()
.call(position, d.parent))
.call(t => group1.transition(t)
.attrTween("opacity", () => d3.interpolate(0, 1))
.call(position, d));
}
// When zooming out, draw the old nodes on top, and fade them out.
function zoomout(d) {
const group0 = group.attr("pointer-events", "none");
const group1 = group = svg.insert("g", "*").call(render, d.parent);
x.domain([d.parent.x0, d.parent.x1]);
y.domain([d.parent.y0, d.parent.y1]);
svg.transition()
.duration(750)
.call(t => group0.transition(t).remove()
.attrTween("opacity", () => d3.interpolate(1, 0))
.call(position, d))
.call(t => group1.transition(t)
.call(position, d.parent));
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.4.4/d3.min.js" integrity="sha512-hnFpvCiJ8Fr1lYLqcw6wLgFUOEZ89kWCkO cEekwcWPIPKyknKV1eZmSSG3UxXfsSuf z/SgmiYB1zFOg3l2UQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<svg id="treemap"></svg>