diff --git a/Orange/widgets/visualize/owmosaic.py b/Orange/widgets/visualize/owmosaic.py index 8616d85c1e1..c6e176ac2c5 100644 --- a/Orange/widgets/visualize/owmosaic.py +++ b/Orange/widgets/visualize/owmosaic.py @@ -1,6 +1,6 @@ from collections import defaultdict from functools import reduce -from itertools import product, chain +from itertools import product, chain, repeat from math import sqrt, log from operator import mul, attrgetter @@ -9,7 +9,8 @@ from scipy.special import comb from AnyQt.QtCore import Qt, QSize, pyqtSignal as Signal from AnyQt.QtGui import QColor, QPainter, QPen, QStandardItem -from AnyQt.QtWidgets import QGraphicsScene, QGraphicsLineItem +from AnyQt.QtWidgets import ( + QGraphicsScene, QGraphicsLineItem, QGraphicsItemGroup) from Orange.data import Table, filter, Variable, Domain from Orange.data.sql.table import SqlTable, LARGE_TABLE, DEFAULT_SAMPLE_TIME @@ -28,6 +29,7 @@ from Orange.widgets.utils.widgetpreview import WidgetPreview from Orange.widgets.visualize.utils import ( CanvasText, CanvasRectangle, ViewWithPress, VizRankDialog) +from Orange.widgets.visualize.utils.plotutils import wrap_legend_items from Orange.widgets.widget import OWWidget, Msg, Input, Output @@ -813,42 +815,29 @@ def line(x1, y1, x2, y2): "{}
Instances: {}

{}".format( condition, n_actual, text[:-4])) - def draw_legend(x0_x1, y0_y1): - x0, x1 = x0_x1 - _, y1 = y0_y1 + def create_legend(): if self.variable_color is None: names = ["<-8", "-8:-4", "-4:-2", "-2:2", "2:4", "4:8", ">8", "Residuals:"] colors = self.RED_COLORS[::-1] + self.BLUE_COLORS[1:] + edges = repeat(Qt.black) else: - names = get_variable_values_sorted(class_var) + \ - [class_var.name + ":"] - colors = [QColor(*col) for col in class_var.colors] - - names = [CanvasText(self.canvas, name, alignment=Qt.AlignVCenter) - for name in names] - totalwidth = sum(text.boundingRect().width() for text in names) - - # compute the x position of the center of the legend - y = y1 + self.ATTR_NAME_OFFSET + self.ATTR_VAL_OFFSET + 35 - distance = 30 - startx = (x0 + x1) / 2 - (totalwidth + (len(names)) * distance) / 2 - - names[-1].setPos(startx + 15, y) - names[-1].show() - xoffset = names[-1].boundingRect().width() + distance + names = get_variable_values_sorted(class_var) + edges = colors = [QColor(*col) for col in class_var.colors] + items = [] size = 8 - for i in range(len(names) - 1): - if self.variable_color is None: - edgecolor = Qt.black - else: - edgecolor = colors[i] - - CanvasRectangle(self.canvas, startx + xoffset, y - size / 2, - size, size, edgecolor, colors[i]) - names[i].setPos(startx + xoffset + 10, y) - xoffset += distance + names[i].boundingRect().width() + for name, color, edgecolor in zip(names, colors, edges): + item = QGraphicsItemGroup() + item.addToGroup( + CanvasRectangle(None, -size / 2, -size / 2, size, size, + edgecolor, color)) + item.addToGroup( + CanvasText(None, name, size, 0, Qt.AlignVCenter)) + items.append(item) + return wrap_legend_items( + items, hspacing=20, vspacing=16 + size, + max_width=self.canvas_view.width() - 2 * xoff) self.canvas.clear() self.areas = [] @@ -896,8 +885,10 @@ def get_max_label_width(attr): maxw = max(int(t.boundingRect().width()), maxw) return maxw - # get the maximum width of rectangle xoff = 20 + legend = create_legend() + + # get the maximum width of rectangle width = 20 max_ylabel_w1 = max_ylabel_w2 = 0 if len(attr_list) > 1: @@ -913,10 +904,11 @@ def get_max_label_width(attr): self.ATTR_VAL_OFFSET + max_ylabel_w2 - 10 # get the maximum height of rectangle - height = 100 yoff = 45 + legendoff = yoff + self.ATTR_NAME_OFFSET + self.ATTR_VAL_OFFSET + 35 square_size = min(self.canvas_view.width() - width - 20, - self.canvas_view.height() - height - 20) + self.canvas_view.height() - legendoff + - legend.boundingRect().height()) if square_size < 0: return # canvas is too small to draw rectangles @@ -937,7 +929,12 @@ def get_max_label_width(attr): draw_data( attr_list, (xoff, xoff + square_size), (yoff, yoff + square_size), 0, "", len(attr_list), [], []) - draw_legend((xoff, xoff + square_size), (yoff, yoff + square_size)) + + self.canvas.addItem(legend) + legend.setPos( + xoff - legend.boundingRect().x() + + max(0, (square_size - legend.boundingRect().width()) / 2), + legendoff + square_size) self.update_selection_rects() @classmethod diff --git a/Orange/widgets/visualize/tests/test_owmosaic.py b/Orange/widgets/visualize/tests/test_owmosaic.py index cd7fab428d8..b9aaa08c50a 100644 --- a/Orange/widgets/visualize/tests/test_owmosaic.py +++ b/Orange/widgets/visualize/tests/test_owmosaic.py @@ -98,7 +98,8 @@ def assertCount(cb_color, cb_attr, areas): assertCount(1, [0, 1, 1, 1], 0) @patch('Orange.widgets.visualize.owmosaic.CanvasRectangle') - def test_different_number_of_attributes(self, canvas_rectangle): + @patch('Orange.widgets.visualize.owmosaic.QGraphicsItemGroup.addToGroup') + def test_different_number_of_attributes(self, _, canvas_rectangle): domain = Domain([DiscreteVariable(c, values="01") for c in "abcd"]) data = Table.from_list( domain, diff --git a/Orange/widgets/visualize/utils/plotutils.py b/Orange/widgets/visualize/utils/plotutils.py index 106b9ccbf62..8b7384dfa9f 100644 --- a/Orange/widgets/visualize/utils/plotutils.py +++ b/Orange/widgets/visualize/utils/plotutils.py @@ -5,8 +5,8 @@ ) from AnyQt.QtGui import QTransform from AnyQt.QtWidgets import ( - QGraphicsLineItem, QGraphicsSceneMouseEvent, QPinchGesture -) + QGraphicsLineItem, QGraphicsSceneMouseEvent, QPinchGesture, + QGraphicsItemGroup) import pyqtgraph as pg @@ -348,3 +348,41 @@ def mouseDragEvent(self, ev, axis=None): else: self.moved.emit(self.item_id, pos.x(), pos.y()) self.graph.show_indicator(self.item_id) + + +def wrap_legend_items(items, max_width, hspacing, vspacing): + def line_width(line): + return sum(item.boundingRect().width() for item in line) \ + + hspacing * (len(line) - 1) + + def create_line(line, yi, fixed_width=None): + x = 0 + for item in line: + item.setPos(x, yi * vspacing) + paragraph.addToGroup(item) + if fixed_width: + x += fixed_width + else: + x += item.boundingRect().width() + hspacing + + max_item = max(item.boundingRect().width() + hspacing for item in items) + in_line = int(max_width // max_item) + if line_width(items) < max_width: # single line + lines = [items] + fixed_width = None + elif in_line < 2: + lines = [[]] + for i, item in enumerate(items): # just a single column - free wrap + lines[-1].append(item) + if line_width(lines[-1]) > max_width and len(lines[-1]) > 1: + lines.append([lines[-1].pop()]) + fixed_width = None + else: # arrange into grid + lines = [items[i:i + in_line] + for i in range(0, len(items) + in_line - 1, in_line)] + fixed_width = max_item + + paragraph = QGraphicsItemGroup() + for yi, line in enumerate(lines): + create_line(line, yi, fixed_width=fixed_width) + return paragraph