This JavaScript program demonstrates how to generate k-means clusters on a 2d graph of randomly generated points. The clusters are diplayed in different colors and the centroids are shown as larger dots.
<!DOCTYPE html>
<html>
<head>
<title>XoaX.net's Javascript</title>
<script type="text/javascript" src="KMeans.js"></script>
</head>
<body onload="Initialize()">
<div style="width:708px;height:622px;">
<canvas id="idCanvas" width="600" height ="600" style="background-color:#F0F0F0;float:left;"></canvas>
<div style="width:108px;height:600px;float:right;">
<input id="idHighY" type="text" size="9" style="width:100px;height:16px;"/>
<div style="width:108px;height:556px;"></div>
<input id="idLowY" type="text" size="9" style="width:100px;height:16px;"/>
</div>
<div style="width:600px;height:22px;float:left;">
<input id="idLowX" type="text" size="9" style="width:100px;height:16px;float:left;"/>
<div style="width:384px;height:22px;float:left;">
<div style="width:fit-content;margin-left:auto;margin-right:auto;"></div>
</div>
<input id="idHighX" type="text" size="9" style="width:100px;height:16px;float:left;"/>
</div>
<div style="width:108px;height:22px;float:right;"></div>
</div>
</body>
</html>class CPoint2D {
constructor(dX, dY) {
this.mdX = dX;
this.mdY = dY;
}
Set(dX, dY) {
this.mdX = dX;
this.mdY = dY;
}
GetX() {
return this.mdX;
}
GetY() {
return this.mdY;
}
DistanceSquared(dP){
var dDX = dP.mdX - this.mdX;
var dDY = dP.mdY - this.mdY;
return dDX*dDX + dDY*dDY;
}
}
class Clusters {
constructor(kqaPoints, kiClusterCount) {
// Allocate a 2D array of the number of clusters to store the clusters
this.mqaaClusters = new Array(kiClusterCount);
// The first point of each cluster array will be reserved for the centroid (mean)
const kiPoints = kqaPoints.length;
if (kiPoints < kiClusterCount) {
throw new Error("Error: There are fewer points than the number of clusters");
}
// Arbitrarily assign the first points to be the centers of the clusters
for (var i = 0; i < kiClusterCount; ++i) {
this.mqaaClusters[i] = new Array();
this.mqaaClusters[i].push(kqaPoints[i]);
}
// Loop over the points and add them randomly to cluster arrays
for(var qPoint of kqaPoints) {
var iClosest = this.FindClosestCluster(qPoint);
this.mqaaClusters[iClosest].push(qPoint);
}
}
FindClosestCluster(qPoint) {
var dMinDistSq = this.mqaaClusters[0][0].DistanceSquared(qPoint);
var iClosestCluster = 0;
for (var i = 1; i < this.mqaaClusters.length; ++i) {
var dDistSq = this.mqaaClusters[i][0].DistanceSquared(qPoint);
if (dDistSq < dMinDistSq) {
dMinDistSq = dDistSq
iClosestCluster = i;
}
}
return iClosestCluster;
}
RecalcuateCentroids() {
for (var i = 0; i < this.mqaaClusters.length; ++i) {
var dX = 0;
var dY = 0;
for (var j = 1; j < this.mqaaClusters[i].length; ++j) {
dX += this.mqaaClusters[i][j].GetX()/(this.mqaaClusters[i].length - 1);
dY += this.mqaaClusters[i][j].GetY()/(this.mqaaClusters[i].length - 1);
}
this.mqaaClusters[i][0].Set(dX, dY);
}
}
RecalculateClusters() {
var bHasChanges = false;
for (var i = 0; i < this.mqaaClusters.length; ++i) {
// Run through the points bacward so that removals will not affect the remaining points.
for (var j = this.mqaaClusters[i].length - 1; j > 0; --j) {
// Find the closest cluster centroid
var iClosestCluster = this.FindClosestCluster(this.mqaaClusters[i][j]);
if (iClosestCluster != i) {
bHasChanges = true;
// Add the point to the closest cluster
this.mqaaClusters[iClosestCluster].push(this.mqaaClusters[i][j]);
// Remove it from the current cluster
this.mqaaClusters[i].splice(j, 1);
}
}
}
return bHasChanges;
}
GenerateClusters() {
this.RecalcuateCentroids();
while (this.RecalculateClusters()) {
this.RecalcuateCentroids();
}
this.RecalcuateCentroids();
}
DrawClusters(qContext2D, qGraphRange) {
// Pick a set of colors
// There are 3 color channels. Take the number of clusters, take the ceiling of division by 2^3 - 1 = 7, and divide 255 by the number, rounding down.
// So, 13 clusters divided by 7 is 2, if we take the ceiling. Divide 255 by 2 to get 127, taking the floor.
// This 127 is the minimum color difference within a channel.
const kiClusterCount = this.mqaaClusters.length;
const kiDivisor = Math.ceil(kiClusterCount / 8);
// The minimum difference between colors within a single channel: RGB
const kiMinColorDiff = Math.floor(255/kiDivisor);
for (var i = 0; i < kiClusterCount; ++i) {
var iQuotient = Math.floor(i/7);
var iRemainder = (i % 8);
// This quotient needs to be fixed for general color selection
var iR = (iQuotient + (iRemainder & 1))*kiMinColorDiff;
var iG = (iQuotient + ((iRemainder & 2) >> 1))*kiMinColorDiff;
var iB = (iQuotient + ((iRemainder & 4) >> 2))*kiMinColorDiff;
// Draw the centroid
qContext2D.fillStyle = "rgb("+iR+", "+iG+", "+iB+")";
// The centroid of each cluster is at index 0
var dX = qGraphRange.XtoPixelX(this.mqaaClusters[i][0].GetX());
var dY = qGraphRange.YtoPixelY(this.mqaaClusters[i][0].GetY());
qContext2D.beginPath();
qContext2D.arc(dX, dY, 4, 0, 2.0*Math.PI, true);
qContext2D.fill();
// Draw the rest of the points in the cluster
for (var j = 1; j < this.mqaaClusters[i].length; ++j) {
qContext2D.fillStyle = "rgb("+iR+", "+iG+", "+iB+", "+.25+")";
dX = qGraphRange.XtoPixelX(this.mqaaClusters[i][j].GetX());
dY = qGraphRange.YtoPixelY(this.mqaaClusters[i][j].GetY());
qContext2D.beginPath();
qContext2D.arc(dX, dY, 1.5, 0, 2.0*Math.PI, true);
qContext2D.fill();
}
}
}
}
class GraphRange2D {
constructor(dLowX, dHighX, dLowY, dHighY, iCanvasW, iCanvasH) {
this.mdLowX = dLowX;
this.mdHighX = dHighX;
this.mdLowY = dLowY;
this.mdHighY = dHighY;
this.miCanvasW = iCanvasW;
this.miCanvasH = iCanvasH;
}
PixelW() {
return (this.mdHighX - this.mdLowX)/this.miCanvasW;
}
PixelH() {
return (this.mdHighY - this.mdLowY)/this.miCanvasH;
}
// The pixel x range is [-.5, CanvasW - .5]
XtoPixelX(dX) {
var dT = (dX - this.mdLowX)/(this.mdHighX - this.mdLowX);
return (dT*this.miCanvasW - .5);
}
// The pixel y range is [-.5, CanvasH - .5], but the orientation is reversed
YtoPixelY(dY) {
var dT = (dY - this.mdLowY)/(this.mdHighY - this.mdLowY);
return (1.0 - dT)*(this.miCanvasH - .5);
}
PixelXToX(dPixelX) {
var dT = (dPixelX - .5)/(this.miCanvasW - 1.0);
return (dT*(this.mdHighX - this.mdLowX) + this.mdLowX);
}
PixelYToY(dPixelY) {
var dT = (dPixelY - .5)/(this.miCanvasH - 1.0);
return ((1.0 - dT)*(this.mdHighY - this.mdLowY) + this.mdLowY);
}
// Find the smallest power greater than x
FindSmallestGreaterPowerOfTwo(dX) {
var dPow = 1.0;
while (dPow < dX) {
dPow *= 2;
}
if (dPow == 1.0) {
while (dPow >= dX) {
dPow /= 2;
}
}
return dPow;
}
FindLeastMultipleBelow(dX, dLowX) {
Math.floor(dLowX/dX)*dX;
}
DrawGridLinesAndAxes(qContext) {
// Draw grid lines of two types
// Get an estimate for a value that will be drawn about every 100 pixels and every 20
var dThickFreqX = this.FindSmallestGreaterPowerOfTwo(this.PixelXToX(100) - this.PixelXToX(0));
var dThinFreqX = dThickFreqX/4;
var dThickFreqY = this.FindSmallestGreaterPowerOfTwo(this.PixelYToY(0) - this.PixelYToY(100));
var dThinFreqY = dThickFreqY/4;
// Draw the thin lines first, then the thick ones, and the axes.
// Draw the thin lines first
// Draw vertical grid lines
// Find the least frequency value below the low x (greatest lower multiple GLM)
var dGLM = Math.floor(this.mdLowX/dThickFreqX)*dThickFreqX;
var dGridValue = dGLM + dThinFreqX;
qContext.strokeStyle = "#E8E8E8";
var i = 1;
while (dGridValue < this.mdHighX) {
if ((i % 4) != 0) {
var dGridX = this.XtoPixelX(dGridValue);
qContext.beginPath();
qContext.moveTo(dGridX, 0);
qContext.lineTo(dGridX, this.miCanvasH);
qContext.stroke();
}
dGridValue += dThinFreqX;
++i;
}
// Draw horizontal grid lines
dGLM = Math.floor(this.mdLowY/dThickFreqY)*dThickFreqY;
dGridValue = dGLM + dThinFreqY;
i = 1;
while (dGridValue < this.mdHighY) {
if ((i % 4) != 0) {
var dGridY = this.YtoPixelY(dGridValue);
qContext.beginPath();
qContext.moveTo(0, dGridY);
qContext.lineTo(this.miCanvasW, dGridY);
qContext.stroke();
}
dGridValue += dThinFreqY;
++i;
}
// Draw the thick lines
// Draw vertical grid lines
qContext.strokeStyle = "#DDDDDD";
dGLM = Math.floor(this.mdLowX/dThickFreqX)*dThickFreqX;
dGridValue = dGLM + dThickFreqX;
while (dGridValue < this.mdHighX) {
var dGridX = this.XtoPixelX(dGridValue);
qContext.beginPath();
qContext.moveTo(dGridX, 0);
qContext.lineTo(dGridX, this.miCanvasH);
qContext.stroke();
dGridValue += dThickFreqX;
}
// Draw horizontal grid lines
dGLM = Math.floor(this.mdLowY/dThickFreqY)*dThickFreqY;
dGridValue = dGLM + dThickFreqY;
while (dGridValue < this.mdHighY) {
var dGridY = this.YtoPixelY(dGridValue);
qContext.beginPath();
qContext.moveTo(0, dGridY);
qContext.lineTo(this.miCanvasW, dGridY);
qContext.stroke();
dGridValue += dThickFreqY;
}
// Lastly, check whether zero is within each range in order to draw the axes.
// Draw the Y-Axis
qContext.strokeStyle = "gray";
qContext.fillStyle = "gray";
if (this.mdLowX <= 0 && this.mdHighX >= 0) {
var dPixelX = this.XtoPixelX(0);
DrawArrow(qContext, dPixelX, this.miCanvasH, dPixelX, 0);
}
// Draw the X-Axis
if (this.mdLowY <= 0 && this.mdHighY >= 0) {
var dPixelY = this.YtoPixelY(0);
DrawArrow(qContext, 0, dPixelY, this.miCanvasW, dPixelY);
}
}
FillInRanges(qLowX, qHighX, qLowY, qHighY) {
// Remove any trailing zeroes and decimal points
qLowX.value = this.mdLowX.toFixed(4).replace(/\.?0+$/, "");
qHighX.value = this.mdHighX.toFixed(4).replace(/\.?0+$/, "");
qLowY.value = this.mdLowY.toFixed(4).replace(/\.?0+$/, "");
qHighY.value = this.mdHighY.toFixed(4).replace(/\.?0+$/, "");
}
}
var gqGraphRange = null;
var gqContext = null;
var gbIsDragging = false;
var giCursorX = 0;
var giCursorY = 0;
var gqClusters = null;
function Initialize() {
var qCanvas = document.getElementById("idCanvas");
gqContext = qCanvas.getContext("2d");
// Get the canvas size
var dLowX = -4.0;
var dHighX = 4.0;
var dLowY = -4.0;
var dHighY = 4.0;
gqGraphRange = new GraphRange2D(dLowX, dHighX, dLowY, dHighY, qCanvas.width, qCanvas.height);
gqGraphRange.DrawGridLinesAndAxes(gqContext);
var qLowX = document.getElementById("idLowX");
var qHighX = document.getElementById("idHighX");
var qLowY = document.getElementById("idLowY");
var qHighY = document.getElementById("idHighY");
gqGraphRange.FillInRanges(qLowX, qHighX, qLowY, qHighY);
// Get the range and generate points for the clusters
var qaPoints = new Array();
const kiPointCount = 10000;
var dDeltaX = dHighX - dLowX;
var dDeltaY = dHighY - dLowY;
for (var i = 0; i < kiPointCount; ++i) {
var dX = Math.random()*dDeltaX + dLowX;
var dY = Math.random()*dDeltaY + dLowY;
qaPoints.push(new CPoint2D(dX, dY));
}
// Create the Clusters object.
const kiClusterCount = 8;
gqClusters = new Clusters(qaPoints, kiClusterCount);
gqClusters.GenerateClusters();
gqClusters.DrawClusters(gqContext, gqGraphRange);
}
function DrawArrow(qContext, dX1, dY1, dX2, dY2) {
// Draw the line portion
qContext.beginPath();
qContext.moveTo(dX1, dY1);
qContext.lineTo(dX2, dY2);
qContext.stroke();
// Draw the head portion
DrawArrowhead(qContext, dX1, dY1, dX2, dY2);
}
function DrawArrowhead(qContext, dX1, dY1, dX2, dY2) {
var dTheta = Math.atan2(dY2-dY1,dX2-dX1);
var dThicknessAngle = Math.PI/6;
var dHeadLength = 10;
qContext.beginPath();
qContext.moveTo(dX2, dY2);
qContext.lineTo(dX2 - dHeadLength*Math.cos(dTheta-dThicknessAngle), dY2 - dHeadLength*Math.sin(dTheta-dThicknessAngle));
qContext.lineTo(dX2 - dHeadLength*Math.cos(dTheta+dThicknessAngle), dY2 - dHeadLength*Math.sin(dTheta+dThicknessAngle));
qContext.closePath();
qContext.fill();
}
© 20072025 XoaX.net LLC. All rights reserved.