skills/creating-dashboards/scripts/generate-dashboard-layout.py

271 lines
9.7 KiB
Python

#!/usr/bin/env python3
"""
Dashboard Layout Generator
Generates responsive grid layouts based on widget configuration and screen size.
"""
import json
import argparse
from typing import Dict, List, Any, Tuple
import math
class DashboardLayoutGenerator:
"""Generates optimized dashboard layouts for different screen sizes."""
def __init__(self):
self.breakpoints = {
'xxs': {'width': 0, 'cols': 2},
'xs': {'width': 480, 'cols': 4},
'sm': {'width': 768, 'cols': 6},
'md': {'width': 996, 'cols': 10},
'lg': {'width': 1200, 'cols': 12},
'xl': {'width': 1920, 'cols': 16}
}
self.widget_sizes = {
'kpi': {'min_w': 2, 'min_h': 2, 'ideal_w': 3, 'ideal_h': 2},
'chart': {'min_w': 4, 'min_h': 3, 'ideal_w': 6, 'ideal_h': 4},
'table': {'min_w': 4, 'min_h': 3, 'ideal_w': 6, 'ideal_h': 4},
'map': {'min_w': 4, 'min_h': 3, 'ideal_w': 8, 'ideal_h': 5},
'timeline': {'min_w': 6, 'min_h': 2, 'ideal_w': 12, 'ideal_h': 3},
'text': {'min_w': 2, 'min_h': 1, 'ideal_w': 4, 'ideal_h': 2},
'gauge': {'min_w': 2, 'min_h': 2, 'ideal_w': 3, 'ideal_h': 3}
}
def generate_layout(self, widgets: List[Dict], breakpoint: str = 'lg') -> List[Dict]:
"""
Generate a layout for the given widgets and breakpoint.
Args:
widgets: List of widget configurations
breakpoint: Screen size breakpoint
Returns:
List of positioned widgets with x, y, w, h coordinates
"""
cols = self.breakpoints[breakpoint]['cols']
layout = []
grid = [[False] * cols for _ in range(100)] # Grid to track occupied cells
# Sort widgets by priority and size (larger first)
sorted_widgets = sorted(
widgets,
key=lambda w: (
-w.get('priority', 0),
-self.widget_sizes.get(w['type'], {}).get('ideal_w', 3) *
self.widget_sizes.get(w['type'], {}).get('ideal_h', 2)
)
)
for widget in sorted_widgets:
position = self._find_position(widget, grid, cols, breakpoint)
if position:
layout.append({
'i': widget['id'],
'x': position[0],
'y': position[1],
'w': position[2],
'h': position[3],
'minW': self.widget_sizes.get(widget['type'], {}).get('min_w', 2),
'minH': self.widget_sizes.get(widget['type'], {}).get('min_h', 2)
})
self._mark_occupied(grid, position)
return layout
def _find_position(self, widget: Dict, grid: List[List[bool]], cols: int, breakpoint: str) -> Tuple[int, int, int, int]:
"""Find the best position for a widget in the grid."""
widget_type = widget.get('type', 'kpi')
size_config = self.widget_sizes.get(widget_type, self.widget_sizes['kpi'])
# Adjust size based on breakpoint
if breakpoint in ['xxs', 'xs']:
# Mobile: full width
width = cols
height = size_config['min_h']
elif breakpoint in ['sm', 'md']:
# Tablet: half to full width
width = min(cols, max(cols // 2, size_config['min_w']))
height = size_config['ideal_h']
else:
# Desktop: ideal size
width = min(cols, size_config['ideal_w'])
height = size_config['ideal_h']
# Find first available position
for y in range(100):
for x in range(cols - width + 1):
if self._can_place(grid, x, y, width, height):
return (x, y, width, height)
return None
def _can_place(self, grid: List[List[bool]], x: int, y: int, w: int, h: int) -> bool:
"""Check if a widget can be placed at the given position."""
for row in range(y, y + h):
for col in range(x, x + w):
if row >= len(grid) or col >= len(grid[0]) or grid[row][col]:
return False
return True
def _mark_occupied(self, grid: List[List[bool]], position: Tuple[int, int, int, int]):
"""Mark grid cells as occupied."""
x, y, w, h = position
for row in range(y, y + h):
for col in range(x, x + w):
if row < len(grid) and col < len(grid[0]):
grid[row][col] = True
def generate_responsive_layouts(self, widgets: List[Dict]) -> Dict[str, List[Dict]]:
"""Generate layouts for all breakpoints."""
layouts = {}
for breakpoint in self.breakpoints.keys():
layouts[breakpoint] = self.generate_layout(widgets, breakpoint)
return layouts
def optimize_layout(self, layout: List[Dict]) -> List[Dict]:
"""Optimize layout by removing gaps and compacting widgets."""
# Sort by y position, then x position
sorted_layout = sorted(layout, key=lambda w: (w['y'], w['x']))
# Compact vertically
for i, widget in enumerate(sorted_layout):
# Try to move widget up
target_y = 0
for y in range(widget['y']):
can_move = True
for other in sorted_layout[:i]:
if (other['x'] < widget['x'] + widget['w'] and
widget['x'] < other['x'] + other['w'] and
other['y'] <= y < other['y'] + other['h']):
can_move = False
target_y = other['y'] + other['h']
break
if can_move and y >= target_y:
widget['y'] = y
break
return sorted_layout
def calculate_layout_score(self, layout: List[Dict], widgets: List[Dict]) -> float:
"""Calculate a quality score for the layout."""
if not layout:
return 0.0
score = 100.0
# Penalize for widgets not placed
placement_rate = len(layout) / len(widgets) if widgets else 1.0
score *= placement_rate
# Penalize for excessive vertical space
max_y = max(w['y'] + w['h'] for w in layout) if layout else 0
if max_y > 20: # Penalize layouts taller than 20 units
score *= (20 / max_y)
# Reward for widgets maintaining ideal sizes
for widget in layout:
widget_type = next((w['type'] for w in widgets if w['id'] == widget['i']), 'kpi')
ideal_size = self.widget_sizes.get(widget_type, {})
if ideal_size:
size_match = (
(widget['w'] / ideal_size.get('ideal_w', widget['w'])) *
(widget['h'] / ideal_size.get('ideal_h', widget['h']))
)
score += min(10, size_match * 10)
return round(score, 2)
def create_sample_widgets(count: int = 10) -> List[Dict]:
"""Create sample widget configurations for testing."""
import random
widget_types = ['kpi', 'chart', 'table', 'map', 'timeline', 'text', 'gauge']
widgets = []
for i in range(count):
widget_type = random.choice(widget_types)
widgets.append({
'id': f'widget-{i+1}',
'type': widget_type,
'title': f'{widget_type.capitalize()} Widget {i+1}',
'priority': random.randint(1, 5)
})
return widgets
def export_to_react(layouts: Dict[str, List[Dict]]) -> str:
"""Export layouts as React component code."""
code = """// Generated Dashboard Layouts
export const dashboardLayouts = {
"""
for breakpoint, layout in layouts.items():
code += f" {breakpoint}: [\n"
for widget in layout:
code += f" {json.dumps(widget)},\n"
code += " ],\n"
code += "};\n"
return code
def main():
"""Main function to run the layout generator."""
parser = argparse.ArgumentParser(description='Generate dashboard layouts')
parser.add_argument('--config', type=str, help='Path to widget configuration JSON file')
parser.add_argument('--widgets', type=int, default=10, help='Number of sample widgets to generate')
parser.add_argument('--breakpoint', type=str, default='all', help='Specific breakpoint or "all"')
parser.add_argument('--optimize', action='store_true', help='Optimize the layout')
parser.add_argument('--export', type=str, choices=['json', 'react'], default='json', help='Export format')
parser.add_argument('--output', type=str, help='Output file path')
args = parser.parse_args()
generator = DashboardLayoutGenerator()
# Load or create widgets
if args.config:
with open(args.config, 'r') as f:
widgets = json.load(f)
else:
widgets = create_sample_widgets(args.widgets)
print(f"Generated {len(widgets)} sample widgets")
# Generate layouts
if args.breakpoint == 'all':
layouts = generator.generate_responsive_layouts(widgets)
else:
layout = generator.generate_layout(widgets, args.breakpoint)
if args.optimize:
layout = generator.optimize_layout(layout)
layouts = {args.breakpoint: layout}
# Calculate scores
print("\nLayout Quality Scores:")
for breakpoint, layout in layouts.items():
score = generator.calculate_layout_score(layout, widgets)
print(f" {breakpoint}: {score}/100")
# Export results
if args.export == 'react':
output = export_to_react(layouts)
else:
output = json.dumps(layouts, indent=2)
if args.output:
with open(args.output, 'w') as f:
f.write(output)
print(f"\nLayout saved to {args.output}")
else:
print("\nGenerated Layout:")
print(output)
if __name__ == '__main__':
main()