def __init__(self): # pylint: disable=missing-docstring super().__init__() self.data = self.discrete_data = None self.attrs = [] self.input_features = None self.areas = [] self.selection = set() self.attr_box = gui.hBox(self.mainArea) model = VariableListModel() model.wrap(self.attrs) combo_args = dict( widget=self.attr_box, master=self, contentsLength=12, callback=self.update_attr, sendSelectedValue=True, valueType=str, model=model) fixed_size = (QSizePolicy.Fixed, QSizePolicy.Fixed) self.attrXCombo = gui.comboBox(value="attrX", **combo_args) gui.widgetLabel(self.attr_box, "\u2715", sizePolicy=fixed_size) self.attrYCombo = gui.comboBox(value="attrY", **combo_args) self.vizrank, self.vizrank_button = SieveRank.add_vizrank( self.attr_box, self, "Score Combinations", self.set_attr) self.vizrank_button.setSizePolicy(*fixed_size) self.canvas = QGraphicsScene() self.canvasView = ViewWithPress( self.canvas, self.mainArea, handler=self.reset_selection) self.mainArea.layout().addWidget(self.canvasView) self.canvasView.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) self.canvasView.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) box = gui.hBox(self.mainArea) box.layout().addWidget(self.graphButton) box.layout().addWidget(self.report_button)
def __init__(self): super().__init__() self.data = None self.input_features = None self.attrs = [] self.attr_box = gui.hBox(self.mainArea) model = VariableListModel() model.wrap(self.attrs) self.attrXCombo = gui.comboBox( self.attr_box, self, value="attrX", contentsLength=12, callback=self.change_attr, sendSelectedValue=True, valueType=str) self.attrXCombo.setModel(model) gui.widgetLabel(self.attr_box, "\u2715").\ setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) self.attrYCombo = gui.comboBox( self.attr_box, self, value="attrY", contentsLength=12, callback=self.change_attr, sendSelectedValue=True, valueType=str) self.attrYCombo.setModel(model) self.canvas = QGraphicsScene() self.canvasView = ViewWithPress(self.canvas, self.mainArea, handler=self.reset_selection) self.mainArea.layout().addWidget(self.canvasView) self.canvasView.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) self.canvasView.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) box = gui.hBox(self.mainArea) box.layout().addWidget(self.graphButton) box.layout().addWidget(self.report_button)
def __init__(self): self.model_selected = VariableListModel(enable_dnd=True) self.model_selected.removed.connect(self.__model_selected_changed) self.model_other = VariableListModel(enable_dnd=True) self.vizrank, self.btn_vizrank = LinearProjectionVizRank.add_vizrank( None, self, "Suggest Features", self.__vizrank_set_attrs) super().__init__()
def __init__(self): self.model_selected = VariableListModel(enable_dnd=True) self.model_selected.rowsInserted.connect(self.__model_selected_changed) self.model_selected.rowsRemoved.connect(self.__model_selected_changed) self.model_other = VariableListModel(enable_dnd=True) self.vizrank, self.btn_vizrank = RadvizVizRank.add_vizrank( None, self, "Suggest features", self.__vizrank_set_attrs ) super().__init__()
class TestListModel(GuiTest): def setUp(self): self.widget = OWWidget() self.widget.foo = None self.attrs = VariableListModel() self.view = gui.listView( self.widget.controlArea, self.widget, "foo", model=self.attrs) def test_select_callback(self): widget = self.widget view = self.view self.assertIsNone(widget.foo) a, b, c = (ContinuousVariable(x) for x in "abc") self.attrs[:] = [a, b, c] view.setCurrentIndex(self.attrs.index(0, 0)) self.assertIs(widget.foo, a) view.setCurrentIndex(self.attrs.index(2, 0)) self.assertIs(widget.foo, c) view.setSelectionMode(view.MultiSelection) sel_model = view.selectionModel() sel_model.clear() view.setCurrentIndex(self.attrs.index(1, 0)) self.assertEqual(widget.foo, [b]) def test_select_callfront(self): widget = self.widget view = self.view a, b, c = (ContinuousVariable(x) for x in "abc") self.attrs[:] = [a, b, c] widget.foo = b selection = view.selectedIndexes() self.assertEqual(len(selection), 1) self.assertEqual(selection[0].row(), 1) view.setSelectionMode(view.MultiSelection) widget.foo = [a, c] selection = view.selectedIndexes() self.assertEqual(len(selection), 2) self.assertEqual({selection[0].row(), selection[1].row()}, {0, 2}) widget.foo = [] selection = view.selectedIndexes() self.assertEqual(len(selection), 0) widget.foo = [2, "b"] selection = view.selectedIndexes() self.assertEqual(len(selection), 2) self.assertEqual({selection[0].row(), selection[1].row()}, {1, 2})
def __init__(self): super().__init__() self.corpus = None # Browse file box fbox = gui.widgetBox(self.controlArea, "Corpus file", orientation=0) self.file_widget = widgets.FileWidget( recent_files=self.recent_files, icon_size=(16, 16), on_open=self.open_file, dialog_format=self.dlgFormats, dialog_title='Open Orange Document Corpus', reload_label='Reload', browse_label='Browse', allow_empty=False, minimal_width=250, ) fbox.layout().addWidget(self.file_widget) # Corpus info ibox = gui.widgetBox(self.controlArea, "Corpus info", addSpace=True) self.info_label = gui.label(ibox, self, "") self.update_info() # Used Text Features fbox = gui.widgetBox(self.controlArea, orientation=0) ubox = gui.widgetBox(fbox, "Used text features", addSpace=False) self.used_attrs_model = VariableListModel(enable_dnd=True) self.used_attrs_view = VariablesListItemView() self.used_attrs_view.setModel(self.used_attrs_model) ubox.layout().addWidget(self.used_attrs_view) aa = self.used_attrs_model aa.dataChanged.connect(self.update_feature_selection) aa.rowsInserted.connect(self.update_feature_selection) aa.rowsRemoved.connect(self.update_feature_selection) # Ignored Text Features ibox = gui.widgetBox(fbox, "Ignored text features", addSpace=False) self.unused_attrs_model = VariableListModel(enable_dnd=True) self.unused_attrs_view = VariablesListItemView() self.unused_attrs_view.setModel(self.unused_attrs_model) ibox.layout().addWidget(self.unused_attrs_view) # Documentation Data Sets & Report box = gui.hBox(self.controlArea) self.browse_documentation = gui.button( box, self, "Browse documentation corpora", callback=lambda: self.file_widget.browse( get_sample_corpora_dir()), autoDefault=False, ) # load first file self.file_widget.select(0)
def __init__(self): self.data = None self.indices = [] box = gui.vBox(self.controlArea, 'Axes') self.combo_ax2 = gui.comboBox( box, self, 'ax2', label='Y axis:', callback=self.replot, sendSelectedValue=True, orientation='horizontal') self.combo_ax1 = gui.comboBox( box, self, 'ax1', label='Radial:', callback=self.replot, sendSelectedValue=True, orientation='horizontal') box = gui.vBox(self.controlArea, 'Aggregation') self.combo_func = gui.comboBox( box, self, 'agg_func', label='Function:', orientation='horizontal', callback=self.replot) func_model = ListModel(AGG_FUNCTIONS, parent=self) self.combo_func.setModel(func_model) self.attrlist_model = VariableListModel(parent=self) self.attrlist = QListView(selectionMode=QListView.ExtendedSelection) self.attrlist.setModel(self.attrlist_model) self.attrlist.selectionModel().selectionChanged.connect( self.attrlist_selectionChanged) box.layout().addWidget(self.attrlist) gui.rubber(self.controlArea) self.chart = chart = Spiralogram(self, selection_callback=self.on_selection) self.mainArea.layout().addWidget(chart)
def _create_layout(self): box = gui.widgetBox(self.controlArea, orientation='horizontal') self.varmodel = VariableListModel(parent=self) self.attr_combo = gui.comboBox(box, self, 'selected_attr', orientation=Qt.Horizontal, label='Region attribute:', callback=self.on_attr_change, sendSelectedValue=True) self.attr_combo.setModel(self.varmodel) self.map_combo = gui.comboBox(box, self, 'selected_map', orientation=Qt.Horizontal, label='Map type:', callback=self.on_map_change, items=Map.all) hexpand = QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) self.attr_combo.setSizePolicy(hexpand) self.map_combo.setSizePolicy(hexpand) url = urljoin('file:', pathname2url(os.path.join( os.path.dirname(__file__), 'resources', 'owgeomap.html'))) self.webview = gui.WebviewWidget(self.controlArea, self, url=QUrl(url)) self.controlArea.layout().addWidget(self.webview) QTimer.singleShot( 0, lambda: self.webview.evalJS('REGIONS = {};'.format({Map.WORLD: CC_WORLD, Map.EUROPE: CC_EUROPE, Map.USA: CC_USA})))
class TestListModel(GuiTest): def test_select(self): widget = OWWidget() widget.foo = None self.attrs = VariableListModel() view = gui.listView(widget.controlArea, widget, "foo", model=self.attrs) self.assertIsNone(widget.foo) a, b, c = (ContinuousVariable(x) for x in "abc") self.attrs[:] = [a, b, c] view.setCurrentIndex(self.attrs.index(0, 0)) self.assertIs(widget.foo, a) view.setCurrentIndex(self.attrs.index(2, 0)) self.assertIs(widget.foo, c) widget.foo = b selection = view.selectedIndexes() self.assertEqual(len(selection), 1) self.assertEqual(selection[0].row(), 1)
def dropMimeData(self, mime, action, row, column, parent): """ Ensure only one variable can be dropped onto the view. """ vars = mime.property('_items') if vars is None or len(self) + len(vars) > 1: return False if action == Qt.IgnoreAction: return True return VariableListModel.dropMimeData( self, mime, action, row, column, parent)
def __init__(self): super().__init__() self.data = None self.subset_data = None self.subset_indices = None self._embedding_coords = None self._rand_indices = None self.__replot_requested = False self.variable_x = ContinuousVariable("radviz-x") self.variable_y = ContinuousVariable("radviz-y") box = gui.vBox(self.mainArea, True, margin=0) self.graph = OWRadvizGraph(self, box) box.layout().addWidget(self.graph.plot_widget) self.variables_selection = VariablesSelection() self.model_selected = selected = VariableListModel(enable_dnd=True) self.model_other = other = VariableListModel(enable_dnd=True) self.variables_selection(self, selected, other, self.controlArea) self.vizrank, self.btn_vizrank = RadvizVizRank.add_vizrank( None, self, "Suggest features", self.vizrank_set_attrs) # Todo: this button introduces some margin at the bottom?! self.variables_selection.add_remove.layout().addWidget( self.btn_vizrank) g = self.graph.gui g.point_properties_box(self.controlArea) g.effects_box(self.controlArea) g.plot_properties_box(self.controlArea) self.graph.box_zoom_select(self.controlArea) gui.auto_commit(self.controlArea, self, "auto_commit", "Send Selection", "Send Automatically") self.graph.view_box.started.connect(self._randomize_indices) self.graph.view_box.moved.connect(self._manual_move) self.graph.view_box.finished.connect(self._finish_manual_move)
def _setup_layout(self): self.controlArea.setMinimumWidth(self.controlArea.sizeHint().width()) self.layout().setSizeConstraint(QLayout.SetFixedSize) widget_box = widgetBox(self.controlArea, 'Info') self.input_data_info = widgetLabel(widget_box, self._NO_DATA_INFO_TEXT) self.connection_info = widgetLabel(widget_box, "") widget_box = widgetBox(self.controlArea, 'Settings') self.cb_image_attr = comboBox( widget=widget_box, master=self, value='cb_image_attr_current_id', label='Image attribute:', orientation=Qt.Horizontal, callback=self._cb_image_attr_changed ) self.cb_embedder = comboBox( widget=widget_box, master=self, value='cb_embedder_current_id', label='Embedder:', orientation=Qt.Horizontal, callback=self._cb_embedder_changed ) self.cb_embedder.setModel(VariableListModel( [EMBEDDERS_INFO[e]['name'] for e in self.embedders])) if not self.cb_embedder_current_id < len(self.embedders): self.cb_embedder_current_id = 0 self.cb_embedder.setCurrentIndex(self.cb_embedder_current_id) current_embedder = self.embedders[self.cb_embedder_current_id] self.embedder_info = widgetLabel( widget_box, EMBEDDERS_INFO[current_embedder]['description'] ) self.auto_commit_widget = auto_commit( widget=self.controlArea, master=self, value='_auto_apply', label='Apply', commit=self.commit ) self.cancel_button = QPushButton( 'Cancel', icon=self.style().standardIcon(QStyle.SP_DialogCancelButton), ) self.cancel_button.clicked.connect(self.cancel) hbox = hBox(self.controlArea) hbox.layout().addWidget(self.cancel_button) self.cancel_button.setDisabled(True)
def test_select(self): widget = OWWidget() widget.foo = None self.attrs = VariableListModel() view = gui.listView(widget.controlArea, widget, "foo", model=self.attrs) self.assertIsNone(widget.foo) a, b, c = (ContinuousVariable(x) for x in "abc") self.attrs[:] = [a, b, c] view.setCurrentIndex(self.attrs.index(0, 0)) self.assertIs(widget.foo, a) view.setCurrentIndex(self.attrs.index(2, 0)) self.assertIs(widget.foo, c) widget.foo = b selection = view.selectedIndexes() self.assertEqual(len(selection), 1) self.assertEqual(selection[0].row(), 1)
def __init__(self): super().__init__() self.data = self.discrete_data = None self.areas = None self.input_features = None self.attrs = [] self.attr_box = gui.hBox(self.mainArea) model = VariableListModel() model.wrap(self.attrs) self.attrXCombo = gui.comboBox(self.attr_box, self, value="attrX", contentsLength=12, callback=self.change_attr, sendSelectedValue=True, valueType=str) self.attrXCombo.setModel(model) gui.widgetLabel(self.attr_box, "\u2715").\ setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) self.attrYCombo = gui.comboBox(self.attr_box, self, value="attrY", contentsLength=12, callback=self.change_attr, sendSelectedValue=True, valueType=str) self.attrYCombo.setModel(model) self.canvas = QGraphicsScene() self.canvasView = ViewWithPress(self.canvas, self.mainArea, handler=self.reset_selection) self.mainArea.layout().addWidget(self.canvasView) self.canvasView.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) self.canvasView.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) box = gui.hBox(self.mainArea) box.layout().addWidget(self.graphButton) box.layout().addWidget(self.report_button)
def __init__(self): super().__init__() self.corpus = None # Browse file box fbox = gui.widgetBox(self.controlArea, "Corpus file", orientation=0) widget = widgets.FileWidget(recent_files=self.recent_files, icon_size=(16, 16), on_open=self.open_file, directory_aliases={"Browse documentation corpora ...": get_sample_corpora_dir()}, dialog_format=self.dlgFormats, dialog_title='Open Orange Document Corpus', allow_empty=False, reload_label='Reload', browse_label='Browse') fbox.layout().addWidget(widget) # Corpus info ibox = gui.widgetBox(self.controlArea, "Corpus info", addSpace=True) corp_info = "Corpus of 0 documents." self.info_label = gui.label(ibox, self, corp_info) # Used Text Features fbox = gui.widgetBox(self.controlArea, orientation=0) ubox = gui.widgetBox(fbox, "Used text features", addSpace=True) self.used_attrs = VariableListModel(enable_dnd=True) self.used_attrs_view = VariablesListItemView() self.used_attrs_view.setModel(self.used_attrs) ubox.layout().addWidget(self.used_attrs_view) aa = self.used_attrs aa.dataChanged.connect(self.update_feature_selection) aa.rowsInserted.connect(self.update_feature_selection) aa.rowsRemoved.connect(self.update_feature_selection) # Ignored Text Features ibox = gui.widgetBox(fbox, "Ignored text features", addSpace=True) self.unused_attrs = VariableListModel(enable_dnd=True) self.unused_attrs_view = VariablesListItemView() self.unused_attrs_view.setModel(self.unused_attrs) ibox.layout().addWidget(self.unused_attrs_view) # load first file widget.select(0)
def _setup_layout(self): self.controlArea.setMinimumWidth(self.controlArea.sizeHint().width()) self.layout().setSizeConstraint(QLayout.SetFixedSize) widget_box = widgetBox(self.controlArea, "Settings") self.cb_image_attr = comboBox( widget=widget_box, master=self, value="cb_image_attr_current_id", label="Image attribute:", orientation=Qt.Horizontal, callback=self._cb_image_attr_changed, ) self.cb_embedder = comboBox( widget=widget_box, master=self, value="cb_embedder_current_id", label="Embedder:", orientation=Qt.Horizontal, callback=self._cb_embedder_changed, ) names = [ EMBEDDERS_INFO[e]["name"] + (" (local)" if EMBEDDERS_INFO[e].get("is_local") else "") for e in self.embedders ] self.cb_embedder.setModel(VariableListModel(names)) if not self.cb_embedder_current_id < len(self.embedders): self.cb_embedder_current_id = 0 self.cb_embedder.setCurrentIndex(self.cb_embedder_current_id) current_embedder = self.embedders[self.cb_embedder_current_id] self.embedder_info = widgetLabel( widget_box, EMBEDDERS_INFO[current_embedder]["description"]) self.auto_commit_widget = auto_commit( widget=self.controlArea, master=self, value="_auto_apply", label="Apply", commit=self.commit, ) self.cancel_button = QPushButton( "Cancel", icon=self.style().standardIcon(QStyle.SP_DialogCancelButton), ) self.cancel_button.clicked.connect(self.cancel) hbox = hBox(self.controlArea) hbox.layout().addWidget(self.cancel_button) self.cancel_button.setDisabled(True)
def __init__(self): # pylint: disable=missing-docstring super().__init__() self.data = self.discrete_data = None self.attrs = [] self.input_features = None self.areas = [] self.selection = set() self.attr_box = gui.hBox(self.mainArea) model = VariableListModel() model.wrap(self.attrs) combo_args = dict(widget=self.attr_box, master=self, contentsLength=12, callback=self.update_attr, sendSelectedValue=True, valueType=str, model=model) fixed_size = (QSizePolicy.Fixed, QSizePolicy.Fixed) self.attrXCombo = gui.comboBox(value="attrX", **combo_args) gui.widgetLabel(self.attr_box, "\u2715", sizePolicy=fixed_size) self.attrYCombo = gui.comboBox(value="attrY", **combo_args) self.vizrank, self.vizrank_button = SieveRank.add_vizrank( self.attr_box, self, "Score Combinations", self.set_attr) self.vizrank_button.setSizePolicy(*fixed_size) self.canvas = QGraphicsScene() self.canvasView = ViewWithPress(self.canvas, self.mainArea, handler=self.reset_selection) self.mainArea.layout().addWidget(self.canvasView) self.canvasView.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) self.canvasView.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) box = gui.hBox(self.mainArea) box.layout().addWidget(self.graphButton) box.layout().addWidget(self.report_button)
def __init__(self): self.data = None box = gui.vBox(self.controlArea, 'Differencing') gui.comboBox(box, self, 'chosen_operation', orientation=Qt.Horizontal, items=[el.value for el in self.Operation], label='Compute:', callback=self.on_changed, sendSelectedValue=True) self.order_spin = gui.spin( box, self, 'diff_order', 1, 2, label='Differencing order:', callback=self.on_changed, tooltip='The value corresponds to n-th order numerical ' 'derivative of the series. \nThe order is fixed to 1 ' 'if the shift period is other than 1.') gui.spin(box, self, 'shift_period', 1, 100, label='Shift:', callback=self.on_changed, tooltip='Set this to other than 1 if you don\'t want to ' 'compute differences for subsequent values but for ' 'values shifted number of spaces apart. \n' 'If this value is different from 1, differencing ' 'order is fixed to 1.') gui.checkBox(box, self, 'invert_direction', label='Invert differencing direction', callback=self.on_changed, tooltip='Influences where the series is padded with nan ' 'values — at the beginning or at the end.') self.view = view = QListView(self, selectionMode=QListView.ExtendedSelection) self.model = model = VariableListModel(parent=self) view.setModel(model) view.selectionModel().selectionChanged.connect(self.on_changed) box.layout().addWidget(view) gui.auto_commit(box, self, 'autocommit', '&Apply')
def __init__(self): hbox = gui.hBox(self.controlArea) _properties = dict(alternatingRowColors=True, defaultDropAction=Qt.MoveAction, dragDropMode=QListView.DragDrop, dragEnabled=True, selectionMode=QListView.ExtendedSelection, selectionBehavior=QListView.SelectRows, showDropIndicator=True, acceptDrops=True) listview_avail = DnDListView(lambda: self.commit(), self, **_properties) self.model_avail = model = VariableListModel(parent=self, enable_dnd=True) listview_avail.setModel(model) listview_key = DnDListView(lambda: self.commit(), self, **_properties) self.model_key = model = VariableListModel(parent=self, enable_dnd=True) listview_key.setModel(model) box = gui.vBox(hbox, 'Available Variables') box.layout().addWidget(listview_avail) box = gui.vBox(hbox, 'Group-By Key') box.layout().addWidget(listview_key) gui.comboBox(self.controlArea, self, 'tiebreaker', label='Which instance to select in each group:', items=tuple(self.TIEBREAKERS.keys()), callback=lambda: self.commit(), sendSelectedValue=True) gui.auto_commit(self.controlArea, self, 'autocommit', 'Commit', orientation=Qt.Horizontal)
def _create_layout(self): box = gui.widgetBox(self.controlArea, orientation='horizontal') self.varmodel = VariableListModel(parent=self) self.attr_combo = gui.comboBox(box, self, 'selected_attr', orientation=Qt.Horizontal, label='Region attribute:', callback=self.on_attr_change, sendSelectedValue=True) self.attr_combo.setModel(self.varmodel) self.map_combo = gui.comboBox(box, self, 'selected_map', orientation=Qt.Horizontal, label='Map type:', callback=self.on_map_change, items=Map.all) hexpand = QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) self.attr_combo.setSizePolicy(hexpand) self.map_combo.setSizePolicy(hexpand) url = urljoin('file:', pathname2url(os.path.join( os.path.dirname(__file__), 'resources', 'owdocmap.html'))) class Bridge(QObject): @pyqtSlot(str) def region_selected(_, regions): return self.region_selected(regions) self.webview = gui.WebviewWidget(self.controlArea, Bridge(), url=QUrl(url), debug=False) self.controlArea.layout().addWidget(self.webview) QTimer.singleShot( 0, lambda: self.webview.evalJS('REGIONS = {};'.format({Map.WORLD: CC_WORLD, Map.EUROPE: CC_EUROPE, Map.USA: CC_USA})))
def setData(self, data): self.closeContext() self.clear() self.data = data if data is not None: domain = data.domain self.allAttrs = (domain.class_vars + domain.metas + domain.attributes) self.stringAttrs = [a for a in domain.metas if a.is_string] self.stringAttrs = sorted(self.stringAttrs, key=lambda attr: 0 if "type" in attr.attributes else 1) indices = [ i for i, var in enumerate(self.stringAttrs) if var.attributes.get("type") == "image" ] if indices: self.imageAttr = indices[0] self.imageAttrCB.setModel(VariableListModel(self.stringAttrs)) self.titleAttrCB.setModel(VariableListModel(self.allAttrs)) self.openContext(data) self.imageAttr = max( min(self.imageAttr, len(self.stringAttrs) - 1), 0) self.titleAttr = max(min(self.titleAttr, len(self.allAttrs) - 1), 0) if self.stringAttrs: self.setupScene() else: self.info.setText("Waiting for input.\n")
def __init__(self): self.data = None self.indices = [] box = gui.vBox(self.controlArea, 'Axes') self.combo_ax2_model = VariableListModel(parent=self) self.combo_ax1_model = VariableListModel(parent=self) for model in (self.combo_ax1_model, self.combo_ax2_model): model[:] = [_enum_str(i) for i in Spiralogram.AxesCategories] self.combo_ax2 = gui.comboBox( box, self, 'ax2', label='Y axis:', callback=self.replot, sendSelectedValue=True, orientation='horizontal', model=self.combo_ax2_model) self.combo_ax1 = gui.comboBox( box, self, 'ax1', label='Radial:', callback=self.replot, sendSelectedValue=True, orientation='horizontal', model=self.combo_ax1_model) gui.checkBox(box, self, 'invert_date_order', 'Invert Y axis order', callback=self.replot) box = gui.vBox(self.controlArea, 'Aggregation') self.combo_func = gui.comboBox( box, self, 'agg_func', label='Function:', orientation='horizontal', callback=self.replot) func_model = ListModel(AGG_FUNCTIONS, parent=self) self.combo_func.setModel(func_model) self.attrlist_model = VariableListModel(parent=self) self.attrlist = QListView(selectionMode=QListView.SingleSelection) self.attrlist.setModel(self.attrlist_model) self.attrlist.selectionModel().selectionChanged.connect( self.attrlist_selectionChanged) box.layout().addWidget(self.attrlist) gui.rubber(self.controlArea) self.chart = chart = Spiralogram(self, selection_callback=self.on_selection) self.mainArea.layout().addWidget(chart)
def __init__(self): super().__init__() self.distances = None self.items = None self.tablemodel = DistanceMatrixModel() view = self.tableview = QTableView() view.setEditTriggers(QTableView.NoEditTriggers) view.setItemDelegate(TableBorderItem()) view.setModel(self.tablemodel) view.setShowGrid(False) for header in (view.horizontalHeader(), view.verticalHeader()): header.setResizeMode(QHeaderView.ResizeToContents) header.setHighlightSections(True) header.setClickable(False) view.verticalHeader().setDefaultAlignment(Qt.AlignRight | Qt.AlignVCenter) selmodel = SymmetricSelectionModel(view.model(), view) view.setSelectionModel(selmodel) view.setSelectionBehavior(QTableView.SelectItems) self.mainArea.layout().addWidget(view) settings_box = gui.widgetBox(self.mainArea, orientation="horizontal") self.annot_combo = gui.comboBox(settings_box, self, "annotation_idx", label="Labels: ", orientation="horizontal", callback=self._invalidate_annotations, contentsLength=12) self.annot_combo.setModel(VariableListModel()) self.annot_combo.model()[:] = ["None", "Enumeration"] gui.rubber(settings_box) self.report_button = gui.button(settings_box, None, "&Report", callback=self.show_report) self.report_button.setAutoDefault(0) gui.separator(settings_box, 40) gui.auto_commit(settings_box, self, "auto_commit", "Send Selected Data", "Auto send is on", box=None) # Signal must be connected after self.commit is redirected selmodel.selectionChanged.connect(self.commit)
def __init__(self): super().__init__() self.distances = None self.items = None self.tablemodel = DistanceMatrixModel() view = self.tableview = QTableView() view.setEditTriggers(QTableView.NoEditTriggers) view.setItemDelegate(TableBorderItem()) view.setModel(self.tablemodel) view.setShowGrid(False) for header in (view.horizontalHeader(), view.verticalHeader()): header.setSectionResizeMode(QHeaderView.ResizeToContents) header.setHighlightSections(True) header.setSectionsClickable(False) view.verticalHeader().setDefaultAlignment(Qt.AlignRight | Qt.AlignVCenter) selmodel = SymmetricSelectionModel(view.model(), view) view.setSelectionModel(selmodel) view.setSelectionBehavior(QTableView.SelectItems) self.mainArea.layout().addWidget(view) settings_box = gui.hBox(self.mainArea) self.annot_combo = gui.comboBox( settings_box, self, "annotation_idx", label="Labels: ", orientation=Qt.Horizontal, callback=self._invalidate_annotations, contentsLength=12, ) self.annot_combo.setModel(VariableListModel()) self.annot_combo.model()[:] = ["None", "Enumeration"] gui.rubber(settings_box) acb = gui.auto_commit( settings_box, self, "auto_commit", "Send Selected", "Send Automatically", box=None, ) acb.setFixedWidth(200) # Signal must be connected after self.commit is redirected selmodel.selectionChanged.connect(self.commit)
def _create_layout(self): box = gui.widgetBox(self.controlArea, orientation='horizontal') self.varmodel = VariableListModel(parent=self) self.attr_combo = gui.comboBox(box, self, 'selected_attr', orientation=Qt.Horizontal, label='Region attribute:', callback=self.on_attr_change, sendSelectedValue=True) self.attr_combo.setModel(self.varmodel) self.map_combo = gui.comboBox(box, self, 'selected_map', orientation=Qt.Horizontal, label='Map type:', callback=self.on_map_change, items=Map.all) hexpand = QtGui.QSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Fixed) self.attr_combo.setSizePolicy(hexpand) self.map_combo.setSizePolicy(hexpand) html = ''' <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <base href="{}/"/> <style> html, body, #map {{margin:0px;padding:0px;width:100%;height:100%;}} </style> <link href="resources/jquery-jvectormap-2.0.2.css" rel="stylesheet"> <script src="resources/jquery-2.1.4.min.js"></script> <script src="resources/jquery-jvectormap-2.0.2.min.js"></script> <script src="resources/jquery-jvectormap-world-mill-en.js"></script> <script src="resources/jquery-jvectormap-europe-mill-en.js"></script> <script src="resources/jquery-jvectormap-us-aea-en.js"></script> <script src="resources/geomap-script.js"></script> </head> <body> <div id="map"></div> </body> </html>'''.format(urljoin('file:', pathname2url(path.abspath(path.dirname(__file__))))) self.webview = gui.WebviewWidget(self.controlArea, self, debug=False) self.controlArea.layout().addWidget(self.webview) self.webview.setHtml(html) QTimer.singleShot( 0, lambda: self.webview.evalJS('REGIONS = {};'.format({Map.WORLD: CC_WORLD, Map.EUROPE: CC_EUROPE, Map.USA: CC_USA})))
def __init__(self): self.data = None self.plots = [] self.configs = [] self.forecasts = OrderedDict() self.varmodel = VariableListModel(parent=self) icon = QIcon(join(dirname(__file__), 'icons', 'LineChart-plus.png')) self.add_button = button = QPushButton(icon, ' &Add plot', self) button.clicked.connect(self.add_plot) self.controlArea.layout().addWidget(button) self.configsArea = gui.vBox(self.controlArea) self.controlArea.layout().addStretch(1) # TODO: allow selecting ranges that are sent to output as subset table self.chart = highstock = Highstock(self, highchart='StockChart') self.mainArea.layout().addWidget(highstock) highstock.chart() QTimer.singleShot(0, self.add_plot)
def set_corpus(self, corpus): self.corpus = corpus if corpus is not None: self.strings_attrs = [a for a in self.corpus.domain.metas if isinstance(a, StringVariable)] self.tweet_attr_combo.setModel(VariableListModel(self.strings_attrs)) self.tweet_attr_combo.currentIndexChanged.emit(self.tweet_attr) # select the first feature from 'text_features' if present ind = [self.strings_attrs.index(tf) for tf in corpus.text_features if tf in self.strings_attrs] if ind: self.tweet_attr = ind[0] self.commit()
def test_delegate(self): cases = ( (DState(Default(Leave()), None, None), ""), (DState(Leave(), None, None), "(leave)"), (DState(MDL(), [1], None), "(entropy)"), (DState(MDL(), [], None), "<removed>"), (DState(EqualFreq(2), [1], None), "(equal frequency k=2)"), (DState(EqualWidth(2), [1], None), "(equal width k=2)"), (DState(Remove(), None, None), "(removed)"), (DState(Custom([1]), None, None), "(custom)"), ) delegate = DiscDelegate() var = DiscreteVariable("C", ("a", "b")) model = VariableListModel() model.append(var) for state, text in cases: model.setData(model.index(0), state, Qt.UserRole) option = QStyleOptionViewItem() delegate.initStyleOption(option, model.index(0)) self.assertIn(text, option.text)
def __init__(self): super().__init__() self.network = None self.auto_data = None self.original_nodes = None self.data = None self.net_index = 0 hb = gui.widgetBox(self.controlArea, orientation=Qt.Horizontal) self.filecombo = gui.comboBox(hb, self, "net_index", callback=self.select_net_file, minimumWidth=250) gui.button(hb, self, '...', callback=self.browse_net_file, disabled=0, icon=self.style().standardIcon(QStyle.SP_DirOpenIcon), sizePolicy=(QSizePolicy.Maximum, QSizePolicy.Fixed)) gui.button(hb, self, 'Reload', callback=self.reload, icon=self.style().standardIcon(QStyle.SP_BrowserReload), sizePolicy=(QSizePolicy.Maximum, QSizePolicy.Fixed)) self.label_model = VariableListModel(placeholder="(Match by rows)") self.label_model[:] = [None] gui.comboBox(self.controlArea, self, "label_variable", box=True, label="Match node labels to data column: ", orientation=Qt.Horizontal, model=self.label_model, callback=self.label_changed) self.populate_comboboxes() self.setFixedHeight(self.sizeHint().height()) self.reload()
def __init__(self): self.data = None box = gui.vBox(self.controlArea, 'Seasonal Adjustment') gui.spin(box, self, 'n_periods', 2, 1000, label='Season period:', callback=self.on_changed, tooltip='The expected length of full cycle. E.g., if you have ' 'monthly data and the apparent season repeats every ' 'year, you put in 12.') gui.radioButtons(box, self, 'decomposition', self.DECOMPOSITION_MODELS, label='Decomposition model:', orientation=Qt.Horizontal, callback=self.on_changed) self.view = view = QListView(self, selectionMode=QListView.ExtendedSelection) self.model = model = VariableListModel(parent=self) view.setModel(model) view.selectionModel().selectionChanged.connect(self.on_changed) box.layout().addWidget(view) gui.auto_commit(box, self, 'autocommit', '&Apply')
def __init__(self): super().__init__() self.distances = None self.items = None self.tablemodel = DistanceMatrixModel() view = self.tableview = TableView() view.setWordWrap(False) view.setTextElideMode(Qt.ElideNone) view.setEditTriggers(QTableView.NoEditTriggers) view.setItemDelegate( TableBorderItem(roles=(Qt.DisplayRole, Qt.BackgroundRole, Qt.ForegroundRole))) view.setModel(self.tablemodel) view.setShowGrid(False) for header in (view.horizontalHeader(), view.verticalHeader()): header.setResizeContentsPrecision(1) header.setSectionResizeMode(QHeaderView.ResizeToContents) header.setHighlightSections(True) header.setSectionsClickable(False) view.verticalHeader().setDefaultAlignment(Qt.AlignRight | Qt.AlignVCenter) selmodel = SymmetricSelectionModel(view.model(), view) selmodel.selectionChanged.connect(self.commit.deferred) view.setSelectionModel(selmodel) view.setSelectionBehavior(QTableView.SelectItems) self.controlArea.layout().addWidget(view) self.annot_combo = gui.comboBox(self.buttonsArea, self, "annotation_idx", label="标签: ", orientation=Qt.Horizontal, callback=self._invalidate_annotations, contentsLength=12) self.annot_combo.setModel(VariableListModel()) self.annot_combo.model()[:] = ["无", "枚举"] gui.rubber(self.buttonsArea) acb = gui.auto_send(self.buttonsArea, self, "auto_commit", box=False) acb.setFixedWidth(200)
def set_data(self, data): self.closeContext() self.clear() self.Warning.no_valid_data.clear() self.data = data if data is not None: domain = data.domain self.allAttrs = (domain.class_vars + domain.metas + domain.attributes) self.stringAttrs = [a for a in domain.metas if a.is_string] self.domainAttrs = len(domain.attributes) self.stringAttrs = sorted(self.stringAttrs, key=lambda attr: 0 if "type" in attr.attributes else 1) indices = [ i for i, var in enumerate(self.stringAttrs) if var.attributes.get("type") == "image" ] if indices: self.imageAttr = indices[0] self.imageAttrCB.setModel(VariableListModel(self.stringAttrs)) # set label combo labels self.label_model.set_domain(domain) self.openContext(data) self.label_attr = self.label_model[0] self.imageAttr = max( min(self.imageAttr, len(self.stringAttrs) - 1), 0) if self.is_valid_data(): self.image_grid = ImageGrid(data) self.setup_scene() else: self.Warning.no_valid_data()
def __init__(self): self.data = None box = gui.vBox(self.controlArea, 'Sequence') group = gui.radioButtons(box, self, 'radio_sequential', callback=self.on_changed) hbox = gui.hBox(box) gui.appendRadioButton(group, 'Sequential attribute:', insertInto=hbox) attrs_model = self.attrs_model = VariableListModel() combo_attrs = self.combo_attrs = gui.comboBox(hbox, self, 'selected_attr', callback=self.on_changed, sendSelectedValue=True) combo_attrs.setModel(attrs_model) gui.appendRadioButton(group, 'Sequence is implied by instance order', insertInto=box) gui.auto_commit(self.controlArea, self, 'autocommit', '&Apply')
def dataset(self, data): self.clear() self.data = data if self.data: # error check if len(self.data) == 0: self.Error.no_instances() return self._smiles_attrs = moleculeembedder.filter_string_attributes(data) if not self._smiles_attrs: self.Error.no_string_att() return # update selection self.controls._smiles_attr.setModel(VariableListModel(self._smiles_attrs)) if self._smiles_attr == '' or self._smiles_attr is None: self._smiles_attr = self._smiles_attrs[0] if self._embedder == '' or self._embedder is None: self._embedder = self._embedders[0] self.commit()
def __init__(self): super().__init__() self.network = None form = QFormLayout() form.setFieldGrowthPolicy(form.AllNonFixedFieldsGrow) gui.widgetBox(self.controlArea, box="Mode indicator", orientation=form) form.addRow( "Feature:", gui.comboBox(None, self, "variable", model=VariableListModel(), callback=self.indicator_changed)) form.addRow( "Connect:", gui.comboBox(None, self, "connect_value", callback=self.connect_combo_changed)) form.addRow( "by:", gui.comboBox(None, self, "connector_value", callback=self.connector_combo_changed)) gui.comboBox(self.controlArea, self, "weighting", box="Edge weights", items=[x.name for x in twomode.Weighting], callback=self.update_output) self.lbout = gui.widgetLabel(gui.hBox(self.controlArea, "Output"), "") self._update_combos() self._set_output_msg()
def set_data(self, data): self.Warning.clear() self.set_input_data_summary(data) if not data: self._input_data = None self.clear_outputs() return self._image_attributes = ImageEmbedder.filter_image_attributes(data) if not self.cb_image_attr_current_id < len(self._image_attributes): self.cb_image_attr_current_id = 0 self.cb_image_attr.setModel(VariableListModel(self._image_attributes)) self.cb_image_attr.setCurrentIndex(self.cb_image_attr_current_id) if not self._image_attributes: self._input_data = None self.Warning.no_image_attribute() self.clear_outputs() return self._input_data = data self.commit()
def __init__(self): self.data = None self.plots = [] self.configs = [] self.forecasts = OrderedDict() self.varmodel = VariableListModel(parent=self) icon = QIcon(join(dirname(__file__), 'icons', 'LineChart-plus.png')) self.add_button = button = QPushButton(icon, ' &Add plot', self) button.clicked.connect(self.add_plot) self.controlArea.layout().addWidget(button) self.configsArea = gui.vBox(self.controlArea) self.controlArea.layout().addStretch(1) # TODO: allow selecting ranges that are sent to output as subset table self.chart = highstock = Highstock(self, highchart='StockChart') self.mainArea.layout().addWidget(highstock) # highstock.evalJS('Highcharts.setOptions({navigator: {enabled:false}});') highstock.chart( # For some reason, these options don't work as global opts applied at Highstock init time # Disable top range selector rangeSelector_enabled=False, rangeSelector_inputEnabled=False, # Disable bottom miniview navigator (it doesn't update) navigator_enabled=False, ) QTimer.singleShot(0, self.add_plot)
def __init__(self): super().__init__() self.data = None self.projection = None self.subset_data = None self._subset_mask = None self._selection = None self.__replot_requested = False self.n_cont_var = 0 #: Remember the saved state to restore self.__pending_selection_restore = self.selection_indices self.selection_indices = None self.variable_x = None self.variable_y = None box = gui.vBox(self.mainArea, True, margin=0) self.graph = OWLinProjGraph(self, box, "Plot", view_box=LinProjInteractiveViewBox) box.layout().addWidget(self.graph.plot_widget) plot = self.graph.plot_widget SIZE_POLICY = (QSizePolicy.Minimum, QSizePolicy.Maximum) self.variables_selection = VariablesSelection() self.model_selected = VariableListModel(enable_dnd=True) self.model_other = VariableListModel(enable_dnd=True) self.variables_selection(self, self.model_selected, self.model_other) self.vizrank, self.btn_vizrank = LinearProjectionVizRank.add_vizrank( self.controlArea, self, "Suggest Features", self._vizrank) self.variables_selection.add_remove.layout().addWidget(self.btn_vizrank) box = gui.widgetBox( self.controlArea, "Placement", sizePolicy=SIZE_POLICY) self.radio_placement = gui.radioButtonsInBox( box, self, "placement", btnLabels=["Circular Placement", "Linear Discriminant Analysis", "Principal Component Analysis", "Use input projection"], callback=self._change_placement ) self.viewbox = plot.getViewBox() self.replot = None g = self.graph.gui box = g.point_properties_box(self.controlArea) self.models = g.points_models g.add_widget(g.JitterSizeSlider, box) box.setSizePolicy(*SIZE_POLICY) box = gui.widgetBox(self.controlArea, "Hide axes", sizePolicy=SIZE_POLICY) self.rslider = gui.hSlider( box, self, "radius", minValue=0, maxValue=100, step=5, label="Radius", createLabel=False, ticks=True, callback=self.update_radius) self.rslider.setTickInterval(0) self.rslider.setPageStep(10) box = gui.vBox(self.controlArea, "Plot Properties") box.setSizePolicy(*SIZE_POLICY) g.add_widgets([g.ShowLegend, g.ToolTipShowsAll, g.ClassDensity, g.LabelOnlySelected], box) box = self.graph.box_zoom_select(self.controlArea) box.setSizePolicy(*SIZE_POLICY) self.icons = gui.attributeIconDict p = self.graph.plot_widget.palette() self.graph.set_palette(p) gui.auto_commit(self.controlArea, self, "auto_commit", "Send Selection", auto_label="Send Automatically") self.graph.zoom_actions(self) self._new_plotdata() self._change_placement() self.graph.jitter_continuous = True
class OWMovingTransform(widget.OWWidget): name = 'Moving Transform' description = 'Apply rolling window functions to the time series.' icon = 'icons/MovingTransform.svg' priority = 20 inputs = [("Time series", Table, 'set_data')] outputs = [("Time series", Timeseries)] want_main_area = False non_overlapping = settings.Setting(False) fixed_wlen = settings.Setting(5) transformations = settings.Setting([]) autocommit = settings.Setting(False) last_win_width = settings.Setting(5) _NON_OVERLAPPING_WINDOWS = 'Non-overlapping windows' UserAdviceMessages = [ widget.Message('Get the simple moving average (SMA) of a series ' 'by setting the aggregation function to "{}".'.format(Mean), 'sma-is-mean'), widget.Message('If "{}" is checked, the rolling windows don\t ' 'overlap. Instead, they run through the series ' 'side-to-side, so the resulting transformed series is ' 'fixed-window-length-times shorter.'.format(_NON_OVERLAPPING_WINDOWS), 'non-overlapping') ] def __init__(self): self.data = None box = gui.vBox(self.controlArea, 'Moving Transform') def _disable_fixed_wlen(): fixed_wlen.setDisabled(not self.non_overlapping) self.view.repaint() self.on_changed() gui.checkBox(box, self, 'non_overlapping', label=self._NON_OVERLAPPING_WINDOWS, callback=_disable_fixed_wlen, tooltip='If this is checked, instead of rolling windows ' 'through the series, they are applied side-to-side, ' 'so the resulting output series will be some ' 'length-of-fixed-window-times shorter.') fixed_wlen = gui.spin(box, self, 'fixed_wlen', 2, 1000, label='Fixed window width:', callback=self.on_changed) fixed_wlen.setDisabled(not self.non_overlapping) # TODO: allow the user to choose left-aligned, right-aligned, or center-aligned window class TableView(gui.TableView): def __init__(self, parent): super().__init__(parent, editTriggers=(self.SelectedClicked | self.CurrentChanged | self.DoubleClicked | self.EditKeyPressed), ) self.horizontalHeader().setStretchLastSection(False) agg_functions = ListModel(AGG_FUNCTIONS + [Cumulative_sum, Cumulative_product], parent=self) self.setItemDelegateForColumn(0, self.VariableDelegate(parent)) self.setItemDelegateForColumn(1, self.SpinDelegate(parent)) self.setItemDelegateForColumn(2, self.ComboDelegate(self, agg_functions)) class _ItemDelegate(QStyledItemDelegate): def updateEditorGeometry(self, widget, option, _index): widget.setGeometry(option.rect) class ComboDelegate(_ItemDelegate): def __init__(self, parent=None, combo_model=None): super().__init__(parent) self._parent = parent if combo_model is not None: self._combo_model = combo_model def createEditor(self, parent, _QStyleOptionViewItem, index): combo = QComboBox(parent) combo.setModel(self._combo_model) return combo def setEditorData(self, combo, index): var = index.model().data(index, Qt.EditRole) combo.setCurrentIndex(self._combo_model.indexOf(var)) def setModelData(self, combo, model, index): var = self._combo_model[combo.currentIndex()] model.setData(index, var, Qt.EditRole) class VariableDelegate(ComboDelegate): @property def _combo_model(self): return self._parent.var_model class SpinDelegate(_ItemDelegate): def paint(self, painter, option, index): # Don't paint window length if non-overlapping windows set if not self.parent().non_overlapping: super().paint(painter, option, index) def createEditor(self, parent, _QStyleOptionViewItem, _index): # Don't edit window length if non-overlapping windows set if self.parent().non_overlapping: return None spin = QSpinBox(parent, minimum=1, maximum=1000) return spin def setEditorData(self, spin, index): spin.setValue(index.model().data(index, Qt.EditRole)) def setModelData(self, spin, model, index): spin.interpretText() model.setData(index, spin.value(), Qt.EditRole) self.var_model = VariableListModel(parent=self) self.table_model = model = PyTableModel(self.transformations, parent=self, editable=True) model.setHorizontalHeaderLabels(['Series', 'Window width', 'Aggregation function']) model.dataChanged.connect(self.on_changed) self.view = view = TableView(self) view.setModel(model) box.layout().addWidget(view) hbox = gui.hBox(box) from os.path import dirname, join self.add_button = button = gui.button( hbox, self, 'Add &Transform', callback=self.on_add_transform) button.setIcon(QIcon(join(dirname(__file__), 'icons', 'LineChart-plus.png'))) self.del_button = button = gui.button( hbox, self, '&Delete Selected', callback=self.on_del_transform) QIcon.setThemeName('gnome') # Works for me button.setIcon(QIcon.fromTheme('edit-delete')) gui.auto_commit(box, self, 'autocommit', '&Apply') def sizeHint(self): return QSize(450, 600) def on_add_transform(self): if self.data is not None: self.table_model.append([self.var_model[0], self.last_win_width, AGG_FUNCTIONS[0]]) self.commit() def on_del_transform(self): for row in sorted([mi.row() for mi in self.view.selectionModel().selectedRows(0)], reverse=True): del self.table_model[row] if len(self.table_model): selection_model = self.view.selectionModel() selection_model.select(self.table_model.index(len(self.table_model) - 1, 0), selection_model.Select | selection_model.Rows) self.commit() def set_data(self, data): self.data = data = None if data is None else Timeseries.from_data_table(data) self.add_button.setDisabled(not len(getattr(data, 'domain', ()))) self.table_model.clear() if data is not None: self.var_model.wrap([var for var in data.domain if var.is_continuous and var is not data.time_variable]) self.on_changed() def on_changed(self): self.commit() def commit(self): data = self.data if not data: self.send(Output.TIMESERIES, None) return ts = moving_transform(data, self.table_model, self.non_overlapping and self.fixed_wlen) self.send(Output.TIMESERIES, ts)
def __init__(self): self.data = None box = gui.vBox(self.controlArea, 'Moving Transform') def _disable_fixed_wlen(): fixed_wlen.setDisabled(not self.non_overlapping) self.view.repaint() self.on_changed() gui.checkBox(box, self, 'non_overlapping', label=self._NON_OVERLAPPING_WINDOWS, callback=_disable_fixed_wlen, tooltip='If this is checked, instead of rolling windows ' 'through the series, they are applied side-to-side, ' 'so the resulting output series will be some ' 'length-of-fixed-window-times shorter.') fixed_wlen = gui.spin(box, self, 'fixed_wlen', 2, 1000, label='Fixed window width:', callback=self.on_changed) fixed_wlen.setDisabled(not self.non_overlapping) # TODO: allow the user to choose left-aligned, right-aligned, or center-aligned window class TableView(gui.TableView): def __init__(self, parent): super().__init__(parent, editTriggers=(self.SelectedClicked | self.CurrentChanged | self.DoubleClicked | self.EditKeyPressed), ) self.horizontalHeader().setStretchLastSection(False) agg_functions = ListModel(AGG_FUNCTIONS + [Cumulative_sum, Cumulative_product], parent=self) self.setItemDelegateForColumn(0, self.VariableDelegate(parent)) self.setItemDelegateForColumn(1, self.SpinDelegate(parent)) self.setItemDelegateForColumn(2, self.ComboDelegate(self, agg_functions)) class _ItemDelegate(QStyledItemDelegate): def updateEditorGeometry(self, widget, option, _index): widget.setGeometry(option.rect) class ComboDelegate(_ItemDelegate): def __init__(self, parent=None, combo_model=None): super().__init__(parent) self._parent = parent if combo_model is not None: self._combo_model = combo_model def createEditor(self, parent, _QStyleOptionViewItem, index): combo = QComboBox(parent) combo.setModel(self._combo_model) return combo def setEditorData(self, combo, index): var = index.model().data(index, Qt.EditRole) combo.setCurrentIndex(self._combo_model.indexOf(var)) def setModelData(self, combo, model, index): var = self._combo_model[combo.currentIndex()] model.setData(index, var, Qt.EditRole) class VariableDelegate(ComboDelegate): @property def _combo_model(self): return self._parent.var_model class SpinDelegate(_ItemDelegate): def paint(self, painter, option, index): # Don't paint window length if non-overlapping windows set if not self.parent().non_overlapping: super().paint(painter, option, index) def createEditor(self, parent, _QStyleOptionViewItem, _index): # Don't edit window length if non-overlapping windows set if self.parent().non_overlapping: return None spin = QSpinBox(parent, minimum=1, maximum=1000) return spin def setEditorData(self, spin, index): spin.setValue(index.model().data(index, Qt.EditRole)) def setModelData(self, spin, model, index): spin.interpretText() model.setData(index, spin.value(), Qt.EditRole) self.var_model = VariableListModel(parent=self) self.table_model = model = PyTableModel(self.transformations, parent=self, editable=True) model.setHorizontalHeaderLabels(['Series', 'Window width', 'Aggregation function']) model.dataChanged.connect(self.on_changed) self.view = view = TableView(self) view.setModel(model) box.layout().addWidget(view) hbox = gui.hBox(box) from os.path import dirname, join self.add_button = button = gui.button( hbox, self, 'Add &Transform', callback=self.on_add_transform) button.setIcon(QIcon(join(dirname(__file__), 'icons', 'LineChart-plus.png'))) self.del_button = button = gui.button( hbox, self, '&Delete Selected', callback=self.on_del_transform) QIcon.setThemeName('gnome') # Works for me button.setIcon(QIcon.fromTheme('edit-delete')) gui.auto_commit(box, self, 'autocommit', '&Apply')
class OWGeoMap(widget.OWWidget): name = "GeoMap" priority = 20000 icon = "icons/GeoMap.svg" inputs = [("Data", Table, "on_data")] outputs = [('Corpus', Corpus)] want_main_area = False selected_attr = settings.Setting('') selected_map = settings.Setting(0) regions = settings.Setting([]) def __init__(self): super().__init__() self.data = None self._create_layout() @QtCore.pyqtSlot(str, result=str) def region_selected(self, regions): """Called from JavaScript""" if not regions: self.regions = [] if not regions or self.data is None: return self.send('Corpus', None) self.regions = regions.split(',') attr = self.data.domain[self.selected_attr] if attr.is_discrete: return # TODO, FIXME: make this work for discrete attrs also from Orange.data.filter import FilterRegex filter = FilterRegex(attr, r'\b{}\b'.format(r'\b|\b'.join(self.regions)), re.IGNORECASE) self.send('Corpus', self.data._filter_values(filter)) def _create_layout(self): box = gui.widgetBox(self.controlArea, orientation='horizontal') self.varmodel = VariableListModel(parent=self) self.attr_combo = gui.comboBox(box, self, 'selected_attr', orientation=Qt.Horizontal, label='Region attribute:', callback=self.on_attr_change, sendSelectedValue=True) self.attr_combo.setModel(self.varmodel) self.map_combo = gui.comboBox(box, self, 'selected_map', orientation=Qt.Horizontal, label='Map type:', callback=self.on_map_change, items=Map.all) hexpand = QtGui.QSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Fixed) self.attr_combo.setSizePolicy(hexpand) self.map_combo.setSizePolicy(hexpand) html = ''' <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <base href="{}/"/> <style> html, body, #map {{margin:0px;padding:0px;width:100%;height:100%;}} </style> <link href="resources/jquery-jvectormap-2.0.2.css" rel="stylesheet"> <script src="resources/jquery-2.1.4.min.js"></script> <script src="resources/jquery-jvectormap-2.0.2.min.js"></script> <script src="resources/jquery-jvectormap-world-mill-en.js"></script> <script src="resources/jquery-jvectormap-europe-mill-en.js"></script> <script src="resources/jquery-jvectormap-us-aea-en.js"></script> <script src="resources/geomap-script.js"></script> </head> <body> <div id="map"></div> </body> </html>'''.format(urljoin('file:', pathname2url(path.abspath(path.dirname(__file__))))) self.webview = gui.WebviewWidget(self.controlArea, self, debug=False) self.controlArea.layout().addWidget(self.webview) self.webview.setHtml(html) QTimer.singleShot( 0, lambda: self.webview.evalJS('REGIONS = {};'.format({Map.WORLD: CC_WORLD, Map.EUROPE: CC_EUROPE, Map.USA: CC_USA}))) def _repopulate_attr_combo(self, data): vars = [a for a in chain(data.domain.metas, data.domain.attributes, data.domain.class_vars) if a.is_string] if data else [] self.varmodel.wrap(vars) # Select default attribute self.selected_attr = next((var.name for var in vars if var.name.lower().startswith(('country', 'location', 'region'))), vars[0].name if vars else '') def on_data(self, data): if data and not isinstance(data, Corpus): data = Corpus.from_table(data.domain, data) self.data = data self._repopulate_attr_combo(data) if not data: self.region_selected('') QTimer.singleShot(0, lambda: self.webview.evalJS('DATA = {}; renderMap();')) else: QTimer.singleShot(0, self.on_attr_change) def on_map_change(self, map_code=''): if map_code: self.map_combo.setCurrentIndex(self.map_combo.findData(map_code)) else: map_code = self.map_combo.itemData(self.selected_map) inv_cc_map, cc_map = {Map.USA: (INV_CC_USA, CC_USA), Map.WORLD: (INV_CC_WORLD, CC_WORLD), Map.EUROPE: (INV_CC_EUROPE, CC_EUROPE)} [map_code] # Set country counts in JS data = defaultdict(int) for cc in getattr(self, 'cc_counts', ()): key = inv_cc_map.get(cc, cc) if key in cc_map: data[key] += self.cc_counts[cc] # Draw the new map self.webview.evalJS('DATA = {};' 'MAP_CODE = "{}";' 'SELECTED_REGIONS = {};' 'renderMap();'.format(dict(data), map_code, self.regions)) def on_attr_change(self): if not self.selected_attr: return attr = self.data.domain[self.selected_attr] self.cc_counts = Counter(chain.from_iterable( set(name.strip() for name in CC_NAMES.findall(i.lower())) if len(i) > 3 else (i,) for i in self.data.get_column_view(self.data.domain.index(attr))[0])) # Auto-select region map values = set(self.cc_counts) if 0 == len(values - SET_CC_USA): map_code = Map.USA elif 0 == len(values - SET_CC_EUROPE): map_code = Map.EUROPE else: map_code = Map.WORLD self.on_map_change(map_code)
class OWLineChart(widget.OWWidget): name = 'Line Chart' description = "Visualize time series' sequence and progression." icon = 'icons/LineChart.svg' priority = 90 class Inputs: time_series = Input("Time series", Table) forecast = Input("Forecast", Timeseries, multiple=True) attrs = settings.Setting({}) # Maps data.name -> [attrs] graph_name = 'chart' def __init__(self): self.data = None self.plots = [] self.configs = [] self.forecasts = OrderedDict() self.varmodel = VariableListModel(parent=self) icon = QIcon(join(dirname(__file__), 'icons', 'LineChart-plus.png')) self.add_button = button = QPushButton(icon, ' &Add plot', self) button.clicked.connect(self.add_plot) self.controlArea.layout().addWidget(button) self.configsArea = gui.vBox(self.controlArea) self.controlArea.layout().addStretch(1) # TODO: allow selecting ranges that are sent to output as subset table self.chart = highstock = Highstock(self, highchart='StockChart') self.mainArea.layout().addWidget(highstock) # highstock.evalJS('Highcharts.setOptions({navigator: {enabled:false}});') highstock.chart( # For some reason, these options don't work as global opts applied at Highstock init time # Disable top range selector rangeSelector_enabled=False, rangeSelector_inputEnabled=False, # Disable bottom miniview navigator (it doesn't update) navigator_enabled=False, ) QTimer.singleShot(0, self.add_plot) def add_plot(self): ax = self.chart.addAxis() config = PlotConfigWidget(self, ax, self.varmodel) # Connect the signals config.sigSelection.connect(self.chart.setSeries) config.sigLogarithmic.connect(self.chart.setLogarithmic) config.sigType.connect(self.chart.setType) config.sigClosed.connect(self.chart.removeAxis) config.sigClosed.connect(lambda ax, widget: widget.setParent(None)) config.sigClosed.connect(lambda ax, widget: self.add_button.setDisabled(False)) self.configs.append(config) self.add_button.setDisabled(len(self.configs) >= 5) self.configsArea.layout().addWidget(config) @Inputs.time_series def set_data(self, data): # TODO: set xAxis resolution and tooltip time contents depending on # data.time_delta. See: http://imgur.com/yrnlgQz # If the same data is updated, short circuit to just updating the chart, # retaining all panels and list view selections ... if data is not None and self.data is not None and data.domain == self.data.domain: self.data = Timeseries.from_data_table(data) for config in self.configs: config.selection_changed() return self.data = data = None if data is None else Timeseries.from_data_table(data) if data is None: self.varmodel.clear() self.chart.clear() return if getattr(data.time_variable, 'utc_offset', False): offset_minutes = data.time_variable.utc_offset.total_seconds() / 60 self.chart.evalJS('Highcharts.setOptions({global: {timezoneOffset: %d}});' % -offset_minutes) # Why is this negative? It works. self.chart.chart() self.chart.setXAxisType( 'datetime' if (data.time_variable and (getattr(data.time_variable, 'have_date', False) or getattr(data.time_variable, 'have_time', False))) else 'linear') self.varmodel.wrap([var for var in data.domain.variables if var.is_continuous and var != data.time_variable]) @Inputs.forecast def set_forecast(self, forecast, id): if forecast is not None: self.forecasts[id] = forecast else: self.forecasts.pop(id, None)
class OWSpiralogram(widget.OWWidget): name = 'Spiralogram' description = "Visualize time series' periodicity in a spiral heatmap." icon = 'icons/Spiralogram.svg' priority = 120 class Inputs: time_series = Input("Time series", Table) class Outputs: time_series = Output("Time series", Timeseries) settingsHandler = settings.DomainContextHandler() ax1 = settings.ContextSetting('months of year') ax2 = settings.ContextSetting('years') agg_attr = settings.ContextSetting([]) agg_func = settings.ContextSetting(0) invert_date_order = settings.Setting(False) graph_name = 'chart' class Error(widget.OWWidget.Error): no_time_variable = widget.Msg( 'Spiralogram requires time series with a time variable.') def __init__(self): self.data = None self.indices = [] box = gui.vBox(self.controlArea, 'Axes') self.combo_ax2_model = VariableListModel(parent=self) self.combo_ax1_model = VariableListModel(parent=self) for model in (self.combo_ax1_model, self.combo_ax2_model): model[:] = [_enum_str(i) for i in Spiralogram.AxesCategories] self.combo_ax2 = gui.comboBox(box, self, 'ax2', label='Y axis:', callback=self.replot, sendSelectedValue=True, orientation='horizontal', model=self.combo_ax2_model) self.combo_ax1 = gui.comboBox(box, self, 'ax1', label='Radial:', callback=self.replot, sendSelectedValue=True, orientation='horizontal', model=self.combo_ax1_model) gui.checkBox(box, self, 'invert_date_order', 'Invert Y axis order', callback=self.replot) box = gui.vBox(self.controlArea, 'Aggregation') self.combo_func = gui.comboBox(box, self, 'agg_func', label='Function:', orientation='horizontal', callback=self.replot) func_model = ListModel(AGG_FUNCTIONS, parent=self) self.combo_func.setModel(func_model) self.attrlist_model = VariableListModel(parent=self) self.attrlist = QListView(selectionMode=QListView.SingleSelection) self.attrlist.setModel(self.attrlist_model) self.attrlist.selectionModel().selectionChanged.connect( self.attrlist_selectionChanged) box.layout().addWidget(self.attrlist) gui.rubber(self.controlArea) self.chart = chart = Spiralogram(self, selection_callback=self.on_selection) self.mainArea.layout().addWidget(chart) def attrlist_selectionChanged(self): self.agg_attr = [ self.attrlist_model[i.row()] for i in self.attrlist.selectionModel().selectedIndexes() ] self.replot() @Inputs.time_series def set_data(self, data): self.Error.clear() self.data = data = None if data is None else Timeseries.from_data_table( data) if data is None: self.commit() return if self.data.time_variable is None or not isinstance( self.data.time_variable, TimeVariable): self.Error.no_time_variable() self.commit() return def init_combos(): for model in (self.combo_ax1_model, self.combo_ax2_model): model.clear() newmodel = [] if data is not None and data.time_variable is not None: for model in (self.combo_ax1_model, self.combo_ax2_model): model[:] = [ _enum_str(i) for i in Spiralogram.AxesCategories ] for var in data.domain.variables if data is not None else []: if (var.is_primitive() and (var is not data.time_variable or isinstance( var, TimeVariable) and data.time_delta is None)): newmodel.append(var) if var.is_discrete: for model in (self.combo_ax1_model, self.combo_ax2_model): model.append(var) self.attrlist_model.wrap(newmodel) init_combos() self.chart.clear() self.closeContext() self.ax2 = next((self.combo_ax2.itemText(i) for i in range(self.combo_ax2.count())), '') self.ax1 = next((self.combo_ax1.itemText(i) for i in range(1, self.combo_ax1.count())), self.ax2) self.agg_attr = [data.domain.variables[0]] if len( data.domain.variables) else [] self.agg_func = 0 if getattr(data, 'time_variable', None) is not None: self.openContext(data.domain) if self.agg_attr: self.attrlist.blockSignals(True) self.attrlist.selectionModel().clear() for attr in self.agg_attr: try: row = self.attrlist_model.indexOf(attr) except ValueError: continue self.attrlist.selectionModel().select( self.attrlist_model.index(row), QItemSelectionModel.SelectCurrent) self.attrlist.blockSignals(False) self.replot() def replot(self): if not self.combo_ax1.count() or not self.agg_attr: return self.chart.clear() vars = self.agg_attr func = AGG_FUNCTIONS[self.agg_func] if any(var.is_discrete for var in vars) and func != Mode: self.combo_func.setCurrentIndex(AGG_FUNCTIONS.index(Mode)) func = Mode try: ax1 = Spiralogram.AxesCategories[_enum_str(self.ax1, True)] except KeyError: ax1 = self.data.domain[self.ax1] # TODO: Allow having only a single (i.e. radial) axis try: ax2 = Spiralogram.AxesCategories[_enum_str(self.ax2, True)] except KeyError: ax2 = self.data.domain[self.ax2] self.chart.setSeries(self.data, vars, ax1, ax2, func) def on_selection(self, indices): self.indices = self.chart.selection_indices(indices) self.commit() def commit(self): self.Outputs.time_series.send( self.data[self.indices] if self.data else None)
class OWCorpus(OWWidget): name = "语料库" description = "加载文档语料库." icon = "icons/TextFile.svg" priority = 100 replaces = ["orangecontrib.text.widgets.owloadcorpus.OWLoadCorpus"] class Inputs: data = Input('Data', Table) class Outputs: corpus = Output('Corpus', Corpus) want_main_area = False resizing_enabled = True dlgFormats = ( "所有可读文档 ({});;".format('*' + ' *'.join(FileFormat.readers.keys())) + ";;".join( "{} (*{})".format(f.DESCRIPTION, ' *'.join(f.EXTENSIONS)) for f in sorted(set(FileFormat.readers.values()), key=list(FileFormat.readers.values()).index))) settingsHandler = PerfectDomainContextHandler( match_values=PerfectDomainContextHandler.MATCH_VALUES_ALL) recent_files = Setting([ "book-excerpts.tab", "grimm-tales-selected.tab", "election-tweets-2016.tab", "friends-transcripts.tab", "andersen.tab", "chinese-example.tab", ]) used_attrs = ContextSetting([]) class Error(OWWidget.Error): read_file = Msg("无法读取文件 {} ({})") no_text_features_used = Msg("至少使用一个文本特征") corpus_without_text_features = Msg("语料库没有文本特征") def __init__(self): super().__init__() self.corpus = None # Browse file box fbox = gui.widgetBox(self.controlArea, "语料库文件", orientation=0) self.file_widget = widgets.FileWidget( recent_files=self.recent_files, icon_size=(16, 16), on_open=self.open_file, dialog_format=self.dlgFormats, dialog_title='打开语料库文档', reload_label='重新加载', browse_label='浏览', allow_empty=False, minimal_width=250, ) fbox.layout().addWidget(self.file_widget) # Corpus info ibox = gui.widgetBox(self.controlArea, "语料库信息", addSpace=True) self.info_label = gui.label(ibox, self, "") self.update_info() # Used Text Features fbox = gui.widgetBox(self.controlArea, orientation=0) ubox = gui.widgetBox(fbox, "已使用的文本特征", addSpace=False) self.used_attrs_model = VariableListModel(enable_dnd=True) self.used_attrs_view = VariablesListItemView() self.used_attrs_view.setModel(self.used_attrs_model) ubox.layout().addWidget(self.used_attrs_view) aa = self.used_attrs_model aa.dataChanged.connect(self.update_feature_selection) aa.rowsInserted.connect(self.update_feature_selection) aa.rowsRemoved.connect(self.update_feature_selection) # Ignored Text Features ibox = gui.widgetBox(fbox, "未使用的文本特征", addSpace=False) self.unused_attrs_model = VariableListModel(enable_dnd=True) self.unused_attrs_view = VariablesListItemView() self.unused_attrs_view.setModel(self.unused_attrs_model) ibox.layout().addWidget(self.unused_attrs_view) # Documentation Data Sets & Report box = gui.hBox(self.controlArea) self.browse_documentation = gui.button( box, self, "浏览语料库文档", callback=lambda: self.file_widget.browse(get_sample_corpora_dir()), autoDefault=False, ) # load first file self.file_widget.select(0) def sizeHint(self): return QSize(400, 300) @Inputs.data def set_data(self, data): have_data = data is not None # Enable/Disable command when data from input self.file_widget.setEnabled(not have_data) self.browse_documentation.setEnabled(not have_data) if have_data: self.open_file(data=data) else: self.file_widget.reload() def open_file(self, path=None, data=None): self.closeContext() self.Error.clear() self.unused_attrs_model[:] = [] self.used_attrs_model[:] = [] if data: self.corpus = Corpus.from_table(data.domain, data) elif path: try: self.corpus = Corpus.from_file(path) self.corpus.name = os.path.splitext(os.path.basename(path))[0] except BaseException as err: self.Error.read_file(path, str(err)) else: return self.update_info() self.used_attrs = list(self.corpus.text_features) if not self.corpus.text_features: self.Error.corpus_without_text_features() self.Outputs.corpus.send(None) return self.openContext(self.corpus) self.used_attrs_model.extend(self.used_attrs) self.unused_attrs_model.extend([ f for f in self.corpus.domain.metas if f.is_string and f not in self.used_attrs_model ]) def update_info(self): def describe(corpus): dom = corpus.domain text_feats = sum(m.is_string for m in dom.metas) other_feats = len(dom.attributes) + len(dom.metas) - text_feats text = \ "{} 个文档, {} 个文本特征, {} 个其他特征.". \ format(len(corpus), text_feats, other_feats) if dom.has_continuous_class: text += "<br/>回归; 数值类." elif dom.has_discrete_class: text += "<br/>分类; 离散值含有 {} 种值.". \ format(len(dom.class_var.values)) elif corpus.domain.class_vars: text += "<br/>多目标; {} 个目标变量.".format( len(corpus.domain.class_vars)) else: text += "<br/>数据没有目标变量" text += "</p>" return text if self.corpus is None: self.info_label.setText("没有加载语料库") else: self.info_label.setText(describe(self.corpus)) def update_feature_selection(self): self.Error.no_text_features_used.clear() # TODO fix VariablesListItemView so it does not emit # duplicated data when reordering inside a single window def remove_duplicates(l): unique = [] for i in l: if i not in unique: unique.append(i) return unique if self.corpus is not None: self.corpus.set_text_features( remove_duplicates(self.used_attrs_model)) self.used_attrs = list(self.used_attrs_model) if len(self.unused_attrs_model ) > 0 and not self.corpus.text_features: self.Error.no_text_features_used() # prevent sending "empty" corpora dom = self.corpus.domain empty = not (dom.variables or dom.metas) \ or len(self.corpus) == 0 \ or not self.corpus.text_features self.Outputs.corpus.send(self.corpus if not empty else None) def send_report(self): def describe(features): if len(features): return ', '.join([f.name for f in features]) else: return '(无)' if self.corpus is not None: domain = self.corpus.domain self.report_items('Corpus', ( ("File", self.file_widget.get_selected_filename()), ("Documents", len(self.corpus)), ("Used text features", describe(self.used_attrs_model)), ("Ignored text features", describe(self.unused_attrs_model)), ('Other features', describe(domain.attributes)), ('Target', describe(domain.class_vars)), ))
def __init__(self): super().__init__() self._axis_font = QFont() self._axis_font.setPixelSize(12) self._label_font = QFont() self._label_font.setPixelSize(11) self.dataset = None self.stats = [] self.dist = self.conts = None self.posthoc_lines = [] self.label_txts = self.mean_labels = self.boxes = self.labels = \ self.label_txts_all = self.attr_labels = self.order = [] self.scale_x = 1 self.scene_min_x = self.scene_max_x = self.scene_width = 0 self.label_width = 0 self.attrs = VariableListModel() sorted_model = SortProxyModel(sortRole=Qt.UserRole) sorted_model.setSourceModel(self.attrs) sorted_model.sort(0) box = gui.vBox(self.controlArea, "Variable") view = self.attr_list = ListViewSearch() view.setModel(sorted_model) view.setSelectionMode(view.SingleSelection) view.selectionModel().selectionChanged.connect(self.attr_changed) view.setMinimumSize(QSize(30, 30)) # Any other policy than Ignored will let the QListBox's scrollbar # set the minimal height (see the penultimate paragraph of # http://doc.qt.io/qt-4.8/qabstractscrollarea.html#addScrollBarWidget) view.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Ignored) box.layout().addWidget(view) gui.checkBox(box, self, "order_by_importance", "Order by relevance to subgroups", tooltip="Order by 𝜒² or ANOVA over the subgroups", callback=self.apply_attr_sorting) self.group_vars = VariableListModel(placeholder="None") sorted_model = SortProxyModel(sortRole=Qt.UserRole) sorted_model.setSourceModel(self.group_vars) sorted_model.sort(0) box = gui.vBox(self.controlArea, "Subgroups") view = self.group_list = ListViewSearch() view.setModel(sorted_model) view.selectionModel().selectionChanged.connect(self.grouping_changed) view.setMinimumSize(QSize(30, 30)) # See the comment above view.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Ignored) box.layout().addWidget(view) gui.checkBox(box, self, "order_grouping_by_importance", "Order by relevance to variable", tooltip="Order by 𝜒² or ANOVA over the variable values", callback=self.apply_group_sorting) # TODO: move Compare median/mean to grouping box # The vertical size policy is needed to let only the list views expand self.display_box = gui.vBox(self.controlArea, "Display", sizePolicy=(QSizePolicy.Minimum, QSizePolicy.Maximum)) gui.checkBox(self.display_box, self, "show_annotations", "Annotate", callback=self.update_graph) self.compare_rb = gui.radioButtonsInBox( self.display_box, self, 'compare', btnLabels=["No comparison", "Compare medians", "Compare means"], callback=self.update_graph) # The vertical size policy is needed to let only the list views expand self.stretching_box = box = gui.vBox(self.controlArea, box="Display", sizePolicy=(QSizePolicy.Minimum, QSizePolicy.Fixed)) self.stretching_box.sizeHint = self.display_box.sizeHint gui.checkBox(box, self, 'stretched', "Stretch bars", callback=self.update_graph, stateWhenDisabled=False) gui.checkBox(box, self, 'show_labels', "Show box labels", callback=self.update_graph) self.sort_cb = gui.checkBox(box, self, 'sort_freqs', "Sort by subgroup frequencies", callback=self.update_graph, stateWhenDisabled=False) gui.vBox(self.mainArea) self.box_scene = QGraphicsScene(self) self.box_scene.selectionChanged.connect(self.on_selection_changed) self.box_view = QGraphicsView(self.box_scene) self.box_view.setRenderHints(QPainter.Antialiasing | QPainter.TextAntialiasing | QPainter.SmoothPixmapTransform) self.box_view.viewport().installEventFilter(self) self.mainArea.layout().addWidget(self.box_view) self.stat_test = "" self.mainArea.setMinimumWidth(300) self.update_box_visibilities()
class OWCorpus(OWWidget): name = "Corpus" description = "Load a corpus of text documents." icon = "icons/TextFile.svg" priority = 100 replaces = ["orangecontrib.text.widgets.owloadcorpus.OWLoadCorpus"] class Inputs: data = Input('Data', Table) class Outputs: corpus = Output('Corpus', Corpus) want_main_area = False resizing_enabled = True dlgFormats = ( "All readable files ({});;".format( '*' + ' *'.join(FileFormat.readers.keys())) + ";;".join("{} (*{})".format(f.DESCRIPTION, ' *'.join(f.EXTENSIONS)) for f in sorted(set(FileFormat.readers.values()), key=list(FileFormat.readers.values()).index))) settingsHandler = PerfectDomainContextHandler( match_values=PerfectDomainContextHandler.MATCH_VALUES_ALL ) recent_files = Setting([ "book-excerpts.tab", "grimm-tales-selected.tab", "election-tweets-2016.tab", "friends-transcripts.tab", "andersen.tab", ]) used_attrs = ContextSetting([]) class Error(OWWidget.Error): read_file = Msg("Can't read file {} ({})") no_text_features_used = Msg("At least one text feature must be used.") corpus_without_text_features = Msg("Corpus doesn't have any textual features.") def __init__(self): super().__init__() self.corpus = None # Browse file box fbox = gui.widgetBox(self.controlArea, "Corpus file", orientation=0) self.file_widget = widgets.FileWidget( recent_files=self.recent_files, icon_size=(16, 16), on_open=self.open_file, dialog_format=self.dlgFormats, dialog_title='Open Orange Document Corpus', reload_label='Reload', browse_label='Browse', allow_empty=False, minimal_width=250, ) fbox.layout().addWidget(self.file_widget) # Corpus info ibox = gui.widgetBox(self.controlArea, "Corpus info", addSpace=True) self.info_label = gui.label(ibox, self, "") self.update_info() # Used Text Features fbox = gui.widgetBox(self.controlArea, orientation=0) ubox = gui.widgetBox(fbox, "Used text features", addSpace=False) self.used_attrs_model = VariableListModel(enable_dnd=True) self.used_attrs_view = VariablesListItemView() self.used_attrs_view.setModel(self.used_attrs_model) ubox.layout().addWidget(self.used_attrs_view) aa = self.used_attrs_model aa.dataChanged.connect(self.update_feature_selection) aa.rowsInserted.connect(self.update_feature_selection) aa.rowsRemoved.connect(self.update_feature_selection) # Ignored Text Features ibox = gui.widgetBox(fbox, "Ignored text features", addSpace=False) self.unused_attrs_model = VariableListModel(enable_dnd=True) self.unused_attrs_view = VariablesListItemView() self.unused_attrs_view.setModel(self.unused_attrs_model) ibox.layout().addWidget(self.unused_attrs_view) # Documentation Data Sets & Report box = gui.hBox(self.controlArea) self.browse_documentation = gui.button( box, self, "Browse documentation corpora", callback=lambda: self.file_widget.browse( get_sample_corpora_dir()), autoDefault=False, ) # load first file self.file_widget.select(0) def sizeHint(self): return QSize(400, 300) @Inputs.data def set_data(self, data): have_data = data is not None # Enable/Disable command when data from input self.file_widget.setEnabled(not have_data) self.browse_documentation.setEnabled(not have_data) if have_data: self.open_file(data=data) else: self.file_widget.reload() def open_file(self, path=None, data=None): self.closeContext() self.Error.clear() self.unused_attrs_model[:] = [] self.used_attrs_model[:] = [] if data: self.corpus = Corpus.from_table(data.domain, data) elif path: try: self.corpus = Corpus.from_file(path) self.corpus.name = os.path.splitext(os.path.basename(path))[0] except BaseException as err: self.Error.read_file(path, str(err)) else: return self.update_info() self.used_attrs = list(self.corpus.text_features) if not self.corpus.text_features: self.Error.corpus_without_text_features() self.Outputs.corpus.send(None) return self.openContext(self.corpus) self.used_attrs_model.extend(self.used_attrs) self.unused_attrs_model.extend( [f for f in self.corpus.domain.metas if f.is_string and f not in self.used_attrs_model]) def update_info(self): def describe(corpus): dom = corpus.domain text_feats = sum(m.is_string for m in dom.metas) other_feats = len(dom.attributes) + len(dom.metas) - text_feats text = \ "{} document(s), {} text features(s), {} other feature(s).". \ format(len(corpus), text_feats, other_feats) if dom.has_continuous_class: text += "<br/>Regression; numerical class." elif dom.has_discrete_class: text += "<br/>Classification; discrete class with {} values.". \ format(len(dom.class_var.values)) elif corpus.domain.class_vars: text += "<br/>Multi-target; {} target variables.".format( len(corpus.domain.class_vars)) else: text += "<br/>Data has no target variable." text += "</p>" return text if self.corpus is None: self.info_label.setText("No corpus loaded.") else: self.info_label.setText(describe(self.corpus)) def update_feature_selection(self): self.Error.no_text_features_used.clear() # TODO fix VariablesListItemView so it does not emit # duplicated data when reordering inside a single window def remove_duplicates(l): unique = [] for i in l: if i not in unique: unique.append(i) return unique if self.corpus is not None: self.corpus.set_text_features( remove_duplicates(self.used_attrs_model)) self.used_attrs = list(self.used_attrs_model) if len(self.unused_attrs_model) > 0 and not self.corpus.text_features: self.Error.no_text_features_used() # prevent sending "empty" corpora dom = self.corpus.domain empty = not (dom.variables or dom.metas) \ or len(self.corpus) == 0 \ or not self.corpus.text_features self.Outputs.corpus.send(self.corpus if not empty else None) def send_report(self): def describe(features): if len(features): return ', '.join([f.name for f in features]) else: return '(none)' if self.corpus is not None: domain = self.corpus.domain self.report_items('Corpus', ( ("File", self.file_widget.get_selected_filename()), ("Documents", len(self.corpus)), ("Used text features", describe(self.used_attrs_model)), ("Ignored text features", describe(self.unused_attrs_model)), ('Other features', describe(domain.attributes)), ('Target', describe(domain.class_vars)), ))
class OWBoxPlot(widget.OWWidget): """ Here's how the widget's functions call each other: - `set_data` is a signal handler fills the list boxes and calls `grouping_changed`. - `grouping_changed` handles changes of grouping attribute: it enables or disables the box for ordering, orders attributes and calls `attr_changed`. - `attr_changed` handles changes of attribute. It recomputes box data by calling `compute_box_data`, shows the appropriate display box (discrete/continuous) and then calls`layout_changed` - `layout_changed` constructs all the elements for the scene (as lists of QGraphicsItemGroup) and calls `display_changed`. It is called when the attribute or grouping is changed (by attr_changed) and on resize event. - `display_changed` puts the elements corresponding to the current display settings on the scene. It is called when the elements are reconstructed (layout is changed due to selection of attributes or resize event), or when the user changes display settings or colors. For discrete attributes, the flow is a bit simpler: the elements are not constructed in advance (by layout_changed). Instead, layout_changed and display_changed call display_changed_disc that draws everything. """ name = "Box Plot" description = "Visualize the distribution of feature values in a box plot." icon = "icons/BoxPlot.svg" priority = 100 keywords = ["whisker"] class Inputs: data = Input("Data", Orange.data.Table) class Outputs: selected_data = Output("Selected Data", Orange.data.Table, default=True) annotated_data = Output(ANNOTATED_DATA_SIGNAL_NAME, Orange.data.Table) #: Comparison types for continuous variables CompareNone, CompareMedians, CompareMeans = 0, 1, 2 settingsHandler = DomainContextHandler() conditions = ContextSetting([]) attribute = ContextSetting(None) order_by_importance = Setting(False) group_var = ContextSetting(None) show_annotations = Setting(True) compare = Setting(CompareMeans) stattest = Setting(0) sig_threshold = Setting(0.05) stretched = Setting(True) show_labels = Setting(True) sort_freqs = Setting(False) auto_commit = Setting(True) _sorting_criteria_attrs = { CompareNone: "", CompareMedians: "median", CompareMeans: "mean" } _pen_axis_tick = QPen(Qt.white, 5) _pen_axis = QPen(Qt.darkGray, 3) _pen_median = QPen(QBrush(QColor(0xff, 0xff, 0x00)), 2) _pen_paramet = QPen(QBrush(QColor(0x33, 0x00, 0xff)), 2) _pen_dotted = QPen(QBrush(QColor(0x33, 0x00, 0xff)), 1) _pen_dotted.setStyle(Qt.DotLine) _post_line_pen = QPen(Qt.lightGray, 2) _post_grp_pen = QPen(Qt.lightGray, 4) for pen in (_pen_paramet, _pen_median, _pen_dotted, _pen_axis, _pen_axis_tick, _post_line_pen, _post_grp_pen): pen.setCosmetic(True) pen.setCapStyle(Qt.RoundCap) pen.setJoinStyle(Qt.RoundJoin) _pen_axis_tick.setCapStyle(Qt.FlatCap) _box_brush = QBrush(QColor(0x33, 0x88, 0xff, 0xc0)) _axis_font = QFont() _axis_font.setPixelSize(12) _label_font = QFont() _label_font.setPixelSize(11) _attr_brush = QBrush(QColor(0x33, 0x00, 0xff)) graph_name = "box_scene" def __init__(self): super().__init__() self.stats = [] self.dataset = None self.posthoc_lines = [] self.label_txts = self.mean_labels = self.boxes = self.labels = \ self.label_txts_all = self.attr_labels = self.order = [] self.scale_x = self.scene_min_x = self.scene_width = 0 self.label_width = 0 self.attrs = VariableListModel() view = gui.listView( self.controlArea, self, "attribute", box="Variable", model=self.attrs, callback=self.attr_changed) view.setMinimumSize(QSize(30, 30)) # Any other policy than Ignored will let the QListBox's scrollbar # set the minimal height (see the penultimate paragraph of # http://doc.qt.io/qt-4.8/qabstractscrollarea.html#addScrollBarWidget) view.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Ignored) gui.separator(view.box, 6, 6) self.cb_order = gui.checkBox( view.box, self, "order_by_importance", "Order by relevance", tooltip="Order by 𝜒² or ANOVA over the subgroups", callback=self.apply_sorting) self.group_vars = DomainModel( placeholder="None", separators=False, valid_types=Orange.data.DiscreteVariable) self.group_view = view = gui.listView( self.controlArea, self, "group_var", box="Subgroups", model=self.group_vars, callback=self.grouping_changed) view.setEnabled(False) view.setMinimumSize(QSize(30, 30)) # See the comment above view.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Ignored) # TODO: move Compare median/mean to grouping box # The vertical size policy is needed to let only the list views expand self.display_box = gui.vBox( self.controlArea, "Display", sizePolicy=(QSizePolicy.Minimum, QSizePolicy.Maximum), addSpace=False) gui.checkBox(self.display_box, self, "show_annotations", "Annotate", callback=self.display_changed) self.compare_rb = gui.radioButtonsInBox( self.display_box, self, 'compare', btnLabels=["No comparison", "Compare medians", "Compare means"], callback=self.layout_changed) # The vertical size policy is needed to let only the list views expand self.stretching_box = box = gui.vBox( self.controlArea, box="Display", sizePolicy=(QSizePolicy.Minimum, QSizePolicy.Fixed)) self.stretching_box.sizeHint = self.display_box.sizeHint gui.checkBox( box, self, 'stretched', "Stretch bars", callback=self.display_changed) gui.checkBox( box, self, 'show_labels', "Show box labels", callback=self.display_changed) self.sort_cb = gui.checkBox( box, self, 'sort_freqs', "Sort by subgroup frequencies", callback=self.display_changed) gui.rubber(box) gui.auto_commit(self.controlArea, self, "auto_commit", "Send Selection", "Send Automatically") gui.vBox(self.mainArea, addSpace=True) self.box_scene = QGraphicsScene() self.box_scene.selectionChanged.connect(self.commit) self.box_view = QGraphicsView(self.box_scene) self.box_view.setRenderHints(QPainter.Antialiasing | QPainter.TextAntialiasing | QPainter.SmoothPixmapTransform) self.box_view.viewport().installEventFilter(self) self.mainArea.layout().addWidget(self.box_view) e = gui.hBox(self.mainArea, addSpace=False) self.infot1 = gui.widgetLabel(e, "<center>No test results.</center>") self.mainArea.setMinimumWidth(300) self.stats = self.dist = self.conts = [] self.is_continuous = False self.update_display_box() def sizeHint(self): return QSize(900, 500) def eventFilter(self, obj, event): if obj is self.box_view.viewport() and \ event.type() == QEvent.Resize: self.layout_changed() return super().eventFilter(obj, event) def reset_attrs(self, domain): self.attrs[:] = [ var for var in chain( domain.class_vars, domain.metas, domain.attributes) if var.is_primitive()] # noinspection PyTypeChecker @Inputs.data def set_data(self, dataset): if dataset is not None and ( not bool(dataset) or not len(dataset.domain) and not any(var.is_primitive() for var in dataset.domain.metas)): dataset = None self.closeContext() self.dataset = dataset self.dist = self.stats = self.conts = [] self.group_var = None self.attribute = None if dataset: domain = dataset.domain self.group_vars.set_domain(domain) self.group_view.setEnabled(len(self.group_vars) > 1) self.reset_attrs(domain) self.select_default_variables(domain) self.openContext(self.dataset) self.grouping_changed() else: self.reset_all_data() self.commit() def select_default_variables(self, domain): # visualize first non-class variable, group by class (if present) if len(self.attrs) > len(domain.class_vars): self.attribute = self.attrs[len(domain.class_vars)] elif self.attrs: self.attribute = self.attrs[0] if domain.class_var and domain.class_var.is_discrete: self.group_var = domain.class_var else: self.group_var = None # Reset to trigger selection via callback def apply_sorting(self): def compute_score(attr): if attr is group_var: return 3 if attr.is_continuous: # One-way ANOVA col = data.get_column_view(attr)[0].astype(float) groups = (col[group_col == i] for i in range(n_groups)) groups = (col[~np.isnan(col)] for col in groups) groups = [group for group in groups if len(group)] p = f_oneway(*groups)[1] if len(groups) > 1 else 2 else: # Chi-square with the given distribution into groups # (see degrees of freedom in computation of the p-value) if not attr.values or not group_var.values: return 2 observed = np.array( contingency.get_contingency(data, group_var, attr)) observed = observed[observed.sum(axis=1) != 0, :] observed = observed[:, observed.sum(axis=0) != 0] if min(observed.shape) < 2: return 2 expected = \ np.outer(observed.sum(axis=1), observed.sum(axis=0)) / \ np.sum(observed) p = chisquare(observed.ravel(), f_exp=expected.ravel(), ddof=n_groups - 1)[1] if math.isnan(p): return 2 return p data = self.dataset if data is None: return domain = data.domain attribute = self.attribute group_var = self.group_var if self.order_by_importance and group_var is not None: n_groups = len(group_var.values) group_col = data.get_column_view(group_var)[0] if \ domain.has_continuous_attributes( include_class=True, include_metas=True) else None self.attrs.sort(key=compute_score) else: self.reset_attrs(domain) self.attribute = attribute def reset_all_data(self): self.clear_scene() self.infot1.setText("") self.attrs.clear() self.group_vars.set_domain(None) self.group_view.setEnabled(False) self.is_continuous = False self.update_display_box() def grouping_changed(self): self.cb_order.setEnabled(self.group_var is not None) self.apply_sorting() self.attr_changed() def select_box_items(self): temp_cond = self.conditions.copy() for box in self.box_scene.items(): if isinstance(box, FilterGraphicsRectItem): box.setSelected(box.filter.conditions in [c.conditions for c in temp_cond]) def attr_changed(self): self.compute_box_data() self.update_display_box() self.layout_changed() if self.is_continuous: heights = 90 if self.show_annotations else 60 self.box_view.centerOn(self.scene_min_x + self.scene_width / 2, -30 - len(self.stats) * heights / 2 + 45) else: self.box_view.centerOn(self.scene_width / 2, -30 - len(self.boxes) * 40 / 2 + 45) def compute_box_data(self): attr = self.attribute if not attr: return dataset = self.dataset self.is_continuous = attr.is_continuous if dataset is None or not self.is_continuous and not attr.values or \ self.group_var and not self.group_var.values: self.stats = self.dist = self.conts = [] return if self.group_var: self.dist = [] self.conts = contingency.get_contingency( dataset, attr, self.group_var) if self.is_continuous: stats, label_texts = [], [] for i, cont in enumerate(self.conts): if np.sum(cont[1]): stats.append(BoxData(cont, attr, i, self.group_var)) label_texts.append(self.group_var.values[i]) self.stats = stats self.label_txts_all = label_texts else: self.label_txts_all = \ [v for v, c in zip(self.group_var.values, self.conts) if np.sum(c) > 0] else: self.dist = distribution.get_distribution(dataset, attr) self.conts = [] if self.is_continuous: self.stats = [BoxData(self.dist, attr, None)] self.label_txts_all = [""] self.label_txts = [txts for stat, txts in zip(self.stats, self.label_txts_all) if stat.n > 0] self.stats = [stat for stat in self.stats if stat.n > 0] def update_display_box(self): if self.is_continuous: self.stretching_box.hide() self.display_box.show() self.compare_rb.setEnabled(self.group_var is not None) else: self.stretching_box.show() self.display_box.hide() self.sort_cb.setEnabled(self.group_var is not None) def clear_scene(self): self.closeContext() self.box_scene.clearSelection() self.box_scene.clear() self.box_view.viewport().update() self.attr_labels = [] self.labels = [] self.boxes = [] self.mean_labels = [] self.posthoc_lines = [] self.openContext(self.dataset) def layout_changed(self): attr = self.attribute if not attr: return self.clear_scene() if self.dataset is None or len(self.conts) == len(self.dist) == 0: return if not self.is_continuous: self.display_changed_disc() return self.mean_labels = [self.mean_label(stat, attr, lab) for stat, lab in zip(self.stats, self.label_txts)] self.draw_axis() self.boxes = [self.box_group(stat) for stat in self.stats] self.labels = [self.label_group(stat, attr, mean_lab) for stat, mean_lab in zip(self.stats, self.mean_labels)] self.attr_labels = [QGraphicsSimpleTextItem(lab) for lab in self.label_txts] for it in chain(self.labels, self.attr_labels): self.box_scene.addItem(it) self.display_changed() def display_changed(self): if self.dataset is None: return if not self.is_continuous: self.display_changed_disc() return self.order = list(range(len(self.stats))) criterion = self._sorting_criteria_attrs[self.compare] if criterion: vals = [getattr(stat, criterion) for stat in self.stats] overmax = max((val for val in vals if val is not None), default=0) \ + 1 vals = [val if val is not None else overmax for val in vals] self.order = sorted(self.order, key=vals.__getitem__) heights = 90 if self.show_annotations else 60 for row, box_index in enumerate(self.order): y = (-len(self.stats) + row) * heights + 10 for item in self.boxes[box_index]: self.box_scene.addItem(item) item.setY(y) labels = self.labels[box_index] if self.show_annotations: labels.show() labels.setY(y) else: labels.hide() label = self.attr_labels[box_index] label.setY(y - 15 - label.boundingRect().height()) if self.show_annotations: label.hide() else: stat = self.stats[box_index] if self.compare == OWBoxPlot.CompareMedians and \ stat.median is not None: pos = stat.median + 5 / self.scale_x elif self.compare == OWBoxPlot.CompareMeans or stat.q25 is None: pos = stat.mean + 5 / self.scale_x else: pos = stat.q25 label.setX(pos * self.scale_x) label.show() r = QRectF(self.scene_min_x, -30 - len(self.stats) * heights, self.scene_width, len(self.stats) * heights + 90) self.box_scene.setSceneRect(r) self.compute_tests() self.show_posthoc() self.select_box_items() def display_changed_disc(self): assert not self.is_continuous self.clear_scene() self.attr_labels = [QGraphicsSimpleTextItem(lab) for lab in self.label_txts_all] if not self.stretched: if self.group_var: self.labels = [ QGraphicsTextItem("{}".format(int(sum(cont)))) for cont in self.conts if np.sum(cont) > 0] else: self.labels = [ QGraphicsTextItem(str(int(sum(self.dist))))] self.order = list(range(len(self.attr_labels))) self.draw_axis_disc() if self.group_var: self.boxes = \ [self.strudel(cont, i) for i, cont in enumerate(self.conts) if np.sum(cont) > 0] self.conts = self.conts[np.sum(np.array(self.conts), axis=1) > 0] if self.sort_freqs: # pylint: disable=invalid-unary-operand-type self.order = sorted(self.order, key=(-np.sum(self.conts, axis=1)).__getitem__) else: self.boxes = [self.strudel(self.dist)] for row, box_index in enumerate(self.order): y = (-len(self.boxes) + row) * 40 + 10 box = self.boxes[box_index] bars, labels = box[::2], box[1::2] self.__draw_group_labels(y, box_index) if not self.stretched: self.__draw_row_counts(y, box_index) if self.show_labels and self.attribute is not self.group_var: self.__draw_bar_labels(y, bars, labels) self.__draw_bars(y, bars) self.box_scene.setSceneRect(-self.label_width - 5, -30 - len(self.boxes) * 40, self.scene_width, len(self.boxes * 40) + 90) self.infot1.setText("") self.select_box_items() def __draw_group_labels(self, y, row): """Draw group labels Parameters ---------- y: int vertical offset of bars row: int row index """ label = self.attr_labels[row] b = label.boundingRect() label.setPos(-b.width() - 10, y - b.height() / 2) self.box_scene.addItem(label) def __draw_row_counts(self, y, row): """Draw row counts Parameters ---------- y: int vertical offset of bars row: int row index """ assert not self.is_continuous label = self.labels[row] b = label.boundingRect() if self.group_var: right = self.scale_x * sum(self.conts[row]) else: right = self.scale_x * sum(self.dist) label.setPos(right + 10, y - b.height() / 2) self.box_scene.addItem(label) def __draw_bar_labels(self, y, bars, labels): """Draw bar labels Parameters ---------- y: int vertical offset of bars bars: List[FilterGraphicsRectItem] list of bars being drawn labels: List[QGraphicsTextItem] list of labels for corresponding bars """ label = bar_part = None for text_item, bar_part in zip(labels, bars): label = self.Label( text_item.toPlainText()) label.setPos(bar_part.boundingRect().x(), y - label.boundingRect().height() - 8) label.setMaxWidth(bar_part.boundingRect().width()) self.box_scene.addItem(label) def __draw_bars(self, y, bars): """Draw bars Parameters ---------- y: int vertical offset of bars bars: List[FilterGraphicsRectItem] list of bars to draw """ for item in bars: item.setPos(0, y) self.box_scene.addItem(item) # noinspection PyPep8Naming def compute_tests(self): # The t-test and ANOVA are implemented here since they efficiently use # the widget-specific data in self.stats. # The non-parametric tests can't do this, so we use statistics.tests # pylint: disable=comparison-with-itself def stat_ttest(): d1, d2 = self.stats if d1.n < 2 or d2.n < 2: return np.nan, np.nan pooled_var = d1.var / d1.n + d2.var / d2.n # pylint: disable=comparison-with-itself if pooled_var == 0 or np.isnan(pooled_var): return np.nan, np.nan df = pooled_var ** 2 / \ ((d1.var / d1.n) ** 2 / (d1.n - 1) + (d2.var / d2.n) ** 2 / (d2.n - 1)) t = abs(d1.mean - d2.mean) / math.sqrt(pooled_var) p = 2 * (1 - scipy.special.stdtr(df, t)) return t, p # TODO: Check this function # noinspection PyPep8Naming def stat_ANOVA(): if any(stat.n == 0 for stat in self.stats): return np.nan, np.nan n = sum(stat.n for stat in self.stats) grand_avg = sum(stat.n * stat.mean for stat in self.stats) / n var_between = sum(stat.n * (stat.mean - grand_avg) ** 2 for stat in self.stats) df_between = len(self.stats) - 1 var_within = sum(stat.n * stat.var for stat in self.stats) df_within = n - len(self.stats) if var_within == 0 or df_within == 0 or df_between == 0: return np.nan, np.nan F = (var_between / df_between) / (var_within / df_within) p = 1 - scipy.special.fdtr(df_between, df_within, F) return F, p if self.compare == OWBoxPlot.CompareNone or len(self.stats) < 2: t = "" elif any(s.n <= 1 for s in self.stats): t = "At least one group has just one instance, " \ "cannot compute significance" elif len(self.stats) == 2: if self.compare == OWBoxPlot.CompareMedians: t = "" # z, p = tests.wilcoxon_rank_sum( # self.stats[0].dist, self.stats[1].dist) # t = "Mann-Whitney's z: %.1f (p=%.3f)" % (z, p) else: t, p = stat_ttest() t = "" if np.isnan(t) else f"Student's t: {t:.3f} (p={p:.3f})" else: if self.compare == OWBoxPlot.CompareMedians: t = "" # U, p = -1, -1 # t = "Kruskal Wallis's U: %.1f (p=%.3f)" % (U, p) else: F, p = stat_ANOVA() t = "" if np.isnan(F) else f"ANOVA: {F:.3f} (p={p:.3f})" self.infot1.setText("<center>%s</center>" % t) def mean_label(self, stat, attr, val_name): label = QGraphicsItemGroup() t = QGraphicsSimpleTextItem( "%.*f" % (attr.number_of_decimals + 1, stat.mean), label) t.setFont(self._label_font) bbox = t.boundingRect() w2, h = bbox.width() / 2, bbox.height() t.setPos(-w2, -h) tpm = QGraphicsSimpleTextItem( " \u00b1 " + "%.*f" % (attr.number_of_decimals + 1, stat.dev), label) tpm.setFont(self._label_font) tpm.setPos(w2, -h) if val_name: vnm = QGraphicsSimpleTextItem(val_name + ": ", label) vnm.setFont(self._label_font) vnm.setBrush(self._attr_brush) vb = vnm.boundingRect() label.min_x = -w2 - vb.width() vnm.setPos(label.min_x, -h) else: label.min_x = -w2 return label def draw_axis(self): """Draw the horizontal axis and sets self.scale_x""" misssing_stats = not self.stats stats = self.stats or [BoxData(np.array([[0.], [1.]]), self.attribute)] mean_labels = self.mean_labels or [self.mean_label(stats[0], self.attribute, "")] bottom = min(stat.a_min for stat in stats) top = max(stat.a_max for stat in stats) first_val, step = compute_scale(bottom, top) while bottom <= first_val: first_val -= step bottom = first_val no_ticks = math.ceil((top - first_val) / step) + 1 top = max(top, first_val + no_ticks * step) gbottom = min(bottom, min(stat.mean - stat.dev for stat in stats)) gtop = max(top, max(stat.mean + stat.dev for stat in stats)) bv = self.box_view viewrect = bv.viewport().rect().adjusted(15, 15, -15, -30) self.scale_x = scale_x = viewrect.width() / (gtop - gbottom) # In principle we should repeat this until convergence since the new # scaling is too conservative. (No chance am I doing this.) mlb = min(stat.mean + mean_lab.min_x / scale_x for stat, mean_lab in zip(stats, mean_labels)) if mlb < gbottom: gbottom = mlb self.scale_x = scale_x = viewrect.width() / (gtop - gbottom) self.scene_min_x = gbottom * scale_x self.scene_width = (gtop - gbottom) * scale_x val = first_val decimals = max(3, 4 - int(math.log10(step))) while True: l = self.box_scene.addLine(val * scale_x, -1, val * scale_x, 1, self._pen_axis_tick) l.setZValue(100) t = self.box_scene.addSimpleText( repr(round(val, decimals)) if not misssing_stats else "?", self._axis_font) t.setFlags( t.flags() | QGraphicsItem.ItemIgnoresTransformations) r = t.boundingRect() t.setPos(val * scale_x - r.width() / 2, 8) if val >= top: break val += step self.box_scene.addLine( bottom * scale_x - 4, 0, top * scale_x + 4, 0, self._pen_axis) def draw_axis_disc(self): """ Draw the horizontal axis and sets self.scale_x for discrete attributes """ assert not self.is_continuous if self.stretched: if not self.attr_labels: return step = steps = 10 else: if self.group_var: max_box = max(float(np.sum(dist)) for dist in self.conts) else: max_box = float(np.sum(self.dist)) if max_box == 0: self.scale_x = 1 return _, step = compute_scale(0, max_box) step = int(step) if step > 1 else 1 steps = int(math.ceil(max_box / step)) max_box = step * steps bv = self.box_view viewrect = bv.viewport().rect().adjusted(15, 15, -15, -30) self.scene_width = viewrect.width() lab_width = max(lab.boundingRect().width() for lab in self.attr_labels) lab_width = max(lab_width, 40) lab_width = min(lab_width, self.scene_width / 3) self.label_width = lab_width right_offset = 0 # offset for the right label if not self.stretched and self.labels: if self.group_var: rows = list(zip(self.conts, self.labels)) else: rows = [(self.dist, self.labels[0])] # available space left of the 'group labels' available = self.scene_width - lab_width - 10 scale_x = (available - right_offset) / max_box max_right = max(sum(dist) * scale_x + 10 + lbl.boundingRect().width() for dist, lbl in rows) right_offset = max(0, max_right - max_box * scale_x) self.scale_x = scale_x = \ (self.scene_width - lab_width - 10 - right_offset) / max_box self.box_scene.addLine(0, 0, max_box * scale_x, 0, self._pen_axis) for val in range(0, step * steps + 1, step): l = self.box_scene.addLine(val * scale_x, -1, val * scale_x, 1, self._pen_axis_tick) l.setZValue(100) t = self.box_scene.addSimpleText(str(val), self._axis_font) t.setPos(val * scale_x - t.boundingRect().width() / 2, 8) if self.stretched: self.scale_x *= 100 def label_group(self, stat, attr, mean_lab): def centered_text(val, pos): t = QGraphicsSimpleTextItem( "%.*f" % (attr.number_of_decimals + 1, val), labels) t.setFont(self._label_font) bbox = t.boundingRect() t.setPos(pos - bbox.width() / 2, 22) return t def line(x, down=1): QGraphicsLineItem(x, 12 * down, x, 20 * down, labels) def move_label(label, frm, to): label.setX(to) to += t_box.width() / 2 path = QPainterPath() path.lineTo(0, 4) path.lineTo(to - frm, 4) path.lineTo(to - frm, 8) p = QGraphicsPathItem(path) p.setPos(frm, 12) labels.addToGroup(p) labels = QGraphicsItemGroup() labels.addToGroup(mean_lab) m = stat.mean * self.scale_x mean_lab.setPos(m, -22) line(m, -1) if stat.median is not None: msc = stat.median * self.scale_x med_t = centered_text(stat.median, msc) med_box_width2 = med_t.boundingRect().width() / 2 line(msc) if stat.q25 is not None: x = stat.q25 * self.scale_x t = centered_text(stat.q25, x) t_box = t.boundingRect() med_left = msc - med_box_width2 if x + t_box.width() / 2 >= med_left - 5: move_label(t, x, med_left - t_box.width() - 5) else: line(x) if stat.q75 is not None: x = stat.q75 * self.scale_x t = centered_text(stat.q75, x) t_box = t.boundingRect() med_right = msc + med_box_width2 if x - t_box.width() / 2 <= med_right + 5: move_label(t, x, med_right + 5) else: line(x) return labels def box_group(self, stat, height=20): def line(x0, y0, x1, y1, *args): return QGraphicsLineItem(x0 * scale_x, y0, x1 * scale_x, y1, *args) scale_x = self.scale_x box = [] whisker1 = line(stat.a_min, -1.5, stat.a_min, 1.5) whisker2 = line(stat.a_max, -1.5, stat.a_max, 1.5) vert_line = line(stat.a_min, 0, stat.a_max, 0) mean_line = line(stat.mean, -height / 3, stat.mean, height / 3) for it in (whisker1, whisker2, mean_line): it.setPen(self._pen_paramet) vert_line.setPen(self._pen_dotted) var_line = line(stat.mean - stat.dev, 0, stat.mean + stat.dev, 0) var_line.setPen(self._pen_paramet) box.extend([whisker1, whisker2, vert_line, mean_line, var_line]) if stat.q25 is not None and stat.q75 is not None: mbox = FilterGraphicsRectItem( stat.conditions, stat.q25 * scale_x, -height / 2, (stat.q75 - stat.q25) * scale_x, height) mbox.setBrush(self._box_brush) mbox.setPen(QPen(Qt.NoPen)) mbox.setZValue(-200) box.append(mbox) if stat.median is not None: median_line = line(stat.median, -height / 2, stat.median, height / 2) median_line.setPen(self._pen_median) median_line.setZValue(-150) box.append(median_line) return box def strudel(self, dist, group_val_index=None): attr = self.attribute ss = np.sum(dist) box = [] if ss < 1e-6: cond = [FilterDiscrete(attr, None)] if group_val_index is not None: cond.append(FilterDiscrete(self.group_var, [group_val_index])) box.append(FilterGraphicsRectItem(cond, 0, -10, 1, 10)) cum = 0 for i, v in enumerate(dist): if v < 1e-6: continue if self.stretched: v /= ss v *= self.scale_x cond = [FilterDiscrete(attr, [i])] if group_val_index is not None: cond.append(FilterDiscrete(self.group_var, [group_val_index])) rect = FilterGraphicsRectItem(cond, cum + 1, -6, v - 2, 12) rect.setBrush(QBrush(QColor(*attr.colors[i]))) rect.setPen(QPen(Qt.NoPen)) if self.stretched: tooltip = "{}: {:.2f}%".format(attr.values[i], 100 * dist[i] / sum(dist)) else: tooltip = "{}: {}".format(attr.values[i], int(dist[i])) rect.setToolTip(tooltip) text = QGraphicsTextItem(attr.values[i]) box.append(rect) box.append(text) cum += v return box def commit(self): self.conditions = [item.filter for item in self.box_scene.selectedItems() if item.filter] selected, selection = None, [] if self.conditions: selected = Values(self.conditions, conjunction=False)(self.dataset) selection = np.in1d( self.dataset.ids, selected.ids, assume_unique=True).nonzero()[0] self.Outputs.selected_data.send(selected) self.Outputs.annotated_data.send( create_annotated_table(self.dataset, selection)) def show_posthoc(self): def line(y0, y1): it = self.box_scene.addLine(x, y0, x, y1, self._post_line_pen) it.setZValue(-100) self.posthoc_lines.append(it) while self.posthoc_lines: self.box_scene.removeItem(self.posthoc_lines.pop()) if self.compare == OWBoxPlot.CompareNone or len(self.stats) < 2: return if self.compare == OWBoxPlot.CompareMedians: crit_line = "median" else: crit_line = "mean" xs = [] height = 90 if self.show_annotations else 60 y_up = -len(self.stats) * height + 10 for pos, box_index in enumerate(self.order): stat = self.stats[box_index] x = getattr(stat, crit_line) if x is None: continue x *= self.scale_x xs.append(x * self.scale_x) by = y_up + pos * height line(by + 12, 3) line(by - 12, by - 25) used_to = [] last_to = to = 0 for frm, frm_x in enumerate(xs[:-1]): for to in range(frm + 1, len(xs)): if xs[to] - frm_x > 1.5: to -= 1 break if to in (last_to, frm): continue for rowi, used in enumerate(used_to): if used < frm: used_to[rowi] = to break else: rowi = len(used_to) used_to.append(to) y = - 6 - rowi * 6 it = self.box_scene.addLine(frm_x - 2, y, xs[to] + 2, y, self._post_grp_pen) self.posthoc_lines.append(it) last_to = to def get_widget_name_extension(self): return self.attribute.name if self.attribute else None def send_report(self): self.report_plot() text = "" if self.attribute: text += "Box plot for attribute '{}' ".format(self.attribute.name) if self.group_var: text += "grouped by '{}'".format(self.group_var.name) if text: self.report_caption(text) class Label(QGraphicsSimpleTextItem): """Boxplot Label with settable maxWidth""" # Minimum width to display label text MIN_LABEL_WIDTH = 25 # padding bellow the text PADDING = 3 __max_width = None def maxWidth(self): return self.__max_width def setMaxWidth(self, max_width): self.__max_width = max_width def paint(self, painter, option, widget): """Overrides QGraphicsSimpleTextItem.paint If label text is too long, it is elided to fit into the allowed region """ if self.__max_width is None: width = option.rect.width() else: width = self.__max_width if width < self.MIN_LABEL_WIDTH: # if space is too narrow, no label return fm = painter.fontMetrics() text = fm.elidedText(self.text(), Qt.ElideRight, width) painter.drawText( option.rect.x(), option.rect.y() + self.boundingRect().height() - self.PADDING, text)
class OWGeoMap(widget.OWWidget): name = "GeoMap" priority = 20000 icon = "icons/GeoMap.svg" inputs = [("Data", Table, "on_data")] outputs = [('Corpus', Corpus)] want_main_area = False selected_attr = settings.Setting('') selected_map = settings.Setting(0) regions = settings.Setting([]) def __init__(self): super().__init__() self.data = None self._create_layout() @pyqtSlot(str) def region_selected(self, regions): """Called from JavaScript""" if not regions: self.regions = [] if not regions or self.data is None: return self.send('Corpus', None) self.regions = regions.split(',') attr = self.data.domain[self.selected_attr] if attr.is_discrete: return # TODO, FIXME: make this work for discrete attrs also from Orange.data.filter import FilterRegex filter = FilterRegex(attr, r'\b{}\b'.format(r'\b|\b'.join(self.regions)), re.IGNORECASE) self.send('Corpus', self.data._filter_values(filter)) def _create_layout(self): box = gui.widgetBox(self.controlArea, orientation='horizontal') self.varmodel = VariableListModel(parent=self) self.attr_combo = gui.comboBox(box, self, 'selected_attr', orientation=Qt.Horizontal, label='Region attribute:', callback=self.on_attr_change, sendSelectedValue=True) self.attr_combo.setModel(self.varmodel) self.map_combo = gui.comboBox(box, self, 'selected_map', orientation=Qt.Horizontal, label='Map type:', callback=self.on_map_change, items=Map.all) hexpand = QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) self.attr_combo.setSizePolicy(hexpand) self.map_combo.setSizePolicy(hexpand) url = urljoin('file:', pathname2url(os.path.join( os.path.dirname(__file__), 'resources', 'owgeomap.html'))) self.webview = gui.WebviewWidget(self.controlArea, self, url=QUrl(url)) self.controlArea.layout().addWidget(self.webview) QTimer.singleShot( 0, lambda: self.webview.evalJS('REGIONS = {};'.format({Map.WORLD: CC_WORLD, Map.EUROPE: CC_EUROPE, Map.USA: CC_USA}))) def _repopulate_attr_combo(self, data): vars = [a for a in chain(data.domain.metas, data.domain.attributes, data.domain.class_vars) if a.is_string] if data else [] self.varmodel.wrap(vars) # Select default attribute self.selected_attr = next((var.name for var in vars if var.name.lower().startswith(('country', 'location', 'region'))), vars[0].name if vars else '') def on_data(self, data): if data and not isinstance(data, Corpus): data = Corpus.from_table(data.domain, data) self.data = data self._repopulate_attr_combo(data) if not data: self.region_selected('') QTimer.singleShot(0, lambda: self.webview.evalJS('DATA = {}; renderMap();')) else: QTimer.singleShot(0, self.on_attr_change) def on_map_change(self, map_code=''): if map_code: self.map_combo.setCurrentIndex(self.map_combo.findData(map_code)) else: map_code = self.map_combo.itemData(self.selected_map) inv_cc_map, cc_map = {Map.USA: (INV_CC_USA, CC_USA), Map.WORLD: (INV_CC_WORLD, CC_WORLD), Map.EUROPE: (INV_CC_EUROPE, CC_EUROPE)}[map_code] # Set country counts for JS data = defaultdict(int) for locations in self._iter_locations(): keys = set(inv_cc_map.get(loc, loc) for loc in locations) for key in keys: if key in cc_map: data[key] += 1 # Draw the new map self.webview.evalJS('DATA = {};' 'MAP_CODE = "{}";' 'SELECTED_REGIONS = {};' 'renderMap();'.format(dict(data), map_code, self.regions)) def on_attr_change(self): if not self.selected_attr: return values = set(chain.from_iterable(self._iter_locations())) # Auto-select region map if 0 == len(values - SET_CC_USA): map_code = Map.USA elif 0 == len(values - SET_CC_EUROPE): map_code = Map.EUROPE else: map_code = Map.WORLD self.on_map_change(map_code) def _iter_locations(self): """ Iterator that yields an iterable per documents with all its's locations. """ attr = self.data.domain[self.selected_attr] for i in self.data.get_column_view(self.data.domain.index(attr))[0]: if len(i) > 3: yield map(lambda x: x.strip(), CC_NAMES.findall(i.lower())) else: yield (i, )
class OWBoxPlot(widget.OWWidget): """ Here's how the widget's functions call each other: - `set_data` is a signal handler fills the list boxes and calls `grouping_changed`. - `grouping_changed` handles changes of grouping attribute: it enables or disables the box for ordering, orders attributes and calls `attr_changed`. - `attr_changed` handles changes of attribute. It recomputes box data by calling `compute_box_data`, shows the appropriate display box (discrete/continuous) and then calls`layout_changed` - `layout_changed` constructs all the elements for the scene (as lists of QGraphicsItemGroup) and calls `display_changed`. It is called when the attribute or grouping is changed (by attr_changed) and on resize event. - `display_changed` puts the elements corresponding to the current display settings on the scene. It is called when the elements are reconstructed (layout is changed due to selection of attributes or resize event), or when the user changes display settings or colors. For discrete attributes, the flow is a bit simpler: the elements are not constructed in advance (by layout_changed). Instead, layout_changed and display_changed call display_changed_disc that draws everything. """ name = "箱形图" description = "在方框图中可视化特征值的分布。" icon = "icons/BoxPlot.svg" priority = 100 keywords = ["whisker"] class Inputs: data = Input("数据", Orange.data.Table) class Outputs: selected_data = Output("所选数据", Orange.data.Table, default=True) annotated_data = Output(ANNOTATED_DATA_SIGNAL_NAME, Orange.data.Table) #: Comparison types for continuous variables CompareNone, CompareMedians, CompareMeans = 0, 1, 2 settingsHandler = DomainContextHandler() conditions = ContextSetting([]) attribute = ContextSetting(None) order_by_importance = Setting(False) group_var = ContextSetting(None) show_annotations = Setting(True) compare = Setting(CompareMeans) stattest = Setting(0) sig_threshold = Setting(0.05) stretched = Setting(True) show_labels = Setting(True) sort_freqs = Setting(False) auto_commit = Setting(True) _sorting_criteria_attrs = { CompareNone: "", CompareMedians: "median", CompareMeans: "mean" } _pen_axis_tick = QPen(Qt.white, 5) _pen_axis = QPen(Qt.darkGray, 3) _pen_median = QPen(QBrush(QColor(0xff, 0xff, 0x00)), 2) _pen_paramet = QPen(QBrush(QColor(0x33, 0x00, 0xff)), 2) _pen_dotted = QPen(QBrush(QColor(0x33, 0x00, 0xff)), 1) _pen_dotted.setStyle(Qt.DotLine) _post_line_pen = QPen(Qt.lightGray, 2) _post_grp_pen = QPen(Qt.lightGray, 4) for pen in (_pen_paramet, _pen_median, _pen_dotted, _pen_axis, _pen_axis_tick, _post_line_pen, _post_grp_pen): pen.setCosmetic(True) pen.setCapStyle(Qt.RoundCap) pen.setJoinStyle(Qt.RoundJoin) _pen_axis_tick.setCapStyle(Qt.FlatCap) _box_brush = QBrush(QColor(0x33, 0x88, 0xff, 0xc0)) _axis_font = QFont() _axis_font.setPixelSize(12) _label_font = QFont() _label_font.setPixelSize(11) _attr_brush = QBrush(QColor(0x33, 0x00, 0xff)) graph_name = "盒式布景" def __init__(self): super().__init__() self.stats = [] self.dataset = None self.posthoc_lines = [] self.label_txts = self.mean_labels = self.boxes = self.labels = \ self.label_txts_all = self.attr_labels = self.order = [] self.p = -1.0 self.scale_x = self.scene_min_x = self.scene_width = 0 self.label_width = 0 self.attrs = VariableListModel() view = gui.listView(self.controlArea, self, "attribute", box="变量", model=self.attrs, callback=self.attr_changed) view.setMinimumSize(QSize(30, 30)) # Any other policy than Ignored will let the QListBox's scrollbar # set the minimal height (see the penultimate paragraph of # http://doc.qt.io/qt-4.8/qabstractscrollarea.html#addScrollBarWidget) view.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Ignored) gui.separator(view.box, 6, 6) self.cb_order = gui.checkBox(view.box, self, "order_by_importance", "按相关性排序", tooltip="由𝜒²或方差对子群排序", callback=self.apply_sorting) self.group_vars = DomainModel(placeholder="无", separators=False, valid_types=Orange.data.DiscreteVariable) self.group_view = view = gui.listView(self.controlArea, self, "group_var", box="子群", model=self.group_vars, callback=self.grouping_changed) view.setEnabled(False) view.setMinimumSize(QSize(30, 30)) # See the comment above view.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Ignored) # TODO: move Compare median/mean to grouping box # The vertical size policy is needed to let only the list views expand self.display_box = gui.vBox(self.controlArea, "Display", sizePolicy=(QSizePolicy.Minimum, QSizePolicy.Maximum), addSpace=False) gui.checkBox(self.display_box, self, "show_annotations", "Annotate", callback=self.display_changed) self.compare_rb = gui.radioButtonsInBox( self.display_box, self, 'compare', btnLabels=["无比较", "中位数比较", "均值比较"], callback=self.layout_changed) # The vertical size policy is needed to let only the list views expand self.stretching_box = box = gui.vBox(self.controlArea, box="显示", sizePolicy=(QSizePolicy.Minimum, QSizePolicy.Fixed)) self.stretching_box.sizeHint = self.display_box.sizeHint gui.checkBox(box, self, 'stretched', "拉杆", callback=self.display_changed) gui.checkBox(box, self, 'show_labels', "显示框标签", callback=self.display_changed) self.sort_cb = gui.checkBox(box, self, 'sort_freqs', "按子组频率排序", callback=self.display_changed) gui.rubber(box) gui.auto_commit(self.controlArea, self, "auto_commit", "选中发送", "自动发送") gui.vBox(self.mainArea, addSpace=True) self.box_scene = QGraphicsScene() self.box_scene.selectionChanged.connect(self.commit) self.box_view = QGraphicsView(self.box_scene) self.box_view.setRenderHints(QPainter.Antialiasing | QPainter.TextAntialiasing | QPainter.SmoothPixmapTransform) self.box_view.viewport().installEventFilter(self) self.mainArea.layout().addWidget(self.box_view) e = gui.hBox(self.mainArea, addSpace=False) self.infot1 = gui.widgetLabel(e, "<center>没有测试结果。</center>") self.mainArea.setMinimumWidth(600) self.stats = self.dist = self.conts = [] self.is_continuous = False self.update_display_box() def sizeHint(self): return QSize(100, 500) # Vertical size is regulated by mainArea def eventFilter(self, obj, event): if obj is self.box_view.viewport() and \ event.type() == QEvent.Resize: self.layout_changed() return super().eventFilter(obj, event) def reset_attrs(self, domain): self.attrs[:] = [ var for var in chain(domain.class_vars, domain.metas, domain.attributes) if var.is_primitive() ] # noinspection PyTypeChecker @Inputs.data def set_data(self, dataset): if dataset is not None and (not bool(dataset) or not len(dataset.domain) and not any(var.is_primitive() for var in dataset.domain.metas)): dataset = None self.closeContext() self.dataset = dataset self.dist = self.stats = self.conts = [] self.group_var = None self.attribute = None if dataset: domain = dataset.domain self.group_vars.set_domain(domain) self.group_view.setEnabled(len(self.group_vars) > 1) self.reset_attrs(domain) self.select_default_variables(domain) self.openContext(self.dataset) self.grouping_changed() else: self.reset_all_data() self.commit() def select_default_variables(self, domain): # visualize first non-class variable, group by class (if present) if len(self.attrs) > len(domain.class_vars): self.attribute = self.attrs[len(domain.class_vars)] elif self.attrs: self.attribute = self.attrs[0] if domain.class_var and domain.class_var.is_discrete: self.group_var = domain.class_var else: self.group_var = None # Reset to trigger selection via callback def apply_sorting(self): def compute_score(attr): if attr is group_var: return 3 if attr.is_continuous: # One-way ANOVA col = data.get_column_view(attr)[0].astype(float) groups = (col[group_col == i] for i in range(n_groups)) groups = (col[~np.isnan(col)] for col in groups) groups = [group for group in groups if len(group)] p = f_oneway(*groups)[1] if len(groups) > 1 else 2 else: # Chi-square with the given distribution into groups # (see degrees of freedom in computation of the p-value) if not attr.values or not group_var.values: return 2 observed = np.array( contingency.get_contingency(data, group_var, attr)) observed = observed[observed.sum(axis=1) != 0, :] observed = observed[:, observed.sum(axis=0) != 0] if min(observed.shape) < 2: return 2 expected = \ np.outer(observed.sum(axis=1), observed.sum(axis=0)) / \ np.sum(observed) p = chisquare(observed.ravel(), f_exp=expected.ravel(), ddof=n_groups - 1)[1] if math.isnan(p): return 2 return p data = self.dataset if data is None: return domain = data.domain attribute = self.attribute group_var = self.group_var if self.order_by_importance and group_var is not None: n_groups = len(group_var.values) group_col = data.get_column_view(group_var)[0] if \ domain.has_continuous_attributes( include_class=True, include_metas=True) else None self.attrs.sort(key=compute_score) else: self.reset_attrs(domain) self.attribute = attribute def reset_all_data(self): self.clear_scene() self.infot1.setText("") self.attrs.clear() self.group_vars.set_domain(None) self.group_view.setEnabled(False) self.is_continuous = False self.update_display_box() def grouping_changed(self): self.cb_order.setEnabled(self.group_var is not None) self.apply_sorting() self.attr_changed() def select_box_items(self): temp_cond = self.conditions.copy() for box in self.box_scene.items(): if isinstance(box, FilterGraphicsRectItem): box.setSelected( box.filter.conditions in [c.conditions for c in temp_cond]) def attr_changed(self): self.compute_box_data() self.update_display_box() self.layout_changed() if self.is_continuous: heights = 90 if self.show_annotations else 60 self.box_view.centerOn(self.scene_min_x + self.scene_width / 2, -30 - len(self.stats) * heights / 2 + 45) else: self.box_view.centerOn(self.scene_width / 2, -30 - len(self.boxes) * 40 / 2 + 45) def compute_box_data(self): attr = self.attribute if not attr: return dataset = self.dataset self.is_continuous = attr.is_continuous if dataset is None or not self.is_continuous and not attr.values or \ self.group_var and not self.group_var.values: self.stats = self.dist = self.conts = [] return if self.group_var: self.dist = [] self.conts = contingency.get_contingency(dataset, attr, self.group_var) if self.is_continuous: stats, label_texts = [], [] for i, cont in enumerate(self.conts): if np.sum(cont[1]): stats.append(BoxData(cont, attr, i, self.group_var)) label_texts.append(self.group_var.values[i]) self.stats = stats self.label_txts_all = label_texts else: self.label_txts_all = \ [v for v, c in zip(self.group_var.values, self.conts) if np.sum(c) > 0] else: self.dist = distribution.get_distribution(dataset, attr) self.conts = [] if self.is_continuous: self.stats = [BoxData(self.dist, attr, None)] self.label_txts_all = [""] self.label_txts = [ txts for stat, txts in zip(self.stats, self.label_txts_all) if stat.n > 0 ] self.stats = [stat for stat in self.stats if stat.n > 0] def update_display_box(self): if self.is_continuous: self.stretching_box.hide() self.display_box.show() self.compare_rb.setEnabled(self.group_var is not None) else: self.stretching_box.show() self.display_box.hide() self.sort_cb.setEnabled(self.group_var is not None) def clear_scene(self): self.closeContext() self.box_scene.clearSelection() self.box_scene.clear() self.box_view.viewport().update() self.attr_labels = [] self.labels = [] self.boxes = [] self.mean_labels = [] self.posthoc_lines = [] self.openContext(self.dataset) def layout_changed(self): attr = self.attribute if not attr: return self.clear_scene() if self.dataset is None or len(self.conts) == len(self.dist) == 0: return if not self.is_continuous: self.display_changed_disc() return self.mean_labels = [ self.mean_label(stat, attr, lab) for stat, lab in zip(self.stats, self.label_txts) ] self.draw_axis() self.boxes = [self.box_group(stat) for stat in self.stats] self.labels = [ self.label_group(stat, attr, mean_lab) for stat, mean_lab in zip(self.stats, self.mean_labels) ] self.attr_labels = [ QGraphicsSimpleTextItem(lab) for lab in self.label_txts ] for it in chain(self.labels, self.attr_labels): self.box_scene.addItem(it) self.display_changed() def display_changed(self): if self.dataset is None: return if not self.is_continuous: self.display_changed_disc() return self.order = list(range(len(self.stats))) criterion = self._sorting_criteria_attrs[self.compare] if criterion: vals = [getattr(stat, criterion) for stat in self.stats] overmax = max((val for val in vals if val is not None), default=0) \ + 1 vals = [val if val is not None else overmax for val in vals] self.order = sorted(self.order, key=vals.__getitem__) heights = 90 if self.show_annotations else 60 for row, box_index in enumerate(self.order): y = (-len(self.stats) + row) * heights + 10 for item in self.boxes[box_index]: self.box_scene.addItem(item) item.setY(y) labels = self.labels[box_index] if self.show_annotations: labels.show() labels.setY(y) else: labels.hide() label = self.attr_labels[box_index] label.setY(y - 15 - label.boundingRect().height()) if self.show_annotations: label.hide() else: stat = self.stats[box_index] if self.compare == OWBoxPlot.CompareMedians and \ stat.median is not None: pos = stat.median + 5 / self.scale_x elif self.compare == OWBoxPlot.CompareMeans or stat.q25 is None: pos = stat.mean + 5 / self.scale_x else: pos = stat.q25 label.setX(pos * self.scale_x) label.show() r = QRectF(self.scene_min_x, -30 - len(self.stats) * heights, self.scene_width, len(self.stats) * heights + 90) self.box_scene.setSceneRect(r) self.compute_tests() self.show_posthoc() self.select_box_items() def display_changed_disc(self): assert not self.is_continuous self.clear_scene() self.attr_labels = [ QGraphicsSimpleTextItem(lab) for lab in self.label_txts_all ] if not self.stretched: if self.group_var: self.labels = [ QGraphicsTextItem("{}".format(int(sum(cont)))) for cont in self.conts if np.sum(cont) > 0 ] else: self.labels = [QGraphicsTextItem(str(int(sum(self.dist))))] self.order = list(range(len(self.attr_labels))) self.draw_axis_disc() if self.group_var: self.boxes = \ [self.strudel(cont, i) for i, cont in enumerate(self.conts) if np.sum(cont) > 0] self.conts = self.conts[np.sum(np.array(self.conts), axis=1) > 0] if self.sort_freqs: # pylint: disable=invalid-unary-operand-type self.order = sorted( self.order, key=(-np.sum(self.conts, axis=1)).__getitem__) else: self.boxes = [self.strudel(self.dist)] for row, box_index in enumerate(self.order): y = (-len(self.boxes) + row) * 40 + 10 box = self.boxes[box_index] bars, labels = box[::2], box[1::2] self.__draw_group_labels(y, box_index) if not self.stretched: self.__draw_row_counts(y, box_index) if self.show_labels and self.attribute is not self.group_var: self.__draw_bar_labels(y, bars, labels) self.__draw_bars(y, bars) self.box_scene.setSceneRect(-self.label_width - 5, -30 - len(self.boxes) * 40, self.scene_width, len(self.boxes * 40) + 90) self.infot1.setText("") self.select_box_items() def __draw_group_labels(self, y, row): """Draw group labels Parameters ---------- y: int vertical offset of bars row: int row index """ label = self.attr_labels[row] b = label.boundingRect() label.setPos(-b.width() - 10, y - b.height() / 2) self.box_scene.addItem(label) def __draw_row_counts(self, y, row): """Draw row counts Parameters ---------- y: int vertical offset of bars row: int row index """ assert not self.is_continuous label = self.labels[row] b = label.boundingRect() if self.group_var: right = self.scale_x * sum(self.conts[row]) else: right = self.scale_x * sum(self.dist) label.setPos(right + 10, y - b.height() / 2) self.box_scene.addItem(label) def __draw_bar_labels(self, y, bars, labels): """Draw bar labels Parameters ---------- y: int vertical offset of bars bars: List[FilterGraphicsRectItem] list of bars being drawn labels: List[QGraphicsTextItem] list of labels for corresponding bars """ label = bar_part = None for text_item, bar_part in zip(labels, bars): label = self.Label(text_item.toPlainText()) label.setPos(bar_part.boundingRect().x(), y - label.boundingRect().height() - 8) label.setMaxWidth(bar_part.boundingRect().width()) self.box_scene.addItem(label) def __draw_bars(self, y, bars): """Draw bars Parameters ---------- y: int vertical offset of bars bars: List[FilterGraphicsRectItem] list of bars to draw """ for item in bars: item.setPos(0, y) self.box_scene.addItem(item) # noinspection PyPep8Naming def compute_tests(self): # The t-test and ANOVA are implemented here since they efficiently use # the widget-specific data in self.stats. # The non-parametric tests can't do this, so we use statistics.tests def stat_ttest(): d1, d2 = self.stats if d1.n == 0 or d2.n == 0: return np.nan, np.nan pooled_var = d1.var / d1.n + d2.var / d2.n df = pooled_var ** 2 / \ ((d1.var / d1.n) ** 2 / (d1.n - 1) + (d2.var / d2.n) ** 2 / (d2.n - 1)) if pooled_var == 0: return np.nan, np.nan t = abs(d1.mean - d2.mean) / math.sqrt(pooled_var) p = 2 * (1 - scipy.special.stdtr(df, t)) return t, p # TODO: Check this function # noinspection PyPep8Naming def stat_ANOVA(): if any(stat.n == 0 for stat in self.stats): return np.nan, np.nan n = sum(stat.n for stat in self.stats) grand_avg = sum(stat.n * stat.mean for stat in self.stats) / n var_between = sum(stat.n * (stat.mean - grand_avg)**2 for stat in self.stats) df_between = len(self.stats) - 1 var_within = sum(stat.n * stat.var for stat in self.stats) df_within = n - len(self.stats) F = (var_between / df_between) / (var_within / df_within) p = 1 - scipy.special.fdtr(df_between, df_within, F) return F, p if self.compare == OWBoxPlot.CompareNone or len(self.stats) < 2: t = "" elif any(s.n <= 1 for s in self.stats): t = "At least one group has just one instance," \ "cannot compute significance" elif len(self.stats) == 2: if self.compare == OWBoxPlot.CompareMedians: t = "" # z, self.p = tests.wilcoxon_rank_sum( # self.stats[0].dist, self.stats[1].dist) # t = "Mann-Whitney's z: %.1f (p=%.3f)" % (z, self.p) else: t, self.p = stat_ttest() t = "Student's t: %.3f (p=%.3f)" % (t, self.p) else: if self.compare == OWBoxPlot.CompareMedians: t = "" # U, self.p = -1, -1 # t = "Kruskal Wallis's U: %.1f (p=%.3f)" % (U, self.p) else: F, self.p = stat_ANOVA() t = "ANOVA: %.3f (p=%.3f)" % (F, self.p) self.infot1.setText("<center>%s</center>" % t) def mean_label(self, stat, attr, val_name): label = QGraphicsItemGroup() t = QGraphicsSimpleTextItem( "%.*f" % (attr.number_of_decimals + 1, stat.mean), label) t.setFont(self._label_font) bbox = t.boundingRect() w2, h = bbox.width() / 2, bbox.height() t.setPos(-w2, -h) tpm = QGraphicsSimpleTextItem( " \u00b1 " + "%.*f" % (attr.number_of_decimals + 1, stat.dev), label) tpm.setFont(self._label_font) tpm.setPos(w2, -h) if val_name: vnm = QGraphicsSimpleTextItem(val_name + ": ", label) vnm.setFont(self._label_font) vnm.setBrush(self._attr_brush) vb = vnm.boundingRect() label.min_x = -w2 - vb.width() vnm.setPos(label.min_x, -h) else: label.min_x = -w2 return label def draw_axis(self): """Draw the horizontal axis and sets self.scale_x""" misssing_stats = not self.stats stats = self.stats or [BoxData(np.array([[0.], [1.]]), self.attribute)] mean_labels = self.mean_labels or [ self.mean_label(stats[0], self.attribute, "") ] bottom = min(stat.a_min for stat in stats) top = max(stat.a_max for stat in stats) first_val, step = compute_scale(bottom, top) while bottom <= first_val: first_val -= step bottom = first_val no_ticks = math.ceil((top - first_val) / step) + 1 top = max(top, first_val + no_ticks * step) gbottom = min(bottom, min(stat.mean - stat.dev for stat in stats)) gtop = max(top, max(stat.mean + stat.dev for stat in stats)) bv = self.box_view viewrect = bv.viewport().rect().adjusted(15, 15, -15, -30) self.scale_x = scale_x = viewrect.width() / (gtop - gbottom) # In principle we should repeat this until convergence since the new # scaling is too conservative. (No chance am I doing this.) mlb = min(stat.mean + mean_lab.min_x / scale_x for stat, mean_lab in zip(stats, mean_labels)) if mlb < gbottom: gbottom = mlb self.scale_x = scale_x = viewrect.width() / (gtop - gbottom) self.scene_min_x = gbottom * scale_x self.scene_width = (gtop - gbottom) * scale_x val = first_val decimals = max(3, 4 - int(math.log10(step))) while True: l = self.box_scene.addLine(val * scale_x, -1, val * scale_x, 1, self._pen_axis_tick) l.setZValue(100) t = self.box_scene.addSimpleText( repr(round(val, decimals)) if not misssing_stats else "?", self._axis_font) t.setFlags(t.flags() | QGraphicsItem.ItemIgnoresTransformations) r = t.boundingRect() t.setPos(val * scale_x - r.width() / 2, 8) if val >= top: break val += step self.box_scene.addLine(bottom * scale_x - 4, 0, top * scale_x + 4, 0, self._pen_axis) def draw_axis_disc(self): """ Draw the horizontal axis and sets self.scale_x for discrete attributes """ assert not self.is_continuous if self.stretched: if not self.attr_labels: return step = steps = 10 else: if self.group_var: max_box = max(float(np.sum(dist)) for dist in self.conts) else: max_box = float(np.sum(self.dist)) if max_box == 0: self.scale_x = 1 return _, step = compute_scale(0, max_box) step = int(step) if step > 1 else 1 steps = int(math.ceil(max_box / step)) max_box = step * steps bv = self.box_view viewrect = bv.viewport().rect().adjusted(15, 15, -15, -30) self.scene_width = viewrect.width() lab_width = max(lab.boundingRect().width() for lab in self.attr_labels) lab_width = max(lab_width, 40) lab_width = min(lab_width, self.scene_width / 3) self.label_width = lab_width right_offset = 0 # offset for the right label if not self.stretched and self.labels: if self.group_var: rows = list(zip(self.conts, self.labels)) else: rows = [(self.dist, self.labels[0])] # available space left of the 'group labels' available = self.scene_width - lab_width - 10 scale_x = (available - right_offset) / max_box max_right = max( sum(dist) * scale_x + 10 + lbl.boundingRect().width() for dist, lbl in rows) right_offset = max(0, max_right - max_box * scale_x) self.scale_x = scale_x = \ (self.scene_width - lab_width - 10 - right_offset) / max_box self.box_scene.addLine(0, 0, max_box * scale_x, 0, self._pen_axis) for val in range(0, step * steps + 1, step): l = self.box_scene.addLine(val * scale_x, -1, val * scale_x, 1, self._pen_axis_tick) l.setZValue(100) t = self.box_scene.addSimpleText(str(val), self._axis_font) t.setPos(val * scale_x - t.boundingRect().width() / 2, 8) if self.stretched: self.scale_x *= 100 def label_group(self, stat, attr, mean_lab): def centered_text(val, pos): t = QGraphicsSimpleTextItem( "%.*f" % (attr.number_of_decimals + 1, val), labels) t.setFont(self._label_font) bbox = t.boundingRect() t.setPos(pos - bbox.width() / 2, 22) return t def line(x, down=1): QGraphicsLineItem(x, 12 * down, x, 20 * down, labels) def move_label(label, frm, to): label.setX(to) to += t_box.width() / 2 path = QPainterPath() path.lineTo(0, 4) path.lineTo(to - frm, 4) path.lineTo(to - frm, 8) p = QGraphicsPathItem(path) p.setPos(frm, 12) labels.addToGroup(p) labels = QGraphicsItemGroup() labels.addToGroup(mean_lab) m = stat.mean * self.scale_x mean_lab.setPos(m, -22) line(m, -1) if stat.median is not None: msc = stat.median * self.scale_x med_t = centered_text(stat.median, msc) med_box_width2 = med_t.boundingRect().width() / 2 line(msc) if stat.q25 is not None: x = stat.q25 * self.scale_x t = centered_text(stat.q25, x) t_box = t.boundingRect() med_left = msc - med_box_width2 if x + t_box.width() / 2 >= med_left - 5: move_label(t, x, med_left - t_box.width() - 5) else: line(x) if stat.q75 is not None: x = stat.q75 * self.scale_x t = centered_text(stat.q75, x) t_box = t.boundingRect() med_right = msc + med_box_width2 if x - t_box.width() / 2 <= med_right + 5: move_label(t, x, med_right + 5) else: line(x) return labels def box_group(self, stat, height=20): def line(x0, y0, x1, y1, *args): return QGraphicsLineItem(x0 * scale_x, y0, x1 * scale_x, y1, *args) scale_x = self.scale_x box = [] whisker1 = line(stat.a_min, -1.5, stat.a_min, 1.5) whisker2 = line(stat.a_max, -1.5, stat.a_max, 1.5) vert_line = line(stat.a_min, 0, stat.a_max, 0) mean_line = line(stat.mean, -height / 3, stat.mean, height / 3) for it in (whisker1, whisker2, mean_line): it.setPen(self._pen_paramet) vert_line.setPen(self._pen_dotted) var_line = line(stat.mean - stat.dev, 0, stat.mean + stat.dev, 0) var_line.setPen(self._pen_paramet) box.extend([whisker1, whisker2, vert_line, mean_line, var_line]) if stat.q25 is not None and stat.q75 is not None: mbox = FilterGraphicsRectItem(stat.conditions, stat.q25 * scale_x, -height / 2, (stat.q75 - stat.q25) * scale_x, height) mbox.setBrush(self._box_brush) mbox.setPen(QPen(Qt.NoPen)) mbox.setZValue(-200) box.append(mbox) if stat.median is not None: median_line = line(stat.median, -height / 2, stat.median, height / 2) median_line.setPen(self._pen_median) median_line.setZValue(-150) box.append(median_line) return box def strudel(self, dist, group_val_index=None): attr = self.attribute ss = np.sum(dist) box = [] if ss < 1e-6: cond = [FilterDiscrete(attr, None)] if group_val_index is not None: cond.append(FilterDiscrete(self.group_var, [group_val_index])) box.append(FilterGraphicsRectItem(cond, 0, -10, 1, 10)) cum = 0 for i, v in enumerate(dist): if v < 1e-6: continue if self.stretched: v /= ss v *= self.scale_x cond = [FilterDiscrete(attr, [i])] if group_val_index is not None: cond.append(FilterDiscrete(self.group_var, [group_val_index])) rect = FilterGraphicsRectItem(cond, cum + 1, -6, v - 2, 12) rect.setBrush(QBrush(QColor(*attr.colors[i]))) rect.setPen(QPen(Qt.NoPen)) if self.stretched: tooltip = "{}: {:.2f}%".format(attr.values[i], 100 * dist[i] / sum(dist)) else: tooltip = "{}: {}".format(attr.values[i], int(dist[i])) rect.setToolTip(tooltip) text = QGraphicsTextItem(attr.values[i]) box.append(rect) box.append(text) cum += v return box def commit(self): self.conditions = [ item.filter for item in self.box_scene.selectedItems() if item.filter ] selected, selection = None, [] if self.conditions: selected = Values(self.conditions, conjunction=False)(self.dataset) selection = np.in1d(self.dataset.ids, selected.ids, assume_unique=True).nonzero()[0] self.Outputs.selected_data.send(selected) self.Outputs.annotated_data.send( create_annotated_table(self.dataset, selection)) def show_posthoc(self): def line(y0, y1): it = self.box_scene.addLine(x, y0, x, y1, self._post_line_pen) it.setZValue(-100) self.posthoc_lines.append(it) while self.posthoc_lines: self.box_scene.removeItem(self.posthoc_lines.pop()) if self.compare == OWBoxPlot.CompareNone or len(self.stats) < 2: return if self.compare == OWBoxPlot.CompareMedians: crit_line = "median" else: crit_line = "mean" xs = [] height = 90 if self.show_annotations else 60 y_up = -len(self.stats) * height + 10 for pos, box_index in enumerate(self.order): stat = self.stats[box_index] x = getattr(stat, crit_line) if x is None: continue x *= self.scale_x xs.append(x * self.scale_x) by = y_up + pos * height line(by + 12, 3) line(by - 12, by - 25) used_to = [] last_to = to = 0 for frm, frm_x in enumerate(xs[:-1]): for to in range(frm + 1, len(xs)): if xs[to] - frm_x > 1.5: to -= 1 break if to in (last_to, frm): continue for rowi, used in enumerate(used_to): if used < frm: used_to[rowi] = to break else: rowi = len(used_to) used_to.append(to) y = -6 - rowi * 6 it = self.box_scene.addLine(frm_x - 2, y, xs[to] + 2, y, self._post_grp_pen) self.posthoc_lines.append(it) last_to = to def get_widget_name_extension(self): return self.attribute.name if self.attribute else None def send_report(self): self.report_plot() text = "" if self.attribute: text += "Box plot for attribute '{}' ".format(self.attribute.name) if self.group_var: text += "grouped by '{}'".format(self.group_var.name) if text: self.report_caption(text) class Label(QGraphicsSimpleTextItem): """Boxplot Label with settable maxWidth""" # Minimum width to display label text MIN_LABEL_WIDTH = 25 # padding bellow the text PADDING = 3 __max_width = None def maxWidth(self): return self.__max_width def setMaxWidth(self, max_width): self.__max_width = max_width def paint(self, painter, option, widget): """Overrides QGraphicsSimpleTextItem.paint If label text is too long, it is elided to fit into the allowed region """ if self.__max_width is None: width = option.rect.width() else: width = self.__max_width if width < self.MIN_LABEL_WIDTH: # if space is too narrow, no label return fm = painter.fontMetrics() text = fm.elidedText(self.text(), Qt.ElideRight, width) painter.drawText( option.rect.x(), option.rect.y() + self.boundingRect().height() - self.PADDING, text)
def __init__(self): super().__init__() self.stats = [] self.dataset = None self.posthoc_lines = [] self.label_txts = self.mean_labels = self.boxes = self.labels = \ self.label_txts_all = self.attr_labels = self.order = [] self.p = -1.0 self.scale_x = self.scene_min_x = self.scene_width = 0 self.label_width = 0 self.attrs = VariableListModel() view = gui.listView(self.controlArea, self, "attribute", box="变量", model=self.attrs, callback=self.attr_changed) view.setMinimumSize(QSize(30, 30)) # Any other policy than Ignored will let the QListBox's scrollbar # set the minimal height (see the penultimate paragraph of # http://doc.qt.io/qt-4.8/qabstractscrollarea.html#addScrollBarWidget) view.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Ignored) gui.separator(view.box, 6, 6) self.cb_order = gui.checkBox(view.box, self, "order_by_importance", "按相关性排序", tooltip="由𝜒²或方差对子群排序", callback=self.apply_sorting) self.group_vars = DomainModel(placeholder="无", separators=False, valid_types=Orange.data.DiscreteVariable) self.group_view = view = gui.listView(self.controlArea, self, "group_var", box="子群", model=self.group_vars, callback=self.grouping_changed) view.setEnabled(False) view.setMinimumSize(QSize(30, 30)) # See the comment above view.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Ignored) # TODO: move Compare median/mean to grouping box # The vertical size policy is needed to let only the list views expand self.display_box = gui.vBox(self.controlArea, "Display", sizePolicy=(QSizePolicy.Minimum, QSizePolicy.Maximum), addSpace=False) gui.checkBox(self.display_box, self, "show_annotations", "Annotate", callback=self.display_changed) self.compare_rb = gui.radioButtonsInBox( self.display_box, self, 'compare', btnLabels=["无比较", "中位数比较", "均值比较"], callback=self.layout_changed) # The vertical size policy is needed to let only the list views expand self.stretching_box = box = gui.vBox(self.controlArea, box="显示", sizePolicy=(QSizePolicy.Minimum, QSizePolicy.Fixed)) self.stretching_box.sizeHint = self.display_box.sizeHint gui.checkBox(box, self, 'stretched', "拉杆", callback=self.display_changed) gui.checkBox(box, self, 'show_labels', "显示框标签", callback=self.display_changed) self.sort_cb = gui.checkBox(box, self, 'sort_freqs', "按子组频率排序", callback=self.display_changed) gui.rubber(box) gui.auto_commit(self.controlArea, self, "auto_commit", "选中发送", "自动发送") gui.vBox(self.mainArea, addSpace=True) self.box_scene = QGraphicsScene() self.box_scene.selectionChanged.connect(self.commit) self.box_view = QGraphicsView(self.box_scene) self.box_view.setRenderHints(QPainter.Antialiasing | QPainter.TextAntialiasing | QPainter.SmoothPixmapTransform) self.box_view.viewport().installEventFilter(self) self.mainArea.layout().addWidget(self.box_view) e = gui.hBox(self.mainArea, addSpace=False) self.infot1 = gui.widgetLabel(e, "<center>没有测试结果。</center>") self.mainArea.setMinimumWidth(600) self.stats = self.dist = self.conts = [] self.is_continuous = False self.update_display_box()
class OWSpiralogram(widget.OWWidget): name = 'Spiralogram' description = "Visualize time series' periodicity in a spiral heatmap." icon = 'icons/Spiralogram.svg' priority = 120 class Inputs: time_series = Input("Time series", Table) class Outputs: time_series = Output("Time series", Timeseries) settingsHandler = settings.DomainContextHandler() ax1 = settings.ContextSetting('months of year') ax2 = settings.ContextSetting('years') agg_attr = settings.ContextSetting([]) agg_func = settings.ContextSetting(0) invert_date_order = settings.Setting(False) graph_name = 'chart' def __init__(self): self.data = None self.indices = [] box = gui.vBox(self.controlArea, 'Axes') self.combo_ax2_model = VariableListModel(parent=self) self.combo_ax1_model = VariableListModel(parent=self) for model in (self.combo_ax1_model, self.combo_ax2_model): model[:] = [_enum_str(i) for i in Spiralogram.AxesCategories] self.combo_ax2 = gui.comboBox( box, self, 'ax2', label='Y axis:', callback=self.replot, sendSelectedValue=True, orientation='horizontal', model=self.combo_ax2_model) self.combo_ax1 = gui.comboBox( box, self, 'ax1', label='Radial:', callback=self.replot, sendSelectedValue=True, orientation='horizontal', model=self.combo_ax1_model) gui.checkBox(box, self, 'invert_date_order', 'Invert Y axis order', callback=self.replot) box = gui.vBox(self.controlArea, 'Aggregation') self.combo_func = gui.comboBox( box, self, 'agg_func', label='Function:', orientation='horizontal', callback=self.replot) func_model = ListModel(AGG_FUNCTIONS, parent=self) self.combo_func.setModel(func_model) self.attrlist_model = VariableListModel(parent=self) self.attrlist = QListView(selectionMode=QListView.SingleSelection) self.attrlist.setModel(self.attrlist_model) self.attrlist.selectionModel().selectionChanged.connect( self.attrlist_selectionChanged) box.layout().addWidget(self.attrlist) gui.rubber(self.controlArea) self.chart = chart = Spiralogram(self, selection_callback=self.on_selection) self.mainArea.layout().addWidget(chart) def attrlist_selectionChanged(self): self.agg_attr = [self.attrlist_model[i.row()] for i in self.attrlist.selectionModel().selectedIndexes()] self.replot() @Inputs.time_series def set_data(self, data): self.data = data = None if data is None else Timeseries.from_data_table(data) def init_combos(): for model in (self.combo_ax1_model, self.combo_ax2_model): model.clear() newmodel = [] if data is not None and data.time_variable is not None: for model in (self.combo_ax1_model, self.combo_ax2_model): model[:] = [_enum_str(i) for i in Spiralogram.AxesCategories] for var in data.domain.variables if data is not None else []: if (var.is_primitive() and (var is not data.time_variable or isinstance(var, TimeVariable) and data.time_delta is None)): newmodel.append(var) if var.is_discrete: for model in (self.combo_ax1_model, self.combo_ax2_model): model.append(var) self.attrlist_model.wrap(newmodel) init_combos() self.chart.clear() if data is None: self.commit() return self.closeContext() self.ax2 = next((self.combo_ax2.itemText(i) for i in range(self.combo_ax2.count())), '') self.ax1 = next((self.combo_ax1.itemText(i) for i in range(1, self.combo_ax1.count())), self.ax2) self.agg_attr = [data.domain.variables[0]] if len(data.domain.variables) else [] self.agg_func = 0 if getattr(data, 'time_variable', None) is not None: self.openContext(data.domain) if self.agg_attr: self.attrlist.blockSignals(True) self.attrlist.selectionModel().clear() for attr in self.agg_attr: try: row = self.attrlist_model.indexOf(attr) except ValueError: continue self.attrlist.selectionModel().select( self.attrlist_model.index(row), QItemSelectionModel.SelectCurrent) self.attrlist.blockSignals(False) self.replot() def replot(self): if not self.combo_ax1.count() or not self.agg_attr: return self.chart.clear() vars = self.agg_attr func = AGG_FUNCTIONS[self.agg_func] if any(var.is_discrete for var in vars) and func != Mode: self.combo_func.setCurrentIndex(AGG_FUNCTIONS.index(Mode)) func = Mode try: ax1 = Spiralogram.AxesCategories[_enum_str(self.ax1, True)] except KeyError: ax1 = self.data.domain[self.ax1] # TODO: Allow having only a sinle (i.e. radial) axis try: ax2 = Spiralogram.AxesCategories[_enum_str(self.ax2, True)] except KeyError: ax2 = self.data.domain[self.ax2] self.chart.setSeries(self.data, vars, ax1, ax2, func) def on_selection(self, indices): self.indices = self.chart.selection_indices(indices) self.commit() def commit(self): self.Outputs.time_series.send(self.data[self.indices] if self.data else None)
class OWLinearProjection(widget.OWWidget): name = "Linear Projection" description = "A multi-axis projection of data onto " \ "a two-dimensional plane." icon = "icons/LinearProjection.svg" priority = 240 selection_indices = settings.Setting(None, schema_only=True) class Inputs: data = Input("Data", Table, default=True) data_subset = Input("Data Subset", Table) projection = Input("Projection", Table) class Outputs: selected_data = Output("Selected Data", Table, default=True) annotated_data = Output(ANNOTATED_DATA_SIGNAL_NAME, Table) components = Output("Components", Table) Placement = Enum("Placement", dict(Circular=0, LDA=1, PCA=2, Projection=3), type=int, qualname="OWLinearProjection.Placement") Component_name = {Placement.Circular: "C", Placement.LDA: "LD", Placement.PCA: "PC"} Variable_name = {Placement.Circular: "circular", Placement.LDA: "lda", Placement.PCA: "pca", Placement.Projection: "projection"} jitter_sizes = [0, 0.1, 0.5, 1.0, 2.0] settings_version = 3 settingsHandler = settings.DomainContextHandler() variable_state = settings.ContextSetting({}) placement = settings.Setting(Placement.Circular) radius = settings.Setting(0) auto_commit = settings.Setting(True) resolution = 256 graph = settings.SettingProvider(OWLinProjGraph) ReplotRequest = QEvent.registerEventType() vizrank = settings.SettingProvider(LinearProjectionVizRank) graph_name = "graph.plot_widget.plotItem" class Warning(widget.OWWidget.Warning): no_cont_features = widget.Msg("Plotting requires numeric features") not_enough_components = widget.Msg("Input projection has less than 2 components") trivial_components = widget.Msg( "All components of the PCA are trivial (explain 0 variance). " "Input data is constant (or near constant).") class Error(widget.OWWidget.Error): proj_and_domain_match = widget.Msg("Projection and Data domains do not match") no_valid_data = widget.Msg("No projection due to invalid data") def __init__(self): super().__init__() self.data = None self.projection = None self.subset_data = None self._subset_mask = None self._selection = None self.__replot_requested = False self.n_cont_var = 0 #: Remember the saved state to restore self.__pending_selection_restore = self.selection_indices self.selection_indices = None self.variable_x = None self.variable_y = None box = gui.vBox(self.mainArea, True, margin=0) self.graph = OWLinProjGraph(self, box, "Plot", view_box=LinProjInteractiveViewBox) box.layout().addWidget(self.graph.plot_widget) plot = self.graph.plot_widget SIZE_POLICY = (QSizePolicy.Minimum, QSizePolicy.Maximum) self.variables_selection = VariablesSelection() self.model_selected = VariableListModel(enable_dnd=True) self.model_other = VariableListModel(enable_dnd=True) self.variables_selection(self, self.model_selected, self.model_other) self.vizrank, self.btn_vizrank = LinearProjectionVizRank.add_vizrank( self.controlArea, self, "Suggest Features", self._vizrank) self.variables_selection.add_remove.layout().addWidget(self.btn_vizrank) box = gui.widgetBox( self.controlArea, "Placement", sizePolicy=SIZE_POLICY) self.radio_placement = gui.radioButtonsInBox( box, self, "placement", btnLabels=["Circular Placement", "Linear Discriminant Analysis", "Principal Component Analysis", "Use input projection"], callback=self._change_placement ) self.viewbox = plot.getViewBox() self.replot = None g = self.graph.gui box = g.point_properties_box(self.controlArea) self.models = g.points_models g.add_widget(g.JitterSizeSlider, box) box.setSizePolicy(*SIZE_POLICY) box = gui.widgetBox(self.controlArea, "Hide axes", sizePolicy=SIZE_POLICY) self.rslider = gui.hSlider( box, self, "radius", minValue=0, maxValue=100, step=5, label="Radius", createLabel=False, ticks=True, callback=self.update_radius) self.rslider.setTickInterval(0) self.rslider.setPageStep(10) box = gui.vBox(self.controlArea, "Plot Properties") box.setSizePolicy(*SIZE_POLICY) g.add_widgets([g.ShowLegend, g.ToolTipShowsAll, g.ClassDensity, g.LabelOnlySelected], box) box = self.graph.box_zoom_select(self.controlArea) box.setSizePolicy(*SIZE_POLICY) self.icons = gui.attributeIconDict p = self.graph.plot_widget.palette() self.graph.set_palette(p) gui.auto_commit(self.controlArea, self, "auto_commit", "Send Selection", auto_label="Send Automatically") self.graph.zoom_actions(self) self._new_plotdata() self._change_placement() self.graph.jitter_continuous = True def reset_graph_data(self): if self.data is not None: self.graph.rescale_data() self._update_graph(reset_view=True) def keyPressEvent(self, event): super().keyPressEvent(event) self.graph.update_tooltip(event.modifiers()) def keyReleaseEvent(self, event): super().keyReleaseEvent(event) self.graph.update_tooltip(event.modifiers()) def _vizrank(self, attrs): self.variables_selection.display_none() self.model_selected[:] = attrs[:] self.model_other[:] = [var for var in self.model_other if var not in attrs] def _change_placement(self): placement = self.placement p_Circular = self.Placement.Circular p_LDA = self.Placement.LDA self.variables_selection.set_enabled(placement in [p_Circular, p_LDA]) self._vizrank_color_change() self.rslider.setEnabled(placement != p_Circular) self._setup_plot() self.commit() def _get_min_radius(self): return self.radius * np.max(np.linalg.norm(self.plotdata.axes, axis=1)) / 100 + 1e-5 def update_radius(self): # Update the anchor/axes visibility pd = self.plotdata assert pd is not None if pd.hidecircle is None: return min_radius = self._get_min_radius() for anchor, item in zip(pd.axes, pd.axisitems): item.setVisible(np.linalg.norm(anchor) > min_radius) pd.hidecircle.setRect(QRectF(-min_radius, -min_radius, 2 * min_radius, 2 * min_radius)) def _new_plotdata(self): self.plotdata = namespace( valid_mask=None, embedding_coords=None, axisitems=[], axes=[], variables=[], data=None, hidecircle=None ) def _anchor_circle(self, variables): # minimum visible anchor radius (radius) min_radius = self._get_min_radius() axisitems = [] for anchor, var in zip(self.plotdata.axes, variables[:]): axitem = AnchorItem(line=QLineF(0, 0, *anchor), text=var.name,) axitem.setVisible(np.linalg.norm(anchor) > min_radius) axitem.setPen(pg.mkPen((100, 100, 100))) axitem.setArrowVisible(True) self.viewbox.addItem(axitem) axisitems.append(axitem) self.plotdata.axisitems = axisitems if self.placement == self.Placement.Circular: return hidecircle = QGraphicsEllipseItem() hidecircle.setRect(QRectF(-min_radius, -min_radius, 2 * min_radius, 2 * min_radius)) _pen = QPen(Qt.lightGray, 1) _pen.setCosmetic(True) hidecircle.setPen(_pen) self.viewbox.addItem(hidecircle) self.plotdata.hidecircle = hidecircle def update_colors(self): self._vizrank_color_change() def clear(self): # Clear/reset the widget state self.data = None self.model_selected.clear() self.model_other.clear() self._clear_plot() self.selection_indices = None def _clear_plot(self): self.Warning.trivial_components.clear() for axisitem in self.plotdata.axisitems: self.viewbox.removeItem(axisitem) if self.plotdata.hidecircle: self.viewbox.removeItem(self.plotdata.hidecircle) self._new_plotdata() self.graph.hide_axes() def invalidate_plot(self): """ Schedule a delayed replot. """ if not self.__replot_requested: self.__replot_requested = True QApplication.postEvent(self, QEvent(self.ReplotRequest), Qt.LowEventPriority - 10) def init_attr_values(self): self.graph.set_domain(self.data) def _vizrank_color_change(self): is_enabled = False if self.data is None: self.btn_vizrank.setToolTip("There is no data.") return vars = [v for v in chain(self.data.domain.variables, self.data.domain.metas) if v.is_primitive and v is not self.graph.attr_color] self.n_cont_var = len(vars) if self.placement not in [self.Placement.Circular, self.Placement.LDA]: msg = "Suggest Features works only for Circular and " \ "Linear Discriminant Analysis Projection" elif self.graph.attr_color is None: msg = "Color variable has to be selected" elif self.graph.attr_color.is_continuous and self.placement == self.Placement.LDA: msg = "Suggest Features does not work for Linear Discriminant Analysis Projection " \ "when continuous color variable is selected." elif len(vars) < 3: msg = "Not enough available continuous variables" else: is_enabled = True msg = "" self.btn_vizrank.setToolTip(msg) self.btn_vizrank.setEnabled(is_enabled) self.vizrank.stop_and_reset(is_enabled) @Inputs.projection def set_projection(self, projection): self.Warning.not_enough_components.clear() if projection and len(projection) < 2: self.Warning.not_enough_components() projection = None if projection is not None: self.placement = self.Placement.Projection self.projection = projection @Inputs.data def set_data(self, data): """ Set the input dataset. Args: data (Orange.data.table): data instances """ def sql(data): if isinstance(data, SqlTable): if data.approx_len() < 4000: data = Table(data) else: self.information("Data has been sampled") data_sample = data.sample_time(1, no_cache=True) data_sample.download_data(2000, partial=True) data = Table(data_sample) return data def settings(data): # get the default encoded state, replacing the position with Inf state = VariablesSelection.encode_var_state( [list(self.model_selected), list(self.model_other)] ) state = {key: (source_ind, np.inf) for key, (source_ind, _) in state.items()} self.openContext(data.domain) selected_keys = [key for key, (sind, _) in self.variable_state.items() if sind == 0] if set(selected_keys).issubset(set(state.keys())): pass if self.__pending_selection_restore is not None: self._selection = np.array(self.__pending_selection_restore, dtype=int) self.__pending_selection_restore = None # update the defaults state (the encoded state must contain # all variables in the input domain) state.update(self.variable_state) # ... and restore it with saved positions taking precedence over # the defaults selected, other = VariablesSelection.decode_var_state( state, [list(self.model_selected), list(self.model_other)]) return selected, other self.closeContext() self.clear() self.Warning.no_cont_features.clear() self.information() data = sql(data) if data is not None: domain = data.domain vars = [var for var in chain(domain.variables, domain.metas) if var.is_continuous] if not len(vars): self.Warning.no_cont_features() data = None self.data = data self.init_attr_values() if data is not None and len(data): self._initialize(data) self.model_selected[:], self.model_other[:] = settings(data) self.vizrank.stop_and_reset() self.vizrank.attrs = self.data.domain.attributes if self.data is not None else [] def _check_possible_opt(self): def set_enabled(is_enabled): for btn in self.radio_placement.buttons: btn.setEnabled(is_enabled) self.variables_selection.set_enabled(is_enabled) p_Circular = self.Placement.Circular p_LDA = self.Placement.LDA p_Input = self.Placement.Projection if self.data: set_enabled(True) domain = self.data.domain if not domain.has_discrete_class or len(domain.class_var.values) < 2: self.radio_placement.buttons[p_LDA].setEnabled(False) if self.placement == p_LDA: self.placement = p_Circular if not self.projection: self.radio_placement.buttons[p_Input].setEnabled(False) if self.placement == p_Input: self.placement = p_Circular self._setup_plot() else: self.graph.new_data(None) self.rslider.setEnabled(False) set_enabled(False) self.commit() @Inputs.data_subset def set_subset_data(self, subset): """ Set the supplementary input subset dataset. Args: subset (Orange.data.table): subset of data instances """ self.subset_data = subset self._subset_mask = None self.controls.graph.alpha_value.setEnabled(subset is None) def handleNewSignals(self): if self.data is not None and self.subset_data is not None: # Update the plot's highlight items dataids = self.data.ids.ravel() subsetids = np.unique(self.subset_data.ids) self._subset_mask = np.in1d(dataids, subsetids, assume_unique=True) self._check_possible_opt() self._change_placement() self.commit() def customEvent(self, event): if event.type() == OWLinearProjection.ReplotRequest: self.__replot_requested = False self._setup_plot() self.commit() else: super().customEvent(event) def closeContext(self): self.variable_state = VariablesSelection.encode_var_state( [list(self.model_selected), list(self.model_other)] ) super().closeContext() def _initialize(self, data): # Initialize the GUI controls from data's domain. vars = [v for v in chain(data.domain.metas, data.domain.attributes) if v.is_continuous] self.model_other[:] = vars[3:] self.model_selected[:] = vars[:3] def prepare_plot_data(self, variables): def projection(variables): if set(self.projection.domain.attributes).issuperset(variables): axes = self.projection[:2, variables].X elif set(f.name for f in self.projection.domain.attributes).issuperset(f.name for f in variables): axes = self.projection[:2, [f.name for f in variables]].X else: self.Error.proj_and_domain_match() axes = None return axes def get_axes(variables): self.Error.proj_and_domain_match.clear() axes = None if self.placement == self.Placement.Circular: axes = LinProj.defaultaxes(len(variables)) elif self.placement == self.Placement.LDA: axes = self._get_lda(self.data, variables) elif self.placement == self.Placement.Projection and self.projection: axes = projection(variables) return axes coords = [column_data(self.data, var, dtype=float) for var in variables] coords = np.vstack(coords) p, N = coords.shape assert N == len(self.data), p == len(variables) axes = get_axes(variables) if axes is None: return None, None, None assert axes.shape == (2, p) valid_mask = ~np.isnan(coords).any(axis=0) coords = coords[:, valid_mask] X, Y = np.dot(axes, coords) if X.size and Y.size: X = normalized(X) Y = normalized(Y) return valid_mask, np.stack((X, Y), axis=1), axes.T def _setup_plot(self): self._clear_plot() if self.data is None: return self.__replot_requested = False names = get_unique_names([v.name for v in chain(self.data.domain.variables, self.data.domain.metas)], ["{}-x".format(self.Variable_name[self.placement]), "{}-y".format(self.Variable_name[self.placement])]) self.variable_x = ContinuousVariable(names[0]) self.variable_y = ContinuousVariable(names[1]) if self.placement in [self.Placement.Circular, self.Placement.LDA]: variables = list(self.model_selected) elif self.placement == self.Placement.Projection: variables = self.model_selected[:] + self.model_other[:] elif self.placement == self.Placement.PCA: variables = [var for var in self.data.domain.attributes if var.is_continuous] if not variables: self.graph.new_data(None) return if self.placement == self.Placement.PCA: valid_mask, ec, axes = self._get_pca() variables = self._pca.orig_domain.attributes else: valid_mask, ec, axes = self.prepare_plot_data(variables) self.plotdata.variables = variables self.plotdata.valid_mask = valid_mask self.plotdata.embedding_coords = ec self.plotdata.axes = axes if any(e is None for e in (valid_mask, ec, axes)): return if not sum(valid_mask): self.Error.no_valid_data() self.graph.new_data(None, None) return self.Error.no_valid_data.clear() self._anchor_circle(variables=variables) self._plot() def _plot(self): domain = self.data.domain new_metas = domain.metas + (self.variable_x, self.variable_y) domain = Domain(attributes=domain.attributes, class_vars=domain.class_vars, metas=new_metas) valid_mask = self.plotdata.valid_mask array = np.zeros((len(self.data), 2), dtype=np.float) array[valid_mask] = self.plotdata.embedding_coords self.plotdata.data = data = self.data.transform(domain) data[:, self.variable_x] = array[:, 0].reshape(-1, 1) data[:, self.variable_y] = array[:, 1].reshape(-1, 1) subset_data = data[self._subset_mask & valid_mask]\ if self._subset_mask is not None and len(self._subset_mask) else None self.plotdata.data = data self.graph.new_data(data[valid_mask], subset_data) if self._selection is not None: self.graph.selection = self._selection[valid_mask] self.graph.update_data(self.variable_x, self.variable_y, False) def _get_lda(self, data, variables): domain = Domain(attributes=variables, class_vars=data.domain.class_vars) data = data.transform(domain) lda = LinearDiscriminantAnalysis(solver='eigen', n_components=2) lda.fit(data.X, data.Y) scalings = lda.scalings_[:, :2].T if scalings.shape == (1, 1): scalings = np.array([[1.], [0.]]) return scalings def _get_pca(self): data = self.data MAX_COMPONENTS = 2 ncomponents = 2 DECOMPOSITIONS = [PCA] # TruncatedSVD cls = DECOMPOSITIONS[0] pca_projector = cls(n_components=MAX_COMPONENTS) pca_projector.component = ncomponents pca_projector.preprocessors = cls.preprocessors + [Normalize()] pca = pca_projector(data) variance_ratio = pca.explained_variance_ratio_ cumulative = np.cumsum(variance_ratio) self._pca = pca if not np.isfinite(cumulative[-1]): self.Warning.trivial_components() coords = pca(data).X valid_mask = ~np.isnan(coords).any(axis=1) # scale axes max_radius = np.min([np.abs(np.min(coords, axis=0)), np.max(coords, axis=0)]) axes = pca.components_.T.copy() axes *= max_radius / np.max(np.linalg.norm(axes, axis=1)) return valid_mask, coords, axes def _update_graph(self, reset_view=False): self.graph.zoomStack = [] if self.graph.data is None: return self.graph.update_data(self.variable_x, self.variable_y, reset_view) def update_density(self): self._update_graph(reset_view=False) def selection_changed(self): if self.graph.selection is not None: self._selection = np.zeros(len(self.data), dtype=np.uint8) self._selection[self.plotdata.valid_mask] = self.graph.selection self.selection_indices = self._selection.tolist() else: self._selection = self.selection_indices = None self.commit() def prepare_data(self): pass def commit(self): def prepare_components(): if self.placement in [self.Placement.Circular, self.Placement.LDA]: attrs = [a for a in self.model_selected[:]] axes = self.plotdata.axes elif self.placement == self.Placement.PCA: axes = self._pca.components_.T attrs = [a for a in self._pca.orig_domain.attributes] if self.placement != self.Placement.Projection: domain = Domain([ContinuousVariable(a.name, compute_value=lambda _: None) for a in attrs], metas=[StringVariable(name='component')]) metas = np.array([["{}{}".format(self.Component_name[self.placement], i + 1) for i in range(axes.shape[1])]], dtype=object).T components = Table(domain, axes.T, metas=metas) components.name = 'components' else: components = self.projection return components selected = annotated = components = None if self.data is not None and self.plotdata.data is not None: components = prepare_components() graph = self.graph mask = self.plotdata.valid_mask.astype(int) mask[mask == 1] = graph.selection if graph.selection is not None \ else [False * len(mask)] selection = np.array([], dtype=np.uint8) if mask is None else np.flatnonzero(mask) name = self.data.name data = self.plotdata.data if len(selection): selected = data[selection] selected.name = name + ": selected" selected.attributes = self.data.attributes if graph.selection is not None and np.max(graph.selection) > 1: annotated = create_groups_table(data, mask) else: annotated = create_annotated_table(data, selection) annotated.attributes = self.data.attributes annotated.name = name + ": annotated" self.Outputs.selected_data.send(selected) self.Outputs.annotated_data.send(annotated) self.Outputs.components.send(components) def send_report(self): if self.data is None: return def name(var): return var and var.name def projection_name(): name = ("Circular Placement", "Linear Discriminant Analysis", "Principal Component Analysis", "Input projection") return name[self.placement] caption = report.render_items_vert(( ("Projection", projection_name()), ("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.graph.jitter_size != 0 and "{} %".format(self.graph.jitter_size)))) self.report_plot() if caption: self.report_caption(caption) @classmethod def migrate_settings(cls, settings_, version): if version < 2: settings_["point_width"] = settings_["point_size"] if version < 3: settings_graph = {} settings_graph["jitter_size"] = settings_["jitter_value"] settings_graph["point_width"] = settings_["point_width"] settings_graph["alpha_value"] = settings_["alpha_value"] settings_graph["class_density"] = settings_["class_density"] settings_["graph"] = settings_graph @classmethod def migrate_context(cls, context, version): if version < 2: domain = context.ordered_domain c_domain = [t for t in context.ordered_domain if t[1] == 2] d_domain = [t for t in context.ordered_domain if t[1] == 1] for d, old_val, new_val in ((domain, "color_index", "attr_color"), (d_domain, "shape_index", "attr_shape"), (c_domain, "size_index", "attr_size")): index = context.values[old_val][0] - 1 context.values[new_val] = (d[index][0], d[index][1] + 100) \ if 0 <= index < len(d) else None if version < 3: context.values["graph"] = { "attr_color": context.values["attr_color"], "attr_shape": context.values["attr_shape"], "attr_size": context.values["attr_size"] }
class OWLinearProjection(OWAnchorProjectionWidget): name = "Linear Projection" description = "A multi-axis projection of data onto " \ "a two-dimensional plane." icon = "icons/LinearProjection.svg" priority = 240 keywords = [] Placement = Enum("Placement", dict(Circular=0, LDA=1, PCA=2), type=int, qualname="OWLinearProjection.Placement") Projection_name = {Placement.Circular: "Circular Placement", Placement.LDA: "Linear Discriminant Analysis", Placement.PCA: "Principal Component Analysis"} settings_version = 5 placement = Setting(Placement.Circular) selected_vars = ContextSetting([]) vizrank = SettingProvider(LinearProjectionVizRank) GRAPH_CLASS = OWLinProjGraph graph = SettingProvider(OWLinProjGraph) class Error(OWAnchorProjectionWidget.Error): no_cont_features = Msg("Plotting requires numeric features") def __init__(self): self.model_selected = VariableListModel(enable_dnd=True) self.model_selected.rowsInserted.connect(self.__model_selected_changed) self.model_selected.rowsRemoved.connect(self.__model_selected_changed) self.model_other = VariableListModel(enable_dnd=True) self.vizrank, self.btn_vizrank = LinearProjectionVizRank.add_vizrank( None, self, "Suggest Features", self.__vizrank_set_attrs) super().__init__() def _add_controls(self): self._add_controls_variables() self._add_controls_placement() super()._add_controls() self.graph.gui.add_control( self._effects_box, gui.hSlider, "Hide radius:", master=self.graph, value="hide_radius", minValue=0, maxValue=100, step=10, createLabel=False, callback=self.__radius_slider_changed ) self.controlArea.layout().removeWidget(self.control_area_stretch) self.control_area_stretch.setParent(None) def _add_controls_variables(self): self.variables_selection = VariablesSelection( self, self.model_selected, self.model_other, self.controlArea ) self.variables_selection.add_remove.layout().addWidget( self.btn_vizrank ) def _add_controls_placement(self): box = gui.widgetBox( self.controlArea, True, sizePolicy=(QSizePolicy.Minimum, QSizePolicy.Maximum) ) self.radio_placement = gui.radioButtonsInBox( box, self, "placement", btnLabels=[self.Projection_name[x] for x in self.Placement], callback=self.__placement_radio_changed ) @property def continuous_variables(self): if self.data is None or self.data.domain is None: return [] dom = self.data.domain return [v for v in chain(dom.variables, dom.metas) if v.is_continuous] @property def effective_variables(self): return self.model_selected[:] def __vizrank_set_attrs(self, attrs): if not attrs: return self.model_selected[:] = attrs[:] self.model_other[:] = [var for var in self.continuous_variables if var not in attrs] def __model_selected_changed(self): self.selected_vars = [(var.name, vartype(var)) for var in self.model_selected] self.projection = None self._check_options() self.init_projection() self.setup_plot() self.commit() def __placement_radio_changed(self): self.controls.graph.hide_radius.setEnabled( self.placement != self.Placement.Circular) self.projection = self.projector = None self._init_vizrank() self.init_projection() self.setup_plot() self.commit() def __radius_slider_changed(self): self.graph.update_radius() def colors_changed(self): super().colors_changed() self._init_vizrank() def set_data(self, data): super().set_data(data) if self.data is not None and len(self.selected_vars): d, selected = self.data.domain, [v[0] for v in self.selected_vars] self.model_selected[:] = [d[attr] for attr in selected] self.model_other[:] = [d[attr.name] for attr in self.continuous_variables if attr.name not in selected] elif self.data is not None: self.model_selected[:] = self.continuous_variables[:3] self.model_other[:] = self.continuous_variables[3:] self._check_options() self._init_vizrank() def _check_options(self): buttons = self.radio_placement.buttons for btn in buttons: btn.setEnabled(True) if self.data is not None: has_discrete_class = self.data.domain.has_discrete_class if not has_discrete_class or len(np.unique(self.data.Y)) < 2: buttons[self.Placement.LDA].setEnabled(False) if self.placement == self.Placement.LDA: self.placement = self.Placement.Circular self.controls.graph.hide_radius.setEnabled( self.placement != self.Placement.Circular) def _init_vizrank(self): is_enabled, msg = False, "" if self.data is None: msg = "There is no data." elif self.attr_color is None: msg = "Color variable has to be selected" elif self.attr_color.is_continuous and \ self.placement == self.Placement.LDA: msg = "Suggest Features does not work for Linear " \ "Discriminant Analysis Projection when " \ "continuous color variable is selected." elif len([v for v in self.continuous_variables if v is not self.attr_color]) < 3: msg = "Not enough available continuous variables" elif len(self.data[self.valid_data]) < 2: msg = "Not enough valid data instances" else: is_enabled = not np.isnan(self.data.get_column_view( self.attr_color)[0].astype(float)).all() self.btn_vizrank.setToolTip(msg) self.btn_vizrank.setEnabled(is_enabled) if is_enabled: self.vizrank.initialize() def check_data(self): def error(err): err() self.data = None super().check_data() if self.data is not None: if not len(self.continuous_variables): error(self.Error.no_cont_features) def init_attr_values(self): super().init_attr_values() self.selected_vars = [] def init_projection(self): if not len(self.effective_variables): return if self.placement == self.Placement.Circular: self.projector = CircularPlacement() elif self.placement == self.Placement.LDA: self.projector = LDA(solver="eigen", n_components=2) elif self.placement == self.Placement.PCA: self.projector = PCA(n_components=2) self.projector.component = 2 self.projector.preprocessors = PCA.preprocessors + [Normalize()] super().init_projection() def get_coordinates_data(self): def normalized(a): span = np.max(a, axis=0) - np.min(a, axis=0) span[span == 0] = 1 return (a - np.mean(a, axis=0)) / span embedding = self.get_embedding() if embedding is None: return None, None norm_emb = normalized(embedding[self.valid_data]) return (norm_emb.ravel(), np.zeros(len(norm_emb), dtype=float)) \ if embedding.shape[1] == 1 else norm_emb.T def _get_send_report_caption(self): def projection_name(): return self.Projection_name[self.placement] return report.render_items_vert(( ("Projection", projection_name()), ("Color", self._get_caption_var_name(self.attr_color)), ("Label", self._get_caption_var_name(self.attr_label)), ("Shape", self._get_caption_var_name(self.attr_shape)), ("Size", self._get_caption_var_name(self.attr_size)), ("Jittering", self.graph.jitter_size != 0 and "{} %".format(self.graph.jitter_size)))) def clear(self): if self.model_selected: self.model_selected.clear() if self.model_other: self.model_other.clear() super().clear() @classmethod def migrate_settings(cls, settings_, version): if version < 2: settings_["point_width"] = settings_["point_size"] if version < 3: settings_graph = {} settings_graph["jitter_size"] = settings_["jitter_value"] settings_graph["point_width"] = settings_["point_width"] settings_graph["alpha_value"] = settings_["alpha_value"] settings_graph["class_density"] = settings_["class_density"] settings_["graph"] = settings_graph if version < 4: if "radius" in settings_: settings_["graph"]["hide_radius"] = settings_["radius"] if "selection_indices" in settings_ and \ settings_["selection_indices"] is not None: selection = settings_["selection_indices"] settings_["selection"] = [(i, 1) for i, selected in enumerate(selection) if selected] if version < 5: if "placement" in settings_ and \ settings_["placement"] not in cls.Placement: settings_["placement"] = cls.Placement.Circular @classmethod def migrate_context(cls, context, version): if version < 2: domain = context.ordered_domain c_domain = [t for t in context.ordered_domain if t[1] == 2] d_domain = [t for t in context.ordered_domain if t[1] == 1] for d, old_val, new_val in ((domain, "color_index", "attr_color"), (d_domain, "shape_index", "attr_shape"), (c_domain, "size_index", "attr_size")): index = context.values[old_val][0] - 1 context.values[new_val] = (d[index][0], d[index][1] + 100) \ if 0 <= index < len(d) else None if version < 3: context.values["graph"] = { "attr_color": context.values["attr_color"], "attr_shape": context.values["attr_shape"], "attr_size": context.values["attr_size"] } if version == 3: values = context.values values["attr_color"] = values["graph"]["attr_color"] values["attr_size"] = values["graph"]["attr_size"] values["attr_shape"] = values["graph"]["attr_shape"] values["attr_label"] = values["graph"]["attr_label"]
def _add_controls(self): self._value_var_model = VariableListModel() sorted_model = SortProxyModel(sortRole=Qt.UserRole) sorted_model.setSourceModel(self._value_var_model) sorted_model.sort(0) view = self._value_var_view = ListViewSearch() view.setModel(sorted_model) view.setMinimumSize(QSize(30, 100)) view.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Ignored) view.selectionModel().selectionChanged.connect( self.__value_var_changed) self._group_var_model = VariableListModel(placeholder="None") sorted_model = SortProxyModel(sortRole=Qt.UserRole) sorted_model.setSourceModel(self._group_var_model) sorted_model.sort(0) view = self._group_var_view = ListViewSearch() view.setModel(sorted_model) view.setMinimumSize(QSize(30, 100)) view.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Ignored) view.selectionModel().selectionChanged.connect( self.__group_var_changed) box = gui.vBox(self.controlArea, "Variable") box.layout().addWidget(self._value_var_view) gui.checkBox(box, self, "order_by_importance", "Order by relevance to subgroups", tooltip="Order by 𝜒² or ANOVA over the subgroups", callback=self.apply_value_var_sorting) box = gui.vBox(self.controlArea, "Subgroups") box.layout().addWidget(self._group_var_view) gui.checkBox(box, self, "order_grouping_by_importance", "Order by relevance to variable", tooltip="Order by 𝜒² or ANOVA over the variable values", callback=self.apply_group_var_sorting) box = gui.vBox(self.controlArea, "Display", sizePolicy=(QSizePolicy.Minimum, QSizePolicy.Maximum)) gui.checkBox(box, self, "show_box_plot", "Box plot", callback=self.__show_box_plot_changed) gui.checkBox(box, self, "show_strip_plot", "Strip plot", callback=self.__show_strip_plot_changed) gui.checkBox(box, self, "show_rug_plot", "Rug plot", callback=self.__show_rug_plot_changed) self._order_violins_cb = gui.checkBox( box, self, "order_violins", "Order subgroups", callback=self.__order_violins_changed, ) gui.radioButtons(box, self, "orientation_index", ["Horizontal", "Vertical"], label="Orientation: ", orientation=Qt.Horizontal, callback=self.__orientation_changed) box = gui.vBox(self.controlArea, "Density Estimation", sizePolicy=(QSizePolicy.Minimum, QSizePolicy.Maximum)) gui.comboBox(box, self, "kernel_index", items=self.KERNEL_LABELS, label="Kernel:", labelWidth=60, orientation=Qt.Horizontal, callback=self.__kernel_changed) self._scale_combo = gui.comboBox(box, self, "scale_index", items=self.SCALE_LABELS, label="Scale:", labelWidth=60, orientation=Qt.Horizontal, callback=self.__scale_changed)
def __init__(self): super().__init__() self.stats = [] self.dataset = None self.posthoc_lines = [] self.label_txts = self.mean_labels = self.boxes = self.labels = \ self.label_txts_all = self.attr_labels = self.order = [] self.scale_x = self.scene_min_x = self.scene_width = 0 self.label_width = 0 self.attrs = VariableListModel() view = gui.listView( self.controlArea, self, "attribute", box="Variable", model=self.attrs, callback=self.attr_changed) view.setMinimumSize(QSize(30, 30)) # Any other policy than Ignored will let the QListBox's scrollbar # set the minimal height (see the penultimate paragraph of # http://doc.qt.io/qt-4.8/qabstractscrollarea.html#addScrollBarWidget) view.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Ignored) gui.separator(view.box, 6, 6) self.cb_order = gui.checkBox( view.box, self, "order_by_importance", "Order by relevance", tooltip="Order by 𝜒² or ANOVA over the subgroups", callback=self.apply_sorting) self.group_vars = DomainModel( placeholder="None", separators=False, valid_types=Orange.data.DiscreteVariable) self.group_view = view = gui.listView( self.controlArea, self, "group_var", box="Subgroups", model=self.group_vars, callback=self.grouping_changed) view.setEnabled(False) view.setMinimumSize(QSize(30, 30)) # See the comment above view.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Ignored) # TODO: move Compare median/mean to grouping box # The vertical size policy is needed to let only the list views expand self.display_box = gui.vBox( self.controlArea, "Display", sizePolicy=(QSizePolicy.Minimum, QSizePolicy.Maximum), addSpace=False) gui.checkBox(self.display_box, self, "show_annotations", "Annotate", callback=self.display_changed) self.compare_rb = gui.radioButtonsInBox( self.display_box, self, 'compare', btnLabels=["No comparison", "Compare medians", "Compare means"], callback=self.layout_changed) # The vertical size policy is needed to let only the list views expand self.stretching_box = box = gui.vBox( self.controlArea, box="Display", sizePolicy=(QSizePolicy.Minimum, QSizePolicy.Fixed)) self.stretching_box.sizeHint = self.display_box.sizeHint gui.checkBox( box, self, 'stretched', "Stretch bars", callback=self.display_changed) gui.checkBox( box, self, 'show_labels', "Show box labels", callback=self.display_changed) self.sort_cb = gui.checkBox( box, self, 'sort_freqs', "Sort by subgroup frequencies", callback=self.display_changed) gui.rubber(box) gui.auto_commit(self.controlArea, self, "auto_commit", "Send Selection", "Send Automatically") gui.vBox(self.mainArea, addSpace=True) self.box_scene = QGraphicsScene() self.box_scene.selectionChanged.connect(self.commit) self.box_view = QGraphicsView(self.box_scene) self.box_view.setRenderHints(QPainter.Antialiasing | QPainter.TextAntialiasing | QPainter.SmoothPixmapTransform) self.box_view.viewport().installEventFilter(self) self.mainArea.layout().addWidget(self.box_view) e = gui.hBox(self.mainArea, addSpace=False) self.infot1 = gui.widgetLabel(e, "<center>No test results.</center>") self.mainArea.setMinimumWidth(300) self.stats = self.dist = self.conts = [] self.is_continuous = False self.update_display_box()
class OWSpiralogram(widget.OWWidget): name = 'Spiralogram' description = "Visualize time series' periodicity in a spiral heatmap." icon = 'icons/Spiralogram.svg' priority = 120 inputs = [("Time series", Table, 'set_data')] outputs = [("Time series", Timeseries)] ax1 = settings.Setting('months of year') ax2 = settings.Setting('years') agg_attr = settings.Setting([]) agg_func = settings.Setting(0) def __init__(self): self.data = None self.indices = [] box = gui.vBox(self.controlArea, 'Axes') self.combo_ax2 = gui.comboBox( box, self, 'ax2', label='Y axis:', callback=self.replot, sendSelectedValue=True, orientation='horizontal') self.combo_ax1 = gui.comboBox( box, self, 'ax1', label='Radial:', callback=self.replot, sendSelectedValue=True, orientation='horizontal') box = gui.vBox(self.controlArea, 'Aggregation') self.combo_func = gui.comboBox( box, self, 'agg_func', label='Function:', orientation='horizontal', callback=self.replot) func_model = ListModel(AGG_FUNCTIONS, parent=self) self.combo_func.setModel(func_model) self.attrlist_model = VariableListModel(parent=self) self.attrlist = QListView(selectionMode=QListView.ExtendedSelection) self.attrlist.setModel(self.attrlist_model) self.attrlist.selectionModel().selectionChanged.connect( self.attrlist_selectionChanged) box.layout().addWidget(self.attrlist) gui.rubber(self.controlArea) self.chart = chart = Spiralogram(self, selection_callback=self.on_selection) self.mainArea.layout().addWidget(chart) def attrlist_selectionChanged(self): self.agg_attr = [self.attrlist_model[i.row()] for i in self.attrlist.selectionModel().selectedIndexes()] self.replot() def set_data(self, data): self.data = data = None if data is None else Timeseries.from_data_table(data) def init_combos(): for combo in (self.combo_ax1, self.combo_ax2): combo.clear() self.attrlist_model[:] = [] for i in Spiralogram.AxesCategories: for combo in (self.combo_ax1, self.combo_ax2): combo.addItem(_enum_str(i)) for var in data.domain if data is not None else []: if (var.is_primitive() and (var is not data.time_variable or isinstance(var, TimeVariable) and data.time_delta is None)): self.attrlist_model.append(var) if var.is_discrete: for combo in (self.combo_ax1, self.combo_ax2): combo.addItem(gui.attributeIconDict[var], var.name) init_combos() self.chart.clear() if data is None: self.commit() return self.ax1 = 'months of year' self.ax2 = 'years' self.replot() def replot(self): vars = self.agg_attr func = AGG_FUNCTIONS[self.agg_func] # TODO test discrete if any(var.is_discrete for var in vars) and func != Mode: self.combo_func.setCurrentIndex(AGG_FUNCTIONS.index(Mode)) return try: ax1 = Spiralogram.AxesCategories[_enum_str(self.ax1, True)] except KeyError: ax1 = self.data.domain[self.ax1] # TODO: Allow having only a sinle (i.e. radial) axis try: ax2 = Spiralogram.AxesCategories[_enum_str(self.ax2, True)] except KeyError: ax2 = self.data.domain[self.ax2] self.chart.setSeries(self.data, vars, ax1, ax2, func) def on_selection(self, indices): self.indices = self.chart.selection_indices(indices) self.commit() def commit(self): self.send('Time series', self.data[self.indices] if self.data else None)
class OWRadviz(widget.OWWidget): name = "Radviz" description = "Radviz" icon = "icons/Radviz.svg" priority = 240 class Inputs: data = Input("Data", Table, default=True) data_subset = Input("Data Subset", Table) class Outputs: selected_data = Output("Selected Data", Table, default=True) annotated_data = Output(ANNOTATED_DATA_SIGNAL_NAME, Table) components = Output("Components", Table) settings_version = 1 settingsHandler = settings.DomainContextHandler() variable_state = settings.ContextSetting({}) auto_commit = settings.Setting(True) graph = settings.SettingProvider(OWRadvizGraph) vizrank = settings.SettingProvider(RadvizVizRank) jitter_sizes = [0, 0.1, 0.5, 1.0, 2.0] ReplotRequest = QEvent.registerEventType() graph_name = "graph.plot_widget.plotItem" class Information(widget.OWWidget.Information): sql_sampled_data = widget.Msg("Data has been sampled") class Warning(widget.OWWidget.Warning): no_features = widget.Msg("At least 2 features have to be chosen") class Error(widget.OWWidget.Error): sparse_data = widget.Msg("Sparse data is not supported") no_features = widget.Msg("At least 3 numeric or categorical variables are required") no_instances = widget.Msg("At least 2 data instances are required") def __init__(self): super().__init__() self.data = None self.subset_data = None self._subset_mask = None self._selection = None # np.array self.__replot_requested = False self._new_plotdata() self.variable_x = ContinuousVariable("radviz-x") self.variable_y = ContinuousVariable("radviz-y") box = gui.vBox(self.mainArea, True, margin=0) self.graph = OWRadvizGraph(self, box, "Plot", view_box=RadvizInteractiveViewBox) self.graph.hide_axes() box.layout().addWidget(self.graph.plot_widget) plot = self.graph.plot_widget SIZE_POLICY = (QSizePolicy.Minimum, QSizePolicy.Maximum) self.variables_selection = VariablesSelection() self.model_selected = VariableListModel(enable_dnd=True) self.model_other = VariableListModel(enable_dnd=True) self.variables_selection(self, self.model_selected, self.model_other) self.vizrank, self.btn_vizrank = RadvizVizRank.add_vizrank( self.controlArea, self, "Suggest features", self.vizrank_set_attrs) self.btn_vizrank.setSizePolicy(*SIZE_POLICY) self.variables_selection.add_remove.layout().addWidget(self.btn_vizrank) self.viewbox = plot.getViewBox() self.replot = None g = self.graph.gui pp_box = g.point_properties_box(self.controlArea) pp_box.setSizePolicy(*SIZE_POLICY) self.models = g.points_models box = gui.vBox(self.controlArea, "Plot Properties") box.setSizePolicy(*SIZE_POLICY) g.add_widget(g.JitterSizeSlider, box) g.add_widgets([g.ShowLegend, g.ClassDensity, g.LabelOnlySelected], box) zoom_select = self.graph.box_zoom_select(self.controlArea) zoom_select.setSizePolicy(*SIZE_POLICY) self.icons = gui.attributeIconDict p = self.graph.plot_widget.palette() self.graph.set_palette(p) gui.auto_commit(self.controlArea, self, "auto_commit", "Send Selection", auto_label="Send Automatically") self.graph.zoom_actions(self) self._circle = QGraphicsEllipseItem() self._circle.setRect(QRectF(-1., -1., 2., 2.)) self._circle.setPen(pg.mkPen(QColor(0, 0, 0), width=2)) def resizeEvent(self, event): self._update_points_labels() def keyPressEvent(self, event): super().keyPressEvent(event) self.graph.update_tooltip(event.modifiers()) def keyReleaseEvent(self, event): super().keyReleaseEvent(event) self.graph.update_tooltip(event.modifiers()) def vizrank_set_attrs(self, attrs): if not attrs: return self.variables_selection.display_none() self.model_selected[:] = attrs[:] self.model_other[:] = [v for v in self.model_other if v not in attrs] def _new_plotdata(self): self.plotdata = namespace( valid_mask=None, embedding_coords=None, points=None, arcarrows=[], point_labels=[], rand=None, data=None, ) def update_colors(self): self._vizrank_color_change() self.cb_class_density.setEnabled(self.graph.can_draw_density()) def sizeHint(self): return QSize(800, 500) def clear(self): """ Clear/reset the widget state """ self.data = None self.model_selected.clear() self.model_other.clear() self._clear_plot() def _clear_plot(self): self._new_plotdata() self.graph.plot_widget.clear() def invalidate_plot(self): """ Schedule a delayed replot. """ if not self.__replot_requested: self.__replot_requested = True QApplication.postEvent(self, QEvent(self.ReplotRequest), Qt.LowEventPriority - 10) def init_attr_values(self): self.graph.set_domain(self.data) def _vizrank_color_change(self): attr_color = self.graph.attr_color is_enabled = self.data is not None and not self.data.is_sparse() and \ (len(self.model_other) + len(self.model_selected)) > 3 and len(self.data) > 1 self.btn_vizrank.setEnabled( is_enabled and attr_color is not None and not np.isnan(self.data.get_column_view(attr_color)[0].astype(float)).all()) self.vizrank.initialize() @Inputs.data def set_data(self, data): """ Set the input dataset and check if data is valid. Args: data (Orange.data.table): data instances """ def sql(data): self.Information.sql_sampled_data.clear() if isinstance(data, SqlTable): if data.approx_len() < 4000: data = Table(data) else: self.Information.sql_sampled_data() data_sample = data.sample_time(1, no_cache=True) data_sample.download_data(2000, partial=True) data = Table(data_sample) return data def settings(data): # get the default encoded state, replacing the position with Inf state = VariablesSelection.encode_var_state( [list(self.model_selected), list(self.model_other)] ) state = {key: (source_ind, np.inf) for key, (source_ind, _) in state.items()} self.openContext(data.domain) selected_keys = [key for key, (sind, _) in self.variable_state.items() if sind == 0] if set(selected_keys).issubset(set(state.keys())): pass # update the defaults state (the encoded state must contain # all variables in the input domain) state.update(self.variable_state) # ... and restore it with saved positions taking precedence over # the defaults selected, other = VariablesSelection.decode_var_state( state, [list(self.model_selected), list(self.model_other)]) return selected, other def is_sparse(data): if data.is_sparse(): self.Error.sparse_data() data = None return data def are_features(data): domain = data.domain vars = [var for var in chain(domain.class_vars, domain.metas, domain.attributes) if var.is_primitive()] if len(vars) < 3: self.Error.no_features() data = None return data def are_instances(data): if len(data) < 2: self.Error.no_instances() data = None return data self.clear_messages() self.btn_vizrank.setEnabled(False) self.closeContext() self.clear() self.information() self.Error.clear() for f in [sql, is_sparse, are_features, are_instances]: if data is None: break data = f(data) if data is not None: self.data = data self.init_attr_values() domain = data.domain vars = [v for v in chain(domain.metas, domain.attributes) if v.is_primitive()] self.model_selected[:] = vars[:5] self.model_other[:] = vars[5:] + list(domain.class_vars) self.model_selected[:], self.model_other[:] = settings(data) self._selection = np.zeros(len(data), dtype=np.uint8) self.invalidate_plot() else: self.data = None @Inputs.data_subset def set_subset_data(self, subset): """ Set the supplementary input subset dataset. Args: subset (Orange.data.table): subset of data instances """ self.subset_data = subset self._subset_mask = None self.controls.graph.alpha_value.setEnabled(subset is None) def handleNewSignals(self): if self.data is not None: self._clear_plot() if self.subset_data is not None and self._subset_mask is None: dataids = self.data.ids.ravel() subsetids = np.unique(self.subset_data.ids) self._subset_mask = np.in1d( dataids, subsetids, assume_unique=True) self.setup_plot(reset_view=True) self.cb_class_density.setEnabled(self.graph.can_draw_density()) else: self.init_attr_values() self.graph.new_data(None) self._vizrank_color_change() self.commit() def customEvent(self, event): if event.type() == OWRadviz.ReplotRequest: self.__replot_requested = False self._clear_plot() self.setup_plot(reset_view=True) else: super().customEvent(event) def closeContext(self): self.variable_state = VariablesSelection.encode_var_state( [list(self.model_selected), list(self.model_other)] ) super().closeContext() def prepare_radviz_data(self, variables): ec, points, valid_mask = radviz(self.data, variables, self.plotdata.points) self.plotdata.embedding_coords = ec self.plotdata.points = points self.plotdata.valid_mask = valid_mask def setup_plot(self, reset_view=True): if self.data is None: return self.graph.jitter_continuous = True self.__replot_requested = False variables = list(self.model_selected) if len(variables) < 2: self.Warning.no_features() self.graph.new_data(None) return self.Warning.clear() self.prepare_radviz_data(variables) if self.plotdata.embedding_coords is None: return domain = self.data.domain new_metas = domain.metas + (self.variable_x, self.variable_y) domain = Domain(attributes=domain.attributes, class_vars=domain.class_vars, metas=new_metas) mask = self.plotdata.valid_mask array = np.zeros((len(self.data), 2), dtype=np.float) array[mask] = self.plotdata.embedding_coords data = self.data.transform(domain) data[:, self.variable_x] = array[:, 0].reshape(-1, 1) data[:, self.variable_y] = array[:, 1].reshape(-1, 1) subset_data = data[self._subset_mask & mask]\ if self._subset_mask is not None and len(self._subset_mask) else None self.plotdata.data = data self.graph.new_data(data[mask], subset_data) if self._selection is not None: self.graph.selection = self._selection[self.plotdata.valid_mask] self.graph.update_data(self.variable_x, self.variable_y, reset_view=reset_view) self.graph.plot_widget.addItem(self._circle) self.graph.scatterplot_points = ScatterPlotItem( x=self.plotdata.points[:, 0], y=self.plotdata.points[:, 1] ) self._update_points_labels() self.graph.plot_widget.addItem(self.graph.scatterplot_points) def randomize_indices(self): ec = self.plotdata.embedding_coords self.plotdata.rand = np.random.choice(len(ec), MAX_POINTS, replace=False) \ if len(ec) > MAX_POINTS else None def manual_move(self): self.__replot_requested = False if self.plotdata.rand is not None: rand = self.plotdata.rand valid_mask = self.plotdata.valid_mask data = self.data[valid_mask] selection = self._selection[valid_mask] selection = selection[rand] ec, _, valid_mask = radviz(data, list(self.model_selected), self.plotdata.points) assert sum(valid_mask) == len(data) data = data[rand] ec = ec[rand] data_x = data.X data_y = data.Y data_metas = data.metas else: self.prepare_radviz_data(list(self.model_selected)) ec = self.plotdata.embedding_coords valid_mask = self.plotdata.valid_mask data_x = self.data.X[valid_mask] data_y = self.data.Y[valid_mask] data_metas = self.data.metas[valid_mask] selection = self._selection[valid_mask] attributes = (self.variable_x, self.variable_y) + self.data.domain.attributes domain = Domain(attributes=attributes, class_vars=self.data.domain.class_vars, metas=self.data.domain.metas) data = Table.from_numpy(domain, X=np.hstack((ec, data_x)), Y=data_y, metas=data_metas) self.graph.new_data(data, None) self.graph.selection = selection self.graph.update_data(self.variable_x, self.variable_y, reset_view=True) self.graph.plot_widget.addItem(self._circle) self.graph.scatterplot_points = ScatterPlotItem( x=self.plotdata.points[:, 0], y=self.plotdata.points[:, 1]) self._update_points_labels() self.graph.plot_widget.addItem(self.graph.scatterplot_points) def _update_points_labels(self): if self.plotdata.points is None: return for point_label in self.plotdata.point_labels: self.graph.plot_widget.removeItem(point_label) self.plotdata.point_labels = [] sx, sy = self.graph.view_box.viewPixelSize() for row in self.plotdata.points: ti = TextItem() metrics = QFontMetrics(ti.textItem.font()) text_width = ((RANGE.width())/2. - np.abs(row[0])) / sx name = row[2].name ti.setText(name) ti.setTextWidth(text_width) ti.setColor(QColor(0, 0, 0)) br = ti.boundingRect() width = metrics.width(name) if metrics.width(name) < br.width() else br.width() width = sx * (width + 5) height = sy * br.height() ti.setPos(row[0] - (row[0] < 0) * width, row[1] + (row[1] > 0) * height) self.plotdata.point_labels.append(ti) self.graph.plot_widget.addItem(ti) def _update_jitter(self): self.invalidate_plot() def reset_graph_data(self, *_): if self.data is not None: self.graph.rescale_data() self._update_graph() def _update_graph(self, reset_view=True, **_): self.graph.zoomStack = [] if self.graph.data is None: return self.graph.update_data(self.variable_x, self.variable_y, reset_view=reset_view) def update_density(self): self._update_graph(reset_view=True) def selection_changed(self): if self.graph.selection is not None: self._selection[self.plotdata.valid_mask] = self.graph.selection self.commit() def prepare_data(self): pass def commit(self): selected = annotated = components = None graph = self.graph if self.plotdata.data is not None: name = self.data.name data = self.plotdata.data mask = self.plotdata.valid_mask.astype(int) mask[mask == 1] = graph.selection if graph.selection is not None \ else [False * len(mask)] selection = np.array([], dtype=np.uint8) if mask is None else np.flatnonzero(mask) if len(selection): selected = data[selection] selected.name = name + ": selected" selected.attributes = self.data.attributes if graph.selection is not None and np.max(graph.selection) > 1: annotated = create_groups_table(data, mask) else: annotated = create_annotated_table(data, selection) annotated.attributes = self.data.attributes annotated.name = name + ": annotated" comp_domain = Domain( self.plotdata.points[:, 2], metas=[StringVariable(name='component')]) metas = np.array([["RX"], ["RY"], ["angle"]]) angle = np.arctan2(np.array(self.plotdata.points[:, 1].T, dtype=float), np.array(self.plotdata.points[:, 0].T, dtype=float)) components = Table.from_numpy( comp_domain, X=np.row_stack((self.plotdata.points[:, :2].T, angle)), metas=metas) components.name = name + ": components" self.Outputs.selected_data.send(selected) self.Outputs.annotated_data.send(annotated) self.Outputs.components.send(components) def send_report(self): if self.data is None: return def name(var): return var and var.name caption = report.render_items_vert(( ("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.graph.jitter_size != 0 and "{} %".format(self.graph.jitter_size)))) self.report_plot() if caption: self.report_caption(caption)
def __init__(self): super().__init__() self.data = None self.subset_data = None self._subset_mask = None self._selection = None # np.array self.__replot_requested = False self._new_plotdata() self.variable_x = ContinuousVariable("radviz-x") self.variable_y = ContinuousVariable("radviz-y") box = gui.vBox(self.mainArea, True, margin=0) self.graph = OWRadvizGraph(self, box, "Plot", view_box=RadvizInteractiveViewBox) self.graph.hide_axes() box.layout().addWidget(self.graph.plot_widget) plot = self.graph.plot_widget SIZE_POLICY = (QSizePolicy.Minimum, QSizePolicy.Maximum) self.variables_selection = VariablesSelection() self.model_selected = VariableListModel(enable_dnd=True) self.model_other = VariableListModel(enable_dnd=True) self.variables_selection(self, self.model_selected, self.model_other) self.vizrank, self.btn_vizrank = RadvizVizRank.add_vizrank( self.controlArea, self, "Suggest features", self.vizrank_set_attrs) self.btn_vizrank.setSizePolicy(*SIZE_POLICY) self.variables_selection.add_remove.layout().addWidget(self.btn_vizrank) self.viewbox = plot.getViewBox() self.replot = None g = self.graph.gui pp_box = g.point_properties_box(self.controlArea) pp_box.setSizePolicy(*SIZE_POLICY) self.models = g.points_models box = gui.vBox(self.controlArea, "Plot Properties") box.setSizePolicy(*SIZE_POLICY) g.add_widget(g.JitterSizeSlider, box) g.add_widgets([g.ShowLegend, g.ClassDensity, g.LabelOnlySelected], box) zoom_select = self.graph.box_zoom_select(self.controlArea) zoom_select.setSizePolicy(*SIZE_POLICY) self.icons = gui.attributeIconDict p = self.graph.plot_widget.palette() self.graph.set_palette(p) gui.auto_commit(self.controlArea, self, "auto_commit", "Send Selection", auto_label="Send Automatically") self.graph.zoom_actions(self) self._circle = QGraphicsEllipseItem() self._circle.setRect(QRectF(-1., -1., 2., 2.)) self._circle.setPen(pg.mkPen(QColor(0, 0, 0), width=2))
class OWRadviz(OWAnchorProjectionWidget): name = "Radviz" description = "Display Radviz projection" icon = "icons/Radviz.svg" priority = 241 keywords = ["viz"] settings_version = 2 selected_vars = ContextSetting([]) vizrank = SettingProvider(RadvizVizRank) GRAPH_CLASS = OWRadvizGraph graph = SettingProvider(OWRadvizGraph) class Warning(OWAnchorProjectionWidget.Warning): no_features = widget.Msg("Radviz requires at least two features.") invalid_embedding = widget.Msg("No projection for selected features") removed_vars = widget.Msg("Categorical variables with more than" " two values are not shown.") class Error(OWAnchorProjectionWidget.Error): no_features = widget.Msg( "At least three numeric or categorical variables are required" ) def __init__(self): self.model_selected = VariableListModel(enable_dnd=True) self.model_selected.rowsInserted.connect(self.__model_selected_changed) self.model_selected.rowsRemoved.connect(self.__model_selected_changed) self.model_other = VariableListModel(enable_dnd=True) self.vizrank, self.btn_vizrank = RadvizVizRank.add_vizrank( None, self, "Suggest features", self.__vizrank_set_attrs ) super().__init__() def _add_controls(self): self.variables_selection = VariablesSelection( self, self.model_selected, self.model_other, self.controlArea ) self.variables_selection.add_remove.layout().addWidget( self.btn_vizrank ) super()._add_controls() self.controlArea.layout().removeWidget(self.control_area_stretch) self.control_area_stretch.setParent(None) @property def primitive_variables(self): if self.data is None or self.data.domain is None: return [] dom = self.data.domain return [v for v in chain(dom.variables, dom.metas) if v.is_continuous or v.is_discrete and len(v.values) == 2] @property def effective_variables(self): return self.model_selected[:] def __vizrank_set_attrs(self, attrs): if not attrs: return self.model_selected[:] = attrs[:] self.model_other[:] = [var for var in self.primitive_variables if var not in attrs] def __model_selected_changed(self): self.selected_vars = [(var.name, vartype(var)) for var in self.model_selected] self.Warning.no_features.clear() if len(self.model_selected) < 2: self.Warning.no_features() return self.init_projection() self.setup_plot() self.commit() def colors_changed(self): super().colors_changed() self._init_vizrank() def set_data(self, data): super().set_data(data) if self.data is not None and len(self.selected_vars): d, selected = self.data.domain, [v[0] for v in self.selected_vars] self.model_selected[:] = [d[name] for name in selected] self.model_other[:] = [d[attr.name] for attr in self.primitive_variables if attr.name not in selected] elif self.data is not None: d, variables = self.data.domain, self.primitive_variables class_var = [variables.pop(variables.index(d.class_var))] \ if d.class_var in variables else [] self.model_selected[:] = variables[:5] self.model_other[:] = variables[5:] + class_var self._init_vizrank() def _init_vizrank(self): is_enabled = self.data is not None and \ len(self.primitive_variables) > 3 and \ self.attr_color is not None and \ not np.isnan(self.data.get_column_view( self.attr_color)[0].astype(float)).all() and \ len(self.data[self.valid_data]) > 1 and \ np.all(np.nan_to_num(np.nanstd(self.data.X, 0)) != 0) self.btn_vizrank.setEnabled(is_enabled) if is_enabled: self.vizrank.initialize() def check_data(self): def error(err): err() self.data = None super().check_data() if self.data is not None: if len(self.primitive_variables) < 3: error(self.Error.no_features) else: domain = self.data.domain vars_ = chain(domain.variables, domain.metas) n_vars = sum(v.is_primitive() for v in vars_) if len(self.primitive_variables) < n_vars: self.Warning.removed_vars() def init_attr_values(self): super().init_attr_values() self.selected_vars = [] def _manual_move(self, anchor_idx, x, y): angle = np.arctan2(y, x) super()._manual_move(anchor_idx, np.cos(angle), np.sin(angle)) def _send_components_x(self): components_ = super()._send_components_x() angle = np.arctan2(*components_[::-1]) return np.row_stack((components_, angle)) def _send_components_metas(self): return np.vstack((super()._send_components_metas(), ["angle"])) def clear(self): if self.model_selected: self.model_selected.clear() if self.model_other: self.model_other.clear() super().clear() self.projector = RadViz() @classmethod def migrate_context(cls, context, version): if version < 2: values = context.values values["attr_color"] = values["graph"]["attr_color"] values["attr_size"] = values["graph"]["attr_size"] values["attr_shape"] = values["graph"]["attr_shape"] values["attr_label"] = values["graph"]["attr_label"]