| Total Complexity | 173 |
| Total Lines | 939 |
| Duplicated Lines | 0 % |
Complex classes like Orange.widgets.visualize.OWHeatMap often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.
Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.
| 1 | import sys |
||
| 167 | class OWHeatMap(widget.OWWidget): |
||
| 168 | name = "Heat Map" |
||
| 169 | description = "Heatmap visualization." |
||
| 170 | icon = "icons/Heatmap.svg" |
||
| 171 | priority = 1040 |
||
| 172 | |||
| 173 | inputs = [("Data", Orange.data.Table, "set_dataset")] |
||
| 174 | outputs = [("Selected Data", Orange.data.Table, widget.Default)] |
||
| 175 | |||
| 176 | settingsHandler = settings.DomainContextHandler() |
||
| 177 | |||
| 178 | NoSorting, Clustering, OrderedClustering = 0, 1, 2 |
||
| 179 | NoPosition, PositionTop, PositionBottom = 0, 1, 2 |
||
| 180 | |||
| 181 | gamma = settings.Setting(0) |
||
| 182 | threshold_low = settings.Setting(0.0) |
||
| 183 | threshold_high = settings.Setting(1.0) |
||
| 184 | # Type of sorting to apply on rows |
||
| 185 | sort_rows = settings.Setting(NoSorting) |
||
| 186 | # Type of sorting to apply on columns |
||
| 187 | sort_columns = settings.Setting(NoSorting) |
||
| 188 | # Display stripe with averages |
||
| 189 | averages = settings.Setting(True) |
||
| 190 | # Display legend |
||
| 191 | legend = settings.Setting(True) |
||
| 192 | # Annotations |
||
| 193 | annotation_index = settings.ContextSetting(0) |
||
| 194 | # Stored color palette settings |
||
| 195 | color_settings = settings.Setting(None) |
||
| 196 | user_palettes = settings.Setting([]) |
||
| 197 | palette_index = settings.Setting(0) |
||
| 198 | column_label_pos = settings.Setting(PositionTop) |
||
| 199 | |||
| 200 | auto_commit = settings.Setting(True) |
||
| 201 | |||
| 202 | want_graph = True |
||
| 203 | |||
| 204 | def __init__(self, parent=None): |
||
| 205 | super().__init__(self, parent) |
||
| 206 | |||
| 207 | # set default settings |
||
| 208 | self.SpaceX = 10 |
||
| 209 | self.ShowAnnotation = 0 |
||
| 210 | |||
| 211 | self.colorSettings = None |
||
| 212 | self.selectedSchemaIndex = 0 |
||
| 213 | |||
| 214 | self.palette = None |
||
| 215 | self.keep_aspect = False |
||
| 216 | #: The data striped of discrete features |
||
| 217 | self.data = None |
||
| 218 | #: The original data with all features (retained to |
||
| 219 | #: preserve the domain on the output) |
||
| 220 | self.input_data = None |
||
| 221 | |||
| 222 | self.annotation_vars = ['(None)'] |
||
| 223 | self.__rows_cache = {} |
||
| 224 | self.__columns_cache = {} |
||
| 225 | |||
| 226 | # GUI definition |
||
| 227 | colorbox = gui.widgetBox(self.controlArea, "Color") |
||
| 228 | self.color_cb = gui.comboBox(colorbox, self, "palette_index") |
||
| 229 | self.color_cb.setIconSize(QSize(64, 16)) |
||
| 230 | palettes = sorted(colorbrewer.colorSchemes["sequential"].items()) |
||
| 231 | palettes += [("Green-Black-Red", |
||
| 232 | {3: [(0, 255, 0), (0, 0, 0), (255, 0, 0)]})] |
||
| 233 | palettes += self.user_palettes |
||
| 234 | model = color_palette_model(palettes, self.color_cb.iconSize()) |
||
| 235 | model.setParent(self) |
||
| 236 | self.color_cb.setModel(model) |
||
| 237 | self.color_cb.activated.connect(self.update_color_schema) |
||
| 238 | self.color_cb.setCurrentIndex(self.palette_index) |
||
| 239 | # TODO: Add 'Manage/Add/Remove' action. |
||
| 240 | |||
| 241 | form = QtGui.QFormLayout( |
||
| 242 | formAlignment=Qt.AlignLeft, |
||
| 243 | labelAlignment=Qt.AlignLeft, |
||
| 244 | fieldGrowthPolicy=QtGui.QFormLayout.AllNonFixedFieldsGrow |
||
| 245 | ) |
||
| 246 | |||
| 247 | lowslider = gui.hSlider( |
||
| 248 | colorbox, self, "threshold_low", minValue=0.0, maxValue=1.0, |
||
| 249 | step=0.05, ticks=True, intOnly=False, |
||
| 250 | createLabel=False, callback=self.update_color_schema) |
||
| 251 | highslider = gui.hSlider( |
||
| 252 | colorbox, self, "threshold_high", minValue=0.0, maxValue=1.0, |
||
| 253 | step=0.05, ticks=True, intOnly=False, |
||
| 254 | createLabel=False, callback=self.update_color_schema) |
||
| 255 | |||
| 256 | form.addRow("Low:", lowslider) |
||
| 257 | form.addRow("High:", highslider) |
||
| 258 | colorbox.layout().addLayout(form) |
||
| 259 | |||
| 260 | sortbox = gui.widgetBox(self.controlArea, "Sorting") |
||
| 261 | # For attributes |
||
| 262 | gui.comboBox(sortbox, self, "sort_columns", |
||
| 263 | items=["No sorting", |
||
| 264 | "Clustering", |
||
| 265 | "Clustering with leaf ordering"], |
||
| 266 | label='Columns', |
||
| 267 | callback=self.update_sorting_attributes) |
||
| 268 | |||
| 269 | # For examples |
||
| 270 | gui.comboBox(sortbox, self, "sort_rows", |
||
| 271 | items=["No sorting", |
||
| 272 | "Clustering", |
||
| 273 | "Clustering with leaf ordering"], |
||
| 274 | label='Rows', |
||
| 275 | callback=self.update_sorting_examples) |
||
| 276 | |||
| 277 | box = gui.widgetBox(self.controlArea, 'Annotation && Legends') |
||
| 278 | |||
| 279 | gui.checkBox(box, self, 'legend', 'Show legend', |
||
| 280 | callback=self.update_legend) |
||
| 281 | |||
| 282 | gui.checkBox(box, self, 'averages', 'Stripes with averages', |
||
| 283 | callback=self.update_averages_stripe) |
||
| 284 | |||
| 285 | annotbox = gui.widgetBox(box, "Row Annotations") |
||
| 286 | annotbox.setFlat(True) |
||
| 287 | self.annotations_cb = gui.comboBox(annotbox, self, "annotation_index", |
||
| 288 | items=self.annotation_vars, |
||
| 289 | callback=self.update_annotations) |
||
| 290 | |||
| 291 | posbox = gui.widgetBox(box, "Column Labels Position") |
||
| 292 | posbox.setFlat(True) |
||
| 293 | |||
| 294 | gui.comboBox( |
||
| 295 | posbox, self, "column_label_pos", |
||
| 296 | items=["None", "Top", "Bottom", "Top and Bottom"], |
||
| 297 | callback=self.update_column_annotations) |
||
| 298 | |||
| 299 | gui.checkBox(self.controlArea, self, "keep_aspect", |
||
| 300 | "Keep aspect ratio", box="Resize", |
||
| 301 | callback=self.__aspect_mode_changed) |
||
| 302 | |||
| 303 | splitbox = gui.widgetBox(self.controlArea, "Split By") |
||
| 304 | self.split_lb = QtGui.QListWidget() |
||
| 305 | self.split_lb.itemSelectionChanged.connect(self.update_heatmaps) |
||
| 306 | splitbox.layout().addWidget(self.split_lb) |
||
| 307 | |||
| 308 | gui.rubber(self.controlArea) |
||
| 309 | gui.auto_commit(self.controlArea, self, "auto_commit", "Commit") |
||
| 310 | |||
| 311 | # Scene with heatmap |
||
| 312 | self.heatmap_scene = self.scene = HeatmapScene(parent=self) |
||
| 313 | self.selection_manager = HeatmapSelectionManager(self) |
||
| 314 | self.selection_manager.selection_changed.connect( |
||
| 315 | self.__update_selection_geometry) |
||
| 316 | self.selection_manager.selection_finished.connect( |
||
| 317 | self.on_selection_finished) |
||
| 318 | self.heatmap_scene.set_selection_manager(self.selection_manager) |
||
| 319 | |||
| 320 | item = QtGui.QGraphicsRectItem(0, 0, 10, 10, None, self.heatmap_scene) |
||
| 321 | self.heatmap_scene.itemsBoundingRect() |
||
| 322 | self.heatmap_scene.removeItem(item) |
||
| 323 | |||
| 324 | policy = (Qt.ScrollBarAlwaysOn if self.keep_aspect |
||
| 325 | else Qt.ScrollBarAlwaysOff) |
||
| 326 | self.sceneView = QGraphicsView( |
||
| 327 | self.scene, |
||
| 328 | verticalScrollBarPolicy=policy, |
||
| 329 | horizontalScrollBarPolicy=policy) |
||
| 330 | |||
| 331 | self.sceneView.viewport().installEventFilter(self) |
||
| 332 | |||
| 333 | self.mainArea.layout().addWidget(self.sceneView) |
||
| 334 | self.heatmap_scene.widget = None |
||
| 335 | |||
| 336 | self.heatmap_widget_grid = [[]] |
||
| 337 | self.attr_annotation_widgets = [] |
||
| 338 | self.attr_dendrogram_widgets = [] |
||
| 339 | self.gene_annotation_widgets = [] |
||
| 340 | self.gene_dendrogram_widgets = [] |
||
| 341 | |||
| 342 | self.selection_rects = [] |
||
| 343 | self.selected_rows = [] |
||
| 344 | self.graphButton.clicked.connect(self.save_graph) |
||
| 345 | |||
| 346 | def sizeHint(self): |
||
| 347 | return QSize(800, 400) |
||
| 348 | |||
| 349 | def color_palette(self): |
||
| 350 | data = self.color_cb.itemData(self.palette_index, role=Qt.UserRole) |
||
| 351 | if data is None: |
||
| 352 | return [] |
||
| 353 | else: |
||
| 354 | _, colors = max(data.items()) |
||
| 355 | return color_palette_table( |
||
| 356 | colors, threshold_low=self.threshold_low, |
||
| 357 | threshold_high=self.threshold_high) |
||
| 358 | |||
| 359 | def selected_split_label(self): |
||
| 360 | """Return the current selected split label.""" |
||
| 361 | item = self.split_lb.currentItem() |
||
| 362 | return str(item.text()) if item else None |
||
| 363 | |||
| 364 | def clear(self): |
||
| 365 | self.data = None |
||
| 366 | self.input_data = None |
||
| 367 | self.annotations_cb.clear() |
||
| 368 | self.annotations_cb.addItem('(None)') |
||
| 369 | self.split_lb.clear() |
||
| 370 | self.annotation_vars = ['(None)'] |
||
| 371 | self.clear_scene() |
||
| 372 | self.selected_rows = [] |
||
| 373 | self.__columns_cache.clear() |
||
| 374 | self.__rows_cache.clear() |
||
| 375 | |||
| 376 | def clear_scene(self): |
||
| 377 | self.selection_manager.set_heatmap_widgets([[]]) |
||
| 378 | self.heatmap_scene.clear() |
||
| 379 | self.heatmap_scene.widget = None |
||
| 380 | self.heatmap_widget_grid = [[]] |
||
| 381 | self.col_annotation_widgets = [] |
||
| 382 | self.col_annotation_widgets_bottom = [] |
||
| 383 | self.col_annotation_widgets_top = [] |
||
| 384 | self.row_annotation_widgets = [] |
||
| 385 | self.col_dendrograms = [] |
||
| 386 | self.row_dendrograms = [] |
||
| 387 | self.selection_rects = [] |
||
| 388 | |||
| 389 | def set_dataset(self, data=None): |
||
| 390 | """Set the input dataset to display.""" |
||
| 391 | self.closeContext() |
||
| 392 | self.clear() |
||
| 393 | |||
| 394 | self.error(0) |
||
| 395 | self.warning(0) |
||
| 396 | input_data = data |
||
| 397 | if data is not None and \ |
||
| 398 | any(var.is_discrete for var in data.domain.attributes): |
||
| 399 | ndisc = sum(var.is_discrete for var in data.domain.attributes) |
||
| 400 | data = data.from_table( |
||
| 401 | Orange.data.Domain([var for var in data.domain.attributes |
||
| 402 | if var.is_continuous], |
||
| 403 | data.domain.class_vars, |
||
| 404 | data.domain.metas), |
||
| 405 | data) |
||
| 406 | if not data.domain.attributes: |
||
| 407 | self.error(0, "No continuous feature columns") |
||
| 408 | input_data = data = None |
||
| 409 | else: |
||
| 410 | self.warning(0, "{} discrete column{} removed" |
||
| 411 | .format(ndisc, "s" if ndisc > 1 else "")) |
||
| 412 | |||
| 413 | self.data = data |
||
| 414 | self.input_data = input_data |
||
| 415 | if data is not None: |
||
| 416 | variables = self.data.domain.class_vars + self.data.domain.metas |
||
| 417 | variables = [var for var in variables |
||
| 418 | if isinstance(var, (Orange.data.DiscreteVariable, |
||
| 419 | Orange.data.StringVariable))] |
||
| 420 | self.annotation_vars.extend(variables) |
||
| 421 | |||
| 422 | for var in variables: |
||
| 423 | self.annotations_cb.addItem(*gui.attributeItem(var)) |
||
| 424 | |||
| 425 | self.split_lb.addItems(candidate_split_labels(data)) |
||
| 426 | |||
| 427 | self.openContext(self.data) |
||
| 428 | if self.annotation_index >= len(self.annotation_vars): |
||
| 429 | self.annotation_index = 0 |
||
| 430 | |||
| 431 | self.update_heatmaps() |
||
| 432 | |||
| 433 | self.commit() |
||
| 434 | |||
| 435 | def update_heatmaps(self): |
||
| 436 | if self.data is not None: |
||
| 437 | self.clear_scene() |
||
| 438 | self.construct_heatmaps(self.data, self.selected_split_label()) |
||
| 439 | self.construct_heatmaps_scene(self.heatmapparts) |
||
| 440 | else: |
||
| 441 | self.clear() |
||
| 442 | |||
| 443 | def _make(self, data, group_var=None, group_key=None): |
||
| 444 | if group_var is not None: |
||
| 445 | assert group_var.is_discrete |
||
| 446 | _col_data, _ = data.get_column_view(group_var) |
||
| 447 | row_groups = [np.flatnonzero(_col_data == i) |
||
| 448 | for i in range(len(group_var.values))] |
||
| 449 | row_indices = [np.flatnonzero(_col_data == i) |
||
| 450 | for i in range(len(group_var.values))] |
||
| 451 | row_groups = [namespace(title=name, indices=ind, cluster=None, |
||
| 452 | cluster_ord=None) |
||
| 453 | for name, ind in zip(group_var.values, row_indices)] |
||
| 454 | else: |
||
| 455 | row_groups = [namespace(title=None, indices=slice(0, -1), |
||
| 456 | cluster=None, cluster_ord=None)] |
||
| 457 | |||
| 458 | if group_key is not None: |
||
| 459 | col_groups = split_domain(data.domain, group_key) |
||
| 460 | assert len(col_groups) > 0 |
||
| 461 | col_indices = [np.array([data.domain.index(var) for var in group]) |
||
| 462 | for _, group in col_groups] |
||
| 463 | col_groups = [namespace(title=name, domain=d, indices=ind, |
||
| 464 | cluster=None, cluster_ord=None) |
||
| 465 | for (name, d), ind in zip(col_groups, col_indices)] |
||
| 466 | else: |
||
| 467 | col_groups = [ |
||
| 468 | namespace( |
||
| 469 | title=None, indices=slice(0, len(data.domain.attributes)), |
||
| 470 | domain=data.domain, cluster=None, cluster_ord=None) |
||
| 471 | ] |
||
| 472 | |||
| 473 | minv, maxv = np.nanmin(data.X), np.nanmax(data.X) |
||
| 474 | |||
| 475 | parts = namespace( |
||
| 476 | rows=row_groups, columns=col_groups, |
||
| 477 | levels=(minv, maxv), |
||
| 478 | ) |
||
| 479 | return parts |
||
| 480 | |||
| 481 | def cluster_rows(self, data, parts, ordered=False): |
||
| 482 | row_groups = [] |
||
| 483 | for row in parts.rows: |
||
| 484 | if row.cluster is not None: |
||
| 485 | cluster = row.cluster |
||
| 486 | else: |
||
| 487 | cluster = None |
||
| 488 | if row.cluster_ord is not None: |
||
| 489 | cluster_ord = row.cluster_ord |
||
| 490 | else: |
||
| 491 | cluster_ord = None |
||
| 492 | |||
| 493 | need_dist = cluster is None or (ordered and cluster_ord is None) |
||
| 494 | if need_dist: |
||
| 495 | subset = data[row.indices] |
||
| 496 | subset = Orange.distance._preprocess(subset) |
||
| 497 | matrix = Orange.distance.Euclidean(subset) |
||
| 498 | |||
| 499 | if cluster is None: |
||
| 500 | cluster = hierarchical.dist_matrix_clustering(matrix) |
||
| 501 | |||
| 502 | if ordered and cluster_ord is None: |
||
| 503 | cluster_ord = hierarchical.optimal_leaf_ordering(cluster, matrix) |
||
| 504 | |||
| 505 | row_groups.append(namespace(title=row.title, indices=row.indices, |
||
| 506 | cluster=cluster, cluster_ord=cluster_ord)) |
||
| 507 | |||
| 508 | return namespace(columns=parts.columns, rows=row_groups, |
||
| 509 | levels=parts.levels) |
||
| 510 | |||
| 511 | def cluster_columns(self, data, parts, ordered=False): |
||
| 512 | if len(parts.columns) > 1: |
||
| 513 | data = vstack_by_subdomain(data, [col.domain for col in parts.columns]) |
||
| 514 | assert all(var.is_continuous for var in data.domain.attributes) |
||
| 515 | |||
| 516 | col0 = parts.columns[0] |
||
| 517 | if col0.cluster is not None: |
||
| 518 | cluster = col0.cluster |
||
| 519 | else: |
||
| 520 | cluster = None |
||
| 521 | if col0.cluster_ord is not None: |
||
| 522 | cluster_ord = col0.cluster_ord |
||
| 523 | else: |
||
| 524 | cluster_ord = None |
||
| 525 | need_dist = cluster is None or (ordered and cluster_ord is None) |
||
| 526 | |||
| 527 | if need_dist: |
||
| 528 | data = Orange.distance._preprocess(data) |
||
| 529 | matrix = Orange.distance.PearsonR(data, axis=0) |
||
| 530 | |||
| 531 | if cluster is None: |
||
| 532 | cluster = hierarchical.dist_matrix_clustering(matrix) |
||
| 533 | if ordered and cluster_ord is None: |
||
| 534 | cluster_ord = hierarchical.optimal_leaf_ordering(cluster, matrix) |
||
| 535 | |||
| 536 | col_groups = [namespace(title=col.title, indices=col.indices, |
||
| 537 | cluster=cluster, cluster_ord=cluster_ord, |
||
| 538 | domain=col.domain) |
||
| 539 | for col in parts.columns] |
||
| 540 | return namespace(columns=col_groups, rows=parts.rows, |
||
| 541 | levels=parts.levels) |
||
| 542 | |||
| 543 | def construct_heatmaps(self, data, split_label=None): |
||
| 544 | |||
| 545 | if split_label is not None: |
||
| 546 | groups = split_domain(data.domain, split_label) |
||
| 547 | assert len(groups) > 0 |
||
| 548 | else: |
||
| 549 | groups = [("", data.domain)] |
||
| 550 | |||
| 551 | if data.domain.has_discrete_class: |
||
| 552 | group_var = data.domain.class_var |
||
| 553 | else: |
||
| 554 | group_var = None |
||
| 555 | |||
| 556 | self.progressBarInit() |
||
| 557 | |||
| 558 | group_label = split_label |
||
| 559 | |||
| 560 | parts = self._make(data, group_var, group_label) |
||
| 561 | # Restore/update the row/columns items descriptions from cache if |
||
| 562 | # available |
||
| 563 | if group_var in self.__rows_cache: |
||
| 564 | parts.rows = self.__rows_cache[group_var].rows |
||
| 565 | if group_label in self.__columns_cache: |
||
| 566 | parts.columns = self.__columns_cache[group_label].columns |
||
| 567 | |||
| 568 | if self.sort_rows != OWHeatMap.NoSorting: |
||
| 569 | parts = self.cluster_rows( |
||
| 570 | self.data, parts, |
||
| 571 | ordered=self.sort_rows == OWHeatMap.OrderedClustering) |
||
| 572 | |||
| 573 | if self.sort_columns != OWHeatMap.NoSorting: |
||
| 574 | parts = self.cluster_columns( |
||
| 575 | self.data, parts, |
||
| 576 | ordered=self.sort_columns == OWHeatMap.OrderedClustering) |
||
| 577 | |||
| 578 | # Cache the updated parts |
||
| 579 | self.__rows_cache[group_var] = parts |
||
| 580 | self.__columns_cache[group_label] = parts |
||
| 581 | |||
| 582 | self.heatmapparts = parts |
||
| 583 | self.progressBarFinished() |
||
| 584 | |||
| 585 | def construct_heatmaps_scene(self, parts): |
||
| 586 | def select_row(item): |
||
| 587 | if self.sort_rows == OWHeatMap.NoSorting: |
||
| 588 | return namespace(title=item.title, indices=item.indices, |
||
| 589 | cluster=None) |
||
| 590 | elif self.sort_rows == OWHeatMap.Clustering: |
||
| 591 | return namespace(title=item.title, indices=item.indices, |
||
| 592 | cluster=item.cluster) |
||
| 593 | elif self.sort_rows == OWHeatMap.OrderedClustering: |
||
| 594 | return namespace(title=item.title, indices=item.indices, |
||
| 595 | cluster=item.cluster_ord) |
||
| 596 | |||
| 597 | def select_col(item): |
||
| 598 | if self.sort_columns == OWHeatMap.NoSorting: |
||
| 599 | return namespace(title=item.title, indices=item.indices, |
||
| 600 | cluster=None, domain=item.domain) |
||
| 601 | elif self.sort_columns == OWHeatMap.Clustering: |
||
| 602 | return namespace(title=item.title, indices=item.indices, |
||
| 603 | cluster=item.cluster, domain=item.domain) |
||
| 604 | elif self.sort_columns == OWHeatMap.OrderedClustering: |
||
| 605 | return namespace(title=item.title, indices=item.indices, |
||
| 606 | cluster=item.cluster_ord, domain=item.domain) |
||
| 607 | |||
| 608 | rows = [select_row(rowitem) for rowitem in parts.rows] |
||
| 609 | cols = [select_col(colitem) for colitem in parts.columns] |
||
| 610 | parts = namespace(columns=cols, rows=rows, levels=parts.levels) |
||
| 611 | |||
| 612 | self.setup_scene(parts) |
||
| 613 | |||
| 614 | def setup_scene(self, parts): |
||
| 615 | # parts = * a list of row descriptors (title, indices, cluster,) |
||
| 616 | # * a list of col descriptors (title, indices, cluster, domain) |
||
| 617 | |||
| 618 | self.heatmap_scene.clear() |
||
| 619 | # The top level container widget |
||
| 620 | widget = GraphicsWidget() |
||
| 621 | widget.layoutDidActivate.connect(self.__update_selection_geometry) |
||
| 622 | |||
| 623 | grid = QtGui.QGraphicsGridLayout() |
||
| 624 | grid.setSpacing(self.SpaceX) |
||
| 625 | self.heatmap_scene.addItem(widget) |
||
| 626 | |||
| 627 | N, M = len(parts.rows), len(parts.columns) |
||
| 628 | |||
| 629 | # Start row/column where the heatmap items are inserted |
||
| 630 | # (after the titles/legends/dendrograms) |
||
| 631 | Row0 = 3 |
||
| 632 | Col0 = 3 |
||
| 633 | LegendRow = 0 |
||
| 634 | # The column for the vertical dendrogram |
||
| 635 | DendrogramColumn = 0 |
||
| 636 | # The row for the horizontal dendrograms |
||
| 637 | DendrogramRow = 1 |
||
| 638 | RightLabelColumn = Col0 + M |
||
| 639 | TopLabelsRow = 2 |
||
| 640 | BottomLabelsRow = Row0 + 2 * N |
||
| 641 | |||
| 642 | widget.setLayout(grid) |
||
| 643 | |||
| 644 | palette = self.color_palette() |
||
| 645 | |||
| 646 | sort_i = [] |
||
| 647 | sort_j = [] |
||
| 648 | |||
| 649 | column_dendrograms = [None] * M |
||
| 650 | row_dendrograms = [None] * N |
||
| 651 | |||
| 652 | for i, rowitem in enumerate(parts.rows): |
||
| 653 | if rowitem.title: |
||
| 654 | title = QtGui.QGraphicsSimpleTextItem(rowitem.title, widget) |
||
| 655 | item = GraphicsSimpleTextLayoutItem(title, parent=grid) |
||
| 656 | grid.addItem(item, Row0 + i * 2, Col0) |
||
| 657 | |||
| 658 | if rowitem.cluster: |
||
| 659 | dendrogram = DendrogramWidget( |
||
| 660 | parent=widget, |
||
| 661 | selectionMode=DendrogramWidget.NoSelection, |
||
| 662 | hoverHighlightEnabled=True) |
||
| 663 | dendrogram.set_root(rowitem.cluster) |
||
| 664 | dendrogram.setMaximumWidth(100) |
||
| 665 | dendrogram.setMinimumWidth(100) |
||
| 666 | # Ignore dendrogram vertical size hint (heatmap's size |
||
| 667 | # should define the row's vertical size). |
||
| 668 | dendrogram.setSizePolicy( |
||
| 669 | QSizePolicy.Expanding, QSizePolicy.Ignored) |
||
| 670 | dendrogram.itemClicked.connect( |
||
| 671 | lambda item, partindex=i: |
||
| 672 | self.__select_by_cluster(item, partindex) |
||
| 673 | ) |
||
| 674 | |||
| 675 | grid.addItem(dendrogram, Row0 + i * 2 + 1, DendrogramColumn) |
||
| 676 | sort_i.append(np.array(leaf_indices(rowitem.cluster))) |
||
| 677 | row_dendrograms[i] = dendrogram |
||
| 678 | else: |
||
| 679 | sort_i.append(None) |
||
| 680 | |||
| 681 | for j, colitem in enumerate(parts.columns): |
||
| 682 | if colitem.title: |
||
| 683 | title = QtGui.QGraphicsSimpleTextItem(colitem.title, widget) |
||
| 684 | item = GraphicsSimpleTextLayoutItem(title, parent=grid) |
||
| 685 | grid.addItem(item, 1, Col0 + j) |
||
| 686 | |||
| 687 | if colitem.cluster: |
||
| 688 | dendrogram = DendrogramWidget( |
||
| 689 | parent=widget, |
||
| 690 | orientation=DendrogramWidget.Top, |
||
| 691 | selectionMode=DendrogramWidget.NoSelection, |
||
| 692 | hoverHighlightEnabled=False) |
||
| 693 | |||
| 694 | dendrogram.set_root(colitem.cluster) |
||
| 695 | dendrogram.setMaximumHeight(100) |
||
| 696 | dendrogram.setMinimumHeight(100) |
||
| 697 | # Ignore dendrogram horizontal size hint (heatmap's width |
||
| 698 | # should define the column width). |
||
| 699 | dendrogram.setSizePolicy( |
||
| 700 | QSizePolicy.Ignored, QSizePolicy.Expanding) |
||
| 701 | grid.addItem(dendrogram, DendrogramRow, Col0 + j) |
||
| 702 | sort_j.append(np.array(leaf_indices(colitem.cluster))) |
||
| 703 | column_dendrograms[j] = dendrogram |
||
| 704 | else: |
||
| 705 | sort_j.append(None) |
||
| 706 | |||
| 707 | heatmap_widgets = [] |
||
| 708 | for i in range(N): |
||
| 709 | heatmap_row = [] |
||
| 710 | for j in range(M): |
||
| 711 | row_ix = parts.rows[i].indices |
||
| 712 | col_ix = parts.columns[j].indices |
||
| 713 | hw = GraphicsHeatmapWidget(parent=widget) |
||
| 714 | data = self.data[row_ix, col_ix].X |
||
| 715 | |||
| 716 | if sort_i[i] is not None: |
||
| 717 | data = data[sort_i[i]] |
||
| 718 | if sort_j[j] is not None: |
||
| 719 | data = data[:, sort_j[j]] |
||
| 720 | |||
| 721 | hw.set_heatmap_data(data) |
||
| 722 | hw.set_levels(parts.levels) |
||
| 723 | hw.set_color_table(palette) |
||
| 724 | hw.set_show_averages(self.averages) |
||
| 725 | |||
| 726 | grid.addItem(hw, Row0 + i * 2 + 1, Col0 + j) |
||
| 727 | grid.setRowStretchFactor(Row0 + i * 2 + 1, data.shape[0] * 100) |
||
| 728 | heatmap_row.append(hw) |
||
| 729 | heatmap_widgets.append(heatmap_row) |
||
| 730 | |||
| 731 | row_annotation_widgets = [] |
||
| 732 | col_annotation_widgets = [] |
||
| 733 | col_annotation_widgets_top = [] |
||
| 734 | col_annotation_widgets_bottom = [] |
||
| 735 | |||
| 736 | for i, rowitem in enumerate(parts.rows): |
||
| 737 | if isinstance(rowitem.indices, slice): |
||
| 738 | indices = np.array( |
||
| 739 | range(*rowitem.indices.indices(self.data.X.shape[0]))) |
||
| 740 | else: |
||
| 741 | indices = rowitem.indices |
||
| 742 | if sort_i[i] is not None: |
||
| 743 | indices = indices[sort_i[i]] |
||
| 744 | |||
| 745 | labels = [str(i) for i in indices] |
||
| 746 | |||
| 747 | labelslist = GraphicsSimpleTextList( |
||
| 748 | labels, parent=widget, orientation=Qt.Vertical) |
||
| 749 | |||
| 750 | labelslist._indices = indices |
||
| 751 | labelslist.setAlignment(Qt.AlignLeft | Qt.AlignVCenter) |
||
| 752 | labelslist.setContentsMargins(0.0, 0.0, 0.0, 0.0) |
||
| 753 | labelslist.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Expanding) |
||
| 754 | |||
| 755 | grid.addItem(labelslist, Row0 + i * 2 + 1, RightLabelColumn) |
||
| 756 | grid.setAlignment(labelslist, Qt.AlignLeft) |
||
| 757 | row_annotation_widgets.append(labelslist) |
||
| 758 | |||
| 759 | for j, colitem in enumerate(parts.columns): |
||
| 760 | # Top attr annotations |
||
| 761 | if isinstance(colitem.indices, slice): |
||
| 762 | indices = np.array( |
||
| 763 | range(*colitem.indices.indices(self.data.X.shape[1]))) |
||
| 764 | else: |
||
| 765 | indices = colitem.indices |
||
| 766 | if sort_j[j] is not None: |
||
| 767 | indices = indices[sort_j[j]] |
||
| 768 | |||
| 769 | labels = [self.data.domain[i].name for i in indices] |
||
| 770 | |||
| 771 | labelslist = GraphicsSimpleTextList( |
||
| 772 | labels, parent=widget, orientation=Qt.Horizontal) |
||
| 773 | labelslist.setAlignment(Qt.AlignBottom | Qt.AlignLeft) |
||
| 774 | labelslist._indices = indices |
||
| 775 | |||
| 776 | labelslist.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) |
||
| 777 | |||
| 778 | grid.addItem(labelslist, TopLabelsRow, Col0 + j, |
||
| 779 | Qt.AlignBottom | Qt.AlignLeft) |
||
| 780 | col_annotation_widgets.append(labelslist) |
||
| 781 | col_annotation_widgets_top.append(labelslist) |
||
| 782 | |||
| 783 | # Bottom attr annotations |
||
| 784 | labelslist = GraphicsSimpleTextList( |
||
| 785 | labels, parent=widget, orientation=Qt.Horizontal) |
||
| 786 | labelslist.setAlignment(Qt.AlignTop | Qt.AlignHCenter) |
||
| 787 | labelslist.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) |
||
| 788 | |||
| 789 | grid.addItem(labelslist, BottomLabelsRow, Col0 + j) |
||
| 790 | col_annotation_widgets.append(labelslist) |
||
| 791 | col_annotation_widgets_bottom.append(labelslist) |
||
| 792 | |||
| 793 | legend = GradientLegendWidget( |
||
| 794 | parts.levels[0], parts.levels[1], |
||
| 795 | parent=widget) |
||
| 796 | |||
| 797 | legend.set_color_table(palette) |
||
| 798 | legend.setMinimumSize(QSizeF(100, 20)) |
||
| 799 | legend.setVisible(self.legend) |
||
| 800 | |||
| 801 | grid.addItem(legend, LegendRow, Col0) |
||
| 802 | |||
| 803 | self.heatmap_scene.widget = widget |
||
| 804 | self.heatmap_widget_grid = heatmap_widgets |
||
| 805 | self.selection_manager.set_heatmap_widgets(heatmap_widgets) |
||
| 806 | |||
| 807 | self.row_annotation_widgets = row_annotation_widgets |
||
| 808 | self.col_annotation_widgets = col_annotation_widgets |
||
| 809 | self.col_annotation_widgets_top = col_annotation_widgets_top |
||
| 810 | self.col_annotation_widgets_bottom = col_annotation_widgets_bottom |
||
| 811 | self.col_dendrograms = column_dendrograms |
||
| 812 | self.row_dendrograms = row_dendrograms |
||
| 813 | |||
| 814 | self.update_annotations() |
||
| 815 | self.update_column_annotations() |
||
| 816 | |||
| 817 | self.__update_size_constraints() |
||
| 818 | |||
| 819 | def __update_size_constraints(self): |
||
| 820 | if self.heatmap_scene.widget is not None: |
||
| 821 | mode = Qt.KeepAspectRatio if self.keep_aspect \ |
||
| 822 | else Qt.IgnoreAspectRatio |
||
| 823 | size = QSizeF(self.sceneView.viewport().size()) |
||
| 824 | widget = self.heatmap_scene.widget |
||
| 825 | layout = widget.layout() |
||
| 826 | if mode == Qt.IgnoreAspectRatio: |
||
| 827 | # Reset the row height constraints ... |
||
| 828 | for i, hm_row in enumerate(self.heatmap_widget_grid): |
||
| 829 | layout.setRowMaximumHeight(3 + i * 2 + 1, np.finfo(np.float32).max) |
||
| 830 | layout.setRowPreferredHeight(3 + i * 2 + 1, 0) |
||
| 831 | # ... and resize to match the viewport, taking the minimum size |
||
| 832 | # into account |
||
| 833 | minsize = widget.minimumSize() |
||
| 834 | size = size.expandedTo(minsize) |
||
| 835 | widget.resize(size) |
||
| 836 | else: |
||
| 837 | # First set/update the widget's width (the layout will |
||
| 838 | # distribute the available width to heatmap widgets in |
||
| 839 | # the grid) |
||
| 840 | minsize = widget.minimumSize() |
||
| 841 | widget.resize(size.expandedTo(minsize).width(), |
||
| 842 | widget.size().height()) |
||
| 843 | # calculate and set the heatmap row's heights based on |
||
| 844 | # the width |
||
| 845 | for i, hm_row in enumerate(self.heatmap_widget_grid): |
||
| 846 | heights = [] |
||
| 847 | for hm in hm_row: |
||
| 848 | hm_size = QSizeF(hm.heatmap_item.pixmap().size()) |
||
| 849 | hm_size = scaled( |
||
| 850 | hm_size, QSizeF(hm.size().width(), -1), |
||
| 851 | Qt.KeepAspectRatioByExpanding) |
||
| 852 | |||
| 853 | heights.append(hm_size.height()) |
||
| 854 | layout.setRowMaximumHeight(3 + i * 2 + 1, max(heights)) |
||
| 855 | layout.setRowPreferredHeight(3 + i * 2 + 1, max(heights)) |
||
| 856 | |||
| 857 | # set/update the widget's height |
||
| 858 | constraint = QSizeF(size.width(), -1) |
||
| 859 | sh = widget.effectiveSizeHint(Qt.PreferredSize, constraint) |
||
| 860 | minsize = widget.effectiveSizeHint(Qt.MinimumSize, constraint) |
||
| 861 | sh = sh.expandedTo(minsize).expandedTo(widget.minimumSize()) |
||
| 862 | |||
| 863 | # print("Resize 2", sh) |
||
| 864 | # print(" old:", widget.size().width(), widget.size().height()) |
||
| 865 | # print(" new:", widget.size().width(), sh.height()) |
||
| 866 | |||
| 867 | widget.resize(sh) |
||
| 868 | # print("Did resize") |
||
| 869 | self.__fixup_grid_layout() |
||
| 870 | |||
| 871 | def __fixup_grid_layout(self): |
||
| 872 | self.__update_margins() |
||
| 873 | rect = self.scene.widget.geometry() |
||
| 874 | self.heatmap_scene.setSceneRect(rect) |
||
| 875 | self.__update_selection_geometry() |
||
| 876 | |||
| 877 | def __aspect_mode_changed(self): |
||
| 878 | if self.keep_aspect: |
||
| 879 | policy = Qt.ScrollBarAlwaysOn |
||
| 880 | else: |
||
| 881 | policy = Qt.ScrollBarAlwaysOff |
||
| 882 | |||
| 883 | viewport = self.sceneView.viewport() |
||
| 884 | # Temp. remove the event filter so we won't process the resize twice |
||
| 885 | viewport.removeEventFilter(self) |
||
| 886 | self.sceneView.setVerticalScrollBarPolicy(policy) |
||
| 887 | self.sceneView.setHorizontalScrollBarPolicy(policy) |
||
| 888 | viewport.installEventFilter(self) |
||
| 889 | self.__update_size_constraints() |
||
| 890 | |||
| 891 | def eventFilter(self, reciever, event): |
||
| 892 | if reciever is self.sceneView.viewport() and \ |
||
| 893 | event.type() == QEvent.Resize: |
||
| 894 | self.__update_size_constraints() |
||
| 895 | |||
| 896 | return super().eventFilter(reciever, event) |
||
| 897 | |||
| 898 | def __update_margins(self): |
||
| 899 | """ |
||
| 900 | Update dendrogram and text list widgets margins to include the |
||
| 901 | space for average stripe. |
||
| 902 | """ |
||
| 903 | def offset(hm): |
||
| 904 | if hm.show_averages: |
||
| 905 | return hm.averages_item.size().width() |
||
| 906 | else: |
||
| 907 | return 0 |
||
| 908 | |||
| 909 | hm_row = self.heatmap_widget_grid[0] |
||
| 910 | hm_col = next(zip(*self.heatmap_widget_grid)) |
||
| 911 | dendrogram_col = self.col_dendrograms |
||
| 912 | dendrogram_row = self.row_dendrograms |
||
| 913 | |||
| 914 | col_annot = zip(self.col_annotation_widgets_top, |
||
| 915 | self.col_annotation_widgets_bottom) |
||
| 916 | row_annot = self.row_annotation_widgets |
||
| 917 | |||
| 918 | for hm, annot, dendrogram in zip(hm_row, col_annot, dendrogram_col): |
||
| 919 | width = hm.size().width() |
||
| 920 | left_offset = offset(hm) |
||
| 921 | col_count = hm.heatmap_data().shape[1] |
||
| 922 | half_col = (width - left_offset) / col_count / 2 |
||
| 923 | if dendrogram is not None: |
||
| 924 | _, top, _, bottom = dendrogram.getContentsMargins() |
||
| 925 | dendrogram.setContentsMargins( |
||
| 926 | left_offset + half_col, top, half_col, bottom) |
||
| 927 | |||
| 928 | _, top, right, bottom = annot[0].getContentsMargins() |
||
| 929 | annot[0].setContentsMargins(left_offset, top, right, bottom) |
||
| 930 | _, top, right, bottom = annot[1].getContentsMargins() |
||
| 931 | annot[1].setContentsMargins(left_offset, top, right, bottom) |
||
| 932 | |||
| 933 | for hm, annot, dendrogram in zip(hm_col, row_annot, dendrogram_row): |
||
| 934 | height = hm.size().height() |
||
| 935 | row_count = hm.heatmap_data().shape[0] |
||
| 936 | half_row = height / row_count / 2 |
||
| 937 | if dendrogram is not None: |
||
| 938 | left, _, right, _ = dendrogram.getContentsMargins() |
||
| 939 | dendrogram.setContentsMargins(left, half_row, right, half_row) |
||
| 940 | |||
| 941 | def heatmap_widgets(self): |
||
| 942 | """Iterate over heatmap widgets. |
||
| 943 | """ |
||
| 944 | for item in self.heatmap_scene.items(): |
||
| 945 | if isinstance(item, GraphicsHeatmapWidget): |
||
| 946 | yield item |
||
| 947 | |||
| 948 | def label_widgets(self): |
||
| 949 | """Iterate over GraphicsSimpleTextList widgets. |
||
| 950 | """ |
||
| 951 | for item in self.heatmap_scene.items(): |
||
| 952 | if isinstance(item, GraphicsSimpleTextList): |
||
| 953 | yield item |
||
| 954 | |||
| 955 | def dendrogram_widgets(self): |
||
| 956 | """Iterate over dendrogram widgets |
||
| 957 | """ |
||
| 958 | for item in self.heatmap_scene.items(): |
||
| 959 | if isinstance(item, DendrogramWidget): |
||
| 960 | yield item |
||
| 961 | |||
| 962 | def legend_widgets(self): |
||
| 963 | for item in self.heatmap_scene.items(): |
||
| 964 | if isinstance(item, GradientLegendWidget): |
||
| 965 | yield item |
||
| 966 | |||
| 967 | def update_averages_stripe(self): |
||
| 968 | """Update the visibility of the averages stripe. |
||
| 969 | """ |
||
| 970 | if self.data is not None: |
||
| 971 | for widget in self.heatmap_widgets(): |
||
| 972 | widget.set_show_averages(self.averages) |
||
| 973 | widget.layout().activate() |
||
| 974 | |||
| 975 | self.scene.widget.layout().activate() |
||
| 976 | self.__fixup_grid_layout() |
||
| 977 | |||
| 978 | def update_grid_spacing(self): |
||
| 979 | """Update layout spacing. |
||
| 980 | """ |
||
| 981 | if self.scene.widget: |
||
| 982 | layout = self.scene.widget.layout() |
||
| 983 | layout.setSpacing(self.SpaceX) |
||
| 984 | self.__fixup_grid_layout() |
||
| 985 | |||
| 986 | def update_color_schema(self): |
||
| 987 | palette = self.color_palette() |
||
| 988 | for heatmap in self.heatmap_widgets(): |
||
| 989 | heatmap.set_color_table(palette) |
||
| 990 | |||
| 991 | for legend in self.legend_widgets(): |
||
| 992 | legend.set_color_table(palette) |
||
| 993 | |||
| 994 | def update_sorting_examples(self): |
||
| 995 | if self.data: |
||
| 996 | self.update_heatmaps() |
||
| 997 | |||
| 998 | def update_sorting_attributes(self): |
||
| 999 | if self.data: |
||
| 1000 | self.update_heatmaps() |
||
| 1001 | |||
| 1002 | def update_legend(self): |
||
| 1003 | for item in self.heatmap_scene.items(): |
||
| 1004 | if isinstance(item, GradientLegendWidget): |
||
| 1005 | item.setVisible(self.legend) |
||
| 1006 | |||
| 1007 | def update_annotations(self): |
||
| 1008 | if self.data is not None: |
||
| 1009 | if self.annotation_vars: |
||
| 1010 | var = self.annotation_vars[self.annotation_index] |
||
| 1011 | if var == '(None)': |
||
| 1012 | var = None |
||
| 1013 | else: |
||
| 1014 | var = None |
||
| 1015 | |||
| 1016 | show = var is not None |
||
| 1017 | if show: |
||
| 1018 | annot_col, _ = self.data.get_column_view(var) |
||
| 1019 | else: |
||
| 1020 | annot_col = None |
||
| 1021 | |||
| 1022 | for labelslist in self.row_annotation_widgets: |
||
| 1023 | labelslist.setVisible(bool(show)) |
||
| 1024 | if show: |
||
| 1025 | indices = labelslist._indices |
||
| 1026 | data = annot_col[indices] |
||
| 1027 | labels = [var.str_val(val) for val in data] |
||
| 1028 | labelslist.set_labels(labels) |
||
| 1029 | |||
| 1030 | def update_column_annotations(self): |
||
| 1031 | if self.data is not None: |
||
| 1032 | show_top = self.column_label_pos & OWHeatMap.PositionTop |
||
| 1033 | show_bottom = self.column_label_pos & OWHeatMap.PositionBottom |
||
| 1034 | |||
| 1035 | for labelslist in self.col_annotation_widgets_top: |
||
| 1036 | labelslist.setVisible(show_top) |
||
| 1037 | |||
| 1038 | TopLabelsRow = 2 |
||
| 1039 | Row0 = 3 |
||
| 1040 | BottomLabelsRow = Row0 + 2 * len(self.heatmapparts.rows) |
||
| 1041 | |||
| 1042 | layout = self.heatmap_scene.widget.layout() |
||
| 1043 | layout.setRowMaximumHeight(TopLabelsRow, -1 if show_top else 0) |
||
| 1044 | layout.setRowSpacing(TopLabelsRow, -1 if show_top else 0) |
||
| 1045 | |||
| 1046 | for labelslist in self.col_annotation_widgets_bottom: |
||
| 1047 | labelslist.setVisible(show_bottom) |
||
| 1048 | |||
| 1049 | layout.setRowMaximumHeight(BottomLabelsRow, -1 if show_top else 0) |
||
| 1050 | |||
| 1051 | self.__fixup_grid_layout() |
||
| 1052 | |||
| 1053 | def __select_by_cluster(self, item, dendrogramindex): |
||
| 1054 | # User clicked on a dendrogram node. |
||
| 1055 | # Select all rows corresponding to the cluster item. |
||
| 1056 | node = item.node |
||
| 1057 | try: |
||
| 1058 | hm = self.heatmap_widget_grid[dendrogramindex][0] |
||
| 1059 | except IndexError: |
||
| 1060 | pass |
||
| 1061 | else: |
||
| 1062 | key = QtGui.QApplication.keyboardModifiers() |
||
| 1063 | clear = not (key & ((Qt.ControlModifier | Qt.ShiftModifier | |
||
| 1064 | Qt.AltModifier))) |
||
| 1065 | remove = (key & (Qt.ControlModifier | Qt.AltModifier)) |
||
| 1066 | append = (key & Qt.ControlModifier) |
||
| 1067 | self.selection_manager.selection_add( |
||
| 1068 | node.value.first, node.value.last - 1, hm, |
||
| 1069 | clear=clear, remove=remove, append=append) |
||
| 1070 | |||
| 1071 | def __update_selection_geometry(self): |
||
| 1072 | for item in self.selection_rects: |
||
| 1073 | item.setParentItem(None) |
||
| 1074 | self.heatmap_scene.removeItem(item) |
||
| 1075 | |||
| 1076 | self.selection_rects = [] |
||
| 1077 | self.selection_manager.update_selection_rects() |
||
| 1078 | rects = self.selection_manager.selection_rects |
||
| 1079 | for rect in rects: |
||
| 1080 | item = QtGui.QGraphicsRectItem(rect, None) |
||
| 1081 | pen = QPen(Qt.black, 2) |
||
| 1082 | pen.setCosmetic(True) |
||
| 1083 | item.setPen(pen) |
||
| 1084 | self.heatmap_scene.addItem(item) |
||
| 1085 | self.selection_rects.append(item) |
||
| 1086 | |||
| 1087 | def on_selection_finished(self): |
||
| 1088 | self.selected_rows = self.selection_manager.selections |
||
| 1089 | self.commit() |
||
| 1090 | |||
| 1091 | def commit(self): |
||
| 1092 | data = None |
||
| 1093 | if self.input_data is not None and self.selected_rows: |
||
| 1094 | sortind = np.hstack([labels._indices for labels in self.row_annotation_widgets]) |
||
| 1095 | indices = sortind[self.selected_rows] |
||
| 1096 | data = self.input_data[indices] |
||
| 1097 | |||
| 1098 | self.send("Selected Data", data) |
||
| 1099 | |||
| 1100 | def save_graph(self): |
||
| 1101 | from Orange.widgets.data.owsave import OWSave |
||
| 1102 | |||
| 1103 | save_img = OWSave(parent=self, data=self.scene, |
||
| 1104 | file_formats=FileFormats.img_writers) |
||
| 1105 | save_img.exec_() |
||
| 1106 | |||
| 1997 |
This can be caused by one of the following:
1. Missing Dependencies
This error could indicate a configuration issue of Pylint. Make sure that your libraries are available by adding the necessary commands.
2. Missing __init__.py files
This error could also result from missing
__init__.pyfiles in your module folders. Make sure that you place one file in each sub-folder.