I'm trying to add two d3 visuals to my index.html page, but only one of the visuals displays at a time. The two visuals use the same code and the only difference is the data used. They work well individually, but I am unable to get them to appear on the same page together.
var margins = {
top: 20,
bottom: 300,
left: 30,
right: 100
};
var height = 800;
var width = 500;
var totalWidth = width + margins.left + margins.right;
var totalHeight = height + margins.top + margins.bottom;
var svg = d3.select('body')
.append('svg')
.attr('width', totalWidth)
.attr('height', totalHeight);
var graphGroup = svg.append('g')
.attr('transform', "translate(" + margins.left + "," + margins.top + ")");
var levels = [
[
{id: 'Gaia'},
{id:'Ouranos', parents:['Gaia']}
],
[
{id:'Aphrodite', parents:['Ouranos']},
{id: 'Themis', parents: ['Gaia', 'Ouranos']},
{id: 'Mnemosyne', parents: ['Gaia', 'Ouranos']},
{id: 'Hyperion', parents: ['Gaia', 'Ouranos']},
{id: 'Thea', parents: ['Gaia', 'Ouranos']},
{id: 'Crius', parents: ['Gaia', 'Ouranos']},
{id: 'Oceanus', parents: ['Gaia', 'Ouranos']},
{id: 'Tethys', parents: ['Gaia', 'Ouranos']},
{id: 'Iapetus', parents: ['Gaia', 'Ouranos']},
{id: 'Coeus', parents: ['Gaia', 'Ouranos']},
{id: 'Phoebe', parents: ['Gaia','Ouranos']},
{id: 'Kronos', parents: ['Gaia', 'Ouranos']},
{id: 'Rhea', parents: ['Gaia', 'Ouranos']},
],
[
{id: 'Pleione', parents:['Oceanus','Tethys']},
{id:'Atlas',parents:['Iapetus']},
{id:'Semele'},
{id:'Maia', parents:['Pleione','Atlas']},
{id:'Leto',parents:['Coeus','Phoebe']},
{id:'Zeus',parents:['Kronos','Rhea']},
{id:'Hera',parents:['Kronos','Rhea']},
{id:'Poseidon',parents:['Kronos','Rhea']},
{id:'Amphitrite'},
{id:'Hestia',parents:['Kronos','Rhea']},
{id:'Hades',parents:['Kronos','Rhea']},
{id:'Demeter',parents:['Kronos','Rhea']},
],
[
{id:'Dionysus',parents:['Semele','Zeus']},
{id:'Hermes',parents:['Maia','Zeus']},
{id:'Apollo',parents:['Leto','Zeus']},
{id:'Artemis',parents:['Leto','Zeus']},
{id:'Athena',parents:['Zeus']},
{id:'Ares',parents:['Zeus','Hera']},
{id:'Hephaistos',parents:['Zeus','Hera']},
{id:'Hebe',parents:['Zeus','Hera']},
{id:'Triton',parents:['Poseidon','Amphitrite']},
{id:'Benthesikyme',parents:['Poseidon','Amphitrite']},
{id:'Rhodos',parents:['Poseidon','Amphitrite']},
{id:'Persephone',parents:['Zeus','Demeter']},
{id:'Zagreus', parents:['Hades','Persephone']},
{id:'Macaria', parents:['Hades','Persephone']}
]
]
// precompute level depth
levels.forEach((l, i) => l.forEach(n => n.level = i));
var nodes = levels.reduce(((a, x) => a.concat(x)), []);
var nodes_index = {};
nodes.forEach(d => nodes_index[d.id] = d);
// objectification
nodes.forEach(d => {
d.parents = (d.parents === undefined ? [] : d.parents).map(p => nodes_index[p])
})
// precompute bundles
levels.forEach((l, i) => {
var index = {}
l.forEach(n => {
if (n.parents.length == 0) {
return
}
var id = n.parents.map(d => d.id).sort().join('--')
if (id in index) {
index[id].parents = index[id].parents.concat(n.parents)
} else {
index[id] = {
id: id,
parents: n.parents.slice(),
level: i
}
}
n.bundle = index[id]
})
l.bundles = Object.keys(index).map(k => index[k])
l.bundles.forEach((b, i) => b.i = i)
})
var links = []
nodes.forEach(d => {
d.parents.forEach(p => links.push({
source: d,
bundle: d.bundle,
target: p
}))
})
var bundles = levels.reduce(((a, x) => a.concat(x.bundles)), [])
// reverse pointer from parent to bundles
bundles.forEach(b => b.parents.forEach(p => {
if (p.bundles_index === undefined) {
p.bundles_index = {}
}
if (!(b.id in p.bundles_index)) {
p.bundles_index[b.id] = []
}
p.bundles_index[b.id].push(b)
}))
nodes.forEach(n => {
if (n.bundles_index !== undefined) {
n.bundles = Object.keys(n.bundles_index).map(k => n.bundles_index[k])
} else {
n.bundles_index = {}
n.bundles = []
}
n.bundles.forEach((b, i) => b.i = i)
})
links.forEach(l => {
if (l.bundle.links === undefined) {
l.bundle.links = []
}
l.bundle.links.push(l)
})
// layout
const padding = 20
const node_height = 22
const node_width = 70
const bundle_width = 14
const level_y_padding = 16
const metro_d = 4
const c = 16
const min_family_height = 16
nodes.forEach(n => n.height = (Math.max(1, n.bundles.length) - 1) * metro_d)
var x_offset = padding
var y_offset = padding
levels.forEach(l => {
x_offset += l.bundles.length * bundle_width
y_offset += level_y_padding
l.forEach((n, i) => {
n.x = n.level * node_width + x_offset
n.y = node_height + y_offset + n.height / 2
y_offset += node_height + n.height
})
})
var i = 0
levels.forEach(l => {
l.bundles.forEach(b => {
b.x = b.parents[0].x + node_width + (l.bundles.length - 1 - b.i) * bundle_width
b.y = i * node_height
})
i += l.length
})
links.forEach(l => {
l.xt = l.target.x
l.yt = l.target.y + l.target.bundles_index[l.bundle.id].i * metro_d - l.target.bundles.length * metro_d / 2 + metro_d / 2
l.xb = l.bundle.x
l.xs = l.source.x
l.ys = l.source.y
})
// compress vertical space
var y_negative_offset = 0
levels.forEach(l => {
y_negative_offset += -min_family_height + d3.min(l.bundles, b => d3.min(b.links, link => (link.ys - c) - (link.yt + c))) || 0
l.forEach(n => n.y -= y_negative_offset)
})
// very ugly, I know
links.forEach(l => {
l.yt = l.target.y + l.target.bundles_index[l.bundle.id].i * metro_d - l.target.bundles.length * metro_d / 2 + metro_d / 2
l.ys = l.source.y
l.c1 = l.source.level - l.target.level > 1 ? node_width + c : c
l.c2 = c
})
const cluster = d3.cluster()
.size([width, height]);
const root = d3.hierarchy(links);
cluster(root);
let oValues = Object.values(root)[0];
let linkks = oValues.map(x => x.bundle.links);
linkks.forEach((linkk) => {
//nodeG1 are nodes that have children
let nodeG1 = svg.append("g")
.selectAll("circle")
.data(linkk)
.join("circle")
.attr("cx", d => d.target.x)
.attr("cy", d => d.target.y)
.attr("fill", "lightblue")
.attr("stroke", (d) => {
return '#' + Math.floor(16777215 * Math.sin(3 * Math.PI / (5 * (parseInt(d.target.level) + 1)))).toString(16);
})
.attr("r", 6);
//nodeG11 are nodes that have parents
let nodeG11 = svg.append("g")
.selectAll("circle")
.data(linkk)
.join("circle")
.attr("cx", d => d.source.x)
.attr("cy", d => d.source.y)
.attr("fill", "gray")
.attr("stroke", (d) => {
return '#' + Math.floor(16777215 * Math.sin(3 * Math.PI / (5 * (parseInt(d.source.level) + 1)))).toString(16);
})
.attr("r", 6);
let nodeG2 = svg.append("g")
.attr("font-family", "papyrus")
.attr("font-size", 16)
.selectAll("text")
.data(linkk)
.join("text")
.attr("class", "text")
.attr("x", d => d.target.x + padding)
.attr("y", d => d.target.y)
.text(d => d.target.id )
.attr("fill", (d) => {
return '#' + 0;
});
let nodeG22 = svg.append("g")
.attr("font-family", "papyrus")
.attr("font-size", 16)
.selectAll("text")
.data(linkk)
.join("text")
.attr("class", "text")
.attr("x", d => d.source.x + padding)
.attr("y", d => d.source.y)
.text(d => d.source.id )
.attr("fill", (d) => {
return '#' + 0;
});
let nodeG = svg.append('g')
.attr('class', 'node')
.selectAll("path")
.data(linkk)
.join('path')
.attr("class", "link")
.attr("d", d3.linkHorizontal()
.source(d => [d.xs, d.ys])
.target(d => [d.xt, d.yt]))
.attr("fill", "none")
.attr("stroke-opacity", 10)
.attr("stroke-width", .75)
.attr("stroke", (d) => {
return '#' + Math.floor(16776960 * Math.sin(3 * Math.PI / (4 * parseInt(d.source.level)))).toString(16);
});
});
path {
display: block;
z-index: 0;
}
text,
circle {
display: block;
z-index: 1000;
}
<!DOCTYPE html>
<link rel="stylesheet" type="text/css" href="greek.css"/>
<link rel="stylesheet" type="text/css" href="main.css"/>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Here</title>
</head>
<body style="background-color: antiquewhite;">
<center>
<h style="font-size:50px">Greek and Norse Gods: </h>
</center>
<script src="https://d3js.org/d3.v5.min.js"></script>
<script src="greek.js"></script>
<script src="norse.js"></script>
</body>
</html>
Is there a way to get the two visuals to appear side-by-side?
Looks like there's only one svg element. I think you're appending both tree structures to the same svg and element and overwriting the first one with the second. Try to create two svg elements and seperately append the greek and norse nodes. Didn't have a chance to test it, however, maybe it helps.
Related
I've been trying to learn how to create a Norse Mythology family tree using D3 and was able to find some code that someone wrote on here that had the perfect layout of the Norse gods -> (found here)
I really like the way that this looks, but the links between parents and children get a little muddled. Is there a way to have the color of the links have an ordinal color scale and have the links be at a right angle?
var margins = {
top: 20,
bottom: 300,
left: 30,
right: 100
};
var height = 600;
var width = 900;
var totalWidth = width + margins.left + margins.right;
var totalHeight = height + margins.top + margins.bottom;
var svg = d3.select('body')
.append('svg')
.attr('width', totalWidth)
.attr('height', totalHeight);
var graphGroup = svg.append('g')
.attr('transform', "translate(" + margins.left + "," + margins.top + ")");
var levels = [
[{ id: 'Ymir' },
{id: 'Auðumbla'}
],
[
{ id: 'Fárbauti ',parents:['Ymir'], info:"pronounce like this" },
{ id: 'Laufey',parents:['Ymir']},
{ id: 'Ægir', parents:['Ymir']},
{ id: 'Rán', parents:['Ymir']},
{id:'Búri', parents:['Auðumbla']}
],
[
{ id: 'Loki', parents:['Fárbauti ', 'Laufey']},
{id: 'A Horse'},
{ id: 'Bára', parents:['Ægir','Rán']},
{ id: 'Blóðughadda', parents:['Ægir','Rán']},
{ id: 'Bylgja', parents:['Ægir','Rán']},
{ id: 'Dúfa', parents:['Ægir','Rán']},
{ id: 'Hefring', parents:['Ægir','Rán']},
{ id: 'Himinglæva', parents:['Ægir','Rán']},
{ id: 'Angrboða', parents: ['Ymir'] },
{ id: 'Hrǫnn', parents:['Ægir','Rán']},
{ id: 'Kólga', parents:['Ægir','Rán']},
{ id: 'Unnr', parents:['Ægir','Rán']},
{id: 'Bestla'},
{id:'Burr', parents:['Búri']},
{id:'Fjörgyn'},
],
[
{ id: 'Fenrir', parents:['Angrboða','Loki']},
{ id:'Jörmungandr', parents:['Angrboða','Loki']},
{ id:'Hel', parents:['Angrboða','Loki']},
{ id:'Sleipnir', parents:['Loki','A Horse']},
{id:'Jörd'},
{id: 'Heimdall', parents:['Bára','Blóðughadda','Bylgja','Dúfa','Hefring','Himinglæva','Hrǫnn','Kólga','Unnr']},
{id:'Hœnir', parents:['Bestla','Burr']},
{id:'Odin', parents:['Bestla','Burr']},
{id:'Vili', parents:['Bestla','Burr']},
{id:'Vé', parents:['Bestla','Burr']},
{id:'Frigg', parents:['Fjörgyn']}
],
[
{id: 'Njörds sister'},
{id:'Njörd'},
{id:'Járnsaxa'},
{id:'Sif'},
{id:'Thor', parents:['Odin','Jörd']},
{id:'Höðr', parents:['Odin', 'Frigg']},
{id:'Bragi', parents:['Odin']},
{id:'Baldur',parents:['Odin','Frigg']},
{id:'Nanna'}
],
[
{id:'Freyr', parents:['Njörd', 'Njörds sister']},
{id:'Freya', parents:['Njörd', 'Njörds sister']},
{id:'Magni', parents:['Thor','Járnsaxa']},
{id:'Thrúd', parents:['Thor','Sif']},
{id:'Módi', parents:['Thor','Sif']},
{id:'Ullr', parents:['Sif','Odin']},
{id:'Forseti', parents:['Baldur', 'Nanna']}
]
]
// precompute level depth
levels.forEach((l, i) => l.forEach(n => n.level = i));
var nodes = levels.reduce(((a, x) => a.concat(x)), []);
var nodes_index = {};
nodes.forEach(d => nodes_index[d.id] = d);
// objectification
nodes.forEach(d => {
d.parents = (d.parents === undefined ? [] : d.parents).map(p => nodes_index[p])
})
// precompute bundles
levels.forEach((l, i) => {
var index = {}
l.forEach(n => {
if (n.parents.length == 0) {
return
}
var id = n.parents.map(d => d.id).sort().join('--')
if (id in index) {
index[id].parents = index[id].parents.concat(n.parents)
} else {
index[id] = {
id: id,
parents: n.parents.slice(),
level: i
}
}
n.bundle = index[id]
})
l.bundles = Object.keys(index).map(k => index[k])
l.bundles.forEach((b, i) => b.i = i)
})
var links = []
nodes.forEach(d => {
d.parents.forEach(p => links.push({
source: d,
bundle: d.bundle,
target: p
}))
})
var bundles = levels.reduce(((a, x) => a.concat(x.bundles)), [])
// reverse pointer from parent to bundles
bundles.forEach(b => b.parents.forEach(p => {
if (p.bundles_index === undefined) {
p.bundles_index = {}
}
if (!(b.id in p.bundles_index)) {
p.bundles_index[b.id] = []
}
p.bundles_index[b.id].push(b)
}))
nodes.forEach(n => {
if (n.bundles_index !== undefined) {
n.bundles = Object.keys(n.bundles_index).map(k => n.bundles_index[k])
} else {
n.bundles_index = {}
n.bundles = []
}
n.bundles.forEach((b, i) => b.i = i)
})
links.forEach(l => {
if (l.bundle.links === undefined) {
l.bundle.links = []
}
l.bundle.links.push(l)
})
// layout
const padding = 8
const node_height = 22
const node_width = 70
const bundle_width = 14
const level_y_padding = 16
const metro_d = 4
const c = 16
const min_family_height = 16
nodes.forEach(n => n.height = (Math.max(1, n.bundles.length) - 1) * metro_d)
var x_offset = padding
var y_offset = padding
levels.forEach(l => {
x_offset += l.bundles.length * bundle_width
y_offset += level_y_padding
l.forEach((n, i) => {
n.x = n.level * node_width + x_offset
n.y = node_height + y_offset + n.height / 2
y_offset += node_height + n.height
})
})
var i = 0
levels.forEach(l => {
l.bundles.forEach(b => {
b.x = b.parents[0].x + node_width + (l.bundles.length - 1 - b.i) * bundle_width
b.y = i * node_height
})
i += l.length
})
links.forEach(l => {
l.xt = l.target.x
l.yt = l.target.y + l.target.bundles_index[l.bundle.id].i * metro_d - l.target.bundles.length * metro_d / 2 + metro_d / 2
l.xb = l.bundle.x
l.xs = l.source.x
l.ys = l.source.y
})
// compress vertical space
var y_negative_offset = 0
levels.forEach(l => {
y_negative_offset += -min_family_height + d3.min(l.bundles, b => d3.min(b.links, link => (link.ys - c) - (link.yt + c))) || 0
l.forEach(n => n.y -= y_negative_offset)
})
// very ugly, I know
links.forEach(l => {
l.yt = l.target.y + l.target.bundles_index[l.bundle.id].i * metro_d - l.target.bundles.length * metro_d / 2 + metro_d / 2
l.ys = l.source.y
l.c1 = l.source.level - l.target.level > 1 ? node_width + c : c
l.c2 = c
})
const cluster = d3.cluster()
.size([width, height]);
const root = d3.hierarchy(links);
cluster(root);
let oValues = Object.values(root)[0];
let linkks = oValues.map(x => x.bundle.links);
linkks.forEach((linkk) => {
let nodeG1 = svg.append("g")
.selectAll("circle")
.data(linkk)
.join("circle")
.attr("cx", d => d.target.x)
.attr("cy", d => d.target.y)
.attr("fill", "none")
.attr("stroke", (d) => {
return '#' + Math.floor(16777215 * Math.sin(3 * Math.PI / (5 * (parseInt(d.target.level) + 1)))).toString(16);
})
.attr("r", 6);
let nodeG11 = svg.append("g")
.selectAll("circle")
.data(linkk)
.join("circle")
.attr("cx", d => d.source.x)
.attr("cy", d => d.source.y)
.attr("fill", "none")
.attr("stroke", (d) => {
return '#' + Math.floor(16777215 * Math.sin(3 * Math.PI / (5 * (parseInt(d.source.level) + 1)))).toString(16);
})
.attr("r", 6);
let nodeG2 = svg.append("g")
.attr("font-family", "sans-serif")
.attr("font-size", 14)
.selectAll("text")
.data(linkk)
.join("text")
.attr("class", "text")
.attr("x", d => d.target.x + padding)
.attr("y", d => d.target.y)
.text(d => d.target.id )
.attr("fill", (d) => {
return '#' + Math.floor(16777215 * Math.sin(3 * Math.PI / (5 * (parseInt(d.target.level) + 2)))).toString(16);
});
let nodeG22 = svg.append("g")
.attr("font-family", "sans-serif")
.attr("font-size", 14)
.selectAll("text")
.data(linkk)
.join("text")
.attr("class", "text")
.attr("x", d => d.source.x + padding)
.attr("y", d => d.source.y)
.text(d => d.source.id )
.attr("fill", (d) => {
return '#' + Math.floor(16777215 * Math.sin(3 * Math.PI / (5 * (parseInt(d.source.level) + 1)))).toString(16);
});
let nodeG = svg.append('g')
.attr('class', 'node')
.selectAll("path")
.data(linkk)
.join('path')
.attr("class", "link")
.attr("d", d3.linkHorizontal()
.source(d => [d.xs, d.ys])
.target(d => [d.xt, d.yt]))
.attr("fill", "none")
.attr("stroke-opacity", 0.325)
.attr("stroke-width", 0.75)
.attr("stroke", (d) => {
return '#' + Math.floor(16777215 * Math.sin(3 * Math.PI / (4 * parseInt(d.source.level)))).toString(16);
});
});
path {
display: block;
z-index: 0;
}
text,
circle {
display: block;
z-index: 1000;
}
<!DOCTYPE html>
<link rel="stylesheet" type="text/css" href="main.css"/>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Test</title>
</head>
<body>
<div id ="Norse"></div>
<!-- Version 7 is the latest version of D3. -->
<script src="https://d3js.org/d3.v5.min.js"></script>
<!-- Our visualization data will be in this '.js' file -->
<script src="main_viz.js"></script>
</div>
</body>
</html>
This is the example that I'm basing the preferred design off of:
I am stuck badly while trying to show negative values in sunburst chart. whenever I change the value in json file as negative the slice just gets hidden. I want the arc to show even if my arc has negative value.
<!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">
<title>Document</title>
<script src="//d3js.org/d3.v7.min.js"></script>
</head>
<body>
<script>
chart = () => {
var color = d3.scaleOrdinal()
.range(['red', 'green', 'lightgreen']);
const root = partition(data);
root.each(d => d.current = d);
const svg = d3.create("svg")
.attr("viewBox", [0, 0, width, width])
.style("font", "11px sans-serif")
// .style('fill', 'black')
console.log(svg)
const g = svg.append("g")
.attr("transform", `translate(${width / 2.5},${width / 2})`);
const path = g.append("g")
.selectAll("path")
.data(root.descendants().slice(1))
.join("path")
.attr('fill', function (d, i, value) {
if (d.data.value > 10) {
return 'green'
} else if (d.data.value < 5) {
return 'red'
}
else if (d.data.value < 1) {
return 'lightgreen'
}
return color(i)
})
.attr("fill-opacity", d => arcVisible(d.current) ? (d.value ? 0.7 : 0.4) : 0)
.attr("d", d => arc(d.current));
path.filter(d => d.children)
.style("cursor", "pointer")
.on("click", clicked);
path.append("title")
.text(d => `${d.ancestors().map(d => d.data.name).reverse().join("-")} : ${format(d.value)} points`);
path.append("info")
.text(d => `${d.ancestors().map(d => d.data.size).reverse().join("/")}\n${format(d.value)}`);
const label = g.append("g")
.attr("pointer-events", "none")
.attr("text-anchor", "middle")
.style("user-select", "none")
.selectAll("text")
.data(root.descendants().slice(1))
.join("text")
.attr("dy", "0.35em")
.attr("fill-opacity", d => +labelVisible(d.current))
.attr("transform", d => labelTransform(d.current))
.text(d => d.data.name + '\n' + d.data.value);
const parent = g.append("circle")
.datum(root)
.attr("r", radius)
.attr("fill", "green")
.attr("pointer-events", "all")
.on("click", clicked);
function clicked(event, p) {
parent.datum(p.parent || root);
root.each(d => d.target = {
x0: Math.max(0, Math.min(1, (d.x0 - p.x0) / (p.x1 - p.x0))) * 2 * Math.PI,
x1: Math.max(0, Math.min(1, (d.x1 - p.x0) / (p.x1 - p.x0))) * 2 * Math.PI,
y0: Math.max(0, d.y0 - p.depth),
y1: Math.max(0, d.y1 - p.depth)
});
const t = g.transition().duration(1050);
// Transition the data on all arcs, even the ones that aren’t visible,
// so that if this transition is interrupted, entering arcs will start
// the next transition from the desired position.
path.transition(t)
.tween("data", d => {
const i = d3.interpolate(d.current, d.target);
return t => d.current = i(t);
})
.filter(function (d) {
return +this.getAttribute("fill-opacity") || arcVisible(d.target);
})
.attr("fill-opacity", d => arcVisible(d.target) ? (d.children ? 0.6 : 0.4) : 0)
.attrTween("d", d => () => arc(d.current));
label.filter(function (d) {
return +this.getAttribute("fill-opacity") || labelVisible(d.target);
}).transition(t)
.attr("fill-opacity", d => +labelVisible(d.target))
.attrTween("transform", d => () => labelTransform(d.current));
}
function arcVisible(d) {
return d.y1 <= 3 && d.y0 >= 1 && d.x1 > d.x0;
}
function labelVisible(d) {
return d.y1 <= 3 && d.y0 >= 1 && (d.y1 - d.y0) * (d.x1 - d.x0) > 0.03;
}
function labelTransform(d) {
const x = (d.x0 + d.x1) / 2 * 180 / Math.PI;
const y = (d.y0 + d.y1) / 2 * radius;
return `rotate(${x - 90}) translate(${y},0) rotate(${x < 180 ? 0 : 180})`;
}
return svg.node();
}
adder = 10
data = {
"name": "KSE100",
"children": [
{
"name": "TEXTILE",
'value': 10,
"children": [
{
"name": "COST",
"value": 4,
// 'points': 4
},
{
"name": "COOT",
"value": 3,
// 'points': 5
},
{
"name": "AHTM",
"value": "-3",
// 'points': 5
},
]
},
{
"name": "OIL",
'value': 8,
"children": [
{
"name": "PSO",
"value": 2,
//'points': -20
},
{
"name": "BYCO",
"value": 6,
// 'points': -20
},
{
"name": "MPCL",
"value": 2,
//'points': 10
},
]
},
/
]
}
partition = data => {
const root = d3.hierarchy(data)
.sum(d => d.value)
.sort((a, b) => b.value - a.value);
return d3.partition()
.size([2 * Math.PI, root.height + 1])
(root);
}
// color = d3.scaleOrdinal(d3.quantize(d3.interpolateRainbow, data.children.length + 1))
format = d3.format(",d")
width = 900
radius = width / 10
arc = d3.arc()
.startAngle(d => d.x0)
.endAngle(d => d.x1)
.padAngle(d => Math.min((d.x1 - d.x0) / 2, 0.005))
.padRadius(radius * 1.5)
.innerRadius(d => d.y0 * radius)
.outerRadius(d => Math.max(d.y0 * radius, d.y1 * radius - 1))
const svg = d3.select('body').append(chart)
</script>
</body>
</html>
I have been stuck in this since ages, I would be really thankfull if any of you can help me with this.
I am following the tutorial in https://observablehq.com/#d3/bar-chart-race-explained.
I am using chrome with version 89.0.4389.90 to test the code.
I mostly copied the code and downloaded the data csv file, and I am trying to run the code locally - here it is:
<!DOCTYPE html>
<html>
<head>
<title>Assignment1</title>
<script src="d3.v6.min.js"></script>
</head>
<body>
<!--<svg width="1600" height="800" id="mainsvg" class="svgs"></svg>-->
<script>
let margin = { top: 16, right: 6, bottom: 6, left: 0 };
let barSize = 48;
let n = 12;
let width = 1600;
let duration = 250;
let height = margin.top + barSize * n + margin.bottom;
d3.csv("category-brands.csv").then((data) => {
const x = d3.scaleLinear([0, 1], [margin.left, width - margin.right]);
const y = d3
.scaleBand()
.domain(d3.range(n + 1))
.rangeRound([margin.top, margin.top + barSize * (n + 1 + 0.1)])
.padding(0.1);
const names = new Set(data.map((d) => d.name));
console.log(names);
let datevalues = Array.from(
d3.rollup(
data,
([d]) => +d.value,
(d) => d.date,
(d) => d.name
)
);
console.log(datevalues);
datevalues = datevalues
.map(([date, data]) => [new Date(date), data])
.sort(([a], [b]) => d3.ascending(a, b));
console.log("datavalues:", datevalues);
function rank(value) {
const data = Array.from(names, (name) => ({
name,
value: value(name),
}));
data.sort((a, b) => d3.descending(a.value, b.value));
for (let i = 0; i < data.length; ++i) data[i].rank = Math.min(n, i);
return data;
}
console.log(
"rank",
rank((name) => datevalues[0][1].get(name))
);
const k = 10;
const keyframes = (function () {
const keyframes = [];
let ka, a, kb, b;
for ([[ka, a], [kb, b]] of d3.pairs(datevalues)) {
for (let i = 0; i < k; ++i) {
const t = i / k;
keyframes.push([
new Date(ka * (1 - t) + kb * t),
rank(
(name) =>
(a.get(name) || 0) * (1 - t) + (b.get(name) || 0) * t
),
]);
}
}
keyframes.push([new Date(kb), rank((name) => b.get(name) || 0)]);
return keyframes;
})();
console.log("total frames:", keyframes.length);
console.log("total frames:", keyframes);
let nameframes = d3.groups(
keyframes.flatMap(([, data]) => data),
(d) => d.name
);
console.log("name frames number:", nameframes.length);
console.log("name frames:", nameframes);
let prev = new Map(
nameframes.flatMap(([, data]) => d3.pairs(data, (a, b) => [b, a]))
);
let next = new Map(nameframes.flatMap(([, data]) => d3.pairs(data)));
console.log("pref:", prev);
console.log("next:", next);
function bars(svg) {
let bar = svg.append("g").attr("fill-opacity", 0.6).selectAll("rect");
return ([date, data], transition) =>
(bar = bar
.data(data.slice(0, n), (d) => d.name)
.join(
(enter) =>
enter
.append("rect")
.attr("fill", color)
.attr("height", y.bandwidth())
.attr("x", x(0))
.attr("y", (d) => y((prev.get(d) || d).rank))
.attr("width", (d) => x((prev.get(d) || d).value) - x(0)),
(update) => update,
(exit) =>
exit
.transition(transition)
.remove()
.attr("y", (d) => y((next.get(d) || d).rank))
.attr("width", (d) => x((next.get(d) || d).value) - x(0))
)
.call((bar) =>
bar
.transition(transition)
.attr("y", (d) => y(d.rank))
.attr("width", (d) => x(d.value) - x(0))
));
}
function labels(svg) {
let label = svg
.append("g")
.style("font", "bold 12px var(--sans-serif)")
.style("font-variant-numeric", "tabular-nums")
.attr("text-anchor", "end")
.selectAll("text");
return ([date, data], transition) =>
(label = label
.data(data.slice(0, n), (d) => d.name)
.join(
(enter) =>
enter
.append("text")
.attr(
"transform",
(d) =>
`translate(${x((prev.get(d) || d).value)},${y(
(prev.get(d) || d).rank
)})`
)
.attr("y", y.bandwidth() / 2)
.attr("x", -6)
.attr("dy", "-0.25em")
.text((d) => d.name)
.call((text) =>
text
.append("tspan")
.attr("fill-opacity", 0.7)
.attr("font-weight", "normal")
.attr("x", -6)
.attr("dy", "1.15em")
),
(update) => update,
(exit) =>
exit
.transition(transition)
.remove()
.attr(
"transform",
(d) =>
`translate(${x((next.get(d) || d).value)},${y(
(next.get(d) || d).rank
)})`
)
.call((g) =>
g
.select("tspan")
.tween("text", (d) =>
textTween(d.value, (next.get(d) || d).value)
)
)
)
.call((bar) =>
bar
.transition(transition)
.attr(
"transform",
(d) => `translate(${x(d.value)},${y(d.rank)})`
)
.call((g) =>
g
.select("tspan")
.tween("text", (d) =>
textTween((prev.get(d) || d).value, d.value)
)
)
));
}
function textTween(a, b) {
const i = d3.interpolateNumber(a, b);
return function (t) {
this.textContent = formatNumber(i(t));
};
}
formatNumber = d3.format(",d");
function axis(svg) {
const g = svg
.append("g")
.attr("transform", `translate(0,${margin.top})`);
const axis = d3
.axisTop(x)
.ticks(width / 160)
.tickSizeOuter(0)
.tickSizeInner(-barSize * (n + y.padding()));
return (_, transition) => {
g.transition(transition).call(axis);
g.select(".tick:first-of-type text").remove();
g.selectAll(".tick:not(:first-of-type) line").attr(
"stroke",
"white"
);
g.select(".domain").remove();
};
}
function ticker(svg) {
const now = svg
.append("text")
.style("font", `bold ${barSize}px var(--sans-serif)`)
.style("font-variant-numeric", "tabular-nums")
.attr("text-anchor", "end")
.attr("x", width - 6)
.attr("y", margin.top + barSize * (n - 0.45))
.attr("dy", "0.32em")
.text(formatDate(keyframes[0][0]));
return ([date], transition) => {
transition.end().then(() => now.text(formatDate(date)));
};
}
let formatDate = d3.utcFormat("%Y");
let color = (function () {
const scale = d3.scaleOrdinal(d3.schemeTableau10);
if (data.some((d) => d.category !== undefined)) {
const categoryByName = new Map(
data.map((d) => [d.name, d.category])
);
scale.domain(categoryByName.values());
return (d) => scale(categoryByName.get(d.name));
}
return (d) => scale(d.name);
})();
const svg = d3.create("svg").attr("viewBox", [0, 0, width, height]);
const updateBars = bars(svg);
const updateAxis = axis(svg);
const updateLabels = labels(svg);
const updateTicker = ticker(svg);
const start = async function () {
for (const keyframe of keyframes) {
const transition = svg
.transition()
.duration(duration)
.ease(d3.easeLinear);
console.log("iteration..");
// Extract the top bar’s value.
x.domain([0, keyframe[1][0].value]);
updateAxis(keyframe, transition);
updateBars(keyframe, transition);
updateLabels(keyframe, transition);
updateTicker(keyframe, transition);
await transition.end();
}
};
start();
});
</script>
</body>
</html>
I have altered the code to run without observablehq's environment, but, after running the code, nothing was showing on the web page.
The console log shows that the data processing logic is normal, but for the rending part, it does nothing except printing iteration.
What is the problem with my code ?
You need to attach the svg you just created to some element in the HTML to see the output.
d3.create just creates a detached element. Since you are not attaching it anywhere, we are not seeing the output in the screen.
We can use d3.select to select where we want to place this chart and use then .append.
d3.select('#chart').append('svg')
const fileUrl =
'https://static.observableusercontent.com/files/aec3792837253d4c6168f9bbecdf495140a5f9bb1cdb12c7c8113cec26332634a71ad29b446a1e8236e0a45732ea5d0b4e86d9d1568ff5791412f093ec06f4f1?response-content-disposition=attachment%3Bfilename*%3DUTF-8%27%27category-brands.csv';
let margin = {
top: 16,
right: 6,
bottom: 6,
left: 0,
};
let barSize = 48;
let n = 12;
let width = 1600;
let duration = 250;
let height = margin.top + barSize * n + margin.bottom;
d3.csv(fileUrl).then((data) => {
const x = d3.scaleLinear([0, 1], [margin.left, width - margin.right]);
const y = d3
.scaleBand()
.domain(d3.range(n + 1))
.rangeRound([margin.top, margin.top + barSize * (n + 1 + 0.1)])
.padding(0.1);
const names = new Set(data.map((d) => d.name));
let datevalues = Array.from(
d3.rollup(
data,
([d]) => +d.value,
(d) => d.date,
(d) => d.name
)
);
datevalues = datevalues
.map(([date, data]) => [new Date(date), data])
.sort(([a], [b]) => d3.ascending(a, b));
function rank(value) {
const data = Array.from(names, (name) => ({
name,
value: value(name),
}));
data.sort((a, b) => d3.descending(a.value, b.value));
for (let i = 0; i < data.length; ++i) data[i].rank = Math.min(n, i);
return data;
}
const k = 10;
const keyframes = (function () {
const keyframes = [];
let ka, a, kb, b;
for ([[ka, a], [kb, b]] of d3.pairs(datevalues)) {
for (let i = 0; i < k; ++i) {
const t = i / k;
keyframes.push([
new Date(ka * (1 - t) + kb * t),
rank((name) => (a.get(name) || 0) * (1 - t) + (b.get(name) || 0) * t),
]);
}
}
keyframes.push([new Date(kb), rank((name) => b.get(name) || 0)]);
return keyframes;
})();
console.log('total frames:', keyframes.length);
console.log('total frames:', keyframes);
let nameframes = d3.groups(
keyframes.flatMap(([, data]) => data),
(d) => d.name
);
console.log('name frames number:', nameframes.length);
console.log('name frames:', nameframes);
let prev = new Map(
nameframes.flatMap(([, data]) => d3.pairs(data, (a, b) => [b, a]))
);
let next = new Map(nameframes.flatMap(([, data]) => d3.pairs(data)));
console.log('pref:', prev);
console.log('next:', next);
function bars(svg) {
let bar = svg.append('g').attr('fill-opacity', 0.6).selectAll('rect');
return ([date, data], transition) =>
(bar = bar
.data(data.slice(0, n), (d) => d.name)
.join(
(enter) =>
enter
.append('rect')
.attr('fill', color)
.attr('height', y.bandwidth())
.attr('x', x(0))
.attr('y', (d) => y((prev.get(d) || d).rank))
.attr('width', (d) => x((prev.get(d) || d).value) - x(0)),
(update) => update,
(exit) =>
exit
.transition(transition)
.remove()
.attr('y', (d) => y((next.get(d) || d).rank))
.attr('width', (d) => x((next.get(d) || d).value) - x(0))
)
.call((bar) =>
bar
.transition(transition)
.attr('y', (d) => y(d.rank))
.attr('width', (d) => x(d.value) - x(0))
));
}
function labels(svg) {
let label = svg
.append('g')
.style('font', 'bold 12px var(--sans-serif)')
.style('font-variant-numeric', 'tabular-nums')
.attr('text-anchor', 'end')
.selectAll('text');
return ([date, data], transition) =>
(label = label
.data(data.slice(0, n), (d) => d.name)
.join(
(enter) =>
enter
.append('text')
.attr(
'transform',
(d) =>
`translate(${x((prev.get(d) || d).value)},${y(
(prev.get(d) || d).rank
)})`
)
.attr('y', y.bandwidth() / 2)
.attr('x', -6)
.attr('dy', '-0.25em')
.text((d) => d.name)
.call((text) =>
text
.append('tspan')
.attr('fill-opacity', 0.7)
.attr('font-weight', 'normal')
.attr('x', -6)
.attr('dy', '1.15em')
),
(update) => update,
(exit) =>
exit
.transition(transition)
.remove()
.attr(
'transform',
(d) =>
`translate(${x((next.get(d) || d).value)},${y(
(next.get(d) || d).rank
)})`
)
.call((g) =>
g
.select('tspan')
.tween('text', (d) =>
textTween(d.value, (next.get(d) || d).value)
)
)
)
.call((bar) =>
bar
.transition(transition)
.attr('transform', (d) => `translate(${x(d.value)},${y(d.rank)})`)
.call((g) =>
g
.select('tspan')
.tween('text', (d) =>
textTween((prev.get(d) || d).value, d.value)
)
)
));
}
function textTween(a, b) {
const i = d3.interpolateNumber(a, b);
return function (t) {
this.textContent = formatNumber(i(t));
};
}
formatNumber = d3.format(',d');
function axis(svg) {
const g = svg.append('g').attr('transform', `translate(0,${margin.top})`);
const axis = d3
.axisTop(x)
.ticks(width / 160)
.tickSizeOuter(0)
.tickSizeInner(-barSize * (n + y.padding()));
return (_, transition) => {
g.transition(transition).call(axis);
g.select('.tick:first-of-type text').remove();
g.selectAll('.tick:not(:first-of-type) line').attr('stroke', 'white');
g.select('.domain').remove();
};
}
function ticker(svg) {
const now = svg
.append('text')
.style('font', `bold ${barSize}px var(--sans-serif)`)
.style('font-variant-numeric', 'tabular-nums')
.attr('text-anchor', 'end')
.attr('x', width - 6)
.attr('y', margin.top + barSize * (n - 0.45))
.attr('dy', '0.32em')
.text(formatDate(keyframes[0][0]));
return ([date], transition) => {
transition.end().then(() => now.text(formatDate(date)));
};
}
let formatDate = d3.utcFormat('%Y');
let color = (function () {
const scale = d3.scaleOrdinal(d3.schemeTableau10);
if (data.some((d) => d.category !== undefined)) {
const categoryByName = new Map(data.map((d) => [d.name, d.category]));
scale.domain(categoryByName.values());
return (d) => scale(categoryByName.get(d.name));
}
return (d) => scale(d.name);
})();
const svg = d3.select('#chart').append('svg').attr('viewBox', [0, 0, width, height]);
const updateBars = bars(svg);
const updateAxis = axis(svg);
const updateLabels = labels(svg);
const updateTicker = ticker(svg);
const start = async function () {
for (const keyframe of keyframes) {
const transition = svg
.transition()
.duration(duration)
.ease(d3.easeLinear);
// Extract the top bar’s value.
x.domain([0, keyframe[1][0].value]);
updateAxis(keyframe, transition);
updateBars(keyframe, transition);
updateLabels(keyframe, transition);
updateTicker(keyframe, transition);
await transition.end();
}
};
start();
});
<script src="https://d3js.org/d3.v6.min.js"></script>
<div id="chart"></div>
I have created 2 level sunburst chart on http://test.smokethis.com/wheel/ which is working perfectly.
But i want to validate my chart to show only 10 child of a particular parent and an additional node called "More child".
The behaviour on "More child" will be same as on click on parent. Which reveal all child of parent.
Image link of current situation
Image link of Resultant behaviour.Which i want
var d3Data = JSON.parse(jQuery('body .Jsondata').text())[0];
const m0 = {
id: "123",
variables: [
{
name: "chart",
inputs: ["partition","data","d3","DOM","width","color","arc","format","radius"],
value: (function(partition,data,d3,DOM,width,color,arc,format,radius)
{
var formatNumber = d3.format(",d");
var b = {
w: 140, h: 30, s: 3, t: 10
};
const root = partition(data);
root.each(d => d.current = d);
const svg = d3.select(DOM.svg(width, width))
.style("width", "100%")
.style("height", "auto")
.style("font", "20px sans-serif");
const g = svg.append("g")
.attr("transform", `translate(${width / 2},${width / 2})`);
const path = g.append("g")
.selectAll("path")
.data(root.descendants().slice(1))
.enter().append("path")
.attr("fill", d => { while (d.depth > 1) d = d.parent; console.log('d value--',d); return (typeof d.data.color != 'undefined' && d.data.color != '') ? d.data.color : color(d.data.name); })
.attr("fill-opacity", d => arcVisible(d.current) ? (d.children ? 0.6 : 0.4) : 0)
.attr("d", d => arc(d.current));
path.filter(d => d.children)
.style("cursor", "pointer")
.on("click", clicked);
path.append("title")
.text(d => `${d.ancestors().map(d => d.data.name).reverse().join("/")}\n${format(d.value)}`);
const label = g.append("g")
.attr("pointer-events", "none")
.attr("text-anchor", "middle")
.style("user-select", "none")
.selectAll("text")
.data(root.descendants().slice(1))
.enter().append("text")
.attr("dy", "0.35em")
.attr("fill-opacity", d => +labelVisible(d.current))
.attr("transform", d => labelTransform(d.current))
.text(d => d.data.name);
const parent = g.append("circle")
.datum(root)
.attr("r", radius)
.attr("fill", "none")
.attr("pointer-events", "all")
.on("click", clicked);
initializeBreadcrumbTrail();
return svg.node();
function clicked(p) {
updateBreadcrumbs(getParents(p),p.value);
var level = p.ancestors().map(p => p.data.name).reverse();
// var str = '';
// if(level.length > 1){
// for(var i=0;i<level.length;i++){
// str += '<span class="str_att show-pointer" style="background-color: #8cb125;margin-right:10px;border-radius: 5px;padding: 5px 10px;">'+level[i]+'</span>';
// }
// }
//jQuery('.breadCrumb').html(str);
console.log('level-=-',level);
parent.datum(p.parent || root);
root.each(d => d.target = {
x0: Math.max(0, Math.min(1, (d.x0 - p.x0) / (p.x1 - p.x0))) * 2 * Math.PI,
x1: Math.max(0, Math.min(1, (d.x1 - p.x0) / (p.x1 - p.x0))) * 2 * Math.PI,
y0: Math.max(0, d.y0 - p.depth),
y1: Math.max(0, d.y1 - p.depth)
});
const t = g.transition().duration(750);
//console.log('p',p);
// Transition the data on all arcs, even the ones that aren’t visible,
// so that if this transition is interrupted, entering arcs will start
// the next transition from the desired position.
path.transition(t)
.tween("data", d => {
const i = d3.interpolate(d.current, d.target);
return t => d.current = i(t);
})
.filter(function(d) {
return +this.getAttribute("fill-opacity") || arcVisible(d.target);
})
.attr("fill-opacity", d => arcVisible(d.target) ? (d.children ? 0.6 : 0.4) : 0)
.attrTween("d", d => () => arc(d.current));
label.filter(function(d) {
return +this.getAttribute("fill-opacity") || labelVisible(d.target);
}).transition(t)
.attr("fill-opacity", d => +labelVisible(d.target))
.attrTween("transform", d => () => labelTransform(d.current));
}
function initializeBreadcrumbTrail() {
// Add the svg area.
var trail = d3.select(".breadCrumb").append("svg:svg")
.attr("width", width)
.attr("height", width/10)
.attr("id", "trail");
// Add the label at the end, for the percentage.
trail.append("svg:text")
.attr("id", "endlabel")
.style("fill", "#000");
}
function updateBreadcrumbs(nodeArray, percentageString) {
// Data join; key function combines name and depth (= position in sequence).
var g = d3.select("#trail")
.selectAll("g").on("click", clicked)
.data(nodeArray, function(x) { return percentageString + x.data.name + x.depth; });
// Add breadcrumb and label for entering nodes.
var entering = g.enter().append("svg:g");
entering.append("svg:polygon")
.attr("points", breadcrumbPoints)
.style("fill", function(x) { return color(x.data.name); });
entering.append("svg:text")
.attr("x", (b.w + b.t) / 2)
.attr("y", b.h / 2)
.attr("dy", "0.35em")
.attr("text-anchor", "middle")
.text(function(x) { return x.data.name; });
entering.attr("transform", function(x, i) { return "translate(" + i* (b.w + b.s) + ", 0)"; });
// Remove exiting nodes.
g.exit().remove();
// Now move and update the percentage at the end.
d3.select("#trail").select("#endlabel")
.attr("x", (nodeArray.length + 0.5) * (b.w + b.s))
.attr("y", b.h / 2)
.attr("dy", "0.35em")
.attr("text-anchor", "middle")
.text(percentageString);
// Make the breadcrumb trail visible, if it's hidden.
d3.select("#trail")
.style("visibility", "");
}
function breadcrumbPoints(x, i) {
var points = [];
points.push("0,0");
points.push(b.w + ",0");
points.push(b.w + b.t + "," + (b.h / 2));
points.push(b.w + "," + b.h);
points.push("0," + b.h);
if (i > 0) { // Leftmost breadcrumb; don't include 6th vertex.
points.push(b.t + "," + (b.h / 2));
}
return points.join(" ");
}
function arcVisible(d) {
return d.y1 <= 3 && d.y0 >= 1 && d.x1 > d.x0;
}
function labelVisible(d) {
return d.y1 <= 3 && d.y0 >= 1 && (d.y1 - d.y0) * (d.x1 - d.x0) > 0.03;
}
function labelTransform(d) {
const x = (d.x0 + d.x1) / 2 * 180 / Math.PI;
const y = (d.y0 + d.y1) / 2 * radius;
return `rotate(${x - 90}) translate(${y},0) rotate(${x < 180 ? 0 : 180})`;
}
function getParents(a){
var nodeArray = [a];
while(a.parent){
nodeArray.push(a.parent);
a = a.parent
}
return nodeArray.reverse();
}
}
)
},
{
name: "data",
value: (function(){
return d3Data;
})
},
{
name: "partition",
inputs: ["d3"],
value: (function(d3){
// console.log('partition',d3);
return(
data => {
const root = d3.hierarchy(data)
.sum(d => d.size)
.sort((a, b) => b.value - a.value);
console.log('data',d3.partition().size([2 * Math.PI, root.height+1])(root));
return d3.partition()
.size([2 * Math.PI, root.height+1])
(root);
}
)
})
},
{
name: "color",
inputs: ["d3","data"],
value: (function(d3,data){return (
d3.scaleOrdinal().range(d3.quantize(d3.interpolateRainbow, data.children.length + 1))
)})
},
{
name: "format",
inputs: ["d3"],
value: (function(d3){return(
d3.format(",d")
)})
},
{
name: "width",
value: (function(){return(
932
)})
},
{
name: "radius",
inputs: ["width"],
value: (function(width){return(
width / 6
)})
},
{
name: "arc",
inputs: ["d3","radius"],
value: (function(d3,radius){return(
d3.arc()
.startAngle(d => d.x0)
.endAngle(d => d.x1)
.padAngle(d => Math.min((d.x1 - d.x0) / 2, 0.005))
.padRadius(radius * 1.5)
.innerRadius(d => d.y0 * radius)
.outerRadius(d => Math.max(d.y0 * radius, d.y1 * radius - 1))
)})
},
{
name: "d3",
inputs: ["require"],
value: (function(require){return(
require("https://d3js.org/d3.v5.min.js")
)})
}
]
};
const notebook = {
id: "123",
modules: [m0]
};
function loadData(){
$.ajax({
type:'GET',
url:'http://shopnz.in/wp-admin/admin-ajax.php?action=wp_ajax_list_items&sunburst=sunburstcat',
success: function(response){
console.log('response',response[0]);
console.log('data-=-=',data);
data = response[0];
}
});
}
export default notebook;
[{"name":"Balance","term_id":"588","slug":"balance","parent":"0","has_children":true,"color":"#aac62d","children":[{"name":"Aroma","term_id":"589","slug":"aroma","parent":"588","has_children":true,"color":"#aac62d","children":[{"name":"Earth","term_id":"593","slug":"earth","parent":"589","has_children":false,"size":"1000","color":"#aac62d"},{"name":"Fruit","term_id":"594","slug":"fruit","parent":"589","has_children":false,"size":"1000","color":"#aac62d"},{"name":"Herb","term_id":"595","slug":"herb","parent":"589","has_children":false,"size":"1000","color":"#aac62d"},{"name":"Pungent","term_id":"596","slug":"pungent","parent":"589","has_children":false,"size":"1000","color":"#aac62d"},{"name":"Spice","term_id":"597","slug":"spice","parent":"589","has_children":false,"size":"1000","color":"#aac62d"}]},{"name":"Health","term_id":"590","slug":"health","parent":"588","has_children":true,"color":"#aac62d","children":[{"name":"Body","term_id":"598","slug":"body","parent":"590","has_children":true,"color":"#aac62d","children":[{"name":"Fatigue","term_id":"613","slug":"fatigue","parent":"598","has_children":true,"color":"#aac62d","children":[{"name":"Sleep","term_id":"616","slug":"sleep","parent":"613","has_children":false,"size":"1000","color":"#aac62d"}]},{"name":"PainRelief","term_id":"614","slug":"painrelief","parent":"598","has_children":true,"color":"#aac62d","children":[{"name":"Spasms","term_id":"617","slug":"spasms","parent":"614","has_children":false,"size":"1000","color":"#aac62d"},{"name":"Relaxed","term_id":"618","slug":"relaxed","parent":"614","has_children":false,"size":"1000","color":"#aac62d"}]},{"name":"Digestion","term_id":"615","slug":"digestion","parent":"598","has_children":true,"color":"#aac62d","children":[{"name":"Appetite","term_id":"619","slug":"appetite","parent":"615","has_children":false,"size":"1000","color":"#aac62d"},{"name":"Nausea","term_id":"620","slug":"nausea","parent":"615","has_children":false,"size":"1000","color":"#aac62d"}]}]},{"name":"Mind","term_id":"599","slug":"mind","parent":"590","has_children":true,"color":"#aac62d","children":[{"name":"Behavior","term_id":"621","slug":"behavior","parent":"599","has_children":true,"color":"#aac62d","children":[{"name":"Sedative","term_id":"623","slug":"sedative","parent":"621","has_children":false,"size":"1000","color":"#aac62d"},{"name":"Energetic","term_id":"624","slug":"energetic","parent":"621","has_children":false,"size":"1000","color":"#aac62d"},{"name":"Focus","term_id":"625","slug":"focus","parent":"621","has_children":false,"size":"1000","color":"#aac62d"}]},{"name":"Mood","term_id":"622","slug":"mood","parent":"599","has_children":true,"color":"#aac62d","children":[{"name":"Creative","term_id":"626","slug":"creative","parent":"622","has_children":false,"size":"1000","color":"#aac62d"},{"name":"Uplifted","term_id":"627","slug":"uplifted","parent":"622","has_children":false,"size":"1000","color":"#aac62d"}]}]}]},{"name":"Medical","term_id":"591","slug":"medical","parent":"588","has_children":true,"color":"#aac62d","children":[{"name":"Condition","term_id":"600","slug":"condition","parent":"591","has_children":true,"color":"#aac62d","children":[{"name":"Cancer","term_id":"602","slug":"cancer","parent":"600","has_children":false,"size":"1000","color":"#aac62d"},{"name":"PMS","term_id":"603","slug":"pms","parent":"600","has_children":false,"size":"1000","color":"#aac62d"},{"name":"Arthritis","term_id":"628","slug":"arthritis","parent":"600","has_children":false,"size":"1000","color":"#aac62d"},{"name":"Asthma","term_id":"629","slug":"asthma","parent":"600","has_children":false,"size":"1000","color":"#aac62d"},{"name":"Cachexia","term_id":"630","slug":"cachexia","parent":"600","has_children":false,"size":"1000","color":"#aac62d"},{"name":"Crohn\u2019s Disease","term_id":"631","slug":"crohns-disease","parent":"600","has_children":false,"size":"1000","color":"#aac62d"},{"name":"Depression (currently a 'symptom')","term_id":"632","slug":"depression-currently-a-symptom","parent":"600","has_children":false,"size":"1000","color":"#aac62d"},{"name":"Epilepsy","term_id":"633","slug":"epilepsy","parent":"600","has_children":false,"size":"1000","color":"#aac62d"},{"name":"Gastrointestinal Disorder","term_id":"634","slug":"gastrointestinal-disorder","parent":"600","has_children":false,"size":"1000","color":"#aac62d"},{"name":"Hypertension","term_id":"635","slug":"hypertension","parent":"600","has_children":false,"size":"1000","color":"#aac62d"},{"name":"Parkinson\u2019s","term_id":"636","slug":"parkinsons","parent":"600","has_children":false,"size":"1000","color":"#aac62d"},{"name":"Phantom Limb Pain","term_id":"637","slug":"phantom-limb-pain","parent":"600","has_children":false,"size":"1000","color":"#aac62d"},{"name":"Spinal Cord Injury","term_id":"638","slug":"spinal-cord-injury","parent":"600","has_children":false,"size":"1000","color":"#aac62d"}]},{"name":"Symptom","term_id":"601","slug":"symptom","parent":"591","has_children":true,"color":"#aac62d","children":[{"name":"Anxiety","term_id":"604","slug":"anxiety","parent":"601","has_children":true,"color":"#aac62d","children":[{"name":"ACDC","term_id":"612","slug":"acdc","parent":"604","has_children":false,"size":"1000","color":"#aac62d"}]},{"name":"Depresssion","term_id":"605","slug":"depresssion","parent":"601","has_children":false,"size":"1000","color":"#aac62d"}]}]},{"name":"Terpenes","term_id":"592","slug":"terpenes","parent":"588","has_children":true,"color":"#aac62d","children":[{"name":"Humulene","term_id":"606","slug":"humulene","parent":"592","has_children":false,"size":"1000","color":"#aac62d"},{"name":"Limonene","term_id":"607","slug":"limonene","parent":"592","has_children":false,"size":"1000","color":"#aac62d"},{"name":"Linalool","term_id":"608","slug":"linalool","parent":"592","has_children":false,"size":"1000","color":"#aac62d"},{"name":"Myrcene","term_id":"609","slug":"myrcene","parent":"592","has_children":false,"size":"1000","color":"#aac62d"},{"name":"Pinene","term_id":"610","slug":"pinene","parent":"592","has_children":true,"color":"#aac62d","children":[{"name":"Romulan","term_id":"611","slug":"romulan","parent":"610","has_children":false,"size":"1000","color":"#aac62d"}]}]}]}]
This is my data.
I have a simple pie chart here:
https://jsfiddle.net/5dexn2kn/1/
Now I want to add an icon in front of each label, like this:
I tried to append image to each label group
https://jsfiddle.net/5dexn2kn/2/
as you can see, because text-anchor may be start or end, also the text length of each label is different, I don't know how to find and place the image to the correct position.
How can I achieve this?
You can use the getBoundingBox() function to achieve this.
Code:
labelG.append('image')
.attr('xlink:href', 'https://placekitten.com/20/20?.jpg')
.attr('height', 20)
.attr('width', 20)
.attr("x", function(d) {
var bbox = this.parentNode.getBBox();
return bbox.x;
})
.attr("y", function() {
var bbox = this.parentNode.getBBox();
return bbox.y;
});
labelG.selectAll("text").attr("dx", 25);
Complete Snippet:
const width = 400
const height = 400
const labelSpace = 50
const donutRadius = Math.min(width, height) / 2
const maxRadius = donutRadius + labelSpace
const colorScheme = d3.scaleOrdinal(d3.schemeCategory20)
const innerRadius = 20
const svgTranslate = [width / 2 + labelSpace * 2, height / 2 + labelSpace * 2]
function getPercent(value, total) {
return Math.round(value / total * 100)
}
const data = [{
label: 'aaaaaaa',
value: 19
}, {
label: 'bbb',
value: 31
}, {
label: 'c',
value: 31
}, {
label: 'ddddddddddd',
value: 8
}, {
label: 'eeee',
value: 10
}]
const total = data.map(d => d.value).reduce((a, b) => a + b)
const svg = d3.select('#donutchart')
.append('svg')
.attr('width', width + maxRadius)
.attr('height', height + maxRadius)
.append('g')
.attr('transform', `translate(${svgTranslate[0]}, ${svgTranslate[1]})`)
const arc = d3.arc()
.innerRadius(innerRadius)
.outerRadius(donutRadius)
const donut = d3.pie()
.sort(null)
.value(d => d.value)
const arcG = svg.selectAll('g.arc-g')
.data(donut(data))
.enter()
.append('g')
arcG.append('path')
.attr('d', arc)
.attr('fill', d => colorScheme(d.data.label))
const labelG = arcG.append('g')
.attr('transform', d => {
const c = arc.centroid(d)
const x = c[0] * 2
const y = c[1] * 2
return `translate(${x}, ${y})`
})
labelG.append('text')
.attr('dy', '.35em')
.html(d =>
`${d.data.label}<tspan class="label-percent"> ${getPercent(d.data.value, total)}%</tspan>`
)
.attr('text-anchor', d =>
(d.endAngle + d.startAngle) / 2 > Math.PI ? 'end' : 'start'
)
labelG.append('image')
.attr('xlink:href', 'https://placekitten.com/20/20?.jpg')
.attr('height', 20)
.attr('width', 20)
.attr("x", function(d) {
var bbox = this.parentNode.getBBox();
return bbox.x;
})
.attr("y", function() {
var bbox = this.parentNode.getBBox();
return bbox.y;
});
labelG.selectAll("text").attr("dx", 25);
body {
font-family: San Francisco Display, sans-serif;
}
.label-percent {
font-weight: bold;
}
<div id="donutchart"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.3.0/d3.min.js"></script>
Maybe this will help : https://jsfiddle.net/5dexn2kn/3/
Give the text an id :
labelG.append('text')
.attr('id', function(d) {
return d.data.label + '_text'
})
This has to be unique. In this case it works but if you have the same label sometimes, it won't.
Then get the width of the corresponding label text like so :
var thisText = document.getElementById(d.data.label + '_text') //select based on data, as id above is given from data
var thisTextWidth = thisText.clientWidth;
Then use the same logic to determine if you want the text anchor to be start or end to determine the position of the image :
if ((d.endAngle + d.startAngle) / 2 > Math.PI) {
return -35 - thisTextWidth;
} else {
return -25;
}