| import plotly.graph_objects as go |
| import plotly.express as px |
| import pandas as pd |
|
|
| def generate_budget_utilization_gauge_chart(total_budget, total_expense): |
| """Generate a gauge chart comparing budget vs. total expense.""" |
|
|
| gauge_steps = [ |
| {'range': [0, total_budget * 0.7], 'color': '#d4f1dd'}, |
| {'range': [total_budget * 0.7, total_budget * 0.9], 'color': '#fff2cc'}, |
| {'range': [total_budget * 0.9, total_budget * 1.2], 'color': '#f8d7da'} |
| ] |
|
|
| gauge_threshold = { |
| 'line': {'color': 'red', 'width': 4}, |
| 'thickness': 0.75, |
| 'value': total_budget |
| } |
|
|
| gauge = go.Indicator( |
| mode="gauge+number+delta", |
| value=total_expense, |
| title={'text': "ค่าใช้จ่ายทั้งหมด", 'font': {'size': 24}}, |
| delta={ |
| 'reference': total_budget, |
| 'increasing': {'color': 'red', 'symbol': "\u25B2"}, |
| 'decreasing': {'color': 'green', 'symbol': "\u25BC"} |
| }, |
| gauge={ |
| 'axis': {'range': [None, total_budget * 1.2], 'tickwidth': 1}, |
| 'bar': {'color': '#1f77b4'}, |
| 'bgcolor': 'white', |
| 'borderwidth': 2, |
| 'bordercolor': 'gray', |
| 'steps': gauge_steps, |
| 'threshold': gauge_threshold |
| } |
| ) |
|
|
| fig = go.Figure(gauge) |
|
|
| fig.update_layout( |
| paper_bgcolor='rgba(0,0,0,0)', |
| plot_bgcolor='rgba(0,0,0,0)', |
| margin=dict(t=50, r=25, l=25, b=25), |
| height=300 |
| ) |
|
|
| return fig |
|
|
| def generate_deliverable_budget_vs_expense_bar_chart(budget_df, expense_df): |
| """Generate a grouped bar chart comparing budget vs. actual expense per deliverable.""" |
|
|
| |
| budget_df['total_budget'] = budget_df[['wage', 'materials', 'tools_equipment', 'services', 'misc']].sum(axis=1) |
| budget_df['title'] = budget_df.get('title', pd.Series(["Unnamed Deliverable"] * len(budget_df))) |
|
|
| |
| budget_summary = budget_df[['title', 'total_budget']].rename(columns={'title': 'deliverable_title'}) |
|
|
| |
| expense_summary = ( |
| expense_df.groupby('associated_deliverable_id')['total_payment_amount'] |
| .sum() |
| .reset_index() |
| ) |
|
|
| |
| deliverable_titles = budget_df[['deliverable_id', 'title']].drop_duplicates() |
| expense_summary = ( |
| pd.merge( |
| expense_summary, |
| deliverable_titles, |
| left_on='associated_deliverable_id', |
| right_on='deliverable_id', |
| how='left' |
| ) |
| .rename(columns={ |
| 'title': 'deliverable_title', |
| 'total_payment_amount': 'spending' |
| }) |
| ) |
|
|
| |
| merged_df = pd.merge( |
| budget_summary, |
| expense_summary[['deliverable_title', 'spending']], |
| on='deliverable_title', |
| how='left' |
| ).fillna({'spending': 0}) |
|
|
| |
| fig = go.Figure() |
|
|
| fig.add_trace(go.Bar( |
| x=merged_df['deliverable_title'], |
| y=merged_df['total_budget'], |
| name='งบประมาณ', |
| marker_color='green' |
| )) |
|
|
| fig.add_trace(go.Bar( |
| x=merged_df['deliverable_title'], |
| y=merged_df['spending'], |
| name='ค่าใช้จ่าย', |
| marker_color='red' |
| )) |
|
|
| fig.update_layout( |
| title='แผนภูมิเปรียบเทียบงบประมาณและค่าใช้จ่าย', |
| barmode='group', |
| xaxis_title='การส่งมอบ', |
| yaxis_title='จำนวนเงิน (บาท)', |
| legend=dict(orientation='h', y=-0.2), |
| height=400, |
| paper_bgcolor='rgba(0,0,0,0)', |
| plot_bgcolor='rgba(0,0,0,0)', |
| margin=dict(t=50, r=25, l=25, b=25) |
| ) |
|
|
| return fig |
|
|
| def generate_spending_distribution_pie_chart(expense_df, budget_df): |
| """Generate a pie chart showing the distribution of spending by deliverables.""" |
|
|
| |
| spending_summary = ( |
| expense_df.groupby('associated_deliverable_id')['total_payment_amount'] |
| .sum() |
| .reset_index() |
| ) |
|
|
| |
| deliverable_titles = budget_df[['deliverable_id', 'title']].drop_duplicates() |
| spending_summary = ( |
| pd.merge( |
| spending_summary, |
| deliverable_titles, |
| left_on='associated_deliverable_id', |
| right_on='deliverable_id', |
| how='left' |
| ) |
| .rename(columns={ |
| 'title': 'deliverable_title', |
| 'total_payment_amount': 'amount' |
| }) |
| ) |
|
|
| |
| spending_summary['deliverable_title'] = spending_summary['deliverable_title'].fillna("Unnamed Deliverable") |
|
|
| |
| fig = px.pie( |
| spending_summary, |
| values='amount', |
| names='deliverable_title', |
| title='ยอดค่าใช้จ่ายของการส่งมอบ', |
| color_discrete_sequence=px.colors.qualitative.Set2 |
| ) |
|
|
| fig.update_traces( |
| textinfo='label+value', |
| hoverinfo='label+percent', |
| textposition='inside', |
| insidetextorientation='radial' |
| ) |
|
|
| fig.update_layout( |
| paper_bgcolor='rgba(0,0,0,0)', |
| plot_bgcolor='rgba(0,0,0,0)', |
| margin=dict(t=50, r=25, l=25, b=25), |
| height=400 |
| ) |
|
|
| return fig |
|
|
| def generate_daily_spending_bar_chart(expense_df): |
| """Generate a bar chart showing daily spending amounts.""" |
|
|
| |
| expense_df['transaction_date'] = pd.to_datetime(expense_df['transaction_date']).dt.date |
| daily_summary = ( |
| expense_df.groupby('transaction_date')['total_payment_amount'] |
| .sum() |
| .reset_index() |
| .rename(columns={ |
| 'transaction_date': 'date', |
| 'total_payment_amount': 'amount' |
| }) |
| .sort_values('date') |
| ) |
|
|
| |
| fig = go.Figure() |
|
|
| fig.add_trace(go.Bar( |
| x=daily_summary['date'], |
| y=daily_summary['amount'], |
| name='ค่าใช้จ่ายในแต่ละวัน', |
| marker_color='#1f77b4' |
| )) |
|
|
| fig.update_layout( |
| title='ค่าใช้จ่ายในแต่ละวัน', |
| xaxis_title='วันที่', |
| yaxis_title='จำนวนเงิน (บาท)', |
| paper_bgcolor='rgba(0,0,0,0)', |
| plot_bgcolor='rgba(0,0,0,0)', |
| margin=dict(t=50, r=25, l=25, b=25), |
| height=400 |
| ) |
|
|
| return fig |
|
|
| def generate_cumulative_spending_line_chart(expense_df): |
| """Generate a line chart showing cumulative spending over time.""" |
|
|
| |
| expense_df['transaction_date'] = pd.to_datetime(expense_df['transaction_date']).dt.date |
| daily_summary = ( |
| expense_df.groupby('transaction_date')['total_payment_amount'] |
| .sum() |
| .reset_index() |
| .rename(columns={'transaction_date': 'date', 'total_payment_amount': 'amount'}) |
| .sort_values('date') |
| ) |
|
|
| |
| daily_summary['cumulative_amount'] = daily_summary['amount'].cumsum() |
|
|
| |
| fig = go.Figure() |
|
|
| fig.add_trace(go.Scatter( |
| x=daily_summary['date'], |
| y=daily_summary['cumulative_amount'], |
| mode='lines+markers', |
| name='Cumulative Spending', |
| marker_color='#ff7f0e' |
| )) |
|
|
| fig.update_layout( |
| title='ยอดค่าใช้จ่ายสะสมตามช่วงเวลา', |
| xaxis_title='วันที่', |
| yaxis_title='ยอดค่าใช้จ่ายสะสม (บาท)', |
| paper_bgcolor='rgba(0,0,0,0)', |
| plot_bgcolor='rgba(0,0,0,0)', |
| margin=dict(t=50, r=25, l=25, b=25), |
| height=400 |
| ) |
|
|
| return fig |
|
|
| def generate_risk_level_distribution_pie_chart(deliverable_df): |
| """Generate a pie chart showing the distribution of deliverables by risk level.""" |
|
|
| risk_summary = ( |
| deliverable_df['risk_level'] |
| .value_counts() |
| .reset_index(name='count') |
| .rename(columns={'index': 'risk_level'}) |
| ) |
|
|
| risk_color_map = { |
| 'green': '#28a745', |
| 'yellow': '#ffc107', |
| 'red': '#dc3545', |
| 'unknown': '#6c757d' |
| } |
|
|
| fig = px.pie( |
| risk_summary, |
| values='count', |
| names='risk_level', |
| title='แผนภูมิระดับความเสี่ยง', |
| color='risk_level', |
| color_discrete_map=risk_color_map |
| ) |
|
|
| fig.update_traces( |
| textinfo='label+percent', |
| hoverinfo='label+value+percent', |
| textposition='inside' |
| ) |
|
|
| fig.update_layout( |
| paper_bgcolor='rgba(0,0,0,0)', |
| plot_bgcolor='rgba(0,0,0,0)', |
| margin=dict(t=50, r=25, l=25, b=25), |
| height=400 |
| ) |
|
|
| return fig |
|
|
| def generate_deliverable_timeline_gantt_chart(deliverable_df): |
| """Generate a Gantt chart showing deliverables over time, colored by risk level.""" |
|
|
| df = deliverable_df.copy() |
|
|
| |
| df['start_date'] = pd.to_datetime(df['start_date']) |
| df['end_date'] = pd.to_datetime(df['end_date']) |
|
|
| |
| df['title'] = df['title'].fillna("Unnamed Deliverable") |
| df['risk_level'] = df['risk_level'].fillna("unknown") |
|
|
| |
| df = df.rename(columns={ |
| 'title': 'หัวข้อ', |
| 'risk_level': 'ระดับความเสี่ยง' |
| }) |
|
|
| risk_color_map = { |
| 'green': '#28a745', |
| 'yellow': '#ffc107', |
| 'red': '#dc3545', |
| 'unknown': '#6c757d' |
| } |
|
|
| |
| fig = px.timeline( |
| df, |
| x_start='start_date', |
| x_end='end_date', |
| y='หัวข้อ', |
| color='ระดับความเสี่ยง', |
| title='ไทม์ไลน์การส่งมอบ (Gantt Chart)', |
| hover_data=['status', 'risk_level_rationale'], |
| color_discrete_map=risk_color_map |
| ) |
|
|
| |
| fig.update_yaxes(autorange='reversed') |
|
|
| |
| fig.update_layout( |
| paper_bgcolor='rgba(0,0,0,0)', |
| plot_bgcolor='rgba(0,0,0,0)', |
| margin=dict(t=50, r=25, l=25, b=25), |
| height=500 |
| ) |
|
|
| return fig |
|
|
| def render_deliverable_summary_cards(deliverable_df): |
| """Render HTML cards for deliverables with expandable risk rationale using <details>/<summary> (JS-free, Gradio-safe).""" |
|
|
| def format_date(date_str): |
| if pd.isna(date_str): |
| return "-" |
| return pd.to_datetime(date_str).strftime("%d %b %Y") |
|
|
| risk_level_colors = { |
| "green": "#28a745", |
| "yellow": "#ffc107", |
| "red": "#dc3545", |
| "unknown": "#6c757d" |
| } |
|
|
| status_colors = { |
| "ongoing": "#17a2b8", |
| "done": "#007bff", |
| } |
|
|
| cards_html = """ |
| <div style='display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 20px;'> |
| """ |
|
|
| for _, row in deliverable_df.iterrows(): |
| title = row.get('title', 'Untitled Deliverable') |
| deliverable_id = row.get('deliverable_id', '-') |
| status = str(row.get('status', 'N/A')).lower() |
| risk_level = str(row.get('risk_level', 'unknown')).lower() |
| rationale = row.get('risk_level_rationale', 'No rationale provided') |
|
|
| status_color = status_colors.get(status, "#6c757d") |
| risk_color = risk_level_colors.get(risk_level, "#6c757d") |
|
|
| status_map = { |
| 'done': 'เสร็จสิ้น', |
| 'ongoing': 'กำลังดำเนินการ' |
| } |
|
|
| thai_status = status_map.get(status, 'ไม่ทราบสถานะ') |
|
|
| start_date = format_date(row.get('start_date')) |
| end_date = format_date(row.get('end_date')) |
|
|
| cards_html += f""" |
| <div style='border: 1px solid #dee2e6; border-radius: 10px; padding: 15px; background-color: #ffffff; box-shadow: 0 2px 6px rgba(0,0,0,0.05);'> |
| <div style='font-weight: bold; font-size: 1.1em; margin-bottom: 5px;'> |
| {title} |
| <div style='font-size: 0.85em; color: #6c757d;'>ID: {deliverable_id}</div> |
| </div> |
| <div style='margin: 8px 0;'> |
| <span style='padding: 3px 8px; border-radius: 4px; font-size: 0.75em; background-color: {status_color}; color: white; margin-right: 5px;'> |
| {thai_status} |
| </span> |
| <span style='padding: 3px 8px; border-radius: 4px; font-size: 0.75em; background-color: {risk_color}; color: white;'> |
| ระดับความเสี่ยง: {risk_level.capitalize()} |
| </span> |
| </div> |
| <div style='font-size: 0.9em; color: #6c757d;'> |
| <strong>เริ่มต้น:</strong> {start_date}<br> |
| <strong>สิ้นสุด:</strong> {end_date} |
| </div> |
| <details style='margin-top: 12px; font-size: 0.85em; color: #495057;'> |
| <summary style='cursor: pointer; color: #007bff;'>แสดงเหตุผลของความเสี่ยง</summary> |
| <div style='margin-top: 6px; padding: 8px; background-color: #f8f9fa; border-radius: 5px; border: 1px solid #dee2e6;'> |
| <strong>เหตุผลของความเสี่ยง:</strong><br>{rationale} |
| </div> |
| </details> |
| </div> |
| """ |
|
|
| cards_html += "</div>" |
| return cards_html |