Built with blockbuilder.org
xxxxxxxxxx
<html>
<head>
<meta charset="utf-8">
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="https://d3js.org/d3-scale-chromatic.v1.min.js"></script>
<style>
body {
background-color: whitesmoke;
margin: 0px;
text-align: center;
}
svg {
background-color: white;
}
figure {
margin: 5px 0px;
}
figcaption {
font-family: sans-serif;
font-size: 10pt;
}
text {
font-family: sans-serif;
font-size: 10pt;
fill: black;
}
.axis text {
font-size: 9pt;
}
.bubble text {
visibility: hidden;
}
.bubble circle {
stroke: white;
stroke-width: 0.5;
}
.hover text {
visibility: visible;
}
.hover circle {
stroke: red;
stroke-width: 1;
}
</style>
</head>
<body>
<figure>
<svg width="940" height="640"></svg>
<!-- <figcaption>Source: "<a href="https://www.equality-of-opportunity.org/data/">Online Data Table 1</a>" dataset from the <a href="https://www.equality-of-opportunity.org/">Equality of Opportunity</a> project.</figcaption> -->
</figure>
<script>
var svg = d3.select("svg");
var g = svg.append("g");
var margin = {
"top": 10, "bottom": 200,
"left": 35, "right": 10
};
var legend = { "width": 100, "height": 20 };
var radius = { "min": 5, "max": 15 };
var plot = {
"width": svg.attr("width") - margin.left - margin.right,
"height": svg.attr("height") - margin.top - margin.bottom
};
// console.log("plot area:", [plot.width, plot.height]);
g.attr("id", "plot");
g.attr("transform", translate(margin.left, margin.top));
d3.csv("forbubblechart.csv", convert, callback);
/* ------------------- */
/*
* called on each row from the csv file. keeps only
* relevant values from the row, renames them to
* something more reasonable, and converts to number
* if appropriate
*/
function convert(row) {
keep = {};
// keep name and id
// rename to something without spaces
// keep["name"] = row["Institution Name"];
keep["name"] = row["District"];
keep["Number_of_records"] = toNumber(row["Number_of_records"]);
keep["Num_Potential_Threats"] = toNumber(row["Num_Potential_Threats"]);
// fix for crazy characters in original file
// if (Object.hasOwnProperty("IPEDS Institution ID")) {
// keep["id"] = toNumber(row["IPEDS Institution ID"]);
// }
// keep location information
// the original data file has a carriage return in the name!
// keep["state"] = row["State"].trim().toUpperCase();
// keep["city"]= row["Metro Area\r(Commuting Zone)"];
// keep values for x, y axis
// convert to number (remove extra spaces, commas)
// keep["success"] = toNumber(row["Success Rate: % of Children in Top Quintile Among Those with Parents in Bottom Quintile"]);
// keep["number_of_records"] = toNumber(row["Call_Number"]);
// keep["access"] = toNumber(row["Low-Income Access: % of Parents in Bottom Quintile"]);
// keep values for circle area and color
// avoid names like "parent" or "child" as they are used by d3
// keep["earnings"] = toNumber(row["Median Child Indiv. Earnings Ages 32-34 ($)"]);
// keep["income"] = toNumber(row["Median Parent Hhold. Income ($)"]);
// keep["num_potential_threats"] = toNumber(row["num_potential_threats"]);
return keep;
}
/*
* converts strings (with extra spaces and comma thousands
* separators) to a number
*/
function toNumber(text) {
// make sure not given an undefined object
if (text) {
text = text.trim();
text = text.replace(/,/, "");
return +text;
}
return text;
}
/*
* draws visualization after data is loaded
*/
function callback(error, data) {
if (error) throw error;
data.sort(function(a, b) {
return a.Number_of_records - b.Number_of_records;
});
// placeholders for our scales and axes
var scales = {};
var axes = {};
var groups = {};
scales["x"] = d3.scaleBand()
.domain(data.map(function(d) {return d.name;}))
.rangeRound([0, plot.width])
.padding(0.5);
scales["y"] = d3.scaleLinear()
.range([plot.height, 0])
.domain([0, 120]);
scales["radius"] = d3.scaleSqrt()
.range([8,32])
.domain([0,68]);
// using a sequential color scale from:
// https://github.com/d3/d3-scale-chromatic
scales["color"] = d3.scaleSequential(d3.interpolateOranges)
.domain([2,119]);
// console.log("x domain:", scales.x.domain());
// console.log("y domain:", scales.y.domain());
// console.log("r domain:", scales.radius.domain());
// console.log("c domain:", scales.color.domain());
var bubbles = g.selectAll(".bubble")
.data(data)
.enter()
.append("g")
.attr("id", function(d) { return "id-" + d.name; })
.attr("class", "bubble");
bubbles.append("circle")
.attr("cx", function(d) { return scales.x(d.name) + 7; })
.attr("cy", function(d) { return scales.y(d.Number_of_records); })
.attr("r", function(d) { return scales.radius(d.Num_Potential_Threats); })
.style("fill", function(d) { return scales.color(d.Number_of_records); });
bubbles.append("text")
.attr("y", function(d) { return scales.y(d.Number_of_records);})
.text(function(d) { return "District: " + d.name;})
bubbles.append("text")
.attr("y", function(d) { return scales.y(d.Number_of_records) + 12; })
.text(function(d) { return "Incidents: " + d.Number_of_records; });
bubbles.append("text")
.attr("y", function(d) { return scales.y(d.Number_of_records) + 24; })
.text(function(d) { return "Life-threatening Incidents: " + d.Num_Potential_Threats; });
bubbles.selectAll("text")
.attr("x", function(d) { return scales.x(d.name); })
.attr("dx", function(d) {
var shift = scales.radius(d.Number_of_records) + 10;
return d.Number_of_records > 50 ? -shift : shift;
})
.attr("dy", "0.35em")
.attr("text-anchor", function(d) {
return d.Number_of_records > 50 ? "end" : "start";
})
.style("fill","red");
// too many to display at once
// only show text labels when group is hovered
bubbles.on("mouseover", function(d) {
// change the class
d3.select(this).classed("hover", true);
// bring the bubble to the front
// breaks sorting, but helps interactivity
d3.select(this).raise();
})
.on("mouseout", function(d, i) {
// remove hover class
d3.select(this).classed("hover", false);
});
// start with Richmond visible
g.select("#id-Richmond").classed("hover", true).raise();
// create x-axis generator and svg group
axes["x"] = d3.axisBottom(scales.x);
groups["x"] = svg.append("g")
.attr("id", "x")
.attr("class", "axis");
// p.s. groups["x"] and groups.x are basically the same
// just different ways of accessing object properties
// the [ ] approach works for property names with spaces
// the dot . approach does not work for names with spaces
// i tend to use [ ] for assignment and . for access
// draw axis
groups.x.call(axes.x)
.selectAll("text")
.attr("y", 0)
.attr("x", 9)
.attr("dy", ".35em")
.attr("transform", "rotate(90)")
.style("text-anchor", "start");
// shift axis to appropriate location
groups.x.attr("transform", translate(margin.left, margin.top + plot.height));
// append axis text
groups.x.append("text")
.attr("x", plot.width)
.attr("y", 0)
.attr("dy", -2)
.attr("text-anchor", "end");
// .text("District name");
// do the same for the y-axis
axes["y"] = d3.axisLeft(scales.y);
groups["y"] = svg.append("g")
.attr("id", "y")
.attr("class", "axis");
groups.y.call(axes.y);
groups.y.attr("transform", translate(margin.left, margin.top));
groups.y.append("text")
.attr("x", 0)
.attr("y", 0)
.attr("dy", -4)
.attr("text-anchor", "start")
.attr("transform", "rotate(90)")
.text("Number of incidents recorded");
// lets try to make a circle legend
groups["radius"] = svg.append("g")
.attr("id", "radius")
.attr("class", "axis");
// use the circle radius range as our "data"
var selection = groups.radius.selectAll("circle")
.data(scales.radius.range())
.enter();
// add our circles
selection.append("circle")
.attr("cx", 0)
.attr("cy", function(d) { return -d; })
.attr("r", function(d) { return d; })
.style("fill", "none")
.style("stroke", "black")
.style("stroke-width", "1px");
// add our circle labels
selection.append("text")
.attr("x", 0)
.attr("y", function(d) { return -2 * d; })
.attr("dx", 0)
.attr("dy", "-2px")
.attr("text-anchor", "middle")
.text(function(d) {
var value = scales.radius.invert(d);
var label = value;
return label;
});
// add our legend label
groups.radius.append("text")
.attr("x", 0)
.attr("y", 0)
.attr("dx", 0)
.attr("dy", "1em")
.attr("text-anchor", "middle")
.text("Life-threatening incidents");
// figure out the size of our legend
var radiusBbox = groups.radius.node().getBBox();
// shift our legend to the upper-right corner
groups.radius.attr("transform", translate(margin.left + plot.width - radiusBbox.width / 2, radiusBbox.height + 320));
// hmmm lets make our color legend the same width?
legend.width = radiusBbox.width;
// lets try to make a color legend
groups["legend"] = svg.append("g")
.attr("id", "color")
.attr("class", "axis");
// our color scale doesn't have an invert() function
// and we need some way of mapping 0% and 100% to our domain
// so we'll create a scale to reverse that mapping
scales["percent"] = d3.scaleLinear()
.domain([2, 119]) // since we reversed the color
.range(scales.color.domain());
// setup gradient for legend
// https://bl.ocks.org/mbostock/1086421
svg.append("defs")
.append("linearGradient")
.attr("id", "gradient")
.selectAll("stop")
.data(d3.ticks(0, 100, 10))
.enter()
.append("stop")
.attr("offset", function(d) {
return d + "%";
})
.attr("stop-color", function(d) {
return scales.color(scales.percent(d));
});
// draw the color rectangle with gradient
groups.legend.append("rect")
.attr("x", 0)
.attr("y", 0)
.attr("width", legend.width)
.attr("height", legend.height)
.attr("fill", "url(#gradient)");
// lets also draw an axis with tick marks at the top
scales["legend"] = d3.scaleLinear()
.domain(scales.color.domain())
.range([0, legend.width]);
axes["legend"] = d3.axisTop(scales.legend)
.tickValues(scales.color.domain())
// .tickFormat(d3.format(".0s"));
groups.legend.call(axes.legend);
// tweak the tick marks
groups.legend.selectAll("text").each(function(d, i) {
if (d == scales.legend.domain()[0]) {
d3.select(this).attr("text-anchor", "start");
}
else if (d == scales.legend.domain()[1]) {
d3.select(this).attr("text-anchor", "end");
}
});
// add text label at the bottom
groups.legend.append("text")
.attr("x", legend.width / 2)
.attr("y", legend.height)
.attr("dx", 0)
.attr("dy", "1em")
.attr("text-anchor", "middle")
.text("Number of Incidents");
// shift to a nice location
groups.legend.attr("transform", translate(margin.left + plot.width - legend.width, margin.top + legend.height + radiusBbox.height + 180));
}
/*
* returns a translate string for the transform attribute
*/
function translate(x, y) {
return "translate(" + String(x) + "," + String(y) + ")";
}
</script>
</body>
</html>
https://d3js.org/d3.v4.min.js
https://d3js.org/d3-scale-chromatic.v1.min.js