1from xml.sax.saxutils import escape 2 3import numpy as np 4 5from AnyQt.QtCore import QSize, Signal, Qt 6from AnyQt.QtWidgets import QApplication 7 8from orangewidget.utils.visual_settings_dlg import VisualSettingsDialog 9 10from Orange.data import ( 11 Table, ContinuousVariable, Domain, Variable, StringVariable 12) 13from Orange.data.util import get_unique_names, array_equal 14from Orange.data.sql.table import SqlTable 15from Orange.statistics.util import bincount 16 17from Orange.widgets import gui, report 18from Orange.widgets.settings import ( 19 Setting, ContextSetting, DomainContextHandler, SettingProvider 20) 21from Orange.widgets.utils import colorpalettes 22from Orange.widgets.utils.annotated_data import ( 23 create_annotated_table, ANNOTATED_DATA_SIGNAL_NAME, create_groups_table 24) 25from Orange.widgets.utils.plot import OWPlotGUI 26from Orange.widgets.utils.sql import check_sql_input 27from Orange.widgets.visualize.owscatterplotgraph import ( 28 OWScatterPlotBase, MAX_COLORS 29) 30from Orange.widgets.visualize.utils.component import OWGraphWithAnchors 31from Orange.widgets.widget import OWWidget, Input, Output, Msg 32 33 34# maximum number of shapes (including Other) 35MAX_SHAPES = len(OWScatterPlotBase.CurveSymbols) - 1 36 37MAX_POINTS_IN_TOOLTIP = 5 38 39 40class OWProjectionWidgetBase(OWWidget, openclass=True): 41 """ 42 Base widget for widgets that use attribute data to set the colors, labels, 43 shapes and sizes of points. 44 45 The widgets defines settings `attr_color`, `attr_label`, `attr_shape` 46 and `attr_size`, but leaves defining the gui to the derived widgets. 47 These are expected to have controls that manipulate these settings, 48 and the controls are expected to use attribute models. 49 50 The widgets also defines attributes `data` and `valid_data` and expects 51 the derived widgets to use them to store an instances of `data.Table` 52 and a bool `np.ndarray` with indicators of valid (that is, shown) 53 data points. 54 """ 55 attr_color = ContextSetting(None, required=ContextSetting.OPTIONAL) 56 attr_label = ContextSetting(None, required=ContextSetting.OPTIONAL) 57 attr_shape = ContextSetting(None, required=ContextSetting.OPTIONAL) 58 attr_size = ContextSetting(None, required=ContextSetting.OPTIONAL) 59 60 class Information(OWWidget.Information): 61 missing_size = Msg( 62 "Points with undefined '{}' are shown in smaller size") 63 missing_shape = Msg( 64 "Points with undefined '{}' are shown as crossed circles") 65 66 def __init__(self): 67 super().__init__() 68 self.data = None 69 self.valid_data = None 70 71 def init_attr_values(self): 72 """ 73 Set the models for `attr_color`, `attr_shape`, `attr_size` and 74 `attr_label`. All values are set to `None`, except `attr_color` 75 which is set to the class variable if it exists. 76 """ 77 data = self.data 78 domain = data.domain if data and len(data) else None 79 for attr in ("attr_color", "attr_shape", "attr_size", "attr_label"): 80 getattr(self.controls, attr).model().set_domain(domain) 81 setattr(self, attr, None) 82 if domain is not None: 83 self.attr_color = domain.class_var 84 85 def get_coordinates_data(self): 86 """A get coordinated method that returns no coordinates. 87 88 Derived classes must override this method. 89 """ 90 return None, None 91 92 def get_subset_mask(self): 93 """ 94 Return the bool array indicating the points in the subset 95 96 The base method does nothing and would usually be overridden by 97 a method that returns indicators from the subset signal. 98 99 Do not confuse the subset with selection. 100 101 Returns: 102 (np.ndarray or `None`): a bool array of indicators 103 """ 104 return None 105 106 def get_column(self, attr, filter_valid=True, 107 max_categories=None, return_labels=False): 108 """ 109 Retrieve the data from the given column in the data table 110 111 The method: 112 - densifies sparse data, 113 - converts arrays with dtype object to floats if the attribute is 114 actually primitive, 115 - filters out invalid data (if `filter_valid` is `True`), 116 - merges infrequent (discrete) values into a single value 117 (if `max_categories` is set). 118 119 Tha latter feature is used for shapes and labels, where only a 120 specified number of different values is shown, and others are 121 merged into category 'Other'. In this case, the method may return 122 either the data (e.g. color indices, shape indices) or the list 123 of retained values, followed by `['Other']`. 124 125 Args: 126 attr (:obj:~Orange.data.Variable): the column to extract 127 filter_valid (bool): filter out invalid data (default: `True`) 128 max_categories (int): merge infrequent values (default: `None`); 129 ignored for non-discrete attributes 130 return_labels (bool): return a list of labels instead of data 131 (default: `False`) 132 133 Returns: 134 (np.ndarray): (valid) data from the column, or a list of labels 135 """ 136 if attr is None: 137 return None 138 139 needs_merging = attr.is_discrete \ 140 and max_categories is not None \ 141 and len(attr.values) >= max_categories 142 if return_labels and not needs_merging: 143 assert attr.is_discrete 144 return attr.values 145 146 all_data = self.data.get_column_view(attr)[0] 147 if all_data.dtype == object and attr.is_primitive(): 148 all_data = all_data.astype(float) 149 if filter_valid and self.valid_data is not None: 150 all_data = all_data[self.valid_data] 151 if not needs_merging: 152 return all_data 153 154 dist = bincount(all_data, max_val=len(attr.values) - 1)[0] 155 infrequent = np.zeros(len(attr.values), dtype=bool) 156 infrequent[np.argsort(dist)[:-(max_categories-1)]] = True 157 if return_labels: 158 return [value for value, infreq in zip(attr.values, infrequent) 159 if not infreq] + ["Other"] 160 else: 161 result = all_data.copy() 162 freq_vals = [i for i, f in enumerate(infrequent) if not f] 163 for i, infreq in enumerate(infrequent): 164 if infreq: 165 result[all_data == i] = max_categories - 1 166 else: 167 result[all_data == i] = freq_vals.index(i) 168 return result 169 170 # Sizes 171 def get_size_data(self): 172 """Return the column corresponding to `attr_size`""" 173 return self.get_column(self.attr_size) 174 175 def impute_sizes(self, size_data): 176 """ 177 Default imputation for size data 178 179 Let the graph handle it, but add a warning if needed. 180 181 Args: 182 size_data (np.ndarray): scaled points sizes 183 """ 184 if self.graph.default_impute_sizes(size_data): 185 self.Information.missing_size(self.attr_size) 186 else: 187 self.Information.missing_size.clear() 188 189 def sizes_changed(self): 190 self.graph.update_sizes() 191 192 # Colors 193 def get_color_data(self): 194 """Return the column corresponding to color data""" 195 return self.get_column(self.attr_color, max_categories=MAX_COLORS) 196 197 def get_color_labels(self): 198 """ 199 Return labels for the color legend 200 201 Returns: 202 (list of str): labels 203 """ 204 if self.attr_color is None: 205 return None 206 if not self.attr_color.is_discrete: 207 return self.attr_color.str_val 208 return self.get_column(self.attr_color, max_categories=MAX_COLORS, 209 return_labels=True) 210 211 def is_continuous_color(self): 212 """ 213 Tells whether the color is continuous 214 215 Returns: 216 (bool): 217 """ 218 return self.attr_color is not None and self.attr_color.is_continuous 219 220 def get_palette(self): 221 """ 222 Return a palette suitable for the current `attr_color` 223 224 This method must be overridden if the widget offers coloring that is 225 not based on attribute values. 226 """ 227 attr = self.attr_color 228 if not attr: 229 return None 230 palette = attr.palette 231 if attr.is_discrete and len(attr.values) >= MAX_COLORS: 232 values = self.get_color_labels() 233 colors = [palette.palette[attr.to_val(value)] 234 for value in values[:-1]] + [[192, 192, 192]] 235 236 palette = colorpalettes.DiscretePalette.from_colors(colors) 237 return palette 238 239 def can_draw_density(self): 240 """ 241 Tells whether the current data and settings are suitable for drawing 242 densities 243 244 Returns: 245 (bool): 246 """ 247 return self.data is not None and self.data.domain is not None and \ 248 len(self.data) > 1 and self.attr_color is not None 249 250 def colors_changed(self): 251 self.graph.update_colors() 252 self._update_opacity_warning() 253 self.cb_class_density.setEnabled(self.can_draw_density()) 254 255 # Labels 256 def get_label_data(self, formatter=None): 257 """Return the column corresponding to label data""" 258 if self.attr_label: 259 label_data = self.get_column(self.attr_label) 260 if formatter is None: 261 formatter = self.attr_label.str_val 262 return np.array([formatter(x) for x in label_data]) 263 return None 264 265 def labels_changed(self): 266 self.graph.update_labels() 267 268 # Shapes 269 def get_shape_data(self): 270 """ 271 Return labels for the shape legend 272 273 Returns: 274 (list of str): labels 275 """ 276 return self.get_column(self.attr_shape, max_categories=MAX_SHAPES) 277 278 def get_shape_labels(self): 279 return self.get_column(self.attr_shape, max_categories=MAX_SHAPES, 280 return_labels=True) 281 282 def impute_shapes(self, shape_data, default_symbol): 283 """ 284 Default imputation for shape data 285 286 Let the graph handle it, but add a warning if needed. 287 288 Args: 289 shape_data (np.ndarray): scaled points sizes 290 default_symbol (str): a string representing the symbol 291 """ 292 if self.graph.default_impute_shapes(shape_data, default_symbol): 293 self.Information.missing_shape(self.attr_shape) 294 else: 295 self.Information.missing_shape.clear() 296 297 def shapes_changed(self): 298 self.graph.update_shapes() 299 300 # Tooltip 301 def _point_tooltip(self, point_id, skip_attrs=()): 302 def show_part(_point_data, singular, plural, max_shown, _vars): 303 cols = [escape('{} = {}'.format(var.name, _point_data[var])) 304 for var in _vars[:max_shown + 2] 305 if _vars == domain.class_vars 306 or var not in skip_attrs][:max_shown] 307 if not cols: 308 return "" 309 n_vars = len(_vars) 310 if n_vars > max_shown: 311 cols[-1] = "... and {} others".format(n_vars - max_shown + 1) 312 return \ 313 "<b>{}</b>:<br/>".format(singular if n_vars < 2 else plural) \ 314 + "<br/>".join(cols) 315 316 domain = self.data.domain 317 parts = (("Class", "Classes", 4, domain.class_vars), 318 ("Meta", "Metas", 4, domain.metas), 319 ("Feature", "Features", 10, domain.attributes)) 320 321 point_data = self.data[point_id] 322 return "<br/>".join(show_part(point_data, *columns) 323 for columns in parts) 324 325 def get_tooltip(self, point_ids): 326 """ 327 Return the tooltip string for the given points 328 329 The method is called by the plot on mouse hover 330 331 Args: 332 point_ids (list): indices into `data` 333 334 Returns: 335 (str): 336 """ 337 point_ids = \ 338 np.flatnonzero(self.valid_data)[np.asarray(point_ids, dtype=int)] 339 text = "<hr/>".join(self._point_tooltip(point_id) 340 for point_id in point_ids[:MAX_POINTS_IN_TOOLTIP]) 341 if len(point_ids) > MAX_POINTS_IN_TOOLTIP: 342 text = "{} instances<hr/>{}<hr/>...".format(len(point_ids), text) 343 return text 344 345 def keyPressEvent(self, event): 346 """Update the tip about using the modifier keys when selecting""" 347 super().keyPressEvent(event) 348 self.graph.update_tooltip(event.modifiers()) 349 350 def keyReleaseEvent(self, event): 351 """Update the tip about using the modifier keys when selecting""" 352 super().keyReleaseEvent(event) 353 self.graph.update_tooltip(event.modifiers()) 354 355 356class OWDataProjectionWidget(OWProjectionWidgetBase, openclass=True): 357 """ 358 Base widget for widgets that get Data and Data Subset (both 359 Orange.data.Table) on the input, and output Selected Data and Data 360 (both Orange.data.Table). 361 362 Beside that the widget displays data as two-dimensional projection 363 of points. 364 """ 365 class Inputs: 366 data = Input("Data", Table, default=True) 367 data_subset = Input("Data Subset", Table) 368 369 class Outputs: 370 selected_data = Output("Selected Data", Table, default=True) 371 annotated_data = Output(ANNOTATED_DATA_SIGNAL_NAME, Table) 372 373 class Warning(OWProjectionWidgetBase.Warning): 374 too_many_labels = Msg( 375 "Too many labels to show (zoom in or label only selected)") 376 subset_not_subset = Msg( 377 "Subset data contains some instances that do not appear in " 378 "input data") 379 subset_independent = Msg( 380 "No subset data instances appear in input data") 381 transparent_subset = Msg( 382 "Increase opacity if subset is difficult to see") 383 384 settingsHandler = DomainContextHandler() 385 selection = Setting(None, schema_only=True) 386 visual_settings = Setting({}, schema_only=True) 387 auto_commit = Setting(True) 388 389 GRAPH_CLASS = OWScatterPlotBase 390 graph = SettingProvider(OWScatterPlotBase) 391 graph_name = "graph.plot_widget.plotItem" 392 embedding_variables_names = ("proj-x", "proj-y") 393 buttons_area_orientation = Qt.Vertical 394 395 input_changed = Signal(object) 396 output_changed = Signal(object) 397 398 def __init__(self): 399 super().__init__() 400 self.subset_data = None 401 self.subset_indices = None 402 self.__pending_selection = self.selection 403 self._invalidated = True 404 self._domain_invalidated = True 405 self.setup_gui() 406 VisualSettingsDialog(self, self.graph.parameter_setter.initial_settings) 407 408 # GUI 409 def setup_gui(self): 410 self._add_graph() 411 self._add_controls() 412 self._add_buttons() 413 self.input_changed.emit(None) 414 self.output_changed.emit(None) 415 416 def _add_graph(self): 417 box = gui.vBox(self.mainArea, True, margin=0) 418 self.graph = self.GRAPH_CLASS(self, box) 419 box.layout().addWidget(self.graph.plot_widget) 420 self.graph.too_many_labels.connect( 421 lambda too_many: self.Warning.too_many_labels(shown=too_many)) 422 423 def _add_controls(self): 424 self.gui = OWPlotGUI(self) 425 area = self.controlArea 426 self._point_box = self.gui.point_properties_box(area) 427 self._effects_box = self.gui.effects_box(area) 428 self._plot_box = self.gui.plot_properties_box(area) 429 430 def _add_buttons(self): 431 gui.rubber(self.controlArea) 432 self.gui.box_zoom_select(self.buttonsArea) 433 gui.auto_send(self.buttonsArea, self, "auto_commit") 434 435 @property 436 def effective_variables(self): 437 return self.data.domain.attributes 438 439 @property 440 def effective_data(self): 441 return self.data.transform(Domain(self.effective_variables, 442 self.data.domain.class_vars, 443 self.data.domain.metas)) 444 445 # Input 446 @Inputs.data 447 @check_sql_input 448 def set_data(self, data): 449 data_existed = self.data is not None 450 effective_data = self.effective_data if data_existed else None 451 self.closeContext() 452 self.data = data 453 self.check_data() 454 self.init_attr_values() 455 self.openContext(self.data) 456 self._invalidated = not ( 457 data_existed and self.data is not None and 458 array_equal(effective_data.X, self.effective_data.X)) 459 self._domain_invalidated = not ( 460 data_existed and self.data is not None and 461 effective_data.domain.checksum() 462 == self.effective_data.domain.checksum()) 463 if self._invalidated: 464 self.clear() 465 self.input_changed.emit(data) 466 self.enable_controls() 467 468 def check_data(self): 469 self.clear_messages() 470 471 def enable_controls(self): 472 self.cb_class_density.setEnabled(self.can_draw_density()) 473 474 @Inputs.data_subset 475 @check_sql_input 476 def set_subset_data(self, subset): 477 self.subset_data = subset 478 479 def handleNewSignals(self): 480 self._handle_subset_data() 481 if self._invalidated: 482 self._invalidated = False 483 self.setup_plot() 484 else: 485 self.graph.update_point_props() 486 self._update_opacity_warning() 487 self.unconditional_commit() 488 489 def _handle_subset_data(self): 490 self.Warning.subset_independent.clear() 491 self.Warning.subset_not_subset.clear() 492 if self.data is None or self.subset_data is None: 493 self.subset_indices = set() 494 else: 495 self.subset_indices = set(self.subset_data.ids) 496 ids = set(self.data.ids) 497 if not self.subset_indices & ids: 498 self.Warning.subset_independent() 499 elif self.subset_indices - ids: 500 self.Warning.subset_not_subset() 501 502 def _update_opacity_warning(self): 503 self.Warning.transparent_subset( 504 shown=self.subset_indices and self.graph.alpha_value < 128) 505 506 def get_subset_mask(self): 507 if not self.subset_indices: 508 return None 509 valid_data = self.data[self.valid_data] 510 return np.fromiter((ex.id in self.subset_indices for ex in valid_data), 511 dtype=bool, count=len(valid_data)) 512 513 # Plot 514 def get_embedding(self): 515 """A get embedding method. 516 517 Derived classes must override this method. The overridden method 518 should return embedding for all data (valid and invalid). Invalid 519 data embedding coordinates should be set to 0 (in some cases to Nan). 520 521 The method should also set self.valid_data. 522 523 Returns: 524 np.array: Array of embedding coordinates with shape 525 len(self.data) x 2 526 """ 527 raise NotImplementedError 528 529 def get_coordinates_data(self): 530 embedding = self.get_embedding() 531 if embedding is not None and len(embedding[self.valid_data]): 532 return embedding[self.valid_data].T 533 return None, None 534 535 def setup_plot(self): 536 self.graph.reset_graph() 537 self.__pending_selection = self.selection or self.__pending_selection 538 self.apply_selection() 539 540 # Selection 541 def apply_selection(self): 542 pending = self.__pending_selection 543 if self.data is not None and pending is not None and len(pending) \ 544 and max(i for i, _ in pending) < self.graph.n_valid: 545 index_group = np.array(pending).T 546 selection = np.zeros(self.graph.n_valid, dtype=np.uint8) 547 selection[index_group[0]] = index_group[1] 548 549 self.selection = self.__pending_selection 550 self.__pending_selection = None 551 self.graph.selection = selection 552 self.graph.update_selection_colors() 553 if self.graph.label_only_selected: 554 self.graph.update_labels() 555 556 def selection_changed(self): 557 sel = None if self.data and isinstance(self.data, SqlTable) \ 558 else self.graph.selection 559 self.selection = [(i, x) for i, x in enumerate(sel) if x] \ 560 if sel is not None else None 561 self.commit() 562 563 # Output 564 def commit(self): 565 self.send_data() 566 567 def send_data(self): 568 group_sel, data, graph = None, self._get_projection_data(), self.graph 569 if graph.selection is not None: 570 group_sel = np.zeros(len(data), dtype=int) 571 group_sel[self.valid_data] = graph.selection 572 selected = self._get_selected_data( 573 data, graph.get_selection(), group_sel) 574 self.output_changed.emit(selected) 575 self.Outputs.selected_data.send(selected) 576 self.Outputs.annotated_data.send( 577 self._get_annotated_data(data, group_sel, 578 graph.selection)) 579 580 def _get_projection_data(self): 581 if self.data is None or self.embedding_variables_names is None: 582 return self.data 583 variables = self._get_projection_variables() 584 data = self.data.transform(Domain(self.data.domain.attributes, 585 self.data.domain.class_vars, 586 self.data.domain.metas + variables)) 587 data.metas[:, -2:] = self.get_embedding() 588 return data 589 590 def _get_projection_variables(self): 591 names = get_unique_names( 592 self.data.domain, self.embedding_variables_names) 593 return ContinuousVariable(names[0]), ContinuousVariable(names[1]) 594 595 @staticmethod 596 def _get_selected_data(data, selection, group_sel): 597 return create_groups_table(data, group_sel, False, "Group") \ 598 if len(selection) else None 599 600 @staticmethod 601 def _get_annotated_data(data, group_sel, graph_sel): 602 if data is None: 603 return None 604 if graph_sel is not None and np.max(graph_sel) > 1: 605 return create_groups_table(data, group_sel) 606 else: 607 if group_sel is None: 608 mask = np.full((len(data), ), False) 609 else: 610 mask = np.nonzero(group_sel)[0] 611 return create_annotated_table(data, mask) 612 613 # Report 614 def send_report(self): 615 if self.data is None: 616 return 617 618 caption = self._get_send_report_caption() 619 self.report_plot() 620 if caption: 621 self.report_caption(caption) 622 623 def _get_send_report_caption(self): 624 return report.render_items_vert(( 625 ("Color", self._get_caption_var_name(self.attr_color)), 626 ("Label", self._get_caption_var_name(self.attr_label)), 627 ("Shape", self._get_caption_var_name(self.attr_shape)), 628 ("Size", self._get_caption_var_name(self.attr_size)), 629 ("Jittering", self.graph.jitter_size != 0 and 630 "{} %".format(self.graph.jitter_size)))) 631 632 # Customize plot 633 def set_visual_settings(self, key, value): 634 self.graph.parameter_setter.set_parameter(key, value) 635 self.visual_settings[key] = value 636 637 @staticmethod 638 def _get_caption_var_name(var): 639 return var.name if isinstance(var, Variable) else var 640 641 # Misc 642 def sizeHint(self): 643 return QSize(1132, 708) 644 645 def clear(self): 646 self.selection = None 647 self.graph.selection = None 648 649 def onDeleteWidget(self): 650 super().onDeleteWidget() 651 self.graph.plot_widget.getViewBox().deleteLater() 652 self.graph.plot_widget.clear() 653 self.graph.clear() 654 655 656class OWAnchorProjectionWidget(OWDataProjectionWidget, openclass=True): 657 """ Base widget for widgets with graphs with anchors. """ 658 SAMPLE_SIZE = 100 659 660 GRAPH_CLASS = OWGraphWithAnchors 661 graph = SettingProvider(OWGraphWithAnchors) 662 663 class Outputs(OWDataProjectionWidget.Outputs): 664 components = Output("Components", Table) 665 666 class Error(OWDataProjectionWidget.Error): 667 sparse_data = Msg("Sparse data is not supported") 668 no_valid_data = Msg("No projection due to no valid data") 669 no_instances = Msg("At least two data instances are required") 670 proj_error = Msg("An error occurred while projecting data.\n{}") 671 672 def __init__(self): 673 self.projector = self.projection = None 674 super().__init__() 675 self.graph.view_box.started.connect(self._manual_move_start) 676 self.graph.view_box.moved.connect(self._manual_move) 677 self.graph.view_box.finished.connect(self._manual_move_finish) 678 679 def check_data(self): 680 def error(err): 681 err() 682 self.data = None 683 684 super().check_data() 685 if self.data is not None: 686 if self.data.is_sparse(): 687 error(self.Error.sparse_data) 688 elif len(self.data) < 2: 689 error(self.Error.no_instances) 690 else: 691 if not np.sum(np.all(np.isfinite(self.data.X), axis=1)): 692 error(self.Error.no_valid_data) 693 694 def init_projection(self): 695 self.projection = None 696 if not self.effective_variables: 697 return 698 try: 699 self.projection = self.projector(self.effective_data) 700 except Exception as ex: # pylint: disable=broad-except 701 self.Error.proj_error(ex) 702 703 def get_embedding(self): 704 self.valid_data = None 705 if self.data is None or self.projection is None: 706 return None 707 embedding = self.projection(self.data).X 708 self.valid_data = np.all(np.isfinite(embedding), axis=1) 709 return embedding 710 711 def get_anchors(self): 712 if self.projection is None: 713 return None, None 714 components = self.projection.components_ 715 if components.shape == (1, 1): 716 components = np.array([[1.], [0.]]) 717 return components.T, [a.name for a in self.effective_variables] 718 719 def _manual_move_start(self): 720 self.graph.set_sample_size(self.SAMPLE_SIZE) 721 722 def _manual_move(self, anchor_idx, x, y): 723 self.projection.components_[:, anchor_idx] = [x, y] 724 self.graph.update_coordinates() 725 726 def _manual_move_finish(self, anchor_idx, x, y): 727 self._manual_move(anchor_idx, x, y) 728 self.graph.set_sample_size(None) 729 self.commit() 730 731 def _get_projection_data(self): 732 if self.data is None or self.projection is None: 733 return None 734 proposed = [a.name for a in self.projection.domain.attributes] 735 names = get_unique_names(self.data.domain, proposed) 736 737 if proposed != names: 738 attributes = tuple([attr.copy(name=name) for name, attr in 739 zip(names, self.projection.domain.attributes)]) 740 else: 741 attributes = self.projection.domain.attributes 742 return self.data.transform( 743 Domain(self.data.domain.attributes, 744 self.data.domain.class_vars, 745 self.data.domain.metas + attributes)) 746 747 def commit(self): 748 super().commit() 749 self.send_components() 750 751 def send_components(self): 752 components = None 753 if self.data is not None and self.projection is not None: 754 proposed = [var.name for var in self.effective_variables] 755 comp_name = get_unique_names(proposed, 'component') 756 meta_attrs = [StringVariable(name=comp_name)] 757 domain = Domain(self.effective_variables, metas=meta_attrs) 758 components = Table(domain, self._send_components_x(), 759 metas=self._send_components_metas()) 760 components.name = "components" 761 self.Outputs.components.send(components) 762 763 def _send_components_x(self): 764 return self.projection.components_ 765 766 def _send_components_metas(self): 767 variable_names = [a.name for a in self.projection.domain.attributes] 768 return np.array(variable_names, dtype=object)[:, None] 769 770 def clear(self): 771 super().clear() 772 self.projector = self.projection = None 773 774 775if __name__ == "__main__": 776 class OWProjectionWidgetWithName(OWDataProjectionWidget): 777 name = "projection" 778 779 def get_embedding(self): 780 if self.data is None: 781 return None 782 self.valid_data = np.any(np.isfinite(self.data.X), 1) 783 x_data = self.data.X 784 x_data[x_data == np.inf] = np.nan 785 x_data = np.nanmean(x_data[self.valid_data], 1) 786 y_data = np.ones(len(x_data)) 787 return np.vstack((x_data, y_data)).T 788 789 app = QApplication([]) 790 ow = OWProjectionWidgetWithName() 791 table = Table("iris") 792 ow.set_data(table) 793 ow.set_subset_data(table[::10]) 794 ow.handleNewSignals() 795 ow.show() 796 app.exec() 797 ow.saveSettings() 798