def to_mask(self, data, view=None): if view is None: view = Ellipsis elif isinstance(view, slice) or np.isscalar(view): view = [view] # Figure out the shape of the final mask given the requested view shape = view_shape(data.shape, view) if data is self.reference_data: slices = self.slices else: # Check if we can transform list of slices to match this dataset order = data.pixel_aligned_data.get(self.reference_data, None) if order is None: # We use broadcast_to for minimal memory usage return broadcast_to(False, shape) else: # Reorder slices slices = [self.slices[idx] for idx in order] if (isinstance(view, np.ndarray) or (isinstance(view, (tuple, list)) and isinstance(view[0], np.ndarray))): mask = np.zeros(data.shape, dtype=bool) mask[slices] = True return mask[view] # The original slices assume the full array, not the array with the view # applied, so we need to now adjust the slices accordingly. if view is Ellipsis: subslices = slices else: subslices = [] for i in range(data.ndim): if i >= len(view): subslices.append(slices[i]) elif np.isscalar(view[i]): beg, end, stp = slices[i].indices(data.shape[i]) if view[i] < beg or view[i] >= end or (view[i] - beg) % stp != 0: return broadcast_to(False, shape) elif isinstance(view[i], slice): if view[i].step is not None and view[i].step < 0: beg, end, step = view[i].indices(data.shape[i]) v = slice(end + 1, beg + 1, -step) else: v = view[i] subslices.append(combine_slices(v, slices[i], data.shape[i])) else: raise TypeError("Unexpected view item: {0}".format(view[i])) # Create mask with final shape mask = np.zeros(shape, dtype=bool) mask[subslices] = True return mask
def compute_fixed_resolution_buffer(self, bounds=None): shape = [bound[2] for bound in bounds] if self.layer_artist is None or self.viewer_state is None: return broadcast_to(0, shape) if isinstance(self.layer_artist.layer, Subset): try: subset_state = self.layer_artist.layer.subset_state result = self.layer_artist.layer.data.compute_fixed_resolution_buffer( target_data=self.layer_artist._viewer_state.reference_data, bounds=bounds, subset_state=subset_state, cache_id=self.layer_artist.id) except IncompatibleAttribute: self.layer_artist.disable_incompatible_subset() return broadcast_to(0, shape) else: self.layer_artist.enable() else: try: result = self.layer_artist.layer.compute_fixed_resolution_buffer( target_data=self.layer_artist._viewer_state.reference_data, bounds=bounds, target_cid=self.layer_artist.state.attribute, cache_id=self.layer_artist.id) except IncompatibleAttribute: self.layer_artist.disable( 'Layer data is not fully linked to reference data') return broadcast_to(0, shape) else: self.layer_artist.enable() return result
def test_compute_statistic_empty_subset(): data = Data(x=np.empty((30, 20, 40))) # A default subset state should be empty subset_state = SubsetState() result = data.compute_statistic('mean', data.id['x'], subset_state=subset_state) assert_equal(result, np.nan) result = data.compute_statistic('maximum', data.id['x'], subset_state=subset_state, axis=1) assert_equal(result, broadcast_to(np.nan, (30, 40))) result = data.compute_statistic('median', data.id['x'], subset_state=subset_state, axis=(1, 2)) assert_equal(result, broadcast_to(np.nan, (30))) result = data.compute_statistic('sum', data.id['x'], subset_state=subset_state, axis=(0, 1, 2)) assert_equal(result, np.nan)
def __getitem__(self, view=None): if (self.layer_artist is None or self.viewer_state is None): return broadcast_to(0, self.shape)[view] try: mask = self.layer_artist.layer.to_mask(view=view) except IncompatibleAttribute: self.layer_artist.disable_incompatible_subset() return broadcast_to(0, self.shape)[view] else: self.layer_artist.enable() return mask
def using(self, *args): # NOTE: in the past, we set any non-specified arguemnts to 0 for the # input coordinates, but this caused issues because in astropy.wcs # if one specifies e.g. (0, 0, 3000.) for (ra, dec, velocity), and if # (0, 0) for RA/Dec would return (nan, nan) normally, the velocity # is also NaN even though it is decoupled from the other coordinates. default = default_world_coords(self.coords) args2 = [None] * self.ndim for f, a in zip(self.from_needed, args): args2[f] = a for i in range(self.ndim): if args2[i] is None: args2[i] = broadcast_to(default[self.ndim - 1 - i], args[0].shape) args2 = tuple(args2) if self.pixel2world: return pixel2world_single_axis(self.coords, *args2[::-1], world_axis=self.ndim - 1 - self.index) else: return world2pixel_single_axis(self.coords, *args2[::-1], pixel_axis=self.ndim - 1 - self.index)
def compute(self, data, view=None): """ For a given data set, compute the component comp_to given the data associated with each comp_from and the ``using`` function This raises an :class:`glue.core.exceptions.IncompatibleAttribute` if the data set doesn't have all the ComponentIDs needed for the transformation Parameters ---------- data : `~glue.core.data.Data` The data set to use view : `None` or `slice` or `tuple` Optional view (e.g. slice) through the data to use Returns ------- result The data associated with comp_to component """ # First we get the values of all the 'from' components. args = [data[join_component_view(f, view)] for f in self._from] # We keep track of the original shape of the arguments original_shape = args[0].shape logger.debug("shape of first argument: %s", original_shape) # We now unbroadcast the arrays to only compute the link with the # smallest number of values we can. This can help for cases where # the link depends only on e.g. pixel components or world coordinates # that themselves only depend on a subset of pixel components. # Unbroadcasting is the act of returning the smallest array that # contains all the information needed to be broadcasted back to its # full value args = [unbroadcast(arg) for arg in args] # We now broadcast these to the smallest common shape in case the # linking functions don't know how to broadcast arrays with different # shapes. args = np.broadcast_arrays(*args) # We call the actual linking function result = self._using(*args) # We call asarray since link functions may return Python scalars in some cases result = np.asarray(result) # In some cases, linking functions return ravelled arrays, so we # fix this here. logger.debug("shape of result: %s", result.shape) if result.shape != args[0].shape: logger.debug("ComponentLink function %s changed shape. Fixing", self._using.__name__) result.shape = args[0].shape # Finally we broadcast the final result to desired shape result = broadcast_to(result, original_shape) return result
def clear(self): """ Remove the layer artist from the visualization """ # We don't want to deallocate here because this can be called if we # disable the layer due to incompatible attributes self._multivol.set_data(self.id, broadcast_to(0, self._multivol._vol_shape))
def compute(self, data, view=None): left = self._left right = self._right if not isinstance(self._left, numbers.Number): left = data[self._left, view] if not isinstance(self._right, numbers.Number): right = data[self._right, view] # As described in more detail in ComponentLink.compute, we can # 'unbroadcast' the arrays to ensure a minimal operation original_shape = None if isinstance(left, np.ndarray): original_shape = left.shape left = unbroadcast(left) if isinstance(right, np.ndarray): original_shape = right.shape right = unbroadcast(right) if original_shape is not None: left, right = np.broadcast_arrays(left, right) result = self._op(left, right) if original_shape is None: return result else: return broadcast_to(result, original_shape)
def translate_pixel(data, pixel_coords, target_cid): """ Given a dataset and pixel coordinates in that dataset, compute a corresponding pixel coordinate for another dataset. Parameters ---------- data : `~glue.core.data.Data` The main data object in which the original pixel coordinates are defined. pixel_coords : tuple or list List of pixel coordinates in the original data object. This should contain as many Numpy arrays as there are dimensions in ``data``. target_cid : `~glue.core.component_id.ComponentID` The component to compute - this can be for a different dataset. Returns ------- coords : `~numpy.ndarray` The values of the translated coordinates dimensions : tuple The dimensions in ``data`` for which pixel coordinates were used in the translation. """ if not len(pixel_coords) == data.ndim: raise ValueError('The number of coordinates in pixel_coords does not ' 'match the number of dimensions in data') if target_cid in data.pixel_component_ids: return pixel_coords[target_cid.axis], [target_cid.axis] # TODO: check that things are efficient if the PixelComponentID is in a # pixel-aligned dataset. component = data.get_component(target_cid) if hasattr(component, 'link'): link = component.link values_all = [] dimensions_all = [] for cid in link._from: values, dimensions = translate_pixel(data, pixel_coords, cid) values_all.append(values) dimensions_all.extend(dimensions) # Unbroadcast arrays to smallest common shape for performance if len(values_all) > 0: shape = values_all[0].shape values_all = broadcast_arrays_minimal(*values_all) results = link._using(*values_all) result = broadcast_to(results, shape) else: result = None return result, sorted(set(dimensions_all)) elif isinstance(component, CoordinateComponent): # FIXME: Hack for now - if we pass arrays in the view, it's interpreted return component._calculate(view=pixel_coords), dependent_axes( data.coords, component.axis) else: raise Exception("Dependency on non-pixel component", target_cid)
def to_mask(self, data, view=None): # TODO: make sure that pixel components don't actually take up much # memory and are just views x = data[self.xatt, view] y = data[self.yatt, view] # We do the import here to avoid circular imports from glue.core.component_id import PixelComponentID if (x.ndim == data.ndim and self.xatt in data.pixel_component_ids and self.yatt in data.pixel_component_ids): # This is a special case - the ROI is defined in pixel space, so we # can apply it to a single slice and then broadcast it to all other # dimensions. We start off by extracting a slice which takes only # the first elements of all dimensions except the attributes in # question, for which we take all the elements. We need to preserve # the dimensionality of the array, hence the use of slice(0, 1). # Note that we can only do this if the view (if present) preserved # the dimensionality, which is why we checked that x.ndim == data.ndim subset = [] for i in range(data.ndim): if i == self.xatt.axis or i == self.yatt.axis: subset.append(slice(None)) else: subset.append(slice(0, 1)) x_slice = x[subset] y_slice = y[subset] if self.roi.defined(): result = self.roi.contains(x_slice, y_slice) else: result = np.zeros(x_slice.shape, dtype=bool) result = broadcast_to(result, x.shape) else: if self.roi.defined(): result = self.roi.contains(x, y) else: result = np.zeros(x.shape, dtype=bool) if result.shape != x.shape: raise ValueError( "Unexpected error: boolean mask has incorrect dimensions") return result
def to_mask(self, data, view=None): # TODO: make sure that pixel components don't actually take up much # memory and are just views x = data[self.xatt, view] y = data[self.yatt, view] # We do the import here to avoid circular imports from glue.core.component_id import PixelComponentID if (x.ndim == data.ndim and isinstance(self.xatt, PixelComponentID) and isinstance(self.yatt, PixelComponentID)): # This is a special case - the ROI is defined in pixel space, so we # can apply it to a single slice and then broadcast it to all other # dimensions. We start off by extracting a slice which takes only # the first elements of all dimensions except the attributes in # question, for which we take all the elements. We need to preserve # the dimensionality of the array, hence the use of slice(0, 1). # Note that we can only do this if the view (if present) preserved # the dimensionality, which is why we checked that x.ndim == data.ndim subset = [] for i in range(data.ndim): if i == self.xatt.axis or i == self.yatt.axis: subset.append(slice(None)) else: subset.append(slice(0, 1)) x_slice = x[subset] y_slice = y[subset] if self.roi.defined(): result = self.roi.contains(x_slice, y_slice) else: result = np.zeros(x_slice.shape, dtype=bool) result = broadcast_to(result, x.shape) else: if self.roi.defined(): result = self.roi.contains(x, y) else: result = np.zeros(x.shape, dtype=bool) if result.shape != x.shape: raise ValueError("Unexpected error: boolean mask has incorrect dimensions") return result
def __call__(self, bounds): if (self.layer_artist is None or self.layer_state is None or self.viewer_state is None): return broadcast_to(np.nan, self.shape) # We should compute the mask even if the layer is not visible as we need # the layer to show up properly when it is made visible (which doesn't # trigger __getitem__) try: mask = self.layer_state.get_sliced_data(bounds=bounds) except IncompatibleAttribute: self.layer_artist.disable_incompatible_subset() return broadcast_to(np.nan, self.shape) else: self.layer_artist.enable(redraw=False) r, g, b = color2rgb(self.layer_state.color) mask = np.dstack((r * mask, g * mask, b * mask, mask * .5)) mask = (255 * mask).astype(np.uint8) return mask
def points_inside_poly(x, y, vx, vy): if x.dtype.kind == 'M' and vx.dtype.kind == 'M': vx = vx.astype(x.dtype).astype(float) x = x.astype(float) if y.dtype.kind == 'M' and vy.dtype.kind == 'M': vy = vy.astype(y.dtype).astype(float) y = y.astype(float) original_shape = x.shape x = unbroadcast(x) y = unbroadcast(y) x = x.astype(float) y = y.astype(float) x, y = np.broadcast_arrays(x, y) reduced_shape = x.shape x = x.flat y = y.flat from matplotlib.path import Path p = Path(np.column_stack((vx, vy))) keep = ((x >= np.min(vx)) & (x <= np.max(vx)) & (y >= np.min(vy)) & (y <= np.max(vy))) inside = np.zeros(len(x), bool) x = x[keep] y = y[keep] coords = np.column_stack((x, y)) inside[keep] = p.contains_points(coords).astype(bool) good = np.isfinite(x) & np.isfinite(y) inside[keep][~good] = False inside = inside.reshape(reduced_shape) inside = broadcast_to(inside, original_shape) return inside
def __getitem__(self, view=None): if (self.layer_artist is None or self.layer_state is None or self.viewer_state is None): return broadcast_to(np.nan, self.shape) # We should compute the mask even if the layer is not visible as we need # the layer to show up properly when it is made visible (which doesn't # trigger __getitem__) try: mask = self.layer_state.get_sliced_data(view=view) except IncompatibleAttribute: self.layer_artist.disable_incompatible_subset() return broadcast_to(np.nan, self.shape) else: self.layer_artist.enable(redraw=False) r, g, b = color2rgb(self.layer_state.color) mask = np.dstack((r * mask, g * mask, b * mask, mask * .5)) mask = (255 * mask).astype(np.uint8) return mask
def pixel2world_single_axis(self, *pixel, **kwargs): """ Convert pixel to world coordinates, preserving input type/shape. This is a wrapper around pixel2world which returns the result for just one axis, and also determines whether the calculation can be sped up if broadcasting is present in the input arrays. Parameters ---------- *pixel : scalars lists, or Numpy arrays The pixel coordinates (0-based) to convert axis : int, optional If only one axis is needed, it should be specified since the calculation will be much more efficient. Returns ------- world : `numpy.ndarray` The world coordinates for the requested axis """ # PY3: the following is needed for Python 2 axis = kwargs.get('axis', None) if axis is None: raise ValueError("axis needs to be set") if np.size(pixel[0]) == 0: return np.array([], dtype=float) original_shape = pixel[0].shape pixel_new = [] # NOTE: the axis passed to this function is the WCS axis not the Numpy # axis, so we need to convert it as needed. dep_axes = self.dependent_axes(len(pixel) - 1 - axis) for ip, p in enumerate(pixel): if (len(pixel) - 1 - ip) in dep_axes: pixel_new.append(unbroadcast(p)) else: pixel_new.append(p.flat[0]) pixel = np.broadcast_arrays(*pixel_new) result = self.pixel2world(*pixel) return broadcast_to(result[axis], original_shape)
def pixel2world_single_axis(wcs, *pixel, world_axis=None): """ Convert pixel to world coordinates, preserving input type/shape. This is a wrapper around pixel_to_world_values which returns the result for just one axis, and also determines whether the calculation can be sped up if broadcasting is present in the input arrays. Parameters ---------- *pixel : scalars lists, or Numpy arrays The pixel coordinates (0-based) to convert world_axis : int, optional The index of the world coordinate that is needed. Returns ------- world : `numpy.ndarray` The world coordinates for the requested axis """ if world_axis is None: raise ValueError("world_axis needs to be set") if np.size(pixel[0]) == 0: return np.array([], dtype=float) original_shape = pixel[0].shape pixel_new = [] # Now find all the pixel coordinates that are needed to calculate this # world coordinate, using the axis correlation matrix pixel_dep = wcs.axis_correlation_matrix[world_axis, :] for ip, p in enumerate(pixel): if pixel_dep[ip]: pixel_new.append(unbroadcast(p)) else: pixel_new.append(p.flat[0]) pixel = np.broadcast_arrays(*pixel_new) result = wcs.pixel_to_world_values(*pixel) return broadcast_to(result[world_axis], original_shape)
def world2pixel_single_axis(wcs, *world, pixel_axis=None): """ Convert world to pixel coordinates, preserving input type/shape. This is a wrapper around world_to_pixel_values which returns the result for just one axis, and also determines whether the calculation can be sped up if broadcasting is present in the input arrays. Parameters ---------- *world : scalars lists, or Numpy arrays The world coordinates to convert pixel_axis : int, optional The index of the pixel coordinate that is needed. Returns ------- pixel : `numpy.ndarray` The pixel coordinates for the requested axis """ if pixel_axis is None: raise ValueError("pixel_axis needs to be set") if np.size(world[0]) == 0: return np.array([], dtype=float) original_shape = world[0].shape world_new = [] # Now find all the world coordinates that are needed to calculate this # world coordinate, using the axis correlation matrix world_dep = wcs.axis_correlation_matrix[:, pixel_axis] for iw, w in enumerate(world): if world_dep[iw]: world_new.append(unbroadcast(w)) else: world_new.append(w.flat[0]) world = np.broadcast_arrays(*world_new) result = wcs.world_to_pixel_values(*world) return broadcast_to(result[pixel_axis], original_shape)
def efficient_pixel_to_pixel(wcs1, wcs2, *inputs): """ Wrapper that performs a pixel -> world -> pixel transformation with two WCS instances, and un-broadcasting arrays whenever possible for efficiency. """ # Shortcut for scalars if np.isscalar(inputs[0]): world_outputs = wcs1.pixel_to_world(*inputs) if not isinstance(world_outputs, (tuple, list)): world_outputs = (world_outputs, ) return wcs2.world_to_pixel(*world_outputs) # Remember original shape original_shape = inputs[0].shape matrix = pixel_to_pixel_correlation_matrix(wcs1, wcs2) split_info = split_matrix(matrix) outputs = [None] * wcs2.pixel_n_dim for (pixel_in_indices, pixel_out_indices) in split_info: pixel_inputs = [] for ipix in range(wcs1.pixel_n_dim): if ipix in pixel_in_indices: pixel_inputs.append(unbroadcast(inputs[ipix])) else: pixel_inputs.append(inputs[ipix].flat[0]) pixel_inputs = np.broadcast_arrays(*pixel_inputs) world_outputs = wcs1.pixel_to_world(*pixel_inputs) if not isinstance(world_outputs, (tuple, list)): world_outputs = (world_outputs, ) pixel_outputs = wcs2.world_to_pixel(*world_outputs) for ipix in range(wcs2.pixel_n_dim): if ipix in pixel_out_indices: outputs[ipix] = broadcast_to(pixel_outputs[ipix], original_shape) return outputs
def efficient_pixel_to_pixel(wcs1, wcs2, *inputs): """ Wrapper that performs a pixel -> world -> pixel transformation with two WCS instances, and un-broadcasting arrays whenever possible for efficiency. """ # Shortcut for scalars if np.isscalar(inputs[0]): world_outputs = wcs1.pixel_to_world(*inputs) if not isinstance(world_outputs, (tuple, list)): world_outputs = (world_outputs,) return wcs2.world_to_pixel(*world_outputs) # Remember original shape original_shape = inputs[0].shape matrix = pixel_to_pixel_correlation_matrix(wcs1, wcs2) split_info = split_matrix(matrix) outputs = [None] * wcs2.pixel_n_dim for (pixel_in_indices, pixel_out_indices) in split_info: pixel_inputs = [] for ipix in range(wcs1.pixel_n_dim): if ipix in pixel_in_indices: pixel_inputs.append(unbroadcast(inputs[ipix])) else: pixel_inputs.append(inputs[ipix].flat[0]) pixel_inputs = np.broadcast_arrays(*pixel_inputs) world_outputs = wcs1.pixel_to_world(*pixel_inputs) if not isinstance(world_outputs, (tuple, list)): world_outputs = (world_outputs,) pixel_outputs = wcs2.world_to_pixel(*world_outputs) for ipix in range(wcs2.pixel_n_dim): if ipix in pixel_out_indices: outputs[ipix] = broadcast_to(pixel_outputs[ipix], original_shape) return outputs
def world2pixel_single_axis(self, *world, **kwargs): """ Convert world to pixel coordinates, preserving input type/shape. This is a wrapper around world2pixel which returns the result for just one axis, and also determines whether the calculation can be sped up if broadcasting is present in the input arrays. Parameters ---------- *world : scalars lists, or Numpy arrays The world coordinates to convert axis : int, optional If only one axis is needed, it should be specified since the calculation will be much more efficient. Returns ------- pixel : `numpy.ndarray` The pixel coordinates for the requested axis """ # PY3: the following is needed for Python 2 axis = kwargs.get('axis', None) if axis is None: raise ValueError("axis needs to be set") original_shape = world[0].shape world_new = [] dep_axes = self.dependent_axes(axis) for iw, w in enumerate(world): if iw in dep_axes: world_new.append(unbroadcast(w)) else: world_new.append(w.flat[0]) world = np.broadcast_arrays(*world_new) result = self.world2pixel(*world) return broadcast_to(result[axis], original_shape)
def using(self, *args): attr = 'pixel2world_single_axis' if self.pixel2world else 'world2pixel_single_axis' func = getattr(self.coords, attr) # NOTE: in the past, we set any non-specified arguemnts to 0 for the # input coordinates, but this caused issues because in astropy.wcs # if one specifies e.g. (0, 0, 3000.) for (ra, dec, velocity), and if # (0, 0) for RA/Dec would return (nan, nan) normally, the velocity # is also NaN even though it is decoupled from the other coordinates. default = self.coords.default_world_coords(self.ndim) args2 = [None] * self.ndim for f, a in zip(self.from_needed, args): args2[f] = a for i in range(self.ndim): if args2[i] is None: args2[i] = broadcast_to(default[self.ndim - 1 - i], args[0].shape) args2 = tuple(args2) return func(*args2[::-1], axis=self.ndim - 1 - self.index)
def _calculate(self, view=None): if self.world: # This can be a bottleneck if we aren't careful, so we need to make # sure that if not all dimensions depend on each other, we use smart # broadcasting pix_coords = [] dep_coords = self._data.coords.dependent_axes(self.axis) for i in range(self._data.ndim): if i in dep_coords: pix_coords.append(np.arange(self._data.shape[i])) else: pix_coords.append(0) pix_coords = np.meshgrid(*pix_coords, indexing='ij', copy=False) world_coords = self._data.coords.pixel2world_single_axis(*pix_coords[::-1], axis=self._data.ndim - 1 - self.axis) world_coords = broadcast_to(world_coords, self._data.shape) if view is None: view = Ellipsis # FIXME: this is inefficient if view is only a small fraction of the # whole array, and should be optimized. return world_coords[view] else: slices = [slice(0, s, 1) for s in self.shape] grids = np.broadcast_arrays(*np.ogrid[slices]) if view is not None: grids = [g[view] for g in grids] return grids[self.axis]
def _calculate(self, view=None): if self.world: # This can be a bottleneck if we aren't careful, so we need to make # sure that if not all dimensions depend on each other, we use smart # broadcasting pix_coords = [] dep_coords = self._data.coords.dependent_axes(self.axis) for i in range(self._data.ndim): if i in dep_coords: pix_coords.append(np.arange(self._data.shape[i])) else: pix_coords.append(0) pix_coords = np.meshgrid(*pix_coords, indexing='ij', copy=False) world_coords = self._data.coords.pixel2world_single_axis( *pix_coords[::-1], axis=self._data.ndim - 1 - self.axis) world_coords = broadcast_to(world_coords, self._data.shape) if view is None: view = Ellipsis # FIXME: this is inefficient if view is only a small fraction of the # whole array, and should be optimized. return world_coords[view] else: slices = [slice(0, s, 1) for s in self.shape] grids = np.broadcast_arrays(*np.ogrid[slices]) if view is not None: grids = [g[view] for g in grids] return grids[self.axis]
def to_mask(self, data, view=None): shp = view_shape(data.shape, view) return broadcast_to(False, shp)
def compute_fixed_resolution_buffer(data, bounds, target_data=None, target_cid=None, subset_state=None, broadcast=True, cache_id=None): """ Get a fixed-resolution buffer for a dataset. Parameters ---------- data : `~glue.core.Data` The dataset from which to extract a fixed resolution buffer bounds : list The list of bounds for the fixed resolution buffer. This list should have as many items as there are dimensions in ``target_data``. Each item should either be a scalar value, or a tuple of ``(min, max, nsteps)``. target_data : `~glue.core.Data`, optional The data in whose frame of reference the bounds are defined. Defaults to ``data``. target_cid : `~glue.core.component_id.ComponentID`, optional If specified, gives the component ID giving the component to use for the data values. Alternatively, use ``subset_state`` to get a subset mask. subset_state : `~glue.core.subset.SubsetState`, optional If specified, gives the subset state for which to compute a mask. Alternatively, use ``target_cid`` if you want to get data values. broadcast : bool, optional If `True`, then if a dimension in ``target_data`` for which ``bounds`` is not a scalar does not affect any of the dimensions in ``data``, then the final array will be effectively broadcast along this dimension, otherwise an error will be raised. """ if target_data is None: target_data = data if target_cid is None and subset_state is None: raise ValueError( "Either target_cid or subset_state should be specified") if target_cid is not None and subset_state is not None: raise ValueError( "Either target_cid or subset_state should be specified (not both)") # If cache_id is specified, we keep a cached version of the resulting array # indexed by cache_id as well as a hash formed of the call arguments to this # function. We then check if the resulting array already exists in the cache. if cache_id is not None: if subset_state is None: # Use uuid for component ID since otherwise component IDs don't return # False when comparing two different CIDs (instead they return a subset state). # For bounds we use a special wrapper that can identify wildcards. current_array_hash = (data, bounds, target_data, target_cid.uuid, broadcast) else: current_array_hash = (data, bounds, target_data, subset_state, broadcast) current_pixel_hash = (data, target_data) if cache_id in ARRAY_CACHE: if ARRAY_CACHE[cache_id]['hash'] == current_array_hash: return ARRAY_CACHE[cache_id]['array'] # To save time later, if the pixel cache doesn't match at the level of the # data and target_data, we just reset the cache. if cache_id in PIXEL_CACHE: if PIXEL_CACHE[cache_id]['hash'] != current_pixel_hash: PIXEL_CACHE.pop(cache_id) # Start off by generating arrays of coordinates in the original dataset pixel_coords = [ np.linspace(*bound) if isinstance(bound, tuple) else bound for bound in bounds ] pixel_coords = np.meshgrid(*pixel_coords, indexing='ij', copy=False) # Keep track of the original shape of these arrays original_shape = pixel_coords[0].shape # Now loop through the dimensions of 'data' to find the corresponding # coordinates in the frame of view of this dataset. translated_coords = [] dimensions_all = [] invalid_all = np.zeros(original_shape, dtype=bool) for ipix, pix in enumerate(data.pixel_component_ids): # At this point, if cache_id is in PIXEL_CACHE, we know that data and # target_data match so we just check the bounds. Note that the bounds # include the AnyScalar wildcard for any dimensions that don't impact # the pixel coordinates here. We do this so that we don't have to # recompute the pixel coordinates when e.g. slicing through cubes. if cache_id in PIXEL_CACHE and ipix in PIXEL_CACHE[ cache_id] and PIXEL_CACHE[cache_id][ipix]['bounds'] == bounds: translated_coord = PIXEL_CACHE[cache_id][ipix]['translated_coord'] dimensions = PIXEL_CACHE[cache_id][ipix]['dimensions'] invalid = PIXEL_CACHE[cache_id][ipix]['invalid'] else: translated_coord, dimensions = translate_pixel( target_data, pixel_coords, pix) # The returned coordinates may often be a broadcasted array. To convert # the coordinates to integers and check which ones are within bounds, we # thus operate on the un-broadcasted array, before broadcasting it back # to the original shape. translated_coord = np.round( unbroadcast(translated_coord)).astype(int) invalid = (translated_coord < 0) | (translated_coord >= data.shape[ipix]) # Since we are going to be using these coordinates later on to index an # array, we need the coordinates to be within the array, so we reset # any invalid coordinates and keep track of which pixels are invalid # to reset them later. translated_coord[invalid] = 0 # We now populate the cache if cache_id is not None: if cache_id not in PIXEL_CACHE: PIXEL_CACHE[cache_id] = {'hash': current_pixel_hash} PIXEL_CACHE[cache_id][ipix] = { 'translated_coord': translated_coord, 'dimensions': dimensions, 'invalid': invalid, 'bounds': bounds_for_cache(bounds, dimensions) } invalid_all |= invalid # Broadcast back to the original shape and add to the list translated_coords.append(broadcast_to(translated_coord, original_shape)) # Also keep track of all the dimensions that contributed to this coordinate dimensions_all.extend(dimensions) translated_coords = tuple(translated_coords) # If a dimension from the target data for which bounds was set to an interval # did not actually contribute to any of the coordinates in data, then if # broadcast is set to False we raise an error, otherwise we proceed and # implicitly broadcast values along that dimension of the target data. if data is not target_data and not broadcast: for i in range(target_data.ndim): if isinstance(bounds[i], tuple) and i not in dimensions_all: raise IncompatibleDataException() # PERF: optimize further - check if we can extract a sub-region that # contains all the valid values. # Take subset_state into account, if present if subset_state is None: array = data.get_data(target_cid, view=translated_coords).astype(float) invalid_value = -np.inf else: array = data.get_mask(subset_state, view=translated_coords) invalid_value = False if np.any(invalid_all): if not array.flags.writeable: array = np.array(array, dtype=type(invalid_value)) array[invalid_all] = invalid_value # Drop dimensions for which bounds were scalars slices = [] for bound in bounds: if isinstance(bound, tuple): slices.append(slice(None)) else: slices.append(0) array = array[tuple(slices)] if cache_id is not None: # For the bounds, we use a special wildcard for bounds that don't affect # the result. This will allow the cache to match regardless of the # value for those bounds. However, we only do this for scalar bounds. cache_bounds = bounds_for_cache(bounds, dimensions_all) current_array_hash = current_array_hash[:1] + ( cache_bounds, ) + current_array_hash[2:] if subset_state is None: ARRAY_CACHE[cache_id] = { 'hash': current_array_hash, 'array': array } else: ARRAY_CACHE[cache_id] = { 'hash': current_array_hash, 'array': array } return array
def _update_visual_attributes(self, changed, force=False): if not self.enabled: return if self.state.markers_visible: if self.state.density_map: if self.state.cmap_mode == 'Fixed': if force or 'color' in changed or 'cmap_mode' in changed: self.density_artist.set_color(self.state.color) self.density_artist.set_clim( self.density_auto_limits.min, self.density_auto_limits.max) elif force or any(prop in changed for prop in CMAP_PROPERTIES): c = ensure_numerical( self.layer[self.state.cmap_att].ravel()) set_mpl_artist_cmap(self.density_artist, c, self.state) if force or 'stretch' in changed: self.density_artist.set_norm( ImageNormalize( stretch=STRETCHES[self.state.stretch]())) if force or 'dpi' in changed: self.density_artist.set_dpi(self._viewer_state.dpi) if force or 'density_contrast' in changed: self.density_auto_limits.contrast = self.state.density_contrast self.density_artist.stale = True else: if self.state.cmap_mode == 'Fixed' and self.state.size_mode == 'Fixed': if force or 'color' in changed or 'fill' in changed: if self.state.fill: self.plot_artist.set_markeredgecolor('none') self.plot_artist.set_markerfacecolor( self.state.color) else: self.plot_artist.set_markeredgecolor( self.state.color) self.plot_artist.set_markerfacecolor('none') if force or 'size' in changed or 'size_scaling' in changed: self.plot_artist.set_markersize( self.state.size * self.state.size_scaling) else: # TEMPORARY: Matplotlib has a bug that causes set_alpha to # change the colors back: https://github.com/matplotlib/matplotlib/issues/8953 if 'alpha' in changed: force = True if self.state.cmap_mode == 'Fixed': if force or 'color' in changed or 'cmap_mode' in changed or 'fill' in changed: self.scatter_artist.set_array(None) if self.state.fill: self.scatter_artist.set_facecolors( self.state.color) self.scatter_artist.set_edgecolors('none') else: self.scatter_artist.set_facecolors('none') self.scatter_artist.set_edgecolors( self.state.color) elif force or any( prop in changed for prop in CMAP_PROPERTIES) or 'fill' in changed: self.scatter_artist.set_edgecolors(None) self.scatter_artist.set_facecolors(None) c = ensure_numerical( self.layer[self.state.cmap_att].ravel()) set_mpl_artist_cmap(self.scatter_artist, c, self.state) if self.state.fill: self.scatter_artist.set_edgecolors('none') else: colors = self.scatter_artist.get_facecolors() self.scatter_artist.set_facecolors('none') self.scatter_artist.set_edgecolors(colors) if force or any(prop in changed for prop in MARKER_PROPERTIES): if self.state.size_mode == 'Fixed': s = self.state.size * self.state.size_scaling s = broadcast_to( s, self.scatter_artist.get_sizes().shape) else: s = ensure_numerical( self.layer[self.state.size_att].ravel()) s = ((s - self.state.size_vmin) / (self.state.size_vmax - self.state.size_vmin)) # The following ensures that the sizes are in the # range 3 to 30 before the final size_scaling. np.clip(s, 0, 1, out=s) s *= 0.95 s += 0.05 s *= (30 * self.state.size_scaling) # Note, we need to square here because for scatter, s is actually # proportional to the marker area, not radius. self.scatter_artist.set_sizes(s**2) if self.state.line_visible: if self.state.cmap_mode == 'Fixed': if force or 'color' in changed or 'cmap_mode' in changed: self.line_collection.set_linearcolor( color=self.state.color) elif force or any(prop in changed for prop in CMAP_PROPERTIES): # Higher up we oversampled the points in the line so that # half a segment on either side of each point has the right # color, so we need to also oversample the color here. c = ensure_numerical(self.layer[self.state.cmap_att].ravel()) self.line_collection.set_linearcolor(data=c, state=self.state) if force or 'linewidth' in changed: self.line_collection.set_linewidth(self.state.linewidth) if force or 'linestyle' in changed: self.line_collection.set_linestyle(self.state.linestyle) if self.state.vector_visible and self.vector_artist is not None: if self.state.cmap_mode == 'Fixed': if force or 'color' in changed or 'cmap_mode' in changed: self.vector_artist.set_array(None) self.vector_artist.set_color(self.state.color) elif force or any(prop in changed for prop in CMAP_PROPERTIES): c = ensure_numerical(self.layer[self.state.cmap_att].ravel()) set_mpl_artist_cmap(self.vector_artist, c, self.state) if self.state.xerr_visible or self.state.yerr_visible: for eartist in ravel_artists(self.errorbar_artist): if self.state.cmap_mode == 'Fixed': if force or 'color' in changed or 'cmap_mode' in changed: eartist.set_color(self.state.color) elif force or any(prop in changed for prop in CMAP_PROPERTIES): c = ensure_numerical( self.layer[self.state.cmap_att].ravel()).copy() c = c[self._errorbar_keep] set_mpl_artist_cmap(eartist, c, self.state) if force or 'alpha' in changed: eartist.set_alpha(self.state.alpha) if force or 'visible' in changed: eartist.set_visible(self.state.visible) if force or 'zorder' in changed: eartist.set_zorder(self.state.zorder) for artist in [ self.scatter_artist, self.plot_artist, self.vector_artist, self.line_collection, self.density_artist ]: if artist is None: continue if force or 'alpha' in changed: artist.set_alpha(self.state.alpha) if force or 'zorder' in changed: artist.set_zorder(self.state.zorder) if force or 'visible' in changed: # We need to hide the density artist if it is not needed because # otherwise it might still show even if there is no data as the # neutral/zero color might not be white. if artist is self.density_artist: artist.set_visible(self.state.visible and self.state.density_map and self.state.markers_visible) else: artist.set_visible(self.state.visible) self.redraw()
def compute_fixed_resolution_buffer(data, bounds, target_data=None, target_cid=None, subset_state=None, broadcast=True, cache_id=None): """ Get a fixed-resolution buffer for a dataset. Parameters ---------- data : `~glue.core.Data` The dataset from which to extract a fixed resolution buffer bounds : list The list of bounds for the fixed resolution buffer. This list should have as many items as there are dimensions in ``target_data``. Each item should either be a scalar value, or a tuple of ``(min, max, nsteps)``. target_data : `~glue.core.Data`, optional The data in whose frame of reference the bounds are defined. Defaults to ``data``. target_cid : `~glue.core.component_id.ComponentID`, optional If specified, gives the component ID giving the component to use for the data values. Alternatively, use ``subset_state`` to get a subset mask. subset_state : `~glue.core.subset.SubsetState`, optional If specified, gives the subset state for which to compute a mask. Alternatively, use ``target_cid`` if you want to get data values. broadcast : bool, optional If `True`, then if a dimension in ``target_data`` for which ``bounds`` is not a scalar does not affect any of the dimensions in ``data``, then the final array will be effectively broadcast along this dimension, otherwise an error will be raised. """ if target_data is None: target_data = data if target_cid is None and subset_state is None: raise ValueError("Either target_cid or subset_state should be specified") if target_cid is not None and subset_state is not None: raise ValueError("Either target_cid or subset_state should be specified (not both)") # If cache_id is specified, we keep a cached version of the resulting array # indexed by cache_id as well as a hash formed of the call arguments to this # function. We then check if the resulting array already exists in the cache. if cache_id is not None: if subset_state is None: # Use uuid for component ID since otherwise component IDs don't return # False when comparing two different CIDs (instead they return a subset state). # For bounds we use a special wrapper that can identify wildcards. current_array_hash = (data, bounds, target_data, target_cid.uuid, broadcast) else: current_array_hash = (data, bounds, target_data, subset_state, broadcast) current_pixel_hash = (data, target_data) if cache_id in ARRAY_CACHE: if ARRAY_CACHE[cache_id]['hash'] == current_array_hash: return ARRAY_CACHE[cache_id]['array'] # To save time later, if the pixel cache doesn't match at the level of the # data and target_data, we just reset the cache. if cache_id in PIXEL_CACHE: if PIXEL_CACHE[cache_id]['hash'] != current_pixel_hash: PIXEL_CACHE.pop(cache_id) # Start off by generating arrays of coordinates in the original dataset pixel_coords = [np.linspace(*bound) if isinstance(bound, tuple) else bound for bound in bounds] pixel_coords = np.meshgrid(*pixel_coords, indexing='ij', copy=False) # Keep track of the original shape of these arrays original_shape = pixel_coords[0].shape # Now loop through the dimensions of 'data' to find the corresponding # coordinates in the frame of view of this dataset. translated_coords = [] dimensions_all = [] invalid_all = np.zeros(original_shape, dtype=bool) for ipix, pix in enumerate(data.pixel_component_ids): # At this point, if cache_id is in PIXEL_CACHE, we know that data and # target_data match so we just check the bounds. Note that the bounds # include the AnyScalar wildcard for any dimensions that don't impact # the pixel coordinates here. We do this so that we don't have to # recompute the pixel coordinates when e.g. slicing through cubes. if cache_id in PIXEL_CACHE and ipix in PIXEL_CACHE[cache_id] and PIXEL_CACHE[cache_id][ipix]['bounds'] == bounds: translated_coord = PIXEL_CACHE[cache_id][ipix]['translated_coord'] dimensions = PIXEL_CACHE[cache_id][ipix]['dimensions'] invalid = PIXEL_CACHE[cache_id][ipix]['invalid'] else: translated_coord, dimensions = translate_pixel(target_data, pixel_coords, pix) # The returned coordinates may often be a broadcasted array. To convert # the coordinates to integers and check which ones are within bounds, we # thus operate on the un-broadcasted array, before broadcasting it back # to the original shape. translated_coord = np.round(unbroadcast(translated_coord)).astype(int) invalid = (translated_coord < 0) | (translated_coord >= data.shape[ipix]) # Since we are going to be using these coordinates later on to index an # array, we need the coordinates to be within the array, so we reset # any invalid coordinates and keep track of which pixels are invalid # to reset them later. translated_coord[invalid] = 0 # We now populate the cache if cache_id is not None: if cache_id not in PIXEL_CACHE: PIXEL_CACHE[cache_id] = {'hash': current_pixel_hash} PIXEL_CACHE[cache_id][ipix] = {'translated_coord': translated_coord, 'dimensions': dimensions, 'invalid': invalid, 'bounds': bounds_for_cache(bounds, dimensions)} invalid_all |= invalid # Broadcast back to the original shape and add to the list translated_coords.append(broadcast_to(translated_coord, original_shape)) # Also keep track of all the dimensions that contributed to this coordinate dimensions_all.extend(dimensions) translated_coords = tuple(translated_coords) # If a dimension from the target data for which bounds was set to an interval # did not actually contribute to any of the coordinates in data, then if # broadcast is set to False we raise an error, otherwise we proceed and # implicitly broadcast values along that dimension of the target data. if data is not target_data and not broadcast: for i in range(target_data.ndim): if isinstance(bounds[i], tuple) and i not in dimensions_all: raise IncompatibleDataException() # PERF: optimize further - check if we can extract a sub-region that # contains all the valid values. # Take subset_state into account, if present if subset_state is None: array = data.get_data(target_cid, view=translated_coords).astype(float) invalid_value = -np.inf else: array = data.get_mask(subset_state, view=translated_coords) invalid_value = False if np.any(invalid_all): if not array.flags.writeable: array = np.array(array, dtype=type(invalid_value)) array[invalid_all] = invalid_value # Drop dimensions for which bounds were scalars slices = [] for bound in bounds: if isinstance(bound, tuple): slices.append(slice(None)) else: slices.append(0) array = array[tuple(slices)] if cache_id is not None: # For the bounds, we use a special wildcard for bounds that don't affect # the result. This will allow the cache to match regardless of the # value for those bounds. However, we only do this for scalar bounds. cache_bounds = bounds_for_cache(bounds, dimensions_all) current_array_hash = current_array_hash[:1] + (cache_bounds,) + current_array_hash[2:] if subset_state is None: ARRAY_CACHE[cache_id] = {'hash': current_array_hash, 'array': array} else: ARRAY_CACHE[cache_id] = {'hash': current_array_hash, 'array': array} return array
def _calculate(self, view=None): if self.world: # Calculating the world coordinates can be a bottleneck if we aren't # careful, so we need to make sure that if not all dimensions depend # on each other, we use smart broadcasting. # The unoptimized way to do this for an N-dimensional dataset would # be to construct N-dimensional arrays of pixel values for each # coordinate. However, if we are computing the coordinates for axis # i, and axis i is not dependent on any other axis, then the result # will be an N-dimensional array where the same 1D array of # coordinates will be repeated over and over. # To optimize this, we therefore essentially consider only the # dependent dimensions and then broacast the result to the full # array size at the very end. # view=None actually adds a dimension which is never what we really # mean, at least in glue. if view is None: view = Ellipsis # If the view is a tuple or list of arrays, we should actually just # convert these straight to world coordinates since the indices # of the pixel coordinates are the pixel coordinates themselves. if isinstance(view, (tuple, list)) and isinstance(view[0], np.ndarray): return np.array(self._data.coords.pixel2world(*view[::-1])[::-1])[self.axis] # For 1D arrays, slice can be given as a single slice but we need # to wrap it in a list to make the following code work correctly, # as it is then consistent with higher-dimensional cases. if isinstance(view, slice) or np.isscalar(view): view = [view] # Some views, e.g. with lists of integer arrays, can give arbitrarily # complex (copied) subsets of arrays, so in this case we don't do any # optimization if view is Ellipsis: optimize_view = False else: for v in view: if not np.isscalar(v) and not isinstance(v, slice): optimize_view = False break else: optimize_view = True pix_coords = [] dep_coords = self._data.coords.dependent_axes(self.axis) final_slice = [] final_shape = [] for i in range(self._data.ndim): if optimize_view and i < len(view) and np.isscalar(view[i]): final_slice.append(0) else: final_slice.append(slice(None)) # We set up a 1D pixel axis along that dimension. pix_coord = np.arange(self._data.shape[i]) # If a view was specified, we need to take it into account for # that axis. if optimize_view and i < len(view): pix_coord = pix_coord[view[i]] if not np.isscalar(view[i]): final_shape.append(len(pix_coord)) else: final_shape.append(self._data.shape[i]) if i not in dep_coords: # The axis is not dependent on this instance's axis, so we # just compute the values once and broadcast along this # dimension later. pix_coord = 0 pix_coords.append(pix_coord) # We build the list of N arrays, one for each pixel coordinate pix_coords = np.meshgrid(*pix_coords, indexing='ij', copy=False) # Finally we convert these to world coordinates axis = self._data.ndim - 1 - self.axis world_coords = self._data.coords.pixel2world_single_axis(*pix_coords[::-1], axis=axis) # We get rid of any dimension for which using the view should get # rid of that dimension. if optimize_view: world_coords = world_coords[tuple(final_slice)] # We then broadcast the final array back to what it should be world_coords = broadcast_to(world_coords, tuple(final_shape)) # We apply the view if we weren't able to optimize before if optimize_view: return world_coords else: return world_coords[view] else: slices = [slice(0, s, 1) for s in self.shape] grids = np.broadcast_arrays(*np.ogrid[slices]) if view is not None: grids = [g[view] for g in grids] return grids[self.axis]
def _update_visual_attributes(self, changed, force=False): if not self.enabled: return if self.state.style == 'Scatter': if self.state.cmap_mode == 'Fixed' and self.state.size_mode == 'Fixed': if force or 'color' in changed: self.plot_artist.set_color(self.state.color) if force or 'size' in changed or 'size_scaling' in changed: self.plot_artist.set_markersize(self.state.size * self.state.size_scaling) artist = self.plot_artist else: # TEMPORARY: Matplotlib has a bug that causes set_alpha to # change the colors back: https://github.com/matplotlib/matplotlib/issues/8953 if 'alpha' in changed: force = True if force or any(prop in changed for prop in CMAP_PROPERTIES): if self.state.cmap_mode == 'Fixed': c = self.state.color vmin = vmax = cmap = None else: c = self.layer[self.state.cmap_att].ravel() vmin = self.state.cmap_vmin vmax = self.state.cmap_vmax cmap = self.state.cmap if self.state.cmap_mode == 'Fixed': self.scatter_artist.set_facecolors(c) else: self.scatter_artist.set_array(c) self.scatter_artist.set_cmap(cmap) if vmin > vmax: self.scatter_artist.set_clim(vmax, vmin) self.scatter_artist.set_norm(InvertedNormalize(vmax, vmin)) else: self.scatter_artist.set_clim(vmin, vmax) self.scatter_artist.set_norm(Normalize(vmin, vmax)) self.scatter_artist.set_edgecolor('none') if force or any(prop in changed for prop in SIZE_PROPERTIES): if self.state.size_mode == 'Fixed': s = self.state.size * self.state.size_scaling s = broadcast_to(s, self.scatter_artist.get_sizes().shape) else: s = self.layer[self.state.size_att].ravel() s = ((s - self.state.size_vmin) / (self.state.size_vmax - self.state.size_vmin)) * 30 s *= self.state.size_scaling # Note, we need to square here because for scatter, s is actually # proportional to the marker area, not radius. self.scatter_artist.set_sizes(s ** 2) artist = self.scatter_artist if self.state.xerr_visible or self.state.yerr_visible: for eartist in list(self.errorbar_artist[2]): if eartist is None: continue if force or 'color' in changed: eartist.set_color(self.state.color) if force or 'alpha' in changed: eartist.set_alpha(self.state.alpha) if force or 'visible' in changed: eartist.set_visible(self.state.visible) if force or 'zorder' in changed: eartist.set_zorder(self.state.zorder) elif self.state.style == 'Line': if force or 'color' in changed: self.line_artist.set_color(self.state.color) if force or 'linewidth' in changed: self.line_artist.set_linewidth(self.state.linewidth) if force or 'linestyle' in changed: self.line_artist.set_linestyle(self.state.linestyle) artist = self.line_artist else: raise NotImplementedError(self.state.style) # pragma: nocover if force or 'alpha' in changed: artist.set_alpha(self.state.alpha) if force or 'zorder' in changed: artist.set_zorder(self.state.zorder) if force or 'visible' in changed: artist.set_visible(self.state.visible) self.redraw()
def _update_visual_attributes(self, changed, force=False): if not self.enabled: return if self.state.markers_visible: if self.state.density_map: if self.state.cmap_mode == 'Fixed': if force or 'color' in changed or 'cmap_mode' in changed: self.density_artist.set_color(self.state.color) self.density_artist.set_clim(self.density_auto_limits.min, self.density_auto_limits.max) elif force or any(prop in changed for prop in CMAP_PROPERTIES): c = ensure_numerical(self.layer[self.state.cmap_att].ravel()) set_mpl_artist_cmap(self.density_artist, c, self.state) if force or 'stretch' in changed: self.density_artist.set_norm(ImageNormalize(stretch=STRETCHES[self.state.stretch]())) if force or 'dpi' in changed: self.density_artist.set_dpi(self._viewer_state.dpi) if force or 'density_contrast' in changed: self.density_auto_limits.contrast = self.state.density_contrast self.density_artist.stale = True else: if self.state.cmap_mode == 'Fixed' and self.state.size_mode == 'Fixed': if force or 'color' in changed or 'fill' in changed: if self.state.fill: self.plot_artist.set_markeredgecolor('none') self.plot_artist.set_markerfacecolor(self.state.color) else: self.plot_artist.set_markeredgecolor(self.state.color) self.plot_artist.set_markerfacecolor('none') if force or 'size' in changed or 'size_scaling' in changed: self.plot_artist.set_markersize(self.state.size * self.state.size_scaling) else: # TEMPORARY: Matplotlib has a bug that causes set_alpha to # change the colors back: https://github.com/matplotlib/matplotlib/issues/8953 if 'alpha' in changed: force = True if self.state.cmap_mode == 'Fixed': if force or 'color' in changed or 'cmap_mode' in changed or 'fill' in changed: self.scatter_artist.set_array(None) if self.state.fill: self.scatter_artist.set_facecolors(self.state.color) self.scatter_artist.set_edgecolors('none') else: self.scatter_artist.set_facecolors('none') self.scatter_artist.set_edgecolors(self.state.color) elif force or any(prop in changed for prop in CMAP_PROPERTIES) or 'fill' in changed: self.scatter_artist.set_edgecolors(None) self.scatter_artist.set_facecolors(None) c = ensure_numerical(self.layer[self.state.cmap_att].ravel()) set_mpl_artist_cmap(self.scatter_artist, c, self.state) if self.state.fill: self.scatter_artist.set_edgecolors('none') else: colors = self.scatter_artist.get_facecolors() self.scatter_artist.set_facecolors('none') self.scatter_artist.set_edgecolors(colors) if force or any(prop in changed for prop in MARKER_PROPERTIES): if self.state.size_mode == 'Fixed': s = self.state.size * self.state.size_scaling s = broadcast_to(s, self.scatter_artist.get_sizes().shape) else: s = ensure_numerical(self.layer[self.state.size_att].ravel()) s = ((s - self.state.size_vmin) / (self.state.size_vmax - self.state.size_vmin)) # The following ensures that the sizes are in the # range 3 to 30 before the final size_scaling. np.clip(s, 0, 1, out=s) s *= 0.95 s += 0.05 s *= (30 * self.state.size_scaling) # Note, we need to square here because for scatter, s is actually # proportional to the marker area, not radius. self.scatter_artist.set_sizes(s ** 2) if self.state.line_visible: if self.state.cmap_mode == 'Fixed': if force or 'color' in changed or 'cmap_mode' in changed: self.line_collection.set_linearcolor(color=self.state.color) elif force or any(prop in changed for prop in CMAP_PROPERTIES): # Higher up we oversampled the points in the line so that # half a segment on either side of each point has the right # color, so we need to also oversample the color here. c = ensure_numerical(self.layer[self.state.cmap_att].ravel()) self.line_collection.set_linearcolor(data=c, state=self.state) if force or 'linewidth' in changed: self.line_collection.set_linewidth(self.state.linewidth) if force or 'linestyle' in changed: self.line_collection.set_linestyle(self.state.linestyle) if self.state.vector_visible and self.vector_artist is not None: if self.state.cmap_mode == 'Fixed': if force or 'color' in changed or 'cmap_mode' in changed: self.vector_artist.set_array(None) self.vector_artist.set_color(self.state.color) elif force or any(prop in changed for prop in CMAP_PROPERTIES): c = ensure_numerical(self.layer[self.state.cmap_att].ravel()) set_mpl_artist_cmap(self.vector_artist, c, self.state) if self.state.xerr_visible or self.state.yerr_visible: for eartist in ravel_artists(self.errorbar_artist): if self.state.cmap_mode == 'Fixed': if force or 'color' in changed or 'cmap_mode' in changed: eartist.set_color(self.state.color) elif force or any(prop in changed for prop in CMAP_PROPERTIES): c = ensure_numerical(self.layer[self.state.cmap_att].ravel()) set_mpl_artist_cmap(eartist, c, self.state) if force or 'alpha' in changed: eartist.set_alpha(self.state.alpha) if force or 'visible' in changed: eartist.set_visible(self.state.visible) if force or 'zorder' in changed: eartist.set_zorder(self.state.zorder) for artist in [self.scatter_artist, self.plot_artist, self.vector_artist, self.line_collection, self.density_artist]: if artist is None: continue if force or 'alpha' in changed: artist.set_alpha(self.state.alpha) if force or 'zorder' in changed: artist.set_zorder(self.state.zorder) if force or 'visible' in changed: artist.set_visible(self.state.visible) self.redraw()
def _update_visual_attributes(self, changed, force=False): if not self.enabled: return if self.state.markers_visible: if self.state.density_map: if self.state.cmap_mode == 'Fixed': if force or 'color' in changed or 'cmap_mode' in changed: self.density_artist.set_color(self.state.color) self.density_artist.set_c(None) self.density_artist.set_clim( self.density_auto_limits.min, self.density_auto_limits.max) elif force or any(prop in changed for prop in CMAP_PROPERTIES): c = self.layer[self.state.cmap_att].ravel() set_mpl_artist_cmap(self.density_artist, c, self.state) if force or 'stretch' in changed: self.density_artist.set_norm( ImageNormalize( stretch=STRETCHES[self.state.stretch]())) if force or 'dpi' in changed: self.density_artist.set_dpi(self._viewer_state.dpi) if force or 'density_contrast' in changed: self.density_auto_limits.contrast = self.state.density_contrast self.density_artist.stale = True else: if self.state.cmap_mode == 'Fixed' and self.state.size_mode == 'Fixed': if force or 'color' in changed: self.plot_artist.set_color(self.state.color) if force or 'size' in changed or 'size_scaling' in changed: self.plot_artist.set_markersize( self.state.size * self.state.size_scaling) else: # TEMPORARY: Matplotlib has a bug that causes set_alpha to # change the colors back: https://github.com/matplotlib/matplotlib/issues/8953 if 'alpha' in changed: force = True if self.state.cmap_mode == 'Fixed': if force or 'color' in changed or 'cmap_mode' in changed: self.scatter_artist.set_facecolors( self.state.color) self.scatter_artist.set_edgecolor('none') elif force or any(prop in changed for prop in CMAP_PROPERTIES): c = self.layer[self.state.cmap_att].ravel() set_mpl_artist_cmap(self.scatter_artist, c, self.state) self.scatter_artist.set_edgecolor('none') if force or any(prop in changed for prop in MARKER_PROPERTIES): if self.state.size_mode == 'Fixed': s = self.state.size * self.state.size_scaling s = broadcast_to( s, self.scatter_artist.get_sizes().shape) else: s = self.layer[self.state.size_att].ravel() s = ((s - self.state.size_vmin) / (self.state.size_vmax - self.state.size_vmin)) * 30 s *= self.state.size_scaling # Note, we need to square here because for scatter, s is actually # proportional to the marker area, not radius. self.scatter_artist.set_sizes(s**2) if self.state.line_visible: if self.state.cmap_mode == 'Fixed': if force or 'color' in changed or 'cmap_mode' in changed: self.line_collection.set_array(None) self.line_collection.set_color(self.state.color) elif force or any(prop in changed for prop in CMAP_PROPERTIES): # Higher up we oversampled the points in the line so that # half a segment on either side of each point has the right # color, so we need to also oversample the color here. c = self.layer[self.state.cmap_att].ravel() cnew = np.zeros((len(c) - 1) * 2) cnew[::2] = c[:-1] cnew[1::2] = c[1:] set_mpl_artist_cmap(self.line_collection, cnew, self.state) if force or 'linewidth' in changed: self.line_collection.set_linewidth(self.state.linewidth) if force or 'linestyle' in changed: self.line_collection.set_linestyle(self.state.linestyle) if self.state.vector_visible and self.vector_artist is not None: if self.state.cmap_mode == 'Fixed': if force or 'color' in changed or 'cmap_mode' in changed: self.vector_artist.set_array(None) self.vector_artist.set_color(self.state.color) elif force or any(prop in changed for prop in CMAP_PROPERTIES): c = self.layer[self.state.cmap_att].ravel() set_mpl_artist_cmap(self.vector_artist, c, self.state) if self.state.xerr_visible or self.state.yerr_visible: for eartist in list(self.errorbar_artist[2]): if eartist is None: continue if self.state.cmap_mode == 'Fixed': if force or 'color' in changed or 'cmap_mode' in changed: eartist.set_color(self.state.color) elif force or any(prop in changed for prop in CMAP_PROPERTIES): c = self.layer[self.state.cmap_att].ravel() set_mpl_artist_cmap(eartist, c, self.state) if force or 'alpha' in changed: eartist.set_alpha(self.state.alpha) if force or 'visible' in changed: eartist.set_visible(self.state.visible) if force or 'zorder' in changed: eartist.set_zorder(self.state.zorder) for artist in [ self.scatter_artist, self.plot_artist, self.vector_artist, self.line_collection, self.density_artist ]: if artist is None: continue if force or 'alpha' in changed: artist.set_alpha(self.state.alpha) if force or 'zorder' in changed: artist.set_zorder(self.state.zorder) if force or 'visible' in changed: artist.set_visible(self.state.visible) self.redraw()