Skip to content

Instantly share code, notes, and snippets.

@gcalmettes
Last active October 3, 2017 22:20
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save gcalmettes/95e3553da26ec90fd0a2890a678f3f69 to your computer and use it in GitHub Desktop.
Save gcalmettes/95e3553da26ec90fd0a2890a678f3f69 to your computer and use it in GitHub Desktop.
Dot plot histogram with nested Enter/Update/Exit patterns
license: gpl-3.0

Dotplot histogram: a series of individual circles is appended in place of the "bar" to directly show the number of elements in the bin (avoiding the need for a y scale).

There are two "layers" of enter/update/exit, one for the g elements representing each bin, and one for the circles attached to each bin g element.

This plot was developped for an in-class activity with the UCLA students taking the STATS13 resampling statistics class at UCLA. Each student was entering their value in an associated Google spreadsheet (the Tabletop.js syntax to fetch the data from the Google spreadsheet has been replaced by d3.csv/d3.shuffle(data).slice combo in this example, to simulate the data being changed as the students enter new values).

A tooltip with information about the selected dot appear when hovering a circle.

<!DOCTYPE html>
<meta charset="utf-8">
<style>
.enter {
fill: #EDCA3A;
}
.update {
fill: #1FBAD6;
}
.exit {
fill: #F25754;
}
.selected {
fill: #E6B0F1;
}
div.tooltip {
color: black;
position: absolute;
text-align: left;
width: auto;
height: auto;
padding: 5px;
font-family: Futura;
font: 12px sans-serif ;
background: #FCB8C3FF;
border: 0px;
border-radius: 8px;
pointer-events: none;
}
</style>
<body>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script>
//SVG setup
const margin = {top: 10, right: 30, bottom: 30, left: 30},
width = 550 - margin.left - margin.right,
height = 480 - margin.top - margin.bottom;
//x scales
const x = d3.scaleLinear()
.rangeRound([0, width])
.domain([2, 11]);
//set up svg
const svg = d3.select("body")
.append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform",
`translate(${margin.left}, ${margin.top})`);
//tooltip
const tooltip = d3.select("body")
.append("div")
.attr("class", "tooltip")
.style("opacity", 0);
const t = d3.transition()
.duration(1000);
const dataFile = "roster.csv"
//number of bins for histogram
const nbins = 20;
//Note: data fetching is done each time the function is ran
//as d3.csv is replaced by tabletop.js request to get data each time
//from google spreadsheet
function update(){
// Get the data
d3.csv(dataFile, function(error, allData) {
allData.forEach(function(d) {
d.Name = d.Name
d.Value = +d.Value;
});
//simulate new data by randomizing/slicing
let data = d3.shuffle(allData)
.slice(0, 35)
//histogram binning
const histogram = d3.histogram()
.domain(x.domain())
.thresholds(x.ticks(nbins))
.value(function(d) { return d.Value;} )
//binning data and filtering out empty bins
const bins = histogram(data).filter(d => d.length>0)
//g container for each bin
let binContainer = svg.selectAll(".gBin")
.data(bins);
binContainer.exit().remove()
let binContainerEnter = binContainer.enter()
.append("g")
.attr("class", "gBin")
.attr("transform", d => `translate(${x(d.x0)}, ${height})`)
//need to populate the bin containers with data the first time
binContainerEnter.selectAll("circle")
.data(d => d.map((p, i) => {
return {idx: i,
name: p.Name,
value: p.Value,
radius: (x(d.x1)-x(d.x0))/2
}
}))
.enter()
.append("circle")
.attr("class", "enter")
.attr("cx", 0) //g element already at correct x pos
.attr("cy", function(d) {
return - d.idx * 2 * d.radius - d.radius; })
.attr("r", 0)
.on("mouseover", tooltipOn)
.on("mouseout", tooltipOff)
.transition()
.duration(500)
.attr("r", function(d) {
return (d.length==0) ? 0 : d.radius; })
binContainerEnter.merge(binContainer)
.attr("transform", d => `translate(${x(d.x0)}, ${height})`)
//enter/update/exit for circles, inside each container
let dots = binContainer.selectAll("circle")
.data(d => d.map((p, i) => {
return {idx: i,
name: p.Name,
value: p.Value,
radius: (x(d.x1)-x(d.x0))/2
}
}))
//EXIT old elements not present in data
dots.exit()
.attr("class", "exit")
.transition(t)
.attr("r", 0)
.remove();
//UPDATE old elements present in new data.
dots.attr("class", "update");
//ENTER new elements present in new data.
dots.enter()
.append("circle")
.attr("class", "enter")
.attr("cx", 0) //g element already at correct x pos
.attr("cy", function(d) {
return - d.idx * 2 * d.radius - d.radius; })
.attr("r", 0)
.merge(dots)
.on("mouseover", tooltipOn)
.on("mouseout", tooltipOff)
.transition()
.duration(500)
.attr("r", function(d) {
return (d.length==0) ? 0 : d.radius; })
});//d3.csv
};//update
function tooltipOn(d) {
//x position of parent g element
let gParent = d3.select(this.parentElement)
let translateValue = gParent.attr("transform")
let gX = translateValue.split(",")[0].split("(")[1]
let gY = height + (+d3.select(this).attr("cy")-50)
d3.select(this)
.classed("selected", true)
tooltip.transition()
.duration(200)
.style("opacity", .9);
tooltip.html(d.name + "<br/> (" + d.value + ")")
.style("left", gX + "px")
.style("top", gY + "px");
}//tooltipOn
function tooltipOff(d) {
d3.select(this)
.classed("selected", false);
tooltip.transition()
.duration(500)
.style("opacity", 0);
}//tooltipOff
// add x axis
svg.append("g")
.attr("class", "axis axis--x")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(x));
//draw everything
update();
//update with new data every 3sec
d3.interval(function() {
update();
}, 3000);
</script>
</body>
</html>
Name Value
name 1 6.629529053
name 2 5.277847907
name 3 5.024497803
name 4 7.948939186
name 5 9.237038796
name 6 5.478688225
name 7 6.137851942
name 8 6.38233063
name 9 7.778363964
name 10 5.719140007
name 11 5.217120487
name 12 5.761643043
name 13 6.972890389
name 14 6.146719444
name 15 9.462969845
name 16 6.575663831
name 17 7.238442978
name 18 7.738132645
name 19 7.668405262
name 20 7.405561765
name 21 9.53265424
name 22 7.586782853
name 23 6.176760737
name 24 5.345532652
name 25 3.542570095
name 26 3.277555954
name 27 5.240213162
name 28 5.712919637
name 29 6.78585084
name 30 8.798133007
name 31 7.788243314
name 32 7.51199945
name 33 6.71894919
name 34 6.133102377
name 35 4.849458538
name 36 5.979778286
name 37 7.808959795
name 38 8.249656203
name 39 6.670594553
name 40 7.309446651
name 41 4.897447661
name 42 5.544802704
name 43 4.748300456
name 44 4.677454649
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment