Add image to D3.js Sunburst Example - javascript

I would like to extend this D3.js sunburst example in the way that it can display images rather than text. http://www.jasondavies.com/coffee-wheel/
The interesting part of the js example code is following:
d3.json("wheel.json", function(error, json) {
var nodes = partition.nodes({children: json});
var path = vis.selectAll("path").data(nodes);
path.enter().append("path")
.attr("id", function(d, i) { return "path-" + i; })
.attr("d", arc)
.attr("fill-rule", "evenodd")
.style("fill", colour)
.on("click", click);
var text = vis.selectAll("text").data(nodes);
var textEnter = text.enter().append("text")
.style("fill-opacity", 1)
.style("fill", function(d) {
return brightness(d3.rgb(colour(d))) < 125 ? "#eee" : "#000";
})
.attr("text-anchor", function(d) {
return x(d.x + d.dx / 2) > Math.PI ? "end" : "start";
})
.attr("dy", ".2em")
.attr("transform", function(d) {
var multiline = (d.name || "").split(" ").length > 1,
angle = x(d.x + d.dx / 2) * 180 / Math.PI - 90,
rotate = angle + (multiline ? -.5 : 0);
return "rotate(" + rotate + ")translate(" + (y(d.y) + padding) + ")rotate(" + (angle > 90 ? -180 : 0) + ")";
})
.on("click", click);
textEnter.append("tspan")
.attr("x", 0)
.text(function(d) { return d.depth ? d.name.split(" ")[0] : ""; });
textEnter.append("tspan")
.attr("x", 0)
.attr("dy", "1em")
.text(function(d) { return d.depth ? d.name.split(" ")[1] || "" : ""; });
The question is how I can replace the text with an image in this scenario.
Thank you very much.

var image = vis.selectAll("image").data(nodes);
var imageEnter = image.enter().append("image")
.attr("xlink:href", "https://github.com/favicon.ico")
.attr("width", 16)
.attr("height", 16)
.attr("x", function (d) {
return 33; // calculate x
}).attr("y", function (d) {
return 33; // calculate y
});
https://developer.mozilla.org/en-US/docs/Web/SVG/Element/image
force-directed images and labels: http://bl.ocks.org/mbostock/950642

Related

D3 wrapping text legends

I am using legends for d3 charts. Below is code -
var padding = { left:50, right:100, top:30, bottom:30 };
var legend = svgColorCode.selectAll(".legend")
.data(keyText) //.filter(x=>x!="Completed On Time")
.enter().append("g")
.attr("class", "legend")
.attr("transform", function (d, i) { return "translate(" + (((keys.length - (i)) * -25) + (i == 0 ? 0 : (i == 1 ? 60 : (i == 2 ? 90 : (i == 3 ? 100 : 0))))) + "," + (height - 190) + ")"; })
.attr("fill", function (d, i) { return colors[i]; });
legend.append("rect")
.attr("x", (x, i) => (padding.top * 2 + labHeight * (i)) + 40)
.attr("width", 18)
.attr("height", 18)
.style("fill", function (d, i) { return colors[operationalKeys[i]]; })
legend.append("text")
.attr("x", (x, i) => (padding.top * 2 + labHeight * i) + 60)
.attr("y", 9)
.attr("font-size", "0.65rem")
.attr("dy", ".35em")
.style("text-anchor", "start")
.text(function (d) { console.log("d");console.log(d); return d.replace("&nbsp","\n"); })
.call(wrap)
;
Here I have long texts for legends and small place to accomodate it. I want to wrap the text for legends.
For that I followed this SO answer and used wrap method. But seems wrap is not working in this case.
How can I wrap my text for legends in d3 js?

How to display parent elements in a nested treemap?

This is my data : https://api.myjson.com/bins/b0m6s
I want to create a nested treemap that shows the parent element and the child elements inside it, like this example : https://bl.ocks.org/mbostock/911ad09bdead40ec0061
Here is my treemap code currently :
var defaultColors = d3.scale.ordinal().range(["#00AEEF", "#8DC63F", "#FFC20E", "#F06EAA", "#AE9986", "#009BA5", "#00A651", "#F7941D", "#B656AB", "#ABA000", "#F3716D", "#8D7B6B", "#EF413D", "#AD772B", "#878787"]);
var treemap;
var h_pad = 2, // 2 pixels vertical padding
v_pad = 4; // 4 pixels of horizontal padding (2 px at each side)
var canvas = d3
.select(id)
.append("svg")
.attr("class", "chart")
.attr("width", cfg.width + cfg.margin.left + cfg.margin.right)
.attr("height", cfg.height + cfg.margin.top + cfg.margin.bottom)
.attr("viewBox", "0 0 960 500")
.attr("preserveAspectRatio", "xMidYMid meet")
.attr("id", "canvas")
var innercanvas = canvas
.append("g")
.attr("class", "innercanvas")
.attr("transform", "translate(" + cfg.margin.left + "," + cfg.margin.top + ")");
treemap = d3.layout
.treemap()
.round(false)
.size([cfg.width, cfg.height])
.padding(.25)
.sticky(true)
.nodes(data);
var cells = innercanvas
.selectAll(".newcell")
.data(treemap)
.enter()
.append("g")
.attr("class", function (d, i) {
return 'newcell _' + i // i provides a unique identifier for each node
+ ' cell-level-' + d.depth // cell-level-0 for root, cell-level-1, cell-level-2, etc
+ (d.name ? ' ' + safe_name(d.name) : '') // if d.name exists, use the 'safe' version
+ (!d.children
? ' leaf' // d has no children => it's a leaf node
: (d.depth === 0
? ' root' // d.depth = 0 => it's the root node
: ' internal ')); // has children, depth > 0 => internal node
})
cells
.append("rect")
.attr("x", function (d) {
return d.x;
})
.attr("y", function (d) {
return d.y;
})
.attr("id", "rectangle")
.attr("width", function (d) {
return d.dx;
})
.attr("height", function (d) {
return d.dy;
})
.style("fill", function (d) {
return d.children && d.parent ? defaultColors(d.name) : cfg.color ? cfg.color(d.name) : null;
})
.attr("stroke", "#000000")
.attr('pointer-events', 'all');
cells
.append("text")
.attr("x", function (d) {
return d.x + d.dx / 2;
})
.attr("y", function (d) {
return d.y + d.dy / 2;
})
.attr("text-anchor", "middle")
.text(function (d) { return d.parent ? d.name : '' })
cells
.append('title')
.text(function (d) {
if (d.parent) {
return categoryKey + " : " + d.parent.name + "\n" + groupKey + " : " + d.name + "\n" + sizeKey + " : " + toCommas(d.value.toFixed(2))
}
return d.name;
});
Wrapping the texts into multiple lines
function groupAddText(selection) {
var v_pad = 2, // vertical padding
h_pad = 4 // horizontal padding
selection.selectAll('.leaf text')
.classed('groupOversize', function (d) {
if (!d.name) {
return false;
}
var bbox = this.getBBox();
if (d.dx <= bbox.width + h_pad || d.dy <= bbox.height + v_pad) {
d3.select(this).node().textContent = "";
var lines = wordwrap2(d.name, d.dx).split('\n');
for (var i = 0; i < lines.length; i++) {
d3.select(this)
.append("tspan")
.attr("dy", 15)
.attr("x", d.x + d.dx / 2)
.text(lines[i]);
}
d3.selectAll(".groupOversize").attr("y", function (d) {
return (d.y + d.dy / 2) - 20;
})
return true;
}
return false;
});
}
function wordwrap2(str, width, brk, cut) {
brk = brk || '\n';
width = width || 75;
cut = cut || false;
if (!str) { return str; }
var regex = '.{1,' + width + '}(\\s|$)' + (cut ? '|.{' + width + '}|.+$' : '|\\S+?(\\s|$)');
return str.match(RegExp(regex, 'g')).join(brk);
}
This produces the following treemap visualization :
As you can see, I separated my cells into 3 classes, root for the root node, internal for the parents and leaf for the children. Right now, it is just showing the children rects. How do I show the parent elements with the children nested inside them?
I want something like this :
[![enter image description here][2]][2]
You can view the parent cells by adding padding to your treemap:
treemap = d3.layout
.treemap()
.round(false)
.size([cfg.width, cfg.height])
.padding(20) // 20px padding all around
.sticky(true)
.nodes(data);
or
treemap = d3.layout
.treemap()
.round(false)
.size([cfg.width, cfg.height])
.padding([20,5,5,5]) // 20px top, 5px sides and bottom
.sticky(true)
.nodes(data);
I made a little demo showing the effects of altering the padding in a d3 treemap here - although note that that is d3 v5, so the options are slightly different.
Here's a demo with your code:
var data = {"children":[{"name":"Central","children":[{"name":"Cellophane Tape","value":"419141.4728"},{"name":"File Separator","value":"327285.0157"},{"name":"Hard Cover File","value":"422707.1194"},{"name":"Highlighter","value":"488978.5362"},{"name":"Office Chair","value":"453843.621"},{"name":"Pencil","value":"416819.1027"},{"name":"Tape Dispenser","value":"393290.5862"},{"name":"File Cabinet","value":"424647.6003"},{"name":"Plastic Comb Binding","value":"230299.6657"},{"name":"White Board Markers","value":"383157.5055"},{"name":"Binder","value":"415871.6793"},{"name":"Eraser","value":"477885.9162"},{"name":"Pen","value":"444834.4362"},{"name":"Pen Set","value":"434495.1303"},{"name":"Desk","value":"247046.3919"}]},{"name":"East","children":[{"name":"Pencil","value":"441970.1055"},{"name":"White Board Markers","value":"416822.5561"},{"name":"Eraser","value":"393738.4951"},{"name":"Hard Cover File","value":"407371.1911"},{"name":"Office Chair","value":"382574.6347"},{"name":"Tape Dispenser","value":"481960.7562"},{"name":"Cellophane Tape","value":"441438.7362"},{"name":"File Cabinet","value":"333187.8858"},{"name":"Binder","value":"462926.3793"},{"name":"File Separator","value":"441311.7555"},{"name":"Plastic Comb Binding","value":"330059.7762"},{"name":"Highlighter","value":"399332.0562"},{"name":"Pen","value":"492374.2362"},{"name":"Pen Set","value":"477206.7762"},{"name":"Desk","value":"254464.9453"}]},{"name":"North","children":[{"name":"Office Chair","value":"459306.6555"},{"name":"Pencil","value":"465763.0477"},{"name":"Eraser","value":"441687.1652"},{"name":"File Cabinet","value":"463598.5893"},{"name":"File Separator","value":"430346.1162"},
{"name":"Hard Cover File","value":"346325.0175"},{"name":"Highlighter","value":"223199.4072"},{"name":"Tape Dispenser","value":"311201.7216"},{"name":"Plastic Comb Binding","value":"445513.5762"},{"name":"Binder","value":"453219.921"},{"name":"White Board Markers","value":"334737.9189"},{"name":"Cellophane Tape","value":"372554.952"},{"name":"Pen","value":"435830.2872"},{"name":"Pen Set","value":"460001.8962"},{"name":"Desk","value":"260294.2303"}]},{"name":"South","children":[{"name":"Pencil","value":"457331.6055"},{"name":"Tape Dispenser","value":"442628.4555"},{"name":"Cellophane Tape","value":"468037.3351"},{"name":"Eraser","value":"341469.2127"},{"name":"File Cabinet","value":"408198.2058"},{"name":"File Separator","value":"416543.8893"},{"name":"Office Chair","value":"466438.7227"},{"name":"Plastic Comb Binding","value":"436440.1272"},{"name":"White Board Markers","value":"437968.1344"},{"name":"Highlighter","value":"411905.4555"},{"name":"Binder","value":"456806.1151"},{"name":"Hard Cover File","value":"493053.3762"},{"name":"Pen","value":"413820.3762"},{"name":"Pen Set","value":"488299.3962"},{"name":"Desk","value":"264499.5623"}]},{"name":"West","children":[{"name":"Pencil","value":"458648.3055"},{"name":"Cellophane Tape","value":"299045.7162"},{"name":"File Cabinet","value":"386045.352"},{"name":"File Separator","value":"435098.0403"},{"name":"Highlighter","value":"457454.0701"},{"name":"Office Chair","value":"262021.1055"},{"name":"Plastic Comb Binding","value":"413222.1555"},{"name":"Eraser","value":"449997.2978"},{"name":"Hard Cover File","value":"364335.5793"},{"name":"Binder","value":"467389.3801"},{"name":"Tape Dispenser","value":"394066.5845"},{"name":"White Board Markers","value":"408833.4789"},{"name":"Pen","value":"481281.6162"},{"name":"Pen Set","value":"398652.9162"},{"name":"Desk","value":"229482.2954"}]}]};
data.name = 'root'
var defaultColors = d3.scale.ordinal().range(["#00AEEF", "#8DC63F", "#FFC20E", "#F06EAA", "#AE9986", "#009BA5", "#00A651", "#F7941D", "#B656AB", "#ABA000", "#F3716D", "#8D7B6B", "#EF413D", "#AD772B", "#878787"]);
var treemap;
var h_pad = 2, // 2 pixels vertical padding
v_pad = 4; // 4 pixels of horizontal padding (2 px at each side)
var id = 'treemap';
var cfg = { width: 960, height: 500, margin: { left: 10, right: 10, bottom: 10, top: 10 }, color: d3.scale.category20() }
var canvas = d3
.select('#' + id)
.append("svg")
.attr("class", "chart")
.attr("width", cfg.width + cfg.margin.left + cfg.margin.right)
.attr("height", cfg.height + cfg.margin.top + cfg.margin.bottom)
.attr("viewBox", "0 0 960 500")
.attr("preserveAspectRatio", "xMidYMid meet")
.attr("id", "canvas")
var innercanvas = canvas
.append("g")
.attr("class", "innercanvas")
.attr("transform", "translate(" + cfg.margin.left + "," + cfg.margin.top + ")");
treemap = d3.layout
.treemap()
.round(false)
.size([cfg.width, cfg.height])
.padding([20,5,5,5])
.sticky(true)
.nodes(data);
var cells = innercanvas
.selectAll(".newcell")
.data(treemap)
.enter()
.append("g")
.attr("class", function (d, i) {
return 'newcell _' + i // i provides a unique identifier for each node
+ ' cell-level-' + d.depth // cell-level-0 for root, cell-level-1, cell-level-2, etc
+ (d.name ? ' ' + d.name : '') // if d.name exists, use the 'safe' version
+ (!d.children
? ' leaf' // d has no children => it's a leaf node
: (d.depth === 0
? ' root' // d.depth = 0 => it's the root node
: ' internal ')); // has children, depth > 0 => internal node
})
cells
.append("rect")
.attr("x", function (d) {
return d.x;
})
.attr("y", function (d) {
return d.y;
})
.attr("id", function(d,i){ return 'rect_' + i; })
.attr("width", function (d) {
return d.dx;
})
.attr("height", function (d) {
return d.dy;
})
.style("fill", function (d) {
return d.children && d.parent ? defaultColors(d.name) : cfg.color ? cfg.color(d.name) : null;
})
.attr("stroke", "#000000")
.attr('pointer-events', 'all');
cells.append("clipPath")
.attr("id", function(d,i) { return "clip_" + i ; })
.append("use")
.attr("xlink:href", function(d,i) {
return "#rect_" + i;
});
cells
.append("text")
.attr("clip-path", function(d,i) { return "url(#clip_" + i })
.attr("x", function (d) {
return d.x + d.dx / 2;
})
.attr("y", function (d) {
return d.children ? d.y + 12 : d.y + d.dy / 2 ;
})
.attr("text-anchor", "middle")
.text(function (d) { return d.name })
cells
.append('title')
.text(function (d) {
if (d.parent) {
return "categoryKey : " + d.parent.name + "\ngroupKey : " + d.name + "\nsizeKey : " + d.value.toFixed(2)
}
return d.name;
});
svg text {
font-size: 10px;
}
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.17/d3.js"></script><div id="treemap"></div>

D3.js hierarchical edge bundling coloring by group

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>

How can I alter the zooming capability on a d3 sunburst chart?

I am trying to alter the traditional zooming feature on a sunburst chart. Traditionally when you click on a partition, that partition grows to cover 100% of the base layer while all other partitions on the same layer disappear. The children of the selected partition all grow to fill the newly created space.
My current code does just what I stated above. I would like to alter my code to allow for the selected partition to only take up 75% of the base layer. The children elements will grow to cover this new space but the remaining 25% will still contain all other non-selected partitions.
I have tried altering the 't' value that is returned from d3.interpolate() but I have had unpredictable results.
I hope my description is clear.
Does anyone have any thoughts on this?
<script>
var width = 960,
height = 700,
radius = Math.min(width, height) / 2;
var x = d3.scale.linear()
.range([0, 2 * Math.PI]);
var y = d3.scale.linear()
.range([0, radius]);
var color = d3.scale.category20c();
function percent(d) {
var percentage = (d.value / 956129) * 100;
return percentage.toFixed(2);
}
var tip = d3.tip()
.attr('class', 'd3-tip')
.offset([-10, 0])
.html(function(d) {
return "<strong>" + d.name + "</strong> <span style='color:red'>" + percent(d) + "%</span>";
})
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + width / 2 + "," + (height / 2 + 10) + ")");
svg.call(tip);
var partition = d3.layout.partition()
.value(function(d) { return d.size; });
var arc = d3.svg.arc()
.startAngle(function(d) { return Math.max(0, Math.min(2 * Math.PI, x(d.x))); })
.endAngle(function(d) { return Math.max(0, Math.min(2 * Math.PI, x(d.x + d.dx))); })
.innerRadius(function(d) { return Math.max(0, y(d.y)) })
.outerRadius(function(d) { return Math.max(0, y(d.y + d.dy)) });
d3.json("flare.json", function(error, root) {
var g = svg.selectAll("g")
.data(partition.nodes(root))
.enter().append("g");
var path = g.append("path")
.attr("d", arc)
// .attr("stroke", 'black')
// .style("fill", function(d) { return color((d.children ? d : d.parent).name); })
.style("fill", function(d, i) {
return color(i);
})
.on("click", click)
.on('mouseover', tip.show)
.on('mouseout', tip.hide);
var text = g.append("text")
.attr("transform", function(d) { return "rotate(" + computeTextRotation(d) + ")"; })
.attr("x", function(d) { return y(d.y); })
.attr("dx", "6") // margin
.attr("dy", ".35em") // vertical-align
.text(function(d) {
if (percent(d) > 1.35) {
return d.name;
}
})
.attr('font-size', function(d) {
if (d.value < 100000) {
return '10px'
} else {
return '20px';
}
})
.on("click", click)
.on('mouseover', tip.show)
.on('mouseout', tip.hide);
function click(d) {
console.log(d)
// fade out all text elements
text.transition().attr("opacity", 0);
path
.transition()
.duration(750)
.attrTween("d", arcTween(d))
.each("end", function(e, i) {
// check if the animated element's data e lies within the visible angle span given in d
if (e.x >= d.x && e.x < (d.x + d.dx)) {
// get a selection of the associated text element
var arcText = d3.select(this.parentNode).select("text");
// fade in the text element and recalculate positions
arcText.transition().duration(750)
.attr("opacity", 1)
.attr("transform", function() { return "rotate(" + computeTextRotation(e) + ")" })
.attr("x", function(d) { return y(d.y); });
}
});
}
});
d3.select(self.frameElement).style("height", height + "px");
// Interpolate the scales!
function arcTween(d) {
console.log(d.name, x.domain())
console.log(d.name, y.domain())
console.log(d.name, y.range())
var xd = d3.interpolate(x.domain(), [d.x, d.x + d.dx]),
yd = d3.interpolate(y.domain(), [d.y, 1]),
yr = d3.interpolate(y.range(), [d.y ? 20 : 0, radius]);
return function(d, i) {
return i
? function(t) { return arc(d); }
: function(t) {
console.log(t)
x.domain(xd(t));
y.domain(yd(t)).range(yr(t));
return arc(d);
};
};
}
function computeTextRotation(d) {
return (x(d.x + d.dx / 2) - Math.PI / 2) / Math.PI * 180;
}
I found the solution here: https://bl.ocks.org/mbostock/1306365. This example manages the zoom without getting rid of the sibling nodes.

Labelling multiple edges in d3.js

I would like to label multiple edges between two nodes in a d3 force diagram. By modifying this example (http://jsfiddle.net/7HZcR/3/) I have two edges displayed separately but the labels associated to the edges overlapping on one edge rather than sitting on their own line. I can't work out why that would be. Any ideas how I could fix this? Here is the relevant part of my code:
d3.csv("GHT_1_A.csv", function(error, links) {
//sort links by source, then target
links.sort(function(a,b) {
if (a.source > b.source) {return 1;}
else if (a.source < b.source) {return -1;}
else {
if (a.target > b.target) {return 1;}
if (a.target < b.target) {return -1;}
else {return 0;}
}
});
//any links with duplicate source and target get an incremented 'linknum'
for (var i=0; i<links.length; i++) {
if (i != 0 &&
links[i].source == links[i-1].source &&
links[i].target == links[i-1].target) {
links[i].linknum = links[i-1].linknum + 1;
}
else {links[i].linknum = 1;};
};
// Compute the distinct nodes from the links.
links.forEach(function(link) {
link.source = nodes[link.source] ||
(nodes[link.source] = {name: link.source});
link.target = nodes[link.target] ||
(nodes[link.target] = {name: link.target});
});
force = d3.layout.force()
.nodes(d3.values(nodes))
.links(links)
.size([width, height])
.linkDistance(200)
.charge(-1000)
.on("tick", tick)
.start();
svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
// build the arrow.
svg.append("svg:defs").selectAll("marker")
.data(["end"]) // Different link/path types can be defined here
.enter().append("svg:marker") // This section adds in the arrows
.attr("id", String)
.attr("viewBox", "0 -5 10 10")
.attr("refX", 15)
.attr("refY", -1.5)
.attr("markerWidth", 6)
.attr("markerHeight", 6)
.attr("orient", "auto")
.append("svg:path")
.attr("d", "M0,-5L10,0L0,5");
var link = svg.append("svg:g").selectAll("g.link")
.data(force.links())
.enter().append('g')
var linkPath = link.append("svg:path")
.attr('class', 'link')
.attr("marker-end", "url(#end)");
var textPath = link.append("svg:path")
.attr("id", function(d) { return d.source.index + "_" + d.target.index; })
.attr("class", "textpath");
var path_label = svg.append("svg:g").selectAll(".path_label")
.data(force.links())
.enter().append("svg:text")
.attr("class", "path_label")
.append("svg:textPath")
.attr("startOffset", "50%")
.attr("text-anchor", "middle")
.attr("xlink:href", function(d) { return "#" + d.source.index + "_" + d.target.index; })
.style("fill", "#000")
.style("font-family", "Arial")
.text(function(d) { return d.linkQ; });
function arcPath(leftHand, d) {
var start = leftHand ? d.source : d.target,
end = leftHand ? d.target : d.source,
dx = end.x - start.x,
dy = end.y - start.y,
dr = 100/d.linknum;
//dr = Math.sqrt(dx * dx + dy * dy),
sweep = leftHand ? 0 : 1;
return "M" + start.x + "," + start.y + "A" + dr + "," + dr + " 0 0," + sweep + " " + end.x + "," + end.y;
}
var circle = svg.append("svg:g").selectAll("circle")
.data(force.nodes())
.enter().append("svg:circle")
.attr("r", 6)
.call(force.drag);
var text = svg.append("svg:g").selectAll("g")
.data(force.nodes())
.enter().append("svg:g");
// A copy of the text with a thick white stroke for legibility.
text.append("svg:text")
.attr("x", 8)
.attr("y", ".31em")
.attr("class", "shadow")
.text(function(d) { return d.name; });
text.append("svg:text")
.attr("x", 8)
.attr("y", ".31em")
.text(function(d) { return d.name; });
// Use elliptical arc path segments to doubly-encode directionality.
function tick() {
linkPath.attr("d", function(d) {
return arcPath(false, d);
});
textPath.attr("d", function(d) {
return arcPath(d.source.x < d.target.x, d);
});
circle.attr("transform", function(d) {
return "translate(" + d.x + "," + d.y + ")";
});
text.attr("transform", function(d) {
return "translate(" + d.x + "," + d.y + ")";
});
};
});

Categories

Resources