|
|
|
|
|
{% extends "layout.html" %}
|
|
|
|
|
|
{% block content %}
|
|
|
<!DOCTYPE html>
|
|
|
<html lang="en">
|
|
|
<head>
|
|
|
<meta charset="UTF-8">
|
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
|
<title>Dynamic DBSCAN Clustering Visualization</title>
|
|
|
<script src="https://cdn.tailwindcss.com"></script>
|
|
|
<script src="https://cdn.plot.ly/plotly-latest.min.js"></script>
|
|
|
|
|
|
<style>
|
|
|
|
|
|
|
|
|
body {
|
|
|
font-family: sans-serif;
|
|
|
}
|
|
|
</style>
|
|
|
</head>
|
|
|
<body class="bg-gray-100 min-h-screen flex flex-col items-center p-6">
|
|
|
<h1 class="text-4xl font-bold text-gray-800 mb-8 text-center">Dynamic DBSCAN Clustering Visualization</h1>
|
|
|
|
|
|
<div class="container mx-auto bg-white shadow-lg rounded-lg p-6 mb-8 w-full max-w-6xl">
|
|
|
<h2 class="text-3xl font-bold text-gray-800 mb-4">Understanding DBSCAN</h2>
|
|
|
<p class="mb-4 text-gray-700">
|
|
|
DBSCAN is a <strong class="font-semibold">density-based clustering algorithm</strong> that groups data points that are closely packed together and marks outliers as noise based on their density in the feature space. It identifies clusters as dense regions in the data space separated by areas of lower density. Unlike K-Means or hierarchical clustering which assumes clusters are compact and spherical, DBSCAN performs well in handling real-world data irregularities such as:
|
|
|
</p>
|
|
|
<ul class="list-disc list-inside mb-4 text-gray-700 ml-4">
|
|
|
<li><strong class="font-semibold">Arbitrary-Shaped Clusters:</strong> Clusters can take any shape, not just circular or convex.</li>
|
|
|
<li><strong class="font-semibold">Noise and Outliers:</strong> It effectively identifies and handles noise points without assigning them to any cluster.</li>
|
|
|
</ul>
|
|
|
<p class="mb-4 text-gray-700">
|
|
|
The figure below shows a dataset with clustering algorithms: K-Means and Hierarchical handling compact, spherical clusters with varying noise tolerance while DBSCAN manages arbitrary-shaped clusters and noise handling.
|
|
|
</p>
|
|
|
|
|
|
<h3 class="text-2xl font-bold text-gray-800 mb-3">Key Parameters in DBSCAN</h3>
|
|
|
<p class="mb-2 text-gray-700">
|
|
|
<strong class="font-semibold">1. eps ($$\epsilon$$):</strong> This defines the radius of the neighborhood around a data point. If the distance between two points is less than or equal to $$\epsilon$$, they are considered neighbors. A common method to determine $\epsilon$ is by analyzing the <strong class="font-semibold">k-distance graph</strong>. Choosing the right $\epsilon$ is important:
|
|
|
</p>
|
|
|
<ul class="list-disc list-inside mb-4 text-gray-700 ml-4">
|
|
|
<li>If $$\epsilon$$ is too small, most points will be classified as noise.</li>
|
|
|
<li>If $$\epsilon$$ is too large, clusters may merge and the algorithm may fail to distinguish between them.</li>
|
|
|
</ul>
|
|
|
<p class="mb-2 text-gray-700">
|
|
|
<strong class="font-semibold">2. MinPts:</strong> This is the minimum number of points required within the $\epsilon$ radius to form a dense region. A general rule of thumb is to set MinPts $$$\ge D+1$$, where $$D$$ is the number of dimensions in the dataset.
|
|
|
</p>
|
|
|
|
|
|
<h3 class="text-2xl font-bold text-gray-800 mb-3">How Does DBSCAN Work?</h3>
|
|
|
<p class="mb-4 text-gray-700">
|
|
|
DBSCAN works by categorizing data points into three types:
|
|
|
</p>
|
|
|
<ul class="list-disc list-inside mb-4 text-gray-700 ml-4">
|
|
|
<li><strong class="font-semibold">Core points:</strong> which have a sufficient number of neighbors within a specified radius (epsilon).</li>
|
|
|
<li><strong class="font-semibold">Border points:</strong> which are near core points but lack enough neighbors to be core points themselves.</li>
|
|
|
<li><strong class="font-semibold">Noise points:</strong> which do not belong to any cluster.</li>
|
|
|
</ul>
|
|
|
|
|
|
<h3 class="text-2xl font-bold text-gray-800 mb-3">Steps in the DBSCAN Algorithm</h3>
|
|
|
<ul class="list-decimal list-inside mb-4 text-gray-700 ml-4">
|
|
|
<li><strong class="font-semibold">Identify Core Points:</strong> For each point in the dataset, count the number of points within its $\epsilon$ neighborhood. If the count meets or exceeds MinPts, mark the point as a core point.</li>
|
|
|
<li><strong class="font-semibold">Form Clusters:</strong> For each core point that is not already assigned to a cluster, create a new cluster. Recursively find all <strong class="font-semibold">density-connected points</strong> (i.e., points within the $\epsilon$ radius of the core point) and add them to the cluster.</li>
|
|
|
<li><strong class="font-semibold">Density Connectivity:</strong> Two points $a$ and $b$ are <strong class="font-semibold">density-connected</strong> if there exists a chain of points where each point is within the $\epsilon$ radius of the next, and at least one point in the chain is a core point. This chaining process ensures that all points in a cluster are connected through a series of dense regions.</li>
|
|
|
<li><strong class="font-semibold">Label Noise Points:</strong> After processing all points, any point that does not belong to a cluster is labeled as <strong class="font-semibold">noise</strong>.</li>
|
|
|
</ul>
|
|
|
|
|
|
<h3 class="text-2xl font-bold text-gray-800 mb-3">How this DBSCAN Visualization Handles User-Added Data</h3>
|
|
|
<p class="mb-4 text-gray-700">
|
|
|
In this interactive visualization, when you click the "Add New Point & Cluster" button, the new point you specify is appended to the existing dataset. Importantly, the entire DBSCAN clustering algorithm is then <strong class="font-semibold">re-run from scratch</strong> on this updated dataset. This means that:
|
|
|
</p>
|
|
|
<ul class="list-disc list-inside mb-4 text-gray-700 ml-4">
|
|
|
<li>The new point is treated as part of the original data, and its type (core, border, or noise) and cluster assignment are determined by the algorithm based on its density relative to all other points.</li>
|
|
|
<li>The cluster assignments and even the types (core/border/noise) of previously existing points might change, as the addition of a new point can alter the density landscape and connectivity.</li>
|
|
|
<li>The visualization dynamically updates to reflect these new cluster structures, showing the real-time effect of adding data on the DBSCAN clustering process.</li>
|
|
|
</ul>
|
|
|
</div>
|
|
|
<a href="/kmeans-Dbscan-image">
|
|
|
<button class="inline-block bg-gray-200 hover:bg-gray-300 centre text-gray-800 px-4 py-2 rounded shadow">🖼️ KMeans + DBSCAN Image Clustering</button>
|
|
|
</a>
|
|
|
<div class="container mx-auto bg-white shadow-lg rounded-lg p-6 w-full max-w-6xl">
|
|
|
<div class="controls grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 mb-6">
|
|
|
<div class="flex flex-col">
|
|
|
<label for="dimensions" class="text-gray-700 text-sm font-semibold mb-1">Dimensions:</label>
|
|
|
<select id="dimensions" class="p-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">
|
|
|
<option value="2D">2D</option>
|
|
|
<option value="3D">3D</option>
|
|
|
</select>
|
|
|
</div>
|
|
|
|
|
|
<div class="flex flex-col">
|
|
|
<label for="eps" class="text-gray-700 text-sm font-semibold mb-1">Epsilon (eps):</label>
|
|
|
<input type="number" id="eps" value="1.5" step="0.1" min="0.1" max="5" class="p-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">
|
|
|
</div>
|
|
|
|
|
|
<div class="flex flex-col">
|
|
|
<label for="min-pts" class="text-gray-700 text-sm font-semibold mb-1">Min. Points (minPts):</label>
|
|
|
<input type="number" id="min-pts" value="5" min="2" max="20" class="p-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">
|
|
|
</div>
|
|
|
|
|
|
<div class="flex flex-col">
|
|
|
<label for="data-points-total" class="text-gray-700 text-sm font-semibold mb-1">Total Data Points:</label>
|
|
|
<input type="number" id="data-points-total" value="150" min="50" max="1000" step="10" class="p-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">
|
|
|
</div>
|
|
|
|
|
|
<div class="flex flex-col">
|
|
|
<label for="new-point-x" class="text-gray-700 text-sm font-semibold mb-1">New Point X:</label>
|
|
|
<input type="number" id="new-point-x" value="0" step="0.1" class="p-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">
|
|
|
</div>
|
|
|
<div class="flex flex-col" id="new-point-y-wrapper">
|
|
|
<label for="new-point-y" class="text-gray-700 text-sm font-semibold mb-1">New Point Y:</label>
|
|
|
<input type="number" id="new-point-y" value="0" step="0.1" class="p-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">
|
|
|
</div>
|
|
|
<div class="flex flex-col hidden" id="new-point-z-wrapper">
|
|
|
<label for="new-point-z" class="text-gray-700 text-sm font-semibold mb-1">New Point Z:</label>
|
|
|
<input type="number" id="new-point-z" value="0" step="0.1" class="p-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">
|
|
|
</div>
|
|
|
|
|
|
<button id="add-point-btn" class="bg-blue-500 hover:bg-blue-600 text-white font-bold py-2 px-4 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">
|
|
|
Add New Point & Cluster
|
|
|
</button>
|
|
|
<button id="reset-data-btn" class="bg-red-500 hover:bg-red-600 text-white font-bold py-2 px-4 rounded-md focus:outline-none focus:ring-2 focus:ring-red-500">
|
|
|
Reset Data & Recluster
|
|
|
</button>
|
|
|
</div>
|
|
|
|
|
|
<div id="plotly-graph" class="w-full h-96 md:h-[500px] lg:h-[600px] border border-gray-300 rounded-lg"></div>
|
|
|
</div>
|
|
|
|
|
|
<script>
|
|
|
|
|
|
class DBSCAN {
|
|
|
constructor(eps, minPts) {
|
|
|
this.eps = eps;
|
|
|
this.minPts = minPts;
|
|
|
this.clusters = [];
|
|
|
this.noisePoints = new Set();
|
|
|
this.pointLabels = [];
|
|
|
this.X = [];
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
_euclideanDistance(p1, p2) {
|
|
|
let sum = 0;
|
|
|
for (let i = 0; i < p1.length; i++) {
|
|
|
sum += Math.pow(p1[i] - p2[i], 2);
|
|
|
}
|
|
|
return Math.sqrt(sum);
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
_getNeighbors(pointIndex) {
|
|
|
const neighbors = [];
|
|
|
const currentPoint = this.X[pointIndex];
|
|
|
for (let i = 0; i < this.X.length; i++) {
|
|
|
if (i === pointIndex) continue;
|
|
|
if (this._euclideanDistance(currentPoint, this.X[i]) <= this.eps) {
|
|
|
neighbors.push(i);
|
|
|
}
|
|
|
}
|
|
|
return neighbors;
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
fit(X) {
|
|
|
if (X.length === 0) {
|
|
|
console.warn("No data for DBSCAN clustering.");
|
|
|
this.clusters = [];
|
|
|
this.noisePoints = new Set();
|
|
|
this.pointLabels = [];
|
|
|
this.X = [];
|
|
|
return [];
|
|
|
}
|
|
|
|
|
|
this.X = X;
|
|
|
this.pointLabels = Array(X.length).fill(0);
|
|
|
this.clusters = [];
|
|
|
this.noisePoints = new Set();
|
|
|
let clusterId = 0;
|
|
|
|
|
|
for (let i = 0; i < X.length; i++) {
|
|
|
if (this.pointLabels[i] !== 0) {
|
|
|
continue;
|
|
|
}
|
|
|
|
|
|
const neighbors = this._getNeighbors(i);
|
|
|
|
|
|
if (neighbors.length < this.minPts) {
|
|
|
this.pointLabels[i] = -1;
|
|
|
this.noisePoints.add(i);
|
|
|
} else {
|
|
|
clusterId++;
|
|
|
this.pointLabels[i] = clusterId;
|
|
|
this.clusters.push(new Set([i]));
|
|
|
|
|
|
let seedSet = [...neighbors];
|
|
|
while (seedSet.length > 0) {
|
|
|
const currentNeighborIndex = seedSet.shift();
|
|
|
|
|
|
if (this.pointLabels[currentNeighborIndex] === -1) {
|
|
|
this.pointLabels[currentNeighborIndex] = clusterId;
|
|
|
this.noisePoints.delete(currentNeighborIndex);
|
|
|
this.clusters[clusterId - 1].add(currentNeighborIndex);
|
|
|
}
|
|
|
|
|
|
if (this.pointLabels[currentNeighborIndex] !== 0) {
|
|
|
|
|
|
continue;
|
|
|
}
|
|
|
|
|
|
this.pointLabels[currentNeighborIndex] = clusterId;
|
|
|
this.clusters[clusterId - 1].add(currentNeighborIndex);
|
|
|
|
|
|
const currentNeighborNeighbors = this._getNeighbors(currentNeighborIndex);
|
|
|
if (currentNeighborNeighbors.length >= this.minPts) {
|
|
|
|
|
|
for (const n of currentNeighborNeighbors) {
|
|
|
if (this.pointLabels[n] === 0 || this.pointLabels[n] === -1) {
|
|
|
seedSet.push(n);
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
return this.pointLabels;
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
predict(observation) {
|
|
|
if (this.clusters.length === 0) {
|
|
|
return -1;
|
|
|
}
|
|
|
|
|
|
|
|
|
for (let i = 0; i < this.X.length; i++) {
|
|
|
if (this._euclideanDistance(observation, this.X[i]) <= this.eps) {
|
|
|
const label = this.pointLabels[i];
|
|
|
if (label !== -1) {
|
|
|
|
|
|
|
|
|
|
|
|
return label;
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
return -1;
|
|
|
}
|
|
|
|
|
|
|
|
|
getPointTypes() {
|
|
|
const types = Array(this.X.length).fill(null);
|
|
|
|
|
|
for (let i = 0; i < this.X.length; i++) {
|
|
|
if (this.pointLabels[i] === -1) {
|
|
|
types[i] = 2;
|
|
|
} else {
|
|
|
const neighbors = this._getNeighbors(i);
|
|
|
if (neighbors.length >= this.minPts) {
|
|
|
types[i] = 0;
|
|
|
} else {
|
|
|
types[i] = 1;
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
return types;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
|
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
|
const plotlyGraph = document.getElementById('plotly-graph');
|
|
|
const dimensionsSelect = document.getElementById('dimensions');
|
|
|
const epsInput = document.getElementById('eps');
|
|
|
const minPtsInput = document.getElementById('min-pts');
|
|
|
const dataPointsTotalInput = document.getElementById('data-points-total');
|
|
|
const addPointBtn = document.getElementById('add-point-btn');
|
|
|
const resetDataBtn = document.getElementById('reset-data-btn');
|
|
|
const newPointXInput = document.getElementById('new-point-x');
|
|
|
const newPointYInput = document.getElementById('new-point-y');
|
|
|
const newPointZInput = document.getElementById('new-point-z');
|
|
|
const newPointYWrapper = document.getElementById('new-point-y-wrapper');
|
|
|
const newPointZWrapper = document.getElementById('new-point-z-wrapper');
|
|
|
|
|
|
let currentData = [];
|
|
|
let dbscanModel;
|
|
|
let currentDimensions = dimensionsSelect.value;
|
|
|
|
|
|
const clusterColorscale = 'Plotly3';
|
|
|
const noiseColor = '#808080';
|
|
|
|
|
|
|
|
|
|
|
|
function generateRandomData(totalPoints, dimensions, numClustersHint = 3, spread = 2) {
|
|
|
const data = [];
|
|
|
|
|
|
const centers = Array.from({ length: numClustersHint }, () => ({
|
|
|
x: (Math.random() - 0.5) * 15,
|
|
|
y: (Math.random() - 0.5) * 15,
|
|
|
z: (Math.random() - 0.5) * 15
|
|
|
}));
|
|
|
|
|
|
for (let i = 0; i < totalPoints; i++) {
|
|
|
|
|
|
const center = centers[Math.floor(Math.random() * numClustersHint)];
|
|
|
const x = center.x + (Math.random() - 0.5) * spread;
|
|
|
const y = center.y + (Math.random() - 0.5) * spread;
|
|
|
|
|
|
if (dimensions === "2D") {
|
|
|
data.push({ x: x, y: y });
|
|
|
} else {
|
|
|
const z = center.z + (Math.random() - 0.5) * spread;
|
|
|
data.push({ x: x, y: y, z: z });
|
|
|
}
|
|
|
}
|
|
|
return data;
|
|
|
}
|
|
|
|
|
|
function prepareDataForModel(data) {
|
|
|
return data.map(d => {
|
|
|
if (currentDimensions === "2D") {
|
|
|
return [d.x, d.y];
|
|
|
} else {
|
|
|
return [d.x, d.y, d.z];
|
|
|
}
|
|
|
});
|
|
|
}
|
|
|
|
|
|
function createPlotlyTraces(data, clusterAssignments, pointTypes, type) {
|
|
|
const traces = [];
|
|
|
const uniqueClusters = [...new Set(clusterAssignments)].filter(id => id !== -1).sort((a,b)=>a-b);
|
|
|
const maxClusterId = Math.max(...uniqueClusters, 0);
|
|
|
|
|
|
|
|
|
const symbolMap = { 0: 'circle', 1: 'square', 2: 'diamond' };
|
|
|
const sizeMap = { 0: 10, 1: 8, 2: 6 };
|
|
|
const opacityMap = { 0: 1.0, 1: 0.8, 2: 0.6 };
|
|
|
|
|
|
|
|
|
uniqueClusters.forEach(clusterId => {
|
|
|
const clusterData = data.filter((_, i) => clusterAssignments[i] === clusterId);
|
|
|
const clusterPointTypes = pointTypes.filter((_, i) => clusterAssignments[i] === clusterId);
|
|
|
|
|
|
|
|
|
[0, 1].forEach(pointType => {
|
|
|
const filteredData = clusterData.filter((_, i) => clusterPointTypes[i] === pointType);
|
|
|
if (filteredData.length === 0) return;
|
|
|
|
|
|
if (type === "2D") {
|
|
|
traces.push({
|
|
|
x: filteredData.map(d => d.x),
|
|
|
y: filteredData.map(d => d.y),
|
|
|
mode: 'markers',
|
|
|
type: 'scatter',
|
|
|
name: `Cluster ${clusterId} (${pointType === 0 ? 'Core' : 'Border'})`,
|
|
|
marker: {
|
|
|
color: clusterId,
|
|
|
colorscale: clusterColorscale,
|
|
|
cmin: 0, cmax: maxClusterId,
|
|
|
symbol: symbolMap[pointType],
|
|
|
size: sizeMap[pointType],
|
|
|
opacity: opacityMap[pointType],
|
|
|
line: { color: 'white', width: 1 }
|
|
|
},
|
|
|
legendgroup: `cluster${clusterId}`,
|
|
|
showlegend: true
|
|
|
});
|
|
|
} else {
|
|
|
traces.push({
|
|
|
x: filteredData.map(d => d.x),
|
|
|
y: filteredData.map(d => d.y),
|
|
|
z: filteredData.map(d => d.z),
|
|
|
mode: 'markers',
|
|
|
type: 'scatter3d',
|
|
|
name: `Cluster ${clusterId} (${pointType === 0 ? 'Core' : 'Border'})`,
|
|
|
marker: {
|
|
|
color: clusterId,
|
|
|
colorscale: clusterColorscale,
|
|
|
cmin: 0, cmax: maxClusterId,
|
|
|
symbol: symbolMap[pointType],
|
|
|
size: sizeMap[pointType],
|
|
|
opacity: opacityMap[pointType],
|
|
|
line: { color: 'white', width: 1 }
|
|
|
},
|
|
|
legendgroup: `cluster${clusterId}`,
|
|
|
showlegend: true
|
|
|
});
|
|
|
}
|
|
|
});
|
|
|
});
|
|
|
|
|
|
|
|
|
const noiseData = data.filter((_, i) => clusterAssignments[i] === -1);
|
|
|
if (noiseData.length > 0) {
|
|
|
if (type === "2D") {
|
|
|
traces.push({
|
|
|
x: noiseData.map(d => d.x),
|
|
|
y: noiseData.map(d => d.y),
|
|
|
mode: 'markers',
|
|
|
type: 'scatter',
|
|
|
name: 'Noise Points',
|
|
|
marker: {
|
|
|
color: noiseColor,
|
|
|
symbol: symbolMap[2],
|
|
|
size: sizeMap[2],
|
|
|
opacity: opacityMap[2],
|
|
|
line: { color: 'white', width: 1 }
|
|
|
},
|
|
|
showlegend: true
|
|
|
});
|
|
|
} else {
|
|
|
traces.push({
|
|
|
x: noiseData.map(d => d.x),
|
|
|
y: noiseData.map(d => d.y),
|
|
|
z: noiseData.map(d => d.z),
|
|
|
mode: 'markers',
|
|
|
type: 'scatter3d',
|
|
|
name: 'Noise Points',
|
|
|
marker: {
|
|
|
color: noiseColor,
|
|
|
symbol: symbolMap[2],
|
|
|
size: sizeMap[2],
|
|
|
opacity: opacityMap[2],
|
|
|
line: { color: 'white', width: 1 }
|
|
|
},
|
|
|
showlegend: true
|
|
|
});
|
|
|
}
|
|
|
}
|
|
|
return traces;
|
|
|
}
|
|
|
|
|
|
function getAxisRanges(data, dimensions) {
|
|
|
if (data.length === 0) {
|
|
|
return {
|
|
|
x: [-10, 10],
|
|
|
y: [-10, 10],
|
|
|
z: [-10, 10]
|
|
|
};
|
|
|
}
|
|
|
|
|
|
const minX = Math.min(...data.map(d => d.x)) - 2;
|
|
|
const maxX = Math.max(...data.map(d => d.x)) + 2;
|
|
|
const minY = Math.min(...data.map(d => d.y)) - 2;
|
|
|
const maxY = Math.max(...data.map(d => d.y)) + 2;
|
|
|
|
|
|
if (dimensions === "2D") {
|
|
|
return {
|
|
|
x: [minX, maxX],
|
|
|
y: [minY, maxY]
|
|
|
};
|
|
|
} else {
|
|
|
const minZ = Math.min(...data.map(d => d.z)) - 2;
|
|
|
const maxZ = Math.max(...data.map(d => d.z)) + 2;
|
|
|
return {
|
|
|
x: [minX, maxX],
|
|
|
y: [minY, maxY],
|
|
|
z: [minZ, maxZ]
|
|
|
};
|
|
|
}
|
|
|
}
|
|
|
|
|
|
function updateGraph() {
|
|
|
const X = prepareDataForModel(currentData);
|
|
|
if (X.length === 0) {
|
|
|
Plotly.purge(plotlyGraph);
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
dbscanModel = new DBSCAN(
|
|
|
parseFloat(epsInput.value),
|
|
|
parseInt(minPtsInput.value)
|
|
|
);
|
|
|
const clusterAssignments = dbscanModel.fit(X);
|
|
|
const pointTypes = dbscanModel.getPointTypes();
|
|
|
|
|
|
const plotlyTraces = createPlotlyTraces(currentData, clusterAssignments, pointTypes, currentDimensions);
|
|
|
let layout;
|
|
|
|
|
|
const ranges = getAxisRanges(currentData, currentDimensions);
|
|
|
|
|
|
if (currentDimensions === "2D") {
|
|
|
layout = {
|
|
|
title: 'DBSCAN Clustering (2D)',
|
|
|
xaxis: { title: 'Feature 1', range: ranges.x },
|
|
|
yaxis: { title: 'Feature 2', range: ranges.y },
|
|
|
hovermode: 'closest',
|
|
|
showlegend: true
|
|
|
};
|
|
|
Plotly.newPlot(plotlyGraph, plotlyTraces, layout);
|
|
|
|
|
|
} else {
|
|
|
layout = {
|
|
|
title: 'DBSCAN Clustering (3D)',
|
|
|
scene: {
|
|
|
xaxis: { title: 'Feature 1', range: ranges.x },
|
|
|
yaxis: { title: 'Feature 2', range: ranges.y },
|
|
|
zaxis: { title: 'Feature 3', range: ranges.z },
|
|
|
},
|
|
|
hovermode: 'closest',
|
|
|
showlegend: true
|
|
|
};
|
|
|
Plotly.newPlot(plotlyGraph, plotlyTraces, layout);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
dimensionsSelect.addEventListener('change', (event) => {
|
|
|
currentDimensions = event.target.value;
|
|
|
if (currentDimensions === "2D") {
|
|
|
newPointZWrapper.classList.add('hidden');
|
|
|
} else {
|
|
|
newPointZWrapper.classList.remove('hidden');
|
|
|
}
|
|
|
|
|
|
currentData = generateRandomData(
|
|
|
parseInt(dataPointsTotalInput.value),
|
|
|
currentDimensions
|
|
|
);
|
|
|
updateGraph();
|
|
|
});
|
|
|
|
|
|
epsInput.addEventListener('change', updateGraph);
|
|
|
minPtsInput.addEventListener('change', updateGraph);
|
|
|
|
|
|
dataPointsTotalInput.addEventListener('change', () => {
|
|
|
currentData = generateRandomData(
|
|
|
parseInt(dataPointsTotalInput.value),
|
|
|
currentDimensions
|
|
|
);
|
|
|
updateGraph();
|
|
|
});
|
|
|
|
|
|
addPointBtn.addEventListener('click', () => {
|
|
|
const x = parseFloat(newPointXInput.value);
|
|
|
const y = parseFloat(newPointYInput.value);
|
|
|
let newPointFeatures;
|
|
|
|
|
|
if (currentDimensions === "2D") {
|
|
|
newPointFeatures = { x: x, y: y };
|
|
|
} else {
|
|
|
const z = parseFloat(newPointZInput.value);
|
|
|
newPointFeatures = { x: x, y: y, z: z };
|
|
|
}
|
|
|
|
|
|
currentData.push(newPointFeatures);
|
|
|
updateGraph();
|
|
|
});
|
|
|
|
|
|
resetDataBtn.addEventListener('click', () => {
|
|
|
currentData = generateRandomData(
|
|
|
parseInt(dataPointsTotalInput.value),
|
|
|
currentDimensions
|
|
|
);
|
|
|
updateGraph();
|
|
|
});
|
|
|
|
|
|
|
|
|
currentData = generateRandomData(
|
|
|
parseInt(dataPointsTotalInput.value),
|
|
|
currentDimensions
|
|
|
);
|
|
|
updateGraph();
|
|
|
});
|
|
|
</script>
|
|
|
|
|
|
</body>
|
|
|
</html>
|
|
|
{% endblock %} |