''' Scatterplot 3D icons/ScatterPlot.svg 2001 ''' from math import log10, ceil, floor from OWWidget import * from plot.owplot3d import * from plot.owtheme import ScatterLightTheme, ScatterDarkTheme from plot import OWPoint import orange Discrete = orange.VarTypes.Discrete Continuous = orange.VarTypes.Continuous from Orange.data.preprocess.scaling import get_variable_values_sorted import OWGUI import orngVizRank from OWkNNOptimization import * from orngScaleScatterPlotData import * import numpy TooltipKind = enum('NONE', 'VISIBLE', 'ALL') Axis = enum('X', 'Y', 'Z') class Plane: '''Internal convenience class.''' def __init__(self, A, B, C, D): self.A = A self.B = B self.C = C self.D = D def normal(self): v1 = self.A - self.B v2 = self.A - self.C return QVector3D.crossProduct(v1, v2).normalized() def visible_from(self, location): normal = self.normal() loc_plane = (self.A - location).normalized() if QVector3D.dotProduct(normal, loc_plane) > 0: return False return True class Edge: def __init__(self, v0, v1): self.v0 = v0 self.v1 = v1 def __add__(self, vec): return Edge(self.v0 + vec, self.v1 + vec) def nicenum(x, round): if x <= 0.: return x # TODO: what to do in such cases? expv = floor(log10(x)) f = x / pow(10., expv) if round: if f < 1.5: nf = 1. elif f < 3.: nf = 2. elif f < 7.: nf = 5. else: nf = 10. else: if f <= 1.: nf = 1. elif f <= 2.: nf = 2. elif f <= 5.: nf = 5. else: nf = 10. return nf * pow(10., expv) def loose_label(min_value, max_value, num_ticks): '''Algorithm by Paul S. Heckbert (Graphics Gems). Generates a list of "nice" values between min and max, given the number of ticks. Also returns the number of fractional digits to use. ''' range = nicenum(max_value-min_value, False) d = nicenum(range / float(num_ticks-1), True) if d <= 0.: # TODO return numpy.arange(min_value, max_value, (max_value-min_value)/num_ticks), 1 plot_min = floor(min_value / d) * d plot_max = ceil(max_value / d) * d num_frac = int(max(-floor(log10(d)), 0)) return numpy.arange(plot_min, plot_max + 0.5*d, d), num_frac class ScatterPlot(OWPlot3D, orngScaleScatterPlotData): def __init__(self, parent=None): self.parent = parent OWPlot3D.__init__(self, parent) orngScaleScatterPlotData.__init__(self) self._theme = ScatterLightTheme() self.show_grid = True self.show_chassis = True self.show_axes = True self._build_axes() self._x_axis_labels = None self._y_axis_labels = None self._z_axis_labels = None self._x_axis_title = '' self._y_axis_title = '' self._z_axis_title = '' # These are public self.show_x_axis_title = self.show_y_axis_title = self.show_z_axis_title = True self.animate_plot = False def set_data(self, data, subset_data=None, **args): if data == None: return args['skipIfSame'] = False orngScaleScatterPlotData.set_data(self, data, subset_data, **args) # Optimization: calling set_plot_data here (and not in update_data) because data won't change. OWPlot3D.set_plot_data(self, self.scaled_data, self.scaled_subset_data) OWPlot3D.initializeGL(self) def update_data(self, x_attr, y_attr, z_attr, color_attr, symbol_attr, size_attr, label_attr): if self.data == None: return self.before_draw_callback = self.before_draw color_discrete = size_discrete = False color_index = -1 if color_attr != '' and color_attr != '(Same color)': color_index = self.attribute_name_index[color_attr] if self.data_domain[color_attr].varType == Discrete: color_discrete = True self.discrete_palette.setNumberOfColors(len(self.data_domain[color_attr].values)) symbol_index = -1 num_symbols_used = -1 if symbol_attr != '' and symbol_attr != 'Same symbol)' and\ len(self.data_domain[symbol_attr].values) < len(Symbol) and\ self.data_domain[symbol_attr].varType == Discrete: symbol_index = self.attribute_name_index[symbol_attr] num_symbols_used = len(self.data_domain[symbol_attr].values) size_index = -1 if size_attr != '' and size_attr != '(Same size)': size_index = self.attribute_name_index[size_attr] if self.data_domain[size_attr].varType == Discrete: size_discrete = True label_index = -1 if label_attr != '' and label_attr != '(No labels)': label_index = self.attribute_name_index[label_attr] x_index = self.attribute_name_index[x_attr] y_index = self.attribute_name_index[y_attr] z_index = self.attribute_name_index[z_attr] x_discrete = self.data_domain[x_attr].varType == Discrete y_discrete = self.data_domain[y_attr].varType == Discrete z_discrete = self.data_domain[z_attr].varType == Discrete colors = [] if color_discrete: for i in range(len(self.data_domain[color_attr].values)): c = self.discrete_palette[i] colors.append(c) data_scale = [self.attr_values[x_attr][1] - self.attr_values[x_attr][0], self.attr_values[y_attr][1] - self.attr_values[y_attr][0], self.attr_values[z_attr][1] - self.attr_values[z_attr][0]] data_translation = [self.attr_values[x_attr][0], self.attr_values[y_attr][0], self.attr_values[z_attr][0]] data_scale = 1. / numpy.array(data_scale) if x_discrete: data_scale[0] = 0.5 / float(len(self.data_domain[x_attr].values)) data_translation[0] = 1. if y_discrete: data_scale[1] = 0.5 / float(len(self.data_domain[y_attr].values)) data_translation[1] = 1. if z_discrete: data_scale[2] = 0.5 / float(len(self.data_domain[z_attr].values)) data_translation[2] = 1. self.data_scale = QVector3D(*data_scale) self.data_translation = QVector3D(*data_translation) self._x_axis_labels = None self._y_axis_labels = None self._z_axis_labels = None self.clear() attr_indices = [x_index, y_index, z_index] if color_index > -1: attr_indices.append(color_index) if size_index > -1: attr_indices.append(size_index) if symbol_index > -1: attr_indices.append(symbol_index) if label_index > -1: attr_indices.append(label_index) valid_data = self.getValidList(attr_indices) self.set_valid_data(valid_data) self.set_features(x_index, y_index, z_index, color_index, symbol_index, size_index, label_index, colors, num_symbols_used, x_discrete, y_discrete, z_discrete) ## Legend def_color = QColor(150, 150, 150) def_symbol = 0 def_size = 10 if color_discrete: num = len(self.data_domain[color_attr].values) values = get_variable_values_sorted(self.data_domain[color_attr]) for ind in range(num): self.legend().add_item(color_attr, values[ind], OWPoint(def_symbol, self.discrete_palette[ind], def_size)) if symbol_index != -1: num = len(self.data_domain[symbol_attr].values) values = get_variable_values_sorted(self.data_domain[symbol_attr]) for ind in range(num): self.legend().add_item(symbol_attr, values[ind], OWPoint(ind, def_color, def_size)) if size_discrete: num = len(self.data_domain[size_attr].values) values = get_variable_values_sorted(self.data_domain[size_attr]) for ind in range(num): self.legend().add_item(size_attr, values[ind], OWPoint(def_symbol, def_color, 6 + round(ind * 5 / len(values)))) if color_index != -1 and self.data_domain[color_attr].varType == Continuous: self.legend().add_color_gradient(color_attr, [("%%.%df" % self.data_domain[color_attr].numberOfDecimals % v) for v in self.attr_values[color_attr]]) self.legend().max_size = QSize(400, 400) self.legend().set_floating(True) self.legend().set_orientation(Qt.Vertical) if self.legend().pos().x() == 0: self.legend().setPos(QPointF(100, 100)) self.legend().update_items() self.legend().setVisible(self.show_legend) ## Axes self._x_axis_title = x_attr self._y_axis_title = y_attr self._z_axis_title = z_attr if x_discrete: self._x_axis_labels = get_variable_values_sorted(self.data_domain[x_attr]) if y_discrete: self._y_axis_labels = get_variable_values_sorted(self.data_domain[y_attr]) if z_discrete: self._z_axis_labels = get_variable_values_sorted(self.data_domain[z_attr]) self.update() def before_draw(self): if self.show_grid: self._draw_grid() if self.show_chassis: self._draw_chassis() if self.show_axes: self._draw_axes() def _draw_chassis(self): glMatrixMode(GL_PROJECTION) glLoadIdentity() glMultMatrixd(numpy.array(self.projection.data(), dtype=float)) glMatrixMode(GL_MODELVIEW) glLoadIdentity() glMultMatrixd(numpy.array(self.view.data(), dtype=float)) glMultMatrixd(numpy.array(self.model.data(), dtype=float)) # TODO: line stipple with shaders? self.qglColor(self._theme.axis_values_color) glEnable(GL_LINE_STIPPLE) glLineStipple(1, 0x00FF) glDisable(GL_DEPTH_TEST) glLineWidth(1) glEnable(GL_BLEND) glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA) edges = [self.x_axis, self.y_axis, self.z_axis, self.x_axis+self.unit_z, self.x_axis+self.unit_y, self.x_axis+self.unit_z+self.unit_y, self.y_axis+self.unit_x, self.y_axis+self.unit_z, self.y_axis+self.unit_x+self.unit_z, self.z_axis+self.unit_x, self.z_axis+self.unit_y, self.z_axis+self.unit_x+self.unit_y] glBegin(GL_LINES) for edge in edges: start, end = edge.v0, edge.v1 glVertex3f(start.x(), start.y(), start.z()) glVertex3f(end.x(), end.y(), end.z()) glEnd() glDisable(GL_LINE_STIPPLE) glEnable(GL_DEPTH_TEST) glDisable(GL_BLEND) def _map_to_original(self, point, coord_index): v = vec_div(self.map_to_data(point), self.data_scale) + self.data_translation if coord_index == 0: return v.x() elif coord_index == 1: return v.y() elif coord_index == 2: return v.z() def _draw_grid(self): self.renderer.set_transform(self.model, self.view, self.projection) cam_in_space = self.camera * self.camera_distance def _draw_grid_plane(axis0, axis1, normal0, normal1, i, j): for axis, normal, coord_index in zip([axis0, axis1], [normal0, normal1], [i, j]): start, end = axis.v0, axis.v1 start_value = self._map_to_original(start, coord_index) end_value = self._map_to_original(end, coord_index) values, _ = loose_label(start_value, end_value, 7) for value in values: if not (start_value <= value <= end_value): continue position = start + (end-start)*((value-start_value) / float(end_value-start_value)) self.renderer.draw_line( position, position - normal*1, color=self._theme.grid_color) glDisable(GL_DEPTH_TEST) glEnable(GL_BLEND) glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA) planes = [self.axis_plane_xy, self.axis_plane_yz, self.axis_plane_xy_back, self.axis_plane_yz_right] axes = [[self.x_axis, self.y_axis], [self.y_axis, self.z_axis], [self.x_axis+self.unit_z, self.y_axis+self.unit_z], [self.z_axis+self.unit_x, self.y_axis+self.unit_x]] normals = [[QVector3D(0,-1, 0), QVector3D(-1, 0, 0)], [QVector3D(0, 0,-1), QVector3D( 0,-1, 0)], [QVector3D(0,-1, 0), QVector3D(-1, 0, 0)], [QVector3D(0,-1, 0), QVector3D( 0, 0,-1)]] coords = [[0, 1], [1, 2], [0, 1], [2, 1]] visible_planes = [plane.visible_from(cam_in_space) for plane in planes] xz_visible = not self.axis_plane_xz.visible_from(cam_in_space) if xz_visible: _draw_grid_plane(self.x_axis, self.z_axis, QVector3D(0, 0, -1), QVector3D(-1, 0, 0), 0, 2) for visible, (axis0, axis1), (normal0, normal1), (i, j) in\ zip(visible_planes, axes, normals, coords): if not visible: _draw_grid_plane(axis0, axis1, normal0, normal1, i, j) glEnable(GL_DEPTH_TEST) glDisable(GL_BLEND) def _build_axes(self): edge_half = 1. / 2. self.x_axis = Edge(QVector3D(-edge_half, -edge_half, -edge_half), QVector3D( edge_half, -edge_half, -edge_half)) self.y_axis = Edge(QVector3D(-edge_half, -edge_half, -edge_half), QVector3D(-edge_half, edge_half, -edge_half)) self.z_axis = Edge(QVector3D(-edge_half, -edge_half, -edge_half), QVector3D(-edge_half, -edge_half, edge_half)) self.unit_x = unit_x = QVector3D(1, 0, 0) self.unit_y = unit_y = QVector3D(0, 1, 0) self.unit_z = unit_z = QVector3D(0, 0, 1) A = self.y_axis.v1 B = self.y_axis.v1 + unit_x C = self.x_axis.v1 D = self.x_axis.v0 E = A + unit_z F = B + unit_z G = C + unit_z H = D + unit_z self.axis_plane_xy = Plane(A, B, C, D) self.axis_plane_yz = Plane(A, D, H, E) self.axis_plane_xz = Plane(D, C, G, H) self.axis_plane_xy_back = Plane(H, G, F, E) self.axis_plane_yz_right = Plane(B, F, G, C) self.axis_plane_xz_top = Plane(E, F, B, A) def _draw_axes(self): self.renderer.set_transform(self.model, self.view, self.projection) def _draw_axis(axis): glLineWidth(2) self.renderer.draw_line(axis.v0, axis.v1, color=self._theme.axis_color) glLineWidth(1) def _draw_discrete_axis_values(axis, coord_index, normal, axis_labels): start, end = axis.v0, axis.v1 start_value = self._map_to_original(start, coord_index) end_value = self._map_to_original(end, coord_index) length = end_value - start_value for i, label in enumerate(axis_labels): value = (i + 1) * 2 if start_value <= value <= end_value: position = start + (end-start)*((value-start_value) / length) self.renderer.draw_line( position, position + normal*0.03, color=self._theme.axis_values_color) position += normal * 0.1 self.renderText(position.x(), position.y(), position.z(), label, font=self._theme.labels_font) def _draw_values(axis, coord_index, normal, axis_labels): glLineWidth(1) if axis_labels != None: _draw_discrete_axis_values(axis, coord_index, normal, axis_labels) return start, end = axis.v0, axis.v1 start_value = self._map_to_original(start, coord_index) end_value = self._map_to_original(end, coord_index) values, num_frac = loose_label(start_value, end_value, 7) for value in values: if not (start_value <= value <= end_value): continue position = start + (end-start)*((value-start_value) / float(end_value-start_value)) text = ('%%.%df' % num_frac) % value self.renderer.draw_line( position, position+normal*0.03, color=self._theme.axis_values_color) position += normal * 0.1 self.renderText(position.x(), position.y(), position.z(), text, font=self._theme.axis_font) def _draw_axis_title(axis, title, normal): middle = (axis.v0 + axis.v1) / 2. middle += normal * 0.1 if axis.v0.y() != axis.v1.y() else normal * 0.2 self.renderText(middle.x(), middle.y(), middle.z(), title, font=self._theme.axis_title_font) glDisable(GL_DEPTH_TEST) glLineWidth(1) glEnable(GL_BLEND) glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA) cam_in_space = self.camera * self.camera_distance # TODO: the code below is horrible and should be simplified planes = [self.axis_plane_xy, self.axis_plane_yz, self.axis_plane_xy_back, self.axis_plane_yz_right] normals = [[QVector3D(0,-1, 0), QVector3D(-1, 0, 0)], [QVector3D(0, 0,-1), QVector3D( 0,-1, 0)], [QVector3D(0,-1, 0), QVector3D(-1, 0, 0)], [QVector3D(0,-1, 0), QVector3D( 0, 0,-1)]] visible_planes = [plane.visible_from(cam_in_space) for plane in planes] xz_visible = not self.axis_plane_xz.visible_from(cam_in_space) if visible_planes[0 if xz_visible else 2]: _draw_axis(self.x_axis) _draw_values(self.x_axis, 0, QVector3D(0, 0, -1), self._x_axis_labels) if self.show_x_axis_title: _draw_axis_title(self.x_axis, self._x_axis_title, QVector3D(0, 0, -1)) elif visible_planes[2 if xz_visible else 0]: _draw_axis(self.x_axis + self.unit_z) _draw_values(self.x_axis + self.unit_z, 0, QVector3D(0, 0, 1), self._x_axis_labels) if self.show_x_axis_title: _draw_axis_title(self.x_axis + self.unit_z, self._x_axis_title, QVector3D(0, 0, 1)) if visible_planes[1 if xz_visible else 3]: _draw_axis(self.z_axis) _draw_values(self.z_axis, 2, QVector3D(-1, 0, 0), self._z_axis_labels) if self.show_z_axis_title: _draw_axis_title(self.z_axis, self._z_axis_title, QVector3D(-1, 0, 0)) elif visible_planes[3 if xz_visible else 1]: _draw_axis(self.z_axis + self.unit_x) _draw_values(self.z_axis + self.unit_x, 2, QVector3D(1, 0, 0), self._z_axis_labels) if self.show_z_axis_title: _draw_axis_title(self.z_axis + self.unit_x, self._z_axis_title, QVector3D(1, 0, 0)) try: rightmost_visible = visible_planes[::-1].index(True) except ValueError: return if rightmost_visible == 0 and visible_planes[0] == True: rightmost_visible = 3 y_axis_translated = [self.y_axis+self.unit_x, self.y_axis+self.unit_x+self.unit_z, self.y_axis+self.unit_z, self.y_axis] normals = [QVector3D(1, 0, 0), QVector3D(0, 0, 1), QVector3D(-1,0, 0), QVector3D(0, 0,-1)] axis = y_axis_translated[rightmost_visible] normal = normals[rightmost_visible] _draw_axis(axis) _draw_values(axis, 1, normal, self._y_axis_labels) if self.show_y_axis_title: _draw_axis_title(axis, self._y_axis_title, normal) class OWScatterPlot3D(OWWidget): settingsList = ['plot.show_legend', 'plot.symbol_size', 'plot.show_x_axis_title', 'plot.show_y_axis_title', 'plot.show_z_axis_title', 'plot.show_legend', 'plot.use_2d_symbols', 'plot.symbol_scale', 'plot.alpha_value', 'plot.show_grid', 'plot.pitch', 'plot.yaw', 'plot.show_chassis', 'plot.show_axes', 'auto_send_selection', 'auto_send_selection_update', 'plot.jitter_size', 'plot.jitter_continuous', 'dark_theme'] contextHandlers = {'': DomainContextHandler('', ['x_attr', 'y_attr', 'z_attr'])} jitter_sizes = [0.0, 0.1, 0.5, 1, 2, 3, 4, 5, 7, 10, 15, 20, 30, 40, 50] def __init__(self, parent=None, signalManager=None, name='ScatterPlot 3D'): OWWidget.__init__(self, parent, signalManager, name, True) self.inputs = [('Data', ExampleTable, self.set_data, Default), ('Subset Examples', ExampleTable, self.set_subset_data)] self.outputs = [('Selected Data', ExampleTable), ('Other Data', ExampleTable)] self.x_attr = '' self.y_attr = '' self.z_attr = '' self.x_attr_discrete = False self.y_attr_discrete = False self.z_attr_discrete = False self.color_attr = '' self.size_attr = '' self.symbol_attr = '' self.label_attr = '' self.tabs = OWGUI.tabWidget(self.controlArea) self.main_tab = OWGUI.createTabPage(self.tabs, 'Main') self.settings_tab = OWGUI.createTabPage(self.tabs, 'Settings', canScroll=True) self.x_attr_cb = OWGUI.comboBox(self.main_tab, self, 'x_attr', box='X-axis attribute', tooltip='Attribute to plot on X axis.', callback=self.on_axis_change, sendSelectedValue=1, valueType=str) self.y_attr_cb = OWGUI.comboBox(self.main_tab, self, 'y_attr', box='Y-axis attribute', tooltip='Attribute to plot on Y axis.', callback=self.on_axis_change, sendSelectedValue=1, valueType=str) self.z_attr_cb = OWGUI.comboBox(self.main_tab, self, 'z_attr', box='Z-axis attribute', tooltip='Attribute to plot on Z axis.', callback=self.on_axis_change, sendSelectedValue=1, valueType=str) self.color_attr_cb = OWGUI.comboBox(self.main_tab, self, 'color_attr', box='Point color', tooltip='Attribute to use for point color', callback=self.on_axis_change, sendSelectedValue=1, valueType=str) # Additional point properties (labels, size, symbol). additional_box = OWGUI.widgetBox(self.main_tab, 'Additional Point Properties') self.size_attr_cb = OWGUI.comboBox(additional_box, self, 'size_attr', label='Point size:', tooltip='Attribute to use for point size', callback=self.on_axis_change, indent=10, emptyString='(Same size)', sendSelectedValue=1, valueType=str) self.symbol_attr_cb = OWGUI.comboBox(additional_box, self, 'symbol_attr', label='Point symbol:', tooltip='Attribute to use for point symbol', callback=self.on_axis_change, indent=10, emptyString='(Same symbol)', sendSelectedValue=1, valueType=str) self.label_attr_cb = OWGUI.comboBox(additional_box, self, 'label_attr', label='Point label:', tooltip='Attribute to use for pointLabel', callback=self.on_axis_change, indent=10, emptyString='(No labels)', sendSelectedValue=1, valueType=str) self.plot = ScatterPlot(self) self.vizrank = OWVizRank(self, self.signalManager, self.plot, orngVizRank.SCATTERPLOT3D, 'ScatterPlot3D') self.optimization_dlg = self.vizrank self.optimization_buttons = OWGUI.widgetBox(self.main_tab, 'Optimization dialogs', orientation='horizontal') OWGUI.button(self.optimization_buttons, self, 'VizRank', callback=self.vizrank.reshow, tooltip='Opens VizRank dialog, where you can search for interesting projections with different subsets of attributes', debuggingEnabled=0) box = OWGUI.widgetBox(self.settings_tab, 'Point properties') OWGUI.hSlider(box, self, 'plot.symbol_scale', label='Symbol scale', minValue=1, maxValue=20, tooltip='Scale symbol size', callback=self.on_checkbox_update) OWGUI.hSlider(box, self, 'plot.alpha_value', label='Transparency', minValue=10, maxValue=255, tooltip='Point transparency value', callback=self.on_checkbox_update) OWGUI.rubber(box) box = OWGUI.widgetBox(self.settings_tab, 'Jittering Options') self.jitter_size_combo = OWGUI.comboBox(box, self, 'plot.jitter_size', label='Jittering size (% of size)'+' ', orientation='horizontal', callback=self.handleNewSignals, items=self.jitter_sizes, sendSelectedValue=1, valueType=float) OWGUI.checkBox(box, self, 'plot.jitter_continuous', 'Jitter continuous attributes', callback=self.handleNewSignals, tooltip='Does jittering apply also on continuous attributes?') self.dark_theme = False box = OWGUI.widgetBox(self.settings_tab, 'General settings') OWGUI.checkBox(box, self, 'plot.show_x_axis_title', 'X axis title', callback=self.on_checkbox_update) OWGUI.checkBox(box, self, 'plot.show_y_axis_title', 'Y axis title', callback=self.on_checkbox_update) OWGUI.checkBox(box, self, 'plot.show_z_axis_title', 'Z axis title', callback=self.on_checkbox_update) OWGUI.checkBox(box, self, 'plot.show_legend', 'Show legend', callback=self.on_checkbox_update) OWGUI.checkBox(box, self, 'plot.use_2d_symbols', '2D symbols', callback=self.update_plot) OWGUI.checkBox(box, self, 'dark_theme', 'Dark theme', callback=self.on_theme_change) OWGUI.checkBox(box, self, 'plot.show_grid', 'Show grid', callback=self.on_checkbox_update) OWGUI.checkBox(box, self, 'plot.show_axes', 'Show axes', callback=self.on_checkbox_update) OWGUI.checkBox(box, self, 'plot.show_chassis', 'Show chassis', callback=self.on_checkbox_update) OWGUI.checkBox(box, self, 'plot.hide_outside', 'Hide outside', callback=self.on_checkbox_update) OWGUI.rubber(box) box = OWGUI.widgetBox(self.settings_tab, 'Mouse', orientation = "horizontal") OWGUI.hSlider(box, self, 'plot.mouse_sensitivity', label='Sensitivity', minValue=1, maxValue=10, step=1, callback=self.plot.update, tooltip='Change mouse sensitivity') gui = self.plot.gui buttons = gui.default_zoom_select_buttons buttons.insert(2, (gui.UserButton, 'Rotate', 'state', ROTATING, None, 'Dlg_undo')) self.zoom_select_toolbar = gui.zoom_select_toolbar(self.main_tab, buttons=buttons) self.connect(self.zoom_select_toolbar.buttons[gui.SendSelection], SIGNAL("clicked()"), self.send_selection) self.connect(self.zoom_select_toolbar.buttons[gui.Zoom], SIGNAL("clicked()"), self.plot.unselect_all_points) self.plot.set_selection_behavior(OWPlot.ReplaceSelection) self.tooltip_kind = TooltipKind.NONE box = OWGUI.widgetBox(self.settings_tab, 'Tooltips Settings') OWGUI.comboBox(box, self, 'tooltip_kind', items = [ 'Don\'t Show Tooltips', 'Show Visible Attributes', 'Show All Attributes']) self.plot.mouseover_callback = self.mouseover_callback self.main_tab.layout().addStretch(100) self.settings_tab.layout().addStretch(100) self.mainArea.layout().addWidget(self.plot) self.connect(self.graphButton, SIGNAL('clicked()'), self.plot.save_to_file) self.loadSettings() self.plot.update_camera() self.on_theme_change() self.data = None self.subset_data = None self.resize(1100, 600) def mouseover_callback(self, index): if self.tooltip_kind == TooltipKind.VISIBLE: self.plot.show_tooltip(self.get_example_tooltip(self.data[index], self.shown_attrs)) elif self.tooltip_kind == TooltipKind.ALL: self.plot.show_tooltip(self.get_example_tooltip(self.data[index])) def get_example_tooltip(self, example, indices=None, max_indices=20): if indices and type(indices[0]) == str: indices = [self.plot.attribute_name_index[i] for i in indices] if not indices: indices = range(len(self.data.domain.attributes)) if example.domain.classVar: classIndex = self.plot.attribute_name_index[example.domain.classVar.name] while classIndex in indices: indices.remove(classIndex) text = 'Attributes:
' for index in indices[:max_indices]: attr = self.plot.data_domain[index].name if attr not in example.domain: text += ' '*4 + '%s = ?
' % (attr) elif example[attr].isSpecial(): text += ' '*4 + '%s = ?
' % (attr) else: text += ' '*4 + '%s = %s
' % (attr, str(example[attr])) if len(indices) > max_indices: text += ' '*4 + ' ...
' if example.domain.classVar: text = text[:-4] text += '
Class:
' if example.getclass().isSpecial(): text += ' '*4 + '%s = ?
' % (example.domain.classVar.name) else: text += ' '*4 + '%s = %s
' % (example.domain.classVar.name, str(example.getclass())) if len(example.domain.getmetas()) != 0: text = text[:-4] text += '
Meta attributes:
' for key in example.domain.getmetas(): try: text += ' '*4 + '%s = %s
' % (example.domain[key].name, str(example[key])) except: pass return text[:-4] def set_data(self, data=None): self.closeContext() self.vizrank.clearResults() same_domain = self.data and data and\ data.domain.checksum() == self.data.domain.checksum() self.data = data if not same_domain: self.init_attr_values() self.openContext('', data) def init_attr_values(self): self.x_attr_cb.clear() self.y_attr_cb.clear() self.z_attr_cb.clear() self.color_attr_cb.clear() self.size_attr_cb.clear() self.symbol_attr_cb.clear() self.label_attr_cb.clear() self.discrete_attrs = {} if not self.data: return self.color_attr_cb.addItem('(Same color)') self.label_attr_cb.addItem('(No labels)') self.symbol_attr_cb.addItem('(Same symbol)') self.size_attr_cb.addItem('(Same size)') icons = OWGUI.getAttributeIcons() for metavar in [self.data.domain.getmeta(mykey) for mykey in self.data.domain.getmetas().keys()]: self.label_attr_cb.addItem(icons[metavar.varType], metavar.name) for attr in self.data.domain: if attr.varType in [Discrete, Continuous]: self.x_attr_cb.addItem(icons[attr.varType], attr.name) self.y_attr_cb.addItem(icons[attr.varType], attr.name) self.z_attr_cb.addItem(icons[attr.varType], attr.name) self.color_attr_cb.addItem(icons[attr.varType], attr.name) self.size_attr_cb.addItem(icons[attr.varType], attr.name) if attr.varType == Discrete and len(attr.values) < len(Symbol): self.symbol_attr_cb.addItem(icons[attr.varType], attr.name) self.label_attr_cb.addItem(icons[attr.varType], attr.name) self.x_attr = str(self.x_attr_cb.itemText(0)) if self.y_attr_cb.count() > 1: self.y_attr = str(self.y_attr_cb.itemText(1)) else: self.y_attr = str(self.y_attr_cb.itemText(0)) if self.z_attr_cb.count() > 2: self.z_attr = str(self.z_attr_cb.itemText(2)) else: self.z_attr = str(self.z_attr_cb.itemText(0)) if self.data.domain.classVar and self.data.domain.classVar.varType in [Discrete, Continuous]: self.color_attr = self.data.domain.classVar.name else: self.color_attr = '' self.symbol_attr = self.size_attr = self.label_attr = '' self.shown_attrs = [self.x_attr, self.y_attr, self.z_attr, self.color_attr] def set_subset_data(self, data=None): self.subset_data = data def handleNewSignals(self): self.plot.set_data(self.data, self.subset_data) self.vizrank.resetDialog() self.update_plot() self.send_selection() def saveSettings(self): OWWidget.saveSettings(self) def sendReport(self): self.startReport('%s [%s - %s - %s]' % (self.windowTitle(), self.x_attr, self.y_attr, self.z_attr)) self.reportSettings('Visualized attributes', [('X', self.x_attr), ('Y', self.y_attr), ('Z', self.z_attr), self.color_attr and ('Color', self.color_attr), self.label_attr and ('Label', self.label_attr), self.symbol_attr and ('Symbol', self.symbol_attr), self.size_attr and ('Size', self.size_attr)]) self.reportSettings('Settings', [('Symbol size', self.plot.symbol_scale), ('Transparency', self.plot.alpha_value), ('Jittering', self.jitter_size), ('Jitter continuous attributes', OWGUI.YesNo[self.jitter_continuous]) ]) self.reportSection('Plot') self.reportImage(self.plot.save_to_file_direct, QSize(400, 400)) def send_selection(self): if self.data == None: return selected = None#selected = self.plot.get_selected_indices() # TODO: crash if selected == None or len(selected) != len(self.data): return unselected = numpy.logical_not(selected) selected = self.data.selectref(list(selected)) unselected = self.data.selectref(list(unselected)) self.send('Selected Data', selected) self.send('Other Data', unselected) def on_axis_change(self): if self.data is not None: self.update_plot() def on_theme_change(self): if self.dark_theme: self.plot.theme = ScatterDarkTheme() else: self.plot.theme = ScatterLightTheme() def on_checkbox_update(self): self.plot.update() def update_plot(self): if self.data is None: return self.plot.update_data(self.x_attr, self.y_attr, self.z_attr, self.color_attr, self.symbol_attr, self.size_attr, self.label_attr) def showSelectedAttributes(self): val = self.vizrank.getSelectedProjection() if not val: return if self.data.domain.classVar: self.attr_color = self.data.domain.classVar.name if not self.plot.have_data: return attr_list = val[3] if attr_list and len(attr_list) == 3: self.x_attr = attr_list[0] self.y_attr = attr_list[1] self.z_attr = attr_list[2] self.update_plot() if __name__ == '__main__': app = QApplication(sys.argv) w = OWScatterPlot3D() data = orange.ExampleTable('../../doc/datasets/iris') w.set_data(data) w.handleNewSignals() w.show() app.exec_()