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.visualize.OWVennDiagram   F
last analyzed

Complexity

Total Complexity 101

Size/Duplication

Total Lines 533
Duplicated Lines 0 %
Metric Value
dl 0
loc 533
rs 1.5789
wmc 101

29 Methods

Rating   Name   Duplication   Size   Complexity  
C _createDiagram() 0 50 7
A match() 0 5 2
B _updateInfo() 0 15 5
F commit() 0 72 17
A save_graph() 0 6 1
A _controlAtIndex() 0 4 1
A __init__() 0 74 2
B setData() 0 22 6
A _invalidate() 0 8 3
A _on_useidentifiersChanged() 0 8 1
A _on_selectionChanged() 0 11 4
B _storeHints() 0 14 5
A itemsetAttr() 0 9 2
A items_by_key() 0 7 4
A _add() 0 14 1
A items_by_eq() 0 2 1
A _on_inputAttrActivated() 0 19 4
A getSettings() 0 3 1
A _createItemsets() 0 15 4
B _itemsForInput() 0 20 7
A _on_itemTextEdited() 0 4 1
A _setAttributes() 0 12 3
A invalidateOutput() 0 2 1
B _remove() 0 24 3
A _update() 0 13 1
F handleNewSignals() 0 40 11
B _restoreHints() 0 19 6
A _updateItemsets() 0 10 4
A instance_key() 0 2 1

How to fix   Complexity   

Complex Class

Complex classes like Orange.widgets.visualize.OWVennDiagram 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
"""
2
Venn Diagram Widget
3
-------------------
4
5
"""
6
7
import math
8
import unicodedata
9
10
from collections import namedtuple, defaultdict, OrderedDict, Counter
11
from itertools import count
12
from functools import reduce
13
from xml.sax.saxutils import escape
14
15
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...
16
17
from PyQt4.QtGui import (
0 ignored issues
show
Configuration introduced by
The import PyQt4.QtGui 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...
18
    QComboBox, QGraphicsScene, QGraphicsView, QGraphicsWidget,
19
    QGraphicsPathItem, QGraphicsTextItem, QPainterPath, QPainter,
20
    QTransform, QColor, QBrush, QPen, QStyle, QPalette,
21
    QApplication
22
)
23
24
from PyQt4.QtCore import Qt, QPointF, QRectF, QLineF
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...
25
from PyQt4.QtCore import pyqtSignal as Signal
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...
26
27
import Orange.data
28
29
from Orange.widgets import widget, gui, settings
30
from Orange.widgets.utils import itemmodels, colorpalette
31
from Orange.widgets.io import FileFormats
32
33
34
_InputData = namedtuple("_InputData", ["key", "name", "table"])
35
_ItemSet = namedtuple("_ItemSet", ["key", "name", "title", "items"])
36
37
38
class OWVennDiagram(widget.OWWidget):
39
    name = "Venn Diagram"
40
    description = "A graphical visualization of an overlap of data instances " \
41
                  "from a collection of input data sets."
42
    icon = "icons/VennDiagram.svg"
43
44
    inputs = [("Data", Orange.data.Table, "setData", widget.Multiple)]
45
    outputs = [("Selected Data", Orange.data.Table)]
46
47
    # Selected disjoint subset indices
48
    selection = settings.Setting([])
49
    #: Stored input set hints
50
    #: {(index, inputname, attributes): (selectedattrname, itemsettitle)}
51
    #: The 'selectedattrname' can be None
52
    inputhints = settings.Setting({})
53
    #: Use identifier columns for instance matching
54
    useidentifiers = settings.Setting(True)
55
    autocommit = settings.Setting(True)
56
57
    want_graph = True
58
59
    def __init__(self, parent=None):
60
        super().__init__(parent)
61
62
        # Diagram update is in progress
63
        self._updating = False
64
        # Input update is in progress
65
        self._inputUpdate = False
66
        # All input tables have the same domain.
67
        self.samedomain = True
68
        # Input datasets in the order they were 'connected'.
69
        self.data = OrderedDict()
70
        # Extracted input item sets in the order they were 'connected'
71
        self.itemsets = OrderedDict()
72
73
        # GUI
74
        box = gui.widgetBox(self.controlArea, "Info")
75
        self.info = gui.widgetLabel(box, "No data on input\n")
76
77
        self.identifiersBox = gui.radioButtonsInBox(
78
            self.controlArea, self, "useidentifiers", [],
79
            box="Data Instance Identifiers",
80
            callback=self._on_useidentifiersChanged
81
        )
82
        self.useequalityButton = gui.appendRadioButton(
83
            self.identifiersBox, "Use instance equality"
84
        )
85
        rb = gui.appendRadioButton(
86
            self.identifiersBox, "Use identifiers"
87
        )
88
        self.inputsBox = gui.indentedBox(
89
            self.identifiersBox, sep=gui.checkButtonOffsetHint(rb)
90
        )
91
        self.inputsBox.setEnabled(bool(self.useidentifiers))
92
93
        for i in range(5):
94
            box = gui.widgetBox(self.inputsBox, "Data set #%i" % (i + 1),
95
                                addSpace=False)
96
            box.setFlat(True)
97
            model = itemmodels.VariableListModel(parent=self)
98
            cb = QComboBox(
99
                minimumContentsLength=12,
100
                sizeAdjustPolicy=QComboBox.AdjustToMinimumContentsLengthWithIcon)
101
            cb.setModel(model)
102
            cb.activated[int].connect(self._on_inputAttrActivated)
103
            box.setEnabled(False)
104
            # Store the combo in the box for later use.
105
            box.combo_box = cb
106
            box.layout().addWidget(cb)
107
108
        gui.rubber(self.controlArea)
109
110
        gui.auto_commit(self.controlArea, self, "autocommit",
111
                        "Commit", "Auto commit")
112
113
        # Main area view
114
        self.scene = QGraphicsScene()
115
        self.view = QGraphicsView(self.scene)
116
        self.view.setRenderHint(QPainter.Antialiasing)
117
        self.view.setBackgroundRole(QPalette.Window)
118
        self.view.setFrameStyle(QGraphicsView.StyledPanel)
119
120
        self.mainArea.layout().addWidget(self.view)
121
        self.vennwidget = VennDiagram()
122
        self.vennwidget.resize(400, 400)
123
        self.vennwidget.itemTextEdited.connect(self._on_itemTextEdited)
124
        self.scene.selectionChanged.connect(self._on_selectionChanged)
125
126
        self.scene.addItem(self.vennwidget)
127
128
        self.resize(self.controlArea.sizeHint().width() + 550,
129
                    max(self.controlArea.sizeHint().height(), 550))
130
131
        self._queue = []
132
        self.graphButton.clicked.connect(self.save_graph)
133
134
    def setData(self, data, key=None):
135
        self.error(0)
136
        if not self._inputUpdate:
137
            # Store hints only on the first setData call.
138
            self._storeHints()
139
            self._inputUpdate = True
140
141
        if key in self.data:
142
            if data is None:
143
                # Remove the input
144
                self._remove(key)
145
            else:
146
                # Update existing item
147
                self._update(key, data)
148
        elif data is not None:
149
            # TODO: Allow setting more them 5 inputs and let the user
0 ignored issues
show
Coding Style introduced by
TODO and FIXME comments should generally be avoided.
Loading history...
150
            # select the 5 to display.
151
            if len(self.data) == 5:
152
                self.error(0, "Can only take 5 inputs.")
153
                return
154
            # Add a new input
155
            self._add(key, data)
156
157
    def handleNewSignals(self):
158
        self._inputUpdate = False
159
160
        # Check if all inputs are from the same domain.
161
        domains = [input.table.domain for input in self.data.values()]
162
        samedomain = all(domain_eq(d1, d2) for d1, d2 in pairwise(domains))
163
164
        self.useequalityButton.setEnabled(samedomain)
165
        self.samedomain = samedomain
166
167
        has_identifiers = all(source_attributes(input.table.domain)
168
                              for input in self.data.values())
169
170
        if not samedomain and not self.useidentifiers:
171
            self.useidentifiers = 1
172
        elif samedomain and not has_identifiers:
173
            self.useidentifiers = 0
174
175
        incremental = all(inc for _, inc in self._queue)
176
177
        if incremental:
178
            # Only received updated data on existing link.
179
            self._updateItemsets()
180
        else:
181
            # Links were removed and/or added.
182
            self._createItemsets()
183
            self._restoreHints()
184
            self._updateItemsets()
185
186
        del self._queue[:]
187
188
        self._createDiagram()
189
        if self.data:
190
            self.info.setText(
191
                "{} data sets on input.\n".format(len(self.data)))
192
        else:
193
            self.info.setText("No data on input\n")
194
195
        self._updateInfo()
196
        super().handleNewSignals()
197
198
    def _invalidate(self, keys=None, incremental=True):
199
        """
200
        Invalidate input for a list of input keys.
201
        """
202
        if keys is None:
203
            keys = list(self.data.keys())
204
205
        self._queue.extend((key, incremental) for key in keys)
206
207
    def itemsetAttr(self, key):
208
        index = list(self.data.keys()).index(key)
209
        _, combo = self._controlAtIndex(index)
210
        model = combo.model()
211
        attr_index = combo.currentIndex()
212
        if attr_index >= 0:
213
            return model[attr_index]
214
        else:
215
            return None
216
217
    def _controlAtIndex(self, index):
218
        group_box = self.inputsBox.layout().itemAt(index).widget()
219
        combo = group_box.combo_box
220
        return group_box, combo
221
222
    def _setAttributes(self, index, attrs):
223
        box, combo = self._controlAtIndex(index)
224
        model = combo.model()
225
226
        if attrs is None:
227
            model[:] = []
228
            box.setEnabled(False)
229
        else:
230
            if model[:] != attrs:
231
                model[:] = attrs
232
233
            box.setEnabled(True)
234
235
    def _add(self, key, table):
236
        name = table.name
237
        index = len(self.data)
238
        attrs = source_attributes(table.domain)
239
240
        self.data[key] = _InputData(key, name, table)
241
242
        self._setAttributes(index, attrs)
243
244
        self._invalidate([key], incremental=False)
245
246
        item = self.inputsBox.layout().itemAt(index)
247
        box = item.widget()
248
        box.setTitle("Data set: {}".format(name))
249
250
    def _remove(self, key):
251
        index = list(self.data.keys()).index(key)
252
253
        # Clear possible warnings.
254
        self.warning(index)
255
256
        self._setAttributes(index, None)
257
258
        del self.data[key]
259
260
        layout = self.inputsBox.layout()
261
        item = layout.takeAt(index)
262
        layout.addItem(item)
263
        inputs = list(self.data.values())
264
265
        for i in range(5):
266
            box, _ = self._controlAtIndex(i)
267
            if i < len(inputs):
268
                title = "Data set: {}".format(inputs[i].name)
269
            else:
270
                title = "Data set #{}".format(i + 1)
271
            box.setTitle(title)
272
273
        self._invalidate([key], incremental=False)
274
275
    def _update(self, key, table):
276
        name = table.name
277
        index = list(self.data.keys()).index(key)
278
        attrs = source_attributes(table.domain)
279
280
        self.data[key] = self.data[key]._replace(name=name, table=table)
281
282
        self._setAttributes(index, attrs)
283
        self._invalidate([key])
284
285
        item = self.inputsBox.layout().itemAt(index)
286
        box = item.widget()
287
        box.setTitle("Data set: {}".format(name))
288
289
    def _itemsForInput(self, key):
290
        useidentifiers = self.useidentifiers or not self.samedomain
291
292
        def items_by_key(key, input):
0 ignored issues
show
Bug Best Practice introduced by
This seems to re-define the built-in input.

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

Loading history...
293
            attr = self.itemsetAttr(key)
294
            if attr is not None:
295
                return [str(inst[attr]) for inst in input.table
296
                        if not numpy.isnan(inst[attr])]
297
            else:
298
                return []
299
300
        def items_by_eq(key, input):
0 ignored issues
show
Bug Best Practice introduced by
This seems to re-define the built-in input.

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

Loading history...
Unused Code introduced by
The argument key seems to be unused.
Loading history...
301
            return list(map(ComparableInstance, input.table))
302
303
        input = self.data[key]
0 ignored issues
show
Bug Best Practice introduced by
This seems to re-define the built-in input.

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

Loading history...
304
        if useidentifiers:
305
            items = items_by_key(key, input)
306
        else:
307
            items = items_by_eq(key, input)
308
        return items
309
310
    def _updateItemsets(self):
311
        assert list(self.data.keys()) == list(self.itemsets.keys())
312
        for key, input in list(self.data.items()):
0 ignored issues
show
Bug Best Practice introduced by
This seems to re-define the built-in input.

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

Loading history...
313
            items = self._itemsForInput(key)
314
            item = self.itemsets[key]
315
            item = item._replace(items=items)
316
            name = input.name
317
            if item.name != name:
318
                item = item._replace(name=name, title=name)
319
            self.itemsets[key] = item
320
321
    def _createItemsets(self):
322
        olditemsets = dict(self.itemsets)
323
        self.itemsets.clear()
324
325
        for key, input in self.data.items():
0 ignored issues
show
Bug Best Practice introduced by
This seems to re-define the built-in input.

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

Loading history...
326
            items = self._itemsForInput(key)
327
            name = input.name
328
            if key in olditemsets and olditemsets[key].name == name:
329
                # Reuse the title (which might have been changed by the user)
330
                title = olditemsets[key].title
331
            else:
332
                title = name
333
334
            itemset = _ItemSet(key=key, name=name, title=title, items=items)
335
            self.itemsets[key] = itemset
336
337
    def _storeHints(self):
338
        if self.data:
339
            self.inputhints.clear()
340
            for i, (key, input) in enumerate(self.data.items()):
0 ignored issues
show
Bug Best Practice introduced by
This seems to re-define the built-in input.

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

Loading history...
341
                attrs = source_attributes(input.table.domain)
342
                attrs = tuple(attr.name for attr in attrs)
343
                selected = self.itemsetAttr(key)
344
                if selected is not None:
345
                    attr_name = selected.name
346
                else:
347
                    attr_name = None
348
                itemset = self.itemsets[key]
349
                self.inputhints[(i, input.name, attrs)] = \
350
                    (attr_name, itemset.title)
351
352
    def _restoreHints(self):
353
        settings = []
0 ignored issues
show
Comprehensibility Bug introduced by
settings is re-defining a name which is already available in the outer-scope (previously defined on line 29).

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...
354
        for i, (key, input) in enumerate(self.data.items()):
0 ignored issues
show
Bug Best Practice introduced by
This seems to re-define the built-in input.

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

Loading history...
355
            attrs = source_attributes(input.table.domain)
356
            attrs = tuple(attr.name for attr in attrs)
357
            hint = self.inputhints.get((i, input.name, attrs), None)
358
            if hint is not None:
359
                attr, name = hint
360
                attr_ind = attrs.index(attr) if attr is not None else -1
361
                settings.append((attr_ind, name))
362
            else:
363
                return
364
365
        # all inputs match the stored hints
366
        for i, key in enumerate(self.itemsets):
367
            attr, itemtitle = settings[i]
368
            self.itemsets[key] = self.itemsets[key]._replace(title=itemtitle)
369
            _, cb = self._controlAtIndex(i)
370
            cb.setCurrentIndex(attr)
371
372
    def _createDiagram(self):
373
        self._updating = True
374
375
        oldselection = list(self.selection)
376
377
        self.vennwidget.clear()
378
        n = len(self.itemsets)
379
        self.disjoint = disjoint(set(s.items) for s in self.itemsets.values())
0 ignored issues
show
Coding Style introduced by
The attribute disjoint 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...
380
381
        vennitems = []
382
        colors = colorpalette.ColorPaletteHSV(n)
383
384
        for i, (key, item) in enumerate(self.itemsets.items()):
0 ignored issues
show
Unused Code introduced by
The variable key seems to be unused.
Loading history...
385
            gr = VennSetItem(text=item.title, count=len(item.items))
386
            color = colors[i]
387
            color.setAlpha(100)
388
            gr.setBrush(QBrush(color))
389
            gr.setPen(QPen(Qt.NoPen))
390
            vennitems.append(gr)
391
392
        self.vennwidget.setItems(vennitems)
393
394
        for i, area in enumerate(self.vennwidget.vennareas()):
395
            area_items = list(map(str, list(self.disjoint[i])))
396
            if i:
397
                area.setText("{0}".format(len(area_items)))
398
399
            label = disjoint_set_label(i, n, simplify=False)
400
            head = "<h4>|{}| = {}</h4>".format(label, len(area_items))
401
            if len(area_items) > 32:
402
                items_str = ", ".join(map(escape, area_items[:32]))
403
                hidden = len(area_items) - 32
404
                tooltip = ("{}<span>{}, ...</br>({} items not shown)<span>"
405
                           .format(head, items_str, hidden))
406
            elif area_items:
407
                tooltip = "{}<span>{}</span>".format(
408
                    head,
409
                    ", ".join(map(escape, area_items))
410
                )
411
            else:
412
                tooltip = head
413
414
            area.setToolTip(tooltip)
415
416
            area.setPen(QPen(QColor(10, 10, 10, 200), 1.5))
417
            area.setFlag(QGraphicsPathItem.ItemIsSelectable, True)
418
            area.setSelected(i in oldselection)
419
420
        self._updating = False
421
        self._on_selectionChanged()
422
423
    def _updateInfo(self):
424
        # Clear all warnings
425
        self.warning(list(range(5)))
426
427
        if not len(self.data):
428
            self.info.setText("No data on input\n")
429
        else:
430
            self.info.setText(
431
                "{0} data sets on input\n".format(len(self.data)))
432
433
        if self.useidentifiers:
434
            for i, key in enumerate(self.data):
435
                if not source_attributes(self.data[key].table.domain):
436
                    self.warning(i, "Data set #{} has no suitable identifiers."
437
                                 .format(i + 1))
438
439
    def _on_selectionChanged(self):
440
        if self._updating:
441
            return
442
443
        areas = self.vennwidget.vennareas()
444
        indices = [i for i, area in enumerate(areas)
445
                   if area.isSelected()]
446
447
        self.selection = indices
448
449
        self.invalidateOutput()
450
451
    def _on_useidentifiersChanged(self):
452
        self.inputsBox.setEnabled(self.useidentifiers == 1)
453
        # Invalidate all itemsets
454
        self._invalidate()
455
        self._updateItemsets()
456
        self._createDiagram()
457
458
        self._updateInfo()
459
460
    def _on_inputAttrActivated(self, attr_index):
0 ignored issues
show
Unused Code introduced by
The argument attr_index seems to be unused.
Loading history...
461
        combo = self.sender()
462
        # Find the input index to which the combo box belongs
463
        # (they are reordered when removing inputs).
464
        index = None
465
        inputs = list(self.data.items())
466
        for i in range(len(inputs)):
467
            _, c = self._controlAtIndex(i)
468
            if c is combo:
469
                index = i
470
                break
471
472
        assert (index is not None)
0 ignored issues
show
Unused Code Coding Style introduced by
There is an unnecessary parenthesis after assert.
Loading history...
473
474
        key, _ = inputs[index]
475
476
        self._invalidate([key])
477
        self._updateItemsets()
478
        self._createDiagram()
479
480
    def _on_itemTextEdited(self, index, text):
481
        text = str(text)
482
        key = list(self.itemsets.keys())[index]
483
        self.itemsets[key] = self.itemsets[key]._replace(title=text)
484
485
    def invalidateOutput(self):
486
        self.commit()
487
488
    def commit(self):
489
        selected_subsets = []
490
491
        selected_items = reduce(
492
            set.union, [self.disjoint[index] for index in self.selection],
493
            set()
494
        )
495
        def match(val):
496
            if numpy.isnan(val):
497
                return False
498
            else:
499
                return str(val) in selected_items
500
501
        source_var = Orange.data.StringVariable("source")
502
        item_id_var = Orange.data.StringVariable("item_id")
503
504
        names = [itemset.title.strip() for itemset in self.itemsets.values()]
505
        names = uniquify(names)
506
507
        for i, (key, input) in enumerate(self.data.items()):
0 ignored issues
show
Bug Best Practice introduced by
This seems to re-define the built-in input.

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

Loading history...
508
            if self.useidentifiers:
509
                attr = self.itemsetAttr(key)
510
                if attr is not None:
511
                    mask = list(map(match, (inst[attr] for inst in input.table)))
512
                else:
513
                    mask = [False] * len(input.table)
514
515
                def instance_key(inst):
516
                    return str(inst[attr])
517
            else:
518
                mask = [ComparableInstance(inst) in selected_items
519
                        for inst in input.table]
520
                _map = {item: str(i) for i, item in enumerate(selected_items)}
521
522
                def instance_key(inst):
523
                    return _map[ComparableInstance(inst)]
524
525
            mask = numpy.array(mask, dtype=bool)
526
            subset = Orange.data.Table(input.table.domain,
527
                                       input.table[mask])
528
            subset.ids = input.table.ids[mask]
529
            if len(subset) == 0:
530
                continue
531
532
            # add columns with source table id and set id
533
534
            id_column = numpy.array([[instance_key(inst)] for inst in subset],
535
                                    dtype=object)
536
            source_names = numpy.array([[names[i]]] * len(subset),
537
                                       dtype=object)
538
539
            subset = append_column(subset, "M", source_var, source_names)
540
            subset = append_column(subset, "M", item_id_var, id_column)
541
542
            selected_subsets.append(subset)
543
544
        if selected_subsets:
545
            data = table_concat(selected_subsets)
546
            # Get all variables which are not constant between the same
547
            # item set
548
            varying = varying_between(data, [item_id_var])
549
550
            if source_var in varying:
551
                varying.remove(source_var)
552
553
            data = reshape_wide(data, varying, [item_id_var], [source_var])
554
            # remove the temporary item set id column
555
            data = drop_columns(data, [item_id_var])
556
        else:
557
            data = None
558
559
        self.send("Selected Data", data)
560
561
    def getSettings(self, *args, **kwargs):
562
        self._storeHints()
563
        return super().getSettings(self, *args, **kwargs)
564
565
    def save_graph(self):
566
        from Orange.widgets.data.owsave import OWSave
567
568
        save_img = OWSave(parent=self, data=self.scene,
569
                          file_formats=FileFormats.img_writers)
570
        save_img.exec_()
571
572
573
def pairwise(iterable):
574
    """
575
    Return an iterator over consecutive pairs in `iterable`.
576
577
    >>> list(pairwise([1, 2, 3, 4])
578
    [(1, 2), (2, 3), (3, 4)]
579
580
    """
581
    it = iter(iterable)
582
    first = next(it)
583
    for second in it:
584
        yield first, second
585
        first = second
586
587
588
# Custom domain comparison (domains do not seem to compare equal
589
# even if they have exactly the same variables).
590
# TODO: What about metas.
0 ignored issues
show
Coding Style introduced by
TODO and FIXME comments should generally be avoided.
Loading history...
591
def domain_eq(d1, d2):
592
    return tuple(d1) == tuple(d2)
593
594
595
# Comparing/hashing Orange.data.Instance across domains ignoring metas.
596
class ComparableInstance:
597
    __slots__ = ["inst", "domain"]
598
599
    def __init__(self, inst):
600
        self.inst = inst
601
        self.domain = inst.domain
602
603
    def __hash__(self):
604
        return hash(self.inst.x.data.tobytes())
605
606
    def __eq__(self, other):
607
        # XXX: comparing NaN with different payload
0 ignored issues
show
Coding Style introduced by
TODO and FIXME comments should generally be avoided.
Loading history...
608
        return (domain_eq(self.domain, other.domain)
609
                and self.inst.x.data.tobytes() == other.inst.x.data.tobytes())
610
611
    def __iter__(self):
612
        return iter(self.inst)
613
614
    def __repr__(self):
615
        return repr(self.inst)
616
617
    def __str__(self):
618
        return str(self.inst)
619
620
621
def table_concat(tables):
622
    """
623
    Concatenate a list of tables.
624
625
    The resulting table will have a union of all attributes of `tables`.
626
627
    """
628
    attributes = []
629
    class_vars = []
630
    metas = []
631
    variables_seen = set()
632
633
    for table in tables:
634
        attributes.extend(v for v in table.domain.attributes
635
                          if v not in variables_seen)
636
        variables_seen.update(table.domain.attributes)
637
638
        class_vars.extend(v for v in table.domain.class_vars
639
                          if v not in variables_seen)
640
        variables_seen.update(table.domain.class_vars)
641
642
        metas.extend(v for v in table.domain.metas
643
                     if v not in variables_seen)
644
645
        variables_seen.update(table.domain.metas)
646
647
    domain = Orange.data.Domain(attributes, class_vars, metas)
648
    new_table = Orange.data.Table(domain)
649
650
    for table in tables:
651
        new_table.extend(Orange.data.Table.from_table(domain, table))
652
653
    return new_table
654
655
656
def copy_descriptor(descriptor, newname=None):
657
    """
658
    Create a copy of the descriptor.
659
660
    If newname is not None the new descriptor will have the same
661
    name as the input.
662
663
    """
664
    if newname is None:
665
        newname = descriptor.name
666
667
    if descriptor.is_discrete:
668
        newf = Orange.data.DiscreteVariable(
669
            newname,
670
            values=descriptor.values,
671
            base_value=descriptor.base_value,
672
            ordered=descriptor.ordered,
673
        )
674
        newf.attributes = dict(descriptor.attributes)
675
676
    elif descriptor.is_continuous:
677
        newf = Orange.data.ContinuousVariable(newname)
678
        newf.number_of_decimals = descriptor.number_of_decimals
679
        newf.attributes = dict(descriptor.attributes)
680
681
    else:
682
        newf = type(descriptor)(newname)
683
        newf.attributes = dict(descriptor.attributes)
684
685
    return newf
686
687
688
def reshape_wide(table, varlist, idvarlist, groupvarlist):
689
    """
690
    Reshape a data table into a wide format.
691
692
    :param Orange.data.Table table:
693
        Source data table in long format.
694
    :param varlist:
695
        A list of variables to reshape.
696
    :param list idvarlist:
697
        A list of variables in `table` uniquely identifying multiple
698
        records in `table` (i.e. subject id).
699
    :param groupvarlist:
700
        A list of variables differentiating multiple records
701
        (i.e. conditions).
702
703
    """
704
    def inst_key(inst, vars):
0 ignored issues
show
Bug Best Practice introduced by
This seems to re-define the built-in vars.

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

Loading history...
705
        return tuple(str(inst[var]) for var in vars)
706
707
    instance_groups = [inst_key(inst, groupvarlist) for inst in table]
708
    # A list of groups (for each element in a group the varying variable
709
    # will be duplicated)
710
    groups = list(unique(instance_groups))
711
    group_names = [", ".join(group) for group in groups]
712
713
    # A list of instance ids (subject ids)
714
    # Each instance in the output will correspond to one of these ids)
715
    instance_ids = [inst_key(inst, idvarlist) for inst in table]
716
    ids = list(unique(instance_ids))
717
718
    # an mapping from ids to an list of input instance indices
719
    # each instance in this list belongs to one group (but not all
720
    # groups need to be present).
721
    inst_by_id = defaultdict(list)
722
723
    for i in range(len(table)):
724
        inst_id = instance_ids[i]
725
        inst_by_id[inst_id].append(i)
726
727
    newfeatures = []
728
    newclass_vars = []
729
    newmetas = []
730
    expanded_features = {}
731
732
    def expanded(feat):
733
        return [copy_descriptor(feat, newname="%s (%s)" %
734
                                (feat.name, group_name))
735
                for group_name in group_names]
736
737
    for feat in table.domain.attributes:
738
        if feat in varlist:
739
            features = expanded(feat)
740
            newfeatures.extend(features)
741
            expanded_features[feat] = dict(zip(groups, features))
742
        elif feat not in groupvarlist:
743
            newfeatures.append(feat)
744
745
    for feat in table.domain.class_vars:
746
        if feat in varlist:
747
            features = expanded(feat)
748
            newclass_vars.extend(features)
749
            expanded_features[feat] = dict(zip(groups, features))
750
        elif feat not in groupvarlist:
751
            newclass_vars.append(feat)
752
753
    for meta in table.domain.metas:
754
        if meta in varlist:
755
            metas = expanded(meta)
756
            newmetas.extend(metas)
757
            expanded_features[meta] = dict(zip(groups, metas))
758
        elif meta not in groupvarlist:
759
            newmetas.append(meta)
760
761
    domain = Orange.data.Domain(newfeatures, newclass_vars, newmetas)
762
    prototype_indices = [inst_by_id[inst_id][0] for inst_id in ids]
763
    newtable = Orange.data.Table.from_table(domain, table)[prototype_indices]
764
    in_expanded = set(f for efd in expanded_features.values() for f in efd.values())
765
766
    for i, inst_id in enumerate(ids):
767
        indices = inst_by_id[inst_id]
768
        instance = newtable[i]
769
770
        for var in domain.variables + domain.metas:
771
            if var in idvarlist or var in in_expanded:
772
                continue
773
            if numpy.isnan(instance[var]):
774
                for ind in indices:
775
                    if not numpy.isnan(table[ind, var]):
776
                        newtable[i, var] = table[ind, var]
777
778
        for index in indices:
779
            source_inst = table[index]
780
            group = instance_groups[index]
781
            for source_var in varlist:
782
                newf = expanded_features[source_var][group]
783
                instance[newf] = source_inst[source_var]
784
785
    return newtable
786
787
788
def unique(seq):
789
    """
790
    Return an iterator over unique items of `seq`.
791
792
    .. note:: Items must be hashable.
793
794
    """
795
    seen = set()
796
    for item in seq:
797
        if item not in seen:
798
            yield item
799
            seen.add(item)
800
801
from Orange.widgets.data.owmergedata import group_table_indices
802
803
804
def unique_non_nan(ar):
805
    # metas have sometimes object dtype, but values are numpy floats
806
    ar = ar.astype('float64')
807
    uniq = numpy.unique(ar)
808
    return uniq[~numpy.isnan(uniq)]
809
810
811
def varying_between(table, idvarlist):
812
    """
813
    Return a list of all variables with non constant values between
814
    groups defined by `idvarlist`.
815
816
    """
817
    def inst_key(inst, vars):
0 ignored issues
show
Bug Best Practice introduced by
This seems to re-define the built-in vars.

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

Loading history...
Unused Code introduced by
The variable inst_key seems to be unused.
Loading history...
818
        return tuple(str(inst[var]) for var in vars)
819
820
    excluded = set(idvarlist)
821
    all_possible = [var for var in table.domain.variables + table.domain.metas
822
                    if var not in excluded]
823
    candidate_set = set(all_possible)
824
825
    idmap = group_table_indices(table, idvarlist)
826
    values = {}
827
    varying = set()
828
    for indices in idmap.values():
829
        subset = table[indices]
830
        for var in list(candidate_set):
831
            values = subset[:, var]
832
            values, _ = subset.get_column_view(var)
833
834
            if var.is_string:
835
                uniq = set(values)
836
            else:
837
                uniq = unique_non_nan(values)
838
839
            if len(uniq) > 1:
840
                varying.add(var)
841
                candidate_set.remove(var)
842
843
    return sorted(varying, key=all_possible.index)
844
845
846
def uniquify(strings):
847
    """
848
    Return a list of unique strings.
849
850
    The string at i'th position will have the same prefix as strings[i]
851
    with an appended suffix to make the item unique (if necessary).
852
853
    >>> uniquify(["cat", "dog", "cat"])
854
    ["cat 1", "dog", "cat 2"]
855
856
    """
857
    counter = Counter(strings)
858
    counts = defaultdict(count)
859
    newstrings = []
860
    for string in strings:
861
        if counter[string] > 1:
862
            newstrings.append(string + (" %i" % (next(counts[string]) + 1)))
863
        else:
864
            newstrings.append(string)
865
866
    return newstrings
867
868
869
def string_attributes(domain):
870
    """
871
    Return all string attributes from the domain.
872
    """
873
    return [attr for attr in domain.variables + domain.metas
874
            if attr.is_string]
875
876
877
def discrete_attributes(domain):
878
    """
879
    Return all discrete attributes from the domain.
880
    """
881
    return [attr for attr in domain.variables + domain.metas
882
                 if attr.is_discrete]
883
884
885
def source_attributes(domain):
886
    """
887
    Return all suitable attributes for the venn diagram.
888
    """
889
    return string_attributes(domain)  # + discrete_attributes(domain)
890
891
892
def disjoint(sets):
893
    """
894
    Return all disjoint subsets.
895
    """
896
    sets = list(sets)
897
    n = len(sets)
898
    disjoint_sets = [None] * (2 ** n)
899
    for i in range(2 ** n):
900
        key = setkey(i, n)
901
        included = [s for s, inc in zip(sets, key) if inc]
902
        excluded = [s for s, inc in zip(sets, key) if not inc]
903
        if any(included):
904
            s = reduce(set.intersection, included)
905
        else:
906
            s = set()
907
908
        s = reduce(set.difference, excluded, s)
909
910
        disjoint_sets[i] = s
911
912
    return disjoint_sets
913
914
915
def disjoint_set_label(i, n, simplify=False):
0 ignored issues
show
Unused Code introduced by
The argument i seems to be unused.
Loading history...
916
    """
917
    Return a html formated label for a disjoint set indexed by `i`.
918
    """
919
    intersection = unicodedata.lookup("INTERSECTION")
920
    # comp = unicodedata.lookup("COMPLEMENT")  #
921
    # This depends on the font but the unicode complement in
922
    # general does not look nice in a super script so we use
923
    # plain c instead.
924
    comp = "c"
925
926
    def label_for_index(i):
927
        return chr(ord("A") + i)
928
929
    if simplify:
930
        return "".join(label_for_index(i) for i, b in enumerate(setkey(i, n))
931
                       if b)
932
    else:
933
        return intersection.join(label_for_index(i) +
934
                                 ("" if b else "<sup>" + comp + "</sup>")
935
                                 for i, b in enumerate(setkey(i, n)))
936
937
938
class VennSetItem(QGraphicsPathItem):
939
    def __init__(self, parent=None, text=None, count=None):
0 ignored issues
show
Comprehensibility Bug introduced by
count is re-defining a name which is already available in the outer-scope (previously defined on line 11).

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...
940
        super(VennSetItem, self).__init__(parent)
941
        self.text = text
942
        self.count = count
943
944
945
# TODO: Use palette's selected/highligted text / background colors to
0 ignored issues
show
Coding Style introduced by
TODO and FIXME comments should generally be avoided.
Loading history...
946
# indicate selection
947
948
class VennIntersectionArea(QGraphicsPathItem):
949
    def __init__(self, parent=None, text=""):
0 ignored issues
show
Unused Code introduced by
The argument text seems to be unused.
Loading history...
950
        super(QGraphicsPathItem, self).__init__(parent)
0 ignored issues
show
Bug introduced by
The first argument passed to super() should be the super-class name, but QGraphicsPathItem was given.
Loading history...
951
        self.setAcceptHoverEvents(True)
952
        self.setPen(QPen(Qt.NoPen))
953
954
        self.text = QGraphicsTextItem(self)
955
        layout = self.text.document().documentLayout()
956
        layout.documentSizeChanged.connect(self._onLayoutChanged)
957
958
        self._text = ""
959
        self._anchor = QPointF()
960
961
    def setText(self, text):
962
        if self._text != text:
963
            self._text = text
964
            self.text.setPlainText(text)
965
966
    def text(self):
0 ignored issues
show
Bug introduced by
This method seems to be hidden by an attribute defined in Orange.widgets.visualize.owvenndiagram on line 954.
Loading history...
967
        return self._text
968
969
    def setTextAnchor(self, pos):
970
        if self._anchor != pos:
971
            self._anchor = pos
972
            self._updateTextAnchor()
973
974
    def hoverEnterEvent(self, event):
975
        self.setZValue(self.zValue() + 1)
976
        return QGraphicsPathItem.hoverEnterEvent(self, event)
977
978
    def hoverLeaveEvent(self, event):
979
        self.setZValue(self.zValue() - 1)
980
        return QGraphicsPathItem.hoverLeaveEvent(self, event)
981
982
    def mousePressEvent(self, event):
983
        if event.button() == Qt.LeftButton:
984
            if event.modifiers() & Qt.AltModifier:
985
                self.setSelected(False)
986
            elif event.modifiers() & Qt.ControlModifier:
987
                self.setSelected(not self.isSelected())
988
            elif event.modifiers() & Qt.ShiftModifier:
989
                self.setSelected(True)
990
            else:
991
                for area in self.parentWidget().vennareas():
992
                    area.setSelected(False)
993
                self.setSelected(True)
994
995
    def mouseReleaseEvent(self, event):
996
        pass
997
998
    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 29).

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 argument widget seems to be unused.
Loading history...
999
        painter.save()
1000
        path = self.path()
1001
        brush = QBrush(self.brush())
1002
        pen = QPen(self.pen())
1003
1004
        if option.state & QStyle.State_Selected:
1005
            pen.setColor(Qt.red)
1006
            brush.setStyle(Qt.DiagCrossPattern)
1007
            brush.setColor(QColor(40, 40, 40, 100))
1008
1009
        elif option.state & QStyle.State_MouseOver:
1010
            pen.setColor(Qt.blue)
1011
1012
        if option.state & QStyle.State_MouseOver:
1013
            brush.setColor(QColor(100, 100, 100, 100))
1014
            if brush.style() == Qt.NoBrush:
1015
                # Make sure the highlight is actually visible.
1016
                brush.setStyle(Qt.SolidPattern)
1017
1018
        painter.setPen(pen)
1019
        painter.setBrush(brush)
1020
        painter.drawPath(path)
1021
        painter.restore()
1022
1023
    def itemChange(self, change, value):
1024
        if change == QGraphicsPathItem.ItemSelectedHasChanged:
1025
            self.setZValue(self.zValue() + (1 if value else -1))
1026
        return QGraphicsPathItem.itemChange(self, change, value)
1027
1028
    def _updateTextAnchor(self):
1029
        rect = self.text.boundingRect()
1030
        pos = anchor_rect(rect, self._anchor)
1031
        self.text.setPos(pos)
1032
1033
    def _onLayoutChanged(self):
1034
        self._updateTextAnchor()
1035
1036
1037
class GraphicsTextEdit(QGraphicsTextItem):
1038
    #: Edit triggers
1039
    NoEditTriggers, DoubleClicked = 0, 1
1040
1041
    editingFinished = Signal()
1042
    editingStarted = Signal()
1043
1044
    documentSizeChanged = Signal()
1045
1046
    def __init__(self, *args, **kwargs):
1047
        super(GraphicsTextEdit, self).__init__(*args, **kwargs)
1048
        self.setTabChangesFocus(True)
1049
        self._edittrigger = GraphicsTextEdit.DoubleClicked
1050
        self._editing = False
1051
        self.document().documentLayout().documentSizeChanged.connect(
1052
            self.documentSizeChanged
1053
        )
1054
1055
    def mouseDoubleClickEvent(self, event):
1056
        super(GraphicsTextEdit, self).mouseDoubleClickEvent(event)
1057
        if self._edittrigger == GraphicsTextEdit.DoubleClicked:
1058
            self._start()
1059
1060
    def focusOutEvent(self, event):
1061
        super(GraphicsTextEdit, self).focusOutEvent(event)
1062
1063
        if self._editing:
1064
            self._end()
1065
1066
    def _start(self):
1067
        self._editing = True
1068
        self.setTextInteractionFlags(Qt.TextEditorInteraction)
1069
        self.setFocus(Qt.MouseFocusReason)
1070
        self.editingStarted.emit()
1071
1072
    def _end(self):
1073
        self._editing = False
1074
        self.setTextInteractionFlags(Qt.NoTextInteraction)
1075
        self.editingFinished.emit()
1076
1077
1078
class VennDiagram(QGraphicsWidget):
1079
    # rect and petal are for future work
1080
    Circle, Ellipse, Rect, Petal = 1, 2, 3, 4
1081
1082
    TitleFormat = "<center><h4>{0}</h4>{1}</center>"
1083
1084
    selectionChanged = Signal()
1085
    itemTextEdited = Signal(int, str)
1086
1087
    def __init__(self, parent=None):
1088
        super(VennDiagram, self).__init__(parent)
1089
        self.shapeType = VennDiagram.Circle
1090
1091
        self._setup()
1092
1093
    def _setup(self):
1094
        self._items = []
1095
        self._vennareas = []
1096
        self._textitems = []
1097
1098
    def item(self, index):
1099
        return self._items[index]
1100
1101
    def items(self):
1102
        return list(self._items)
1103
1104
    def count(self):
1105
        return len(self._items)
1106
1107
    def setItems(self, items):
1108
        if self._items:
1109
            self.clear()
1110
1111
        self._items = list(items)
0 ignored issues
show
Coding Style introduced by
The attribute _items 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...
1112
1113
        for item in self._items:
1114
            item.setParentItem(self)
1115
            item.setVisible(True)
1116
1117
        fmt = self.TitleFormat.format
1118
1119
        font = self.font()
1120
        font.setPixelSize(14)
1121
1122
        for item in items:
1123
            text = GraphicsTextEdit(self)
1124
            text.setFont(font)
1125
            text.setDefaultTextColor(QColor("#333"))
1126
            text.setHtml(fmt(escape(item.text), item.count))
1127
            text.adjustSize()
1128
            text.editingStarted.connect(self._on_editingStarted)
1129
            text.editingFinished.connect(self._on_editingFinished)
1130
            text.documentSizeChanged.connect(
1131
                self._on_itemTextSizeChanged
1132
            )
1133
1134
            self._textitems.append(text)
1135
1136
        self._vennareas = [
0 ignored issues
show
Coding Style introduced by
The attribute _vennareas 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...
1137
            VennIntersectionArea(parent=self)
1138
            for i in range(2 ** len(items))
1139
        ]
1140
        self._subsettextitems = [
0 ignored issues
show
Coding Style introduced by
The attribute _subsettextitems 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...
1141
            QGraphicsTextItem(parent=self)
1142
            for i in range(2 ** len(items))
1143
        ]
1144
1145
        self._updateLayout()
1146
1147
    def clear(self):
1148
        scene = self.scene()
1149
        items = self.vennareas() + list(self.items()) + self._textitems
1150
1151
        for item in self._textitems:
1152
            item.editingStarted.disconnect(self._on_editingStarted)
1153
            item.editingFinished.disconnect(self._on_editingFinished)
1154
            item.documentSizeChanged.disconnect(
1155
                self._on_itemTextSizeChanged
1156
            )
1157
1158
        self._items = []
0 ignored issues
show
Coding Style introduced by
The attribute _items 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...
1159
        self._vennareas = []
0 ignored issues
show
Coding Style introduced by
The attribute _vennareas 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...
1160
        self._textitems = []
0 ignored issues
show
Coding Style introduced by
The attribute _textitems 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...
1161
1162
        for item in items:
1163
            item.setVisible(False)
1164
            item.setParentItem(None)
1165
            if scene is not None:
1166
                scene.removeItem(item)
1167
1168
    def vennareas(self):
1169
        return list(self._vennareas)
1170
1171
    def setFont(self, font):
1172
        if self._font != font:
1173
            self.prepareGeometryChange()
1174
            self._font = font
0 ignored issues
show
Coding Style introduced by
The attribute _font 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...
1175
1176
            for item in self.items():
1177
                item.setFont(font)
1178
1179
    def _updateLayout(self):
1180
        rect = self.geometry()
1181
        n = len(self._items)
1182
        if not n:
1183
            return
1184
1185
        regions = venn_diagram(n, shape=self.shapeType)
1186
1187
        # The y axis in Qt points downward
1188
        transform = QTransform().scale(1, -1)
1189
        regions = list(map(transform.map, regions))
1190
1191
        union_brect = reduce(QRectF.united,
1192
                             (path.boundingRect() for path in regions))
1193
1194
        scalex = rect.width() / union_brect.width()
1195
        scaley = rect.height() / union_brect.height()
1196
        scale = min(scalex, scaley)
1197
1198
        transform = QTransform().scale(scale, scale)
1199
1200
        regions = [transform.map(path) for path in regions]
1201
1202
        center = rect.width() / 2, rect.height() / 2
1203
        for item, path in zip(self.items(), regions):
1204
            item.setPath(path)
1205
            item.setPos(*center)
1206
1207
        intersections = venn_intersections(regions)
1208
        assert len(intersections) == 2 ** n
1209
        assert len(self.vennareas()) == 2 ** n
1210
1211
        anchors = [(0, 0)] + subset_anchors(self._items)
1212
1213
        anchor_transform = QTransform().scale(rect.width(), -rect.height())
1214
        for i, area in enumerate(self.vennareas()):
1215
            area.setPath(intersections[setkey(i, n)])
1216
            area.setPos(*center)
1217
            x, y = anchors[i]
1218
            anchor = anchor_transform.map(QPointF(x, y))
1219
            area.setTextAnchor(anchor)
1220
            area.setZValue(30)
1221
1222
        self._updateTextAnchors()
1223
1224
    def _updateTextAnchors(self):
1225
        n = len(self._items)
1226
1227
        items = self._items
1228
        dist = 15
1229
1230
        shape = reduce(QPainterPath.united, [item.path() for item in items])
1231
        brect = shape.boundingRect()
1232
        bradius = max(brect.width() / 2, brect.height() / 2)
1233
1234
        center = self.boundingRect().center()
1235
1236
        anchors = _category_anchors(items)
1237
        self._textanchors = []
0 ignored issues
show
Coding Style introduced by
The attribute _textanchors 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...
1238
        for angle, anchor_h, anchor_v in anchors:
1239
            line = QLineF.fromPolar(bradius, angle)
1240
            ext = QLineF.fromPolar(dist, angle)
1241
            line = QLineF(line.p1(), line.p2() + ext.p2())
1242
            line = line.translated(center)
1243
1244
            anchor_pos = line.p2()
1245
            self._textanchors.append((anchor_pos, anchor_h, anchor_v))
1246
1247
        for i in range(n):
1248
            self._updateTextItemPos(i)
1249
1250
    def _updateTextItemPos(self, i):
1251
        item = self._textitems[i]
1252
        anchor_pos, anchor_h, anchor_v = self._textanchors[i]
1253
        rect = item.boundingRect()
1254
        pos = anchor_rect(rect, anchor_pos, anchor_h, anchor_v)
1255
        item.setPos(pos)
1256
1257
    def setGeometry(self, geometry):
1258
        super(VennDiagram, self).setGeometry(geometry)
1259
        self._updateLayout()
1260
1261
    def paint(self, painter, option, w):
1262
        super(VennDiagram, self).paint(painter, option, w)
1263
#         painter.drawRect(self.boundingRect())
1264
1265
    def _on_editingStarted(self):
1266
        item = self.sender()
1267
        index = self._textitems.index(item)
1268
        text = self._items[index].text
1269
        item.setTextWidth(-1)
1270
        item.setHtml(self.TitleFormat.format(escape(text), "<br/>"))
1271
1272
    def _on_editingFinished(self):
1273
        item = self.sender()
1274
        index = self._textitems.index(item)
1275
        text = item.toPlainText()
1276
        if text != self._items[index].text:
1277
            self._items[index].text = text
1278
1279
            self.itemTextEdited.emit(index, text)
1280
1281
        item.setHtml(
1282
            self.TitleFormat.format(escape(text), self._items[index].count))
1283
        item.adjustSize()
1284
1285
    def _on_itemTextSizeChanged(self):
1286
        item = self.sender()
1287
        index = self._textitems.index(item)
1288
        self._updateTextItemPos(index)
1289
1290
1291
def anchor_rect(rect, anchor_pos,
1292
                anchor_h=Qt.AnchorHorizontalCenter,
1293
                anchor_v=Qt.AnchorVerticalCenter):
1294
1295
    if anchor_h == Qt.AnchorLeft:
1296
        x = anchor_pos.x()
1297
    elif anchor_h == Qt.AnchorHorizontalCenter:
1298
        x = anchor_pos.x() - rect.width() / 2
1299
    elif anchor_h == Qt.AnchorRight:
1300
        x = anchor_pos.x() - rect.width()
1301
    else:
1302
        raise ValueError(anchor_h)
1303
1304
    if anchor_v == Qt.AnchorTop:
1305
        y = anchor_pos.y()
1306
    elif anchor_v == Qt.AnchorVerticalCenter:
1307
        y = anchor_pos.y() - rect.height() / 2
1308
    elif anchor_v == Qt.AnchorBottom:
1309
        y = anchor_pos.y() - rect.height()
1310
    else:
1311
        raise ValueError(anchor_v)
1312
1313
    return QPointF(x, y)
1314
1315
1316
def radians(angle):
1317
    return 2 * math.pi * angle / 360
1318
1319
1320
def unit_point(x, r=1.0):
1321
    x = radians(x)
1322
    return (r * math.cos(x), r * math.sin(x))
1323
1324
1325
def _category_anchors(shapes):
1326
    n = len(shapes)
1327
    return _CATEGORY_ANCHORS[n - 1]
1328
1329
1330
# (angle, horizontal anchor, vertical anchor)
1331
_CATEGORY_ANCHORS = (
1332
    # n == 1
1333
    ((90, Qt.AnchorHorizontalCenter, Qt.AnchorBottom),),
1334
    # n == 2
1335
    ((180, Qt.AnchorRight, Qt.AnchorVerticalCenter),
1336
     (0, Qt.AnchorLeft, Qt.AnchorVerticalCenter)),
1337
    # n == 3
1338
    ((150, Qt.AnchorRight, Qt.AnchorBottom),
1339
     (30, Qt.AnchorLeft, Qt.AnchorBottom),
1340
     (270, Qt.AnchorHorizontalCenter, Qt.AnchorTop)),
1341
    # n == 4
1342
    ((270 + 45, Qt.AnchorLeft, Qt.AnchorTop),
1343
     (270 - 45, Qt.AnchorRight, Qt.AnchorTop),
1344
     (90 - 15, Qt.AnchorLeft, Qt.AnchorBottom),
1345
     (90 + 15, Qt.AnchorRight, Qt.AnchorBottom)),
1346
    # n == 5
1347
    ((90 - 5, Qt.AnchorHorizontalCenter, Qt.AnchorBottom),
1348
     (18 - 5, Qt.AnchorLeft, Qt.AnchorVerticalCenter),
1349
     (306 - 5, Qt.AnchorLeft, Qt.AnchorTop),
1350
     (234 - 5, Qt.AnchorRight, Qt.AnchorTop),
1351
     (162 - 5, Qt.AnchorRight, Qt.AnchorVerticalCenter),)
1352
)
1353
1354
1355
def subset_anchors(shapes):
1356
    n = len(shapes)
1357
    if n == 1:
1358
        return [(0, 0)]
1359
    elif n == 2:
1360
        return [unit_point(180, r=1/3),
1361
                unit_point(0, r=1/3),
1362
                (0, 0)]
1363
    elif n == 3:
1364
        return [unit_point(150, r=0.35),  # A
1365
                unit_point(30, r=0.35),   # B
1366
                unit_point(90, r=0.27),   # AB
1367
                unit_point(270, r=0.35),  # C
1368
                unit_point(210, r=0.27),  # AC
1369
                unit_point(330, r=0.27),  # BC
1370
                unit_point(0, r=0),       # ABC
1371
                ]
1372
    elif n == 4:
1373
        anchors = [
1374
            (0.400, 0.110),    # A
1375
            (-0.400, 0.110),   # B
1376
            (0.000, -0.285),   # AB
1377
            (0.180, 0.330),    # C
1378
            (0.265, 0.205),    # AC
1379
            (-0.240, -0.110),  # BC
1380
            (-0.100, -0.190),  # ABC
1381
            (-0.180, 0.330),   # D
1382
            (0.240, -0.110),   # AD
1383
            (-0.265, 0.205),   # BD
1384
            (0.100, -0.190),   # ABD
1385
            (0.000, 0.250),    # CD
1386
            (0.153, 0.090),    # ACD
1387
            (-0.153, 0.090),   # BCD
1388
            (0.000, -0.060),   # ABCD
1389
        ]
1390
        return anchors
1391
1392
    elif n == 5:
1393
        anchors = [None] * 32
1394
        # Base anchors
1395
        A = (0.033, 0.385)
1396
        AD = (0.095, 0.250)
1397
        AE = (-0.100, 0.265)
1398
        ACE = (-0.130, 0.220)
1399
        ADE = (0.010, 0.225)
1400
        ACDE = (-0.095, 0.175)
1401
        ABCDE = (0.0, 0.0)
1402
1403
        anchors[-1] = ABCDE
1404
1405
        bases = [(0b00001, A),
1406
                 (0b01001, AD),
1407
                 (0b10001, AE),
1408
                 (0b10101, ACE),
1409
                 (0b11001, ADE),
1410
                 (0b11101, ACDE)]
1411
1412
        for i in range(5):
1413
            for index, anchor in bases:
1414
                index = bit_rot_left(index, i, bits=5)
1415
                assert anchors[index] is None
1416
                anchors[index] = rotate_point(anchor, - 72 * i)
1417
1418
        assert all(anchors[1:])
1419
        return anchors[1:]
1420
1421
1422
def bit_rot_left(x, y, bits=32):
1423
    mask = 2 ** bits - 1
1424
    x_masked = x & mask
1425
    return (x << y) & mask | (x_masked >> bits - y)
1426
1427
1428
def rotate_point(p, angle):
1429
    r = radians(angle)
1430
    R = numpy.array([[math.cos(r), -math.sin(r)],
1431
                     [math.sin(r), math.cos(r)]])
1432
    x, y = numpy.dot(R, p)
1433
    return (float(x), float(y))
1434
1435
1436
def line_extended(line, distance):
1437
    """
1438
    Return an QLineF extended by `distance` units in the positive direction.
1439
    """
1440
    angle = line.angle() / 360 * 2 * math.pi
1441
    dx, dy = unit_point(angle, r=distance)
1442
    return QLineF(line.p1(), line.p2() + QPointF(dx, dy))
1443
1444
1445
def circle_path(center, r=1.0):
1446
    return ellipse_path(center, r, r, rotation=0)
1447
1448
1449
def ellipse_path(center, a, b, rotation=0):
1450
    if not isinstance(center, QPointF):
1451
        center = QPointF(*center)
1452
1453
    brect = QRectF(-a, -b, 2 * a, 2 * b)
1454
1455
    path = QPainterPath()
1456
    path.addEllipse(brect)
1457
1458
    if rotation != 0:
1459
        transform = QTransform().rotate(rotation)
1460
        path = transform.map(path)
1461
1462
    path.translate(center)
1463
    return path
1464
1465
1466
# TODO: Should include anchors for text layout (both inside and outside).
0 ignored issues
show
Coding Style introduced by
TODO and FIXME comments should generally be avoided.
Loading history...
1467
# for each item {path: QPainterPath,
1468
#                text_anchors: [{center}] * (2 ** n)
1469
#                mayor_axis: QLineF,
1470
#                boundingRect QPolygonF (with 4 vertices)}
1471
#
1472
# Should be a new class with overloads for ellipse/circle, rect, and petal
1473
# shapes, should store all constructor parameters, rotation, center,
1474
# mayor/minor axis.
1475
1476
1477
def venn_diagram(n, shape=VennDiagram.Circle):
0 ignored issues
show
Unused Code introduced by
The argument shape seems to be unused.
Loading history...
1478
    if n < 1 or n > 5:
1479
        raise ValueError()
1480
1481
    paths = []
1482
1483
    if n == 1:
1484
        paths = [circle_path(center=(0, 0), r=0.5)]
1485
    elif n == 2:
1486
        angles = [180, 0]
1487
        paths = [circle_path(center=unit_point(x, r=1/6), r=1/3)
1488
                 for x in angles]
1489
    elif n == 3:
1490
        angles = [150 - 120 * i for i in range(3)]
1491
        paths = [circle_path(center=unit_point(x, r=1/6), r=1/3)
1492
                 for x in angles]
1493
    elif n == 4:
1494
        # Constants shamelessly stolen from VennDiagram R package
1495
        paths = [
1496
            ellipse_path((0.65 - 0.5, 0.47 - 0.5), 0.35, 0.20, 45),
1497
            ellipse_path((0.35 - 0.5, 0.47 - 0.5), 0.35, 0.20, 135),
1498
            ellipse_path((0.5 - 0.5, 0.57 - 0.5), 0.35, 0.20, 45),
1499
            ellipse_path((0.5 - 0.5, 0.57 - 0.5), 0.35, 0.20, 134),
1500
        ]
1501
    elif n == 5:
1502
        # Constants shamelessly stolen from VennDiagram R package
1503
        d = 0.13
1504
        a, b = 0.24, 0.48
1505
        a, b = b, a
1506
        a, b = 0.48, 0.24
1507
        paths = [ellipse_path(unit_point((1 - i) * 72, r=d),
1508
                              a, b, rotation=90 - (i * 72))
1509
                 for i in range(5)]
1510
1511
    return paths
1512
1513
1514
def setkey(intval, n):
1515
    return tuple(bool(intval & (2 ** i)) for i in range(n))
1516
1517
1518
def keyrange(n):
1519
    if n < 0:
1520
        raise ValueError()
1521
1522
    for i in range(2 ** n):
1523
        yield setkey(i, n)
1524
1525
1526
def venn_intersections(paths):
1527
    n = len(paths)
1528
    return {key: venn_intersection(paths, key) for key in keyrange(n)}
1529
1530
1531
def venn_intersection(paths, key):
1532
    if not any(key):
1533
        return QPainterPath()
1534
1535
    # first take the intersection of all included paths
1536
    path = reduce(QPainterPath.intersected,
1537
                  (path for path, included in zip(paths, key) if included))
1538
1539
    # subtract all the excluded sets (i.e. take the intersection
1540
    # with the excluded set complements)
1541
    path = reduce(QPainterPath.subtracted,
1542
                  (path for path, included in zip(paths, key) if not included),
1543
                  path)
1544
1545
    return path
1546
1547
1548
def append_column(data, where, variable, column):
1549
    X, Y, M, W = data.X, data.Y, data.metas, data.W
1550
    domain = data.domain
1551
    attr = domain.attributes
1552
    class_vars = domain.class_vars
1553
    metas = domain.metas
1554
1555
    if where == "X":
1556
        attr = attr + (variable,)
1557
        X = numpy.hstack((X, column))
1558
    elif where == "Y":
1559
        class_vars = class_vars + (variable,)
1560
        Y = numpy.hstack((Y, column))
1561
    elif where == "M":
1562
        metas = metas + (variable,)
1563
        M = numpy.hstack((M, column))
1564
    else:
1565
        raise ValueError
1566
    domain = Orange.data.Domain(attr, class_vars, metas)
1567
    table = Orange.data.Table.from_numpy(domain, X, Y, M, W if W.size else None)
1568
    table.ids = data.ids
1569
    return table
1570
1571
1572
def drop_columns(data, columns):
1573
    columns = set(data.domain[col] for col in columns)
1574
1575
    def filter_vars(vars):
0 ignored issues
show
Bug Best Practice introduced by
This seems to re-define the built-in vars.

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

Loading history...
1576
        return tuple(var for var in vars if var not in columns)
1577
1578
    domain = Orange.data.Domain(
1579
        filter_vars(data.domain.attributes),
1580
        filter_vars(data.domain.class_vars),
1581
        filter_vars(data.domain.metas)
1582
    )
1583
    return Orange.data.Table.from_table(domain, data)
1584
1585
1586
def test():
1587
    import sklearn.cross_validation as skl_cross_validation
0 ignored issues
show
Configuration introduced by
The import sklearn.cross_validation 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...
1588
    app = QApplication([])
1589
    w = OWVennDiagram()
1590
    data = Orange.data.Table("brown-selected")
1591
    data = append_column(data, "M", Orange.data.StringVariable("Test"),
1592
                         numpy.arange(len(data)).reshape(-1, 1) % 30)
1593
1594
    indices = skl_cross_validation.ShuffleSplit(
1595
        len(data), n_iter=5, test_size=0.7
1596
    )
1597
1598
    indices = iter(indices)
1599
1600
    def select(data):
1601
        sample, _ = next(indices)
1602
        return data[sample]
1603
1604
    d1 = select(data)
1605
    d2 = select(data)
1606
    d3 = select(data)
1607
    d4 = select(data)
1608
    d5 = select(data)
1609
1610
    for i, data in enumerate([d1, d2, d3, d4, d5]):
1611
        data.name = chr(ord("A") + i)
1612
        w.setData(data, key=i)
1613
1614
    w.handleNewSignals()
1615
    w.show()
1616
    app.exec_()
1617
1618
    del w
1619
    app.processEvents()
1620
    return app
1621
1622
1623
def test1():
1624
    app = QApplication([])
1625
    w = OWVennDiagram()
1626
    data1 = Orange.data.Table("brown-selected")
1627
    data2 = Orange.data.Table("brown-selected")
1628
    w.setData(data1, 1)
1629
    w.setData(data2, 2)
1630
    w.handleNewSignals()
1631
1632
    w.show()
1633
    w.raise_()
1634
    app.exec_()
1635
1636
    del w
1637
    return app
1638
1639
if __name__ == "__main__":
1640
    test()
1641