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 |
|
|
|
|
16
|
|
|
|
17
|
|
|
from PyQt4.QtGui import ( |
|
|
|
|
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 |
|
|
|
|
25
|
|
|
from PyQt4.QtCore import pyqtSignal as Signal |
|
|
|
|
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 |
|
|
|
|
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): |
|
|
|
|
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): |
|
|
|
|
301
|
|
|
return list(map(ComparableInstance, input.table)) |
302
|
|
|
|
303
|
|
|
input = self.data[key] |
|
|
|
|
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()): |
|
|
|
|
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(): |
|
|
|
|
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()): |
|
|
|
|
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 = [] |
|
|
|
|
354
|
|
|
for i, (key, input) in enumerate(self.data.items()): |
|
|
|
|
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()) |
|
|
|
|
380
|
|
|
|
381
|
|
|
vennitems = [] |
382
|
|
|
colors = colorpalette.ColorPaletteHSV(n) |
383
|
|
|
|
384
|
|
|
for i, (key, item) in enumerate(self.itemsets.items()): |
|
|
|
|
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): |
|
|
|
|
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) |
|
|
|
|
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()): |
|
|
|
|
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. |
|
|
|
|
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 |
|
|
|
|
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): |
|
|
|
|
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): |
|
|
|
|
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): |
|
|
|
|
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): |
|
|
|
|
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 |
|
|
|
|
946
|
|
|
# indicate selection |
947
|
|
|
|
948
|
|
|
class VennIntersectionArea(QGraphicsPathItem): |
949
|
|
|
def __init__(self, parent=None, text=""): |
|
|
|
|
950
|
|
|
super(QGraphicsPathItem, self).__init__(parent) |
|
|
|
|
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): |
|
|
|
|
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): |
|
|
|
|
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) |
|
|
|
|
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 = [ |
|
|
|
|
1137
|
|
|
VennIntersectionArea(parent=self) |
1138
|
|
|
for i in range(2 ** len(items)) |
1139
|
|
|
] |
1140
|
|
|
self._subsettextitems = [ |
|
|
|
|
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 = [] |
|
|
|
|
1159
|
|
|
self._vennareas = [] |
|
|
|
|
1160
|
|
|
self._textitems = [] |
|
|
|
|
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 |
|
|
|
|
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 = [] |
|
|
|
|
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). |
|
|
|
|
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): |
|
|
|
|
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): |
|
|
|
|
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 |
|
|
|
|
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
|
|
|
|
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.
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.