From 0db26ea18ae4242ff02bc92bd3b644b18bb9e33d Mon Sep 17 00:00:00 2001 From: Trevor Manz Date: Tue, 9 Aug 2022 16:11:03 -0400 Subject: [PATCH] refactor(Overlay): use event emitter (#5) --- index.html | 6 +- package.json | 3 +- pnpm-lock.yaml | 7 ++ src/Legend.ts | 190 ++++++++++++++++++++++++++++++ src/Overlay.ts | 305 +++++++----------------------------------------- src/Renderer.ts | 88 ++++++++------ src/index.ts | 13 +-- src/utils.ts | 41 ++++--- 8 files changed, 324 insertions(+), 329 deletions(-) create mode 100644 src/Legend.ts diff --git a/index.html b/index.html index da2794d..1f769dc 100644 --- a/index.html +++ b/index.html @@ -9,12 +9,16 @@ - + diff --git a/package.json b/package.json index 094d79f..2936b6a 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "eigen-tour", "version": "0.0.0", "scripts": { - "dev": "vite", + "dev": "vite --port 3000", "build": "vite build", "preview": "vite preview", "check": "tsc --noEmit", @@ -16,6 +16,7 @@ "apache-arrow": "^9.0.0", "d3": "^7.6.1", "mathjs": "^11.0.1", + "nanoevents": "^7.0.1", "numeric": "^1.2.6" }, "devDependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 127cb50..b180054 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -6,6 +6,7 @@ specifiers: apache-arrow: ^9.0.0 d3: ^7.6.1 mathjs: ^11.0.1 + nanoevents: ^7.0.1 numeric: ^1.2.6 typescript: ^4.7.4 vite: ^3.0.4 @@ -16,6 +17,7 @@ dependencies: apache-arrow: 9.0.0 d3: 7.6.1 mathjs: 11.0.1 + nanoevents: 7.0.1 numeric: 1.2.6 devDependencies: @@ -896,6 +898,11 @@ packages: typed-function: 3.0.0 dev: false + /nanoevents/7.0.1: + resolution: {integrity: sha512-o6lpKiCxLeijK4hgsqfR6CNToPyRU3keKyyI6uwuHRvpRTbZ0wXw51WRgyldVugZqoJfkGFrjrIenYH3bfEO3Q==} + engines: {node: ^14.0.0 || ^16.0.0 || >=18.0.0} + dev: false + /nanoid/3.3.4: resolution: {integrity: sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} diff --git a/src/Legend.ts b/src/Legend.ts new file mode 100644 index 0000000..fc31172 --- /dev/null +++ b/src/Legend.ts @@ -0,0 +1,190 @@ +import * as d3 from "d3"; +import * as nanoevents from "nanoevents"; +import * as utils from "./utils"; + +interface Margin { + top: number; + right: number; + bottom: number; + left: number; +} + +type LegendDatum = [label: string, color: d3.Color]; + +interface LegendEvents { + select: (classes: Set) => void; + mouseout: (classes: Set) => void; +} + +export class Legend { + #data: LegendDatum[]; + #margin: Margin; + #emitter: nanoevents.Emitter; + #root: d3.Selection; + #mark: d3.Selection; + #box: d3.Selection; + #text: d3.Selection; + #title?: d3.Selection; + #titleBg?: d3.Selection; + + constructor( + data: LegendDatum[], + options: { + root: d3.Selection, + title?: string; + margin?: Partial; + }, + ) { + this.#data = data; + this.#root = options.root; + this.#margin = { top: 20, bottom: 0, left: 0, right: 0, ...options.margin }; + this.#emitter = nanoevents.createNanoEvents(); + let selected = new Set; + + this.#box = this.#root.selectAll(".legendBox") + .data([0]) + .enter() + .append("rect") + .attr("class", "legendBox") + .attr("fill", utils.CLEAR_COLOR.formatRgb()) + .attr("stroke", "#c1c1c1") + .attr("stroke-width", 1); + + this.#mark = this.#root.selectAll(".legendMark") + .data(this.#data) + .enter() + .append("circle") + .attr("class", "legendMark"); + + let restoreAlpha = () => { + this.#mark.attr( + "opacity", + (_, i) => selected.size === 0 || selected.has(i) ? 1.0 : 0.1, + ); + this.#emitter.emit("mouseout", selected); + }; + + let select = (sel: d3.Selection) => { + let emitter = this.#emitter; + return function (this: Element) { + const e = sel!.nodes(); + const i = e.indexOf(this); + let classes = new Set(selected); + if (!classes.has(i)) { + classes.add(i); + } + emitter.emit("select", classes); + } + } + + let click = (sel: d3.Selection) => { + let emitter = this.#emitter; + return function (this: Element) { + const e = sel!.nodes(); + const i = e.indexOf(this); + if (selected.has(i)) { + selected.delete(i); + } else { + selected.add(i); + } + emitter.emit("select", selected); + if (selected.size == data.length) { + selected.clear(); + } + } + } + + this.#mark + .attr("fill", ([_, color]) => color.formatRgb()) + .on("mouseover", select(this.#mark)) + .on("mouseout", restoreAlpha) + .on("click", click(this.#mark)); + + this.#text = this.#root.selectAll(".legendText") + .data(this.#data) + .enter() + .append("text") + .attr("class", "legendText"); + + this.#text + .attr("text-anchor", "start") + .attr("fill", "#333") + .text(([label, _]) => label) + .on("mouseover", select(this.#text)) + .on("mouseout", restoreAlpha) + .on("click", click(this.#text)); + + if (options.title && options.title !== "") { + + this.#titleBg = this.#root.selectAll(".legendTitleBg") + .data([0]) + .enter() + .append("rect") + .attr("class", "legendTitleBg") + .attr("fill", utils.CLEAR_COLOR.formatRgb()); + + this.#title = this.#root.selectAll(".legendTitle") + .data([options.title]) + .enter() + .append("text") + .attr("class", "legendTitle") + .attr("alignment-baseline", "middle") + .attr("text-anchor", "middle") + .text((d) => d); + } + } + + resize() { + let width = this.#root.node()!.clientWidth; + let padding = 8; + + let sx = d3.scaleLinear() + .domain([0, 1]) + .range([width - this.#margin.left, width - this.#margin.right]); + + let sy = d3.scaleLinear() + .domain([-1, 0, this.#data.length, this.#data.length + 1]) + .range([ + this.#margin.top - padding, + this.#margin.top, + this.#margin.top + 170, + this.#margin.top + 170 + padding, + ]); + + let r = (sy(1) - sy(0)) / 4; + + this.#mark + .attr("cx", sx(0.001) + 2.5 * r) + .attr("cy", (_, i) => sy(i + 0.5)) + .attr("r", r); + + this.#text + .attr("x", sx(0.0) + 2.5 * r + 2.5 * r) + .attr("y", (_, i) => sy(i + 0.7)); + + this.#box + .attr("x", sx.range()[0]) + .attr("y", sy(-1)) + .attr("width", sx.range()[1] - sx.range()[0]) + .attr("height", sy(this.#data.length + 1) - sy(-1)) + .attr("rx", r); + + if (this.#title && this.#titleBg) { + this.#title + .attr("x", sx(0.5)) + .attr("y", sy(-1)); + + let rectData = this.#title.node()!.getBBox(); + let padding = 2; + this.#titleBg + .attr("x", rectData.x - padding) + .attr("y", rectData.y - padding) + .attr("width", rectData.width + 2 * padding) + .attr("height", rectData.height + 2 * padding); + } + } + + on(event: E, callback: LegendEvents[E]) { + return this.#emitter.on(event, callback); + } +} diff --git a/src/Overlay.ts b/src/Overlay.ts index b3aec79..3192d90 100644 --- a/src/Overlay.ts +++ b/src/Overlay.ts @@ -1,14 +1,11 @@ import * as d3 from "d3"; import * as math from "mathjs"; import * as utils from "./utils"; +import { Legend } from "./Legend"; import type { Renderer } from "./Renderer"; -import type { ColorRGB, Scale } from "./types"; - -export interface OverlayOptions {} export class Overlay { - selectedClasses: Set; figure: d3.Selection; epochSlider: d3.Selection; playButton: d3.Selection; @@ -16,20 +13,9 @@ export class Overlay { grandtourButton: d3.Selection; epochIndicator: d3.Selection; svg: d3.Selection; - anchorRadius?: number; annotate?: (renderer: Renderer) => void; - - legendBox?: d3.Selection; - legendTitle?: d3.Selection; - legendTitleBg?: d3.Selection; - legendMark?: d3.Selection< - SVGCircleElement, - d3.RGBColor, - SVGSVGElement, - unknown - >; - legendText?: d3.Selection; + legend?: Legend; anchors?: d3.Selection< SVGCircleElement, [number, number], @@ -37,19 +23,10 @@ export class Overlay { unknown >; - legend_sx?: Scale; - legend_sy?: Scale; - - constructor( - public renderer: Renderer, - _opts: Partial = {}, - ) { - this.selectedClasses = new Set(); - this.renderer = renderer; - + constructor(public renderer: Renderer) { this.figure = d3.select(renderer.gl.canvas.parentNode as HTMLElement); - let self = this; + this.epochSlider = this.figure .insert("input", ":first-child") .attr("type", "range") @@ -142,7 +119,7 @@ export class Overlay { renderer.shouldCentralizeOrigin = renderer.shouldPlayGrandTour; renderer.isScaleInTransition = true; - renderer.setScaleFactor(1.0); + renderer.scaleFactor = 1.0; renderer.scaleTransitionProgress = renderer.shouldCentralizeOrigin ? Math.min(1, renderer.scaleTransitionProgress) : Math.max(0, renderer.scaleTransitionProgress); @@ -160,7 +137,6 @@ export class Overlay { d3.select(this).style("opacity", 0.3); } }); - this.svg = this.figure .insert("svg", ":first-child") .attr("class", "overlay") @@ -191,40 +167,31 @@ export class Overlay { } } - get canvas() { - return this.renderer.gl.canvas; - } - get width() { - return this.canvas.clientWidth; + return this.renderer.gl.canvas.clientWidth; } get height() { - return this.canvas.clientHeight; + return this.renderer.gl.canvas.clientHeight; } - getDataset() { + get dataset() { return utils.getDataset() as keyof typeof utils.legendTitle; } - updateArchorRadius() { + resize() { + this.svg.attr("width", this.width); + this.svg.attr("height", this.height); + + this.legend?.resize(); + this.anchorRadius = utils.clamp( 7, 10, Math.min(this.width, this.height) / 50, ); this.anchors?.attr("r", this.anchorRadius); - } - resize() { - this.svg.attr("width", this.width); - this.svg.attr("height", this.height); - this.initLegendScale(); - this.updateArchorRadius(); - this.repositionAll(); - } - - repositionAll() { let sliderLeft = parseFloat(this.epochSlider.style("left")); let sliderWidth = parseFloat(this.epochSlider.style("width")); let sliderMiddle = sliderLeft + sliderWidth / 2; @@ -238,51 +205,10 @@ export class Overlay { .attr("x", this.width / 2 - 10) .attr("y", this.height - 20); } - - if (!(this.legend_sx && this.legend_sy)) return; - - let r = (this.legend_sy(1) - this.legend_sy(0)) / 4; - - this.legendMark - ?.attr("cx", this.legend_sx(0.001) + 2.5 * r) - .attr("cy", (_, i) => this.legend_sy!(i + 0.5)) - .attr("r", r); - - this.legendText - ?.attr("x", +this.legend_sx(0.0) + 2.5 * r + 2.5 * r) - .attr("y", (_, i) => this.legend_sy!(i + 0.5)); - - this.legendBox - ?.attr("x", this.legend_sx.range()[0]) - .attr("y", this.legend_sy(-1)) - .attr("width", this.legend_sx.range()[1] - this.legend_sx.range()[0]) - .attr( - "height", - this.legend_sy(utils.getLabelNames().length + 1) - this.legend_sy(-1), - ) - .attr("rx", r); - - if (this.legendTitle !== undefined) { - this.legendTitle - .attr("x", this.legend_sx(0.5)) - .attr("y", this.legend_sy(-1)) - .text(utils.legendTitle[this.getDataset()] || ""); - - let rectData = this.legendTitle.node()!.getBBox(); - let padding = 2; - this.legendTitleBg! - .attr("x", rectData.x - padding) - .attr("y", rectData.y - padding) - .attr("width", rectData.width + 2 * padding) - .attr("height", rectData.height + 2 * padding) - .attr("opacity", utils.legendTitle[this.getDataset()] ? 1 : 0); - } } init() { - let labels = utils.getLabelNames(false, this.getDataset()); - let colors = utils.baseColors.slice(0, labels.length); - this.initLegend(colors, labels); + this.initLegend(); this.resize(); this.drawAxes(); if (this.annotate !== undefined) { @@ -362,183 +288,38 @@ export class Overlay { .attr("cy", (_, i) => this.renderer.sy(handlePos[i][1])); } - initLegendScale() { - let width = +this.svg.attr("width"); - let marginTop = 20; - let padding = 8; - - let legendLeft = width - utils.legendLeft[this.getDataset()]; - let legendRight = width - utils.legendRight[this.getDataset()]; - - this.legend_sx = d3.scaleLinear() - .domain([0, 1]) - .range([legendLeft, legendRight]); - this.legend_sy = d3.scaleLinear() - .domain([ - -1, - 0, - utils.getLabelNames().length, - utils.getLabelNames().length + 1, - ]) - .range([ - marginTop - padding, - marginTop, - marginTop + 170, - marginTop + 170 + padding, - ]); - } - - initLegend(colors: ColorRGB[], labels: string[]) { - this.initLegendScale(); - - let clearColor = d3.rgb( - ...utils.CLEAR_COLOR.map((d) => d * 255) as ColorRGB, + initLegend() { + let data = utils.zip( + utils.getLabelNames(false, this.dataset), + utils.baseColors, ); - - if (this.legendBox === undefined) { - this.legendBox = this.svg.selectAll(".legendBox") - .data([0]) - .enter() - .append("rect") - .attr("class", "legendBox") - .attr("fill", clearColor.formatRgb()) - .attr("stroke", "#c1c1c1") - .attr("stroke-width", 1); - } - - let legendTitleText = utils.legendTitle[this.getDataset()]; - if ( - this.legendTitle === undefined && legendTitleText !== undefined - ) { - this.legendTitleBg = this.svg.selectAll(".legendTitleBg") - .data([0]) - .enter() - .append("rect") - .attr("class", "legendTitleBg") - .attr("fill", clearColor.formatRgb()); - - this.legendTitle = this.svg.selectAll(".legendTitle") - .data([legendTitleText]) - .enter() - .append("text") - .attr("class", "legendTitle") - .attr("alignment-baseline", "middle") - .attr("text-anchor", "middle") - .text((d) => d); - } - - let self = this; - - this.legendMark = this.svg.selectAll(".legendMark") - .data(colors.map((c) => d3.rgb(...c))) - .enter() - .append("circle") - .attr("class", "legendMark"); - - this.legendMark - .attr("fill", (color) => color.formatRgb()) - .on("mouseover", function () { - const e = self.legendMark!.nodes(); - const i = e.indexOf(this); - - let classes = new Set(self.selectedClasses); - if (!classes.has(i)) { - classes.add(i); - } - self.onSelectLegend(classes); - }) - .on("mouseout", () => this.restoreAlpha()) - .on("click", function () { - const e = self.legendMark!.nodes(); - const i = e.indexOf(this); - - if (self.selectedClasses.has(i)) { - self.selectedClasses.delete(i); - } else { - self.selectedClasses.add(i); - } - self.onSelectLegend(self.selectedClasses); - if (self.selectedClasses.size == self.renderer.dataObj?.ndim) { - self.selectedClasses = new Set(); - } - }); - - this.legendText = this.svg.selectAll(".legendText") - .data(labels) - .enter() - .append("text") - .attr("class", "legendText"); - - this.legendText - .attr("alignment-baseline", "middle") - .attr("fill", "#333") - .text((label) => label) - .on("mouseover", function () { - const e = self.legendText!.nodes(); - const i = e.indexOf(this); - let classes = new Set(self.selectedClasses); - if (!classes.has(i)) { - classes.add(i); - } - self.onSelectLegend(classes); - }) - .on("mouseout", () => this.restoreAlpha()) - .on("click", function () { - const e = self.legendText!.nodes(); - const i = e.indexOf(this); - - if (self.selectedClasses.has(i)) { - self.selectedClasses.delete(i); - } else { - self.selectedClasses.add(i); - } - self.onSelectLegend(self.selectedClasses); - - if (self.selectedClasses.size == self.renderer.dataObj?.ndim) { - self.selectedClasses = new Set(); - } - }); - } - - onSelectLegend(labelClasses: number | number[] | Set) { - if (!this.renderer.dataObj) return; - - if (typeof labelClasses === "number") { - labelClasses = [labelClasses]; - } - let labelSet = new Set(labelClasses); - - for (let i = 0; i < this.renderer.dataObj.npoint; i++) { - if (labelSet.has(this.renderer.dataObj.labels[i])) { - this.renderer.dataObj.alphas[i] = 255; - } else { - this.renderer.dataObj.alphas[i] = 0; - } - } - - this.legendMark?.attr("opacity", (_, i) => labelSet.has(i) ? 1.0 : 0.1); - } - - restoreAlpha() { - if (!this.renderer.dataObj) return; - if (this.selectedClasses.size == 0) { - for (let i = 0; i < this.renderer.dataObj.npoint; i++) { - this.renderer.dataObj.alphas[i] = 255; + this.legend = new Legend(data, { + root: this.svg, + title: utils.legendTitle[this.dataset], + margin: { + left: utils.legendLeft[this.dataset], + right: utils.legendRight[this.dataset], + }, + }); + this.legend.on("select", (classes) => { + if (!this.renderer.dataObj) return; + let { npoint, labels, alphas } = this.renderer.dataObj; + for (let i = 0; i < npoint; i++) { + alphas[i] = classes.has(labels[i]) ? 255 : 0; } - } else { - for (let i = 0; i < this.renderer.dataObj.npoint; i++) { - if (this.selectedClasses.has(this.renderer.dataObj.labels[i])) { - this.renderer.dataObj.alphas[i] = 255; - } else { - this.renderer.dataObj.alphas[i] = 0; + }); + this.legend.on("mouseout", (classes) => { + if (!this.renderer.dataObj) return; + let { npoint, labels, alphas } = this.renderer.dataObj; + if (classes.size === 0) { + for (let i = 0; i < npoint; i++) { + alphas[i] = 255; } + return; + } + for (let i = 0; i < npoint; i++) { + alphas[i] = classes.has(labels[i]) ? 255 : 0; } - } - - this.legendMark?.attr("opacity", (_, i) => { - return this.selectedClasses.size == 0 || this.selectedClasses.has(i) - ? 1.0 - : 0.1; }); } } diff --git a/src/Renderer.ts b/src/Renderer.ts index 52f98f1..38f98f9 100644 --- a/src/Renderer.ts +++ b/src/Renderer.ts @@ -3,7 +3,7 @@ import * as d3 from "d3"; import * as math from "mathjs"; import * as utils from "./utils"; import { GrandTour } from "./GrandTour"; -import { Overlay, OverlayOptions } from "./Overlay"; +import { Overlay } from "./Overlay"; import type { ColorRGBA } from "./types"; @@ -25,7 +25,6 @@ interface RendererOptions { shouldAutoNextEpoch: boolean; shouldPlayGrandTour: boolean; isFullScreen: boolean; - overlayKwargs: OverlayOptions; pointSize: number; } @@ -36,7 +35,6 @@ export class Renderer { scaleTransitionDelta = 0; colorFactor = 0.9; isFullScreen = false; - isDataReady = false; shouldRender = true; scaleFactor = 1.0; s = 1.0; @@ -46,7 +44,7 @@ export class Renderer { epochIndex: number; shouldAutoNextEpoch: boolean; shouldPlayGrandTour: boolean; - pointSize: number; + #pointSize: number; pointSize0: number; overlay: Overlay; sx_span: d3.ScaleLinear; @@ -92,9 +90,8 @@ export class Renderer { this.epochIndex = opts.epochIndex ?? this.epochs[0]; this.shouldAutoNextEpoch = opts.shouldAutoNextEpoch ?? true; this.shouldPlayGrandTour = opts.shouldPlayGrandTour ?? true; - this.pointSize = opts.pointSize ?? 6.0; - this.pointSize0 = this.pointSize; - this.overlay = new Overlay(this, opts.overlayKwargs); + this.pointSize0 = this.#pointSize = opts.pointSize ?? 6.0; + this.overlay = new Overlay(this); this.sx_span = d3.scaleLinear(); this.sy_span = d3.scaleLinear(); @@ -107,10 +104,6 @@ export class Renderer { this.sz = this.sz_center; } - setScaleFactor(s: number) { - this.scaleFactor = s; - } - async initData(buffer: ArrayBuffer) { let table = arrow.tableFromIPC(buffer); let ndim = 5; @@ -134,7 +127,6 @@ export class Renderer { let dataTensor = utils.reshape(new Float32Array(arr), shape); this.shouldRecalculateColorRect = true; - this.isDataReady = true; this.dataObj = { labels, @@ -145,12 +137,12 @@ export class Renderer { ndim, npoint, nepoch, - alphas: d3.range(npoint + 5 * npoint).map(() => 255), + alphas: Array.from({ length: npoint + 5 * npoint }, () => 255), }; this.initGL(this.dataObj); - if (this.isDataReady && this.isPlaying === undefined) { + if (this.isPlaying === undefined) { // renderer.isPlaying===undefined indicates the renderer on init // otherwise it is reloading other dataset this.isPlaying = true; @@ -159,7 +151,6 @@ export class Renderer { } if ( - this.isDataReady && (this.animId == null || this.shouldRender == false) ) { this.shouldRender = true; @@ -184,12 +175,20 @@ export class Renderer { this.gl.viewport(0, 0, canvas.width, canvas.height); } + get #clearColor() { + return this.gl.getParameter(this.gl.COLOR_CLEAR_VALUE) as [ + number, + number, + number, + number, + ]; + } + initGL(dataObj: Data) { - let program = this.program; utils.resizeCanvas(this.gl.canvas); this.gl.viewport(0, 0, this.gl.canvas.width, this.gl.canvas.height); - this.gl.clearColor(...utils.CLEAR_COLOR, 1.0); + this.gl.clearColor(...this.#clearColor); this.gl.enable(this.gl.BLEND); this.gl.disable(this.gl.DEPTH_TEST); @@ -201,30 +200,39 @@ export class Renderer { ); this.gl.clear(this.gl.COLOR_BUFFER_BIT | this.gl.DEPTH_BUFFER_BIT); - this.gl.useProgram(program); + this.gl.useProgram(this.program); this.colorBuffer = this.gl.createBuffer()!; - this.colorLoc = this.gl.getAttribLocation(program, "a_color"); + this.colorLoc = this.gl.getAttribLocation(this.program, "a_color"); this.positionBuffer = this.gl.createBuffer()!; - this.positionLoc = this.gl.getAttribLocation(program, "a_position"); + this.positionLoc = this.gl.getAttribLocation(this.program, "a_position"); - this.pointSizeLoc = this.gl.getUniformLocation(program, "point_size")!; + this.pointSizeLoc = this.gl.getUniformLocation(this.program, "point_size")!; this.isDrawingAxisLoc = this.gl.getUniformLocation( - program, + this.program, "isDrawingAxis", )!; - this.canvasWidthLoc = this.gl.getUniformLocation(program, "canvasWidth")!; - this.canvasHeightLoc = this.gl.getUniformLocation(program, "canvasHeight")!; + this.canvasWidthLoc = this.gl.getUniformLocation( + this.program, + "canvasWidth", + )!; + this.canvasHeightLoc = this.gl.getUniformLocation( + this.program, + "canvasHeight", + )!; this.gl.uniform1f(this.canvasWidthLoc, this.gl.canvas.clientWidth); this.gl.uniform1f(this.canvasHeightLoc, this.gl.canvas.clientHeight); - this.modeLoc = this.gl.getUniformLocation(program, "mode")!; + this.modeLoc = this.gl.getUniformLocation(this.program, "mode")!; this.gl.uniform1i(this.modeLoc, 0); // "point" mode - this.colorFactorLoc = this.gl.getUniformLocation(program, "colorFactor")!; + this.colorFactorLoc = this.gl.getUniformLocation( + this.program, + "colorFactor", + )!; this.setColorFactor(this.colorFactor); if (this.gt === undefined || this.gt.ndim != dataObj.ndim) { @@ -274,8 +282,12 @@ export class Renderer { this.gl.uniform1f(this.colorFactorLoc!, f); } - setPointSize(s: number) { - this.pointSize = s; + get pointSize() { + return this.#pointSize; + } + + set pointSize(s: number) { + this.#pointSize = s; this.gl.uniform1f(this.pointSizeLoc!, s * window.devicePixelRatio); } @@ -349,25 +361,25 @@ export class Renderer { ); } - utils.updateScale_center( + utils.updateScaleCenter( points, this.gl.canvas, this.sx_center, this.sy_center, this.sz_center, this.scaleFactor, - utils.legendLeft[this.overlay.getDataset()] + 15, + utils.legendLeft[this.overlay.dataset] + 15, 65, ); - utils.updateScale_span( + utils.updateScaleSpan( points, this.gl.canvas, this.sx_span, this.sy_span, this.sz_span, this.scaleFactor, - utils.legendLeft[this.overlay.getDataset()] + 15, + utils.legendLeft[this.overlay.dataset] + 15, 65, ); @@ -377,12 +389,14 @@ export class Renderer { } else { transition = (t: number) => 1 - Math.pow(1 - t, 0.5); } + this.sx = utils.mixScale( this.sx_center, this.sx_span, this.scaleTransitionProgress, transition, ); + this.sy = utils.mixScale( this.sy_center, this.sy_span, @@ -398,13 +412,13 @@ export class Renderer { let colors: ColorRGBA[] = labels .map((d) => utils.baseColors[d]) .concat(utils.createAxisColors(dataObj.ndim)) - .map((c, i) => [c[0], c[1], c[2], dataObj.alphas[i]]); + .map((c, i) => [c.r, c.g, c.b, dataObj.alphas[i]]); dataObj.colors = colors; this.gl.viewport(0, 0, this.gl.canvas.width, this.gl.canvas.height); - this.gl.clearColor(...utils.CLEAR_COLOR, 1.0); + this.gl.clearColor(...this.#clearColor); this.gl.clear(this.gl.COLOR_BUFFER_BIT | this.gl.DEPTH_BUFFER_BIT); this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.positionBuffer!); @@ -430,19 +444,19 @@ export class Renderer { this.gl.bufferData( this.gl.ARRAY_BUFFER, new Uint8Array( - bgColors.map((c) => [c[0], c[1], c[2], utils.pointAlpha]).flat() + bgColors.map((c) => [c.r, c.g, c.b, utils.pointAlpha]).flat(), ), this.gl.STATIC_DRAW, ); this.gl.uniform1i(this.isDrawingAxisLoc!, 0); - this.setPointSize(this.pointSize0 * Math.sqrt(this.scaleFactor)); + this.pointSize = this.pointSize0 * Math.sqrt(this.scaleFactor); this.gl.drawArrays(this.gl.POINTS, 0, dataObj.npoint); this.gl.bufferData( this.gl.ARRAY_BUFFER, new Uint8Array( - colors.map((c, i) => [c[0], c[1], c[2], dataObj.alphas[i]]).flat() + colors.map((c, i) => [c[0], c[1], c[2], dataObj.alphas[i]]).flat(), ), this.gl.STATIC_DRAW, ); diff --git a/src/index.ts b/src/index.ts index ced808d..9c571a1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -13,13 +13,12 @@ async function main() { renderer.overlay.playButton.style("top", "calc(100% - 34px)"); renderer.overlay.grandtourButton.style("top", "calc(100% - 34px)"); - let removeBanner = utils.createLoadingBanner(renderer.overlay.figure); - let response = await fetch( - new URL("../data/eigs.arrow", import.meta.url) - ); - await renderer.initData(await response.arrayBuffer()); - - removeBanner(); + { + let clearBanner = utils.createLoadingBanner(renderer.overlay.figure); + let res = await fetch(new URL("../data/eigs.arrow", import.meta.url)) + await renderer.initData(await res.arrayBuffer()); + clearBanner(); + } window.addEventListener("resize", () => { renderer.setFullScreen(renderer.isFullScreen); diff --git a/src/utils.ts b/src/utils.ts index d93e23c..ae89953 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -2,9 +2,9 @@ import * as d3 from "d3"; import * as math from "mathjs"; import numeric from "numeric"; -import type { ColorRGB, Scale } from "./types"; +import type { Scale } from "./types"; -export const CLEAR_COLOR = [1, 1, 1] as const; +export const CLEAR_COLOR = d3.rgb(0, 0, 0, 0); export let dataset = "mnist"; export const pointAlpha = 255 * 0.1; @@ -23,7 +23,7 @@ export function mixScale( s0: Scale, s1: Scale, progress: number, - func: (x: number) => number, + func: (t: number) => number, ) { let range0 = s0.range(); let range1 = s1.range(); @@ -51,7 +51,7 @@ export function data2canvas( return points; } -export function updateScale_span( +export function updateScaleSpan( points: number[][], canvas: HTMLCanvasElement, sx: d3.ScaleLinear, @@ -94,7 +94,7 @@ export function updateScale_span( .range([0, 1]); } -export function updateScale_center( +export function updateScaleCenter( points: number[][], canvas: HTMLCanvasElement, sx: Scale, @@ -210,7 +210,9 @@ export function createLoadingBanner(sel: d3.Selection) { } repeat(); - return () => { banner.remove() }; + return () => { + banner.remove(); + }; } // TODO: fail when shape isn't tuple... Right now returns `number`. @@ -265,23 +267,12 @@ export function resizeCanvas(canvas: HTMLCanvasElement) { canvas.style.height = String(canvas.clientHeight); } -export const baseColorsHex = [...d3.schemeCategory10]; - -function hexToRgb(hex: string): ColorRGB { - let result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)!; - return [ - parseInt(result[1], 16), - parseInt(result[2], 16), - parseInt(result[3], 16), - ]; -} - -export const baseColors = baseColorsHex.map(hexToRgb); +export const baseColors = d3.schemeCategory10.map((c) => d3.rgb(c)!); export const bgColors = numeric.add( - numeric.mul(baseColors, 0.6), + numeric.mul(baseColors.map((c) => [c.r, c.g, c.b]), 0.6), 0.95 * 255 * 0.4, -); +).map((c) => d3.rgb(...c as [number, number, number])); export function createAxisPoints(ndim: number) { let res = (math.identity(ndim) as math.Matrix).toArray(); @@ -293,7 +284,7 @@ export function createAxisPoints(ndim: number) { } export function createAxisColors(ndim: number) { - const gray: ColorRGB = [150, 150, 150]; + const gray = d3.rgb(150, 150, 150); return Array.from({ length: ndim * 2 }, () => gray); } @@ -436,3 +427,11 @@ export function flatten(v: Matrix) { return floats; } + +export function zip(a: A[], b: B[]): [A, B][] { + let out: [A, B][] = []; + for (let i = 0; i < Math.min(a.length, b.length); i++) { + out.push([a[i], b[i]]); + } + return out; +}