Data Dashboard Two-Stage Separation Architecture: Design Time vs Runtime Decoupling
โข6 min read
๐ฏ Core Question of This Chapter
What is the core contradiction of Dashboards?
Dimension
Problems with Traditional Approaches
Our Solution
Performance
LLM called on every refresh, 3-5 second latency
Only SQL executed at runtime, completes within 200ms
Cost
100 refreshes = 100 LLM API calls
LLM called only once at design time, zero cost afterwards
Maintainability
Hardcoded UI layout, requirement changes need code changes
Configuration-driven, modify JSON to take effect
Flexibility
Fixed templates, cannot customize layout
Grid system + drag-and-drop interaction
๐ Architecture Overview: Two-Phase Separation Model
Data Dashboard Two-Phase Separation Architecture
Core Idea
LLM called at design time to write configuration, only SQL executed at runtime
Why Is This Design Revolutionary?
Analogy:
Think of it like prepared meals vs. cooking on the spot:
Design time = chef carefully prepares a dish (calls LLM to generate configuration)
Runtime = just heat it up in the microwave (read configuration + execute SQL)
Fatal Flaw of Traditional Approaches:
1 2 3 4 5 6 7 8 9 10
# โ Traditional approach: LLM called every time asyncdefget_dashboard_data(dashboard_id: int): dashboard = await db.get(dashboard_id)
# SQL must be regenerated for every widget! for widget in dashboard.widgets: sql = await llm.generate_sql(widget.description) # ๐ธ Tokens consumed each time data = await db.execute(sql) # โณ Waiting for LLM response
return data
Our Approach:
1 2 3 4 5 6 7 8 9 10 11 12 13
# โ Our approach: No LLM calls at runtime asyncdefget_dashboard_data(dashboard_id: int): # Step 1: Load pre-generated configuration directly from the database config = await load_dashboard_config(dashboard_id) # ~10ms
# Step 2: Execute all widget SQLs concurrently (no LLM calls) tasks = [ execute_query(widget.sql_query) for widget in config.widgets ] results = await asyncio.gather(*tasks) # Concurrent execution, ~200ms
returndict(zip([w.idfor w in config.widgets], results))
๐จ I. Phase One: Design Time โ LLM-Driven Configuration Generation
asyncdefcreate_from_natural_language( db: AsyncSession, datasource_id: int, description: str, name: Optional[str] = None, ) -> Dashboard: """ Create a Dashboard from a natural language description
Args: datasource_id: Data source ID description: User description, e.g., "Create a sales dashboard for me..." name: Dashboard name (optional, LLM can generate automatically) """ # Step 1: Get semantic model (injected into the Prompt) semantic_model = await get_semantic_model(db, datasource_id)
# Step 2: Build the Prompt prompt = f"""You are a professional data visualization expert. Based on the user's requirements and database structure, generate a complete Dashboard configuration object.
## Output Requirements Please return a configuration object that conforms to the following JSON Schema (do not include any other text): {DASHBOARD_CONFIG_SCHEMA}"""
from pydantic import BaseModel, Field from typing importList, Optional, Any
classWidgetConfig(BaseModel): """Configuration for a single Widget""" type: str = Field(..., description="Component type: kpi_card/line_chart/bar_chart/pie_chart/table") x: int = Field(0, description="Grid X coordinate (0-11)") y: int = Field(0, description="Grid Y coordinate") width: int = Field(6, description="Grid width (1-12)") height: int = Field(2, description="Grid height") title: Optional[str] = Field(None, description="Title") sql: str = Field(..., description="Precompiled SQL query statement") format: Optional[str] = Field("auto", description="Formatting method: currency/percent/number/auto") chart_options: Optional[dict] = Field(None, description="ECharts custom options")
classLayoutConfig(BaseModel): """Grid layout configuration""" cols: int = Field(12, description="Number of columns") rows: int = Field(8, description="Number of rows") gap: int = Field(16, description="Gap (pixels)")
1.3 Practical Example: From Natural Language to Complete Configuration
User Input:
1 2 3 4
Create a sales data dashboard for me, including: โข Top left: KPI card showing today's sales amount โข Top right: Line chart showing the sales trend for the last 7 days โข Bottom: Bar chart showing sales amount by category (TOP10)
Configuration Generated by LLM (stored in database):
Each Widget stores a complete SQL query (shown with line breaks for readability)
๐ก Key Observations
Each Widget has an independent, complete SQL query โ this is the prerequisite for operating without LLM at runtime
Coordinates use the Grid system (x/y/w/h) โ supports drag-and-drop to adjust position and size
chart_options is optional โ allows advanced users to fine-tune chart styling
โก II. Phase Two: Runtime โ Efficient Data Retrieval and Rendering
@router.post("/{dashboard_id}/data") asyncdefget_dashboard_data( dashboard_id: int, db: AsyncSession = Depends(get_db), ): """ Get real-time data for all Widgets in a Dashboard
Performance optimizations: 1. Use asyncio.gather() to execute multiple SQL queries concurrently 2. No LLM calls (SQL is precompiled at design time) 3. Result caching (optional, see Chapter 8)
Total time โ max(single SQL time), not sum() """ # Load Widgets widgets_result = await db.execute( select(DashboardWidget).where(DashboardWidget.dashboard_id == dashboard_id) ) widgets = list(widgets_result.scalars().all())
ifnot widgets: return {}
asyncdeffetch_widget_data(widget: DashboardWidget) -> tuple[int, list]: """Query data for a single Widget""" try: data = await execute_query(widget.sql_query, datasource_id=None) return widget.id, data except Exception as e: logger.warning(f"Widget {widget.id} query failed: {e}") return widget.id, [{"error": str(e)}]
# ๐ฅ Core: execute all Widget SQLs concurrently tasks = [fetch_widget_data(w) for w in widgets] results = await asyncio.gather(*tasks)
# Assemble into {widget_id: data} mapping return {wid: data for wid, data in results}
๐ฏ Why Is Concurrency So Important?
Assume there are 5 Widgets, each SQL execution takes 100ms:
// Load configuration onMounted(async () => { const res = await fetch(`/api/dashboards/${dashboardId.value}`) const data = await res.json() dashboard.value = data widgets.value = data.widgets
// Auto-load data await refreshData()
// Set up periodic refresh (default 60 seconds) setInterval(refreshData, 60000) })
// Refresh data (only re-fetch data, not configuration) async function refreshData() { loadingWidgets.value = new Set(widgets.value.map((w: any) => w.id))
try { const res = await fetch(`/api/dashboards/${dashboardId.value}/data`, { method: 'POST', }) const data = await res.json() widgetData.value = data lastRefresh.value = new Date().toLocaleTimeString() } finally { loadingWidgets.value.clear() } } </script>
The next article will dive into the implementation details of the frontend drag-and-drop interaction system โ HTML5 Drag API, Grid layout, mousedown move/resize (collision detection as planned capability)!