diff --git a/clusterview2.ui b/clusterview2.ui
index 4d5c2f7..1f1316a 100644
--- a/clusterview2.ui
+++ b/clusterview2.ui
@@ -103,12 +103,12 @@
-
- Centroids
+ Clusters
-
-
+
0
@@ -130,42 +130,32 @@
-
-
+
- true
+ false
- Choose Centroids
+ Unweighted Clustering
-
-
+
false
- Unweighted Clustering
+ Weighted Clustering
- -
+
-
Reset
- -
-
-
- false
-
-
- Weighted Clustering
-
-
-
diff --git a/clusterview2/debug.py b/clusterview2/debug.py
new file mode 100644
index 0000000..0ad0c5e
--- /dev/null
+++ b/clusterview2/debug.py
@@ -0,0 +1,9 @@
+def debug_trace():
+ """
+ A wrapper for pdb that works with PyQt5.
+ """
+ from PyQt5.QtCore import pyqtRemoveInputHook
+
+ from pdb import set_trace
+ pyqtRemoveInputHook()
+ set_trace()
diff --git a/clusterview2/mode.py b/clusterview2/mode.py
index e54cbe9..f7699d3 100644
--- a/clusterview2/mode.py
+++ b/clusterview2/mode.py
@@ -13,6 +13,5 @@ class Mode(Enum):
MOVE = 3
DELETE = 4
LOADED = 5
- CHOOSE_CENTROIDS = 6
- UNWEIGHTED_CLUSTERING = 7
- WEIGHTED_CLUSTERING = 8
+ UNWEIGHTED_CLUSTERING = 6
+ WEIGHTED_CLUSTERING = 7
diff --git a/clusterview2/point_manager.py b/clusterview2/point_manager.py
index 135c04b..84931c0 100644
--- a/clusterview2/point_manager.py
+++ b/clusterview2/point_manager.py
@@ -11,7 +11,7 @@ class PointManager():
"""
point_set = None
- centroids = []
+ clusters = []
@staticmethod
def load(location):
diff --git a/clusterview2/points.py b/clusterview2/points.py
index bec91fd..5c75c38 100644
--- a/clusterview2/points.py
+++ b/clusterview2/points.py
@@ -14,7 +14,7 @@ class Point(BasePoint):
"""
def __init__(self, x, y, color, point_size,
- viewport_width, viewport_height):
+ viewport_width, viewport_height, weight=1.0):
"""
Initializes a new point with a point_size bounding box, viewport
awareness, and a color.
@@ -35,8 +35,14 @@ class Point(BasePoint):
"type Color.")
self._point_size = point_size
+
+ # Unfortunately, it appears decorated property methods are not
+ # inheirited and instead of redoing everything we will just repeat
+ # the properties here.
self._x = x
self._y = y
+ self._cluster = None
+ self._weight = weight
self._color = color
@@ -60,6 +66,18 @@ class Point(BasePoint):
return self._y
@property
+ def weight(self):
+ return self._weight
+
+ @property
+ def cluster(self):
+ return self._cluster
+
+ @cluster.setter
+ def cluster(self, cluster):
+ self._cluster = cluster
+
+ @property
def point_size(self):
return self._point_size
diff --git a/clusterview2/ui/mode_handlers.py b/clusterview2/ui/mode_handlers.py
index 857f068..1ee1dd1 100644
--- a/clusterview2/ui/mode_handlers.py
+++ b/clusterview2/ui/mode_handlers.py
@@ -3,12 +3,14 @@ import random
from PyQt5.QtCore import QEvent, Qt
from PyQt5.QtGui import QCursor
+from kmeans.algorithms import k_means
+
from clusterview2.colors import Color
from clusterview2.exceptions import ExceededWindowBoundsError
from clusterview2.mode import Mode
-from .opengl_widget import (set_drawing_event, set_move_bb_top_left,
- set_move_bb_bottom_right, reset_move_bbs,
- viewport_height, viewport_width)
+from clusterview2.ui.opengl_widget import (set_drawing_event, set_move_bb_top_left,
+ set_move_bb_bottom_right, reset_move_bbs,
+ viewport_height, viewport_width)
from clusterview2.point_manager import PointManager
@@ -293,64 +295,9 @@ def _handle_info_updates(ctx, event):
ctx.mouse_position_label.setText(f"{event.x(), event.y()}")
-def _handle_choose_centroids(ctx, event):
- """
- Similar to move in terms of selecting points, however this
- function assigns a random color up to the maximum number
- of centroids, and after the maximum number has been selected it will
- enable the group button.
- """
- global _centroid_count
+def reset_colors():
global _remaining_colors
- _handle_info_updates(ctx, event)
-
- if _centroid_count == ctx.number_of_centroids.value():
- # We have specified the number of centroids required
- return
-
- if (event.button() == Qt.LeftButton and
- event.type() == QEvent.MouseButtonPress):
-
- point = None
-
- for test_point in PointManager.point_set.points:
- if test_point.hit(event.x(), event.y()):
- point = test_point
-
- if point is None:
- # No point was found on the click, do nothing
- return
-
- if point in PointManager.centroids:
- # Centroids must be unique
- return
-
- _centroid_count += 1
-
- color = random.choice(_remaining_colors)
- _remaining_colors.remove(color)
-
- point.color = color
-
- # Recolor the point and restash the point in centroids
- PointManager.centroids.append(point)
-
- if _centroid_count == ctx.number_of_centroids.value():
- # Prevent the user from changing the centroids
- ctx.number_of_centroids.setEnabled(False)
- ctx.choose_centroids_button.setEnabled(False)
- ctx.unweighted_clustering_button.setEnabled(True)
- ctx.weighted_clustering_button.setEnabled(True)
-
- ctx.opengl_widget.update()
-
-
-def reset_centroid_count_and_colors():
- global _centroid_count
- global _remaining_colors
-
- _centroid_count = 0
_remaining_colors = [c for c in Color if c not in [Color.BLUE, Color.GREY]]
for point in PointManager.point_set.points:
@@ -389,6 +336,27 @@ def generate_random_points(point_count, x_bound, y_bound):
PointManager.point_set.add_point(point[0], point[1], Color.GREY)
+def _handle_clustering(ctx, _):
+ points = list(PointManager.point_set.points)
+
+ if not ctx.clustering_solved:
+ clusters = k_means(points, ctx.number_of_clusters.value(), 0.001)
+
+ # We can leverage the paint function by first assigning every cluster
+ # a color (for completeness) and then assigning every point in that
+ # cluster the cluster's color.
+ for i, cluster in enumerate(clusters):
+ cluster.color = _remaining_colors[i]
+
+ for point in cluster.points:
+ point.color = cluster.color
+
+ PointManager.clusters = clusters
+
+ ctx.opengl_widget.update()
+ ctx.clustering_solved = True
+
+
# Simple dispatcher to make it easy to dispatch the right mode
# function when the OpenGL window is acted on.
MODE_HANDLER_MAP = {
@@ -398,5 +366,5 @@ MODE_HANDLER_MAP = {
Mode.EDIT: _handle_edit_point,
Mode.MOVE: _handle_move_points,
Mode.DELETE: _handle_delete_point,
- Mode.CHOOSE_CENTROIDS: _handle_choose_centroids
+ Mode.UNWEIGHTED_CLUSTERING: _handle_clustering
}
diff --git a/clusterview2/ui/opengl_widget.py b/clusterview2/ui/opengl_widget.py
index d52659e..a53da3d 100644
--- a/clusterview2/ui/opengl_widget.py
+++ b/clusterview2/ui/opengl_widget.py
@@ -183,7 +183,6 @@ def paint_gl():
if (__current_context.mode is Mode.ADD or
__current_context.mode is Mode.DELETE or
__current_context.mode is Mode.LOADED or
- __current_context.mode is Mode.CHOOSE_CENTROIDS or
__current_context.mode is Mode.UNWEIGHTED_CLUSTERING or
__current_context.mode is Mode.WEIGHTED_CLUSTERING):
diff --git a/clusterview2_ui.py b/clusterview2_ui.py
index a14fa48..5c3cb56 100644
--- a/clusterview2_ui.py
+++ b/clusterview2_ui.py
@@ -64,31 +64,27 @@ class Ui_MainWindow(object):
self.label_2 = QtWidgets.QLabel(self.groupBox_3)
self.label_2.setObjectName("label_2")
self.formLayout.setWidget(0, QtWidgets.QFormLayout.LabelRole, self.label_2)
- self.number_of_centroids = QtWidgets.QSpinBox(self.groupBox_3)
+ self.number_of_clusters = QtWidgets.QSpinBox(self.groupBox_3)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
- sizePolicy.setHeightForWidth(self.number_of_centroids.sizePolicy().hasHeightForWidth())
- self.number_of_centroids.setSizePolicy(sizePolicy)
- self.number_of_centroids.setMinimumSize(QtCore.QSize(50, 26))
- self.number_of_centroids.setMaximumSize(QtCore.QSize(50, 16777215))
- self.number_of_centroids.setObjectName("number_of_centroids")
- self.formLayout.setWidget(0, QtWidgets.QFormLayout.FieldRole, self.number_of_centroids)
- self.choose_centroids_button = QtWidgets.QPushButton(self.groupBox_3)
- self.choose_centroids_button.setEnabled(True)
- self.choose_centroids_button.setObjectName("choose_centroids_button")
- self.formLayout.setWidget(3, QtWidgets.QFormLayout.LabelRole, self.choose_centroids_button)
+ sizePolicy.setHeightForWidth(self.number_of_clusters.sizePolicy().hasHeightForWidth())
+ self.number_of_clusters.setSizePolicy(sizePolicy)
+ self.number_of_clusters.setMinimumSize(QtCore.QSize(50, 26))
+ self.number_of_clusters.setMaximumSize(QtCore.QSize(50, 16777215))
+ self.number_of_clusters.setObjectName("number_of_clusters")
+ self.formLayout.setWidget(0, QtWidgets.QFormLayout.FieldRole, self.number_of_clusters)
self.unweighted_clustering_button = QtWidgets.QPushButton(self.groupBox_3)
self.unweighted_clustering_button.setEnabled(False)
self.unweighted_clustering_button.setObjectName("unweighted_clustering_button")
- self.formLayout.setWidget(4, QtWidgets.QFormLayout.LabelRole, self.unweighted_clustering_button)
- self.reset_button = QtWidgets.QPushButton(self.groupBox_3)
- self.reset_button.setObjectName("reset_button")
- self.formLayout.setWidget(6, QtWidgets.QFormLayout.LabelRole, self.reset_button)
+ self.formLayout.setWidget(3, QtWidgets.QFormLayout.LabelRole, self.unweighted_clustering_button)
self.weighted_clustering_button = QtWidgets.QPushButton(self.groupBox_3)
self.weighted_clustering_button.setEnabled(False)
self.weighted_clustering_button.setObjectName("weighted_clustering_button")
- self.formLayout.setWidget(5, QtWidgets.QFormLayout.LabelRole, self.weighted_clustering_button)
+ self.formLayout.setWidget(4, QtWidgets.QFormLayout.LabelRole, self.weighted_clustering_button)
+ self.reset_button = QtWidgets.QPushButton(self.groupBox_3)
+ self.reset_button.setObjectName("reset_button")
+ self.formLayout.setWidget(5, QtWidgets.QFormLayout.LabelRole, self.reset_button)
self.verticalLayout.addWidget(self.groupBox_3)
spacerItem = QtWidgets.QSpacerItem(20, 20, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Fixed)
self.verticalLayout.addItem(spacerItem)
@@ -170,11 +166,10 @@ class Ui_MainWindow(object):
MainWindow.setWindowTitle(_translate("MainWindow", "ClusterView2"))
self.groupBox.setTitle(_translate("MainWindow", "Point List"))
self.groupBox_3.setTitle(_translate("MainWindow", "Solver"))
- self.label_2.setText(_translate("MainWindow", "Centroids"))
- self.choose_centroids_button.setText(_translate("MainWindow", "Choose Centroids"))
+ self.label_2.setText(_translate("MainWindow", "Clusters"))
self.unweighted_clustering_button.setText(_translate("MainWindow", "Unweighted Clustering"))
- self.reset_button.setText(_translate("MainWindow", "Reset"))
self.weighted_clustering_button.setText(_translate("MainWindow", "Weighted Clustering"))
+ self.reset_button.setText(_translate("MainWindow", "Reset"))
self.groupBox_2.setTitle(_translate("MainWindow", "Canvas Information"))
self.label.setText(_translate("MainWindow", "Mouse Position:"))
self.menu_file.setTitle(_translate("MainWindow", "File"))
diff --git a/main_window.py b/main_window.py
index 4860a64..e4ae60e 100644
--- a/main_window.py
+++ b/main_window.py
@@ -10,7 +10,7 @@ from clusterview2.mode import Mode
from clusterview2.ui.mode_handlers import (MODE_HANDLER_MAP,
ogl_keypress_handler,
refresh_point_list,
- reset_centroid_count_and_colors,
+ reset_colors,
generate_random_points)
from clusterview2.ui.opengl_widget import (clear_selection, initialize_gl,
mouse_leave, paint_gl, resize_gl,
@@ -50,12 +50,12 @@ class MainWindow(QMainWindow, Ui_MainWindow):
self._viewport_width,
self._viewport_height)
- # Spin box should only allow the number of centroids to be no
+ # Spin box should only allow the number of clusters to be no
# greater than the number of supported colors minus 2 to exclude
# the color for selection (Color.BLUE) and the default color for points
# (Color.GREY).
- self.number_of_centroids.setMinimum(0)
- self.number_of_centroids.setMaximum(Color.count() - 2)
+ self.number_of_clusters.setMinimum(0)
+ self.number_of_clusters.setMaximum(Color.count() - 2)
# We only need to set the context in our OpenGL state machine
# wrapper once here since the window is fixed size.
@@ -83,9 +83,8 @@ class MainWindow(QMainWindow, Ui_MainWindow):
self.point_list_widget.itemClicked.connect(partial(item_click_handler,
self))
- self.choose_centroids_button.clicked.connect(self._choose_centroids)
-
self.unweighted_clustering_button.clicked.connect(self._unweighted_clustering)
+ self.number_of_clusters.valueChanged.connect(self._clustering_enabled)
self.reset_button.clicked.connect(self._reset)
@@ -123,6 +122,9 @@ class MainWindow(QMainWindow, Ui_MainWindow):
self.opengl_widget.mouseMoveEvent = self._ogl_click_dispatcher
self.opengl_widget.mouseReleaseEvent = self._ogl_click_dispatcher
+ # Clustering flag so it does not continue to run
+ self.clustering_solved = False
+
# -----------------------------------------------------------------
# Mode changers - these will be used to signal the action in the
# OpenGL Widget.
@@ -164,33 +166,22 @@ class MainWindow(QMainWindow, Ui_MainWindow):
clear_selection()
self.opengl_widget.update()
- def _choose_centroids(self):
- self._mode = Mode.CHOOSE_CENTROIDS
- self.opengl_widget.setCursor(QCursor(Qt.CursorShape.CrossCursor))
- self.status_bar.showMessage('CHOOSE CENTROIDS')
-
- clear_selection()
- self.opengl_widget.update()
-
def _unweighted_clustering(self):
+ clear_selection()
self._mode = Mode.UNWEIGHTED_CLUSTERING
-
self.opengl_widget.setCursor(QCursor(Qt.CursorShape.ArrowCursor))
self.status_bar.showMessage('UNWEIGHTED CLUSTERING')
- clear_selection()
- # unweighted_clustering(self)
- self._off_mode()
self.opengl_widget.update()
def _reset(self):
self._off_mode()
- self.number_of_centroids.setEnabled(True)
- self.number_of_centroids.setValue(0)
- self.choose_centroids_button.setEnabled(True)
+ self.number_of_clusters.setEnabled(True)
+ self.number_of_clusters.setValue(0)
self.unweighted_clustering_button.setEnabled(False)
self.weighted_clustering_button.setEnabled(False)
- PointManager.centroids = []
- reset_centroid_count_and_colors()
+ self.clustering_solved = False
+ PointManager.clusters = []
+ reset_colors()
def _generate_random_points(self):
value, ok = QInputDialog.getInt(self, 'Number of Points',
@@ -208,6 +199,10 @@ class MainWindow(QMainWindow, Ui_MainWindow):
refresh_point_list(self)
+ def _clustering_enabled(self):
+ self.unweighted_clustering_button.setEnabled(self.number_of_clusters.value() > 0)
+ self.weighted_clustering_button.setEnabled(self.number_of_clusters.value() > 0)
+
@property
def mode(self):
""""