UNPKG

distortions

Version:

Helpers for visualizing distortion in nonlinear dimensionality reduction.

168 lines (149 loc) 6.11 kB
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"); }