def histogram( self, data, bins=None, logscale=None, title="", color=None, xaxis="bottom", yaxis="left", ): """ Make 1D Histogram `plot item` (:py:class:`guiqwt.histogram.HistogramItem` object) * data (1D NumPy array) * bins: number of bins (int) * logscale: Y-axis scale (bool) """ basename = _("Histogram") histparam = HistogramParam(title=basename, icon="histogram.png") curveparam = CurveParam(_("Curve"), icon="curve.png") curveparam.read_config(CONF, "histogram", "curve") if not title: global HISTOGRAM_COUNT HISTOGRAM_COUNT += 1 title = make_title(basename, HISTOGRAM_COUNT) curveparam.label = title if color is not None: curveparam.line.color = color if bins is not None: histparam.n_bins = bins if logscale is not None: histparam.logscale = logscale return self.phistogram(data, curveparam, histparam, xaxis, yaxis)
def histogram(self, data, bins=None, logscale=None, title="", color=None, xaxis="bottom", yaxis="left"): """ Make 1D Histogram `plot item` (:py:class:`guiqwt.histogram.HistogramItem` object) * data (1D NumPy array) * bins: number of bins (int) * logscale: Y-axis scale (bool) """ basename = _("Histogram") histparam = HistogramParam(title=basename, icon='histogram.png') curveparam = CurveParam(_("Curve"), icon='curve.png') curveparam.read_config(CONF, "histogram", "curve") if not title: global HISTOGRAM_COUNT HISTOGRAM_COUNT += 1 title = make_title(basename, HISTOGRAM_COUNT) curveparam.label = title if color is not None: curveparam.line.color = color if bins is not None: histparam.n_bins = bins if logscale is not None: histparam.logscale = logscale return self.phistogram(data, curveparam, histparam, xaxis, yaxis)
class CrossSectionPlot(CurvePlot): """Cross section plot""" CURVE_LABEL = _("Cross section") LABEL_TEXT = _("Enable a marker") _height = None _width = None CS_AXIS = None Z_AXIS = None Z_MAX_MAJOR = 5 CURVETYPE = None SHADE = .2 def __init__(self, parent=None): super(CrossSectionPlot, self).__init__(parent=parent, title="", section="cross_section") self.perimage_mode = True self.autoscale_mode = True self.autorefresh_mode = True self.apply_lut = False self.single_source = False self.last_obj = None self.known_items = {} self._shapes = {} self.curveparam = CurveParam(_("Curve"), icon="curve.png") self.set_curve_style("cross_section", "curve") if self._height is not None: self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Minimum) elif self._width is not None: self.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Expanding) self.label = make.label(self.LABEL_TEXT, "C", (0, 0), "C") self.label.set_readonly(True) self.add_item(self.label) self.setAxisMaxMajor(self.Z_AXIS, self.Z_MAX_MAJOR) self.setAxisMaxMinor(self.Z_AXIS, 0) def set_curve_style(self, section, option): self.curveparam.read_config(CONF, section, option) self.curveparam.label = self.CURVE_LABEL def connect_plot(self, plot): if not isinstance(plot, ImagePlot): # Connecting only to image plot widgets (allow mixing image and # curve widgets for the same plot manager -- e.g. in pyplot) return plot.SIG_ITEMS_CHANGED.connect(self.items_changed) plot.SIG_LUT_CHANGED.connect(self.lut_changed) plot.SIG_MASK_CHANGED.connect(lambda item: self.update_plot()) plot.SIG_ACTIVE_ITEM_CHANGED.connect(self.active_item_changed) plot.SIG_MARKER_CHANGED.connect(self.marker_changed) plot.SIG_ANNOTATION_CHANGED.connect(self.shape_changed) plot.SIG_PLOT_LABELS_CHANGED.connect(self.plot_labels_changed) plot.SIG_AXIS_DIRECTION_CHANGED.connect(self.axis_dir_changed) self.plot_labels_changed(plot) for axis_id in plot.AXIS_IDS: self.axis_dir_changed(plot, axis_id) self.items_changed(plot) def register_shape(self, plot, shape, final, refresh=True): known_shapes = self._shapes.get(plot, []) if shape in known_shapes: return self._shapes[plot] = known_shapes+[shape] self.update_plot(shape, refresh=refresh and self.autorefresh_mode) def unregister_shape(self, shape): for plot in self._shapes: shapes = self._shapes[plot] if shape in shapes: shapes.pop(shapes.index(shape)) if len(shapes) == 0 or shape is self.get_last_obj(): for curve in self.get_cross_section_curves(): curve.clear_data() self.replot() break def create_cross_section_item(self): raise NotImplementedError def add_cross_section_item(self, source): curve = self.create_cross_section_item() curve.set_source_image(source) curve.set_readonly(True) self.add_item(curve, z=0) self.known_items[source] = curve def get_cross_section_curves(self): return list(self.known_items.values()) def items_changed(self, plot): # Del obsolete cross section items new_sources = plot.get_items(item_type=ICSImageItemType) for source in self.known_items.copy(): if source not in new_sources: curve = self.known_items.pop(source) curve.clear_data() # useful to emit SIG_CS_CURVE_CHANGED # (eventually notify other panels that the # cross section curve is now empty) self.del_item(curve) if not new_sources: self.replot() return self.curveparam.shade = self.SHADE/len(new_sources) for source in new_sources: if source not in self.known_items and source.isVisible(): if not self.single_source or not self.known_items: self.add_cross_section_item(source=source) def active_item_changed(self, plot): """Active item has just changed""" self.shape_changed(plot.get_active_item()) def plot_labels_changed(self, plot): """Plot labels have changed""" raise NotImplementedError def axis_dir_changed(self, plot, axis_id): """An axis direction has changed""" raise NotImplementedError def marker_changed(self, marker): self.update_plot(marker) def is_shape_known(self, shape): for shapes in list(self._shapes.values()): if shape in shapes: return True else: return False def shape_changed(self, shape): if self.autorefresh_mode: if self.is_shape_known(shape): self.update_plot(shape) def get_last_obj(self): if self.last_obj is not None: return self.last_obj() def update_plot(self, obj=None, refresh=True): """ Update cross section curve(s) associated to object *obj* *obj* may be a marker or a rectangular shape (see :py:class:`guiqwt.tools.CrossSectionTool` and :py:class:`guiqwt.tools.AverageCrossSectionTool`) If obj is None, update the cross sections of the last active object """ if obj is None: obj = self.get_last_obj() if obj is None: return else: self.last_obj = weakref.ref(obj) if obj.plot() is None: self.unregister_shape(obj) return if self.label.isVisible(): self.label.hide() for index, (_item, curve) in enumerate(iter(list(self.known_items.items()))): if not self.perimage_mode and index > 0: curve.hide() else: curve.show() curve.perimage_mode = self.perimage_mode curve.autoscale_mode = self.autoscale_mode curve.apply_lut = self.apply_lut if refresh: curve.update_item(obj) if self.autoscale_mode: self.do_autoscale(replot=True) if self.apply_lut: self.set_axis_title(self.Z_AXIS, LUT_AXIS_TITLE) self.set_axis_color(self.Z_AXIS, "red") else: self.plot_labels_changed(obj.plot()) def toggle_perimage_mode(self, state): self.perimage_mode = state self.update_plot() def toggle_autoscale(self, state): self.autoscale_mode = state self.update_plot() def toggle_autorefresh(self, state): self.autorefresh_mode = state if state: self.update_plot() def toggle_apply_lut(self, state): self.apply_lut = state self.update_plot() def lut_changed(self, plot): if self.apply_lut: self.update_plot()
class CrossSectionPlot(CurvePlot): """Cross section plot""" CURVE_LABEL = _("Cross section") LABEL_TEXT = _("Enable a marker") _height = None _width = None CS_AXIS = None Z_AXIS = None Z_MAX_MAJOR = 5 SHADE = .2 def __init__(self, parent=None): super(CrossSectionPlot, self).__init__(parent=parent, title="", section="cross_section") self.perimage_mode = True self.autoscale_mode = True self.autorefresh_mode = True self.apply_lut = False self.single_source = False self.last_obj = None self.known_items = {} self._shapes = {} self.curveparam = CurveParam(_("Curve"), icon="curve.png") self.set_curve_style("cross_section", "curve") if self._height is not None: self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Minimum) elif self._width is not None: self.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Expanding) self.label = make.label(self.LABEL_TEXT, "C", (0, 0), "C") self.label.set_readonly(True) self.add_item(self.label) self.setAxisMaxMajor(self.Z_AXIS, self.Z_MAX_MAJOR) self.setAxisMaxMinor(self.Z_AXIS, 0) def set_curve_style(self, section, option): self.curveparam.read_config(CONF, section, option) self.curveparam.label = self.CURVE_LABEL def connect_plot(self, plot): if not isinstance(plot, ImagePlot): # Connecting only to image plot widgets (allow mixing image and # curve widgets for the same plot manager -- e.g. in pyplot) return plot.SIG_ITEMS_CHANGED.connect(self.items_changed) plot.SIG_LUT_CHANGED.connect(self.lut_changed) plot.SIG_MASK_CHANGED.connect(lambda item: self.update_plot()) plot.SIG_ACTIVE_ITEM_CHANGED.connect(self.active_item_changed) plot.SIG_MARKER_CHANGED.connect(self.marker_changed) plot.SIG_ANNOTATION_CHANGED.connect(self.shape_changed) plot.SIG_PLOT_LABELS_CHANGED.connect(self.plot_labels_changed) plot.SIG_AXIS_DIRECTION_CHANGED.connect(self.axis_dir_changed) self.plot_labels_changed(plot) for axis_id in plot.AXIS_IDS: self.axis_dir_changed(plot, axis_id) self.items_changed(plot) def register_shape(self, plot, shape, final, refresh=True): known_shapes = self._shapes.get(plot, []) if shape in known_shapes: return self._shapes[plot] = known_shapes + [shape] self.update_plot(shape, refresh=refresh and self.autorefresh_mode) def unregister_shape(self, shape): for plot in self._shapes: shapes = self._shapes[plot] if shape in shapes: shapes.pop(shapes.index(shape)) if len(shapes) == 0 or shape is self.get_last_obj(): for curve in self.get_cross_section_curves(): curve.clear_data() self.replot() break def create_cross_section_item(self): raise NotImplementedError def add_cross_section_item(self, source): curve = self.create_cross_section_item() curve.set_source_image(source) curve.set_readonly(True) self.add_item(curve, z=0) self.known_items[source] = curve def get_cross_section_curves(self): return list(self.known_items.values()) def items_changed(self, plot): # Del obsolete cross section items new_sources = plot.get_items(item_type=ICSImageItemType) for source in self.known_items.copy(): if source not in new_sources: curve = self.known_items.pop(source) curve.clear_data() # useful to emit SIG_CS_CURVE_CHANGED # (eventually notify other panels that the # cross section curve is now empty) self.del_item(curve) if not new_sources: self.replot() return self.curveparam.shade = self.SHADE / len(new_sources) for source in new_sources: if source not in self.known_items and source.isVisible(): if not self.single_source or not self.known_items: self.add_cross_section_item(source=source) def active_item_changed(self, plot): """Active item has just changed""" self.shape_changed(plot.get_active_item()) def plot_labels_changed(self, plot): """Plot labels have changed""" raise NotImplementedError def axis_dir_changed(self, plot, axis_id): """An axis direction has changed""" raise NotImplementedError def marker_changed(self, marker): self.update_plot(marker) def is_shape_known(self, shape): for shapes in list(self._shapes.values()): if shape in shapes: return True else: return False def shape_changed(self, shape): if self.autorefresh_mode: if self.is_shape_known(shape): self.update_plot(shape) def get_last_obj(self): if self.last_obj is not None: return self.last_obj() def update_plot(self, obj=None, refresh=True): """ Update cross section curve(s) associated to object *obj* *obj* may be a marker or a rectangular shape (see :py:class:`guiqwt.tools.CrossSectionTool` and :py:class:`guiqwt.tools.AverageCrossSectionTool`) If obj is None, update the cross sections of the last active object """ if obj is None: obj = self.get_last_obj() if obj is None: return else: self.last_obj = weakref.ref(obj) if obj.plot() is None: self.unregister_shape(obj) return if self.label.isVisible(): self.label.hide() for index, (_item, curve) in enumerate(iter(list(self.known_items.items()))): if not self.perimage_mode and index > 0: curve.hide() else: curve.show() curve.perimage_mode = self.perimage_mode curve.autoscale_mode = self.autoscale_mode curve.apply_lut = self.apply_lut if refresh: curve.update_item(obj) if self.autoscale_mode: self.do_autoscale(replot=True) if self.apply_lut: self.set_axis_title(self.Z_AXIS, LUT_AXIS_TITLE) self.set_axis_color(self.Z_AXIS, "red") else: self.plot_labels_changed(obj.plot()) def toggle_perimage_mode(self, state): self.perimage_mode = state self.update_plot() def toggle_autoscale(self, state): self.autoscale_mode = state self.update_plot() def toggle_autorefresh(self, state): self.autorefresh_mode = state if state: self.update_plot() def toggle_apply_lut(self, state): self.apply_lut = state self.update_plot() def lut_changed(self, plot): if self.apply_lut: self.update_plot()
class LevelsHistogram(CurvePlot): """Image levels histogram widget""" #: Signal emitted by LevelsHistogram when LUT range was changed SIG_VOI_CHANGED = Signal() def __init__(self, parent=None): super(LevelsHistogram, self).__init__(parent=parent, title="", section="histogram") self.antialiased = False # a dict of dict : plot -> selected items -> HistogramItem self._tracked_items = {} self.curveparam = CurveParam(_("Curve"), icon="curve.png") self.curveparam.read_config(CONF, "histogram", "curve") self.histparam = HistogramParam(_("Histogram"), icon="histogram.png") self.histparam.logscale = False self.histparam.n_bins = 256 self.range = XRangeSelection(0, 1) self.range_mono_color = self.range.shapeparam.sel_line.color self.range_multi_color = CONF.get("histogram", "range/multi/color", "red") self.add_item(self.range, z=5) self.SIG_RANGE_CHANGED.connect(self.range_changed) self.set_active_item(self.range) self.setMinimumHeight(80) self.setAxisMaxMajor(self.Y_LEFT, 5) self.setAxisMaxMinor(self.Y_LEFT, 0) if parent is None: self.set_axis_title("bottom", "Levels") def connect_plot(self, plot): if not isinstance(plot, ImagePlot): # Connecting only to image plot widgets (allow mixing image and # curve widgets for the same plot manager -- e.g. in pyplot) return self.SIG_VOI_CHANGED.connect(plot.notify_colormap_changed) plot.SIG_ITEM_SELECTION_CHANGED.connect(self.selection_changed) plot.SIG_ITEM_REMOVED.connect(self.item_removed) plot.SIG_ACTIVE_ITEM_CHANGED.connect(self.active_item_changed) def tracked_items_gen(self): for plot, items in list(self._tracked_items.items()): for item in list(items.items()): yield item # tuple item,curve def __del_known_items(self, known_items, items): del_curves = [] for item in list(known_items.keys()): if item not in items: curve = known_items.pop(item) del_curves.append(curve) self.del_items(del_curves) def selection_changed(self, plot): items = plot.get_selected_items(item_type=IVoiImageItemType) known_items = self._tracked_items.setdefault(plot, {}) if items: self.__del_known_items(known_items, items) if len(items) == 1: # Removing any cached item for other plots for other_plot, _items in list(self._tracked_items.items()): if other_plot is not plot: if not other_plot.get_selected_items( item_type=IVoiImageItemType): other_known_items = self._tracked_items[other_plot] self.__del_known_items(other_known_items, []) else: # if all items are deselected we keep the last known # selection (for one plot only) for other_plot, _items in list(self._tracked_items.items()): if other_plot.get_selected_items(item_type=IVoiImageItemType): self.__del_known_items(known_items, []) break for item in items: if item not in known_items: curve = HistogramItem(self.curveparam, self.histparam) curve.set_hist_source(item) self.add_item(curve, z=0) known_items[item] = curve nb_selected = len(list(self.tracked_items_gen())) if not nb_selected: self.replot() return self.curveparam.shade = 1.0 / nb_selected for item, curve in self.tracked_items_gen(): self.curveparam.update_curve(curve) self.histparam.update_hist(curve) self.active_item_changed(plot) # Rescaling histogram plot axes for better visibility ymax = None for item in known_items: curve = known_items[item] _x, y = curve.get_data() ymax0 = y.mean() + 3 * y.std() if ymax is None or ymax0 > ymax: ymax = ymax0 ymin, _ymax = self.get_axis_limits("left") if ymax is not None: self.set_axis_limits("left", ymin, ymax) self.replot() def item_removed(self, item): for plot, items in list(self._tracked_items.items()): if item in items: curve = items.pop(item) self.del_items([curve]) self.replot() break def active_item_changed(self, plot): items = plot.get_selected_items(item_type=IVoiImageItemType) if not items: # XXX: workaround return active = plot.get_last_active_item(IVoiImageItemType) if active: active_range = active.get_lut_range() else: active_range = None multiple_ranges = False for item, curve in self.tracked_items_gen(): if active_range != item.get_lut_range(): multiple_ranges = True if active_range is not None: _m, _M = active_range self.set_range_style(multiple_ranges) self.range.set_range(_m, _M, dosignal=False) self.replot() def set_range_style(self, multiple_ranges): if multiple_ranges: self.range.shapeparam.sel_line.color = self.range_multi_color else: self.range.shapeparam.sel_line.color = self.range_mono_color self.range.shapeparam.update_range(self.range) def set_range(self, _min, _max): if _min < _max: self.set_range_style(False) self.range.set_range(_min, _max) self.replot() return True else: # Range was not changed return False def range_changed(self, _rangesel, _min, _max): for item, curve in self.tracked_items_gen(): item.set_lut_range([_min, _max]) self.SIG_VOI_CHANGED.emit() def set_full_range(self): """Set range bounds to image min/max levels""" _min = _max = None for item, curve in self.tracked_items_gen(): imin, imax = item.get_lut_range_full() if _min is None or _min > imin: _min = imin if _max is None or _max < imax: _max = imax if _min is not None: self.set_range(_min, _max) def apply_min_func(self, item, curve, min): _min, _max = item.get_lut_range() return min, _max def apply_max_func(self, item, curve, max): _min, _max = item.get_lut_range() return _min, max def reduce_range_func(self, item, curve, percent): return lut_range_threshold(item, curve.bins, percent) def apply_range_function(self, func, *args, **kwargs): item = None for item, curve in self.tracked_items_gen(): _min, _max = func(item, curve, *args, **kwargs) item.set_lut_range([_min, _max]) self.SIG_VOI_CHANGED.emit() if item is not None: self.active_item_changed(item.plot()) def eliminate_outliers(self, percent): """ Eliminate outliers: eliminate percent/2*N counts on each side of the histogram (where N is the total count number) """ self.apply_range_function(self.reduce_range_func, percent) def set_min(self, _min): self.apply_range_function(self.apply_min_func, _min) def set_max(self, _max): self.apply_range_function(self.apply_max_func, _max)
class LevelsHistogram(CurvePlot): """Image levels histogram widget""" def __init__(self, parent=None): super(LevelsHistogram, self).__init__(parent=parent, title="", section="histogram") self.antialiased = False # a dict of dict : plot -> selected items -> HistogramItem self._tracked_items = {} self.curveparam = CurveParam(_("Curve"), icon="curve.png") self.curveparam.read_config(CONF, "histogram", "curve") self.histparam = HistogramParam(_("Histogram"), icon="histogram.png") self.histparam.logscale = False self.histparam.n_bins = 256 self.range = XRangeSelection(0, 1) self.range_mono_color = self.range.shapeparam.sel_line.color self.range_multi_color = CONF.get("histogram", "range/multi/color", "red") self.add_item(self.range, z=5) self.connect(self, SIG_RANGE_CHANGED, self.range_changed) self.set_active_item(self.range) self.setMinimumHeight(80) self.setAxisMaxMajor(self.Y_LEFT, 5) self.setAxisMaxMinor(self.Y_LEFT, 0) if parent is None: self.set_axis_title('bottom', 'Levels') def connect_plot(self, plot): if not isinstance(plot, ImagePlot): # Connecting only to image plot widgets (allow mixing image and # curve widgets for the same plot manager -- e.g. in pyplot) return self.connect(self, SIG_VOI_CHANGED, plot.notify_colormap_changed) self.connect(plot, SIG_ITEM_SELECTION_CHANGED, self.selection_changed) self.connect(plot, SIG_ITEM_REMOVED, self.item_removed) self.connect(plot, SIG_ACTIVE_ITEM_CHANGED, self.active_item_changed) def tracked_items_gen(self): for plot, items in list(self._tracked_items.items()): for item in list(items.items()): yield item # tuple item,curve def __del_known_items(self, known_items, items): del_curves = [] for item in list(known_items.keys()): if item not in items: curve = known_items.pop(item) del_curves.append(curve) self.del_items(del_curves) def selection_changed(self, plot): items = plot.get_selected_items(item_type=IVoiImageItemType) known_items = self._tracked_items.setdefault(plot, {}) if items: self.__del_known_items(known_items, items) if len(items) == 1: # Removing any cached item for other plots for other_plot, _items in list(self._tracked_items.items()): if other_plot is not plot: if not other_plot.get_selected_items( item_type=IVoiImageItemType): other_known_items = self._tracked_items[other_plot] self.__del_known_items(other_known_items, []) else: # if all items are deselected we keep the last known # selection (for one plot only) for other_plot, _items in list(self._tracked_items.items()): if other_plot.get_selected_items(item_type=IVoiImageItemType): self.__del_known_items(known_items, []) break for item in items: if item not in known_items: curve = HistogramItem(self.curveparam, self.histparam) curve.set_hist_source(item) self.add_item(curve, z=0) known_items[item] = curve nb_selected = len(list(self.tracked_items_gen())) if not nb_selected: self.replot() return self.curveparam.shade = 1.0/nb_selected for item, curve in self.tracked_items_gen(): self.curveparam.update_curve(curve) self.histparam.update_hist(curve) self.active_item_changed(plot) # Rescaling histogram plot axes for better visibility ymax = None for item in known_items: curve = known_items[item] _x, y = curve.get_data() ymax0 = y.mean()+3*y.std() if ymax is None or ymax0 > ymax: ymax = ymax0 ymin, _ymax = self.get_axis_limits("left") if ymax is not None: self.set_axis_limits("left", ymin, ymax) self.replot() def item_removed(self, item): for plot, items in list(self._tracked_items.items()): if item in items: items.pop(item) break def active_item_changed(self, plot): items = plot.get_selected_items(item_type=IVoiImageItemType) if not items: #XXX: workaround return active = plot.get_last_active_item(IVoiImageItemType) if active: active_range = active.get_lut_range() else: active_range = None multiple_ranges = False for item, curve in self.tracked_items_gen(): if active_range != item.get_lut_range(): multiple_ranges = True if active_range is not None: _m, _M = active_range self.set_range_style(multiple_ranges) self.range.set_range(_m, _M, dosignal=False) self.replot() def set_range_style(self, multiple_ranges): if multiple_ranges: self.range.shapeparam.sel_line.color = self.range_multi_color else: self.range.shapeparam.sel_line.color = self.range_mono_color self.range.shapeparam.update_range(self.range) def set_range(self, _min, _max): if _min < _max: self.set_range_style(False) self.range.set_range(_min, _max) self.replot() return True else: # Range was not changed return False def range_changed(self, _rangesel, _min, _max): for item, curve in self.tracked_items_gen(): item.set_lut_range([_min, _max]) self.emit(SIG_VOI_CHANGED) def set_full_range(self): """Set range bounds to image min/max levels""" _min = _max = None for item, curve in self.tracked_items_gen(): imin, imax = item.get_lut_range_full() if _min is None or _min>imin: _min = imin if _max is None or _max<imax: _max = imax if _min is not None: self.set_range(_min, _max) def apply_min_func(self, item, curve, min): _min, _max = item.get_lut_range() return min, _max def apply_max_func(self, item, curve, max): _min, _max = item.get_lut_range() return _min, max def reduce_range_func(self, item, curve, percent): return lut_range_threshold(item, curve.bins, percent) def apply_range_function(self, func, *args, **kwargs): item = None for item, curve in self.tracked_items_gen(): _min, _max = func(item, curve, *args, **kwargs) item.set_lut_range([_min, _max]) self.emit(SIG_VOI_CHANGED) if item is not None: self.active_item_changed(item.plot()) def eliminate_outliers(self, percent): """ Eliminate outliers: eliminate percent/2*N counts on each side of the histogram (where N is the total count number) """ self.apply_range_function(self.reduce_range_func, percent) def set_min(self, _min): self.apply_range_function(self.apply_min_func, _min) def set_max(self, _max): self.apply_range_function(self.apply_max_func, _max)