Last active
July 9, 2020 12:18
-
-
Save MichaelCurrie/5e2da378a53ea624082cb55e78fdfa05 to your computer and use it in GitHub Desktop.
Bubble chart
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
a, a:visited, a:active { | |
color: #444; | |
} | |
.container { | |
max-width: 940px; | |
margin: auto; | |
} | |
.button { | |
min-width: 130px; | |
padding: 4px 5px; | |
cursor: pointer; | |
text-align: center; | |
font-size: 13px; | |
border: 1px solid #e0e0e0; | |
text-decoration: none; | |
} | |
.button.active { | |
background: #000; | |
color: #fff; | |
} | |
#vis { | |
width: 940px; | |
height: 700px; | |
clear: both; | |
margin-bottom: 10px; | |
margin-top: 30px; | |
} | |
#toolbar { | |
margin-top: 10px; | |
} | |
.bubble_group_label { | |
font-size: 21px; | |
fill: #aaa; | |
cursor: default; | |
} | |
.tooltip { | |
position: absolute; | |
top: 100px; | |
left: 100px; | |
-moz-border-radius:5px; | |
border-radius: 5px; | |
border: 2px solid #000; | |
background: #fff; | |
opacity: .9; | |
color: black; | |
padding: 10px; | |
width: 300px; | |
font-size: 12px; | |
z-index: 10; | |
} | |
.tooltip .title { | |
font-size: 13px; | |
} | |
.tooltip .name { | |
font-weight:bold; | |
} | |
.footer { | |
text-align: center; | |
} | |
.world_map { | |
stroke: white; | |
stroke-width: 0.25px; | |
fill: grey; | |
} |
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
/* Bubble chart | |
* | |
* Based on Jim Vallandingham's work | |
* Organization and style inspired by: | |
* https://bost.ocks.org/mike/chart/ | |
* | |
*/ | |
function createBubbleChart() { | |
/* bubbleChart creation function. Returns a function that will | |
* instantiate a new bubble chart given a DOM element to display | |
* it in and a dataset to visualize. | |
*/ | |
// Tooltip object for mouseover functionality, width 200 | |
var tooltip = floatingTooltip('bubble_chart_tooltip', 200); | |
// These will be set in the `bubbleChart` function | |
var svg = null, inner_svg = null; | |
var bubbles = null; | |
var forceSim = null; | |
var fillColorScale = null; | |
var radiusScale = null; | |
var nodes = []; | |
var margin = null; | |
var width = null; | |
var height = null; | |
var dataExtents = {}; | |
// For scatterplots (initialized if applicable) | |
var xAxis = null; | |
var yAxis = null; | |
var xScale = null; | |
var yScale = null; | |
// For the map | |
var bubbleMercProjection = d3.geoAlbers(); | |
function getFillColorScale() { | |
// Obtain a color mapping from keys to color values specified in our parameters file | |
// Get the keys and values from the parameters file | |
var color_groupsKeys = Object.keys(BUBBLE_PARAMETERS.fill_color.color_groups) | |
var color_groupsValues = [] | |
for (var i=0; i<color_groupsKeys.length; i++) { | |
var key = color_groupsKeys[i] | |
color_groupsValues.push(BUBBLE_PARAMETERS.fill_color.color_groups[key]) | |
} | |
// Generate the key -> value mapping for colors | |
var fillColorScale = d3.scaleOrdinal() | |
.domain(color_groupsKeys) | |
.range(color_groupsValues); | |
return fillColorScale; | |
} | |
function createNodes(rawData) { | |
/* | |
* This data manipulation function takes the raw data from | |
* the CSV file and converts it into an array of node objects. | |
* Each node will store data and visualization values to visualize | |
* a bubble. | |
* | |
* rawData is expected to be an array of data objects, read in from | |
* one of d3's loading functions like d3.csv. | |
* | |
* This function returns the new node array, with a node in that | |
* array for each element in the rawData input. | |
*/ | |
// Use map() to convert raw data into node data. | |
var myNodes = rawData.map(function (data_row) { | |
node = { | |
id: data_row.id, | |
scaled_radius: radiusScale(+data_row[BUBBLE_PARAMETERS.radius_field]), | |
actual_radius: +data_row[BUBBLE_PARAMETERS.radius_field], | |
fill_color_group: data_row[BUBBLE_PARAMETERS.fill_color.data_field], | |
// Put each node initially in a random location | |
x: Math.random() * width, | |
y: Math.random() * height | |
}; | |
for(var key in data_row) { | |
// Skip loop if the property is from prototype | |
if (!data_row.hasOwnProperty(key)) continue; | |
node[key] = data_row[key]; | |
} | |
return node; | |
}); | |
// Sort them to prevent occlusion of smaller nodes. | |
myNodes.sort(function (a, b) { return b.actual_radius - a.actual_radius; }); | |
return myNodes; | |
} | |
function getGridTargetFunction(mode) { | |
// Given a mode, return an anonymous function that maps nodes to target coordinates | |
if (mode.type != "grid") { | |
throw "Error: getGridTargetFunction called with mode != 'grid'"; | |
} | |
return function (node) { | |
// Given a mode and node, return the correct target | |
if(mode.size == 1) { | |
// If there is no grid, our target is the default center | |
target = mode.gridCenters[""]; | |
} else { | |
// If the grid size is greater than 1, look up the appropriate target | |
// coordinate using the relevant node_tag for the mode we are in | |
node_tag = node[mode.dataField] | |
target = mode.gridCenters[node_tag]; | |
} | |
return target; | |
} | |
} | |
function showLabels(mode) { | |
/* | |
* Shows labels for each of the positions in the grid. | |
*/ | |
var currentLabels = mode.labels; | |
var bubble_group_labels = inner_svg.selectAll('.bubble_group_label') | |
.data(currentLabels); | |
var grid_element_half_height = height / (mode.gridDimensions.rows * 2); | |
bubble_group_labels.enter().append('text') | |
.attr('class', 'bubble_group_label') | |
.attr('x', function (d) { return mode.gridCenters[d].x; }) | |
.attr('y', function (d) { return mode.gridCenters[d].y - grid_element_half_height; }) | |
.attr('text-anchor', 'middle') // centre the text | |
.attr('dominant-baseline', 'hanging') // so the text is immediately below the bounding box, rather than above | |
.text(function (d) { return d; }); | |
// GRIDLINES FOR DEBUGGING PURPOSES | |
/* | |
var grid_element_half_height = height / (mode.gridDimensions.rows * 2); | |
var grid_element_half_width = width / (mode.gridDimensions.columns * 2); | |
for (var key in currentMode.gridCenters) { | |
if (currentMode.gridCenters.hasOwnProperty(key)) { | |
var rectangle = inner_svg.append("rect") | |
.attr("class", "mc_debug") | |
.attr("x", currentMode.gridCenters[key].x - grid_element_half_width) | |
.attr("y", currentMode.gridCenters[key].y - grid_element_half_height) | |
.attr("width", grid_element_half_width*2) | |
.attr("height", grid_element_half_height*2) | |
.attr("stroke", "red") | |
.attr("fill", "none"); | |
var ellipse = inner_svg.append("ellipse") | |
.attr("class", "mc_debug") | |
.attr("cx", currentMode.gridCenters[key].x) | |
.attr("cy", currentMode.gridCenters[key].y) | |
.attr("rx", 15) | |
.attr("ry", 10); | |
} | |
}*/ | |
} | |
function tooltipContent(d) { | |
/* | |
* Helper function to generate the tooltip content | |
* | |
* Parameters: d, a dict from the node | |
* Returns: a string representing the formatted HTML to display | |
*/ | |
var content = '' | |
// Loop through all lines we want displayed in the tooltip | |
for(var i=0; i<BUBBLE_PARAMETERS.tooltip.length; i++) { | |
var cur_tooltip = BUBBLE_PARAMETERS.tooltip[i]; | |
var value_formatted; | |
// If a format was specified, use it | |
if ("format_string" in cur_tooltip) { | |
value_formatted = | |
d3.format(cur_tooltip.format_string)(d[cur_tooltip.data_field]); | |
} else { | |
value_formatted = d[cur_tooltip.data_field]; | |
} | |
// If there was a previous tooltip line, add a newline separator | |
if (i > 0) { | |
content += '<br/>'; | |
} | |
content += '<span class="name">' + cur_tooltip.title + ': </span>'; | |
content += '<span class="value">' + value_formatted + '</span>'; | |
} | |
return content; | |
} | |
function showTooltip(d) { | |
/* | |
* Function called on mouseover to display the | |
* details of a bubble in the tooltip. | |
*/ | |
// Change the circle's outline to indicate hover state. | |
d3.select(this).attr('stroke', 'black'); | |
// Show the tooltip | |
tooltip.showTooltip(tooltipContent(d), d3.event); | |
} | |
function hideTooltip(d) { | |
/* | |
* Hide tooltip | |
*/ | |
// Reset the circle's outline back to its original color. | |
var originalColor = d3.rgb(fillColorScale(d.fill_color_group)).darker() | |
d3.select(this).attr('stroke', originalColor); | |
// Hide the tooltip | |
tooltip.hideTooltip(); | |
} | |
function ticked() { | |
bubbles.each(function (node) {}) | |
.attr("cx", function(d) { return d.x; }) | |
.attr("cy", function(d) { return d.y; }); | |
} | |
function showAxis(mode) { | |
/* | |
* Show the axes. | |
*/ | |
// Set up axes | |
xAxis = xScale; //d3.scaleBand().rangeRound([0, width]).padding(0.1); | |
yAxis = yScale; //d3.scaleLinear().rangeRound([height, 0]); | |
inner_svg.append("g") | |
.attr("class", "axis axis--x") | |
.attr("transform", "translate(0," + height + ")") | |
.call(d3.axisBottom(xAxis)) | |
inner_svg.append("text") | |
.attr("class", "axis axis--x label") | |
.attr("transform", "translate(" + (width/2) + " , " + (height) + ")") | |
// so the text is immediately below the bounding box, rather than above | |
.attr('dominant-baseline', 'hanging') | |
.attr("dy", "1.5em") | |
.style("text-anchor", "middle") | |
.text(mode.xDataField); | |
inner_svg.append("g") | |
.attr("class", "axis axis--y") | |
.call(d3.axisLeft(yAxis).ticks(10))//, "%")) | |
inner_svg.append("text") | |
.attr("class", "axis axis--y label") | |
// We need to compose a rotation with a translation to place the y-axis label | |
.attr("transform", "translate(" + 0 + ", " + (height/2) + ")rotate(-90)") | |
.attr("dy", "-3em") | |
.attr("text-anchor", "middle") | |
.text(mode.yDataField); | |
} | |
function createBubbles() { | |
// Bind nodes data to what will become DOM elements to represent them. | |
inner_svg.selectAll('.bubble') | |
.data(nodes, function (d) { return d.id; }) | |
// Create new circle elements each with class `bubble`. | |
// There will be one circle.bubble for each object in the nodes array. | |
.enter() | |
.append('circle').attr('r', 0) // Initially, their radius (r attribute) will be 0. | |
.classed('bubble', true) | |
.attr('fill', function (d) { return fillColorScale(d.fill_color_group); }) | |
.attr('stroke', function (d) { return d3.rgb(fillColorScale(d.fill_color_group)).darker(); }) | |
.attr('stroke-width', 2) | |
.on('mouseover', showTooltip) | |
.on('mouseout', hideTooltip); | |
bubbles = d3.selectAll('.bubble'); | |
// Fancy transition to make bubbles appear, ending with the correct radius | |
bubbles.transition() | |
.duration(2000) | |
.attr('r', function (d) { return d.scaled_radius; }); | |
} | |
function addForceLayout(isStatic) { | |
if (forceSim) { | |
// Stop any forces currently in progress | |
forceSim.stop(); | |
} | |
// Configure the force layout holding the bubbles apart | |
forceSim = d3.forceSimulation() | |
.nodes(nodes) | |
.velocityDecay(0.3) | |
.on("tick", ticked); | |
if (!isStatic) { | |
// Decide what kind of force layout to use: "collide" or "charge" | |
if(BUBBLE_PARAMETERS.force_type == "collide") { | |
var bubbleCollideForce = d3.forceCollide() | |
.radius(function(d) { return d.scaled_radius + 0.5; }) | |
.iterations(4) | |
forceSim | |
.force("collide", bubbleCollideForce) | |
} | |
if(BUBBLE_PARAMETERS.force_type == "charge") { | |
function bubbleCharge(d) { | |
return -Math.pow(d.scaled_radius, 2.0) * (+BUBBLE_PARAMETERS.force_strength); | |
} | |
forceSim | |
.force('charge', d3.forceManyBody().strength(bubbleCharge)); | |
} | |
} | |
} | |
function createCanvas(parentDOMElement) { | |
// Create a SVG element inside the provided selector with desired size. | |
svg = d3.select(parentDOMElement) | |
.append('svg') | |
.attr('width', BUBBLE_PARAMETERS.width) | |
.attr('height', BUBBLE_PARAMETERS.height); | |
// Specify margins and the inner width and height | |
margin = {top: 20, right: 20, bottom: 50, left: 80}, | |
width = +svg.attr("width") - margin.left - margin.right, | |
height = +svg.attr("height") - margin.top - margin.bottom; | |
// Create an inner SVG panel with padding on all sides for axes | |
inner_svg = svg.append("g") | |
.attr("transform", "translate(" + margin.left + "," + margin.top + ")"); | |
} | |
////////////////////////////////////////////////////////////// | |
var bubbleChart = function bubbleChart(parentDOMElement, rawData) { | |
/* | |
* Main entry point to the bubble chart. This function is returned | |
* by the parent closure. It prepares the rawData for visualization | |
* and adds an svg element to the provided selector and starts the | |
* visualization creation process. | |
* | |
* parentDOMElement is expected to be a DOM element or CSS selector that | |
* points to the parent element of the bubble chart. Inside this | |
* element, the code will add the SVG continer for the visualization. | |
* | |
* rawData is expected to be an array of data objects as provided by | |
* a d3 loading function like d3.csv. | |
*/ | |
// Capture all the maximums and minimums in the numeric fields, which | |
// will be used in any scatterplots. | |
for (var numeric_field_index in BUBBLE_PARAMETERS.numeric_fields) { | |
var numeric_field = BUBBLE_PARAMETERS.numeric_fields[numeric_field_index]; | |
dataExtents[numeric_field] = d3.extent(rawData, function (d) { return +d[numeric_field]; }); | |
} | |
// Scale bubble radii using ^(0.5) | |
// We size bubbles based on area instead of radius | |
var maxRadius = dataExtents[BUBBLE_PARAMETERS.radius_field][1]; | |
radiusScale = d3.scalePow() | |
.exponent(0.5) | |
.range([2, 30]) // Range between 2 and 25 pixels | |
.domain([0, maxRadius]); // Domain between 0 and the largest bubble radius | |
fillColorScale = getFillColorScale(); | |
// Initialize the "nodes" with the data we've loaded | |
nodes = createNodes(rawData); | |
// Initialize svg and inner_svg, which we will attach all our drawing objects to. | |
createCanvas(parentDOMElement); | |
// Create a container for the map before creating the bubbles | |
// Then we will draw the map inside this container, so it will appear behind the bubbles | |
inner_svg.append("g") | |
.attr("class", "world_map_container"); | |
// Create the bubbles and the force holding them apart | |
createBubbles(); | |
}; | |
bubbleChart.switchMode = function (buttonID) { | |
/* | |
* Externally accessible function (this is attached to the | |
* returned chart function). Allows the visualization to toggle | |
* between display modes. | |
* | |
* buttonID is expected to be a string corresponding to one of the modes. | |
*/ | |
// Get data on the new mode we have just switched to | |
currentMode = new ViewMode(buttonID, width, height); | |
// Remove current labels | |
inner_svg.selectAll('.bubble_group_label').remove(); | |
// Remove current debugging elements | |
inner_svg.selectAll('.mc_debug').remove(); // DEBUG | |
// Remove axes components | |
inner_svg.selectAll('.axis').remove(); | |
// Remove map | |
inner_svg.selectAll('.world_map').remove(); | |
// SHOW LABELS (if we have more than one category to label) | |
if (currentMode.type == "grid" && currentMode.size > 1) { | |
showLabels(currentMode); | |
} | |
// SHOW AXIS (if our mode is scatter plot) | |
if (currentMode.type == "scatterplot") { | |
xScale = d3.scaleLinear().range([0, width]) | |
.domain([dataExtents[currentMode.xDataField][0], dataExtents[currentMode.xDataField][1]]); | |
yScale = d3.scaleLinear().range([height, 0]) | |
.domain([dataExtents[currentMode.yDataField][0], dataExtents[currentMode.yDataField][1]]); | |
showAxis(currentMode); | |
} | |
// ADD FORCE LAYOUT | |
if (currentMode.type == "scatterplot" || currentMode.type == "map") { | |
addForceLayout(true); // make it static so we can plot bubbles | |
} else { | |
addForceLayout(false); // the bubbles should repel about the grid centers | |
} | |
// SHOW MAP (if our mode is "map") | |
if (currentMode.type == "map") { | |
var path = d3.geoPath().projection(bubbleMercProjection); | |
d3.queue() | |
.defer(d3.json, BUBBLE_PARAMETERS.map_file) | |
.await(ready); | |
function ready(error, topology) { | |
if (error) throw error; | |
inner_svg.selectAll(".world_map_container") | |
.append("g") | |
.attr("class", "world_map") | |
.selectAll("path") | |
.data(topojson.feature(topology, topology.objects.states).features) | |
.enter() | |
.append("path") | |
.attr("d", path); | |
} | |
} | |
// MOVE BUBBLES TO THEIR NEW LOCATIONS | |
var targetFunction; | |
if (currentMode.type == "grid") { | |
targetFunction = getGridTargetFunction(currentMode); | |
} | |
if (currentMode.type == "scatterplot") { | |
targetFunction = function (d) { | |
return { | |
x: xScale(d[currentMode.xDataField]), | |
y: yScale(d[currentMode.yDataField]) | |
}; | |
}; | |
} | |
if (currentMode.type == "map") { | |
targetFunction = function (d) { | |
return { | |
x: bubbleMercProjection([+d.Longitude, +d.Latitude])[0], | |
y: bubbleMercProjection([+d.Longitude, +d.Latitude])[1] | |
}; | |
}; | |
} | |
// Given the mode we are in, obtain the node -> target mapping | |
var targetForceX = d3.forceX(function(d) {return targetFunction(d).x}) | |
.strength(+BUBBLE_PARAMETERS.force_strength); | |
var targetForceY = d3.forceY(function(d) {return targetFunction(d).y}) | |
.strength(+BUBBLE_PARAMETERS.force_strength); | |
// Specify the target of the force layout for each of the circles | |
forceSim | |
.force("x", targetForceX) | |
.force("y", targetForceY); | |
// Restart the force layout simulation | |
forceSim.alphaTarget(1).restart(); | |
}; | |
// Return the bubbleChart function from closure. | |
return bubbleChart; | |
} | |
///////////////////////////////////////////////////////////////////////////////////// | |
function ViewMode(button_id, width, height) { | |
/* ViewMode: an object that has useful parameters for each view mode. | |
* initialize it with your desired view mode, then use its parameters. | |
* Attributes: | |
- mode_index (which button was pressed) | |
- buttonId (which button was pressed) | |
- gridDimensions e.g. {"rows": 10, "columns": 20} | |
- gridCenters e.g. {"group1": {"x": 10, "y": 20}, ...} | |
- dataField (string) | |
- labels (an array) | |
- type (type of grouping: "grouping" or "scatterplot") | |
- size (number of groups) | |
*/ | |
// Find which button was pressed | |
var mode_index; | |
for(mode_index=0; mode_index<BUBBLE_PARAMETERS.modes.length; mode_index++) { | |
if(BUBBLE_PARAMETERS.modes[mode_index].button_id == button_id) { | |
break; | |
} | |
} | |
if(mode_index>=BUBBLE_PARAMETERS.modes.length) { | |
console.log("Error: can't find mode with button_id = ", button_id) | |
} | |
var curMode = BUBBLE_PARAMETERS.modes[mode_index]; | |
this.buttonId = curMode.button_id; | |
this.type = curMode.type; | |
if (this.type == "grid") { | |
this.gridDimensions = curMode.grid_dimensions; | |
this.labels = curMode.labels; | |
if (this.labels == null) { this.labels = [""]; } | |
this.dataField = curMode.data_field; | |
this.size = this.labels.length; | |
// Loop through all grid labels and assign the centre coordinates | |
this.gridCenters = {}; | |
for(var i=0; i<this.size; i++) { | |
var cur_row = Math.floor(i / this.gridDimensions.columns); // indexed starting at zero | |
var cur_col = i % this.gridDimensions.columns; // indexed starting at zero | |
var currentCenter = { | |
x: (2 * cur_col + 1) * (width / (this.gridDimensions.columns * 2)), | |
y: (2 * cur_row + 1) * (height / (this.gridDimensions.rows * 2)) | |
}; | |
this.gridCenters[this.labels[i]] = currentCenter; | |
} | |
} | |
if (this.type == "scatterplot") { | |
// Set up the x and y scales (domains need to be set using the actual data) | |
this.xDataField = curMode.x_data_field; | |
this.yDataField = curMode.y_data_field; | |
this.xFormatString = curMode.x_format_string; | |
this.yFormatString = curMode.y_format_string; | |
} | |
if (this.type == "map") { | |
this.latitudeField = curMode.latitude_field; | |
this.longitudeField = curMode.longitude_field; | |
} | |
}; | |
///////////////////////////////////////////////////////////////////////////////////// | |
///////////////////////////////////////////////////////////////////////////////////// | |
// Set title | |
document.title = BUBBLE_PARAMETERS.report_title; | |
report_title.innerHTML = BUBBLE_PARAMETERS.report_title; | |
// Set footer | |
document.getElementById("footer_text").innerHTML = BUBBLE_PARAMETERS.footer_text; | |
// Create a new bubble chart instance | |
var myBubbleChart = createBubbleChart(); | |
// Load data | |
d3.csv(BUBBLE_PARAMETERS.data_file, function (error, data) { | |
// Once the data is loaded... | |
if (error) { console.log(error); } | |
// Display bubble chart inside the #vis div. | |
myBubbleChart('#vis', data); | |
// Start the visualization with the first button | |
myBubbleChart.switchMode(BUBBLE_PARAMETERS.modes[0].button_id) | |
}); | |
function setupButtons() { | |
// As the data is being loaded: setup buttons | |
// Create the buttons | |
// TODO: change this to use native d3js selection methods | |
for (i = 0; i<BUBBLE_PARAMETERS.modes.length; i++) { | |
var button_element = document.createElement("a"); | |
button_element.href = "#"; | |
if (i == 0) { | |
button_element.className = "button active"; | |
} else { | |
button_element.className = "button"; | |
} | |
button_element.id = BUBBLE_PARAMETERS.modes[i].button_id; | |
button_element.innerHTML = BUBBLE_PARAMETERS.modes[i].button_text; | |
document.getElementById("toolbar").appendChild(button_element); | |
} | |
// Handle button click | |
// Set up the layout buttons to allow for toggling between view modes. | |
d3.select('#toolbar') | |
.selectAll('.button') | |
.on('click', function () { | |
// Remove active class from all buttons | |
d3.selectAll('.button').classed('active', false); | |
// Set the button just clicked to active | |
d3.select(this).classed('active', true); | |
// Get the id of the button | |
var buttonId = d3.select(this).attr('id'); | |
// Switch the bubble chart to the mode of | |
// the currently clicked button. | |
myBubbleChart.switchMode(buttonId); | |
}); | |
} | |
setupButtons(); |
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
var BUBBLE_PARAMETERS = { | |
"data_file": "DonaldTrumpNetWorth2016.csv", | |
"map_file": "us.json", | |
"report_title": "Donald Trump Net Worth 2016", | |
"footer_text": "Net Value of Assets owned by Donald J. Trump. Source: Jennifer Wang/Forbes.", | |
"width": 940, | |
"height": 600, | |
"force_strength": 0.03, | |
"force_type": "charge", | |
"radius_field": "Net Value", | |
"numeric_fields": ["Asset Value", "Debt", "Net Value", "Change vs 2015", "Stake"], | |
"fill_color": { | |
"data_field": "Change", | |
"color_groups": { | |
"Down": "#d84b2a", | |
"No Change": "#beccae", | |
"Up": "#7aa25c" | |
} | |
}, | |
"tooltip": [ | |
{"title": "Asset", "data_field": "Asset"}, | |
{"title": "Type", "data_field": "Type"}, | |
{"title": "Asset Value", "data_field": "Asset Value", "format_string": ",.2r"}, | |
{"title": "Debt", "data_field": "Debt", "format_string": ",.2r"}, | |
{"title": "Net Value", "data_field": "Net Value", "format_string": ",.2r"}, | |
{"title": "Change vs 2015", "data_field": "Change vs 2015", "format_string": ",.2r"} | |
], | |
"modes": [ | |
{ | |
"button_text": "All Assets", | |
"button_id": "all", | |
"type": "grid", | |
"labels": null, | |
"grid_dimensions": {"rows": 1, "columns": 1}, | |
"data_field": null | |
}, | |
{ | |
"button_text": "Assets by Type", | |
"button_id": "region", | |
"type": "grid", | |
"labels": ["Office and retail", "Residential and retail", "Hotel, condos and retail", "Affordable housing units", "Personal assets", "Golf resort", "Winery", "Licensing agreements", "Industrial warehouse"], | |
"grid_dimensions": {"rows": 3, "columns": 3}, | |
"data_field": "Type" | |
}, | |
{ | |
"button_text": "Assets by Change in Value", | |
"button_id": "Change", | |
"type": "grid", | |
"labels": ["Down", "No Change", "Up"], | |
"grid_dimensions": {"rows": 1, "columns": 3}, | |
"data_field": "Change" | |
}, | |
{ | |
"button_text": "Change in value vs Net Value", | |
"button_id": "change_vs_net_value", | |
"type": "scatterplot", | |
"x_data_field": "Net Value", | |
"y_data_field": "Change vs 2015", | |
"x_format_string": ",.2r", | |
"y_format_string": ",.2r" | |
}, | |
{ | |
"button_text": "Assets by Location", | |
"button_id": "assets_on_map", | |
"type": "map", | |
"latitude_field": "Latitude", | |
"longitude_field": "Longitude" | |
} | |
] | |
}; |
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
Asset | Type | Asset Value | Debt | Net Value | Change vs 2015 | Change | Stake | Latitude | Longitude | Notes | |
---|---|---|---|---|---|---|---|---|---|---|---|
Trump Tower (New York City) | Office and retail | 471 | 100 | 371 | -159 | Down | 1 | 40.768277 | -73.981455 | Opened 1983 | |
1290 Avenue of the Americas (New York City) | Office and retail | 2310 | 950 | 408 | -62 | Down | 0.3 | 40.768277 | -73.981455 | ||
Niketown (New York City) | Office and retail | 400 | 10 | 390 | -52 | Down | 1 | 40.768277 | -73.981455 | Ground lease through 2079 | |
40 Wall Street (New York City) | Office and retail | 501 | 156 | 345 | -28 | Down | 1 | 40.768277 | -73.981455 | ||
Trump Park Avenue (New York City) | Residential and retail | 191 | 14.3 | 176.7 | -27 | Down | 1 | 40.768277 | -73.981455 | 49,564 sq. ft. of condos; 27,467 sq. ft. of retail | |
Trump Parc/Trump Parc East (New York City) | Residential and retail | 88 | 0 | 88 | 17 | Up | 1 | 40.768277 | -73.981455 | 11,750 sq. ft. of condos; 14,963 sq. feet of retail; 13,108 sq. ft. of garage | |
Trump International Hotel and Tower, Central Park West (New York City) | Hotel, condos and retail | 38 | 0 | 38 | 21 | Up | 1 | 40.768277 | -73.981455 | ||
Trump World Tower, 845 United Nations Plaza (New York City) | Residential and retail | 27 | 0 | 27 | -16 | Down | 1 | 40.768277 | -73.981455 | 9,007 sq. ft. of retail; 28,579 sq. ft. of garage; one 2,835-square-foot condo | |
Spring Creek Towers (Brooklyn, N.Y.) | Affordable housing units | 1000 | 408 | 23.68 | 0 | No Change | 0.04 | 40.768277 | -73.981455 | Trump’s father, Fred, amassed a portfolio of 20,000 Brooklyn and Queens apartments worth hundreds of millions at one point. But Donald was more interested in Manhattan. Over time the family sold most of the outer-borough holdings. The lone remaining asset from his father’s era is a 4% interest in Spring Creek Towers, a massive, 46-tower government subsidized housing complex with 5,881 units in Brooklyn’s East New York neighborhood that the Trumps reportedly bought into in 1973. | |
Trump Plaza (New York City) | Residential and retail | 27.7 | 14.7 | 13 | -16 | Down | 1 | 40.768277 | -73.981455 | Ground lease through 2082 | |
Trump Tower Penthouse (New York City) | Personal assets | 90 | 0 | 90 | -10 | Down | 1 | 40.768277 | -73.981455 | Personal residence, 30,000 sq. ft. | |
555 California Street (San Francisco) | Office and retail | 1645 | 589 | 316.8 | 32 | Up | 0.3 | 37.792282 | -122.403747 | The other half of the deal that Trump’s Chinese investors completed in 2006. In exchange for a 78-acre tract of land on New York’s Upper West Side, the Chinese got 1290 Avenue of Americas in New York (see above) and 555 California Street in San Francisco, then called the Bank of America Center. While valuations for San Francisco office space have dipped, the building has brought a higher net income, raising the value of Trump’s stake by $32 million. | |
Trump National Doral Miami | Golf resort | 275 | 106 | 169 | -25 | Down | 1 | 25.813875 | -80.339183 | ||
Mar-A-Lago (Palm Beach, Florida) | Golf resort | 150 | 0 | 150 | -50 | Down | 1 | 26.677316 | -80.036959 | Private club | |
U.S. Golf Courses | Golf resort | 225 | 18.5 | 206.5 | -72 | Down | 1 | 39.80949 | -71.886246 | 10 golf courses in 6 states plus the District of Columbia | |
Scotland & Ireland Golf Courses | Golf resort | 85 | 0 | 85 | -3 | Down | 1 | 55.314129 | -4.828675 | 3 golf resorts in Scotland and Ireland | |
Trump Chicago | Hotel, condos and retail | 169 | 50 | 119 | -39 | Down | 1 | 41.888988 | -87.625997 | ||
Trump International Hotel Washington, D.C. | Hotel, condos and retail | 229 | 125 | 104 | -97 | Down | 1 | 38.89454 | -77.027011 | Ground lease through 2075 | |
Trump International Hotel Las Vegas | Hotel, condos and retail | 156 | 18 | 69 | -27 | Down | 0.5 | 36.129588 | -115.172671 | The gleaming hotel–which claims to be encased in 24-karat gold glass–has been more successful than Trump’s previous forays in gambling zones. While his Atlantic City casinos suffered through corporate bankruptcies, eventually reducing his stake to nothing, this joint venture with fellow real estate billionaire Phil Ruffin has become a premier destination by the Strip. It has 1,282 suites, more than half of which have been sold since it opened in 2008. It apparently has managed some of the properties for owners, renting them out. | |
Cash/Liquid Assets | Personal assets | 230 | 0 | 230 | -97 | Down | 1 | 39.80949 | -71.886246 | ||
Trump Winery (Charlottesville, Va.) | Winery | 30 | 0 | 30 | 0 | No Change | 1 | 37.93909 | -78.498251 | ||
Seven Springs (Bedford, N.Y.) | Personal assets | 37.5 | 20 | 17.5 | -5.5 | Down | 1 | 41.170502 | -73.700027 | Private estate | |
Trump Hotel Management & Licensing Business | Licensing agreements | 123 | 0 | 123 | -229 | Down | 1 | 39.80949 | -71.886246 | The management and licensing company has roughly two dozen properties under its umbrella. Trump’s organization manages some of the hotels and resorts, including Trump Vancouver. Others, like India’s luxury condo Trump Tower, merely pay Trump to use his name. The highly lucrative business has enabled the Donald to spread his brand from the Philippines to Uruguay. While Trump has struck more licensing arrangements in the past year, FORBES cut the value of the portfolio after several sources suggested that the revenue from wholly owned properties like Doral Miami and Trump Las Vegas (disclosed in Federal Election Commission filings) should not be included in the valuation. FORBES already values those assets separately and thus this year subtracted their estimated revenues from the management business to avoid double counting. | |
Product Licensing | Licensing agreements | 14 | 0 | 14 | -8.75 | Down | 1 | 39.80949 | -71.886246 | Products: Trump Home, Select by Trump (coffee), Trump Natural Spring Water, Trump Fragrance | |
Aircraft | Personal assets | 35 | 0 | 35 | -27 | Down | 1 | 39.80949 | -71.886246 | Models: Two 1989 Sikorsky S-76B Helicopters, one 1990 Sikorsky S-76B Helicopter, one 1991 Boeing 757, one 1997 Cessna 750 Citation X. | |
Two Palm Beach, Fla. Residences | Personal assets | 14.5 | 0 | 14.5 | 2.85 | Up | 1 | 26.714198 | -80.054904 | ||
809 N. Canon Drive, Beverly Hills | Personal assets | 9 | 0 | 9 | 0.5 | Up | 1 | 34.079731 | -118.413402 | ||
Stark Industrial Park, Charleston, S.C. | Industrial warehouse | 3.5 | 0 | 3.5 | 0 | No Change | 1 | 32.859908 | -79.906982 |
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> | |
<head> | |
<meta charset="utf-8"> | |
<title>Bubble Chart Template</title> | |
<meta name="viewport" content="width=device-width,initial-scale=1"> | |
<link rel="stylesheet" href="reset.css"> | |
<link rel="stylesheet" href="bubble_chart.css"> | |
<script src="bubble_parameters.js"></script> | |
</head> | |
<body> | |
<div class="container"> | |
<div id="header"> | |
<h1 id="report_title"></h1> | |
</div> | |
<div id="toolbar"> | |
</div> | |
<div id="vis"></div> | |
<div class="footer"> | |
<p id="footer_text"></p> | |
<p> | |
<a href="https://gist.github.com/MichaelCurrie/5e2da378a53ea624082cb55e78fdfa05">Code</a> | |
</p> | |
</div> | |
</div> | |
<script src="https://d3js.org/d3.v4.min.js"></script> | |
<script src="https://d3js.org/d3-scale-chromatic.v1.min.js"></script> | |
<script src="https://d3js.org/topojson.v2.min.js"></script> | |
<script src="tooltip.js"></script> | |
<script src="bubble_chart.js"></script> | |
</body> | |
</html> |
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
/* HTML5 ✰ Boilerplate | |
* ==|== normalize ========================================================== | |
*/ | |
article, aside, details, figcaption, figure, footer, header, hgroup, nav, section { display: block; } | |
audio, canvas, video { display: inline-block; *display: inline; *zoom: 1; } | |
audio:not([controls]) { display: none; } | |
[hidden] { display: none; } | |
html { font-size: 100%; overflow-y: scroll; -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; } | |
body { margin: 0; font-size: 13px; line-height: 1.231; } | |
body, button, input, select, textarea { font-family: sans-serif; color: #222; } | |
::-moz-selection { background: #fe57a1; color: #fff; text-shadow: none; } | |
::selection { background: #fe57a1; color: #fff; text-shadow: none; } | |
a { color: #00e; } | |
a:visited { color: #551a8b; } | |
a:hover { color: #06e; } | |
a:focus { outline: thin dotted; } | |
a:hover, a:active { outline: 0; } | |
abbr[title] { border-bottom: 1px dotted; } | |
b, strong { font-weight: bold; } | |
blockquote { margin: 1em 40px; } | |
dfn { font-style: italic; } | |
hr { display: block; height: 1px; border: 0; border-top: 1px solid #ccc; margin: 1em 0; padding: 0; } | |
ins { background: #ff9; color: #000; text-decoration: none; } | |
mark { background: #ff0; color: #000; font-style: italic; font-weight: bold; } | |
pre, code, kbd, samp { font-family: monospace, monospace; _font-family: 'courier new', monospace; font-size: 1em; } | |
pre { white-space: pre; white-space: pre-wrap; word-wrap: break-word; } | |
q { quotes: none; } | |
q:before, q:after { content: ""; content: none; } | |
small { font-size: 85%; } | |
sub, sup { font-size: 75%; line-height: 0; position: relative; vertical-align: baseline; } | |
sup { top: -0.5em; } | |
sub { bottom: -0.25em; } | |
ul, ol { margin: 1em 0; padding: 0 0 0 40px; } | |
dd { margin: 0 0 0 40px; } | |
nav ul, nav ol { list-style: none; list-style-image: none; margin: 0; padding: 0; } | |
img { border: 0; -ms-interpolation-mode: bicubic; vertical-align: middle; } | |
svg:not(:root) { overflow: hidden; } | |
figure { margin: 0; } | |
form { margin: 0; } | |
fieldset { border: 0; margin: 0; padding: 0; } | |
label { cursor: pointer; } | |
legend { border: 0; *margin-left: -7px; padding: 0; } | |
button, input, select, textarea { font-size: 100%; margin: 0; vertical-align: baseline; *vertical-align: middle; } | |
button, input { line-height: normal; *overflow: visible; } | |
table button, table input { *overflow: auto; } | |
button, input[type="button"], input[type="reset"], input[type="submit"] { cursor: pointer; -webkit-appearance: button; } | |
input[type="checkbox"], input[type="radio"] { box-sizing: border-box; } | |
input[type="search"] { -webkit-appearance: textfield; -moz-box-sizing: content-box; -webkit-box-sizing: content-box; box-sizing: content-box; } | |
input[type="search"]::-webkit-search-decoration { -webkit-appearance: none; } | |
button::-moz-focus-inner, input::-moz-focus-inner { border: 0; padding: 0; } | |
textarea { overflow: auto; vertical-align: top; resize: vertical; } | |
input:valid, textarea:valid { } | |
input:invalid, textarea:invalid { background-color: #f0dddd; } | |
table { border-collapse: collapse; border-spacing: 0; } | |
td { vertical-align: top; } | |
/* ==|== primary styles ===================================================== | |
Author: | |
========================================================================== */ | |
/* ==|== non-semantic helper classes ======================================== */ | |
.ir { display: block; border: 0; text-indent: -999em; overflow: hidden; background-color: transparent; background-repeat: no-repeat; text-align: left; direction: ltr; } | |
.ir br { display: none; } | |
.hidden { display: none !important; visibility: hidden; } | |
.visuallyhidden { border: 0; clip: rect(0 0 0 0); height: 1px; margin: -1px; overflow: hidden; padding: 0; position: absolute; width: 1px; } | |
.visuallyhidden.focusable:active, .visuallyhidden.focusable:focus { clip: auto; height: auto; margin: 0; overflow: visible; position: static; width: auto; } | |
.invisible { visibility: hidden; } | |
.clearfix:before, .clearfix:after { content: ""; display: table; } | |
.clearfix:after { clear: both; } | |
.clearfix { zoom: 1; } | |
/* ==|== media queries ====================================================== */ | |
@media only screen and (min-width: 480px) { | |
} | |
@media only screen and (min-width: 768px) { | |
} | |
/* ==|== print styles ======================================================= */ | |
@media print { | |
* { background: transparent !important; color: black !important; text-shadow: none !important; filter:none !important; -ms-filter: none !important; } | |
a, a:visited { text-decoration: underline; } | |
a[href]:after { content: " (" attr(href) ")"; } | |
abbr[title]:after { content: " (" attr(title) ")"; } | |
.ir a:after, a[href^="javascript:"]:after, a[href^="#"]:after { content: ""; } | |
pre, blockquote { border: 1px solid #999; page-break-inside: avoid; } | |
thead { display: table-header-group; } | |
tr, img { page-break-inside: avoid; } | |
img { max-width: 100% !important; } | |
@page { margin: 0.5cm; } | |
p, h2, h3 { orphans: 3; widows: 3; } | |
h2, h3 { page-break-after: avoid; } | |
} |
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
function floatingTooltip(tooltipID, width) { | |
/* | |
* Creates tooltip with provided id that | |
* floats on top of visualization. | |
* Most styling is expected to come from CSS | |
* so check out bubble_chart.css for more details. | |
*/ | |
// Local variable to hold tooltip div for | |
// manipulation in other functions. | |
var tt = d3.select('body') | |
.append('div') | |
.attr('class', 'tooltip') | |
.attr('id', tooltipID) | |
.style('pointer-events', 'none'); | |
// Set a width if it is provided. | |
if (width) { | |
tt.style('width', width); | |
} | |
// Initially it is hidden. | |
hideTooltip(); | |
function showTooltip(content, event) { | |
/* | |
* Display tooltip with provided content. | |
* | |
* content is expected to be HTML string. | |
* | |
* event is d3.event for positioning. | |
*/ | |
tt.style('opacity', 1.0) | |
.html(content); | |
updatePosition(event); | |
} | |
function hideTooltip() { | |
/* | |
* Hide the tooltip div. | |
*/ | |
tt.style('opacity', 0.0); | |
} | |
function updatePosition(event) { | |
/* | |
* Figure out where to place the tooltip | |
* based on d3 mouse event. | |
*/ | |
var xOffset = 20; | |
var yOffset = 10; | |
var ttw = tt.style('width'); | |
var tth = tt.style('height'); | |
var wscrY = window.scrollY; | |
var wscrX = window.scrollX; | |
var curX = (document.all) ? event.clientX + wscrX : event.pageX; | |
var curY = (document.all) ? event.clientY + wscrY : event.pageY; | |
var ttleft = ((curX - wscrX + xOffset * 2 + ttw) > window.innerWidth) ? | |
curX - ttw - xOffset * 2 : curX + xOffset; | |
if (ttleft < wscrX + xOffset) { | |
ttleft = wscrX + xOffset; | |
} | |
var tttop = ((curY - wscrY + yOffset * 2 + tth) > window.innerHeight) ? | |
curY - tth - yOffset * 2 : curY + yOffset; | |
if (tttop < wscrY + yOffset) { | |
tttop = curY + yOffset; | |
} | |
tt.style('top', tttop + 'px'); | |
tt.style('left', ttleft + 'px'); | |
} | |
return { | |
showTooltip: showTooltip, | |
hideTooltip: hideTooltip, | |
updatePosition: updatePosition | |
}; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment