Built with blockbuilder.org
xxxxxxxxxx
<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 {
fill-opacity: 0.75;
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="580"></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": 25,
"left": 55, "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("mrc_table1.csv", convert, callback);
d3.csv("data.csv", 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["k_median"]=row["Students' Median Income"]
// keep values for x, y axis
// convert to number (remove extra spaces, commas)
keep["par_median"] = toNumber(row["Parent's Median Income"]);
keep["count1"] = toNumber(row["The Number of Students in a Single Cohort"]);
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;
// make sure to sort the data by the value used for circle area
/*data.sort(function(a, b) {
return a.k_median - b.k_median;
});
*/
// placeholders for our scales and axes
var scales = {};
var axes = {};
var groups = {};
// you can decide on the scales using d3.min, max, and extent
// we already know good values from our tableau prototype
scales["x"] = d3.scaleLinear()
.range([0, plot.width-15])
.domain([0, 9000]);
scales["y"] = d3.scaleLinear()
.range([plot.height, 0])
.domain([-5, 200000]);
// lets programatically set the domains for our other scales
// always use sqrt() to scale circle area
scales["radius"] = d3.scaleSqrt()
.range([5, 15])
.domain(d3.extent(data, function(d) { return d.k_median; }))
.nice();
// using a sequential color scale from:
// https://github.com/d3/d3-scale-chromatic
scales["color"] = d3.scaleSequential(d3.interpolatePRGn)
.domain(d3.extent(data, function(d) {
return d.k_median;
}).reverse())
.nice();
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());
// lets draw our bubbles!
var bubbles = g.selectAll(".bubble")
.data(data)
.enter()
.append("g")
.attr("id", function(d) { return "id-" + d.id; })
.attr("class", "bubble");
bubbles.append("circle")
.attr("cx", function(d) { return scales.x(d.count1); })
.attr("cy", function(d) { return scales.y(d.p_median); })
.attr("r", function(d) { return scales.radius(d.k_median); })
.style("fill", function(d) { return scales.color(d.k_median); });
// how about some text labels?
/*
bubbles.append("text")
.attr("x", function(d) { return scales.x(d.count1); })
.attr("y", function(d) { return scales.y(d.p_median); })
// adjust position based on location in chart
.attr("dx", function(d) {
var shift = scales.radius(d.count1) + 1;
return d.access > 40 ? -shift : shift;
})
.attr("dy", "0.35em")
.attr("text-anchor", function(d) {
return d.access > 40 ? "end" : "start";
})
.text(function(d) { return d.name; });
*/
// 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);
});
// 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);
// 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("Number of Students in a Cohort");
// 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("Parents' Median Income");
// 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 = d3.format(".0s")(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("Child Earnings");
// 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));
// 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([0, 100].reverse()) // 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().reverse())
.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("Parent Income");
// shift to a nice location
groups.legend.attr("transform", translate(margin.left + plot.width - legend.width, margin.top + legend.height + radiusBbox.height + 20));
}
/*
* 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