Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { BubbleSortStarter } from "./visualizers/bubble-sort/start";
import { BubbleSortRender } from "./visualizers/bubble-sort/render";
import { DBScanStarter } from "./visualizers/dbscan/start";
import { DBScanRender } from "./visualizers/dbscan/render";
import { useVisualizer } from "./lib/hooks";
import { useEffect, useState } from "react";

Expand Down Expand Up @@ -48,8 +48,8 @@ const App = () => {
<button onClick={() => setEventOverride(0)}>Beginning</button>
<button onClick={() => setEventOverride(events.length - 1)}>End</button>
<small>Use arrow keys to navigate</small>
<BubbleSortStarter doStart={start}/>
<BubbleSortRender curState={curStateOverride} curEvent={curEventOverride}/>
<DBScanStarter doStart={start}/>
<DBScanRender curState={curStateOverride} curEvent={curEventOverride}/>
<div>
{events && events.map((x, i) => {
return <div key={i}>
Expand Down
2 changes: 1 addition & 1 deletion src/lib/hooks.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useSyncExternalStore } from "react";
import { globalStore } from "../visualizers/bubble-sort";
import { globalStore } from "../visualizers/dbscan";

export const useVisualizer = () => {
return useSyncExternalStore(globalStore.subscribe, globalStore.getCurSnapshot);
Expand Down
101 changes: 101 additions & 0 deletions src/visualizers/dbscan/dbscan.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { bind, here } from "./";

export const dbscan = async (points: number[][], eps: number, min_samples: number) => {
let labels = new Array(points.length).fill(0);
let cluster = new Mutable<number>(1);

bind("points", points);
bind("labels", labels);
bind("cluster", cluster);

await here("start");
let neighbors = findNeighbors(points, eps);
for (let i = 0; i < points.length; i++) {
if (await expandCluster(labels, neighbors, i, cluster, min_samples)) {
cluster.value++;
await here("new_cluster", cluster.value);
}
}
await here("done");
}

const expandCluster = async (
labels: number[],
neighbors: number[][],
i: number,
cluster: Mutable<number>,
min_samples: number
): Promise<boolean> => {
if (labels[i] !== 0) {
return false;
}

if (neighbors[i].length < min_samples) {
labels[i] = -1;
await here("noise", i, neighbors[i].length);
return false;
}

labels[i] = cluster.value;
await here("expand_cluster", i, neighbors[i].length);
for (let neighbor of neighbors[i]) {
if (labels[neighbor] === 0) {
await expandCluster(labels, neighbors, neighbor, cluster, min_samples);
}
}

return true;
}

const findNeighbors = (points: number[][], eps: number): number[][] => {
let neighbors = new Array(points.length).fill(null).map(() => []);
for (let i = 0; i < points.length - 1; i++) {
for (let j = i + 1; j < points.length; j++) {
if (distance(points[i], points[j]) < eps) {
neighbors[i].push(j);
neighbors[j].push(i);
}
}
}
return neighbors;
}

const distance = (p1: number[], p2: number[]): number =>
Math.sqrt(p1.reduce((acc, cur, i) => acc + Math.pow(cur - p2[i], 2), 0));

// TODO: move to lib?
class Mutable<T> {
constructor(public value: T) {}
}

export type DBScanState = {
cluster: Mutable<number>;
labels: number[];
points: number[][];
}

export type DBScanEvent = Start | Done | NewCluster | ExpandCluster | Noise;

// TODO: move Start and Done to lib?
type Start = {
name: "start",
args: [],
}
type Done = {
name: "done",
args: [],
}
type NewCluster = {
name: "new_cluster";
args: [cluster: number];
}
type ExpandCluster = {
name: "expand_cluster";
args: [point: number, neighbors_count: number];
}
type Noise = {
name: "noise";
args: [point: number, neighbors_count: number];
}

export type DBScanArguments = [number[][], number, number];
21 changes: 21 additions & 0 deletions src/visualizers/dbscan/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { AlgorithmManifest } from "../../lib/manifest";
import { RuntimeStore } from "../../lib/store";
import { DBScanArguments, DBScanEvent, DBScanState, dbscan } from "./dbscan";
import { DBScanRender } from "./render";
import { DBScanStarter } from "./start";

export const manifest: AlgorithmManifest<DBScanState, DBScanEvent, DBScanArguments> = {
algo: dbscan,
startComponent: DBScanStarter,
renderComponent: DBScanRender
}

export const globalStore = new RuntimeStore<DBScanState, DBScanEvent, DBScanArguments>(dbscan);

export const bind = (name: keyof DBScanState, value: DBScanState[keyof DBScanState]) => {
globalStore.bind(name, value);
}

export const here = async (name: DBScanEvent["name"], ...args: DBScanEvent["args"]): Promise<void> => {
return globalStore.here(name, ...args);
}
11 changes: 11 additions & 0 deletions src/visualizers/dbscan/render.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { DBScanEvent, DBScanState } from "./dbscan";

export type RenderProps = {
curState: DBScanState;
curEvent: DBScanEvent;
}

export const DBScanRender = ({ curState, curEvent }) => {
console.log("Rendering", curState, curEvent);
return <div>TODO: render</div>;
}
18 changes: 18 additions & 0 deletions src/visualizers/dbscan/start.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { DBScanArguments } from "./dbscan"

type Props = {
doStart: (args: DBScanArguments, noStop: boolean) => void
}

const args: DBScanArguments = [
[[0, 0], [1, 0], [0, 1], [1, 1], [10, 3], [5, 5], [5, 6], [6, 5], [6, 6]],
2,
3,
];

export const DBScanStarter = ({ doStart }: Props) => {
return <div>
<button onClick={() => doStart(args, false)}>Start</button>
<button onClick={() => doStart(args, true)}>Run Full</button>
</div>;
}