Skip to content

Commit 75b0253

Browse files
committed
owlinearprojection: Improve NaN handling
Fix an error when a column contains (all/some) NaN values. Fixes gh-1938
1 parent fa4c7b6 commit 75b0253

File tree

1 file changed

+50
-27
lines changed

1 file changed

+50
-27
lines changed

Orange/widgets/visualize/owlinearprojection.py

Lines changed: 50 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -695,10 +695,12 @@ def __deactivate_selection(self):
695695

696696
self.varmodel_other.extend(variables)
697697

698-
def _get_data(self, var):
699-
"""Return the column data for variable `var`."""
698+
def _get_data(self, var, dtype):
699+
"""
700+
Return the column data and mask for variable `var`
701+
"""
700702
X, _ = self.data.get_column_view(var)
701-
return X.ravel()
703+
return column_data(self.data, var, dtype)
702704

703705
def _setup_plot(self, reset_view=True):
704706
self.__replot_requested = False
@@ -708,7 +710,7 @@ def _setup_plot(self, reset_view=True):
708710
if not variables:
709711
return
710712

711-
coords = [self._get_data(var) for var in variables]
713+
coords = [self._get_data(var, dtype=float)[0] for var in variables]
712714
coords = numpy.vstack(coords)
713715
p, N = coords.shape
714716
assert N == len(self.data), p == len(variables)
@@ -721,8 +723,9 @@ def _setup_plot(self, reset_view=True):
721723
coords = coords[:, mask]
722724

723725
X, Y = numpy.dot(axes, coords)
724-
X = plotutils.normalized(X)
725-
Y = plotutils.normalized(Y)
726+
if X.size and Y.size:
727+
X = plotutils.normalized(X)
728+
Y = plotutils.normalized(Y)
726729

727730
pen_data, brush_data = self._color_data(mask)
728731
size_data = self._size_data(mask)
@@ -773,7 +776,7 @@ def _setup_plot(self, reset_view=True):
773776
def _color_data(self, mask=None):
774777
color_var = self.color_var()
775778
if color_var is not None:
776-
color_data = self._get_data(color_var)
779+
color_data, _ = self._get_data(color_var, dtype=float)
777780
if color_var.is_continuous:
778781
color_data = plotutils.continuous_colors(
779782
color_data, None, *color_var.colors)
@@ -866,14 +869,12 @@ def _shape_data(self, mask):
866869
shape_data = numpy.array(["o"] * len(self.data))
867870
else:
868871
assert shape_var.is_discrete
869-
max_symbol = len(ScatterPlotItem.Symbols) - 1
870-
shape = self._get_data(shape_var)
871-
shape_mask = numpy.isnan(shape)
872-
shape %= max_symbol - 1
873-
shape[shape_mask] = max_symbol
874-
875872
symbols = numpy.array(list(ScatterPlotItem.Symbols))
876-
shape_data = symbols[numpy.asarray(shape, dtype=int)]
873+
max_symbol = symbols.size - 1
874+
shapeidx, shape_mask = column_data(self.data, shape_var, dtype=int)
875+
shapeidx[shape_mask] = max_symbol
876+
shapeidx[~shape_mask] %= max_symbol -1
877+
shape_data = symbols[shapeidx]
877878
if mask is None:
878879
return shape_data
879880
else:
@@ -892,12 +893,20 @@ def _size_data(self, mask=None):
892893
size_data = numpy.full((len(self.data),), self.point_size,
893894
dtype=float)
894895
else:
895-
size_data = plotutils.normalized(self._get_data(size_var))
896-
size_data -= numpy.nanmin(size_data)
897-
size_mask = numpy.isnan(size_data)
896+
nan_size = OWLinearProjection.MinPointSize - 2
897+
size_data, size_mask = self._get_data(size_var, dtype=float)
898+
size_data_valid = size_data[~size_mask]
899+
if size_data_valid.size:
900+
smin, smax = numpy.min(size_data_valid), numpy.max(size_data_valid)
901+
sspan = smax - smin
902+
else:
903+
sspan = smax = smin = 0
904+
size_data[~size_mask] -= smin
905+
if sspan > 0:
906+
size_data[~size_mask] /= sspan
898907
size_data = \
899908
size_data * self.point_size + OWLinearProjection.MinPointSize
900-
size_data[size_mask] = OWLinearProjection.MinPointSize - 2
909+
size_data[size_mask] = nan_size
901910
if mask is None:
902911
return size_data
903912
else:
@@ -1541,8 +1550,25 @@ def gestureEvent(self, event):
15411550
return False
15421551

15431552

1553+
def column_data(table, var, dtype):
1554+
dtype = numpy.dtype(dtype)
1555+
col, copy = table.get_column_view(var)
1556+
if var.is_primitive() and not isinstance(col.dtype.type, numpy.inexact):
1557+
# from mixes metas domain
1558+
col = col.astype(float)
1559+
copy = True
1560+
mask = numpy.isnan(col)
1561+
if dtype != col.dtype:
1562+
col = col.astype(dtype)
1563+
copy = True
1564+
1565+
if not copy:
1566+
col = col.copy()
1567+
return col, mask
1568+
1569+
15441570
class plotutils:
1545-
@ staticmethod
1571+
@staticmethod
15461572
def continuous_colors(data, palette=None,
15471573
low=(220, 220, 220), high=(0,0,0),
15481574
through_black=False):
@@ -1552,14 +1578,7 @@ def continuous_colors(data, palette=None,
15521578
amin, amax = numpy.nanmin(data), numpy.nanmax(data)
15531579
span = amax - amin
15541580
data = (data - amin) / (span or 1)
1555-
1556-
mask = numpy.isnan(data)
1557-
# Unknown values as gray
1558-
# TODO: This should already be a part of palette
1559-
colors = numpy.empty((len(data), 3))
1560-
colors[mask] = (128, 128, 128)
1561-
colors[~mask] = [palette.getRGB(v) for v in data[~mask]]
1562-
return colors
1581+
return palette.getRGB(data)
15631582

15641583
@staticmethod
15651584
def discrete_colors(data, nvalues, palette=None, color_index=None):
@@ -1577,7 +1596,11 @@ def discrete_colors(data, nvalues, palette=None, color_index=None):
15771596

15781597
@staticmethod
15791598
def normalized(a):
1599+
if not a.size:
1600+
return a.copy()
15801601
amin, amax = numpy.nanmin(a), numpy.nanmax(a)
1602+
if numpy.isnan(amin):
1603+
return a.copy()
15811604
span = amax - amin
15821605
mean = numpy.nanmean(a)
15831606
return (a - mean) / (span or 1)

0 commit comments

Comments
 (0)