GitHub Access Token became invalid

It seems like the GitHub access token used for retrieving details about this repository from GitHub became invalid. This might prevent certain types of inspections from being run (in particular, everything related to pull requests).
Please ask an admin of your repository to re-new the access token on this website.

Orange.widgets.unsupervised.OWMDS   F
last analyzed

Complexity

Total Complexity 172

Size/Duplication

Total Lines 897
Duplicated Lines 0 %
Metric Value
dl 0
loc 897
rs 1.2632
wmc 172

38 Methods

Rating   Name   Duplication   Size   Complexity  
B __invalidate_embedding() 0 22 5
A scale() 0 6 2
A activate_tool() 0 12 4
A _on_connected_changed() 0 3 1
A _on_label_index_changed() 0 3 1
D __start() 0 51 9
A _on_size_index_changed() 0 3 1
A stop() 0 3 2
A _on_shape_index_changed() 0 3 1
F _setup_plot() 0 208 50
A _toggle_run() 0 6 2
B select_indices() 0 15 6
A set_data() 0 11 4
D __next_step() 0 22 9
A __selection_end() 0 5 1
B start() 0 9 5
A _clear() 0 19 1
A _on_color_index_changed() 0 3 1
A handleNewSignals() 0 9 2
F commit() 0 40 13
A _invalidate_output() 0 2 1
F update_controls() 0 34 11
F _initialize() 0 34 10
A save_graph() 0 6 1
A onDeleteWidget() 0 4 1
A _clear_plot() 0 10 4
A __invalidate_refresh() 0 11 3
B update_loop() 0 29 4
B select() 0 14 5
A column() 0 3 1
B __set_update_loop() 0 34 3
A _update_plot() 0 5 2
F __init__() 0 202 9
A icon() 0 4 1
A attributes() 0 2 1
A button() 0 5 1
A set_disimilarity() 0 8 4
A customEvent() 0 16 4

How to fix   Complexity   

Complex Class

Complex classes like Orange.widgets.unsupervised.OWMDS often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

1
import sys
2
import warnings
3
from xml.sax.saxutils import escape
4
5
import pkg_resources
6
7
import numpy
0 ignored issues
show
Configuration introduced by
The import numpy could not be resolved.

This can be caused by one of the following:

1. Missing Dependencies

This error could indicate a configuration issue of Pylint. Make sure that your libraries are available by adding the necessary commands.

# .scrutinizer.yml
before_commands:
    - sudo pip install abc # Python2
    - sudo pip3 install abc # Python3
Tip: We are currently not using virtualenv to run pylint, when installing your modules make sure to use the command for the correct version.

2. Missing __init__.py files

This error could also result from missing __init__.py files in your module folders. Make sure that you place one file in each sub-folder.

Loading history...
8
import scipy.spatial.distance
0 ignored issues
show
Configuration introduced by
The import scipy.spatial.distance could not be resolved.

This can be caused by one of the following:

1. Missing Dependencies

This error could indicate a configuration issue of Pylint. Make sure that your libraries are available by adding the necessary commands.

# .scrutinizer.yml
before_commands:
    - sudo pip install abc # Python2
    - sudo pip3 install abc # Python3
Tip: We are currently not using virtualenv to run pylint, when installing your modules make sure to use the command for the correct version.

2. Missing __init__.py files

This error could also result from missing __init__.py files in your module folders. Make sure that you place one file in each sub-folder.

Loading history...
9
from itertools import chain
10
11
from PyQt4 import QtGui
0 ignored issues
show
Configuration introduced by
The import PyQt4 could not be resolved.

This can be caused by one of the following:

1. Missing Dependencies

This error could indicate a configuration issue of Pylint. Make sure that your libraries are available by adding the necessary commands.

# .scrutinizer.yml
before_commands:
    - sudo pip install abc # Python2
    - sudo pip3 install abc # Python3
Tip: We are currently not using virtualenv to run pylint, when installing your modules make sure to use the command for the correct version.

2. Missing __init__.py files

This error could also result from missing __init__.py files in your module folders. Make sure that you place one file in each sub-folder.

Loading history...
12
from PyQt4.QtCore import Qt, QEvent
0 ignored issues
show
Configuration introduced by
The import PyQt4.QtCore could not be resolved.

This can be caused by one of the following:

1. Missing Dependencies

This error could indicate a configuration issue of Pylint. Make sure that your libraries are available by adding the necessary commands.

# .scrutinizer.yml
before_commands:
    - sudo pip install abc # Python2
    - sudo pip3 install abc # Python3
Tip: We are currently not using virtualenv to run pylint, when installing your modules make sure to use the command for the correct version.

2. Missing __init__.py files

This error could also result from missing __init__.py files in your module folders. Make sure that you place one file in each sub-folder.

Loading history...
13
14
import pyqtgraph as pg
0 ignored issues
show
Configuration introduced by
The import pyqtgraph could not be resolved.

This can be caused by one of the following:

1. Missing Dependencies

This error could indicate a configuration issue of Pylint. Make sure that your libraries are available by adding the necessary commands.

# .scrutinizer.yml
before_commands:
    - sudo pip install abc # Python2
    - sudo pip3 install abc # Python3
Tip: We are currently not using virtualenv to run pylint, when installing your modules make sure to use the command for the correct version.

2. Missing __init__.py files

This error could also result from missing __init__.py files in your module folders. Make sure that you place one file in each sub-folder.

Loading history...
15
import pyqtgraph.graphicsItems.ScatterPlotItem
0 ignored issues
show
Configuration introduced by
The import pyqtgraph.graphicsItems.ScatterPlotItem could not be resolved.

This can be caused by one of the following:

1. Missing Dependencies

This error could indicate a configuration issue of Pylint. Make sure that your libraries are available by adding the necessary commands.

# .scrutinizer.yml
before_commands:
    - sudo pip install abc # Python2
    - sudo pip3 install abc # Python3
Tip: We are currently not using virtualenv to run pylint, when installing your modules make sure to use the command for the correct version.

2. Missing __init__.py files

This error could also result from missing __init__.py files in your module folders. Make sure that you place one file in each sub-folder.

Loading history...
16
17
from Orange.widgets import widget, gui, settings
18
from Orange.widgets.utils import colorpalette
19
20
from Orange.widgets.utils import itemmodels
21
22
import Orange.data
23
import Orange.projection
24
import Orange.distance
25
import Orange.misc
26
from Orange.widgets.io import FileFormats
27
28
29
def torgerson(distances, n_components=2):
30
    """
31
    Perform classical mds (Torgerson scaling).
32
33
    ..note ::
34
        If the distances are euclidean then this is equivalent to projecting
35
        the original data points to the first `n` principal components.
36
37
    """
38
    distances = numpy.asarray(distances)
39
    assert distances.shape[0] == distances.shape[1]
40
    N = distances.shape[0]
41
    # O ^ 2
42
    D_sq = distances ** 2
43
44
    # double center the D_sq
45
    rsum = numpy.sum(D_sq, axis=1, keepdims=True)
46
    csum = numpy.sum(D_sq, axis=0, keepdims=True)
47
    total = numpy.sum(csum)
48
    D_sq -= rsum / N
49
    D_sq -= csum / N
50
    D_sq += total / (N ** 2)
51
    B = numpy.multiply(D_sq, -0.5, out=D_sq)
52
53
    U, L, _ = numpy.linalg.svd(B)
54
    if n_components > N:
55
        U = numpy.hstack((U, numpy.zeros((N, n_components - N))))
56
        L = numpy.hstack((L, numpy.zeros((n_components - N))))
57
    U = U[:, :n_components]
58
    L = L[:n_components]
59
    D = numpy.diag(numpy.sqrt(L))
60
    return numpy.dot(U, D)
61
62
63
def stress(X, D):
64
    assert X.shape[0] == D.shape[0] == D.shape[1]
65
    D1_c = scipy.spatial.distance.pdist(X, metric="euclidean")
66
    D1 = scipy.spatial.distance.squareform(D1_c, checks=False)
67
    delta = D1 - D
68
    delta_sq = numpy.square(delta, out=delta)
69
    return delta_sq.sum(axis=0) / 2
70
71
72
def make_pen(color, width=1.5, style=Qt.SolidLine, cosmetic=False):
73
    pen = QtGui.QPen(color, width, style)
74
    pen.setCosmetic(cosmetic)
75
    return pen
76
77
78
class ScatterPlotItem(pg.ScatterPlotItem):
79
    Symbols = pyqtgraph.graphicsItems.ScatterPlotItem.Symbols
80
81
    def __init__(self, *args, **kwargs):
82
        super().__init__(*args, **kwargs)
83
84
    def paint(self, painter, option, widget=None):
0 ignored issues
show
Comprehensibility Bug introduced by
widget is re-defining a name which is already available in the outer-scope (previously defined on line 17).

It is generally a bad practice to shadow variables from the outer-scope. In most cases, this is done unintentionally and might lead to unexpected behavior:

param = 5

class Foo:
    def __init__(self, param):   # "param" would be flagged here
        self.param = param
Loading history...
85
        if self.opts["pxMode"]:
86
            painter.setRenderHint(QtGui.QPainter.SmoothPixmapTransform, True)
87
88
        if self.opts["antialias"]:
89
            painter.setRenderHint(QtGui.QPainter.Antialiasing, True)
90
91
        super().paint(painter, option, widget)
92
93
94
class OWMDS(widget.OWWidget):
95
    name = "MDS"
96
    description = "Two-dimensional data projection by multidimensional " \
97
                  "scaling constructed from a distance matrix."
98
    icon = "icons/MDS.svg"
99
    inputs = [("Data", Orange.data.Table, "set_data"),
100
              ("Distances", Orange.misc.DistMatrix, "set_disimilarity")]
101
    outputs = [("Data", Orange.data.Table, widget.Default),
102
               ("Selected Data", Orange.data.Table)]
103
104
    #: Initialization type
105
    PCA, Random = 0, 1
106
107
    #: Refresh rate
108
    RefreshRate = [
109
        ("Every iteration", 1),
110
        ("Every 5 steps", 5),
111
        ("Every 10 steps", 10),
112
        ("Every 25 steps", 25),
113
        ("Every 50 steps", 50),
114
        ("None", -1)
115
    ]
116
117
    #: Runtime state
118
    Running, Finished, Waiting = 1, 2, 3
119
120
    settingsHandler = settings.DomainContextHandler()
121
122
    max_iter = settings.Setting(300)
123
    initialization = settings.Setting(PCA)
124
    refresh_rate = settings.Setting(3)
125
126
    # output embedding role.
127
    NoRole, AttrRole, AddAttrRole, MetaRole = 0, 1, 2, 3
128
129
    output_embedding_role = settings.Setting(2)
130
    autocommit = settings.Setting(True)
131
132
    color_value = settings.ContextSetting("")
133
    shape_value = settings.ContextSetting("")
134
    size_value = settings.ContextSetting("")
135
    label_value = settings.ContextSetting("")
136
137
    symbol_size = settings.Setting(8)
138
    symbol_opacity = settings.Setting(230)
139
    connected_pairs = settings.Setting(5)
140
    spread_equal_points = settings.Setting(False)
141
142
    legend_anchor = settings.Setting(((1, 0), (1, 0)))
143
144
    want_graph = True
145
146
    def __init__(self, parent=None):
147
        super().__init__(parent)
148
        self.matrix = None
149
        self.data = None
150
        self.matrix_data = None
151
        self.signal_data = None
152
153
        self._pen_data = None
154
        self._shape_data = None
155
        self._size_data = None
156
        self._label_data = None
157
        self._similar_pairs = None
158
        self._scatter_item = None
159
        self._legend_item = None
160
        self._selection_mask = None
161
        self._invalidated = False
162
        self._effective_matrix = None
163
164
        self.__update_loop = None
165
        self.__state = OWMDS.Waiting
166
        self.__in_next_step = False
167
        self.__draw_similar_pairs = False
168
169
        box = gui.widgetBox(self.controlArea, "MDS Optimization")
170
        form = QtGui.QFormLayout(
171
            labelAlignment=Qt.AlignLeft,
172
            formAlignment=Qt.AlignLeft,
173
            fieldGrowthPolicy=QtGui.QFormLayout.AllNonFixedFieldsGrow,
174
            verticalSpacing=10
175
        )
176
177
        form.addRow("Max iterations:",
178
                    gui.spin(box, self, "max_iter", 10, 10 ** 4, step=1))
179
180
        form.addRow("Initialization",
181
                    gui.comboBox(box, self, "initialization",
182
                                 items=["PCA (Torgerson)", "Random"],
183
                                 callback=self.__invalidate_embedding))
184
185
        box.layout().addLayout(form)
186
        form.addRow("Refresh",
187
                    gui.comboBox(
188
                        box, self, "refresh_rate",
189
                        items=[t for t, _ in OWMDS.RefreshRate],
190
                        callback=self.__invalidate_refresh))
191
        gui.separator(box, 10)
192
        gui.checkBox(box, self, "spread_equal_points",
193
                     "Spread points at zero-distances",
194
                     callback=self.__invalidate_embedding)
195
        gui.separator(box, 10)
196
        self.runbutton = gui.button(
197
            box, self, "Run", callback=self._toggle_run)
198
199
        box = gui.widgetBox(self.controlArea, "Graph")
200
        self.colorvar_model = itemmodels.VariableListModel()
201
202
        common_options = {"sendSelectedValue": True, "valueType": str,
203
                          "orientation": "horizontal", "labelWidth": 50,
204
                          "contentsLength": 12}
205
206
        self.cb_color_value = gui.comboBox(
207
            box, self, "color_value", label="Color",
208
            callback=self._on_color_index_changed, **common_options)
209
        self.cb_color_value.setModel(self.colorvar_model)
210
211
        self.shapevar_model = itemmodels.VariableListModel()
212
        self.cb_shape_value = gui.comboBox(
213
            box, self, "shape_value", label="Shape",
214
            callback=self._on_shape_index_changed, **common_options)
215
        self.cb_shape_value.setModel(self.shapevar_model)
216
217
        self.sizevar_model = itemmodels.VariableListModel()
218
        self.cb_size_value = gui.comboBox(
219
            box, self, "size_value", label="Size",
220
            callback=self._on_size_index_changed, **common_options)
221
        self.cb_size_value.setModel(self.sizevar_model)
222
223
        self.labelvar_model = itemmodels.VariableListModel()
224
        self.cb_label_value = gui.comboBox(
225
            box, self, "label_value", label="Label",
226
            callback=self._on_label_index_changed, **common_options)
227
        self.cb_label_value.setModel(self.labelvar_model)
228
229
        form = QtGui.QFormLayout(
230
            labelAlignment=Qt.AlignLeft,
231
            formAlignment=Qt.AlignLeft,
232
            fieldGrowthPolicy=QtGui.QFormLayout.AllNonFixedFieldsGrow,
233
            verticalSpacing=10
234
        )
235
        form.addRow("Symbol size",
236
                    gui.hSlider(box, self, "symbol_size",
237
                                minValue=1, maxValue=20,
238
                                callback=self._on_size_index_changed,
239
                                createLabel=False))
240
        form.addRow("Symbol opacity",
241
                    gui.hSlider(box, self, "symbol_opacity",
242
                                minValue=100, maxValue=255, step=100,
243
                                callback=self._on_color_index_changed,
244
                                createLabel=False))
245
        form.addRow("Show similar pairs",
246
                    gui.hSlider(
247
                        gui.widgetBox(self.controlArea,
248
                                      orientation="horizontal"),
249
                        self, "connected_pairs", minValue=0, maxValue=20,
250
                        createLabel=False,
251
                        callback=self._on_connected_changed))
252
        box.layout().addLayout(form)
253
254
        gui.rubber(self.controlArea)
255
256
        box = QtGui.QGroupBox("Zoom/Select", )
257
        box.setLayout(QtGui.QHBoxLayout())
258
        box.layout().setMargin(2)
259
260
        group = QtGui.QActionGroup(self, exclusive=True)
261
262
        def icon(name):
263
            path = "icons/Dlg_{}.png".format(name)
264
            path = pkg_resources.resource_filename(widget.__name__, path)
265
            return QtGui.QIcon(path)
266
267
        action_select = QtGui.QAction(
268
            "Select", self, checkable=True, checked=True, icon=icon("arrow"),
269
            shortcut=QtGui.QKeySequence(Qt.ControlModifier + Qt.Key_1))
270
        action_zoom = QtGui.QAction(
271
            "Zoom", self, checkable=True, checked=False, icon=icon("zoom"),
272
            shortcut=QtGui.QKeySequence(Qt.ControlModifier + Qt.Key_2))
273
        action_pan = QtGui.QAction(
274
            "Pan", self, checkable=True, checked=False, icon=icon("pan_hand"),
275
            shortcut=QtGui.QKeySequence(Qt.ControlModifier + Qt.Key_3))
276
277
        action_reset_zoom = QtGui.QAction(
278
            "Zoom to fit", self, icon=icon("zoom_reset"),
279
            shortcut=QtGui.QKeySequence(Qt.ControlModifier + Qt.Key_0))
280
        action_reset_zoom.triggered.connect(
281
            lambda: self.plot.autoRange(padding=0.1,
282
                                        items=[self._scatter_item]))
283
        group.addAction(action_select)
284
        group.addAction(action_zoom)
285
        group.addAction(action_pan)
286
        self.addActions(group.actions() + [action_reset_zoom])
287
        action_select.setChecked(True)
288
289
        def button(action):
290
            b = QtGui.QToolButton()
291
            b.setToolButtonStyle(Qt.ToolButtonIconOnly)
292
            b.setDefaultAction(action)
293
            return b
294
295
        box.layout().addWidget(button(action_select))
296
        box.layout().addWidget(button(action_zoom))
297
        box.layout().addWidget(button(action_pan))
298
        box.layout().addSpacing(4)
299
        box.layout().addWidget(button(action_reset_zoom))
300
        box.layout().addStretch()
301
302
        self.controlArea.layout().addWidget(box)
303
304
        box = gui.widgetBox(self.controlArea, "Output")
305
        gui.comboBox(box, self, "output_embedding_role",
306
                     items=["Original features only",
307
                            "Coordinates only",
308
                            "Coordinates as features",
309
                            "Coordinates as meta attributes"],
310
                     callback=self._invalidate_output, addSpace=4)
311
        gui.auto_commit(box, self, "autocommit", "Send data",
312
                        checkbox_label="Send after any change",
313
                        box=None)
314
315
        self.plot = pg.PlotWidget(background="w", enableMenu=False)
316
        self.plot.getPlotItem().hideAxis("bottom")
317
        self.plot.getPlotItem().hideAxis("left")
318
        self.plot.getPlotItem().hideButtons()
319
        self.plot.setRenderHint(QtGui.QPainter.Antialiasing)
320
        self.mainArea.layout().addWidget(self.plot)
321
322
        self.selection_tool = PlotSelectionTool(parent=self)
323
        self.zoom_tool = PlotZoomTool(parent=self)
324
        self.pan_tool = PlotPanTool(parent=self)
325
        self.pinch_tool = PlotPinchZoomTool(parent=self)
326
        self.pinch_tool.setViewBox(self.plot.getViewBox())
327
        self.selection_tool.setViewBox(self.plot.getViewBox())
328
        self.selection_tool.selectionFinished.connect(self.__selection_end)
329
        self.current_tool = self.selection_tool
330
331
        def activate_tool(action):
332
            self.current_tool.setViewBox(None)
333
334
            if action is action_select:
335
                active, cur = self.selection_tool, Qt.ArrowCursor
336
            elif action is action_zoom:
337
                active, cur = self.zoom_tool, Qt.ArrowCursor
338
            elif action is action_pan:
339
                active, cur = self.pan_tool, Qt.OpenHandCursor
340
            self.current_tool = active
341
            self.current_tool.setViewBox(self.plot.getViewBox())
342
            self.plot.getViewBox().setCursor(QtGui.QCursor(cur))
343
344
        group.triggered[QtGui.QAction].connect(activate_tool)
345
        self.graphButton.clicked.connect(self.save_graph)
346
347
        self._initialize()
348
349
    def set_data(self, data):
350
        self.signal_data = data
351
352
        if self.matrix is not None and data is not None and len(self.matrix) == len(data):
353
            self.closeContext()
354
            self.data = data
355
            self.update_controls()
356
            self.openContext(data)
357
        else:
358
            self._invalidated = True
359
        self._selection_mask = None
360
361
    def set_disimilarity(self, matrix):
362
        self.matrix = matrix
363
        if matrix is not None and matrix.row_items:
364
            self.matrix_data = matrix.row_items
365
        if matrix is None:
366
            self.matrix_data = None
367
        self._invalidated = True
368
        self._selection_mask = None
369
370
    def _clear(self):
371
        self._pen_data = None
372
        self._shape_data = None
373
        self._size_data = None
374
        self._label_data = None
375
        self._similar_pairs = None
376
377
        self.colorvar_model[:] = ["Same color"]
378
        self.shapevar_model[:] = ["Same shape"]
379
        self.sizevar_model[:] = ["Same size"]
380
        self.labelvar_model[:] = ["No labels"]
381
382
        self.color_value = self.colorvar_model[0]
383
        self.shape_value = self.shapevar_model[0]
384
        self.size_value = self.sizevar_model[0]
385
        self.label_value = self.labelvar_model[0]
386
387
        self.__set_update_loop(None)
388
        self.__state = OWMDS.Waiting
389
390
    def _clear_plot(self):
391
        self.plot.clear()
392
        self._scatter_item = None
393
        if self._legend_item is not None:
394
            anchor = legend_anchor_pos(self._legend_item)
395
            if anchor is not None:
396
                self.legend_anchor = anchor
397
            if self._legend_item.scene() is not None:
398
                self._legend_item.scene().removeItem(self._legend_item)
399
            self._legend_item = None
400
401
    def update_controls(self):
402
        if getattr(self.matrix, 'axis', 1) == 0:
403
            # Column-wise distances
404
            attr = "Attribute names"
405
            self.labelvar_model[:] = ["No labels", attr]
406
            self.shapevar_model[:] = ["Same shape", attr]
407
            self.colorvar_model[:] = ["Same color", attr]
408
409
            self.color_value = attr
410
            self.shape_value = attr
411
        else:
412
            # initialize the graph state from data
413
            domain = self.data.domain
414
            all_vars = list(domain.variables + domain.metas)
415
            cd_vars = [var for var in all_vars if var.is_primitive()]
416
            disc_vars = [var for var in all_vars if var.is_discrete]
417
            cont_vars = [var for var in all_vars if var.is_continuous]
418
            shape_vars = [var for var in disc_vars
419
                          if len(var.values) <= len(ScatterPlotItem.Symbols) - 1]
420
            self.colorvar_model[:] = chain(["Same color"],
421
                                           [self.colorvar_model.Separator],
422
                                           cd_vars)
423
            self.shapevar_model[:] = chain(["Same shape"],
424
                                           [self.shapevar_model.Separator],
425
                                           shape_vars)
426
            self.sizevar_model[:] = chain(["Same size", "Stress"],
427
                                          [self.sizevar_model.Separator],
428
                                          cont_vars)
429
            self.labelvar_model[:] = chain(["No labels"],
430
                                           [self.labelvar_model.Separator],
431
                                           all_vars)
432
433
            if domain.class_var is not None:
434
                self.color_value = domain.class_var.name
435
436
    def _initialize(self):
437
        # clear everything
438
        self.closeContext()
439
        self._clear()
440
        self.data = None
441
        self._effective_matrix = None
442
        self.embedding = None
443
444
        # if no data nor matrix is present reset plot
445
        if self.signal_data is None and self.matrix is None:
446
            return
447
448
        if self.signal_data and self.matrix_data and len(self.signal_data) != len(self.matrix_data):
449
            self.error(1, "Data and distances dimensions do not match.")
450
            self._update_plot()
451
            return
452
453
        self.error(1)
454
455
        if self.signal_data:
456
            self.data = self.signal_data
457
        elif self.matrix_data:
458
            self.data = self.matrix_data
459
460
        if self.matrix is not None:
461
            self._effective_matrix = self.matrix
462
            if self.matrix.axis == 0:
463
                self.data = None
464
        else:
465
            preprocessed_data = Orange.projection.MDS().preprocess(self.data)
466
            self._effective_matrix = Orange.distance.Euclidean(preprocessed_data)
467
468
        self.update_controls()
469
        self.openContext(self.data)
470
471
    def _toggle_run(self):
472
        if self.__state == OWMDS.Running:
473
            self.stop()
474
            self._invalidate_output()
475
        else:
476
            self.start()
477
478
    def start(self):
479
        if self.__state == OWMDS.Running:
480
            return
481
        elif self.__state == OWMDS.Finished:
482
            # Resume/continue from a previous run
483
            self.__start()
484
        elif self.__state == OWMDS.Waiting and \
485
                self._effective_matrix is not None:
486
            self.__start()
487
488
    def stop(self):
489
        if self.__state == OWMDS.Running:
490
            self.__set_update_loop(None)
491
492
    def __start(self):
493
        self.__draw_similar_pairs = False
494
        X = self._effective_matrix
495
        if self.spread_equal_points:
496
            maxval = numpy.max(X)
497
            X = numpy.clip(X, maxval / 10, maxval)
498
499
        if self.embedding is not None:
500
            init = self.embedding
501
        elif self.initialization == OWMDS.PCA:
502
            init = torgerson(X, n_components=2)
503
        else:
504
            init = None
505
506
        # number of iterations per single GUI update step
507
        _, step_size = OWMDS.RefreshRate[self.refresh_rate]
508
        if step_size == -1:
509
            step_size = self.max_iter
510
511
        def update_loop(X, max_iter, step, init):
512
            """
513
            return an iterator over successive improved MDS point embeddings.
514
            """
515
            # NOTE: this code MUST NOT call into QApplication.processEvents
516
            done = False
517
            iterations_done = 0
518
            oldstress = numpy.finfo(numpy.float).max
519
520
            while not done:
521
                step_iter = min(max_iter - iterations_done, step)
522
                mds = Orange.projection.MDS(
523
                    dissimilarity="precomputed", n_components=2,
524
                    n_init=1, max_iter=step_iter)
525
526
                mdsfit = mds.fit(X, init=init)
527
                iterations_done += step_iter
528
529
                embedding, stress = mdsfit.embedding_, mdsfit.stress_
0 ignored issues
show
Comprehensibility Bug introduced by
stress is re-defining a name which is already available in the outer-scope (previously defined on line 63).

It is generally a bad practice to shadow variables from the outer-scope. In most cases, this is done unintentionally and might lead to unexpected behavior:

param = 5

class Foo:
    def __init__(self, param):   # "param" would be flagged here
        self.param = param
Loading history...
530
                stress /= numpy.sqrt(numpy.sum(embedding ** 2, axis=1)).sum()
531
532
                if iterations_done >= max_iter:
533
                    done = True
534
                elif (oldstress - stress) < mds.params["eps"]:
535
                    done = True
536
                init = embedding
537
                oldstress = stress
538
539
                yield embedding, mdsfit.stress_, iterations_done / max_iter
540
541
        self.__set_update_loop(update_loop(X, self.max_iter, step_size, init))
542
        self.progressBarInit(processEvents=None)
543
544
    def __set_update_loop(self, loop):
545
        """
546
        Set the update `loop` coroutine.
547
548
        The `loop` is a generator yielding `(embedding, stress, progress)`
549
        tuples where `embedding` is a `(N, 2) ndarray` of current updated
550
        MDS points, `stress` is the current stress and `progress` a float
551
        ratio (0 <= progress <= 1)
552
553
        If an existing update loop is already in palace it is interrupted
554
        (closed).
555
556
        .. note::
557
            The `loop` must not explicitly yield control flow to the event
558
            loop (i.e. call `QApplication.processEvents`)
559
560
        """
561
        if self.__update_loop is not None:
562
            self.__update_loop.close()
563
            self.__update_loop = None
564
            self.progressBarFinished(processEvents=None)
565
566
        self.__update_loop = loop
567
568
        if loop is not None:
569
            self.progressBarInit(processEvents=None)
570
            self.setStatusMessage("Running")
571
            self.runbutton.setText("Stop")
572
            self.__state = OWMDS.Running
573
            QtGui.QApplication.postEvent(self, QEvent(QEvent.User))
574
        else:
575
            self.setStatusMessage("")
576
            self.runbutton.setText("Start")
577
            self.__state = OWMDS.Finished
578
579
    def __next_step(self):
580
        if self.__update_loop is None:
581
            return
582
583
        loop = self.__update_loop
584
        try:
585
            embedding, stress, progress = next(self.__update_loop)
0 ignored issues
show
Comprehensibility Bug introduced by
stress is re-defining a name which is already available in the outer-scope (previously defined on line 63).

It is generally a bad practice to shadow variables from the outer-scope. In most cases, this is done unintentionally and might lead to unexpected behavior:

param = 5

class Foo:
    def __init__(self, param):   # "param" would be flagged here
        self.param = param
Loading history...
Unused Code introduced by
The variable stress seems to be unused.
Loading history...
586
            assert self.__update_loop is loop
587
        except StopIteration:
588
            self.__set_update_loop(None)
589
            self.unconditional_commit()
590
            self.__draw_similar_pairs = True
591
            self._update_plot()
592
            self.plot.autoRange(padding=0.1, items=[self._scatter_item])
593
        else:
594
            self.progressBarSet(100.0 * progress, processEvents=None)
595
            self.embedding = embedding
0 ignored issues
show
Coding Style introduced by
The attribute embedding was defined outside __init__.

It is generally a good practice to initialize all attributes to default values in the __init__ method:

class Foo:
    def __init__(self, x=None):
        self.x = x
Loading history...
596
            self._update_plot()
597
            self.plot.autoRange(padding=0.1, items=[self._scatter_item])
598
            # schedule next update
599
            QtGui.QApplication.postEvent(
600
                self, QEvent(QEvent.User), Qt.LowEventPriority)
601
602
    def customEvent(self, event):
603
        if event.type() == QEvent.User and self.__update_loop is not None:
604
            if not self.__in_next_step:
605
                self.__in_next_step = True
606
                try:
607
                    self.__next_step()
608
                finally:
609
                    self.__in_next_step = False
610
            else:
611
                warnings.warn(
612
                    "Re-entry in update loop detected. "
613
                    "A rogue `proccessEvents` is on the loose.",
614
                    RuntimeWarning)
615
                # re-schedule the update iteration.
616
                QtGui.QApplication.postEvent(self, QEvent(QEvent.User))
617
        return super().customEvent(event)
618
619
    def __invalidate_embedding(self):
620
        # reset/invalidate the MDS embedding, to the default initialization
621
        # (Random or PCA), restarting the optimization if necessary.
622
        if self.embedding is None:
623
            return
624
        state = self.__state
625
        if self.__update_loop is not None:
626
            self.__set_update_loop(None)
627
628
        X = self._effective_matrix
629
630
        if self.initialization == OWMDS.PCA:
631
            self.embedding = torgerson(X)
0 ignored issues
show
Coding Style introduced by
The attribute embedding was defined outside __init__.

It is generally a good practice to initialize all attributes to default values in the __init__ method:

class Foo:
    def __init__(self, x=None):
        self.x = x
Loading history...
632
        else:
633
            self.embedding = numpy.random.rand(len(X), 2)
0 ignored issues
show
Coding Style introduced by
The attribute embedding was defined outside __init__.

It is generally a good practice to initialize all attributes to default values in the __init__ method:

class Foo:
    def __init__(self, x=None):
        self.x = x
Loading history...
634
635
        self._update_plot()
636
        self.plot.autoRange(padding=0.1, items=[self._scatter_item])
637
638
        # restart the optimization if it was interrupted.
639
        if state == OWMDS.Running:
640
            self.__start()
641
642
    def __invalidate_refresh(self):
643
        state = self.__state
644
645
        if self.__update_loop is not None:
646
            self.__set_update_loop(None)
647
648
        # restart the optimization if it was interrupted.
649
        # TODO: decrease the max iteration count by the already
0 ignored issues
show
Coding Style introduced by
TODO and FIXME comments should generally be avoided.
Loading history...
650
        # completed iterations count.
651
        if state == OWMDS.Running:
652
            self.__start()
653
654
    def handleNewSignals(self):
655
        if self._invalidated:
656
            self._invalidated = False
657
            self._initialize()
658
            self.start()
659
        self.__draw_similar_pairs = False
660
        self._update_plot()
661
        self.plot.autoRange(padding=0.1)
662
        self.unconditional_commit()
663
664
    def _invalidate_output(self):
665
        self.commit()
666
667
    def _on_color_index_changed(self):
668
        self._pen_data = None
669
        self._update_plot()
670
671
    def _on_shape_index_changed(self):
672
        self._shape_data = None
673
        self._update_plot()
674
675
    def _on_size_index_changed(self):
676
        self._size_data = None
677
        self._update_plot()
678
679
    def _on_label_index_changed(self):
680
        self._label_data = None
681
        self._update_plot()
682
683
    def _on_connected_changed(self):
684
        self._similar_pairs = None
685
        self._update_plot()
686
687
    def _update_plot(self):
688
        self._clear_plot()
689
690
        if self.embedding is not None:
691
            self._setup_plot()
692
693
    def _setup_plot(self):
694
        have_data = self.data is not None
695
        have_matrix_transposed = self.matrix is not None and not self.matrix.axis
696
        plotstyle = mdsplotutils.plotstyle
697
698
        def column(data, variable):
699
            a, _ = data.get_column_view(variable)
700
            return a.ravel()
701
702
        def attributes(matrix):
703
            return matrix.row_items.domain.attributes
704
705
        def scale(a):
706
            dmin, dmax = numpy.nanmin(a), numpy.nanmax(a)
707
            if dmax - dmin > 0:
708
                return (a - dmin) / (dmax - dmin)
709
            else:
710
                return numpy.zeros_like(a)
711
712
        if self._pen_data is None:
713
            if self._selection_mask is not None:
714
                pointflags = numpy.where(
715
                    self._selection_mask,
716
                    mdsplotutils.Selected, mdsplotutils.NoFlags)
717
            else:
718
                pointflags = None
719
720
            color_index = self.cb_color_value.currentIndex()
721
            if have_data and color_index > 0:
722
                color_var = self.colorvar_model[color_index]
723
                if color_var.is_discrete:
724
                    palette = colorpalette.ColorPaletteGenerator(
725
                        len(color_var.values)
726
                    )
727
                    plotstyle = plotstyle.updated(discrete_palette=palette)
728
                else:
729
                    palette = None
730
731
                color_data = mdsplotutils.color_data(
732
                    self.data, color_var, plotstyle=plotstyle)
733
                color_data = numpy.hstack(
734
                    (color_data,
735
                     numpy.full((len(color_data), 1), self.symbol_opacity))
736
                )
737
                pen_data = mdsplotutils.pen_data(color_data * 0.8, pointflags)
738
                brush_data = mdsplotutils.brush_data(color_data)
739
            elif have_matrix_transposed and \
740
                    self.colorvar_model[color_index] == 'Attribute names':
741
                attr = attributes(self.matrix)
742
                palette = colorpalette.ColorPaletteGenerator(len(attr))
743
                color_data = [palette.getRGB(i) for i in range(len(attr))]
744
                color_data = numpy.hstack((
745
                    color_data,
746
                    numpy.full((len(color_data), 1), self.symbol_opacity))
747
                )
748
                pen_data = mdsplotutils.pen_data(color_data * 0.8, pointflags)
749
                brush_data = mdsplotutils.brush_data(color_data)
750
            else:
751
                pen_data = make_pen(QtGui.QColor(Qt.darkGray), cosmetic=True)
752
                if self._selection_mask is not None:
753
                    pen_data = numpy.array(
754
                        [pen_data, plotstyle.selected_pen])
755
                    pen_data = pen_data[self._selection_mask.astype(int)]
756
                else:
757
                    pen_data = numpy.full(len(self.data), pen_data,
758
                                          dtype=object)
759
                brush_data = numpy.full(
760
                    len(self.data),
761
                    pg.mkColor((192, 192, 192, self.symbol_opacity)),
762
                    dtype=object)
763
764
            self._pen_data = pen_data
765
            self._brush_data = brush_data
0 ignored issues
show
Coding Style introduced by
The attribute _brush_data was defined outside __init__.

It is generally a good practice to initialize all attributes to default values in the __init__ method:

class Foo:
    def __init__(self, x=None):
        self.x = x
Loading history...
766
767
        if self._shape_data is None:
768
            shape_index = self.cb_shape_value.currentIndex()
769
            if have_data and shape_index > 0:
770
                Symbols = ScatterPlotItem.Symbols
771
                symbols = numpy.array(list(Symbols.keys()))
772
773
                shape_var = self.shapevar_model[shape_index]
774
                data = column(self.data, shape_var)
775
                data = data % (len(Symbols) - 1)
776
                data[numpy.isnan(data)] = len(Symbols) - 1
777
                shape_data = symbols[data.astype(int)]
778
            elif have_matrix_transposed and \
779
                    self.shapevar_model[shape_index] == 'Attribute names':
780
                Symbols = ScatterPlotItem.Symbols
781
                symbols = numpy.array(list(Symbols.keys()))
782
                attr = [i % (len(Symbols) - 1)
783
                        for i, _ in enumerate(attributes(self.matrix))]
784
                shape_data = symbols[attr]
785
            else:
786
                shape_data = "o"
787
            self._shape_data = shape_data
788
789
        if self._size_data is None:
790
            MinPointSize = 3
791
            point_size = self.symbol_size + MinPointSize
792
            size_index = self.cb_size_value.currentIndex()
793
            if have_data and size_index == 1:
794
                # size by stress
795
                size_data = stress(self.embedding, self._effective_matrix)
796
                size_data = scale(size_data)
797
                size_data = MinPointSize + size_data * point_size
798
            elif have_data and size_index > 0:
799
                size_var = self.sizevar_model[size_index]
800
                size_data = column(self.data, size_var)
801
                size_data = scale(size_data)
802
                size_data = MinPointSize + size_data * point_size
803
            else:
804
                size_data = point_size
805
            self._size_data = size_data
806
807
        if self._label_data is None:
808
            label_index = self.cb_label_value.currentIndex()
809
            if have_data and label_index > 0:
810
                label_var = self.labelvar_model[label_index]
811
                label_data = column(self.data, label_var)
812
                label_data = [label_var.str_val(val) for val in label_data]
813
                label_items = [pg.TextItem(text, anchor=(0.5, 0), color=0.0)
814
                               for text in label_data]
815
            elif have_matrix_transposed and \
816
                    self.labelvar_model[label_index] == 'Attribute names':
817
                attr = attributes(self.matrix)
818
                label_items = [pg.TextItem(str(text), anchor=(0.5, 0))
819
                               for text in attr]
820
            else:
821
                label_items = None
822
            self._label_data = label_items
823
824
        emb_x, emb_y = self.embedding[:, 0], self.embedding[:, 1]
825
826
        if self.connected_pairs and self.__draw_similar_pairs:
827
            if self._similar_pairs is None:
828
                # This code requires storing lower triangle of X (n x n / 2
829
                # doubles), n x n / 2 * 2 indices to X, n x n / 2 indices for
830
                # argsort result. If this becomes an issue, it can be reduced to
831
                # n x n argsort indices by argsorting the entire X. Then we
832
                # take the first n + 2 * p indices. We compute their coordinates
833
                # i, j in the original matrix. We keep those for which i < j.
834
                # n + 2 * p will suffice to exclude the diagonal (i = j). If the
835
                # number of those for which i < j is smaller than p, we instead
836
                # take i > j. Among those that remain, we take the first p.
837
                # Assuming that MDS can't show so many points that memory could
838
                # become an issue, I preferred using simpler code.
839
                m = self._effective_matrix
840
                n = len(m)
841
                p = (n * (n - 1) // 2 * self.connected_pairs) // 100
842
                indcs = numpy.triu_indices(n, 1)
843
                sorted = numpy.argsort(m[indcs])[:p]
0 ignored issues
show
Bug Best Practice introduced by
This seems to re-define the built-in sorted.

It is generally discouraged to redefine built-ins as this makes code very hard to read.

Loading history...
844
                self._similar_pairs = fpairs = numpy.empty(2 * p, dtype=int)
845
                fpairs[::2] = indcs[0][sorted]
846
                fpairs[1::2] = indcs[1][sorted]
847
            for i in range(int(len(emb_x[self._similar_pairs]) / 2)):
848
                item = QtGui.QGraphicsLineItem(
849
                    emb_x[self._similar_pairs][i * 2],
850
                    emb_y[self._similar_pairs][i * 2],
851
                    emb_x[self._similar_pairs][i * 2 + 1],
852
                    emb_y[self._similar_pairs][i * 2 + 1]
853
                )
854
                pen = QtGui.QPen(QtGui.QBrush(QtGui.QColor(204, 204, 204)), 2)
855
                pen.setCosmetic(True)
856
                item.setPen(pen)
857
                self.plot.addItem(item)
858
859
        data = numpy.arange(len(self.data if have_data else self.matrix))
860
        self._scatter_item = item = ScatterPlotItem(
861
            x=emb_x, y=emb_y,
862
            pen=self._pen_data, brush=self._brush_data, symbol=self._shape_data,
863
            size=self._size_data, data=data,
864
            antialias=True
865
        )
866
        self.plot.addItem(item)
867
868
        if self._label_data is not None:
869
            for (x, y), text_item in zip(self.embedding, self._label_data):
870
                self.plot.addItem(text_item)
871
                text_item.setPos(x, y)
872
873
        self._legend_item = LegendItem()
874
        self._legend_item.setParentItem(self.plot.getViewBox())
875
        self._legend_item.anchor(*self.legend_anchor)
876
877
        color_var = shape_var = None
878
        color_index = self.cb_color_value.currentIndex()
879
        if have_data and 1 <= color_index < len(self.colorvar_model):
880
            color_var = self.colorvar_model[color_index]
881
            assert isinstance(color_var, Orange.data.Variable)
882
        shape_index = self.cb_shape_value.currentIndex()
883
        if have_data and 1 <= shape_index < len(self.shapevar_model):
884
            shape_var = self.shapevar_model[shape_index]
885
            assert isinstance(shape_var, Orange.data.Variable)
886
887
        if shape_var is not None or \
888
                (color_var is not None and color_var.is_discrete):
889
890
            legend_data = mdsplotutils.legend_data(
891
                color_var, shape_var, plotstyle=plotstyle)
892
893
            for color, symbol, text in legend_data:
894
                self._legend_item.addItem(
895
                    ScatterPlotItem(pen=color, brush=color, symbol=symbol,
896
                                    size=10),
897
                    escape(text)
898
                )
899
        else:
900
            self._legend_item.hide()
901
902
    def commit(self):
903
        if self.embedding is not None:
904
            output = embedding = Orange.data.Table.from_numpy(
905
                Orange.data.Domain([Orange.data.ContinuousVariable("X"),
906
                                    Orange.data.ContinuousVariable("Y")]),
907
                self.embedding
908
            )
909
        else:
910
            output = embedding = None
911
912
        if self.embedding is not None and self.data is not None:
913
            domain = self.data.domain
914
            attrs = domain.attributes
915
            class_vars = domain.class_vars
916
            metas = domain.metas
917
918
            if self.output_embedding_role == OWMDS.AttrRole:
919
                attrs = embedding.domain.attributes
920
            elif self.output_embedding_role == OWMDS.AddAttrRole:
921
                attrs = domain.attributes + embedding.domain.attributes
922
            elif self.output_embedding_role == OWMDS.MetaRole:
923
                metas += embedding.domain.attributes
924
925
            domain = Orange.data.Domain(attrs, class_vars, metas)
926
            output = Orange.data.Table.from_table(domain, self.data)
927
928
            if self.output_embedding_role == OWMDS.AttrRole:
929
                output.X[:] = embedding.X
930
            if self.output_embedding_role == OWMDS.AddAttrRole:
931
                output.X[:, -2:] = embedding.X
932
            elif self.output_embedding_role == OWMDS.MetaRole:
933
                output.metas[:, -2:] = embedding.X
934
935
        self.send("Data", output)
936
        if output is not None and self._selection_mask is not None and \
937
                numpy.any(self._selection_mask):
938
            subset = output[self._selection_mask]
939
        else:
940
            subset = None
941
        self.send("Selected Data", subset)
942
943
    def onDeleteWidget(self):
944
        super().onDeleteWidget()
945
        self._clear_plot()
946
        self._clear()
947
948
    def __selection_end(self, path):
949
        self.select(path)
950
        self._pen_data = None
951
        self._update_plot()
952
        self._invalidate_output()
953
954
    def select(self, region):
955
        item = self._scatter_item
956
        if item is None:
957
            return
958
959
        indices = numpy.array(
960
            [spot.data() for spot in item.points()
961
             if region.contains(spot.pos())],
962
            dtype=int)
963
964
        if not QtGui.QApplication.keyboardModifiers():
965
            self._selection_mask = None
966
967
        self.select_indices(indices, QtGui.QApplication.keyboardModifiers())
968
969
    def select_indices(self, indices, modifiers=Qt.NoModifier):
970
        if self.data is None:
971
            return
972
973
        if self._selection_mask is None or \
974
                not modifiers & (Qt.ControlModifier | Qt.ShiftModifier |
975
                                 Qt.AltModifier):
976
            self._selection_mask = numpy.zeros(len(self.data), dtype=bool)
977
978
        if modifiers & Qt.AltModifier:
979
            self._selection_mask[indices] = False
980
        elif modifiers & Qt.ControlModifier:
981
            self._selection_mask[indices] = ~self._selection_mask[indices]
982
        else:
983
            self._selection_mask[indices] = True
984
985
    def save_graph(self):
986
        from Orange.widgets.data.owsave import OWSave
987
988
        save_img = OWSave(parent=self, data=self.plot.plotItem,
989
                          file_formats=FileFormats.img_writers)
990
        save_img.exec_()
991
992
993
def colors(data, variable, palette=None):
994
    if palette is None:
995
        if variable.is_discrete:
996
            palette = colorpalette.ColorPaletteGenerator(len(variable.values))
997
        elif variable.is_continuous:
998
            palette = colorpalette.ColorPaletteBW()
999
            palette = colorpalette.ContinuousPaletteGenerator(
1000
                QtGui.QColor(220, 220, 220),
1001
                QtGui.QColor(0, 0, 0),
1002
                False
1003
            )
1004
        else:
1005
            raise TypeError()
1006
1007
    x = data[:, variable]
1008
    if variable in data.domain.metas:
1009
        x = numpy.array(x.metas, dtype='float').ravel()
1010
    else:
1011
        x = numpy.array(x).ravel()
1012
1013
    if variable.is_discrete:
1014
        nvalues = len(variable.values)
1015
        x[numpy.isnan(x)] = nvalues
1016
        color_index = palette.getRGB(numpy.arange(nvalues + 1))
1017
        # Unknown values as gray
1018
        # TODO: This should already be a part of palette
0 ignored issues
show
Coding Style introduced by
TODO and FIXME comments should generally be avoided.
Loading history...
1019
        color_index[nvalues] = (128, 128, 128)
1020
        colors = color_index[x.astype(int)]
0 ignored issues
show
Comprehensibility Bug introduced by
colors is re-defining a name which is already available in the outer-scope (previously defined on line 993).

It is generally a bad practice to shadow variables from the outer-scope. In most cases, this is done unintentionally and might lead to unexpected behavior:

param = 5

class Foo:
    def __init__(self, param):   # "param" would be flagged here
        self.param = param
Loading history...
1021
    else:
1022
        x, _ = scaled(x)
1023
        mask = numpy.isnan(x)
1024
        colors = numpy.empty((len(x), 3))
1025
        colors[mask] = (128, 128, 128)
1026
        colors[~mask] = [palette.getRGB(v) for v in x[~mask]]
1027
#         colors[~mask] = interpolate(palette, x[~mask], left=Qt.gray)
1028
1029
    return colors
1030
1031
1032
def scaled(a):
1033
    amin, amax = numpy.nanmin(a), numpy.nanmax(a)
1034
    span = amax - amin
1035
    return (a - amin) / (span or 1), (amin, amax)
1036
1037
from types import SimpleNamespace as namespace
1038
1039
from Orange.widgets.visualize.owlinearprojection import \
1040
    PlotSelectionTool, PlotZoomTool, PlotPanTool, PlotPinchZoomTool, \
1041
    LegendItem, legend_anchor_pos
1042
from Orange.widgets.visualize.owlinearprojection import plotutils
1043
1044
1045
class namespace(namespace):
0 ignored issues
show
Bug introduced by
class already defined line 1037
Loading history...
1046
    def updated(self, **kwargs):
1047
        ns = self.__dict__.copy()
1048
        ns.update(**kwargs)
1049
        return namespace(**ns)
1050
1051
1052
class mdsplotutils(plotutils):
1053
    NoFlags, Selected, Highlight = 0, 1, 2
1054
    NoFill, Filled = 0, 1
1055
1056
    plotstyle = namespace(
1057
        selected_pen=make_pen(Qt.yellow, width=3, cosmetic=True),
1058
        highligh_pen=QtGui.QPen(Qt.blue, 1),
1059
        selected_brush=None,
1060
        default_color=QtGui.QColor(Qt.darkGray).rgba(),
1061
        discrete_palette=colorpalette.ColorPaletteGenerator(),
1062
        continuous_palette=colorpalette.ContinuousPaletteGenerator(
1063
            QtGui.QColor(220, 220, 220),
1064
            QtGui.QColor(0, 0, 0),
1065
            False
1066
        ),
1067
        symbols=ScatterPlotItem.Symbols,
1068
        point_size=10,
1069
        min_point_size=5,
1070
    )
1071
1072
    @staticmethod
1073
    def column_data(table, var, mask=None):
1074
        col, _ = table.get_column_view(var)
1075
        dtype = float if var.is_primitive() else object
1076
        col = numpy.asarray(col, dtype=dtype)
1077
        if mask is not None:
1078
            mask = numpy.asarray(mask, dtype=bool)
1079
            return col[mask]
1080
        else:
1081
            return col
1082
1083
    @staticmethod
1084
    def color_data(table, var=None, mask=None, plotstyle=None):
1085
        N = len(table)
1086
        if mask is not None:
1087
            mask = numpy.asarray(mask, dtype=bool)
1088
            N = numpy.count_nonzero(mask)
1089
1090
        if plotstyle is None:
1091
            plotstyle = mdsplotutils.plotstyle
1092
1093
        if var is None:
1094
            col = numpy.zeros(N, dtype=float)
1095
            color_data = numpy.full(N, plotstyle.default_color, dtype=object)
1096
        elif var.is_primitive():
1097
            col = mdsplotutils.column_data(table, var, mask)
1098
            if var.is_discrete:
1099
                palette = plotstyle.discrete_palette
1100
                if len(var.values) >= palette.number_of_colors:
1101
                    palette = colorpalette.ColorPaletteGenerator(len(var.values))
1102
1103
                color_data = plotutils.discrete_colors(
1104
                    col, nvalues=len(var.values), palette=palette)
1105
            elif var.is_continuous:
1106
                color_data = plotutils.continuous_colors(
1107
                    col, palette=plotstyle.continuous_palette)
1108
        else:
1109
            raise TypeError("Discrete/Continuous variable or None expected.")
1110
1111
        return color_data
1112
1113
    @staticmethod
1114
    def pen_data(basecolors, flags=None, plotstyle=None):
1115
        if plotstyle is None:
1116
            plotstyle = mdsplotutils.plotstyle
1117
1118
        pens = numpy.array(
1119
            [mdsplotutils.make_pen(QtGui.QColor(*rgba), width=1)
1120
             for rgba in basecolors],
1121
            dtype=object)
1122
1123
        if flags is None:
1124
            return pens
1125
1126
        selected_mask = flags & mdsplotutils.Selected
1127
        if numpy.any(selected_mask):
1128
            pens[selected_mask.astype(bool)] = plotstyle.selected_pen
1129
1130
        highlight_mask = flags & mdsplotutils.Highlight
1131
        if numpy.any(highlight_mask):
1132
            pens[highlight_mask.astype(bool)] = plotstyle.hightlight_pen
1133
1134
        return pens
1135
1136
    @staticmethod
1137
    def brush_data(basecolors, flags=None, plotstyle=None):
1138
        if plotstyle is None:
1139
            plotstyle = mdsplotutils.plotstyle
1140
1141
        brush = numpy.array(
1142
            [mdsplotutils.make_brush(QtGui.QColor(*c))
1143
             for c in basecolors],
1144
            dtype=object)
1145
1146
        if flags is None:
1147
            return brush
1148
1149
        fill_mask = flags & mdsplotutils.Filled
1150
1151
        if not numpy.all(fill_mask):
1152
            brush[~fill_mask] = QtGui.QBrush(Qt.NoBrush)
1153
        return brush
1154
1155
    @staticmethod
1156
    def shape_data(table, var, mask=None, plotstyle=None):
1157
        if plotstyle is None:
1158
            plotstyle = mdsplotutils.plotstyle
1159
1160
        N = len(table)
1161
        if mask is not None:
1162
            mask = numpy.asarray(mask, dtype=bool)
1163
            N = numpy.nonzero(mask)
1164
1165
        if var is None:
1166
            return numpy.full(N, "o", dtype=object)
1167
        elif var.is_discrete:
1168
            shape_data = mdsplotutils.column_data(table, var, mask)
1169
            maxsymbols = len(plotstyle.symbols) - 1
1170
            validmask = numpy.isfinite(shape_data)
1171
            shape = shape_data % (maxsymbols - 1)
1172
            shape[~validmask] = maxsymbols  # Special symbol for unknown values
1173
            symbols = numpy.array(list(plotstyle.symbols))
1174
            shape_data = symbols[numpy.asarray(shape, dtype=int)]
1175
1176
            if mask is None:
1177
                return shape_data
1178
            else:
1179
                return shape_data[mask]
1180
        else:
1181
            raise TypeError()
1182
1183
    @staticmethod
1184
    def size_data(table, var, mask=None, plotstyle=None):
1185
        if plotstyle is None:
1186
            plotstyle = mdsplotutils.plotstyle
1187
1188
        N = len(table)
1189
        if mask is not None:
1190
            mask = numpy.asarray(mask, dtype=bool)
1191
            N = numpy.nonzero(mask)
1192
1193
        if var is None:
1194
            return numpy.full(N, plotstyle.point_size, dtype=float)
1195
        else:
1196
            size_data = mdsplotutils.column_data(table, var, mask)
1197
            size_data = mdsplotutils.normalized(size_data)
1198
            size_mask = numpy.isnan(size_data)
1199
            size_data = size_data * plotstyle.point_size + \
1200
                        plotstyle.min_point_size
1201
            size_data[size_mask] = plotstyle.min_point_size - 2
1202
1203
            if mask is None:
1204
                return size_data
1205
            else:
1206
                return size_data[mask]
1207
1208
    @staticmethod
1209
    def legend_data(color_var=None, shape_var=None, plotstyle=None):
1210
        if plotstyle is None:
1211
            plotstyle = mdsplotutils.plotstyle
1212
1213
        if color_var is not None and not color_var.is_discrete:
1214
            color_var = None
1215
        assert shape_var is None or shape_var.is_discrete
1216
        if color_var is None and shape_var is None:
1217
            return []
1218
1219
        if color_var is not None:
1220
            palette = plotstyle.discrete_palette
1221
            if len(color_var.values) >= palette.number_of_colors:
1222
                palette = colorpalette.ColorPaletteGenerator(len(color_var.values))
1223
        else:
1224
            palette = None
1225
1226
        symbols = list(plotstyle.symbols)
1227
1228
        if shape_var is color_var:
1229
            items = [(palette[i], symbols[i], name)
1230
                     for i, name in enumerate(color_var.values)]
1231
        else:
1232
            colors = shapes = []
0 ignored issues
show
Comprehensibility Bug introduced by
colors is re-defining a name which is already available in the outer-scope (previously defined on line 993).

It is generally a bad practice to shadow variables from the outer-scope. In most cases, this is done unintentionally and might lead to unexpected behavior:

param = 5

class Foo:
    def __init__(self, param):   # "param" would be flagged here
        self.param = param
Loading history...
1233
            if color_var is not None:
1234
                colors = [(palette[i], "o", name)
1235
                          for i, name in enumerate(color_var.values)]
1236
            if shape_var is not None:
1237
                shapes = [(QtGui.QColor(Qt.gray),
1238
                           symbols[i % (len(symbols) - 1)], name)
1239
                          for i, name in enumerate(shape_var.values)]
1240
            items = colors + shapes
1241
1242
        return items
1243
1244
    @staticmethod
1245
    def make_pen(color, width=1, cosmetic=True):
1246
        pen = QtGui.QPen(color)
1247
        pen.setWidthF(width)
1248
        pen.setCosmetic(cosmetic)
1249
        return pen
1250
1251
    @staticmethod
1252
    def make_brush(color, ):
1253
        return QtGui.QBrush(color, )
1254
1255
1256
def main_test(argv=sys.argv):
0 ignored issues
show
Bug Best Practice introduced by
The default value sys.argv (builtins.list) might cause unintended side-effects.

Objects as default values are only created once in Python and not on each invocation of the function. If the default object is modified, this modification is carried over to the next invocation of the method.

# Bad:
# If array_param is modified inside the function, the next invocation will
# receive the modified object.
def some_function(array_param=[]):
    # ...

# Better: Create an array on each invocation
def some_function(array_param=None):
    array_param = array_param or []
    # ...
Loading history...
1257
    import gc
1258
    argv = list(argv)
1259
    app = QtGui.QApplication(argv)
1260
1261
    if len(argv) > 1:
1262
        filename = argv[1]
1263
    else:
1264
        filename = "iris"
1265
1266
    data = Orange.data.Table(filename)
1267
    w = OWMDS()
1268
    w.set_data(data)
1269
    w.handleNewSignals()
1270
1271
    w.show()
1272
    w.raise_()
1273
    rval = app.exec_()
1274
1275
    w.saveSettings()
1276
    w.onDeleteWidget()
1277
    w.deleteLater()
1278
    del w
1279
    gc.collect()
1280
    app.processEvents()
1281
    return rval
1282
1283
if __name__ == "__main__":
1284
    sys.exit(main_test())
1285