import streamlit as st import pandas as pd import plotly.express as px import plotly.graph_objects as go from datetime import datetime, timedelta from config.database import get_database_connection import io import uuid from plotly.subplots import make_subplots from io import BytesIO class DashboardManager: def __init__(self): self.conn = get_database_connection() self.colors = { 'primary': '#4CAF50', 'secondary': '#2196F3', 'warning': '#FFA726', 'danger': '#F44336', 'info': '#00BCD4', 'success': '#66BB6A', 'purple': '#9C27B0', 'background': '#1E1E1E', 'card': '#2D2D2D', 'text': '#FFFFFF', 'subtext': '#B0B0B0' } def apply_dashboard_style(self): """Apply custom styling for dashboard""" st.markdown(""" """, unsafe_allow_html=True) def get_resume_metrics(self): """Get resume-related metrics from database""" cursor = self.conn.cursor() # Get current date now = datetime.now() start_of_day = now.replace(hour=0, minute=0, second=0, microsecond=0) start_of_week = now - timedelta(days=now.weekday()) start_of_month = now.replace(day=1) # Fetch metrics for different time periods metrics = {} for period, start_date in [ ('Today', start_of_day), ('This Week', start_of_week), ('This Month', start_of_month), ('All Time', datetime(2000, 1, 1)) ]: cursor.execute(""" SELECT COUNT(DISTINCT rd.id) as total_resumes, ROUND(AVG(ra.ats_score), 1) as avg_ats_score, ROUND(AVG(ra.keyword_match_score), 1) as avg_keyword_score, COUNT(DISTINCT CASE WHEN ra.ats_score >= 70 THEN rd.id END) as high_scoring FROM resume_data rd LEFT JOIN resume_analysis ra ON rd.id = ra.resume_id WHERE rd.created_at >= ? """, (start_date.strftime('%Y-%m-%d %H:%M:%S'),)) row = cursor.fetchone() if row: metrics[period] = { 'total': row[0] or 0, 'ats_score': row[1] or 0, 'keyword_score': row[2] or 0, 'high_scoring': row[3] or 0 } else: metrics[period] = { 'total': 0, 'ats_score': 0, 'keyword_score': 0, 'high_scoring': 0 } return metrics def get_skill_distribution(self): """Get skill distribution data""" cursor = self.conn.cursor() cursor.execute(""" WITH RECURSIVE split(skill, rest) AS ( SELECT '', skills || ',' FROM resume_data UNION ALL SELECT substr(rest, 0, instr(rest, ',')), substr(rest, instr(rest, ',') + 1) FROM split WHERE rest <> '' ), SkillCategories AS ( SELECT CASE WHEN LOWER(TRIM(skill, '[]" ')) LIKE '%python%' OR LOWER(TRIM(skill, '[]" ')) LIKE '%java%' OR LOWER(TRIM(skill, '[]" ')) LIKE '%javascript%' OR LOWER(TRIM(skill, '[]" ')) LIKE '%c++%' OR LOWER(TRIM(skill, '[]" ')) LIKE '%programming%' THEN 'Programming' WHEN LOWER(TRIM(skill, '[]" ')) LIKE '%sql%' OR LOWER(TRIM(skill, '[]" ')) LIKE '%database%' OR LOWER(TRIM(skill, '[]" ')) LIKE '%mongodb%' THEN 'Database' WHEN LOWER(TRIM(skill, '[]" ')) LIKE '%aws%' OR LOWER(TRIM(skill, '[]" ')) LIKE '%cloud%' OR LOWER(TRIM(skill, '[]" ')) LIKE '%azure%' THEN 'Cloud' WHEN LOWER(TRIM(skill, '[]" ')) LIKE '%agile%' OR LOWER(TRIM(skill, '[]" ')) LIKE '%scrum%' OR LOWER(TRIM(skill, '[]" ')) LIKE '%management%' THEN 'Management' ELSE 'Other' END as category, COUNT(*) as count FROM split WHERE skill <> '' GROUP BY category ) SELECT category, count FROM SkillCategories ORDER BY count DESC """) categories, counts = [], [] for row in cursor.fetchall(): categories.append(row[0]) counts.append(row[1]) return categories, counts def get_weekly_trends(self): """Get weekly submission trends""" cursor = self.conn.cursor() now = datetime.now() dates = [(now - timedelta(days=x)).strftime('%Y-%m-%d') for x in range(6, -1, -1)] submissions = [] for date in dates: cursor.execute(""" SELECT COUNT(*) FROM resume_data WHERE DATE(created_at) = DATE(?) """, (date,)) submissions.append(cursor.fetchone()[0]) return [d[-3:] for d in dates], submissions # Return shortened date format (e.g., 'Mon', 'Tue') def get_job_category_stats(self): """Get statistics by job category""" cursor = self.conn.cursor() cursor.execute(""" SELECT COALESCE(target_category, 'Other') as category, COUNT(*) as count, ROUND(AVG(CASE WHEN ra.ats_score >= 70 THEN 1 ELSE 0 END) * 100, 1) as success_rate FROM resume_data rd LEFT JOIN resume_analysis ra ON rd.id = ra.resume_id GROUP BY category ORDER BY count DESC LIMIT 5 """) categories, success_rates = [], [] for row in cursor.fetchall(): categories.append(row[0]) success_rates.append(row[2] or 0) return categories, success_rates def render_admin_panel(self): """Render admin panel with data management tools""" st.sidebar.markdown("### 👋 Welcome Admin!") st.sidebar.markdown("---") if st.sidebar.button("🚪 Logout"): st.session_state.is_admin = False st.rerun() st.sidebar.markdown("### 🛠️ Admin Tools") # Data Export Options export_format = st.sidebar.selectbox( "Export Format", ["Excel", "CSV", "JSON"], key="export_format" ) if st.sidebar.button("📥 Export Data"): if export_format == "Excel": excel_data = self.export_to_excel() if excel_data: st.sidebar.download_button( "⬇️ Download Excel", data=excel_data, file_name=f"resume_data_{datetime.now().strftime('%Y%m%d_%H%M')}.xlsx", mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" ) elif export_format == "CSV": csv_data = self.export_to_csv() if csv_data: st.sidebar.download_button( "⬇️ Download CSV", data=csv_data, file_name=f"resume_data_{datetime.now().strftime('%Y%m%d_%H%M')}.csv", mime="text/csv" ) else: json_data = self.export_to_json() if json_data: st.sidebar.download_button( "⬇️ Download JSON", data=json_data, file_name=f"resume_data_{datetime.now().strftime('%Y%m%d_%H%M')}.json", mime="application/json" ) # Database Stats st.sidebar.markdown("### 📊 Database Stats") stats = self.get_database_stats() st.sidebar.markdown(f""" - Total Resumes: {stats['total_resumes']} - Today's Submissions: {stats['today_submissions']} - Storage Used: {stats['storage_size']} """) def get_resume_data(self): """Get all resume data""" cursor = self.conn.cursor() try: cursor.execute(''' SELECT r.id, r.name, r.email, r.phone, r.linkedin, r.github, r.portfolio, r.target_role, r.target_category, r.created_at, a.ats_score, a.keyword_match_score, a.format_score, a.section_score FROM resume_data r LEFT JOIN resume_analysis a ON r.id = a.resume_id ORDER BY r.created_at DESC ''') return cursor.fetchall() except Exception as e: print(f"Error fetching resume data: {str(e)}") return [] def render_resume_data_section(self): """Render resume data section with Excel download""" st.markdown("

Resume Submissions

", unsafe_allow_html=True) # Get resume data resume_data = self.get_resume_data() if resume_data: # Convert to DataFrame columns = [ 'ID', 'Name', 'Email', 'Phone', 'LinkedIn', 'GitHub', 'Portfolio', 'Target Role', 'Target Category', 'Submission Date', 'ATS Score', 'Keyword Match', 'Format Score', 'Section Score' ] df = pd.DataFrame(resume_data, columns=columns) # Format scores as percentages score_columns = ['ATS Score', 'Keyword Match', 'Format Score', 'Section Score'] for col in score_columns: df[col] = df[col].apply(lambda x: f"{x*100:.1f}%" if pd.notnull(x) else "N/A") # Style the dataframe st.markdown(""" """, unsafe_allow_html=True) with st.container(): st.markdown('
', unsafe_allow_html=True) # Add filters col1, col2 = st.columns(2) with col1: target_role = st.selectbox( "Filter by Target Role", options=["All"] + list(df['Target Role'].unique()), key="role_filter" ) with col2: target_category = st.selectbox( "Filter by Category", options=["All"] + list(df['Target Category'].unique()), key="category_filter" ) # Apply filters filtered_df = df.copy() if target_role != "All": filtered_df = filtered_df[filtered_df['Target Role'] == target_role] if target_category != "All": filtered_df = filtered_df[filtered_df['Target Category'] == target_category] # Display filtered data st.dataframe( filtered_df, use_container_width=True, hide_index=True ) # Add download buttons col1, col2 = st.columns(2) with col1: # Download filtered data excel_buffer = BytesIO() filtered_df.to_excel(excel_buffer, index=False, engine='openpyxl') excel_buffer.seek(0) st.download_button( label="📥 Download Filtered Data", data=excel_buffer, file_name=f"resume_data_filtered_{datetime.now().strftime('%Y%m%d_%H%M%S')}.xlsx", mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", key="download_filtered_data" ) with col2: # Download all data excel_buffer_all = BytesIO() df.to_excel(excel_buffer_all, index=False, engine='openpyxl') excel_buffer_all.seek(0) st.download_button( label="📥 Download All Data", data=excel_buffer_all, file_name=f"resume_data_all_{datetime.now().strftime('%Y%m%d_%H%M%S')}.xlsx", mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", key="download_all_data" ) st.markdown('
', unsafe_allow_html=True) else: st.info("No resume submissions available") def render_admin_section(self): """Render admin section with logs and Excel download""" # Render resume data section self.render_resume_data_section() # Render admin logs section st.markdown("

Admin Activity Logs

", unsafe_allow_html=True) # Get admin logs admin_logs = self.get_admin_logs() if admin_logs: # Convert to DataFrame df = pd.DataFrame(admin_logs, columns=['Admin Email', 'Action', 'Timestamp']) # Style the dataframe st.markdown(""" """, unsafe_allow_html=True) with st.container(): st.markdown('
', unsafe_allow_html=True) st.dataframe( df, use_container_width=True, hide_index=True ) # Add download button excel_buffer = BytesIO() df.to_excel(excel_buffer, index=False, engine='openpyxl') excel_buffer.seek(0) st.download_button( label="📥 Download Admin Logs as Excel", data=excel_buffer, file_name=f"admin_logs_{datetime.now().strftime('%Y%m%d_%H%M%S')}.xlsx", mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", key="download_admin_logs" ) st.markdown('
', unsafe_allow_html=True) else: st.info("No admin activity logs available") def export_to_excel(self): """Export data to Excel format""" query = """ SELECT rd.name, rd.email, rd.phone, rd.linkedin, rd.github, rd.portfolio, rd.summary, rd.target_role, rd.target_category, rd.education, rd.experience, rd.projects, rd.skills, ra.ats_score, ra.keyword_match_score, ra.format_score, ra.section_score, ra.missing_skills, ra.recommendations, rd.created_at FROM resume_data rd LEFT JOIN resume_analysis ra ON rd.id = ra.resume_id """ try: df = pd.read_sql_query(query, self.conn) # Create Excel writer object output = BytesIO() with pd.ExcelWriter(output, engine='xlsxwriter') as writer: # Write main data df.to_excel(writer, sheet_name='Resume Data', index=False) # Get the workbook and the worksheet workbook = writer.book worksheet = writer.sheets['Resume Data'] # Add formatting header_format = workbook.add_format({ 'bold': True, 'text_wrap': True, 'valign': 'top', 'fg_color': '#D7E4BC', 'border': 1 }) # Write headers with formatting for col_num, value in enumerate(df.columns.values): worksheet.write(0, col_num, value, header_format) # Auto-adjust columns' width for i, col in enumerate(df.columns): max_length = max( df[col].astype(str).apply(len).max(), len(str(col)) ) + 2 worksheet.set_column(i, i, min(max_length, 50)) # Return the Excel file output.seek(0) return output.getvalue() except Exception as e: st.error(f"Error exporting to Excel: {str(e)}") return None def export_to_csv(self): """Export data to CSV format""" query = """ SELECT rd.name, rd.email, rd.phone, rd.linkedin, rd.github, rd.portfolio, rd.summary, rd.target_role, rd.target_category, rd.education, rd.experience, rd.projects, rd.skills, ra.ats_score, ra.keyword_match_score, ra.format_score, ra.section_score, ra.missing_skills, ra.recommendations, rd.created_at FROM resume_data rd LEFT JOIN resume_analysis ra ON rd.id = ra.resume_id """ try: df = pd.read_sql_query(query, self.conn) return df.to_csv(index=False).encode('utf-8') except Exception as e: st.error(f"Error exporting to CSV: {str(e)}") return None def export_to_json(self): """Export data to JSON format""" query = """ SELECT rd.*, ra.* FROM resume_data rd LEFT JOIN resume_analysis ra ON rd.id = ra.resume_id """ try: df = pd.read_sql_query(query, self.conn) return df.to_json(orient='records', date_format='iso') except Exception as e: st.error(f"Error exporting to JSON: {str(e)}") return None def get_database_stats(self): """Get database statistics""" cursor = self.conn.cursor() stats = {} # Total resumes cursor.execute("SELECT COUNT(*) FROM resume_data") stats['total_resumes'] = cursor.fetchone()[0] # Today's submissions cursor.execute(""" SELECT COUNT(*) FROM resume_data WHERE DATE(created_at) = DATE('now') """) stats['today_submissions'] = cursor.fetchone()[0] # Database size (approximate) cursor.execute("PRAGMA page_count") page_count = cursor.fetchone()[0] cursor.execute("PRAGMA page_size") page_size = cursor.fetchone()[0] size_bytes = page_count * page_size if size_bytes < 1024: stats['storage_size'] = f"{size_bytes} bytes" elif size_bytes < 1024 * 1024: stats['storage_size'] = f"{size_bytes/1024:.1f} KB" else: stats['storage_size'] = f"{size_bytes/(1024*1024):.1f} MB" return stats def get_admin_logs(self): """Get admin logs""" cursor = self.conn.cursor() try: cursor.execute(''' SELECT admin_email, action, timestamp FROM admin_logs ORDER BY timestamp DESC ''') return cursor.fetchall() except Exception as e: print(f"Error fetching admin logs: {str(e)}") return [] def render_dashboard(self): """Main dashboard rendering function""" # Apply styling st.markdown(""" """, unsafe_allow_html=True) # Dashboard Header st.markdown("""
📊 Resume Analytics Dashboard
Last updated: {}
""".format(datetime.now().strftime('%B %d, %Y %I:%M %p')), unsafe_allow_html=True) # Quick Stats stats = self.get_quick_stats() trend_indicators = self.get_trend_indicators() st.markdown("""

{}

Total Resumes

{} {}%

{}

Avg ATS Score

{} {}%

{}

High Performing

{} {}%

{}

Success Rate

{} {}%
""".format( stats['Total Resumes'], trend_indicators['resumes']['class'], trend_indicators['resumes']['icon'], trend_indicators['resumes']['value'], stats['Avg ATS Score'], trend_indicators['ats']['class'], trend_indicators['ats']['icon'], trend_indicators['ats']['value'], stats['High Performing'], trend_indicators['high_performing']['class'], trend_indicators['high_performing']['icon'], trend_indicators['high_performing']['value'], stats['Success Rate'], trend_indicators['success_rate']['class'], trend_indicators['success_rate']['icon'], trend_indicators['success_rate']['value'] ), unsafe_allow_html=True) # Performance Analytics Section st.markdown('
📈 Performance Analytics
', unsafe_allow_html=True) col1, col2 = st.columns(2) with col1: st.markdown('
', unsafe_allow_html=True) fig = self.create_enhanced_ats_gauge(float(stats['Avg ATS Score'].rstrip('%'))) st.plotly_chart(fig, use_container_width=True) st.markdown('
', unsafe_allow_html=True) with col2: st.markdown('
', unsafe_allow_html=True) fig = self.create_skill_distribution_chart() st.plotly_chart(fig, use_container_width=True) st.markdown('
', unsafe_allow_html=True) # Additional Analytics col1, col2 = st.columns(2) with col1: st.markdown('
', unsafe_allow_html=True) fig = self.create_submission_trends_chart() st.plotly_chart(fig, use_container_width=True) st.markdown('
', unsafe_allow_html=True) with col2: st.markdown('
', unsafe_allow_html=True) fig = self.create_job_category_chart() st.plotly_chart(fig, use_container_width=True) st.markdown('
', unsafe_allow_html=True) # Key Insights Section st.markdown('
🎯 Key Insights
', unsafe_allow_html=True) insights = self.get_detailed_insights() st.markdown('
', unsafe_allow_html=True) for insight in insights: st.markdown(f"""

{insight['icon']} {insight['title']}

{insight['description']}

{insight['trend_icon']} {insight['trend_value']}
""", unsafe_allow_html=True) st.markdown('
', unsafe_allow_html=True) # Admin logs section with Excel download functionality if st.session_state.get('is_admin', False): self.render_admin_section() def get_trend_indicators(self): """Get trend indicators for stats""" cursor = self.conn.cursor() indicators = {} # Compare with last week's data for metric in ['resumes', 'ats', 'high_performing', 'success_rate']: try: if metric == 'resumes': cursor.execute(""" SELECT (COUNT(*) - ( SELECT COUNT(*) FROM resume_data WHERE created_at < date('now', '-7 days') )) * 100.0 / NULLIF(( SELECT COUNT(*) FROM resume_data WHERE created_at < date('now', '-7 days') ), 0) FROM resume_data """) elif metric == 'ats': cursor.execute(""" SELECT (AVG(ats_score) - ( SELECT AVG(ats_score) FROM resume_analysis WHERE created_at < date('now', '-7 days') )) * 100.0 / NULLIF(( SELECT AVG(ats_score) FROM resume_analysis WHERE created_at < date('now', '-7 days') ), 0) FROM resume_analysis """) change = cursor.fetchone()[0] or 0 indicators[metric] = { 'value': abs(round(change, 1)), 'icon': '↑' if change >= 0 else '↓', 'class': 'trend-up' if change >= 0 else 'trend-down' } except Exception: indicators[metric] = { 'value': 0, 'icon': '→', 'class': 'trend-neutral' } return indicators def get_detailed_insights(self): """Get detailed insights from the database""" cursor = self.conn.cursor() insights = [] # Most Successful Job Category cursor.execute(""" SELECT target_category, AVG(ats_score) as avg_score, COUNT(*) as submission_count FROM resume_data rd JOIN resume_analysis ra ON rd.id = ra.resume_id GROUP BY target_category ORDER BY avg_score DESC LIMIT 1 """) top_category = cursor.fetchone() if top_category: insights.append({ 'title': 'Top Performing Category', 'icon': '🏆', 'description': f"{top_category[0]} leads with {top_category[1]:.1f}% average ATS score across {top_category[2]} submissions", 'trend_class': 'trend-up', 'trend_icon': '↑', 'trend_value': f"{top_category[1]:.1f}%" }) # Recent Improvement cursor.execute(""" SELECT (SELECT AVG(ats_score) FROM resume_analysis WHERE created_at >= date('now', '-7 days')) as recent_score, (SELECT AVG(ats_score) FROM resume_analysis WHERE created_at < date('now', '-7 days')) as old_score """) scores = cursor.fetchone() if scores and scores[0] and scores[1]: change = scores[0] - scores[1] insights.append({ 'title': 'Weekly Trend', 'icon': '📈', 'description': f"ATS scores have {'improved' if change >= 0 else 'decreased'} by {abs(change):.1f}% in the last week", 'trend_class': 'trend-up' if change >= 0 else 'trend-down', 'trend_icon': '↑' if change >= 0 else '↓', 'trend_value': f"{abs(change):.1f}%" }) # Most Common Skills cursor.execute(""" WITH RECURSIVE split(skill, rest) AS ( SELECT '', skills || ',' FROM resume_data WHERE skills IS NOT NULL UNION ALL SELECT substr(rest, 0, instr(rest, ',')), substr(rest, instr(rest, ',') + 1) FROM split WHERE rest <> '' ), cleaned_skills AS ( SELECT TRIM(REPLACE(REPLACE(skill, '[', ''), ']', '')) as skill FROM split WHERE skill <> '' ) SELECT skill, COUNT(*) as count FROM cleaned_skills GROUP BY skill ORDER BY count DESC LIMIT 3 """) top_skills = cursor.fetchall() if top_skills: skills_text = f"Most in-demand skills: Python ({top_skills[0][1]} resumes), Java ({top_skills[1][1]} resumes), Express ({top_skills[2][1]} resumes)" insights.append({ 'title': 'Top Skills', 'icon': '💡', 'description': f"Most in-demand skills: {skills_text}", 'trend_class': 'trend-up', 'trend_icon': '🔝', 'trend_value': f"Top {len(top_skills)}" }) return insights def get_quick_stats(self): """Get quick statistics for the dashboard""" cursor = self.conn.cursor() # Total Resumes cursor.execute("SELECT COUNT(*) FROM resume_data") total_resumes = cursor.fetchone()[0] # Average ATS Score cursor.execute("SELECT AVG(ats_score) FROM resume_analysis") avg_ats = cursor.fetchone()[0] or 0 # High Performing Resumes cursor.execute("SELECT COUNT(*) FROM resume_analysis WHERE ats_score >= 70") high_performing = cursor.fetchone()[0] # Success Rate success_rate = (high_performing / total_resumes * 100) if total_resumes > 0 else 0 return { "Total Resumes": f"{total_resumes:,}", "Avg ATS Score": f"{avg_ats:.1f}%", "High Performing": f"{high_performing:,}", "Success Rate": f"{success_rate:.1f}%" } def create_enhanced_ats_gauge(self, value): """Create an enhanced ATS score gauge chart""" reference = 70 # Target score delta = value - reference fig = go.Figure(go.Indicator( mode="gauge+number+delta", value=value, delta={ 'reference': reference, 'valueformat': '.1f', 'increasing': {'color': '#2ecc71'}, 'decreasing': {'color': '#e74c3c'} }, number={'font': {'size': 40, 'color': 'white'}}, gauge={ 'axis': { 'range': [0, 100], 'tickwidth': 1, 'tickcolor': 'white', 'tickfont': {'color': 'white'} }, 'bar': {'color': '#3498db'}, 'bgcolor': 'rgba(0,0,0,0)', 'borderwidth': 2, 'bordercolor': 'white', 'steps': [ {'range': [0, 40], 'color': '#e74c3c'}, {'range': [40, 70], 'color': '#f1c40f'}, {'range': [70, 100], 'color': '#2ecc71'} ], 'threshold': { 'line': {'color': 'white', 'width': 4}, 'thickness': 0.75, 'value': reference } } )) fig.update_layout( title={ 'text': 'ATS Score Performance', 'font': {'size': 24, 'color': 'white'}, 'y': 0.85 }, paper_bgcolor='rgba(0,0,0,0)', plot_bgcolor='rgba(0,0,0,0)', font={'color': 'white'}, height=350, margin=dict(l=20, r=20, t=80, b=20) ) return fig def create_skill_distribution_chart(self): """Create a skill distribution chart""" categories, counts = self.get_skill_distribution() fig = go.Figure(data=[ go.Bar( x=categories, y=counts, marker_color=self.colors['info'], text=counts, textposition='auto', ) ]) fig.update_layout( title={ 'text': 'Skill Distribution', 'y':0.95, 'x':0.5, 'xanchor': 'center', 'yanchor': 'top' }, height=350, plot_bgcolor='rgba(0,0,0,0)', paper_bgcolor='rgba(0,0,0,0)', font=dict(color=self.colors['text']), margin=dict(l=40, r=40, t=60, b=40), xaxis=dict( showgrid=False, showline=True, linecolor='rgba(255,255,255,0.2)', tickfont=dict(size=12) ), yaxis=dict( showgrid=True, gridcolor='rgba(255,255,255,0.1)', zeroline=False ), bargap=0.3 ) return fig def create_submission_trends_chart(self): """Create a weekly submission trend chart""" dates, submissions = self.get_weekly_trends() fig = go.Figure() fig.add_trace(go.Scatter( x=dates, y=submissions, mode='lines+markers', line=dict(color=self.colors['info'], width=3), marker=dict(size=8, color=self.colors['info']) )) fig.update_layout( title="Weekly Submission Pattern", paper_bgcolor=self.colors['card'], plot_bgcolor=self.colors['card'], font={'color': self.colors['text']}, height=300, margin=dict(l=20, r=20, t=50, b=20) ) fig.update_xaxes(title_text="Day of Week", color=self.colors['text']) fig.update_yaxes(title_text="Number of Submissions", color=self.colors['text']) return fig def create_job_category_chart(self): """Create a success rate by category chart""" categories, rates = self.get_job_category_stats() fig = go.Figure(go.Bar( x=categories, y=rates, marker_color=[self.colors['success'], self.colors['info'], self.colors['warning'], self.colors['purple'], self.colors['secondary']], text=[f"{rate}%" for rate in rates], textposition='auto', )) fig.update_layout( title="Success Rate by Job Category", paper_bgcolor=self.colors['card'], plot_bgcolor=self.colors['card'], font={'color': self.colors['text']}, height=300, margin=dict(l=20, r=20, t=50, b=20) ) fig.update_xaxes(title_text="Job Category", color=self.colors['text']) fig.update_yaxes(title_text="Success Rate (%)", color=self.colors['text']) return fig