|
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__.pyfiles in your module folders. Make sure that you place one file in each sub-folder.