Files
dpg-gauges/codex/ARCHITECTURE.md
2026-07-02 12:40:20 +02:00

10 KiB

dpg-gauges Architecture

This document defines the internal architecture for dpg-gauges.

The central rule is:

Dear PyGui item and draw operations happen on the GUI thread. Gauge state is kept separately from draw commands so high-rate value updates can redraw only the dynamic parts of a gauge.

dpg-gauges does not need the full thread-safe command architecture used by dpg-map; gauge updates are intended to be made from the GUI thread. The package still needs a clean state/render split so animation, smoothing, sizing, and high-rate updates stay stable.

High-level architecture

Public API
  ↓
Validation and configuration normalization
  ↓
Gauge registry and GaugeState
  ↓
Renderer on GUI thread
  ↓
Dear PyGui child/group + drawlist/text/items

Critical invariants

Threading invariants

  1. Dear PyGui APIs are called only on the GUI thread.
  2. Widget creation functions are GUI-thread-only.
  3. Runtime updates are GUI-thread-only unless explicitly documented otherwise.
  4. Worker threads may produce raw data, but application code must marshal values to the GUI thread.
  5. Locks should not be needed for normal gauge updates in the first release.

State invariants

  1. Every gauge has one GaugeState stored in the registry.
  2. Public tags identify logical gauges and should align with Dear PyGui behaviour where practical.
  3. Raw target value, clamped value, and displayed animated value are separate concepts.
  4. Thresholds and markers are configuration, not business logic.
  5. Out-of-range values clamp before rendering.
  6. Non-finite values enter an invalid display state instead of crashing.

Rendering invariants

  1. Static geometry and dynamic geometry should be separate where practical.
  2. Value updates should not rebuild static ticks, labels, threshold bands, or frames unless needed.
  3. Configuration changes mark the smallest reasonable dirty set.
  4. Redraws must not flicker through an empty state.
  5. Hidden or zero-size layouts must not permanently collapse a gauge.

Sizing invariants

  1. Requested size and measured size are stored separately.
  2. Drawlists use concrete measured dimensions.
  3. A measured size of zero while hidden does not overwrite the last useful non-zero size.
  4. Square gauges use the smaller dimension by default.
  5. Stretchable gauges can use the full available width/height.

Package layout

src/dpg_gauges/
  __init__.py
  api.py
  widget.py
  state.py
  renderer.py
  draw_layers.py
  sizing.py
  animation.py
  smoothing.py
  scales.py
  themes.py
  gauges.py
  diagnostics.py
  types.py
  exceptions.py
examples/
  basic_analog.py
  basic_digital.py
  basic_bar.py
  basic_level.py
  basic_status.py
  basic_segmented.py
  basic_compass.py
  basic_thermometer.py
  basic_battery.py
  basic_multi_value.py
  thresholds_markers.py
  animations.py
  dashboard.py
  sizing.py
  live_high_rate.py
  live_worst_case.py
tests/
docs/
  GETTING_STARTED.md
  EXAMPLES.md
  API_REFERENCE.md
codex/
  README.md
  FEATURES.md
  ARCHITECTURE.md
  STEPS.md
  AGENTS.md

Module responsibilities

__init__.py

Exports the public API only.

No expensive Dear PyGui initialization should happen here.

api.py

Contains thin public wrappers:

  • resolve gauge tags
  • validate and normalize simple arguments
  • call creation helpers
  • call state update helpers
  • expose stable runtime functions

api.py should not contain detailed drawing code.

types.py

Defines shared types and dataclasses:

Tag = str | int
Color = tuple[int, int, int] | tuple[int, int, int, int]
GaugeValue = float | int | bool | str | None

@dataclass(frozen=True)
class ThresholdBand: ...

@dataclass(frozen=True)
class GaugeMarker: ...

@dataclass(frozen=True)
class AnimationConfig: ...

@dataclass(frozen=True)
class SmoothingConfig: ...

@dataclass(frozen=True)
class GaugeStats: ...

exceptions.py

Public exceptions:

class DpgGaugesError(Exception): ...
class GaugeNotFoundError(DpgGaugesError): ...
class GaugeConfigError(DpgGaugesError): ...
class GaugeValueError(DpgGaugesError): ...
class GaugeTypeError(DpgGaugesError): ...
class ThreadingError(DpgGaugesError): ...

state.py

Owns global configuration, the gauge registry, and GaugeState.

@dataclass
class DpgGaugesConfig:
    default_theme: str | GaugeTheme | None = None
    animation: AnimationConfig | bool | None = None
    smoothing: SmoothingConfig | bool | None = None
    default_width: int | None = None
    default_height: int | None = None
    frame_rate_limit: float | None = None
    debug: bool = False

@dataclass
class GaugeState:
    tag: Tag
    gauge_type: str
    container_tag: Tag
    drawlist_tag: Tag | None
    text_tags: dict[str, Tag]
    config: GaugeConfig
    target_value: GaugeValue
    clamped_value: float | None
    displayed_value: float | None
    previous_displayed_value: float | None
    value_revision: int = 0
    config_revision: int = 0
    measured_width: int = 0
    measured_height: int = 0
    last_nonzero_width: int = 0
    last_nonzero_height: int = 0
    dirty: DirtyFlags = DirtyFlags.FULL
    renderer: GaugeRenderer | None = None

gauges.py

Defines normalized configuration dataclasses for gauge families:

  • GaugeConfig
  • AnalogGaugeConfig
  • DigitalGaugeConfig
  • BarGaugeConfig
  • LevelGaugeConfig
  • StatusLightConfig
  • SegmentedGaugeConfig
  • CompassGaugeConfig
  • ThermometerGaugeConfig
  • BatteryGaugeConfig
  • MultiValueGaugeConfig

This module should be mostly pure and easy to test.

widget.py

Creates Dear PyGui items for gauges.

Typical internal structure:

group or child_window
  drawlist
  optional text/value items

Rules:

  • Creation functions enter a context manager when appropriate.
  • The logical gauge is registered before yielding.
  • Raw internal draw item tags are not required as public API.
  • Callback and tooltip wiring happens here or in a small helper.

renderer.py

GUI-thread-only renderer.

Responsibilities:

  • measure gauge size
  • apply animation/smoothing progression
  • rebuild dirty static layer
  • rebuild dirty dynamic layer
  • update text/value items
  • avoid unnecessary static redraws
  • expose debug state

Gauge rendering should prefer Dear PyGui drawlists for shapes, arcs, needles, bars, and custom visuals. Plain Dear PyGui text items may be used when that is simpler and more reliable.

draw_layers.py

Tracks draw item tags by purpose:

@dataclass
class DrawLayer:
    name: str
    item_tags: set[Tag] = field(default_factory=set)

Suggested layers:

  • background
  • static scale
  • thresholds
  • dynamic value
  • markers
  • text/debug

The renderer can delete and rebuild only selected layers.

sizing.py

Provides helpers for measured dimensions and aspect decisions:

  • track requested vs measured size
  • preserve last non-zero size
  • calculate square gauge rect
  • calculate stretch rect
  • detect hidden-to-visible transitions

animation.py

Provides animation interpolation:

  • linear interpolation
  • simple easing names
  • target/displayed value separation
  • frame scheduling rules

Avoid an overbuilt animation framework in the first release.

smoothing.py

Provides optional readability smoothing:

  • exponential moving average via alpha
  • optional max_step
  • reset behaviour when value jumps or gauge is reconfigured

scales.py

Pure math helpers:

  • clamp and normalize values
  • map value to angle
  • map value to bar fill fraction
  • generate tick values
  • format values
  • resolve threshold geometry

No Dear PyGui imports.

themes.py

Lightweight presets and style normalization.

This should complement Dear PyGui theming, not replace it.

Built-in presets can include:

  • default
  • dark
  • minimal
  • motorsport
  • industrial

diagnostics.py

Provides:

def get_gauge_debug_state(tag) -> dict[str, Any]: ...

Include:

  • gauge type
  • target value
  • clamped value
  • displayed value
  • measured size
  • last non-zero size
  • dirty flags
  • value/config revisions
  • animation state
  • smoothing state

Dirty flag model

Use bit flags:

class DirtyFlags(IntFlag):
    NONE = 0
    VALUE = auto()
    STATIC = auto()
    DYNAMIC = auto()
    TEXT = auto()
    SIZE = auto()
    STYLE = auto()
    DEBUG = auto()
    FULL = VALUE | STATIC | DYNAMIC | TEXT | SIZE | STYLE | DEBUG

Expected usage:

Operation Dirty flags
value update `VALUE
animation tick `DYNAMIC
threshold update `STATIC
marker update `STATIC
label/unit/precision update TEXT
min/max update `STATIC
resize `SIZE
style/theme update `STYLE

Frame and update lifecycle

For each gauge update:

1. Public API validates value/config.
2. State target value/config is updated.
3. Dirty flags are marked.
4. Renderer is scheduled if needed.
5. Renderer measures size.
6. Renderer advances animation/smoothing.
7. Renderer redraws static layers only if needed.
8. Renderer redraws dynamic/text layers as needed.
9. Dirty flags are cleared.
10. Renderer continues scheduling frames only while animation/smoothing is active.

High-rate telemetry strategy

The first release should not promise background-thread-safe UI calls. Instead, examples should show the correct Dear PyGui pattern:

producer thread writes latest raw value to a lock-protected variable or queue
GUI frame callback reads the newest value
GUI frame callback calls dpgg.set_value(...)
gauge renderer coalesces/animates/smooths visual changes

This preserves Dear PyGui thread rules while still covering worst-case telemetry rates.

Initial setup instructions

mkdir dpg-gauges
cd dpg-gauges
git init
uv init --package dpg-gauges
uv add dearpygui
uv add --dev pytest ruff pyright

Recommended pyproject.toml additions:

[tool.ruff]
line-length = 100
target-version = "py311"

[tool.ruff.lint]
select = ["E", "F", "I", "UP", "B", "SIM"]

[tool.pyright]
typeCheckingMode = "basic"