import streamlit as st import numpy as np import pandas as pd import plotly.graph_objects as go import plotly.subplots as sp from plotly.subplots import make_subplots from math import pi, radians, degrees import time # Set page configuration st.set_page_config( page_title="Trigonometry Basics Explorer", page_icon="📐", layout="wide" ) def safe_division(numerator, denominator, undefined_value=float('inf')): """Safely handle division by zero for trigonometric functions.""" return numerator / denominator if abs(denominator) > 1e-10 else undefined_value def calculate_trig_functions(angle_rad): """Calculate all six trigonometric functions for a given angle in radians.""" sin_val = np.sin(angle_rad) cos_val = np.cos(angle_rad) # Primary functions sine = sin_val cosine = cos_val tangent = safe_division(sin_val, cos_val) # Reciprocal functions cosecant = safe_division(1, sin_val) secant = safe_division(1, cos_val) cotangent = safe_division(cos_val, sin_val) return { 'sin': sine, 'cos': cosine, 'tan': tangent, 'csc': cosecant, 'sec': secant, 'cot': cotangent } @st.cache_data def create_animated_unit_circle(angle_degrees, show_special_angles=True): """Create unit circle with Plotly - optimized version.""" # Convert angle to radians angle_rad = radians(angle_degrees) x_point = np.cos(angle_rad) y_point = np.sin(angle_rad) # Create figure with minimal traces fig = go.Figure() # Single trace for unit circle theta = np.linspace(0, 2*pi, 100) fig.add_trace(go.Scatter( x=np.cos(theta), y=np.sin(theta), mode='lines', line=dict(color='blue', width=2), name='Unit Circle', hoverinfo='skip' )) # Enhanced special angles display with radians if show_special_angles: # Major special angles with their radian equivalents special_angles_data = [ (0, "0°\n(0)"), (30, "30°\n(π/6)"), (45, "45°\n(π/4)"), (60, "60°\n(π/3)"), (90, "90°\n(π/2)"), (120, "120°\n(2π/3)"), (135, "135°\n(3π/4)"), (150, "150°\n(5π/6)"), (180, "180°\n(π)"), (210, "210°\n(7π/6)"), (225, "225°\n(5π/4)"), (240, "240°\n(4π/3)"), (270, "270°\n(3π/2)"), (300, "300°\n(5π/3)"), (315, "315°\n(7π/4)"), (330, "330°\n(11π/6)") ] special_x = [np.cos(radians(angle)) for angle, _ in special_angles_data] special_y = [np.sin(radians(angle)) for angle, _ in special_angles_data] special_labels = [label for _, label in special_angles_data] # Single trace for all special points with degree and radian labels fig.add_trace(go.Scatter( x=special_x, y=special_y, mode='markers+text', marker=dict(size=6, color='gray', opacity=0.8), text=special_labels, textposition="top center", textfont=dict(size=7), showlegend=False, name='Special Angles', hovertemplate='%{text}' )) # Add arc to show the angle sweep from x-axis to current position if angle_degrees > 0: # Create arc from 0 to current angle arc_angles = np.linspace(0, angle_rad, max(10, int(angle_degrees/10))) arc_radius = 0.3 # Smaller radius for the arc arc_x = arc_radius * np.cos(arc_angles) arc_y = arc_radius * np.sin(arc_angles) fig.add_trace(go.Scatter( x=arc_x, y=arc_y, mode='lines', line=dict(color='orange', width=3), name=f'Angle Arc ({angle_degrees}°)', showlegend=False, hoverinfo='skip' )) # Add arrow at the end of the arc to show direction if len(arc_x) > 1: # Arrow direction dx = arc_x[-1] - arc_x[-2] dy = arc_y[-1] - arc_y[-2] fig.add_annotation( x=arc_x[-1], y=arc_y[-1], ax=arc_x[-1] - dx*5, ay=arc_y[-1] - dy*5, xref="x", yref="y", axref="x", ayref="y", arrowhead=2, arrowsize=1.5, arrowwidth=2, arrowcolor="orange", showarrow=True ) # Current point fig.add_trace(go.Scatter( x=[x_point], y=[y_point], mode='markers', marker=dict(size=10, color='red'), name=f'Point at {angle_degrees}°', hovertemplate=f'({x_point:.3f}, {y_point:.3f})' )) # Radius line fig.add_trace(go.Scatter( x=[0, x_point], y=[0, y_point], mode='lines', line=dict(color='red', width=3), name=f'Radius', showlegend=False, hoverinfo='skip' )) # Coordinate lines fig.add_trace(go.Scatter( x=[x_point, x_point, None, 0, x_point], y=[0, y_point, None, 0, 0], mode='lines', line=dict(color='green', width=2, dash='dash'), name=f'sin={y_point:.3f}, cos={x_point:.3f}', hoverinfo='skip' )) # Optimized layout fig.update_layout( title=f'Unit Circle at {angle_degrees}° ({angle_rad:.3f} rad)', xaxis=dict( range=[-1.2, 1.2], scaleanchor="y", scaleratio=1, showgrid=True, zeroline=True ), yaxis=dict( range=[-1.2, 1.2], showgrid=True, zeroline=True ), showlegend=True, width=600, height=600, margin=dict(l=50, r=50, t=50, b=50) ) return fig @st.cache_data def create_enhanced_function_plots(angle_min, angle_max, selected_functions_str, current_angle=None, show_special_angles=True): """Optimized function plots with single subplot per function.""" selected_functions = selected_functions_str.split(',') if selected_functions_str else [] if not selected_functions: return None # Use fewer points for better performance angle_range = np.linspace(angle_min, angle_max, 500) # Reduced from 1000 angle_rad = np.radians(angle_range) # Simple subplot layout num_functions = len(selected_functions) cols = min(2, num_functions) rows = (num_functions + cols - 1) // cols fig = make_subplots( rows=rows, cols=cols, subplot_titles=[f'{func.capitalize()} Function' for func in selected_functions] ) functions = { 'sin': {'func': np.sin, 'color': 'blue'}, 'cos': {'func': np.cos, 'color': 'red'}, 'tan': {'func': lambda x: np.clip(np.tan(x), -10, 10), 'color': 'green'}, # Clip for performance 'csc': {'func': lambda x: np.clip(1/np.sin(np.where(np.abs(np.sin(x)) > 0.01, x, np.nan)), -10, 10), 'color': 'purple'}, 'sec': {'func': lambda x: np.clip(1/np.cos(np.where(np.abs(np.cos(x)) > 0.01, x, np.nan)), -10, 10), 'color': 'orange'}, 'cot': {'func': lambda x: np.clip(1/np.tan(np.where(np.abs(np.tan(x)) > 0.01, x, np.nan)), -10, 10), 'color': 'brown'} } for idx, func_name in enumerate(selected_functions): row = idx // cols + 1 col = idx % cols + 1 if func_name in functions: func_info = functions[func_name] y_values = func_info['func'](angle_rad) # Main function plot fig.add_trace( go.Scatter( x=angle_range, y=y_values, mode='lines', line=dict(color=func_info['color'], width=2), name=func_name, showlegend=False ), row=row, col=col ) # Current angle marker (if provided) if current_angle is not None and angle_min <= current_angle <= angle_max: current_y = func_info['func'](radians(current_angle)) fig.add_trace( go.Scatter( x=[current_angle], y=[current_y], mode='markers', marker=dict(size=8, color='red'), showlegend=False ), row=row, col=col ) # Simplified layout fig.update_layout( height=300 * rows, # Smaller height showlegend=False, margin=dict(l=50, r=50, t=50, b=50) ) return fig def create_comparison_table(angle_degrees): """Create an enhanced comparison table with more information.""" trig_values = calculate_trig_functions(radians(angle_degrees)) # Determine quadrant quadrant = "" if 0 <= angle_degrees < 90: quadrant = "I" elif 90 <= angle_degrees < 180: quadrant = "II" elif 180 <= angle_degrees < 270: quadrant = "III" else: quadrant = "IV" # Create enhanced dataframe values_df = pd.DataFrame({ 'Function': ['sin(θ)', 'cos(θ)', 'tan(θ)', 'csc(θ)', 'sec(θ)', 'cot(θ)'], 'Value': [ f"{trig_values['sin']:.4f}", f"{trig_values['cos']:.4f}", f"{trig_values['tan']:.4f}" if abs(trig_values['tan']) < 1000 else "undefined", f"{trig_values['csc']:.4f}" if abs(trig_values['csc']) < 1000 else "undefined", f"{trig_values['sec']:.4f}" if abs(trig_values['sec']) < 1000 else "undefined", f"{trig_values['cot']:.4f}" if abs(trig_values['cot']) < 1000 else "undefined" ], 'Sign': [ "+" if trig_values['sin'] >= 0 else "-", "+" if trig_values['cos'] >= 0 else "-", "+" if abs(trig_values['tan']) < 1000 and trig_values['tan'] >= 0 else "-" if abs(trig_values['tan']) < 1000 else "N/A", "+" if abs(trig_values['csc']) < 1000 and trig_values['csc'] >= 0 else "-" if abs(trig_values['csc']) < 1000 else "N/A", "+" if abs(trig_values['sec']) < 1000 and trig_values['sec'] >= 0 else "-" if abs(trig_values['sec']) < 1000 else "N/A", "+" if abs(trig_values['cot']) < 1000 and trig_values['cot'] >= 0 else "-" if abs(trig_values['cot']) < 1000 else "N/A" ], 'Definition': [ 'y-coordinate / opposite', 'x-coordinate / adjacent', 'sin(θ)/cos(θ) = opp/adj', '1/sin(θ) = hyp/opp', '1/cos(θ) = hyp/adj', 'cos(θ)/sin(θ) = adj/opp' ] }) return values_df, quadrant def main(): st.title("📐 Trigonometry Basics Explorer") st.markdown("### Learn the Six Basic Trigonometric Functions Interactively!") # Stable tip selection using session state if 'current_tip' not in st.session_state: tips = [ "💡 **Tip**: Remember SOHCAHTOA - Sine=Opposite/Hypotenuse, Cosine=Adjacent/Hypotenuse, Tangent=Opposite/Adjacent", "🎯 **Did you know?**: The word 'sine' comes from the Latin word 'sinus' meaning 'bay' or 'fold'", "🔄 **Pattern**: Notice how sine and cosine are just shifted versions of each other!", "📊 **Memory trick**: In Quadrant I, all functions are positive. Use 'All Students Take Calculus' for Q1,Q2,Q3,Q4", "🌊 **Cool fact**: Trigonometric functions model waves, from sound waves to ocean tides!" ] st.session_state.current_tip = np.random.choice(tips) # Add a button to change tip if desired col_tip, col_button = st.columns([4, 1]) with col_tip: st.info(st.session_state.current_tip) with col_button: if st.button("💡 New Tip", key="new_tip_button"): tips = [ "💡 **Tip**: Remember SOHCAHTOA - Sine=Opposite/Hypotenuse, Cosine=Adjacent/Hypotenuse, Tangent=Opposite/Adjacent", "🎯 **Did you know?**: The word 'sine' comes from the Latin word 'sinus' meaning 'bay' or 'fold'", "🔄 **Pattern**: Notice how sine and cosine are just shifted versions of each other!", "📊 **Memory trick**: In Quadrant I, all functions are positive. Use 'All Students Take Calculus' for Q1,Q2,Q3,Q4", "🌊 **Cool fact**: Trigonometric functions model waves, from sound waves to ocean tides!" ] st.session_state.current_tip = np.random.choice(tips) st.rerun() # Sidebar controls st.sidebar.header("🎛️ Controls") # Enhanced function selection with unique keys st.sidebar.subheader("📊 Function Plots") col1, col2 = st.sidebar.columns(2) with col1: show_primary = st.checkbox("Primary Functions", value=True, key="show_primary_funcs") with col2: show_reciprocal = st.checkbox("Reciprocal Functions", value=False, key="show_reciprocal_funcs") selected_functions = [] if show_primary: selected_functions.extend(['sin', 'cos', 'tan']) if show_reciprocal: selected_functions.extend(['csc', 'sec', 'cot']) # Plot options with unique keys plot_range = st.sidebar.slider("Plot range (degrees)", 180, 720, 360, 90, key="plot_range_slider") show_special_angles = st.sidebar.checkbox("Show special angle markers", value=True, key="show_special_angles_cb") # Enhanced angle input with unique keys angle_input_method = st.sidebar.radio( "Choose angle input method:", ["Slider", "Text Input", "Common Angles"], key="angle_input_method_radio" ) # Settings with unique keys st.sidebar.subheader("🔧 Settings") angle_unit = st.sidebar.radio("Angle Units:", ["Degrees", "Radians"], key="angle_unit_radio") # Modify angle input methods to support both units with unique keys if angle_input_method == "Slider": if angle_unit == "Degrees": angle = st.sidebar.slider("Angle (degrees)", 0, 360, 45, 5, key="angle_degrees_slider") else: angle_rad = st.sidebar.slider("Angle (radians)", 0.0, 2*pi, pi/4, 0.1, key="angle_radians_slider") angle = degrees(angle_rad) elif angle_input_method == "Text Input": if angle_unit == "Degrees": angle = st.sidebar.number_input("Angle (degrees)", value=45.0, step=1.0, min_value=0.0, max_value=360.0, key="angle_degrees_input") else: angle_rad = st.sidebar.number_input("Angle (radians)", value=pi/4, step=0.1, min_value=0.0, max_value=2*pi, key="angle_radians_input") angle = degrees(angle_rad) else: # Common Angles common_angles = { "0°": 0, "30°": 30, "45°": 45, "60°": 60, "90°": 90, "120°": 120, "135°": 135, "150°": 150, "180°": 180, "210°": 210, "225°": 225, "240°": 240, "270°": 270, "300°": 300, "315°": 315, "330°": 330, "360°": 360 } selected_angle = st.sidebar.selectbox("Select common angle:", list(common_angles.keys()), key="common_angles_selectbox") angle = common_angles[selected_angle] # Main content area - optimized col1, col2 = st.columns([1, 1]) with col1: st.subheader("🔄 Enhanced Unit Circle") # Fixed parameter name fig_circle = create_animated_unit_circle(angle, show_special_angles) st.plotly_chart(fig_circle, use_container_width=True, config={'displayModeBar': False}) # Enhanced values table values_df, quadrant = create_comparison_table(angle) st.subheader(f"📊 Function Values (Quadrant {quadrant})") st.dataframe(values_df, use_container_width=True) st.info(f"**Angle {angle}° is in Quadrant {quadrant}**") with col2: if selected_functions: st.subheader("📈 Enhanced Function Graphs") # Convert list to string for caching selected_functions_str = ','.join(selected_functions) fig_functions = create_enhanced_function_plots( -plot_range//2, plot_range//2, selected_functions_str, angle, show_special_angles ) if fig_functions: st.plotly_chart(fig_functions, use_container_width=True, config={'displayModeBar': False}) else: st.info("Select function types from the sidebar to see their graphs.") # Simplified calculator st.subheader("🧮 Quick Calculator") calc_angle = st.number_input("Calculate for angle:", value=float(angle), step=1.0, key="calc_angle_input") if st.button("Calculate", key="calc_button"): calc_values = calculate_trig_functions(radians(calc_angle)) col_calc1, col_calc2 = st.columns(2) with col_calc1: st.metric("sin", f"{calc_values['sin']:.4f}") st.metric("cos", f"{calc_values['cos']:.4f}") st.metric("tan", f"{calc_values['tan']:.4f}" if abs(calc_values['tan']) < 1000 else "undefined") with col_calc2: st.metric("csc", f"{calc_values['csc']:.4f}" if abs(calc_values['csc']) < 1000 else "undefined") st.metric("sec", f"{calc_values['sec']:.4f}" if abs(calc_values['sec']) < 1000 else "undefined") st.metric("cot", f"{calc_values['cot']:.4f}" if abs(calc_values['cot']) < 1000 else "undefined") # Educational content (unchanged) st.markdown("---") st.subheader("📚 Understanding Trigonometric Functions") tab1, tab2, tab3, tab4, tab5 = st.tabs(["Definitions", "Relationships", "Key Angles", "Patterns", "Applications"]) with tab1: st.markdown(""" **Primary Functions:** - **Sine (sin)**: The y-coordinate of a point on the unit circle - **Cosine (cos)**: The x-coordinate of a point on the unit circle - **Tangent (tan)**: The ratio sin/cos, representing the slope of the radius line **Reciprocal Functions:** - **Cosecant (csc)**: 1/sin, reciprocal of sine - **Secant (sec)**: 1/cos, reciprocal of cosine - **Cotangent (cot)**: 1/tan or cos/sin, reciprocal of tangent """) with tab2: st.markdown(""" **Fundamental Identity:** - sin²(θ) + cos²(θ) = 1 **Quotient Identities:** - tan(θ) = sin(θ)/cos(θ) - cot(θ) = cos(θ)/sin(θ) **Reciprocal Identities:** - csc(θ) = 1/sin(θ) - sec(θ) = 1/cos(θ) - cot(θ) = 1/tan(θ) """) with tab3: key_angles_df = pd.DataFrame({ 'Angle': ['0°', '30°', '45°', '60°', '90°', '120°', '135°', '150°', '180°', '270°', '360°'], 'sin': ['0', '1/2', '√2/2', '√3/2', '1', '√3/2', '√2/2', '1/2', '0', '-1', '0'], 'cos': ['1', '√3/2', '√2/2', '1/2', '0', '-1/2', '-√2/2', '-√3/2', '-1', '0', '1'], 'tan': ['0', '√3/3', '1', '√3', 'undefined', '-√3', '-1', '-√3/3', '0', 'undefined', '0'] }) st.dataframe(key_angles_df, use_container_width=True) st.subheader("Radian Equivalents") radian_df = pd.DataFrame({ 'Degrees': ['0°', '30°', '45°', '60°', '90°', '180°', '270°', '360°'], 'Radians': ['0', 'π/6', 'π/4', 'π/3', 'π/2', 'π', '3π/2', '2π'], 'Decimal': ['0', '0.524', '0.785', '1.047', '1.571', '3.142', '4.712', '6.283'] }) st.dataframe(radian_df, use_container_width=True) with tab4: st.markdown(""" **Sign Patterns by Quadrant:** - **Quadrant I (0° to 90°)**: All functions positive - **Quadrant II (90° to 180°)**: Only sine positive - **Quadrant III (180° to 270°)**: Only tangent positive - **Quadrant IV (270° to 360°)**: Only cosine positive **Remember**: "All Students Take Calculus" (All, Sin, Tan, Cos) """) with tab5: st.markdown(""" **Real-world Applications:** - **Physics**: Wave motion, oscillations, circular motion - **Engineering**: Signal processing, electrical circuits - **Navigation**: GPS systems, celestial navigation - **Computer Graphics**: Rotations, animations - **Music**: Sound waves, harmonics - **Architecture**: Designing arches and domes """) if __name__ == "__main__": main()