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.
xxxxxxxxxx
<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>
https://d3js.org/d3.v4.min.js