Skip to content

Instantly share code, notes, and snippets.

@nacmacfeegle
Created May 3, 2018 08:13
Show Gist options
  • Save nacmacfeegle/aedf839dd5cfd42c37f42e29fb101cd9 to your computer and use it in GitHub Desktop.
Save nacmacfeegle/aedf839dd5cfd42c37f42e29fb101cd9 to your computer and use it in GitHub Desktop.
d3.v4 collapsible force zoomable draggable radial tree using svg
license: mit
{
"name": "name0",
"children": [
{
"name": "name1",
"children": [
{
"name": "name2",
"children": [
{
"children": [
{
"name": "name4"
},
{
"name": "name5"
},
{
"name": "name6"
},
{
"name": "name7"
},
{
"name": "name8"
},
{
"name": "name9"
},
{
"name": "name10"
},
{
"name": "name11"
},
{
"name": "name12"
},
{
"name": "name13"
},
{
"name": "name14"
},
{
"name": "name15"
},
{
"name": "name16"
},
{
"name": "name17"
},
{
"name": "name18"
},
{
"name": "name19"
},
{
"name": "name20"
},
{
"name": "name21"
},
{
"name": "name22"
},
{
"name": "name23"
},
{
"name": "name24"
},
{
"name": "name25"
},
{
"name": "name26"
},
{
"name": "name27"
},
{
"name": "name28"
},
{
"name": "name29"
},
{
"name": "name30"
},
{
"name": "name31"
},
{
"name": "name32"
},
{
"name": "name33"
},
{
"name": "name34"
},
{
"name": "name35"
},
{
"name": "name36"
},
{
"name": "name37"
},
{
"name": "name38"
},
{
"name": "name39"
},
{
"name": "name40"
},
{
"name": "name41"
},
{
"name": "name42"
},
{
"name": "name43"
},
{
"name": "name44"
},
{
"name": "name45"
},
{
"name": "name46"
},
{
"name": "name47"
},
{
"name": "name48"
},
{
"name": "name49"
},
{
"name": "name50"
},
{
"name": "name51"
},
{
"name": "name52"
},
{
"name": "name53"
},
{
"name": "name54"
},
{
"name": "name55"
},
{
"name": "name56"
},
{
"name": "name57"
},
{
"name": "name58"
},
{
"name": "name59"
},
{
"name": "name60"
},
{
"name": "name61"
},
{
"name": "name62"
},
{
"name": "name63"
},
{
"name": "name64"
},
{
"name": "name65"
},
{
"name": "name66"
},
{
"name": "name67"
},
{
"name": "name68"
},
{
"name": "name69"
},
{
"name": "name70"
},
{
"name": "name71"
},
{
"name": "name72"
},
{
"name": "name73"
},
{
"name": "name74"
},
{
"name": "name75"
},
{
"name": "name76"
},
{
"name": "name77"
},
{
"name": "name78"
},
{
"name": "name79"
},
{
"name": "name80"
},
{
"name": "name81"
},
{
"name": "name82"
},
{
"name": "name83"
},
{
"name": "name84"
},
{
"name": "name85"
},
{
"name": "name86"
},
{
"name": "name87"
},
{
"name": "name88"
},
{
"name": "name89"
},
{
"name": "name90"
},
{
"name": "name91"
},
{
"name": "name92"
},
{
"name": "name93"
},
{
"name": "name94"
},
{
"name": "name95"
},
{
"name": "name96"
},
{
"name": "name97"
},
{
"name": "name98"
},
{
"name": "name99"
},
{
"name": "name100"
},
{
"name": "name101"
},
{
"name": "name102"
}
],
"name": "name3"
},
{
"children": [
{
"name": "name104"
},
{
"name": "name105"
},
{
"name": "name106"
},
{
"name": "name107"
},
{
"name": "name108"
},
{
"name": "name109"
},
{
"name": "name110"
},
{
"name": "name111"
},
{
"name": "name112"
},
{
"name": "name113"
},
{
"name": "name114"
},
{
"name": "name115"
},
{
"name": "name116"
},
{
"name": "name117"
},
{
"name": "name118"
},
{
"name": "name119"
},
{
"name": "name120"
},
{
"name": "name121"
},
{
"name": "name122"
},
{
"name": "name123"
},
{
"name": "name124"
},
{
"name": "name125"
},
{
"name": "name126"
},
{
"name": "name127"
},
{
"name": "name128"
},
{
"name": "name129"
},
{
"name": "name130"
},
{
"name": "name131"
},
{
"name": "name132"
},
{
"name": "name133"
},
{
"name": "name134"
},
{
"name": "name135"
},
{
"name": "name136"
},
{
"name": "name137"
},
{
"name": "name138"
},
{
"name": "name139"
},
{
"name": "name140"
},
{
"name": "name141"
},
{
"name": "name142"
},
{
"name": "name143"
},
{
"name": "name144"
},
{
"name": "name145"
},
{
"name": "name146"
},
{
"name": "name147"
},
{
"name": "name148"
},
{
"name": "name149"
},
{
"name": "name150"
},
{
"name": "name151"
},
{
"name": "name152"
},
{
"name": "name153"
},
{
"name": "name154"
},
{
"name": "name155"
},
{
"name": "name156"
},
{
"name": "name157"
},
{
"name": "name158"
},
{
"name": "name159"
},
{
"name": "name160"
},
{
"name": "name161"
},
{
"name": "name162"
},
{
"name": "name163"
},
{
"name": "name164"
},
{
"name": "name165"
},
{
"name": "name166"
},
{
"name": "name167"
},
{
"name": "name168"
},
{
"name": "name169"
},
{
"name": "name170"
},
{
"name": "name171"
},
{
"name": "name172"
},
{
"name": "name173"
},
{
"name": "name174"
},
{
"name": "name175"
},
{
"name": "name176"
},
{
"name": "name177"
},
{
"name": "name178"
},
{
"name": "name179"
},
{
"name": "name180"
},
{
"name": "name181"
},
{
"name": "name182"
},
{
"name": "name183"
},
{
"name": "name184"
},
{
"name": "name185"
},
{
"name": "name186"
},
{
"name": "name187"
},
{
"name": "name188"
},
{
"name": "name189"
},
{
"name": "name190"
},
{
"name": "name191"
},
{
"name": "name192"
},
{
"name": "name193"
},
{
"name": "name194"
},
{
"name": "name195"
},
{
"name": "name196"
},
{
"name": "name197"
},
{
"name": "name198"
},
{
"name": "name199"
},
{
"name": "name200"
},
{
"name": "name201"
},
{
"name": "name202"
}
],
"name": "name103"
}
]
},
{
"name": "name203",
"children": [
{
"children": [
{
"name": "name205"
},
{
"name": "name206"
},
{
"name": "name207"
},
{
"name": "name208"
},
{
"name": "name209"
},
{
"name": "name210"
},
{
"name": "name211"
},
{
"name": "name212"
},
{
"name": "name213"
},
{
"name": "name214"
},
{
"name": "name215"
},
{
"name": "name216"
},
{
"name": "name217"
},
{
"name": "name218"
},
{
"name": "name219"
},
{
"name": "name220"
},
{
"name": "name221"
},
{
"name": "name222"
},
{
"name": "name223"
},
{
"name": "name224"
},
{
"name": "name225"
},
{
"name": "name226"
},
{
"name": "name227"
},
{
"name": "name228"
},
{
"name": "name229"
},
{
"name": "name230"
},
{
"name": "name231"
},
{
"name": "name232"
},
{
"name": "name233"
},
{
"name": "name234"
},
{
"name": "name235"
},
{
"name": "name236"
},
{
"name": "name237"
},
{
"name": "name238"
},
{
"name": "name239"
},
{
"name": "name240"
},
{
"name": "name241"
},
{
"name": "name242"
},
{
"name": "name243"
},
{
"name": "name244"
},
{
"name": "name245"
},
{
"name": "name246"
},
{
"name": "name247"
},
{
"name": "name248"
},
{
"name": "name249"
},
{
"name": "name250"
},
{
"name": "name251"
},
{
"name": "name252"
},
{
"name": "name253"
},
{
"name": "name254"
},
{
"name": "name255"
},
{
"name": "name256"
},
{
"name": "name257"
},
{
"name": "name258"
},
{
"name": "name259"
},
{
"name": "name260"
},
{
"name": "name261"
},
{
"name": "name262"
},
{
"name": "name263"
},
{
"name": "name264"
},
{
"name": "name265"
},
{
"name": "name266"
},
{
"name": "name267"
},
{
"name": "name268"
},
{
"name": "name269"
},
{
"name": "name270"
},
{
"name": "name271"
},
{
"name": "name272"
},
{
"name": "name273"
},
{
"name": "name274"
},
{
"name": "name275"
},
{
"name": "name276"
},
{
"name": "name277"
},
{
"name": "name278"
},
{
"name": "name279"
},
{
"name": "name280"
},
{
"name": "name281"
},
{
"name": "name282"
},
{
"name": "name283"
},
{
"name": "name284"
},
{
"name": "name285"
},
{
"name": "name286"
},
{
"name": "name287"
},
{
"name": "name288"
},
{
"name": "name289"
},
{
"name": "name290"
},
{
"name": "name291"
},
{
"name": "name292"
},
{
"name": "name293"
},
{
"name": "name294"
},
{
"name": "name295"
},
{
"name": "name296"
},
{
"name": "name297"
},
{
"name": "name298"
},
{
"name": "name299"
},
{
"name": "name300"
},
{
"name": "name301"
},
{
"name": "name302"
},
{
"name": "name303"
}
],
"name": "name204"
},
{
"children": [
{
"name": "name305"
},
{
"name": "name306"
},
{
"name": "name307"
},
{
"name": "name308"
},
{
"name": "name309"
},
{
"name": "name310"
},
{
"name": "name311"
},
{
"name": "name312"
},
{
"name": "name313"
},
{
"name": "name314"
},
{
"name": "name315"
},
{
"name": "name316"
},
{
"name": "name317"
},
{
"name": "name318"
},
{
"name": "name319"
},
{
"name": "name320"
},
{
"name": "name321"
},
{
"name": "name322"
},
{
"name": "name323"
},
{
"name": "name324"
},
{
"name": "name325"
},
{
"name": "name326"
},
{
"name": "name327"
},
{
"name": "name328"
},
{
"name": "name329"
},
{
"name": "name330"
},
{
"name": "name331"
},
{
"name": "name332"
},
{
"name": "name333"
},
{
"name": "name334"
},
{
"name": "name335"
},
{
"name": "name336"
},
{
"name": "name337"
},
{
"name": "name338"
},
{
"name": "name339"
},
{
"name": "name340"
},
{
"name": "name341"
},
{
"name": "name342"
},
{
"name": "name343"
},
{
"name": "name344"
},
{
"name": "name345"
},
{
"name": "name346"
},
{
"name": "name347"
},
{
"name": "name348"
},
{
"name": "name349"
},
{
"name": "name350"
},
{
"name": "name351"
},
{
"name": "name352"
},
{
"name": "name353"
},
{
"name": "name354"
},
{
"name": "name355"
},
{
"name": "name356"
},
{
"name": "name357"
},
{
"name": "name358"
},
{
"name": "name359"
},
{
"name": "name360"
},
{
"name": "name361"
},
{
"name": "name362"
},
{
"name": "name363"
},
{
"name": "name364"
},
{
"name": "name365"
},
{
"name": "name366"
},
{
"name": "name367"
},
{
"name": "name368"
},
{
"name": "name369"
},
{
"name": "name370"
},
{
"name": "name371"
},
{
"name": "name372"
},
{
"name": "name373"
},
{
"name": "name374"
},
{
"name": "name375"
},
{
"name": "name376"
},
{
"name": "name377"
},
{
"name": "name378"
},
{
"name": "name379"
},
{
"name": "name380"
},
{
"name": "name381"
},
{
"name": "name382"
},
{
"name": "name383"
},
{
"name": "name384"
},
{
"name": "name385"
},
{
"name": "name386"
},
{
"name": "name387"
},
{
"name": "name388"
},
{
"name": "name389"
},
{
"name": "name390"
},
{
"name": "name391"
},
{
"name": "name392"
},
{
"name": "name393"
},
{
"name": "name394"
},
{
"name": "name395"
},
{
"name": "name396"
},
{
"name": "name397"
},
{
"name": "name398"
},
{
"name": "name399"
},
{
"name": "name400"
},
{
"name": "name401"
},
{
"name": "name402"
},
{
"name": "name403"
}
],
"name": "name304"
}
]
}
]
},
{
"name": "name404",
"children": [
{
"name": "name405",
"children": [
{
"name": "name406",
"children": [
{
"name": "name407"
},
{
"name": "name408"
},
{
"name": "name409"
},
{
"name": "name410"
},
{
"name": "name411"
},
{
"name": "name412"
},
{
"name": "name413"
},
{
"name": "name414"
},
{
"name": "name415"
},
{
"name": "name416"
},
{
"name": "name417"
},
{
"name": "name418"
},
{
"name": "name419"
},
{
"name": "name420"
},
{
"name": "name421"
},
{
"name": "name422"
},
{
"name": "name423"
},
{
"name": "name424"
},
{
"name": "name425"
},
{
"name": "name426"
},
{
"name": "name427"
},
{
"name": "name428"
},
{
"name": "name429"
},
{
"name": "name430"
},
{
"name": "name431"
},
{
"name": "name432"
},
{
"name": "name433"
},
{
"name": "name434"
},
{
"name": "name435"
},
{
"name": "name436"
},
{
"name": "name437"
},
{
"name": "name438"
},
{
"name": "name439"
},
{
"name": "name440"
},
{
"name": "name441"
},
{
"name": "name442"
},
{
"name": "name443"
},
{
"name": "name444"
},
{
"name": "name445"
},
{
"name": "name446"
},
{
"name": "name447"
},
{
"name": "name448"
},
{
"name": "name449"
},
{
"name": "name450"
},
{
"name": "name451"
},
{
"name": "name452"
},
{
"name": "name453"
},
{
"name": "name454"
},
{
"name": "name455"
},
{
"name": "name456"
},
{
"name": "name457"
},
{
"name": "name458"
},
{
"name": "name459"
},
{
"name": "name460"
},
{
"name": "name461"
},
{
"name": "name462"
},
{
"name": "name463"
},
{
"name": "name464"
},
{
"name": "name465"
},
{
"name": "name466"
},
{
"name": "name467"
},
{
"name": "name468"
},
{
"name": "name469"
},
{
"name": "name470"
},
{
"name": "name471"
},
{
"name": "name472"
},
{
"name": "name473"
},
{
"name": "name474"
},
{
"name": "name475"
},
{
"name": "name476"
},
{
"name": "name477"
},
{
"name": "name478"
},
{
"name": "name479"
},
{
"name": "name480"
},
{
"name": "name481"
},
{
"name": "name482"
},
{
"name": "name483"
},
{
"name": "name484"
},
{
"name": "name485"
},
{
"name": "name486"
},
{
"name": "name487"
},
{
"name": "name488"
},
{
"name": "name489"
},
{
"name": "name490"
},
{
"name": "name491"
},
{
"name": "name492"
},
{
"name": "name493"
},
{
"name": "name494"
},
{
"name": "name495"
},
{
"name": "name496"
},
{
"name": "name497"
},
{
"name": "name498"
},
{
"name": "name499"
},
{
"name": "name500"
},
{
"name": "name501"
},
{
"name": "name502"
},
{
"name": "name503"
},
{
"name": "name504"
},
{
"name": "name505"
}
]
}
]
}
]
},
{
"name": "name506",
"children": [
{
"name": "name507"
},
{
"name": "name508"
},
{
"name": "name509"
}
]
},
{
"name": "name510",
"children": [
{
"name": "name511"
},
{
"name": "name512"
}
]
},
{
"name": "name513",
"children": [
{
"name": "name514"
},
{
"name": "name515"
}
]
}
]
}
<!DOCTYPE html>
<head>
<meta charset="utf-8">
<script src="https://d3js.org/d3.v4.min.js"></script>
<style>
body { margin:0;position:fixed;top:0;right:0;bottom:0;left:0; }
</style>
</head>
<body>
<div class="">
<div id="myGraph"></div>
</div>
<script>
d3.json('data.json', data => {
networkChart = renderChartCollapsibleNetwork()
.svgHeight(window.innerHeight - 30)
.svgWidth(window.innerWidth - 30)
.container('#myGraph')
.data({ root: data })
.debug(true)
.run()
})
</script>
<script>
/*
This code is based on following convention:
https://github.com/bumbeishvili/d3-coding-conventions
*/
function renderChartCollapsibleNetwork(params) {
// exposed variables
var attrs = {
id: 'id' + Math.floor(Math.random() * 1000000),
svgWidth: 960,
svgHeight: 600,
marginTop: 0,
marginBottom: 5,
marginRight: 0,
marginLeft: 30,
nodeRadius: 18,
container: 'body',
distance: 100,
hiddenChildLevel: 1,
//hiddenChildLevel: 5,
nodeStroke: '#41302D',
nodeTextColor: '#E5E5E5',
linkColor: '#303030',
activeLinkColor: "blue",
hoverOpacity: 0.2,
maxTextDisplayZoomLevel: 1,
textDisplayed: true,
lineStrokeWidth: 1.5,
data: null
};
/*############### IF EXISTS OVERWRITE ATTRIBUTES FROM PASSED PARAM ####### */
var attrKeys = Object.keys(attrs);
attrKeys.forEach(function (key) {
if (params && params[key]) {
attrs[key] = params[key];
}
})
//innerFunctions which will update visuals
var updateData;
var filter;
//main chart object
var main = function (selection) {
selection.each(function scope() {
//calculated properties
var calc = {}
calc.chartLeftMargin = attrs.marginLeft;
calc.chartTopMargin = attrs.marginTop;
calc.chartWidth = attrs.svgWidth - attrs.marginRight - calc.chartLeftMargin;
calc.chartHeight = attrs.svgHeight - attrs.marginBottom - calc.chartTopMargin;
//########################## HIERARCHY STUFF #########################
var hierarchy = {};
hierarchy.root = d3.hierarchy(attrs.data.root);
//########################### BEHAVIORS #########################
var behaviors = {};
behaviors.zoom = d3.zoom().scaleExtent([0.75, 100, 8]).on('zoom', zoomed);
behaviors.drag = d3.drag().on("start", dragstarted).on("drag", dragged).on("end", dragended);
//########################### LAYOUTS #########################
var layouts = {};
// custom radial kayout
layouts.radial = d3.radial();
//########################### FORCE STUFF #########################
var force = {};
force.link = d3.forceLink().id(d => d.id);
force.charge = d3.forceManyBody()
force.center = d3.forceCenter(calc.chartWidth / 2, calc.chartHeight / 2)
// prevent collide
force.collide = d3.forceCollide().radius(d => {
// if parent has many children, reduce collide strength
if (d.parent) {
if (d.parent.children.length > 10) {
// also slow down node movement
slowDownNodes();
return 7;
}
}
// increase collide strength
if (d.children && d.depth > 2) {
return attrs.nodeRadius;
}
return attrs.nodeRadius * 2;
});
//manually set x positions (which is calculated using custom radial layout)
force.x = d3.forceX()
.strength(0.5)
.x(function (d, i) {
// if node does not have children and is channel (depth=2) , then position it on parent's coordinate
if (!d.children && d.depth > 2) {
if (d.parent) {
d = d.parent
}
}
// custom circle projection - radius will be - (d.depth - 1) * 150
return projectCircle(d.proportion, (d.depth - 1) * 150)[0];
});
//manually set y positions (which is calculated using d3.cluster)
force.y = d3.forceY()
.strength(0.5)
.y(function (d, i) {
// if node does not have children and is channel (depth=2) , then position it on parent's coordinate
if (!d.children && d.depth > 2) {
if (d.parent) {
d = d.parent
}
}
// custom circle projection - radius will be - (d.depth - 1) * 150
return projectCircle(d.proportion, (d.depth - 1) * 150)[1];
})
//--------------------------------- INITIALISE FORCE SIMULATION ----------------------------
// get based on top parameter simulation
force.simulation = d3.forceSimulation()
.force('link', force.link)
.force('charge', force.charge)
.force('center', force.center)
.force("collide", force.collide)
.force('x', force.x)
.force('y', force.y)
//########################### HIERARCHY STUFF #########################
// flatten root
var arr = flatten(hierarchy.root);
// hide members based on their depth
arr.forEach(d => {
if (d.depth > attrs.hiddenChildLevel) {
d._children = d.children;
d.children = null;
}
})
//#################################### DRAWINGS #######################
//drawing containers
var container = d3.select(this);
//add svg
var svg = container.patternify({ tag: 'svg', selector: 'svg-chart-container' })
.attr('width', attrs.svgWidth)
.attr('height', attrs.svgHeight)
.call(behaviors.zoom)
//add container g element
var chart = svg.patternify({ tag: 'g', selector: 'chart' })
.attr('transform', 'translate(' + (calc.chartLeftMargin) + ',' + calc.chartTopMargin + ')');
//################################ Chart Content Drawing ##################################
//link wrapper
var linksWrapper = chart.patternify({ tag: 'g', selector: 'links-wrapper' })
//node wrapper
var nodesWrapper = chart.patternify({ tag: 'g', selector: 'nodes-wrapper' })
var nodes, links, enteredNodes;
// reusable function which updates visual based on data change
update();
//update visual based on data change
function update(clickedNode) {
// set xy and proportion properties with custom radial layout
layouts.radial(hierarchy.root);
//nodes and links array
var nodesArr = flatten(hierarchy.root, true)
.orderBy(d => d.depth)
.filter(d => !d.hidden);
var linksArr = hierarchy.root.links()
.filter(d => !d.source.hidden)
.filter(d => !d.target.hidden)
// make new nodes to appear near the parents
nodesArr.forEach(function (d, i) {
if (clickedNode && clickedNode.id == (d.parent && d.parent.id)) {
d.x = d.parent.x;
d.y = d.parent.y;
}
});
//links
links = linksWrapper.selectAll('.link').data(linksArr, d => d.target.id);
links.exit().remove();
links = links.enter()
.append('line')
.attr('class', 'link')
.merge(links).attr('stroke', '#9ecae1');
links.attr('stroke', attrs.linkColor).attr('stroke-width', attrs.lineStrokeWidth)
//node groups
nodes = nodesWrapper.selectAll('.node').data(nodesArr, d => d.id);
var exited = nodes.exit().remove();
var enteredNodes = nodes.enter()
.append('g')
.attr('class', 'node')
//bind event handlers
enteredNodes.on('click', nodeClick)
.on('mouseenter', nodeMouseEnter)
.on('mouseleave', nodeMouseLeave)
.call(behaviors.drag)
//node texts
enteredNodes.append('text').attr('class', 'node-texts')
.attr('x', 30).attr('fill', attrs.nodeTextColor)
.text(d => d.data.name)
.style('display', attrs.textDisplayed ? "initial" : "none")
//channels grandchildren
var channelsGrandchildren = enteredNodes
.append("circle")
.attr('r', 7)
.attr('stroke-width', 5)
.attr('stroke', attrs.nodeStroke)
//merge node groups and style it
nodes = enteredNodes.merge(nodes);
nodes
.attr('fill', d => {
return d._children ? "#3182bd" : d.children ? "#c6dbef" : "#fd8d3c";
})
.style('cursor', 'pointer')
//force simulation
force.simulation.nodes(nodesArr)
.on('tick', ticked);
// links simulation
force.simulation.force("link").links(links).id(d => d.id).distance(100).strength(d => 1);
}
//####################################### EVENT HANDLERS ########################
// zoom handler
function zoomed() {
//get transform event
var transform = d3.event.transform;
attrs.lastTransform = transform;
// apply transform event props to the wrapper
chart.attr('transform', transform)
svg.selectAll('.node').attr("transform", function (d) { return `translate(${d.x},${d.y}) scale(${1 / (attrs.lastTransform ? attrs.lastTransform.k : 1)})`; });
svg.selectAll('.link').attr("stroke-width", attrs.lineStrokeWidth / (attrs.lastTransform ? attrs.lastTransform.k : 1));
// hide texts if zooming is less than certain level
if (transform.k < attrs.maxTextDisplayZoomLevel) {
svg.selectAll('.node-texts').style('display', 'none');
attrs.textDisplayed = false;
} else {
svg.selectAll('.node-texts').style('display', 'initial');
attrs.textDisplayed = true;
}
}
//tick handler
function ticked() {
// set links position
links.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; });
//set nodes position
svg.selectAll('.node').attr("transform", function (d) { return `translate(${d.x},${d.y}) scale(${1 / (attrs.lastTransform ? attrs.lastTransform.k : 1)})`; });
}
//handler drag start event
function dragstarted(d) {
//disable node fixing
nodes.each(d => { d.fx = null; d.fy = null })
}
// handle dragging event
function dragged(d) {
// make dragged node fixed
d.fx = d3.event.x;
d.fy = d3.event.y;
}
//-------------------- handle drag end event ---------------
function dragended(d) {
// we are doing nothing, here , aren't we?
}
//-------------------------- node mouse hover handler ---------------
function nodeMouseEnter(d) {
//get hovered node
var node = d3.select(this);
//get links
var links = hierarchy.root.links();
//get hovered node connected links
var connectedLinks = links.filter(l => l.source.id == d.id || l.target.id == d.id);
//get hovered node linked nodes
var linkedNodes = connectedLinks.map(s => s.source.id).concat(connectedLinks.map(d => d.target.id))
//reduce all other nodes opacity
nodesWrapper.selectAll('.node')
.filter(n => linkedNodes.indexOf(n.id) == -1)
.attr('opacity', attrs.hoverOpacity);
//reduce all other links opacity
linksWrapper.selectAll('.link').attr('opacity', attrs.hoverOpacity);
//highlight hovered nodes connections
linksWrapper.selectAll('.link')
.filter(l => l.source.id == d.id || l.target.id == d.id)
.attr('opacity', 1)
.attr('stroke', attrs.activeLinkColor)
}
// --------------- handle mouseleave event ---------------
function nodeMouseLeave(d) {
// return things back to normal
nodesWrapper.selectAll('.node').attr('opacity', 1);
linksWrapper.selectAll('.link').attr('opacity', 1).attr('stroke', attrs.linkColor)
}
// --------------- handle node click event ---------------
function nodeClick(d) {
//free fixed nodes
nodes.each(d => { d.fx = null; d.fy = null })
// collapse or expand node
if (d.children) {
d._children = d.children;
d.children = null;
update();
force.simulation.restart();
force.simulation.alphaTarget(0.15);
} else if (d._children) {
d.children = d._children;
d._children = null;
update(d);
force.simulation.restart();
force.simulation.alphaTarget(0.15);
} else {
//nothing is to collapse or expand
}
freeNodes();
}
//######################################### UTIL FUNCS ##################################
updateData = function () {
main.run();
}
function slowDownNodes() {
force.simulation.alphaTarget(0.05);
};
function speedUpNodes() {
force.simulation.alphaTarget(0.45);
}
function freeNodes() {
d3.selectAll('.node').each(n => { n.fx = null; n.fy = null; })
}
function projectCircle(value, radius) {
var r = radius || 0;
var corner = value * 2 * Math.PI;
return [Math.sin(corner) * r, -Math.cos(corner) * r]
}
//recursively loop on children and extract nodes as an array
function flatten(root, clustered) {
var nodesArray = [];
var i = 0;
function recurse(node, depth) {
if (node.children)
node.children.forEach(function (child) {
recurse(child, depth + 1);
});
if (!node.id) node.id = ++i;
else ++i;
node.depth = depth;
if (clustered) {
if (!node.cluster) {
// if cluster coordinates are not set, set it
node.cluster = { x: node.x, y: node.y }
}
}
nodesArray.push(node);
}
recurse(root, 1);
return nodesArray;
}
function debug() {
if (attrs.isDebug) {
//stringify func
var stringified = scope + "";
// parse variable names
var groupVariables = stringified
//match var x-xx= {};
.match(/var\s+([\w])+\s*=\s*{\s*}/gi)
//match xxx
.map(d => d.match(/\s+\w*/gi).filter(s => s.trim()))
//get xxx
.map(v => v[0].trim())
//assign local variables to the scope
groupVariables.forEach(v => {
main['P_' + v] = eval(v)
})
}
}
debug();
});
};
//----------- PROTOTYEPE 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)
selection.exit().remove();
selection = selection.enter().append(elementTag).merge(selection)
selection.attr('class', selector);
return selection;
}
// custom radial layout
d3.radial = function () {
return function setProportions(root) {
recurse(root, 0, 1);
function recurse(node, min, max) {
node.proportion = (max + min) / 2;
if (!node.x) {
// if node has parent, match entered node positions to it's parent
if (node.parent) {
node.x = node.parent.x;
} else {
node.x = 0;
}
}
// if node had parent, match entered node positions to it's parent
if (!node.y) {
if (node.parent) {
node.y = node.parent.y;
} else {
node.y = 0;
}
}
//recursively do the same for children
if (node.children) {
var offset = (max - min) / node.children.length;
node.children.forEach(function (child, i, arr) {
var newMin = min + offset * i;
var newMax = newMin + offset;
recurse(child, newMin, newMax);
});
}
}
}
}
//https://github.com/bumbeishvili/d3js-boilerplates#orderby
Array.prototype.orderBy = function (func) {
this.sort((a, b) => {
var a = func(a);
var b = func(b);
if (typeof a === 'string' || a instanceof String) {
return a.localeCompare(b);
}
return a - b;
});
return this;
}
//########################## BOILEPLATE STUFF ################
//dinamic keys functions
Object.keys(attrs).forEach(key => {
// Attach variables to main function
return main[key] = function (_) {
var string = `attrs['${key}'] = _`;
if (!arguments.length) { return eval(` attrs['${key}'];`); }
eval(string);
return main;
};
});
//set attrs as property
main.attrs = attrs;
//debugging visuals
main.debug = function (isDebug) {
attrs.isDebug = isDebug;
if (isDebug) {
if (!window.charts) window.charts = [];
window.charts.push(main);
}
return main;
}
//exposed update functions
main.data = function (value) {
if (!arguments.length) return attrs.data;
attrs.data = value;
if (typeof updateData === 'function') {
updateData();
}
return main;
}
// run visual
main.run = function () {
d3.selectAll(attrs.container).call(main);
return main;
}
main.filter = function (filterParams) {
if (!arguments.length) return attrs.filterParams;
attrs.filterParams = filterParams;
if (typeof filter === 'function') {
filter();
}
return main;
}
return main;
}
</script>
</body>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment