Skip to content

Latest commit

 

History

History
534 lines (431 loc) · 17 KB

File metadata and controls

534 lines (431 loc) · 17 KB

ADR-004: Lightweight Data Visualization Strategy

Status

Proposed

Context

Data visualization is critical for analytics comprehension, but presents unique challenges in MCP/LobeHub environment:

Current Challenges

Challenge Impact Current Workaround
Token Cost Base64 images consume 30% more tokens than text No visualization
Library Weight Matplotlib + dependencies = 200MB+ Skip visualization
Generation Time 2-5s for complex charts Text-only output
Context Pollution Large images reduce conversation capacity External links
Quality vs Speed High-quality plots are slow Low-quality fallback

Requirements

From user feedback and analytics workflows:

  1. Fast Generation: <500ms for standard charts
  2. Low Token Overhead: Max 500 tokens for chart reference
  3. High Quality: Publication-ready output when needed
  4. Lightweight: Minimal Docker image size increase
  5. Flexible: Support common chart types (bar, line, scatter, heatmap)

Decision

Implement tiered visualization strategy using lightweight libraries with intelligent fallback and token-optimized output formats.

Technology Stack

┌─────────────────────────────────────────────────────────────────────────────┐
│                    Visualization Architecture                                │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│  TIER 1: ULTRALIGHT (Default)                                               │
│  ┌─────────────────────────────────────────────────────────────────────┐   │
│  │  Library: plotext (pure Python, 0 deps)                             │   │
│  │  Output: ASCII/Unicode charts                                       │   │
│  │  Size: ~50KB         Time: <50ms         Tokens: ~100-300          │   │
│  │  Use: Quick insights, terminal-friendly, low-context                │   │
│  └─────────────────────────────────────────────────────────────────────┘   │
│                                   │                                         │
│                                   ▼                                         │
│  TIER 2: LIGHTWEIGHT (Standard)                                             │
│  ┌─────────────────────────────────────────────────────────────────────┐   │
│  │  Library: matplotlib (Agg backend only)                             │   │
│  │  Output: PNG base64                                                 │   │
│  │  Size: ~15MB         Time: <200ms        Tokens: ~400-800          │   │
│  │  Use: Standard visualizations, reports, sharing                     │   │
│  └─────────────────────────────────────────────────────────────────────┘   │
│                                   │                                         │
│                                   ▼                                         │
│  TIER 3: ADVANCED (On-Demand)                                               │
│  ┌─────────────────────────────────────────────────────────────────────┐   │
│  │  Library: plotly (static export) + kaleido                          │   │
│  │  Output: SVG or high-res PNG                                        │   │
│  │  Size: ~50MB         Time: <500ms        Tokens: External link     │   │
│  │  Use: Interactive dashboards, drill-down, publication               │   │
│  └─────────────────────────────────────────────────────────────────────┘   │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘

Library Selection Rationale

1. Plotext (Tier 1 - Ultralight)

Why:

  • Pure Python, zero dependencies
  • 50KB vs 200MB for matplotlib
  • Generates ASCII/Unicode plots
  • Extremely fast (<50ms)
  • Zero token overhead for simple charts

Best For:

  • Quick data exploration
  • Terminal/CLI output
  • Low-context environments
  • Bar charts, line plots, histograms

Example:

import plotext as plt

plt.bar(["A", "B", "C"], [100, 150, 80])
plt.title("Sales by Category")
chart = plt.build()  # Returns string
print(chart)

Output:

Sales by Category

Category | Sales
----------------
   A     | ████████████ 100
   B     | ██████████████████ 150
   C     | █████████ 80

Token Cost: ~100 tokens (text)

2. Matplotlib (Tier 2 - Lightweight)

Why:

  • Industry standard
  • Agg backend (no GUI deps)
  • Small footprint with careful install
  • Wide format support

Optimization Strategy:

# Minimal matplotlib installation
RUN pip install matplotlib --no-deps \
    && pip install numpy pillow kiwisolver pyparsing cycler fonttools packaging

Size Reduction:

  • Full install: 200MB+
  • Optimized: ~15MB
  • Removed: GUI backends, sample data, tests

Token Optimization:

import matplotlib.pyplot as plt
import io
import base64

def generate_chart_optimized(data, width=400, height=300, dpi=80):
    """Generate token-efficient visualization"""
    plt.figure(figsize=(width/100, height/100), dpi=dpi)
    
    # Plot data
    plt.bar(data['categories'], data['values'])
    plt.title(data.get('title', 'Chart'))
    
    # Save to buffer with compression
    buf = io.BytesIO()
    plt.savefig(buf, format='png', 
                dpi=dpi, 
                bbox_inches='tight',
                pad_inches=0.1,
                optimize=True)  # PNG optimization
    plt.close()
    
    # Convert to base64
    buf.seek(0)
    img_base64 = base64.b64encode(buf.read()).decode()
    
    return {
        "type": "image",
        "format": "png",
        "base64": img_base64,
        "dimensions": {"width": width, "height": height}
    }

Token Cost: ~400-800 tokens (base64 image)

3. Plotly + Kaleido (Tier 3 - Advanced)

Why:

  • Interactive HTML output
  • SVG support (vector, scalable)
  • Best quality for complex charts
  • Export to file (external link)

Usage Pattern:

  • Save to workspace file
  • Return file path, not base64
  • LobeHub renders via URL

Token Cost: ~50 tokens (file path reference)

Auto-Tier Selection

def select_tier(query_context: dict) -> str:
    """
    Automatically select visualization tier based on context
    """
    available_tokens = query_context.get('available_tokens', 2000)
    urgency = query_context.get('urgency', 'normal')
    quality_required = query_context.get('quality', 'standard')
    
    # Critical context pressure → ASCII
    if available_tokens < 500 or urgency == 'fast':
        return 'ultralight'
    
    # Standard use case → PNG
    if available_tokens < 2000 and quality_required != 'high':
        return 'lightweight'
    
    # High quality, interactive → Plotly file
    return 'advanced'

Token-Efficient Chart Formats

Format 1: ASCII/Unicode (Tier 1)

{
  "visualization": {
    "tier": "ultralight",
    "type": "bar_chart",
    "format": "text",
    "content": "Category | Sales\nA | ████ 100\nB | ███████ 150",
    "tokens_used": 85
  }
}

Format 2: Compressed Base64 (Tier 2)

{
  "visualization": {
    "tier": "lightweight", 
    "type": "bar_chart",
    "format": "base64_png",
    "content": "iVBORw0KGgoAAAANSUhEUgAA...",
    "dimensions": {"width": 400, "height": 300},
    "tokens_used": 650,
    "compression": "png_optimized"
  }
}

Format 3: External Reference (Tier 3)

{
  "visualization": {
    "tier": "advanced",
    "type": "interactive_chart",
    "format": "file_reference",
    "file_path": "/workspace/charts/sales_chart.html",
    "file_size": "45KB",
    "tokens_used": 45,
    "interactive": true
  }
}

Compression Strategies

1. Image Size Optimization

CHART_PRESETS = {
    "thumbnail": {"width": 200, "height": 150, "dpi": 60},
    "compact": {"width": 400, "height": 300, "dpi": 80},
    "standard": {"width": 600, "height": 400, "dpi": 100},
    "high_res": {"width": 1200, "height": 800, "dpi": 150}
}

def get_optimal_preset(available_tokens: int):
    if available_tokens < 1000:
        return CHART_PRESETS["compact"]
    elif available_tokens < 2000:
        return CHART_PRESETS["standard"]
    return CHART_PRESETS["high_res"]

2. Color Palette Optimization

# Limited palette for smaller PNGs
EFFICIENT_PALETTE = {
    "primary": "#1f77b4",    # Blue
    "secondary": "#ff7f0e",  # Orange
    "tertiary": "#2ca02c",   # Green
    "quaternary": "#d62728", # Red
    "background": "#ffffff",
    "grid": "#e0e0e0"
}

3. Metadata Stripping

def strip_metadata(image_bytes: bytes) -> bytes:
    """Remove EXIF and metadata to reduce size"""
    from PIL import Image
    import io
    
    img = Image.open(io.BytesIO(image_bytes))
    
    # Strip metadata
    data = list(img.getdata())
    clean_img = Image.new(img.mode, img.size)
    clean_img.putdata(data)
    
    buf = io.BytesIO()
    clean_img.save(buf, format='PNG', optimize=True)
    return buf.getvalue()

Supported Chart Types by Tier

Chart Type Tier 1 (ASCII) Tier 2 (PNG) Tier 3 (Interactive)
Bar Chart ✅ Full ✅ Full ✅ Full
Line Chart ✅ Full ✅ Full ✅ Full
Scatter ⚠️ Limited ✅ Full ✅ Full
Histogram ✅ Full ✅ Full ✅ Full
Pie Chart ✅ Full ✅ Full ✅ Full
Heatmap ⚠️ Simplified ✅ Full ✅ Full
Box Plot ✅ Full ✅ Full
Violin Plot ✅ Full ✅ Full
3D Plots ✅ Limited ✅ Full
Interactive ✅ Full

Implementation Architecture

class VisualizationManager:
    def __init__(self):
        self.tiers = {
            'ultralight': PlotextRenderer(),
            'lightweight': MatplotlibRenderer(),
            'advanced': PlotlyRenderer()
        }
        self.cache = VisualizationCache()
    
    async def render(self, data, config, context_budget):
        # Select tier based on budget
        tier_name = select_tier(context_budget)
        renderer = self.tiers[tier_name]
        
        # Check cache
        cache_key = generate_cache_key(data, config, tier_name)
        cached = await self.cache.get(cache_key)
        if cached:
            return cached
        
        # Generate visualization
        result = await renderer.render(data, config)
        
        # Cache result
        await self.cache.set(cache_key, result)
        
        return result

MCP Tool Integration

{
  "name": "analytics/visualize",
  "description": "Generate visualization for analytical data",
  "inputSchema": {
    "type": "object",
    "properties": {
      "data_source": {
        "type": "string",
        "description": "Query ID or file path"
      },
      "chart_type": {
        "type": "string",
        "enum": ["bar", "line", "scatter", "histogram", "heatmap", "pie"]
      },
      "x_column": {"type": "string"},
      "y_column": {"type": "string"},
      "tier": {
        "type": "string",
        "enum": ["auto", "ultralight", "lightweight", "advanced"],
        "default": "auto"
      },
      "color_column": {"type": "string"},
      "title": {"type": "string"},
      "width": {"type": "integer", "default": 600},
      "height": {"type": "integer", "default": 400}
    },
    "required": ["data_source", "chart_type"]
  }
}

Context Budget Integration

// In LobeHub agent logic
async function handleVisualizationRequest(userQuery: string, currentContext: number) {
  const availableTokens = CONTEXT_WINDOW - currentContext;
  
  // Request visualization with budget
  const result = await mcp.visualize({
    data_source: 'query_123',
    chart_type: 'bar',
    tier: 'auto',  // Will select based on available_tokens
    context_budget: availableTokens
  });
  
  // Result includes token count
  if (result.tokens_used > availableTokens * 0.5) {
    // Fall back to lighter tier
    return await mcp.visualize({
      ...params,
      tier: 'ultralight'
    });
  }
  
  return result;
}

Caching Strategy

class VisualizationCache:
    def __init__(self):
        self.l1 = {}  # In-memory: chart string/base64
        self.l2 = {}  # File paths for external charts
    
    def get_cache_key(self, data_hash, config_hash, tier):
        return f"{data_hash}:{config_hash}:{tier}"
    
    async def get(self, key):
        # Try L1
        if key in self.l1:
            return self.l1[key]
        
        # Try L2
        if key in self.l2:
            return {"file_path": self.l2[key]}
        
        return None

Consequences

Positive

  1. Flexible Token Usage: 100-800 tokens vs 2000+ for standard matplotlib
  2. Fast Generation: <200ms for 90% of charts
  3. Quality Options: From quick ASCII to publication-ready
  4. Minimal Dependencies: Core functionality with 15MB vs 200MB
  5. Graceful Degradation: Falls back to text when context is limited
  6. Cacheable: All tiers support efficient caching

Negative

  1. Three Libraries to Maintain: plotext, matplotlib, plotly
  2. Tier Selection Complexity: Auto-selection may not always be optimal
  3. ASCII Limitations: Some chart types not available in Tier 1
  4. Image Quality Trade-offs: Lower token = lower resolution

Neutral

  1. Learning Curve: Users need to understand tier differences
  2. Configuration: More options to choose from

Docker Image Impact

Component Size Cumulative
Base Python 50MB 50MB
Polars/DuckDB 80MB 130MB
plotext 0.05MB 130.05MB
matplotlib (optimized) 15MB 145MB
plotly + kaleido 35MB 180MB
Total ~180MB

vs Full matplotlib: ~250MB+

Implementation Roadmap

Phase 1: Tier 1 (MVP)

  • Integrate plotext
  • Basic chart types (bar, line, histogram)
  • Auto-tier selection
  • ASCII output format

Phase 2: Tier 2 (Standard)

  • Optimize matplotlib installation
  • PNG generation with compression
  • Base64 encoding
  • Size presets

Phase 3: Tier 3 (Advanced)

  • Plotly integration
  • HTML/SVG export
  • Interactive features
  • File-based output

Phase 4: Intelligence

  • Smart tier prediction
  • User preference learning
  • Automatic compression optimization
  • Cross-chart caching

Success Metrics

Metric Before Target Measurement
Chart generation time 2-5s <200ms Timing logs
Avg tokens/chart 2000+ <500 Token counting
Docker image size 250MB <200MB Image inspection
Cache hit rate 0% >50% Access logs
User satisfaction Low High Feedback

Related Decisions

  • ADR-001: Analytics Architecture
  • ADR-002: Context Management Strategy
  • ADR-003: Caching and Query Optimization
  • MCP-SPECIFICATION: Tool specifications

References

Decision Record

Date Author Decision Rationale
2026-03-17 Sisyphus Adopt 3-tier visualization strategy Maximizes flexibility while minimizing token and resource overhead

Status Legend:

  • Proposed: Under review
  • Accepted: Approved for implementation
  • Deprecated: Replaced by newer ADR
  • Superseded: See referenced ADR