From 82c53824b822f1517536580c62b858b66793391e Mon Sep 17 00:00:00 2001 From: vansh sehgal Date: Mon, 13 Oct 2025 21:20:39 +0530 Subject: [PATCH 1/2] enhance ui --- main.py | 301 +++++++++++++++++++++++++++++++++-------------- requirements.txt | 1 + 2 files changed, 216 insertions(+), 86 deletions(-) diff --git a/main.py b/main.py index 5b5e6a4..ea1857d 100644 --- a/main.py +++ b/main.py @@ -14,11 +14,16 @@ "Selection Sort": selection_sort, } +ALGO_DESCRIPTIONS = { + "": "", + +} + def draw_state_fig(state, highlight=(), info=""): """Draws a bar chart with axis labels, title, and highlight indices.""" plt.style.use('seaborn-v0_8-darkgrid') - fig, ax = plt.subplots(figsize=(9, 4)) + fig, ax = plt.subplots(figsize=(10, 3)) # If state is a grid (list of lists), flatten for now if not isinstance(state, (list, tuple)) or (len(state) and isinstance(state[0], (list, tuple))): # Fallback: show a simple text when non-list state @@ -45,117 +50,241 @@ def draw_state_fig(state, highlight=(), info=""): height = rect.get_height() ax.annotate(f'{val}', xy=(rect.get_x() + rect.get_width() / 2, height), xytext=(0, 3), textcoords='offset points', ha='center', va='bottom', fontsize=8) - ax.set_title(info, fontsize=12) + # Title removed to avoid duplicating the action description (shown in bottom bar) plt.tight_layout() return fig st.set_page_config(page_title="Algorithm Visualizer", layout="wide") -st.title("Algorithm Visualizer — Web Demo") +if 'algo_name' not in st.session_state: + st.session_state.algo_name = "Bubble Sort" +st.title(f"Algorithm Visualizer — {st.session_state.algo_name}") + +# UI state defaults +if 'setup_visible' not in st.session_state: + st.session_state.setup_visible = True +if 'arr_text' not in st.session_state: + st.session_state.arr_text = "5,2,4,1,3" +if 'rand_size' not in st.session_state: + st.session_state.rand_size = 10 +if 'rand_seed' not in st.session_state: + # fixed seed per session for stability of random previews unless size changes + st.session_state.rand_seed = int(time.time()) +if 'rand_array' not in st.session_state: + st.session_state.rand_array = [] +if 'playing' not in st.session_state: + st.session_state.playing = False +if 'frames' not in st.session_state: + st.session_state.frames = [] +if 'idx' not in st.session_state: + st.session_state.idx = 0 +if 'speed_mult' not in st.session_state: + st.session_state.speed_mult = 1.0 + +## Removed top hide/show toggle to keep layout stable -# Sidebar controls +# Sidebar controls (Setup Panel) with st.sidebar: - st.header("Controls") - - # Form for array input and visualization trigger - with st.form("visualization_form"): - algo_name = st.selectbox("Algorithm", list(ALGOS.keys()) + ["Binary Search"]) - st.markdown("**Array input (required)**") - arr_text = st.text_input("Enter numbers separated by commas", "5,2,4,1,3") - - if algo_name == "Binary Search": - target = st.number_input("Target value", value=5) - - submitted = st.form_submit_button("Visualize!") - - # Process form submission - if submitted: - # Derive array strictly from user input (no randomization) - try: - arr = [int(x.strip()) for x in arr_text.split(",") if x.strip()!=''] - if not arr: - st.error("Please enter at least one number") - else: - # Build frames list from generator so we can step/play - if algo_name == "Binary Search": - st.session_state.frames = list(binary_search(sorted(arr), int(target))) + with st.expander("Setup Panel", expanded=st.session_state.setup_visible): + # Form for setup and visualization trigger + with st.form("visualization_form"): + algo_name = st.selectbox("Algorithm", list(ALGOS.keys()) + ["Binary Search"]) + st.session_state.algo_name = algo_name + + st.markdown("**Data Input**") + tabs = st.tabs(["Manual Input", "Generate Random"]) + with tabs[0]: + st.session_state.arr_text = st.text_area( + "Enter comma-separated values", + st.session_state.arr_text, + height=80, + key="manual_arr_text", + ) + with tabs[1]: + size = st.slider("Array Size", min_value=2, max_value=50, value=st.session_state.rand_size, key="rand_size_slider") + # Regenerate preview array only when size changes + if size != st.session_state.rand_size or not st.session_state.rand_array: + import random as _r + rng = _r.Random(st.session_state.rand_seed) + st.session_state.rand_array = [rng.randint(1, 99) for _ in range(size)] + st.session_state.rand_size = size + # Show and bind to arr_text + preview = ",".join(map(str, st.session_state.rand_array)) + st.session_state.arr_text = preview + st.code(preview) + + if algo_name == "Binary Search": + target = st.number_input("Target value", value=5) + + # Primary action at bottom + visualize_submit = st.form_submit_button("Visualize", use_container_width=True, type="primary") + + if visualize_submit: + try: + arr = [int(x.strip()) for x in st.session_state.arr_text.split(",") if x.strip()!=''] + if not arr: + st.error("Please enter at least one number") else: - st.session_state.frames = list(ALGOS[algo_name](arr)) + if algo_name == "Binary Search": + st.session_state.frames = list(binary_search(sorted(arr), int(target))) + else: + st.session_state.frames = list(ALGOS[algo_name](arr)) + st.session_state.idx = 0 + st.session_state.playing = False + st.success(f"Generated {len(st.session_state.frames)} frames!") + except Exception: + st.error("Invalid array - use comma separated integers") + + # Display array info + try: + _arr_preview = [int(x.strip()) for x in st.session_state.arr_text.split(",") if x.strip()!=''] + st.write(f"Array size: {len(_arr_preview)}") + except Exception: + st.write("Array size: 0") + + # (Playback and speed moved to bottom bar) + +# Main display +image_placeholder = st.empty() + +def get_algo_data(): + """Returns a dictionary of algorithm details.""" + return { + "Bubble Sort": { + "description": "A simple algorithm that repeatedly steps through the list, swapping adjacent elements if they are out of order.", + "time_complexity": "O(n²) Average/Worst", + "space_complexity": "O(1)" + }, + "Selection Sort": { + "description": "Repeatedly finds the minimum element from the unsorted part and places it at the beginning of the sorted part.", + "time_complexity": "O(n²) Average/Worst", + "space_complexity": "O(1)" + }, + "Insertion Sort": { + "description": "Builds the final sorted array one item at a time, much like sorting a hand of playing cards.", + "time_complexity": "O(n²) Average/Worst", + "space_complexity": "O(1)" + }, + "Binary Search": { + "description": "Searches a sorted array by repeatedly dividing the search interval in half.", + "time_complexity": "O(log n) Average/Worst", + "space_complexity": "O(1)" + } + } + + +# Bottom Bar: fixed-style container at the bottom (stays visible) +bottom = st.container() +with bottom: + st.markdown( + """ + + """, + unsafe_allow_html=True, + ) + if st.session_state.frames: + st.markdown('
', unsafe_allow_html=True) + + # Playback row + c_left, c_mid, c_right = st.columns([3, 2, 5]) + with c_left: + b1, b2, b3, b4 = st.columns([1,1,1,1]) + with b1: + if st.button("≪", key="reset_btn", help="Reset"): + st.session_state.playing = False st.session_state.idx = 0 + with b2: + if st.button("‹", key="step_back_btn", help="Step Back"): st.session_state.playing = False - st.success(f"Generated {len(st.session_state.frames)} frames!") - except Exception: - st.error("Invalid array - use comma separated integers") - - # Display array info outside form - try: - arr = [int(x.strip()) for x in arr_text.split(",") if x.strip()!=''] - st.write(f"Array size: {len(arr)}") - except Exception: - st.write("Array size: 0") - - # Speed control: smooth slider for intuitive speed adjustment - base_delay_ms = 1000 # Base delay in milliseconds (1 second) - speed = st.slider("Animation Speed", min_value=1, max_value=10, value=5, - help="1 = slowest, 10 = fastest") - st.write(f"Speed: {speed}x") + st.session_state.idx = max(st.session_state.idx - 1, 0) + with b3: + # Single toggle button with clear text + icon + play_label = "❚❚" + pause_label = "▶" + if st.button(pause_label if st.session_state.playing else play_label, key="play_pause_btn", help="Play/Pause"): + st.session_state.playing = not st.session_state.playing + with b4: + if st.button("›", key="step_forward_btn", help="Step Forward"): + st.session_state.playing = False + st.session_state.idx = min(st.session_state.idx + 1, max(len(st.session_state.frames) - 1, 0)) + + with c_mid: + # Replaced popover/radio with a slider for speed control + st.session_state.speed_mult = st.slider( + "Animation Speed", + min_value=0.25, + max_value=4.0, + value=st.session_state.get('speed_mult', 1.0), + step=0.25, + format="%.2fx", + key="speed_slider" + ) + + with c_right: + step_placeholder = st.empty() + info_placeholder = st.empty() + if st.session_state.frames and 0 <= st.session_state.idx < len(st.session_state.frames): + frame = st.session_state.frames[st.session_state.idx] + step_placeholder.markdown(f"
Step: {st.session_state.idx} / {len(st.session_state.frames)-1}
", unsafe_allow_html=True) + _info_txt = str(frame.get('info', '')).replace('&','&').replace('<','<').replace('>','>') + info_placeholder.markdown(f"
{_info_txt}
", unsafe_allow_html=True) + else: + step_placeholder.markdown("
Step: 0 / 0
", unsafe_allow_html=True) + info_placeholder.markdown("
", unsafe_allow_html=True) + + st.markdown('
', unsafe_allow_html=True) + # Spacer to ensure content not hidden behind fixed bar + st.markdown('
', unsafe_allow_html=True) + else: + # Intro content when no frames yet + st.subheader("Welcome to Algorithm Visualizer") + st.write("Configure an algorithm and data in the left sidebar, then click Visualize to see an animated step-by-step walkthrough.") + st.markdown("---") - st.write("Playback") - if 'playing' not in st.session_state: - st.session_state.playing = False - if 'frames' not in st.session_state: - st.session_state.frames = [] - if 'idx' not in st.session_state: - st.session_state.idx = 0 - - # Clean play/pause/step buttons - c1, c2, c3 = st.columns([1,1,1]) - with c1: - if st.button("Play "): - st.session_state.playing = True - with c2: - if st.button("Pause "): - st.session_state.playing = False - with c3: - if st.button("Step "): - st.session_state.playing = False - st.session_state.idx = min(st.session_state.idx + 1, max(len(st.session_state.frames) - 1, 0)) - - if st.button("Reset"): - st.session_state.idx = 0 - st.session_state.playing = False - - st.write("Current frame:", st.session_state.idx, "/", max(len(st.session_state.frames) - 1, 0)) + st.header("About the Algorithms") -# Main display -placeholder = st.empty() + ALGO_DATA = get_algo_data() + for algo_name, data in ALGO_DATA.items(): + st.subheader(algo_name) + st.write(data["description"]) + st.markdown(f""" + - **Time Complexity:** `{data['time_complexity']}` + - **Space Complexity:** `{data['space_complexity']}` + """) + st.markdown("---") def render_frame_at(i: int): if 0 <= i < len(st.session_state.frames): frame = st.session_state.frames[i] fig = draw_state_fig(frame.get('state', []), frame.get('highlight', ()), frame.get('info', '')) - placeholder.pyplot(fig) + image_placeholder.pyplot(fig) plt.close(fig) + # Update right info area live + try: + step_placeholder.markdown(f"**Step:** {i} / {len(st.session_state.frames)-1}") + info_placeholder.write(frame.get('info', '')) + except Exception: + pass -# If frames are present, show current frame +# Always render current frame if present if st.session_state.frames: render_frame_at(st.session_state.idx) -# Playback loop (blocking): iterates frames while the playing flag is True. -# Note: this is a simple approach that does not support pausing mid-block reliably -# because Streamlit processes events between runs. It still provides Start/Stop/Step. +# Smooth blocking playback that updates only placeholders (image + info) if st.session_state.playing and st.session_state.frames: - # compute delay in seconds from base_delay_ms divided by speed slider value - delay = max(0.001, (base_delay_ms / max(1, speed)) / 1000.0) - # iterate from current index - for i in range(st.session_state.idx, len(st.session_state.frames)): - # if playing was switched off via UI before starting this iteration, break + base_delay_ms = 1000 + delay = max(0.001, (base_delay_ms / max(0.1, st.session_state.speed_mult)) / 1000.0) + for i in range(st.session_state.idx + 1, len(st.session_state.frames)): if not st.session_state.playing: break + st.session_state.idx = i render_frame_at(i) - st.session_state.idx = i + 1 time.sleep(delay) st.session_state.playing = False - if st.session_state.idx >= len(st.session_state.frames): - st.success("Done") diff --git a/requirements.txt b/requirements.txt index 4183b9c..acecf81 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ matplotlib numpy pytest +streamlit From d0a83e146220f4b61a0d12785d23ae161b188897 Mon Sep 17 00:00:00 2001 From: vansh sehgal Date: Mon, 13 Oct 2025 21:45:17 +0530 Subject: [PATCH 2/2] enhance ui --- main.py | 305 ++++++++++++++++++++++++++++++-------------------------- 1 file changed, 163 insertions(+), 142 deletions(-) diff --git a/main.py b/main.py index ea1857d..1bea29d 100644 --- a/main.py +++ b/main.py @@ -3,10 +3,68 @@ import streamlit as st import matplotlib.pyplot as plt -from algorithms.bubble_sort import bubble_sort -from algorithms.insertion_sort import insertion_sort -from algorithms.selection_sort import selection_sort -from algorithms.binary_search import binary_search +# --- Mock algorithm functions for stand-alone execution --- +# (In your actual project, you would import these from your files) +def bubble_sort(arr): + n = len(arr) + yield {"state": arr.copy(), "highlight": (), "info": "Initial array"} + for i in range(n): + swapped = False + for j in range(0, n - i - 1): + yield {"state": arr.copy(), "highlight": (j, j + 1), "info": f"Comparing {arr[j]} and {arr[j+1]}"} + if arr[j] > arr[j + 1]: + arr[j], arr[j + 1] = arr[j + 1], arr[j] + swapped = True + yield {"state": arr.copy(), "highlight": (j, j + 1), "info": f"Swapping {arr[j+1]} and {arr[j]}"} + if not swapped: + break + yield {"state": arr.copy(), "highlight": (), "info": "Array is sorted"} + +def insertion_sort(arr): + yield {"state": arr.copy(), "highlight": (), "info": "Initial array"} + for i in range(1, len(arr)): + key = arr[i] + j = i - 1 + yield {"state": arr.copy(), "highlight": (i, j), "info": f"Select {key} to insert"} + while j >= 0 and key < arr[j]: + arr[j + 1] = arr[j] + yield {"state": arr.copy(), "highlight": (j, j + 1), "info": f"Shifting {arr[j]} right"} + j -= 1 + arr[j + 1] = key + yield {"state": arr.copy(), "highlight": (j + 1,), "info": f"Inserted {key}"} + yield {"state": arr.copy(), "highlight": (), "info": "Array is sorted"} + +def selection_sort(arr): + n = len(arr) + yield {"state": arr.copy(), "highlight": (), "info": "Initial array"} + for i in range(n): + min_idx = i + for j in range(i + 1, n): + yield {"state": arr.copy(), "highlight": (min_idx, j), "info": f"Finding minimum in unsorted part"} + if arr[j] < arr[min_idx]: + min_idx = j + arr[i], arr[min_idx] = arr[min_idx], arr[i] + yield {"state": arr.copy(), "highlight": (i, min_idx), "info": f"Swapping minimum {arr[i]} to position {i}"} + yield {"state": arr.copy(), "highlight": (), "info": "Array is sorted"} + +def binary_search(arr, target): + low, high = 0, len(arr) - 1 + yield {"state": arr, "highlight": (low, high), "info": f"Initial search range: index {low} to {high}"} + while low <= high: + mid = (low + high) // 2 + yield {"state": arr, "highlight": (low, mid, high), "info": f"Checking middle index {mid} (value: {arr[mid]})"} + if arr[mid] == target: + yield {"state": arr, "highlight": (mid,), "info": f"Target {target} found at index {mid}"} + return + elif arr[mid] < target: + low = mid + 1 + yield {"state": arr, "highlight": (low, high), "info": f"Target is greater. New range: {low} to {high}"} + else: + high = mid - 1 + yield {"state": arr, "highlight": (low, high), "info": f"Target is smaller. New range: {low} to {high}"} + yield {"state": arr, "highlight": (), "info": f"Target {target} not found in the array"} +# --- End of mock functions --- + ALGOS = { "Bubble Sort": bubble_sort, @@ -14,24 +72,15 @@ "Selection Sort": selection_sort, } -ALGO_DESCRIPTIONS = { - "": "", - -} - - def draw_state_fig(state, highlight=(), info=""): """Draws a bar chart with axis labels, title, and highlight indices.""" plt.style.use('seaborn-v0_8-darkgrid') fig, ax = plt.subplots(figsize=(10, 3)) - # If state is a grid (list of lists), flatten for now if not isinstance(state, (list, tuple)) or (len(state) and isinstance(state[0], (list, tuple))): - # Fallback: show a simple text when non-list state ax.text(0.5, 0.5, str(state), ha='center', va='center') ax.set_xticks([]) ax.set_yticks([]) else: - # colored bars with a subtle gradient and highlighted indices cmap = plt.get_cmap('Blues') n = len(state) colors = [cmap(0.3 + 0.7 * (i / max(1, n - 1))) for i in range(n)] @@ -45,30 +94,26 @@ def draw_state_fig(state, highlight=(), info=""): ax.set_xticks(range(len(state))) ax.set_xlim(-0.5, max(len(state) - 0.5, 0.5)) ax.set_ylim(0, max(state) * 1.1 if state else 1) - # annotate bar values for clarity for rect, val in zip(bars, state): height = rect.get_height() ax.annotate(f'{val}', xy=(rect.get_x() + rect.get_width() / 2, height), xytext=(0, 3), textcoords='offset points', ha='center', va='bottom', fontsize=8) - # Title removed to avoid duplicating the action description (shown in bottom bar) plt.tight_layout() return fig - st.set_page_config(page_title="Algorithm Visualizer", layout="wide") if 'algo_name' not in st.session_state: st.session_state.algo_name = "Bubble Sort" st.title(f"Algorithm Visualizer — {st.session_state.algo_name}") -# UI state defaults +# --- UI STATE DEFAULTS --- if 'setup_visible' not in st.session_state: st.session_state.setup_visible = True -if 'arr_text' not in st.session_state: - st.session_state.arr_text = "5,2,4,1,3" +if 'manual_arr_text' not in st.session_state: + st.session_state.manual_arr_text = "5,2,4,1,3" if 'rand_size' not in st.session_state: st.session_state.rand_size = 10 if 'rand_seed' not in st.session_state: - # fixed seed per session for stability of random previews unless size changes st.session_state.rand_seed = int(time.time()) if 'rand_array' not in st.session_state: st.session_state.rand_array = [] @@ -80,100 +125,99 @@ def draw_state_fig(state, highlight=(), info=""): st.session_state.idx = 0 if 'speed_mult' not in st.session_state: st.session_state.speed_mult = 1.0 +if 'input_method' not in st.session_state: + st.session_state.input_method = "Manual Input" -## Removed top hide/show toggle to keep layout stable -# Sidebar controls (Setup Panel) +# --- SIDEBAR CONTROLS --- with st.sidebar: with st.expander("Setup Panel", expanded=st.session_state.setup_visible): - # Form for setup and visualization trigger - with st.form("visualization_form"): - algo_name = st.selectbox("Algorithm", list(ALGOS.keys()) + ["Binary Search"]) - st.session_state.algo_name = algo_name + + algo_name = st.selectbox("Algorithm", list(ALGOS.keys()) + ["Binary Search"]) + st.session_state.algo_name = algo_name - st.markdown("**Data Input**") - tabs = st.tabs(["Manual Input", "Generate Random"]) - with tabs[0]: - st.session_state.arr_text = st.text_area( + st.markdown("**Data Input**") + + # --- FIX: Moved radio button outside the form --- + # This allows it to trigger a rerun and update the UI immediately. + st.radio( + "Data Source", + ("Manual Input", "Generate Random"), + key='input_method' + ) + + with st.form("visualization_form"): + # --- FIX: Conditional UI is now inside the form --- + # It will display the correct input based on the radio button's state + if st.session_state.input_method == "Manual Input": + st.text_area( "Enter comma-separated values", - st.session_state.arr_text, - height=80, key="manual_arr_text", + height=80, ) - with tabs[1]: + else: # "Generate Random" is selected size = st.slider("Array Size", min_value=2, max_value=50, value=st.session_state.rand_size, key="rand_size_slider") - # Regenerate preview array only when size changes + # Automatically update the random array preview if size changes if size != st.session_state.rand_size or not st.session_state.rand_array: - import random as _r - rng = _r.Random(st.session_state.rand_seed) + rng = random.Random(st.session_state.rand_seed) st.session_state.rand_array = [rng.randint(1, 99) for _ in range(size)] st.session_state.rand_size = size - # Show and bind to arr_text + preview = ",".join(map(str, st.session_state.rand_array)) - st.session_state.arr_text = preview st.code(preview) + if algo_name == "Binary Search": - target = st.number_input("Target value", value=5) + target = st.number_input("Target value", value=5, key="target_value") - # Primary action at bottom visualize_submit = st.form_submit_button("Visualize", use_container_width=True, type="primary") if visualize_submit: try: - arr = [int(x.strip()) for x in st.session_state.arr_text.split(",") if x.strip()!=''] + if st.session_state.input_method == "Manual Input": + source_text = st.session_state.manual_arr_text + else: # Generate Random + source_text = ",".join(map(str, st.session_state.rand_array)) + + arr = [int(x.strip()) for x in source_text.split(",") if x.strip() != ''] + if not arr: st.error("Please enter at least one number") else: if algo_name == "Binary Search": - st.session_state.frames = list(binary_search(sorted(arr), int(target))) + st.session_state.frames = list(binary_search(sorted(arr), int(st.session_state.target_value))) else: st.session_state.frames = list(ALGOS[algo_name](arr)) st.session_state.idx = 0 st.session_state.playing = False st.success(f"Generated {len(st.session_state.frames)} frames!") - except Exception: - st.error("Invalid array - use comma separated integers") + except Exception as e: + st.error(f"An error occurred: {e}") - # Display array info - try: - _arr_preview = [int(x.strip()) for x in st.session_state.arr_text.split(",") if x.strip()!=''] - st.write(f"Array size: {len(_arr_preview)}") - except Exception: - st.write("Array size: 0") + try: + if st.session_state.get('input_method') == "Manual Input": + current_text = st.session_state.get('manual_arr_text', "") + else: + current_text = ",".join(map(str, st.session_state.get('rand_array', []))) + _arr_preview = [int(x.strip()) for x in current_text.split(",") if x.strip() != ''] + st.write(f"Array size: {len(_arr_preview)}") + except Exception: + st.write("Array size: 0") - # (Playback and speed moved to bottom bar) -# Main display +# --- MAIN DISPLAY & PLAYBACK --- image_placeholder = st.empty() def get_algo_data(): """Returns a dictionary of algorithm details.""" return { - "Bubble Sort": { - "description": "A simple algorithm that repeatedly steps through the list, swapping adjacent elements if they are out of order.", - "time_complexity": "O(n²) Average/Worst", - "space_complexity": "O(1)" - }, - "Selection Sort": { - "description": "Repeatedly finds the minimum element from the unsorted part and places it at the beginning of the sorted part.", - "time_complexity": "O(n²) Average/Worst", - "space_complexity": "O(1)" - }, - "Insertion Sort": { - "description": "Builds the final sorted array one item at a time, much like sorting a hand of playing cards.", - "time_complexity": "O(n²) Average/Worst", - "space_complexity": "O(1)" - }, - "Binary Search": { - "description": "Searches a sorted array by repeatedly dividing the search interval in half.", - "time_complexity": "O(log n) Average/Worst", - "space_complexity": "O(1)" - } + "Bubble Sort": {"description": "A simple algorithm that repeatedly steps through the list, swapping adjacent elements if they are out of order.", "time_complexity": "O(n²) Average/Worst", "space_complexity": "O(1)"}, + "Selection Sort": {"description": "Repeatedly finds the minimum element from the unsorted part and places it at the beginning of the sorted part.", "time_complexity": "O(n²) Average/Worst", "space_complexity": "O(1)"}, + "Insertion Sort": {"description": "Builds the final sorted array one item at a time, much like sorting a hand of playing cards.", "time_complexity": "O(n²) Average/Worst", "space_complexity": "O(1)"}, + "Binary Search": {"description": "Searches a sorted array by repeatedly dividing the search interval in half.", "time_complexity": "O(log n) Average/Worst", "space_complexity": "O(1)"} } - -# Bottom Bar: fixed-style container at the bottom (stays visible) +# --- BOTTOM BAR --- bottom = st.container() with bottom: st.markdown( @@ -190,41 +234,28 @@ def get_algo_data(): ) if st.session_state.frames: st.markdown('
', unsafe_allow_html=True) - - # Playback row c_left, c_mid, c_right = st.columns([3, 2, 5]) with c_left: b1, b2, b3, b4 = st.columns([1,1,1,1]) - with b1: - if st.button("≪", key="reset_btn", help="Reset"): - st.session_state.playing = False - st.session_state.idx = 0 - with b2: - if st.button("‹", key="step_back_btn", help="Step Back"): - st.session_state.playing = False - st.session_state.idx = max(st.session_state.idx - 1, 0) - with b3: - # Single toggle button with clear text + icon - play_label = "❚❚" - pause_label = "▶" - if st.button(pause_label if st.session_state.playing else play_label, key="play_pause_btn", help="Play/Pause"): - st.session_state.playing = not st.session_state.playing - with b4: - if st.button("›", key="step_forward_btn", help="Step Forward"): - st.session_state.playing = False - st.session_state.idx = min(st.session_state.idx + 1, max(len(st.session_state.frames) - 1, 0)) + if b1.button("≪", key="reset_btn", help="Reset"): + st.session_state.playing = False + st.session_state.idx = 0 + st.rerun() + if b2.button("‹", key="step_back_btn", help="Step Back"): + st.session_state.playing = False + st.session_state.idx = max(st.session_state.idx - 1, 0) + st.rerun() + play_label = "▶" if not st.session_state.playing else "❚❚" + if b3.button(play_label, key="play_pause_btn", help="Play/Pause"): + st.session_state.playing = not st.session_state.playing + st.rerun() + if b4.button("›", key="step_forward_btn", help="Step Forward"): + st.session_state.playing = False + st.session_state.idx = min(st.session_state.idx + 1, max(len(st.session_state.frames) - 1, 0)) + st.rerun() with c_mid: - # Replaced popover/radio with a slider for speed control - st.session_state.speed_mult = st.slider( - "Animation Speed", - min_value=0.25, - max_value=4.0, - value=st.session_state.get('speed_mult', 1.0), - step=0.25, - format="%.2fx", - key="speed_slider" - ) + st.session_state.speed_mult = st.slider("Animation Speed", min_value=0.25, max_value=4.0, value=st.session_state.get('speed_mult', 1.0), step=0.25, format="%.2fx", key="speed_slider") with c_right: step_placeholder = st.empty() @@ -234,57 +265,47 @@ def get_algo_data(): step_placeholder.markdown(f"
Step: {st.session_state.idx} / {len(st.session_state.frames)-1}
", unsafe_allow_html=True) _info_txt = str(frame.get('info', '')).replace('&','&').replace('<','<').replace('>','>') info_placeholder.markdown(f"
{_info_txt}
", unsafe_allow_html=True) - else: - step_placeholder.markdown("
Step: 0 / 0
", unsafe_allow_html=True) - info_placeholder.markdown("
", unsafe_allow_html=True) st.markdown('
', unsafe_allow_html=True) - # Spacer to ensure content not hidden behind fixed bar st.markdown('
', unsafe_allow_html=True) else: - # Intro content when no frames yet st.subheader("Welcome to Algorithm Visualizer") st.write("Configure an algorithm and data in the left sidebar, then click Visualize to see an animated step-by-step walkthrough.") - - st.markdown("---") - st.header("About the Algorithms") - - ALGO_DATA = get_algo_data() - - for algo_name, data in ALGO_DATA.items(): - st.subheader(algo_name) - st.write(data["description"]) - st.markdown(f""" - - **Time Complexity:** `{data['time_complexity']}` - - **Space Complexity:** `{data['space_complexity']}` - """) st.markdown("---") + st.header("About the Algorithms") + ALGO_DATA = get_algo_data() + for algo_name, data in ALGO_DATA.items(): + st.subheader(algo_name) + st.write(data["description"]) + st.markdown(f"""- **Time Complexity:** `{data['time_complexity']}`\n- **Space Complexity:** `{data['space_complexity']}`""") + st.markdown("---") + def render_frame_at(i: int): if 0 <= i < len(st.session_state.frames): frame = st.session_state.frames[i] fig = draw_state_fig(frame.get('state', []), frame.get('highlight', ()), frame.get('info', '')) image_placeholder.pyplot(fig) plt.close(fig) - # Update right info area live - try: - step_placeholder.markdown(f"**Step:** {i} / {len(st.session_state.frames)-1}") - info_placeholder.write(frame.get('info', '')) - except Exception: - pass -# Always render current frame if present +# Playback loop +def run_playback(): + if st.session_state.playing and st.session_state.frames: + base_delay_ms = 1000 + delay = max(0.001, (base_delay_ms / max(0.1, st.session_state.speed_mult)) / 1000.0) + + while st.session_state.idx < len(st.session_state.frames) - 1: + if not st.session_state.playing: + break + st.session_state.idx += 1 + render_frame_at(st.session_state.idx) + # A short sleep is necessary for the UI to feel responsive + time.sleep(delay) + + st.session_state.playing = False + st.rerun() + +# --- SCRIPT EXECUTION FLOW --- if st.session_state.frames: render_frame_at(st.session_state.idx) - -# Smooth blocking playback that updates only placeholders (image + info) -if st.session_state.playing and st.session_state.frames: - base_delay_ms = 1000 - delay = max(0.001, (base_delay_ms / max(0.1, st.session_state.speed_mult)) / 1000.0) - for i in range(st.session_state.idx + 1, len(st.session_state.frames)): - if not st.session_state.playing: - break - st.session_state.idx = i - render_frame_at(i) - time.sleep(delay) - st.session_state.playing = False - +if st.session_state.playing: + run_playback() \ No newline at end of file