distortions
Version:
Helpers for visualizing distortion in nonlinear dimensionality reduction.
168 lines (149 loc) • 6.11 kB
JavaScript
import * as d3 from "https://esm.sh/d3@7";
export function draw_boxplot(params, summaries, outliers) {
params.g.append("g")
.attr("class", params.opts.className)
draw_whiskers(params, summaries)
draw_rects(params, summaries)
draw_outliers(params, outliers)
outlier_reactivity(params, outliers)
annotate_outliers(params)
}
function draw_rects(params, summaries) {
// box between q1 and q3
params.g.select(`.${params.opts.className}`)
.selectAll("rect")
.data(summaries, d => d.bin).enter()
.append("rect")
.attr("x", d => params.xBoxScale(d.bin))
.attr("y", d => params.yBoxScale(d.q3))
.attr("width", params.xBoxScale.bandwidth())
.attr("height", d => params.yBoxScale(d.q1) - params.yBoxScale(d.q3))
.attr("fill", params.opts.fill)
.attr("stroke", params.opts.stroke)
// line at the median
params.g.select(`.${params.opts.className}`)
.selectAll("line")
.data(summaries, d => d.id).enter()
.append("line")
.attr("x1", d => params.xBoxScale(d.bin))
.attr("y1", d => params.yBoxScale(d.q2))
.attr("x2", d => params.xBoxScale(d.bin) + params.xBoxScale.bandwidth())
.attr("y2", d => params.yBoxScale(d.q2))
.attr("stroke", params.opts.stroke)
}
function draw_whiskers(params, summaries) {
params.g.select(`.${params.opts.className}`)
.selectAll(".whisker")
.data(summaries, d => d.bin).enter()
.append("line")
.attr("class", "whisker")
.attr("x1", d => params.xBoxScale(d.bin) + params.xBoxScale.bandwidth() / 2)
.attr("y1", d => params.yBoxScale(d.lower))
.attr("x2", d => params.xBoxScale(d.bin) + params.xBoxScale.bandwidth() / 2)
.attr("y2", d => params.yBoxScale(d.upper))
.attr("stroke", params.opts.stroke);
}
function draw_outliers(params, outliers) {
params.g.select(`.${params.opts.className}`)
.selectAll("circle")
.data(outliers, d => `${d.center}-${d.neighbor}`).enter()
.append("circle")
.attr("cx", d => params.xBoxScale(d.bin) + params.xBoxScale.bandwidth() / 2)
.attr("cy", d => params.yBoxScale(d.value))
.attr("r", params.opts.outlierRadius)
.attr("fill", params.opts.fill)
}
function outlier_reactivity(params, outliers) {
// Define brush behavior and add it to the boxplot group
let brush = d3.brush()
.on("brush end", brushed)
.extent(
[[params.xBoxScale.range()[0] - 15, params.yBoxScale.range()[1] - 15],
[params.xBoxScale.range()[1] + 15, params.yBoxScale.range()[0] + 15]]
)
params.g.select(`.${params.opts.className}`)
.call(brush);
params.g.insert("g").attr("id", "link_highlight");
function brushed(event) {
if (!event.selection) return;
// Find outliers within the brush selection
let [[x0, y0], [x1, y1]] = event.selection;
let selected = outliers.filter(d => {
let cx = params.xBoxScale(d.bin) + params.xBoxScale.bandwidth() / 2
let cy = params.yBoxScale(d.value)
return x0 <= cx && cx <= x1 && y0 <= cy && cy <= y1
});
// update view based on selection
highlight_outliers(params, selected)
highlight_links(params, selected)
}
}
function highlight_outliers(params, selected) {
const ids = new Set(selected.map(d => `${d.center}-${d.neighbor}`));
params.g.select(`.${params.opts.className}`)
.selectAll("circle")
.attr("fill", d => ids.has(`${d.center}-${d.neighbor}`) ? params.opts.highlightColor : params.opts.fill);
}
function highlight_links(params, selected) {
// get data from the current links
let links = []
for (let i = 0; i < selected.length; i++) {
links.push({
"center": params.dataset[selected[i]["center"]],
"neighbor": params.dataset[selected[i]["neighbor"]],
"center_id": selected[i]["center"],
"neighbor_id": selected[i]["neighbor"]
})
}
// select the group for link highlights
let link_selection = params.g.select("#link_highlight")
.selectAll("line")
.data(links, d => `${d.center_id}-${d.neighbor_id}`);
// update the link selection
link_selection.exit().remove();
link_selection.enter()
.append("line")
.attr("x1", d => params.xScale(d.center.embedding_0))
.attr("y1", d => params.yScale(d.center.embedding_1))
.attr("x2", d => params.xScale(d.neighbor.embedding_0))
.attr("y2", d => params.yScale(d.neighbor.embedding_1))
.attr("stroke", params.opts.highlightColor)
.attr("stroke-width", params.opts.strokeWidth)
.attr("id", d => `${d.center_id}-${d.neighbor_id}`);
// highlight the points at the endpoints of the links
const highlight_ids = new Set(links.flatMap(d => [d.center_id, d.neighbor_id]));
for (let i = 0; i < params.opts.otherClasses.length; i++) {
params.g.select(`.${params.opts.otherClasses[i]}`)
.selectAll("*")
.attr("stroke", d => {
return highlight_ids.has(d._id) ? params.opts.highlightColor : null
})
.attr("stroke-width", d => highlight_ids.has(d._id) ? params.opts.highlightStrokeWidth : null)
.attr("fill-opacity", d => highlight_ids.has(d._id) ? params.opts.opacity : params.opts.backgroundOpacity);
}
}
function annotate_outliers(params) {
// define the axis elements
params.g.select(`.${params.opts.className}`).append("g")
.attr("class", "x-axis")
.attr("transform", `translate(0,${params.yBoxScale.range()[0]})`)
.call(d3.axisBottom(params.xBoxScale))
.selectAll("text")
.attr("transform", "rotate(90)")
.attr("x", 10)
.attr("y", -params.xBoxScale.bandwidth() * 0.25)
.style("text-anchor", "start");
params.g.select(`.${params.opts.className}`).append("g")
.append("g")
.attr("class", "y-axis")
.attr("transform", `translate(${params.xBoxScale.range()[0]},0)`)
.call(d3.axisLeft(params.yBoxScale).ticks(5));
params.g.select(`.${params.opts.className}`)
.append("text")
.attr("text-anchor", "middle")
.attr("x", (params.xBoxScale.range()[0] + params.xBoxScale.range()[1]) / 2)
.attr("y", params.yBoxScale.range()[1] - 10)
.style("fill", "#0c0c0c")
.style("font-size", "10px")
.text("Embedding vs. Original Distance");
}