Source code for fastplotlib.ui._colorbar

import weakref

import numpy as np
import wgpu
from cmap import Colormap
from imgui_bundle import imgui

from ..graphics import Graphic, ImageGraphic, ImageVolumeGraphic
from ..graphics._positions_base import PositionsGraphic
from ..utils.functions import COLORMAP_NAMES, quick_min_max
from ._base import ImguiContainer


def colormaps_equal(a: str | Colormap, b: str | Colormap) -> bool:
    """
    Whether two colormaps, given as names or as ``Colormap`` instances, are the same.

    ``Colormap.__eq__`` compares the color stops and raises if the two colormaps do not have the
    same number of them, which means they are not the same colormap.
    """
    try:
        return a == b
    except ValueError:
        return False


[docs] class ImguiColorbar(ImguiContainer): LUT_HEIGHT = 256 TEX_WIDTH = 2 HANDLE_HEIGHT = 8 HANDLE_OVERHANG = 3 # how far a handle extends past the bar on each side BAR_BORDER = 1.0 # width of the outline drawn around the bar image HIST_WIDTH = 50 # width in pixels of the optional histogram drawn left of the bar HIST_GAP = 4 # gap in pixels between the histogram and the bar FILL_OVERHANG = ( 4 # how far the vmin/vmax fill and lines extend past the histogram line-plot ) # preview textures for the colormap picker, keyed by imgui renderer. They are the same for # every colorbar and there is one per non-qualitative colormap, so they are made once per # figure and shared. Weakly keyed so a closed figure is not kept alive by its textures. _PICKER_TEXTURE_IDS = weakref.WeakKeyDictionary() def __init__( self, graphics: ImageGraphic | ImageVolumeGraphic | PositionsGraphic | list[Graphic], histogram: tuple[np.ndarray, np.ndarray] | None = None, data_range: tuple[float, float] | None = None, title: str | None = None, height: int | None = None, bar_width: int = 16, region_drag: bool = True, ): """ An imgui colorbar with draggable vmin/vmax handles, an optional histogram, a gamma slider, and a right-click colormap picker. Parameters ---------- graphics: ImageGraphic | ImageVolumeGraphic | LineGraphic | ScatterGraphic | list the graphic(s) whose colormap and value range this colorbar controls. The bar drives ``vmin`` and ``vmax`` of an image, and ``cmap_range`` of a line or scatter, which must already have a ``cmap``. histogram: tuple[np.ndarray, np.ndarray], optional a precomputed ``(counts, edges)`` histogram drawn to the left of the bar. It is not recomputed when the graphic's data changes, set the ``histogram`` property to update it. data_range: (min, max), optional the value range spanned by the bar. Defaults to the histogram edges if a histogram is provided, otherwise to the data range of the first graphic. title: str, optional title drawn above the bar, no title is drawn if ``None`` height: int, optional height of the colorbar in pixels, fills the available vertical space if ``None`` bar_width: int width of the colored bar in pixels region_drag: bool if ``True``, dragging between the handles shifts the vmin/vmax window without changing its width """ super().__init__() self._graphics = self._validate_graphics(graphics) graphic = self._graphics[0] self._vmin, self._vmax = self._get_clim(graphic) # rgb(a) images have no cmap, display the bar with "gray" so vmin, vmax are still adjustable self._cmap_name = graphic.cmap if graphic.cmap is not None else "gray" self._gamma = 1.0 self._title = title self._height = int(height) if height is not None else None self._bar_width = int(bar_width) self._region_drag = bool(region_drag) # offset in data units between the grabbed value and the value under the cursor, captured when a drag # starts so the handle tracks the cursor without jumping self._grab_offset = 0.0 # prevents feedback loops when syncing vmin, vmax, cmap between this colorbar and the graphics self._block_reentrance = False # whether a handle is hovered this frame, drives the resize cursor self._hovering_handle = False self._resize_cursor_set = False # GPU resources, created in _fpl_add_hook() once the figure and its device are known self._device = None self._bar_texture = None self._bar_tex_id = None # setting the histogram also sets the value axis to the histogram edges self._histogram = None self.histogram = histogram # data_range defaults to the histogram edges, otherwise the data range of the first graphic if data_range is None: if self._histogram is not None: counts, edges = self._histogram data_range = (float(edges[0]), float(edges[-1])) else: data_range = self._get_data_range(graphic) self._data_min, self._data_max = self._validate_range(data_range) def _fpl_add_hook(self, figure): super()._fpl_add_hook(figure) self._device = figure.renderer.device self._bar_texture = self._device.create_texture( size=(self.TEX_WIDTH, self.LUT_HEIGHT, 1), usage=wgpu.TextureUsage.COPY_DST | wgpu.TextureUsage.TEXTURE_BINDING, dimension=wgpu.TextureDimension.d2, format=wgpu.TextureFormat.rgba8unorm, mip_level_count=1, sample_count=1, ) self._bar_tex_id = figure.imgui_renderer.backend.register_texture( self._bar_texture.create_view() ) self._update_bar_texture() # sync the colorbar when a graphic's value range, cmap, or gamma is changed elsewhere for graphic in self._graphics: self._connect_graphic(graphic) @staticmethod def _validate_graphics(graphics) -> list[Graphic]: """the graphics this colorbar can manage, as a list""" if isinstance(graphics, Graphic): graphics = [graphics] graphics = list(graphics) if len(graphics) == 0: raise ValueError("must provide at least one graphic") for graphic in graphics: if not isinstance( graphic, (ImageGraphic, ImageVolumeGraphic, PositionsGraphic) ): raise TypeError( f"a colorbar can manage images, lines and scatters, you have passed a: " f"{type(graphic).__name__}" ) if isinstance(graphic, PositionsGraphic) and graphic.cmap is None: raise ValueError( "a line or scatter must have a `cmap` set to be managed by a colorbar, the bar drives its " "`cmap_range`" ) return graphics @staticmethod def _get_clim(graphic) -> tuple[float, float]: """the (min, max) that ``graphic`` maps onto the colormap""" if isinstance(graphic, PositionsGraphic): return graphic.cmap_range return float(graphic.vmin), float(graphic.vmax) @staticmethod def _set_clim(graphic, vmin: float, vmax: float): """set the (min, max) that ``graphic`` maps onto the colormap""" if isinstance(graphic, PositionsGraphic): graphic.cmap_range = (vmin, vmax) return graphic.vmin = vmin graphic.vmax = vmax @staticmethod def _get_data_range(graphic) -> tuple[float, float]: """the value range the bar spans by default""" if isinstance(graphic, PositionsGraphic): transform = graphic.cmap_transform return float(transform.min()), float(transform.max()) return quick_min_max(graphic.data.value) @property def _has_gamma(self) -> bool: """whether any managed graphic has a gamma, lines and scatters do not""" return any( isinstance(g, (ImageGraphic, ImageVolumeGraphic)) for g in self._graphics ) @property def graphics(self) -> tuple: """get or set the graphics managed by this colorbar""" return tuple(self._graphics) @graphics.setter def graphics(self, new_graphics): self._disconnect_graphics() self._graphics = self._validate_graphics(new_graphics) # adopt the value range and cmap of the new first graphic graphic = self._graphics[0] self._vmin, self._vmax = self._get_clim(graphic) self._cmap_name = graphic.cmap if graphic.cmap is not None else "gray" self._update_bar_texture() for g in self._graphics: self._connect_graphic(g) @property def title(self) -> str | None: """get or set the title drawn above the bar, ``None`` for no title""" return self._title @title.setter def title(self, value: str | None): self._title = value @property def height(self) -> int | None: """get or set the height of the colorbar in pixels, ``None`` fills the available space""" return self._height @height.setter def height(self, value: int | None): self._height = int(value) if value is not None else None @property def cmap(self) -> str: """get or set the colormap""" return self._cmap_name @cmap.setter def cmap(self, name: str): if self._block_reentrance or name is None: return if colormaps_equal(name, self._cmap_name): return self._block_reentrance = True try: self._cmap_name = name self._update_bar_texture() for graphic in self._graphics: if graphic.cmap is None: # rgb(a) images have no cmap continue graphic.cmap = name finally: self._block_reentrance = False @property def vmin(self) -> float: """get or set the lower contrast limit""" return max(self._vmin, self._axis_range()[0]) @vmin.setter def vmin(self, value: float): value = float(value) if self._block_reentrance or value == self._vmin: return self._block_reentrance = True try: self._vmin = value self._update_bar_texture() self._apply_clim() finally: self._block_reentrance = False @property def vmax(self) -> float: """get or set the upper contrast limit""" return min(self._vmax, self._axis_range()[1]) @vmax.setter def vmax(self, value: float): value = float(value) if self._block_reentrance or value == self._vmax: return self._block_reentrance = True try: self._vmax = value self._update_bar_texture() self._apply_clim() finally: self._block_reentrance = False @property def histogram(self) -> tuple[np.ndarray, np.ndarray] | None: """the histogram as a precomputed (counts, edges) tuple, or ``None`` for no histogram""" return self._histogram @histogram.setter def histogram(self, value): if value is None: self._histogram = None return counts, edges = value counts = np.asarray(counts, dtype=np.float32) edges = np.asarray(edges, dtype=np.float64) if edges.shape[0] != counts.shape[0] + 1: raise ValueError( "histogram edges must have one more element than counts, you have passed " f"counts: {counts.shape[0]} and edges: {edges.shape[0]}" ) self._histogram = (counts, edges) # the histogram defines the value axis self._data_min = float(edges[0]) self._data_max = float(edges[-1]) self._update_bar_texture() @property def data_range(self) -> tuple[float, float]: """the value range spanned by the bar""" return (self._data_min, self._data_max) @data_range.setter def data_range(self, value): self._data_min, self._data_max = self._validate_range(value) self._update_bar_texture() @property def gamma(self) -> float: """get or set the gamma, applied to the images and the bar""" return self._gamma @gamma.setter def gamma(self, value: float): value = float(value) if self._block_reentrance or value == self._gamma: return self._block_reentrance = True try: self._gamma = value self._update_bar_texture() for graphic in self._graphics: if isinstance(graphic, PositionsGraphic): # lines and scatters have no gamma continue graphic.gamma = value finally: self._block_reentrance = False @property def bar_width(self) -> int: """get or set the width of the colored bar in pixels""" return self._bar_width @bar_width.setter def bar_width(self, value: int): self._bar_width = int(value) @staticmethod def _validate_range(data_range): data_min, data_max = float(data_range[0]), float(data_range[1]) if data_max <= data_min: raise ValueError( f"data_range max ({data_max}) must be greater than min ({data_min})" ) return data_min, data_max def _apply_clim(self): """write the current vmin, vmax to the managed graphics""" for graphic in self._graphics: self._set_clim(graphic, self._vmin, self._vmax) def _graphic_event_handler(self, ev): """when a graphic's value range, cmap, or gamma changes, update this colorbar to match""" if ev.type == "cmap_range": self.vmin, self.vmax = ev.info["value"] return setattr(self, ev.type, ev.info["value"]) def _connect_graphic(self, graphic): """subscribe to the events of the properties this colorbar drives""" if isinstance(graphic, PositionsGraphic): events = ["cmap_range", "cmap"] else: events = ["vmin", "vmax", "gamma"] # rgb(a) images have no cmap feature to listen to if graphic.cmap is not None: events.append("cmap") graphic.add_event_handler(self._graphic_event_handler, *events) def _disconnect_graphics(self, *args): """disconnect the event handlers of the managed graphics""" for graphic in self._graphics: for ev, handlers in graphic.event_handlers: if self._graphic_event_handler in handlers: graphic.remove_event_handler(self._graphic_event_handler, ev) def _get_picker_texture_ids(self) -> dict: """the colormap preview textures of the picker, made on first use and shared per figure""" renderer = self._figure.imgui_renderer texture_ids = self._PICKER_TEXTURE_IDS.get(renderer) if texture_ids is not None: return texture_ids # a preview texture for each non-qualitative colormap texture_ids = dict() for category, names in COLORMAP_NAMES.items(): if category == "qualitative": continue for name in names: texture_ids[name] = self._make_picker_texture(name) self._PICKER_TEXTURE_IDS[renderer] = texture_ids return texture_ids def _make_picker_texture(self, name): lut = (Colormap(name)(np.linspace(0, 1, 256)) * 255).astype(np.uint8) data = np.ascontiguousarray(np.tile(lut[None, :, :], (2, 1, 1))) h, w = data.shape[:2] texture = self._device.create_texture( size=(w, h, 1), usage=wgpu.TextureUsage.COPY_DST | wgpu.TextureUsage.TEXTURE_BINDING, dimension=wgpu.TextureDimension.d2, format=wgpu.TextureFormat.rgba8unorm, mip_level_count=1, sample_count=1, ) self._device.queue.write_texture( {"texture": texture, "mip_level": 0, "origin": (0, 0, 0)}, data, {"offset": 0, "bytes_per_row": w * 4}, (w, h, 1), ) return self._renderer.backend.register_texture(texture.create_view()) def _update_bar_texture(self): if self._bar_texture is None: # not added to a figure yet, no device return # the bar spans the flanked axis so it aligns with the histogram and the handles axis_min, axis_max = self._axis_range() span = axis_max - axis_min lo = (self.vmin - axis_min) / span hi = (self.vmax - axis_min) / span t = np.linspace(1.0, 0.0, self.LUT_HEIGHT) norm = np.clip((t - lo) / (hi - lo), 0.0, 1.0) norm = norm**self._gamma colors = (Colormap(self._cmap_name)(norm) * 255).astype(np.uint8) data = np.ascontiguousarray(np.tile(colors[:, None, :], (1, self.TEX_WIDTH, 1))) self._device.queue.write_texture( {"texture": self._bar_texture, "mip_level": 0, "origin": (0, 0, 0)}, data, {"offset": 0, "bytes_per_row": self.TEX_WIDTH * 4}, (self.TEX_WIDTH, self.LUT_HEIGHT, 1), ) @property def _renderer(self): return self._figure.imgui_renderer def _axis_range(self) -> tuple[float, float]: """the value axis: the data range flanked on each side so handles can move past the data extremes""" flank = 0.1 * (self._data_max - self._data_min) return self._data_min - flank, self._data_max + flank
[docs] def update(self): draw_list = imgui.get_window_draw_list() avail = imgui.get_content_region_avail() line_h = imgui.get_text_line_height_with_spacing() p0 = imgui.get_cursor_screen_pos() total_h = avail.y if self._height is None else float(self._height) if self._title is not None: # centered above the bar, the region below it is what the bar is drawn in text_w = imgui.calc_text_size(self._title).x imgui.set_cursor_pos_x( imgui.get_cursor_pos_x() + max(0.0, (avail.x - text_w) * 0.5) ) imgui.text(self._title) p0 = imgui.get_cursor_screen_pos() total_h -= line_h bar_w = self._bar_width # the value axis spans the height minus a line of padding at the top and bottom bar_y = p0.y + line_h bar_h = max(50.0, total_h - 2 * line_h) # accumulated across the region lines and bar handles to drive the resize cursor self._hovering_handle = False # anchor the bar to the right edge of the window; the histogram and value text sit to its left, # the handle overhang stays within the window padding bar_x = p0.x + avail.x - self.HANDLE_OVERHANG - bar_w has_hist = self._histogram is not None if has_hist: # the histogram has a fixed width (HIST_WIDTH), drawn to the left of the bar hist_x_right = bar_x - self.HIST_GAP hist_x_left = hist_x_right - self.HIST_WIDTH # histogram line profile, inset so the vmin/vmax fill and lines extend beyond it self._draw_histogram( draw_list, hist_x_left + self.FILL_OVERHANG, hist_x_right - self.FILL_OVERHANG, bar_y, bar_h, ) # draggable vmin, vmax lines, shaded region, and value text drawn over the histogram self._draw_region(hist_x_left, hist_x_right, bar_y, bar_h) # the colorbar bar imgui.set_cursor_screen_pos((bar_x, bar_y)) imgui.push_style_color(imgui.Col_.border, (1.0, 1.0, 1.0, 1.0)) imgui.push_style_var(imgui.StyleVar_.image_border_size, self.BAR_BORDER) imgui.image(self._bar_tex_id, image_size=(bar_w - 2 * self.BAR_BORDER, bar_h)) imgui.pop_style_var() imgui.pop_style_color() # right-click for the gamma slider and colormap picker if imgui.begin_popup_context_window("##colorbar_popup"): self._draw_popup() imgui.end_popup() # without a histogram the vmin, vmax handles live on the bar itself if not has_hist: self._draw_bar_handles(bar_x, bar_y, bar_w, bar_h) # show a vertical-resize cursor while hovering any handle if self._hovering_handle and not self._resize_cursor_set: self._figure.canvas.set_cursor("ns_resize") self._resize_cursor_set = True elif not self._hovering_handle and self._resize_cursor_set: self._figure.canvas.set_cursor("default") self._resize_cursor_set = False # reserve the region so anything drawn after the colorbar is placed below it imgui.set_cursor_screen_pos(p0) imgui.dummy((avail.x, total_h))
def _value_to_y(self, v, y0, bar_h): axis_min, axis_max = self._axis_range() return y0 + (1.0 - (v - axis_min) / (axis_max - axis_min)) * bar_h def _y_to_value(self, y, y0, bar_h): axis_min, axis_max = self._axis_range() return axis_min + (1.0 - (y - y0) / bar_h) * (axis_max - axis_min) def _draw_histogram(self, draw_list, x_left, x_right, bar_y, bar_h): counts, edges = self._histogram cmin = counts.min() cmax = counts.max() span = cmax - cmin if span <= 0: return color = imgui.color_convert_float4_to_u32((0.7, 0.7, 0.7, 1.0)) hist_w = x_right - x_left if hist_w <= 0: return # min count maps to the right edge next to the bar, max count to the left edge, filling the width norm = (counts - cmin) / span centers = 0.5 * (edges[:-1] + edges[1:]) # frequency increases to the left, away from the bar, value maps to y, drawn as a line profile points = [ imgui.ImVec2(x_right - frac * hist_w, self._value_to_y(c, bar_y, bar_h)) for frac, c in zip(norm, centers) ] draw_list.add_polyline(points, color, 1.5, 0) def _draw_region(self, x_left, x_right, bar_y, bar_h): draw_list = imgui.get_window_draw_list() white = imgui.color_convert_float4_to_u32((1.0, 1.0, 1.0, 1.0)) # yellow highlight when a line is hovered/dragged, like the HistogramLUTTool yellow = imgui.color_convert_float4_to_u32((1.0, 1.0, 0.0, 1.0)) # dark blue fill, the same color as the HistogramLUTTool LinearRegionSelector fill_color = imgui.color_convert_float4_to_u32((0.0, 0.0, 0.35, 0.4)) axis_min, axis_max = self._axis_range() span = axis_max - axis_min width = x_right - x_left grab = self.HANDLE_HEIGHT min_sep = (grab / bar_h) * span def cursor_value(): # the data value under the cursor. Lines track this absolute position (plus the grab offset) # rather than accumulating per-frame deltas, so a fast drag past an edge pins the line to the extreme return self._y_to_value(imgui.get_io().mouse_pos.y, bar_y, bar_h) # shaded fill between the vmin and vmax lines y_vmax = self._value_to_y(self.vmax, bar_y, bar_h) y_vmin = self._value_to_y(self.vmin, bar_y, bar_h) draw_list.add_rect_filled((x_left, y_vmax), (x_right, y_vmin), fill_color) # drag the region between the lines to move both together if self._region_drag: top = y_vmax + grab / 2 bottom = y_vmin - grab / 2 if bottom > top: imgui.set_cursor_screen_pos((x_left, top)) imgui.invisible_button("##region", (width, bottom - top)) if imgui.is_item_activated(): self._grab_offset = 0.5 * (self.vmin + self.vmax) - cursor_value() if imgui.is_item_active(): half = 0.5 * (self.vmax - self.vmin) center = cursor_value() + self._grab_offset center = max(axis_min + half, min(axis_max - half, center)) self.vmin = center - half self.vmax = center + half # each line has a hit-window for hovering/dragging; the line turns yellow when hovered or dragged for label, attr, lo_fn, hi_fn in ( ("##vmax_line", "vmax", lambda: self.vmin + min_sep, lambda: axis_max), ("##vmin_line", "vmin", lambda: axis_min, lambda: self.vmax - min_sep), ): cur = getattr(self, attr) y = self._value_to_y(cur, bar_y, bar_h) imgui.set_cursor_screen_pos((x_left, y - grab / 2)) imgui.invisible_button(label, (width, grab)) hovered = imgui.is_item_hovered() or imgui.is_item_active() self._hovering_handle = self._hovering_handle or hovered if imgui.is_item_activated(): self._grab_offset = cur - cursor_value() if imgui.is_item_active(): setattr( self, attr, max(lo_fn(), min(hi_fn(), cursor_value() + self._grab_offset)), ) y = self._value_to_y(getattr(self, attr), bar_y, bar_h) draw_list.add_line( (x_left, y), (x_right, y), yellow if hovered else white, 2.0 ) # current vmax above its line, vmin below its line y_vmax = self._value_to_y(self.vmax, bar_y, bar_h) y_vmin = self._value_to_y(self.vmin, bar_y, bar_h) self._text_right( draw_list, f"{self.vmax:.4g}", x_right, y_vmax - imgui.get_text_line_height(), ) self._text_right(draw_list, f"{self.vmin:.4g}", x_right, y_vmin) def _text_right(self, draw_list, text: str, x_right: float, y: float): """draw text right-aligned so it ends at x_right""" tw = imgui.calc_text_size(text).x draw_list.add_text( (x_right - tw, y), imgui.get_color_u32(imgui.Col_.text), text ) def _draw_bar_handles(self, bar_x, bar_y, bar_w, bar_h): draw_list = imgui.get_window_draw_list() white = imgui.color_convert_float4_to_u32((1.0, 1.0, 1.0, 1.0)) # yellow highlight when a handle is hovered/dragged, like the region lines yellow = imgui.color_convert_float4_to_u32((1.0, 1.0, 0.0, 1.0)) outline = imgui.color_convert_float4_to_u32((0.0, 0.0, 0.0, 1.0)) text_color = imgui.get_color_u32(imgui.Col_.text) axis_min, axis_max = self._axis_range() span = axis_max - axis_min h = self.HANDLE_HEIGHT # the handles extend past the bar on each side x_left = bar_x - self.HANDLE_OVERHANG x_right = bar_x + bar_w + self.HANDLE_OVERHANG min_sep = (h / bar_h) * span def cursor_value(): return self._y_to_value(imgui.get_io().mouse_pos.y, bar_y, bar_h) # thin reference lines at the data min and max, so the flank beyond the data range is visible ref = imgui.color_convert_float4_to_u32((1.0, 1.0, 1.0, 1.0)) for v in (self._data_min, self._data_max): y = self._value_to_y(v, bar_y, bar_h) draw_list.add_line((x_left, y), (x_right, y), ref, 1.0) # drag the region between the handles to move vmin and vmax together if self._region_drag: y_vmax = self._value_to_y(self.vmax, bar_y, bar_h) y_vmin = self._value_to_y(self.vmin, bar_y, bar_h) top = y_vmax + h / 2 bottom = y_vmin - h / 2 if bottom > top: imgui.set_cursor_screen_pos((x_left, top)) imgui.invisible_button("##bar_region", (x_right - x_left, bottom - top)) if imgui.is_item_activated(): self._grab_offset = 0.5 * (self.vmin + self.vmax) - cursor_value() if imgui.is_item_active(): half = 0.5 * (self.vmax - self.vmin) center = cursor_value() + self._grab_offset center = max(axis_min + half, min(axis_max - half, center)) self.vmin = center - half self.vmax = center + half for label, attr, lo_fn, hi_fn in ( ("##bar_vmax", "vmax", lambda: self.vmin + min_sep, lambda: axis_max), ("##bar_vmin", "vmin", lambda: axis_min, lambda: self.vmax - min_sep), ): cur = getattr(self, attr) y = self._value_to_y(cur, bar_y, bar_h) imgui.set_cursor_screen_pos((x_left, y - h / 2)) imgui.invisible_button(label, (x_right - x_left, h)) hovered = imgui.is_item_hovered() or imgui.is_item_active() self._hovering_handle = self._hovering_handle or hovered if imgui.is_item_activated(): self._grab_offset = cur - cursor_value() if imgui.is_item_active(): setattr( self, attr, max(lo_fn(), min(hi_fn(), cursor_value() + self._grab_offset)), ) y = self._value_to_y(getattr(self, attr), bar_y, bar_h) draw_list.add_rect_filled( (x_left, y - h / 2), (x_right, y + h / 2), yellow if hovered else white ) draw_list.add_rect( (x_left, y - h / 2), (x_right, y + h / 2), outline, thickness=1.0 ) # current value to the left of the bar, vmax above its handle and vmin below text = f"{getattr(self, attr):.4g}" ty = y - imgui.get_text_line_height() if attr == "vmax" else y tw = imgui.calc_text_size(text).x draw_list.add_text((x_left - 3 - tw, ty), text_color, text) def _draw_popup(self): if self._has_gamma: imgui.set_next_item_width(150) changed, gamma = imgui.slider_float("gamma", self._gamma, 0.1, 5.0) if changed: self.gamma = gamma # reset gamma to 1.0 if imgui.menu_item("Reset gamma", "", False)[0]: self.gamma = 1.0 # reset the value range using the data of each graphic if imgui.menu_item("Reset range", "", False)[0]: for graphic in self._graphics: if isinstance(graphic, PositionsGraphic): self._set_clim(graphic, *self._get_data_range(graphic)) else: graphic.reset_vmin_vmax() texture_height = imgui.get_font_size() - 2 picker_texture_ids = self._get_picker_texture_ids() # colormaps grouped by category, qualitative colormaps are not useful for a quantitative colorbar for category, names in COLORMAP_NAMES.items(): if category == "qualitative": continue imgui.separator() imgui.text(category.capitalize()) for name in names: imgui.push_style_color(imgui.Col_.border, (1.0, 1.0, 1.0, 1.0)) imgui.push_style_var(imgui.StyleVar_.image_border_size, 1.0) imgui.image(picker_texture_ids[name], image_size=(75, texture_height)) imgui.pop_style_var() imgui.pop_style_color() imgui.same_line() clicked, selected = imgui.selectable( name, p_selected=colormaps_equal(name, self._cmap_name) ) if clicked and selected: self.cmap = name