.. DO NOT EDIT. .. THIS FILE WAS AUTOMATICALLY GENERATED BY SPHINX-GALLERY. .. TO MAKE CHANGES, EDIT THE SOURCE PYTHON FILE: .. "_gallery/colorbar.py" .. LINE NUMBERS ARE GIVEN BELOW. .. only:: html .. note:: :class: sphx-glr-download-link-note :ref:`Go to the end ` to download the full example code. .. rst-class:: sphx-glr-example-title .. _sphx_glr__gallery_colorbar.py: Colorbar with contrast limit handles ==================================== An ImGUI colorbar widget rendered with wgpu, with draggable ``vmin``/``vmax`` handles, a gamma slider, and a right-click colormap picker. .. GENERATED FROM PYTHON SOURCE LINES 8-343 .. image-sg:: /_gallery/images/sphx_glr_colorbar_001.webp :alt: colorbar :srcset: /_gallery/images/sphx_glr_colorbar_001.webp :class: sphx-glr-single-img .. rst-class:: sphx-glr-script-out .. code-block:: none Unable to find extension: VK_EXT_physical_device_drm No windowing system present. Using surfaceless platform No config found! No config found! | .. code-block:: Python import numpy as np import wgpu from wgpu.utils.imgui import ImguiRenderer from cmap import Colormap from imgui_bundle import imgui from rendercanvas.auto import RenderCanvas, loop class ImguiColorbar: 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 def __init__( self, device, imgui_renderer, cmap_name: str, vmin: float, vmax: float, cmaps: list[str], data_range: tuple[float, float] | None = None, width: int = 15, height: int = 200, ): self._device = device self._renderer = imgui_renderer self._width = int(width) self._height = int(height) self._vmin = float(vmin) self._vmax = float(vmax) if data_range is None: data_range = (vmin, vmax) self._data_min, self._data_max = self._validate_range(data_range) self.gamma = 1.0 # When True, dragging the bar between the handles moves both handles # together (shifts the vmin/vmax window without changing its width). self._region_drag = True # 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 self._picker_tex_ids = { name: self._make_picker_texture(name) for name in cmaps } # The active colormap must be one of the colormaps in the picker. if cmap_name not in self._picker_tex_ids: raise ValueError( f"cmap_name {cmap_name!r} is not one of the available " f"colormaps: {list(self._picker_tex_ids)}" ) self._cmap_name = cmap_name self._bar_texture = 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 = imgui_renderer.backend.register_texture( self._bar_texture.create_view() ) self._update_bar_texture() @property def cmap_name(self): return self._cmap_name @cmap_name.setter def cmap_name(self, name): if name not in self._picker_tex_ids: raise ValueError( f"cmap_name {name!r} is not one of the available colormaps: " f"{list(self._picker_tex_ids)}" ) if name != self._cmap_name: self._cmap_name = name self._update_bar_texture() @property def vmin(self): return self._vmin @vmin.setter def vmin(self, value): value = float(value) if value != self._vmin: self._vmin = value self._update_bar_texture() @property def vmax(self): return self._vmax @vmax.setter def vmax(self, value): value = float(value) if value != self._vmax: self._vmax = value self._update_bar_texture() @property def width(self): return self._width @width.setter def width(self, value): self._width = int(value) @property def height(self): return self._height @height.setter def height(self, value): self._height = int(value) @property def region_drag(self): return self._region_drag @region_drag.setter def region_drag(self, value): self._region_drag = bool(value) @property def data_range(self): 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() @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 " f"min ({data_min})" ) return data_min, data_max 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): span = self._data_max - self._data_min lo = (self.vmin - self._data_min) / span hi = (self.vmax - self._data_min) / span t = np.linspace(1.0, 0.0, self.LUT_HEIGHT) norm = np.clip((t - lo) / (hi - lo), 0.0, 1.0) 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), ) def draw(self): imgui.text(f"{self.vmax:.4g}") 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=(self._width, self._height)) imgui.pop_style_var() imgui.pop_style_color() # The border insets the colored bar from the item rect, so offset by the # border to get the bar's content origin. This keeps the handles centered # on the bar rather than on the (larger) bordered image. item_min = imgui.get_item_rect_min() bar_x = item_min.x + self.BAR_BORDER bar_y = item_min.y + self.BAR_BORDER if imgui.begin_popup_context_item("##colorbar_popup"): self._draw_popup() imgui.end_popup() imgui.text(f"{self.vmin:.4g}") self._draw_handles(bar_x, bar_y, self._width, self._height) def _value_to_y(self, v, y0, bar_h): span = self._data_max - self._data_min return y0 + (1.0 - (v - self._data_min) / span) * bar_h def _y_to_value(self, y, y0, bar_h): span = self._data_max - self._data_min return self._data_min + (1.0 - (y - y0) / bar_h) * span def _draw_handles(self, bar_x, bar_y, bar_w, bar_h): draw_list = imgui.get_window_draw_list() fill = imgui.color_convert_float4_to_u32((1.0, 1.0, 1.0, 1.0)) outline = imgui.color_convert_float4_to_u32((0.0, 0.0, 0.0, 1.0)) h = self.HANDLE_HEIGHT # Extend the handle HANDLE_OVERHANG pixels past the bar's outer (bordered) # edge on each side. x0 = bar_x - self.BAR_BORDER - self.HANDLE_OVERHANG x1 = bar_x + bar_w + self.BAR_BORDER + self.HANDLE_OVERHANG span = self._data_max - self._data_min min_sep = (h / bar_h) * span def cursor_value(): # The data value under the cursor. Handles track this absolute # position (plus the grab offset) rather than accumulating per-frame # deltas, so a fast drag past the top/bottom edge pins the handle to # the extreme instead of stalling partway. return self._y_to_value(imgui.get_io().mouse_pos.y, bar_y, bar_h) # Drag the region between the handles to move both handles together. if self.region_drag: top = self._value_to_y(self.vmax, bar_y, bar_h) + h / 2 bottom = self._value_to_y(self.vmin, bar_y, bar_h) - h / 2 if bottom > top: imgui.set_cursor_screen_pos((x0, top)) imgui.invisible_button("##colorbar_region", (x1 - x0, 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(self._data_min + half, min(self._data_max - half, center)) vmin, vmax = center - half, center + half if (vmin, vmax) != (self._vmin, self._vmax): self._vmin, self._vmax = vmin, vmax self._update_bar_texture() for v in (self.vmax, self.vmin): y = self._value_to_y(v, bar_y, bar_h) draw_list.add_rect_filled((x0, y - h / 2), (x1, y + h / 2), fill) draw_list.add_rect( (x0, y - h / 2), (x1, y + h / 2), outline, thickness=1.0 ) for label, attr, lo_fn, hi_fn in ( ("##vmax_handle", "vmax", lambda: self.vmin + min_sep, lambda: self._data_max), ("##vmin_handle", "vmin", lambda: self._data_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((x0, y - h / 2)) imgui.invisible_button(label, (x1 - x0, h)) 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)), ) def _draw_popup(self): _, self.gamma = imgui.slider_float("gamma", self.gamma, 0.1, 5.0) imgui.separator() texture_height = imgui.get_font_size() - 2 for name, tex_id in self._picker_tex_ids.items(): _clicked, selected = imgui.menu_item( name, "", p_selected=(self.cmap_name == name) ) imgui.same_line() 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(tex_id, image_size=(100, texture_height)) imgui.pop_style_var() imgui.pop_style_color() if selected: self.cmap_name = name canvas = RenderCanvas(size=(256, 400), max_fps=60, update_mode="continuous") adapter = wgpu.gpu.request_adapter_sync(power_preference="high-performance") device = adapter.request_device_sync() imgui_renderer = ImguiRenderer(device, canvas) colorbar = ImguiColorbar( device=device, imgui_renderer=imgui_renderer, cmap_name="green", vmin=0.0, vmax=1.0, cmaps=["green", "red", "blue", "gray"], ) def update_gui(): imgui.set_next_window_pos((20, 20), imgui.Cond_.appearing) # Give the window an explicit initial size so it renders on the first frame. # An auto-fit window has no size until its second frame, which would leave # the gallery screenshot (a single rendered frame) blank. imgui.set_next_window_size((120, 340), imgui.Cond_.appearing) imgui.begin("colorbar", None) colorbar.draw() imgui.end() imgui_renderer.set_gui(update_gui) canvas.request_draw(imgui_renderer.render) if __name__ == "__main__": loop.run() .. rst-class:: sphx-glr-timing **Total running time of the script:** (0 minutes 1.875 seconds) .. _sphx_glr_download__gallery_colorbar.py: .. only:: html .. container:: sphx-glr-footer sphx-glr-footer-example .. container:: sphx-glr-download sphx-glr-download-jupyter :download:`Download Jupyter notebook: colorbar.ipynb ` .. container:: sphx-glr-download sphx-glr-download-python :download:`Download Python source code: colorbar.py ` .. container:: sphx-glr-download sphx-glr-download-zip :download:`Download zipped: colorbar.zip ` .. only:: html .. rst-class:: sphx-glr-signature `Gallery generated by Sphinx-Gallery `_