def __propagateLocation(self, dest): """Called by the :meth:`__locationChanged` and :meth:`__worldLocationChanged` methods. The ``dest`` argument may be either ``'world'`` (the ``worldLocation`` is updated from the ``location``), or ``'display'`` (vice-versa). """ if self.displaySpace == 'world': if dest == 'world': with props.skip(self, 'worldLocation', self.__name): self.worldLocation = self.location else: with props.skip(self, 'location', self.__name): self.location = self.worldLocation return ref = self.displaySpace opts = self.getOpts(ref) if dest == 'world': with props.skip(self, 'location', self.__name): self.worldLocation = opts.transformCoords( self.location, 'display', 'world') else: with props.skip(self, 'worldLocation', self.__name): self.location = opts.transformCoords(self.worldLocation, 'world', 'display')
def __setTransform(self, image): """Sets the :attr:`.NiftiOpts.transform` property associated with the given :class:`.Nifti` overlay to a sensible value, given the current value of the :attr:`.displaySpace` property. Called by the :meth:`__displaySpaceChanged` method, and by :meth:`__overlayListChanged` for any :class:`.Image` overlays which have been newly added to the :class:`.OverlayList`. :arg image: An :class:`.Image` overlay. """ space = self.displaySpace opts = self.getOpts(image) # Disable notification of the bounds # property so the __overlayBoundsChanged # method does not get called. Use # ignoreInvalid, because this method might # get called before we have registered a # listener on the bounds property. with props.skip(opts, 'bounds', self.__name, ignoreInvalid=True): if space == 'world': opts.transform = 'affine' elif image is space: opts.transform = 'pixdim-flip' else: opts.transform = 'reference'
def __onListRemove(self, ev): """Called when the user removes an item from the :class:`.EditableListBox`. Removes the corresponding :class:`.DataSeries` instance from the :attr:`.PlotPanel.dataSeries` list of the :class:`.OverlayPlotPanel`. """ with props.skip(self.__plotPanel, 'dataSeries', self.name): self.__plotPanel.dataSeries.remove(ev.data)
def __volumeChanged(self, *args, **kwargs): """Called when the :attr:`volume` property changes, and also by the :meth:`__init__` method. Re-calculates some things for the new overlay volume. """ opts = self.__opts overlay = self.overlay # We cache the following for each volume # so they don't need to be recalculated: # - finite data # - non-zero data # - finite minimum # - finite maximum # # The cache size is restricted (see its # creation in __init__) so we don't blow # out RAM volkey = (opts.volumeDim, opts.volume) volprops = self.__volCache.get(volkey, None) if volprops is None: log.debug('Volume changed {} - extracting ' 'finite/non-zero data'.format(volkey)) finData = overlay[opts.index()] finData = finData[np.isfinite(finData)] nzData = finData[finData != 0] dmin = finData.min() dmax = finData.max() self.__volCache.put(volkey, (finData, nzData, dmin, dmax)) else: log.debug('Volume changed {} - got finite/' 'non-zero data from cache'.format(volkey)) finData, nzData, dmin, dmax = volprops dist = (dmax - dmin) / 10000.0 with props.suppressAll(self): self.dataRange.xmin = dmin self.dataRange.xmax = dmax + dist self.dataRange.xlo = dmin self.dataRange.xhi = dmax + dist self.nbins = autoBin(nzData, self.dataRange.x) self.__finiteData = finData self.__nonZeroData = nzData self.__dataRangeChanged() with props.skip(self, 'dataRange', self.__name): self.propNotify('dataRange')
def __onResize(self, ev): """Called on ``wx.EVT_SIZE`` events, when the canvas is resized. When the canvas is resized, we have to update the display bounds to preserve the aspect ratio. """ ev.Skip() with props.skip(self.opts, 'displayBounds', self.name): centre = self.getDisplayCentre() self._updateDisplayBounds() self.centreDisplayAt(*centre)
def __colourChanged(self, *a): """Called when :attr:`.colour` changes. Updates :attr:`.Display.alpha` from the alpha component. """ alpha = self.colour[3] * 100 log.debug('Propagating MeshOpts.colour[3] to ' 'Display.alpha [{}]'.format(alpha)) with props.skip(self.display, 'alpha', self.name): self.display.alpha = alpha
def __lbRemove(self, ev): """Called when an item is removed from the overlay listbox. Removes the corresponding overlay from the :class:`.OverlayList`. """ overlay = self.displayCtx.overlayOrder[ev.idx] overlay = self.overlayList[overlay] with props.skip(self.overlayList, 'overlays', self.name), \ props.skip(self.displayCtx, 'overlayOrder', self.name): if not removeoverlay.removeOverlay(self.overlayList, self.displayCtx, overlay): ev.Veto() # The overlayListChanged method # must be called asynchronously, # otherwise it will corrupt the # EditableListBox state else: idle.idle(self.__overlayListChanged)
def __alphaChanged(self, *a): """Called when :attr:`.Display.alpha` changes. Updates the alpha component of :attr:`.colour`. """ alpha = self.display.alpha / 100.0 r, g, b, _ = self.colour log.debug('Propagating Display.alpha to MeshOpts.' 'colour[3] [{}]'.format(alpha)) with props.skip(self, 'colour', self.name): self.colour = r, g, b, alpha
def __colourChanged(self, *a): """Called when :attr:`.colour` changes. Updates :attr:`.Display.alpha` from the alpha component. """ # modulateAlpha may cause the # alpha property to be disabled if not self.display.propertyIsEnabled('alpha'): return alpha = self.colour[3] * 100 log.debug('Propagating MeshOpts.colour[3] to ' 'Display.alpha [{}]'.format(alpha)) with props.skip(self.display, 'alpha', self.name): self.display.alpha = alpha
def __onGridSelect(self, ev): """Called when a row is selected on the :class:`.WidgetGrid`. Makes sure that the 'new tag' control in the corresponding :class:`.TextTagPanel` is focused. """ component = ev.row opts = self.displayCtx.getOpts(self.__overlay) log.debug('Grid row selected (component {}) - updating ' 'overlay volume'.format(component)) with props.skip(opts, 'volume', self.name): opts.volume = component tags = self.__grid.GetWidget(ev.row, 1) tags.FocusNewTagCtrl()
def __overlayListChanged(self, *a): """Called when the :class:`.OverlayList` changes. If a 3D mask overlay was being shown, and it has been removed from the ``OverlayList``, the :attr:`.HistogramSeries.showOverlay` property is updated accordingly. """ if self.__histMask is None: return # If a 3D overlay was being shown, and it # has been removed from the overlay list # by the user, turn the showOverlay property # off if self.__histMask not in self.__overlayList: with props.skip(self.__histSeries, 'showOverlay', self.__name): self.__histSeries.showOverlay = False self.__showOverlayChanged()
def __setLut(self, lut): """Updates this ``LookupTablePanel`` to display the labels for the given ``lut`` (assumed to be a :class:`.LookupTable` instance). If the currently selected overlay is associated with a :class:`.LabelOpts` instance, its :attr:`.LabelOpts.lut` property is set to the new ``LookupTable``. """ if self.__selectedLut == lut: return log.debug('Selecting lut: {}'.format(lut)) if self.__selectedLut is not None: self.__selectedLut.deregister(self.name, 'saved') self.__selectedLut.deregister(self.name, 'added') self.__selectedLut.deregister(self.name, 'removed') self.__selectedLut = lut if lut is not None: lut.register(self.name, self.__lutSaveStateChanged, 'saved') lut.register(self.name, self.__lutLabelAdded, 'added') lut.register(self.name, self.__lutLabelRemoved, 'removed') if lut is not None and self.__selectedOpts is not None: with props.skip(self.__selectedOpts, 'lut', self.name): self.__selectedOpts.lut = lut allLuts = fslcmaps.getLookupTables() self.__lutChoice.SetSelection(allLuts.index(lut)) self.__lutSaveStateChanged() self.__createLabelList()
def applyLabels(labelFile, overlay, allLabels, newOverlay): # labelFile: Path to the loaded label file # overlay: Overlay to apply them to # allLabels: Loaded labels (list of (component, [label]) tuples) # newOverlay: True if the selected overlay has changed, False # otherwise lut = self.__lut volLabels = self.overlayList.getData(overlay, 'VolumeLabels') ncomps = volLabels.numComponents() nlabels = len(allLabels) # Error: number of labels in the # file is greater than the number # of components in the overlay. if ncomps < nlabels: msg = strings.messages[self, 'wrongNComps'].format( labelFile, overlay.dataSource) title = strings.titles[ self, 'loadError'] wx.MessageBox(msg, title, wx.ICON_ERROR | wx.OK) return # Number of labels in the file is # less than number of components # in the overlay - we pad the # labels with 'Unknown' elif ncomps > nlabels: for i in range(nlabels, ncomps): allLabels.append(['Unknown']) # Disable notification while applying # labels so the component/label grids # don't confuse themselves. with volLabels.skip(self.__componentGrid.name), \ volLabels.skip(self.__labelGrid .name): volLabels.clear() for comp, lbls in enumerate(allLabels): for lbl in lbls: volLabels.addLabel(comp, lbl) # Make sure a colour in the melodic # lookup table exists for all labels for label in volLabels.getAllLabels(): label = volLabels.getDisplayLabel(label) lutLabel = lut.getByName(label) if lutLabel is None: log.debug('New melodic classification ' 'label: {}'.format(label)) lut.new(label, colour=fslcm.randomBrightColour()) # New overlay was loaded if newOverlay: # Make sure the new image is selected. with props.skip(self.displayCtx, 'selectedOverlay', self.name): self.displayCtx.selectOverlay(overlay) self.__componentGrid.setOverlay(overlay) self.__labelGrid .setOverlay(overlay) # Labels were applied to # already selected overlay. else: self.__componentGrid.refreshTags() self.__labelGrid .refreshTags()
def __onLoadButton(self, ev): """Called when the *Load labels* button is pushed. Prompts the user to select a label file to load, then does the following: 1. If the selected label file refers to the currently selected melodic_IC overlay, the labels are applied to the overlay. 2. If the selected label file refers to a different melodic_IC overlay, the user is asked whether they want to load the different melodic_IC file (the default), or whether they want the labels applied to the existing overlay. 3. If the selected label file does not refer to any overlay (it only contains the bad component list), the user is asked whether they want the labels applied to the current melodic_IC overlay. If the number of labels in the file is less than the number of melodic_IC components, the remaining components are labelled as unknown. If the number of labels in the file is greater than the number of melodic_IC components, an error is shown, and nothing is done. """ # The aim of the code beneath the # applyLabels function is to load # a set of component labels, and # to figure out which overlay # they should be added to. # When it has done this, it calls # applyLabels, which applies the # loaded labels to the overlay. def applyLabels(labelFile, overlay, allLabels, newOverlay): # labelFile: Path to the loaded label file # overlay: Overlay to apply them to # allLabels: Loaded labels (list of (component, [label]) tuples) # newOverlay: True if the selected overlay has changed, False # otherwise lut = self.__lut volLabels = self.overlayList.getData(overlay, 'VolumeLabels') ncomps = volLabels.numComponents() nlabels = len(allLabels) # Error: number of labels in the # file is greater than the number # of components in the overlay. if ncomps < nlabels: msg = strings.messages[self, 'wrongNComps'].format( labelFile, overlay.dataSource) title = strings.titles[ self, 'loadError'] wx.MessageBox(msg, title, wx.ICON_ERROR | wx.OK) return # Number of labels in the file is # less than number of components # in the overlay - we pad the # labels with 'Unknown' elif ncomps > nlabels: for i in range(nlabels, ncomps): allLabels.append(['Unknown']) # Disable notification while applying # labels so the component/label grids # don't confuse themselves. with volLabels.skip(self.__componentGrid.name), \ volLabels.skip(self.__labelGrid .name): volLabels.clear() for comp, lbls in enumerate(allLabels): for lbl in lbls: volLabels.addLabel(comp, lbl) # Make sure a colour in the melodic # lookup table exists for all labels for label in volLabels.getAllLabels(): label = volLabels.getDisplayLabel(label) lutLabel = lut.getByName(label) if lutLabel is None: log.debug('New melodic classification ' 'label: {}'.format(label)) lut.new(label, colour=fslcm.randomBrightColour()) # New overlay was loaded if newOverlay: # Make sure the new image is selected. with props.skip(self.displayCtx, 'selectedOverlay', self.name): self.displayCtx.selectOverlay(overlay) self.__componentGrid.setOverlay(overlay) self.__labelGrid .setOverlay(overlay) # Labels were applied to # already selected overlay. else: self.__componentGrid.refreshTags() self.__labelGrid .refreshTags() # If the current overlay is a compatible # Image, the open file dialog starting # point will be its directory. overlay = self.__overlay if overlay is not None and overlay.dataSource is not None: loadDir = op.dirname(overlay.dataSource) # Otherwise it will be the most # recent overlay load directory. else: loadDir = fslsettings.read('loadSaveOverlayDir', os.getcwd()) # Ask the user to select a label file dlg = wx.FileDialog( self, message=strings.titles[self, 'loadDialog'], defaultDir=loadDir, style=wx.FD_OPEN) # User cancelled the dialog if dlg.ShowModal() != wx.ID_OK: return # Load the specified label file filename = dlg.GetPath() emsg = strings.messages[self, 'loadError'].format(filename) etitle = strings.titles[ self, 'loadError'] try: with status.reportIfError(msg=emsg, title=etitle): melDir, allLabels = fixlabels.loadLabelFile(filename) except Exception: return # Ok we've got the labels, now # we need to figure out which # overlay to add them to. # If the label file does not refer # to a Melodic directory, and the # current overlay is a compatible # image, apply the labels to the # image. if overlay is not None and melDir is None: applyLabels(filename, overlay, allLabels, False) return # If the label file refers to a # Melodic directory, and the # current overlay is a compatible # image. if overlay is not None and melDir is not None: if isinstance(overlay, fslmelimage.MelodicImage): overlayDir = overlay.getMelodicDir() elif overlay.dataSource is not None: overlayDir = op.dirname(overlay.dataSource) else: overlayDir = 'none' # And both the current overlay and # the label file refer to the same # directory, then we apply the # labels to the curent overlay. if op.abspath(melDir) == op.abspath(overlayDir): applyLabels(filename, overlay, allLabels, False) return # Otherwise, if the overlay and the # label file refer to different # directories... # Ask the user whether they want to load # the image specified in the label file, # or apply the labels to the currently # selected image. dlg = wx.MessageDialog( self, strings.messages[self, 'diffMelDir'].format( melDir, overlayDir), style=wx.ICON_QUESTION | wx.YES_NO | wx.CANCEL) dlg.SetYesNoLabels( strings.messages[self, 'diffMelDir.labels'], strings.messages[self, 'diffMelDir.overlay']) response = dlg.ShowModal() # User cancelled the dialog if response == wx.ID_CANCEL: return # User chose to load the melodic # image specified in the label # file. We'll carry on with this # processing below. elif response == wx.ID_YES: pass # Apply the labels to the current # overlay, even though they are # from different analyses. else: applyLabels(filename, overlay, allLabels, False) return # If we've reached this far, we are # going to attempt to identify the # image associated with the label # file, load that image, and then # apply the labels. # The label file does not # specify a melodic directory if melDir is None: msg = strings.messages[self, 'noMelDir'].format(filename) title = strings.titles[ self, 'loadError'] wx.MessageBox(msg, title, wx.ICON_ERROR | wx.OK) return # Try loading the melodic_IC image # specified in the label file. try: overlay = fslmelimage.MelodicImage(melDir) log.debug('Adding {} to overlay list'.format(overlay)) with props.skip(self.overlayList, 'overlays', self.name),\ props.skip(self.displayCtx, 'selectedOverlay', self.name): self.overlayList.append(overlay) if self.displayCtx.autoDisplay: autodisplay.autoDisplay(overlay, self.overlayList, self.displayCtx) fslsettings.write('loadSaveOverlayDir', op.abspath(melDir)) except Exception as e: e = str(e) msg = strings.messages[self, 'loadError'].format(filename, e) title = strings.titles[ self, 'loadError'] log.debug('Error loading classification file ' '({}), ({})'.format(filename, e), exc_info=True) wx.MessageBox(msg, title, wx.ICON_ERROR | wx.OK) # Apply the loaded labels # to the loaded overlay. applyLabels(filename, overlay, allLabels, True)
def __histPropsChanged(self, *a): """Called internally, and when any histogram settings change. Re-calculates the histogram data. """ log.debug('Calculating histogram for ' 'overlay {}'.format(self.overlay.name)) status.update('Calculating histogram for ' 'overlay {}'.format(self.overlay.name)) if np.isclose(self.dataRange.xhi, self.dataRange.xlo): self.__xdata = np.array([]) self.__ydata = np.array([]) self.__nvals = 0 return if self.ignoreZeros: if self.includeOutliers: data = self.__nonZeroData else: data = self.__clippedNonZeroData else: if self.includeOutliers: data = self.__finiteData else: data = self.__clippedFiniteData # Figure out the number of bins to use if self.autoBin: nbins = autoBin(data, self.dataRange.x) else: nbins = self.nbins # nbins is unclamped, but # we don't allow < 10 if nbins < 10: nbins = 10 # Update the nbins property with props.skip(self, 'nbins', self.__name): self.nbins = nbins # We cache calculated bins and counts # for each combination of parameters, # as histogram calculation can take # time. hrange = (self.dataRange.xlo, self.dataRange.xhi) drange = (self.dataRange.xmin, self.dataRange.xmax) histkey = ((self.__opts.volumeDim, self.__opts.volume), self.includeOutliers, hrange, drange, self.nbins) cached = self.__histCache.get(histkey, None) if cached is not None: histX, histY, nvals = cached else: histX, histY, nvals = histogram(data, self.nbins, hrange, drange, self.includeOutliers, True) self.__histCache.put(histkey, (histX, histY, nvals)) self.__xdata = histX self.__ydata = histY self.__nvals = nvals status.update('Histogram for {} calculated.'.format(self.overlay.name)) log.debug('Calculated histogram for overlay ' '{} (number of values: {}, number ' 'of bins: {})'.format(self.overlay.name, self.__nvals, self.nbins))
def __overlayListChanged(self, *a): """Called when the :attr:`.OverlayList.overlays` property changes. Ensures that a :class:`.Display` and :class:`.DisplayOpts` object exists for every image, updates the :attr:`bounds` property, makes sure that the :attr:`overlayOrder` property is consistent, and updates constraints on the :attr:`selectedOverlay` property. """ # Discard all Display instances # which refer to overlays that # are no longer in the list for overlay in list(self.__displays.keys()): if overlay not in self.__overlayList: display = self.__displays.pop(overlay) opts = display.opts display.removeListener('overlayType', self.__name) opts.removeListener('bounds', self.__name) # The display instance will destroy the # opts instance, so we don't do it here display.destroy() # Ensure that a Display object exists # for every overlay in the list for overlay in self.__overlayList: ovlType = self.__overlayList.initOverlayType(overlay) # The getDisplay method # will create a Display object # if one does not already exist display = self.getDisplay(overlay, ovlType) opts = display.opts # Register a listener on the overlay type, # because when it changes, the DisplayOpts # instance will change, and we will need # to re-register the DisplayOpts.bounds # listener (see the next statement) display.addListener('overlayType', self.__name, self.__overlayListChanged, overwrite=True) # Register a listener on the DisplayOpts.bounds # property for every overlay - if the display # bounds for any overlay changes, we need to # update our own bounds property. This is only # done on child DCs, as the parent DC bounds # only gets used for synchronisation if self.__child: opts.addListener('bounds', self.__name, self.__overlayBoundsChanged, overwrite=True) # If detachDisplaySpace has been called, # make sure the opts bounds (and related) # properties are also detached if not self.canBeSyncedToParent('displaySpace'): opts.detachFromParent('bounds') if isinstance(overlay, fslimage.Nifti): opts.detachFromParent('transform') # Ensure that the displaySpace # property options are in sync # with the overlay list. self.__updateDisplaySpaceOptions() # Stuff which only needs to # be done on the parent DC if not self.__child: # Limit the selectedOverlay property # so it cannot take a value greater # than len(overlayList)-1. selectedOverlay # is always synchronised, so we only # need to do this on the parent DC. nOverlays = len(self.__overlayList) if nOverlays > 0: self.setAttribute('selectedOverlay', 'maxval', nOverlays - 1) else: self.setAttribute('selectedOverlay', 'maxval', 0) return # Ensure that the overlayOrder # property is valid self.__syncOverlayOrder() # If the overlay list was empty, # and is now non-empty, we need # to initialise the display space # and the display location initDS = self.__initDS and \ np.all(np.isclose(self.bounds, 0)) and \ len(self.__overlayList) > 0 self.__initDS = len(self.__overlayList) == 0 # Initialise the display space. We # have to do this before updating # image transforms, and updating # the display bounds if initDS: displaySpace = 'world' if self.defaultDisplaySpace == 'ref': for overlay in self.__overlayList: if isinstance(overlay, fslimage.Nifti): displaySpace = overlay break with props.skip(self, 'displaySpace', self.__name): self.displaySpace = displaySpace # Initialise the transform property # of any Image overlays which have # just been added to the list, oldList = self.__overlayList.getLastValue('overlays')[:] for overlay in self.__overlayList: if isinstance(overlay, fslimage.Nifti) and \ (overlay not in oldList): self.__setTransform(overlay) # Ensure that the bounds # property is accurate self.__updateBounds() # Initialise the display location to # the centre of the display bounds if initDS: b = self.bounds self.location.xyz = [ b.xlo + b.xlen / 2.0, b.ylo + b.ylen / 2.0, b.zlo + b.zlen / 2.0 ] self.__propagateLocation('world') else: self.__propagateLocation('display')
def __updateShowOverlayRange(self, datax, which=False): """Called by the ``overlayRange`` mouse event handlers. Updates the :attr:`.HistogramSeries.showOverlayRange`. :arg datax: X data coordinate corresponding to the mouse position. :arg which: Used to keep track of which value in the ``showOverlayRange`` property the user is currently modifying. On mouse down events, this method figures out which range should be modified, and returns either ``'lo'`` or ``'hi'``. On subsequent calls to this method (on mouse drag and mouse up events), that return value should be passed back into this method so that the same value continues to get modified. """ hs = self.__currentHs rangePolygon = self.__rangePolygons.get(hs, None) if hs is None: return if rangePolygon is None: return rangelo, rangehi = hs.showOverlayRange if which == 'lo': newRange = [datax, rangehi] elif which == 'hi': newRange = [rangelo, datax] else: # Less than low range if datax < rangelo: which = 'lo' newRange = (datax, rangehi) # Less than high range elif datax > rangehi: which = 'hi' newRange = (rangelo, datax) # In between low/high ranges - # is the mouse location closer # to the low or high range? else: lodist = abs(datax - rangelo) hidist = abs(datax - rangehi) if lodist < hidist: which = 'lo' newRange = [datax, rangehi] else: which = 'hi' newRange = [rangelo, datax] if newRange[1] < newRange[0]: if which == 'lo': newRange[0] = newRange[1] elif which == 'hi': newRange[1] = newRange[0] # The range polygon will automatically update itself # when any HistogramSeries properties change. But the # canvas draw is faster if we do it manually. Hence # the listener skip. with props.skip(hs, 'showOverlayRange', rangePolygon._rp_name): hs.showOverlayRange = newRange # Manually refresh the histogram range polygon. rangePolygon.updatePolygon() return which
def setHistogramData(self, data, key): """Must be called by sub-classes whenever the underlying histogram data changes. :arg data: A ``numpy`` array containing the data that the histogram is to be calculated on. Pass in ``None`` to indicate that there is currently no histogram data. :arg key: Something which identifies the ``data``, and can be used as a ``dict`` key. """ if data is None: self.__nvals = 0 self.__dataKey = None self.__xdata = np.array([]) self.__ydata = np.array([]) self.__finiteData = np.array([]) self.__nonZeroData = np.array([]) self.__clippedFiniteData = np.array([]) self.__clippedNonZeroData = np.array([]) # force the panel to refresh with props.skip(self, 'dataRange', self.name): self.propNotify('dataRange') return # We cache the following data, based # on the provided key, so they don't # need to be recalculated: # - finite data # - non-zero data # - finite minimum # - finite maximum # # The cache size is restricted (see its # creation in __init__) so we don't blow # out RAM cached = self.__dataCache.get(key, None) if cached is None: log.debug('New histogram data {} - extracting ' 'finite/non-zero data'.format(key)) finData = data[np.isfinite(data)] nzData = finData[finData != 0] dmin = finData.min() dmax = finData.max() self.__dataCache.put(key, (finData, nzData, dmin, dmax)) else: log.debug('Got histogram data {} from cache'.format(key)) finData, nzData, dmin, dmax = cached # The upper bound on the dataRange # is exclusive, so we initialise it # to a bit more than the data max. dist = (dmax - dmin) / 10000.0 with props.suppressAll(self): self.dataRange.xmin = dmin self.dataRange.xmax = dmax + dist self.dataRange.xlo = dmin self.dataRange.xhi = dmax + dist self.nbins = autoBin(nzData, self.dataRange.x) self.__dataKey = key self.__finiteData = finData self.__nonZeroData = nzData self.__dataRangeChanged() with props.skip(self, 'dataRange', self.name): self.propNotify('dataRange')