let w = window, d = document, e = d.documentElement, g = d.getElementsByTagName('body')[0], width = w.innerWidth || e.clientWidth || g.clientWidth, height = w.innerHeight|| e.clientHeight|| g.clientHeight; headerHeight = d3.select('.header').style('height').split('p')[0]; keyHeight = d3.select('.key').style('height').split('p')[0]; height = height - headerHeight - keyHeight - 10; let svg = d3.select('.screen').attr('width', width).attr('height', height); // Keep reference to important nodes. 'True' will be replaced with node let ref = {} // Store data for year2 let yearData = {}; // Distinction between prevYear and year allows to conditionally trigger adding or removing of nodes let prevYear = 0; let year = 1; let radiusScaler = 11; // links and nodes are storages for arrays of nodes / connections let links, nodes; // link and node are pointers to the pertinent svg selector let link = svg.append('g').attr('class', 'links').selectAll('line'); let node = svg.append('g').attr('class', 'nodes').selectAll('circle'); let key = d3.select('.key'); let headline = d3.select('.headline'); let headlines; // store json data in graph var let graph, data; let simulation = d3.forceSimulation() .force('charge', d3.forceManyBody()) .force('collide', d3.forceCollide()) .force('link', d3.forceLink()) .force('center', d3.forceCenter()) .force('forceY', d3.forceY()) /* * Read in data and intitialize chart */ d3.json('data.json', (error, _graph) => { if (error) { console.log('error fetching dataz!', error); } data = _graph.base; graph = d3.hierarchy(data); nodes = graph.descendants(); links = graph.links(); // populate main reference pointers in node storage object nodes.forEach(el => { ref[el.data.id] = el; }); // populate each year's data in yearData for (let year in _graph) { if (year !== 'base') { yearData[year] = _graph[year]; } } // Add 1st year's nodes / links yearData['year1'].forEach(company => { addNodes(company); }) update(); unfocusKey(ref['SORT']); }); d3.json('headlines.json', (error, data) => { if (error) { console.log('error retrieving headlines', error); } headlines = data; }) /* * Update simulation */ function update() { updateSizes(ref.SORT); initializeDisplay(); initializeSimulation(); updateForces(); simulation.alpha(1).restart(); } function updateHeadline(year) { const nums = headlines['year' + year]; headline.select('.num-accounts').html(nums.numAccounts); headline.select('.aum').html(nums.aum); headline.select('.avg-balance').html(nums.avgBalance); headline.select('.revenue').html(nums.revenue); } // Makes the values equal to the sum of all descendant values-- Used to set radius // Set count property which equals the number of children leaf nodes function updateSizes(root) { let aggArea = 0; let count = 0; root.children.forEach(child => { // if at a leaf node if (!child.children) { // child.value = child.data.value; child.value = computeValueWithInterest(child.data.value, child.data.year, year); child.count = 1; } else { updateSizes(child); } aggArea += child.value || 0; count += child.count; }) root.value = aggArea; root.count = count; } function computeValueWithInterest(value, startYear, currYear) { const intRate = 0.2; const timesCompoundedPerYear = 4; return value * (1 + (intRate / timesCompoundedPerYear)) ** (timesCompoundedPerYear * (currYear - startYear)); } function computeRadius(node) { return Math.sqrt(node.value / Math.PI) * radiusScaler; } function initializeDisplay() { let t = d3.transition().duration(1000); // create link svg elements link = link.data(links); link.exit().remove(); link = link.enter().append('line').merge(link); // create node svg elements node = node.data(nodes, d => d.data.id); node.exit().remove(); node = node .enter().append('circle') .attr('fill', d => { let id = d.data.id; if (id === 'SORT') { return 'rgb(255, 77, 120)'; } else if (id === 'FI') { return 'rgb(56, 60, 70)' } else if (id === 'Match') { return 'rgb(23, 63, 88)'; } else if (id === 'B2C') { return 'rgb(48, 157, 191)'; } else if (d.children) { return 'rgb(36, 218, 229)'; } else { return 'rgb(230, 255, 250)'; } }) .attr('r', 0) .call(d3.drag() .on('start', dragStarted) .on('drag', dragged) .on('end', dragEnded) ) .merge(node) .on('mouseenter', focusKey) .on('mouseleave', unfocusKey) node.transition(t) .attr('r', computeRadius); } function initializeSimulation() { simulation .nodes(nodes) .on('tick', ticked); } function updateForces() { simulation.force('charge') .strength((d, i) => d.data.id === 'SORT' ? 0 : -120); simulation.force('center') .x(width / 2) .y(height / 2) simulation.force('forceY') .y(height / 2) .strength(0.04) simulation.force('link') .links(links) .iterations(10) .distance(d => { const sourceRadius = computeRadius(d.source); const targetRadius = computeRadius(d.target); return sourceRadius + targetRadius + 5; }); simulation.force('collide') .radius(d => Math.sqrt(d.value / Math.PI) * radiusScaler + 5) .strength(1); } /* * Add and remove nodes / links */ function changeYear(val) { prevYear = year; year = val; updateHeadline(year); d3.select('#year_slider_output').text(val); if (year > prevYear) { addYear(year); } else { removeYear(year); } unfocusKey(); } function addYear(year) { yearData['year' + year].forEach(entry => { if (entry.id === 'nodesToAdd') { entry.categories.forEach(category => { let possibleParents = category.parent === 'B2C' ? [ref[category.parent]] : ref[category.parent].children; for (let i = 0; i < category.count; i++) { const randInd = Math.floor(Math.random() * possibleParents.length); const parent = possibleParents[randInd]; const newNode = createNode(parent, year); addNodes(newNode); } }) } else { addNodes(entry); } }); update(); }; function createNode(parent, year) { const value = parent.data.parent === 'FI' ? 4 : 1; return { id: parent.data.id + parent.count + 1, parent: parent.data.id, year, value } } function addNodes(company) { let parent = ref[company.parent]; let hier = d3.hierarchy(company); let currNodes = hier.descendants(); let currLinks = hier.links(); // Store references to each node based on id currNodes.forEach(el => { if (!ref[el.data.id]) { ref[el.data.id] = el; } }); positioner(currNodes, parent); nodes = nodes.concat(currNodes); links = links.concat(currLinks); links.push({source: parent, target: currNodes[0]}); if (!parent.children) { parent.children = []; } parent.children.push(currNodes[0]); } // Mutates inputted nodes to add position properties based on parent function positioner(nodeList, parentNode) { nodeList.forEach(el => { el.x = parentNode.x + 5 * Math.random(); el.y = parentNode.y - 5 * Math.random(); }) } function removeYear(year) { let removedNodes = new Set(); Object.keys(ref).forEach(category => { if (category !== 'SORT' && ref[category].children) { ref[category].children = ref[category].children.filter(child => { return child.data.year <= +year }); } }); // Remove node pointers in ref for nodes removed by year so can create new ones if year is readded Object.keys(ref).forEach(key => { if (ref[key].data.year > year) { delete ref[key]; } }) nodes = nodes.filter(node => { if (node.data.year <= year) { return true; } removedNodes.add(node); return false; }); links = links.filter(link => !(removedNodes.has(link.target) || removedNodes.has(link.source))); update(); }; /* * Tooltip functionality */ function focusKey(circle) { if (!d3.event.active) { simulation.alpha(0.05).restart(); } createKeySelection(circle) .transition() .duration(400) .style('color', 'rgb(255, 77, 120)'); } function unfocusKey(circle) { if (circle) { createKeySelection(circle) .transition() .duration(800) .style('color', 'rgb(23, 63, 88)'); } } function createKeySelection(circle) { if (!circle.data.children) { return key.select('.key-customers'); } else if (circle.data.id === 'SORT') { return key.select('.key-sort'); } else if (circle.data.id === 'FI' || circle.data.parent === 'FI') { return key.select('.key-fi'); } else if (circle.data.id === 'Match' || circle.data.parent === 'Match') { return key.select('.key-match'); } else if (circle.data.id === 'B2C' || circle.data.parent === 'B2C') { return key.select('.key-b2c'); } } function ticked() { link .attr('x1', d => d.source.x) .attr('y1', d => d.source.y) .attr('x2', d => d.target.x) .attr('y2', d => d.target.y) node .attr('cx', d => d.x) .attr('cy', d => d.y) } /* * Node drag functionality */ function dragStarted(d) { if (!d3.event.active) { // alpha # (0-1) gives how quickly a node will snap back into place once stopped dragging simulation.alphaTarget(0.3).restart(); } d.fx = d.x; d.fy = d.y; } function dragged(d) { d.fx = d3.event.x; d.fy = d3.event.y; } function dragEnded(d) { if (!d3.event.active) { simulation.alphaTarget(0.001); } d.fx = null; d.fy = null; }