Published
Edited
Jun 23, 2020
Importers
4 stars
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
function makeSubclusterNodes (cluster, prerenderedCluster, { getPadding, coordinateScale }) {
return cluster.x.map((x, i) => {
const { papers } = prerenderedCluster[i]
const halfClusterSize = 0.5 * getBoundingSquareSize(papers)
const padding = getPadding(papers)
const x0 = coordinateScale * x
const y0 = coordinateScale * cluster.y[i]
return {
topicId: cluster.topic[i],
x0,
y0,
x: x0,
y: y0,
r: halfClusterSize + padding,
papers
}
})
}
Insert cell
function getFixedPadding (padding, papers) {
return padding
}
Insert cell
function getConfidencePadding (scale, papers) {
/*
const meanProb = d3.mean(papers, p => p.prob)
return scale * (1.0 - meanProb) */
const medianProb = d3.median(papers, p => p.prob)
return scale * (1.0 - medianProb)
}
Insert cell
function resolvePaddingFunction (name, { fixedPadding, confidencePaddingScale }) {
switch (name) {
case 'fixed padding':
return getFixedPadding.bind(null, fixedPadding)
case 'confidence padding':
return getConfidencePadding.bind(null, confidencePaddingScale)
default:
throw new RangeError(`no such padding function: ${name}`)
}
}
Insert cell
Insert cell
function initializeSubclusterArrangingForce (nodes, options) {
const force = d3.forceSimulation(nodes)
// collide
const collide = d3.forceCollide()
.radius(n => n.r)
force.force('collide', collide)
// gravity
if (options.gravity) {
const gravityX = d3.forceX(0)
const gravityY = d3.forceY(0)
force
.force('gravityX', gravityX)
.force('gravityY', gravityY)
}
// anchor
if (options.anchorPosition) {
const anchorX = d3.forceX()
.x(n => n.x0)
const anchorY = d3.forceY()
.y(n => n.y0)
force
.force('anchorX', anchorX)
.force('anchorY', anchorY)
}
// distance
if (options.keepDistance) {
const link = d3.forceLink(makeNodeLinks(nodes, options.distanceScale))
.distance(l => l.distance)
force.force('link', link)
}
// centers the bounding box
const center = forceBoundingBoxCenter()
force.force('center', center)
return force
}
Insert cell
Insert cell
function forceBoundingBoxCenter () {
let _nodes

function force () {
const minX = d3.min(_nodes.map(node => node.x - node.r))
const maxX = d3.max(_nodes.map(node => node.x + node.r))
const minY = d3.min(_nodes.map(node => node.y - node.r))
const maxY = d3.max(_nodes.map(node => node.y + node.r))
const cX = minX + (0.5 * (maxX - minX))
const cY = minY + (0.5 * (maxY - minY))
_nodes.forEach(node => {
node.x0 -= cX
node.y0 -= cY
node.x -= cX
node.y -= cY
})
}

force.initialize = function (nodes) {
_nodes = nodes
}

return force
}
Insert cell
function estimateIslandContours (projection, subclusters, options) {
const {
padding,
estimatorBandwidth,
estimatorThresholds,
numContours
} = options
const clusterR = (0.5 * getBoundingSquareSize(subclusters)) + padding
const estimatorSize = Math.round((clusterR / 3.0) * 600) // 600 for clusterR ≈ 3.0 empirically works well
const estimatorProject = d3.scaleLinear()
.domain([-clusterR, clusterR])
.range([0, estimatorSize])
// projects all of the papers in all subclusters
const projectedPapers = Array.prototype.concat.apply(
[],
subclusters.map(subcluster => {
const { papers } = subcluster
return papers.map(paper => {
return {
x: estimatorProject(paper.x + subcluster.x),
y: estimatorProject(paper.y + subcluster.y)
}
})
})
)
// estimates density
const densityEstimator = d3.contourDensity()
.size([estimatorSize, estimatorSize])
.x(d => d.x)
.y(d => d.y)
.bandwidth(estimatorBandwidth) // empirical value
.thresholds(estimatorThresholds) // empirical value
const contours = densityEstimator(projectedPapers)
return {
contours: contours.slice(0, numContours),
domain: [-clusterR, clusterR],
estimatorSize: estimatorSize
}
}
Insert cell
Insert cell
function initializeProjection (scale) {
const screenCenterX = 0.5 * screenWidth
const screenCenterY = 0.5 * screenHeight
const _projectX = d3.scaleLinear()
.domain([-1.0, 1.0])
.range([screenCenterX - scale, screenCenterX + scale])
const _projectY = d3.scaleLinear()
.domain([-1.0, 1.0])
.range([screenCenterY + scale, screenCenterY - scale]) // upside down
const _scale = a => a * scale
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)
}
scale (a) {
return _scale(a)
}
translate (dX, dY) {
return new Projection(this.offsetX + dX, this.offsetY + dY)
}
}
return new Projection(0, 0)
}
Insert cell
function renderSubclusters (pane, projection, subclusters) {
const clusterPane = pane.select('g.cluster-pane')
clusterPane.selectAll('circle.subcluster')
.data(subclusters)
.join('circle')
.attr('class', 'subcluster')
.attr('cx', d => projection.projectX(d.x))
.attr('cy', d => projection.projectY(d.y))
.attr('r', d => projection.scale(d.r))
.on('click', function (d) {
pane.selectAll('text.message')
.text(`cluster ${d.topicId}`)
})
clusterPane.selectAll('text.subcluster')
.data(subclusters)
.join('text')
.attr('class', 'subcluster')
.attr('x', d => projection.projectX(d.x))
.attr('y', d => projection.projectY(d.y) + 6)
.text(d => `${d.topicId}`)
}
Insert cell
function renderPapers (pane, projection, subclusters) {
const clusterPane = pane.select('g.cluster-pane')
subclusters.forEach(subcluster => {
const subclusterId = `subcluster-${subcluster.topicId}`
const subclusterProjection = projection.translate(subcluster.x, subcluster.y)
clusterPane.selectAll(`circle.paper-dot.${subclusterId}`)
.data(subcluster.papers)
.join('circle')
.attr('class', `paper-dot ${subclusterId}`)
.attr('cx', d => subclusterProjection.projectX(d.x))
.attr('cy', d => subclusterProjection.projectY(d.y))
.attr('r', paperRadius)
})
}
Insert cell
function renderIslandContours (pane, projection, contours) {
const {
domain,
estimatorSize
} = contours
const landPane = pane.select('g.land-pane')
const minX = projection.projectX(domain[0])
const maxX = projection.projectX(domain[1])
const minY = projection.projectY(domain[0])
const maxY = projection.projectY(domain[1])
const islandProjectX = d3.scaleLinear()
.domain([0, estimatorSize])
.range([minX, maxX])
const islandProjectY = d3.scaleLinear()
.domain([0, estimatorSize])
.range([minY, maxY])
const geoPath = d3.geoPath()
.projection(d3.geoTransform({
point (x, y) {
this.stream.point(islandProjectX(x), islandProjectY(y))
}
}))
landPane.selectAll('path.island-contour')
.data(contours.contours)
.join('path')
.attr('class', (_, i) => `island-contour island-${i}`)
.attr('d', geoPath)
}
Insert cell
Insert cell
html`<style>
rect.ocean {
stroke: none;
fill: lightgray; /* you may change the background color. */
}

circle.subcluster {
stroke: black; /* you may hide subcluster circles by setting this 'none'. */
stroke-width: 1.0;
stroke-dasharray: 1 1;
fill: white;
fill-opacity: 0.0;
}
/* you may change the style of settled subclusters. */
circle.subcluster.settled {
stroke: none;
}

text.subcluster {
text-anchor: middle;
}

circle.paper-dot {
stroke: none;
fill: black;
fill-opacity: 0.5;
}

path.island-contour {
stroke: black;
stroke-width: 0.5;
fill: white;
}
path.island-contour.island-0 {
stroke: black; /* you may hide the outer contour of an island by setting this 'none'. */
}
path.island-contour.island-1 {
stroke: black; /* you may hide the inner contour of an island by setting this 'none'. */
}
/* you may reference the ith contour by '.island-i' */
</style>`
Insert cell
Insert cell
d3 = require('d3@5')
Insert cell
screenWidth = 450
Insert cell
screenHeight = 450
Insert cell
width = screenWidth * 2
Insert cell
height = screenHeight
Insert cell
function makeSvg () {
return d3.select(DOM.svg())
.attr('width', width)
.attr('height', height)
}
Insert cell
function getBoundingSquareSize (nodes) {
const {
minX,
maxX,
minY,
maxY
} = getBoundingBox(nodes)
return Math.max(maxX - minX, maxY - minY)
}
Insert cell
function getBoundingBox (nodes) {
return {
minX: d3.min(nodes, n => n.x - n.r),
maxX: d3.max(nodes, n => n.x + n.r),
minY: d3.min(nodes, n => n.y - n.r),
maxY: d3.max(nodes, n => n.y + n.r)
}
}
Insert cell
Insert cell
Insert cell
function makeCoordinateScaleSlider (defaultValue) {
return slider({
min: 1,
max: 30,
step: 0.1,
value: defaultValue,
title: 'Coordinate scale',
description: 'Factor to be mutiplied to LDA coordinates to determine initial subcluster positions'
})
}
Insert cell
function makeGetPaddingFunctionRadio (defaultValue) {
return radio({
options: [
'fixed padding',
'confidence padding'
],
value: defaultValue,
title: 'Padding type',
description: 'Function that chooses a padding for given papers in a subcluster'
})
}
Insert cell
function makeFixedPaddingSizeSlider (defaultValue) {
return slider({
min: 0,
max: 10,
step: 0.1,
value: defaultValue,
title: 'Fixed padding size',
description: 'Size of a fixed padding. Ignored unless "Padding type" is "fixed padding".'
})
}
Insert cell
function makeConfidencePaddingScaleSlider (defaultValue) {
return slider({
min: 0.1,
max: 10.0,
step: 0.1,
value: defaultValue,
title: 'Confidence padding scale',
description: 'Factor to be multiplied to a confidence (`1.0 - medianProb`) to calculate a padding. Ignored unless "Padding type" is "confidence padding".'
})
}
Insert cell
function makeAnchorPositionCheckbox (defaultValue) {
return checkbox({
description: 'Whether subclusters try to stick to the initial positions derived from the LDA coordinates',
options: [
{
value: 'true', // an error occurs if this is a boolean value
label: 'Anchor to the initial position'
}
],
value: defaultValue
})
}
Insert cell
function makeGravityCheckbox (defaultValue) {
return checkbox({
description: 'Whether subclusters are attracted toward the center of the cluster',
options: [
{
value: 'true', // an error occurs if this is a boolean value
label: 'Gravity'
}
],
value: defaultValue
})
}
Insert cell
function makeKeepDistanceCheckbox (defaultValue) {
return checkbox({
description: 'Whether distance between subclusters is kept during arrangement',
options: [
{
value: 'true', // an error occurs if this is a boolean value
label: 'Keep distance'
}
],
value: defaultValue
})
}
Insert cell
function makeDistanceScaleSlider (defaultValue) {
return slider({
min: 1,
max: 30,
step: 1,
value: defaultValue,
title: 'Distance scale',
description: 'Factor to be multiplied to the distance between subclusters'
})
}
Insert cell
function makeIslandEstimatorBandwidthSlider (defaultValue) {
return slider({
min: 1,
max: 150,
step: 1,
value: defaultValue,
title: 'Island estimator bandwidth',
description: 'Controls how large the slope of an island estimator filter is. See `d3.contourDensity`.'
})
}
Insert cell
function makeIslandEstimatorThresholdsSlider (defaultValue) {
return slider({
min: 1,
max: 150,
step: 1,
value: defaultValue,
title: 'Island estimator thresholds',
description: 'Controls how many contours an island estimator makes. See `d3.contourDensity`.'
})
}
Insert cell
function makeIslandContourCountSlider (defaultValue) {
return slider({
min: 1,
max: 20,
step: 1,
value: defaultValue,
title: '# of island contours',
description: 'Controls how many contours on an island are rendered.'
})
}
Insert cell
function makeAddNoiseCheckbox (defaultValue) {
return checkbox({
description: 'Whether Perlin Noise is added to island contours.',
options: [
{
value: 'true', // an error occurs if value is boolean
label: 'Add noise'
}
],
value: defaultValue
})
}
Insert cell
md`## Noisy Contour`
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
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