Public
Edited
Nov 5, 2023
22 forks
Importers
71 stars
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
data = dataFlattened.map(d=>{
const width = 350 //Math.round(Math.random()*50+300);
const height = 160 // Math.round(Math.random()*20+130);
const cornerShape = 'ORIGINAL'; //['ORIGINAL','ROUNDED','CIRCLE']
const nodeImageWidth = 100;
const nodeImageHeight = 100;
const centerTopDistance = 0;
const centerLeftDistance = 0;
const expanded = false; //d.id=="O-6"
const titleMarginLeft = nodeImageWidth/2+20+centerLeftDistance
const contentMarginLeft = width/2+25
return {
nodeId:d.id,
parentNodeId:d.parentId,
width:width,
height:height,
borderWidth:0.6,
borderRadius:0,
borderColor:{
red:15,
green:140,
blue:121,
alpha:0.7,
},
backgroundColor:{
red:247,
green:247,
blue:249,
alpha:0.4,
},
nodeIcon:{
size:50,
},
nodeImage:{
url:d.imageUrl,
width:nodeImageWidth,
height:nodeImageHeight,
centerTopDistance:66,
centerLeftDistance:66,
cornerShape:'CIRCLE', // ORIGINAL,CIRCLE
shadow:false,
borderWidth:0,
borderColor:{
red:19,
green:123,
blue:128,
alpha:0,
},
},
template:`<div style="color:#2A2A2A;height:160px;margin-top:-30px;background-image: linear-gradient(to right , #FFFFFF, #ECEDF0);">
<div style="margin-left:${titleMarginLeft+77}px;
margin-top:30px;
padding-top:20px;
font-size:20px;
font-weight:bold;
">${d.name} </div>
<div style="margin-left:${titleMarginLeft+77}px;
margin-top:3px;
font-size:16px;
">${d.positionName} </div>

<div style="margin-left:${titleMarginLeft+77}px;
margin-top:3px;
font-size:14px;
">${d.unit.value}</div>

<div style="margin-left:${contentMarginLeft+70}px;
margin-top:15px;
font-size:13px;
position:absolute;
bottom:5px;
">
<div>${d.office}</div>
<div style="margin-top:5px">${d.area}</div>
</div>
</div>`,
connectorLineColor:{
red:59,
green:60,
blue:63,
alpha:1
},
connectorLineWidth:2,
dashArray:'',
expanded:expanded
}
})
Insert cell
Insert cell
chart = new TreeChart()
Insert cell
chartRendering = {
chart
.container(container)
.data(data)
.svgWidth(width)
.onNodeClick(d=> console.log(d+' node clicked'))
.render()
}
Insert cell
Insert cell
class TreeChart {
constructor() {
// Exposed variables
const attrs = {
id: `ID${Math.floor(Math.random() * 1000000)}`, // Id for event handlings
svgWidth: 800,
svgHeight: 600,
marginTop: 0,
marginBottom: 0,
marginRight: 0,
marginLeft: 0,
container: 'body',
defaultTextFill: '#2C3E50',
nodeTextFill: 'white',
defaultFont: 'Helvetica',
backgroundColor: '#DFE3E9',
data: null,
depth: 180,
duration: 600,
strokeWidth: 3,
dropShadowId: null,
initialZoom: 1,
onNodeClick: d => d,
};

this.getChartState = () => attrs;

// Dynamically set getter and setter functions for Chart class
Object.keys(attrs).forEach((key) => {
//@ts-ignore
this[key] = function (_) {
var string = `attrs['${key}'] = _`;
if (!arguments.length) {
return eval(`attrs['${key}'];`);
}
eval(string);
return this;
};
});


this.initializeEnterExitUpdatePattern();
}

initializeEnterExitUpdatePattern() {
d3.selection.prototype.patternify = function (params) {
var container = this;
var selector = params.selector;
var elementTag = params.tag;
var data = params.data || [selector];

// Pattern in action
var selection = container.selectAll('.' + selector).data(data, (d, i) => {
if (typeof d === 'object') {
if (d.id) {
return d.id;
}
}
return i;
});
selection.exit().remove();
selection = selection.enter().append(elementTag).merge(selection);
selection.attr('class', selector);
return selection;
};
}

// This method retrieves passed node's children IDs (including node)
getNodeChildrenIds({
data,
children,
_children
}, nodeIdsStore) {

// Store current node ID
nodeIdsStore.push(data.nodeId);

// Loop over children and recursively store descendants id (expanded nodes)
if (children) {
children.forEach(d => {
this.getNodeChildrenIds(d, nodeIdsStore)
})
}

// Loop over _children and recursively store descendants id (collapsed nodes)
if (_children) {
_children.forEach(d => {
this.getNodeChildrenIds(d, nodeIdsStore)
})
}

// Return result
return nodeIdsStore;
}

// This method can be invoked via chart.setZoomFactor API, it zooms to particulat scale
setZoomFactor(zoomLevel) {
const attrs = this.getChartState();
const calc = attrs.calc;

// Store passed zoom level
attrs.initialZoom = zoomLevel;

// Rescale container element accordingly
attrs.centerG.attr('transform', ` translate(${calc.centerX}, ${calc.nodeMaxHeight / 2}) scale(${attrs.initialZoom})`)
}

render() {
//InnerFunctions which will update visuals

const attrs = this.getChartState();
const thisObjRef = this;

//Drawing containers
const container = d3.select(attrs.container);
const containerRect = container.node().getBoundingClientRect();
if (containerRect.width > 0) attrs.svgWidth = containerRect.width;

//Attach drop shadow id to attrs object
this.setDropShadowId(attrs);

//Calculated properties
const calc = {
id: null,
chartTopMargin: null,
chartLeftMargin: null,
chartWidth: null,
chartHeight: null
};
calc.id = `ID${Math.floor(Math.random() * 1000000)}`; // id for event handlings
calc.chartLeftMargin = attrs.marginLeft;
calc.chartTopMargin = attrs.marginTop;
calc.chartWidth = attrs.svgWidth - attrs.marginRight - calc.chartLeftMargin;
calc.chartHeight = attrs.svgHeight - attrs.marginBottom - calc.chartTopMargin;
attrs.calc = calc;

// Get maximum node width and height
calc.nodeMaxWidth = d3.max(attrs.data, ({
width
}) => width);
calc.nodeMaxHeight = d3.max(attrs.data, ({
height
}) => height);

// Calculate max node depth (it's needed for layout heights calculation)
attrs.depth = calc.nodeMaxHeight + 100;
calc.centerX = calc.chartWidth / 2;

//******************** LAYOUTS ***********************
const layouts = {
treemap: null
}
attrs.layouts = layouts;

// Generate tree layout function
layouts.treemap = d3.tree().size([calc.chartWidth, calc.chartHeight])
.nodeSize([calc.nodeMaxWidth + 100, calc.nodeMaxHeight + attrs.depth])

// ******************* BEHAVIORS . **********************
const behaviors = {
zoom: null
}

// Get zooming function
behaviors.zoom = d3.zoom().on("zoom", d => this.zoomed(d))

//****************** ROOT node work ************************

// Convert flat data to hierarchical
attrs.root = d3.stratify()
.id(({
nodeId
}) => nodeId)
.parentId(({
parentNodeId
}) => parentNodeId)
(attrs.data)

// Set child nodes enter appearance positions
attrs.root.x0 = 0;
attrs.root.y0 = 0;

/** Get all nodes as array (with extended parent & children properties set)
This way we can access any node's parent directly using node.parent - pretty cool, huh?
*/
attrs.allNodes = attrs.layouts.treemap(attrs.root).descendants()

// Assign direct children and total subordinate children's cound
attrs.allNodes.forEach(d => {
Object.assign(d.data, {
directSubordinates: d.children ? d.children.length : 0,
totalSubordinates: d.descendants().length - 1
})
})

// Collapse all children at first
attrs.root.children.forEach(d => this.collapse(d));

// Then expand some nodes, which have `expanded` property set
attrs.root.children.forEach(d => this.expandSomeNodes(d));

// ************************* DRAWING **************************
//Add svg
const svg = container
.patternify({
tag: 'svg',
selector: 'svg-chart-container'
})
.attr('width', attrs.svgWidth)
.attr('height', attrs.svgHeight)
.attr('font-family', attrs.defaultFont)
.call(behaviors.zoom)
.on('dblclick.zoom',null)
.attr('cursor', 'move')
.style('background-image','linear-gradient(to right , #fcfaf8, #E3E7EC);padding-top:10px')
attrs.svg = svg;

//Add container g element
const chart = svg
.patternify({
tag: 'g',
selector: 'chart'
})
.attr('transform', `translate(${calc.chartLeftMargin},${calc.chartTopMargin})`);

// Add one more container g element, for better positioning controls
attrs.centerG = chart.patternify({
tag: 'g',
selector: 'center-group'
})
.attr('transform', `translate(${calc.centerX},${calc.nodeMaxHeight / 2}) scale(${attrs.initialZoom})`);

attrs.chart = chart;

// ************************** ROUNDED AND SHADOW IMAGE WORK USING SVG FILTERS **********************

//Adding defs element for rounded image
attrs.defs = svg.patternify({
tag: 'defs',
selector: 'image-defs'
});

// Adding defs element for image's shadow
const filterDefs = svg.patternify({
tag: 'defs',
selector: 'filter-defs'
});

// Adding shadow element - (play with svg filter here - https://bit.ly/2HwnfyL)
const filter = filterDefs.patternify({
tag: 'filter',
selector: 'shadow-filter-element'
})
.attr('id', attrs.dropShadowId)
.attr('y', `${-200}%`)
.attr('x', `${-200}%`)
.attr('height', `${400}%`)
.attr('width', `${400}%`);

// Add gaussian blur element for shadows - we can control shadow length with this
filter.patternify({
tag: 'feGaussianBlur',
selector: 'feGaussianBlur-element'
})
.attr('in', 'SourceAlpha')
.attr('stdDeviation', 5.1)
.attr('result', 'blur');

// Add fe-offset element for shadows - we can control shadow positions with it
filter.patternify({
tag: 'feOffset',
selector: 'feOffset-element'
})
.attr('in', 'blur')
.attr('result', 'offsetBlur')
.attr("dx", 15.28)
.attr("dy", 10.48)
.attr("x", -350)
.attr("y", -140)

// Add fe-flood element for shadows - we can control shadow color and opacity with this element
filter.patternify({
tag: 'feFlood',
selector: 'feFlood-element'
})
.attr("in", "offsetBlur")
.attr("flood-color", 'black')
.attr("flood-opacity", 0.2)
.attr("result", "offsetColor");

// Add feComposite element for shadows
filter.patternify({
tag: 'feComposite',
selector: 'feComposite-element'
})
.attr("in", "offsetColor")
.attr("in2", "offsetBlur")
.attr("operator", "in")
.attr("result", "offsetBlur");

// Add feMerge element for shadows
const feMerge = filter.patternify({
tag: 'feMerge',
selector: 'feMerge-element'
});

// Add feMergeNode element for shadows
feMerge.patternify({
tag: 'feMergeNode',
selector: 'feMergeNode-blur'
})
.attr('in', 'offsetBlur')

// Add another feMergeNode element for shadows
feMerge.patternify({
tag: 'feMergeNode',
selector: 'feMergeNode-graphic'
})
.attr('in', 'SourceGraphic')

// Display tree contenrs
this.update(attrs.root)



//######################################### UTIL FUNCS ##################################
// This function restyles foreign object elements ()




d3.select(window).on(`resize.${attrs.id}`, () => {
const containerRect = container.node().getBoundingClientRect();
// if (containerRect.width > 0) attrs.svgWidth = containerRect.width;
// main();
});


return this;
}

// This function sets drop shadow ID to the passed object
setDropShadowId(d) {

// If it's already set, then return
if (d.dropShadowId) return;

// Generate drop shadow ID
let id = `${d.id}-drop-shadow`;

// If DOM object is available, then use UID method to generated shadow id
//@ts-ignore
if (typeof DOM != 'undefined') {
//@ts-ignore
id = DOM.uid(d.id).id;
}

// Extend passed object with drop shadow ID
Object.assign(d, {
dropShadowId: id
})
}


// This function can be invoked via chart.addNode API, and it adds node in tree at runtime
addNode(obj) {
const attrs = this.getChartState();
attrs.data.push(obj);

// Update state of nodes and redraw graph
this.updateNodesState();
return this;
}

// This function can be invoked via chart.removeNode API, and it removes node from tree at runtime
removeNode(nodeId) {
const attrs = this.getChartState();
const node = attrs.allNodes.filter(({
data
}) => data.nodeId == nodeId)[0];


// Remove all node childs
if (node) {
// Retrieve all children nodes ids (including current node itself)
const nodeChildrenIds = this.getNodeChildrenIds(node, []);

// Filter out retrieved nodes and reassign data
attrs.data = attrs.data.filter(d => !nodeChildrenIds.includes(d.nodeId))

const updateNodesState = this.updateNodesState.bind(this);
// Update state of nodes and redraw graph
updateNodesState();
}
}

// This function basically redraws visible graph, based on nodes state
update({
x0,
y0,
x,
y
}) {

const attrs = this.getChartState();
const calc = attrs.calc;

// Assigns the x and y position for the nodes
const treeData = attrs.layouts.treemap(attrs.root);

// Get tree nodes and links and attach some properties
const nodes = treeData.descendants()
.map(d => {
// If at least one property is already set, then we don't want to reset other properties
if (d.width) return d;

// Declare properties with deffault values
let imageWidth = 100;
let imageHeight = 100;
let imageBorderColor = 'steelblue';
let imageBorderWidth = 0;
let imageRx = 0;
let imageCenterTopDistance = 0;
let imageCenterLeftDistance = 0;
let borderColor = 'steelblue';
let backgroundColor = 'steelblue';
let width = d.data.width;
let height = d.data.height;
let dropShadowId = `none`;

// Override default values based on data
if (d.data.nodeImage && d.data.nodeImage.shadow) {
dropShadowId = `url(#${attrs.dropShadowId})`
}
if (d.data.nodeImage && d.data.nodeImage.width) {
imageWidth = d.data.nodeImage.width
};
if (d.data.nodeImage && d.data.nodeImage.height) {
imageHeight = d.data.nodeImage.height
};
if (d.data.nodeImage && d.data.nodeImage.borderColor) {
imageBorderColor = this.rgbaObjToColor(d.data.nodeImage.borderColor)
};
if (d.data.nodeImage && d.data.nodeImage.borderWidth) {
imageBorderWidth = d.data.nodeImage.borderWidth
};
if (d.data.nodeImage && d.data.nodeImage.centerTopDistance) {
imageCenterTopDistance = d.data.nodeImage.centerTopDistance
};
if (d.data.nodeImage && d.data.nodeImage.centerLeftDistance) {
imageCenterLeftDistance = d.data.nodeImage.centerLeftDistance
};
if (d.data.borderColor) {
borderColor = this.rgbaObjToColor(d.data.borderColor);
}
if (d.data.backgroundColor) {
backgroundColor = this.rgbaObjToColor(d.data.backgroundColor);
}
if (d.data.nodeImage &&
d.data.nodeImage.cornerShape.toLowerCase() == "circle") {
imageRx = Math.max(imageWidth, imageHeight);
}
if (d.data.nodeImage &&
d.data.nodeImage.cornerShape.toLowerCase() == "rounded") {
imageRx = Math.min(imageWidth, imageHeight) / 6;
}

// Extend node object with calculated properties
return Object.assign(d, {
imageWidth,
imageHeight,
imageBorderColor,
imageBorderWidth,
borderColor,
backgroundColor,
imageRx,
width,
height,
imageCenterTopDistance,
imageCenterLeftDistance,
dropShadowId
});
});

// Get all links
const links = treeData.descendants().slice(1);

// Set constant depth for each nodes
nodes.forEach(d => d.y = d.depth * attrs.depth);

// ------------------- FILTERS ---------------------

// Add patterns for each node (it's needed for rounded image implementation)
const patternsSelection = attrs.defs.selectAll('.pattern')
.data(nodes, ({
id
}) => id);

// Define patterns enter selection
const patternEnterSelection = patternsSelection.enter().append('pattern')

// Patters update selection
const patterns = patternEnterSelection
.merge(patternsSelection)
.attr('class', 'pattern')
.attr('height', 1)
.attr('width', 1)
.attr('id', ({
id
}) => id)

// Add images to patterns
const patternImages = patterns.patternify({
tag: 'image',
selector: 'pattern-image',
data: d => [d]
})
.attr('x', 0)
.attr('y', 0)
.attr('height', ({
imageWidth
}) => imageWidth)
.attr('width', ({
imageHeight
}) => imageHeight)
.attr('xlink:href', ({
data
}) => data.nodeImage && data.nodeImage.url)
.attr('viewbox', ({
imageWidth,
imageHeight
}) => `0 0 ${imageWidth * 2} ${imageHeight}`)
.attr('preserveAspectRatio', 'xMidYMin slice')

// Remove patterns exit selection after animation
patternsSelection.exit().transition().duration(attrs.duration).remove();

// -------------------------- LINKS ----------------------
// Get links selection
const linkSelection = attrs.centerG.selectAll('path.link')
.data(links, ({
id
}) => id);

// Enter any new links at the parent's previous position.
const linkEnter = linkSelection.enter()
.insert('path', "g")
.attr("class", "link")
.attr('d', d => {
const o = {
x: x0,
y: y0
};
return this.diagonal(o, o)
});

// Get links update selection
const linkUpdate = linkEnter.merge(linkSelection);

// Styling links
linkUpdate
.attr("fill", "none")
.attr("stroke-width", ({
data
}) => data.connectorLineWidth || 2)
.attr('stroke', ({
data
}) => {
if (data.connectorLineColor) {
return this.rgbaObjToColor(data.connectorLineColor);
}
return 'green';
})
.attr('stroke-dasharray', ({
data
}) => {
if (data.dashArray) {
return data.dashArray;
}
return '';
})

// Transition back to the parent element position
linkUpdate.transition()
.duration(attrs.duration)
.attr('d', d => this.diagonal(d, d.parent));

// Remove any links which is exiting after animation
const linkExit = linkSelection.exit().transition()
.duration(attrs.duration)
.attr('d', d => {
const o = {
x: x,
y: y
};
return this.diagonal(o, o)
})
.remove();

// -------------------------- NODES ----------------------
// Get nodes selection
const nodesSelection = attrs.centerG.selectAll('g.node')
.data(nodes, ({
id
}) => id)

// Enter any new nodes at the parent's previous position.
const nodeEnter = nodesSelection.enter().append('g')
.attr('class', 'node')
.attr("transform", d => `translate(${x0},${y0})`)
.attr('cursor', 'pointer')
.on('click', ({
data
}) => {
if ([...d3.event.srcElement.classList].includes('node-button-circle')) {
return;
}
attrs.onNodeClick(data.nodeId);
});

// Add background rectangle for the nodes
nodeEnter
.patternify({
tag: 'rect',
selector: 'node-rect',
data: d => [d]
})
.style("fill", ({
_children
}) => _children ? "lightsteelblue" : "#fff")


/*
// Add node icon image inside node
nodeEnter
.patternify({
tag: 'image',
selector: 'node-icon-image',
data: d => [d]
})
.attr('width', ({
data
}) => data.nodeIcon && data.nodeIcon.size)
.attr('height', ({
data
}) => data.nodeIcon && data.nodeIcon.size)
.attr("xlink:href", ({
data
}) => data.nodeIcon && data.nodeIcon.icon)
.attr('x', ({
width
}) => -width / 2 + 5)
.attr('y', ({
height,
data
}) => height / 2 - (data.nodeIcon && data.nodeIcon.size || 0) - 5)

// Add total descendants text
nodeEnter
.patternify({
tag: 'text',
selector: 'node-icon-text-total',
data: d => [d]
})
.text('test')
.attr('x', ({
width
}) => -width / 2 + 7)
.attr('y', ({
height,
data
}) => height / 2 - (data.nodeIcon && data.nodeIcon.size) - 5)
.text(({
data
}) => `${data.totalSubordinates} Subordinates`)
.attr('fill', attrs.nodeTextFill)
.attr('font-weight', 'bold')

// Add direct descendants text
nodeEnter
.patternify({
tag: 'text',
selector: 'node-icon-text-direct',
data: d => [d]
})
.text('test')
.attr('x', ({
width,
data
}) => -width / 2 + 10 + (data.nodeIcon && data.nodeIcon.size))
.attr('y', ({
height
}) => height / 2 - 10)
.text(({
data
}) => `${data.directSubordinates} Direct `)
.attr('fill', attrs.nodeTextFill)
.attr('font-weight', 'bold')
*/




// Node update styles
const nodeUpdate = nodeEnter.merge(nodesSelection)
.style('font', '12px sans-serif');



// Add foreignObject element inside rectangle
const fo = nodeUpdate
.patternify({
tag: 'foreignObject',
selector: 'node-foreign-object',
data: d => [d]
})

const nodeImageGroups = nodeUpdate.patternify({
tag: 'g',
selector: 'node-image-group',
data: d => [d]
})
// Add background rectangle for node image
nodeImageGroups
.patternify({
tag: 'rect',
selector: 'node-image-rect',
data: d => [d]
})

// Add foreign object
fo.patternify({
tag: 'xhtml:div',
selector: 'node-foreign-object-div',
data: d => [d]
})

this.restyleForeignObjectElements();



// Add Node button circle's group (expand-collapse button)
const nodeButtonGroups = nodeEnter
.patternify({
tag: 'g',
selector: 'node-button-g',
data: d => [d]
})
.on('click', d => this.onButtonClick(d))

// Add expand collapse button circle
nodeButtonGroups
.patternify({
tag: 'circle',
selector: 'node-button-circle',
data: d => [d]
})

// Add button text
nodeButtonGroups
.patternify({
tag: 'text',
selector: 'node-button-text',
data: d => [d]
})
.attr('pointer-events', 'none')

// Transition to the proper position for the node
nodeUpdate.transition()
.attr('opacity', 0)
.duration(attrs.duration)
.attr("transform", ({
x,
y
}) => `translate(${x},${y})`)
.attr('opacity', 1)

// Move images to desired positions
nodeUpdate.selectAll('.node-image-group')
.attr('transform', ({
imageWidth,
width,
imageHeight,
height
}) => {
let x = -imageWidth / 2 - width / 2;
let y = -imageHeight / 2 - height / 2;
return `translate(${x},${y})`
})

// Style node image rectangles
nodeUpdate.select('.node-image-rect')
.attr('fill', ({
id
}) => `url(#${id})`)
.attr('width', ({
imageWidth
}) => imageWidth)
.attr('height', ({
imageHeight
}) => imageHeight)
.attr('stroke', ({
imageBorderColor
}) => imageBorderColor)
.attr('stroke-width', ({
imageBorderWidth
}) => imageBorderWidth)
.attr('rx', ({
imageRx
}) => imageRx)
.attr('y', ({
imageCenterTopDistance
}) => imageCenterTopDistance)
.attr('x', ({
imageCenterLeftDistance
}) => imageCenterLeftDistance)
.attr('filter', ({
dropShadowId
}) => dropShadowId)

// Style node rectangles
nodeUpdate.select('.node-rect')
.attr('width', ({
data
}) => data.width)
.attr('height', ({
data
}) => data.height)
.attr('x', ({
data
}) => -data.width / 2)
.attr('y', ({
data
}) => -data.height / 2)
.attr('rx', ({
data
}) => data.borderRadius || 0)
.attr('stroke-width', ({
data
}) => data.borderWidth!=null? data.borderWidth : attrs.strokeWidth)
.attr('cursor', 'pointer')
.attr('stroke', ({
borderColor
}) => borderColor)
.style("fill", ({
backgroundColor
}) => backgroundColor)
.attr('filter', ({
dropShadowId
}) => dropShadowId)

// Move node button group to the desired position
nodeUpdate.select('.node-button-g')
.attr('transform', ({
data
}) => `translate(0,${data.height / 2})`)
.attr('opacity', ({
children,
_children
}) => {
if (children || _children) {
return 1;
}
return 0;
})

// Restyle node button circle
nodeUpdate.select('.node-button-circle')
.attr('r', 16)
.attr('stroke-width', ({
data
}) => data.borderWidth || attrs.strokeWidth)
.attr('fill', attrs.backgroundColor)
.attr('stroke', ({
borderColor
}) => borderColor)

// Restyle button texts
nodeUpdate.select('.node-button-text')
.attr('text-anchor', 'middle')
.attr('alignment-baseline', 'middle')
.attr('fill', attrs.defaultTextFill)
.attr('font-size', ({
children
}) => {
if (children) return 40;
return 26;
})
.text(({
children
}) => {
if (children) return '-';
return '+';
})
.attr('y', this.isEdge() ? 10 : 0)

// Remove any exiting nodes after transition
const nodeExitTransition = nodesSelection.exit()
.attr('opacity', 1)
.transition()
.duration(attrs.duration)
.attr("transform", d => `translate(${x},${y})`)
.on('end', function () {
d3.select(this).remove();
})
.attr('opacity', 0);

// On exit reduce the node rects size to 0
nodeExitTransition.selectAll('.node-rect')
.attr('width', 10)
.attr('height', 10)
.attr('x', 0)
.attr('y', 0);

// On exit reduce the node image rects size to 0
nodeExitTransition.selectAll('.node-image-rect')
.attr('width', 10)
.attr('height', 10)
.attr('x', ({
width
}) => width / 2)
.attr('y', ({
height
}) => height / 2)

// Store the old positions for transition.
nodes.forEach(d => {
d.x0 = d.x;
d.y0 = d.y;
});
}

// This function detects whether current browser is edge
isEdge() {
return window.navigator.userAgent.includes("Edge");
}

/* Function converts rgba objects to rgba color string
{red:110,green:150,blue:255,alpha:1} => rgba(110,150,255,1)
*/
rgbaObjToColor({
red,
green,
blue,
alpha
}) {
return `rgba(${red},${green},${blue},${alpha})`;
}

// Generate custom diagonal - play with it here - https://observablehq.com/@bumbeishvili/curved-edges?collection=@bumbeishvili/work-components
diagonal(s, t) {

// Calculate some variables based on source and target (s,t) coordinates
const x = s.x;
const y = s.y;
const ex = t.x;
const ey = t.y;
let xrvs = ex - x < 0 ? -1 : 1;
let yrvs = ey - y < 0 ? -1 : 1;
let rdef = 35;
let rInitial = Math.abs(ex - x) / 2 < rdef ? Math.abs(ex - x) / 2 : rdef;
let r = Math.abs(ey - y) / 2 < rInitial ? Math.abs(ey - y) / 2 : rInitial;
let h = Math.abs(ey - y) / 2 - r;
let w = Math.abs(ex - x) - r * 2;

// Build the path
const path = `
M ${x} ${y}
L ${x} ${y + h * yrvs}
C ${x} ${y + h * yrvs + r * yrvs} ${x} ${y + h * yrvs + r * yrvs} ${x + r * xrvs} ${y + h * yrvs + r * yrvs}
L ${x + w * xrvs + r * xrvs} ${y + h * yrvs + r * yrvs}
C ${ex} ${y + h * yrvs + r * yrvs} ${ex} ${y + h * yrvs + r * yrvs} ${ex} ${ey - h * yrvs}
L ${ex} ${ey}
`
// Return result
return path;
}

restyleForeignObjectElements() {
const attrs = this.getChartState();

attrs.svg.selectAll('.node-foreign-object')
.attr('width', ({
width
}) => width)
.attr('height', ({
height
}) => height)
.attr('x', ({
width
}) => -width / 2)
.attr('y', ({
height
}) => -height / 2)
attrs.svg.selectAll('.node-foreign-object-div')
.style('width', ({
width
}) => `${width}px`)
.style('height', ({
height
}) => `${height}px`)
.style('color', 'white')
.html(({
data
}) => data.template)
}

// Toggle children on click.
onButtonClick(d) {

// If childrens are expanded
if (d.children) {

//Collapse them
d._children = d.children;
d.children = null;

// Set descendants expanded property to false
this.setExpansionFlagToChildren(d, false);
} else {

// Expand children
d.children = d._children;
d._children = null;

// Set each children as expanded
d.children.forEach(({
data
}) => data.expanded = true)
}

// Redraw Graph
this.update(d);
}

// This function changes `expanded` property to descendants
setExpansionFlagToChildren({
data,
children,
_children
}, flag) {

// Set flag to the current property
data.expanded = flag;

// Loop over and recursively update expanded children's descendants
if (children) {
children.forEach(d => {
this.setExpansionFlagToChildren(d, flag)
})
}

// Loop over and recursively update collapsed children's descendants
if (_children) {
_children.forEach(d => {
this.setExpansionFlagToChildren(d, flag)
})
}
}

// This function can be invoked via chart.setExpanded API, it expands or collapses particular node
setExpanded(id, expandedFlag) {
const attrs = this.getChartState();
// Retrieve node by node Id
const node = attrs.allNodes.filter(({
data
}) => data.nodeId == id)[0]

// If node exists, set expansion flag
if (node) node.data.expanded = expandedFlag;

// First expand all nodes
attrs.root.children.forEach(d => this.expand(d));
// Expand root's chilren in case they are collapsed
if(attrs.root._children){
attrs.root._children.forEach(d => this.expand(d));
}
// Then collapse all nodes
attrs.root.children.forEach(d => this.collapse(d));

// Then expand only the nodes, which were previously expanded, or have an expand flag set
attrs.root.children.forEach(d => this.expandSomeNodes(d));

// Redraw graph
this.update(attrs.root);
}

// Method which only expands nodes, which have property set "expanded=true"
expandSomeNodes(d) {

// If node has expanded property set
if (d.data.expanded) {

// Retrieve node's parent
let parent = d.parent;

// While we can go up
while (parent) {

// Expand all current parent's children
if (parent._children) {
parent.children = parent._children;
}

// Replace current parent holding object
parent = parent.parent;
}
}

// Recursivelly do the same for collapsed nodes
if (d._children) {
d._children.forEach(ch => this.expandSomeNodes(ch));
}

// Recursivelly do the same for expanded nodes
if (d.children) {
d.children.forEach(ch => this.expandSomeNodes(ch));
}
}


// This function updates nodes state and redraws graph, usually after data change
updateNodesState() {
const attrs = this.getChartState();
// Store new root by converting flat data to hierarchy
attrs.root = d3.stratify()
.id(({
nodeId
}) => nodeId)
.parentId(({
parentNodeId
}) => parentNodeId)
(attrs.data)

// Store positions, where children appear during their enter animation
attrs.root.x0 = 0;
attrs.root.y0 = 0;

// Store all nodes in flat format (although, now we can browse parent, see depth e.t.c. )
attrs.allNodes = attrs.layouts.treemap(attrs.root).descendants()

// Store direct and total descendants count
attrs.allNodes.forEach(d => {
Object.assign(d.data, {
directSubordinates: d.children ? d.children.length : 0,
totalSubordinates: d.descendants().length - 1
})
})

// Expand all nodes first
attrs.root.children.forEach(this.expand);

// Then collapse them all
attrs.root.children.forEach(d => this.collapse(d));

// Then only expand nodes, which have expanded proprty set to true
attrs.root.children.forEach(ch => this.expandSomeNodes(ch));

// Redraw Graphs
this.update(attrs.root)
}


// Function which collapses passed node and it's descendants
collapse(d) {
if (d.children) {
d._children = d.children;
d._children.forEach(ch => this.collapse(ch));
d.children = null;
}
}

// Function which expands passed node and it's descendants
expand(d) {
if (d._children) {
d.children = d._children;
d.children.forEach(ch => this.expand(ch));
d._children = null;
}
}

// Zoom handler function
zoomed() {
const attrs = this.getChartState();
const chart = attrs.chart;

// Get d3 event's transform object
const transform = d3.event.transform;

// Store it
attrs.lastTransform = transform;

// Reposition and rescale chart accordingly
chart.attr('transform', transform);

// Apply new styles to the foreign object element
if (this.isEdge()) {
this.restyleForeignObjectElements();
}

}

}



Insert cell
Insert cell
Insert cell
Type JavaScript, then Shift-Enter. Ctrl-space for more options. Arrow ↑/↓ to switch modes.

Insert cell
Insert cell
function typescriptify(str){

return str.replace(/\t/g,' ')
.replace(/attrs\s?=/g,'attrs:any = ')
.replace(/calc\s?=/g,'calc:any = ')
.replace(/\(d\)\s*\{/g,'(d:any){')
.replace(/d=>/g,'(d:any)=>')
.replace(/d =>/g,'(d:any)=>')
.replace(/ch =>/g,'(ch:any)=>')
.replace(/\(d, i\)/g,'(d:any,i:number)')
.replace(/\(s, t\)/g,'(s:any,t:any)')
.replace(/\(params\)/g,'(params:any)')
.replace(/\(id, expandedFlag\)/g,'(id:any,expandedFlag:boolean)')
.replace(/\(node, flag\)/g,'(node:any,flag:boolean)')
.replace(/\(node, nodeIdsStore\)/g,'(node:any,nodeIdsStore:any[])')
.replace(/layouts\s?=/g,'layouts:any = ')
.replace(/behaviors\s?=/g,'behaviors:any = ')
.replace(/\(source\)/g,'(source:any)')
.replace(/\(\s?d\s?=>/g,'((d:any)=>')
return str;
}
Insert cell
Insert cell
{
// EXPAND
addBtn;
if(this){
setTimeout(d=>{
const node = {
"nodeId": DOM.uid().id,
"parentNodeId": "O-1",
"width": 330,
"height": 147,
"borderWidth": 1,
"borderRadius": 5,
"borderColor": {
"red": 15,
"green": 140,
"blue": 121,
"alpha": 1
},
"backgroundColor": {
"red": 51,
"green": 182,
"blue": 208,
"alpha": 1
},
"nodeImage": {
"url": "https://raw.githubusercontent.com/bumbeishvili/Assets/master/Projects/D3/Organization%20Chart/general.jpg",
"width": 100,
"height": 100,
"centerTopDistance": 0,
"centerLeftDistance": 0,
"cornerShape": "ROUNDED",
"shadow": false,
"borderWidth": 0,
"borderColor": {
"red": 19,
"green": 123,
"blue": 128,
"alpha": 1
}
},
"nodeIcon": {
"icon": "https://to.ly/1yZnX",
"size": 30
},
"connectorLineColor": {
"red": 220,
"green": 189,
"blue": 207,
"alpha": 1
},
"connectorLineWidth": 5,
"dashArray": "",
"expanded": false,
template:`<div>
<div style="margin-left:80px;
margin-top:10px;
font-size:20px;
font-weight:bold;
">Added Root Child </div>
<div style="margin-left:80px;
margin-top:3px;
font-size:16px;
">Added position </div>

<div style="margin-left:80px;
margin-top:3px;
font-size:14px;
">Added unit</div>

<div style="margin-left:200px;
margin-top:15px;
font-size:13px;
position:absolute;
bottom:5px;
">
<div>Added office</div>
<div style="margin-top:5px">Added area</div>
</div>
</div>`
}

chart.addNode(node)
},600)
return true;
}
return !this;
}
Insert cell
{
// EXPAND
addBtnG;
if(this){
setTimeout(d=>{
const node = {
"nodeId": DOM.uid().id,
"parentNodeId": "O-2",
"width": 330,
"height": 147,
"borderWidth": 1,
"borderRadius": 5,
"borderColor": {
"red": 15,
"green": 140,
"blue": 121,
"alpha": 1
},
"backgroundColor": {
"red": 51,
"green": 182,
"blue": 208,
"alpha": 1
},
"nodeImage": {
"url": "https://raw.githubusercontent.com/bumbeishvili/Assets/master/Projects/D3/Organization%20Chart/general.jpg",
"width": 100,
"height": 100,
"centerTopDistance": 0,
"centerLeftDistance": 0,
"cornerShape": "ROUNDED",
"shadow": false,
"borderWidth": 0,
"borderColor": {
"red": 19,
"green": 123,
"blue": 128,
"alpha": 1
}
},
"nodeIcon": {
"icon": "https://to.ly/1yZnX",
"size": 30
},
"connectorLineColor": {
"red": 220,
"green": 189,
"blue": 207,
"alpha": 1
},
"connectorLineWidth": 5,
"dashArray": "",
"expanded":true,
template:`<div>
<div style="margin-left:80px;
margin-top:10px;
font-size:20px;
font-weight:bold;
">Added Root Grand Child </div>
<div style="margin-left:80px;
margin-top:3px;
font-size:16px;
">Added position </div>

<div style="margin-left:80px;
margin-top:3px;
font-size:14px;
">Added unit</div>

<div style="margin-left:200px;
margin-top:15px;
font-size:13px;
position:absolute;
bottom:5px;
">
<div>Added office</div>
<div style="margin-top:5px">Added area</div>
</div>
</div>`
}

chart.addNode(node)
},600)
return true;
}
return !this;
}
Insert cell
{
// EXPAND
removeBtn;
if(this){
setTimeout(d=>{
chart.removeNode("O-5")
},300)
return true;
}
return !this;
}
Insert cell
{
removeGrandChildBtn;
if(this){
setTimeout(d=>{
chart.removeNode("O-6")
},450)
return true;
}
return !this;
}
Insert cell
{
chartRendering;
//ZOOM
chart.setZoomFactor(zoom);
}
Insert cell
{
// EXPAND
expandBtn;
if(this){
setTimeout(d=>{
chart.setExpanded("O-111",true)
},900)
return Math.random();
}
return !this;
}
Insert cell
{
// COLLAPSE
collapseBtn;
if(this){
setTimeout(d=>{
chart.setExpanded("O-111",false)
},100)
return true;
}
return !this;
}
Insert cell
Insert cell
Insert cell
dataFlattened = d3.hierarchy(dataSource)
.descendants()
.map((d,i)=>Object.assign(d,{id:DOM.uid().id}))
.map(d=>Object.assign(d.data,{
id:d.id,
parentId:d.parent && d.parent.id
}))
Insert cell
Insert cell
d3 = require('d3@v5')
Insert cell
Insert cell
Insert cell

One platform to build and deploy the best data apps

Experiment and prototype by building visualizations in live JavaScript notebooks. Collaborate with your team and decide which concepts to build out.
Use Observable Framework to build data apps locally. Use data loaders to build in any language or library, including Python, SQL, and R.
Seamlessly deploy to Observable. Test before you ship, use automatic deploy-on-commit, and ensure your projects are always up-to-date.
Learn more