| | """ |
| | Main Visualizer class for controlling algorithm visualization. |
| | |
| | π CONCEPT: Composition and State Management |
| | |
| | The Visualizer is the main controller for visualization. |
| | It COMPOSES (contains) other objects: |
| | - A StepRenderer (for creating HTML) |
| | - A list of Steps (from an algorithm) |
| | - Current state (playing, paused, etc.) |
| | |
| | This is COMPOSITION: building complex behavior from simpler parts. |
| | The Visualizer doesn't know HOW to render (that's the Renderer's job) |
| | It knows WHEN and WHAT to render (its own job). |
| | """ |
| |
|
| | from typing import List, Optional |
| |
|
| | from .state import VisualizationState, VisualizationConfig |
| | from .factory import RendererFactory |
| | from .renderers import StepRenderer |
| | from ..models import GestureImage, Step, StepType |
| |
|
| |
|
| | class Visualizer: |
| | """ |
| | π CONCEPT: Controller Class |
| | |
| | The Visualizer coordinates between: |
| | - Algorithm steps (the DATA) |
| | - Renderers (the DISPLAY) |
| | - User interactions (the CONTROLS) |
| | |
| | It follows the SINGLE RESPONSIBILITY PRINCIPLE: |
| | - Algorithms: Generate steps (Phase 2 & 3) |
| | - Renderers: Convert steps to HTML (above) |
| | - Visualizer: Manage navigation and state (here) |
| | |
| | This separation makes each part: |
| | - Testable independently |
| | - Reusable in different contexts |
| | - Easy to modify without breaking others |
| | """ |
| | |
| | def __init__(self, config: VisualizationConfig = None): |
| | """ |
| | Initialize a new Visualizer. |
| | |
| | Args: |
| | config: Optional configuration. Uses defaults if not provided. |
| | """ |
| | |
| | self._config = config or VisualizationConfig() |
| | |
| | |
| | self._state = VisualizationState.IDLE |
| | self._steps: List[Step] = [] |
| | self._current_step_index: int = 0 |
| | self._images: List[GestureImage] = [] |
| | self._renderer: Optional[StepRenderer] = None |
| | self._algorithm_name: str = "" |
| | |
| | |
| | self._stats = { |
| | "total_steps": 0, |
| | "comparisons": 0, |
| | "swaps": 0, |
| | "max_depth": 0, |
| | } |
| | |
| | |
| | |
| | |
| | |
| | @property |
| | def state(self) -> VisualizationState: |
| | """Current visualization state.""" |
| | return self._state |
| | |
| | @property |
| | def current_step(self) -> int: |
| | """Current step index (0-based).""" |
| | return self._current_step_index |
| | |
| | @property |
| | def total_steps(self) -> int: |
| | """Total number of steps.""" |
| | return len(self._steps) |
| | |
| | @property |
| | def is_at_start(self) -> bool: |
| | """True if at the first step.""" |
| | return self._current_step_index == 0 |
| | |
| | @property |
| | def is_at_end(self) -> bool: |
| | """True if at the last step.""" |
| | return self._current_step_index >= len(self._steps) - 1 |
| | |
| | @property |
| | def progress_percentage(self) -> float: |
| | """Progress through visualization as percentage.""" |
| | if not self._steps: |
| | return 0.0 |
| | return (self._current_step_index / (len(self._steps) - 1)) * 100 |
| | |
| | |
| | |
| | |
| | |
| | def load_steps( |
| | self, |
| | steps: List[Step], |
| | images: List[GestureImage], |
| | algorithm_name: str |
| | ) -> None: |
| | """ |
| | Load algorithm steps for visualization. |
| | |
| | π CONCEPT: State Transition |
| | |
| | When we load steps, we transition from IDLE to READY. |
| | This is part of a STATE MACHINE pattern - valid transitions: |
| | |
| | IDLE -> READY (load steps) |
| | READY -> PLAYING (play) |
| | READY -> STEPPING (next/prev) |
| | PLAYING -> PAUSED (pause) |
| | PAUSED -> PLAYING (resume) |
| | STEPPING -> COMPLETE (reach end) |
| | |
| | Args: |
| | steps: List of Steps from an algorithm |
| | images: The final state of images (for reference) |
| | algorithm_name: Name of the algorithm (for renderer selection) |
| | """ |
| | if not steps: |
| | raise ValueError("Cannot load empty step list") |
| | |
| | self._steps = steps |
| | self._images = images |
| | self._algorithm_name = algorithm_name |
| | self._current_step_index = 0 |
| | |
| | |
| | self._renderer = RendererFactory.create(algorithm_name) |
| | |
| | |
| | self._calculate_statistics() |
| | |
| | |
| | self._state = VisualizationState.READY |
| | |
| | def _calculate_statistics(self) -> None: |
| | """Calculate statistics from loaded steps.""" |
| | self._stats = { |
| | "total_steps": len(self._steps), |
| | "comparisons": sum(1 for s in self._steps if s.type == StepType.COMPARE), |
| | "swaps": sum(1 for s in self._steps if s.type == StepType.SWAP), |
| | "max_depth": max((s.depth for s in self._steps), default=0), |
| | } |
| | |
| | def render_current(self) -> str: |
| | """ |
| | Render the current step as HTML. |
| | |
| | Returns: |
| | HTML string for display in Gradio |
| | """ |
| | if self._state == VisualizationState.IDLE: |
| | return self._render_idle_state() |
| | |
| | if not self._steps or not self._renderer: |
| | return "<p>No visualization loaded.</p>" |
| | |
| | |
| | step = self._steps[self._current_step_index] |
| | |
| | |
| | step_images = step.array_state if step.array_state else self._images |
| | |
| | |
| | step_html = self._renderer.render_step(step, step_images) |
| | |
| | |
| | header_html = self._render_header() |
| | |
| | |
| | legend_html = self._renderer.get_legend() if self._config.show_legend else "" |
| | |
| | |
| | stats_html = self._render_statistics() if self._config.show_statistics else "" |
| | |
| | |
| | return f""" |
| | <div style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;"> |
| | {header_html} |
| | {legend_html} |
| | {step_html} |
| | {stats_html} |
| | </div> |
| | """ |
| | |
| | def _render_idle_state(self) -> str: |
| | """Render placeholder when no visualization is loaded.""" |
| | return """ |
| | <div style=" |
| | text-align: center; |
| | padding: 50px; |
| | color: #666; |
| | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; |
| | "> |
| | <div style="font-size: 48px; margin-bottom: 20px;">π</div> |
| | <h3>No Visualization Loaded</h3> |
| | <p>Capture some images and run an algorithm to see the visualization!</p> |
| | </div> |
| | """ |
| | |
| | def _render_header(self) -> str: |
| | """Render the visualization header with progress.""" |
| | progress = self.progress_percentage |
| | current = self._current_step_index + 1 |
| | total = len(self._steps) |
| | |
| | return f""" |
| | <div style=" |
| | background: linear-gradient(135deg, #002D62 0%, #9B2335 100%); |
| | color: white; |
| | padding: 15px; |
| | border-radius: 12px 12px 0 0; |
| | margin-bottom: 10px; |
| | "> |
| | <div style="display: flex; justify-content: space-between; align-items: center;"> |
| | <div> |
| | <strong style="font-size: 16px;">{self._algorithm_name}</strong> |
| | </div> |
| | <div style="font-size: 14px;"> |
| | Step {current} of {total} |
| | </div> |
| | </div> |
| | <div style=" |
| | background: rgba(255,255,255,0.3); |
| | height: 6px; |
| | border-radius: 3px; |
| | margin-top: 10px; |
| | overflow: hidden; |
| | "> |
| | <div style=" |
| | background: #FABD0F; |
| | height: 100%; |
| | width: {progress}%; |
| | transition: width 0.3s ease; |
| | "></div> |
| | </div> |
| | </div> |
| | """ |
| | |
| | def _render_statistics(self) -> str: |
| | """Render statistics panel.""" |
| | return f""" |
| | <div style=" |
| | display: flex; |
| | justify-content: space-around; |
| | padding: 10px; |
| | background: #f0f0f0; |
| | border-radius: 0 0 12px 12px; |
| | margin-top: 10px; |
| | font-size: 12px; |
| | "> |
| | <div>π Steps: {self._stats['total_steps']}</div> |
| | <div>βοΈ Comparisons: {self._stats['comparisons']}</div> |
| | <div>π Swaps: {self._stats['swaps']}</div> |
| | <div>π Max Depth: {self._stats['max_depth']}</div> |
| | </div> |
| | """ |
| | |
| | |
| | |
| | |
| | |
| | def next_step(self) -> str: |
| | """ |
| | Move to the next step and return rendered HTML. |
| | |
| | Returns: |
| | HTML for the new current step |
| | """ |
| | if self._state == VisualizationState.IDLE: |
| | return self._render_idle_state() |
| | |
| | if not self.is_at_end: |
| | self._current_step_index += 1 |
| | self._state = VisualizationState.STEPPING |
| | else: |
| | self._state = VisualizationState.COMPLETE |
| | |
| | return self.render_current() |
| | |
| | def prev_step(self) -> str: |
| | """ |
| | Move to the previous step and return rendered HTML. |
| | |
| | Returns: |
| | HTML for the new current step |
| | """ |
| | if self._state == VisualizationState.IDLE: |
| | return self._render_idle_state() |
| | |
| | if not self.is_at_start: |
| | self._current_step_index -= 1 |
| | self._state = VisualizationState.STEPPING |
| | |
| | return self.render_current() |
| | |
| | def go_to_step(self, step_index: int) -> str: |
| | """ |
| | Jump to a specific step. |
| | |
| | Args: |
| | step_index: 0-based step index |
| | |
| | Returns: |
| | HTML for the specified step |
| | """ |
| | if self._state == VisualizationState.IDLE: |
| | return self._render_idle_state() |
| | |
| | |
| | self._current_step_index = max(0, min(step_index, len(self._steps) - 1)) |
| | self._state = VisualizationState.STEPPING |
| | |
| | return self.render_current() |
| | |
| | def go_to_start(self) -> str: |
| | """Jump to the first step.""" |
| | return self.go_to_step(0) |
| | |
| | def go_to_end(self) -> str: |
| | """Jump to the last step.""" |
| | return self.go_to_step(len(self._steps) - 1) |
| | |
| | def reset(self) -> None: |
| | """Reset the visualizer to initial state.""" |
| | self._steps = [] |
| | self._current_step_index = 0 |
| | self._images = [] |
| | self._renderer = None |
| | self._algorithm_name = "" |
| | self._state = VisualizationState.IDLE |
| | self._stats = {"total_steps": 0, "comparisons": 0, "swaps": 0, "max_depth": 0} |
| | |
| | |
| | |
| | |
| | |
| | def play(self) -> None: |
| | """Start auto-playing the visualization.""" |
| | if self._state in [VisualizationState.READY, VisualizationState.PAUSED, |
| | VisualizationState.STEPPING]: |
| | self._state = VisualizationState.PLAYING |
| | |
| | def pause(self) -> None: |
| | """Pause the visualization.""" |
| | if self._state == VisualizationState.PLAYING: |
| | self._state = VisualizationState.PAUSED |
| | |
| | def is_playing(self) -> bool: |
| | """Check if visualization is currently playing.""" |
| | return self._state == VisualizationState.PLAYING |
| |
|