<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
/*circle {
fill: orange;
}*/
</style>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/6.2.0/d3.min.js"></script>
<script src="https://dagrejs.github.io/project/dagre/latest/dagre.min.js"></script>
<script src="https://dagrejs.github.io/project/dagre-d3/latest/dagre-d3.js"></script>
</head>
<body>
<div id="content">
<svg></svg>
<br>
<button type="button" onclick="setforce('free')">Free</button>
<button type="button" onclick="setforce('grouped')">Grouped</button>
<button type="button" onclick="setforce('dagre')">Dagre</button>
<!-- <svg width="700" height="400">
<g transform="translate(50, 200)"></g>
</svg> -->
</div>
<script>
var width = 1200, height = 800;
// zoom
// https://bl.ocks.org/puzzler10/49f13307e818ea9a909ba5adba5b6ed9
let zoom = d3.zoom()
.on('zoom', (event) => {
// d3.select('svg g')
// .attr('transform', event.transform)
d3.select('svg g.container')
.attr('transform', event.transform)
d3.select('svg g.overlay')
.attr('transform', event.transform)
})
const svg = d3.select('svg')
.attr('width', width)
.attr('height', height)
.style('border', '1px solid black')
const container = svg.append('g').classed('container', true).attr('x', 0).attr('y', 0)
const overlay = svg.append('g').classed('overlay', true).attr('x', 0).attr('y', 0)
svg.call(zoom)
var colorScale = [
'orange',
'lightblue',
'#B19CD9', // purple
'#b7eb8f', // green
'#ffadd2', // pink
'#87e8de', // cyan blue
];
var xCenter = [100, 300, 500];
w4 = 250
const xGap = 950
const yGap = 900
const xStart = 250
const yStart = 200
const yCols = [
3, // O O O
2, // O O
7, // O O O O O O O
4, // O O O O
1 // O
]
const totalGroups = d3.sum(yCols)
var color
//color = d3.scaleSequential().domain([0, totalGroups]).range(d3.schemeSet3);
color = d3.scaleDiverging().interpolator(d3.interpolateSpectral).domain([0, totalGroups])
//color = d3.scaleDiverging().interpolator(d3.interpolateRainbow).domain([0, totalGroups])
// var xyCenter = [
// // [200, 200],
// // [600, 200],
// // [1000, 200],
// // [200, 650],
// // [600, 650],
// // [1000, 650],
// [w4*col1, 200],
// [w4*col2, 200],
// [w4*col3, 200],
// [w4*col1, 800],
// [w4*col2, 800],
// [w4*col3, 800],
// ]
var xyCenter = yCols.reduce((g,c,i) => {
console.log('reduce', g, c, i)
for (let x = 0; x < c; x++)
g = [...g, [xStart + (x*xGap), yStart + (i*yGap)]]
return g
}, [])
console.log(xyCenter)
// var numNodes = 100;
// var nodes = d3.range(numNodes).map(function(d, i) {
// return {
// radius: Math.random() * 25,
// category: i % 6
// }
// });
var customNodes = [...new Array(d3.sum(yCols))].reduce((g,c,i) => {
const limit = 7 + Math.floor(Math.random()*8)
for (let x = 0; x < limit; x++){
g.push({
radius: 30,
category: i,
gender: (g.length%2 == 0) ? 'men' : 'women',
id: (g.length%2==0) ? g.length : g.length-1,
})
}
return g
}, [])
//https://stackoverflow.com/questions/7430580/setting-rounded-corners-for-svgimage
var defs = svg.append("defs")
const avatarRadius = 25
defs.append("clipPath")
.attr("id", "avatar-clip")
.append("circle")
.attr("cx", avatarRadius)
.attr("cy", avatarRadius)
.attr("r", avatarRadius)
var simulation, link, links, nodes, gnodes, _gnodes, _glinks
function setforce(t){
//d3.select('svg g').selectAll('circle.tag').remove()
overlay.html('')
if (t == 'free'){
simulation
.force('x', null)
.force('y', null)
.force('center', d3.forceCenter(width / 2, height / 2))
.force('link', d3.forceLink(links)
.distance(20)
.strength(0.1)
.iterations(2)
)
.force('charge', d3.forceManyBody().strength(-200))
//.force('charge', null)
.force('collision', d3.forceCollide().radius(function(d) {
return d.radius*2;
}))
simulation
//.alphaDecay(0.01)
.alpha(0.5)
.alphaTarget(0.01)
.restart();
}else if (t == 'grouped'){
overlay
.selectAll('circle.tag')
.data(xyCenter)
.join('circle')
.classed('tag', true)
.attr('r', 5)
.attr('cx', d => d[0])
.attr('cy', d => d[1])
.attr('fill', 'red')
simulation
.force('link', d3.forceLink(links)
.distance(0)
.strength(0)
.iterations(0)
)
.force('x', d3.forceX().x(function(d) {
return xyCenter[d.category][0];
})
//.strength(0)
)
.force('y', d3.forceY().y(function(d) {
return xyCenter[d.category][1];
}))
.force('center', null)
.force('charge', d3.forceManyBody().strength(5))
.force('collision', d3.forceCollide().radius(function(d) {
return d.radius*1.3;
}))
simulation
.alpha(1)
.alphaTarget(0.1)
.restart();
setTimeout(() => {
labelAnimation()
}, 200)
}else if (t == 'dagre'){
simulation.stop()
d3.selectAll('svg g line.t1').attr('stroke', 'none')
const g = new dagre.graphlib.Graph();
g.setGraph({
rankdir: 'TB',
nodesep: 100,
ranksep: 100,
//ranker: 'longest-path',
ranker: 'tight-tree',
//ranker: 'network-simplex', // default
});
g.setDefaultEdgeLabel(function() { return {}; });
lnodes = JSON.parse(_gnodes)
llinks = JSON.parse(_glinks)
lnodes.map((d,i) => {
g.setNode(i, {width: 30, height: 30})
})
llinks.map((d,i) => {
g.setEdge(d.source, d.target)
})
dagre.layout(g)
g.nodes().forEach(function(v) {
//console.log("Node " + v + ": " + JSON.stringify(g.node(v)));
});
g.edges().forEach(function(e) {
//console.log("Edge " + e.v + " -> " + e.w + ": " + JSON.stringify(g.edge(e)));
});
simulation
.force('link', d3.forceLink(links)
.distance(0)
.strength(0)
.iterations(0)
)
.force('x', d3.forceX().x(function(d,i) {
return g.node(i).x;
})
)
.force('y', d3.forceY().y(function(d,i) {
return g.node(i).y
}))
.force('center', null)
.force('charge', null)
.force('collision', null)
simulation
.alpha(1)
.alphaTarget(0.1)
.restart();
}
}
const labelAnimation = () => {
const localg = overlay.selectAll('g.categoryLabels')
.data(xyCenter)
.enter()
.append('g')
.classed('categoryLabels', true)
.attr('transform', d => `translate(${d[0]-80}, ${d[1]+140})`)
localg.append('rect')
.attr('fill', 'lightgrey')
.attr('width', 200)
.attr('height', 60)
.attr('x', 0)
.attr('y', 0)
.attr('opacity', '0.8')
localg.append('text')
.attr('x', 100)
.attr('y', 38)
.attr('text-anchor', 'middle')
.attr('font-size', '25px')
.text((d,i) => `Category ${i}`)
localg.transition()
.duration(600)
.attr('transform', d => `translate(${d[0]-100}, ${d[1]+140})`)
}
const drag = simulation => {
function dragstarted(event, d) {
if (!event.active) simulation.alphaTarget(0.4).restart();
d.fx = d.x;
d.fy = d.y;
}
function dragged(event, d) {
d.fx = event.x;
d.fy = event.y;
}
function dragended(event, d) {
if (!event.active) simulation.alphaTarget(0.5);
d.fx = null;
d.fy = null;
}
return d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended);
}
const bfs = (head, nodes=[], links=[]) => {
head.idx = nodes.length
nodes.push(head.idx)
if (head.children){
for (let c of head.children){
bfs(c, nodes, links)
links.push({
source: nodes.indexOf(head.idx),
target: nodes.indexOf(c.idx),
})
}
}
return [nodes, links]
}
const parseData = (data) => {
console.log(data)
return bfs(data)
}
d3.json('redesignedChartLongData.json').then(rs => {
// nodes = rs.nodes.map((d,i) => {
// return {
// ...d,
// radius: 30,
// category: +d.group > 5 ? 5 : +d.group,
// }
// })
// nodes = [...customNodes]
// links = rs.links.map(d => Object.create(d))
let [pnodes, plinks] = parseData(rs)
links = plinks
nodes = pnodes.map((d,i) => {
return {
radius: 30,
category: Math.floor(Math.random()*totalGroups),
gender: (i%2 == 0) ? 'men' : 'women',
id: (i%2==0) ? i : i-1,
}
})
_gnodes = JSON.stringify(nodes)
_glinks = JSON.stringify(links)
simulation = d3.forceSimulation(nodes)
.force('link', d3.forceLink(links)
.distance(0)
.strength(0)
)
.force('x', d3.forceX().x(function(d) {
return xyCenter[d.category][0];
}))
.force('y', d3.forceY().y(function(d) {
return xyCenter[d.category][1];
}))
.force('charge', d3.forceManyBody().strength(5))
.force('center', d3.forceCenter(width / 2, height / 2))
.force('collision', d3.forceCollide().radius(function(d) {
return d.radius;
}))
.on('tick', ticked);
//.attr("stroke", d => color(d.type))
d3.select('svg g')
.selectAll('line')
.data(links)
.join('line')
.attr('stroke', 'black')
.attr('stroke-width', '1')
const randomAvatar = (gender, idx) => {
//const gender = Math.random()*100 > 50 ? 'women' : 'men'
return ''
return `https://randomuser.me/api/portraits/${gender}/${idx}.jpg`
}
gnodes = container.selectAll('g')
.data(nodes)
.join('g')
.call(drag(simulation))
gnodes.append('circle')
.attr('r', function(d) {
return d.radius;
})
.style('fill', function(d) {
return color(d.category)
//return colorScale[d.category];
})
gnodes
.append('svg:image')
.attr('width', '50px')
.attr('height', '50px')
.attr('xlink:href', (d, i) => randomAvatar(d.gender, d.id))
.attr("clip-path", "url(#avatar-clip")
.attr('transform', 'translate(-25,-25)')
function ticked() {
d3.select('svg g')
.selectAll('line')
.join('line')
.attr("x1", function(d) { return d.source.x; })
.attr("y1", function(d) { return d.source.y; })
.attr("x2", function(d) { return d.target.x; })
.attr("y2", function(d) { return d.target.y; });
gnodes
.attr('transform', d => `translate(${d.x}, ${d.y})`)
// var u = d3.select('svg g')
// .selectAll('circle')
// .data(nodes)
// .join('circle')
// .attr('r', function(d) {
// return d.radius;
// })
// .style('fill', function(d) {
// return colorScale[d.category];
// })
// .attr('cx', function(d) {
// return d.x;
// })
// .attr('cy', function(d) {
// return d.y;
// })
}
//setforce('free')
//setforce('grouped')
setforce('dagre')
})
</script>
</body>
</html>
https://bl.ocks.org/willzjc/a11626a31c65ba5d319fcf8b8870f281
dataset from https://raw.githubusercontent.com/bumbeishvili/Assets/master/Projects/D3/Organization%20Chart/redesignedChartLongData.json