PageRenderTime 252ms CodeModel.GetById 25ms RepoModel.GetById 0ms app.codeStats 1ms

/Orange/widgets/data/owpaintdata.py

https://gitlab.com/zaverichintan/orange3
Python | 1237 lines | 1118 code | 67 blank | 52 comment | 20 complexity | 83c527d3249a2badaf5e22b0f8e779e7 MD5 | raw file
  1. import os
  2. import sys
  3. import unicodedata
  4. import itertools
  5. from functools import partial
  6. import numpy as np
  7. from PyQt4 import QtCore
  8. from PyQt4 import QtGui
  9. from PyQt4.QtGui import (
  10. QListView, QAction, QIcon, QSizePolicy, QPen
  11. )
  12. from PyQt4.QtCore import Qt, QObject, QTimer, QSize, QSizeF, QPointF, QRectF
  13. from PyQt4.QtCore import pyqtSignal as Signal
  14. import pyqtgraph as pg
  15. import Orange.data
  16. from Orange.widgets import widget, gui
  17. from Orange.widgets.settings import Setting
  18. from Orange.widgets.utils import itemmodels, colorpalette
  19. from Orange.widgets.io import FileFormat
  20. from Orange.util import scale, namegen
  21. def indices_to_mask(indices, size):
  22. """
  23. Convert an array of integer indices into a boolean mask index.
  24. The elements in indices must be unique.
  25. :param ndarray[int] indices: Integer indices.
  26. :param int size: Size of the resulting mask.
  27. """
  28. mask = np.zeros(size, dtype=bool)
  29. mask[indices] = True
  30. return mask
  31. def split_on_condition(array, condition):
  32. """
  33. Split an array in two parts based on a boolean mask array `condition`.
  34. """
  35. return array[condition], array[~condition]
  36. def stack_on_condition(a, b, condition):
  37. """
  38. Inverse of `split_on_condition`.
  39. """
  40. axis = 0
  41. N = condition.size
  42. shape = list(a.shape)
  43. shape[axis] = N
  44. shape = tuple(shape)
  45. arr = np.empty(shape, dtype=a.dtype)
  46. arr[condition] = a
  47. arr[~condition] = b
  48. return arr
  49. # ###########################
  50. # Data manipulation operators
  51. # ###########################
  52. from collections import namedtuple
  53. if sys.version_info < (3, 4):
  54. # use singledispatch backports from pypi
  55. from singledispatch import singledispatch
  56. else:
  57. from functools import singledispatch
  58. # Base commands
  59. Append = namedtuple("Append", ["points"])
  60. Insert = namedtuple("Insert", ["indices", "points"])
  61. Move = namedtuple("Move", ["indices", "delta"])
  62. DeleteIndices = namedtuple("DeleteIndices", ["indices"])
  63. # A composite of two operators
  64. Composite = namedtuple("Composite", ["f", "g"])
  65. # Non-base commands
  66. # These should be `normalized` (expressed) using base commands
  67. AirBrush = namedtuple("AirBrush", ["pos", "radius", "intensity", "rstate"])
  68. Jitter = namedtuple("Jitter", ["pos", "radius", "intensity", "rstate"])
  69. Magnet = namedtuple("Magnet", ["pos", "radius", "density"])
  70. SelectRegion = namedtuple("SelectRegion", ["region"])
  71. DeleteSelection = namedtuple("DeleteSelection", [])
  72. MoveSelection = namedtuple("MoveSelection", ["delta"])
  73. # Transforms functions for base commands
  74. @singledispatch
  75. def transform(command, data):
  76. """
  77. Generic transform for base commands
  78. :param command: An instance of base command
  79. :param ndarray data: Input data array
  80. :rval:
  81. A (transformed_data, command) tuple of the transformed input data
  82. and a base command expressing the inverse operation.
  83. """
  84. raise NotImplementedError
  85. @transform.register(Append)
  86. def append(command, data):
  87. np.clip(command.points[:, :2], 0, 1, out=command.points[:, :2])
  88. return (np.vstack([data, command.points]),
  89. DeleteIndices(slice(len(data),
  90. len(data) + len(command.points))))
  91. @transform.register(Insert)
  92. def insert(command, data):
  93. indices = indices_to_mask(command.indices, len(data) + len(command.points))
  94. return (stack_on_condition(command.points, data, indices),
  95. DeleteIndices(indices))
  96. @transform.register(DeleteIndices)
  97. def delete(command, data, ):
  98. if isinstance(command.indices, slice):
  99. condition = indices_to_mask(command.indices, len(data))
  100. else:
  101. indices = np.asarray(command.indices)
  102. if indices.dtype == np.bool:
  103. condition = indices
  104. else:
  105. condition = indices_to_mask(indices, len(data))
  106. data, deleted = split_on_condition(data, ~condition)
  107. return data, Insert(command.indices, deleted)
  108. @transform.register(Move)
  109. def move(command, data):
  110. data[command.indices] += command.delta
  111. return data, Move(command.indices, -command.delta)
  112. @transform.register(Composite)
  113. def compositum(command, data):
  114. data, ginv = command.g(data)
  115. data, finv = command.f(data)
  116. return data, Composite(ginv, finv)
  117. class PaintViewBox(pg.ViewBox):
  118. def __init__(self, *args, **kwargs):
  119. super().__init__(*args, **kwargs)
  120. self.setAcceptHoverEvents(True)
  121. self.tool = None
  122. def handle(event, eventType):
  123. if self.tool is not None and getattr(self.tool, eventType)(event):
  124. event.accept()
  125. else:
  126. getattr(super(self.__class__, self), eventType)(event)
  127. for eventType in ('mousePressEvent', 'mouseMoveEvent', 'mouseReleaseEvent',
  128. 'mouseClickEvent', 'mouseDragEvent',
  129. 'mouseEnterEvent', 'mouseLeaveEvent'):
  130. setattr(self, eventType, partial(handle, eventType=eventType))
  131. def crosshairs(color, radius=24, circle=False):
  132. radius = max(radius, 16)
  133. pixmap = QtGui.QPixmap(radius, radius)
  134. pixmap.fill(Qt.transparent)
  135. painter = QtGui.QPainter()
  136. painter.begin(pixmap)
  137. painter.setRenderHints(painter.Antialiasing)
  138. pen = QtGui.QPen(QtGui.QBrush(color), 1)
  139. pen.setWidthF(1.5)
  140. painter.setPen(pen)
  141. if circle:
  142. painter.drawEllipse(2, 2, radius - 2, radius - 2)
  143. painter.drawLine(radius / 2, 7, radius / 2, radius / 2 - 7)
  144. painter.drawLine(7, radius / 2, radius / 2 - 7, radius / 2)
  145. painter.end()
  146. return pixmap
  147. class DataTool(QObject):
  148. """
  149. A base class for data tools that operate on PaintViewBox.
  150. """
  151. #: Tool mouse cursor has changed
  152. cursorChanged = Signal(QtGui.QCursor)
  153. #: User started an editing operation.
  154. editingStarted = Signal()
  155. #: User ended an editing operation.
  156. editingFinished = Signal()
  157. #: Emits a data transformation command
  158. issueCommand = Signal(object)
  159. # Makes for a checkable push-button
  160. checkable = True
  161. # The tool only works if (at least) two dimensions
  162. only2d = True
  163. def __init__(self, parent, plot):
  164. super().__init__(parent)
  165. self._cursor = Qt.ArrowCursor
  166. self._plot = plot
  167. def cursor(self):
  168. return QtGui.QCursor(self._cursor)
  169. def setCursor(self, cursor):
  170. if self._cursor != cursor:
  171. self._cursor = QtGui.QCursor(cursor)
  172. self.cursorChanged.emit()
  173. def mousePressEvent(self, event):
  174. return False
  175. def mouseMoveEvent(self, event):
  176. return False
  177. def mouseReleaseEvent(self, event):
  178. return False
  179. def mouseClickEvent(self, event):
  180. return False
  181. def mouseDragEvent(self, event):
  182. return False
  183. def hoverEnterEvent(self, event):
  184. return False
  185. def hoverLeaveEvent(self, event):
  186. return False
  187. def mapToPlot(self, point):
  188. """Map a point in ViewBox local coordinates into plot coordinates.
  189. """
  190. box = self._plot.getViewBox()
  191. return box.mapToView(point)
  192. def activate(self, ):
  193. """Activate the tool"""
  194. pass
  195. def deactivate(self, ):
  196. """Deactivate a tool"""
  197. pass
  198. class PutInstanceTool(DataTool):
  199. """
  200. Add a single data instance with a mouse click.
  201. """
  202. only2d = False
  203. def mousePressEvent(self, event):
  204. if event.button() == Qt.LeftButton:
  205. self.editingStarted.emit()
  206. pos = self.mapToPlot(event.pos())
  207. self.issueCommand.emit(Append([pos]))
  208. event.accept()
  209. self.editingFinished.emit()
  210. return True
  211. else:
  212. return super().mousePressEvent(event)
  213. class PenTool(DataTool):
  214. """
  215. Add points on a path specified with a mouse drag.
  216. """
  217. def mousePressEvent(self, event):
  218. if event.button() == Qt.LeftButton:
  219. self.editingStarted.emit()
  220. self.__handleEvent(event)
  221. return True
  222. else:
  223. return super().mousePressEvent()
  224. def mouseMoveEvent(self, event):
  225. if event.buttons() & Qt.LeftButton:
  226. self.__handleEvent(event)
  227. return True
  228. else:
  229. return super().mouseMoveEvent()
  230. def mouseReleaseEvent(self, event):
  231. if event.button() == Qt.LeftButton:
  232. self.editingFinished.emit()
  233. return True
  234. else:
  235. return super().mouseReleaseEvent()
  236. def __handleEvent(self, event):
  237. pos = self.mapToPlot(event.pos())
  238. self.issueCommand.emit(Append([pos]))
  239. event.accept()
  240. class AirBrushTool(DataTool):
  241. """
  242. Add points with an 'air brush'.
  243. """
  244. only2d = False
  245. def __init__(self, parent, plot):
  246. super().__init__(parent, plot)
  247. self.__timer = QTimer(self, interval=50)
  248. self.__timer.timeout.connect(self.__timout)
  249. self.__count = itertools.count()
  250. self.__pos = None
  251. def mousePressEvent(self, event):
  252. if event.button() == Qt.LeftButton:
  253. self.editingStarted.emit()
  254. self.__pos = self.mapToPlot(event.pos())
  255. self.__timer.start()
  256. return True
  257. else:
  258. return super().mousePressEvent(event)
  259. def mouseMoveEvent(self, event):
  260. if event.buttons() & Qt.LeftButton:
  261. self.__pos = self.mapToPlot(event.pos())
  262. return True
  263. else:
  264. return super().mouseMoveEvent(event)
  265. def mouseReleaseEvent(self, event):
  266. if event.button() == Qt.LeftButton:
  267. self.__timer.stop()
  268. self.editingFinished.emit()
  269. return True
  270. else:
  271. return super().mouseReleaseEvent(event)
  272. def __timout(self):
  273. self.issueCommand.emit(
  274. AirBrush(self.__pos, None, None, next(self.__count))
  275. )
  276. def random_state(rstate):
  277. if isinstance(rstate, np.random.RandomState):
  278. return rstate
  279. else:
  280. return np.random.RandomState(rstate)
  281. def create_data(x, y, radius, size, rstate):
  282. random = random_state(rstate)
  283. x = random.normal(x, radius / 2, size=size)
  284. y = random.normal(y, radius / 2, size=size)
  285. return np.c_[x, y]
  286. class MagnetTool(DataTool):
  287. """
  288. Draw points closer to the mouse position.
  289. """
  290. def __init__(self, parent, plot):
  291. super().__init__(parent, plot)
  292. self.__timer = QTimer(self, interval=50)
  293. self.__timer.timeout.connect(self.__timeout)
  294. self._radius = 20.0
  295. self._density = 4.0
  296. self._pos = None
  297. def mousePressEvent(self, event):
  298. if event.button() == Qt.LeftButton:
  299. self.editingStarted.emit()
  300. self._pos = self.mapToPlot(event.pos())
  301. self.__timer.start()
  302. return True
  303. else:
  304. return super().mousePressEvent(event)
  305. def mouseMoveEvent(self, event):
  306. if event.buttons() & Qt.LeftButton:
  307. self._pos = self.mapToPlot(event.pos())
  308. return True
  309. else:
  310. return super().mouseMoveEvent(event)
  311. def mouseReleaseEvent(self, event):
  312. if event.button() == Qt.LeftButton:
  313. self.__timer.stop()
  314. self.editingFinished.emit()
  315. return True
  316. else:
  317. return super().mouseReleaseEvent(event)
  318. def __timeout(self):
  319. self.issueCommand.emit(
  320. Magnet(self._pos, self._radius, self._density)
  321. )
  322. class JitterTool(DataTool):
  323. """
  324. Jitter points around the mouse position.
  325. """
  326. def __init__(self, parent, plot):
  327. super().__init__(parent, plot)
  328. self.__timer = QTimer(self, interval=50)
  329. self.__timer.timeout.connect(self._do)
  330. self._pos = None
  331. self._radius = 20.0
  332. self._intensity = 5.0
  333. self.__count = itertools.count()
  334. def mousePressEvent(self, event):
  335. if event.button() == Qt.LeftButton:
  336. self.editingStarted.emit()
  337. self._pos = self.mapToPlot(event.pos())
  338. self.__timer.start()
  339. return True
  340. else:
  341. return super().mousePressEvent(event)
  342. def mouseMoveEvent(self, event):
  343. if event.buttons() & Qt.LeftButton:
  344. self._pos = self.mapToPlot(event.pos())
  345. return True
  346. else:
  347. return super().mouseMoveEvent(event)
  348. def mouseReleaseEvent(self, event):
  349. if event.button() == Qt.LeftButton:
  350. self.__timer.stop()
  351. self.editingFinished.emit()
  352. return True
  353. else:
  354. return super().mouseReleaseEvent(event)
  355. def _do(self):
  356. self.issueCommand.emit(
  357. Jitter(self._pos, self._radius, self._intensity,
  358. next(self.__count))
  359. )
  360. class _RectROI(pg.ROI):
  361. def __init__(self, pos, size, **kwargs):
  362. super().__init__(pos, size, **kwargs)
  363. def setRect(self, rect):
  364. self.setPos(rect.topLeft(), finish=False)
  365. self.setSize(rect.size(), finish=False)
  366. def rect(self):
  367. return QRectF(self.pos(), QSizeF(*self.size()))
  368. class SelectTool(DataTool):
  369. cursor = Qt.ArrowCursor
  370. def __init__(self, parent, plot):
  371. super().__init__(parent, plot)
  372. self._item = None
  373. self._start_pos = None
  374. self._selection_rect = None
  375. self._mouse_dragging = False
  376. self._delete_action = QAction(
  377. "Delete", self, shortcutContext=Qt.WindowShortcut
  378. )
  379. self._delete_action.setShortcuts([QtGui.QKeySequence.Delete,
  380. QtGui.QKeySequence("Backspace")])
  381. self._delete_action.triggered.connect(self.delete)
  382. def setSelectionRect(self, rect):
  383. if self._selection_rect != rect:
  384. self._selection_rect = QRectF(rect)
  385. self._item.setRect(self._selection_rect)
  386. def selectionRect(self):
  387. return self._item.rect()
  388. def mousePressEvent(self, event):
  389. if event.button() == Qt.LeftButton:
  390. pos = self.mapToPlot(event.pos())
  391. if self._item.isVisible():
  392. if self.selectionRect().contains(pos):
  393. # Allow the event to propagate to the item.
  394. event.setAccepted(False)
  395. self._item.setCursor(Qt.ClosedHandCursor)
  396. return False
  397. self._mouse_dragging = True
  398. self._start_pos = pos
  399. self._item.setVisible(True)
  400. self._plot.addItem(self._item)
  401. self.setSelectionRect(QRectF(pos, pos))
  402. event.accept()
  403. return True
  404. else:
  405. return super().mousePressEvent(event)
  406. def mouseMoveEvent(self, event):
  407. if event.buttons() & Qt.LeftButton:
  408. pos = self.mapToPlot(event.pos())
  409. self.setSelectionRect(QRectF(self._start_pos, pos).normalized())
  410. event.accept()
  411. return True
  412. else:
  413. return super().mouseMoveEvent(event)
  414. def mouseReleaseEvent(self, event):
  415. if event.button() == Qt.LeftButton:
  416. pos = self.mapToPlot(event.pos())
  417. self.setSelectionRect(QRectF(self._start_pos, pos).normalized())
  418. event.accept()
  419. self.issueCommand.emit(SelectRegion(self.selectionRect()))
  420. self._item.setCursor(Qt.OpenHandCursor)
  421. self._mouse_dragging = False
  422. return True
  423. else:
  424. return super().mouseReleaseEvent(event)
  425. def activate(self):
  426. if self._item is None:
  427. self._item = _RectROI((0, 0), (0, 0), pen=(25, 25, 25))
  428. self._item.setAcceptedMouseButtons(Qt.LeftButton)
  429. self._item.setVisible(False)
  430. self._item.setCursor(Qt.OpenHandCursor)
  431. self._item.sigRegionChanged.connect(self._on_region_changed)
  432. self._item.sigRegionChangeStarted.connect(
  433. self._on_region_change_started)
  434. self._item.sigRegionChangeFinished.connect(
  435. self._on_region_change_finished)
  436. self._plot.addItem(self._item)
  437. self._mouse_dragging = False
  438. self._plot.addAction(self._delete_action)
  439. def deactivate(self):
  440. self._reset()
  441. self._plot.removeAction(self._delete_action)
  442. def _reset(self):
  443. self.setSelectionRect(QRectF())
  444. self._item.setVisible(False)
  445. self._mouse_dragging = False
  446. def delete(self):
  447. if not self._mouse_dragging and self._item.isVisible():
  448. self.issueCommand.emit(DeleteSelection())
  449. self._reset()
  450. def _on_region_changed(self):
  451. if not self._mouse_dragging:
  452. newrect = self._item.rect()
  453. delta = newrect.topLeft() - self._selection_rect.topLeft()
  454. self._selection_rect = newrect
  455. self.issueCommand.emit(MoveSelection(delta))
  456. def _on_region_change_started(self):
  457. if not self._mouse_dragging:
  458. self.editingStarted.emit()
  459. def _on_region_change_finished(self):
  460. if not self._mouse_dragging:
  461. self.editingFinished.emit()
  462. class ClearTool(DataTool):
  463. cursor = None
  464. checkable = False
  465. only2d = False
  466. def activate(self):
  467. self.issueCommand.emit(SelectRegion(self._plot.rect()))
  468. self.issueCommand.emit(DeleteSelection())
  469. self.editingFinished.emit()
  470. class SimpleUndoCommand(QtGui.QUndoCommand):
  471. """
  472. :param function redo: A function expressing a redo action.
  473. :param function undo: A function expressing a undo action.
  474. """
  475. def __init__(self, redo, undo, parent=None):
  476. super().__init__(parent)
  477. self.redo = redo
  478. self.undo = undo
  479. class UndoCommand(QtGui.QUndoCommand):
  480. """An QUndoCommand applying a data transformation operation
  481. """
  482. def __init__(self, command, model, parent=None, text=None):
  483. super().__init__(parent,)
  484. self._command = command
  485. self._model = model
  486. self._undo = None
  487. if text is not None:
  488. self.setText(text)
  489. def redo(self):
  490. self._undo = self._model.execute(self._command)
  491. def undo(self):
  492. self._model.execute(self._undo)
  493. def mergeWith(self, other):
  494. if self.id() != other.id():
  495. return False
  496. composit = Composite(self._command, other._command)
  497. merged_command = merge_cmd(composit)
  498. if merged_command is composit:
  499. return False
  500. assert other._undo is not None
  501. composit = Composite(other._undo, self._undo)
  502. merged_undo = merge_cmd(composit)
  503. if merged_undo is composit:
  504. return False
  505. self._command = merged_command
  506. self._undo = merged_undo
  507. return True
  508. def id(self):
  509. return 1
  510. def indices_eq(ind1, ind2):
  511. if isinstance(ind1, tuple) and isinstance(ind2, tuple):
  512. if len(ind1) != len(ind2):
  513. return False
  514. else:
  515. return all(indices_eq(i1, i2) for i1, i2 in zip(ind1, ind2))
  516. elif isinstance(ind1, slice) and isinstance(ind2, slice):
  517. return ind1 == ind2
  518. elif ind1 is ... and ind2 is ...:
  519. return True
  520. ind1, ind1 = np.array(ind1), np.array(ind2)
  521. if ind1.shape != ind2.shape or ind1.dtype != ind2.dtype:
  522. return False
  523. else:
  524. return (ind1 == ind2).all()
  525. def merge_cmd(composit):
  526. f = composit.f
  527. g = composit.g
  528. if isinstance(g, Composite):
  529. g = merge_cmd(g)
  530. if isinstance(f, Append) and isinstance(g, Append):
  531. return Append(np.vstack((f.points, g.points)))
  532. elif isinstance(f, Move) and isinstance(g, Move):
  533. if indices_eq(f.indices, g.indices):
  534. return Move(f.indices, f.delta + g.delta)
  535. else:
  536. # TODO: union of indices, ...
  537. return composit
  538. # elif isinstance(f, DeleteIndices) and isinstance(g, DeleteIndices):
  539. # indices = np.array(g.indices)
  540. # return DeleteIndices(indices)
  541. else:
  542. return composit
  543. def apply_attractor(data, point, density, radius):
  544. delta = data - point
  545. dist_sq = np.sum(delta ** 2, axis=1)
  546. dist = np.sqrt(dist_sq)
  547. dist[dist < radius] = 0
  548. dist_sq = dist ** 2
  549. valid = (dist_sq > 100 * np.finfo(dist.dtype).eps)
  550. assert valid.shape == (dist.shape[0],)
  551. df = 0.05 * density / dist_sq[valid]
  552. df_bound = 1 - radius / dist[valid]
  553. df = np.clip(df, 0, df_bound)
  554. dx = np.zeros_like(delta)
  555. dx[valid] = df.reshape(-1, 1) * delta[valid]
  556. return dx
  557. def apply_jitter(data, point, density, radius, rstate=None):
  558. random = random_state(rstate)
  559. delta = data - point
  560. dist_sq = np.sum(delta ** 2, axis=1)
  561. dist = np.sqrt(dist_sq)
  562. valid = dist_sq > 100 * np.finfo(dist_sq.dtype).eps
  563. df = 0.05 * density / dist_sq[valid]
  564. df_bound = 1 - radius / dist[valid]
  565. df = np.clip(df, 0, df_bound)
  566. dx = np.zeros_like(delta)
  567. jitter = random.normal(0, 0.1, size=(df.size, data.shape[1]))
  568. dx[valid, :] = df.reshape(-1, 1) * jitter
  569. return dx
  570. class ColoredListModel(itemmodels.PyListModel):
  571. def __init__(self, iterable, parent, flags,
  572. list_item_role=QtCore.Qt.DisplayRole,
  573. supportedDropActions=QtCore.Qt.MoveAction):
  574. super().__init__(iterable, parent, flags, list_item_role,
  575. supportedDropActions)
  576. self.colors = colorpalette.ColorPaletteGenerator(
  577. len(colorpalette.DefaultRGBColors))
  578. def data(self, index, role=QtCore.Qt.DisplayRole):
  579. if self._is_index_valid_for(index, self) and \
  580. role == QtCore.Qt.DecorationRole and \
  581. 0 <= index.row() < self.colors.number_of_colors:
  582. return gui.createAttributePixmap("", self.colors[index.row()])
  583. else:
  584. return super().data(index, role)
  585. def _i(name, icon_path="icons/paintdata",
  586. widg_path=os.path.dirname(os.path.abspath(__file__))):
  587. return os.path.join(widg_path, icon_path, name)
  588. class OWPaintData(widget.OWWidget):
  589. TOOLS = [
  590. ("Brush", "Create multiple instances", AirBrushTool, _i("brush.svg")),
  591. ("Put", "Put individual instances", PutInstanceTool, _i("put.svg")),
  592. ("Select", "Select and move instances", SelectTool,
  593. _i("select-transparent_42px.png")),
  594. ("Jitter", "Jitter instances", JitterTool, _i("jitter.svg")),
  595. ("Magnet", "Attract multiple instances", MagnetTool, _i("magnet.svg")),
  596. ("Clear", "Clear the plot", ClearTool, _i("../../../icons/Dlg_clear.png"))
  597. ]
  598. name = "Paint Data"
  599. description = "Create data by painting data points in the plane."
  600. long_description = ""
  601. icon = "icons/PaintData.svg"
  602. priority = 10
  603. keywords = ["data", "paint", "create"]
  604. outputs = [("Data", Orange.data.Table)]
  605. inputs = [("Data", Orange.data.Table, "set_data")]
  606. autocommit = Setting(False)
  607. table_name = Setting("Painted data")
  608. attr1 = Setting("x")
  609. attr2 = Setting("y")
  610. hasAttr2 = Setting(True)
  611. brushRadius = Setting(75)
  612. density = Setting(7)
  613. graph_name = "plot"
  614. def __init__(self):
  615. super().__init__()
  616. self.data = None
  617. self.current_tool = None
  618. self._selected_indices = None
  619. self._scatter_item = None
  620. self.labels = ["C1", "C2"]
  621. self.undo_stack = QtGui.QUndoStack(self)
  622. self.class_model = ColoredListModel(
  623. self.labels, self,
  624. flags=QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled |
  625. QtCore.Qt.ItemIsEditable)
  626. self.class_model.dataChanged.connect(self._class_value_changed)
  627. self.class_model.rowsInserted.connect(self._class_count_changed)
  628. self.class_model.rowsRemoved.connect(self._class_count_changed)
  629. self.data = np.zeros((0, 3))
  630. self.colors = colorpalette.ColorPaletteGenerator(
  631. len(colorpalette.DefaultRGBColors))
  632. self.tools_cache = {}
  633. self._init_ui()
  634. def _init_ui(self):
  635. namesBox = gui.widgetBox(self.controlArea, "Names")
  636. hbox = gui.widgetBox(namesBox, orientation='horizontal', margin=0, spacing=0)
  637. gui.lineEdit(hbox, self, "attr1", "Variable X ",
  638. controlWidth=80, orientation="horizontal",
  639. enterPlaceholder=True, callback=self._attr_name_changed)
  640. gui.separator(hbox, 18)
  641. hbox = gui.widgetBox(namesBox, orientation='horizontal', margin=0, spacing=0)
  642. attr2 = gui.lineEdit(hbox, self, "attr2", "Variable Y ",
  643. controlWidth=80, orientation="horizontal",
  644. enterPlaceholder=True, callback=self._attr_name_changed)
  645. gui.checkBox(hbox, self, "hasAttr2", '', disables=attr2,
  646. labelWidth=0,
  647. callback=self.set_dimensions)
  648. gui.separator(namesBox)
  649. gui.widgetLabel(namesBox, "Labels")
  650. self.classValuesView = listView = QListView(
  651. selectionMode=QListView.SingleSelection,
  652. sizePolicy=QSizePolicy(QSizePolicy.Ignored,
  653. QSizePolicy.Maximum)
  654. )
  655. listView.setModel(self.class_model)
  656. itemmodels.select_row(listView, 0)
  657. namesBox.layout().addWidget(listView)
  658. self.addClassLabel = QAction(
  659. "+", self,
  660. toolTip="Add new class label",
  661. triggered=self.add_new_class_label
  662. )
  663. self.removeClassLabel = QAction(
  664. unicodedata.lookup("MINUS SIGN"), self,
  665. toolTip="Remove selected class label",
  666. triggered=self.remove_selected_class_label
  667. )
  668. actionsWidget = itemmodels.ModelActionsWidget(
  669. [self.addClassLabel, self.removeClassLabel], self
  670. )
  671. actionsWidget.layout().addStretch(10)
  672. actionsWidget.layout().setSpacing(1)
  673. namesBox.layout().addWidget(actionsWidget)
  674. tBox = gui.widgetBox(self.controlArea, "Tools", addSpace=True)
  675. buttonBox = gui.widgetBox(tBox, orientation="horizontal")
  676. toolsBox = gui.widgetBox(buttonBox, orientation=QtGui.QGridLayout())
  677. self.toolActions = QtGui.QActionGroup(self)
  678. self.toolActions.setExclusive(True)
  679. self.toolButtons = []
  680. for i, (name, tooltip, tool, icon) in enumerate(self.TOOLS):
  681. action = QAction(
  682. name, self,
  683. toolTip=tooltip,
  684. checkable=tool.checkable,
  685. icon=QIcon(icon),
  686. )
  687. action.triggered.connect(partial(self.set_current_tool, tool))
  688. button = QtGui.QToolButton(
  689. iconSize=QSize(24, 24),
  690. toolButtonStyle=Qt.ToolButtonTextUnderIcon,
  691. sizePolicy=QSizePolicy(QSizePolicy.MinimumExpanding,
  692. QSizePolicy.Fixed)
  693. )
  694. button.setDefaultAction(action)
  695. self.toolButtons.append((button, tool))
  696. toolsBox.layout().addWidget(button, i / 3, i % 3)
  697. self.toolActions.addAction(action)
  698. for column in range(3):
  699. toolsBox.layout().setColumnMinimumWidth(column, 10)
  700. toolsBox.layout().setColumnStretch(column, 1)
  701. undo = self.undo_stack.createUndoAction(self)
  702. redo = self.undo_stack.createRedoAction(self)
  703. undo.setShortcut(QtGui.QKeySequence.Undo)
  704. redo.setShortcut(QtGui.QKeySequence.Redo)
  705. self.addActions([undo, redo])
  706. gui.separator(tBox)
  707. indBox = gui.indentedBox(tBox, sep=8)
  708. form = QtGui.QFormLayout(
  709. formAlignment=Qt.AlignLeft,
  710. labelAlignment=Qt.AlignLeft,
  711. fieldGrowthPolicy=QtGui.QFormLayout.AllNonFixedFieldsGrow
  712. )
  713. indBox.layout().addLayout(form)
  714. slider = gui.hSlider(
  715. indBox, self, "brushRadius", minValue=1, maxValue=100,
  716. createLabel=False
  717. )
  718. form.addRow("Radius", slider)
  719. slider = gui.hSlider(
  720. indBox, self, "density", None, minValue=1, maxValue=100,
  721. createLabel=False
  722. )
  723. form.addRow("Intensity", slider)
  724. gui.rubber(self.controlArea)
  725. gui.auto_commit(self.controlArea, self, "autocommit",
  726. "Send", "Send on change")
  727. # main area GUI
  728. viewbox = PaintViewBox(enableMouse=False)
  729. self.plotview = pg.PlotWidget(background="w", viewBox=viewbox)
  730. self.plotview.sizeHint = lambda: QSize(200, 100) # Minimum size for 1-d painting
  731. self.plot = self.plotview.getPlotItem()
  732. axis_color = self.palette().color(QtGui.QPalette.Text)
  733. axis_pen = QtGui.QPen(axis_color)
  734. tickfont = QtGui.QFont(self.font())
  735. tickfont.setPixelSize(max(int(tickfont.pixelSize() * 2 // 3), 11))
  736. axis = self.plot.getAxis("bottom")
  737. axis.setLabel(self.attr1)
  738. axis.setPen(axis_pen)
  739. axis.setTickFont(tickfont)
  740. axis = self.plot.getAxis("left")
  741. axis.setLabel(self.attr2)
  742. axis.setPen(axis_pen)
  743. axis.setTickFont(tickfont)
  744. if not self.hasAttr2:
  745. self.plot.hideAxis('left')
  746. self.plot.hideButtons()
  747. self.plot.setXRange(0, 1, padding=0.01)
  748. self.mainArea.layout().addWidget(self.plotview)
  749. # enable brush tool
  750. self.toolActions.actions()[0].setChecked(True)
  751. self.set_current_tool(self.TOOLS[0][2])
  752. self.set_dimensions()
  753. def set_dimensions(self):
  754. if self.hasAttr2:
  755. self.plot.setYRange(0, 1, padding=0.01)
  756. self.plot.showAxis('left')
  757. self.plotview.setSizePolicy(QSizePolicy.Ignored, QSizePolicy.Minimum)
  758. else:
  759. self.plot.setYRange(-.5, .5, padding=0.01)
  760. self.plot.hideAxis('left')
  761. self.plotview.setSizePolicy(QSizePolicy.Ignored, QSizePolicy.Maximum)
  762. self._replot()
  763. for button, tool in self.toolButtons:
  764. if tool.only2d:
  765. button.setDisabled(not self.hasAttr2)
  766. def set_data(self, data):
  767. if data:
  768. try:
  769. y = next(cls for cls in data.domain.class_vars if cls.is_discrete)
  770. except StopIteration:
  771. y = np.ones(X.shape[0])
  772. else:
  773. y = data[:, y].Y
  774. while len(self.class_model) < np.unique(y).size:
  775. self.add_new_class_label(undoable=False)
  776. X = np.array([scale(vals) for vals in data.X[:, :2].T]).T
  777. self.data = np.column_stack((X, y))
  778. self._replot()
  779. def add_new_class_label(self, undoable=True):
  780. newlabel = next(label for label in namegen('C', 1)
  781. if label not in self.class_model)
  782. command = SimpleUndoCommand(
  783. lambda: self.class_model.append(newlabel),
  784. lambda: self.class_model.__delitem__(-1)
  785. )
  786. if undoable:
  787. self.undo_stack.push(command)
  788. else:
  789. command.redo()
  790. def remove_selected_class_label(self):
  791. index = self.selected_class_label()
  792. if index is None:
  793. return
  794. label = self.class_model[index]
  795. mask = self.data[:, 2] == index
  796. move_mask = self.data[~mask][:, 2] > index
  797. self.undo_stack.beginMacro("Delete class label")
  798. self.undo_stack.push(UndoCommand(DeleteIndices(mask), self))
  799. self.undo_stack.push(UndoCommand(Move((move_mask, 2), -1), self))
  800. self.undo_stack.push(
  801. SimpleUndoCommand(lambda: self.class_model.__delitem__(index),
  802. lambda: self.class_model.insert(index, label)))
  803. self.undo_stack.endMacro()
  804. newindex = min(max(index - 1, 0), len(self.class_model) - 1)
  805. itemmodels.select_row(self.classValuesView, newindex)
  806. def _class_count_changed(self):
  807. self.labels = list(self.class_model)
  808. self.removeClassLabel.setEnabled(len(self.class_model) > 1)
  809. self.addClassLabel.setEnabled(
  810. len(self.class_model) < self.colors.number_of_colors)
  811. if self.selected_class_label() is None:
  812. itemmodels.select_row(self.classValuesView, 0)
  813. def _class_value_changed(self, index, _):
  814. index = index.row()
  815. newvalue = self.class_model[index]
  816. oldvalue = self.labels[index]
  817. if newvalue != oldvalue:
  818. self.labels[index] = newvalue
  819. # command = Command(
  820. # lambda: self.class_model.__setitem__(index, newvalue),
  821. # lambda: self.class_model.__setitem__(index, oldvalue),
  822. # )
  823. # self.undo_stack.push(command)
  824. def selected_class_label(self):
  825. rows = self.classValuesView.selectedIndexes()
  826. if rows:
  827. return rows[0].row()
  828. else:
  829. return None
  830. def set_current_tool(self, tool):
  831. prev_tool = self.current_tool.__class__
  832. if self.current_tool is not None:
  833. self.current_tool.deactivate()
  834. self.current_tool.editingStarted.disconnect(
  835. self._on_editing_started)
  836. self.current_tool.editingFinished.disconnect(
  837. self._on_editing_finished)
  838. self.current_tool = None
  839. self.plot.getViewBox().tool = None
  840. if tool not in self.tools_cache:
  841. newtool = tool(self, self.plot)
  842. newtool.editingFinished.connect(self.invalidate)
  843. self.tools_cache[tool] = newtool
  844. newtool.issueCommand.connect(self._add_command)
  845. self._selected_region = QRectF()
  846. self.current_tool = tool = self.tools_cache[tool]
  847. self.plot.getViewBox().tool = tool
  848. tool.editingStarted.connect(self._on_editing_started)
  849. tool.editingFinished.connect(self._on_editing_finished)
  850. tool.activate()
  851. if not tool.checkable:
  852. self.set_current_tool(prev_tool)
  853. def _on_editing_started(self):
  854. self.undo_stack.beginMacro("macro")
  855. def _on_editing_finished(self):
  856. self.undo_stack.endMacro()
  857. def execute(self, command):
  858. if isinstance(command, (Append, DeleteIndices, Insert, Move)):
  859. if isinstance(command, (DeleteIndices, Insert)):
  860. self._selected_indices = None
  861. if isinstance(self.current_tool, SelectTool):
  862. self.current_tool._reset()
  863. self.data, undo = transform(command, self.data)
  864. self._replot()
  865. return undo
  866. else:
  867. assert False, "Non normalized command"
  868. def _add_command(self, cmd):
  869. name = "Name"
  870. if (not self.hasAttr2 and
  871. isinstance(cmd, (Move, MoveSelection, Jitter, Magnet))):
  872. # tool only supported if both x and y are enabled
  873. return
  874. if isinstance(cmd, Append):
  875. cls = self.selected_class_label()
  876. points = np.array([(p.x(), p.y() if self.hasAttr2 else 0, cls)
  877. for p in cmd.points])
  878. self.undo_stack.push(UndoCommand(Append(points), self, text=name))
  879. elif isinstance(cmd, Move):
  880. self.undo_stack.push(UndoCommand(cmd, self, text=name))
  881. elif isinstance(cmd, SelectRegion):
  882. indices = [i for i, (x, y) in enumerate(self.data[:, :2])
  883. if cmd.region.contains(QPointF(x, y))]
  884. indices = np.array(indices, dtype=int)
  885. self._selected_indices = indices
  886. elif isinstance(cmd, DeleteSelection):
  887. indices = self._selected_indices
  888. if indices is not None and indices.size:
  889. self.undo_stack.push(
  890. UndoCommand(DeleteIndices(indices), self, text="Delete")
  891. )
  892. elif isinstance(cmd, MoveSelection):
  893. indices = self._selected_indices
  894. if indices is not None and indices.size:
  895. self.undo_stack.push(
  896. UndoCommand(
  897. Move((self._selected_indices, slice(0, 2)),
  898. np.array([cmd.delta.x(), cmd.delta.y()])),
  899. self, text="Move")
  900. )
  901. elif isinstance(cmd, DeleteIndices):
  902. self.undo_stack.push(UndoCommand(cmd, self, text="Delete"))
  903. elif isinstance(cmd, Insert):
  904. self.undo_stack.push(UndoCommand(cmd, self))
  905. elif isinstance(cmd, AirBrush):
  906. data = create_data(cmd.pos.x(), cmd.pos.y(),
  907. self.brushRadius / 1000,
  908. 1 + self.density / 20, cmd.rstate)
  909. self._add_command(Append([QPointF(*p) for p in zip(*data.T)]))
  910. elif isinstance(cmd, Jitter):
  911. point = np.array([cmd.pos.x(), cmd.pos.y()])
  912. delta = - apply_jitter(self.data[:, :2], point,
  913. self.density / 100.0, 0, cmd.rstate)
  914. self._add_command(Move((..., slice(0, 2)), delta))
  915. elif isinstance(cmd, Magnet):
  916. point = np.array([cmd.pos.x(), cmd.pos.y()])
  917. delta = - apply_attractor(self.data[:, :2], point,
  918. self.density / 100.0, 0)
  919. self._add_command(Move((..., slice(0, 2)), delta))
  920. else:
  921. assert False, "unreachable"
  922. def _replot(self):
  923. def pen(color):
  924. pen = QPen(color, 1)
  925. pen.setCosmetic(True)
  926. return pen
  927. if self._scatter_item is not None:
  928. self.plot.removeItem(self._scatter_item)
  929. self._scatter_item = None
  930. nclasses = len(self.class_model)
  931. pens = [pen(self.colors[i]) for i in range(nclasses)]
  932. self._scatter_item = pg.ScatterPlotItem(
  933. self.data[:, 0],
  934. self.data[:, 1] if self.hasAttr2 else np.zeros(self.data.shape[0]),
  935. symbol="+",
  936. pen=[pens[int(ci)] for ci in self.data[:, 2]]
  937. )
  938. self.plot.addItem(self._scatter_item)
  939. def _attr_name_changed(self):
  940. self.plot.getAxis("bottom").setLabel(self.attr1)
  941. self.plot.getAxis("left").setLabel(self.attr2)
  942. self.invalidate()
  943. def invalidate(self):
  944. self.commit()
  945. def commit(self):
  946. if self.hasAttr2:
  947. X, Y = self.data[:, :2], self.data[:, 2]
  948. attrs = (Orange.data.ContinuousVariable(self.attr1),
  949. Orange.data.ContinuousVariable(self.attr2))
  950. else:
  951. X, Y = self.data[:, np.newaxis, 0], self.data[:, 2]
  952. attrs = (Orange.data.ContinuousVariable(self.attr1),)
  953. if len(np.unique(Y)) >= 2:
  954. domain = Orange.data.Domain(
  955. attrs,
  956. Orange.data.DiscreteVariable(
  957. "Class", values=list(self.class_model))
  958. )
  959. data = Orange.data.Table.from_numpy(domain, X, Y)
  960. else:
  961. domain = Orange.data.Domain(attrs)
  962. data = Orange.data.Table.from_numpy(domain, X)
  963. data.name = self.table_name
  964. self.send("Data", data)
  965. def sizeHint(self):
  966. sh = super().sizeHint()
  967. return sh.expandedTo(QSize(1200, 800))
  968. def onDeleteWidget(self):
  969. self.plot.clear()
  970. def send_report(self):
  971. if self.data is None:
  972. return
  973. settings = []
  974. if self.attr1 != "x" or self.attr2 != "y":
  975. settings += [("Axis x", self.attr1), ("Axis y", self.attr2)]
  976. settings += [("Number of points", len(self.data))]
  977. self.report_items("Painted data", settings)
  978. self.report_plot()
  979. def test():
  980. import gc
  981. import sip
  982. app = QtGui.QApplication([])
  983. ow = OWPaintData()
  984. ow.show()
  985. ow.raise_()
  986. rval = app.exec_()
  987. ow.saveSettings()
  988. ow.onDeleteWidget()
  989. sip.delete(ow)
  990. del ow
  991. gc.collect()
  992. app.processEvents()
  993. return rval
  994. if __name__ == "__main__":
  995. sys.exit(test())