<script type="text/python"> from math import sqrt from random import randint from browser import document as doc import math class KMeans: def __init__(self, x, y, n_clusters = 1, limit = 10): self.x = x self.x_min = min(x) self.x_max = max(x) self.y = y self.y_min = min(y) self.y_max = max(y) self.n_clusters = n_clusters self.limit = limit self._init_centroids() self.iterations = 0 def _init_centroids(self): self.x_centroids = [] self.y_centroids = [] self.colors = [] for i in range(self.n_clusters): self.x_centroids.append(randint(self.x_min, self.x_max)) self.y_centroids.append(randint(self.y_min, self.y_max)) r = randint(0,255) g = randint(0,255) b = randint(0,255) color = 'rgb({0},{1},{2})'.format(r, g, b) self.colors.append(color) self._assign_centroids() def _assign_centroids(self): self.c = [] # Maximum possible distance to a centroid dmax = sqrt((self.x_min - self.x_max)**2 + (self.y_min - self.y_max)**2) for xi, yi in zip(self.x, self.y): cluster = 0 d0 = dmax for i in range(self.n_clusters): d = sqrt((xi - self.x_centroids[i])**2 + (yi - self.y_centroids[i])**2) if d < d0: cluster = i d0 = d self.c.append(cluster) def _recalculate_centroids(self): self._x_centroids = self.x_centroids[:] self._y_centroids = self.y_centroids[:] for n in range(self.n_clusters): x0 = 0 y0 = 0 cont = 0 for i, c in enumerate(self.c): if c == n: cont += 1 x0 += self.x[i] y0 += self.y[i] self.x_centroids[n] = x0 / cont self.y_centroids[n] = y0 / cont self._assign_centroids() def _check_stop(self): for i in range(self.n_clusters): d = sqrt( (self._x_centroids[i] - self.x_centroids[i])**2 + (self._y_centroids[i] - self.y_centroids[i])**2 ) if d > self.limit: return False return True def __iter__(self): return self def __next__(self): self.iterations += 1 self._recalculate_centroids() stop = self._check_stop() if stop == True: raise StopIteration return self ## Base classes for higher level objects class Figure: """ Base class to create other elements. """ def __init__(self, canvasid, facecolor = "white", edgecolor = "black", borderwidth = None): """ Parameters ---------- *canvasid*: String String indicating the canvas id where the image should be rendered. *facecolor*: String String value containing a valid HTML color *edgecolor*: String String value containing a valid HTML color *borderwidth*: Integer Value indicating the width of the border in pixels. If not provided it will 0 and the edgecolor will not be visible """ if isinstance(canvasid, str): self.id = canvasid else: raise Exception("The canvasid parameter should be a string") try: self.canvas = doc[self.id] except: raise Exception("No HTML element with id=%s" % self.id) try: self._W = self.canvas.width self._H = self.canvas.height self._ctx = self.canvas.getContext("2d") except: raise Exception("You must provide the ID of a <canvas> element") self.facecolor = facecolor self.borderwidth = borderwidth self.edgecolor = edgecolor self.clf() def clf(self): "clear the figure" self._ctx.save() # The following line should clear the canvas but I found a # problem when I use beginPath ¿¿¿??? #self._ctx.clearRect(0, 0, self._W, self._H) # So I use the following line that is less performant but # this operation shouldn't be done very often... self.canvas.width = self.canvas.width self._ctx.fillStyle = self.facecolor self._ctx.fillRect(0, 0, self._W, self._H) self._ctx.fill() if self.borderwidth: self._ctx.lineWidth = self.borderwidth self._ctx.strokeStyle = self.edgecolor self._ctx.strokeRect(0, 0, self._W, self._H) self._ctx.stroke() self._ctx.restore() class Shape: """ Base class to create other elements. """ def __init__(self, context, x, y, facecolor = "black", edgecolor = "black", #alpha = 1, borderwidth = None): """ Parameters ---------- *context*: a canvas context a valid canvas context where the text will be rendered *x*: int or float x value for location in pixels *y*: int or float y value for location in pixels *facecolor*: String String value containing a valid HTML color *edgecolor*: String String value containing a valid HTML color *alpha*: int or float Value between 0 (transparent) and 1 (opaque) to set the transparency of the text *borderwidth*: Integer Value indicating the width of the border in pixels. If not provided it will 0 and the edgecolor will not be visible """ self._ctx = context self.x = x self.y = y self.facecolor = facecolor self.borderwidth = borderwidth self.edgecolor = edgecolor #self.alpha = alpha class Circle(Shape): def __init__(self, *args, radius = 10, **kwargs): """ Parameters ---------- *radius*: int or float radius of the circle in pixels. """ Shape.__init__(self, *args, **kwargs) self.r = radius self.draw() def draw(self): self._ctx.save() #self._ctx.globalAlpha = self.alpha self._ctx.beginPath() self._ctx.fillStyle = self.facecolor self._ctx.arc(self.x, self.y, self.r, 0, 2 * math.pi) self._ctx.fill() if self.borderwidth: self._ctx.lineWidth = self.borderwidth self._ctx.strokeStyle = self.edgecolor self._ctx.arc(self.x, self.y, self.r, 0, 2 * math.pi) self._ctx.stroke() self._ctx.closePath() self._ctx.restore() class Line(Shape): def __init__(self, *args, polygon = False, borderwidth = 2, **kwargs): Shape.__init__(self, *args, **kwargs) self.borderwidth = borderwidth self.polygon = polygon self.draw() def draw(self): self._ctx.save() #self._ctx.globalAlpha = self.alpha self._ctx.beginPath() self._ctx.moveTo(self.x[0], self.y[0]) for i in range(len(self.x)): self._ctx.lineTo(self.x[i], self.y[i]) if self.polygon: self._ctx.closePath() if self.facecolor: self._ctx.fillStyle = self.facecolor self._ctx.fill() if self.borderwidth: self._ctx.lineWidth = self.borderwidth self._ctx.strokeStyle = self.edgecolor self._ctx.stroke() self._ctx.restore() fig = Figure('cnvs01', borderwidth = 2) n_points = 50 x = [randint(10, fig._W - 10) for value in range(n_points)] y = [randint(10, fig._H - 10) for value in range(n_points)] kmeans = KMeans(x, y, n_clusters = 4, limit = 1) def plot(obj): fig._ctx.save() fig._ctx.fillStyle= "#ffffff" fig._ctx.globalAlpha = 0.3 fig._ctx.fillRect(2,2,fig._W-4,fig._H-4) fig._ctx.restore() x = obj.x y = obj.y npoints = len(x) colors = obj.colors xc = obj.x_centroids yc = obj.y_centroids c = obj.c for i in range(npoints): color = colors[c[i]] Line(fig._ctx, [x[i], xc[c[i]]], [y[i], yc[c[i]]], facecolor = color, edgecolor = color) Circle(fig._ctx, x[i], y[i], facecolor = color, edgecolor = 'black', borderwidth = 1, radius = 4) for xci, yci, color in zip(xc, yc, colors): Circle(fig._ctx, xci, yci, facecolor = color, edgecolor = 'black', borderwidth = 1, radius = 8) def update(ev): plot(kmeans) try: next(kmeans) except: #doc['button'].disabled = True del doc['button'] doc['button'].bind('click', update) </script> <div id="main"> <p> <canvas id="cnvs01" width=500 height=500></canvas> </p> <p> <button id="button" class="btn">Next step</button> </p> </div>