class GroupedSubset(Subset): """ A member of a SubsetGroup, whose internal representation is shared with other group members """ subset_state = Pointer('group.subset_state') label = Pointer('group.label') def __init__(self, data, group): """ :param data: :class:`~glue.core.data.Data` instance to bind to :param group: :class:`~glue.core.subset_group.SubsetGroup` """ # We deliberately don't call Subset.__init__ here because we don't want # to set e.g. the subset state, color, transparency, etc. Instead we # just want to defer to the SubsetGroup for these. self._broadcasting = False # must be first def self.group = group self.data = data self.label = group.label # trigger disambiguation @property def style(self): return self.group.style @property def subset_group(self): return self.group.subset_group @property def verbose_label(self): return "%s (%s)" % (self.label, self.data.label) def __eq__(self, other): return other is self # In Python 3, if __eq__ is defined, then __hash__ has to be re-defined if six.PY3: __hash__ = object.__hash__ def __gluestate__(self, context): return dict(group=context.id(self.group), style=context.do(self.style)) @classmethod def __setgluestate__(cls, rec, context): dummy_grp = SubsetGroup() # __init__ needs group.label self = cls(None, dummy_grp) yield self self.group = context.object(rec['group'])
class GingaLayerArtist(LayerArtistBase): zorder = Pointer('_zorder') visible = Pointer('_visible') def __init__(self, layer, canvas): super(GingaLayerArtist, self).__init__(layer) self._canvas = canvas self._visible = True def redraw(self, whence=0): self._canvas.redraw(whence=whence)
class GroupedSubset(Subset): """ A member of a SubsetGroup, whose internal representation is shared with other group members """ subset_state = Pointer('group.subset_state') label = Pointer('group.label') def __init__(self, data, group): """ :param data: :class:`~glue.core.data.Data` instance to bind to :param group: :class:`~glue.core.subset_group.SubsetGroup` """ self.group = group super(GroupedSubset, self).__init__(data, label=group.label, color=group.style.color, alpha=group.style.alpha) def _setup(self, color, alpha, label): self.color = color self.label = label # trigger disambiguation self.style = VisualAttributes(parent=self) self.style.markersize *= 2.5 self.style.color = color self.style.alpha = alpha # skip state setting here @property def verbose_label(self): return "%s (%s)" % (self.label, self.data.label) def sync_style(self, other): self.style.set(other) def __eq__(self, other): return other is self # In Python 3, if __eq__ is defined, then __hash__ has to be re-defined if six.PY3: __hash__ = object.__hash__ def __gluestate__(self, context): return dict(group=context.id(self.group), style=context.do(self.style)) @classmethod def __setgluestate__(cls, rec, context): dummy_grp = SubsetGroup() # __init__ needs group.label self = cls(None, dummy_grp) yield self self.group = context.object(rec['group']) self.style = context.object(rec['style'])
class LayerArtistBase(PropertySetMixin): _property_set = ['zorder', 'visible', 'layer'] # the order of this layer in the visualizations. High-zorder # layers are drawn on top of low-zorder layers. # Subclasses should refresh plots when this property changes zorder = Pointer('_zorder') # whether this layer should be rendered. # Subclasses should refresh plots when this property changes visible = Pointer('_visible') # whether this layer is capable of being rendered # Subclasses should refresh plots when this property changes enabled = Pointer('_enabled') def __init__(self, layer): """Create a new LayerArtist Parameters ---------- layer : :class:`~glue.core.data.Data` or :class:`~glue.core.subset.Subset` Data or Subset to draw layer : :class:`~glue.core.data.Data` or `glue.core.subset.Subset` """ self._visible = True self._zorder = 0 self._enabled = True self._layer = layer self.view = None # cache of last view, if relevant self._state = None # cache of subset state, if relevant self._changed = True # hint at whether underlying data has changed since last render self._disabled_reason = '' # A string explaining why this layer is disabled. def get_layer_color(self): # This method can return either a plain color or a colormap. This is # used by the UI layer to determine a 'representative' color or colormap # for the layer to be used e.g. in icons. return self._layer.style.color def enable(self): if self.enabled: return self._disabled_reason = '' self._enabled = True self.redraw() if self._layer is not None and self._layer.hub is not None: message = LayerArtistEnabledMessage(self) self._layer.hub.broadcast(message) def disable(self, reason): """ Disable the layer for a particular reason. Layers should only be disabled when drawing is impossible, e.g. because a subset cannot be applied to a dataset. Parameters ---------- reason : str A short explanation for why the layer can't be drawn. Used by the UI """ self._disabled_reason = reason self._enabled = False self.clear() if self._layer is not None and self._layer.hub is not None: message = LayerArtistDisabledMessage(self) self._layer.hub.broadcast(message) def disable_invalid_attributes(self, *attributes): """ Disable a layer because visualization depends on knowing a set of ComponentIDs that cannot be derived from a dataset or subset Automatically generates a disabled message. Parameters ---------- attributes : sequence of ComponentIDs """ if len(attributes) == 0: self.disable('') return datasets = ', '.join( sorted(set([cid.parent.label for cid in attributes]))) self.disable(DISABLED_LAYER_WARNING.format(datasets)) def disable_incompatible_subset(self): """ Disable a layer because the subset mask cannot be computed. Automatically generates a disabled message. """ self.disable(DISABLED_MASK_MESSAGE) @property def disabled_message(self): """ Returns why a layer is disabled """ if self.enabled: return '' return "Cannot visualize this layer: %s" % self._disabled_reason @property def layer(self): """ The Data or Subset visualized in this layer """ return self._layer @layer.setter def layer(self, value): self._layer = value def redraw(self): """ Re-render the plot """ pass def update(self): """ Sync the visual appearance of the layer, and redraw """ pass def clear(self): """ Clear the visualization for this layer """ pass def remove(self): """ Remove the visualization for this layer. This is called when the layer artist is removed for good from the viewer. It defaults to calling clear, but can be overriden in cases where clear and remove should be different. """ self.clear() def force_update(self, *args, **kwargs): """ Sets the _changed flag to true, and calls update. Force an update of the layer, overriding any caching that might be going on for speed """ self._changed = True return self.update(*args, **kwargs) def _check_subset_state_changed(self): """Checks to see if layer is a subset and, if so, if it has changed subset state. Sets _changed flag to True if so""" if not isinstance(self.layer, Subset): return state = self.layer.subset_state if state is not self._state: self._changed = True self._state = state def __str__(self): return "%s for %s" % (self.__class__.__name__, self.layer.label) def __gluestate__(self, context): # note, this doesn't yet have a restore method. Will rely on client return dict((k, context.id(v)) for k, v in self.properties.items()) __repr__ = __str__
class SpectrumContext(object): """ Base class for different interaction contexts """ client = Pointer('main.client') data = Pointer('main.data') profile_axis = Pointer('main.profile_axis') canvas = Pointer('main.canvas') profile = Pointer('main.profile') def __init__(self, main): self.main = main self.grip = None self.panel = None self.widget = None self._setup_grip() self._setup_widget() self._connect() def _setup_grip(self): """ Create a :class:`~glue.plugins.tools.spectrum_tool.profile_viewer.Grip` object to interact with the plot. Assign to self.grip """ raise NotImplementedError() def _setup_widget(self): """ Create a context-specific widget """ # this is the widget that is displayed to the right of the # spectrum raise NotImplementedError() def _connect(self): """ Attach event handlers """ pass def set_enabled(self, enabled): self.enable() if enabled else self.disable() def enable(self): if self.grip is not None: self.grip.enable() def disable(self): if self.grip is not None: self.grip.disable() def recenter(self, lim): """Re-center the grip to the given x axlis limit tuple""" if self.grip is None: return if hasattr(self.grip, 'value'): self.grip.value = sum(lim) / 2. return # Range grip cen = sum(lim) / 2 wid = max(lim) - min(lim) self.grip.range = cen - wid / 4, cen + wid / 4
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 LayerArtistBase(PropertySetMixin): _property_set = ['zorder', 'visible', 'layer'] # the order of this layer in the visualizations. High-zorder # layers are drawn on top of low-zorder layers. # Subclasses should refresh plots when this property changes zorder = Pointer('_zorder') # whether this layer should be rendered. # Subclasses should refresh plots when this property changes visible = Pointer('_visible') # whether this layer is capable of being rendered # Subclasses should refresh plots when this property changes enabled = Pointer('_enabled') def __init__(self, layer): """Create a new LayerArtist Parameters ---------- layer : :class:`~glue.core.data.Data` or :class:`~glue.core.subset.Subset` Data or Subset to draw layer : :class:`~glue.core.data.Data` or `glue.core.subset.Subset` """ self._visible = True self._zorder = 0 self._enabled = True self._layer = layer self.view = None # cache of last view, if relevant self._state = None # cache of subset state, if relevant self._changed = True # hint at whether underlying data has changed since last render self._disabled_reason = '' # A string explaining why this layer is disabled. def disable(self, reason): """ Disable the layer for a particular reason. Layers should only be disabled when drawing is impossible, e.g. because a subset cannot be applied to a dataset. Parameters ---------- reason : str A short explanation for why the layer can't be drawn. Used by the UI """ self._disabled_reason = reason self._enabled = False self.clear() def disable_invalid_attributes(self, *attributes): """ Disable a layer because visualization depends on knowing a set of ComponentIDs that cannot be derived from a dataset or subset Automatically generates a disabled message. Parameters ---------- attributes : sequence of ComponentIDs """ if len(attributes) == 0: self.disable('') msg = ('Layer depends on attributes that ' 'cannot be derived for %s:\n -%s' % (self._layer.data.label, '\n -'.join(map(str, attributes)))) self.disable(msg) @property def disabled_message(self): """ Returns why a layer is disabled """ if self.enabled: return '' return "Cannot visualize this layer\n%s" % self._disabled_reason @property def layer(self): """ The Data or Subset visualized in this layer """ return self._layer @layer.setter def layer(self, value): self._layer = value @abstractmethod def redraw(self): """ Re-render the plot """ raise NotImplementedError() @abstractmethod def update(self, view=None): """ Sync the visual appearance of the layer, and redraw Subclasses may skip the update if the _changed attribute is set to False. Parameters ---------- view : (ComponentID, numpy_style view) or None A hint about what sub-view into the data is relevant. """ raise NotImplementedError() @abstractmethod def clear(self): """Clear the visulaization for this layer""" raise NotImplementedError() def force_update(self, *args, **kwargs): """ Sets the _changed flag to true, and calls update. Force an update of the layer, overriding any caching that might be going on for speed """ self._changed = True return self.update(*args, **kwargs) def _check_subset_state_changed(self): """Checks to see if layer is a subset and, if so, if it has changed subset state. Sets _changed flag to True if so""" if not isinstance(self.layer, Subset): return state = self.layer.subset_state if state is not self._state: self._changed = True self._state = state def __str__(self): return "%s for %s" % (self.__class__.__name__, self.layer.label) def __gluestate__(self, context): # note, this doesn't yet have a restore method. Will rely on client return dict((k, context.id(v)) for k, v in self.properties.items()) __repr__ = __str__
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 RGBImageLayerArtist(ImageLayerArtist, RGBImageLayerBase): _property_set = ImageLayerArtist._property_set + \ ['r', 'g', 'b', 'rnorm', 'gnorm', 'bnorm', 'color_visible'] r = ChangedTrigger() g = ChangedTrigger() b = ChangedTrigger() rnorm = Pointer('_rnorm') gnorm = Pointer('_gnorm') bnorm = Pointer('_bnorm') # dummy class-level variables will be masked # at instance level, needed for ABC to be happy layer_visible = None contrast_layer = None def __init__(self, layer, ax, last_view=None): super(RGBImageLayerArtist, self).__init__(layer, ax) self.contrast_layer = 'green' self.aspect = 'equal' self.layer_visible = dict(red=True, green=True, blue=True) self.last_view = last_view def set_norm(self, *args, **kwargs): spr = super(RGBImageLayerArtist, self).set_norm if self.contrast_layer == 'red': self.norm = self.rnorm self.rnorm = spr(*args, **kwargs) if self.contrast_layer == 'green': self.norm = self.gnorm self.gnorm = spr(*args, **kwargs) if self.contrast_layer == 'blue': self.norm = self.bnorm self.bnorm = spr(*args, **kwargs) def update(self, view=None, transpose=False, aspect=None): self.clear() if aspect is not None: self.aspect = aspect if self.r is None or self.g is None or self.b is None: return if view is None: view = self.last_view if view is None: return self.last_view = view views = view_cascade(self.layer, view) artists = [] for v in views: extent = get_extent(v, transpose) # first argument = component. swap r = tuple([self.r] + list(v[1:])) g = tuple([self.g] + list(v[1:])) b = tuple([self.b] + list(v[1:])) r = self.layer[r] g = self.layer[g] b = self.layer[b] if transpose: r = r.T g = g.T b = b.T self.rnorm = self.rnorm or self._default_norm(r) self.gnorm = self.gnorm or self._default_norm(g) self.bnorm = self.bnorm or self._default_norm(b) if v is views[0]: self.rnorm.update_clip(small_view(self.layer, self.r)) self.gnorm.update_clip(small_view(self.layer, self.g)) self.bnorm.update_clip(small_view(self.layer, self.b)) image = np.dstack((self.rnorm(r), self.gnorm(g), self.bnorm(b))) if not self.layer_visible['red']: image[:, :, 0] *= 0 if not self.layer_visible['green']: image[:, :, 1] *= 0 if not self.layer_visible['blue']: image[:, :, 2] *= 0 artists.append( self._axes.imshow(image, interpolation='nearest', origin='lower', extent=extent, zorder=0)) self._axes.set_aspect(self.aspect, adjustable='datalim') self.artists = artists self._sync_style()