UNPKG

distortions

Version:

Helpers for visualizing distortion in nonlinear dimensionality reduction.

125 lines (107 loc) 4.38 kB
import * as d3 from "https://esm.sh/d3@7"; import { SVD } from "https://esm.sh/svd-js"; import { matrix, add, subtract, multiply, inv, diag, zeros, size, transpose, det } from 'https://esm.sh/mathjs'; export function isometry_update(ev, g, dataset, metrics, mappingObj, xScale, yScale, rScale, opts, margin) { // metric at the current mouseover location let f_star = [xScale.invert(ev.layerX - margin.left), yScale.invert(ev.layerY - margin.top)] let { h_star } = local_metric(metrics, f_star, dataset, opts.metric_bw) let kn_t = local_metric(metrics, f_star, dataset, opts.transformation_bw)["kn"] // normalize so that transformation is between 0 and 1 kn_t = kn_t.map(k => k / d3.max(kn_t)) let h_star_ = inv(square_root_reorient(h_star)) let h_star_inv = inv(h_star) // get transformed coordinates let new_coords = [] let N = Object.values(dataset).length for (let n = 0; n < N; n++) { let f_n = matrix([dataset[n][mappingObj.x], dataset[n][mappingObj.y]]) let f_tilde_n = add(multiply(h_star_, subtract(f_n, f_star)), f_star) let f_n_transform = add(multiply(kn_t[n], f_tilde_n), multiply(1 - kn_t[n], f_n)) // new SVD data, for ellipse orientation let h_product = multiply(h_star_inv, matrix(metrics[n])) let { q, v } = SVD(h_product._data) let sv_transform = [ kn_t[n] * q[0] + (1 - kn_t[n]) * dataset[n]["s0"], kn_t[n] * q[1] + (1 - kn_t[n]) * dataset[n]["s1"] ] let v_transform = [ kn_t[n] * v[0][0] + (1 - kn_t[n]) * dataset[n]["x0"], kn_t[n] * v[0][1] + (1 - kn_t[n]) * dataset[n]["y0"] ] new_coords.push({ [mappingObj.x]: f_n_transform._data[0], [mappingObj.y]: f_n_transform._data[1], "s0": sv_transform[0], "s1": sv_transform[1], "x0": v_transform[0], "y0": v_transform[1], "new_angle": angle(v_transform[0], v_transform[1]), "_id": dataset[n]["_id"] }) } for (let c in opts.otherClasses) { g.select(`.${opts.otherClasses[c]}`) .selectAll("*") .data(new_coords, d => d._id) .attr("cx", d => xScale(d[mappingObj.x])) .attr("cy", d => yScale(d[mappingObj.y])) .attr("rx", d => rScale(d[mappingObj.a])) .attr("ry", d => rScale(d[mappingObj.b])) .attr("transform", d => `rotate(${d["new_angle"]} ${xScale(d[mappingObj.x])} ${yScale(d[mappingObj.y])})`) } g.select(".isometry-links") .selectAll("line") .data(new_coords, d => d._id) .attr("x1", d => xScale(d[mappingObj.x])) .attr("y1", d => yScale(d[mappingObj.y])) } function similarities(f_star, dataset, gamma) { let result = [] for (let n = 0; n < Object.values(dataset).length; n++) { let f_n = [dataset[n].embedding_0, dataset[n].embedding_1]; result.push(similarity(f_star, f_n, gamma)); } let total = d3.sum(result); return result.map(d => d / total) } function local_metric(metrics, f_star, dataset, gamma) { let N = Object.values(metrics).length let h0 = matrix(metrics[0]) let h_star = zeros(size(h0)); let kn = similarities(f_star, dataset, gamma) for (let n = 0; n < N; n++) { h_star = add(h_star, multiply(kn[n], matrix(metrics[n]))) } let { q } = SVD(h_star._data) return { "h_star": h_star, "sv": q, "kn": kn } } function square_root_reorient(A) { let svd_result = SVD(A._data) let { v, q } = reorient(svd_result) return multiply(matrix(v), diag(q.map(qk => Math.sqrt(qk)))) } function reorient(svd_result) { let v = matrix(svd_result["v"]) let q = svd_result["q"] if (Math.abs(v._data[0][0]) < Math.abs(v._data[0][1])) { let P = matrix([[0, 1], [1, 0]]) v = multiply(v, P) q = [q[1], q[0]] } if (det(v) < 0) { v = multiply(v, diag([1, -1])) } if (v._data[0][0] < 0) { v = multiply(v, diag([-1, -1])) } return { "v": v, "q": q } } function similarity(a, b, gamma = 1) { let d = Math.sqrt((a[0] - b[0]) ** 2 + (a[1] - b[1]) ** 2); let result = Math.exp(-gamma * d); if (isNaN(result)) { return 0; } return result; } function angle(x1, y1) { return Math.atan(y1 / x1) * (180 / Math.PI) + 90; }