def __updatePen(self): self.prepareGeometryChange() self.__boundingRect = None if self.__dynamic: if self.__dynamicEnabled: color = QColor(0, 150, 0, 150) else: color = QColor(150, 0, 0, 150) normal = QPen(QBrush(color), 2.0) hover = QPen(QBrush(color.darker(120)), 2.1) else: normal = QPen(QBrush(QColor("#9CACB4")), 2.0) hover = QPen(QBrush(QColor("#7D7D7D")), 2.1) if self.__state & LinkItem.Empty: pen_style = Qt.DashLine else: pen_style = Qt.SolidLine normal.setStyle(pen_style) hover.setStyle(pen_style) if self.hover: pen = hover else: pen = normal self.curveItem.setPen(pen)
def shape(self): if self.__shape is None: path = self.curvePath() pen = QPen(self.pen()) pen.setWidthF(max(pen.widthF(), 7.0)) pen.setStyle(Qt.SolidLine) self.__shape = stroke_path(path, pen) return self.__shape
def make_pen(brush=Qt.black, width=1, style=Qt.SolidLine, cap_style=Qt.SquareCap, join_style=Qt.BevelJoin, cosmetic=False): pen = QPen(brush) pen.setWidth(width) pen.setStyle(style) pen.setCapStyle(cap_style) pen.setJoinStyle(join_style) pen.setCosmetic(cosmetic) return pen
def __init__(self, parent): super().__init__(parent) self.__range = None # type: Tuple[float] self.__value_range = None # type: Tuple[float] self.__model_output = None # type: float self.__base_value = None # type: float self.__group = QGraphicsItemGroup(self) low_color, high_color = QColor(*RGB_LOW), QColor(*RGB_HIGH) self.__low_item = QGraphicsRectItem() self.__low_item.setPen(QPen(low_color)) self.__low_item.setBrush(QBrush(low_color)) self.__high_item = QGraphicsRectItem() self.__high_item.setPen(QPen(high_color)) self.__high_item.setBrush(QBrush(high_color)) self.__low_cover_item = LowCoverItem() self.__high_cover_item = HighCoverItem() pen = QPen(IndicatorItem.COLOR) pen.setStyle(Qt.DashLine) pen.setWidth(1) self.__model_output_line = QGraphicsLineItem() self.__model_output_line.setPen(pen) self.__base_value_line = QGraphicsLineItem() self.__base_value_line.setPen(pen) self.__model_output_ind = IndicatorItem("Model prediction: {}") self.__base_value_ind = IndicatorItem("Base value: {}\nThe average " "prediction for selected class.") self.__group.addToGroup(self.__low_item) self.__group.addToGroup(self.__high_item) self.__group.addToGroup(self.__low_cover_item) self.__group.addToGroup(self.__high_cover_item) self.__group.addToGroup(self.__model_output_line) self.__group.addToGroup(self.__base_value_line) self.__group.addToGroup(self.__model_output_ind) self.__group.addToGroup(self.__base_value_ind) self.__low_parts = [] # type: List[LowPartItem] self.__high_parts = [] # type: List[HighPartItem]
def update_pen(pen, brush=None, width=None, style=None, cap_style=None, join_style=None, cosmetic=None): pen = QPen(pen) if brush is not None: pen.setBrush(QBrush(brush)) if width is not None: pen.setWidth(width) if style is not None: pen.setStyle(style) if cap_style is not None: pen.setCapStyle(cap_style) if join_style is not None: pen.setJoinStyle(join_style) if cosmetic is not None: pen.setCosmetic(cosmetic) return pen
class GraphAttributes: """ Creates entire graph of explanations, paint function is the main one, it delegates painting of attributes to draw_attribute, header and scale are dealt with in draw_header_footer. Header is fixed in size. Parameters ---------- scene: QGraphicsScene scene to add elements to num_of_atr : int number of attributes to plot space: int space between columns with atr names, values offset_y : int distance between the line border of attribute box plot and the box itself rect_height : int height of a rectangle, representing score of the attribute """ def __init__(self, scene, num_of_atr=3, space=35, offset_y=10, rect_height=40): self.scene = scene self.num_of_atr = num_of_atr self.space = space self.graph_space = 80 self.offset_y = offset_y self.black_pen = QPen(Qt.black, 2) self.gray_pen = QPen(Qt.gray, 1) self.light_gray_pen = QPen(QColor("#DFDFDF"), 1) self.light_gray_pen.setStyle(Qt.DashLine) self.brush = QBrush(QColor(0x33, 0x88, 0xff, 0xc0)) self.blue_pen = QPen(QBrush(QColor(0x33, 0x00, 0xff)), 2) """placeholders""" self.rect_height = rect_height self.max_contrib = None self.atr_area_h = None self.atr_area_w = None self.scale = None def get_needed_offset(self, explanations): max_n = 0 word = "" max_v = 0 val = "" for e in explanations: if max_n < len(str(e._metas[0])): word = str(e._metas[0]) max_n = len(str(e._metas[0])) if max_v < len(str(e._metas[1])): val = str(e._metas[1]) max_v = len(str(e._metas[1])) w = QGraphicsSimpleTextItem(word, None) v = QGraphicsSimpleTextItem(val, None) return w.boundingRect().width(), v.boundingRect().width() def paint(self, wp, explanations=None, header_h=100): """ Coordinates drawing Parameters ---------- wp : QWidget current viewport explanations : Orange.data.table data table with name, value, score and error of attributes to plot header_h : int space to be left on the top and the bottom of graph for header and scale """ self.name_w, self.val_w = self.get_needed_offset(explanations) self.offset_left = self.space + self.name_w + \ self.space + self.val_w + self.graph_space self.offset_right = self.graph_space + 50 self.atr_area_h = wp.height() / 2 - header_h self.atr_area_w = (wp.width() - self.offset_left - self.offset_right) / 2 coords = self.split_boxes_area(self.atr_area_h, self.num_of_atr, header_h) self.max_contrib = np.max( abs(explanations.X[:, 0]) + explanations.X[:, 1]) self.unit = self.get_scale() unit_pixels = np.floor(self.atr_area_w / (self.max_contrib / self.unit)) self.scale = unit_pixels / self.unit self.draw_header_footer(wp, header_h, unit_pixels, coords[self.num_of_atr - 1], coords[0]) for y, e in zip(coords, explanations[:self.num_of_atr]): self.draw_attribute(y, atr_name=str(e._metas[0]), atr_val=str(e._metas[1]), atr_contrib=e._x[0], error=e._x[1]) def draw_header_footer(self, wp, header_h, unit_pixels, last_y, first_y, marking_len=15): """header""" max_x = self.max_contrib * self.scale atr_label = QGraphicsSimpleTextItem("Name", None) val_label = QGraphicsSimpleTextItem("Value", None) score_label = QGraphicsSimpleTextItem("Score", None) font = score_label.font() font.setBold(True) font.setPointSize(13) atr_label.setFont(font) val_label.setFont(font) score_label.setFont(font) white_pen = QPen(Qt.white, 3) fix = self.offset_left + self.atr_area_w self.place_left(val_label, -self.atr_area_h - header_h * 0.85) self.place_left_edge(atr_label, -self.atr_area_h - header_h * 0.85) self.place_right(score_label, -self.atr_area_h - header_h * 0.85) self.scene.addLine(-max_x + fix, -self.atr_area_h - header_h, max_x + fix, -self.atr_area_h - header_h, white_pen) """footer""" line_y = max(first_y + wp.height() + header_h / 2 - 10, last_y + header_h / 2 + self.rect_height) self.scene.addLine(-max_x + fix, line_y, max_x + fix, line_y, self.black_pen) previous = 0 recomended_d = 35 for i in range(0, int(self.max_contrib / self.unit) + 1): x = unit_pixels * i """grid lines""" self.scene.addLine(x + fix, first_y, x + fix, line_y, self.light_gray_pen) self.scene.addLine(-x + fix, first_y, -x + fix, line_y, self.light_gray_pen) self.scene.addLine(x + fix, line_y, x + fix, line_y + marking_len, self.black_pen) self.scene.addLine(-x + fix, line_y, -x + fix, line_y + marking_len, self.black_pen) """markings on the ruler""" if x + fix - previous > recomended_d: self.place_centered(self.format_marking(i * self.unit), x + fix, line_y + marking_len + 5) if x > 0: self.place_centered(self.format_marking(-i * self.unit), -x + fix, line_y + marking_len + 5) previous = x + fix def format_marking(self, x, places=2): return QGraphicsSimpleTextItem(str(round(x, places)), None) def get_scale(self): """figures out on what scale is max score (1, .1, .01) TESTING NEEDED, maybe something more elegant. """ if self.max_contrib > 10: return 10 elif self.max_contrib > 1: return 1 elif self.max_contrib > 0.1: return 0.1 else: return 0.01 def draw_attribute(self, y, atr_name, atr_val, atr_contrib, error): fix = (self.offset_left + self.atr_area_w) """vertical line where x = 0""" self.scene.addLine(0 + fix, y, 0 + fix, y + self.rect_height, self.black_pen) """borders""" self.scene.addLine(self.offset_left, y, fix + self.atr_area_w, y, self.gray_pen) self.scene.addLine(self.offset_left, y + self.rect_height, fix + self.atr_area_w, y + self.rect_height, self.gray_pen) if atr_name is not None and atr_val is not None and atr_contrib is not None: atr_contrib_x = atr_contrib * self.scale + fix error_x = error * self.scale padded_rect = self.rect_height - 2 * self.offset_y len_rec = 2 * error_x graphed_rect = QGraphicsRectItem(atr_contrib_x - error_x, y + self.offset_y, len_rec, padded_rect) graphed_rect.setBrush(self.brush) graphed_rect.setPen(QPen(Qt.NoPen)) self.scene.addItem(graphed_rect) """vertical line marks calculated contribution of attribute""" self.atr_line = self.scene.addLine( atr_contrib_x, y + self.offset_y + 2, atr_contrib_x, y + self.rect_height - self.offset_y - 2, self.blue_pen) """atr name and value on the left""" self.place_left(QGraphicsSimpleTextItem(atr_val, None), y + self.rect_height / 2) self.place_left_edge(QGraphicsSimpleTextItem(atr_name, None), y + self.rect_height / 2) """atr score on the right""" self.place_right(self.format_marking(atr_contrib), y + self.rect_height / 2) def place_left(self, text, y): """places text to the left""" self.place_centered(text, 2 * self.space + self.name_w + self.val_w / 2, y) def place_left_edge(self, text, y): """places text more left than place_left""" self.scene.addLine(0, y, 0 - 10, y + 2, QPen(Qt.white, 0)) self.place_centered(text, self.space + self.name_w / 2, y) def place_right(self, text, y): x = self.offset_left + 2 * self.atr_area_w + self.graph_space + 15 self.scene.addLine(x, y, x, y + 2, QPen(Qt.white, 0)) self.place_centered( text, self.offset_left + 2 * self.atr_area_w + self.graph_space, y) def place_centered(self, text, x, y): """centers the text around given coordinates""" to_center = text.boundingRect().width() / 2 text.setPos(x - to_center, y) self.scene.addItem(text) def split_boxes_area(self, h, num_boxes, header_h): """calculates y coordinates of boxes to be plotted, calculates rect_height Parameters --------- h : int height of area num_boxes : int number of boxes to fill our area header_h : int height of header Returns: list y_coordinates """ return [(-h + i * self.rect_height) for i in range(num_boxes)]
class GraphAttributes: """ Creates entire graph of explanations, paint function is the main one, it delegates painting of attributes to draw_attribute, header and scale are dealt with in draw_header_footer. Header is fixed in size. Parameters ---------- scene: QGraphicsScene scene to add elements to num_of_atr : int number of attributes to plot space: int space between columns with atr names, values offset_y : int distance between the line border of attribute box plot and the box itself rect_height : int height of a rectangle, representing score of the attribute """ def __init__(self, scene, num_of_atr=3, space=35, offset_y=10, rect_height=40): self.scene = scene self.num_of_atr = num_of_atr self.space = space self.graph_space = 80 self.offset_y = offset_y self.black_pen = QPen(Qt.black, 2) self.gray_pen = QPen(Qt.gray, 1) self.light_gray_pen = QPen(QColor("#DFDFDF"), 1) self.light_gray_pen.setStyle(Qt.DashLine) self.brush = QBrush(QColor(0x33, 0x88, 0xff, 0xc0)) self.blue_pen = QPen(QBrush(QColor(0x33, 0x00, 0xff)), 2) """placeholders""" self.rect_height = rect_height self.max_contrib = None self.atr_area_h = None self.atr_area_w = None self.scale = None def get_needed_offset(self, explanations): max_n = 0 word = "" max_v = 0 val = "" for e in explanations: if max_n < len(str(e._metas[0])): word = str(e._metas[0]) max_n = len(str(e._metas[0])) if max_v < len(str(e._metas[1])): val = str(e._metas[1]) max_v = len(str(e._metas[1])) w = QGraphicsSimpleTextItem(word, None) v = QGraphicsSimpleTextItem(val, None) return w.boundingRect().width(), v.boundingRect().width() def paint(self, wp, explanations=None, header_h=100): """ Coordinates drawing Parameters ---------- wp : QWidget current viewport explanations : Orange.data.table data table with name, value, score and error of attributes to plot header_h : int space to be left on the top and the bottom of graph for header and scale """ self.name_w, self.val_w = self.get_needed_offset(explanations) self.offset_left = self.space + self.name_w + \ self.space + self.val_w + self.graph_space self.offset_right = self.graph_space + 50 self.atr_area_h = wp.height()/2 - header_h self.atr_area_w = (wp.width() - self.offset_left - self.offset_right) / 2 coords = self.split_boxes_area( self.atr_area_h, self.num_of_atr, header_h) self.max_contrib = np.max( abs(explanations.X[:, 0]) + explanations.X[:, 1]) self.unit = self.get_scale() unit_pixels = np.floor(self.atr_area_w/(self.max_contrib/self.unit)) self.scale = unit_pixels / self.unit self.draw_header_footer( wp, header_h, unit_pixels, coords[self.num_of_atr - 1], coords[0]) for y, e in zip(coords, explanations[:self.num_of_atr]): self.draw_attribute(y, atr_name=str(e._metas[0]), atr_val=str( e._metas[1]), atr_contrib=e._x[0], error=e._x[1]) def draw_header_footer(self, wp, header_h, unit_pixels, last_y, first_y, marking_len=15): """header""" max_x = self.max_contrib * self.scale atr_label = QGraphicsSimpleTextItem("Name", None) val_label = QGraphicsSimpleTextItem("Value", None) score_label = QGraphicsSimpleTextItem("Score", None) font = score_label.font() font.setBold(True) font.setPointSize(13) atr_label.setFont(font) val_label.setFont(font) score_label.setFont(font) white_pen = QPen(Qt.white, 3) fix = self.offset_left + self.atr_area_w self.place_left(val_label, -self.atr_area_h - header_h*0.85) self.place_left_edge(atr_label, -self.atr_area_h - header_h*0.85) self.place_right(score_label, -self.atr_area_h - header_h*0.85) self.scene.addLine(-max_x + fix, -self.atr_area_h - header_h, max_x + fix, -self.atr_area_h - header_h, white_pen) """footer""" line_y = max(first_y + wp.height() + header_h/2 - 10, last_y + header_h/2 + self.rect_height) self.scene.addLine(-max_x + fix, line_y, max_x + fix, line_y, self.black_pen) previous = 0 recomended_d = 35 for i in range(0, int(self.max_contrib / self.unit) + 1): x = unit_pixels * i """grid lines""" self.scene.addLine(x + fix, first_y, x + fix, line_y, self.light_gray_pen) self.scene.addLine(-x + fix, first_y, -x + fix, line_y, self.light_gray_pen) self.scene.addLine(x + fix, line_y, x + fix, line_y + marking_len, self.black_pen) self.scene.addLine(-x + fix, line_y, -x + fix, line_y + marking_len, self.black_pen) """markings on the ruler""" if x + fix - previous > recomended_d: self.place_centered(self.format_marking( i*self.unit), x + fix, line_y + marking_len + 5) if x > 0: self.place_centered( self.format_marking(-i*self.unit), -x + fix, line_y + marking_len + 5) previous = x + fix def format_marking(self, x, places=2): return QGraphicsSimpleTextItem(str(round(x, places)), None) def get_scale(self): """figures out on what scale is max score (1, .1, .01) TESTING NEEDED, maybe something more elegant. """ if self.max_contrib > 10: return 10 elif self.max_contrib > 1: return 1 elif self.max_contrib > 0.1: return 0.1 else: return 0.01 def draw_attribute(self, y, atr_name, atr_val, atr_contrib, error): fix = (self.offset_left + self.atr_area_w) """vertical line where x = 0""" self.scene.addLine(0 + fix, y, 0 + fix, y + self.rect_height, self.black_pen) """borders""" self.scene.addLine(self.offset_left, y, fix + self.atr_area_w, y, self.gray_pen) self.scene.addLine(self.offset_left, y + self.rect_height, fix + self.atr_area_w, y + self.rect_height, self.gray_pen) if atr_name is not None and atr_val is not None and atr_contrib is not None: atr_contrib_x = atr_contrib * self.scale + fix error_x = error * self.scale padded_rect = self.rect_height - 2 * self.offset_y len_rec = 2 * error_x graphed_rect = QGraphicsRectItem( atr_contrib_x - error_x, y + self.offset_y, len_rec, padded_rect) graphed_rect.setBrush(self.brush) graphed_rect.setPen(QPen(Qt.NoPen)) self.scene.addItem(graphed_rect) """vertical line marks calculated contribution of attribute""" self.atr_line = self.scene.addLine(atr_contrib_x, y + self.offset_y + 2, atr_contrib_x, y + self.rect_height - self.offset_y - 2, self.blue_pen) """atr name and value on the left""" self.place_left(QGraphicsSimpleTextItem( atr_val, None), y + self.rect_height/2) self.place_left_edge(QGraphicsSimpleTextItem( atr_name, None), y + self.rect_height/2) """atr score on the right""" self.place_right(self.format_marking( atr_contrib), y + self.rect_height/2) def place_left(self, text, y): """places text to the left""" self.place_centered(text, 2 * self.space + self.name_w + self.val_w/2, y) def place_left_edge(self, text, y): """places text more left than place_left""" self.scene.addLine(0, y, 0 - 10, y + 2, QPen(Qt.white, 0)) self.place_centered(text, self.space + self.name_w/2, y) def place_right(self, text, y): x = self.offset_left + 2 * self.atr_area_w + self.graph_space + 15 self.scene.addLine(x, y, x, y + 2, QPen(Qt.white, 0)) self.place_centered(text, self.offset_left + 2 * self.atr_area_w + self.graph_space, y) def place_centered(self, text, x, y): """centers the text around given coordinates""" to_center = text.boundingRect().width()/2 text.setPos(x - to_center, y) self.scene.addItem(text) def split_boxes_area(self, h, num_boxes, header_h): """calculates y coordinates of boxes to be plotted, calculates rect_height Parameters --------- h : int height of area num_boxes : int number of boxes to fill our area header_h : int height of header Returns: list y_coordinates """ return [(-h + i*self.rect_height) for i in range(num_boxes)]