Trig_Basics / src /streamlit_app.py
NavyDevilDoc's picture
Update src/streamlit_app.py
4b8f4a2 verified
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}<extra></extra>'
))
# 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})<extra></extra>'
))
# 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()