From afe55c02463279bf2688719e62f761fb12649c52 Mon Sep 17 00:00:00 2001 From: Trevor Manz Date: Sat, 6 Aug 2022 17:48:14 -0400 Subject: [PATCH] feat: migrate to ES6 classes and strict types (#3) --- index.html | 1 - package.json | 44 +-- src/GrandTour.ts | 272 +++++++-------- src/TeaserOverlay.ts | 751 +++++++++++++++++++----------------------- src/TeaserRenderer.ts | 521 ++++++++++++++--------------- src/index.ts | 79 ++--- src/types.ts | 25 ++ src/utils.ts | 424 ++++++++---------------- 8 files changed, 930 insertions(+), 1187 deletions(-) create mode 100644 src/types.ts diff --git a/index.html b/index.html index 6ed913e..da2794d 100644 --- a/index.html +++ b/index.html @@ -13,7 +13,6 @@ - diff --git a/package.json b/package.json index fd82b29..094d79f 100644 --- a/package.json +++ b/package.json @@ -1,23 +1,25 @@ { - "name": "eigen-tour", - "version": "0.0.0", - "scripts": { - "dev": "vite", - "build": "vite build", - "preview": "vite preview" - }, - "author": "Trevor Manz", - "license": "MIT", - "dependencies": { - "@types/d3": "^7.4.0", - "@types/numeric": "^1.2.2", - "apache-arrow": "^9.0.0", - "d3": "^7.6.1", - "mathjs": "^11.0.1", - "numeric": "^1.2.6" - }, - "devDependencies": { - "typescript": "^4.7.4", - "vite": "^3.0.4" - } + "name": "eigen-tour", + "version": "0.0.0", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview", + "check": "tsc --noEmit", + "fmt": "deno fmt --ignore=dist,node_modules --options-use-tabs" + }, + "author": "Trevor Manz", + "license": "MIT", + "dependencies": { + "@types/d3": "^7.4.0", + "@types/numeric": "^1.2.2", + "apache-arrow": "^9.0.0", + "d3": "^7.6.1", + "mathjs": "^11.0.1", + "numeric": "^1.2.6" + }, + "devDependencies": { + "typescript": "^4.7.4", + "vite": "^3.0.4" + } } diff --git a/src/GrandTour.ts b/src/GrandTour.ts index c3a9c88..bbe1c2b 100644 --- a/src/GrandTour.ts +++ b/src/GrandTour.ts @@ -1,137 +1,149 @@ -// @ts-check import * as math from "mathjs"; import numeric from "numeric"; import * as utils from "./utils"; -export function GrandTour(ndim, init_matrix) { - this.ndim = ndim; - this.N = ndim*ndim; - - this.STEPSIZE = 0.02; - - - this.angles; - - this.initThetas = function(N) { - this.thetas = new Array(N); - for (let i=0; i this.ndim){ - for(let i=this.N; irow.slice(0,newNdim)); - this.matrix = utils.orthogonalize(this.matrix); - } - this.ndim = newNdim; - this.N = this.ndim * this.ndim; - return this.matrix; - }; - - this.getMatrix = function(dt) { - if (dt !== undefined) { - if (this.angles === undefined) { - // torus method - // this.angles = this.thetas.map(theta=>0); - // - // another implementation similar to torus method - this.angles = this.thetas; - this.matrix = math.identity(this.ndim)._data; - } else { - // torus method - // this.angles = this.angles.map( - // (a,i) => a+dt*this.STEPSIZE*this.thetas[i]); - // - // another implementation similar to torus method - this.angles = this.thetas.map( (theta) => - theta * dt * this.STEPSIZE ); - } - // torus method - // this.matrix = math.identity(this.ndim)._data; - let k = -1; - for (let i=0; id.slice()); - let columnI = matrix.map((d)=>d[i]); - let columnJ = matrix.map((d)=>d[j]); - for (let rowIndex=0; rowIndex (Math.random() + 0.5) * Math.PI); +} - this.setNdim(this.ndim); - this.matrix = this.getMatrix(0); - if(init_matrix !== undefined){ - this.setMatrix(init_matrix); - } +export class GrandTour { + STEPSIZE = 0.02; + matrix: number[][]; + + STEPSIZE_PREV?: number; + angles?: number[]; + thetas: number[]; + #ndim: number; + + constructor(ndim: number, init_matrix?: number[][]) { + this.#ndim = ndim; + this.thetas = initThetas(this.N); + this.matrix = this.getMatrix(0); + if (init_matrix) { + this.setMatrix(init_matrix); + } + } + + get N() { + return this.ndim * this.ndim; + } + + get ndim() { + return this.#ndim; + } + + set ndim(newNdim: number) { + if (newNdim > this.#ndim) { + for (let i = this.N; i < newNdim * newNdim; i++) { + this.thetas[i] = (Math.random() - 0.5) * 2 * Math.PI; + } + this.matrix = utils.embed( + this.matrix, + (math.identity(newNdim) as math.Matrix).toArray() as number[][], + ); + } else if (newNdim < this.ndim) { + this.matrix = this.matrix.slice(0, newNdim).map((row) => + row.slice(0, newNdim) + ); + this.matrix = utils.orthogonalize(this.matrix); + } + this.#ndim = newNdim; + } + + getMatrix(dt?: number) { + if (dt !== undefined) { + if (this.angles === undefined) { + // torus method + // this.angles = this.thetas.map(theta=>0); + // + // another implementation similar to torus method + this.angles = this.thetas; + let mat = math.identity(this.ndim) as math.Matrix; + this.matrix = mat.toArray() as number[][]; + } else { + // torus method + // this.angles = this.angles.map( + // (a,i) => a+dt*this.STEPSIZE*this.thetas[i]); + // + // another implementation similar to torus method + this.angles = this.thetas.map((theta) => theta * dt * this.STEPSIZE); + } + // torus method + // this.matrix = math.identity(this.ndim)._data; + let k = -1; + for (let i = 0; i < this.ndim; i++) { + for (let j = 0; j < this.ndim; j++) { + if (i !== j && (true || i <= 3 || j <= 3)) { + k++; + this.matrix = this.multiplyRotationMatrix( + this.matrix, + i, + j, + this.angles[k], + ); + } + } + } + } + return this.matrix; + } + + setMatrix(m: number[][]) { + this.matrix = numeric.clone(m); + } + + getRotationMatrix(dim0: number, dim1: number, theta: number) { + let m = math.identity(this.ndim) as math.Matrix; + let res = m.toArray() as number[][]; + res[dim0][dim0] = Math.cos(theta); + res[dim0][dim1] = Math.sin(theta); + res[dim1][dim0] = -Math.sin(theta); + res[dim1][dim1] = Math.cos(theta); + return res; + } + + multiplyRotationMatrix( + matrix: number[][], + i: number, + j: number, + theta: number, + ) { + if (theta == 0) { + return matrix; + } + let sin = Math.sin(theta); + let cos = Math.cos(theta); + // var res = matrix.map(d=>d.slice()); + let columnI = matrix.map((d) => d[i]); + let columnJ = matrix.map((d) => d[j]); + for (let rowIndex = 0; rowIndex < matrix.length; rowIndex++) { + matrix[rowIndex][i] = columnI[rowIndex] * cos + + columnJ[rowIndex] * (-sin); + matrix[rowIndex][j] = columnI[rowIndex] * sin + columnJ[rowIndex] * cos; + } + return matrix; + } + + get3dRotationMatrix(t: number) { + let theta = 0.0 * t; + let cos = Math.cos(theta); + let sin = Math.sin(theta); + return [ + [cos, 0, sin] as const, + [0, 1, 0] as const, + [-sin, 0, cos] as const, + ] as const; + } + + project(data: number[][], dt?: number, view?: number[][]) { + let matrix = this.getMatrix(dt); + matrix = math.transpose(matrix); + matrix = matrix.slice(0, 3); + matrix = math.transpose(matrix); + if (view !== undefined) { + matrix = math.multiply(view, matrix) as number[][]; + } + return math.multiply(data, matrix.slice(0, data[0].length)) as number[][]; + } } diff --git a/src/TeaserOverlay.ts b/src/TeaserOverlay.ts index e972b57..00c656d 100644 --- a/src/TeaserOverlay.ts +++ b/src/TeaserOverlay.ts @@ -3,305 +3,244 @@ import * as d3 from "d3"; import * as math from "mathjs"; import * as utils from "./utils"; -export function TeaserOverlay(renderer, kwargs) { - let canvas = renderer.gl.canvas; - let width = canvas.clientWidth; - let height = canvas.clientHeight; - this.selectedClasses = new Set(); - this.renderer = renderer; - Object.assign(this, kwargs); - - let that = this; - let figure = d3.select("d-figure." + renderer.gl.canvas.id); - this.figure = figure; - - this.getDataset = function () { - return this.renderer.fixed_dataset || utils.getDataset(); +import type { TeaserRenderer } from "./TeaserRenderer"; +import { ColorRGB, Scale } from "./types"; + +export interface TeaserOverlayOptions {} + +export class TeaserOverlay { + selectedClasses: Set; + figure: d3.Selection; + epochSlider: d3.Selection; + playButton: d3.Selection; + fullScreenButton: d3.Selection; + grandtourButton: d3.Selection; + svg: d3.Selection & { + sc?: (color: number) => string; + anchors?: d3.Selection< + SVGCircleElement, + [number, number], + SVGSVGElement, + unknown + >; + drag?: d3.DragBehavior; }; + epochIndicator: d3.Selection; + + anchorRadius?: number; + annotate?: (renderer: TeaserRenderer) => void; + + legendBox?: d3.Selection; + legendTitle?: d3.Selection; + legendTitleBg?: d3.Selection; + legendMark?: d3.Selection< + SVGCircleElement, + d3.RGBColor, + SVGSVGElement, + unknown + >; + legendText?: d3.Selection; + + legend_sx?: Scale; + legend_sy?: Scale; + + constructor( + public renderer: TeaserRenderer, + _opts: Partial = {}, + ) { + this.selectedClasses = new Set(); + this.renderer = renderer; + + let figure = d3.select("d-figure." + renderer.gl.canvas.id); + this.figure = figure; - this.epochSlider = figure - .insert("input", ":first-child") - .attr("type", "range") - .attr("class", "slider epochSlider") - .attr("min", renderer.epochs[0]) - .attr("max", renderer.epochs[renderer.epochs.length - 1]) - .attr("value", renderer.epochIndex) - .on("input", function () { - let value = d3.select(this).property("value"); - renderer.shouldAutoNextEpoch = false; - renderer.setEpochIndex(parseInt(value)); - // renderer.render(0); - that.playButton.attr("class", "tooltip play-button fa fa-play"); - that.playButton.select("span").text("Play training"); - }); - - //special treatment when showing only one peoch - if (renderer.epochs.length <= 1) { - this.epochSlider.style("display", "none"); - } + let self = this; + this.epochSlider = figure + .insert("input", ":first-child") + .attr("type", "range") + .attr("class", "slider epochSlider") + .attr("min", renderer.epochs[0]) + .attr("max", renderer.epochs[renderer.epochs.length - 1]) + .attr("value", renderer.epochIndex) + .on("input", function () { + let value = d3.select(this).property("value"); + renderer.shouldAutoNextEpoch = false; + renderer.setEpochIndex(parseInt(value)); + // renderer.render(0); + self.playButton.attr("class", "tooltip play-button fa fa-play"); + self.playButton.select("span").text("Play training"); + }); - this.playButton = figure - .insert("i", ":first-child") - .attr( - "class", - "play-button tooltip fa " + - (renderer.shouldAutoNextEpoch ? "fa-pause" : "fa-play"), - ) - .on("mouseover", function () { - d3.select(this).style("opacity", 1); - }) - .on("mouseout", function () { - d3.select(this).style("opacity", 0.7); - }) - .on("click", function () { - renderer.shouldAutoNextEpoch = !renderer.shouldAutoNextEpoch; - if (renderer.shouldAutoNextEpoch) { - d3.select(this).attr("class", "tooltip play-button fa fa-pause"); - d3.select(this).select("span") - .text("Pause training"); - } else { - d3.select(this).attr("class", "tooltip play-button fa fa-play"); - d3.select(this).select("span") - .text("Play training"); - } - }); - this.playButton.append("span") - .attr("class", "tooltipText") - .text("Pause training"); + this.playButton = figure + .insert("i", ":first-child") + .attr( + "class", + "play-button tooltip fa " + + (renderer.shouldAutoNextEpoch ? "fa-pause" : "fa-play"), + ) + .on("mouseover", function () { + d3.select(this).style("opacity", 1); + }) + .on("mouseout", function () { + d3.select(this).style("opacity", 0.7); + }) + .on("click", function () { + renderer.shouldAutoNextEpoch = !renderer.shouldAutoNextEpoch; + if (renderer.shouldAutoNextEpoch) { + d3.select(this).attr("class", "tooltip play-button fa fa-pause"); + d3.select(this).select("span").text("Pause training"); + } else { + d3.select(this).attr("class", "tooltip play-button fa fa-play"); + d3.select(this).select("span").text("Play training"); + } + }); - if (renderer.epochs.length <= 1) { - this.playButton.style("display", "none"); - } + this.playButton.append("span") + .attr("class", "tooltipText") + .text("Pause training"); - this.fullScreenButton = figure - .insert("i", ":first-child") - .attr("class", "tooltip teaser-fullscreenButton fas fa-expand-arrows-alt") - .on("mouseover", function () { - d3.select(this).style("opacity", 0.7); - }) - .on("mouseout", function () { - if (renderer.isFullScreen) { - d3.select(this).style("opacity", 0.7); - } else { - d3.select(this).style("opacity", 0.3); - } - }) - .on("click", function () { - renderer.setFullScreen(!renderer.isFullScreen); - // that.resize(); + if (renderer.epochs.length <= 1) { + this.playButton.style("display", "none"); + } - if (renderer.isFullScreen) { + this.fullScreenButton = figure + .insert("i", ":first-child") + .attr("class", "tooltip teaser-fullscreenButton fas fa-expand-arrows-alt") + .on("mouseover", function () { d3.select(this).style("opacity", 0.7); - } else { - d3.select(this).style("opacity", 0.3); - } - }); + }) + .on("mouseout", function () { + d3.select(this).style("opacity", renderer.isFullScreen ? 0.7 : 0.3); + }) + .on("click", function () { + renderer.setFullScreen(!renderer.isFullScreen); + d3.select(this).style("opacity", renderer.isFullScreen ? 0.7 : 0.3); + }); - this.fullScreenButton.append("span") - .attr("class", "tooltipTextBottom") - .text("Toggle fullscreen"); - - this.grandtourButton = figure - .insert("i", ":first-child") - .attr("class", "teaser-grandtourButton tooltip fas fa-globe-americas") - .attr("width", 32) - .attr("height", 32) - .style("opacity", renderer.shouldPlayGrandTour ? 0.7 : 0.3) - .on("mouseover", function () { - d3.select(this).style("opacity", 0.7); - }) - .on("mouseout", function () { - if (renderer.shouldPlayGrandTour) { + this.fullScreenButton.append("span") + .attr("class", "tooltipTextBottom") + .text("Toggle fullscreen"); + + this.grandtourButton = figure + .insert("i", ":first-child") + .attr("class", "teaser-grandtourButton tooltip fas fa-globe-americas") + .attr("width", 32) + .attr("height", 32) + .style("opacity", renderer.shouldPlayGrandTour ? 0.7 : 0.3) + .on("mouseover", function () { d3.select(this).style("opacity", 0.7); - } else { - d3.select(this).style("opacity", 0.3); - } - }); + }) + .on("mouseout", function () { + d3.select(this).style( + "opacity", + renderer.shouldPlayGrandTour ? 0.7 : 0.3, + ); + }); - this.grandtourButton.append("span") - .attr("class", "tooltipText") - .text("Pause Grand Tour"); - - this.grandtourButton - .on("click", function () { - renderer.shouldPlayGrandTour = !renderer.shouldPlayGrandTour; - renderer.shouldCentralizeOrigin = renderer.shouldPlayGrandTour; - - renderer.isScaleInTransition = true; - renderer.setScaleFactor(1.0); - renderer.scaleTransitionProgress = renderer.shouldCentralizeOrigin - ? Math.min(1, renderer.scaleTransitionProgress) - : Math.max(0, renderer.scaleTransitionProgress); - - let dt = 0.03; - renderer.scaleTransitionDelta = renderer.shouldCentralizeOrigin - ? -dt - : dt; - - if (renderer.shouldPlayGrandTour) { - d3.select(this).select("span") - .text("Pause Grand Tour"); - d3.select(this).style("opacity", 0.7); - } else { - d3.select(this).select("span") - .text("Play Grand Tour"); - d3.select(this).style("opacity", 0.3); - } - }); + this.grandtourButton.append("span") + .attr("class", "tooltipText") + .text("Pause Grand Tour"); + + this.grandtourButton + .on("click", function () { + renderer.shouldPlayGrandTour = !renderer.shouldPlayGrandTour; + renderer.shouldCentralizeOrigin = renderer.shouldPlayGrandTour; + + renderer.isScaleInTransition = true; + renderer.setScaleFactor(1.0); + renderer.scaleTransitionProgress = renderer.shouldCentralizeOrigin + ? Math.min(1, renderer.scaleTransitionProgress) + : Math.max(0, renderer.scaleTransitionProgress); + + let dt = 0.03; + renderer.scaleTransitionDelta = renderer.shouldCentralizeOrigin + ? -dt + : dt; + + if (renderer.shouldPlayGrandTour) { + d3.select(this).select("span").text("Pause Grand Tour"); + d3.select(this).style("opacity", 0.7); + } else { + d3.select(this).select("span").text("Play Grand Tour"); + d3.select(this).style("opacity", 0.3); + } + }); - this.svg = figure - .insert("svg", ":first-child") - .attr("class", "overlay") - .attr("width", width) - .attr("height", height) - .on("dblclick", function () { - // renderer.shouldPlayGrandTour = !renderer.shouldPlayGrandTour; - }) - .on("mousemove", () => { - //handle unsuccessful onscreen event - if (renderer.shouldRender == false) { - renderer.shouldRender = true; - if (renderer.animId === null) { - renderer.play(); + this.svg = figure + .insert("svg", ":first-child") + .attr("class", "overlay") + .attr("width", this.width) + .attr("height", this.height) + .on("dblclick", function () { + // renderer.shouldPlayGrandTour = !renderer.shouldPlayGrandTour; + }) + .on("mousemove", () => { + //handle unsuccessful onscreen event + if (renderer.shouldRender == false) { + renderer.shouldRender = true; + if (renderer.animId === null) { + renderer.play(); + } } - } - }); + }); - this.epochIndicator = this.svg.append("text") - .attr("id", "epochIndicator") - .attr("text-anchor", "middle") - .text(`Epoch: ${renderer.epochIndex}/99`); - - this.controlOptionGroup = figure - .insert("div", ":first-child"); - - // this.datasetOption = this.controlOptionGroup - // .insert('div', ':first-child') - // .attr('class', 'form-group datasetOption'); - // this.datasetOption.append('label') - // .text('Dataset: '); - // this.datasetSelection = this.datasetOption.append('select') - // .attr('class', 'datasetSelection') - // .on('change', function() { - // let dataset = d3.select(this).property('value'); - // utils.setDataset(dataset) - // }); - // this.datasetSelection.selectAll('option') - // .data([ - // {value:'mnist',text:'MNIST'}, - // {value:'fashion-mnist',text:'fashion-MNIST'}, - // {value:'cifar10',text:'CIFAR-10'}]) - // .enter() - // .append('option') - // .text(d=>d.text) - // .attr('value', d=>d.value) - // .property('selected', d=>{ - // //show default selection - // return d.value == this.getDataset(); - // }); - // - this.zoomSliderDiv = this.controlOptionGroup - .insert("div", ":first-child") - .attr("class", "form-group zoomSliderDiv"); - this.zoomLabel = this.zoomSliderDiv - .append("label") - .text("Zoom: "); - this.zoomSlider = this.zoomLabel - .append("input") - .attr("type", "range") - .attr("class", "slider zoomSlider") - .attr("min", 0.5) - .attr("max", 2.0) - .attr("value", this.renderer.scaleFactor) - .attr("step", 0.01) - .on("input", function () { - let value = +d3.select(this).property("value"); - renderer.setScaleFactor(value); - }); + this.epochIndicator = this.svg.append("text") + .attr("id", "epochIndicator") + .attr("text-anchor", "middle") + .text(`Epoch: ${renderer.epochIndex}/99`); - this.modeOption = this.controlOptionGroup - .insert("div", ":first-child") - .attr("class", "form-group modeOption"); - this.modeLabel = this.modeOption.append("label") - .text("Instances as: "); - let select = this.modeLabel.append("select") - .on("change", function () { - let mode = d3.select(this).property("value"); - renderer.setMode(mode); - that.updateArchorRadius(mode); - }); - select.selectAll("option") - .data(["point", "image"]) - .enter() - .append("option") - .text((d) => d) - .attr("selected", (d) => { - return (d == this.renderer.mode) ? "selected" : null; - }); + //special treatment when showing only one peoch + if (renderer.epochs.length <= 1) { + this.epochSlider.style("display", "none"); + this.epochIndicator.style("display", "none"); + } + } - this.datasetOption = this.controlOptionGroup - .insert("div", ":first-child") - .attr("class", "form-group datasetOption"); - this.datasetLabel = this.datasetOption - .append("label") - .text("Dataset: "); - this.datasetSelection = this.datasetLabel - .append("select") - .on("change", function () { - let dataset = d3.select(this).property("value"); - utils.setDataset(dataset); - }); - this.datasetSelection.selectAll("option") - .data([ - { value: "mnist", text: "MNIST" }, - { value: "fashion-mnist", text: "fashion-MNIST" }, - { value: "cifar10", text: "CIFAR-10" }, - ]) - .enter() - .append("option") - .text((d) => d.text) - .attr("value", (d) => d.value) - .property("selected", (d) => { - return d.value == this.getDataset(); - }); + get canvas() { + return this.renderer.gl.canvas; + } + + get width() { + return this.canvas.clientWidth; + } + + get height() { + return this.canvas.clientHeight; + } + + getDataset() { + return utils.getDataset() as keyof typeof utils.legendTitle; + } - this.banner = figure.selectAll(".banner") - .data([0]) - .enter() - .append("div") - .attr("class", "banner"); - this.banner = figure.selectAll(".banner"); - this.bannerText = this.banner - .selectAll(".bannerText") - .data([0]) - .enter() - .append("p") - .attr("class", "bannerText"); - this.bannerText = this.banner.selectAll(".bannerText"); - - - this.updateArchorRadius = function (mode) { + updateArchorRadius(mode: string) { if (mode == "point") { - this.archorRadius = utils.clamp(7, 10, Math.min(width, height) / 50); + this.anchorRadius = utils.clamp( + 7, + 10, + Math.min(this.width, this.height) / 50, + ); } else { - this.archorRadius = utils.clamp(7, 15, Math.min(width, height) / 30); + this.anchorRadius = utils.clamp( + 7, + 15, + Math.min(this.width, this.height) / 30, + ); } this.svg.selectAll(".anchor") - .attr("r", this.archorRadius); - }; + .attr("r", this.anchorRadius); + } - this.resize = function () { - width = canvas.clientWidth; - height = canvas.clientHeight; + resize() { + let width = this.renderer.gl.canvas.clientWidth; + let height = this.renderer.gl.canvas.clientHeight; this.svg.attr("width", width); this.svg.attr("height", height); - this.initLegendScale(); - this.updateArchorRadius(renderer.mode); + this.updateArchorRadius(this.renderer.mode); this.repositionAll(); - }; + } - this.repositionAll = function () { + repositionAll() { let width = +this.svg.attr("width"); let height = +this.svg.attr("height"); @@ -313,24 +252,27 @@ export function TeaserOverlay(renderer, kwargs) { .attr("x", sliderMiddle) .attr("y", height - 35); - if (renderer.epochs.length <= 1) { + if (this.renderer.epochs.length <= 1) { this.epochIndicator .attr("x", width / 2 - 10) .attr("y", 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.0) + 2.5 * r) - .attr("cy", (c, i) => this.legend_sy(i + 0.5)) + ?.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", (l, i) => this.legend_sy(i + 0.5)); + ?.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("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( @@ -345,78 +287,70 @@ export function TeaserOverlay(renderer, kwargs) { .attr("y", this.legend_sy(-1)) .text(utils.legendTitle[this.getDataset()] || ""); - let rectData = this.legendTitle.node().getBBox(); + let rectData = this.legendTitle.node()!.getBBox(); let padding = 2; - this.legendTitleBg + 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); } - if (this.banner) { - this.banner.remove(); - } - }; + } - this.init = function () { + init() { let labels = utils.getLabelNames(false, this.getDataset()); - this.initLegend( - utils.baseColors.slice(0, labels.length), - labels, - ); + let colors = utils.baseColors.slice(0, labels.length); + this.initLegend(colors, labels); this.resize(); this.initAxisHandle(); if (this.annotate !== undefined) { this.annotate(this.renderer); } - // if(this.banner){ - // this.banner.remove(); - // } - }; + } - this.initAxisHandle = function () { + initAxisHandle() { this.svg.sc = d3.interpolateGreys; this.drawAxes(); - }; + } - this.drawAxes = function () { + drawAxes() { let svg = this.svg; - let ndim = renderer.dataObj.ndim || 10; - let coordinates = math.zeros(ndim, ndim)._data; + let ndim = this.renderer.dataObj?.ndim || 10; + let renderer = this.renderer; + + let mat = math.zeros(ndim, ndim); + let coordinates = (mat as unknown as { _data: [number, number][] })._data; - svg.selectAll(".anchor") + let anchors = svg.selectAll(".anchor") .data(coordinates) .enter() .append("circle") .attr("class", "anchor") .attr("opacity", 0.2); - let anchors = svg.selectAll(".anchor") - .attr("cx", (d) => renderer.sx(d[0])) - .attr("cy", (d) => renderer.sy(d[1])) - .attr("r", this.archorRadius) - .attr("fill", (_, i) => d3.rgb(...utils.baseColors[i]).darker()) + anchors + .attr("cx", ([x, _]) => this.renderer.sx(x)) + .attr("cy", ([_, y]) => this.renderer.sy(y)) + .attr("r", this.anchorRadius!) .attr("stroke", () => "white") .style("cursor", "pointer"); svg.anchors = anchors; let self = this; - svg.drag = d3.drag() + let drag = d3.drag() .on("start", () => { renderer.shouldPlayGrandTourPrev = renderer.shouldPlayGrandTour; renderer.shouldPlayGrandTour = false; renderer.isDragging = true; }) .on("drag", function (event) { + if (!renderer.gt) return; let dx = renderer.sx.invert(event.dx) - renderer.sx.invert(0); let dy = renderer.sy.invert(event.dy) - renderer.sy.invert(0); let matrix = renderer.gt.getMatrix(); - - const e = svg.anchors.nodes(); - const i = e.indexOf(this); - + let i = anchors.nodes().indexOf(this); matrix[i][0] += dx; matrix[i][1] += dy; matrix = utils.orthogonalize(matrix, i); @@ -425,45 +359,36 @@ export function TeaserOverlay(renderer, kwargs) { }) .on("end", function () { renderer.isDragging = false; - renderer.shouldPlayGrandTour = renderer.shouldPlayGrandTourPrev; - renderer.shouldPlayGrandTourPrev = null; + renderer.shouldPlayGrandTour = renderer.shouldPlayGrandTourPrev ?? + false; + renderer.shouldPlayGrandTourPrev = undefined; }); anchors .on("mouseover", () => { + if (!renderer.gt) return; renderer.gt.STEPSIZE_PREV = renderer.gt.STEPSIZE; renderer.gt.STEPSIZE = renderer.gt.STEPSIZE * 0.2; }) .on("mouseout", () => { + if (renderer.gt?.STEPSIZE_PREV === undefined) return; renderer.gt.STEPSIZE = renderer.gt.STEPSIZE_PREV; delete renderer.gt.STEPSIZE_PREV; }) - .call(svg.drag); - }; - - this.redrawAxis = function () { - let svg = this.svg; - - if (renderer.gt !== undefined) { - let handlePos = renderer.gt.project( - math.identity(renderer.dataObj.ndim)._data, - ); - - svg.selectAll(".anchor") - .attr("cx", (_, i) => renderer.sx(handlePos[i][0])) - .attr("cy", (_, i) => renderer.sy(handlePos[i][1])); - } - - // svg.anchors.filter((_,j)=>renderer.gt.fixedAxes[j].isFixed) - // .attr('fill', 'red') - // .attr('opacity', 0.5); + .call(drag); + } - // svg.anchors.filter((_,j)=>!renderer.gt.fixedAxes[j].isFixed) - // .attr('fill', 'black') - // .attr('opacity', 0.1); - }; + redrawAxis() { + if (this.renderer.gt === undefined) return; + let m = math.identity(this.renderer.dataObj?.ndim ?? 10); + let points = (m as unknown as { _data: number[][] })._data; + let handlePos = this.renderer.gt.project(points); + this.svg.selectAll(".anchor") + .attr("cx", (_, i) => this.renderer.sx(handlePos[i][0])) + .attr("cy", (_, i) => this.renderer.sy(handlePos[i][1])); + } - this.initLegendScale = function () { + initLegendScale() { let width = +this.svg.attr("width"); let marginTop = 20; let padding = 8; @@ -487,35 +412,40 @@ export function TeaserOverlay(renderer, kwargs) { marginTop + 170, marginTop + 170 + padding, ]); - }; + } - this.initLegend = function (colors, labels) { + initLegend(colors: ColorRGB[], labels: string[]) { this.initLegendScale(); + let clearColor = d3.rgb( + ...utils.CLEAR_COLOR.map((d) => d * 255) as ColorRGB, + ); + if (this.legendBox === undefined) { this.legendBox = this.svg.selectAll(".legendBox") .data([0]) .enter() .append("rect") .attr("class", "legendBox") - .attr("fill", d3.rgb(...utils.CLEAR_COLOR.map((d) => d * 255))) + .attr("fill", clearColor.formatRgb()) .attr("stroke", "#c1c1c1") .attr("stroke-width", 1); } + let legendTitleText = + utils.legendTitle[this.getDataset() as keyof typeof utils.legendTitle]; if ( - this.legendTitle === undefined && - utils.legendTitle[this.getDataset()] !== undefined + this.legendTitle === undefined && legendTitleText !== undefined ) { this.legendTitleBg = this.svg.selectAll(".legendTitleBg") .data([0]) .enter() .append("rect") .attr("class", "legendTitleBg") - .attr("fill", d3.rgb(...utils.CLEAR_COLOR.map((d) => d * 255))); + .attr("fill", clearColor.formatRgb()); this.legendTitle = this.svg.selectAll(".legendTitle") - .data([utils.legendTitle[this.getDataset()]]) + .data([legendTitleText]) .enter() .append("text") .attr("class", "legendTitle") @@ -524,110 +454,121 @@ export function TeaserOverlay(renderer, kwargs) { .text((d) => d); } - this.svg.selectAll(".legendMark") - .data(colors) + let self = this; + + this.legendMark = this.svg.selectAll(".legendMark") + .data(colors.map((c) => d3.rgb(...c))) .enter() .append("circle") - .attr("class", "legendMark") - .attr("fill", (c, i) => "rgb(" + c + ")") - .on("mouseover", (_, i) => { - let classes = new Set(this.selectedClasses); + .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); } - this.onSelectLegend(classes); + self.onSelectLegend(classes); }) .on("mouseout", () => this.restoreAlpha()) - .on("click", (_, i) => { - if (this.selectedClasses.has(i)) { - this.selectedClasses.delete(i); + .on("click", function () { + const e = self.legendMark!.nodes(); + const i = e.indexOf(this); + + if (self.selectedClasses.has(i)) { + self.selectedClasses.delete(i); } else { - this.selectedClasses.add(i); + self.selectedClasses.add(i); } - this.onSelectLegend(this.selectedClasses); - if (this.selectedClasses.size == renderer.dataObj.ndim) { - this.selectedClasses = new Set(); + self.onSelectLegend(self.selectedClasses); + if (self.selectedClasses.size == self.renderer.dataObj?.ndim) { + self.selectedClasses = new Set(); } }); - this.legendMark = this.svg.selectAll(".legendMark"); - this.svg.selectAll(".legendText") + + this.legendText = this.svg.selectAll(".legendText") .data(labels) .enter() .append("text") .attr("class", "legendText"); - this.legendText = this.svg.selectAll(".legendText") + this.legendText .attr("alignment-baseline", "middle") .attr("fill", "#333") - .text((l) => l) - .on("mouseover", (_, i) => { - let classes = new Set(this.selectedClasses); + .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); } - this.onSelectLegend(classes); + self.onSelectLegend(classes); }) .on("mouseout", () => this.restoreAlpha()) - .on("click", (_, i) => { - if (this.selectedClasses.has(i)) { - this.selectedClasses.delete(i); + .on("click", function () { + const e = self.legendText!.nodes(); + const i = e.indexOf(this); + + if (self.selectedClasses.has(i)) { + self.selectedClasses.delete(i); } else { - this.selectedClasses.add(i); + self.selectedClasses.add(i); } - this.onSelectLegend(this.selectedClasses); - if (this.selectedClasses.size == renderer.dataObj.ndim) { - this.selectedClasses = new Set(); + 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; - this.onSelectLegend = function (labelClasses) { - if (typeof (labelClasses) === "number") { + if (typeof labelClasses === "number") { labelClasses = [labelClasses]; } - labelClasses = new Set(labelClasses); + let labelSet = new Set(labelClasses); - for (let i = 0; i < renderer.dataObj.npoint; i++) { - if (labelClasses.has(renderer.dataObj.labels[i])) { - renderer.dataObj.alphas[i] = 255; + 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 { - renderer.dataObj.alphas[i] = 0; + this.renderer.dataObj.alphas[i] = 0; } } - this.svg.selectAll(".legendMark") - .attr("opacity", (d, j) => { - if (!labelClasses.has(j)) { - return 0.1; - } else { - return 1.0; - } - }); - // renderer.render(0); - }; - this.restoreAlpha = function () { + this.legendMark?.attr("opacity", (_, j) => labelSet.has(j) ? 1.0 : 0.1); + } + + restoreAlpha() { + if (!this.renderer.dataObj) return; let labelClasses = new Set(this.selectedClasses); if (labelClasses.size == 0) { - for (let i = 0; i < renderer.dataObj.npoint; i++) { - renderer.dataObj.alphas[i] = 255; + for (let i = 0; i < this.renderer.dataObj.npoint; i++) { + this.renderer.dataObj.alphas[i] = 255; } } else { - for (let i = 0; i < renderer.dataObj.npoint; i++) { - if (labelClasses.has(renderer.dataObj.labels[i])) { - renderer.dataObj.alphas[i] = 255; + for (let i = 0; i < this.renderer.dataObj.npoint; i++) { + if (labelClasses.has(this.renderer.dataObj.labels[i])) { + this.renderer.dataObj.alphas[i] = 255; } else { - renderer.dataObj.alphas[i] = 0; + this.renderer.dataObj.alphas[i] = 0; } } } - this.svg.selectAll(".legendMark") - .attr("opacity", (d, i) => { - if (labelClasses.size == 0) { - return 1.0; - } else { - return labelClasses.has(i) ? 1.0 : 0.1; - } - }); - }; + this.legendMark?.attr("opacity", (_, i) => { + if (labelClasses.size == 0) { + return 1.0; + } else { + return labelClasses.has(i) ? 1.0 : 0.1; + } + }); + } } diff --git a/src/TeaserRenderer.ts b/src/TeaserRenderer.ts index e5cfa41..55c29ba 100644 --- a/src/TeaserRenderer.ts +++ b/src/TeaserRenderer.ts @@ -1,111 +1,153 @@ -// @ts-check import * as arrow from "apache-arrow"; import * as d3 from "d3"; import * as math from "mathjs"; import * as utils from "./utils"; import { GrandTour } from "./GrandTour"; -import { TeaserOverlay } from "./TeaserOverlay"; +import { TeaserOverlay, TeaserOverlayOptions } from "./TeaserOverlay"; + +import type { ColorRGBA, Renderer } from "./types"; + +interface Data { + labels: number[]; + dataTensor: number[][][]; + dmax: number; + ndim: number; + npoint: number; + nepoch: number; + alphas: number[]; + points?: number[][]; + colors?: ColorRGBA[]; +} interface TeaserRendererOptions { - epochs: number; - shouldAutoNextEpoch: boolean; + epochs: number[]; + epochIndex: number; + shouldAutoNextEpoch: boolean; + shouldPlayGrandTour: boolean; + isFullScreen: boolean; + overlayKwargs: TeaserOverlayOptions; + pointSize: number; } -export function TeaserRenderer(gl: WebGLRenderingContext, program: WebGLProgram, kwargs: TeaserRendererOptions) { - this.gl = gl; - this.program = program; - this.id = gl.canvas.id; - - this.framesPerTransition = 30; - this.framesPerEpoch = 60; - this.scaleTransitionProgress = 0; - - Object.assign(this, kwargs); - - this.dataObj = {}; - this.mode = this.mode || "point"; // default point mode, or overwritten by kwargs - this.epochIndex = this.epochIndex || this.epochs[0]; - this.colorFactor = utils.COLOR_FACTOR; - this.isFullScreen = false; - if (this.shouldPlayGrandTour === undefined) { - this.shouldPlayGrandTour = true; +export class TeaserRenderer implements Renderer { + framesPerTransition = 30; + framesPerEpoch = 60; + scaleTransitionProgress = 0; + scaleTransitionDelta = 0; + colorFactor = utils.COLOR_FACTOR; + isFullScreen = false; + isDataReady = false; + shouldRender = true; + scaleFactor = 1.0; + s = 1.0; + mode: "point" = "point"; + + id: string; + epochs: number[]; + epochIndex: number; + shouldAutoNextEpoch: boolean; + shouldPlayGrandTour: boolean; + pointSize: number; + pointSize0: number; + overlay: TeaserOverlay; + sx_span: d3.ScaleLinear; + sy_span: d3.ScaleLinear; + sz_span: d3.ScaleLinear; + sx_center: d3.ScaleLinear; + sy_center: d3.ScaleLinear; + sz_center: d3.ScaleLinear; + sx: d3.ScaleLinear; + sy: d3.ScaleLinear; + sz?: d3.ScaleLinear; + + dataObj?: Data; + shouldRecalculateColorRect?: boolean; + isPlaying?: boolean; + animId?: number; + isScaleInTransition?: boolean; + isDragging?: boolean; + shouldPlayGrandTourPrev?: boolean; + + colorBuffer?: WebGLBuffer; + colorLoc?: number; + positionBuffer?: WebGLBuffer; + positionLoc?: number; + pointSizeLoc?: WebGLUniformLocation; + isDrawingAxisLoc?: WebGLUniformLocation; + canvasWidthLoc?: WebGLUniformLocation; + canvasHeightLoc?: WebGLUniformLocation; + modeLoc?: WebGLUniformLocation; + colorFactorLoc?: WebGLUniformLocation; + gt?: GrandTour; + shouldCentralizeOrigin?: boolean; + + constructor( + public gl: WebGLRenderingContext, + public program: WebGLProgram, + opts: Partial = {}, + ) { + this.id = gl.canvas.id; + this.epochs = opts.epochs ?? [0]; + 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 TeaserOverlay(this, opts.overlayKwargs); + + this.sx_span = d3.scaleLinear(); + this.sy_span = d3.scaleLinear(); + this.sz_span = d3.scaleLinear(); + this.sx_center = d3.scaleLinear(); + this.sy_center = d3.scaleLinear(); + this.sz_center = d3.scaleLinear(); + this.sx = this.sx_center; + this.sy = this.sy_center; } - if (!this.hasOwnProperty("shouldAutoNextEpoch")) { - this.shouldAutoNextEpoch = true; - } - this.pointSize0 = this.pointSize || 6.0; - - this.overlay = new TeaserOverlay(this, this.overlayKwargs); - this.sx_span = d3.scaleLinear(); - this.sy_span = d3.scaleLinear(); - this.sz_span = d3.scaleLinear(); - this.sx_center = d3.scaleLinear(); - this.sy_center = d3.scaleLinear(); - this.sz_center = d3.scaleLinear(); - this.sx = this.sx_center; - this.sy = this.sy_center; - this.scaleFactor = 1.0; - - this.setScaleFactor = function (s) { + setScaleFactor(s: number) { this.scaleFactor = s; - }; + } - this.initData = async function (buffer, url) { + async initData(buffer: ArrayBuffer) { let table = arrow.tableFromIPC(buffer); - let ndim = 5; let nepoch = 1; - let dsample = 10; let labels = []; let arr = []; - let fields = d3.range(ndim).map((i) => "E" + i); - let mapping = Object.fromEntries( + let fields = d3.range(ndim).map((i) => "E" + i); + let labelMapping = Object.fromEntries( ["A0", "A1", "B0", "B1", "B2"].map((name, i) => [name, i]), ); - for (let row of utils.iterN(table, dsample)) { - labels.push(mapping[row["name"]]); - for (let field of fields) { - arr.push(row[field]); - } + for (let row of utils.iterN(table, 10)) { + labels.push(labelMapping[row.name]); + for (let field of fields) arr.push(row[field]); } let npoint = labels.length; + let shape: [number, number, number] = [nepoch, npoint, ndim]; + let dataTensor = utils.reshape(new Float32Array(arr), shape); - arr = new Float32Array(arr); - - this.dataObj.labels = labels; this.shouldRecalculateColorRect = true; - this.dataObj.dataTensor = utils.reshape(arr, [nepoch, npoint, ndim]); + this.isDataReady = true; - if ( - this.dataObj.dataTensor !== undefined && - this.dataObj.labels !== undefined - ) { - this.isDataReady = true; - let { dataTensor } = this.dataObj; - // this.dataObj.trajectoryLength = 5; - this.dataObj.dmax = 1.05 * math.max( + this.dataObj = { + labels, + dataTensor, + dmax: 1.05 * math.max( math.abs(dataTensor[dataTensor.length - 1]), - ); - this.dataObj.ndim = dataTensor[0][0].length; - this.dataObj.npoint = dataTensor[0].length; - this.dataObj.nepoch = dataTensor.length; - if (this.dataObj.alphas === undefined) { - this.dataObj.alphas = d3.range( - this.dataObj.npoint + 5 * this.dataObj.npoint, - ).map((_) => 255); - } else { - this.overlay.restoreAlpha(); - } - this.initGL(this.dataObj); - } else { - this.isDataReady = false; - } + ), + ndim, + npoint, + nepoch, + alphas: d3.range(npoint + 5 * npoint).map(() => 255), + }; + + this.initGL(this.dataObj); if (this.isDataReady && this.isPlaying === undefined) { // renderer.isPlaying===undefined indicates the renderer on init @@ -117,135 +159,81 @@ export function TeaserRenderer(gl: WebGLRenderingContext, program: WebGLProgram, if ( this.isDataReady && - (this.animId == null || - this.shouldRender == false) + (this.animId == null || this.shouldRender == false) ) { this.shouldRender = true; // this.shouldRecalculateColorRect = true; this.play(); } - }; + } - this.setFullScreen = function (shouldSet) { + setFullScreen(shouldSet: boolean) { this.isFullScreen = shouldSet; let canvas = this.gl.canvas; let canvasSelection = d3.select("#" + canvas.id); - let topBarHeight = 0 || d3.select("nav").node().clientHeight; - - d3.select(canvas.parentNode) + d3.select(canvas.parentNode as HTMLElement) .classed("fullscreen", shouldSet); - if (shouldSet) { - canvasSelection - .classed("fullscreen", true); - } else { - canvasSelection - .classed("fullscreen", false); - } + canvasSelection.classed("fullscreen", shouldSet); utils.resizeCanvas(canvas); this.overlay.resize(); - gl.uniform1f(this.canvasWidthLoc, canvas.clientWidth); - gl.uniform1f(this.canvasHeightLoc, canvas.clientHeight); - gl.viewport(0, 0, canvas.width, canvas.height); - }; - - this.setMode = function (mode = "point") { - this.mode = mode; - if (mode === "point") { - gl.uniform1i(this.modeLoc, 0); - } else if (mode === "image") { - gl.uniform1i(this.modeLoc, 1); - } - }; + this.gl.uniform1f(this.canvasWidthLoc!, canvas.clientWidth); + this.gl.uniform1f(this.canvasHeightLoc!, canvas.clientHeight); + this.gl.viewport(0, 0, canvas.width, canvas.height); + } - this.initGL = function (dataObj) { - let gl = this.gl; + initGL(dataObj: Data) { let program = this.program; - // init - utils.resizeCanvas(gl.canvas); + utils.resizeCanvas(this.gl.canvas); - gl.viewport(0, 0, gl.canvas.width, gl.canvas.height); - gl.clearColor(...utils.CLEAR_COLOR, 1.0); + this.gl.viewport(0, 0, this.gl.canvas.width, this.gl.canvas.height); + this.gl.clearColor(...utils.CLEAR_COLOR, 1.0); // gl.enable(gl.DEPTH_TEST); - gl.enable(gl.BLEND); - gl.disable(gl.DEPTH_TEST); - gl.blendFuncSeparate( - gl.SRC_ALPHA, - gl.ONE_MINUS_SRC_ALPHA, - gl.ONE, - gl.ONE_MINUS_SRC_ALPHA, + this.gl.enable(this.gl.BLEND); + this.gl.disable(this.gl.DEPTH_TEST); + this.gl.blendFuncSeparate( + this.gl.SRC_ALPHA, + this.gl.ONE_MINUS_SRC_ALPHA, + this.gl.ONE, + this.gl.ONE_MINUS_SRC_ALPHA, ); - gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); - gl.useProgram(program); - - this.colorBuffer = gl.createBuffer(); - this.colorLoc = gl.getAttribLocation(program, "a_color"); + this.gl.clear(this.gl.COLOR_BUFFER_BIT | this.gl.DEPTH_BUFFER_BIT); + this.gl.useProgram(program); - this.positionBuffer = gl.createBuffer(); - this.positionLoc = gl.getAttribLocation(program, "a_position"); + this.colorBuffer = this.gl.createBuffer()!; + this.colorLoc = this.gl.getAttribLocation(program, "a_color"); - this.textureCoordBuffer = gl.createBuffer(); - this.textureCoordLoc = gl.getAttribLocation(program, "a_textureCoord"); + this.positionBuffer = this.gl.createBuffer()!; + this.positionLoc = this.gl.getAttribLocation(program, "a_position"); - this.pointSizeLoc = gl.getUniformLocation(program, "point_size"); - - let textureCoords = []; - for (let i = 0; i < dataObj.npoint; i++) { - textureCoords.push(...utils.getTextureCoord(i)); - } - for (let i = 0; i < dataObj.ndim * 2; i++) { - textureCoords.push([0, 0]); - } + this.pointSizeLoc = this.gl.getUniformLocation(program, "point_size")!; - if (this.textureCoordLoc !== -1) { - gl.bindBuffer(gl.ARRAY_BUFFER, this.textureCoordBuffer); - gl.bufferData( - gl.ARRAY_BUFFER, - utils.flatten(textureCoords), - gl.STATIC_DRAW, - ); - gl.vertexAttribPointer(this.textureCoordLoc, 2, gl.FLOAT, false, 0, 0); - gl.enableVertexAttribArray(this.textureCoordLoc); - } + this.isDrawingAxisLoc = this.gl.getUniformLocation( + program, + "isDrawingAxis", + )!; - let texture = utils.loadTexture( - gl, - utils.getTextureURL(this.overlay.getDataset()), - ); - this.samplerLoc = gl.getUniformLocation(program, "uSampler"); - gl.activeTexture(gl.TEXTURE0); - gl.bindTexture(gl.TEXTURE_2D, texture); - gl.uniform1i(this.samplerLoc, 0); + this.canvasWidthLoc = this.gl.getUniformLocation(program, "canvasWidth")!; + this.canvasHeightLoc = this.gl.getUniformLocation(program, "canvasHeight")!; + this.gl.uniform1f(this.canvasWidthLoc, this.gl.canvas.clientWidth); + this.gl.uniform1f(this.canvasHeightLoc, this.gl.canvas.clientHeight); - this.isDrawingAxisLoc = gl.getUniformLocation(program, "isDrawingAxis"); + this.modeLoc = this.gl.getUniformLocation(program, "mode")!; + this.gl.uniform1i(this.modeLoc, 0); // "point" mode - this.canvasWidthLoc = gl.getUniformLocation(program, "canvasWidth"); - this.canvasHeightLoc = gl.getUniformLocation(program, "canvasHeight"); - gl.uniform1f(this.canvasWidthLoc, gl.canvas.clientWidth); - gl.uniform1f(this.canvasHeightLoc, gl.canvas.clientHeight); - - this.modeLoc = gl.getUniformLocation(program, "mode"); - this.setMode(this.mode); - - this.colorFactorLoc = gl.getUniformLocation(program, "colorFactor"); + this.colorFactorLoc = this.gl.getUniformLocation(program, "colorFactor")!; this.setColorFactor(this.colorFactor); if (this.gt === undefined || this.gt.ndim != dataObj.ndim) { - let gt = new GrandTour(dataObj.ndim, this.init_matrix); - this.gt = gt; + this.gt = new GrandTour(dataObj.ndim); } - }; - - // this.shouldCentralizeOrigin = this.shouldPlayGrandTour; - - this.shouldRender = true; - this.s = 0; + } - this.play = (t = 0) => { + play(_t = 0) { let dt = 0; if ( @@ -280,42 +268,42 @@ export function TeaserRenderer(gl: WebGLRenderingContext, program: WebGLProgram, } this.animId = requestAnimationFrame(this.play.bind(this)); - }; + } - this.setColorFactor = function (f) { + setColorFactor(f: number) { this.colorFactor = f; - this.gl.uniform1f(this.colorFactorLoc, f); - }; + this.gl.uniform1f(this.colorFactorLoc!, f); + } - this.setPointSize = function (s) { + setPointSize(s: number) { this.pointSize = s; - gl.uniform1f(this.pointSizeLoc, s * window.devicePixelRatio); - }; + this.gl.uniform1f(this.pointSizeLoc!, s * window.devicePixelRatio); + } - this.pause = function () { + pause() { if (this.animId) { cancelAnimationFrame(this.animId); - this.animId = null; + this.animId = undefined; } this.shouldRender = false; console.log("paused"); - }; + } - this.setEpochIndex = (i) => { + setEpochIndex(i: number) { this.epochIndex = i; this.overlay.epochSlider.property("value", i); - + if (!this.dataObj) return; this.overlay.svg.select("#epochIndicator") .text(`Epoch: ${this.epochIndex}/${(this.dataObj.nepoch - 1)}`); - }; + } - this.playFromEpoch = function (epoch) { + playFromEpoch(epoch: number) { this.shouldAutoNextEpoch = true; this.setEpochIndex(epoch); this.overlay.playButton.attr("class", "tooltip play-button fa fa-pause"); - }; + } - this.nextEpoch = function () { + nextEpoch() { if (this.epochs.length == 1) { return; } @@ -325,9 +313,9 @@ export function TeaserRenderer(gl: WebGLRenderingContext, program: WebGLProgram, } else { this.setEpochIndex(this.epochs[0]); } - }; + } - this.prevEpoch = function () { + prevEpoch() { if (this.epochs.length == 1) { return; } @@ -336,21 +324,17 @@ export function TeaserRenderer(gl: WebGLRenderingContext, program: WebGLProgram, } else { this.setEpochIndex(this.epochs.length - 1); } - }; + } + + render(dt: number) { + if (!this.dataObj || !this.gt) return; - this.render = function (dt: number) { - if (this.dataObj.dataTensor === undefined) { - return; - } let dataObj = this.dataObj; let data = this.dataObj.dataTensor[this.epochIndex]; let labels = this.dataObj.labels; - let gl = this.gl; - let gt = this.gt; - let program = this.program; data = data.concat(utils.createAxisPoints(dataObj.ndim)); - let points = gt.project(data, dt); + let points = this.gt.project(data, dt); if ( this.epochIndex > 0 && @@ -358,7 +342,7 @@ export function TeaserRenderer(gl: WebGLRenderingContext, program: WebGLProgram, ) { let data0 = this.dataObj.dataTensor[this.epochIndex - 1]; data0 = data0.concat(utils.createAxisPoints(dataObj.ndim)); - let points0 = gt.project(data0, dt / this.framesPerTransition); + let points0 = this.gt.project(data0, dt / this.framesPerTransition); points = utils.linearInterpolate( points0, points, @@ -368,7 +352,7 @@ export function TeaserRenderer(gl: WebGLRenderingContext, program: WebGLProgram, utils.updateScale_center( points, - gl.canvas, + this.gl.canvas, this.sx_center, this.sy_center, this.sz_center, @@ -379,7 +363,7 @@ export function TeaserRenderer(gl: WebGLRenderingContext, program: WebGLProgram, utils.updateScale_span( points, - gl.canvas, + this.gl.canvas, this.sx_span, this.sy_span, this.sz_span, @@ -390,9 +374,9 @@ export function TeaserRenderer(gl: WebGLRenderingContext, program: WebGLProgram, let transition; if (this.scaleTransitionDelta > 0) { - transition = (t) => Math.pow(t, 0.5); + transition = (t: number) => Math.pow(t, 0.5); } else { - transition = (t) => 1 - Math.pow(1 - t, 0.5); + transition = (t: number) => 1 - Math.pow(1 - t, 0.5); } this.sx = utils.mixScale( this.sx_center, @@ -410,103 +394,76 @@ export function TeaserRenderer(gl: WebGLRenderingContext, program: WebGLProgram, points = utils.data2canvas(points, this.sx, this.sy, this.sz); - if (this.mode == "image") { - points = utils.point2rect( - points, - dataObj.npoint, - 14 * math.sqrt(this.scaleFactor), - ); - } - dataObj.points = points; - let colors = labels.map((d) => utils.baseColors[d]); let bgColors = labels.map((d) => utils.bgColors[d]); + 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]]); - colors = colors.concat(utils.createAxisColors(dataObj.ndim)); - colors = colors.map((c, i) => [c[0], c[1], c[2], dataObj.alphas[i]]); - - if (this.mode == "image") { - if (this.colorRect === undefined || this.shouldRecalculateColorRect) { - this.colorRect = utils.color2rect(colors, dataObj.npoint, dataObj.ndim); - this.bgColorRect = utils.color2rect( - bgColors, - dataObj.npoint, - dataObj.ndim, - ); - this.shouldRecalculateColorRect = false; - } - colors = this.colorRect; - bgColors = this.bgColorRect; - } dataObj.colors = colors; - let colorBuffer = this.colorBuffer; - let positionBuffer = this.positionBuffer; - let colorLoc = this.colorLoc; - let positionLoc = this.positionLoc; + this.gl.viewport(0, 0, this.gl.canvas.width, this.gl.canvas.height); - gl.viewport(0, 0, gl.canvas.width, gl.canvas.height); + this.gl.clearColor(...utils.CLEAR_COLOR, 1.0); + this.gl.clear(this.gl.COLOR_BUFFER_BIT | this.gl.DEPTH_BUFFER_BIT); - gl.clearColor(...utils.CLEAR_COLOR, 1.0); - gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); - - gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer); - gl.bufferData(gl.ARRAY_BUFFER, utils.flatten(points), gl.STATIC_DRAW); - gl.vertexAttribPointer(positionLoc, 3, gl.FLOAT, false, 0, 0); - gl.enableVertexAttribArray(positionLoc); + this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.positionBuffer!); + this.gl.bufferData( + this.gl.ARRAY_BUFFER, + utils.flatten(points), + this.gl.STATIC_DRAW, + ); + this.gl.vertexAttribPointer( + this.positionLoc!, + 3, + this.gl.FLOAT, + false, + 0, + 0, + ); + this.gl.enableVertexAttribArray(this.positionLoc!); - gl.bindBuffer(gl.ARRAY_BUFFER, colorBuffer); - gl.bufferData( - gl.ARRAY_BUFFER, + this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.colorBuffer!); + this.gl.bufferData( + this.gl.ARRAY_BUFFER, new Uint8Array(utils.flatten(colors)), - gl.STATIC_DRAW, + this.gl.STATIC_DRAW, + ); + this.gl.vertexAttribPointer( + this.colorLoc!, + 4, + this.gl.UNSIGNED_BYTE, + true, + 0, + 0, ); - gl.vertexAttribPointer(colorLoc, 4, gl.UNSIGNED_BYTE, true, 0, 0); - gl.enableVertexAttribArray(colorLoc); + this.gl.enableVertexAttribArray(this.colorLoc!); - let c0 = bgColors.map((c, i) => [c[0], c[1], c[2], utils.pointAlpha]); - gl.bufferData( - gl.ARRAY_BUFFER, + let c0 = bgColors.map((c) => [c[0], c[1], c[2], utils.pointAlpha]); + this.gl.bufferData( + this.gl.ARRAY_BUFFER, new Uint8Array(utils.flatten(c0)), - gl.STATIC_DRAW, + this.gl.STATIC_DRAW, ); let c1; - if (this.mode === "point") { - gl.uniform1i(this.isDrawingAxisLoc, 0); - this.setPointSize(this.pointSize0 * Math.sqrt(this.scaleFactor)); + this.gl.uniform1i(this.isDrawingAxisLoc!, 0); + this.setPointSize(this.pointSize0 * Math.sqrt(this.scaleFactor)); - gl.drawArrays(gl.POINTS, 0, dataObj.npoint); - c1 = colors.map((c, i) => [c[0], c[1], c[2], dataObj.alphas[i]]); - } else { - gl.drawArrays(gl.TRIANGLES, 0, dataObj.npoint * 6); - c1 = colors.map(( - c, - i, - ) => [c[0], c[1], c[2], dataObj.alphas[Math.floor(i / 6)]]); - } - gl.bufferData( - gl.ARRAY_BUFFER, + this.gl.drawArrays(this.gl.POINTS, 0, dataObj.npoint); + c1 = colors.map((c, i) => [c[0], c[1], c[2], dataObj.alphas[i]]); + this.gl.bufferData( + this.gl.ARRAY_BUFFER, new Uint8Array(utils.flatten(c1)), - gl.STATIC_DRAW, + this.gl.STATIC_DRAW, ); - if (this.mode === "point") { - gl.uniform1i(this.isDrawingAxisLoc, 0); - gl.drawArrays(gl.POINTS, 0, dataObj.npoint); + this.gl.uniform1i(this.isDrawingAxisLoc!, 0); + this.gl.drawArrays(this.gl.POINTS, 0, dataObj.npoint); - gl.uniform1i(this.isDrawingAxisLoc, 1); - gl.drawArrays(gl.LINES, dataObj.npoint, dataObj.ndim * 2); - } else { - gl.uniform1i(this.isDrawingAxisLoc, 0); - gl.drawArrays(gl.TRIANGLES, 0, dataObj.npoint * 6); - - this.setMode("point"); - gl.uniform1i(this.isDrawingAxisLoc, 1); - gl.drawArrays(gl.LINES, dataObj.npoint * 6, dataObj.ndim * 2); - this.setMode("image"); - } - return; - }; + this.gl.uniform1i(this.isDrawingAxisLoc!, 1); + this.gl.drawArrays(this.gl.LINES, dataObj.npoint, dataObj.ndim * 2); + } } diff --git a/src/index.ts b/src/index.ts index f4b742f..edf614d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,67 +1,28 @@ -// @ts-check import * as d3 from "d3"; -import { TeaserRenderer } from './TeaserRenderer'; -import * as utils from './utils'; +import { TeaserRenderer } from "./TeaserRenderer"; +import * as utils from "./utils"; +import fs from "./shaders/teaser_fragment.glsl"; +import vs from "./shaders/teaser_vertex.glsl"; -import fs from './shaders/teaser_fragment.glsl'; -import vs from './shaders/teaser_vertex.glsl'; - -const teaserFigure = document.querySelector("d-figure.teaser")!; -let teaser: typeof TeaserRenderer; -let allViews: typeof teaser[] = []; - -let c = utils.CLEAR_COLOR.map(d => d * 255); -// @ts-expect-error -d3.selectAll('.flex-item').style('background', d3.rgb(...c)) - -teaserFigure.addEventListener("ready", function() { - console.log('teaserFigure ready'); - var epochs = d3.range(0, 1, 1); - var urls = utils.getChromTeaserDataURL(); - var { gl, program } = utils.initGL("#teaser", fs, vs); - - teaser = new TeaserRenderer(gl, program, { - epochs: epochs, - shouldAutoNextEpoch: true - }); - - allViews.push(teaser); - - teaser.overlay.fullScreenButton.style('top', '18px'); - teaser.overlay.epochSlider.style('top', 'calc(100% - 28px)'); - teaser.overlay.playButton.style('top', ' calc(100% - 34px)'); - teaser.overlay.grandtourButton.style('top', ' calc(100% - 34px)'); - - // teaser.overlay.fullScreenButton.remove(); - teaser.overlay.modeOption.remove(); - teaser.overlay.datasetOption.remove(); - teaser.overlay.zoomSliderDiv.remove(); - // teaser.overlay.grandtourButton.remove(); - - teaser = utils.loadDataToRenderer(urls, teaser); - - window.addEventListener('resize', ()=>{ - teaser.setFullScreen(teaser.isFullScreen); - }); +let figure = document.querySelector("d-figure.teaser")!; +let canvas = figure.querySelector("canvas")!; +let { gl, program } = utils.initGL(canvas, fs, vs); +let teaser = new TeaserRenderer(gl, program, { + epochs: d3.range(0, 1, 1), + shouldAutoNextEpoch: true, }); -teaserFigure.addEventListener("onscreen", function() { - console.log('teaser onscreen'); - if(teaser && teaser.play){ - teaser.shouldRender = true; - teaser.play(); - } - for(let view of allViews){ - if(view !== teaser && view.pause){ - view.pause(); - } - } -}); +teaser.overlay.fullScreenButton.style("top", "18px"); +teaser.overlay.epochSlider.style("top", "calc(100% - 28px)"); +teaser.overlay.playButton.style("top", " calc(100% - 34px)"); +teaser.overlay.grandtourButton.style("top", " calc(100% - 34px)"); + +await utils.loadDataToRenderer([ + new URL("../data/eigs.arrow", import.meta.url).href, +], teaser); -teaserFigure.addEventListener("offscreen", function() { - if(teaser && teaser.pause){ - teaser.pause(); - } +window.addEventListener("resize", () => { + teaser.setFullScreen(teaser.isFullScreen); }); diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..d2711cc --- /dev/null +++ b/src/types.ts @@ -0,0 +1,25 @@ +import type { BaseType, ScaleContinuousNumeric, Selection } from "d3"; + +export type Point = [number, number, number]; + +export type Scale = ScaleContinuousNumeric; + +export type ColorRGB = [number, number, number]; + +export type ColorRGBA = [number, number, number, number]; + +export interface Renderer { + gl: WebGLRenderingContext; + render(dt: number): void; + play(t?: number): void; + pause(): void; + initData( + buffer: ArrayBuffer, + url?: string, + i?: number, + length?: number, + ): Promise; + overlay: { + figure: Selection; + }; +} diff --git a/src/utils.ts b/src/utils.ts index 90a1ce1..ec7a4e9 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -2,12 +2,14 @@ import * as d3 from "d3"; import * as math from "mathjs"; import numeric from "numeric"; +import type { ColorRGB, ColorRGBA, Point, Renderer, Scale } from "./types"; + export const CLEAR_COLOR = [1, 1, 1] as const; export const CLEAR_COLOR_SMALL_MULTIPLE = [1, 1, 1] as const; export const MIN_EPOCH = 0; export const MAX_EPOCH = 99; export const COLOR_FACTOR = 0.9; -export const dataset = "mnist"; +export let dataset = "mnist"; export const datasetListener = []; export const pointAlpha = 255 * 0.1; @@ -35,11 +37,16 @@ export const buttonColors = { "off": "#f3f3f3", }; -export function clamp(min, max, v) { +export function clamp(min: number, max: number, v: number) { return Math.max(max, Math.min(min, v)); } -export function mixScale(s0, s1, progress, func) { +export function mixScale( + s0: Scale, + s1: Scale, + progress: number, + func: (x: number) => number, +) { let range0 = s0.range(); let range1 = s1.range(); @@ -54,7 +61,12 @@ export function mixScale(s0, s1, progress, func) { .range(mix(range0, range1, progress)); } -export function data2canvas(points, sx, sy, sz) { +export function data2canvas( + points: number[][], + sx: Scale, + sy: Scale, + sz: Scale, +) { points = points.map((row) => { return [sx(row[0]), sy(row[1]), sz(row[2])]; }); @@ -62,28 +74,19 @@ export function data2canvas(points, sx, sy, sz) { } export function updateScale_span( - points, - canvas, - sx, - sy, - sz, + points: number[][], + canvas: HTMLCanvasElement, + sx: d3.ScaleLinear, + sy: d3.ScaleLinear, + sz: d3.ScaleLinear, scaleFactor = 1.0, - marginRight = undefined, - marginBottom = undefined, - marginLeft = undefined, - marginTop = undefined, + marginRight?: number, + marginBottom = 65, + marginLeft = 32, + marginTop = 22, ) { - if (marginTop === undefined) { - marginTop = 22; - } - if (marginBottom === undefined) { - marginBottom = 65; - } - if (marginLeft === undefined) { - marginLeft = 32; - } if (marginRight === undefined) { - marginRight = d3.max(Object.values(legendLeft)) + 15; + marginRight = d3.max(Object.values(legendLeft))! + 15; } let vmin = math.min(points, 0); @@ -114,16 +117,16 @@ export function updateScale_span( } export function updateScale_center( - points, - canvas, - sx, - sy, - sz, + points: number[][], + canvas: HTMLCanvasElement, + sx: Scale, + sy: Scale, + sz: Scale, scaleFactor = 1.0, - marginRight = undefined, - marginBottom = undefined, - marginLeft = undefined, - marginTop = undefined, + marginRight?: number, + marginBottom?: number, + marginLeft?: number, + marginTop?: number, ) { if (marginTop === undefined) { marginTop = 22; @@ -135,7 +138,7 @@ export function updateScale_center( marginLeft = 32; } if (marginRight === undefined) { - marginRight = d3.max(Object.values(legendLeft)) + 15; + marginRight = d3.max(Object.values(legendLeft))! + 15; } let vmax = math.max(math.abs(points), 0); @@ -166,21 +169,7 @@ export function updateScale_center( .range([0, 1]); } -export function toDataURL(url, callback) { - var xhr = new XMLHttpRequest(); - xhr.onload = function () { - var reader = new FileReader(); - reader.onloadend = function () { - callback(reader.result); - }; - reader.readAsDataURL(xhr.response); - }; - xhr.open("GET", url); - xhr.responseType = "blob"; - xhr.send(); -} - -export function embed(matrix, canvas) { +export function embed(matrix: T[][], canvas: T[][]) { for (let i = 0; i < matrix.length; i++) { for (let j = 0; j < matrix[0].length; j++) { canvas[i][j] = matrix[i][j]; @@ -189,124 +178,48 @@ export function embed(matrix, canvas) { return canvas; } -// huh: https://eslint.org/docs/rules/guard-for-in -export function walkObject(obj, f) { - for (let key in obj) { - if (Object.prototype.hasOwnProperty.call(obj, key)) { - f(key); - } - } -} - -export function scaleRows(matrix, isRowSelected, beta1, beta0) { - let selectedCount = numeric.sum(isRowSelected); - let res = matrix.map((row, i) => { - row = row.slice(); - if (isRowSelected[i]) { - row = numeric.mul(row, beta1 / selectedCount); - } else { - row = numeric.mul(row, beta0 / (matrix.length - selectedCount)); - } - return row; - }); - return res; -} - -export function setDataset(datasetName, callback0) { - this.dataset = datasetName; - for (let callback of datasetListener) { - callback(datasetName); - } - if (callback0 !== undefined) { - callback0(); - } - // } -} - export function getDataset() { return dataset; } -export function addDatasetListener(callback) { - datasetListener.push(callback); -} - -export function clearDatasetListener() { - for (let i = 0; i < datasetListener.length; i++) { - datasetListener.pop(); - } -} - -export function getLabelNames(adversarial = false, dataset = undefined) { +export function getLabelNames(_adversarial = false, dataset?: string) { if (dataset === undefined) { dataset = getDataset(); } let res; if (dataset == "mnist") { res = ["A0", "A1", "B0", "B1", "B2"]; - } else if (dataset == "fashion-mnist") { - res = [ - "T-shirt/top", - "Trouser", - "Pullover", - "Dress", - "Coat", - "Sandal", - "Shirt", - "Sneaker", - "Bag", - "Ankle boot", - ]; - } else if (dataset == "cifar10") { - res = [ - "Airplane", - "Automobile", - "Bird", - "Cat", - "Deer", - "Dog", - "Frog", - "Horse", - "Ship", - "Truck", - ]; } else { throw new Error("Unrecognized dataset " + dataset); } - if (adversarial) { - res.push("adversarial"); - } return res; } -export function getChromTeaserDataURL() { - return [ - new URL('../data/eigs.arrow', import.meta.url).href - ] -} - export function getTextureURL(dataset = getDataset(), datasetType = "test") { return "data/softmax/" + dataset + "/input-" + datasetType + ".png"; } -export function initGL(canvasid: string, fs: string, vs: string) { - let canvas = document.getElementById(canvasid.slice(1)) as HTMLCanvasElement; +export function initGL(canvas: HTMLCanvasElement, fs: string, vs: string) { let gl = canvas.getContext("webgl", { premultipliedAlpha: false })!; let program = initShaders(gl, fs, vs); return { gl, program }; } -export function loadDataWithCallback(urls, callback) { - for (let i = 0; i < urls.length; i++) { - loadDataBin(urls[i], (buffer, url) => { - callback(buffer, url, i, urls.length); - }); - } -} +export async function loadDataToRenderer(urls: string[], renderer: Renderer) { + let banner = renderer.overlay.figure.selectAll(".banner") + .data([0]) + .enter() + .append("div") + .attr("class", "banner"); -function bannerAnimation(renderer) { - let banner = renderer.overlay.banner; - let bannerText = renderer.overlay.bannerText; + let bannerText = banner + .selectAll(".bannerText") + .data([0]) + .enter() + .append("p") + .attr("class", "bannerText"); + + // render loop function repeat() { bannerText .text("Loading") @@ -322,47 +235,33 @@ function bannerAnimation(renderer) { .on("end", repeat); } repeat(); -} -function createBanner(renderer) { - let overlay = renderer.overlay; - if (overlay.figure) { - overlay.banner = overlay.figure.selectAll(".banner") - .data([0]) - .enter() - .append("div") - .attr("class", "banner"); - overlay.banner = overlay.figure.selectAll(".banner"); - overlay.bannerText = overlay.banner - .selectAll(".bannerText") - .data([0]) - .enter() - .append("p") - .attr("class", "bannerText"); - overlay.bannerText = overlay.banner.selectAll(".bannerText"); - } -} - -export function loadDataToRenderer(urls, renderer, onReadyCallback) { - if (renderer.overlay) { - createBanner(renderer); - bannerAnimation(renderer); - } + await Promise.all( + urls.map(async (url, i) => { + let buffer = await loadDataBin(url); + return renderer.initData(buffer, url, i, urls.length); + }), + ); - for (let i = 0; i < urls.length; i++) { - loadDataBin(urls[i], (buffer, url) => { - renderer.initData(buffer, url, i, urls.length, onReadyCallback); - }); - } - return renderer; + banner.remove(); } -export function reshape(array, shape) { +// TODO: fail when shape isn't tuple... Right now returns `number`. +type NestedArray = Shape extends + [infer _, ...infer Rest] + ? Rest extends number[] ? NestedArray[] : never + : T; + +export function reshape( + array: ArrayLike, + shape: Shape, +): NestedArray { let res = []; if (shape.length == 2) { for (let row = 0; row < shape[0]; row++) { res.push([]); for (let col = 0; col < shape[1]; col++) { + // @ts-expect-error res[res.length - 1].push(array[shape[1] * row + col]); } } @@ -371,37 +270,32 @@ export function reshape(array, shape) { for (let i = 0; i < shape[0]; i++) { res.push( reshape( + // @ts-expect-error array.slice(i * blocksize, (i + 1) * blocksize), shape.slice(1), ), ); } } - return res; + return res as any; } -export function cacheAll(urls) { - for (let url of urls) loadDataBin(url, () => {}); +export async function cacheAll(urls: string[]) { + await Promise.all(urls.map(loadDataBin)); } -const cache = {}; -export async function loadDataBin(url, callback) { - if (!(url in cache)) { +const cache = new Map(); +export async function loadDataBin(url: string) { + let buffer = cache.get(url); + if (!buffer) { let response = await fetch(url); - cache[url] = await response.arrayBuffer(); + buffer = await response.arrayBuffer(); + cache.set(url, buffer); } - callback(cache[url], url); + return buffer; } -export function loadDataCsv(fns, renderer) { - let promises = fns.map((fn) => d3.text(fn)); - Promise.all(promises).then(function (dataRaw) { - renderer.initData(dataRaw); - renderer.play(); - }); -} - -export function resizeCanvas(canvas) { +export function resizeCanvas(canvas: HTMLCanvasElement) { let DPR = window.devicePixelRatio; let displayWidth = DPR * canvas.clientWidth; @@ -415,21 +309,21 @@ export function resizeCanvas(canvas) { canvas.width = displayWidth; canvas.height = displayHeight; } - canvas.style.width = canvas.clientWidth; - canvas.style.height = canvas.clientHeight; + canvas.style.width = String(canvas.clientWidth); + canvas.style.height = String(canvas.clientHeight); } export const baseColorsHex = [...d3.schemeCategory10]; baseColorsHex.push("#444444"); baseColorsHex.push("#444444"); -function hexToRgb(hex: string) { +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), - ] as const; + ]; } export const baseColors = baseColorsHex.map(hexToRgb); @@ -439,62 +333,72 @@ export const bgColors = numeric.add( 0.95 * 255 * 0.4, ); -export function createAxisPoints(ndim) { - let res = math.identity(ndim)._data; +export function createAxisPoints(ndim: number) { + let res = (math.identity(ndim) as math.Matrix).toArray(); for (let i = ndim - 1; i >= 0; i--) { - res.splice(i, 0, math.zeros(ndim)._data); + let zeros = (math.zeros(ndim) as math.Matrix).toArray() as number[]; + res.splice(i, 0, zeros); } - return res; + return res as number[][]; } -export function createAxisColors(ndim) { +export function createAxisColors(ndim: number) { return d3.range(ndim * 2).map( (_, i) => baseColors[Math.floor(i / 2) % baseColors.length], ); } -export function linearInterpolate(data1, data2, p) { +export function linearInterpolate( + data1: T, + data2: T, + p: number, +) { // let res = math.zeros(data1.length, data1[0].length)._data; // for (let i=0; i(data1: T, data2: T, p: number) { return linearInterpolate(data1, data2, p); } -export function orthogonalize(matrix, priorityRowIndex = 0) { +export function orthogonalize( + matrix: M, + priorityRowIndex = 0, +): M { // make row vectors in matrix pairwise orthogonal; - function proj(u, v) { + function proj(u: M[number], v: M[number]): M[number] { // @ts-expect-error return numeric.mul(numeric.dot(u, v) / numeric.dot(u, u), u); } - function normalize(v, unitlength = 1) { + function normalize(v: M[number], unitlength = 1): M[number] { if (numeric.norm2(v) <= 0) { return v; } else { + // @ts-expect-error return numeric.div(v, numeric.norm2(v) / unitlength); } } // Gram–Schmidt orthogonalization let priorityRow = matrix[priorityRowIndex]; - let firstRow = matrix[0]; + let firstRow = matrix[0] as M[number]; matrix[0] = priorityRow; matrix[priorityRowIndex] = firstRow; matrix[0] = normalize(matrix[0]); for (let i = 1; i < matrix.length; i++) { for (let j = 0; j < i; j++) { + // @ts-expect-error matrix[i] = numeric.sub(matrix[i], proj(matrix[j], matrix[i])); } matrix[i] = normalize(matrix[i]); @@ -505,7 +409,12 @@ export function orthogonalize(matrix, priorityRowIndex = 0) { return matrix; } -export function point2rect(points, npoint, sideLength, yUp = false) { +export function point2rect( + points: Point[], + npoint: number, + sideLength: number, + yUp = false, +) { let res = []; //points @@ -538,7 +447,11 @@ export function point2rect(points, npoint, sideLength, yUp = false) { return res; } -export function color2rect(colors, npoint, ndim) { +export function color2rect( + colors: Color[], + npoint: number, + ndim: number, +) { let pointColors = colors.slice(0, npoint) .map((c) => [c, c, c, c, c, c]) .reduce((a, b) => a.concat(b), []); @@ -546,47 +459,8 @@ export function color2rect(colors, npoint, ndim) { return pointColors.concat(axisColors); } -export function getTextureCoord( - i, - nRow = 10, - nCol = 100, - isAdversarial = false, - epoch = 99, - nepoch = 100, -) { - let nRow0 = nRow; - let npoint; - if (isAdversarial) { - npoint = nRow * nCol; - nRow = nRow + nepoch; - } - - let ul, ur, ll, lr; - let numPerRow = nCol; - let numPerCol = nRow; - let dx = 1 / numPerRow; - let dy = 1 / numPerCol; - if (isAdversarial && i >= npoint - 89) { // hardcoded: last 89 are adversarial examples - ul = [ - dx * ((i - (npoint - 89)) % numPerRow), - dy * Math.floor(nRow0 + epoch), - ]; - } else { - ul = [dx * (i % numPerRow), dy * Math.floor(i / numPerRow)]; - } - ur = ul.slice(); - ur[0] += dx; - ll = ul.slice(); - ll[1] += dy; - lr = ul.slice(); - lr[0] += dx; - lr[1] += dy; - - return [ur, ul, ll, ur, ll, lr]; -} - -export function loadTexture(gl, url) { - function isPowerOf2(x) { +export function loadTexture(gl: WebGLRenderingContext, url: string) { + function isPowerOf2(x: number) { // @ts-expect-error return x & (x - 1) == 0; } @@ -638,41 +512,6 @@ export function loadTexture(gl, url) { return texture; } -export function setTeaser( - renderer, - datasetname, - epochIndex, - classes, - shouldAutoNextEpoch = true, - timeout = 0, - callback = undefined, -) { - setDataset(datasetname, () => { - renderer.setEpochIndex(epochIndex); - if (classes.length > 0) { - renderer.overlay.selectedClasses = new Set(classes); - renderer.overlay.onSelectLegend(classes); - } else { - renderer.overlay.selectedClasses = new Set(); - renderer.overlay.restoreAlpha(); - } - - renderer.shouldAutoNextEpoch = shouldAutoNextEpoch; - d3.select(renderer.overlay.svg.node().parentElement) - .select(".play-button") - .attr("class", () => { - if (renderer.shouldAutoNextEpoch) { - return "tooltip play-button fa fa-pause"; - } else { - return "tooltip play-button fa fa-play"; - } - }); - if (callback) { - callback(); - } - }); -} - export function* iterN(it: Iterable, n: number) { let i = 0; for (let x of it) { @@ -681,7 +520,11 @@ export function* iterN(it: Iterable, n: number) { } } -function getShader(gl: WebGLRenderingContext, shaderScript: string, type: number) { +function getShader( + gl: WebGLRenderingContext, + shaderScript: string, + type: number, +) { var shader = gl.createShader(type)!; gl.shaderSource(shader, shaderScript); gl.compileShader(shader); @@ -702,12 +545,14 @@ export function initShaders(gl: WebGLRenderingContext, fs: string, vs: string) { return program; } -export function transpose(m) { +type Matrix = number[][] & { matrix?: true }; + +export function transpose(m: Matrix) { if (!m.matrix) { - return "transpose(): trying to transpose a non-matrix"; + throw new Error("transpose(): trying to transpose a non-matrix"); } - var result = []; + var result = [] as unknown as Matrix; for (var i = 0; i < m.length; ++i) { result.push([]); for (var j = 0; j < m[i].length; ++j) { @@ -720,7 +565,7 @@ export function transpose(m) { return result; } -export function flatten(v) { +export function flatten(v: Matrix) { if (v.matrix === true) { v = transpose(v); } @@ -744,6 +589,7 @@ export function flatten(v) { } } else { for (var i = 0; i < v.length; ++i) { + // @ts-expect-error floats[i] = v[i]; } }