Published
Edited
Apr 16, 2021
1 star
Insert cell
md`# Sankey plus Nodes Francisco Cunha & Bruno Azevedo`
Insert cell
Insert cell
Insert cell
/*
1) Navigation plus Browse (Nice to Have)
2) 3 Tooltips + HTML Buttons (see prototype) - Check
3) Readjust the Color Palette - Check
4) Change recommenders name: Right position - Check
*/

chart = {
const svg = d3.select(DOM.svg(width, height))
.style("width", "100%")
.style("height", "auto")
.style("padding", "8px")
.style("box-sizing", "border-box")
.style("font", "10px sans-serif")
const {nodes, links} = sankey(data);
const g = svg.append("g");
const link = g.append("g")
.attr("fill", "none")
.attr("stroke-opacity", 0.5)
.selectAll("g")
.data(links)
.join("g")
var node = g.append("g").selectAll(".node")
.data(nodes)
.join("g")
.attr("class", d => d.type == "person" ? "node circle": "node rect")


g.selectAll(".node.rect").append("rect")
.attr("x", d => d.x0)
.attr("y", d => (d.y1 + d.y0) / 2 - 15)
.attr("height", d => 30)
.attr("width", d => d.x0 > width/2 ? 0 : 30)
.attr("fill", "#be005b")
//.attr("fill", d => d.type == "person" ? color(d.name) : d.colour)

var fo = node.append("foreignObject")
.attr("x",function(d){
if(d.depth == 1){
return d.x0 - 150
}})
.attr("y", d => ((d.y1 + d.y0) / 2) -100)
.attr("height", 250)
.attr("width", function (d) {
if (d.type == "article" && d.x0 < width/2){
return 359
}
else{
return 0
}})
var div = fo.append("xhtml:div")
.style('position', 'absolute')
.style('width', '300px')
.style('height', '225px')
.style('padding', '10px')
.style('background', 'rgba(255,255,255)')
.style('border-radius', '4px')
.style('color', 'black')
.style("box-shadow", "5px 5px 10px grey")
.html(function (d) {
return "<h2 style=margin:0>Title</h2><hr style=padding:0.5rem;margin:0><p>"+d.name+"</p><h3 style=color:#be005b;padding-top:5%>DOI</h3>"+ d.DOI+"<hr style=padding:0.5rem;margin:0><h3 style=color:#be005b;padding-top:5%>Authors</h3>" + d.authors + "<hr style=padding:0.5rem;margin:0><h3 style=color:#be005b;padding-top:5%>Year</h3>" + d.year
})
.on("mouseover", function(d) {
var index = d.index
d.sourceLinks.forEach(function(d) {
var id = d.target.id
filterOver3(id,index)
})
d3.select(this).style("cursor", "pointer")
})
.on('mouseout', function (d,nodes) {
filterOut()
});;
g.selectAll(".node.circle").append("circle")
.attr("cx", d => (d.x1 + d.x0)/2 + 72.5)
.attr("cy", d => (d.y1 + d.y0)/2)
.attr("r", d => 50)
.attr("fill", d => d.colour)
.style("fill", d =>`url(#pattern${d.id})`)
.on("mouseover", function(d) {
d3.select(this).attr("r",60)
d3.selectAll("#text" + d.id).attr("font-weight", 600).attr("font-size", 35)
var id = d.id
var image = "<img src= https://static.observableusercontent.com/files/5546d2446f6cb2f1c0bedddb1c5f5412f1aa0f48cfbcab99dcc7db4b733c6d63ea15462328579a5224bad0952482823c07e5fa907f763eecd012329b686c9b5c />"
d.targetLinks.forEach(function(d) {
var index = d.source.index
filterOver2(index,id)
})
d3.select(this).style("cursor", "pointer")
tooltip
.html("<p style=padding:0;margin:0>"+d.name+"</p>" + "<hr>"+ image +"<h3 style=color:#be005b;padding-top:5%;white-space:nowrap >Profile</h3><p>"+ d.profile)
.style('visibility', 'visible')
})
.on('mousemove', function (d) {
tooltip
.style('top', d3.event.clientY + 10 + 'px')
.style('left', d3.event.clientX + 30 + 'px')
.style("display", "inline-block");
})
.on('mouseout', function (d,nodes) {
filterOut()
d3.selectAll("#text" + d.id).attr("font-weight", 600).attr("font-size", 30)
d3.select(this).attr("r",50)
tooltip.html(``).style('visibility', 'hidden');
});



const gradient = link.append("linearGradient")
.attr("id", d => (d.uid = DOM.uid("link")).id)
.attr("gradientUnits", "userSpaceOnUse")
.attr("x1", d => d.source.x1)
.attr("x2", d => d.target.x0);

gradient.append("stop")
.attr("offset", "0%")
.attr("stop-color", d => d.source.colour);

gradient.append("stop")
.attr("offset", "100%")
.attr("stop-color", d => d.target.colour);

/* gradient.append("animate")
.attr("attributeName","x1")
.attr("values","0%; 50%") //let x1 run to 200% instead of 100%
.attr("dur","3s")
.attr("repeatCount","indefinite");

gradient.append("animate")
.attr("attributeName","x2")
.attr("values","50%; 100%") //let x2 run to 300% instead of 200%
.attr("dur","3s")
.attr("repeatCount","indefinite");*/

var linking = d3.linkHorizontal()
.x(function(d) {
return d.x1
})
.y(function(d) {
return ((d.y1 + d.y0) / 2);
})
link.append("path")
.attr("d", d => linking(d))
.attr("id", d => d.target.depth == 2 ? "path" + d.target.id : "path" + d.target.index)
.attr("class", d => d.target.depth == 1? "path" + d.target.index : "path" + d.source.index)
.attr("opacity", 0)
/* .attr("stroke", d => edgeColor === "none" ? "#aaa"
: edgeColor === "path" ? d.uid
: edgeColor === "input" ? color(d.source.name)
: color(d.target.name)) */
//.attr("stroke", d => d.target.colour)
.attr("stroke", d => d.uid )
//.attr("stroke-width", d => Math.max(1, d.width));
.attr("stroke-width", 10);


g.append("g")
.style("font", "10px sans-serif")
.selectAll("text")
.data(nodes)
.join("text")
// .attr("x", d => d.x0 < width / 2 ? d.x1 + 6 : d.x0 + 150)
.attr("x",function(d){
if(d.x0 < width / 2){
if(d.depth == 0){
return d.x0 - 10
}
else { return d.x0}
}
else{
return d.x0 + 160
}
})
.attr("y", d => d.depth == 1 ? ((d.y1 + d.y0) / 2) - 50: (d.y1 + d.y0) / 2)
.attr("dy", "0.35em")
.attr("id", d => "text" + d.id)
.attr("font-weight", d => d.depth == 1 ? 100: 600)
.attr("font-size", d => d.depth == 1 ? 0: 30)
.attr("text-anchor", d => d.x0 < width / 2 ? "End" : "Start")
.attr("text-anchor",function(d){
if(d.x0 < width / 2){
if(d.depth == 1){
return "Middle"
}
else { return "End"}
}
else{
return "Start"
}
})
.text(d => d.sourceLinks.length == 0 && d.targetLinks.length == 0 ? null : d.name)
/* .on("mouseover", function(d) {
d3.select(this).style("cursor", "pointer")
tooltip
.html("Name: " + d.name)
.style('visibility', 'visible')})
.on('mousemove', function (d) {
tooltip
.style('top', d3.event.clientY + 10 + 'px')
.style('left', d3.event.clientX + 20 + 'px')
.style("display", "inline-block");
})
.on('mouseout', function (d, nodes) {
tooltip.html(``).style('visibility', 'hidden');
});*/


var uniform =
d3.range(0, 7).map((_,i) => ({
id: i,
size: Math.ceil(d3.randomUniform(1, 5)(_)),
url: image_urls[i]
}));
let pack = data => d3.pack()
.size([width, width])
.padding(0)
(d3.hierarchy({children: data})
.sum(d => d.size))
const root = pack(uniform);
const defs = svg.append("defs")
.selectAll("pattern")
.data(root.leaves())
.enter();
defs.append("pattern")
.attr("id", d => "pattern" + d.data.id)
.attr("width", 1)
.attr("height", 1)
.append('image')
.attr("xlink:href", d => d.data.url)
document.body.appendChild(svg.node());

const box = g.node().getBBox();

svg.remove()
.attr("width", box.width)
.attr("height", box.height)
.attr("viewBox", `${box.x} ${box.y} ${box.width + 100} ${box.height}`);
svg.call(d3.zoom()
.extent([[0, 0], [width, height]])
.scaleExtent([0.75, 8])
.on("zoom", zoomed));

function zoomed() {
g.attr("transform", d3.event.transform);
}

d3.select('#NoOneButton').on('click', () => {
d3.selectAll('path')
.attr("opacity", 0)

d3.selectAll("text").attr("pointer-events", "default");
d3.selectAll("rect").attr("pointer-events", "default");
d3.selectAll("circle").attr("pointer-events", "default")//.attr("cx", d => (d.x1 + d.x0)/2 + 72.5);;
});


d3.select('#AllButton').on('click', () => {
d3.selectAll('path')
.attr("opacity", 0.8)

d3.selectAll("text").attr("pointer-events", "none");
d3.selectAll("rect").attr("pointer-events", "none");
d3.selectAll("circle").attr("pointer-events", "none")//.attr("cx", d => (d.x1 + d.x0)/2 + 62.5);
});


return svg.node();
}
Insert cell
sankey = {
const sankey = d3.sankey()
.nodeWidth(30)
.nodePadding(1000)
.extent([[0,1], [width - 500, height - 100]]);
return ({nodes, links}) => sankey({
nodes: nodes.map(d => Object.assign({}, d)),
links: links.map(d => Object.assign({}, d))
});
}
Insert cell
format = {
const f = d3.format(",.0f");
return d => `${f(d)} TWh`;
}
Insert cell
color = {
const color = d3.scaleOrdinal(d3.schemeCategory10);
return name => color(name.replace(/ .*/, ""));
}
Insert cell
data = FileAttachment("data@6.json").json()
Insert cell
tooltip = d3
.select('body')
.append('div')
.attr('class', 'd3-tooltip')
.style('position', 'absolute')
.style('z-index', '10')
.style('width', '180px')
.style('height', '120px')
.style('visibility', 'hidden')
.style('padding', '10px')
.style('background', 'rgba(255,255,255)')
.style('border-radius', '4px')
.style('color', 'black')
.style("box-shadow", "5px 5px 10px grey")
.text('a simple tooltip');
Insert cell
image_urls = [
await FileAttachment("Bruno.jpg").url(),
await FileAttachment("Imagem1.png").url(),
await FileAttachment("Imagem2@1.jpg").url(),
await FileAttachment("Imagem4.png").url(),
await FileAttachment("Imagem3@2.jpg").url(),
await FileAttachment("Imagem5@1.png").url()
]

Insert cell
function filterOver(d){
console.log(d)
d3.selectAll("#path"+d)
.attr("opacity", 0.8);

}
Insert cell
function filterOver2(d,c){
d3.selectAll("#path"+d)
.attr("opacity", 0.8);

d3.selectAll("#path"+c)
.attr("opacity", 0.8);
}
Insert cell
function filterOver3(d,c){
d3.selectAll(".path"+d)
.attr("opacity", 0.8);

d3.selectAll(".path"+c)
.attr("opacity", 0.8);
}
Insert cell
function filterOut(){
d3.selectAll("path")
.attr("opacity",0);
}
Insert cell
d3.selection.prototype.moveToBack = function() {
return this.each(function() {
var firstChild = this.parentNode.firstChild;
if (firstChild) {
this.parentNode.insertBefore(this, firstChild);
}
});
};
Insert cell
height = 1600
Insert cell
width = 1800
Insert cell
tagWithOptions = impl => (...args) => {
if (!Array.isArray(args[0])) {
return impl(args[0]);
}
return impl({})(...args);
}
Insert cell
interpolate = (strings, ...interpolations) => {
let s = '';
for (let i = 0; i < strings.length; i++) {
s += strings[i];
if (i < interpolations.length && interpolations[i]) {
s += String(interpolations[i]);
}
}
return dedent(s);
}
Insert cell
css = tagWithOptions(({ transform }) => (...args) => {
const source = interpolate(...args);
const style = html`<style>`;
style.textContent = transform ? transform(source) : source;
const el = md`
~~~css
${source}
~~~
${style}
`;
el.value = style.cloneNode(true);
return el;
})
Insert cell
collapsedCSS = tagWithOptions(opts => (...args) => {
if (typeof opts === 'string') {
opts = { title: opts };
}
const content = css(opts)(...args);
const el = html`
<details>
<summary style="cursor: pointer; user-select: none">${opts.title ||
'CSS'}</summary>
${content}
</details>
`;
el.value = content.value;
return el;
})
Insert cell
collapsedCSS
`img {
width:50px;
border-radius:50%;
}`
Insert cell
collapsedCSS
`hr {
padding:0.5rem;
margin:0;
}`
Insert cell
collapsedCSS
`h2 {
margin:0;
}`
Insert cell
collapsedCSS
`p {
font-size: 10px;
}`
Insert cell
collapsedCSS
`h3 {
font-size: 10px;
}`
Insert cell
dedent = (await import('https://unpkg.com/dentist@1.0.3/src/index.js')).dedent
Insert cell
d3 = require("d3@5", "d3-sankey@0.12")
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more