Published
Edited
Jan 22, 2021
1 fork
Insert cell
md`# Foam Tree Visualization : Full Transcriptomic`
Insert cell
d3 = require("d3@5", "d3-weighted-voronoi", "d3-voronoi-map", "d3-voronoi-treemap", 'seedrandom@2.4.3/seedrandom.min.js')
Insert cell
md`## Color Averaging Functions`
Insert cell
// Cut the hex into pieces
function cutHex(h) {
if (h.charAt(1) === 'x') {
return h.substring(2, 8);
}

return h.substring(1, 7);
}
Insert cell
// Get the red of RGB from a hex value
function hexToR(h) {
return parseInt((cutHex(h)).substring(0, 2), 16);
}
Insert cell
// Get the green of RGB from a hex value
function hexToG(h) {
return parseInt((cutHex(h)).substring(2, 4), 16);
}
Insert cell
// Get the blue of RGB from a hex value
function hexToB(h) {
return parseInt((cutHex(h)).substring(4, 6), 16);
}
Insert cell
function componentToHex(c) {
var hex = c.toString(16);
return hex.length === 1 ? '0' + hex : hex;
}
Insert cell
function rgbToHex(r, g, b) {
return '#' + componentToHex(r) + componentToHex(g) + componentToHex(b);
}
Insert cell
function averageColor(colorArray) {
var red = 0;
var green = 0;
var blue = 0;

for (var i = 0; i < colorArray.length; i++) {
var hex = colorArray[i];
red += hexToR(hex);
green += hexToG(hex);
blue += hexToB(hex);
}

// Average RGB
red = Math.round(red / colorArray.length);
green = Math.round(green / colorArray.length);
blue = Math.round(blue / colorArray.length);
return rgbToHex(red, green, blue);
};
Insert cell
md`# Utils`
Insert cell
margin = ({top: 20, right: 20, bottom: 20, left: 20})
Insert cell
height = 410 - margin.top - margin.bottom
Insert cell
width = 790 - margin.left - margin.right
Insert cell
ellipse = d3
.range(100)
.map(i => [
(width * (1 + 0.99 * Math.cos((i / 50) * Math.PI))) / 4,
(height * (1 + 0.99 * Math.sin((i / 50) * Math.PI))) / 2
])
Insert cell
function colorHierarchy(hierarchy) {
// For every element find the color to be average of all children.
const descendants = hierarchy.descendants();
if(hierarchy.children){
const colorArray = [];
descendants.forEach(d => {
if(d.data.node_attributes[0]['nodePar.col']) colorArray.push(d.data.node_attributes[0]['nodePar.col']);
});
const color = averageColor(colorArray);
hierarchy.color = color;
} else {
hierarchy.color = hierarchy.data.node_attributes[0]['nodePar.col'];
}

if(hierarchy.children) {
hierarchy.children.forEach( child => colorHierarchy(child))
}
}
Insert cell
function area(pts) {
var area=0;
var nPts = pts.length;
var j=nPts-1;
var p1; var p2;

for (var i=0;i<nPts;j=i++) {
p1=pts[i]; p2=pts[j];
area+=p1.x*p2.y;
area-=p1.y*p2.x;
}
area/=2;
return area;
};
Insert cell
function computeCentroid(pts) {
var nPts = pts.length;
var x=0; var y=0;
var f;
var j=nPts-1;
var p1; var p2;

for (var i=0;i<nPts;j=i++) {
p1=pts[i]; p2=pts[j];
f=p1.x*p2.y-p2.x*p1.y;
x+=(p1.x+p2.x)*f;
y+=(p1.y+p2.y)*f;
}

f=area(pts)*6;
return [x/f,y/f];
};
Insert cell
function computeCentroidForLabel(hierarchy) {
const centroidPoints = [];
hierarchy.polygon.forEach(point=>{
centroidPoints.push({x: point[0], y: point[1]});
})
const centroid = computeCentroid(centroidPoints);
hierarchy.centroid = centroid;
if(hierarchy.children){
hierarchy.children.forEach(child => computeCentroidForLabel(child));
}
}
Insert cell
md`# Data`
Insert cell
data = d3.json("https://gist.githubusercontent.com/akibmayadav/1e87a83db6927dd3c45466c88e401b96/raw/4e99350f894301dae662de6e76ae47ca4a9d8969/taxonomy.json");
Insert cell
taxonomy_hierarchy = d3.hierarchy(data, d => d.children)
.sum(d => d.node_attributes[0].members)
Insert cell
md`# Foam Tree`
Insert cell
{
const svg = d3.select(DOM.svg(width + margin.left + margin.right, height + margin.left + margin.right));
svg
.append("rect")
.attr("width", "100%")
.attr("height", "100%")
.style("fill", "#F5F5F2");
const voronoi = svg.append("g").attr("transform", "translate(" + margin.left + "," + margin.top + ")");
const voronoi2 = svg.append("g").attr("transform", "translate(" + (width/2 + margin.left*1.5) + "," + margin.top + ")");
let seed = new Math.seedrandom(20);
let voronoiTreeMap = d3.voronoiTreemap()
.prng(seed) // To generate the same pattern again.
.clip(ellipse);
voronoiTreeMap(taxonomy_hierarchy);
colorHierarchy(taxonomy_hierarchy); // Assigning color to every element of the hierarchy
computeCentroidForLabel(taxonomy_hierarchy);
console.log(taxonomy_hierarchy)
let currentDepthLevel = 0;
let currentParentNodes = [taxonomy_hierarchy.descendants()[0].data.node_attributes[0].cell_set_accession];

function whatOpacity(currentDepth, selectedDepth){
if(currentDepth === selectedDepth) return 0.8;
if(currentDepth < selectedDepth) return 0.6;
return 0;
}
function isVisible(node, currentDepthLevel, currentParentNodes){
// If lowest level node
if(currentDepthLevel === 0 && node.depth === currentDepthLevel) { return true }
// If node had been selected at any point of time
if(node.depth < currentDepthLevel && currentParentNodes.includes(node.data.node_attributes[0].cell_set_accession)){ return true }
// Current node selection
if(node.depth === currentDepthLevel && currentParentNodes.includes(node.parent.data.node_attributes[0].cell_set_accession)) { return true }
// Don't show any other node
return false;
}
let allNodes = taxonomy_hierarchy.descendants()
.sort((a, b) => b.depth - a.depth)
.map((d, i) => Object.assign({}, d, {id: i}));
// Filter Nodes above the selected depth level
let filteredNodes = allNodes
.filter(node => isVisible(node, currentDepthLevel, currentParentNodes))
.sort((a, b) => a.depth - b.depth);
function onClick(d){
if(d.height === 0) return // Disable click on leaf nodes.
currentDepthLevel = d.depth + 1;
currentParentNodes = currentParentNodes.slice(0, currentDepthLevel);
currentParentNodes.push(d.data.node_attributes[0].cell_set_accession);
filteredNodes = allNodes
.filter(node => isVisible(node, currentDepthLevel, currentParentNodes))
.sort((a, b) => a.depth - b.depth);
// *********** TAXONOMY 1 ***************
voronoi.selectAll('path').remove();
voronoi.selectAll('path')
.data(filteredNodes)
.enter()
.append('path')
.attr('d', d => "M" + d.polygon.join("L") + "Z")
.attr('transform', d => {
if(d.depth !== 0) {
return (`translate(${width/4}, ${height/2})
scale(${1 - (d.depth * 0.02)})
translate(${-width/4}, ${-height/2})`)
}
return 'scale(1)';
})
.style('fill', d => d.color)
.attr('stroke', "white")
.attr('stroke-width', d=> 1 )
.style('fill-opacity',d=> whatOpacity(d.depth, currentDepthLevel))
.on('click', onClick)
.append('text')
.text(d=> d.data.node_attributes[0].label);
voronoi.selectAll('text').remove();
voronoi.selectAll('text')
.data(filteredNodes.filter(node=> node.depth === currentDepthLevel))
.enter()
.append('text')
.style('text-anchor', 'middle')
.attr('font-family', 'arial')
.style('fill', 'white')
.text(d => d.data.node_attributes[0].label || 'Neuron Label')
.attr('x', d => d.centroid[0])
.attr('y', d => d.centroid[1])
.attr('transform', d => {
if(d.depth !== 0) {
return (`translate(${width/4}, ${height/2})
scale(${1 - (d.depth * 0.02)})
translate(${-width/4}, ${-height/2})`)
}
return 'scale(1)';
})
voronoi.selectAll('circle').remove();
voronoi.selectAll('circle')
.data(filteredNodes.filter(node=> node.depth === currentDepthLevel))
.enter()
.append('circle')
.attr('cx', d => d.centroid[0])
.attr('cy', d => d.centroid[1] + 10)
.attr('r', 3)
.style('fill', 'white')
.attr('transform', d => {
if(d.depth !== 0) {
return (`translate(${width/4}, ${height/2})
scale(${1 - (d.depth * 0.02)})
translate(${-width/4}, ${-height/2})`)
}
return 'scale(1)';
})
// *********** TAXONOMY 2 ***************
voronoi2.selectAll('path').remove();
voronoi2.selectAll('path')
.data(filteredNodes)
.enter()
.append('path')
.attr('d', d => "M" + d.polygon.join("L") + "Z")
.attr('transform', d => {
if(d.depth !== 0) {
return (`translate(${width/4}, ${height/2})
scale(${1 - (d.depth * 0.02)})
translate(${-width/4}, ${-height/2})`)
}
return 'scale(1)';
})
.style('fill', d => d.color)
.attr('stroke', "white")
.attr('stroke-width', d=> 1 )
.style('fill-opacity',d=> whatOpacity(d.depth, currentDepthLevel))
.on('click', onClick)
.append('text')
.text(d=> d.data.node_attributes[0].label);
voronoi2.selectAll('text').remove();
voronoi2.selectAll('text')
.data(filteredNodes.filter(node=> node.depth === currentDepthLevel))
.enter()
.append('text')
.style('text-anchor', 'middle')
.attr('font-family', 'arial')
.style('fill', 'white')
.text(d => d.data.node_attributes[0].label || 'Neuron Label')
.attr('x', d => d.centroid[0])
.attr('y', d => d.centroid[1])
.attr('transform', d => {
if(d.depth !== 0) {
return (`translate(${width/4}, ${height/2})
scale(${1 - (d.depth * 0.02)})
translate(${-width/4}, ${-height/2})`)
}
return 'scale(1)';
})
voronoi2.selectAll('circle').remove();
voronoi2.selectAll('circle')
.data(filteredNodes.filter(node=> node.depth === currentDepthLevel))
.enter()
.append('circle')
.attr('cx', d => d.centroid[0])
.attr('cy', d => d.centroid[1] + 10)
.attr('r', 3)
.style('fill', 'white')
.attr('transform', d => {
if(d.depth !== 0) {
return (`translate(${width/4}, ${height/2})
scale(${1 - (d.depth * 0.02)})
translate(${-width/4}, ${-height/2})`)
}
return 'scale(1)';
})
}
// Tooltip Render
function renderTooltip(d){
console.log(d);
}
voronoi.selectAll('path')
.data(filteredNodes)
.enter()
.append('path')
.attr('d', d => "M" + d.polygon.join("L") + "Z")
.attr('transform', d => {
if(d.depth !== 0) {
return (`translate(${width/2}, ${height/2})
scale(${1 - (d.depth * 0.1)})
translate(${-width/2}, ${-height/2})`)
}
return 'scale(1)';
})
.style('fill', d => d.color)
.attr('stroke', "white")
.attr('stroke-width', d => 1 )
.style('fill-opacity',d => whatOpacity(d.depth, currentDepthLevel))
.on('click', onClick)
.on('hover', renderTooltip);
voronoi2.selectAll('path')
.data(filteredNodes)
.enter()
.append('path')
.attr('d', d => "M" + d.polygon.join("L") + "Z")
.attr('transform', d => {
if(d.depth !== 0) {
return (`translate(${width/2}, ${height/2})
scale(${1 - (d.depth * 0.1)})
translate(${-width/2}, ${-height/2})`)
}
return 'scale(1)';
})
.style('fill', d => d.color)
.attr('stroke', "white")
.attr('stroke-width', d => 1 )
.style('fill-opacity',d => whatOpacity(d.depth, currentDepthLevel))
.on('hover', renderTooltip);
// Adding Labels
voronoi.selectAll('text')
.data(filteredNodes)
.enter()
.append('text')
.text(d => d.data.node_attributes[0].label || 'Neuron Label')
.style('text-anchor', 'middle')
.style('fill', 'white')
.attr('font-family', 'arial')
.attr('x', d => d.centroid[0])
.attr('y', d => d.centroid[1])
.attr('transform', d => {
if(d.depth !== 0) {
return (`translate(${width/2}, ${height/2})
scale(${1 - (d.depth * 0.02)})
translate(${-width/2}, ${-height/2})`)
}
return 'scale(1)';
})
voronoi.selectAll('circle')
.data(filteredNodes.filter(node=> node.depth === currentDepthLevel))
.enter()
.append('circle')
.attr('cx', d => d.centroid[0])
.attr('cy', d => d.centroid[1] + 10)
.attr('r', 3)
.style('fill', 'white')
.attr('transform', d => {
if(d.depth !== 0) {
return (`translate(${width/2}, ${height/2})
scale(${1 - (d.depth * 0.02)})
translate(${-width/2}, ${-height/2})`)
}
return 'scale(1)';
})
voronoi2.selectAll('text')
.data(filteredNodes)
.enter()
.append('text')
.text(d => d.data.node_attributes[0].label || 'Neuron Label')
.style('text-anchor', 'middle')
.style('fill', 'white')
.attr('font-family', 'arial')
.attr('x', d => d.centroid[0])
.attr('y', d => d.centroid[1])
.attr('transform', d => {
if(d.depth !== 0) {
return (`translate(${width/2}, ${height/2})
scale(${1 - (d.depth * 0.02)})
translate(${-width/2}, ${-height/2})`)
}
return 'scale(1)';
})
voronoi2.selectAll('circle')
.data(filteredNodes.filter(node=> node.depth === currentDepthLevel))
.enter()
.append('circle')
.attr('cx', d => d.centroid[0])
.attr('cy', d => d.centroid[1] + 10)
.attr('r', 3)
.style('fill', 'white')
.attr('transform', d => {
if(d.depth !== 0) {
return (`translate(${width/2}, ${height/2})
scale(${1 - (d.depth * 0.02)})
translate(${-width/2}, ${-height/2})`)
}
return 'scale(1)';
})
return svg.node();
}
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