From fa8132fe1f328f88b1fd154accb9bc79d185197c Mon Sep 17 00:00:00 2001 From: IMBurbank Date: Mon, 17 Jul 2017 19:09:13 -0600 Subject: [PATCH] 1.0.0 working version. --- app/index.html | 47 +++++-- app/scripts/es6/main.es6 | 240 +++++++++++++++++++++++++++++++++-- app/scripts/js/main.js | 177 +++++++++++++++++++++++--- app/styles/css/main.css | 117 +++++++++++++++++ app/styles/css/main.css.map | 2 +- app/styles/sass/main.sass | 137 ++++++++++++++++++++ dist/index.html | 47 +++++-- dist/scripts/js/main.min.js | 177 +++++++++++++++++++++++--- dist/styles/css/main.min.css | 117 +++++++++++++++++ package.json | 9 +- 10 files changed, 1003 insertions(+), 67 deletions(-) diff --git a/app/index.html b/app/index.html index 93d9686..0fc4dcb 100644 --- a/app/index.html +++ b/app/index.html @@ -2,10 +2,10 @@ - TrumpWorldGraph + Trump World Connections Graph - - + + @@ -13,18 +13,49 @@ +

Trump World Connections

-
+
+

+ From the Buzzfeed article + + Help Us Map TrumpWorld + +

+ +

+ No American president has taken office with a giant network of businesses, investments, + and corporate connections like that amassed by Donald J. Trump. His family and advisers + have touched a staggering number of ventures, from a hotel in Azerbaijan to a poker company + in Las Vegas. +

+ +

+ The force-directed graph below represents the relationship between all known entities + with three or more links to other entities within Trump's world. I made use + of this great dataset and + d3.js to create this graph. Click the nodes for more information. +

+
+ +
+ + - - + + - - + + diff --git a/app/scripts/es6/main.es6 b/app/scripts/es6/main.es6 index 69592a5..f9be49f 100644 --- a/app/scripts/es6/main.es6 +++ b/app/scripts/es6/main.es6 @@ -1,13 +1,133 @@ -const dataPath = 'assets/datasets/trumpworld.csv'; +/** + * Set Global Graph/Similation Variables + */ + +const width = 900, + height = width / 1.6, + fillColors = { Person: 'rgba(44, 143, 204, 0.95)', Organization: 'rgba(244, 89, 58, 0.95)' }, + dataPath = 'assets/datasets/trumpworld.csv'; + +const simulation = d3.forceSimulation() + .force('link', + d3.forceLink() + .id( d => d.id ) + .distance([50]) + ) + .force('charge', + d3.forceManyBody() + .strength( d => [-4 * d.links**(1/2) - 20] ) + .distanceMin([0.001]) + .distanceMax([width / 2]) + ) + .force('center', + d3.forceCenter(width / 2, height / 2) + ); + + +/** + * Create/Append HTML Graph Components + */ + +const svg = d3.select('#graph') + .append('svg') + .attrs({ height, width, class: 'chart-palette' }); + +const detailDiv = d3.select("#graph") + .append("div") + .attrs({ id: "details", class: "details" }) + .style("opacity", 0); + +const tooltipDiv = d3.select("body") + .append("div") + .attr("class", "tooltip") + .style("opacity", 0); + + +/** + * Define Graph & Simulation Functions + */ + +const updateDetailDiv = function updateDetailDiv(d) { + detailDiv + .transition() + .duration(200) + .styles({opacity: 0.9, 'z-index': 5 }); + + detailDiv + .styles({ + 'min-width': width / 3 + 'px', + 'height': height / 3 + 'px' + }) + .html( + `${d.id}
+
+ Type: ${d.group}
+ Links: ${d.links}
+
+ + + + + + + + + + + + + + ${d.connections.reduce( (a, b, i) => { + return `${a} + + + + + `; + }, "")} + + +
` + ); +} + + +const dragStarted = function dragSimulationStarted(d) { + if (!d3.event.active) simulation.alphaTarget(0.3).restart(); + + d.fx = d.x; + d.fy = d.y +} + + +const dragged = function draggedSimulation(d) { + d.fx = Math.max(d.radius, Math.min(width - d.radius, d3.event.x)); + d.fy = Math.max(d.radius, Math.min(height - d.radius, d3.event.y)); +} + + +const dragEnded = function dragSimulationEnded(d) { + if (!d3.event.active) simulation.alphaTarget(0); + d.fx = null; + d.fy = null; + + updateDetailDiv(d); +} const treatData = function treatGraphData(data) { - const entityLetters = ['A', 'B']; + const entityLetters = ['A', 'B'], + linksCutoff = 3; let nodeData = {}, - nodes = [], - links = [], - entity = ''; + rowObj = {}, + nodes = [], + links = [], + endNodes = [], + finalEntities = new Set(), + entity = ''; data.forEach( row => { entityLetters.forEach( (key, i) => { @@ -16,10 +136,10 @@ const treatData = function treatGraphData(data) { if (nodeData[entity]) { nodeData[entity].links++; nodeData[entity].connections.push({ - Name: row[`Entity ${entityLetters[1 - i]}`], - Type: row[`Entity ${entityLetters[1 - i]} Type`], - Connection: row['Connection'], - 'Source(s)': row['Source(s)'] + name: row[`Entity ${entityLetters[1 - i]}`], + type: row[`Entity ${entityLetters[1 - i]} Type`], + connection: row['Connection'], + source: row['Source(s)'] }); } else { @@ -28,10 +148,10 @@ const treatData = function treatGraphData(data) { links: 1, group: row[`Entity ${key} Type`], connections: [{ - Name: row[`Entity ${entityLetters[1 - i]}`], - Type: row[`Entity ${entityLetters[1 - i]} Type`], - Connection: row['Connection'], - 'Source(s)': row['Source(s)'] + name: row[`Entity ${entityLetters[1 - i]}`], + type: row[`Entity ${entityLetters[1 - i]} Type`], + connection: row['Connection'], + source: row['Source(s)'] }], }; } @@ -40,17 +160,109 @@ const treatData = function treatGraphData(data) { links.push({source: row['Entity A'], target: row['Entity B']}); }); - for (let key in nodeData) nodes.push(nodeData[key]); + for (let key in nodeData) { + rowObj = nodeData[key]; + + if (rowObj.links <= linksCutoff) endNodes.push(key); + else rowObj['radius'] = rowObj.links**(9/16) + 3, nodes.push(rowObj); + } + + links = links.filter( el => !endNodes.includes(el.source) && !endNodes.includes(el.target) ); + + links.forEach( el => {finalEntities.add(el.source), finalEntities.add(el.target)} ); + + nodes = nodes + .filter( el => finalEntities.has(el.id)) + .sort( (a, b) => b.links - a.links ); + + nodes.forEach( el => el.connections.sort( (a, b) => a.name <= b.name ? -1 : 1) ); return {nodes, links}; } const forceDirectedGraph = function createForceDirectedGraph(nodes, links) { + const link = svg.append('g') + .attr('class', 'link') + .selectAll('line') + .data(links) + .enter() + .append('line') + + const node = svg.append('g') + .attr('class', 'node') + .selectAll('circle') + .data(nodes) + .enter() + .append('circle') + .attrs({ + r: d => d.radius, + fill: d => fillColors[d.group] + }) + .on('mouseover', d => { + tooltipDiv.transition() + .duration(200) + .style('opacity', 0.9); + tooltipDiv + .html( + `${d.id}
+ Type: ${d.group}
+ Links: ${d.links}` + ) + .styles({ left: (d3.event.pageX) + 'px', top: (d3.event.pageY + 12) + "px"}); + }) + .on('mouseout', d => { + tooltipDiv.transition() + .duration(500) + .style("opacity", 0); + }) + .call(d3.drag() + .on('start', dragStarted) + .on('drag', dragged) + .on('end', dragEnded) + ) + + const ticked = function linkTicked() { + link.attrs({ + 'x1': d => d.source.x, + 'y1': d => d.source.y, + 'x2': d => d.target.x, + 'y2': d => d.target.y + }); + + node.attrs({ + cx: d => d.x = Math.max(d.radius, Math.min(width - d.radius, d.x)), + cy: d => d.y = Math.max(d.radius, Math.min(height - d.radius, d.y)) + }); + } + + + d3.select('body') + .on('click', e => { + if (d3.event.target.localName !== 'circle' && + d3.event.path.every( el => el.id !== 'details')) { + detailDiv + .transition() + .duration(333) + .styles({ opacity: 0, 'z-index': -1 }); + } + }); + + simulation + .nodes(nodes) + .on('tick', ticked); + + simulation + .force('link') + .links(links); } +/** + * Run Force Directed Graph Simulation + */ + d3.csv(dataPath, (error, data) => { if (error) throw error; diff --git a/app/scripts/js/main.js b/app/scripts/js/main.js index 88ab318..9f39bb8 100644 --- a/app/scripts/js/main.js +++ b/app/scripts/js/main.js @@ -1,14 +1,75 @@ 'use strict'; -var dataPath = 'assets/datasets/trumpworld.csv'; +/** + * Set Global Graph/Similation Variables + */ + +var width = 900, + height = width / 1.6, + fillColors = { Person: 'rgba(44, 143, 204, 0.95)', Organization: 'rgba(244, 89, 58, 0.95)' }, + dataPath = 'assets/datasets/trumpworld.csv'; + +var simulation = d3.forceSimulation().force('link', d3.forceLink().id(function (d) { + return d.id; +}).distance([50])).force('charge', d3.forceManyBody().strength(function (d) { + return [-4 * Math.pow(d.links, 1 / 2) - 20]; +}).distanceMin([0.001]).distanceMax([width / 2])).force('center', d3.forceCenter(width / 2, height / 2)); + +/** + * Create/Append HTML Graph Components + */ + +var svg = d3.select('#graph').append('svg').attrs({ height: height, width: width, class: 'chart-palette' }); + +var detailDiv = d3.select("#graph").append("div").attrs({ id: "details", class: "details" }).style("opacity", 0); + +var tooltipDiv = d3.select("body").append("div").attr("class", "tooltip").style("opacity", 0); + +/** + * Define Graph & Simulation Functions + */ + +var updateDetailDiv = function updateDetailDiv(d) { + detailDiv.transition().duration(200).styles({ opacity: 0.9, 'z-index': 5 }); + + detailDiv.styles({ + 'min-width': width / 3 + 'px', + 'height': height / 3 + 'px' + }).html('' + d.id + '
\n
\n Type: ' + d.group + '
\n Links: ' + d.links + '
\n
\n \n \n \n \n \n \n \n \n \n \n \n \n \n ' + d.connections.reduce(function (a, b, i) { + return a + '\n \n \n \n \n '; + }, "") + '\n \n \n
'); +}; + +var dragStarted = function dragSimulationStarted(d) { + if (!d3.event.active) simulation.alphaTarget(0.3).restart(); + + d.fx = d.x; + d.fy = d.y; +}; + +var dragged = function draggedSimulation(d) { + d.fx = Math.max(d.radius, Math.min(width - d.radius, d3.event.x)); + d.fy = Math.max(d.radius, Math.min(height - d.radius, d3.event.y)); +}; + +var dragEnded = function dragSimulationEnded(d) { + if (!d3.event.active) simulation.alphaTarget(0); + d.fx = null; + d.fy = null; + + updateDetailDiv(d); +}; var treatData = function treatGraphData(data) { var entityLetters = ['A', 'B'], - groupValues = { Organization: 0, Person: 1 }; + linksCutoff = 3; var nodeData = {}, + rowObj = {}, nodes = [], links = [], + endNodes = [], + finalEntities = new Set(), entity = ''; data.forEach(function (row) { @@ -18,21 +79,21 @@ var treatData = function treatGraphData(data) { if (nodeData[entity]) { nodeData[entity].links++; nodeData[entity].connections.push({ - Name: row['Entity ' + entityLetters[1 - i]], - Type: row['Entity ' + entityLetters[1 - i] + ' Type'], - Connection: row['Connection'], - 'Source(s)': row['Source(s)'] + name: row['Entity ' + entityLetters[1 - i]], + type: row['Entity ' + entityLetters[1 - i] + ' Type'], + connection: row['Connection'], + source: row['Source(s)'] }); } else { nodeData[entity] = { id: entity, - group: groupValues[row['Entity ' + key + ' Type']], links: 1, + group: row['Entity ' + key + ' Type'], connections: [{ - Name: row['Entity ' + entityLetters[1 - i]], - Type: row['Entity ' + entityLetters[1 - i] + ' Type'], - Connection: row['Connection'], - 'Source(s)': row['Source(s)'] + name: row['Entity ' + entityLetters[1 - i]], + type: row['Entity ' + entityLetters[1 - i] + ' Type'], + connection: row['Connection'], + source: row['Source(s)'] }] }; } @@ -42,10 +103,95 @@ var treatData = function treatGraphData(data) { }); for (var key in nodeData) { - nodes.push(nodeData[key]); - }return { nodes: nodes, links: links }; + rowObj = nodeData[key]; + + if (rowObj.links <= linksCutoff) endNodes.push(key);else rowObj['radius'] = Math.pow(rowObj.links, 9 / 16) + 3, nodes.push(rowObj); + } + + links = links.filter(function (el) { + return !endNodes.includes(el.source) && !endNodes.includes(el.target); + }); + + links.forEach(function (el) { + finalEntities.add(el.source), finalEntities.add(el.target); + }); + + nodes = nodes.filter(function (el) { + return finalEntities.has(el.id); + }).sort(function (a, b) { + return b.links - a.links; + }); + + nodes.forEach(function (el) { + return el.connections.sort(function (a, b) { + return a.name <= b.name ? -1 : 1; + }); + }); + + return { nodes: nodes, links: links }; +}; + +var forceDirectedGraph = function createForceDirectedGraph(nodes, links) { + var link = svg.append('g').attr('class', 'link').selectAll('line').data(links).enter().append('line'); + + var node = svg.append('g').attr('class', 'node').selectAll('circle').data(nodes).enter().append('circle').attrs({ + r: function r(d) { + return d.radius; + }, + fill: function fill(d) { + return fillColors[d.group]; + } + }).on('mouseover', function (d) { + tooltipDiv.transition().duration(200).style('opacity', 0.9); + tooltipDiv.html(d.id + '
\n Type: ' + d.group + '
\n Links: ' + d.links).styles({ left: d3.event.pageX + 'px', top: d3.event.pageY + 12 + "px" }); + }).on('mouseout', function (d) { + tooltipDiv.transition().duration(500).style("opacity", 0); + }).call(d3.drag().on('start', dragStarted).on('drag', dragged).on('end', dragEnded)); + + var ticked = function linkTicked() { + link.attrs({ + 'x1': function x1(d) { + return d.source.x; + }, + 'y1': function y1(d) { + return d.source.y; + }, + 'x2': function x2(d) { + return d.target.x; + }, + 'y2': function y2(d) { + return d.target.y; + } + }); + + node.attrs({ + cx: function cx(d) { + return d.x = Math.max(d.radius, Math.min(width - d.radius, d.x)); + }, + cy: function cy(d) { + return d.y = Math.max(d.radius, Math.min(height - d.radius, d.y)); + } + }); + }; + + d3.select('body').on('click', function (e) { + if (d3.event.target.localName !== 'circle' && d3.event.path.every(function (el) { + return el.id !== 'details'; + })) { + + detailDiv.transition().duration(333).styles({ opacity: 0, 'z-index': -1 }); + } + }); + + simulation.nodes(nodes).on('tick', ticked); + + simulation.force('link').links(links); }; +/** + * Run Force Directed Graph Simulation + */ + d3.csv(dataPath, function (error, data) { if (error) throw error; @@ -53,8 +199,5 @@ d3.csv(dataPath, function (error, data) { nodes = _treatData.nodes, links = _treatData.links; - console.log(nodes[0]); - console.log(links[0]); - - //forceDirectedGraph(nodes, links); + forceDirectedGraph(nodes, links); }); \ No newline at end of file diff --git a/app/styles/css/main.css b/app/styles/css/main.css index 6537dac..5290dff 100644 --- a/app/styles/css/main.css +++ b/app/styles/css/main.css @@ -1,2 +1,119 @@ +body { + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-orient: vertical; + -webkit-box-direction: normal; + -ms-flex-direction: column; + flex-direction: column; + -webkit-box-pack: justify; + -ms-flex-pack: justify; + justify-content: space-between; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + width: 100vw; + min-height: 100vh; + margin: 0; + padding: 0; + text-align: center; } + +div.details, div.tooltip { + position: absolute; + padding: 8px; + border: 0; + font: 12px sans-serif; + color: #fff; + background: #505964; + border-radius: 8px; } + +div.details { + right: 0; + bottom: 4px; } + +div.tooltip { + z-index: 10; + min-width: 80px; + min-height: 30px; + pointer-events: none; } + +h1, +h2, +h3 { + margin: 0; } + +h1 { + font-family: 'Diplomata SC', cursive; + font-size: 1.8rem; } + +header, +footer { + width: 100%; + margin: 0; + padding: .25rem 0; + color: #f2f152; + background: #000; } + header a, + footer a { + color: #f2f152; } + header .author, + footer .author { + margin-right: 3rem; } + +footer { + font-size: .8em; } + +.intro-content { + width: 900px; + text-align: left; } + .intro-content .quote { + padding: 0 6rem 0 3rem; + color: #6d787e; + box-shadow: inset 8px 0 0 rgba(0, 0, 0, 0.3); } + .intro-content .page-summary { + margin-bottom: 0; + font-size: 1.1rem; } + .intro-content a { + color: #296cbd; } + +.graph-container { + position: relative; + display: inline-block; } + +.chart-palette { + background: #fff; + box-shadow: 0 1px 4px rgba(0, 0, 0, 0.3), 0 0 40px rgba(0, 0, 0, 0.1) inset, 0 10px 6px -6px #777; } + +.link line { + stroke: #999; + stroke-width: 1.5px; + stroke-opacity: .6; } + +.node circle { + stroke: #fff; + stroke-width: 1.5px; } + +.table-container { + width: 100%; + height: 120px; + margin-top: 1em; + font-size: .8em; + overflow-y: scroll; } + +.link-table-name, .link-table-details { + max-width: 30em; } + +.link-table-type { + width: 10em; } + +.link-table th { + border-top: 2px solid rgba(180, 180, 180, 0.4); + border-bottom: 2px solid rgba(180, 180, 180, 0.4); } + +.link-table td { + border-bottom: 1px solid rgba(180, 180, 180, 0.3); } + +.link-table a { + color: #fff; } /*# sourceMappingURL=main.css.map */ diff --git a/app/styles/css/main.css.map b/app/styles/css/main.css.map index 15e38bb..6a68888 100644 --- a/app/styles/css/main.css.map +++ b/app/styles/css/main.css.map @@ -1 +1 @@ -{"version":3,"sources":[],"names":[],"mappings":"","file":"main.css","sourceRoot":"/source/","sourcesContent":[]} \ No newline at end of file +{"version":3,"sources":["main.css"],"names":[],"mappings":"AAAA;EACE,qBAAc;EAAd,qBAAc;EAAd,cAAc;EACd,6BAAuB;EAAvB,8BAAuB;MAAvB,2BAAuB;UAAvB,uBAAuB;EACvB,0BAA+B;MAA/B,uBAA+B;UAA/B,+BAA+B;EAC/B,0BAAoB;MAApB,uBAAoB;UAApB,oBAAoB;EACpB,aAAa;EACb,kBAAkB;EAClB,UAAU;EACV,WAAW;EACX,mBAAmB,EAAE;;AAEvB;EACE,mBAAmB;EACnB,aAAa;EACb,UAAU;EACV,sBAAsB;EACtB,YAAY;EACZ,oBAAoB;EACpB,mBAAmB,EAAE;;AAEvB;EACE,SAAS;EACT,YAAY,EAAE;;AAEhB;EACE,YAAY;EACZ,gBAAgB;EAChB,iBAAiB;EACjB,qBAAqB,EAAE;;AAEzB;;;EAGE,UAAU,EAAE;;AAEd;EACE,qCAAqC;EACrC,kBAAkB,EAAE;;AAEtB;;EAEE,YAAY;EACZ,UAAU;EACV,kBAAkB;EAClB,eAAe;EACf,iBAAiB,EAAE;EACnB;;IAEE,eAAe,EAAE;EACnB;;IAEE,mBAAmB,EAAE;;AAEzB;EACE,gBAAgB,EAAE;;AAEpB;EACE,aAAa;EACb,iBAAiB,EAAE;EACnB;IACE,uBAAuB;IACvB,eAAe;IACf,6CAA6C,EAAE;EACjD;IACE,iBAAiB;IACjB,kBAAkB,EAAE;EACtB;IACE,eAAe,EAAE;;AAErB;EACE,mBAAmB;EACnB,sBAAsB,EAAE;;AAE1B;EACE,iBAAiB;EACjB,kGAAkG,EAAE;;AAEtG;EACE,aAAa;EACb,oBAAoB;EACpB,mBAAmB,EAAE;;AAEvB;EACE,aAAa;EACb,oBAAoB,EAAE;;AAExB;EACE,YAAY;EACZ,cAAc;EACd,gBAAgB;EAChB,gBAAgB;EAChB,mBAAmB,EAAE;;AAEvB;EACE,gBAAgB,EAAE;;AAEpB;EACE,YAAY,EAAE;;AAEhB;EACE,+CAA+C;EAC/C,kDAAkD,EAAE;;AAEtD;EACE,kDAAkD,EAAE;;AAEtD;EACE,YAAY,EAAE","file":"main.css","sourcesContent":["body {\n display: flex;\n flex-direction: column;\n justify-content: space-between;\n align-items: center;\n width: 100vw;\n min-height: 100vh;\n margin: 0;\n padding: 0;\n text-align: center; }\n\ndiv.details, div.tooltip {\n position: absolute;\n padding: 8px;\n border: 0;\n font: 12px sans-serif;\n color: #fff;\n background: #505964;\n border-radius: 8px; }\n\ndiv.details {\n right: 0;\n bottom: 4px; }\n\ndiv.tooltip {\n z-index: 10;\n min-width: 80px;\n min-height: 30px;\n pointer-events: none; }\n\nh1,\nh2,\nh3 {\n margin: 0; }\n\nh1 {\n font-family: 'Diplomata SC', cursive;\n font-size: 1.8rem; }\n\nheader,\nfooter {\n width: 100%;\n margin: 0;\n padding: .25rem 0;\n color: #f2f152;\n background: #000; }\n header a,\n footer a {\n color: #f2f152; }\n header .author,\n footer .author {\n margin-right: 3rem; }\n\nfooter {\n font-size: .8em; }\n\n.intro-content {\n width: 900px;\n text-align: left; }\n .intro-content .quote {\n padding: 0 6rem 0 3rem;\n color: #6d787e;\n box-shadow: inset 8px 0 0 rgba(0, 0, 0, 0.3); }\n .intro-content .page-summary {\n margin-bottom: 0;\n font-size: 1.1rem; }\n .intro-content a {\n color: #296cbd; }\n\n.graph-container {\n position: relative;\n display: inline-block; }\n\n.chart-palette {\n background: #fff;\n box-shadow: 0 1px 4px rgba(0, 0, 0, 0.3), 0 0 40px rgba(0, 0, 0, 0.1) inset, 0 10px 6px -6px #777; }\n\n.link line {\n stroke: #999;\n stroke-width: 1.5px;\n stroke-opacity: .6; }\n\n.node circle {\n stroke: #fff;\n stroke-width: 1.5px; }\n\n.table-container {\n width: 100%;\n height: 120px;\n margin-top: 1em;\n font-size: .8em;\n overflow-y: scroll; }\n\n.link-table-name, .link-table-details {\n max-width: 30em; }\n\n.link-table-type {\n width: 10em; }\n\n.link-table th {\n border-top: 2px solid rgba(180, 180, 180, 0.4);\n border-bottom: 2px solid rgba(180, 180, 180, 0.4); }\n\n.link-table td {\n border-bottom: 1px solid rgba(180, 180, 180, 0.3); }\n\n.link-table a {\n color: #fff; }\n"],"sourceRoot":"/source/"} \ No newline at end of file diff --git a/app/styles/sass/main.sass b/app/styles/sass/main.sass index e69de29..9d6c732 100644 --- a/app/styles/sass/main.sass +++ b/app/styles/sass/main.sass @@ -0,0 +1,137 @@ +// Variables + +$primary-light-color: #fff +$secondary-light-color: #f2f152 +$primary-dark-color: #000 +$secondary-dark-color: #505964 +$quote-text-color: #6d787e +$intro-link-color: #296cbd +$link-color: #999 +$line-width: 1.5px + + +// Mixins + +=table-border($side, $width, $opacity) + border-#{$side}: $width solid rgba(180, 180, 180, $opacity) + + +// Styling + +body + display: flex + flex-direction: column + justify-content: space-between + align-items: center + width: 100vw + min-height: 100vh + margin: 0 + padding: 0 + text-align: center + +div + &.details, + &.tooltip + position: absolute + padding: 8px + border: 0 + font: 12px sans-serif + color: $primary-light-color + background: $secondary-dark-color + border-radius: 8px + + &.details + right: 0 + bottom: 4px + + &.tooltip + z-index: 10 + min-width: 80px + min-height: 30px + pointer-events: none + +h1, +h2, +h3 + margin: 0 + +h1 + font-family: 'Diplomata SC', cursive + font-size: 1.8rem + +header, +footer + width: 100% + margin: 0 + padding: .25rem 0 + color: $secondary-light-color + background: $primary-dark-color + + a + color: $secondary-light-color + + .author + margin-right: 3rem + +footer + font-size: .8em + +.intro-content + width: 900px + text-align: left + + .quote + padding: 0 6rem 0 3rem + color: $quote-text-color + box-shadow: inset 8px 0 0 rgba(0, 0, 0, .3) + + .page-summary + margin-bottom: 0 + font-size: 1.1rem + + a + color: $intro-link-color + +.graph-container + position: relative + display: inline-block + +.chart-palette + background: $primary-light-color + box-shadow: 0 1px 4px rgba(0, 0, 0, .3), 0 0 40px rgba(0, 0, 0, .1) inset, 0 10px 6px -6px #777 + +.link + line + stroke: $link-color + stroke-width: $line-width + stroke-opacity: .6 + +.node + circle + stroke: $primary-light-color + stroke-width: $line-width + +.table-container + width: 100% + height: 120px + margin-top: 1em + font-size: .8em + overflow-y: scroll + +.link-table + &-name, + &-details + max-width: 30em + + &-type + width: 10em + + th + +table-border(top, 2px, .4) + +table-border(bottom, 2px, .4) + + td + +table-border(bottom, 1px, .3) + + a + color: $primary-light-color diff --git a/dist/index.html b/dist/index.html index fc04bbd..db95818 100644 --- a/dist/index.html +++ b/dist/index.html @@ -2,27 +2,58 @@ - TrumpWorldGraph + Trump World Connections Graph - - + + +

Trump World Connections

+ +
+

+ From the Buzzfeed article + + Help Us Map TrumpWorld + +

+ +

+ No American president has taken office with a giant network of businesses, investments, + and corporate connections like that amassed by Donald J. Trump. His family and advisers + have touched a staggering number of ventures, from a hotel in Azerbaijan to a poker company + in Las Vegas. +

+ +

+ The force-directed graph below represents the relationship between all known entities + with three or more links to other entities within Trump's world. I made use + of this great dataset and + d3.js to create this graph. Click the nodes for more information. +

+
-
+
+ + - - + + - - + + diff --git a/dist/scripts/js/main.min.js b/dist/scripts/js/main.min.js index 88ab318..9f39bb8 100644 --- a/dist/scripts/js/main.min.js +++ b/dist/scripts/js/main.min.js @@ -1,14 +1,75 @@ 'use strict'; -var dataPath = 'assets/datasets/trumpworld.csv'; +/** + * Set Global Graph/Similation Variables + */ + +var width = 900, + height = width / 1.6, + fillColors = { Person: 'rgba(44, 143, 204, 0.95)', Organization: 'rgba(244, 89, 58, 0.95)' }, + dataPath = 'assets/datasets/trumpworld.csv'; + +var simulation = d3.forceSimulation().force('link', d3.forceLink().id(function (d) { + return d.id; +}).distance([50])).force('charge', d3.forceManyBody().strength(function (d) { + return [-4 * Math.pow(d.links, 1 / 2) - 20]; +}).distanceMin([0.001]).distanceMax([width / 2])).force('center', d3.forceCenter(width / 2, height / 2)); + +/** + * Create/Append HTML Graph Components + */ + +var svg = d3.select('#graph').append('svg').attrs({ height: height, width: width, class: 'chart-palette' }); + +var detailDiv = d3.select("#graph").append("div").attrs({ id: "details", class: "details" }).style("opacity", 0); + +var tooltipDiv = d3.select("body").append("div").attr("class", "tooltip").style("opacity", 0); + +/** + * Define Graph & Simulation Functions + */ + +var updateDetailDiv = function updateDetailDiv(d) { + detailDiv.transition().duration(200).styles({ opacity: 0.9, 'z-index': 5 }); + + detailDiv.styles({ + 'min-width': width / 3 + 'px', + 'height': height / 3 + 'px' + }).html('' + d.id + '
\n
\n Type: ' + d.group + '
\n Links: ' + d.links + '
\n
\n \n \n \n \n \n \n \n \n \n \n \n \n \n ' + d.connections.reduce(function (a, b, i) { + return a + '\n \n \n \n \n '; + }, "") + '\n \n \n
'); +}; + +var dragStarted = function dragSimulationStarted(d) { + if (!d3.event.active) simulation.alphaTarget(0.3).restart(); + + d.fx = d.x; + d.fy = d.y; +}; + +var dragged = function draggedSimulation(d) { + d.fx = Math.max(d.radius, Math.min(width - d.radius, d3.event.x)); + d.fy = Math.max(d.radius, Math.min(height - d.radius, d3.event.y)); +}; + +var dragEnded = function dragSimulationEnded(d) { + if (!d3.event.active) simulation.alphaTarget(0); + d.fx = null; + d.fy = null; + + updateDetailDiv(d); +}; var treatData = function treatGraphData(data) { var entityLetters = ['A', 'B'], - groupValues = { Organization: 0, Person: 1 }; + linksCutoff = 3; var nodeData = {}, + rowObj = {}, nodes = [], links = [], + endNodes = [], + finalEntities = new Set(), entity = ''; data.forEach(function (row) { @@ -18,21 +79,21 @@ var treatData = function treatGraphData(data) { if (nodeData[entity]) { nodeData[entity].links++; nodeData[entity].connections.push({ - Name: row['Entity ' + entityLetters[1 - i]], - Type: row['Entity ' + entityLetters[1 - i] + ' Type'], - Connection: row['Connection'], - 'Source(s)': row['Source(s)'] + name: row['Entity ' + entityLetters[1 - i]], + type: row['Entity ' + entityLetters[1 - i] + ' Type'], + connection: row['Connection'], + source: row['Source(s)'] }); } else { nodeData[entity] = { id: entity, - group: groupValues[row['Entity ' + key + ' Type']], links: 1, + group: row['Entity ' + key + ' Type'], connections: [{ - Name: row['Entity ' + entityLetters[1 - i]], - Type: row['Entity ' + entityLetters[1 - i] + ' Type'], - Connection: row['Connection'], - 'Source(s)': row['Source(s)'] + name: row['Entity ' + entityLetters[1 - i]], + type: row['Entity ' + entityLetters[1 - i] + ' Type'], + connection: row['Connection'], + source: row['Source(s)'] }] }; } @@ -42,10 +103,95 @@ var treatData = function treatGraphData(data) { }); for (var key in nodeData) { - nodes.push(nodeData[key]); - }return { nodes: nodes, links: links }; + rowObj = nodeData[key]; + + if (rowObj.links <= linksCutoff) endNodes.push(key);else rowObj['radius'] = Math.pow(rowObj.links, 9 / 16) + 3, nodes.push(rowObj); + } + + links = links.filter(function (el) { + return !endNodes.includes(el.source) && !endNodes.includes(el.target); + }); + + links.forEach(function (el) { + finalEntities.add(el.source), finalEntities.add(el.target); + }); + + nodes = nodes.filter(function (el) { + return finalEntities.has(el.id); + }).sort(function (a, b) { + return b.links - a.links; + }); + + nodes.forEach(function (el) { + return el.connections.sort(function (a, b) { + return a.name <= b.name ? -1 : 1; + }); + }); + + return { nodes: nodes, links: links }; +}; + +var forceDirectedGraph = function createForceDirectedGraph(nodes, links) { + var link = svg.append('g').attr('class', 'link').selectAll('line').data(links).enter().append('line'); + + var node = svg.append('g').attr('class', 'node').selectAll('circle').data(nodes).enter().append('circle').attrs({ + r: function r(d) { + return d.radius; + }, + fill: function fill(d) { + return fillColors[d.group]; + } + }).on('mouseover', function (d) { + tooltipDiv.transition().duration(200).style('opacity', 0.9); + tooltipDiv.html(d.id + '
\n Type: ' + d.group + '
\n Links: ' + d.links).styles({ left: d3.event.pageX + 'px', top: d3.event.pageY + 12 + "px" }); + }).on('mouseout', function (d) { + tooltipDiv.transition().duration(500).style("opacity", 0); + }).call(d3.drag().on('start', dragStarted).on('drag', dragged).on('end', dragEnded)); + + var ticked = function linkTicked() { + link.attrs({ + 'x1': function x1(d) { + return d.source.x; + }, + 'y1': function y1(d) { + return d.source.y; + }, + 'x2': function x2(d) { + return d.target.x; + }, + 'y2': function y2(d) { + return d.target.y; + } + }); + + node.attrs({ + cx: function cx(d) { + return d.x = Math.max(d.radius, Math.min(width - d.radius, d.x)); + }, + cy: function cy(d) { + return d.y = Math.max(d.radius, Math.min(height - d.radius, d.y)); + } + }); + }; + + d3.select('body').on('click', function (e) { + if (d3.event.target.localName !== 'circle' && d3.event.path.every(function (el) { + return el.id !== 'details'; + })) { + + detailDiv.transition().duration(333).styles({ opacity: 0, 'z-index': -1 }); + } + }); + + simulation.nodes(nodes).on('tick', ticked); + + simulation.force('link').links(links); }; +/** + * Run Force Directed Graph Simulation + */ + d3.csv(dataPath, function (error, data) { if (error) throw error; @@ -53,8 +199,5 @@ d3.csv(dataPath, function (error, data) { nodes = _treatData.nodes, links = _treatData.links; - console.log(nodes[0]); - console.log(links[0]); - - //forceDirectedGraph(nodes, links); + forceDirectedGraph(nodes, links); }); \ No newline at end of file diff --git a/dist/styles/css/main.min.css b/dist/styles/css/main.min.css index 6537dac..5290dff 100644 --- a/dist/styles/css/main.min.css +++ b/dist/styles/css/main.min.css @@ -1,2 +1,119 @@ +body { + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-orient: vertical; + -webkit-box-direction: normal; + -ms-flex-direction: column; + flex-direction: column; + -webkit-box-pack: justify; + -ms-flex-pack: justify; + justify-content: space-between; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + width: 100vw; + min-height: 100vh; + margin: 0; + padding: 0; + text-align: center; } + +div.details, div.tooltip { + position: absolute; + padding: 8px; + border: 0; + font: 12px sans-serif; + color: #fff; + background: #505964; + border-radius: 8px; } + +div.details { + right: 0; + bottom: 4px; } + +div.tooltip { + z-index: 10; + min-width: 80px; + min-height: 30px; + pointer-events: none; } + +h1, +h2, +h3 { + margin: 0; } + +h1 { + font-family: 'Diplomata SC', cursive; + font-size: 1.8rem; } + +header, +footer { + width: 100%; + margin: 0; + padding: .25rem 0; + color: #f2f152; + background: #000; } + header a, + footer a { + color: #f2f152; } + header .author, + footer .author { + margin-right: 3rem; } + +footer { + font-size: .8em; } + +.intro-content { + width: 900px; + text-align: left; } + .intro-content .quote { + padding: 0 6rem 0 3rem; + color: #6d787e; + box-shadow: inset 8px 0 0 rgba(0, 0, 0, 0.3); } + .intro-content .page-summary { + margin-bottom: 0; + font-size: 1.1rem; } + .intro-content a { + color: #296cbd; } + +.graph-container { + position: relative; + display: inline-block; } + +.chart-palette { + background: #fff; + box-shadow: 0 1px 4px rgba(0, 0, 0, 0.3), 0 0 40px rgba(0, 0, 0, 0.1) inset, 0 10px 6px -6px #777; } + +.link line { + stroke: #999; + stroke-width: 1.5px; + stroke-opacity: .6; } + +.node circle { + stroke: #fff; + stroke-width: 1.5px; } + +.table-container { + width: 100%; + height: 120px; + margin-top: 1em; + font-size: .8em; + overflow-y: scroll; } + +.link-table-name, .link-table-details { + max-width: 30em; } + +.link-table-type { + width: 10em; } + +.link-table th { + border-top: 2px solid rgba(180, 180, 180, 0.4); + border-bottom: 2px solid rgba(180, 180, 180, 0.4); } + +.link-table td { + border-bottom: 1px solid rgba(180, 180, 180, 0.3); } + +.link-table a { + color: #fff; } /*# sourceMappingURL=main.css.map */ diff --git a/package.json b/package.json index 9421363..aee7c9a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "trump-world-graph", - "version": "0.1.0", + "version": "1.0.0", "description": "TrumpWorld Associate Connections - Force Directed Graph", "main": "index.html", "scripts": { @@ -17,7 +17,12 @@ "force-directed", "graph", "javascript", - "freecodecamp" + "freecodecamp", + "trump", + "donald trump", + "corruption", + "interactive", + "visualization" ], "author": "Isaac Burbank", "license": "MIT",