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`
)
Related
I'd like to replicate these tags with a triangle at the end in D3 to show the last data points.
The way I can think of is to append a triangle at the end of a rect element, but is quite messy since I have to align the triangle with the rectangle position. I was wonder if there was there a better way of doing it?
You can use a path with a fill to get the solid shape you are looking for:
.join("path")
.attr("d", (d, i) => `M ${1} ${yScale(d)} l 6,-6 h 24 v 12 h -24 l -6,-6`)
.attr("fill", (d, i) => d3.schemeTableau10[i])
This is simpler than drawing a rect and then bolting on the little triangle. See below:
const width = 200;
const height = 180;
const svg = d3.select("body").append("svg").attr("width", width).attr("height", height);
const yScale = d3.scaleLinear().domain([0, 1]).range([160, 10]);
const yAxis = d3.axisLeft(yScale);
const yAxisG = svg.append("g").attr("transform", `translate(60, 10)`).call(yAxis);
const markerY = [0.25, 0.45, 0.65];
const markers = yAxisG.selectAll(".marker")
.data(markerY)
.join("path")
.attr("d", (d, i) => `M ${1} ${yScale(d)} l 6,-6 h 24 v 12 h -24 l -6,-6`)
.attr("fill", (d, i) => d3.schemeTableau10[i])
const labels = yAxisG.selectAll(".label")
.data(markerY)
.join("text")
.text(d => d)
.attr("x", 0)
.attr("y", d => yScale(d))
.attr("dx", 8)
.attr("dy", 4)
.attr("text-anchor", "start")
.attr("fill", "#000");
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.6.1/d3.min.js"></script>
My issue is that the animation works fine when the images have no content to them as below, but once they are loaded I get the following error: Error: <image> attribute x: Expected length, "NaN".
Here is a code snippet:
var data = ['https://cdn.britannica.com/60/8160-050-08CCEABC/German-shepherd.jpg', 'https://cdn.britannica.com/60/8160-050-08CCEABC/German-shepherd.jpg', 'https://cdn.britannica.com/60/8160-050-08CCEABC/German-shepherd.jpg', 'https://cdn.britannica.com/60/8160-050-08CCEABC/German-shepherd.jpg', 'https://cdn.britannica.com/60/8160-050-08CCEABC/German-shepherd.jpg'];
var w = window,
d = document,
e = d.documentElement,
g = d.getElementsByTagName('body')[0],
wid = 400
y = 400;
var svg = d3.select("body").append("svg")
.attr("width", wid)
.attr("height", "400")
.on('mousemove', () => {
let x = event.x - 20;
d3.selectAll('.content')
.attr('x', (d, i) => fisheye(d, x))
})
.on('mouseleave', () => {
d3.selectAll('.content').transition().attr(
'x', (d, i) => xScale(i))
})
var chart = svg.append('g')
.classed('group', true)
let xScale = d3.scaleBand().domain(d3.range(5)).range([0, wid]).padding(0)
let rects = svg.selectAll('content')
.data(
d3.range(5)
//data //(uncomment this, and comment line above to try loading images)
)
rects.exit().remove();
rects.enter()
.append("svg:image")
.attr("xlink:href", d => d)
.attr("class", "content")
.attr("y", 0)
.attr("x", (d, i) => xScale(i))
.attr("width", "300px")
.style("opacity", 1)
.attr("stroke", "white")
.style('fill', 'rgb(81, 170, 232)')
.attr("height", 400);
let distortion = 10;
function fisheye(_, a) {
let x = xScale(_),
left = x < a,
range = d3.extent(xScale.range()),
min = range[0],
max = range[1],
m = left ? a - min : max - a;
if (m === 0) m = max - min;
return (left ? -1 : 1) * m * (distortion + 1) / (distortion + (m / Math.abs(x - a))) + a;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
I'm trying to make something that looks like this:
https://www.nytimes.com/newsgraphics/2013/09/13/fashion-week-editors-picks/index.html
I would really appreciate some help on getting that snippet to work and if possible as smoothly as this website link! That was made by the Bosstock himself and I'm a d3 newbie so I'm quite out of my depth.
I figured out my issue-
let xScale = d3.scaleBand().domain(data).range([0,wid]).padding(0)
The domain was d3.range(5) rather than my data variable.
I am working on the d3 treemap v5 in which I need to persist the state of the treemap in localstorage on each user click. My code is in https://codesandbox.io/s/d3-treemap-wfbtg
When a user clicks the top parent tile it is drilled down to the children tiles. How to persist that in local storage when the user reloads the browser he wants to see the drilled down children tiles.
class Treegraph extends React.Component {
createTreeChart = () => {
const width = 550;
const height = 500;
var paddingAllowance = 2;
const format = d3.format(",d");
const checkLowVal = d => {
console.log("ChecklowVal", d);
if (d.value < 2) {
return true;
}
};
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", "16px sans-serif");
const x = d3.scaleLinear().rangeRound([0, width]);
const y = d3.scaleLinear().rangeRound([0, height]);
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)));
var tool = d3
.select("body")
.append("div")
.attr("class", "toolTip");
d3.select(window.frameElement).style("height", height - 20 + "px");
d3.select(window.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.name}<br />(${format(d.data.value)})`);
})
.on("click", function(d) {
tool.style("display", "none");
})
.on("mouseout", function(d) {
tool.style("display", "none");
});
node
.append("foreignObject")
.attr("class", "foreignObject")
.attr("width", function(d) {
return d.dx - paddingAllowance;
})
.attr("height", function(d) {
return d.dy - paddingAllowance;
})
.append("xhtml:body")
.attr("class", "labelbody")
.append("div")
.attr("class", "label")
.text(function(d) {
return d.name;
})
.attr("text-anchor", "middle");
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)
: checkLowVal(d)
? d.data.name.split(/(\s+)/).concat(format(d.data.value))
: d.data.name.split(/(\s+)/).concat(format(d.data.value))
)
.join("tspan")
.attr("x", 3)
.attr(
"y",
(d, i, nodes) =>
`${(i === nodes.length - 1) * 0.3 + (i - nodes.length / 2) * 0.9}em`
)
.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) {
console.log("The zoomin func", d.data);
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) {
console.log("The zoomout func", d.parent.data);
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>
);
}
}
Here is a quick idea, store the name as ID on local storage. Hopefully name is unique, otherwise make sure you have an unique ID for each node.
https://codesandbox.io/s/d3-treemap-4ehbf?file=/src/treegraph.js
first load localstorage for lastSelection with last selected node name
componentDidMount() {
const lastSelection = localStorage.getItem("lastSelection");
console.log("lastSelection:", lastSelection);
this.createTreeChart(lastSelection);
}
add a parameter to you createTreeChart so when it loads and there is a lastaSelection you have to reload that state
createTreeChart = (lastSelection = null) => {
decouple your treemap in a variable, filter the lastSelection node and zoomin into it. this step could be improved, not sure if the refreshing zoomin is interesting.
const tree = treemap(data);
let group = svg.append("g").call(render, tree);
if (lastSelection !== null) {
const lastNode = tree
.descendants()
.find(e => e.data.name === lastSelection);
zoomin(lastNode);
}
finally update localStorage on node click.
node
.filter(d => (d === root ? d.parent : d.children))
.attr("cursor", "pointer")
.on("click", d => {
if (d === root) {
localStorage.setItem("lastSelection", null);
zoomout(root);
} else {
localStorage.setItem("lastSelection", d.data.name);
zoomin(d);
}
});
This could be improved, but it's a starting point.
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 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>