diff --git a/src/App.tsx b/src/App.tsx index 112cdea..e0fb978 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -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"; @@ -48,8 +48,8 @@ const App = () => { Use arrow keys to navigate - - + +
{events && events.map((x, i) => { return
diff --git a/src/lib/hooks.tsx b/src/lib/hooks.tsx index 8810f14..626650f 100644 --- a/src/lib/hooks.tsx +++ b/src/lib/hooks.tsx @@ -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); diff --git a/src/visualizers/dbscan/dbscan.tsx b/src/visualizers/dbscan/dbscan.tsx new file mode 100644 index 0000000..cc807c0 --- /dev/null +++ b/src/visualizers/dbscan/dbscan.tsx @@ -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(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, + min_samples: number +): Promise => { + 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 { + constructor(public value: T) {} +} + +export type DBScanState = { + cluster: Mutable; + 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]; diff --git a/src/visualizers/dbscan/index.tsx b/src/visualizers/dbscan/index.tsx new file mode 100644 index 0000000..69d6655 --- /dev/null +++ b/src/visualizers/dbscan/index.tsx @@ -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 = { + algo: dbscan, + startComponent: DBScanStarter, + renderComponent: DBScanRender +} + +export const globalStore = new RuntimeStore(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 => { + return globalStore.here(name, ...args); +} diff --git a/src/visualizers/dbscan/render.tsx b/src/visualizers/dbscan/render.tsx new file mode 100644 index 0000000..53dcc86 --- /dev/null +++ b/src/visualizers/dbscan/render.tsx @@ -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
TODO: render
; +} \ No newline at end of file diff --git a/src/visualizers/dbscan/start.tsx b/src/visualizers/dbscan/start.tsx new file mode 100644 index 0000000..9498d31 --- /dev/null +++ b/src/visualizers/dbscan/start.tsx @@ -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
+ + +
; +}