|
<!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> |