Skip to content

Commit bf04d31

Browse files
committed
OWSOM: Coloring by numeric features
1 parent 85da115 commit bf04d31

File tree

1 file changed

+67
-34
lines changed

1 file changed

+67
-34
lines changed

Orange/widgets/unsupervised/owsom.py

Lines changed: 67 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -12,23 +12,25 @@
1212
QGraphicsItem, QGraphicsRectItem, QGraphicsItemGroup, QSizePolicy, \
1313
QGraphicsPathItem
1414

15-
from Orange.widgets.visualize.utils import CanvasRectangle, CanvasText
16-
from Orange.widgets.utils.annotated_data import ANNOTATED_DATA_SIGNAL_NAME, \
17-
create_annotated_table
15+
from Orange.data import Table, Domain
16+
from Orange.projection.som import SOM
1817

19-
from Orange.data import Table, Domain, DiscreteVariable
2018
from Orange.widgets import gui
21-
from Orange.widgets.visualize.utils.plotutils import wrap_legend_items
2219
from Orange.widgets.widget import OWWidget, Msg, Input, Output
2320
from Orange.widgets.settings import \
2421
DomainContextHandler, ContextSetting, Setting
2522
from Orange.widgets.utils.itemmodels import DomainModel
2623
from Orange.widgets.utils.widgetpreview import WidgetPreview
27-
from Orange.projection.som import SOM
24+
from Orange.widgets.utils.annotated_data import \
25+
create_annotated_table, ANNOTATED_DATA_SIGNAL_NAME
26+
from Orange.widgets.utils.colorpalette import ContinuousPaletteGenerator
27+
from Orange.widgets.visualize.utils import CanvasRectangle, CanvasText
28+
from Orange.widgets.visualize.utils.plotutils import wrap_legend_items
2829

2930

3031
sqrt3_2 = np.sqrt(3) / 2
3132

33+
3234
class SomView(QGraphicsView):
3335
SelectionClear, SelectionAdd, SelectionRemove, SelectionToggle = 1, 2, 4, 8
3436
SelectionSet = SelectionClear | SelectionAdd
@@ -207,6 +209,7 @@ def __init__(self):
207209
self.sizes = None
208210
self.cells = self.member_data = None
209211
self.selection = set()
212+
self.colors = self.bins = None
210213

211214
box = gui.vBox(self.controlArea, box=True)
212215
hbox = gui.hBox(box)
@@ -241,7 +244,7 @@ def __init__(self):
241244
box, self, "attr_color", maximumContentsLength=15,
242245
callback=self.on_attr_color_change,
243246
model=DomainModel(placeholder="(Same color)",
244-
valid_types=DiscreteVariable))
247+
valid_types=DomainModel.PRIMITIVE))
245248
gui.checkBox(
246249
box, self, "pie_charts", label="Show pie charts",
247250
callback=self.on_pie_chart_change)
@@ -314,12 +317,9 @@ def set_data(self, data):
314317

315318
if self.data is not None:
316319
self.controls.attr_color.model().set_domain(data.domain)
317-
class_var = data.domain.class_var
318-
if class_var is not None and class_var.is_discrete:
319-
self.attr_color = class_var
320-
else:
321-
self.attr_color = None
320+
self.attr_color = data.domain.class_var
322321
self.openContext(data)
322+
self.set_color_bins()
323323
self.create_legend()
324324
self.recompute_dimensions()
325325
self.replot()
@@ -341,6 +341,7 @@ def clear(self):
341341
self.data = self.cont_x = None
342342
self.sizes = None
343343
self.cells = self.member_data = None
344+
self.colors = self.bins = None
344345
self.assignments = None
345346
if self.elements is not None:
346347
self.scene.removeItem(self.elements)
@@ -381,6 +382,7 @@ def on_geometry_change(self):
381382

382383
def on_attr_color_change(self):
383384
self.controls.pie_charts.setEnabled(self.attr_color is not None)
385+
self.set_color_bins()
384386
self.create_legend()
385387
self.rescale()
386388
self._redraw()
@@ -474,14 +476,10 @@ def _redraw(self):
474476
self.scene.addItem(self.elements)
475477
if self.attr_color is None:
476478
self._redraw_same_color()
479+
elif self.pie_charts:
480+
self._redraw_pie_charts()
477481
else:
478-
color_column = \
479-
self.data.get_column_view(self.attr_color)[0].astype(float)
480-
colors = [QColor(*color) for color in self.attr_color.colors]
481-
if self.pie_charts:
482-
self._redraw_pie_charts(color_column, colors)
483-
else:
484-
self._redraw_colored_circles(color_column, colors)
482+
self._redraw_colored_circles()
485483

486484
@property
487485
def _grid_factors(self):
@@ -503,40 +501,53 @@ def _redraw_same_color(self):
503501
ellipse.setBrush(brush)
504502
self.elements.addToGroup(ellipse)
505503

506-
def _redraw_pie_charts(self, color_column, colors):
504+
def _get_color_column(self):
505+
color_column = \
506+
self.data.get_column_view(self.attr_color)[0].astype(float,
507+
copy=False)
508+
if self.attr_color.is_discrete:
509+
with np.errstate(invalid="ignore"):
510+
int_col = color_column.astype(int)
511+
int_col[np.isnan(color_column)] = len(self.colors)
512+
else:
513+
int_col = np.zeros(len(color_column), dtype=int)
514+
int_col[np.isnan(color_column)] = len(self.colors)
515+
for i, thresh in enumerate(self.bins, start=1):
516+
int_col[color_column >= thresh] = i
517+
return int_col
518+
519+
def _redraw_pie_charts(self):
507520
fx, fy = self._grid_factors
508-
color_column = color_column.copy()
509-
color_column[np.isnan(color_column)] = len(colors)
510-
color_column = color_column.astype(int)
511-
colors.append(Qt.gray)
521+
color_column = self._get_color_column()
522+
colors = self.colors + [Qt.gray]
512523
for y in range(self.size_y):
513524
for x in range(self.size_x - self.hexagonal * (y % 2)):
514525
r = self.sizes[x, y]
515526
if not r:
516527
continue
517528
members = self.get_member_indices(x, y)
518-
color_dist = np.bincount(
519-
color_column[members], minlength=len(self.attr_color.values))
529+
color_dist = np.bincount(color_column[members],
530+
minlength=len(colors))
520531
color_dist = color_dist.astype(float) / len(members)
521532
pie = PieChart(color_dist, r / 2, colors)
522533
self.elements.addToGroup(pie)
523534
pie.setPos(x + (y % 2) * fx, y * fy)
524535

525-
def _redraw_colored_circles(self, color_column, colors):
536+
def _redraw_colored_circles(self):
526537
fx, fy = self._grid_factors
538+
color_column = self._get_color_column()
527539
for y in range(self.size_y):
528540
for x in range(self.size_x - self.hexagonal * (y % 2)):
529541
r = self.sizes[x, y]
530542
if not r:
531543
continue
532544
members = self.get_member_indices(x, y)
533545
color_dist = color_column[members]
534-
color_dist = color_dist[np.isfinite(color_dist)]
546+
color_dist = color_dist[color_dist < len(self.colors)]
535547
if len(color_dist) != len(members):
536548
self.Warning.missing_colors(self.attr_color.name)
537-
color_dist = color_dist.astype(int)
538-
bc = np.bincount(color_dist, minlength=len(self.attr_color.values))
539-
color = colors[np.argmax(bc)]
549+
bc = np.bincount(color_dist, minlength=len(self.colors))
550+
color = self.colors[np.argmax(bc)]
540551
pen = QPen(QBrush(color), 4)
541552
brush = QBrush(color.lighter(200 - 100 * np.max(bc) / len(members)))
542553
pen.setCosmetic(True)
@@ -703,18 +714,40 @@ def update_output(self):
703714
self.Outputs.annotated_data.send(None)
704715
self.info.set_output_summary(self.info.NoOutput)
705716

717+
def set_color_bins(self):
718+
if self.attr_color is None:
719+
self.bins = self.colors = None
720+
elif self.attr_color.is_discrete:
721+
self.bins = None
722+
self.colors = [QColor(*color) for color in self.attr_color.colors]
723+
else:
724+
col = self.data.get_column_view(self.attr_color)[0].astype(float)
725+
# TODO: Use intelligent binning from #3896, when it's merged
726+
self.bins = np.linspace(np.min(col), np.max(col), 6)[1:-1]
727+
palette = ContinuousPaletteGenerator(*self.attr_color.colors)
728+
nbins = len(self.bins) + 1
729+
self.colors = [palette[i / (nbins - 1)] for i in range(nbins)]
730+
706731
def create_legend(self):
707732
if self.legend is not None:
708733
self.scene.removeItem(self.legend)
709734
self.legend = None
710735
if self.attr_color is None:
711736
return None
712737

713-
names = self.attr_color.values
714-
colors = [QColor(*color) for color in self.attr_color.colors]
738+
if self.attr_color.is_discrete:
739+
names = self.attr_color.values
740+
else:
741+
sval = self.attr_color.repr_val
742+
names = \
743+
[f"< {sval(self.bins[0])}"] \
744+
+ [f"{sval(x)} - {sval(y)}"
745+
for x, y in zip(self.bins, self.bins[1:])] \
746+
+ [f"> {sval(self.bins[-1])}"]
747+
715748
items = []
716749
size = 8
717-
for name, color in zip(names, colors):
750+
for name, color in zip(names, self.colors):
718751
item = QGraphicsItemGroup()
719752
item.addToGroup(
720753
CanvasRectangle(None, -size / 2, -size / 2, size, size,

0 commit comments

Comments
 (0)