Skip to content

Commit e1ee0bd

Browse files
committed
Initial.
0 parents  commit e1ee0bd

11 files changed

+1053
-0
lines changed

README.md

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
![Interactive K-means](preview.gif)
2+
3+
# Interactive K-means
4+
5+
Visualize and interact with the clustering algorithm k-means.
6+
Try it at [lettier.com/kmeans](http://www.lettier.com/kmeans/).
7+
Read more about [k-means](https://lettier.github.io/posts/2016-04-24-k-means-from-scratch.html).
8+
9+
## Download & Run
10+
11+
```bash
12+
git clone https://github.com/lettier/interactivekmeans.git
13+
cd interactivekmeans
14+
nohup python -m http.server &> /dev/null &
15+
python -mwebbrowser http://localhost:8000
16+
```
17+
18+
## Directions
19+
20+
- Lay down data points by clicking the mouse.
21+
- You can also use the `scatter` button located in the controls.
22+
- Set your value for `k` and `maxIterations`.
23+
- Press `runKMeans` to cluster the on-screen data points.
24+
- Remove data points by clicking on them.
25+
- Use the silhouette coefficient to find the optimal `k`.
26+
27+
(C) 2015 David Lettier.
28+
[lettier.com](https://www.lettier.com/)

css/index.css

+57
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/*
2+
* David Lettier
3+
* (C) 2015
4+
* http://www.lettier.com/
5+
*/
6+
7+
html {
8+
position: relative;
9+
min-height: 100%;
10+
}
11+
body {
12+
background-color: #3C3C3C;
13+
width: 100%;
14+
height: 100%;
15+
margin: 0px;
16+
overflow: hidden;
17+
font-family: Raleway;
18+
letter-spacing: 1.17px;
19+
-webkit-touch-callout: none;
20+
-webkit-user-select: none;
21+
-khtml-user-select: none;
22+
-moz-user-select: none;
23+
-ms-user-select: none;
24+
user-select: none;
25+
}
26+
a {
27+
color: #eef;
28+
}
29+
a:hover {
30+
color: #eef;
31+
text-decoration: none;
32+
}
33+
.footer {
34+
position: absolute;
35+
bottom: 0;
36+
width: 100%;
37+
height: 70px;
38+
background-color: rgba(68, 171, 218, 0.8);
39+
font-size: 18px;
40+
line-height: 70px;
41+
color: #fff;
42+
}
43+
.fa-github {
44+
line-height: 70px;
45+
color: #fff;
46+
}
47+
.align-right {
48+
text-align: right;
49+
}
50+
.instructions{
51+
font-weight: bold;
52+
}
53+
.logo {
54+
width: 50px;
55+
vertical-align: sub;
56+
}
57+

images/logo.png

16.9 KB
Loading

index.html

+45
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head>
4+
<title>K-Means | Lettier</title>
5+
<meta property='og:title' content='Interactive K-Means'>
6+
<meta property='og:url' content='http://www.lettier.com/kmeans'>
7+
<meta property='og:image' content='http://i.imgur.com/w7SwySp.png'>
8+
<meta property='og:image:width' content='500'>
9+
<meta property='og:image:heigth' content='290'>
10+
<meta property='og:description' content='Interactive K-Means clustering with silhouette coefficient.'>
11+
<script src='scripts/utils.js' type='text/javascript'></script>
12+
<script src='scripts/mean.js' type='text/javascript'></script>
13+
<script src='scripts/data_point.js' type='text/javascript'></script>
14+
<script src='scripts/kmeans.js' type='text/javascript'></script>
15+
<script src='scripts/silhouette_coefficient.js' type='text/javascript'></script>
16+
<script src='https://code.createjs.com/easeljs-0.8.0.min.js' type='text/javascript'></script>
17+
<script src='https://cdnjs.cloudflare.com/ajax/libs/dat-gui/0.5/dat.gui.min.js' type='text/javascript'></script>
18+
<link rel='stylesheet' type='text/css' href='https://maxcdn.bootstrapcdn.com/bootstrap/3.3.4/css/bootstrap.min.css'>
19+
<link rel='stylesheet' href='//maxcdn.bootstrapcdn.com/font-awesome/4.3.0/css/font-awesome.min.css'>
20+
<link href='http://fonts.googleapis.com/css?family=Raleway' rel='stylesheet' type='text/css'>
21+
<link rel='stylesheet' type='text/css' href='css/index.css'>
22+
</head>
23+
<body>
24+
<script src='scripts/canvas.js' type='text/javascript'></script>
25+
<footer class='footer'>
26+
<div class='container-fluid'>
27+
<div class='row'>
28+
<div class='col-md-11'>
29+
<span class='instructions'>
30+
Click to lay down a dot. &nbsp;Click on a dot to remove.
31+
</span>
32+
</div>
33+
<div class='col-md-1 align-right'>
34+
<a href='https://github.com/lettier/interactivekmeans'>
35+
<i class='fa fa-github fa-3x'></i>
36+
</a>
37+
<a href='http://www.lettier.com/'>
38+
<img class='logo' src='images/logo.png'>
39+
</a>
40+
</div>
41+
</div>
42+
</div>
43+
</foot>
44+
</body>
45+
</html>

preview.gif

4.8 MB
Loading

scripts/canvas.js

+209
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
/*
2+
* David Lettier
3+
* (C) 2015
4+
* http://www.lettier.com/
5+
*/
6+
7+
var DATAPOINTSIZE = 15;
8+
var DATAPOINTCOLOR = '#999';
9+
10+
var VISUALMEANCOLOR = '#000';
11+
var VISUALMEANSIZE = 8;
12+
13+
var MAXKMEANSITERATIONS = 20000;
14+
var MAXKMEANSK = 10;
15+
16+
var canvas = document.createElement('canvas');
17+
var stage = null;
18+
19+
var kmeans = null;
20+
21+
var clusterColors = uniqueColors(MAXKMEANSK, 360 / MAXKMEANSK);
22+
23+
var makeVisualDataPoint = function (position) {
24+
var id = uniqueNumber();
25+
var shape = new createjs.Shape();
26+
var dataPoint = new DataPoint(
27+
[position.x, position.y]
28+
);
29+
if (kmeans !== null) {
30+
kmeans.assign(dataPoint);
31+
color = clusterColors[dataPoint.meanId];
32+
shape.graphics.setStrokeStyle(
33+
1
34+
).beginFill(
35+
color
36+
).drawCircle(
37+
0,
38+
0,
39+
DATAPOINTSIZE
40+
);
41+
} else {
42+
shape.graphics.setStrokeStyle(
43+
1
44+
).beginFill(
45+
DATAPOINTCOLOR
46+
).drawCircle(
47+
0,
48+
0,
49+
DATAPOINTSIZE
50+
);
51+
}
52+
shape.x = dataPoint.features[0];
53+
shape.y = dataPoint.features[1];
54+
shape.id = id;
55+
dataPointLayer.addChild(shape);
56+
shape.on('click', function (event) {
57+
var id = this.id;
58+
dataPointsTemp = dataPoints.filter(function (dataPoint) {
59+
return dataPoint.id !== id;
60+
});
61+
dataPoints = dataPointsTemp;
62+
dataPointLayer.removeChild(this);
63+
stage.update();
64+
event.stopImmediatePropagation();
65+
});
66+
dataPoint.id = id;
67+
dataPoint.shape = shape;
68+
dataPoints.push(dataPoint);
69+
stage.update();
70+
};
71+
72+
var dataPointLayer = new createjs.Container();
73+
var dataPointLayerBase = new createjs.Shape();
74+
var dataPoints = [];
75+
var visualMeans = [];
76+
77+
var guiControls = null;
78+
var gui = null;
79+
80+
var GUIControls = function () {
81+
var that = this;
82+
83+
this.k = MAXKMEANSK;
84+
this.maxIterations = MAXKMEANSITERATIONS;
85+
86+
this.reset = function () {
87+
dataPoints.forEach(function (dataPoint) {
88+
dataPointLayer.removeChild(dataPoint.shape);
89+
});
90+
visualMeans.forEach(function (visualMean) {
91+
dataPointLayer.removeChild(visualMean);
92+
});
93+
kmeans = null;
94+
that.silhouetteCoefficient = 'Press runKMeans.';
95+
dataPoints.length = 0;
96+
visualMeans.length = 0;
97+
stage.update();
98+
};
99+
100+
this.scatter = function () {
101+
var minX = 0 + (DATAPOINTSIZE * 2);
102+
var minY = 0 + (DATAPOINTSIZE * 2);
103+
var maxX = canvas.width - (DATAPOINTSIZE * 2);
104+
var maxY = canvas.height - (DATAPOINTSIZE * 2);
105+
for (var i=0; i<15; ++i) {
106+
var valueX = randomValueInRange(minX, maxX);
107+
var valueY = randomValueInRange(minY, maxY);
108+
makeVisualDataPoint({x: valueX, y: valueY});
109+
}
110+
};
111+
112+
this.runKMeans = function () {
113+
var updateSilhouetteCoefficient = function (silhouetteCoefficient) {
114+
that.silhouetteCoefficient = silhouetteCoefficient;
115+
};
116+
var calculateSilhouetteCoefficient = function (kmeans) {
117+
silhouetteCoefficient = new SilhouetteCoefficient(
118+
{
119+
dataPointClusterMembersFunction: kmeans.dataPointClusterMembers.bind(
120+
kmeans
121+
),
122+
dataPointNearestClusterMembersFunction: kmeans.dataPointNearestClusterMembers.bind(
123+
kmeans
124+
),
125+
distanceFunction: kmeans.euclideanDistance.bind(kmeans)
126+
}
127+
);
128+
129+
that.silhouetteCoefficient = 'Calculating...';
130+
131+
silhouetteCoefficient.model(kmeans.dataPoints, updateSilhouetteCoefficient);
132+
};
133+
var onIterate = function (kmeans) {
134+
var i = 0;
135+
if (dataPoints.length === 0) {return;}
136+
kmeans.clusters.forEach(function (cluster) {
137+
var color = clusterColors[i];
138+
if (cluster.length === 0) {return;}
139+
cluster.forEach(function (dataPoint) {
140+
dataPoint.shape.graphics.clear();
141+
dataPoint.shape.graphics.beginFill(color).drawCircle(
142+
0, 0, DATAPOINTSIZE
143+
);
144+
});
145+
stage.update();
146+
i += 1;
147+
});
148+
visualMeans.forEach(function (visualMean) {
149+
dataPointLayer.removeChild(visualMean);
150+
});
151+
visualMeans.length = 0;
152+
kmeans.means.forEach(function (mean) {
153+
var shape = new createjs.Shape();
154+
shape.graphics.beginFill(VISUALMEANCOLOR).drawRect(0, 0, VISUALMEANSIZE, VISUALMEANSIZE);
155+
shape.x = mean.features[0];
156+
shape.y = mean.features[1];
157+
dataPointLayer.addChild(shape);
158+
visualMeans.push(shape);
159+
});
160+
stage.update();
161+
};
162+
163+
if (dataPoints.length === 0) {return;}
164+
165+
if (that.k > dataPoints.length) {that.k = dataPoints.length;}
166+
167+
kmeans = new KMeans({
168+
dataPoints: dataPoints,
169+
maxIterations: Math.round(that.maxIterations),
170+
k: Math.round(that.k),
171+
onIterate: onIterate,
172+
onComplete: calculateSilhouetteCoefficient
173+
});
174+
175+
kmeans.iterate();
176+
};
177+
178+
this.silhouetteCoefficient = 'Press runKMeans.';
179+
};
180+
181+
guiControls = new GUIControls();
182+
gui = new dat.GUI({width: 330});
183+
184+
canvas.id = 'canvas';
185+
document.body.appendChild(canvas);
186+
stage = new createjs.Stage('canvas');
187+
stage.canvas.width = window.innerWidth;
188+
stage.canvas.height = window.innerHeight;
189+
190+
gui.add(guiControls, 'k', 1, MAXKMEANSK).listen();
191+
gui.add(guiControls, 'maxIterations', 1, MAXKMEANSITERATIONS);
192+
gui.add(guiControls, 'reset');
193+
gui.add(guiControls, 'scatter');
194+
gui.add(guiControls, 'runKMeans');
195+
gui.add(guiControls, 'silhouetteCoefficient').listen();
196+
197+
dataPointLayer.name = 'dataPointLayer';
198+
dataPointLayer.x = 0;
199+
dataPointLayer.y = 0;
200+
dataPointLayerBase.graphics.beginFill('#3C3C3C').drawRect(
201+
0, 0, window.innerWidth, window.innerHeight
202+
);
203+
dataPointLayer.addChild(dataPointLayerBase);
204+
stage.addChild(dataPointLayer);
205+
dataPointLayer.on('click', function (event) {
206+
makeVisualDataPoint({x: event.stageX, y: event.stageY});
207+
});
208+
209+
stage.update();

scripts/data_point.js

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
/*
2+
* David Lettier
3+
* (C) 2015
4+
* http://www.lettier.com/
5+
*/
6+
7+
function DataPoint(features) {
8+
if (features instanceof Array) {
9+
this.features = features;
10+
} else {
11+
this.features = [];
12+
}
13+
this.meanId = -1;
14+
}

0 commit comments

Comments
 (0)