From 561c5d9d1747776adfc174dcebd52f223c75f050 Mon Sep 17 00:00:00 2001 From: janezd Date: Fri, 7 Oct 2016 11:21:53 +0200 Subject: [PATCH 01/15] itemmodels.py: Add DomainModel --- Orange/widgets/utils/itemmodels.py | 59 +++++++++++++++++++++++++++++- 1 file changed, 57 insertions(+), 2 deletions(-) diff --git a/Orange/widgets/utils/itemmodels.py b/Orange/widgets/utils/itemmodels.py index 985edee4048..77196d1ba2d 100644 --- a/Orange/widgets/utils/itemmodels.py +++ b/Orange/widgets/utils/itemmodels.py @@ -6,9 +6,10 @@ from collections import namedtuple, Sequence, defaultdict from contextlib import contextmanager from functools import reduce, partial, lru_cache +from itertools import chain from xml.sax.saxutils import escape -from PyQt4.QtGui import QItemSelectionModel, QColor +from PyQt4.QtGui import QItemSelectionModel, QColor from PyQt4.QtCore import ( Qt, QAbstractListModel, QAbstractTableModel, QModelIndex, QByteArray ) @@ -20,7 +21,7 @@ import numpy -from Orange.data import Variable, Storage +from Orange.data import Variable, Storage, DiscreteVariable, ContinuousVariable from Orange.widgets import gui from Orange.widgets.utils import datacaching from Orange.statistics import basic_stats @@ -646,6 +647,60 @@ def python_variable_tooltip(self, var): text += self.variable_labels_tooltip(var) return text + +class DomainModel(VariableListModel): + ATTRIBUTES, CLASSES, METAS = 1, 2, 4 + MIXED = ATTRIBUTES | CLASSES | METAS + SEPARATED = (CLASSES, PyListModel.Separator, + METAS, PyListModel.Separator, + ATTRIBUTES) + PRIMITIVE = (DiscreteVariable, ContinuousVariable) + + def __init__(self, order=SEPARATED, valid_types=None, alphabetical=False): + super().__init__() + if isinstance(order, int): + order = [order] + self.order = order + self.valid_types = valid_types + self.alphabetical = alphabetical + self.set_domain(None) + + def set_domain(self, domain): + self.beginResetModel() + content = [] + # The logic related to separators is a bit complicated: it ensures that + # even when a section is empty we don't have two separators in a row + # or a separator at the end + add_separator = False + for section in self.order: + if section is self.Separator: + add_separator = True + continue + if isinstance(section, int): + if domain is None: + continue + to_add = chain( + *(vars for i, vars in enumerate( + (domain.attributes, domain.class_vars, domain.metas)) + if (1 << i) & section)) + if self.valid_types is not None: + to_add = [var for var in to_add + if isinstance(var, self.valid_types)] + if self.alphabetical: + to_add = sorted(to_add) + elif isinstance(section, list): + to_add = section + else: + to_add = [section] + if to_add: + if add_separator: + content.append(self.Separator) + add_separator = False + content += to_add + self[:] = content + self.endResetModel() + + _html_replace = [("<", "<"), (">", ">")] From 53a97b9d5c653d81c0ca3e3f08d2919e7693b000 Mon Sep 17 00:00:00 2001 From: janezd Date: Fri, 7 Oct 2016 11:30:06 +0200 Subject: [PATCH 02/15] gui.py: Add callback and callfront for combo boxes with domain models --- Orange/widgets/gui.py | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/Orange/widgets/gui.py b/Orange/widgets/gui.py index bae78143dba..5289f16a571 100644 --- a/Orange/widgets/gui.py +++ b/Orange/widgets/gui.py @@ -2263,6 +2263,19 @@ def __call__(self, value): return super().__call__(self.control2attributeDict.get(value, value)) +class ValueCallbackComboModel(ValueCallback): + def __init__(self, widget, attribute, model, emptyString=""): + super().__init__(widget, attribute) + self.model = model + self.emptyString = emptyString + + def __call__(self, index): + value = self.model[index] + # Can't use super here since, it doesn't set `None`'s?! + return self.acyclic_setattr( + None if value == self.emptyString else value) + + class ValueCallbackLineEdit(ControlledCallback): def __init__(self, control, widget, attribute, f=None): ControlledCallback.__init__(self, widget, attribute, f) @@ -2462,6 +2475,28 @@ def action(self, value): self.control.setCurrentIndex(value) +class CallFrontComboBoxModel(ControlledCallFront): + def __init__(self, control, model, emptyString=None): + super().__init__(control) + self.model = model + self.emptyString = emptyString + + def action(self, value): + if value is None or value == "": # the latter accomodates PyListModel + if self.emptyString is None: + return + value = self.emptyString + if value in self.model: + self.control.setCurrentIndex(self.model.indexOf(value)) + return + elif isinstance(value, str): + for i, val in enumerate(self.model): + if value == str(val): + self.control.setCurrentIndex(i) + return + raise ValueError("Combo box does not contain item " + repr(value)) + + class CallFrontHSlider(ControlledCallFront): def action(self, value): if value is not None: From 8b3afb25c2fc9e5c5bb0490cc81840bdd8d1047b Mon Sep 17 00:00:00 2001 From: janezd Date: Fri, 7 Oct 2016 11:37:30 +0200 Subject: [PATCH 03/15] gui.py: Remove attribute2controlDict from combo boxes; use emptyString instead --- Orange/widgets/gui.py | 39 +++++++++++---------------------------- 1 file changed, 11 insertions(+), 28 deletions(-) diff --git a/Orange/widgets/gui.py b/Orange/widgets/gui.py index 5289f16a571..e2d63659895 100644 --- a/Orange/widgets/gui.py +++ b/Orange/widgets/gui.py @@ -1526,14 +1526,12 @@ def minimumSizeHint(self): # TODO comboBox looks overly complicated: -# - is the argument control2attributeDict needed? doesn't emptyString do the -# job? # - can valueType be anything else than str? # - sendSelectedValue is not a great name def comboBox(widget, master, value, box=None, label=None, labelWidth=None, orientation=Qt.Vertical, items=(), callback=None, sendSelectedValue=False, valueType=str, - control2attributeDict=None, emptyString=None, editable=False, + emptyString=None, editable=False, contentsLength=None, maximumContentsLength=25, **misc): """ @@ -1543,9 +1541,6 @@ def comboBox(widget, master, value, box=None, label=None, labelWidth=None, selected row (if `sendSelected` is left at default, `False`) or a value converted to `valueType` (`str` by default). - Furthermore, the value is converted by looking up into dictionary - `control2attributeDict`. - :param widget: the widget into which the box is inserted :type widget: PyQt4.QtGui.QWidget or None :param master: master widget @@ -1571,9 +1566,6 @@ def comboBox(widget, master, value, box=None, label=None, labelWidth=None, :param valueType: the type into which the selected value is converted if sentSelectedValue is `False` :type valueType: type - :param control2attributeDict: a dictionary through which the value is - converted - :type control2attributeDict: dict or None :param emptyString: the string value in the combo box that gets stored as an empty string in `value` :type emptyString: str @@ -1624,20 +1616,14 @@ def comboBox(widget, master, value, box=None, label=None, labelWidth=None, cindex = 0 combo.setCurrentIndex(cindex) - if sendSelectedValue: - if control2attributeDict is None: - control2attributeDict = {} - if emptyString: - control2attributeDict[emptyString] = "" connectControl( master, value, callback, combo.activated[str], - CallFrontComboBox(combo, valueType, control2attributeDict), - ValueCallbackCombo(master, value, valueType, - control2attributeDict)) + CallFrontComboBox(combo, valueType, emptyString), + ValueCallbackCombo(master, value, valueType, emptyString)) else: connectControl( master, value, callback, combo.activated[int], - CallFrontComboBox(combo, None, control2attributeDict)) + CallFrontComboBox(combo, None, emptyString)) miscellanea(combo, hb, widget, **misc) combo.emptyString = emptyString return combo @@ -2254,13 +2240,13 @@ def __call__(self, value): class ValueCallbackCombo(ValueCallback): - def __init__(self, widget, attribute, f=None, control2attributeDict=None): + def __init__(self, widget, attribute, f=None, emptyString=""): super().__init__(widget, attribute, f) - self.control2attributeDict = control2attributeDict or {} + self.emptyString = emptyString def __call__(self, value): value = str(value) - return super().__call__(self.control2attributeDict.get(value, value)) + return super().__call__("" if value == self.emptyString else value) class ValueCallbackComboModel(ValueCallback): @@ -2447,18 +2433,15 @@ def action(self, value): class CallFrontComboBox(ControlledCallFront): - def __init__(self, control, valType=None, control2attributeDict=None): + def __init__(self, control, valType=None, emptyString=""): super().__init__(control) self.valType = valType - if control2attributeDict is None: - self.attribute2controlDict = {} - else: - self.attribute2controlDict = \ - {y: x for x, y in control2attributeDict.items()} + self.emptyString = emptyString def action(self, value): if value is not None: - value = self.attribute2controlDict.get(value, value) + if value == "": + value = self.emptyString if self.valType: for i in range(self.control.count()): if self.valType(str(self.control.itemText(i))) == value: From 5da1ab4d9a1998584371c4efec14cb6d017fd2b5 Mon Sep 17 00:00:00 2001 From: janezd Date: Fri, 7 Oct 2016 11:38:24 +0200 Subject: [PATCH 04/15] gui.py: Use DomainModel in comboBox --- Orange/widgets/gui.py | 30 +++++++++++++++++++++++------- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/Orange/widgets/gui.py b/Orange/widgets/gui.py index e2d63659895..4d36637bf6a 100644 --- a/Orange/widgets/gui.py +++ b/Orange/widgets/gui.py @@ -1582,6 +1582,10 @@ def comboBox(widget, master, value, box=None, label=None, labelWidth=None, length (default: 25, use 0 to disable) :rtype: PyQt4.QtGui.QComboBox """ + + # Local import to avoid circular imports + from Orange.widgets.utils.itemmodels import VariableListModel + if box or label: hb = widgetBox(widget, box, orientation, addToLayout=False) if label is not None: @@ -1607,15 +1611,27 @@ def comboBox(widget, master, value, box=None, label=None, labelWidth=None, if value: cindex = getdeepattr(master, value) - if isinstance(cindex, str): - if items and cindex in items: - cindex = items.index(getdeepattr(master, value)) - else: + model = misc.get("model", None) + if isinstance(model, VariableListModel): + callfront = CallFrontComboBoxModel(combo, model, emptyString) + callfront.action(cindex) + else: + if isinstance(cindex, str): + if items and cindex in items: + cindex = items.index(cindex) + else: + cindex = 0 + if cindex > combo.count() - 1: cindex = 0 - if cindex > combo.count() - 1: - cindex = 0 - combo.setCurrentIndex(cindex) + combo.setCurrentIndex(cindex) + if isinstance(model, VariableListModel): + connectControl( + master, value, callback, combo.activated[int], + callfront, + ValueCallbackComboModel(master, value, model, emptyString) + ) + elif sendSelectedValue: connectControl( master, value, callback, combo.activated[str], CallFrontComboBox(combo, valueType, emptyString), From f76c3231a3d93907ff9ae60bfc09f6aca4c85011 Mon Sep 17 00:00:00 2001 From: janezd Date: Fri, 7 Oct 2016 11:40:10 +0200 Subject: [PATCH 05/15] datacaching: Fix data caching --- Orange/widgets/utils/datacaching.py | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/Orange/widgets/utils/datacaching.py b/Orange/widgets/utils/datacaching.py index e0923aeda8f..5259c15caed 100644 --- a/Orange/widgets/utils/datacaching.py +++ b/Orange/widgets/utils/datacaching.py @@ -1,19 +1,17 @@ from collections import defaultdict from operator import itemgetter -def getCached(data, funct, params=(), kwparams=None): +def getCached(data, funct, params=(), **kwparams): + # pylint: disable=protected-access if data is None: return None - info = getattr(data, "__data_cache", None) - if info is not None: - if funct in data.info: - return data.info(funct) - else: - info = data.info = {} + if not hasattr(data, "__data_cache"): + data.__data_cache = {} + info = data.__data_cache + if funct in info: + return info[funct] if isinstance(funct, str): return None - if kwparams is None: - kwparams = {} info[funct] = res = funct(*params, **kwparams) return res @@ -21,10 +19,9 @@ def getCached(data, funct, params=(), kwparams=None): def setCached(data, name, value): if data is None: return - info = getattr(data, "__data_cache", None) - if info is None: - info = data.info = {} - info[name] = value + if not hasattr(data, "__data_cache"): + data.__data_cache = {} + data.__data_cache[name] = value def delCached(data, name): info = data is not None and getattr(data, "__data_cache") From 3929d797041aa5fc59299bf37f8c91bfc945d292 Mon Sep 17 00:00:00 2001 From: janezd Date: Fri, 7 Oct 2016 11:56:16 +0200 Subject: [PATCH 06/15] Move checksum and get_variable_values_sorted from widget.utils.scaling to widget.utils. In the same commit, I removed get_variable_value_indices that is still used in several functions in scaling, but will be removed in a future commit. Please inform me if I should invest some more time in splitting this commit in two. --- Orange/widgets/utils/__init__.py | 22 +++++++++++ Orange/widgets/utils/plot/owtools.py | 2 +- Orange/widgets/utils/scaling.py | 39 +------------------ Orange/widgets/visualize/owmosaic.py | 3 +- .../visualize/owparallelcoordinates.py | 2 +- Orange/widgets/visualize/owparallelgraph.py | 3 +- .../widgets/visualize/owscatterplotgraph.py | 5 +-- 7 files changed, 30 insertions(+), 46 deletions(-) diff --git a/Orange/widgets/utils/__init__.py b/Orange/widgets/utils/__init__.py index 26d6f682778..d3156166dee 100644 --- a/Orange/widgets/utils/__init__.py +++ b/Orange/widgets/utils/__init__.py @@ -37,3 +37,25 @@ def to_html(str): replace("<", "<").replace(">", ">").replace("=\\=", "≠") getHtmlCompatibleString = to_html + + +def checksum(x): + if x is None: + return None + try: + return x.checksum() + except: + return float('nan') + + +def get_variable_values_sorted(variable): + """ + Return a list of sorted values for given attribute, if all its values can be + cast to int's. + """ + if variable.is_continuous: + return [] + try: + return sorted(variable.values, key=int) + except ValueError: + return variable.values diff --git a/Orange/widgets/utils/plot/owtools.py b/Orange/widgets/utils/plot/owtools.py index c7ac9542e62..c5462033f8b 100644 --- a/Orange/widgets/utils/plot/owtools.py +++ b/Orange/widgets/utils/plot/owtools.py @@ -39,7 +39,7 @@ QGraphicsPixmapItem, QGraphicsPathItem, QPainterPath, qRgb, QImage, QPixmap) from PyQt4.QtCore import Qt, QRectF, QPointF, QPropertyAnimation, qVersion from Orange.widgets.utils.colorpalette import ColorPaletteDlg -from Orange.widgets.utils.scaling import get_variable_values_sorted +from Orange.widgets.utils import get_variable_values_sorted from .owcurve import * from .owpalette import OWPalette diff --git a/Orange/widgets/utils/scaling.py b/Orange/widgets/utils/scaling.py index 90e77f94e5a..a378735514a 100644 --- a/Orange/widgets/utils/scaling.py +++ b/Orange/widgets/utils/scaling.py @@ -7,47 +7,10 @@ import Orange from Orange.statistics.basic_stats import DomainBasicStats from Orange.widgets.settings import Setting +from Orange.widgets.utils import checksum from Orange.widgets.utils.datacaching import getCached, setCached -# noinspection PyBroadException -def checksum(x): - if x is None: - return None - try: - return x.checksum() - except: - return float('nan') - - -def get_variable_values_sorted(variable): - """ - Return a list of sorted values for given attribute, if all its values can be - cast to int's. - """ - if variable.is_continuous: - return [] - try: - return sorted(variable.values, key=int) - except ValueError: - return variable.values - - -def get_variable_value_indices(variable, sort_values=True): - """ - Create a dictionary with given variable. Keys are variable values, values - are indices (transformed from string to int); in case all values are - integers, we also sort them. - """ - if variable.is_continuous: - return {} - if sort_values: - values = get_variable_values_sorted(variable) - else: - values = variable.values - return {value: i for i, value in enumerate(values)} - - class ScaleData: jitter_size = Setting(10) jitter_continuous = Setting(False) diff --git a/Orange/widgets/visualize/owmosaic.py b/Orange/widgets/visualize/owmosaic.py index 3dd535b7a32..369a18b827c 100644 --- a/Orange/widgets/visualize/owmosaic.py +++ b/Orange/widgets/visualize/owmosaic.py @@ -17,8 +17,7 @@ from Orange.widgets import gui from Orange.widgets.settings import ( Setting, DomainContextHandler, ContextSetting) -from Orange.widgets.utils import to_html -from Orange.widgets.utils.scaling import get_variable_values_sorted +from Orange.widgets.utils import to_html, get_variable_values_sorted from Orange.widgets.visualize.utils import ( CanvasText, CanvasRectangle, ViewWithPress) from Orange.widgets.widget import OWWidget, Default, Msg diff --git a/Orange/widgets/visualize/owparallelcoordinates.py b/Orange/widgets/visualize/owparallelcoordinates.py index 59b7b62ca4f..657942198a4 100644 --- a/Orange/widgets/visualize/owparallelcoordinates.py +++ b/Orange/widgets/visualize/owparallelcoordinates.py @@ -10,7 +10,7 @@ from Orange.widgets.gui import attributeIconDict from Orange.widgets.settings import DomainContextHandler, Setting, SettingProvider from Orange.widgets.utils.plot import xBottom -from Orange.widgets.utils.scaling import checksum +from Orange.widgets.utils import checksum from Orange.widgets.utils.toolbar import ZoomSelectToolbar, ZOOM, PAN, SPACE, REMOVE_ALL, SEND_SELECTION from Orange.widgets.visualize.owparallelgraph import OWParallelGraph from Orange.widgets.visualize.owviswidget import OWVisWidget diff --git a/Orange/widgets/visualize/owparallelgraph.py b/Orange/widgets/visualize/owparallelgraph.py index cc1b18ce82e..73571464dd1 100644 --- a/Orange/widgets/visualize/owparallelgraph.py +++ b/Orange/widgets/visualize/owparallelgraph.py @@ -15,7 +15,8 @@ from Orange.widgets.utils.colorpalette import ContinuousPaletteGenerator from Orange.widgets.utils.plot import OWPlot, UserAxis, AxisStart, AxisEnd, OWCurve, OWPoint, PolygonCurve, \ xBottom, yLeft, OWPlotItem -from Orange.widgets.utils.scaling import get_variable_values_sorted, ScaleData +from Orange.widgets.utils.scaling import ScaleData +from Orange.widgets.utils import get_variable_values_sorted from Orange.widgets.visualize.utils.lac import lac, create_contingencies NO_STATISTICS = 0 diff --git a/Orange/widgets/visualize/owscatterplotgraph.py b/Orange/widgets/visualize/owscatterplotgraph.py index 2a57dbe00e7..e166b8b4f9d 100644 --- a/Orange/widgets/visualize/owscatterplotgraph.py +++ b/Orange/widgets/visualize/owscatterplotgraph.py @@ -17,13 +17,12 @@ from PyQt4.QtGui import QStaticText, QPainterPath, QTransform, QPinchGesture, QPainter from Orange.widgets import gui -from Orange.widgets.utils import classdensity +from Orange.widgets.utils import classdensity, get_variable_values_sorted from Orange.widgets.utils.colorpalette import (ColorPaletteGenerator, ContinuousPaletteGenerator) from Orange.widgets.utils.plot import \ OWPalette, OWPlotGUI, SELECT, PANNING, ZOOMING -from Orange.widgets.utils.scaling import (get_variable_values_sorted, - ScaleScatterPlotData) +from Orange.widgets.utils.scaling import ScaleScatterPlotData from Orange.widgets.settings import Setting, ContextSetting From a0ba1f4696a51c3fc67bd7e5dc0428cf4e2538f1 Mon Sep 17 00:00:00 2001 From: janezd Date: Fri, 7 Oct 2016 12:16:58 +0200 Subject: [PATCH 07/15] Use DomainModel in scatter plot + related changes in scaling and parallel coordinates --- Orange/widgets/utils/scaling.py | 316 ++++++------------ Orange/widgets/visualize/owparallelgraph.py | 81 ++--- Orange/widgets/visualize/owscatterplot.py | 140 +++----- .../widgets/visualize/owscatterplotgraph.py | 131 ++++---- .../visualize/tests/test_owscatterplot.py | 17 +- 5 files changed, 261 insertions(+), 424 deletions(-) diff --git a/Orange/widgets/utils/scaling.py b/Orange/widgets/utils/scaling.py index a378735514a..ceff8725975 100644 --- a/Orange/widgets/utils/scaling.py +++ b/Orange/widgets/utils/scaling.py @@ -15,222 +15,117 @@ class ScaleData: jitter_size = Setting(10) jitter_continuous = Setting(False) - def __init__(self): - self.raw_data = None # input data - self.attribute_names = [] # list of attribute names from self.raw_data - self.attribute_flip_info = {} # dictionary with attrName: 0/1 attribute is flipped or not - - self.data_has_class = False - self.data_has_continuous_class = False - self.data_has_discrete_class = False - self.data_class_name = None - self.data_domain = None - self.data_class_index = None - self.have_data = False - - self.jitter_seed = 0 - + def _reset_data(self): + self.domain = None + self.data = None + self.original_data = None # as numpy array + self.scaled_data = None # in [0, 1] + self.jittered_data = None self.attr_values = {} self.domain_data_stat = [] - self.original_data = None # input (nonscaled) data in a numpy array - self.scaled_data = None # scaled data to the interval 0-1 - self.no_jittering_scaled_data = None - self.valid_data_array = None - - def rescale_data(self): - """ - Force the existing data to be rescaled due to changes like - jitter_continuous, jitter_size, ... - """ - self.set_data(self.raw_data, skipIfSame=0) - - def set_data(self, data, **args): - if args.get("skipIfSame", 1): - if checksum(data) == checksum(self.raw_data): - return - - self.domain_data_stat = [] - self.attr_values = {} - self.original_data = None - self.scaled_data = None - self.no_jittering_scaled_data = None self.valid_data_array = None + self.attribute_flip_info = {} # dictionary with attr: 0/1 if flipped + self.jitter_seed = 0 - self.raw_data = None - self.have_data = False - self.data_has_class = False - self.data_has_continuous_class = False - self.data_has_discrete_class = False - self.data_class_name = None - self.data_domain = None - self.data_class_index = None - - if data is None: - return - full_data = data - self.raw_data = data - - len_data = data and len(data) or 0 - - self.attribute_names = [attr.name for attr in full_data.domain] - self.attribute_flip_info = {} - - self.data_domain = full_data.domain - self.data_has_class = bool(full_data.domain.class_var) - self.data_has_continuous_class = full_data.domain.has_continuous_class - self.data_has_discrete_class = full_data.domain.has_discrete_class - - self.data_class_name = self.data_has_class and full_data.domain.class_var.name - if self.data_has_class: - self.data_class_index = self.data_domain.index(self.data_class_name) - self.have_data = bool(self.raw_data and len(self.raw_data) > 0) - - self.domain_data_stat = getCached(full_data, - DomainBasicStats, - (full_data,)) + def __init__(self): + self._reset_data() - sort_values_for_discrete_attrs = args.get("sort_values_for_discrete_attrs", - 1) + def rescale_data(self): + self._compute_jittered_data() - for index in range(len(full_data.domain)): - attr = full_data.domain[index] + def _compute_domain_data_stat(self): + stt = self.domain_data_stat = \ + getCached(self.data, DomainBasicStats, (self.data,)) + for index in range(len(self.domain)): + attr = self.domain[index] if attr.is_discrete: - self.attr_values[attr.name] = [0, len(attr.values)] + self.attr_values[attr] = [0, len(attr.values)] elif attr.is_continuous: - self.attr_values[attr.name] = [self.domain_data_stat[index].min, - self.domain_data_stat[index].max] - - if 'no_data' in args: + self.attr_values[attr] = [stt[index].min, stt[index].max] + + def _compute_scaled_data(self): + data = self.data + # We cache scaled_data and validArray to share them between widgets + cached = getCached(data, "visualizationData") + if cached: + self.original_data, self.scaled_data, self.valid_data_array = cached return - # the original_data, no_jittering_scaled_data and validArray are arrays - # that we can cache so that other visualization widgets don't need to - # compute it. The scaled_data on the other hand has to be computed for - # each widget separately because of different - # jitter_continuous and jitter_size values - if getCached(data, "visualizationData"): - self.original_data, self.no_jittering_scaled_data, self.valid_data_array = getCached(data, - "visualizationData") - else: - no_jittering_data = np.c_[full_data.X, full_data.Y].T - valid_data_array = ~np.isnan(no_jittering_data) - original_data = no_jittering_data.copy() - - for index in range(len(data.domain)): - attr = data.domain[index] - if attr.is_discrete: - # see if the values for discrete attributes have to be resorted - variable_value_indices = get_variable_value_indices(data.domain[index], - sort_values_for_discrete_attrs) - if 0 in [i == variable_value_indices[attr.values[i]] - for i in range(len(attr.values))]: - # make the array a contiguous, otherwise the putmask - # function does not work - line = no_jittering_data[index].copy() - indices = [np.where(line == val, 1, 0) - for val in range(len(attr.values))] - for i in range(len(attr.values)): - np.putmask(line, indices[i], - variable_value_indices[attr.values[i]]) - no_jittering_data[index] = line # save the changed array - original_data[index] = line # reorder also the values in the original data - no_jittering_data[index] = ((no_jittering_data[index] * 2.0 + 1.0) - / float(2 * len(attr.values))) - - elif attr.is_continuous: - diff = self.domain_data_stat[index].max - self.domain_data_stat[ - index].min or 1 # if all values are the same then prevent division by zero - no_jittering_data[index] = (no_jittering_data[index] - - self.domain_data_stat[index].min) / diff - - self.original_data = original_data - self.no_jittering_scaled_data = no_jittering_data - self.valid_data_array = valid_data_array - - if data: - setCached(data, "visualizationData", - (self.original_data, self.no_jittering_scaled_data, - self.valid_data_array)) - - # compute the scaled_data arrays - scaled_data = self.no_jittering_scaled_data - - # Random generators for jittering + Y = data.Y if data.Y.ndim == 2 else np.atleast_2d(data.Y).T + self.original_data = np.hstack((data.X, Y)).T + self.scaled_data = no_jit = self.original_data.copy() + self.valid_data_array = ~np.isnan(no_jit) + for index in range(len(data.domain)): + attr = data.domain[index] + if attr.is_discrete: + no_jit[index] *= 2 + no_jit[index] += 1 + no_jit[index] /= 2 * len(attr.values) + else: + dstat = self.domain_data_stat[index] + no_jit[index] -= dstat.min + if dstat.max != dstat.min: + no_jit[index] /= dstat.max - dstat.min + setCached(data, "visualizationData", + (self.original_data, self.scaled_data, self.valid_data_array)) + + def _compute_jittered_data(self): + data = self.data + self.jittered_data = self.scaled_data.copy() random = np.random.RandomState(seed=self.jitter_seed) - rand_seeds = random.random_integers(0, 2 ** 30 - 1, - size=len(data.domain)) - for index, rseed in zip(list(range(len(data.domain))), rand_seeds): + for index, col in enumerate(self.jittered_data): # Need to use a different seed for each feature - random = np.random.RandomState(seed=rseed) attr = data.domain[index] if attr.is_discrete: - scaled_data[index] += (self.jitter_size / (50.0 * max(1, len(attr.values)))) * \ - (random.rand(len(full_data)) - 0.5) - + off = self.jitter_size / (25 * max(1, len(attr.values))) elif attr.is_continuous and self.jitter_continuous: - scaled_data[index] += self.jitter_size / 50.0 * (0.5 - random.rand(len(full_data))) - scaled_data[index] = np.absolute(scaled_data[index]) # fix values below zero - ind = np.where(scaled_data[index] > 1.0, 1, 0) # fix values above 1 - np.putmask(scaled_data[index], ind, 2.0 - np.compress(ind, scaled_data[index])) + off = self.jitter_size / 25 + else: + continue + col += random.uniform(-off, off, len(data)) + # fix values outside [0, 1] + col = np.absolute(col) + + above_1 = col > 1 + col[above_1] = 2 - col[above_1] + + # noinspection PyAttributeOutsideInit + def set_data(self, data, skip_if_same=False, no_data=False): + if skip_if_same and checksum(data) == checksum(self.data): + return + self._reset_data() + if data is None: + return - self.scaled_data = scaled_data[:, :len_data] + self.domain = data.domain + self.data = data + self.attribute_flip_info = {} + if not no_data: + self._compute_domain_data_stat() + self._compute_scaled_data() + self._compute_jittered_data() - def scale_example_value(self, instance, index): - """ - Scale instance's value at index index to a range between 0 and 1 with - respect to self.raw_data. - """ - if instance[index].isSpecial(): - print("Warning: scaling instance with missing value") - return 0.5 - if instance.domain[index].is_discrete: - d = get_variable_value_indices(instance.domain[index]) - return (d[instance[index].value] * 2 + 1) / float(2 * len(d)) - elif instance.domain[index].is_continuous: - diff = self.domain_data_stat[index].max - self.domain_data_stat[index].min - if diff == 0: - diff = 1 # if all values are the same then prevent division by zero - return (instance[index] - self.domain_data_stat[index].min) / diff - - def get_attribute_label(self, attr_name): - if (self.attribute_flip_info.get(attr_name, 0) and - self.data_domain[attr_name].is_continuous): - return "-" + attr_name - return attr_name - - def flip_attribute(self, attr_name): - if attr_name not in self.attribute_names: - return 0 - if self.data_domain[attr_name].is_discrete: + def flip_attribute(self, attr): + if attr.is_discrete: return 0 + index = self.domain.index(attr) + self.attribute_flip_info[attr] = 1 - self.attribute_flip_info.get(attr, 0) + if attr.is_continuous: + self.attr_values[attr] = [-self.attr_values[attr][1], + -self.attr_values[attr][0]] - index = self.data_domain.index(attr_name) - self.attribute_flip_info[attr_name] = 1 - self.attribute_flip_info.get(attr_name, 0) - if self.data_domain[attr_name].is_continuous: - self.attr_values[attr_name] = [-self.attr_values[attr_name][1], -self.attr_values[attr_name][0]] - + self.jittered_data[index] = 1 - self.jittered_data[index] self.scaled_data[index] = 1 - self.scaled_data[index] - self.no_jittering_scaled_data[index] = 1 - self.no_jittering_scaled_data[index] return 1 - def get_min_max_val(self, attr): - if type(attr) == int: - attr = self.attribute_names[attr] - diff = self.attr_values[attr][1] - self.attr_values[attr][0] - return diff or 1.0 - - def get_valid_list(self, indices, also_class_if_exists=1): + def get_valid_list(self, indices): """ - Get array of 0 and 1 of len = len(self.raw_data). If there is a missing + Get array of 0 and 1 of len = len(self.data). If there is a missing value at any attribute in indices return 0 for that instance. """ if self.valid_data_array is None or len(self.valid_data_array) == 0: return np.array([], np.bool) - - inds = indices[:] - if also_class_if_exists and self.data_has_class: - inds.append(self.data_class_index) - return np.all(self.valid_data_array[inds], axis=0) + return np.all(self.valid_data_array[indices], axis=0) def get_valid_indices(self, indices): """ @@ -240,55 +135,36 @@ def get_valid_indices(self, indices): valid_list = self.get_valid_list(indices) return np.nonzero(valid_list)[0] - def rnd_correction(self, max): - """ - Return a number from -max to max. - """ - return (random.random() - 0.5) * 2 * max - class ScaleScatterPlotData(ScaleData): - def get_original_data(self, indices): - data = self.original_data.take(indices, axis = 0) - for i, ind in enumerate(indices): - [minVal, maxVal] = self.attr_values[self.data_domain[ind].name] - if self.data_domain[ind].is_discrete: - data[i] += (self.jitter_size/50.0)*(np.random.random(len(self.raw_data)) - 0.5) - elif self.data_domain[ind].is_continuous and self.jitter_continuous: - data[i] += (self.jitter_size/(50.0*(maxVal-minVal or 1)))*(np.random.random(len(self.raw_data)) - 0.5) - return data - - getOriginalData = get_original_data - - # @deprecated_keywords({"xAttr": "xattr", "yAttr": "yattr"}) def get_xy_data_positions(self, xattr, yattr, filter_valid=False, copy=True): """ Create x-y projection of attributes in attrlist. """ - xattr_index = self.data_domain.index(xattr) - yattr_index = self.data_domain.index(yattr) + xattr_index = self.domain.index(xattr) + yattr_index = self.domain.index(yattr) if filter_valid is True: filter_valid = self.get_valid_list([xattr_index, yattr_index]) if isinstance(filter_valid, np.ndarray): - xdata = self.scaled_data[xattr_index, filter_valid] - ydata = self.scaled_data[yattr_index, filter_valid] + xdata = self.jittered_data[xattr_index, filter_valid] + ydata = self.jittered_data[yattr_index, filter_valid] elif copy: - xdata = self.scaled_data[xattr_index].copy() - ydata = self.scaled_data[yattr_index].copy() + xdata = self.jittered_data[xattr_index].copy() + ydata = self.jittered_data[yattr_index].copy() else: - xdata = self.scaled_data[xattr_index] - ydata = self.scaled_data[yattr_index] + xdata = self.jittered_data[xattr_index] + ydata = self.jittered_data[yattr_index] - if self.data_domain[xattr_index].is_discrete: - xdata *= len(self.data_domain[xattr_index].values) + if self.domain[xattr_index].is_discrete: + xdata *= len(self.domain[xattr_index].values) xdata -= 0.5 else: xdata *= self.attr_values[xattr][1] - self.attr_values[xattr][0] xdata += float(self.attr_values[xattr][0]) - if self.data_domain[yattr_index].is_discrete: - ydata *= len(self.data_domain[yattr_index].values) + if self.domain[yattr_index].is_discrete: + ydata *= len(self.domain[yattr_index].values) ydata -= 0.5 else: ydata *= self.attr_values[yattr][1] - self.attr_values[yattr][0] diff --git a/Orange/widgets/visualize/owparallelgraph.py b/Orange/widgets/visualize/owparallelgraph.py index 73571464dd1..d322b60b52f 100644 --- a/Orange/widgets/visualize/owparallelgraph.py +++ b/Orange/widgets/visualize/owparallelgraph.py @@ -84,7 +84,7 @@ def update_data(self, attributes, mid_labels=None): self.clear() - if not (self.have_data): + if self.data is None: return if len(attributes) < 2: return @@ -97,14 +97,14 @@ def update_data(self, attributes, mid_labels=None): self.alpha_value_2 = TRANSPARENT self.attributes = attributes - self.attribute_indices = [self.data_domain.index(name) + self.attribute_indices = [self.domain.index(name) for name in self.attributes] self.valid_data = self.get_valid_list(self.attribute_indices) self.visualized_mid_labels = mid_labels self.add_relevant_selections(old_selection_conditions) - class_var = self.data_domain.class_var + class_var = self.domain.class_var if not class_var: self.colors = None elif class_var.is_discrete: @@ -143,14 +143,15 @@ def draw_axes(self): a.title_margin = -10 a.text_margin = 0 a.setZValue(5) - self.set_axis_title(axis_id, self.data_domain[self.attributes[i]].name) + self.set_axis_title(axis_id, self.domain[self.attributes[i]].name) self.set_show_axis_title(axis_id, self.show_attr_values) if self.show_attr_values: - attr = self.data_domain[self.attributes[i]] + attr = self.domain[self.attributes[i]] if attr.is_continuous: - self.set_axis_scale(axis_id, self.attr_values[attr.name][0], self.attr_values[attr.name][1]) + self.set_axis_scale(axis_id, self.attr_values[attr][0], + self.attr_values[attr][1]) elif attr.is_discrete: - attribute_values = get_variable_values_sorted(self.data_domain[self.attributes[i]]) + attribute_values = get_variable_values_sorted(self.domain[self.attributes[i]]) attr_len = len(attribute_values) values = [float(1.0 + 2.0 * j) / float(2 * attr_len) for j in range(len(attribute_values))] a.set_bounds((0, 1)) @@ -168,7 +169,7 @@ def is_selected(example): diff, mins = [], [] for i in self.attribute_indices: - var = self.data_domain[i] + var = self.domain[i] if var.is_discrete: diff.append(len(var.values)) mins.append(-0.5) @@ -225,7 +226,7 @@ def draw_groups(self): diff, mins = [], [] for i in self.attribute_indices: - var = self.data_domain[i] + var = self.domain[i] if var.is_discrete: diff.append(len(var.values)) mins.append(-0.5) @@ -262,19 +263,21 @@ def callback(i, n): return self.groups[key] def draw_legend(self): - if self.data_has_class: - if self.data_domain.has_discrete_class: + domain = self.data.domain + class_var = domain.class_var + if class_var: + if class_var.is_discrete: self.legend().clear() - values = get_variable_values_sorted(self.data_domain.class_var) + values = get_variable_values_sorted(class_var) for i, value in enumerate(values): self.legend().add_item( - self.data_domain.class_var.name, value, + class_var.name, value, OWPoint(OWPoint.Rect, QColor(*self.colors[i]), 10)) else: - values = self.attr_values[self.data_domain.class_var.name] - decimals = self.data_domain.class_var.number_of_decimals - self.legend().add_color_gradient(self.data_domain.class_var.name, - ["%%.%df" % decimals % v for v in values]) + values = self.attr_values[class_var] + decimals = class_var.number_of_decimals + self.legend().add_color_gradient( + class_var.name, ["%%.%df" % decimals % v for v in values]) else: self.legend().clear() self.old_legend_keys = [] @@ -287,14 +290,15 @@ def draw_mid_labels(self, mid_labels): def draw_statistics(self): """Draw lines that represent standard deviation or quartiles""" return # TODO: Implement using BasicStats - if self.show_statistics and self.have_data: + if self.show_statistics and self.data is not None: data = [] + domain = self.data.domain for attr_idx in self.attribute_indices: - if not self.data_domain[attr_idx].is_continuous: + if not self.domain[attr_idx].is_continuous: data.append([()]) continue # only for continuous attributes - if not self.data_has_class or self.data_has_continuous_class: # no class + if not domain.class_var or domain.has_continuous_class: if self.show_statistics == MEANS: m = self.domain_data_stat[attr_idx].mean dev = self.domain_data_stat[attr_idx].var @@ -311,10 +315,11 @@ def draw_statistics(self): data.append([(0, 0, 0)]) else: curr = [] - class_values = get_variable_values_sorted(self.data_domain.class_var) + class_values = get_variable_values_sorted(self.domain.class_var) + class_index = self.domain.index(self.domain.class_var) for c in range(len(class_values)): - attr_values = self.data[attr_idx, self.data[self.data_class_index] == c] + attr_values = self.data[attr_idx, self.data[class_index] == c] attr_values = attr_values[~np.isnan(attr_values)] if len(attr_values) == 0: @@ -349,10 +354,10 @@ def draw_statistics(self): yData=[data[i][c][2], data[i][c][2]], lineWidth=4) # draw lines with mean/median values - if not self.data_has_class or self.data_has_continuous_class: + if not domain.class_var or domain.has_continuous_class: class_count = 1 else: - class_count = len(self.data_domain.class_var.values) + class_count = len(self.domain.class_var.values) for c in range(class_count): diff = - 0.03 * (class_count - 1) / 2.0 + c * 0.03 ys = [] @@ -376,23 +381,23 @@ def draw_statistics(self): def draw_distributions(self): """Draw distributions with discrete attributes""" - if not (self.show_distributions and self.have_data and self.data_has_discrete_class): + if not (self.show_distributions and self.data is not None and self.domain.has_discrete_class): return - class_count = len(self.data_domain.class_var.values) - class_ = self.data_domain.class_var + class_count = len(self.domain.class_var.values) + class_ = self.domain.class_var # we create a hash table of possible class values (happens only if we have a discrete class) if self.domain_contingencies is None: self.domain_contingencies = dict( - zip([attr for attr in self.data_domain if attr.is_discrete], - get_contingencies(self.raw_data, skipContinuous=True))) - self.domain_contingencies[class_] = get_contingency(self.raw_data, class_, class_) + zip([attr for attr in self.domain if attr.is_discrete], + get_contingencies(self.data, skipContinuous=True))) + self.domain_contingencies[class_] = get_contingency(self.data, class_, class_) max_count = max([contingency.max() for contingency in self.domain_contingencies.values()] or [1]) - sorted_class_values = get_variable_values_sorted(self.data_domain.class_var) + sorted_class_values = get_variable_values_sorted(self.domain.class_var) for axis_idx, attr_idx in enumerate(self.attribute_indices): - attr = self.data_domain[attr_idx] + attr = self.domain[attr_idx] if attr.is_discrete: continue @@ -438,11 +443,11 @@ def event(self, ev): contact, (index, pos) = self.testArrowContact(int(round(x_float)), canvas_position.x(), canvas_position.y()) if contact: - attr = self.data_domain[self.attributes[index]] + attr = self.domain[self.attributes[index]] if attr.is_continuous: condition = self.selection_conditions.get(attr.name, [0, 1]) - val = self.attr_values[attr.name][0] + condition[pos] * ( - self.attr_values[attr.name][1] - self.attr_values[attr.name][0]) + val = self.attr_values[attr][0] + condition[pos] * ( + self.attr_values[attr][1] - self.attr_values[attr][0]) str_val = attr.name + "= %%.%df" % attr.number_of_decimals % val QToolTip.showText(ev.globalPos(), str_val) else: @@ -504,15 +509,15 @@ def mouseMoveEvent(self, e): canvas_position = self.mapToScene(e.pos()) y = min(1, max(0, self.inv_transform(yLeft, canvas_position.y()))) index, pos = self.pressed_arrow - attr = self.data_domain[self.attributes[index]] + attr = self.domain[self.attributes[index]] old_condition = self.selection_conditions.get(attr.name, [0, 1]) old_condition[pos] = y self.selection_conditions[attr.name] = old_condition self.update_data(self.attributes, self.visualized_mid_labels) if attr.is_continuous: - val = self.attr_values[attr.name][0] + old_condition[pos] * ( - self.attr_values[attr.name][1] - self.attr_values[attr.name][0]) + val = self.attr_values[attr][0] + old_condition[pos] * ( + self.attr_values[attr][1] - self.attr_values[attr][0]) strVal = attr.name + "= %.2f" % val QToolTip.showText(e.globalPos(), strVal) if self.sendSelectionOnUpdate and self.auto_send_selection_callback: diff --git a/Orange/widgets/visualize/owscatterplot.py b/Orange/widgets/visualize/owscatterplot.py index f677d3517c7..0a9ad191b68 100644 --- a/Orange/widgets/visualize/owscatterplot.py +++ b/Orange/widgets/visualize/owscatterplot.py @@ -14,6 +14,7 @@ from Orange.widgets import gui from Orange.widgets.settings import \ DomainContextHandler, Setting, ContextSetting, SettingProvider +from Orange.widgets.utils.itemmodels import DomainModel from Orange.widgets.visualize.owscatterplotgraph import OWScatterPlotGraph from Orange.widgets.visualize.utils import VizRankDialogAttrPair from Orange.widgets.widget import OWWidget, Default, AttributeList, Msg @@ -57,9 +58,9 @@ def iterate_states(self, initial_state): def compute_score(self, state): graph = self.master.graph - ind12 = [graph.data_domain.index(self.attrs[x]) for x in state] + ind12 = [graph.domain.index(self.attrs[x]) for x in state] valid = graph.get_valid_list(ind12) - X = graph.scaled_data[ind12, :][:, valid].T + X = graph.jittered_data[ind12, :][:, valid].T Y = self.master.data.Y[valid] if X.shape[0] < self.K: return @@ -73,7 +74,7 @@ def compute_score(self, state): (len(Y) / len(self.master.data)) def score_heuristic(self): - X = self.master.graph.scaled_data.T + X = self.master.graph.jittered_data.T Y = self.master.data.Y mdomain = self.master.data.domain dom = Domain([ContinuousVariable(str(i)) for i in range(X.shape[1])], @@ -111,8 +112,8 @@ class OWScatterPlot(OWWidget): auto_sample = Setting(True) toolbar_selection = Setting(0) - attr_x = ContextSetting("") - attr_y = ContextSetting("") + attr_x = ContextSetting(None) + attr_y = ContextSetting(None) graph = SettingProvider(OWScatterPlotGraph) @@ -150,12 +151,14 @@ def __init__(self): labelWidth=50, orientation=Qt.Horizontal, sendSelectedValue=True, valueType=str) box = gui.vBox(self.controlArea, "Axis Data") - self.cb_attr_x = gui.comboBox(box, self, "attr_x", label="Axis x:", - callback=self.update_attr, - **common_options) - self.cb_attr_y = gui.comboBox(box, self, "attr_y", label="Axis y:", - callback=self.update_attr, - **common_options) + dmod = DomainModel + self.xy_model = DomainModel(dmod.MIXED, valid_types=dmod.PRIMITIVE) + gui.comboBox( + box, self, "attr_x", label="Axis x:", callback=self.update_attr, + model=self.xy_model, **common_options) + self.cb_attr_y = gui.comboBox( + box, self, "attr_y", label="Axis y:", callback=self.update_attr, + model=self.xy_model, **common_options) vizrank_box = gui.hBox(box) gui.separator(vizrank_box, width=common_options["labelWidth"]) @@ -179,22 +182,36 @@ def __init__(self): self.sampling.setVisible(False) box = gui.vBox(self.controlArea, "Points") + color_model = DomainModel( + order=("(Same color)", dmod.Separator) + dmod.SEPARATED, + valid_types=dmod.PRIMITIVE) self.cb_attr_color = gui.comboBox( box, self, "graph.attr_color", label="Color:", emptyString="(Same color)", callback=self.update_colors, - **common_options) + model=color_model, **common_options) + label_model = DomainModel( + order=("(No labels)", dmod.Separator) + dmod.SEPARATED, + valid_types=dmod.PRIMITIVE) self.cb_attr_label = gui.comboBox( box, self, "graph.attr_label", label="Label:", emptyString="(No labels)", callback=self.graph.update_labels, - **common_options) + model=label_model, **common_options) + shape_model = DomainModel( + order=("(Same shape)", dmod.Separator) + dmod.SEPARATED, + valid_types=DiscreteVariable) self.cb_attr_shape = gui.comboBox( box, self, "graph.attr_shape", label="Shape:", emptyString="(Same shape)", callback=self.graph.update_shapes, - **common_options) + model=shape_model, **common_options) + size_model = DomainModel( + order=("(Same size)", dmod.Separator) + dmod.SEPARATED, + valid_types=ContinuousVariable) self.cb_attr_size = gui.comboBox( box, self, "graph.attr_size", label="Size:", emptyString="(Same size)", callback=self.graph.update_sizes, - **common_options) + model=size_model, **common_options) + self.models = [self.xy_model, color_model, label_model, + shape_model, size_model] g = self.graph.gui g.point_properties_box(self.controlArea, box) @@ -358,10 +375,10 @@ def set_subset_data(self, subset_data): def handleNewSignals(self): self.graph.new_data(self.data_metas_X, self.subset_data) if self.attribute_selection_list and \ - all(attr in self.graph.data_domain + all(attr in self.graph.domain for attr in self.attribute_selection_list): - self.attr_x = self.attribute_selection_list[0].name - self.attr_y = self.attribute_selection_list[1].name + self.attr_x = self.attribute_selection_list[0] + self.attr_y = self.attribute_selection_list[1] self.attribute_selection_list = None self.update_graph() self.cb_class_density.setEnabled(self.graph.can_draw_density()) @@ -377,64 +394,19 @@ def get_shown_attributes(self): return self.attr_x, self.attr_y def init_attr_values(self): - self.cb_attr_x.clear() - self.attr_x = None - self.cb_attr_y.clear() - self.attr_y = None - self.cb_attr_color.clear() - self.cb_attr_color.addItem("(Same color)") - self.graph.attr_color = None - self.cb_attr_label.clear() - self.cb_attr_label.addItem("(No labels)") - self.graph.attr_label = None - self.cb_attr_shape.clear() - self.cb_attr_shape.addItem("(Same shape)") + domain = self.data and self.data.domain + for model in self.models: + model.set_domain(domain) + self.attr_x = self.xy_model[0] if self.xy_model else None + self.attr_y = self.xy_model[1] if len(self.xy_model) >= 2 \ + else self.attr_x + self.graph.attr_color = domain and self.data.domain.class_var or None self.graph.attr_shape = None - self.cb_attr_size.clear() - self.cb_attr_size.addItem("(Same size)") self.graph.attr_size = None - if not self.data: - return - - for var in self.data.domain.metas: - if not var.is_primitive(): - self.cb_attr_label.addItem(self.icons[var], var.name) - for attr in self.data.domain.variables: - self.cb_attr_x.addItem(self.icons[attr], attr.name) - self.cb_attr_y.addItem(self.icons[attr], attr.name) - self.cb_attr_color.addItem(self.icons[attr], attr.name) - if attr.is_discrete: - self.cb_attr_shape.addItem(self.icons[attr], attr.name) - else: - self.cb_attr_size.addItem(self.icons[attr], attr.name) - self.cb_attr_label.addItem(self.icons[attr], attr.name) - for var in self.data.domain.metas: - if var.is_primitive(): - self.cb_attr_x.addItem(self.icons[var], var.name) - self.cb_attr_y.addItem(self.icons[var], var.name) - self.cb_attr_color.addItem(self.icons[var], var.name) - if var.is_discrete: - self.cb_attr_shape.addItem(self.icons[var], var.name) - else: - self.cb_attr_size.addItem(self.icons[var], var.name) - self.cb_attr_label.addItem(self.icons[var], var.name) - - self.attr_x = self.cb_attr_x.itemText(0) - if self.cb_attr_y.count() > 1: - self.attr_y = self.cb_attr_y.itemText(1) - else: - self.attr_y = self.cb_attr_y.itemText(0) - - if self.data.domain.class_var: - self.graph.attr_color = self.data.domain.class_var.name - else: - self.graph.attr_color = "" - self.graph.attr_shape = "" - self.graph.attr_size = "" - self.graph.attr_label = "" + self.graph.attr_label = None def set_attr(self, attr_x, attr_y): - self.attr_x, self.attr_y = attr_x.name, attr_y.name + self.attr_x, self.attr_y = attr_x, attr_y self.update_attr() def update_attr(self): @@ -451,7 +423,7 @@ def update_density(self): def update_graph(self, reset_view=True, **_): self.graph.zoomStack = [] - if not self.graph.have_data: + if self.graph.data is None: return self.graph.update_data(self.attr_x, self.attr_y, reset_view) @@ -493,21 +465,19 @@ def commit(self): def get_widget_name_extension(self): if self.data is not None: - return "{} vs {}".format(self.combo_value(self.cb_attr_x), - self.combo_value(self.cb_attr_y)) + return "{} vs {}".format(self.attr_x.name, self.attr_y.name) def send_report(self): - disc_attr = False - if self.data: - domain = self.data.domain - disc_attr = domain[self.attr_x].is_discrete or \ - domain[self.attr_y].is_discrete + def name(var): + return var and var.name caption = report.render_items_vert(( - ("Color", self.combo_value(self.cb_attr_color)), - ("Label", self.combo_value(self.cb_attr_label)), - ("Shape", self.combo_value(self.cb_attr_shape)), - ("Size", self.combo_value(self.cb_attr_size)), - ("Jittering", (self.graph.jitter_continuous or disc_attr) and + ("Color", name(self.graph.attr_color)), + ("Label", name(self.graph.attr_label)), + ("Shape", name(self.graph.attr_shape)), + ("Size", name(self.graph.attr_size)), + ("Jittering", (self.attr_x.is_discrete or + self.attr_y.is_discrete or + self.graph.jitter_continuous) and self.graph.jitter_size))) self.report_plot() if caption: diff --git a/Orange/widgets/visualize/owscatterplotgraph.py b/Orange/widgets/visualize/owscatterplotgraph.py index e166b8b4f9d..66ed4c2ab79 100644 --- a/Orange/widgets/visualize/owscatterplotgraph.py +++ b/Orange/widgets/visualize/owscatterplotgraph.py @@ -452,11 +452,15 @@ def _define_symbols(): class OWScatterPlotGraph(gui.OWComponent, ScaleScatterPlotData): - attr_color = ContextSetting("", ContextSetting.OPTIONAL, exclude_metas=False) - attr_label = ContextSetting("", ContextSetting.OPTIONAL, exclude_metas=False) + attr_color = ContextSetting( + None, ContextSetting.OPTIONAL, exclude_metas=False) + attr_label = ContextSetting( + None, ContextSetting.OPTIONAL, exclude_metas=False) + attr_shape = ContextSetting( + None, ContextSetting.OPTIONAL, exclude_metas=False) + attr_size = ContextSetting( + None, ContextSetting.OPTIONAL, exclude_metas=False) label_only_selected = Setting(False) - attr_shape = ContextSetting("", ContextSetting.OPTIONAL, exclude_metas=False) - attr_size = ContextSetting("", ContextSetting.OPTIONAL, exclude_metas=False) point_width = Setting(10) alpha_value = Setting(128) @@ -503,8 +507,7 @@ def __init__(self, scatter_widget, parent=None, _="None"): "missing_shape", "Points with undefined '{}' are shown as crossed circles") self.shown_attribute_indices = [] - self.shown_x = "" - self.shown_y = "" + self.shown_x = self.shown_y = None self.pen_colors = self.brush_colors = None self.valid_data = None # np.ndarray @@ -572,22 +575,21 @@ def update_data(self, attr_x, attr_y, reset_view=True): self.master.Information.missing_coords.clear() self._clear_plot_widget() - self.shown_x = attr_x - self.shown_y = attr_y + self.shown_x, self.shown_y = attr_x, attr_y - if self.scaled_data is None or not len(self.scaled_data): + if self.jittered_data is None or not len(self.jittered_data): self.valid_data = None else: - index_x = self.data_domain.index(attr_x) - index_y = self.data_domain.index(attr_y) - self.valid_data = self.get_valid_list([index_x, index_y], - also_class_if_exists=False) + index_x = self.domain.index(attr_x) + index_y = self.domain.index(attr_y) + self.valid_data = self.get_valid_list([index_x, index_y]) if not np.any(self.valid_data): self.valid_data = None if self.valid_data is None: self.selection = None self.n_points = 0 - self.master.Warning.missing_coords(self.shown_x, self.shown_y) + self.master.Warning.missing_coords( + self.shown_x.name, self.shown_y.name) return x_data, y_data = self.get_xy_data_positions( @@ -607,7 +609,7 @@ def update_data(self, attr_x, attr_y, reset_view=True): for axis, name, index in (("bottom", attr_x, index_x), ("left", attr_y, index_y)): self.set_axis_title(axis, name) - var = self.data_domain[index] + var = self.domain[index] if var.is_discrete: self.set_labels(axis, get_variable_values_sorted(var)) else: @@ -627,7 +629,8 @@ def update_data(self, attr_x, attr_y, reset_view=True): data_indices = np.flatnonzero(self.valid_data) if len(data_indices) != self.original_data.shape[1]: - self.master.Information.missing_coords(self.shown_x, self.shown_y) + self.master.Information.missing_coords( + self.shown_x.name, self.shown_y.name) self.scatterplot_item = ScatterPlotItem( x=x_data, y=y_data, data=data_indices, @@ -649,19 +652,11 @@ def update_data(self, attr_x, attr_y, reset_view=True): self.plot_widget.replot() def can_draw_density(self): - if self.data_domain is None: - return False - discrete_color = False - attr_color = self.attr_color - if attr_color != "" and attr_color != "(Same color)": - color_var = self.data_domain[attr_color] - discrete_color = color_var.is_discrete - continuous_x = False - continuous_y = False - if self.shown_x and self.shown_y: - continuous_x = self.data_domain[self.shown_x].is_continuous - continuous_y = self.data_domain[self.shown_y].is_continuous - return discrete_color and continuous_x and continuous_y + return self.domain is not None and \ + self.attr_color is not None and \ + self.attr_color.is_discrete and \ + self.shown_x.is_continuous and \ + self.shown_y.is_continuous def should_draw_density(self): return self.class_density and self.n_points > 1 and self.can_draw_density() @@ -678,11 +673,9 @@ def set_axis_title(self, axis, title): self.plot_widget.setLabel(axis=axis, text=title) def get_size_index(self): - size_index = -1 - attr_size = self.attr_size - if attr_size != "" and attr_size != "(Same size)": - size_index = self.data_domain.index(attr_size) - return size_index + if self.attr_size is None: + return -1 + return self.domain.index(self.attr_size) def compute_sizes(self): self.master.Information.missing_size.clear() @@ -692,7 +685,7 @@ def compute_sizes(self): else: size_data = \ self.MinShapeSize + \ - self.no_jittering_scaled_data[size_index, self.valid_data] * \ + self.scaled_data[size_index, self.valid_data] * \ self.point_width nans = np.isnan(size_data) if np.any(nans): @@ -709,18 +702,15 @@ def update_sizes(self): update_point_size = update_sizes def get_color_index(self): - color_index = -1 - attr_color = self.attr_color - if attr_color != "" and attr_color != "(Same color)": - color_index = self.data_domain.index(attr_color) - color_var = self.data_domain[attr_color] - colors = color_var.colors - if color_var.is_discrete: - self.discrete_palette = ColorPaletteGenerator( - number_of_colors=len(colors), rgb_colors=colors) - else: - self.continuous_palette = ContinuousPaletteGenerator(*colors) - return color_index + if self.attr_color is None: + return -1 + colors = self.attr_color.colors + if self.attr_color.is_discrete: + self.discrete_palette = ColorPaletteGenerator( + number_of_colors=len(colors), rgb_colors=colors) + else: + self.continuous_palette = ContinuousPaletteGenerator(*colors) + return self.domain.index(self.attr_color) def compute_colors_sel(self, keep_colors=False): if not keep_colors: @@ -753,7 +743,7 @@ def make_pen(color, width): subset = None if self.subset_indices: subset = np.array([ex.id in self.subset_indices - for ex in self.raw_data[self.valid_data]]) + for ex in self.data[self.valid_data]]) if color_index == -1: # same color color = self.plot_widget.palette().color(OWPalette.Data) @@ -768,7 +758,7 @@ def make_pen(color, width): return pen, brush c_data = self.original_data[color_index, self.valid_data] - if self.data_domain[color_index].is_continuous: + if self.domain[color_index].is_continuous: if self.pen_colors is None: self.scale = DiscretizedScale(np.nanmin(c_data), np.nanmax(c_data)) c_data -= self.scale.offset @@ -846,15 +836,15 @@ def create_labels(self): self.labels.append(ti) def update_labels(self): - if not self.attr_label or \ + if self.attr_label is None or \ self.label_only_selected and self.selection is None: for label in self.labels: label.setText("") return if not self.labels: self.create_labels() - label_column = self.raw_data.get_column_view(self.attr_label)[0] - formatter = self.raw_data.domain[self.attr_label].str_val + label_column = self.data.get_column_view(self.attr_label)[0] + formatter = self.attr_label.str_val label_data = map(formatter, label_column) black = pg.mkColor(0, 0, 0) if self.label_only_selected: @@ -866,13 +856,10 @@ def update_labels(self): label.setText(text, black) def get_shape_index(self): - shape_index = -1 - attr_shape = self.attr_shape - if attr_shape and attr_shape != "(Same shape)" and \ - len(self.data_domain[attr_shape].values) <= \ - len(self.CurveSymbols): - shape_index = self.data_domain.index(attr_shape) - return shape_index + if self.attr_shape is None or \ + len(self.attr_shape.values) > len(self.CurveSymbols): + return -1 + return self.domain.index(self.attr_shape) def compute_symbols(self): self.master.Information.missing_shape.clear() @@ -930,7 +917,7 @@ def make_color_legend(self): color_index = self.get_color_index() if color_index == -1: return - color_var = self.data_domain[color_index] + color_var = self.domain[color_index] use_shape = self.get_shape_index() == color_index if color_var.is_discrete: if not self.legend: @@ -959,7 +946,7 @@ def make_shape_legend(self): return if not self.legend: self.create_legend() - shape_var = self.data_domain[shape_index] + shape_var = self.domain[shape_index] color = self.plot_widget.palette().color(OWPalette.Data) pen = QPen(color.darker(self.DarkerValue)) color.setAlpha(self.alpha_value) @@ -1004,12 +991,12 @@ def unselect_all(self): def select(self, points): # noinspection PyArgumentList - if self.raw_data is None: + if self.data is None: return keys = QApplication.keyboardModifiers() if self.selection is None or not keys & ( Qt.ShiftModifier + Qt.ControlModifier + Qt.AltModifier): - self.selection = np.full(len(self.raw_data), False, dtype=np.bool) + self.selection = np.full(len(self.data), False, dtype=np.bool) indices = [p.data() for p in points] if keys & Qt.AltModifier: self.selection[indices] = False @@ -1046,22 +1033,22 @@ def help_event(self, event): index = p.data() text += "Attributes:\n" if self.tooltip_shows_all and \ - len(self.data_domain.attributes) < 30: + len(self.domain.attributes) < 30: text += "".join( ' {} = {}\n'.format(attr.name, - self.raw_data[index][attr]) - for attr in self.data_domain.attributes) + self.data[index][attr]) + for attr in self.domain.attributes) else: text += ' {} = {}\n {} = {}\n'.format( - self.shown_x, self.raw_data[index][self.shown_x], - self.shown_y, self.raw_data[index][self.shown_y]) + self.shown_x, self.data[index][self.shown_x], + self.shown_y, self.data[index][self.shown_y]) if self.tooltip_shows_all: text += " ... and {} others\n\n".format( - len(self.data_domain.attributes) - 2) - if self.data_domain.class_var: + len(self.domain.attributes) - 2) + if self.domain.class_var: text += 'Class:\n {} = {}\n'.format( - self.data_domain.class_var.name, - self.raw_data[index][self.raw_data.domain.class_var]) + self.domain.class_var.name, + self.data[index][self.data.domain.class_var]) if i < len(points) - 1: text += '------------------\n' diff --git a/Orange/widgets/visualize/tests/test_owscatterplot.py b/Orange/widgets/visualize/tests/test_owscatterplot.py index 43ba15fdacf..26a6a21ae0f 100644 --- a/Orange/widgets/visualize/tests/test_owscatterplot.py +++ b/Orange/widgets/visualize/tests/test_owscatterplot.py @@ -18,16 +18,15 @@ def test_set_data(self): self.send_signal("Data", self.iris) # First two attribute should be selected as x an y - self.assertEqual(self.widget.attr_x, self.iris.domain[0].name) - self.assertEqual(self.widget.attr_y, self.iris.domain[1].name) + self.assertEqual(self.widget.attr_x, self.iris.domain[0]) + self.assertEqual(self.widget.attr_y, self.iris.domain[1]) # Class var should be selected as color - self.assertEqual(self.widget.graph.attr_color, - self.iris.domain.class_var.name) + self.assertIs(self.widget.graph.attr_color, self.iris.domain.class_var) # Change which attributes are displayed - self.widget.attr_x = self.iris.domain[2].name - self.widget.attr_y = self.iris.domain[3].name + self.widget.attr_x = self.iris.domain[2] + self.widget.attr_y = self.iris.domain[3] # Disconnect the data self.send_signal("Data", None) @@ -44,8 +43,8 @@ def test_set_data(self): # same attributes that were used last time should be selected self.send_signal("Data", self.iris) - self.assertEqual(self.widget.attr_x, self.iris.domain[2].name) - self.assertEqual(self.widget.attr_y, self.iris.domain[3].name) + self.assertIs(self.widget.attr_x, self.iris.domain[2]) + self.assertIs(self.widget.attr_y, self.iris.domain[3]) def test_score_heuristics(self): domain = Domain([ContinuousVariable(c) for c in "abcd"], @@ -63,7 +62,7 @@ def test_optional_combos(self): [domain.attributes[2]]) t1 = Table(d1, self.iris) self.send_signal("Data", t1) - self.widget.graph.attr_size = domain.attributes[2].name + self.widget.graph.attr_size = domain.attributes[2] d2 = Domain(domain.attributes[:2], domain.class_var, [domain.attributes[3]]) From dc23dfaa30c6338af0b1fcb826b49d4dbb793ffc Mon Sep 17 00:00:00 2001 From: janezd Date: Fri, 7 Oct 2016 12:18:15 +0200 Subject: [PATCH 08/15] scaling.py: Remove unused exotic experimental functions for some clustering --- Orange/widgets/utils/scaling.py | 213 -------------------------------- 1 file changed, 213 deletions(-) diff --git a/Orange/widgets/utils/scaling.py b/Orange/widgets/utils/scaling.py index ceff8725975..1b3eaa807b4 100644 --- a/Orange/widgets/utils/scaling.py +++ b/Orange/widgets/utils/scaling.py @@ -172,216 +172,3 @@ def get_xy_data_positions(self, xattr, yattr, filter_valid=False, return xdata, ydata getXYDataPositions = get_xy_data_positions - - # @deprecated_keywords({"attrIndices": "attr_indices", - # "settingsDict": "settings_dict"}) - def get_projected_point_position(self, attr_indices, values, **settings_dict): - """ - For attributes in attr_indices and values of these attributes in values - compute point positions this function has more sense in radviz and - polyviz methods. settings_dict has to be because radviz and polyviz have - this parameter. - """ - return values - - getProjectedPointPosition = get_projected_point_position - - # @deprecated_keywords({"attrIndices": "attr_indices", - # "settingsDict": "settings_dict"}) - def create_projection_as_example_table(self, attr_indices, **settings_dict): - """ - Create the projection of attribute indices given in attr_indices and - create an example table with it. - - """ - if self.data_has_class: - domain = settings_dict.get("domain") or \ - Orange.data.Domain([Orange.feature.Continuous(self.data_domain[attr_indices[0]].name), - Orange.feature.Continuous(self.data_domain[attr_indices[1]].name), - Orange.feature.Discrete(self.data_domain.class_var.name, - values = get_variable_values_sorted(self.data_domain.class_var))]) - else: - domain = settings_dict.get("domain") or \ - Orange.data.Domain([Orange.feature.Continuous(self.data_domain[attr_indices[0]].name), - Orange.feature.Continuous(self.data_domain[attr_indices[1]].name)]) - - data = self.create_projection_as_numeric_array(attr_indices, - **settings_dict) - if data != None: - return Orange.data.Table(domain, data) - else: - return Orange.data.Table(domain) - - createProjectionAsExampleTable = create_projection_as_example_table - - # @deprecated_keywords({"attrIndices": "attr_indices", - # "settingsDict": "settings_dict"}) - def create_projection_as_example_table_3D(self, attr_indices, **settings_dict): - """ - Create the projection of attribute indices given in attr_indices and - create an example table with it. - - """ - if self.data_has_class: - domain = settings_dict.get("domain") or \ - Orange.data.Domain([Orange.feature.Continuous(self.data_domain[attr_indices[0]].name), - Orange.feature.Continuous(self.data_domain[attr_indices[1]].name), - Orange.feature.Continuous(self.data_domain[attr_indices[2]].name), - Orange.feature.Discrete(self.data_domain.class_var.name, - values = get_variable_values_sorted(self.data_domain.class_var))]) - else: - domain = settings_dict.get("domain") or \ - Orange.data.Domain([Orange.feature.Continuous(self.data_domain[attr_indices[0]].name), - Orange.feature.Continuous(self.data_domain[attr_indices[1]].name), - Orange.feature.Continuous(self.data_domain[attr_indices[2]].name)]) - - data = self.create_projection_as_numeric_array_3D(attr_indices, - **settings_dict) - if data != None: - return Orange.data.Table(domain, data) - else: - return Orange.data.Table(domain) - - createProjectionAsExampleTable3D = create_projection_as_example_table_3D - - # @deprecated_keywords({"attrIndices": "attr_indices", - # "settingsDict": "settings_dict", - # "validData": "valid_data", - # "classList": "class_list", - # "jutterSize": "jitter_size"}) - def create_projection_as_numeric_array(self, attr_indices, **settings_dict): - valid_data = settings_dict.get("valid_data") - class_list = settings_dict.get("class_list") - jitter_size = settings_dict.get("jitter_size", 0.0) - - if valid_data == None: - valid_data = self.get_valid_list(attr_indices) - if sum(valid_data) == 0: - return None - - if class_list == None and self.data_has_class: - class_list = self.original_data[self.data_class_index] - - xarray = self.no_jittering_scaled_data[attr_indices[0]] - yarray = self.no_jittering_scaled_data[attr_indices[1]] - if jitter_size > 0.0: - xarray += (np.random.random(len(xarray))-0.5)*jitter_size - yarray += (np.random.random(len(yarray))-0.5)*jitter_size - if class_list != None: - data = np.compress(valid_data, np.array((xarray, yarray, class_list)), axis = 1) - else: - data = np.compress(valid_data, np.array((xarray, yarray)), axis = 1) - data = np.transpose(data) - return data - - createProjectionAsNumericArray = create_projection_as_numeric_array - - # @deprecated_keywords({"attrIndices": "attr_indices", - # "settingsDict": "settings_dict", - # "validData": "valid_data", - # "classList": "class_list", - # "jutterSize": "jitter_size"}) - def create_projection_as_numeric_array_3D(self, attr_indices, **settings_dict): - valid_data = settings_dict.get("valid_data") - class_list = settings_dict.get("class_list") - jitter_size = settings_dict.get("jitter_size", 0.0) - - if valid_data == None: - valid_data = self.get_valid_list(attr_indices) - if sum(valid_data) == 0: - return None - - if class_list == None and self.data_has_class: - class_list = self.original_data[self.data_class_index] - - xarray = self.no_jittering_scaled_data[attr_indices[0]] - yarray = self.no_jittering_scaled_data[attr_indices[1]] - zarray = self.no_jittering_scaled_data[attr_indices[2]] - if jitter_size > 0.0: - xarray += (np.random.random(len(xarray))-0.5)*jitter_size - yarray += (np.random.random(len(yarray))-0.5)*jitter_size - zarray += (np.random.random(len(zarray))-0.5)*jitter_size - if class_list != None: - data = np.compress(valid_data, np.array((xarray, yarray, zarray, class_list)), axis = 1) - else: - data = np.compress(valid_data, np.array((xarray, yarray, zarray)), axis = 1) - data = np.transpose(data) - return data - - createProjectionAsNumericArray3D = create_projection_as_numeric_array_3D - - # @deprecated_keywords({"attributeNameOrder": "attribute_name_order", - # "addResultFunct": "add_result_funct"}) - def get_optimal_clusters(self, attribute_name_order, add_result_funct): - if not self.data_has_class or self.data_has_continuous_class: - return - - jitter_size = 0.001 * self.clusterOptimization.jitterDataBeforeTriangulation - domain = Orange.data.Domain([Orange.feature.Continuous("xVar"), - Orange.feature.Continuous("yVar"), - self.data_domain.class_var]) - - # init again, in case that the attribute ordering took too much time - start_time = time.time() - test_index = 0 - - count = len(attribute_name_order) * (len(attribute_name_order) - 1) / 2 - with self.scatterWidget.progressBar(count) as progressBar: - for i in range(len(attribute_name_order)): - for j in range(i): - try: - index = self.data_domain.index - attr1 = index(attribute_name_order[j]) - attr2 = index(attribute_name_order[i]) - test_index += 1 - if self.clusterOptimization.isOptimizationCanceled(): - secs = time.time() - start_time - self.clusterOptimization.setStatusBarText( - "Evaluation stopped " - "(evaluated %d projections in %d min, %d sec)" - % (test_index, secs / 60, secs % 60)) - return - - data = self.create_projection_as_example_table( - [attr1, attr2], - domain=domain, jitter_size=jitter_size) - graph, valuedict, closuredict, polygon_vertices_dict, \ - enlarged_closure_dict, other_dict = \ - self.clusterOptimization.evaluateClusters(data) - - all_value = 0.0 - classes_dict = {} - for key in valuedict.keys(): - cls = int(graph.objects[polygon_vertices_dict - [key][0]].getclass()) - add_result_funct( - valuedict[key], closuredict[key], - polygon_vertices_dict[key], - [attribute_name_order[i], - attribute_name_order[j]], - cls, - enlarged_closure_dict[key], other_dict[key]) - classes_dict[key] = cls - all_value += valuedict[key] - # add all the clusters - add_result_funct( - all_value, closuredict, polygon_vertices_dict, - [attribute_name_order[i], attribute_name_order[j]], - classes_dict, enlarged_closure_dict, other_dict) - - self.clusterOptimization.setStatusBarText( - "Evaluated %d projections..." % test_index) - progressBar.advance() - del data, graph, valuedict, closuredict, \ - polygon_vertices_dict, enlarged_closure_dict, \ - other_dict, classes_dict - except: - type, val, traceback = sys.exc_info() - sys.excepthook(type, val, traceback) # print the exception - - secs = time.time() - start_time - self.clusterOptimization.setStatusBarText( - "Finished evaluation (evaluated %d projections in %d min, %d sec)" - % (test_index, secs / 60, secs % 60)) - - getOptimalClusters = get_optimal_clusters From 815914c0002691bee6f8db6ac3b110be1d65a8c5 Mon Sep 17 00:00:00 2001 From: janezd Date: Fri, 7 Oct 2016 12:19:03 +0200 Subject: [PATCH 09/15] Scaling: Remove unused imports --- Orange/widgets/utils/scaling.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/Orange/widgets/utils/scaling.py b/Orange/widgets/utils/scaling.py index 1b3eaa807b4..4adfe37eedf 100644 --- a/Orange/widgets/utils/scaling.py +++ b/Orange/widgets/utils/scaling.py @@ -1,10 +1,5 @@ -from datetime import time -import random -import sys - import numpy as np -import Orange from Orange.statistics.basic_stats import DomainBasicStats from Orange.widgets.settings import Setting from Orange.widgets.utils import checksum From 546834b5197087dc4a2386ffe20fd0600b54e891 Mon Sep 17 00:00:00 2001 From: janezd Date: Fri, 7 Oct 2016 12:19:48 +0200 Subject: [PATCH 10/15] Parallel graph: Add missing call of _compute_domain_data_stat --- Orange/widgets/visualize/owparallelgraph.py | 1 + 1 file changed, 1 insertion(+) diff --git a/Orange/widgets/visualize/owparallelgraph.py b/Orange/widgets/visualize/owparallelgraph.py index d322b60b52f..8c656e1806d 100644 --- a/Orange/widgets/visualize/owparallelgraph.py +++ b/Orange/widgets/visualize/owparallelgraph.py @@ -76,6 +76,7 @@ def set_data(self, data, subset_data=None, **args): self.groups = {} OWPlot.setData(self, data) ScaleData.set_data(self, data, no_data=True, **args) + self._compute_domain_data_stat() self.end_progress() From 6d076e0448dff8934a795acde929f4ef9e913852 Mon Sep 17 00:00:00 2001 From: janezd Date: Fri, 7 Oct 2016 12:20:41 +0200 Subject: [PATCH 11/15] Parallel graph: Fix a bug in select_color --- Orange/widgets/visualize/owparallelgraph.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/Orange/widgets/visualize/owparallelgraph.py b/Orange/widgets/visualize/owparallelgraph.py index 8c656e1806d..3ab188db98c 100644 --- a/Orange/widgets/visualize/owparallelgraph.py +++ b/Orange/widgets/visualize/owparallelgraph.py @@ -200,15 +200,14 @@ def scale_row(row): self._draw_curves(background_curves) def select_color(self, row_index): - if self.data_has_class: - if self.data_has_continuous_class: - return self.continuous_palette.getRGB( - self.data[row_index, self.data_class_index]) - else: - return self.colors[ - int(self.data[row_index, self.data_class_index])] - else: + domain = self.data.domain + if domain.class_var is None: return 0, 0, 0 + class_val = self.data[row_index, domain.index(domain.class_var)] + if domain.has_continuous_class: + return self.continuous_palette.getRGB(class_val) + else: + return self.colors[int(class_val)] def _draw_curves(self, selected_curves): n_attr = len(self.attributes) From 7f4a34d43ef6d03008ad7515ef553b551e9c179d Mon Sep 17 00:00:00 2001 From: janezd Date: Fri, 7 Oct 2016 12:22:17 +0200 Subject: [PATCH 12/15] ScatterPlot: Replace 'iris' with the more diverse 'heart_disease' in main function --- Orange/widgets/visualize/owscatterplot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Orange/widgets/visualize/owscatterplot.py b/Orange/widgets/visualize/owscatterplot.py index 0a9ad191b68..be07b301c55 100644 --- a/Orange/widgets/visualize/owscatterplot.py +++ b/Orange/widgets/visualize/owscatterplot.py @@ -498,7 +498,7 @@ def test_main(argv=None): if len(argv) > 1: filename = argv[1] else: - filename = "iris" + filename = "heart_disease" ow = OWScatterPlot() ow.show() From 70ce819dd6f4646c0304f2088685c2c336c7007e Mon Sep 17 00:00:00 2001 From: janezd Date: Fri, 30 Sep 2016 11:10:39 +0200 Subject: [PATCH 13/15] Placeholders for Nones --- Orange/widgets/gui.py | 22 +++++++++------------- Orange/widgets/utils/itemmodels.py | 16 ++++++++++++---- Orange/widgets/visualize/owscatterplot.py | 20 ++++++++------------ 3 files changed, 29 insertions(+), 29 deletions(-) diff --git a/Orange/widgets/gui.py b/Orange/widgets/gui.py index 4d36637bf6a..af74a1a597a 100644 --- a/Orange/widgets/gui.py +++ b/Orange/widgets/gui.py @@ -1613,7 +1613,7 @@ def comboBox(widget, master, value, box=None, label=None, labelWidth=None, cindex = getdeepattr(master, value) model = misc.get("model", None) if isinstance(model, VariableListModel): - callfront = CallFrontComboBoxModel(combo, model, emptyString) + callfront = CallFrontComboBoxModel(combo, model) callfront.action(cindex) else: if isinstance(cindex, str): @@ -1629,7 +1629,7 @@ def comboBox(widget, master, value, box=None, label=None, labelWidth=None, connectControl( master, value, callback, combo.activated[int], callfront, - ValueCallbackComboModel(master, value, model, emptyString) + ValueCallbackComboModel(master, value, model) ) elif sendSelectedValue: connectControl( @@ -2266,16 +2266,13 @@ def __call__(self, value): class ValueCallbackComboModel(ValueCallback): - def __init__(self, widget, attribute, model, emptyString=""): + def __init__(self, widget, attribute, model): super().__init__(widget, attribute) self.model = model - self.emptyString = emptyString def __call__(self, index): - value = self.model[index] # Can't use super here since, it doesn't set `None`'s?! - return self.acyclic_setattr( - None if value == self.emptyString else value) + return self.acyclic_setattr(self.model[index]) class ValueCallbackLineEdit(ControlledCallback): @@ -2475,16 +2472,15 @@ def action(self, value): class CallFrontComboBoxModel(ControlledCallFront): - def __init__(self, control, model, emptyString=None): + def __init__(self, control, model): super().__init__(control) self.model = model - self.emptyString = emptyString def action(self, value): - if value is None or value == "": # the latter accomodates PyListModel - if self.emptyString is None: - return - value = self.emptyString + if value == "": # the latter accomodates PyListModel + value = None + if value is None and None not in self.model: + return # e.g. attribute x in uninitialized scatter plot if value in self.model: self.control.setCurrentIndex(self.model.indexOf(value)) return diff --git a/Orange/widgets/utils/itemmodels.py b/Orange/widgets/utils/itemmodels.py index 77196d1ba2d..5d4122a2bfa 100644 --- a/Orange/widgets/utils/itemmodels.py +++ b/Orange/widgets/utils/itemmodels.py @@ -589,11 +589,15 @@ def data(self, index, role=Qt.DisplayRole): class VariableListModel(PyListModel): MIME_TYPE = "application/x-Orange-VariableList" + def __init__(self, *args, placeholder=None, **kwargs): + super().__init__(*args, **kwargs) + self.placeholder = placeholder + def data(self, index, role=Qt.DisplayRole): if self._is_index_valid_for(index, self): var = self[index.row()] if var is None and role == Qt.DisplayRole: - return "None" + return self.placeholder or "None" if not isinstance(var, Variable): return super().data(index, role) elif role == Qt.DisplayRole: @@ -656,10 +660,14 @@ class DomainModel(VariableListModel): ATTRIBUTES) PRIMITIVE = (DiscreteVariable, ContinuousVariable) - def __init__(self, order=SEPARATED, valid_types=None, alphabetical=False): - super().__init__() + def __init__(self, order=SEPARATED, placeholder=None, + valid_types=None, alphabetical=False): + super().__init__(placeholder=placeholder) if isinstance(order, int): - order = [order] + order = (order,) + if placeholder is not None and None not in order: + # don't use insert(0, .), it would modify the argument + order = (None,) + order self.order = order self.valid_types = valid_types self.alphabetical = alphabetical diff --git a/Orange/widgets/visualize/owscatterplot.py b/Orange/widgets/visualize/owscatterplot.py index be07b301c55..7759bc3e3f6 100644 --- a/Orange/widgets/visualize/owscatterplot.py +++ b/Orange/widgets/visualize/owscatterplot.py @@ -183,32 +183,28 @@ def __init__(self): box = gui.vBox(self.controlArea, "Points") color_model = DomainModel( - order=("(Same color)", dmod.Separator) + dmod.SEPARATED, - valid_types=dmod.PRIMITIVE) + placeholder="(Same color)", valid_types=dmod.PRIMITIVE) self.cb_attr_color = gui.comboBox( box, self, "graph.attr_color", label="Color:", - emptyString="(Same color)", callback=self.update_colors, + callback=self.update_colors, model=color_model, **common_options) label_model = DomainModel( - order=("(No labels)", dmod.Separator) + dmod.SEPARATED, - valid_types=dmod.PRIMITIVE) + placeholder="(No labels)", valid_types=dmod.PRIMITIVE) self.cb_attr_label = gui.comboBox( box, self, "graph.attr_label", label="Label:", - emptyString="(No labels)", callback=self.graph.update_labels, + callback=self.graph.update_labels, model=label_model, **common_options) shape_model = DomainModel( - order=("(Same shape)", dmod.Separator) + dmod.SEPARATED, - valid_types=DiscreteVariable) + placeholder="(Same shape)", valid_types=DiscreteVariable) self.cb_attr_shape = gui.comboBox( box, self, "graph.attr_shape", label="Shape:", - emptyString="(Same shape)", callback=self.graph.update_shapes, + callback=self.graph.update_shapes, model=shape_model, **common_options) size_model = DomainModel( - order=("(Same size)", dmod.Separator) + dmod.SEPARATED, - valid_types=ContinuousVariable) + placeholder="(Same size)", valid_types=ContinuousVariable) self.cb_attr_size = gui.comboBox( box, self, "graph.attr_size", label="Size:", - emptyString="(Same size)", callback=self.graph.update_sizes, + callback=self.graph.update_sizes, model=size_model, **common_options) self.models = [self.xy_model, color_model, label_model, shape_model, size_model] From d9e509822b88602b79b939b7f29d470c63c43add Mon Sep 17 00:00:00 2001 From: janezd Date: Fri, 30 Sep 2016 12:42:11 +0200 Subject: [PATCH 14/15] BoxPlot: Remove (broken) caching --- Orange/widgets/utils/itemmodels.py | 15 +++++++++------ Orange/widgets/visualize/owboxplot.py | 9 +++------ 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/Orange/widgets/utils/itemmodels.py b/Orange/widgets/utils/itemmodels.py index 5d4122a2bfa..586cde63101 100644 --- a/Orange/widgets/utils/itemmodels.py +++ b/Orange/widgets/utils/itemmodels.py @@ -666,8 +666,11 @@ def __init__(self, order=SEPARATED, placeholder=None, if isinstance(order, int): order = (order,) if placeholder is not None and None not in order: - # don't use insert(0, .), it would modify the argument - order = (None,) + order + # Add None for the placeholder if it's not already there + # Include separator if the current order uses them + order = (None,) + \ + (self.Separator, ) * (self.Separator in order) + \ + order self.order = order self.valid_types = valid_types self.alphabetical = alphabetical @@ -687,21 +690,21 @@ def set_domain(self, domain): if isinstance(section, int): if domain is None: continue - to_add = chain( + to_add = list(chain( *(vars for i, vars in enumerate( (domain.attributes, domain.class_vars, domain.metas)) - if (1 << i) & section)) + if (1 << i) & section))) if self.valid_types is not None: to_add = [var for var in to_add if isinstance(var, self.valid_types)] if self.alphabetical: - to_add = sorted(to_add) + to_add = sorted(to_add, key=lambda x: x.name) elif isinstance(section, list): to_add = section else: to_add = [section] if to_add: - if add_separator: + if add_separator and content: content.append(self.Separator) add_separator = False content += to_add diff --git a/Orange/widgets/visualize/owboxplot.py b/Orange/widgets/visualize/owboxplot.py index 06486308eaf..448bfe86ab2 100644 --- a/Orange/widgets/visualize/owboxplot.py +++ b/Orange/widgets/visualize/owboxplot.py @@ -15,7 +15,6 @@ from Orange.widgets import widget, gui from Orange.widgets.settings import (Setting, DomainContextHandler, ContextSetting) -from Orange.widgets.utils import datacaching from Orange.widgets.utils.itemmodels import VariableListModel @@ -260,15 +259,13 @@ def compute_box_data(self): self.is_continuous = attr.is_continuous if self.group_var: self.dist = [] - self.conts = datacaching.getCached( - dataset, contingency.get_contingency, - (dataset, attr, self.group_var)) + self.conts = contingency.get_contingency( + dataset, attr, self.group_var) if self.is_continuous: self.stats = [BoxData(cont) for cont in self.conts] self.label_txts_all = self.group_var.values else: - self.dist = datacaching.getCached( - dataset, distribution.get_distribution, (dataset, attr)) + self.dist = distribution.get_distribution(dataset, attr) self.conts = [] if self.is_continuous: self.stats = [BoxData(self.dist)] From 4c8a628800641ec6a89e315549e2d30b74233870 Mon Sep 17 00:00:00 2001 From: janezd Date: Fri, 30 Sep 2016 15:56:16 +0200 Subject: [PATCH 15/15] DomainModel: Add tests --- Orange/widgets/tests/test_itemmodels.py | 75 ++++++++++++++++++++++++- 1 file changed, 74 insertions(+), 1 deletion(-) diff --git a/Orange/widgets/tests/test_itemmodels.py b/Orange/widgets/tests/test_itemmodels.py index cf65d4f56ae..387a15b84d5 100644 --- a/Orange/widgets/tests/test_itemmodels.py +++ b/Orange/widgets/tests/test_itemmodels.py @@ -4,7 +4,10 @@ from unittest import TestCase from PyQt4.QtCore import Qt -from Orange.widgets.utils.itemmodels import PyTableModel, PyListModel + +from Orange.data import Domain, ContinuousVariable +from Orange.widgets.utils.itemmodels import \ + PyTableModel, PyListModel, DomainModel class TestPyTableModel(TestCase): @@ -183,3 +186,73 @@ def test_insert_delete_rows(self): success = model.removeRows(3, 4) self.assertIs(success, True) self.assertSequenceEqual(model, [None, None, None]) + + +class TestDomainModel(TestCase): + def test_init_with_single_section(self): + model = DomainModel(order=DomainModel.CLASSES) + self.assertEqual(model.order, (DomainModel.CLASSES, )) + + def test_separators(self): + attrs = [ContinuousVariable(n) for n in "abg"] + classes = [ContinuousVariable(n) for n in "deh"] + metas = [ContinuousVariable(n) for n in "ijf"] + + model = DomainModel() + sep = [model.Separator] + model.set_domain(Domain(attrs, classes, metas)) + self.assertEqual(list(model), classes + sep + metas + sep + attrs) + + model = DomainModel() + model.set_domain(Domain(attrs, [], metas)) + self.assertEqual(list(model), metas + sep + attrs) + + model = DomainModel() + model.set_domain(Domain([], [], metas)) + self.assertEqual(list(model), metas) + + model = DomainModel(placeholder="foo") + model.set_domain(Domain([], [], metas)) + self.assertEqual(list(model), [None] + sep + metas) + + model = DomainModel(placeholder="foo") + model.set_domain(Domain(attrs, [], metas)) + self.assertEqual(list(model), [None] + sep + metas + sep + attrs) + + def test_placeholder_placement(self): + model = DomainModel(placeholder="foo") + sep = model.Separator + self.assertEqual(model.order, (None, sep) + model.SEPARATED) + + model = DomainModel(order=("bar", ), placeholder="foo") + self.assertEqual(model.order, (None, "bar")) + + model = DomainModel(order=("bar", None, "baz"), placeholder="foo") + self.assertEqual(model.order, ("bar", None, "baz")) + + model = DomainModel(order=("bar", sep, "baz"), + placeholder="foo") + self.assertEqual(model.order, (None, sep, "bar", sep, "baz")) + + def test_subparts(self): + attrs = [ContinuousVariable(n) for n in "abg"] + classes = [ContinuousVariable(n) for n in "deh"] + metas = [ContinuousVariable(n) for n in "ijf"] + + m = DomainModel + sep = m.Separator + model = DomainModel( + order=(m.ATTRIBUTES | m.METAS, sep, m.CLASSES)) + model.set_domain(Domain(attrs, classes, metas)) + self.assertEqual(list(model), attrs + metas + [sep] + classes) + + m = DomainModel + sep = m.Separator + model = DomainModel( + order=(m.ATTRIBUTES | m.METAS, sep, m.CLASSES), + alphabetical=True) + model.set_domain(Domain(attrs, classes, metas)) + self.assertEqual(list(model), + sorted(attrs + metas, key=lambda x: x.name) + + [sep] + + sorted(classes, key=lambda x: x.name))