skills/creating-dashboards/references/library-guide.md

18 KiB
Raw Permalink Blame History

Dashboard Libraries Implementation Guide

Table of Contents

Tremor Quick Start

Installation & Setup

# Install Tremor with Tailwind CSS
npm install @tremor/react tailwindcss @tailwindcss/forms

# Initialize Tailwind if not already configured
npx tailwindcss init -p

Tailwind Configuration for Tremor

// tailwind.config.js
module.exports = {
  content: [
    './src/**/*.{js,ts,jsx,tsx}',
    './node_modules/@tremor/**/*.{js,ts,jsx,tsx}', // Tremor components
  ],
  theme: {
    extend: {
      colors: {
        tremor: {
          brand: {
            faint: '#eff6ff',
            muted: '#bfdbfe',
            subtle: '#60a5fa',
            DEFAULT: '#3b82f6',
            emphasis: '#1d4ed8',
            inverted: '#ffffff',
          },
          background: {
            muted: '#f9fafb',
            subtle: '#f3f4f6',
            DEFAULT: '#ffffff',
            emphasis: '#374151',
          },
          // Additional Tremor colors...
        }
      }
    }
  },
  plugins: [require('@tailwindcss/forms')]
};

Basic Tremor Dashboard

import {
  Card,
  Title,
  Text,
  Tab,
  TabList,
  TabGroup,
  TabPanel,
  TabPanels,
  Grid,
  Metric,
  BadgeDelta,
  Flex,
  ProgressBar,
  AreaChart,
  BarChart,
  DonutChart,
  Table,
  TableHead,
  TableRow,
  TableHeaderCell,
  TableBody,
  TableCell
} from '@tremor/react';

function TremorDashboard() {
  return (
    <main className="p-6 sm:p-10">
      <Title>Analytics Dashboard</Title>
      <Text>Track your key metrics and performance indicators</Text>

      {/* KPI Cards Row */}
      <Grid numItemsSm={2} numItemsLg={4} className="gap-6 mt-6">
        <Card>
          <Flex alignItems="start">
            <div>
              <Text>Revenue</Text>
              <Metric>$45,231.89</Metric>
            </div>
            <BadgeDelta deltaType="increase">12.5%</BadgeDelta>
          </Flex>
        </Card>

        <Card>
          <Flex alignItems="start">
            <div>
              <Text>New Customers</Text>
              <Metric>1,234</Metric>
            </div>
            <BadgeDelta deltaType="decrease">-2.3%</BadgeDelta>
          </Flex>
        </Card>

        <Card>
          <Flex alignItems="start">
            <div>
              <Text>Active Users</Text>
              <Metric>8,456</Metric>
            </div>
            <BadgeDelta deltaType="increase">5.1%</BadgeDelta>
          </Flex>
        </Card>

        <Card>
          <Flex alignItems="start">
            <div>
              <Text>Retention Rate</Text>
              <Metric>92.5%</Metric>
            </div>
            <BadgeDelta deltaType="unchanged">0.0%</BadgeDelta>
          </Flex>
        </Card>
      </Grid>

      {/* Charts Section */}
      <div className="mt-6">
        <Card>
          <Title>Revenue Trend</Title>
          <AreaChart
            className="h-72 mt-4"
            data={revenueData}
            index="date"
            categories={["Revenue", "Target"]}
            colors={["blue", "gray"]}
            valueFormatter={dataFormatter}
          />
        </Card>
      </div>

      {/* Table Section */}
      <Card className="mt-6">
        <Title>Recent Transactions</Title>
        <Table className="mt-5">
          <TableHead>
            <TableRow>
              <TableHeaderCell>Transaction</TableHeaderCell>
              <TableHeaderCell>Amount</TableHeaderCell>
              <TableHeaderCell>Status</TableHeaderCell>
            </TableRow>
          </TableHead>
          <TableBody>
            {transactions.map(item => (
              <TableRow key={item.id}>
                <TableCell>{item.name}</TableCell>
                <TableCell>{item.amount}</TableCell>
                <TableCell>{item.status}</TableCell>
              </TableRow>
            ))}
          </TableBody>
        </Table>
      </Card>
    </main>
  );
}

Advanced Tremor Components

Multi-Metric Cards

function MultiMetricCard() {
  return (
    <Card className="max-w-lg mx-auto">
      <Flex className="space-x-8" justifyContent="start" alignItems="baseline">
        <div>
          <Text>Sales</Text>
          <Metric>$12,699</Metric>
          <Text className="mt-2">from $10,456</Text>
        </div>
        <div>
          <Text>Profit</Text>
          <Metric>$4,034</Metric>
          <Text className="mt-2">from $3,210</Text>
        </div>
      </Flex>
      <ProgressBar value={32} className="mt-4" />
      <Text className="mt-2">32% of annual target</Text>
    </Card>
  );
}

Tabbed Dashboard

function TabbedDashboard() {
  return (
    <Card>
      <TabGroup>
        <TabList className="mt-8">
          <Tab>Overview</Tab>
          <Tab>Detail</Tab>
          <Tab>Analysis</Tab>
        </TabList>
        <TabPanels>
          <TabPanel>
            <Grid numItemsMd={2} numItemsLg={3} className="gap-6 mt-6">
              {/* Overview widgets */}
            </Grid>
          </TabPanel>
          <TabPanel>
            {/* Detailed charts */}
          </TabPanel>
          <TabPanel>
            {/* Analysis tables */}
          </TabPanel>
        </TabPanels>
      </TabGroup>
    </Card>
  );
}

React Grid Layout Setup

Installation

npm install react-grid-layout

# Required CSS
npm install react-resizable

Basic Setup

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

function BasicGridDashboard() {
  const layout = [
    { 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: 12, h: 4 }
  ];

  return (
    <GridLayout
      className="layout"
      layout={layout}
      cols={12}
      rowHeight={80}
      width={1200}
    >
      <div key="a" className="widget">
        <KPICard title="Revenue" value="$45K" />
      </div>
      <div key="b" className="widget">
        <KPICard title="Users" value="1.2K" />
      </div>
      <div key="c" className="widget">
        <KPICard title="Orders" value="342" />
      </div>
      <div key="d" className="widget">
        <ChartWidget />
      </div>
    </GridLayout>
  );
}

Responsive Grid Layout

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

const ResponsiveGridLayout = WidthProvider(Responsive);

function ResponsiveDashboard() {
  const [layouts, setLayouts] = useState(getFromLS('layouts') || {});

  const handleLayoutChange = (layout, layouts) => {
    saveToLS('layouts', layouts);
    setLayouts(layouts);
  };

  return (
    <ResponsiveGridLayout
      className="layout"
      layouts={layouts}
      breakpoints={{ lg: 1200, md: 996, sm: 768, xs: 480, xxs: 0 }}
      cols={{ lg: 12, md: 10, sm: 6, xs: 4, xxs: 2 }}
      rowHeight={60}
      onLayoutChange={handleLayoutChange}
    >
      <div key="1">Widget 1</div>
      <div key="2">Widget 2</div>
      <div key="3">Widget 3</div>
    </ResponsiveGridLayout>
  );
}

// Local Storage helpers
function getFromLS(key) {
  let ls = {};
  if (global.localStorage) {
    try {
      ls = JSON.parse(global.localStorage.getItem('rgl-8')) || {};
    } catch (e) {}
  }
  return ls[key];
}

function saveToLS(key, value) {
  if (global.localStorage) {
    global.localStorage.setItem(
      'rgl-8',
      JSON.stringify({
        [key]: value
      })
    );
  }
}

Advanced Grid Features

Drag Handle & Resize Restrictions

function RestrictedGridDashboard() {
  const layout = [
    {
      i: 'header',
      x: 0,
      y: 0,
      w: 12,
      h: 1,
      static: true // Cannot be moved or resized
    },
    {
      i: 'kpi1',
      x: 0,
      y: 1,
      w: 3,
      h: 2,
      minW: 2,
      maxW: 4,
      minH: 1,
      maxH: 3
    },
    {
      i: 'chart',
      x: 3,
      y: 1,
      w: 9,
      h: 4,
      minW: 6,
      minH: 3
    }
  ];

  return (
    <GridLayout
      layout={layout}
      cols={12}
      rowHeight={60}
      draggableHandle=".drag-handle" // Only draggable by handle
      resizeHandles={['se', 'sw']} // Only bottom corners
      compactType="vertical"
      preventCollision={false}
    >
      <div key="header" className="widget">
        <h1>Dashboard Header (Static)</h1>
      </div>
      <div key="kpi1" className="widget">
        <div className="drag-handle">⋮⋮</div>
        <KPIWidget />
      </div>
      <div key="chart" className="widget">
        <div className="drag-handle">⋮⋮</div>
        <ChartWidget />
      </div>
    </GridLayout>
  );
}

Widget Catalog with Add/Remove

function CustomizableDashboard() {
  const [widgets, setWidgets] = useState([
    { id: 'widget-1', type: 'kpi', x: 0, y: 0, w: 3, h: 2 }
  ]);

  const widgetCatalog = [
    { type: 'kpi', name: 'KPI Card', defaultSize: { w: 3, h: 2 } },
    { type: 'chart', name: 'Chart', defaultSize: { w: 6, h: 4 } },
    { type: 'table', name: 'Table', defaultSize: { w: 6, h: 3 } }
  ];

  const addWidget = (type) => {
    const catalogItem = widgetCatalog.find(w => w.type === type);
    const newWidget = {
      id: `widget-${Date.now()}`,
      type,
      x: 0,
      y: Infinity, // Will be placed at bottom
      ...catalogItem.defaultSize
    };
    setWidgets([...widgets, newWidget]);
  };

  const removeWidget = (id) => {
    setWidgets(widgets.filter(w => w.id !== id));
  };

  return (
    <div>
      {/* Widget Catalog */}
      <div className="widget-catalog">
        {widgetCatalog.map(item => (
          <button
            key={item.type}
            onClick={() => addWidget(item.type)}
          >
            Add {item.name}
          </button>
        ))}
      </div>

      {/* Grid Dashboard */}
      <ResponsiveGridLayout>
        {widgets.map(widget => (
          <div key={widget.id} data-grid={widget}>
            <div className="widget-wrapper">
              <button
                className="remove-btn"
                onClick={() => removeWidget(widget.id)}
              >
                ×
              </button>
              {renderWidget(widget)}
            </div>
          </div>
        ))}
      </ResponsiveGridLayout>
    </div>
  );
}

Combining Libraries

Tremor Components in Grid Layout

import { Card, Metric, Text, AreaChart } from '@tremor/react';
import { Responsive, WidthProvider } from 'react-grid-layout';

const ResponsiveGridLayout = WidthProvider(Responsive);

function HybridDashboard() {
  const layouts = {
    lg: [
      { i: 'revenue-kpi', x: 0, y: 0, w: 3, h: 2 },
      { i: 'users-kpi', x: 3, y: 0, w: 3, h: 2 },
      { i: 'chart', x: 0, y: 2, w: 6, h: 4 },
      { i: 'table', x: 6, y: 0, w: 6, h: 6 }
    ]
  };

  return (
    <ResponsiveGridLayout
      layouts={layouts}
      breakpoints={{ lg: 1200, md: 996, sm: 768 }}
      cols={{ lg: 12, md: 10, sm: 6 }}
      rowHeight={60}
    >
      {/* Tremor KPI Card */}
      <div key="revenue-kpi">
        <Card className="h-full">
          <Text>Revenue</Text>
          <Metric>$45,231</Metric>
        </Card>
      </div>

      {/* Tremor KPI Card */}
      <div key="users-kpi">
        <Card className="h-full">
          <Text>Active Users</Text>
          <Metric>1,234</Metric>
        </Card>
      </div>

      {/* Tremor Chart */}
      <div key="chart">
        <Card className="h-full">
          <AreaChart
            data={chartData}
            index="date"
            categories={["Sales"]}
            colors={["blue"]}
          />
        </Card>
      </div>

      {/* Custom Table Widget */}
      <div key="table">
        <Card className="h-full">
          <CustomTableWidget />
        </Card>
      </div>
    </ResponsiveGridLayout>
  );
}

Custom Widget Wrapper

function WidgetWrapper({ widget, children }) {
  const [isLoading, setIsLoading] = useState(true);
  const [hasError, setHasError] = useState(false);

  useEffect(() => {
    // Simulate loading
    setTimeout(() => setIsLoading(false), 1000);
  }, []);

  return (
    <Card className="h-full relative">
      {/* Widget Header */}
      <div className="widget-header drag-handle flex justify-between items-center mb-4">
        <Title>{widget.title}</Title>
        <div className="widget-actions">
          <button className="refresh-btn"></button>
          <button className="settings-btn"></button>
        </div>
      </div>

      {/* Widget Content */}
      <div className="widget-content">
        {isLoading ? (
          <div className="animate-pulse">
            <div className="h-4 bg-gray-200 rounded w-3/4 mb-2"></div>
            <div className="h-8 bg-gray-200 rounded w-1/2"></div>
          </div>
        ) : hasError ? (
          <div className="error-state">
            <Text>Failed to load widget</Text>
            <button onClick={() => setHasError(false)}>Retry</button>
          </div>
        ) : (
          children
        )}
      </div>

      {/* Resize Handle (for react-grid-layout) */}
      <span className="react-resizable-handle react-resizable-handle-se" />
    </Card>
  );
}

Migration Strategies

Migrating from Static to Grid Layout

// Before: Static Dashboard
function StaticDashboard() {
  return (
    <div className="dashboard">
      <div className="row">
        <KPICard />
        <KPICard />
        <KPICard />
      </div>
      <div className="row">
        <ChartWidget />
        <TableWidget />
      </div>
    </div>
  );
}

// After: Grid Layout Dashboard
function MigratedDashboard() {
  // Convert static positions to grid layout
  const staticToGridLayout = () => {
    return [
      { i: 'kpi-1', x: 0, y: 0, w: 4, h: 2 },
      { i: 'kpi-2', x: 4, y: 0, w: 4, h: 2 },
      { i: 'kpi-3', x: 8, y: 0, w: 4, h: 2 },
      { i: 'chart', x: 0, y: 2, w: 6, h: 4 },
      { i: 'table', x: 6, y: 2, w: 6, h: 4 }
    ];
  };

  const [layout, setLayout] = useState(staticToGridLayout());
  const [isCustomizable, setIsCustomizable] = useState(false);

  return (
    <>
      <button onClick={() => setIsCustomizable(!isCustomizable)}>
        {isCustomizable ? 'Lock Layout' : 'Customize Layout'}
      </button>

      <GridLayout
        layout={layout}
        cols={12}
        rowHeight={60}
        width={1200}
        isDraggable={isCustomizable}
        isResizable={isCustomizable}
        onLayoutChange={setLayout}
      >
        <div key="kpi-1"><KPICard /></div>
        <div key="kpi-2"><KPICard /></div>
        <div key="kpi-3"><KPICard /></div>
        <div key="chart"><ChartWidget /></div>
        <div key="table"><TableWidget /></div>
      </GridLayout>
    </>
  );
}

Advanced Configurations

Dynamic Grid Configuration

function DynamicGridConfig() {
  const [config, setConfig] = useState({
    cols: 12,
    rowHeight: 60,
    compactType: 'vertical',
    margin: [10, 10]
  });

  const updateConfig = (key, value) => {
    setConfig({ ...config, [key]: value });
  };

  return (
    <div>
      {/* Configuration Panel */}
      <div className="config-panel">
        <label>
          Columns:
          <input
            type="number"
            value={config.cols}
            onChange={(e) => updateConfig('cols', parseInt(e.target.value))}
          />
        </label>
        <label>
          Row Height:
          <input
            type="number"
            value={config.rowHeight}
            onChange={(e) => updateConfig('rowHeight', parseInt(e.target.value))}
          />
        </label>
        <label>
          Compact Type:
          <select
            value={config.compactType}
            onChange={(e) => updateConfig('compactType', e.target.value)}
          >
            <option value="vertical">Vertical</option>
            <option value="horizontal">Horizontal</option>
            <option value={null}>None</option>
          </select>
        </label>
      </div>

      {/* Grid with dynamic config */}
      <GridLayout
        {...config}
        width={1200}
      >
        {/* Widgets */}
      </GridLayout>
    </div>
  );
}

Theme Integration

// Tremor with custom theme
const customTheme = {
  dark: {
    background: '#1a1a1a',
    card: '#2a2a2a',
    text: '#ffffff',
    border: '#3a3a3a'
  },
  light: {
    background: '#ffffff',
    card: '#f9fafb',
    text: '#111827',
    border: '#e5e7eb'
  }
};

function ThemedDashboard({ theme = 'light' }) {
  const colors = customTheme[theme];

  return (
    <div
      className="dashboard"
      style={{
        backgroundColor: colors.background,
        color: colors.text
      }}
    >
      <ResponsiveGridLayout>
        {widgets.map(widget => (
          <div key={widget.id}>
            <Card
              style={{
                backgroundColor: colors.card,
                borderColor: colors.border
              }}
            >
              {renderWidget(widget)}
            </Card>
          </div>
        ))}
      </ResponsiveGridLayout>
    </div>
  );
}

Troubleshooting

Common Issues and Solutions

1. Grid Layout CSS Not Loading

// Ensure CSS imports are correct
import 'react-grid-layout/css/styles.css';
import 'react-resizable/css/styles.css';

// Or import in your main CSS file
/* styles.css */
@import '~react-grid-layout/css/styles.css';
@import '~react-resizable/css/styles.css';

2. Widgets Overlapping

// Set preventCollision to true
<GridLayout
  preventCollision={true}
  compactType="vertical"
  // This will prevent widgets from overlapping
>

3. Responsive Layout Not Working

// Must wrap with WidthProvider
const ResponsiveGridLayout = WidthProvider(Responsive);

// Don't pass width prop when using WidthProvider
<ResponsiveGridLayout
  // width={1200} // Don't do this
  layouts={layouts}
>

4. Tremor Components Not Styled

// Ensure Tailwind processes Tremor files
// tailwind.config.js
module.exports = {
  content: [
    './src/**/*.{js,jsx,ts,tsx}',
    './node_modules/@tremor/**/*.{js,ts,jsx,tsx}', // Important!
  ]
};

5. Performance Issues with Many Widgets

// Use React.memo for widget components
const MemoizedWidget = React.memo(({ data }) => {
  return <Card>{/* Widget content */}</Card>;
}, (prevProps, nextProps) => {
  // Custom comparison
  return prevProps.data === nextProps.data;
});

// Virtualize if > 50 widgets
import { FixedSizeGrid } from 'react-window';