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;
+}