def __updateView(self, view: QGraphicsView, rect: QRectF) -> None: view.setSceneRect(rect) viewrect = view.mapFromScene(rect).boundingRect() view.setFixedHeight(int(math.ceil(viewrect.height()))) container = view.parent() if rect.isEmpty(): container.setVisible(False) return # map the rect to (main) viewport coordinates viewrect = qgraphicsview_map_rect_from_scene(self, rect).boundingRect() viewrect = qrectf_to_inscribed_rect(viewrect) viewportrect = self.viewport().rect() visible = (viewrect.top() < viewportrect.top() or viewrect.y() + viewrect.height() > viewportrect.y() + viewportrect.height()) container.setVisible(visible) # force immediate layout of the container overlay QCoreApplication.sendEvent(container, QEvent(QEvent.LayoutRequest))
class OWExplainPrediction(OWWidget, ConcurrentWidgetMixin): name = "Explain Prediction" description = "Prediction explanation widget." icon = "icons/ExplainPred.svg" priority = 110 class Inputs: model = Input("Model", Model) background_data = Input("Background Data", Table) data = Input("Data", Table) class Outputs: scores = Output("Scores", Table) class Error(OWWidget.Error): domain_transform_err = Msg("{}") unknown_err = Msg("{}") class Information(OWWidget.Information): multiple_instances = Msg("Explaining prediction for the first " "instance in 'Data'.") settingsHandler = ClassValuesContextHandler() target_index = ContextSetting(0) stripe_len = Setting(10) graph_name = "scene" def __init__(self): OWWidget.__init__(self) ConcurrentWidgetMixin.__init__(self) self.__results = None # type: Optional[Results] self.model = None # type: Optional[Model] self.background_data = None # type: Optional[Table] self.data = None # type: Optional[Table] self._stripe_plot = None # type: Optional[StripePlot] self.mo_info = "" self.bv_info = "" self.setup_gui() def setup_gui(self): self._add_controls() self._add_plot() self.info.set_input_summary(self.info.NoInput) def _add_plot(self): self.scene = QGraphicsScene() self.view = QGraphicsView(self.scene) self.view.setRenderHint(QPainter.Antialiasing, True) self.view.setAlignment(Qt.AlignVCenter | Qt.AlignLeft) self.mainArea.layout().addWidget(self.view) def _add_controls(self): box = gui.vBox(self.controlArea, "Target class") self._target_combo = gui.comboBox(box, self, "target_index", callback=self.__target_combo_changed, contentsLength=12) box = gui.hBox(self.controlArea, "Zoom") gui.hSlider(box, self, "stripe_len", None, minValue=1, maxValue=500, createLabel=False, callback=self.__size_slider_changed) gui.rubber(self.controlArea) box = gui.vBox(self.controlArea, "Prediction info") gui.label(box, self, "%(mo_info)s") # type: QLabel bv_label = gui.label(box, self, "%(bv_info)s") # type: QLabel bv_label.setToolTip("The average prediction for selected class.") def __target_combo_changed(self): self.update_scene() def __size_slider_changed(self): if self._stripe_plot is not None: self._stripe_plot.set_height(self.stripe_len) @Inputs.data @check_sql_input def set_data(self, data: Optional[Table]): self.data = data @Inputs.background_data @check_sql_input def set_background_data(self, data: Optional[Table]): self.background_data = data @Inputs.model def set_model(self, model: Optional[Model]): self.closeContext() self.model = model self.setup_controls() self.openContext(self.model.domain.class_var if self.model else None) def setup_controls(self): self._target_combo.clear() self._target_combo.setEnabled(True) if self.model is not None: if self.model.domain.has_discrete_class: self._target_combo.addItems(self.model.domain.class_var.values) self.target_index = 0 elif self.model.domain.has_continuous_class: self.target_index = -1 self._target_combo.setEnabled(False) else: raise NotImplementedError def handleNewSignals(self): self.clear() self.check_inputs() data = self.data and self.data[:1] self.start(run, data, self.background_data, self.model) def clear(self): self.mo_info = "" self.bv_info = "" self.__results = None self.cancel() self.clear_scene() self.clear_messages() def check_inputs(self): if self.data and len(self.data) > 1: self.Information.multiple_instances() summary, details, kwargs = self.info.NoInput, "", {} if self.data or self.background_data: n_data = len(self.data) if self.data else 0 n_background_data = len(self.background_data) \ if self.background_data else 0 summary = f"{self.info.format_number(n_background_data)}, " \ f"{self.info.format_number(n_data)}" kwargs = {"format": Qt.RichText} details = format_multiple_summaries([("Background data", self.background_data), ("Data", self.data)]) self.info.set_input_summary(summary, details, **kwargs) def clear_scene(self): self.scene.clear() self.scene.setSceneRect(QRectF()) self.view.setSceneRect(QRectF()) self._stripe_plot = None def update_scene(self): self.clear_scene() self.mo_info = "" self.bv_info = "" scores = None if self.__results is not None: data = self.__results.transformed_data pred = self.__results.predictions base = self.__results.base_value values, _, labels, ranges = prepare_force_plot_data( self.__results.values, data, pred, self.target_index) index = 0 HIGH, LOW = 0, 1 plot_data = PlotData(high_values=values[index][HIGH], low_values=values[index][LOW][::-1], high_labels=labels[index][HIGH], low_labels=labels[index][LOW][::-1], value_range=ranges[index], model_output=pred[index][self.target_index], base_value=base[self.target_index]) self.setup_plot(plot_data) self.mo_info = f"Model prediction: {_str(plot_data.model_output)}" self.bv_info = f"Base value: {_str(plot_data.base_value)}" assert isinstance(self.__results.values, list) scores = self.__results.values[self.target_index][0, :] names = [a.name for a in data.domain.attributes] scores = self.create_scores_table(scores, names) self.Outputs.scores.send(scores) def setup_plot(self, plot_data: PlotData): self._stripe_plot = StripePlot() self._stripe_plot.set_data(plot_data, self.stripe_len) self._stripe_plot.layout().activate() self._stripe_plot.geometryChanged.connect(self.update_scene_rect) self.scene.addItem(self._stripe_plot) self.update_scene_rect() def update_scene_rect(self): geom = self._stripe_plot.geometry() self.scene.setSceneRect(geom) self.view.setSceneRect(geom) @staticmethod def create_scores_table(scores: np.ndarray, names: List[str]) -> Table: domain = Domain([ContinuousVariable("Score")], metas=[StringVariable("Feature")]) scores_table = Table(domain, scores[:, None], metas=np.array(names)[:, None]) scores_table.name = "Feature Scores" return scores_table def on_partial_result(self, _): pass def on_done(self, results: Optional[RunnerResults]): self.__results = results self.update_scene() def on_exception(self, ex: Exception): if isinstance(ex, DomainTransformationError): self.Error.domain_transform_err(ex) else: self.Error.unknown_err(ex) def onDeleteWidget(self): self.shutdown() super().onDeleteWidget() def sizeHint(self) -> QSizeF: sh = self.controlArea.sizeHint() return sh.expandedTo(QSize(700, 700)) def send_report(self): if not self.data or not self.background_data or not self.model: return items = {"Target class": "None"} if self.model.domain.has_discrete_class: class_var = self.model.domain.class_var items["Target class"] = class_var.values[self.target_index] self.report_items(items) self.report_plot()
class EditLinksDialog(QDialog): """ A dialog for editing links. >>> dlg = EditLinksDialog() >>> dlg.setNodes(source_node, sink_node) >>> dlg.setLinks([(source_node.output_channel("Data"), ... sink_node.input_channel("Data"))]) >>> if dlg.exec_() == EditLinksDialog.Accepted: ... new_links = dlg.links() ... """ def __init__(self, parent=None, **kwargs): # type: (Optional[QWidget], Any) -> None super().__init__(parent, **kwargs) self.setModal(True) self.__setupUi() def __setupUi(self): layout = QVBoxLayout() # Scene with the link editor. self.scene = LinksEditScene() self.view = QGraphicsView(self.scene) self.view.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) self.view.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) self.view.setRenderHint(QPainter.Antialiasing) self.scene.editWidget.geometryChanged.connect(self.__onGeometryChanged) # Ok/Cancel/Clear All buttons. buttons = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel | QDialogButtonBox.Reset, Qt.Horizontal) clear_button = buttons.button(QDialogButtonBox.Reset) clear_button.setText(self.tr("Clear All")) buttons.accepted.connect(self.accept) buttons.rejected.connect(self.reject) clear_button.clicked.connect(self.scene.editWidget.clearLinks) layout.addWidget(self.view) layout.addWidget(buttons) self.setLayout(layout) layout.setSizeConstraint(QVBoxLayout.SetFixedSize) self.setSizeGripEnabled(False) def setNodes(self, source_node, sink_node): # type: (SchemeNode, SchemeNode) -> None """ Set the source/sink nodes (:class:`.SchemeNode` instances) between which to edit the links. .. note:: This should be called before :func:`setLinks`. """ self.scene.editWidget.setNodes(source_node, sink_node) def setLinks(self, links): # type: (List[IOPair]) -> None """ Set a list of links to display between the source and sink nodes. The `links` is a list of (`OutputSignal`, `InputSignal`) tuples where the first element is an output signal of the source node and the second an input signal of the sink node. """ self.scene.editWidget.setLinks(links) def links(self): # type: () -> List[IOPair] """ Return the links between the source and sink node. """ return self.scene.editWidget.links() def __onGeometryChanged(self): size = self.scene.editWidget.size() left, top, right, bottom = self.getContentsMargins() self.view.setFixedSize(size.toSize() + \ QSize(left + right + 4, top + bottom + 4)) self.view.setSceneRect(self.scene.editWidget.geometry())
class EditLinksDialog(QDialog): """ A dialog for editing links. >>> dlg = EditLinksDialog() >>> dlg.setNodes(file_node, test_learners_node) >>> dlg.setLinks([(file_node.output_channel("Data"), ... (test_learners_node.input_channel("Data")]) >>> if dlg.exec_() == EditLinksDialog.Accpeted: ... new_links = dlg.links() ... """ def __init__(self, *args, **kwargs): QDialog.__init__(self, *args, **kwargs) self.setModal(True) self.__setupUi() def __setupUi(self): layout = QVBoxLayout() # Scene with the link editor. self.scene = LinksEditScene() self.view = QGraphicsView(self.scene) self.view.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) self.view.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) self.view.setRenderHint(QPainter.Antialiasing) self.scene.editWidget.geometryChanged.connect(self.__onGeometryChanged) # Ok/Cancel/Clear All buttons. buttons = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel | QDialogButtonBox.Reset, Qt.Horizontal) clear_button = buttons.button(QDialogButtonBox.Reset) clear_button.setText(self.tr("Clear All")) buttons.accepted.connect(self.accept) buttons.rejected.connect(self.reject) clear_button.clicked.connect(self.scene.editWidget.clearLinks) layout.addWidget(self.view) layout.addWidget(buttons) self.setLayout(layout) layout.setSizeConstraint(QVBoxLayout.SetFixedSize) self.setSizeGripEnabled(False) def setNodes(self, source_node, sink_node): """ Set the source/sink nodes (:class:`~Orange.canvas.scheme.SchemeNode` instances) between which to edit the links. .. note:: This should be called before :func:`setLinks`. """ self.scene.editWidget.setNodes(source_node, sink_node) def setLinks(self, links): """ Set a list of links to display between the source and sink nodes. The `links` is a list of (`OutputSignal`, `InputSignal`) tuples where the first element is an output signal of the source node and the second an input signal of the sink node. """ self.scene.editWidget.setLinks(links) def links(self): """ Return the links between the source and sink node. """ return self.scene.editWidget.links() def __onGeometryChanged(self): size = self.scene.editWidget.size() left, top, right, bottom = self.getContentsMargins() self.view.setFixedSize(size.toSize() + \ QSize(left + right + 4, top + bottom + 4)) self.view.setSceneRect(self.scene.editWidget.geometry())