Skip to content

Commit

Permalink
Scatter plot: Remove discrete attributes for x and y
Browse files Browse the repository at this point in the history
  • Loading branch information
janezd committed Dec 6, 2018
1 parent 97dee02 commit 8f62e96
Show file tree
Hide file tree
Showing 2 changed files with 69 additions and 120 deletions.
76 changes: 15 additions & 61 deletions Orange/widgets/visualize/owscatterplot.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,15 @@

import pyqtgraph as pg

from Orange.data import Table, Domain, DiscreteVariable, Variable
from Orange.data import Table, Domain, DiscreteVariable, Variable, \
ContinuousVariable
from Orange.data.sql.table import SqlTable, AUTO_DL_LIMIT
from Orange.preprocess.score import ReliefF, RReliefF

from Orange.widgets import gui, report
from Orange.widgets.io import MatplotlibFormat, MatplotlibPDFFormat
from Orange.widgets.settings import (
Setting, ContextSetting, SettingProvider
)
from Orange.widgets.utils import get_variable_values_sorted
Setting, ContextSetting, SettingProvider, IncompatibleContext)
from Orange.widgets.utils.itemmodels import DomainModel
from Orange.widgets.utils.widgetpreview import WidgetPreview
from Orange.widgets.visualize.owscatterplotgraph import OWScatterPlotBase
Expand Down Expand Up @@ -97,7 +96,6 @@ def score_heuristic(self):

class OWScatterPlotGraph(OWScatterPlotBase):
show_reg_line = Setting(False)
jitter_continuous = Setting(False)

def __init__(self, scatter_widget, parent):
super().__init__(scatter_widget, parent)
Expand All @@ -107,52 +105,18 @@ def clear(self):
super().clear()
self.reg_line_item = None

def set_axis_labels(self, axis, labels):
axis = self.plot_widget.getAxis(axis)
if labels:
axis.setTicks([list(enumerate(labels))])
else:
axis.setTicks(None)

def set_axis_title(self, axis, title):
self.plot_widget.setLabel(axis=axis, text=title)

def update_coordinates(self):
super().update_coordinates()
self.update_regression_line()
self.update_tooltip()

def _get_jittering_tooltip(self):
def is_discrete(attr):
return attr and attr.is_discrete

if self.jitter_continuous or is_discrete(self.master.attr_x) or \
is_discrete(self.master.attr_y):
return super()._get_jittering_tooltip()
return ""

def jitter_coordinates(self, x, y):
def get_span(attr):
if attr.is_discrete:
# Assuming the maximal jitter size is 10, a span of 4 will
# jitter by 4 * 10 / 100 = 0.4, so there will be no overlap
return 4
elif self.jitter_continuous:
return None # Let _jitter_data determine the span
else:
return 0 # No jittering
span_x = get_span(self.master.attr_x)
span_y = get_span(self.master.attr_y)
if self.jitter_size == 0 or (span_x == 0 and span_y == 0):
return x, y
return self._jitter_data(x, y, span_x, span_y)

def update_regression_line(self):
if self.reg_line_item is not None:
self.plot_widget.removeItem(self.reg_line_item)
self.reg_line_item = None
if not (self.show_reg_line
and self.master.can_draw_regresssion_line()):
if not self.show_reg_line:
return
x, y = self.master.get_coordinates_data()
if x is None:
Expand Down Expand Up @@ -208,6 +172,7 @@ class Warning(OWDataProjectionWidget.Warning):
missing_coords = Msg(
"Plot cannot be displayed because '{}' or '{}' "
"is missing for all data points")
no_continuous_vars = Msg("Data has not continuous variables")

class Information(OWDataProjectionWidget.Information):
sampled_sql = Msg("Large SQL table; showing a sample.")
Expand All @@ -231,7 +196,6 @@ def _add_controls(self):
self._add_controls_axis()
self._add_controls_sampling()
super()._add_controls()
self.gui.add_widget(self.gui.JitterNumericValues, self._effects_box)
self.gui.add_widgets(
[self.gui.ShowGridLines,
self.gui.ToolTipShowsAll,
Expand All @@ -245,7 +209,7 @@ def _add_controls_axis(self):
)
box = gui.vBox(self.controlArea, True)
dmod = DomainModel
self.xy_model = DomainModel(dmod.MIXED, valid_types=dmod.PRIMITIVE)
self.xy_model = DomainModel(dmod.MIXED, valid_types=ContinuousVariable)
self.cb_attr_x = gui.comboBox(
box, self, "attr_x", label="Axis x:", callback=self.attr_changed,
model=self.xy_model, **common_options)
Expand Down Expand Up @@ -324,6 +288,11 @@ def check_data(self):
if self.auto_sample:
self.__timer.start()

if self.data is not None:
if not self.data.domain.has_continuous_attributes(True, True):
self.Warning.no_continuous_vars()
self.data = None

if self.data is not None and (len(self.data) == 0 or
len(self.data.domain) == 0):
self.data = None
Expand Down Expand Up @@ -359,12 +328,6 @@ def _point_tooltip(self, point_id, skip_attrs=()):
text = "<b>{}</b><br/><br/>{}".format(text, others)
return text

def can_draw_regresssion_line(self):
return self.data is not None and\
self.data.domain is not None and \
self.attr_x.is_continuous and \
self.attr_y.is_continuous

def add_data(self, time=0.4):
if self.data and len(self.data) > 2000:
self.__timer.stop()
Expand Down Expand Up @@ -413,7 +376,6 @@ def handleNewSignals(self):
else:
super().handleNewSignals()
self._vizrank_color_change()
self.cb_reg_line.setEnabled(self.can_draw_regresssion_line())

@Inputs.features
def set_shown_attributes(self, attributes):
Expand All @@ -428,19 +390,13 @@ def set_attr(self, attr_x, attr_y):
self.attr_changed()

def attr_changed(self):
self.cb_reg_line.setEnabled(self.can_draw_regresssion_line())
self.setup_plot()
self.commit()

def setup_plot(self):
super().setup_plot()
for axis, var in (("bottom", self.attr_x), ("left", self.attr_y)):
self.graph.set_axis_title(axis, var)
if var and var.is_discrete:
self.graph.set_axis_labels(axis,
get_variable_values_sorted(var))
else:
self.graph.set_axis_labels(axis, None)
self.graph.set_axis_title("bottom", self.attr_x)
self.graph.set_axis_title("left", self.attr_y)

def colors_changed(self):
super().colors_changed()
Expand All @@ -465,10 +421,8 @@ def _get_send_report_caption(self):
("Label", self._get_caption_var_name(self.attr_label)),
("Shape", self._get_caption_var_name(self.attr_shape)),
("Size", self._get_caption_var_name(self.attr_size)),
("Jittering", (self.attr_x.is_discrete or
self.attr_y.is_discrete or
self.graph.jitter_continuous) and
self.graph.jitter_size)))
("Jittering", (self.graph.jitter_size > 0
and "{} %".format(self.graph.jitter_size)))))

@classmethod
def migrate_settings(cls, settings, version):
Expand Down
113 changes: 54 additions & 59 deletions Orange/widgets/visualize/tests/test_owscatterplot.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,18 +134,6 @@ def test_data_column_infs(self):
attr_x = self.widget.controls.attr_x
simulate.combobox_activate_item(attr_x, "b")

def test_regression_line(self):
"""It is possible to draw the line only for pair of continuous attrs"""
self.send_signal(self.widget.Inputs.data, self.data)
self.assertTrue(self.widget.cb_reg_line.isEnabled())
self.assertIsNone(self.widget.graph.reg_line_item)
self.widget.cb_reg_line.setChecked(True)
self.assertIsNotNone(self.widget.graph.reg_line_item)
self.widget.cb_attr_y.activated.emit(4)
self.widget.cb_attr_y.setCurrentIndex(4)
self.assertFalse(self.widget.cb_reg_line.isEnabled())
self.assertIsNone(self.widget.graph.reg_line_item)

def test_points_combo_boxes(self):
"""Check Point box combo models and values"""
self.send_signal(self.widget.Inputs.data, self.data)
Expand Down Expand Up @@ -266,8 +254,8 @@ def test_points_selection(self):
self.assertEqual(len(selected_data), 50)

# Changing the dataset should clear selection
titanic = Table("titanic")
self.send_signal(self.widget.Inputs.data, titanic)
heart = Table("heart_disease")
self.send_signal(self.widget.Inputs.data, heart)
selected_data = self.get_output(self.widget.Outputs.selected_data)
self.assertIsNone(selected_data)

Expand All @@ -291,7 +279,6 @@ def test_set_strings_settings(self):
"""
Test if settings can be loaded as strings and successfully put
in new owplotgui combos.
GH-2240
"""
self.send_signal(self.widget.Inputs.data, self.data)
settings = self.widget.settingsHandler.pack_data(self.widget)
Expand All @@ -310,7 +297,6 @@ def test_set_strings_settings(self):
def test_features_and_no_data(self):
"""
Prevent crashing when features are sent but no data.
GH-2384
"""
domain = Table("iris").domain
self.send_signal(self.widget.Inputs.features,
Expand Down Expand Up @@ -391,7 +377,7 @@ def assert_vizrank_enabled(data, is_enabled):

def test_vizrank_nonprimitives(self):
"""VizRank does not try to include non primitive attributes"""
data = Table("zoo")
data = Table("brown-selected")
self.send_signal(self.widget.Inputs.data, data)
with patch("Orange.widgets.visualize.owscatterplot.ReliefF",
new=lambda *_1, **_2: lambda data: np.arange(len(data))):
Expand All @@ -415,56 +401,56 @@ def test_auto_send_selection(self):
self.assertIsInstance(output, Table)

def test_color_is_optional(self):
zoo = Table("zoo")
backbone, breathes, airborne, type = \
[zoo.domain[x] for x in ["backbone", "breathes", "airborne", "type"]]
default_x, default_y, default_color = \
zoo.domain[0], zoo.domain[1], zoo.domain.class_var
heart = Table("heart_disease")
age, rest_sbp, max_hr, cholesterol, gender, narrowing = \
[heart.domain[x]
for x in ["age", "rest SBP", "max HR", "cholesterol", "gender",
"diameter narrowing"]]
attr_x = self.widget.controls.attr_x
attr_y = self.widget.controls.attr_y
attr_color = self.widget.controls.attr_color

# Send dataset, ensure defaults are what we expect them to be
self.send_signal(self.widget.Inputs.data, zoo)
self.assertEqual(attr_x.currentText(), default_x.name)
self.assertEqual(attr_y.currentText(), default_y.name)
self.assertEqual(attr_color.currentText(), default_color.name)
self.send_signal(self.widget.Inputs.data, heart)
self.assertEqual(attr_x.currentText(), age.name)
self.assertEqual(attr_y.currentText(), rest_sbp.name)
self.assertEqual(attr_color.currentText(), narrowing.name)
# Select different values
simulate.combobox_activate_item(attr_x, backbone.name)
simulate.combobox_activate_item(attr_y, breathes.name)
simulate.combobox_activate_item(attr_color, airborne.name)
simulate.combobox_activate_item(attr_x, max_hr.name)
simulate.combobox_activate_item(attr_y, cholesterol.name)
simulate.combobox_activate_item(attr_color, gender.name)

# Send compatible dataset, values should not change
zoo2 = zoo[:, (backbone, breathes, airborne, type)]
self.send_signal(self.widget.Inputs.data, zoo2)
self.assertEqual(attr_x.currentText(), backbone.name)
self.assertEqual(attr_y.currentText(), breathes.name)
self.assertEqual(attr_color.currentText(), airborne.name)
heart2 = heart[:, (cholesterol, gender, max_hr, narrowing)]
self.send_signal(self.widget.Inputs.data, heart2)
simulate.combobox_activate_item(attr_x, max_hr.name)
simulate.combobox_activate_item(attr_y, cholesterol.name)
simulate.combobox_activate_item(attr_color, gender.name)

# Send dataset without color variable
# x and y should remain, color reset to default
zoo3 = zoo[:, (backbone, breathes, type)]
self.send_signal(self.widget.Inputs.data, zoo3)
self.assertEqual(attr_x.currentText(), backbone.name)
self.assertEqual(attr_y.currentText(), breathes.name)
self.assertEqual(attr_color.currentText(), default_color.name)
heart3 = heart[:, (age, max_hr, cholesterol, narrowing)]
self.send_signal(self.widget.Inputs.data, heart3)
simulate.combobox_activate_item(attr_x, max_hr.name)
simulate.combobox_activate_item(attr_y, cholesterol.name)
self.assertEqual(attr_color.currentText(), narrowing.name)

# Send dataset without x
# y and color should be the same as with zoo
zoo4 = zoo[:, (default_x, default_y, breathes, airborne, type)]
self.send_signal(self.widget.Inputs.data, zoo4)
self.assertEqual(attr_x.currentText(), default_x.name)
self.assertEqual(attr_y.currentText(), default_y.name)
self.assertEqual(attr_color.currentText(), default_color.name)

# Send dataset compatible with zoo2 and zoo3
# Color should reset to one in zoo3, as it was used more
# y and color should be the same as with heart
heart4 = heart[:, (age, rest_sbp, cholesterol, narrowing)]
self.send_signal(self.widget.Inputs.data, heart4)
self.assertEqual(attr_x.currentText(), age.name)
self.assertEqual(attr_y.currentText(), rest_sbp.name)
self.assertEqual(attr_color.currentText(), narrowing.name)

# Send dataset compatible with heart2 and heart3
# Color should reset to one in heart3, as it was used more
# recently
zoo5 = zoo[:, (default_x, backbone, breathes, airborne, type)]
self.send_signal(self.widget.Inputs.data, zoo5)
self.assertEqual(attr_x.currentText(), backbone.name)
self.assertEqual(attr_y.currentText(), breathes.name)
self.assertEqual(attr_color.currentText(), type.name)
heart5 = heart[:, (age, max_hr, cholesterol, gender, narrowing)]
self.send_signal(self.widget.Inputs.data, heart5)
simulate.combobox_activate_item(attr_x, max_hr.name)
simulate.combobox_activate_item(attr_y, cholesterol.name)
self.assertEqual(attr_color.currentText(), narrowing.name)

def test_handle_metas(self):
"""
Expand Down Expand Up @@ -518,10 +504,10 @@ def test_tooltip(self):
self.send_signal(self.widget.Inputs.data, data)
widget = self.widget
graph = widget.graph
scatterplot_item = graph.scatterplot_item

widget.controls.attr_x = data.domain["chest pain"]
widget.controls.attr_y = data.domain["cholesterol"]
widget.attr_x = data.domain["age"]
widget.attr_y = data.domain["max HR"]
scatterplot_item = graph.scatterplot_item
all_points = scatterplot_item.points()

event = MagicMock()
Expand All @@ -536,8 +522,8 @@ def test_tooltip(self):
self.assertTrue(graph.help_event(event))
(_, text), _ = show_text.call_args
self.assertIn("age = {}".format(data[42, "age"]), text)
self.assertIn("gender = {}".format(data[42, "gender"]), text)
self.assertNotIn("max HR = {}".format(data[42, "max HR"]), text)
self.assertIn("max HR = {}".format(data[42, "max HR"]), text)
self.assertNotIn("gender = {}".format(data[42, "gender"]), text)
self.assertNotIn("others", text)

# Show all attributes
Expand Down Expand Up @@ -582,7 +568,7 @@ def prepare_data():

def assert_equal(data, max):
self.send_signal(self.widget.Inputs.data, data)
pen_data, brush_data = self.widget.graph.get_colors()
pen_data, _ = self.widget.graph.get_colors()
self.assertEqual(max, len(np.unique([id(p) for p in pen_data])), )

assert_equal(prepare_data(), MAX_CATEGORIES)
Expand All @@ -591,6 +577,15 @@ def assert_equal(data, max):
data.Y[42] = np.nan
assert_equal(data, MAX_CATEGORIES + 1)

def test_change_data(self):
self.send_signal(self.widget.Inputs.data, self.data)
self.send_signal(self.widget.Inputs.data, Table("titanic"))
self.assertTrue(self.widget.Warning.no_continuous_vars.is_shown())
self.assertIsNone(self.widget.data)
self.send_signal(self.widget.Inputs.data, self.data)
self.assertFalse(self.widget.Warning.no_continuous_vars.is_shown())
self.assertIs(self.widget.data, self.data)


if __name__ == "__main__":
import unittest
Expand Down

0 comments on commit 8f62e96

Please sign in to comment.