class BoolElement(FormElement): """ A checkbox representing a boolean setting The shorthand notation is True or False:: e = FormElement.auto(False) """ state = ButtonProperty('ui') @classmethod def recognizes(cls, params): return isinstance(params, bool) def _build_ui(self): w = QtWidgets.QCheckBox() w.setChecked(self.params) w.toggled.connect(nonpartial(self.changed)) return w def value(self, layer=None, view=None): return self.ui.isChecked()
class PreferencesDialog(QtWidgets.QDialog): theme = CurrentComboTextProperty('ui.combo_theme') background = ColorProperty('ui.color_background') foreground = ColorProperty('ui.color_foreground') data_color = ColorProperty('ui.color_default_data') data_alpha = ValueProperty('ui.slider_alpha', value_range=(0, 1)) data_apply = ButtonProperty('ui.checkbox_apply') save_to_disk = ButtonProperty('ui.checkbox_save') font_size = ValueProperty('ui.spinner_font_size') def __init__(self, application, parent=None): super(PreferencesDialog, self).__init__(parent=parent) self._app = weakref.ref(application) self.ui = load_ui('preferences.ui', self, directory=os.path.dirname(__file__)) self.ui.cancel.clicked.connect(self.reject) self.ui.ok.clicked.connect(self.accept) self.ui.combo_theme.currentIndexChanged.connect(self._update_colors_from_theme) self.ui.button_reset_dialogs.clicked.connect(self._reset_dialogs) # The following is needed because of a bug in Qt which means that # tab titles don't get scaled right. if platform.system() == 'Darwin': app = get_qapp() app_font = app.font() self.ui.tab_widget.setStyleSheet('font-size: {0}px'.format(app_font.pointSize())) from glue.config import settings self.background = settings.BACKGROUND_COLOR self.foreground = settings.FOREGROUND_COLOR self.data_color = settings.DATA_COLOR self.data_alpha = settings.DATA_ALPHA self.font_size = settings.FONT_SIZE self._update_theme_from_colors() self.panes = [] from glue.config import preference_panes for label, widget_cls in sorted(preference_panes): pane = widget_cls() self.ui.tab_widget.addTab(pane, label) self.panes.append(pane) def _update_theme_from_colors(self, *args): if (rgb(self.background) == (1, 1, 1) and rgb(self.foreground) == (0, 0, 0) and rgb(self.data_color) == (0.35, 0.35, 0.35) and np.allclose(self.data_alpha, 0.8)): self.theme = 'Black on White' elif (rgb(self.background) == (0, 0, 0) and rgb(self.foreground) == (1, 1, 1) and rgb(self.data_color) == (0.75, 0.75, 0.75) and np.allclose(self.data_alpha, 0.8)): self.theme = 'White on Black' else: self.theme = 'Custom' def _update_colors_from_theme(self, *args): if self.theme == 'Black on White': self.foreground = 'black' self.background = 'white' self.data_color = '0.35' self.data_alpha = 0.8 elif self.theme == 'White on Black': self.foreground = 'white' self.background = 'black' self.data_color = '0.75' self.data_alpha = 0.8 elif self.theme != 'Custom': raise ValueError("Unknown theme: {0}".format(self.theme)) def _reset_dialogs(self, *args): from glue.config import settings for key, _, _ in settings: if key.lower().startswith(('show_info', 'show_warn', 'show_large')): setattr(settings, key, True) def accept(self): # Update default settings from glue.config import settings settings.FOREGROUND_COLOR = self.foreground settings.BACKGROUND_COLOR = self.background settings.DATA_COLOR = self.data_color settings.DATA_ALPHA = self.data_alpha settings.FONT_SIZE = self.font_size for pane in self.panes: pane.finalize() # Save to disk if requested if self.save_to_disk: save_settings() else: settings._save_to_disk = True # Trigger viewers to update defaults app = self._app() if app is not None: app._hub.broadcast(SettingsChangeMessage(self, ('FOREGROUND_COLOR', 'BACKGROUND_COLOR', 'FONT_SIZE'))) if self.data_apply: # If requested, trigger data to update color app.set_data_color(settings.DATA_COLOR, settings.DATA_ALPHA) super(PreferencesDialog, self).accept()
class HistogramWidget(DataViewer): LABEL = "Histogram" _property_set = DataViewer._property_set + \ 'component xlog ylog normed cumulative autoscale xmin xmax nbins'.split( ) xmin = FloatLineProperty('ui.xmin', 'Minimum value') xmax = FloatLineProperty('ui.xmax', 'Maximum value') normed = ButtonProperty('ui.normalized_box', 'Normalized?') autoscale = ButtonProperty('ui.autoscale_box', 'Autoscale view to histogram?') cumulative = ButtonProperty('ui.cumulative_box', 'Cumulative?') nbins = ValueProperty('ui.binSpinBox', 'Number of bins') xlog = ButtonProperty('ui.xlog_box', 'Log-scale the x axis?') ylog = ButtonProperty('ui.ylog_box', 'Log-scale the y axis?') def __init__(self, session, parent=None): super(HistogramWidget, self).__init__(session, parent) self.central_widget = MplWidget() self.setCentralWidget(self.central_widget) self.option_widget = QtGui.QWidget() self.ui = load_ui('options_widget.ui', self.option_widget, directory=os.path.dirname(__file__)) self._tweak_geometry() self.client = HistogramClient(self._data, self.central_widget.canvas.fig, layer_artist_container=self._layer_artist_container) self._init_limits() self.make_toolbar() self._connect() # maps _hash(componentID) -> componentID self._component_hashes = {} @staticmethod def _get_default_tools(): return [] def _init_limits(self): validator = QtGui.QDoubleValidator(None) validator.setDecimals(7) self.ui.xmin.setValidator(validator) self.ui.xmax.setValidator(validator) lo, hi = self.client.xlimits self.ui.xmin.setText(str(lo)) self.ui.xmax.setText(str(hi)) def _tweak_geometry(self): self.central_widget.resize(600, 400) self.resize(self.central_widget.size()) def _connect(self): ui = self.ui cl = self.client ui.attributeCombo.currentIndexChanged.connect(self._set_attribute_from_combo) ui.normalized_box.toggled.connect(partial(setattr, cl, 'normed')) ui.autoscale_box.toggled.connect(partial(setattr, cl, 'autoscale')) ui.cumulative_box.toggled.connect(partial(setattr, cl, 'cumulative')) connect_int_spin(cl, 'nbins', ui.binSpinBox) connect_float_edit(cl, 'xmin', ui.xmin) connect_float_edit(cl, 'xmax', ui.xmax) connect_bool_button(cl, 'xlog', ui.xlog_box) connect_bool_button(cl, 'ylog', ui.ylog_box) def make_toolbar(self): result = GlueToolbar(self.central_widget.canvas, self, name='Histogram') for mode in self._mouse_modes(): result.add_mode(mode) self.addToolBar(result) return result def _mouse_modes(self): axes = self.client.axes def apply_mode(mode): return self.apply_roi(mode.roi()) rect = HRangeMode(axes, roi_callback=apply_mode) return [rect] @defer_draw def _update_attributes(self): """Repopulate the combo box that selects the quantity to plot""" combo = self.ui.attributeCombo component = self.component new = self.client.component or component combo.blockSignals(True) combo.clear() # implementation note: # PySide doesn't robustly store python objects with setData # use _hash(x) instead model = QtGui.QStandardItemModel() data_ids = set(_hash(d) for d in self._data) self._component_hashes = dict((_hash(c), c) for d in self._data for c in d.components) found = False for d in self._data: if d not in self._layer_artist_container: continue item = QtGui.QStandardItem(d.label) item.setData(_hash(d), role=Qt.UserRole) assert item.data(Qt.UserRole) == _hash(d) item.setFlags(item.flags() & ~Qt.ItemIsEnabled) model.appendRow(item) for c in d.visible_components: if (not d.get_component(c).categorical and not d.get_component(c).numeric): continue if c is new: found = True item = QtGui.QStandardItem(c.label) item.setData(_hash(c), role=Qt.UserRole) model.appendRow(item) combo.setModel(model) # separators below data items for i in range(combo.count()): if combo.itemData(i) in data_ids: combo.insertSeparator(i + 1) combo.blockSignals(False) if found: self.component = new else: combo.setCurrentIndex(2) # skip first data + separator self._set_attribute_from_combo() @property def component(self): combo = self.ui.attributeCombo index = combo.currentIndex() return self._component_hashes.get(combo.itemData(index), None) @component.setter def component(self, component): combo = self.ui.attributeCombo if combo.count() == 0: # cold start problem, when restoring self._update_attributes() # combo.findData doesn't seem to work robustly for i in range(combo.count()): data = combo.itemData(i) if data == _hash(component): combo.setCurrentIndex(i) return raise IndexError("Component not present: %s" % component) @defer_draw def _set_attribute_from_combo(self, *args): if self.component is not None: for d in self._data: try: component = d.get_component(self.component) except: continue else: break if component.categorical: if self.ui.xlog_box.isEnabled(): self.ui.xlog_box.setEnabled(False) self.xlog = False else: if not self.ui.xlog_box.isEnabled(): self.ui.xlog_box.setEnabled(True) self.client.set_component(self.component) self.update_window_title() @defer_draw def add_data(self, data): """ Add data item to combo box. If first addition, also update attributes """ if self.data_present(data): return True if data.size > WARN_SLOW and not self._confirm_large_data(data): return False self.client.add_layer(data) self._update_attributes() return True def add_subset(self, subset): pass def _remove_data(self, data): """ Remove data item from the combo box """ pass def data_present(self, data): return data in self._layer_artist_container def register_to_hub(self, hub): super(HistogramWidget, self).register_to_hub(hub) self.client.register_to_hub(hub) hub.subscribe(self, msg.DataCollectionDeleteMessage, handler=lambda x: self._remove_data(x.data)) hub.subscribe(self, msg.DataUpdateMessage, handler=lambda *args: self._update_labels()) hub.subscribe(self, msg.ComponentsChangedMessage, handler=lambda x: self._update_attributes()) def unregister(self, hub): super(HistogramWidget, self).unregister(hub) self.client.unregister(hub) hub.unsubscribe_all(self) @property def window_title(self): c = self.client.component if c is not None: label = str(c.label) else: label = 'Histogram' return label def _update_labels(self): self.update_window_title() self._update_attributes() def __str__(self): return "Histogram Widget" def options_widget(self): return self.option_widget
class DendroWidget(DataViewer): """ An interactive dendrogram display """ LABEL = 'Dendrogram' _property_set = DataViewer._property_set + \ 'ylog height parent order'.split() ylog = ButtonProperty('ui.ylog', 'log scaling on y axis?') height = CurrentComboProperty('ui.heightCombo', 'height attribute') parent = CurrentComboProperty('ui.parentCombo', 'parent attribute') order = CurrentComboProperty('ui.orderCombo', 'layout sorter attribute') _toolbar_cls = MatplotlibViewerToolbar tools = ['Pick'] def __init__(self, session, parent=None): super(DendroWidget, self).__init__(session, parent) self.central_widget = MplWidget() self.option_widget = QtWidgets.QWidget() self.setCentralWidget(self.central_widget) self.ui = load_ui('options_widget.ui', self.option_widget, directory=os.path.dirname(__file__)) self.client = DendroClient( self._data, self.central_widget.canvas.fig, layer_artist_container=self._layer_artist_container) self._connect() self.initialize_toolbar() self.statusBar().setSizeGripEnabled(False) def _connect(self): ui = self.ui cl = self.client connect_bool_button(cl, 'ylog', ui.ylog) connect_current_combo(cl, 'parent_attr', ui.parentCombo) connect_current_combo(cl, 'height_attr', ui.heightCombo) connect_current_combo(cl, 'order_attr', ui.orderCombo) def initialize_toolbar(self): super(DendroWidget, self).initialize_toolbar() def on_move(mode): if mode._drag: self.client.apply_roi(mode.roi()) self.toolbar.tools['Pick']._move_callback = on_move def apply_roi(self, roi): self.client.apply_roi(roi) def _update_combos(self, data=None): data = data or self.client.display_data if data is None: return for combo in [ self.ui.heightCombo, self.ui.parentCombo, self.ui.orderCombo ]: combo.blockSignals(True) ids = [] idx = combo.currentIndex() old = combo.itemData(idx) if idx > 0 else None combo.clear() for cid in data.components: if cid.hidden and cid is not data.pixel_component_ids[0]: continue combo.addItem(cid.label, userData=cid) ids.append(cid) try: combo.setCurrentIndex(ids.index(old)) except ValueError: combo.setCurrentIndex(0) combo.blockSignals(False) def add_data(self, data): """Add a new data set to the widget :returns: True if the addition was expected, False otherwise """ if data in self.client: return self._update_combos(data) self.client.add_layer(data) return True def add_subset(self, subset): """Add a subset to the widget :returns: True if the addition was accepted, False otherwise """ self.add_data(subset.data) if subset.data in self.client: self.client.add_layer(subset) return True def register_to_hub(self, hub): super(DendroWidget, self).register_to_hub(hub) self.client.register_to_hub(hub) hub.subscribe(self, core.message.ComponentsChangedMessage, nonpartial(self._update_combos())) def unregister(self, hub): super(DendroWidget, self).unregister(hub) hub.unsubscribe_all(self.client) hub.unsubscribe_all(self) def options_widget(self): return self.option_widget @defer_draw def restore_layers(self, rec, context): from glue.core.callback_property import delay_callback with delay_callback(self.client, 'height_attr', 'parent_attr', 'order_attr'): self.client.restore_layers(rec, context) self._update_combos()
class AttributeLimitsHelper(object): """ This class is a helper for attribute-dependent min/max level values. Given an attribute combo as well as line edit widgets for the min/max values, this helper takes care of populating the attribute combo, setting the initial values of the min/max values, and keeping a cache of the min/max values as a function of attribute. This means that if the user edits the min/max values and then changes attribute then changes back, the original min/max values will be retained. In addition, this helper class can optionally link a combo for the scale mode, for example using the min/max values or percentile values, as well as a button for flipping the min/max values. Parameters ---------- attribute_combo : ``QComboBox`` instance The attribute combo - this will be populated once a dataset is assigned to the helper. lower_value, upper_value : ``QLineEdit`` instances The fields for the lower/upper levels mode_combo : ``QComboBox`` instance, optional The scale mode combo - this will be populated by presets such as Min/Max, various percentile levels, and Custom. flip_button : ``QToolButton`` instance, optional The flip button log_button : ``QToolButton`` instance, optional A button indicating whether the attribute should be shown in log space data : :class:`glue.core.data.Data` The dataset to attach to the helper - this will be used to populate the attribute combo as well as determine the limits automatically given the scale mode preset. Notes ----- Once the helper is instantiated, the data associated with the helper can be set/changed with: >>> helper = AttributeLimitsHelper(...) >>> helper.data = data The data can also be passed to the initializer as described in the list of parameters above. """ component_data = CurrentComboDataProperty('component_id_combo') scale_mode = CurrentComboTextProperty('mode_combo') percentile = CurrentComboDataProperty('mode_combo') vlo = FloatLineProperty('lower_value') vhi = FloatLineProperty('upper_value') vlog = ButtonProperty('log_button') def __init__(self, attribute_combo, lower_value, upper_value, mode_combo=None, flip_button=None, log_button=None, data=None, limits_cache=None): self.component_id_combo = attribute_combo self.mode_combo = mode_combo self.lower_value = lower_value self.upper_value = upper_value self.flip_button = flip_button self.log_button = log_button self.component_id_combo.currentIndexChanged.connect( self._update_limits) self.lower_value.editingFinished.connect(self._manual_edit) self.upper_value.editingFinished.connect(self._manual_edit) if self.log_button is None: self.log_button = QtWidgets.QToolButton() self.log_button.toggled.connect(self._manual_edit) if self.mode_combo is None: # Make hidden combo box to avoid having to always figure out if the # combo mode exists. This will then always be set to Min/Max. self.mode_combo = QtWidgets.QComboBox() self._setup_mode_combo() self.mode_combo.currentIndexChanged.connect(self._update_mode) if self.flip_button is not None: self.flip_button.clicked.connect(self._flip_limits) if limits_cache is None: limits_cache = {} self._limits = limits_cache self._callbacks = [] def set_limits(self, vlo, vhi): self.lower_value.blockSignals(True) self.upper_value.blockSignals(True) self.vlo = vlo self.vhi = vhi self.lower_value.blockSignals(False) self.upper_value.blockSignals(False) self.lower_value.editingFinished.emit() self.upper_value.editingFinished.emit() def _setup_mode_combo(self): self.mode_combo.clear() self.mode_combo.addItem("Min/Max", userData=100) self.mode_combo.addItem("99.5%", userData=99.5) self.mode_combo.addItem("99%", userData=99) self.mode_combo.addItem("95%", userData=95) self.mode_combo.addItem("90%", userData=90) self.mode_combo.addItem("Custom", userData=None) self.mode_combo.setCurrentIndex(-1) def _flip_limits(self): self.set_limits(self.vhi, self.vlo) def _manual_edit(self): self._cache_limits() def _update_mode(self): if self.scale_mode != 'Custom': self._auto_limits() self._cache_limits() def _invalidate_cache(self): self._limits.clear() def _cache_limits(self): self._limits[ self.component_id] = self.scale_mode, self.vlo, self.vhi, self.vlog def _update_limits(self): if self.component_id in self._limits: self.scale_mode, lower, upper, self.vlog = self._limits[ self.component_id] self.set_limits(lower, upper) else: self.mode_combo.blockSignals(True) self.scale_mode = 'Min/Max' self.mode_combo.blockSignals(False) self._auto_limits() self.vlog = False @property def component_id(self): if self.component_data is not None: return self.component_data else: return None @property def data(self): if self.component_data is not None: return self.component_data.parent else: return None def _auto_limits(self): if self.component_data is None: return exclude = (100 - self.percentile) / 2. # For subsets in 'data' mode, we want to compute the limits based on # the full dataset, not just the subset. if isinstance(self.data, Subset): data_values = self.data.data[self.component_id] else: data_values = self.data[self.component_id] try: lower = np.nanpercentile(data_values, exclude) upper = np.nanpercentile(data_values, 100 - exclude) except AttributeError: # Numpy < 1.9 data_values = data_values[~np.isnan(data_values)] lower = np.percentile(data_values, exclude) upper = np.percentile(data_values, 100 - exclude) if isinstance(self.data, Subset): lower = 0 self.set_limits(lower, upper)
class QtPlotlyExporter(QtWidgets.QDialog): save_settings = ButtonProperty('checkbox_save') username = TextProperty('text_username') api_key = TextProperty('text_api_key') title = TextProperty('text_title') legend = ButtonProperty('checkbox_legend') def __init__(self, plotly_args=[], plotly_kwargs={}, parent=None): super(QtPlotlyExporter, self).__init__(parent=parent) self.plotly_args = plotly_args self.plotly_kwargs = plotly_kwargs self.ui = load_ui('exporter.ui', self, directory=os.path.dirname(__file__)) self.button_cancel.clicked.connect(self.reject) self.button_export.clicked.connect(self.accept) # Set up radio button groups self._radio_account = QtWidgets.QButtonGroup() self._radio_account.addButton(self.ui.radio_account_glue) self._radio_account.addButton(self.ui.radio_account_config) self._radio_account.addButton(self.ui.radio_account_manual) self._radio_sharing = QtWidgets.QButtonGroup() self._radio_sharing.addButton(self.ui.radio_sharing_public) self._radio_sharing.addButton(self.ui.radio_sharing_secret) self._radio_sharing.addButton(self.ui.radio_sharing_private) # Find out stored credentials (note that this will create the # credentials file if it doesn't already exist) from plotly import plotly credentials = plotly.get_credentials() config_available = credentials['username'] != "" and credentials['api_key'] != "" if config_available: self.ui.radio_account_config.setChecked(True) label = self.ui.radio_account_config.text() self.ui.radio_account_config.setText(label + " (username: {0})".format(credentials['username'])) else: self.ui.radio_account_glue.setChecked(True) self.ui.radio_account_config.setEnabled(False) self.ui.radio_sharing_secret.setChecked(True) self.ui.text_username.textChanged.connect(nonpartial(self._set_manual_mode)) self.ui.text_api_key.textChanged.connect(nonpartial(self._set_manual_mode)) self.ui.radio_account_glue.toggled.connect(nonpartial(self._set_allowed_sharing_modes)) self.set_status('', color='black') self._set_allowed_sharing_modes() def _set_manual_mode(self): self.ui.radio_account_manual.setChecked(True) def _set_allowed_sharing_modes(self): if self.ui.radio_account_glue.isChecked(): self.ui.radio_sharing_public.setChecked(True) self.ui.radio_sharing_secret.setEnabled(False) self.ui.radio_sharing_private.setEnabled(False) else: self.ui.radio_sharing_secret.setEnabled(True) self.ui.radio_sharing_private.setEnabled(True) QtWidgets.QApplication.instance().processEvents() def accept(self): # In future we might be able to use more fine-grained exceptions # https://github.com/plotly/plotly.py/issues/524 self.set_status('Signing in and plotting...', color='blue') auth = {} if self.ui.radio_account_glue.isChecked(): auth['username'] = '******' auth['api_key'] = 't24aweai14' elif self.ui.radio_account_config.isChecked(): auth['username'] = '' auth['api_key'] = '' else: if self.username == "": self.set_status("Username not set", color='red') return elif self.api_key == "": self.set_status("API key not set", color='red') return else: auth['username'] = self.username auth['api_key'] = self.api_key from plotly import plotly from plotly.exceptions import PlotlyError from plotly.tools import set_credentials_file if self.ui.radio_sharing_public.isChecked(): self.plotly_kwargs['sharing'] = 'public' elif self.ui.radio_sharing_secret.isChecked(): self.plotly_kwargs['sharing'] = 'secret' else: self.plotly_kwargs['sharing'] = 'private' # We need to fix URLs, so we can't let plotly open it yet # https://github.com/plotly/plotly.py/issues/526 self.plotly_kwargs['auto_open'] = False # Get title and legend preferences from the window self.plotly_args[0]['layout']['showlegend'] = self.legend self.plotly_args[0]['layout']['title'] = self.title try: plotly.sign_in(auth['username'], auth['api_key']) plotly_url = plotly.plot(*self.plotly_args, **self.plotly_kwargs) except PlotlyError as exc: print("Plotly exception:") print('-' * 60) traceback.print_exc(file=sys.stdout) print('-' * 60) if ('the supplied API key doesn\'t match our records' in exc.args[0] or 'Sign in failed' in exc.args[0]): username = auth['username'] or plotly.get_credentials()['username'] self.set_status("Authentication failed".format(username), color='red') elif "filled its quota of private files" in exc.args[0]: self.set_status("Maximum number of private plots reached", color='red') else: self.set_status("An unexpected error occurred", color='red') return except Exception: print("Plotly exception:") print('-' * 60) traceback.print_exc(file=sys.stdout) print('-' * 60) self.set_status("An unexpected error occurred", color='red') return self.set_status('Exporting succeeded', color='blue') if self.save_settings and self.ui.radio_account_manual.isChecked(): try: set_credentials_file(**auth) except Exception: print("Plotly exception:") print('-' * 60) traceback.print_exc(file=sys.stdout) print('-' * 60) self.set_status('Exporting succeeded (but saving login failed)', color='blue') # We need to fix URL # https://github.com/plotly/plotly.py/issues/526 if self.plotly_kwargs['sharing'] == 'secret': pos = plotly_url.find('?share_key') if pos >= 0: if plotly_url[pos - 1] != '/': plotly_url = plotly_url.replace('?share_key', '/?share_key') print("Plotly URL: {0}".format(plotly_url)) webbrowser.open_new_tab(plotly_url) super(QtPlotlyExporter, self).accept() def set_status(self, text, color): self.ui.text_status.setText(text) self.ui.text_status.setStyleSheet("color: {0}".format(color)) QtWidgets.QApplication.instance().processEvents()
class VispyOptionsWidget(QtWidgets.QWidget): x_att = CurrentComboProperty('ui.combo_x_attribute') x_min = FloatLineProperty('ui.value_x_min') x_max = FloatLineProperty('ui.value_x_max') x_stretch = FloatLineProperty('ui.value_x_stretch') y_att = CurrentComboProperty('ui.combo_y_attribute') y_min = FloatLineProperty('ui.value_y_min') y_max = FloatLineProperty('ui.value_y_max') y_stretch = FloatLineProperty('ui.value_y_stretch') z_att = CurrentComboProperty('ui.combo_z_attribute') z_min = FloatLineProperty('ui.value_z_min') z_max = FloatLineProperty('ui.value_z_max') z_stretch = FloatLineProperty('ui.value_z_stretch') visible_box = ButtonProperty('ui.checkbox_axes') perspective_view = ButtonProperty('ui.checkbox_perspective') def __init__(self, parent=None, vispy_widget=None, data_viewer=None): super(VispyOptionsWidget, self).__init__(parent=parent) self.ui = load_ui('viewer_options.ui', self, directory=os.path.dirname(__file__)) self._vispy_widget = vispy_widget vispy_widget.options = self self._data_viewer = data_viewer self.stretch_sliders = [self.ui.slider_x_stretch, self.ui.slider_y_stretch, self.ui.slider_z_stretch] self.stretch_values = [self.ui.value_x_stretch, self.ui.value_y_stretch, self.ui.value_z_stretch] self._event_lock = False for slider, label in zip(self.stretch_sliders, self.stretch_values): slider.valueChanged.connect(partial(self._update_labels_from_sliders, label, slider)) label.editingFinished.connect(partial(self._update_sliders_from_labels, slider, label)) label.setText('1.0') label.editingFinished.emit() slider.valueChanged.connect(self._update_stretch) connect_bool_button(self._vispy_widget, 'visible_axes', self.ui.checkbox_axes) connect_bool_button(self._vispy_widget, 'perspective_view', self.ui.checkbox_perspective) if self._data_viewer is not None: self.ui.combo_x_attribute.currentIndexChanged.connect(self._data_viewer._update_attributes) self.ui.combo_y_attribute.currentIndexChanged.connect(self._data_viewer._update_attributes) self.ui.combo_z_attribute.currentIndexChanged.connect(self._data_viewer._update_attributes) self.ui.combo_x_attribute.currentIndexChanged.connect(self._update_attribute_limits) self.ui.combo_y_attribute.currentIndexChanged.connect(self._update_attribute_limits) self.ui.combo_z_attribute.currentIndexChanged.connect(self._update_attribute_limits) self.ui.value_x_min.editingFinished.connect(self._update_limits) self.ui.value_y_min.editingFinished.connect(self._update_limits) self.ui.value_z_min.editingFinished.connect(self._update_limits) self.ui.value_x_max.editingFinished.connect(self._update_limits) self.ui.value_y_max.editingFinished.connect(self._update_limits) self.ui.value_z_max.editingFinished.connect(self._update_limits) self.ui.button_flip_x.clicked.connect(self._flip_x) self.ui.button_flip_y.clicked.connect(self._flip_y) self.ui.button_flip_z.clicked.connect(self._flip_z) self.ui.reset_button.clicked.connect(self._vispy_widget._reset_view) self._components = {} self._set_attributes_enabled(False) self._set_limits_enabled(False) self._first_attributes = True def set_limits(self, x_min, x_max, y_min, y_max, z_min, z_max): self._set_limits_enabled(False) self.x_min = x_min self.x_max = x_max self.y_min = y_min self.y_max = y_max self.z_min = z_min self.z_max = z_max self._set_limits_enabled(True) self.ui.value_x_min.editingFinished.emit() def _flip_x(self): self._set_limits_enabled(False) self.x_min, self.x_max = self.x_max, self.x_min self._set_limits_enabled(True) self.ui.value_x_min.editingFinished.emit() def _flip_y(self): self._set_limits_enabled(False) self.y_min, self.y_max = self.y_max, self.y_min self._set_limits_enabled(True) self.ui.value_y_min.editingFinished.emit() def _flip_z(self): self._set_limits_enabled(False) self.z_min, self.z_max = self.z_max, self.z_min self._set_limits_enabled(True) self.ui.value_z_min.editingFinished.emit() def _set_attributes_enabled(self, value): self.ui.combo_x_attribute.setEnabled(value) self.ui.combo_y_attribute.setEnabled(value) self.ui.combo_z_attribute.setEnabled(value) self.ui.combo_x_attribute.blockSignals(not value) self.ui.combo_y_attribute.blockSignals(not value) self.ui.combo_z_attribute.blockSignals(not value) def _set_limits_enabled(self, value): self.ui.value_x_min.setEnabled(value) self.ui.value_y_min.setEnabled(value) self.ui.value_z_min.setEnabled(value) self.ui.value_x_max.setEnabled(value) self.ui.value_y_max.setEnabled(value) self.ui.value_z_max.setEnabled(value) self.ui.value_x_min.blockSignals(not value) self.ui.value_y_min.blockSignals(not value) self.ui.value_z_min.blockSignals(not value) self.ui.value_x_max.blockSignals(not value) self.ui.value_y_max.blockSignals(not value) self.ui.value_z_max.blockSignals(not value) def _update_attributes_from_data(self, data): components = data.visible_components for component_id in components: component = data.get_component(component_id) if component.categorical: continue if self.ui.combo_x_attribute.findData(component_id) == -1: self.ui.combo_x_attribute.addItem(component_id.label, userData=component_id) if self.ui.combo_y_attribute.findData(component_id) == -1: self.ui.combo_y_attribute.addItem(component_id.label, userData=component_id) if self.ui.combo_z_attribute.findData(component_id) == -1: self.ui.combo_z_attribute.addItem(component_id.label, userData=component_id) self._components[component_id] = component if self._first_attributes: n_max = len(components) self.ui.combo_x_attribute.setCurrentIndex(0) self.ui.combo_y_attribute.setCurrentIndex(min(1, n_max - 1)) self.ui.combo_z_attribute.setCurrentIndex(min(2, n_max - 1)) self._set_attributes_enabled(True) self._first_attributes = False self._update_attribute_limits() def _update_attribute_limits(self): if not hasattr(self, '_limits'): self._limits = {} self._set_limits_enabled(False) if self.x_att not in self._limits: data = self._components[self.x_att].data self._limits[self.x_att] = np.nanmin(data), np.nanmax(data) self.x_min, self.x_max = self._limits[self.x_att] if self.y_att not in self._limits: data = self._components[self.y_att].data self._limits[self.y_att] = np.nanmin(data), np.nanmax(data) self.y_min, self.y_max = self._limits[self.y_att] if self.z_att not in self._limits: data = self._components[self.z_att].data self._limits[self.z_att] = np.nanmin(data), np.nanmax(data) self.z_min, self.z_max = self._limits[self.z_att] self._set_limits_enabled(True) self.ui.value_x_min.editingFinished.emit() def _update_limits(self): if not hasattr(self, '_limits'): self._limits = {} self._limits[self.x_att] = self.x_min, self.x_max self._limits[self.y_att] = self.y_min, self.y_max self._limits[self.z_att] = self.z_min, self.z_max self._vispy_widget._update_limits() def _update_stretch(self): self._vispy_widget._update_stretch(self.x_stretch, self.y_stretch, self.z_stretch) def _update_labels_from_sliders(self, label, slider): if self._event_lock: return # prevent infinite event loop self._event_lock = True try: label.setText("{0:6.2f}".format(10 ** (slider.value() / 1e4))) finally: self._event_lock = False def _update_sliders_from_labels(self, slider, label): if self._event_lock: return # prevent infinite event loop self._event_lock = True try: slider.setValue(1e4 * math.log10(float(label.text()))) finally: self._event_lock = False def __gluestate__(self, context): return dict(x_att=context.id(self.x_att), x_min=self.x_min, x_max=self.x_max, x_stretch=self.x_stretch, y_att=context.id(self.y_att), y_min=self.y_min, y_max=self.y_max, y_stretch=self.y_stretch, z_att=context.id(self.z_att), z_min=self.z_min, z_max=self.z_max, z_stretch=self.z_stretch, visible_box=self.visible_box, perspective_view=self.perspective_view)
class SubsetFacet(QtGui.QDialog): log = ButtonProperty('ui.checkbox_log') vmin = FloatLineProperty('ui.value_min') vmax = FloatLineProperty('ui.value_max') steps = ValueProperty('ui.value_n_subsets') data = Pointer('ui.component_selector.data') component = Pointer('ui.component_selector.component') def __init__(self, collect, default=None, parent=None): """Create a new dialog for subset faceting :param collect: The :class:`~glue.core.data_collection.DataCollection` to use :param default: The default dataset in the collection (optional) """ super(SubsetFacet, self).__init__(parent=parent) self.ui = load_ui('subset_facet.ui', self, directory=os.path.dirname(__file__)) self.ui.setWindowTitle("Subset Facet") self._collect = collect self.ui.component_selector.setup(self._collect) if default is not None: self.ui.component_selector.data = default val = QtGui.QDoubleValidator(-1e100, 1e100, 4, None) self.ui.component_selector.component_changed.connect(self._set_limits) combo = self.ui.color_scale for cmap in [cm.cool, cm.RdYlBu, cm.RdYlGn, cm.RdBu, cm.Purples]: combo.addItem(QtGui.QIcon(cmap2pixmap(cmap)), cmap.name, cmap) def _set_limits(self): data = self.ui.component_selector.data cid = self.ui.component_selector.component vals = data[cid] wmin = self.ui.value_min wmax = self.ui.value_max wmin.setText(pretty_number(np.nanmin(vals))) wmax.setText(pretty_number(np.nanmax(vals))) @property def cmap(self): combo = self.ui.color_scale index = combo.currentIndex() return combo.itemData(index) def _apply(self): try: lo, hi = self.vmin, self.vmax except ValueError: return # limits not set. Abort if not np.isfinite(lo) or not np.isfinite(hi): return subsets = facet_subsets(self._collect, self.component, lo=lo, hi=hi, steps=self.steps, log=self.log) colorize_subsets(subsets, self.cmap) @classmethod def facet(cls, collect, default=None, parent=None): """Class method to create facted subsets The arguments are the same as __init__ """ self = cls(collect, parent=parent, default=default) value = self.exec_() if value == QtGui.QDialog.Accepted: self._apply()
class ImageWidgetBase(DataViewer): """ Widget for ImageClient This base class avoids any matplotlib-specific logic """ LABEL = "Image Viewer" _property_set = DataViewer._property_set + \ 'data attribute rgb_mode rgb_viz ratt gatt batt slice'.split() attribute = CurrentComboProperty('ui.attributeComboBox', 'Current attribute') data = CurrentComboProperty('ui.displayDataCombo', 'Current data') aspect_ratio = CurrentComboProperty('ui.aspectCombo', 'Aspect ratio for image') rgb_mode = ButtonProperty('ui.rgb', 'RGB Mode?') rgb_viz = Pointer('ui.rgb_options.rgb_visible') _layer_style_widget_cls = {ScatterLayerArtist: ScatterLayerStyleWidget} def __init__(self, session, parent=None): super(ImageWidgetBase, self).__init__(session, parent) self._setup_widgets() self.client = self.make_client() self._connect() def _setup_widgets(self): self.central_widget = self.make_central_widget() self.label_widget = QtWidgets.QLabel("", self.central_widget) self.setCentralWidget(self.central_widget) self.option_widget = QtWidgets.QWidget() self.ui = load_ui('options_widget.ui', self.option_widget, directory=os.path.dirname(__file__)) self.ui.slice = DataSlice() self.ui.slice_layout.addWidget(self.ui.slice) self._tweak_geometry() self.ui.aspectCombo.addItem("Square Pixels", userData='equal') self.ui.aspectCombo.addItem("Automatic", userData='auto') def make_client(self): """ Instantiate and return an ImageClient subclass """ raise NotImplementedError() def make_central_widget(self): """ Create and return the central widget to display the image """ raise NotImplementedError() def _tweak_geometry(self): self.central_widget.resize(600, 400) self.resize(self.central_widget.size()) self.ui.rgb_options.hide() self.statusBar().setSizeGripEnabled(False) self.setFocusPolicy(Qt.StrongFocus) @defer_draw def add_data(self, data): """ Add a new dataset to the viewer """ # overloaded from DataViewer # need to delay callbacks, otherwise might # try to set combo boxes to nonexisting items with delay_callback(self.client, 'display_data', 'display_attribute'): # If there is not already any image data set, we can't add 1-D # datasets (tables/catalogs) to the image widget yet. if data.data.ndim == 1 and self.client.display_data is None: QtWidgets.QMessageBox.information( self.window(), "Note", "Cannot create image viewer from a 1-D " "dataset. You will need to first " "create an image viewer using data " "with 2 or more dimensions, after " "which you will be able to overlay 1-D " "data as a scatter plot.", buttons=QtWidgets.QMessageBox.Ok) return r = self.client.add_layer(data) if r is not None and self.client.display_data is not None: self.add_data_to_combo(data) if self.client.can_image_data(data): self.client.display_data = data self.set_attribute_combo(self.client.display_data) return r is not None @defer_draw def add_subset(self, subset): self.client.add_scatter_layer(subset) assert subset in self.client.artists def add_data_to_combo(self, data): """ Add a data object to the combo box, if not already present """ if not self.client.can_image_data(data): return combo = self.ui.displayDataCombo try: pos = _find_combo_data(combo, data) except ValueError: combo.addItem(data.label, userData=data) @property def ratt(self): """ComponentID assigned to R channel in RGB Mode""" return self.ui.rgb_options.attributes[0] @ratt.setter def ratt(self, value): att = list(self.ui.rgb_options.attributes) att[0] = value self.ui.rgb_options.attributes = att @property def gatt(self): """ComponentID assigned to G channel in RGB Mode""" return self.ui.rgb_options.attributes[1] @gatt.setter def gatt(self, value): att = list(self.ui.rgb_options.attributes) att[1] = value self.ui.rgb_options.attributes = att @property def batt(self): """ComponentID assigned to B channel in RGB Mode""" return self.ui.rgb_options.attributes[2] @batt.setter def batt(self, value): att = list(self.ui.rgb_options.attributes) att[2] = value self.ui.rgb_options.attributes = att @property def slice(self): return self.client.slice @slice.setter def slice(self, value): self.client.slice = value def set_attribute_combo(self, data): """ Update attribute combo box to reflect components in data""" labeldata = ((f.label, f) for f in data.visible_components) update_combobox(self.ui.attributeComboBox, labeldata) def _connect(self): ui = self.ui ui.monochrome.toggled.connect(self._update_rgb_console) ui.rgb_options.colors_changed.connect(self.update_window_title) # sync client and widget slices ui.slice.slice_changed.connect( lambda: setattr(self, 'slice', self.ui.slice.slice)) update_ui_slice = lambda val: setattr(ui.slice, 'slice', val) add_callback(self.client, 'slice', update_ui_slice) add_callback(self.client, 'display_data', self.ui.slice.set_data) # sync window title to data/attribute add_callback(self.client, 'display_data', nonpartial(self._display_data_changed)) add_callback(self.client, 'display_attribute', nonpartial(self._display_attribute_changed)) add_callback(self.client, 'display_aspect', nonpartial(self.client._update_aspect)) # sync data/attribute combos with client properties connect_current_combo(self.client, 'display_data', self.ui.displayDataCombo) connect_current_combo(self.client, 'display_attribute', self.ui.attributeComboBox) connect_current_combo(self.client, 'display_aspect', self.ui.aspectCombo) def _display_data_changed(self): if self.client.display_data is None: self.ui.attributeComboBox.clear() return with self.client.artists.ignore_empty(): self.set_attribute_combo(self.client.display_data) self.client.add_layer(self.client.display_data) self.client._update_and_redraw() self.update_window_title() def _display_attribute_changed(self): if self.client.display_attribute is None: return self.client._update_and_redraw() self.update_window_title() @defer_draw def _update_rgb_console(self, is_monochrome): if is_monochrome: self.ui.rgb_options.hide() self.ui.mono_att_label.show() self.ui.attributeComboBox.show() self.client.rgb_mode(False) else: self.ui.mono_att_label.hide() self.ui.attributeComboBox.hide() self.ui.rgb_options.show() rgb = self.client.rgb_mode(True) if rgb is not None: self.ui.rgb_options.artist = rgb def register_to_hub(self, hub): super(ImageWidgetBase, self).register_to_hub(hub) self.client.register_to_hub(hub) dc_filt = lambda x: x.sender is self.client._data display_data_filter = lambda x: x.data is self.client.display_data hub.subscribe(self, core.message.DataCollectionAddMessage, handler=lambda x: self.add_data_to_combo(x.data), filter=dc_filt) hub.subscribe(self, core.message.DataCollectionDeleteMessage, handler=lambda x: self.remove_data_from_combo(x.data), filter=dc_filt) hub.subscribe(self, core.message.DataUpdateMessage, handler=lambda x: self._sync_data_labels()) hub.subscribe(self, core.message.ComponentsChangedMessage, handler=lambda x: self.set_attribute_combo(x.data), filter=display_data_filter) def unregister(self, hub): super(ImageWidgetBase, self).unregister(hub) for obj in [self, self.client]: hub.unsubscribe_all(obj) def remove_data_from_combo(self, data): """ Remove a data object from the combo box, if present """ combo = self.ui.displayDataCombo pos = combo.findText(data.label) if pos >= 0: combo.removeItem(pos) def _set_norm(self, mode): """ Use the `ContrastMouseMode` to adjust the transfer function """ # at least one of the clip/vmin pairs will be None clip_lo, clip_hi = mode.get_clip_percentile() vmin, vmax = mode.get_vmin_vmax() stretch = mode.stretch return self.client.set_norm(clip_lo=clip_lo, clip_hi=clip_hi, stretch=stretch, vmin=vmin, vmax=vmax, bias=mode.bias, contrast=mode.contrast) @property def window_title(self): if self.client.display_data is None or self.client.display_attribute is None: title = '' else: data = self.client.display_data.label a = self.client.rgb_mode() if a is None: # monochrome mode title = "%s - %s" % (self.client.display_data.label, self.client.display_attribute.label) else: r = a.r.label if a.r is not None else '' g = a.g.label if a.g is not None else '' b = a.b.label if a.b is not None else '' title = "%s Red = %s Green = %s Blue = %s" % (data, r, g, b) return title def _sync_data_combo_labels(self): combo = self.ui.displayDataCombo for i in range(combo.count()): combo.setItemText(i, combo.itemData(i).label) def _sync_data_labels(self): self.update_window_title() self._sync_data_combo_labels() def __str__(self): return "Image Widget" def _confirm_large_image(self, data): """Ask user to confirm expensive operations :rtype: bool. Whether the user wishes to continue """ warn_msg = ("WARNING: Image has %i pixels, and may render slowly." " Continue?" % data.size) title = "Contour large image?" ok = QtWidgets.QMessageBox.Ok cancel = QtWidgets.QMessageBox.Cancel buttons = ok | cancel result = QtWidgets.QMessageBox.question(self, title, warn_msg, buttons=buttons, defaultButton=cancel) return result == ok def options_widget(self): return self.option_widget @defer_draw def restore_layers(self, rec, context): with delay_callback(self.client, 'display_data', 'display_attribute'): self.client.restore_layers(rec, context) for artist in self.layers: self.add_data_to_combo(artist.layer.data) self.set_attribute_combo(self.client.display_data) self._sync_data_combo_labels() def closeEvent(self, event): # close window and all plugins super(ImageWidgetBase, self).closeEvent(event)
class ScatterWidget(DataViewer): """ An interactive scatter plot. """ LABEL = "Scatter Plot" _property_set = DataViewer._property_set + \ 'xlog ylog xflip yflip hidden xatt yatt xmin xmax ymin ymax'.split() xlog = ButtonProperty('ui.xLogCheckBox', 'log scaling on x axis?') ylog = ButtonProperty('ui.yLogCheckBox', 'log scaling on y axis?') xflip = ButtonProperty('ui.xFlipCheckBox', 'invert the x axis?') yflip = ButtonProperty('ui.yFlipCheckBox', 'invert the y axis?') xmin = FloatLineProperty('ui.xmin', 'Lower x limit of plot') xmax = FloatLineProperty('ui.xmax', 'Upper x limit of plot') ymin = FloatLineProperty('ui.ymin', 'Lower y limit of plot') ymax = FloatLineProperty('ui.ymax', 'Upper y limit of plot') hidden = ButtonProperty('ui.hidden_attributes', 'Show hidden attributes') xatt = CurrentComboProperty('ui.xAxisComboBox', 'Attribute to plot on x axis') yatt = CurrentComboProperty('ui.yAxisComboBox', 'Attribute to plot on y axis') def __init__(self, session, parent=None): super(ScatterWidget, self).__init__(session, parent) self.central_widget = MplWidget() self.option_widget = QtGui.QWidget() self.setCentralWidget(self.central_widget) self.ui = load_ui('options_widget.ui', self.option_widget, directory=os.path.dirname(__file__)) self._tweak_geometry() self.client = ScatterClient( self._data, self.central_widget.canvas.fig, layer_artist_container=self._layer_artist_container) self._connect() self.unique_fields = set() tb = self.make_toolbar() cache_axes(self.client.axes, tb) self.statusBar().setSizeGripEnabled(False) self.setFocusPolicy(Qt.StrongFocus) @staticmethod def _get_default_tools(): return [] def _tweak_geometry(self): self.central_widget.resize(600, 400) self.resize(self.central_widget.size()) def _connect(self): ui = self.ui cl = self.client connect_bool_button(cl, 'xlog', ui.xLogCheckBox) connect_bool_button(cl, 'ylog', ui.yLogCheckBox) connect_bool_button(cl, 'xflip', ui.xFlipCheckBox) connect_bool_button(cl, 'yflip', ui.yFlipCheckBox) ui.xAxisComboBox.currentIndexChanged.connect(self.update_xatt) ui.yAxisComboBox.currentIndexChanged.connect(self.update_yatt) ui.hidden_attributes.toggled.connect(lambda x: self._update_combos()) ui.swapAxes.clicked.connect(nonpartial(self.swap_axes)) ui.snapLimits.clicked.connect(cl.snap) connect_float_edit(cl, 'xmin', ui.xmin) connect_float_edit(cl, 'xmax', ui.xmax) connect_float_edit(cl, 'ymin', ui.ymin) connect_float_edit(cl, 'ymax', ui.ymax) def make_toolbar(self): result = GlueToolbar(self.central_widget.canvas, self, name='Scatter Plot') for mode in self._mouse_modes(): result.add_mode(mode) self.addToolBar(result) return result def _mouse_modes(self): axes = self.client.axes def apply_mode(mode): return self.apply_roi(mode.roi()) rect = RectangleMode(axes, roi_callback=apply_mode) xra = HRangeMode(axes, roi_callback=apply_mode) yra = VRangeMode(axes, roi_callback=apply_mode) circ = CircleMode(axes, roi_callback=apply_mode) poly = PolyMode(axes, roi_callback=apply_mode) return [rect, xra, yra, circ, poly] @defer_draw def _update_combos(self): """ Update contents of combo boxes """ # have to be careful here, since client and/or widget # are potentially out of sync layer_ids = [] # show hidden attributes if needed if ((self.client.xatt and self.client.xatt.hidden) or (self.client.yatt and self.client.yatt.hidden)): self.hidden = True # determine which components to put in combos for l in self.client.data: if not self.client.is_layer_present(l): continue for lid in self.client.plottable_attributes( l, show_hidden=self.hidden): if lid not in layer_ids: layer_ids.append(lid) oldx = self.xatt oldy = self.yatt newx = self.client.xatt or oldx newy = self.client.yatt or oldy for combo, target in zip( [self.ui.xAxisComboBox, self.ui.yAxisComboBox], [newx, newy]): combo.blockSignals(True) combo.clear() if not layer_ids: # empty component list continue # populate for lid in layer_ids: combo.addItem(lid.label, userData=lid) idx = layer_ids.index(target) if target in layer_ids else 0 combo.setCurrentIndex(idx) combo.blockSignals(False) # ensure client and widget synced self.client.xatt = self.xatt self.client.lyatt = self.yatt @defer_draw def add_data(self, data): """Add a new data set to the widget :returns: True if the addition was expected, False otherwise """ if self.client.is_layer_present(data): return if data.size > WARN_SLOW and not self._confirm_large_data(data): return False first_layer = self.client.layer_count == 0 self.client.add_data(data) self._update_combos() if first_layer: # forces both x and y axes to be rescaled self.update_xatt(None) self.update_yatt(None) self.ui.xAxisComboBox.setCurrentIndex(0) if len(data.visible_components) > 1: self.ui.yAxisComboBox.setCurrentIndex(1) else: self.ui.yAxisComboBox.setCurrentIndex(0) self.update_window_title() return True @defer_draw def add_subset(self, subset): """Add a subset to the widget :returns: True if the addition was accepted, False otherwise """ if self.client.is_layer_present(subset): return data = subset.data if data.size > WARN_SLOW and not self._confirm_large_data(data): return False first_layer = self.client.layer_count == 0 self.client.add_layer(subset) self._update_combos() if first_layer: # forces both x and y axes to be rescaled self.update_xatt(None) self.update_yatt(None) self.ui.xAxisComboBox.setCurrentIndex(0) if len(data.visible_components) > 1: self.ui.yAxisComboBox.setCurrentIndex(1) else: self.ui.yAxisComboBox.setCurrentIndex(0) self.update_window_title() return True def register_to_hub(self, hub): super(ScatterWidget, self).register_to_hub(hub) self.client.register_to_hub(hub) hub.subscribe(self, core.message.DataUpdateMessage, nonpartial(self._sync_labels)) hub.subscribe(self, core.message.ComponentsChangedMessage, nonpartial(self._update_combos)) hub.subscribe(self, core.message.ComponentReplacedMessage, self._on_component_replace) def _on_component_replace(self, msg): # let client update its state first self.client._on_component_replace(msg) self._update_combos() def unregister(self, hub): super(ScatterWidget, self).unregister(hub) hub.unsubscribe_all(self.client) hub.unsubscribe_all(self) @defer_draw def swap_axes(self): xid = self.ui.xAxisComboBox.currentIndex() yid = self.ui.yAxisComboBox.currentIndex() xlog = self.ui.xLogCheckBox.isChecked() ylog = self.ui.yLogCheckBox.isChecked() xflip = self.ui.xFlipCheckBox.isChecked() yflip = self.ui.yFlipCheckBox.isChecked() self.ui.xAxisComboBox.setCurrentIndex(yid) self.ui.yAxisComboBox.setCurrentIndex(xid) self.ui.xLogCheckBox.setChecked(ylog) self.ui.yLogCheckBox.setChecked(xlog) self.ui.xFlipCheckBox.setChecked(yflip) self.ui.yFlipCheckBox.setChecked(xflip) @defer_draw def update_xatt(self, index): component_id = self.xatt self.client.xatt = component_id @defer_draw def update_yatt(self, index): component_id = self.yatt self.client.yatt = component_id @property def window_title(self): data = self.client.data label = ', '.join([d.label for d in data if self.client.is_visible(d)]) return label def _sync_labels(self): self.update_window_title() def options_widget(self): return self.option_widget @defer_draw def restore_layers(self, rec, context): self.client.restore_layers(rec, context) self._update_combos() # manually force client attributes to sync self.update_xatt(None) self.update_yatt(None)
class WWTOptionPanel(QtWidgets.QWidget): ra_att = CurrentComboDataProperty('ui.combo_ra_att') dec_att = CurrentComboDataProperty('ui.combo_dec_att') background = CurrentComboDataProperty('ui.combo_background') opacity = ValueProperty('ui.value_opacity') foreground = CurrentComboDataProperty('ui.combo_foreground') galactic_plane = ButtonProperty('ui.checkbox_galactic_plane') def __init__(self, viewer, parent=None): super(WWTOptionPanel, self).__init__(parent=parent) self.viewer = viewer self.ui = load_ui('options_widget.ui', self, directory=os.path.dirname(__file__)) self._setup_combos() self._connect() @property def ra(self): if self.ra_att is None: return None else: return self.ra_att[0] @property def dec(self): if self.dec_att is None: return None else: return self.dec_att[0] def _setup_combos(self): layers = [ 'Digitized Sky Survey (Color)', 'VLSS: VLA Low-frequency Sky Survey (Radio)', 'WMAP ILC 5-Year Cosmic Microwave Background', 'SFD Dust Map (Infrared)', 'WISE All Sky (Infrared)', 'GLIMPSE/MIPSGAL', 'Hydrogen Alpha Full Sky Map' ] labels = ['DSS', 'VLSS', 'WMAP', 'SFD', 'WISE', 'GLIMPSE', 'H Alpha'] thumbnails = [ 'DSS', 'VLA', 'wmap5yr_ilc_200uk', 'dust', 'glimpsemipsgaltn', 'halpha' ] base = ('http://www.worldwidetelescope.org/wwtweb/' 'thumbnail.aspx?name=%s') for i, row in enumerate(zip(layers, labels, thumbnails)): layer, text, thumb = row url = base % thumb data = urlopen(url).read() pm = QtGui.QPixmap() pm.loadFromData(data) icon = QtGui.QIcon(pm) self.ui.combo_foreground.addItem(icon, text, layer) self.ui.combo_foreground.setItemData(i, layer, role=Qt.ToolTipRole) self.ui.combo_background.addItem(icon, text, layer) self.ui.combo_background.setItemData(i, layer, role=Qt.ToolTipRole) self.ui.combo_foreground.setIconSize(QtCore.QSize(60, 60)) self.ui.combo_background.setIconSize(QtCore.QSize(60, 60)) self.ra_att_helper = ComponentIDComboHelper(self.ui.combo_ra_att, self.viewer._data, categorical=False, numeric=True) self.dec_att_helper = ComponentIDComboHelper(self.ui.combo_dec_att, self.viewer._data, categorical=False, numeric=True) def add_data(self, data): # TODO: the following logic should go in the component ID helpers. It # isn't quite right at the moment because if there are multiple # datasets/subsets with the same components, we only want to show those # once. if isinstance(data, Subset): self.ra_att_helper.append(data.data) self.dec_att_helper.append(data.data) else: self.ra_att_helper.append(data) self.dec_att_helper.append(data) def remove_data(self, data): if isinstance(data, Subset): self.ra_att_helper.remove(data.data) self.dec_att_helper.remove(data.data) else: self.ra_att_helper.remove(data) self.dec_att_helper.remove(data) def _connect(self): self.ui.combo_ra_att.currentIndexChanged.connect( self.viewer._update_all) self.ui.combo_dec_att.currentIndexChanged.connect( self.viewer._update_all) self.ui.combo_foreground.currentIndexChanged.connect( self.viewer._update_foreground) self.ui.combo_background.currentIndexChanged.connect( self.viewer._update_background) self.ui.value_opacity.valueChanged.connect(self.viewer._update_opacity) self.ui.checkbox_galactic_plane.toggled.connect( self.viewer._update_galactic_plane_mode) self.opacity = 100
class VolumeLayerStyleWidget(QtGui.QWidget): # GUI elements attribute = CurrentComboProperty('ui.combo_attribute') vmin = FloatLineProperty('ui.value_min') vmax = FloatLineProperty('ui.value_max') alpha = ValueProperty('ui.slider_alpha') subset_outline = ButtonProperty('ui.radio_subset_outline') subset_data = ButtonProperty('ui.radio_subset_data') def __init__(self, layer_artist): super(VolumeLayerStyleWidget, self).__init__() self.ui = load_ui('layer_style_widget.ui', self, directory=os.path.dirname(__file__)) self.layer_artist = layer_artist self.layer = layer_artist.layer # Set up attribute and visual options self._setup_options() self._connect_global() # Set initial values self.layer_artist.color = self.layer.style.color self.layer_artist.alpha = self.layer.style.alpha with delay_callback(self.layer_artist, 'attribute'): self.attribute = self.visible_components[0] self._update_limits() if isinstance(self.layer, Subset): self.ui.radio_subset_data.setChecked(True) self.layer_artist.visible = True def _connect_global(self): connect_color(self.layer.style, 'color', self.ui.label_color) connect_value(self.layer.style, 'alpha', self.ui.slider_alpha, value_range=(0, 1)) def _setup_options(self): """ Set up the combo box with the list of attributes """ # Set up radio buttons for subset mode selection if this is a subset if isinstance(self.layer, Subset): self._radio_size = QtGui.QButtonGroup() self._radio_size.addButton(self.ui.radio_subset_outline) self._radio_size.addButton(self.ui.radio_subset_data) else: self.ui.radio_subset_outline.hide() self.ui.radio_subset_data.hide() self.ui.label_subset_mode.hide() # Set up attribute list label_data = [(comp.label, comp) for comp in self.visible_components] update_combobox(self.ui.combo_attribute, label_data) # Set up connections with layer artist connect_current_combo(self.layer_artist, 'attribute', self.ui.combo_attribute) connect_float_edit(self.layer_artist, 'vmin', self.ui.value_min) connect_float_edit(self.layer_artist, 'vmax', self.ui.value_max) connect_color(self.layer_artist, 'color', self.ui.label_color) connect_value(self.layer_artist, 'alpha', self.ui.slider_alpha, value_range=(0, 1)) # Set up internal connections self.ui.radio_subset_outline.toggled.connect(self._update_subset_mode) self.ui.radio_subset_data.toggled.connect(self._update_subset_mode) self.ui.value_min.editingFinished.connect(self._cache_limits) self.ui.value_max.editingFinished.connect(self._cache_limits) self.ui.combo_attribute.currentIndexChanged.connect( self._update_limits) def _update_subset_mode(self): if self.ui.radio_subset_outline.isChecked(): self.layer_artist.subset_mode = 'outline' else: self.layer_artist.subset_mode = 'data' self._update_limits() def _update_limits(self): if isinstance(self.layer, Subset): if self.layer_artist.subset_mode == 'outline': self.ui.value_min.setEnabled(False) self.ui.value_max.setEnabled(False) self.vmin, self.vmax = 0, 2 return else: self.ui.value_min.setEnabled(False) self.ui.value_max.setEnabled(True) if not hasattr(self, '_limits'): self._limits = {} if self.attribute in self._limits: self.vmin, self.vmax = self._limits[self.attribute] else: self.vmin, self.vmax = self.default_limits(self.attribute) self._limits[self.attribute] = self.vmin, self.vmax def _cache_limits(self): if not isinstance(self.layer, Subset) or self.layer_artist.subset_mode == 'data': self._limits[self.attribute] = self.vmin, self.vmax def default_limits(self, attribute): # For subsets, we want to compute the limits based on the full # dataset not just the subset. if isinstance(self.layer, Subset): vmin = 0 vmax = np.nanmax(self.layer.data[attribute]) else: vmin = np.nanmin(self.layer[attribute]) vmax = np.nanmax(self.layer[attribute]) return vmin, vmax @property def visible_components(self): if isinstance(self.layer, Subset): return self.layer.data.visible_components else: return self.layer.visible_components
class PreferencesDialog(QtGui.QDialog): theme = CurrentComboTextProperty('ui.combo_theme') background = ColorProperty('ui.color_background') foreground = ColorProperty('ui.color_foreground') data_color = ColorProperty('ui.color_default_data') data_alpha = ValueProperty('ui.slider_alpha', value_range=(0, 1)) data_apply = ButtonProperty('ui.checkbox_apply') save_to_disk = ButtonProperty('ui.checkbox_save') def __init__(self, application, parent=None): super(PreferencesDialog, self).__init__(parent=parent) self.app = application self.ui = load_ui('preferences.ui', self, directory=os.path.dirname(__file__)) self.ui.cancel.clicked.connect(self.reject) self.ui.ok.clicked.connect(self.accept) self.ui.combo_theme.currentIndexChanged.connect( nonpartial(self._update_colors_from_theme)) from glue.config import settings self.background = settings.BACKGROUND_COLOR self.foreground = settings.FOREGROUND_COLOR self.data_color = settings.DATA_COLOR self.data_alpha = settings.DATA_ALPHA self._update_theme_from_colors() self.panes = [] from glue.config import preference_panes for label, widget_cls in sorted(preference_panes): pane = widget_cls() self.ui.tab_widget.addTab(pane, label) self.panes.append(pane) def _update_theme_from_colors(self): if (rgb(self.background) == (1, 1, 1) and rgb(self.foreground) == (0, 0, 0) and rgb(self.data_color) == (0.35, 0.35, 0.35) and np.allclose(self.data_alpha, 0.8)): self.theme = 'Black on White' elif (rgb(self.background) == (0, 0, 0) and rgb(self.foreground) == (1, 1, 1) and rgb(self.data_color) == (0.75, 0.75, 0.75) and np.allclose(self.data_alpha, 0.8)): self.theme = 'White on Black' else: self.theme = 'Custom' def _update_colors_from_theme(self): if self.theme == 'Black on White': self.foreground = 'black' self.background = 'white' self.data_color = '0.35' self.data_alpha = 0.8 elif self.theme == 'White on Black': self.foreground = 'white' self.background = 'black' self.data_color = '0.75' self.data_alpha = 0.8 elif self.theme != 'Custom': raise ValueError("Unknown theme: {0}".format(self.theme)) def accept(self): # Update default settings from glue.config import settings settings.FOREGROUND_COLOR = self.foreground settings.BACKGROUND_COLOR = self.background settings.DATA_COLOR = self.data_color settings.DATA_ALPHA = self.data_alpha for pane in self.panes: pane.finalize() # Save to disk if requested if self.save_to_disk: save_settings() # Trigger viewers to update defaults self.app._hub.broadcast( SettingsChangeMessage(self, ('FOREGROUND_COLOR', 'BACKGROUND_COLOR'))) # If requested, trigger data to update color if self.data_apply: self.app.set_data_color(settings.DATA_COLOR, settings.DATA_ALPHA) super(PreferencesDialog, self).accept()
class SliceWidget(QtWidgets.QWidget): label = TextProperty('_ui_label') slider_label = TextProperty('_ui_slider.label') slider_unit = TextProperty('_ui_slider.text_unit') slice_center = ValueProperty('_ui_slider.slider') mode = CurrentComboProperty('_ui_mode') use_world = ButtonProperty('_ui_slider.checkbox_world') slice_changed = QtCore.Signal(int) mode_changed = QtCore.Signal(str) def __init__(self, label='', world=None, lo=0, hi=10, parent=None, aggregation=None, world_unit=None, world_warning=False): super(SliceWidget, self).__init__(parent) if aggregation is not None: raise NotImplemented("Aggregation option not implemented") self._world = np.asarray(world) self._world_warning = world_warning self._world_unit = world_unit layout = QtWidgets.QVBoxLayout() layout.setContentsMargins(3, 1, 3, 1) layout.setSpacing(0) top = QtWidgets.QHBoxLayout() top.setContentsMargins(3, 3, 3, 3) label = QtWidgets.QLabel(label) top.addWidget(label) mode = QtWidgets.QComboBox() mode.addItem('x', 'x') mode.addItem('y', 'y') mode.addItem('slice', 'slice') mode.currentIndexChanged.connect( lambda x: self.mode_changed.emit(self.mode)) mode.currentIndexChanged.connect(self._update_mode) top.addWidget(mode) layout.addLayout(top) slider = load_ui('data_slice_widget.ui', None, directory=os.path.dirname(__file__)) self._ui_slider = slider font = slider.label_warning.font() font.setPointSize(font.pointSize() * 0.75) slider.label_warning.setFont(font) slider.button_first.setStyleSheet('border: 0px') slider.button_first.setIcon(get_icon('playback_first')) slider.button_prev.setStyleSheet('border: 0px') slider.button_prev.setIcon(get_icon('playback_prev')) slider.button_back.setStyleSheet('border: 0px') slider.button_back.setIcon(get_icon('playback_back')) slider.button_stop.setStyleSheet('border: 0px') slider.button_stop.setIcon(get_icon('playback_stop')) slider.button_forw.setStyleSheet('border: 0px') slider.button_forw.setIcon(get_icon('playback_forw')) slider.button_next.setStyleSheet('border: 0px') slider.button_next.setIcon(get_icon('playback_next')) slider.button_last.setStyleSheet('border: 0px') slider.button_last.setIcon(get_icon('playback_last')) slider.slider.setMinimum(lo) slider.slider.setMaximum(hi) slider.slider.setValue((lo + hi) / 2) slider.slider.valueChanged.connect( lambda x: self.slice_changed.emit(self.mode)) slider.slider.valueChanged.connect( nonpartial(self.set_label_from_slider)) slider.label.setMinimumWidth(80) slider.label.setText(str(slider.slider.value())) slider.label.editingFinished.connect( nonpartial(self.set_slider_from_label)) self._play_timer = QtCore.QTimer() self._play_timer.setInterval(500) self._play_timer.timeout.connect(nonpartial(self._play_slice)) slider.button_first.clicked.connect( nonpartial(self._browse_slice, 'first')) slider.button_prev.clicked.connect( nonpartial(self._browse_slice, 'prev')) slider.button_back.clicked.connect( nonpartial(self._adjust_play, 'back')) slider.button_stop.clicked.connect( nonpartial(self._adjust_play, 'stop')) slider.button_forw.clicked.connect( nonpartial(self._adjust_play, 'forw')) slider.button_next.clicked.connect( nonpartial(self._browse_slice, 'next')) slider.button_last.clicked.connect( nonpartial(self._browse_slice, 'last')) slider.checkbox_world.toggled.connect( nonpartial(self.set_label_from_slider)) if world is None: self.use_world = False slider.checkbox_world.hide() else: self.use_world = not world_warning if world_unit: self.slider_unit = world_unit else: self.slider_unit = '' layout.addWidget(slider) self.setLayout(layout) self._ui_label = label self._ui_mode = mode self._update_mode() self._frozen = False self._play_speed = 0 self.set_label_from_slider() def set_label_from_slider(self): value = self._ui_slider.slider.value() if self.use_world: text = str(self._world[value]) if self._world_warning: self._ui_slider.label_warning.show() else: self._ui_slider.label_warning.hide() self.slider_unit = self._world_unit else: text = str(value) self._ui_slider.label_warning.hide() self.slider_unit = '' self._ui_slider.label.setText(text) def set_slider_from_label(self): text = self._ui_slider.label.text() if self.use_world: # Don't want to assume world is sorted, pick closest value value = np.argmin(np.abs(self._world - float(text))) self._ui_slider.label.setText(str(self._world[value])) else: value = int(text) self._ui_slider.slider.setValue(value) def _adjust_play(self, action): if action == 'stop': self._play_speed = 0 elif action == 'back': if self._play_speed > 0: self._play_speed = -1 else: self._play_speed -= 1 elif action == 'forw': if self._play_speed < 0: self._play_speed = +1 else: self._play_speed += 1 if self._play_speed == 0: self._play_timer.stop() else: self._play_timer.start() self._play_timer.setInterval(500 / abs(self._play_speed)) def _play_slice(self): if self._play_speed > 0: self._browse_slice('next', play=True) elif self._play_speed < 0: self._browse_slice('prev', play=True) def _browse_slice(self, action, play=False): imin = self._ui_slider.slider.minimum() imax = self._ui_slider.slider.maximum() value = self._ui_slider.slider.value() # If this was not called from _play_slice, we should stop the # animation. if not play: self._adjust_play('stop') if action == 'first': value = imin elif action == 'last': value = imax elif action == 'prev': value = value - 1 if value < imin: value = imax elif action == 'next': value = value + 1 if value > imax: value = imin else: raise ValueError("Action should be one of first/prev/next/last") self._ui_slider.slider.setValue(value) def _update_mode(self, *args): if self.mode != 'slice': self._ui_slider.hide() self._adjust_play('stop') else: self._ui_slider.show() def freeze(self): self.mode = 'slice' self._ui_mode.setEnabled(False) self._ui_slider.hide() self._frozen = True @property def frozen(self): return self._frozen
class PreferencesDialog(QtWidgets.QDialog): theme = CurrentComboTextProperty('ui.combo_theme') background = ColorProperty('ui.color_background') foreground = ColorProperty('ui.color_foreground') data_color = ColorProperty('ui.color_default_data') data_alpha = ValueProperty('ui.slider_alpha', value_range=(0, 1)) data_apply = ButtonProperty('ui.checkbox_apply') show_large_data_warning = ButtonProperty( 'ui.checkbox_show_large_data_warning') individual_subset_color = ButtonProperty( 'ui.checkbox_individual_subset_color') save_to_disk = ButtonProperty('ui.checkbox_save') def __init__(self, application, parent=None): super(PreferencesDialog, self).__init__(parent=parent) self.app = application self.ui = load_ui('preferences.ui', self, directory=os.path.dirname(__file__)) self.ui.cancel.clicked.connect(self.reject) self.ui.ok.clicked.connect(self.accept) self.ui.combo_theme.currentIndexChanged.connect( nonpartial(self._update_colors_from_theme)) # The following is needed because of a bug in Qt which means that # tab titles don't get scaled right. app = get_qapp() app_font = app.font() self.ui.tab_widget.setStyleSheet('font-size: {0}px'.format( app_font.pointSize())) from glue.config import settings self.background = settings.BACKGROUND_COLOR self.foreground = settings.FOREGROUND_COLOR self.data_color = settings.DATA_COLOR self.data_alpha = settings.DATA_ALPHA self.show_large_data_warning = settings.SHOW_LARGE_DATA_WARNING self.individual_subset_color = settings.INDIVIDUAL_SUBSET_COLOR self._update_theme_from_colors() self.panes = [] from glue.config import preference_panes for label, widget_cls in sorted(preference_panes): pane = widget_cls() self.ui.tab_widget.addTab(pane, label) self.panes.append(pane) def _update_theme_from_colors(self): if (rgb(self.background) == (1, 1, 1) and rgb(self.foreground) == (0, 0, 0) and rgb(self.data_color) == (0.35, 0.35, 0.35) and np.allclose(self.data_alpha, 0.8)): self.theme = 'Black on White' elif (rgb(self.background) == (0, 0, 0) and rgb(self.foreground) == (1, 1, 1) and rgb(self.data_color) == (0.75, 0.75, 0.75) and np.allclose(self.data_alpha, 0.8)): self.theme = 'White on Black' else: self.theme = 'Custom' def _update_colors_from_theme(self): if self.theme == 'Black on White': self.foreground = 'black' self.background = 'white' self.data_color = '0.35' self.data_alpha = 0.8 elif self.theme == 'White on Black': self.foreground = 'white' self.background = 'black' self.data_color = '0.75' self.data_alpha = 0.8 elif self.theme != 'Custom': raise ValueError("Unknown theme: {0}".format(self.theme)) def accept(self): # Update default settings from glue.config import settings settings.FOREGROUND_COLOR = self.foreground settings.BACKGROUND_COLOR = self.background settings.DATA_COLOR = self.data_color settings.DATA_ALPHA = self.data_alpha settings.SHOW_LARGE_DATA_WARNING = self.show_large_data_warning settings.INDIVIDUAL_SUBSET_COLOR = self.individual_subset_color for pane in self.panes: pane.finalize() # Save to disk if requested if self.save_to_disk: save_settings() # Trigger viewers to update defaults self.app._hub.broadcast( SettingsChangeMessage(self, ('FOREGROUND_COLOR', 'BACKGROUND_COLOR'))) # If requested, trigger data to update color if self.data_apply: self.app.set_data_color(settings.DATA_COLOR, settings.DATA_ALPHA) super(PreferencesDialog, self).accept()