Built with blockbuilder.org
Last active
April 9, 2021 20:33
-
-
Save bumbeishvili/09a03b81ae788d2d14f750afe59eb7de to your computer and use it in GitHub Desktop.
Organizational chart using d3.v5
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
license: mit |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<title> D3 V5 Org Chart</title> | |
</head> | |
<body translate="no"> | |
<div class="chart-container" style=" padding-top:10px; width:1200px; height:1200px "> </div> | |
<a href="https://github.com/bumbeishvili/d3-organization-chart"> | |
<img style="position:fixed;top:0;right:0;border:0;z-index:2;" width="149" height="149" src="https://bumbeishvili.github.io/d3-tooltip/forkme.png" alt="Fork me on GitHub"> | |
</a> | |
<script src='https://d3js.org/d3.v5.min.js'></script> | |
<script id="rendered-js"> | |
d3.json('https://gist.githubusercontent.com/bumbeishvili/dc0d47bc95ef359fdc75b63cd65edaf2/raw/c33a3a1ef4ba927e3e92b81600c8c6ada345c64b/orgChart.json') | |
.then(data=>{ | |
Chart() | |
.container('.chart-container') | |
.data(data) | |
.svgWidth(window.innerWidth) | |
.svgHeight(window.innerHeight) | |
.initialZoom(0.6) | |
.onNodeClick(d=> console.log(d+' node clicked')) | |
.render() | |
}) | |
function Chart() { | |
// Exposed variables | |
var 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: '#fafafa', | |
data: null, | |
depth: 180, | |
duration: 600, | |
strokeWidth: 3, | |
dropShadowId: null, | |
initialZoom:1, | |
onNodeClick:d=>d | |
}; | |
//InnerFunctions which will update visuals | |
var updateData; | |
//Main chart object | |
var main = function() { | |
//Drawing containers | |
var container = d3.select(attrs.container); | |
var containerRect = container.node().getBoundingClientRect(); | |
if (containerRect.width > 0) attrs.svgWidth = containerRect.width; | |
setDropShadowId(attrs); | |
//Calculated properties | |
var 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; | |
calc.nodeMaxWidth = d3.max(attrs.data, d => d.width); | |
calc.nodeMaxHeight = d3.max(attrs.data, d => d.height); | |
attrs.depth = calc.nodeMaxHeight + 100; | |
calc.centerX = calc.chartWidth / 2; | |
//******************** LAYOUTS *********************** | |
const layouts = { | |
treemap: null | |
} | |
layouts.treemap = d3.tree().size([calc.chartWidth, calc.chartHeight]) | |
.nodeSize([calc.nodeMaxWidth + 100, calc.nodeMaxHeight + attrs.depth]) | |
// ******************* BEHAVIORS . ********************** | |
const behaviors = { | |
zoom: null | |
} | |
behaviors.zoom = d3.zoom().on("zoom", zoomed) | |
//****************** ROOT node work ************************ | |
const root = d3.stratify() | |
.id(function(d) { | |
return d.nodeId; | |
}) | |
.parentId(function(d) { | |
return d.parentNodeId; | |
}) | |
(attrs.data) | |
root.x0 = 0; | |
root.y0 = 0; | |
const allNodes = layouts.treemap(root).descendants() | |
allNodes.forEach(d=>{ | |
Object.assign(d.data,{ | |
directSubordinates:d.children?d.children.length:0, | |
totalSubordinates:d.descendants().length-1 | |
}) | |
}) | |
root.children.forEach(collapse); | |
root.children.forEach(expandSomeNodes); | |
//Add svg | |
var 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) | |
.attr('cursor', 'move') | |
.style('background-color', attrs.backgroundColor) | |
//Add container g element | |
var chart = svg | |
.patternify({ | |
tag: 'g', | |
selector: 'chart' | |
}) | |
.attr('transform', 'translate(' + calc.chartLeftMargin + ',' + calc.chartTopMargin + ')'); | |
var centerG = chart.patternify({ | |
tag: 'g', | |
selector: 'center-group' | |
}) | |
.attr('transform', `translate(${calc.centerX},${calc.nodeMaxHeight/2}) scale(${attrs.initialZoom})`) | |
if(attrs.lastTransform) | |
{ | |
behaviors.zoom | |
.scaleBy(chart,attrs.lastTransform.k) | |
.translateTo(chart,attrs.lastTransform.x,attrs.lastTransform.y) | |
} | |
const defs = svg.patternify({ | |
tag: 'defs', | |
selector: 'image-defs' | |
}); | |
const filterDefs = svg.patternify({ | |
tag: 'defs', | |
selector: 'filter-defs' | |
}); | |
var filter = filterDefs.patternify({tag:'filter',selector:'shadow-filter-element'}) | |
.attr('id', attrs.dropShadowId) | |
.attr('y',`${-50}%`) | |
.attr('x',`${-50}%`) | |
.attr('height', `${200}%`) | |
.attr('width',`${200}%`) | |
filter.patternify({tag:'feGaussianBlur',selector:'feGaussianBlur-element'}) | |
.attr('in', 'SourceAlpha') | |
.attr('stdDeviation', 3.1) | |
.attr('result', 'blur'); | |
filter.patternify({tag:'feOffset',selector:'feOffset-element'}) | |
.attr('in', 'blur') | |
.attr('result', 'offsetBlur') | |
.attr("dx", 4.28) | |
.attr("dy", 4.48) | |
.attr("x", 8) | |
.attr("y", 8) | |
filter.patternify({tag:'feFlood',selector:'feFlood-element'}) | |
.attr("in", "offsetBlur") | |
.attr("flood-color",'black') | |
.attr("flood-opacity", 0.3) | |
.attr("result", "offsetColor"); | |
filter.patternify({tag:'feComposite',selector:'feComposite-element'}) | |
.attr("in", "offsetColor") | |
.attr("in2", "offsetBlur") | |
.attr("operator", "in") | |
.attr("result", "offsetBlur"); | |
var feMerge = filter.patternify({tag:'feMerge',selector:'feMerge-element'}) | |
feMerge.patternify({tag:'feMergeNode',selector:'feMergeNode-blur'}) | |
.attr('in', 'offsetBlur') | |
feMerge.patternify({tag:'feMergeNode',selector:'feMergeNode-graphic'}) | |
.attr('in', 'SourceGraphic') | |
// Display tree contenrs | |
update(root) | |
// Smoothly handle data updating | |
updateData = function() {}; | |
//######################################### UTIL FUNCS ################################## | |
function setDropShadowId(d) { | |
if (d.dropShadowId) return; | |
let id = d.id + "-drop-shadow"; | |
//@ts-ignore | |
if (typeof DOM != 'undefined') { | |
//@ts-ignore | |
id = DOM.uid(d.id).id; | |
} | |
Object.assign(d, { | |
dropShadowId: id | |
}) | |
} | |
function rgbaObjToColor(d) { | |
return `rgba(${d.red},${d.green},${d.blue},${d.alpha})` | |
} | |
// Zoom handler func | |
function zoomed() { | |
var transform = d3.event.transform; | |
attrs.lastTransform = transform; | |
chart.attr('transform', transform); | |
} | |
// Toggle children on click. | |
function click(d) { | |
if (d.children) { | |
d._children = d.children; | |
d.children = null; | |
} else { | |
d.children = d._children; | |
d._children = null; | |
} | |
update(d); | |
} | |
function diagonal(s, t) { | |
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 r = Math.abs(ex - x) / 2 < rdef ? Math.abs(ex - x) / 2 : rdef; | |
r = Math.abs(ey - y) / 2 < r ? Math.abs(ey - y) / 2 : r; | |
let h = Math.abs(ey - y) / 2 - r; | |
let w = Math.abs(ex - x) - r * 2; | |
//w=0; | |
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 path; | |
} | |
function collapse(d) { | |
if (d.children) { | |
d._children = d.children; | |
d._children.forEach(collapse); | |
d.children = null; | |
} | |
} | |
function expandSomeNodes(d){ | |
if(d.data.expanded){ | |
let parent = d.parent; | |
while(parent){ | |
if(parent._children){ | |
parent.children = parent._children; | |
//parent._children=null; | |
} | |
parent = parent.parent; | |
} | |
} | |
if(d._children){ | |
d._children.forEach(expandSomeNodes); | |
} | |
} | |
function update(source) { | |
// Assigns the x and y position for the nodes | |
const treeData = layouts.treemap(root); | |
// Get tree nodes and links | |
const nodes = treeData.descendants().map(d => { | |
if (d.width) return d; | |
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` | |
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 = 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 = rgbaObjToColor(d.data.borderColor); | |
} | |
if (d.data.backgroundColor) { | |
backgroundColor = 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; | |
} | |
return Object.assign(d, { | |
imageWidth, | |
imageHeight, | |
imageBorderColor, | |
imageBorderWidth, | |
borderColor, | |
backgroundColor, | |
imageRx, | |
width, | |
height, | |
imageCenterTopDistance, | |
imageCenterLeftDistance, | |
dropShadowId | |
}); | |
}); | |
const links = treeData.descendants().slice(1); | |
// Set constant depth for each nodes | |
nodes.forEach(d => d.y = d.depth * attrs.depth); | |
// ------------------- FILTERS --------------------- | |
const patternsSelection = defs.selectAll('.pattern') | |
.data(nodes, d => d.id); | |
const patternEnterSelection = patternsSelection.enter().append('pattern') | |
const patterns = patternEnterSelection | |
.merge(patternsSelection) | |
.attr('class', 'pattern') | |
.attr('height', 1) | |
.attr('width', 1) | |
.attr('id', d => d.id) | |
const patternImages = patterns.patternify({ | |
tag: 'image', | |
selector: 'pattern-image', | |
data: d => [d] | |
}) | |
.attr('x', 0) | |
.attr('y', 0) | |
.attr('height', d => d.imageWidth) | |
.attr('width', d => d.imageHeight) | |
.attr('xlink:href', d => d.data.nodeImage.url) | |
.attr('viewbox', d => `0 0 ${d.imageWidth*2} ${d.imageHeight}`) | |
.attr('preserveAspectRatio', 'xMidYMin slice') | |
patternsSelection.exit().transition().duration(attrs.duration).remove(); | |
// -------------------------- LINKS ---------------------- | |
// Update the links... | |
var linkSelection = centerG.selectAll('path.link') | |
.data(links, function(d) { | |
return d.id; | |
}); | |
// Enter any new links at the parent's previous position. | |
var linkEnter = linkSelection.enter() | |
.insert('path', "g") | |
.attr("class", "link") | |
.attr('d', function(d) { | |
var o = { | |
x: source.x0, | |
y: source.y0 | |
} | |
return diagonal(o, o) | |
}); | |
// UPDATE | |
var linkUpdate = linkEnter.merge(linkSelection) | |
// Styling links | |
linkUpdate | |
.attr("fill", "none") | |
.attr("stroke-width", d => d.data.connectorLineWidth || 2) | |
.attr('stroke', d => { | |
if (d.data.connectorLineColor) { | |
return rgbaObjToColor(d.data.connectorLineColor); | |
} | |
return 'green'; | |
}) | |
.attr('stroke-dasharray', d => { | |
if (d.data.dashArray) { | |
return d.data.dashArray; | |
} | |
return ''; | |
}) | |
// Transition back to the parent element position | |
linkUpdate.transition() | |
.duration(attrs.duration) | |
.attr('d', function(d) { | |
return diagonal(d, d.parent) | |
}); | |
// Remove any exiting links | |
var linkExit = linkSelection.exit().transition() | |
.duration(attrs.duration) | |
.attr('d', function(d) { | |
var o = { | |
x: source.x, | |
y: source.y | |
} | |
return diagonal(o, o) | |
}) | |
.remove(); | |
// -------------------------- NODES ---------------------- | |
// Updating nodes | |
const nodesSelection = centerG.selectAll('g.node') | |
.data(nodes, d => d.id) | |
// Enter any new nodes at the parent's previous position. | |
var nodeEnter = nodesSelection.enter().append('g') | |
.attr('class', 'node') | |
.attr("transform", function(d) { | |
return "translate(" + source.x0 + "," + source.y0 + ")"; | |
}) | |
.attr('cursor', 'pointer') | |
.on('click', function(d){ | |
if([...d3.event.srcElement.classList]. | |
includes('node-button-circle')){ | |
return; | |
} | |
attrs.onNodeClick(d.data.nodeId); | |
}) | |
// Add rectangle for the nodes | |
nodeEnter | |
.patternify({ | |
tag: 'rect', | |
selector: 'node-rect', | |
data: d => [d] | |
}) | |
.attr('width', 1e-6) | |
.attr('height', 1e-6) | |
.style("fill", function(d) { | |
return d._children ? "lightsteelblue" : "#fff"; | |
}) | |
// Add foreignObject element | |
const fo = nodeEnter | |
.patternify({ | |
tag: 'foreignObject', | |
selector: 'node-foreign-object', | |
data: d => [d] | |
}) | |
.attr('width', d=> d.width) | |
.attr('height',d=> d.height) | |
.attr('x', d=> -d.width/2) | |
.attr('y', d=> -d.height/2 ) | |
// Add foreign object | |
fo.patternify({ | |
tag: 'xhtml:div', | |
selector: 'node-foreign-object-div', | |
data: d => [d] | |
}) | |
.style('width', d=> d.width + 'px') | |
.style('height',d=> d.height + 'px') | |
.style('color', 'white') | |
.html(d=>d.data.template) | |
nodeEnter | |
.patternify({ | |
tag: 'image', | |
selector: 'node-icon-image', | |
data: d => [d] | |
}) | |
.attr('width', d=> d.data.nodeIcon.size) | |
.attr('height', d=> d.data.nodeIcon.size) | |
.attr("xlink:href",d=>d.data.nodeIcon.icon) | |
.attr('x',d=>-d.width/2+5) | |
.attr('y',d=> d.height/2 - d.data.nodeIcon.size-5) | |
nodeEnter | |
.patternify({ | |
tag: 'text', | |
selector: 'node-icon-text-total', | |
data: d => [d] | |
}) | |
.text('test') | |
.attr('x',d=>-d.width/2+7 ) | |
.attr('y',d=> d.height/2 - d.data.nodeIcon.size-5) | |
//.attr('text-anchor','middle') | |
.text(d=>d.data.totalSubordinates + ' Subordinates') | |
.attr('fill',attrs.nodeTextFill) | |
.attr('font-weight','bold') | |
nodeEnter | |
.patternify({ | |
tag: 'text', | |
selector: 'node-icon-text-direct', | |
data: d => [d] | |
}) | |
.text('test') | |
.attr('x',d=>-d.width/2+10+d.data.nodeIcon.size) | |
.attr('y',d=> d.height/2 - 10) | |
.text(d=>d.data.directSubordinates +' Direct ') | |
.attr('fill',attrs.nodeTextFill) | |
.attr('font-weight','bold') | |
// Node images | |
const nodeImageGroups = nodeEnter.patternify({ | |
tag: 'g', | |
selector: 'node-image-group', | |
data: d => [d] | |
}) | |
// Node image rectangle | |
nodeImageGroups | |
.patternify({ | |
tag: 'rect', | |
selector: 'node-image-rect', | |
data: d => [d] | |
}) | |
// Node button circle group | |
const nodeButtonGroups = nodeEnter | |
.patternify({ | |
tag: 'g', | |
selector: 'node-button-g', | |
data: d => [d] | |
}) | |
.on('click', click) | |
// Add 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') | |
// Node update styles | |
var nodeUpdate = nodeEnter.merge(nodesSelection) | |
.style('font', '12px sans-serif') | |
// Transition to the proper position for the node | |
nodeUpdate.transition() | |
.attr('opacity', 0) | |
.duration(attrs.duration) | |
.attr("transform", function(d) { | |
return "translate(" + d.x + "," + d.y + ")"; | |
}) | |
.attr('opacity', 1) | |
// Move images to desired positions | |
nodeUpdate.selectAll('.node-image-group') | |
.attr('transform', d => { | |
let x = -d.imageWidth / 2 - d.width / 2; | |
let y = -d.imageHeight / 2 - d.height / 2; | |
return `translate(${x},${y})` | |
}) | |
nodeUpdate.select('.node-image-rect') | |
.attr('fill', d => `url(#${d.id})`) | |
.attr('width', d => d.imageWidth) | |
.attr('height', d => d.imageHeight) | |
.attr('stroke', d => d.imageBorderColor) | |
.attr('stroke-width', d => d.imageBorderWidth) | |
.attr('rx', d => d.imageRx) | |
.attr('y', d => d.imageCenterTopDistance) | |
.attr('x', d => d.imageCenterLeftDistance) | |
.attr('filter',d=> d.dropShadowId) | |
// Update node attributes and style | |
nodeUpdate.select('.node-rect') | |
.attr('width', d => d.data.width) | |
.attr('height', d => d.data.height) | |
.attr('x', d => -d.data.width / 2) | |
.attr('y', d => -d.data.height / 2) | |
.attr('rx', d => d.data.borderRadius || 0) | |
.attr('stroke-width', d => d.data.borderWidth || attrs.strokeWidth) | |
.attr('cursor', 'pointer') | |
.attr('stroke', d => d.borderColor) | |
.style("fill", d => d.backgroundColor) | |
// Move node button group to the desired position | |
nodeUpdate.select('.node-button-g') | |
.attr('transform', d => { | |
return `translate(0,${d.data.height/2})` | |
}) | |
.attr('opacity', d => { | |
if (d.children || d._children) { | |
return 1; | |
} | |
return 0; | |
}) | |
// Restyle node button circle | |
nodeUpdate.select('.node-button-circle') | |
.attr('r', 16) | |
.attr('stroke-width', d => d.data.borderWidth || attrs.strokeWidth) | |
.attr('fill', attrs.backgroundColor) | |
.attr('stroke', d => d.borderColor) | |
// Restyle texts | |
nodeUpdate.select('.node-button-text') | |
.attr('text-anchor', 'middle') | |
.attr('alignment-baseline', 'middle') | |
.attr('fill', attrs.defaultTextFill) | |
.attr('font-size', d => { | |
if (d.children) return 40; | |
return 26; | |
}) | |
.text(d => { | |
if (d.children) return '-'; | |
return '+'; | |
}) | |
// Remove any exiting nodes | |
var nodeExitTransition = nodesSelection.exit() | |
.attr('opacity', 1) | |
.transition() | |
.duration(attrs.duration) | |
.attr("transform", function(d) { | |
return "translate(" + source.x + "," + source.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', d => d.width / 2) | |
.attr('y', d => d.height / 2) | |
// Store the old positions for transition. | |
nodes.forEach(function(d) { | |
d.x0 = d.x; | |
d.y0 = d.y; | |
}); | |
// debugger; | |
} | |
d3.select(window).on('resize.' + attrs.id, function() { | |
var containerRect = container.node().getBoundingClientRect(); | |
// if (containerRect.width > 0) attrs.svgWidth = containerRect.width; | |
// main(); | |
}); | |
}; | |
//----------- PROTOTYPE FUNCTIONS ---------------------- | |
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; | |
}; | |
//Dynamic keys functions | |
Object.keys(attrs).forEach((key) => { | |
// Attach variables to main function | |
//@ts-ignore | |
main[key] = function(_) { | |
var string = `attrs['${key}'] = _`; | |
if (!arguments.length) { | |
return eval(` attrs['${key}'];`); | |
} | |
eval(string); | |
return main; | |
}; | |
return main; | |
}); | |
//Set attrs as property | |
//@ts-ignore | |
main['attrs'] = attrs; | |
//Exposed update functions | |
//@ts-ignore | |
main['data'] = function(value) { | |
if (!arguments.length) return attrs.data; | |
attrs.data = value; | |
if (typeof updateData === 'function') { | |
updateData(); | |
} | |
return main; | |
}; | |
// Run visual | |
//@ts-ignore | |
main['render'] = function() { | |
main(); | |
return main; | |
}; | |
return main; | |
} | |
</script> | |
<script src="https://static.codepen.io/assets/editor/live/css_reload-5619dc0905a68b2e6298901de54f73cefe4e079f65a75406858d92924b4938bf.js"></script> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment