Public
Edited
Apr 29
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
plot = {
// our SVG drawing canvas
let svg = d3.create('svg').attr('width', width).attr('height', total_height)
// central plot: provide for margins for drawing axes
let central_plot = svg.append('g').attr('transform', `translate(${margin},${margin})`)
// loading view for x axis
let x_loading_g = central_plot.append('g')
.attr('transform', `translate(0,${plot_size})`)
// loading view for y axis
let y_loading_g = central_plot.append('g')
.attr('transform', `translate(${plot_size},0)`)
// scatterplot view
let scatterplot_g = central_plot.append('g')
// cluster view
let cluster_g = central_plot.append('g')
.attr('transform', `translate(0,${plot_size+2*attribute_pad+loading_size+2*margin})`)
// frame our views, so we can see what we are working with
x_loading_g.append('rect')
.attr('width', plot_size).attr('height', loading_size)
.attr('fill', 'none').attr('stroke', d3.hcl(0,0,30)).attr('stroke-width', .8)
y_loading_g.append('rect')
.attr('width', loading_size).attr('height', plot_size)
.attr('fill', 'none').attr('stroke', d3.hcl(0,0,30)).attr('stroke-width', .8)
scatterplot_g.append('rect')
.attr('width', plot_size).attr('height', plot_size)
.attr('fill', 'none').attr('stroke', d3.hcl(0,0,30)).attr('stroke-width', .8)
cluster_g.append('rect')
.attr('width', plot_size).attr('height', clustering_size)
.attr('fill', 'none').attr('stroke', d3.hcl(0,0,30)).attr('stroke-width', .8)
// scatterplot
create_scatterplot(scatterplot_g)
// x loading
create_x_loading(x_loading_g)
// y loading
create_y_loading(y_loading_g)
// clustering
create_clustering(cluster_g)
return svg.node()
}
Insert cell
viewof plot_view = drawdom(plot, 10)
Insert cell
plot_view
Insert cell
create_scatterplot = (g) => {

// perform data join on `projection` to create circles, using scales we created to position circles
g.selectAll("circle").data(projection).enter().append("circle")
.attr("cx", d => scatter_xScale(d[0]))
.attr("cy", d => scatter_yScale(d[1]))
.attr("r", 5)
.attr('fill', 'rosybrown').attr('stroke', d3.hcl(0,0,30))
.attr('opacity', 0.4)

// create axes from our scales
const xAxis = d3.axisTop(scatter_xScale).ticks(5)
g.append("g").call(xAxis).call(axis_tweak)

const yAxis = d3.axisLeft(scatter_yScale)
g.append("g").call(yAxis).call(axis_tweak)

}
Insert cell
create_x_loading = (g) => {
// create a single line that will span zero for our loading value, positioned at the center of the view
g.append('line')
.attr('x1', 0).attr('x2', plot_size)
.attr('y1', loading_size / 2).attr('y2', loading_size / 2)
.attr('stroke', d3.hcl(0,0,30))

// perform data join on `x_loading` to create bars, using our band scale to position bar horizontally, and the linear scale to determine the bar's y position and height.
g.append('g').selectAll('rect').data(x_loading).enter().append("rect")
.attr("x", d => x_loading_xScale(d.attribute))
.attr("y", d => {if (d.loading > 0) return x_loading_yScale(d.loading) + loading_size / 2; return loading_size / 2})
.attr("width", x_loading_xScale.bandwidth() - 2)
.attr("height", d => Math.abs(x_loading_yScale(d.loading)))
.attr('stroke', d3.hcl(0,0,30))
.attr('fill', d => {if (d.loading > 0) return 'steelblue'; return 'tomato'})
// create a text label for each attribute name, can be accomplished via data join on `attribute_names`, requires specifying a transform that (1) translates, then (2) rotates by 90 degrees for proper placement
g.selectAll('text').data(x_loading).enter().append("text")
.attr("transform", d =>
`translate(${x_loading_xScale(d.attribute) + x_loading_xScale.bandwidth() / 2}, ${loading_size + 2})
rotate(90)`)
.attr('dominant-baseline', 'middle')
.attr('font-size', '12')
.text(d => d.attribute)
}
Insert cell
create_y_loading = (g) => {
// create a single line that will span zero for our loading value, positioned at the center of the view
g.append('line')
.attr('y1', 0).attr('y2', plot_size)
.attr('x1', loading_size / 2).attr('x2', loading_size / 2)
.attr('stroke', d3.hcl(0,0,30))

// perform data join on `x_loading` to create bars, using our band scale to position bar horizontally, and the linear scale to determine the bar's y position and height.
g.append('g').selectAll('rect').data(y_loading).enter().append("rect")
.attr("y", d => y_loading_yScale(d.attribute))
.attr("x", d => {if (d.loading > 0) return y_loading_xScale(d.loading) + loading_size / 2; return loading_size / 2})
.attr("height", y_loading_yScale.bandwidth() - 2)
.attr("width", d => Math.abs(y_loading_xScale(d.loading)))
.attr('stroke', d3.hcl(0,0,30))
.attr('fill', d => {if (d.loading > 0) return 'steelblue'; return 'tomato'})
// create a text label for each attribute name, can be accomplished via data join on `attribute_names`, requires specifying a transform that (1) translates, then (2) rotates by 90 degrees for proper placement
g.selectAll('text').data(y_loading).enter().append("text")
.attr("transform", d =>
`translate(${loading_size + 2}, ${y_loading_yScale(d.attribute) + y_loading_yScale.bandwidth() / 2})`)
.attr('dominant-baseline', 'middle')
.attr('font-size', '12')
.text(d => d.attribute)
}
Insert cell
create_clustering = (g) => {
// from our nested data structure, perform:
// (1) a data join on the given nested data, giving us data at the level of cluster labels
// (2) create group elements for each label
// (3) issue a nested data join, from the `values` property of the data bound to the group elements, giving us data at the level of attributes
// (4) create rectangles

// One group for each label
const groups = g.append('g').selectAll('g').data(cluster_labels).enter().append('g')
.attr('transform', d => `translate(${x_loading_xScale(d.attribute)}, 0)`)

cluster_indexes.forEach(n => groups.append('rect')
.attr('x', 0)
.attr('y', clustering_size / 5 * n)
.attr('width', x_loading_xScale.bandwidth())
.attr('height', clustering_size / 5)
.attr('fill', d => colorScale(d.labels[n]))
.attr('stroke', 'darkslategray')
.attr('stroke-width', 2)
)
const axis_group = g.append('g').attr('transform', `translate(${plot_size + 10}, 0)`)
axis_group.append('text')
.attr('text-anchor', 'middle')
.attr('transform', `translate(100, -5)`)
.text('Attribute Counts')
create_color_axis(axis_group, [0,100], [0,200], 0, 15, colorScale)
}
Insert cell
Insert cell
margin = 40
Insert cell
attribute_pad = 20
Insert cell
Insert cell
plot_size = 500
Insert cell
Insert cell
loading_size = 80
Insert cell
The `clustering_size` variable will be the height of the heatmap - use this in your scales as appropriate.
Insert cell
clustering_size = 140
Insert cell
total_height = plot_size + loading_size + clustering_size + 5 * margin
Insert cell
Insert cell
Insert cell
scatter_xScale = d3.scaleLinear()
.domain([d3.min(projection, d => d[0]) - 0.1, d3.max(projection, d => d[0]) + 0.1])
.range([0, plot_size])
Insert cell
scatter_yScale = d3.scaleLinear()
//.domain([d3.max(projection, d => d[1]), d3.min(projection, d => d[1])] // Invert max and min
.domain([2.5, -1.5]) // Just to keep it like the example
.range([0, plot_size])
Insert cell
Insert cell
x_loading_yScale = d3.scaleLinear()
.domain([x_loading_yMax, -x_loading_yMax])
.range([-loading_size/2, loading_size/2])
Insert cell
x_loading_xScale = d3.scaleBand()
.domain(x_loading.map(d => d.attribute))
.range([0, plot_size])
Insert cell
y_loading_xScale = d3.scaleLinear()
.domain([y_loading_xMax, -y_loading_xMax])
.range([-loading_size/2, loading_size/2])
Insert cell
y_loading_yScale = d3.scaleBand()
.domain(y_loading.map(d => d.attribute))
.range([0, plot_size])
Insert cell
Insert cell
colorScale = d3.scaleSequential()
.domain([0, 100])
.interpolator(d3.interpolateHcl('ivory', 'darkgreen'))
Insert cell
Insert cell
Insert cell
// Code Here
Insert cell
Insert cell
x_loading_yMax = d3.max(x_loading, d => Math.abs(d.loading))
Insert cell
y_loading_xMax = d3.max(y_loading, d => Math.abs(d.loading))
Insert cell
Insert cell
// Code Here
attribute_names = y_loading.map(d => d.attribute)
Insert cell
Insert cell
// Code Here
cluster_labels = y_loading.map(d => {
let kmeans = kmeans_data.filter(k => k.value === 1).filter(k => k.attribute === d.attribute)
return {'attribute': d.attribute, 'labels': cluster_indexes.map(n => kmeans.filter(k => k.label === n).length)}
})

Insert cell
cluster_indexes = [0,1,2,3,4,5]
Insert cell
Insert cell
// Code Here
Insert cell
Insert cell
Insert cell
pca_data = await FileAttachment("pca_data.json").json()
Insert cell
projection = pca_data.projection
Insert cell
x_loading = pca_data.loading_x
Insert cell
y_loading = pca_data.loading_y
Insert cell
kmeans_data = await FileAttachment("kmeans_data.json").json()
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
d3 = require('d3@7')
Insert cell
import { drawdom } from '91007ee9d5fd152b'
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