Public
Edited
Apr 20, 2021
2 forks
10 stars
Insert cell
md `# D3 scatterplot, tooltip and data in table`
Insert cell
elemContainer = html`<div id="container">${elemGraph}${elemTable}</div>${style}`
Insert cell
elemGraph = html`<div id="graph"></div>`
Insert cell
elemTable = html`
<div id="table">
<table>
<thead>
<tr>
<th>&nbsp;</th>
<th>volume (ft<sup>3</sup>)</th>
<th>girth (in)</th>
<th>height (ft)</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
`
Insert cell
style = html`
<style type="text/css">
#container {
width: 900px;
height: 900px;
display: flex;
justify-content: center;
}

#graph {
width: 620px;
height: 100%;
}

#table {
width: 280px;
height: 100%;
}

.grid line {
stroke: lightgrey;
stroke-opacity: 0.7;
shape-rendering: crispEdges;
}

.grid path {
stroke-width: 0;
}

.tick text {
font-size: larger;
}

#table table {
border-collapse: collapse;
font-size: smaller;
}

#table table td, #table table th {
text-align: right;
}

#table table tr td, #table table tr th {
width: 7rem;
}

#table table tr td:first-child, #table table tr th:first-child {
width: 2rem;
}

#table table th, #table table td {
border-bottom-width: 1px;
border-bottom-color: lightgray;
border-bottom-style: solid;
padding-top: 0.2rem;
padding-bottom: 0.2rem;
}

.tippy-box table {
font-family: sans-serif;
font-weight: bold;
font-size: smaller;
padding: 0.25rem;
border-radius: 2px;
opacity: 0.9;
}

.tippy-box table td {
text-align: left;
vertical-align: bottom;
line-height: 1.25rem;
color: white;
}

.tippy-box table tr td:first-child {
padding-right: 1rem;
}
</style>
`
Insert cell
Insert cell
d3 = require("d3@6")
Insert cell
Insert cell
popperjs = require("@popperjs/core@2")
Insert cell
Insert cell
tippy = require("tippy.js@6")
Insert cell
Insert cell
function fmtNumber(n) {
const fmt = new Intl.NumberFormat('en-US', { minimumFractionDigits: 2 })
return n !== "" ? fmt.format(n) : n
}
Insert cell
Insert cell
function cleanData(e) {
const index = parseInt(e["Index"], 10)
const girth = parseFloat(e[" \"Girth (in)\""])
const height = parseFloat(e[" \"Height (ft)\""])
const volume = parseFloat(e[" \"Volume(ft^3)\""])
if (index > 0) {
return {
index: index,
girth: girth,
height: height,
volume: volume,
}
}
return null
}
Insert cell
Insert cell
async function getData() {
let data = await FileAttachment("trees.csv").csv() // await d3.csv("trees.csv")
return data.map(e => cleanData(e))
.filter(e => e !== null)
.sort((a,b) => d3.descending(a.volume, b.volume)
|| d3.descending(a.girth, b.girth)
|| d3.descending(a.height, b.height))
}
Insert cell
md`Scales`
Insert cell
function xScale(width, data) {
return d3.scaleLinear()
.domain([d3.min(data, e => e.girth), d3.max(data, e => e.girth)]).nice()
.range([0, width])
}
Insert cell
function yScale(height, data) {
return d3.scaleLinear()
.domain([d3.min(data, e => e.height), d3.max(data, e => e.height)]).nice()
.range([height, 0])
}
Insert cell
function colorScale(data) {
return d3.scaleSequential(d3.interpolateWarm)
.domain([d3.min(data, e => e.volume), d3.max(data, e => e.volume)]).nice()
}
Insert cell
md`Axis`
Insert cell
function axis(trX, trY, axis, svg) {
svg.append("g")
.attr("transform", `translate(${trX}, ${trY})`)
.call(axis)
}
Insert cell
function labelAxisX(trX, trY, label, svg) {
svg.append("text")
.attr("transform", `translate(${trX}, ${trY})`)
.style("text-anchor", "middle")
.text(label)
}
Insert cell
function labelAxisY(x, y, label, svg) {
svg.append("text")
.attr("transform", `translate(${x}, ${y}) rotate(-90)`)
.style("text-anchor", "middle")
.text(label)
}
Insert cell
md`Grid`
Insert cell
function grid(trX, trY, grid, svg) {
svg.append('g')
.attr('class', 'grid')
.attr('transform', `translate(${trX}, ${trY})`)
.call(grid)
}
Insert cell
md`Highlight row in the data table`
Insert cell
function highlightTr(index) {
const tds = d3.selectAll(`#table tr[data-id="${index}"] td.highlight`)
tds.style("background-color", "#ffff99")
}
Insert cell
function nohighlightTr(index) {
const tds = d3.selectAll(`#table tr[data-id="${index}"] td.highlight`)
tds.style("background-color", "white")
}
Insert cell
md`Dots`
Insert cell
function dots(x, y, margin, colorScale, data, svg) {
const dot = svg.append("g")
.attr("stroke-width", 1.5)
.selectAll("rect")
.data(data)
.join("rect")
.attr("fill", d => colorScale(d.volume))
.attr("stroke", d => colorScale(d.volume))
.attr("transform",
d => `translate(${x(d.girth) + margin.left - 3}, ${y(d.height) + margin.top - 3})`)
.attr("width", 6)
.attr("height", 6)
.attr('class', 'dot')
.attr('data-id', d => d.index)
.on("mouseover", function(event, d) {
highlightTr(d.index)
})
.on("mouseout", function(event, d) {
nohighlightTr(d.index)
})
}
Insert cell
md`Tooltips with tippy library`
Insert cell
function tooltips(svg) {
const dots = svg.selectAll('rect.dot')
const html = d => "<table>"
+ `<tr><td>girth (in)</td><td>${fmtNumber(d.girth)}</td></tr>`
+ `<tr><td>height (ft)</td><td>${fmtNumber(d.height)}</td></tr>`
+ `<tr><td>volume (ft<sup>3</sup>)</td><td>${fmtNumber(d.volume)}</td></tr>`
+ "</table>"
dots.attr('data-tippy-allowHTML', true)
dots.attr('data-tippy-content', d => html(d))
tippy(dots.nodes())
}
Insert cell
function getDotTooltip(id) {
return d3.select(`#graph .dot[data-id="${id}"]`)
.node()
._tippy
}
Insert cell
md`Display data in the html table`
Insert cell
function table(elem, colorScale, data) {
const tr = elem.select("tbody")
.selectAll("tr")
.data(data)
.enter()
.append("tr")
.style("background-color", d => colorScale(d.volume))
.attr('data-id', d => d.index)
.on("mouseover", function(event, d) {
highlightTr(d.index)
getDotTooltip(d.index).show()
})
.on("mouseout", function(event, d) {
nohighlightTr(d.index)
getDotTooltip(d.index).hide()
})
tr.selectAll("td")
.data(d => [ '', d.volume, d.girth, d.height ])
.enter()
.append("td")
.attr('class', (d, i) => i > 0 ? "highlight" : null)
.style("background-color", d => d === "" ? null : "white")
.text(d => fmtNumber(d))
}
Insert cell
md`Main function`
Insert cell
async function main() {
const margin = {
top: 5,
right: 10,
bottom: 50,
left: 70
}
const outerWidth = elemGraph.offsetWidth
const outerHeight = elemGraph.offsetHeight
const innerWidth = outerWidth - margin.right - (margin.left * 2)
const innerHeight = outerHeight - margin.top - (margin.bottom * 2)
const data = await getData()
const x = xScale(innerWidth, data)
const y = yScale(innerHeight, data)
const xAxis = d3.axisBottom(x)
const yAxis = d3.axisLeft(y)
const xGrid = d3.axisBottom(x)
.tickFormat('')
.tickSizeInner(- innerHeight)
const yGrid = d3.axisLeft(y)
.tickFormat('')
.tickSizeInner(- innerWidth)
const graph = d3.select(elemGraph)
const svg = graph.append("svg")
.attr("width", outerWidth)
.attr("height", outerHeight)
.attr("viewBox", [0, 0, outerWidth, outerHeight])
axis(margin.left, innerHeight + margin.top, xAxis, svg)
axis(margin.left, margin.top, yAxis, svg)
labelAxisX((innerWidth / 2) + margin.left, innerHeight + margin.bottom, "girth (in)", svg)
labelAxisY(margin.left / 2, innerHeight / 2, "height (ft)", svg)
grid(margin.left, innerHeight + margin.top, xGrid, svg)
grid(margin.left, margin.top, yGrid, svg)
const cs = colorScale(data)
dots(x, y, margin, cs, data, svg)
tooltips(svg)
table(d3.select(elemTable), cs, data)
}
Insert cell
md`Run it!`
Insert cell
main()
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more