Published
Edited
May 24, 2020
Importers
5 stars
Insert cell
Insert cell
{
const svg = makeSvg()
const basePane = svg.append('g')
const densityPane = svg.append('g')
const paperPane = svg.append('g')
const projection = initializeProjection(data, width, height)
// renders clusters of the first layer
basePane.selectAll('ellipse.cluster')
.data(data)
.join('ellipse')
.attr('class', 'cluster')
.attr('cx', d => projection.projectX(d.x))
.attr('cy', d => projection.projectY(d.y))
.attr('rx', d => projection.scaleX(d.r))
.attr('ry', d => projection.scaleY(d.r))
// renders subclusters
data.forEach(cluster => {
const clusterId = `cluster-${cluster.topicId}`
const clusterProjection = projection.translate(cluster.x, cluster.y)
basePane.selectAll(`ellipse.subcluster.${clusterId}`)
.data(cluster.subclusters)
.join('ellipse')
.attr('class', `cluster subcluster ${clusterId}`)
.attr('cx', d => clusterProjection.projectX(d.x))
.attr('cy', d => clusterProjection.projectY(d.y))
.attr('rx', d => clusterProjection.scaleX(d.r))
.attr('ry', d => clusterProjection.scaleY(d.r))
// renders papers
cluster.subclusters.forEach(subcluster => {
const subclusterId = `subcluster-${cluster.topicId}-${subcluster.topicId}`
const subclusterProjection = clusterProjection.translate(subcluster.x, subcluster.y)
const papers = subcluster.papers
paperPane.selectAll(`ellipse.paper.${subclusterId}`)
.data(papers)
.join('ellipse')
.attr('class', `paper ${subclusterId}`)
.attr('cx', d => subclusterProjection.projectX(d.x))
.attr('cy', d => subclusterProjection.projectY(d.y))
.attr('rx', 0.5)
.attr('ry', 0.5)
// renders density contours
const {
domain,
estimatorSize,
contours
} = subcluster.densityContours
const minX = subclusterProjection.projectX(domain[0])
const maxX = subclusterProjection.projectX(domain[1])
const minY = subclusterProjection.projectY(domain[0])
const maxY = subclusterProjection.projectY(domain[1])
const densityProjectX = d3.scaleLinear()
.domain([0, estimatorSize])
.range([minX, maxX])
const densityProjectY = d3.scaleLinear()
.domain([0, estimatorSize])
.range([minY, maxY]) // minY and maxY are flipped in the screen coordinates
const geoPath = d3.geoPath()
.projection(d3.geoTransform({
point (x, y) {
this.stream.point(densityProjectX(x), densityProjectY(y))
}
}))
densityPane.selectAll(`path.density-contour.${subclusterId}`)
.data(contours)
.join('path')
.attr('class', `density-contour ${subclusterId}`)
.attr('d', geoPath)
.attr('fill', d => d3.interpolateGreys(d.meanProb))
})
// renders island contours
{
const {
domain,
estimatorSize,
contours
} = cluster.islandContours
const minX = clusterProjection.projectX(domain[0])
const maxX = clusterProjection.projectX(domain[1])
const minY = clusterProjection.projectY(domain[0])
const maxY = clusterProjection.projectY(domain[1])
const contourProjectX = d3.scaleLinear()
.domain([0, estimatorSize])
.range([minX, maxX])
const contourProjectY = d3.scaleLinear()
.domain([0, estimatorSize])
.range([minY, maxY]) // minX and maxY are flipped in the screen coordinates
const geoPath = d3.geoPath()
.projection(d3.geoTransform({
point (x, y) {
this.stream.point(contourProjectX(x), contourProjectY(y))
}
}))
basePane.selectAll(`path.island-contour.${clusterId}`)
.data(contours.slice(0, 1)) // only outmost contour
.join('path')
.attr('class', `island-contour ${clusterId}`)
.attr('d', geoPath)
}
})
return svg.node()
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
function makeSvg () {
return d3.select(DOM.svg())
.attr('width', width)
.attr('height', height)
}
Insert cell
function initializeProjection (clusters, screenWidth, screenHeight) {
const padding = 0.5
const clusterR = (0.5 * getBoundingSquareSize(clusters)) + padding
const _projectX = d3.scaleLinear()
.domain([-clusterR, clusterR])
.range([0, screenWidth])
const _projectY = d3.scaleLinear()
.domain([-clusterR, clusterR])
.range([screenHeight, 0]) // upside down
const _scaleX = d3.scaleLinear()
.domain([0, clusterR])
.range([0, 0.5 * screenWidth])
const _scaleY = d3.scaleLinear()
.domain([0, clusterR])
.range([0, 0.5 * screenHeight])
class Projection {
constructor (offsetX, offsetY) {
this.offsetX = offsetX
this.offsetY = offsetY
}

projectX (x) {
return _projectX(x + this.offsetX)
}

projectY (y) {
return _projectY(y + this.offsetY)
}

scaleX (x) {
return _scaleX(x)
}

scaleY (y) {
return _scaleY(y)
}

translate (dX, dY) {
return new Projection(this.offsetX + dX, this.offsetY + dY)
}
}
return new Projection(0, 0)
}
Insert cell
function getBoundingBox (nodes) {
return {
minX: d3.min(nodes.map(n => n.x - n.r)),
maxX: d3.max(nodes.map(n => n.x + n.r)),
minY: d3.min(nodes.map(n => n.y - n.r)),
maxY: d3.max(nodes.map(n => n.y + n.r))
}
}
Insert cell
function getBoundingSquareSize (nodes) {
const {
minX,
maxX,
minY,
maxY
} = getBoundingBox(nodes)
return Math.max(maxX - minX, maxY - minY)
}
Insert cell
html`
<style>
ellipse.cluster {
stroke: none;
stroke-width: 1.0;
stroke-dasharray: 1 1;
fill: white;
fill-opacity: 0.0;
}
ellipse.cluster.subcluster {
stroke: none;
}

ellipse.paper {
stroke: none;
fill: none;
fill-opacity: 0.5;
}

path.island-contour {
stroke: black;
stroke-width: 0.5;
fill: none;
}

path.density-contour {
stroke: none;
stroke-width: 0.5;
}
</style>
`
Insert cell
Insert cell
data = FileAttachment('clusters@8.json').json()
Insert cell
data.map(cluster => {
const flatPapers = Array.prototype.concat.apply(
[],
cluster.subclusters.map(subcluster => subcluster.papers)
)
cluster.meanProb = d3.mean(flatPapers, p => p.prob)
return cluster
})
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