Related
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 am working with the d3.js treemap layout . Find my code link here https://codesandbox.io/s/loving-mccarthy-wfbtg. The tooltip consists of the hierarchy like flare/vis 436777 but I need only the Label and the total count or value like vis 436777 .
Current tooltip
Finally added custom tooltip but the tooltip sticks on the canvas on click of a segment rect. Added th screenshot
I checked and found out that you have written a function called "name" where you are finding ancestors and joining them. Please found below my finding and code you need to write to fulfill your requirement. Add the CSS to styles.css and replace your class Treegraph with the below class.
class Treegraph extends React.Component {
state = {
width: 400,
height: 400
};
createTreeChart = () => {
const width = 550;
const height = 500;
const padding = 60;
const format = d3.format(",d");
const name = d =>
d
.ancestors()
.reverse()
.map(d => d.data.segment)
.join(" / ");
// const name = d => d.data.segment;
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);
}
}
const treemap = data =>
d3.treemap().tile(tile)(
d3
.hierarchy(data)
.sum(d => d.value)
.sort((a, b) => b.value - a.value)
);
const svg = d3
.select("#chart")
.append("svg")
.attr("viewBox", [0.5, -30.5, width, height + 30])
.style("font", "16px sans-serif");
const x = d3.scaleLinear().rangeRound([0, width]);
const y = d3.scaleLinear().rangeRound([0, height]);
// const svg = d3
// .create("svg")
// .select("#chart")
// .append("svg")
// .attr("viewBox", [0.5, -30.5, width, height + 30])
// .style("font", "10px 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", d => (d === root ? zoomout(root) : zoomin(d)));
//node.append("title").text(d => `${name(d)}\n(${format(d.data.count)})`);
var tool = d3
.select("body")
.append("div")
.attr("class", "toolTip");
d3.select(self.frameElement).style("height", height + 300 + "px");
d3.select(self.frameElement).style("width", width + 20 + "px");
node
.append("rect")
.attr("id", d => (d.leafUid = "leaf"))
.attr("fill", d =>
d === root ? "#fff" : d.children ? "#045c79" : "#045c79"
)
.attr("stroke", "#fff")
.on("mousemove", function(d) {
tool.style("left", d3.event.pageX + 10 + "px");
tool.style("top", d3.event.pageY - 20 + "px");
tool.style("display", "inline-block");
tool.html(`${d.data.segment}<br />(${format(d.data.count)})`);
})
.on("mouseout", function(d) {
tool.style("display", "none");
});
node
.append("clipPath")
.attr("id", d => (d.clipUid = "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))
.attr("font-size", d => {
if (d === root) return "0.8em";
const width = x(d.x1) - x(d.x0),
height = y(d.y1) - y(d.y0);
return Math.max(
Math.min(
width / 5,
height / 2,
Math.sqrt(width * width + height * height) / 25
),
9
);
})
.attr("text-anchor", d => (d === root ? null : "middle"))
.attr("transform", d =>
d === root ?
null :
`translate(${(x(d.x1) - x(d.x0)) / 2}, ${(y(d.y1) - y(d.y0)) /
2})`
)
.selectAll("tspan")
.data(d =>
d === root ?
name(d).split(/(?=\/)/g) :
d.value < 2 ?
`${d.data.segment.substring(0, 3)}...`.split(/(\s+)/) :
d.data.segment.split(/(\s+)/).concat(format(d.data.count))
)
.join("tspan")
.attr("x", 3)
.attr(
"y",
(d, i, nodes) =>
`${(i === nodes.length - 1) * 0.3 + (i - nodes.length / 2) * 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);
node
.selectAll("text")
.classed("text-title", d => d === root)
.classed("text-tile", d => d !== root)
.filter(d => d === root)
.selectAll("tspan")
.attr("y", "1.1em")
.attr("x", undefined);
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) {
x.domain([d.x0, d.x1]);
y.domain([d.y0, d.y1]);
const group0 = group.attr("pointer-events", "none");
const group1 = (group = svg.append("g").call(render, d));
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) {
x.domain([d.parent.x0, d.parent.x1]);
y.domain([d.parent.y0, d.parent.y1]);
const group0 = group.attr("pointer-events", "none");
const group1 = (group = svg.insert("g", "*").call(render, d.parent));
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));
}
return svg.node();
};
componentDidMount() {
this.createTreeChart();
}
render() {
return ( <
React.Fragment >
<
div id = "chart" / >
<
/React.Fragment>
);
}
}
.toolTip {
position: absolute;
display: none;
width: auto;
height: auto;
background: none repeat scroll 0 0 white;
border: 0 none;
border-radius: 8px 8px 8px 8px;
box-shadow: -3px 3px 15px #888888;
color: black;
font: 12px sans-serif;
padding: 5px;
text-align: center;
}
I am working with the d3.js treemap layout . In the tiles the font size is too small . How to display in a bigger font for big tiles and smaller for small tiles or display the label text at the center of each tiles. Find my code link here https://codesandbox.io/s/loving-mccarthy-wfbtg. The label can be centered and the font size can be increased for better view. Also the title contains the total which I dont want to show how to remove the count in the breadcrumb title alone.
Logic is here
createTreeChart = () => {
const width = 550;
const height = 600;
const padding = 60;
const format = d3.format(",d");
const 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);
}
}
const treemap = data =>
d3.treemap().tile(tile)(
d3
.hierarchy(data)
.sum(d => d.value)
.sort((a, b) => b.value - a.value)
);
const svg = d3
.select("#chart")
.append("svg")
.attr("viewBox", [0.5, -30.5, width, height + 30])
.style("font", "10px sans-serif");
const x = d3.scaleLinear().rangeRound([0, width]);
const y = d3.scaleLinear().rangeRound([0, height]);
// const svg = d3
// .create("svg")
// .select("#chart")
// .append("svg")
// .attr("viewBox", [0.5, -30.5, width, height + 30])
// .style("font", "10px 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", d => (d === root ? zoomout(root) : zoomin(d)));
node.append("title").text(d => `${name(d)}\n${format(d.value)}`);
node
.append("rect")
.attr("id", d => (d.leafUid = "leaf"))
.attr("fill", d => (d === root ? "#fff" : d.children ? "#ccc" : "#ddd"))
.attr("stroke", "#fff");
node
.append("clipPath")
.attr("id", d => (d.clipUid = "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 === root ? name(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));
}
return svg.node();
};
Change the font-size attribute of the text nodes. Add .attr("font-size", ...) as the following.
node
.append("text")
.attr("clip-path", d => d.clipUid)
.attr("font-weight", d => (d === root ? "bold" : null))
.attr("font-size", d => {
if (d === root) return "1em";
const width = x(d.x1) - x(d.x0), height = y(d.y1) - y(d.y0);
return Math.max(Math.min(width/5, height/2, Math.sqrt((width*width + height*height))/10), 9)
})
.selectAll("tspan")
...
This sets the font-size to 10% of the diagonal of each rect.
In case the text becomes too small to read, font-size is set to the minimum of 9. The texts might become too big for rects that are vertically long, so the maximum set to one fifth of the width of the rect or half of the height of the rect, whichever is bigger.
The font-size doesn't scale correctly after zooming because x and y scales' domains are updated only after render. Update the domains before calling render in zoomin and zoomout functions.
function zoomin(d) {
x.domain([d.x0, d.x1]);
y.domain([d.y0, d.y1]);
const group0 = group.attr("pointer-events", "none");
const group1 = (group = svg.append("g").call(render, d));
...
}
function zoomout(d) {
x.domain([d.parent.x0, d.parent.x1]);
y.domain([d.parent.y0, d.parent.y1]);
const group0 = group.attr("pointer-events", "none");
const group1 = (group = svg.insert("g", "*").call(render, d.parent));
...
}
Breadcrumb fix. When you set the rectangles' tspans' x and y, you are also setting the x and y of the tspans in the breadcrumb with the same logic. The easiest way to solve this is simply to make the breadcrumb one tspan. This, however, limits the styling of the breadcrumb. Instead, style the breadcrumb text separately at the end of the render function.
...
.text(d => d);
node.selectAll('text').filter(d => d === root)
.selectAll("tspan").attr("y", '1.1em').attr("x", undefined);
group.call(position, root);
And the regex for the breadcrumb text doesn't look like it's working out. It's separating the text at odd places. Change name(d).split(/(?=[A-Z][^A-Z])/g) to name(d).split(/(?=\/)/g).
Text centering. There's a lot to be done. Text anchor is set to middle, and transform: translate text boxes by half of width/height.
.attr("text-anchor", d => d === root ? null : "middle")
.attr("transform", d=> d === root ? null :
`translate(${(x(d.x1) - x(d.x0))/2}, ${(y(d.y1) - y(d.y0))/2})`)
.selectAll("tspan")
And change tspans' y to center them vertically.
.attr("x", 3)
.attr(
"y",
(d, i, nodes) => `${(i === nodes.length - 1) * 0.3 + (i - nodes.length/2) * 0.9}em`
)
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 am trying to color the connections in my hierarchical edge bundling visualization based on the groups they are connecting to. An example of this can be seen here.
Here is my current mouseover function:
function mouseover(d) {
svg.selectAll("path.link.target-" + d.key)
.classed("target", true)
.each(updateNodes("source", true));
svg.selectAll("path.link.source-" + d.key)
.classed("source", true)
.each(updateNodes("target", true));
}
And here is the mouseover function from the example I've posted:
function mouseovered(d)
{
// Handle tooltip
// Tooltips should avoid crossing into the center circle
d3.selectAll("#tooltip").remove();
d3.selectAll("#vis")
.append("xhtml:div")
.attr("id", "tooltip")
.style("opacity", 0)
.html(d.title);
var mouseloc = d3.mouse(d3.select("#vis")[0][0]),
my = ((rotateit(d.x) > 90) && (rotateit(d.x) < 270)) ? mouseloc[1] + 10 : mouseloc[1] - 35,
mx = (rotateit(d.x) < 180) ? (mouseloc[0] + 10) : Math.max(130, (mouseloc[0] - 10 - document.getElementById("tooltip").offsetWidth));
d3.selectAll("#tooltip").style({"top" : my + "px", "left": mx + "px"});
d3.selectAll("#tooltip")
.transition()
.duration(500)
.style("opacity", 1);
node.each(function(n) { n.target = n.source = false; });
currnode = d3.select(this)[0][0].__data__;
link.classed("link--target", function(l) {
if (l.target === d)
{
return l.source.source = true;
}
if (l.source === d)
{
return l.target.target = true;
}
})
.filter(function(l) { return l.target === d || l.source === d; })
.attr("stroke", function(d){
if (d[0].name == currnode.name)
{
return color(d[2].cat);
}
return color(d[0].cat);
})
.each(function() { this.parentNode.appendChild(this); });
d3.selectAll(".link--clicked").each(function() { this.parentNode.appendChild(this); });
node.classed("node--target", function(n) {
return (n.target || n.source);
});
}
I am somewhat new to D3, but I am assuming what I'll need to do is check the group based on the key and then match it to the same color as that group.
My full code is here:
<script type="text/javascript">
color = d3.scale.category10();
var w = 840,
h = 800,
rx = w / 2,
ry = h / 2,
m0,
rotate = 0
pi = Math.PI;
var splines = [];
var cluster = d3.layout.cluster()
.size([360, ry - 180])
.sort(function(a, b) {
return d3.ascending(a.key, b.key);
});
var bundle = d3.layout.bundle();
var line = d3.svg.line.radial()
.interpolate("bundle")
.tension(.5)
.radius(function(d) {
return d.y;
})
.angle(function(d) {
return d.x / 180 * Math.PI;
});
// Chrome 15 bug: <http://code.google.com/p/chromium/issues/detail?id=98951>
var div = d3.select("#bundle")
.style("width", w + "px")
.style("height", w + "px")
.style("position", "absolute");
var svg = div.append("svg:svg")
.attr("width", w)
.attr("height", w)
.append("svg:g")
.attr("transform", "translate(" + rx + "," + ry + ")");
svg.append("svg:path")
.attr("class", "arc")
.attr("d", d3.svg.arc().outerRadius(ry - 180).innerRadius(0).startAngle(0).endAngle(2 * Math.PI))
.on("mousedown", mousedown);
d3.json("TASKS AND PHASES.json", function(classes) {
var nodes = cluster.nodes(packages.root(classes)),
links = packages.imports(nodes),
splines = bundle(links);
var path = svg.selectAll("path.link")
.data(links)
.enter().append("svg:path")
.attr("class", function(d) {
return "link source-" + d.source.key + " target-" + d.target.key;
})
.attr("d", function(d, i) {
return line(splines[i]);
});
var groupData = svg.selectAll("g.group")
.data(nodes.filter(function(d) {
return (d.key == 'Department' || d.key == 'Software' || d.key == 'Tasks' || d.key == 'Phases') && d.children;
}))
.enter().append("group")
.attr("class", "group");
var groupArc = d3.svg.arc()
.innerRadius(ry - 177)
.outerRadius(ry - 157)
.startAngle(function(d) {
return (findStartAngle(d.__data__.children) - 2) * pi / 180;
})
.endAngle(function(d) {
return (findEndAngle(d.__data__.children) + 2) * pi / 180
});
svg.selectAll("g.arc")
.data(groupData[0])
.enter().append("svg:path")
.attr("d", groupArc)
.attr("class", "groupArc")
.attr("id", function(d, i) {console.log(d.__data__.key); return d.__data__.key;})
.style("fill", function(d, i) {return color(i);})
.style("fill-opacity", 0.5)
.each(function(d,i) {
var firstArcSection = /(^.+?)L/;
var newArc = firstArcSection.exec( d3.select(this).attr("d") )[1];
newArc = newArc.replace(/,/g , " ");
svg.append("path")
.attr("class", "hiddenArcs")
.attr("id", "hidden"+d.__data__.key)
.attr("d", newArc)
.style("fill", "none");
});
svg.selectAll(".arcText")
.data(groupData[0])
.enter().append("text")
.attr("class", "arcText")
.attr("dy", 15)
.append("textPath")
.attr("startOffset","50%")
.style("text-anchor","middle")
.attr("xlink:href",function(d,i){return "#hidden" + d.__data__.key;})
.text(function(d){return d.__data__.key;});
svg.selectAll("g.node")
.data(nodes.filter(function(n) {
return !n.children;
}))
.enter().append("svg:g")
.attr("class", "node")
.attr("id", function(d) {
return "node-" + d.key;
})
.attr("transform", function(d) {
return "rotate(" + (d.x - 90) + ")translate(" + d.y + ")";
})
.append("svg:text")
.attr("dx", function(d) {
return d.x < 180 ? 25 : -25;
})
.attr("dy", ".31em")
.attr("text-anchor", function(d) {
return d.x < 180 ? "start" : "end";
})
.attr("transform", function(d) {
return d.x < 180 ? null : "rotate(180)";
})
.text(function(d) {
return d.key.replace(/_/g, ' ');
})
.on("mouseover", mouseover)
.on("mouseout", mouseout);
d3.select("input[type=range]").on("change", function() {
line.tension(this.value / 100);
path.attr("d", function(d, i) {
return line(splines[i]);
});
});
});
d3.select(window)
.on("mousemove", mousemove)
.on("mouseup", mouseup);
function mouse(e) {
return [e.pageX - rx, e.pageY - ry];
}
function mousedown() {
m0 = mouse(d3.event);
d3.event.preventDefault();
}
function mousemove() {
if (m0) {
var m1 = mouse(d3.event),
dm = Math.atan2(cross(m0, m1), dot(m0, m1)) * 180 / Math.PI;
div.style("-webkit-transform", "translate3d(0," + (ry - rx) + "px,0)rotate3d(0,0,0," + dm + "deg)translate3d(0," + (rx - ry) + "px,0)");
}
}
function mouseup() {
if (m0) {
var m1 = mouse(d3.event),
dm = Math.atan2(cross(m0, m1), dot(m0, m1)) * 180 / Math.PI;
rotate += dm;
if (rotate > 360) rotate -= 360;
else if (rotate < 0) rotate += 360;
m0 = null;
div.style("-webkit-transform", "rotate3d(0,0,0,0deg)");
svg.attr("transform", "translate(" + rx + "," + ry + ")rotate(" + rotate + ")")
.selectAll("g.node text")
.attr("dx", function(d) {
return (d.x + rotate) % 360 < 180 ? 25 : -25;
})
.attr("text-anchor", function(d) {
return (d.x + rotate) % 360 < 180 ? "start" : "end";
})
.attr("transform", function(d) {
return (d.x + rotate) % 360 < 180 ? null : "rotate(180)";
});
}
}
function mouseover(d) {
svg.selectAll("path.link.target-" + d.key)
.classed("target", true)
.each(updateNodes("source", true));
svg.selectAll("path.link.source-" + d.key)
.classed("source", true)
.each(updateNodes("target", true));
}
function mouseout(d) {
svg.selectAll("path.link.source-" + d.key)
.classed("source", false)
.each(updateNodes("target", false));
svg.selectAll("path.link.target-" + d.key)
.classed("target", false)
.each(updateNodes("source", false));
}
function updateNodes(name, value) {
return function(d) {
if (value) this.parentNode.appendChild(this);
svg.select("#node-" + d[name].key).classed(name, value);
};
}
function cross(a, b) {
return a[0] * b[1] - a[1] * b[0];
}
function dot(a, b) {
return a[0] * b[0] + a[1] * b[1];
}
function findStartAngle(children) {
var min = children[0].x;
children.forEach(function(d) {
if (d.x < min)
min = d.x;
});
return min;
}
function findEndAngle(children) {
var max = children[0].x;
children.forEach(function(d) {
if (d.x > max)
max = d.x;
});
return max;
}
</script>
Here's an example solution in D3 v6 adapting the Observable example plus my answer to this other question. Basic points:
You will to add the 'group' into the input data - for the data you mention in the comments I've defined group as the 2nd element (per dot separation) of the name. The hierarchy function in the Observable appears to strip this.
It's probably fortunate that all the name values are e.g. root.parent.child - this makes the leafGroups work quite well for your data (but might not for asymmetric hierarchies).
Define a colour range e.g. const colors = d3.scaleOrdinal().domain(leafGroups.map(d => d[0])).range(d3.schemeTableau10); which you can use for arcs, label text (nodes), paths (links)
I've avoided using the mix-blend-mode styling with the example as it doesn't look good to me.
I'm applying the styles in overed and outed - see below for the logic.
See the comments in overed for styling logic on mouseover:
function overed(event, d) {
//link.style("mix-blend-mode", null);
d3.select(this)
// set dark/ bold on hovered node
.style("fill", colordark)
.attr("font-weight", "bold");
d3.selectAll(d.incoming.map(d => d.path))
// each link has data with source and target so you can get group
// and therefore group color; 0 for incoming and 1 for outgoing
.attr("stroke", d => colors(d[0].data.group))
// increase stroke width for emphasis
.attr("stroke-width", 4)
.raise();
d3.selectAll(d.outgoing.map(d => d.path))
// each link has data with source and target so you can get group
// and therefore group color; 0 for incoming and 1 for outgoing
.attr("stroke", d => colors(d[1].data.group))
// increase stroke width for emphasis
.attr("stroke-width", 4)
.raise()
d3.selectAll(d.incoming.map(([d]) => d.text))
// source and target nodes to go dark and bold
.style("fill", colordark)
.attr("font-weight", "bold");
d3.selectAll(d.outgoing.map(([, d]) => d.text))
// source and target nodes to go dark and bold
.style("fill", colordark)
.attr("font-weight", "bold");
}
See the comments in outed for styling logic on mouseout:
function outed(event, d) {
//link.style("mix-blend-mode", "multiply");
d3.select(this)
// hovered node to revert to group colour on mouseout
.style("fill", d => colors(d.data.group))
.attr("font-weight", null);
d3.selectAll(d.incoming.map(d => d.path))
// incoming links to revert to 'colornone' and width 1 on mouseout
.attr("stroke", colornone)
.attr("stroke-width", 1);
d3.selectAll(d.outgoing.map(d => d.path))
// incoming links to revert to 'colornone' and width 1 on mouseout
.attr("stroke", colornone)
.attr("stroke-width", 1);
d3.selectAll(d.incoming.map(([d]) => d.text))
// incoming nodes to revert to group colour on mouseout
.style("fill", d => colors(d.data.group))
.attr("font-weight", null);
d3.selectAll(d.outgoing.map(([, d]) => d.text))
// incoming nodes to revert to group colour on mouseout
.style("fill", d => colors(d.data.group))
.attr("font-weight", null);
}
Working example with the data you mentioned in the comments:
const url = "https://gist.githubusercontent.com/robinmackenzie/5c5d2af4e3db47d9150a2c4ba55b7bcd/raw/9f9c6b92d24bd9f9077b7fc6c4bfc5aebd2787d5/harvard_vis.json";
const colornone = "#ccc";
const colordark = "#222";
const width = 600;
const radius = width / 2;
d3.json(url).then(json => {
// hack in the group name to each object
json.forEach(o => o.group = o.name.split(".")[1]);
// then render
render(json);
});
function render(data) {
const line = d3.lineRadial()
.curve(d3.curveBundle.beta(0.85))
.radius(d => d.y)
.angle(d => d.x);
const tree = d3.cluster()
.size([2 * Math.PI, radius - 100]);
const root = tree(bilink(d3.hierarchy(hierarchy(data))
.sort((a, b) => d3.ascending(a.height, b.height) || d3.ascending(a.data.name, b.data.name))));
const svg = d3.select("body")
.append("svg")
.attr("width", width)
.attr("height", width)
.append("g")
.attr("transform", `translate(${radius},${radius})`);
const arcInnerRadius = radius - 100;
const arcWidth = 20;
const arcOuterRadius = arcInnerRadius + arcWidth;
const arc = d3
.arc()
.innerRadius(arcInnerRadius)
.outerRadius(arcOuterRadius)
.startAngle((d) => d.start)
.endAngle((d) => d.end);
const leafGroups = d3.groups(root.leaves(), d => d.parent.data.name);
const arcAngles = leafGroups.map(g => ({
name: g[0],
start: d3.min(g[1], d => d.x),
end: d3.max(g[1], d => d.x)
}));
const colors = d3.scaleOrdinal().domain(leafGroups.map(d => d[0])).range(d3.schemeTableau10);
svg
.selectAll(".arc")
.data(arcAngles)
.enter()
.append("path")
.attr("id", (d, i) => `arc_${i}`)
.attr("d", (d) => arc({start: d.start, end: d.end}))
.attr("fill", d => colors(d.name))
svg
.selectAll(".arcLabel")
.data(arcAngles)
.enter()
.append("text")
.attr("x", 5)
.attr("dy", (d) => ((arcOuterRadius - arcInnerRadius) * 0.8))
.append("textPath")
.attr("class", "arcLabel")
.attr("xlink:href", (d, i) => `#arc_${i}`)
.text((d, i) => ((d.end - d.start) < (6 * Math.PI / 180)) ? "" : d.name);
// add nodes
const node = svg.append("g")
.attr("font-family", "sans-serif")
.attr("font-size", 10)
.selectAll("g")
.data(root.leaves())
.join("g")
.attr("transform", d => `rotate(${d.x * 180 / Math.PI - 90}) translate(${d.y}, 0)`)
.append("text")
.attr("dy", "0.31em")
.attr("x", d => d.x < Math.PI ? (arcWidth + 5) : (arcWidth + 5) * -1)
.attr("text-anchor", d => d.x < Math.PI ? "start" : "end")
.attr("transform", d => d.x >= Math.PI ? "rotate(180)" : null)
.text(d => d.data.name)
.style("fill", d => colors(d.data.group))
.each(function(d) { d.text = this; })
.on("mouseover", overed)
.on("mouseout", outed)
.call(text => text.append("title").text(d => `${id(d)} ${d.outgoing.length} outgoing ${d.incoming.length} incoming`));
// add edges
const link = svg.append("g")
.attr("stroke", colornone)
.attr("fill", "none")
.selectAll("path")
.data(root.leaves().flatMap(leaf => leaf.outgoing))
.join("path")
//.style("mix-blend-mode", "multiply")
.attr("d", ([i, o]) => line(i.path(o)))
.each(function(d) { d.path = this; });
function overed(event, d) {
//link.style("mix-blend-mode", null);
d3.select(this)
.style("fill", colordark)
.attr("font-weight", "bold");
d3.selectAll(d.incoming.map(d => d.path))
.attr("stroke", d => colors(d[0].data.group))
.attr("stroke-width", 4)
.raise();
d3.selectAll(d.outgoing.map(d => d.path))
.attr("stroke", d => colors(d[1].data.group))
.attr("stroke-width", 4)
.raise()
d3.selectAll(d.incoming.map(([d]) => d.text))
.style("fill", colordark)
.attr("font-weight", "bold");
d3.selectAll(d.outgoing.map(([, d]) => d.text))
.style("fill", colordark)
.attr("font-weight", "bold");
}
function outed(event, d) {
//link.style("mix-blend-mode", "multiply");
d3.select(this)
.style("fill", d => colors(d.data.group))
.attr("font-weight", null);
d3.selectAll(d.incoming.map(d => d.path))
.attr("stroke", colornone)
.attr("stroke-width", 1);
d3.selectAll(d.outgoing.map(d => d.path))
.attr("stroke", colornone)
.attr("stroke-width", 1);
d3.selectAll(d.incoming.map(([d]) => d.text))
.style("fill", d => colors(d.data.group))
.attr("font-weight", null);
d3.selectAll(d.outgoing.map(([, d]) => d.text))
.style("fill", d => colors(d.data.group))
.attr("font-weight", null);
}
function id(node) {
return `${node.parent ? id(node.parent) + "." : ""}${node.data.name}`;
}
function bilink(root) {
const map = new Map(root.leaves().map(d => [id(d), d]));
for (const d of root.leaves()) d.incoming = [], d.outgoing = d.data.imports.map(i => [d, map.get(i)]);
for (const d of root.leaves()) for (const o of d.outgoing) o[1].incoming.push(o);
return root;
}
function hierarchy(data, delimiter = ".") {
let root;
const map = new Map;
data.forEach(function find(data) {
const {name} = data;
if (map.has(name)) return map.get(name);
const i = name.lastIndexOf(delimiter);
map.set(name, data);
if (i >= 0) {
find({name: name.substring(0, i), children: []}).children.push(data);
data.name = name.substring(i + 1);
} else {
root = data;
}
return data;
});
return root;
}
}
.node {
font: 300 11px "Helvetica Neue", Helvetica, Arial, sans-serif;
fill: #fff;
}
.arcLabel {
font: 300 14px "Helvetica Neue", Helvetica, Arial, sans-serif;
fill: #fff;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/6.0.0/d3.min.js"></script>