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