from shiny.express import input, render, ui
from shiny import reactive
from shinywidgets import render_widget, output_widget
import plotly.graph_objects as go
import data_loader
import figures as figs
# =============================================================================
# 1. CONFIGURATION
# =============================================================================
# CONFIG OPTION A: HORIZONTAL (Standard Flex Wrapping)
config_horizontal = [
{"id": "submitters_chart", "width": 4, "title": "Total Projects By Submitters", "height": "380px"},
{"id": "ym_chart", "width": 8, "title": "Total Project By YYYY-MM", "height": "380px"},
{"id": "timeline_pie", "width": 4, "title": "Total Projects By Timeline", "height": "360px"},
{"id": "status_pie", "width": 4, "title": "Total Projects By Idea Status","height": "360px"},
{"id": "location_chart", "width": 4, "title": "Total Projects By Location", "height": "360px"}
]
# CONFIG OPTION B: VERTICAL (Independent Columns)
config_vertical = [
{
"width": 8, # LEFT MAIN COLUMN
"charts": [
{"title": "Total Project By YYYY-MM", "id": "ym_chart", "width": 12, "height": "380px"},
{"title": "Total Projects By Submitters", "id": "submitters_chart", "width": 6, "height": "380px"},
{"title": "Total Projects By Location", "id": "location_chart", "width": 6, "height": "380px"},
]
},
{
"width": 4, # RIGHT SIDEBAR COLUMN
"charts": [
{"title": "Idea Status", "id": "status_pie", "width": 12, "height": "300px"},
{"title": "Timeline Overview", "id": "timeline_pie", "width": 12, "height": "300px"},
]
}
]
kpi_config = [
{"label": "TOTAL PROJECTS", "id": "kpi_proj"},
{"label": "TOTAL SUBMITTERS", "id": "kpi_sub"},
{"label": "TOTAL OWNERS", "id": "kpi_own"},
{"label": "TOTAL APPROVERS", "id": "kpi_app"},
]
# =============================================================================
# 2. HELPER FUNCTIONS (Returning UI Objects)
# =============================================================================
def kpi_card(label, output_id):
"""
Creates a UI card for a KPI.
Note: We use ui.output_ui(output_id) so the value can be updated reactively.
"""
return ui.div(
ui.div(
ui.output_ui(output_id, style="font-size: 28px; font-weight: 700; color: #c23b5a;"),
ui.div(label, style="color: #555;"),
class_="card-body"
),
class_="card shadow-sm border-0",
style="border-radius: 14px;"
)
def make_card(item, width_basis=12):
"""
Creates a wrapper div containing a Card, Header, and Widget.
Uses Flexbox styles to ensure the chart fills the card height.
"""
width_pct = (item['width'] / width_basis) * 100
return ui.div(
ui.div(
# 1. Card Header
ui.div(item['title'], class_="card-header", style="font-weight: 600;"),
# 2. Card Body with Flexbox properties
ui.div(
output_widget(item['id'], height="100%", width="100%"),
class_="card-body",
# FLEX MAGIC: flex: 1 -> Fill remaining space
style="padding: 10px; flex: 1 1 auto; min-height: 0; overflow: hidden;"
),
class_="card shadow-sm",
style=f"border-radius: 14px; height: {item['height']}; display: flex; flex-direction: column;"
),
style=f"width: {width_pct}%; padding: 10px; box-sizing: border-box;"
)
def build_horizontal_layout():
return ui.div(
*[make_card(item, width_basis=12) for item in config_horizontal],
style="display: flex; flex-wrap: wrap; margin: -10px;"
)
def build_vertical_layout():
cols = []
for col_cfg in config_vertical:
stack_content = [make_card(chart, width_basis=12) for chart in col_cfg['charts']]
cols.append(
ui.column(
col_cfg['width'],
ui.div(
*stack_content,
style="display: flex; flex-wrap: wrap; margin: -10px;"
)
)
)
return ui.row(*cols)
# =============================================================================
# 3. APP LAYOUT (Linear Execution)
# =============================================================================
# Add global styles
ui.tags.style("body { background-color: #f5f6fa; }")
with ui.div(style="padding: 18px; min-height: 100vh;"):
# --- Header ---
with ui.row(class_="gy-2 mb-3 align-items-center"):
with ui.column(10):
ui.h3("PLANISWARE PROJECT OVERVIEW", style="margin: 0; font-weight: 700;")
ui.div("Overview of Campaigns, Status, and Project Initiatives", style="color: #666;")
with ui.column(2):
ui.input_action_button("btn_refresh", "Refresh Data", class_="btn-primary w-100")
ui.hr()
# --- KPI Grid ---
ui.div(
*[kpi_card(k['label'], k['id']) for k in kpi_config],
style="display: grid; grid-template-columns: repeat(4, 1fr); gap: 20px; margin-bottom: 20px; justify-content: center;"
)
# --- Charts ---
# Switch layouts here by uncommenting
build_horizontal_layout()
# build_vertical_layout()
# =============================================================================
# 4. REACTIVE LOGIC
# =============================================================================
@reactive.calc
def get_data():
input.btn_refresh() # Trigger update on button click
return data_loader.load_data()
@reactive.calc
def get_kpis_data():
df = get_data()
return figs.get_kpis(df)
# --- Render KPIs ---
@render.ui
def kpi_proj():
return get_kpis_data().get("projects", "0")
@render.ui
def kpi_sub():
return get_kpis_data().get("submitters", "0")
@render.ui
def kpi_own():
return get_kpis_data().get("owners", "0")
@render.ui
def kpi_app():
return get_kpis_data().get("approvers", "0")
# --- Render Charts ---
@render_widget
def submitters_chart():
return figs.create_bar_submitters(get_data())
@render_widget
def ym_chart():
return figs.create_bar_timeline(get_data())
@render_widget
def timeline_pie():
return figs.create_pie_chart(get_data(), figs.COL_TIMELINE)
@render_widget
def status_pie():
return figs.create_pie_chart(get_data(), figs.COL_STATUS)
@render_widget
def location_chart():
return figs.create_bar_location(get_data())