skills/creating-dashboards/references/layout-strategies.md

14 KiB

Dashboard Layout Strategies

Table of Contents

Grid System Fundamentals

12-Column Grid System

.dashboard-grid {
  display: grid;
  grid-template-columns: repeat(12, 1fr);
  gap: var(--dashboard-gap, 1rem);
  padding: var(--dashboard-padding, 1.5rem);
}

/* Widget spanning */
.widget-sm { grid-column: span 3; }  /* 1/4 width */
.widget-md { grid-column: span 4; }  /* 1/3 width */
.widget-lg { grid-column: span 6; }  /* 1/2 width */
.widget-xl { grid-column: span 8; }  /* 2/3 width */
.widget-full { grid-column: span 12; } /* Full width */

CSS Grid Dashboard Layout

function GridDashboard({ widgets }) {
  return (
    <div className="dashboard-grid">
      {widgets.map(widget => (
        <div
          key={widget.id}
          className={`widget-${widget.size}`}
          style={{
            gridRow: `span ${widget.height || 1}`,
          }}
        >
          <Widget {...widget} />
        </div>
      ))}
    </div>
  );
}

Fixed Layout Pattern

Executive Dashboard Layout

const executiveLayout = {
  desktop: [
    { id: 'revenue', col: 0, row: 0, w: 3, h: 2 },
    { id: 'users', col: 3, row: 0, w: 3, h: 2 },
    { id: 'conversion', col: 6, row: 0, w: 3, h: 2 },
    { id: 'churn', col: 9, row: 0, w: 3, h: 2 },
    { id: 'chart', col: 0, row: 2, w: 8, h: 4 },
    { id: 'table', col: 8, row: 2, w: 4, h: 4 },
    { id: 'map', col: 0, row: 6, w: 6, h: 3 },
    { id: 'timeline', col: 6, row: 6, w: 6, h: 3 }
  ]
};

function FixedDashboard() {
  return (
    <div className="fixed-dashboard">
      {executiveLayout.desktop.map(item => (
        <div
          key={item.id}
          className="widget-container"
          style={{
            gridColumn: `${item.col + 1} / span ${item.w}`,
            gridRow: `${item.row + 1} / span ${item.h}`
          }}
        >
          <Widget id={item.id} />
        </div>
      ))}
    </div>
  );
}

Predefined Layout Templates

const layoutTemplates = {
  salesDashboard: {
    name: 'Sales Analytics',
    widgets: [
      { type: 'kpi', position: { x: 0, y: 0, w: 3, h: 2 } },
      { type: 'kpi', position: { x: 3, y: 0, w: 3, h: 2 } },
      { type: 'revenue-chart', position: { x: 0, y: 2, w: 6, h: 4 } },
      { type: 'product-table', position: { x: 6, y: 2, w: 6, h: 4 } }
    ]
  },

  operationsDashboard: {
    name: 'Operations Monitoring',
    widgets: [
      { type: 'status-grid', position: { x: 0, y: 0, w: 12, h: 2 } },
      { type: 'timeline', position: { x: 0, y: 2, w: 8, h: 3 } },
      { type: 'alerts', position: { x: 8, y: 2, w: 4, h: 3 } }
    ]
  }
};

Customizable Grid Layout

React Grid Layout Implementation

import GridLayout from 'react-grid-layout';
import 'react-grid-layout/css/styles.css';
import 'react-resizable/css/styles.css';

function CustomizableDashboard() {
  const [layout, setLayout] = useState([
    { i: 'a', x: 0, y: 0, w: 4, h: 2 },
    { i: 'b', x: 4, y: 0, w: 4, h: 2 },
    { i: 'c', x: 8, y: 0, w: 4, h: 2 },
    { i: 'd', x: 0, y: 2, w: 6, h: 4 },
    { i: 'e', x: 6, y: 2, w: 6, h: 4 }
  ]);

  const onLayoutChange = (newLayout) => {
    setLayout(newLayout);
    // Save to localStorage or API
    localStorage.setItem('dashboardLayout', JSON.stringify(newLayout));
  };

  return (
    <GridLayout
      className="layout"
      layout={layout}
      cols={12}
      rowHeight={60}
      width={1200}
      onLayoutChange={onLayoutChange}
      draggableHandle=".widget-header"
      resizeHandles={['se']}
      compactType="vertical"
      preventCollision={false}
    >
      <div key="a"><KPIWidget title="Revenue" /></div>
      <div key="b"><KPIWidget title="Users" /></div>
      <div key="c"><KPIWidget title="Orders" /></div>
      <div key="d"><ChartWidget /></div>
      <div key="e"><TableWidget /></div>
    </GridLayout>
  );
}

Responsive Grid Layout

import { Responsive, WidthProvider } from 'react-grid-layout';

const ResponsiveGridLayout = WidthProvider(Responsive);

function ResponsiveDashboard() {
  const [layouts, setLayouts] = useState({
    lg: [
      { i: 'kpi1', x: 0, y: 0, w: 3, h: 2 },
      { i: 'kpi2', x: 3, y: 0, w: 3, h: 2 },
      { i: 'kpi3', x: 6, y: 0, w: 3, h: 2 },
      { i: 'kpi4', x: 9, y: 0, w: 3, h: 2 }
    ],
    md: [
      { i: 'kpi1', x: 0, y: 0, w: 5, h: 2 },
      { i: 'kpi2', x: 5, y: 0, w: 5, h: 2 },
      { i: 'kpi3', x: 0, y: 2, w: 5, h: 2 },
      { i: 'kpi4', x: 5, y: 2, w: 5, h: 2 }
    ],
    sm: [
      { i: 'kpi1', x: 0, y: 0, w: 6, h: 2 },
      { i: 'kpi2', x: 0, y: 2, w: 6, h: 2 },
      { i: 'kpi3', x: 0, y: 4, w: 6, h: 2 },
      { i: 'kpi4', x: 0, y: 6, w: 6, h: 2 }
    ]
  });

  return (
    <ResponsiveGridLayout
      layouts={layouts}
      breakpoints={{ lg: 1200, md: 996, sm: 768 }}
      cols={{ lg: 12, md: 10, sm: 6 }}
      rowHeight={60}
      onLayoutChange={(layout, layouts) => setLayouts(layouts)}
    >
      {/* Widgets */}
    </ResponsiveGridLayout>
  );
}

Template-Based Layouts

Dashboard Template System

interface DashboardTemplate {
  id: string;
  name: string;
  description: string;
  category: string;
  widgets: WidgetConfig[];
  layout: LayoutConfig;
  filters: FilterConfig[];
}

const templates: DashboardTemplate[] = [
  {
    id: 'sales-overview',
    name: 'Sales Overview',
    description: 'Track sales performance and revenue metrics',
    category: 'Sales',
    widgets: [
      {
        type: 'kpi',
        dataSource: 'revenue',
        config: { period: 'monthly' }
      },
      {
        type: 'area-chart',
        dataSource: 'revenue-trend',
        config: { timeRange: '12months' }
      }
    ],
    layout: {
      desktop: { cols: 12, rowHeight: 80 },
      tablet: { cols: 8, rowHeight: 70 },
      mobile: { cols: 4, rowHeight: 60 }
    },
    filters: [
      { type: 'date-range', default: 'last-30-days' },
      { type: 'region', options: ['all', 'north', 'south'] }
    ]
  }
];

function TemplateSelector({ onSelect }) {
  return (
    <div className="template-grid">
      {templates.map(template => (
        <div
          key={template.id}
          className="template-card"
          onClick={() => onSelect(template)}
        >
          <h3>{template.name}</h3>
          <p>{template.description}</p>
          <span className="category">{template.category}</span>
        </div>
      ))}
    </div>
  );
}

Applying Templates

function DashboardFromTemplate({ templateId }) {
  const template = templates.find(t => t.id === templateId);
  const [widgets, setWidgets] = useState([]);

  useEffect(() => {
    // Initialize widgets from template
    const initializedWidgets = template.widgets.map((config, index) => ({
      id: `widget-${index}`,
      ...config,
      data: null,
      loading: true
    }));

    setWidgets(initializedWidgets);

    // Load data for each widget
    initializedWidgets.forEach(widget => {
      loadWidgetData(widget).then(data => {
        setWidgets(prev => prev.map(w =>
          w.id === widget.id ? { ...w, data, loading: false } : w
        ));
      });
    });
  }, [template]);

  return (
    <DashboardLayout
      layout={template.layout}
      widgets={widgets}
      filters={template.filters}
    />
  );
}

Responsive Breakpoints

Breakpoint Strategy

const breakpoints = {
  mobile: 0,     // 0-767px
  tablet: 768,   // 768-1199px
  desktop: 1200, // 1200-1919px
  wide: 1920     // 1920px+
};

const getLayoutForBreakpoint = (width: number): LayoutType => {
  if (width >= breakpoints.wide) return 'wide';
  if (width >= breakpoints.desktop) return 'desktop';
  if (width >= breakpoints.tablet) return 'tablet';
  return 'mobile';
};

Responsive Widget Sizing

function ResponsiveWidget({ widget }) {
  const [dimensions, setDimensions] = useState({
    width: 'auto',
    height: 'auto'
  });

  useEffect(() => {
    const updateDimensions = () => {
      const viewport = window.innerWidth;

      if (viewport < 768) {
        // Mobile: full width, fixed height
        setDimensions({ width: '100%', height: '200px' });
      } else if (viewport < 1200) {
        // Tablet: half width, medium height
        setDimensions({ width: '50%', height: '250px' });
      } else {
        // Desktop: flexible based on widget type
        setDimensions({
          width: widget.defaultWidth || '25%',
          height: widget.defaultHeight || '300px'
        });
      }
    };

    updateDimensions();
    window.addEventListener('resize', updateDimensions);
    return () => window.removeEventListener('resize', updateDimensions);
  }, [widget]);

  return (
    <div className="widget" style={dimensions}>
      <WidgetContent {...widget} />
    </div>
  );
}

Mobile-First Layout

/* Mobile First Approach */
.dashboard {
  display: flex;
  flex-direction: column;
  gap: 1rem;
  padding: 1rem;
}

.widget {
  width: 100%;
  min-height: 200px;
}

/* Tablet */
@media (min-width: 768px) {
  .dashboard {
    display: grid;
    grid-template-columns: repeat(2, 1fr);
    gap: 1.5rem;
    padding: 1.5rem;
  }
}

/* Desktop */
@media (min-width: 1200px) {
  .dashboard {
    grid-template-columns: repeat(12, 1fr);
    gap: 2rem;
    padding: 2rem;
  }

  .widget-small { grid-column: span 3; }
  .widget-medium { grid-column: span 4; }
  .widget-large { grid-column: span 6; }
  .widget-full { grid-column: span 12; }
}

Layout Persistence

Saving User Layouts

class LayoutPersistence {
  private storageKey = 'dashboard-layout';

  saveLayout(userId: string, layout: Layout) {
    const key = `${this.storageKey}-${userId}`;

    // Save to localStorage
    localStorage.setItem(key, JSON.stringify(layout));

    // Also save to backend
    fetch('/api/dashboard/layout', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ userId, layout })
    });
  }

  loadLayout(userId: string): Layout | null {
    const key = `${this.storageKey}-${userId}`;

    // Try localStorage first
    const stored = localStorage.getItem(key);
    if (stored) {
      return JSON.parse(stored);
    }

    // Fallback to backend
    return this.fetchLayoutFromBackend(userId);
  }

  async fetchLayoutFromBackend(userId: string): Promise<Layout | null> {
    try {
      const response = await fetch(`/api/dashboard/layout/${userId}`);
      if (response.ok) {
        return await response.json();
      }
    } catch (error) {
      console.error('Failed to load layout:', error);
    }
    return null;
  }

  resetLayout(userId: string) {
    const key = `${this.storageKey}-${userId}`;
    localStorage.removeItem(key);

    fetch(`/api/dashboard/layout/${userId}`, {
      method: 'DELETE'
    });
  }
}

Layout Versioning

interface VersionedLayout {
  version: number;
  created: Date;
  modified: Date;
  layout: Layout;
  metadata: {
    browser: string;
    screenResolution: string;
    userId: string;
  };
}

function migrateLayout(layout: VersionedLayout): VersionedLayout {
  let migrated = { ...layout };

  // Migrate from v1 to v2
  if (layout.version === 1) {
    migrated.layout = convertV1ToV2(layout.layout);
    migrated.version = 2;
  }

  // Migrate from v2 to v3
  if (migrated.version === 2) {
    migrated.layout = convertV2ToV3(migrated.layout);
    migrated.version = 3;
  }

  return migrated;
}

Performance Considerations

Virtualized Grid for Large Dashboards

import { FixedSizeGrid } from 'react-window';

function VirtualizedDashboard({ widgets, columns = 4 }) {
  const rowCount = Math.ceil(widgets.length / columns);

  const Cell = ({ columnIndex, rowIndex, style }) => {
    const index = rowIndex * columns + columnIndex;
    if (index >= widgets.length) return null;

    return (
      <div style={style}>
        <Widget {...widgets[index]} />
      </div>
    );
  };

  return (
    <FixedSizeGrid
      columnCount={columns}
      columnWidth={300}
      height={800}
      rowCount={rowCount}
      rowHeight={250}
      width={1200}
    >
      {Cell}
    </FixedSizeGrid>
  );
}

Layout Optimization Techniques

// Memoize expensive layout calculations
const calculateOptimalLayout = useMemo(() => {
  return (widgets: Widget[], containerWidth: number) => {
    // Complex layout algorithm
    const columns = Math.floor(containerWidth / 300);
    const layout = [];

    widgets.forEach((widget, index) => {
      const row = Math.floor(index / columns);
      const col = index % columns;

      layout.push({
        id: widget.id,
        x: col * (containerWidth / columns),
        y: row * 250,
        width: containerWidth / columns - 20,
        height: 230
      });
    });

    return layout;
  };
}, []);

// Debounce resize events
const useDebounceResize = (callback: Function, delay = 300) => {
  useEffect(() => {
    let timeoutId: NodeJS.Timeout;

    const handleResize = () => {
      clearTimeout(timeoutId);
      timeoutId = setTimeout(callback, delay);
    };

    window.addEventListener('resize', handleResize);
    return () => {
      window.removeEventListener('resize', handleResize);
      clearTimeout(timeoutId);
    };
  }, [callback, delay]);
};

Progressive Layout Loading

function ProgressiveDashboard({ widgets }) {
  const [visibleWidgets, setVisibleWidgets] = useState([]);
  const [loadingPhase, setLoadingPhase] = useState(0);

  useEffect(() => {
    // Phase 1: Load above-the-fold widgets
    const aboveFold = widgets.slice(0, 4);
    setVisibleWidgets(aboveFold);
    setLoadingPhase(1);

    // Phase 2: Load next batch after delay
    setTimeout(() => {
      const nextBatch = widgets.slice(4, 8);
      setVisibleWidgets(prev => [...prev, ...nextBatch]);
      setLoadingPhase(2);
    }, 100);

    // Phase 3: Load remaining widgets
    setTimeout(() => {
      const remaining = widgets.slice(8);
      setVisibleWidgets(prev => [...prev, ...remaining]);
      setLoadingPhase(3);
    }, 300);
  }, [widgets]);

  return (
    <div className="progressive-dashboard">
      {visibleWidgets.map(widget => (
        <Widget key={widget.id} {...widget} />
      ))}
      {loadingPhase < 3 && (
        <div className="loading-indicator">
          Loading more widgets...
        </div>
      )}
    </div>
  );
}