Skip to content

Building Dashboards

This guide walks through building a complete LMS analytics dashboard step by step. You will define the layout, create rows with stat cards and charts, configure data sources, and wire up data polling.

What We Are Building

An LMS analytics dashboard with three rows:

  1. Stat cards row -- Total Students, Active Courses, Completion Rate, Average Score (4 columns)
  2. Charts row -- Enrollment trend line chart (wide) + Course distribution pie (narrow)
  3. Detail row -- Recent completions data table (full width)

Step 1: Define Component Descriptors

Before building the layout, you need components. Define them as ComponentDescriptor instances.

Stat Card

dart
PropertyCollection buildStatCardProperties() {
  return (PropertyCollectionBuilder()
    ..group('content', 'Content')
    ..string('title', 'Title', defaultValue: 'Metric')
    ..integer('value', 'Value', defaultValue: 0)
    ..string('unit', 'Unit', defaultValue: '')
    ..string('trend', 'Trend', defaultValue: '+0%')
    ..group('appearance', 'Appearance')
    ..enumeration('trendDirection', 'Direction',
      values: ['up', 'down', 'neutral'],
      defaultValue: 'up',
    )
    ..string('iconName', 'Icon', defaultValue: 'people')
    ..string('color', 'Accent Color', defaultValue: '#2196F3')
  ).buildCollection();
}

final statCardDescriptor = ComponentDescriptor(
  schemaType: 'lms.stat_card',
  componentKey: 'lms_stat_card',
  displayName: 'Stat Card',
  description: 'Single metric with trend indicator',
  icon: Icons.analytics,
  category: lmsCategory,
  minCellHeight: 100,
  properties: buildStatCardProperties,
  render: (context, props) => LmsStatCardView(properties: props),
);

Line Chart

dart
PropertyCollection buildLineChartProperties() {
  return (PropertyCollectionBuilder()
    ..group('data', 'Data Source')
    ..string('endpoint', 'API Endpoint',
      defaultValue: '/api/enrollments/monthly',
    )
    ..string('labelField', 'Label Field', defaultValue: 'month')
    ..string('valueField', 'Value Field', defaultValue: 'count')
    ..string('seriesField', 'Series Field', defaultValue: 'level')
    ..group('appearance', 'Appearance')
    ..boolean('showLegend', 'Show Legend', defaultValue: true)
    ..boolean('showArea', 'Show Area Fill', defaultValue: false)
    ..string('seriesColors', 'Series Colors (JSON)', defaultValue: '')
  ).buildCollection();
}

final lineChartDescriptor = ComponentDescriptor(
  schemaType: 'lms.line_chart',
  componentKey: 'lms_line_chart',
  displayName: 'Enrollment Trend',
  icon: Icons.show_chart,
  category: lmsCategory,
  minCellHeight: 250,
  defaultSize: const Size(4, 3),
  properties: buildLineChartProperties,
  render: (context, props) => LmsLineChartView(properties: props),
);

Pie Chart

dart
final pieChartDescriptor = ComponentDescriptor(
  schemaType: 'lms.pie_chart',
  componentKey: 'lms_pie_chart',
  displayName: 'Course Distribution',
  icon: Icons.pie_chart,
  category: lmsCategory,
  minCellHeight: 250,
  properties: () => (PropertyCollectionBuilder()
    ..string('endpoint', 'API Endpoint',
      defaultValue: '/api/courses/by-level',
    )
    ..string('labelField', 'Label Field', defaultValue: 'level')
    ..string('valueField', 'Value Field', defaultValue: 'count')
    ..boolean('showLegend', 'Show Legend', defaultValue: true)
  ).buildCollection(),
  render: (context, props) => LmsPieChartView(properties: props),
);

Data Table

dart
final completionsTableDescriptor = ComponentDescriptor(
  schemaType: 'lms.completions_table',
  componentKey: 'lms_completions_table',
  displayName: 'Recent Completions',
  icon: Icons.table_chart,
  category: lmsCategory,
  minCellHeight: 200,
  defaultSize: const Size(6, 3),
  properties: () => (PropertyCollectionBuilder()
    ..string('endpoint', 'API Endpoint',
      defaultValue: '/api/completions/recent',
    )
    ..integer('limit', 'Max Rows', defaultValue: 10)
  ).buildCollection(),
  render: (context, props) => LmsCompletionsTableView(properties: props),
);

Step 2: Register Components

Group all components into a DashboardDescriptor and register:

dart
const lmsCategory = ComponentCategory(
  id: 'lms',
  displayName: 'LMS',
  icon: Icons.school,
  sortOrder: 10,
);

final registry = DashboardEditorRegistry([
  DashboardDescriptor(
    name: 'LMS',
    description: 'Learning Management System dashboards',
    tags: ['education', 'analytics'],
    components: [
      statCardDescriptor,
      lineChartDescriptor,
      pieChartDescriptor,
      completionsTableDescriptor,
    ],
  ),
);

Step 3: Define the Layout

Create the three-row layout with appropriate flex values. Remember: each row has 6 flex units total.

dart
// Row 1: Four stat cards (flex 2+1+1+2 = 6)
final statsRow = DashboardRow(
  id: 'row-stats',
  order: 0,
  title: 'Key Metrics',
  height: 140,
  columns: [
    DashboardColumn.withComponent('col-students',
      ComponentInstance.withDefaults(
        id: 's-students',
        componentKey: 'lms_stat_card',
        registry: registry,
        configOverrides: {
          'title': 'Total Students',
          'value': 1247,
          'trend': '+12%',
          'trendDirection': 'up',
          'iconName': 'people',
          'color': '#2196F3',
        },
      ),
      flex: 2,
    ),
    DashboardColumn.withComponent('col-courses',
      ComponentInstance.withDefaults(
        id: 's-courses',
        componentKey: 'lms_stat_card',
        registry: registry,
        configOverrides: {
          'title': 'Active Courses',
          'value': 42,
          'trend': '+3',
          'trendDirection': 'up',
          'iconName': 'book',
          'color': '#4CAF50',
        },
      ),
      flex: 1,
    ),
    DashboardColumn.withComponent('col-rate',
      ComponentInstance.withDefaults(
        id: 's-rate',
        componentKey: 'lms_stat_card',
        registry: registry,
        configOverrides: {
          'title': 'Completion Rate',
          'value': 87,
          'unit': '%',
          'trend': '+5%',
          'trendDirection': 'up',
          'iconName': 'check_circle',
          'color': '#FF9800',
        },
      ),
      flex: 1,
    ),
    DashboardColumn.withComponent('col-score',
      ComponentInstance.withDefaults(
        id: 's-score',
        componentKey: 'lms_stat_card',
        registry: registry,
        configOverrides: {
          'title': 'Average Score',
          'value': 78,
          'unit': '%',
          'trend': '-2%',
          'trendDirection': 'down',
          'iconName': 'grade',
          'color': '#9C27B0',
        },
      ),
      flex: 2,
    ),
  ],
);

// Row 2: Charts (flex 4+2 = 6)
final chartsRow = DashboardRow(
  id: 'row-charts',
  order: 1,
  title: 'Trends & Distribution',
  height: 300,
  columns: [
    DashboardColumn.withComponent('col-trend',
      ComponentInstance.withDefaults(
        id: 's-trend',
        componentKey: 'lms_line_chart',
        registry: registry,
        title: 'Enrollment Trends',
      ),
      flex: 4,
    ),
    DashboardColumn.withComponent('col-dist',
      ComponentInstance.withDefaults(
        id: 's-dist',
        componentKey: 'lms_pie_chart',
        registry: registry,
        title: 'Course Distribution',
      ),
      flex: 2,
    ),
  ],
);

// Row 3: Detail table (flex 6 = full width)
final detailRow = DashboardRow(
  id: 'row-detail',
  order: 2,
  title: 'Recent Completions',
  height: 250,
  columns: [
    DashboardColumn.withComponent('col-table',
      ComponentInstance.withDefaults(
        id: 's-table',
        componentKey: 'lms_completions_table',
        registry: registry,
      ),
      flex: 6,
    ),
  ],
);

// Assemble the dashboard
final lmsDashboard = DashboardLayout(
  metadata: {
    'apiBaseUrl': 'https://api.lms.example.com',
    'name': 'LMS Analytics',
    'description': 'Learning Management System dashboard',
  },
  rows: [statsRow, chartsRow, detailRow],
);

Step 4: Wire Up the Engine

Create the engine, add extensions, and wrap it in a controller:

dart
final controller = DashboardEditorController(
  initialLayout: lmsDashboard,
  registry: registry,
);

The controller automatically creates and wires up all four built-in extensions: history, persistence, selection, and analytics.

Step 5: Configure Data Sources

In your component render callbacks, set up data fetching:

dart
class EnrollmentTrendDataFormat implements ComponentDataFormat {
  @override
  String get displayName => 'Enrollment Trend';

  @override
  String get description => 'Expected enrollment trend response shape';

  @override
  Map<String, dynamic> getExpectedFormat() => {
    'data': {'type': 'array', 'required': true},
  };

  @override
  ValidationResult validate(dynamic data) {
    return DataFormatValidator.validate(data, this);
  }
}

final enrollmentTrendDataFormat = EnrollmentTrendDataFormat();

class LmsLineChartView extends StatefulWidget {
  final PropertyCollection properties;
  const LmsLineChartView({super.key, required this.properties});

  @override
  State<LmsLineChartView> createState() => _LmsLineChartViewState();
}

class _LmsLineChartViewState extends State<LmsLineChartView> {
  late final DashboardDataStore _store;

  @override
  void initState() {
    super.initState();
    final endpoint = widget.properties.stringValue('endpoint')
        ?? '/api/enrollments/monthly';

    _store = DashboardDataStore(
      storeKey: 'enrollment-trends',
      apiDataSource: ApiDataSource(baseUrl: 'https://api.lms.example.com'),
      dataFormat: enrollmentTrendDataFormat,
    );

    _store.fetchFromApi(endpoint);
  }

  @override
  Widget build(BuildContext context) {
    return Observer(
      builder: (_) {
        if (_store.isLoading) return const Center(child: CircularProgressIndicator());
        if (_store.error != null) return Center(child: Text(_store.error!));

        // Transform data and render chart
        final chartData = transformToChartData(_store.data);
        final renderer = ChartRendererFactory().createRenderer(chartData);
        return renderer.build(context, chartData, showLegend: true);
      },
    );
  }

  @override
  void dispose() {
    _store.clear();
    super.dispose();
  }
}

Step 6: Set Up Polling

For real-time data, start polling in the data store:

dart
// In initState or after initial fetch
_store.startPolling(
  sourceType: 'api',
  sourceData: {'endpoint': endpoint, 'method': 'GET'},
  intervalSeconds: 30,
);

The store automatically handles:

  • Background fetching at the specified interval
  • Updating the observable data property
  • Continuing polling even if a fetch fails
  • Stopping when stopPolling() is called or the store is cleared

Step 7: Render the Editor

Display the full dashboard editor with canvas, palette, and config panel:

dart
final dashboardProperties = buildDashboardProperties();
final layoutSettings = buildDashboardLayoutSettingsProperties();
final previewModeNotifier = ValueNotifier(false);
final paletteController = PaletteVisibilityController();

@override
Widget build(BuildContext context) {
  return Scaffold(
    body: DashboardDesignerTab(
      controller: controller,
      dashboardProperties: dashboardProperties,
      layoutSettings: layoutSettings,
      previewModeNotifier: previewModeNotifier,
      paletteController: paletteController,
    ),
  );
}

Modifying the Dashboard Programmatically

After the initial layout, you can modify the dashboard through the engine API:

dart
// Add a new row below the stat cards
final newRowId = await controller.addRow(position: 1);

// Split it into 3 columns
await controller.splitRow(newRowId, 3);

// Place a component in the first column
await controller.addComponent(
  rowId: newRowId,
  columnId: controller.engine.layout.rows[1].columns[0].id,
  componentKey: 'lms_stat_card',
);

// Resize columns: 3+2+1 = 6
await controller.updateRowFlex(
  rowId: newRowId,
  flexByColumnId: {
    controller.engine.layout.rows[1].columns[0].id: 3,
    controller.engine.layout.rows[1].columns[1].id: 2,
    controller.engine.layout.rows[1].columns[2].id: 1,
  },
);

// Undo everything
await controller.undo();
await controller.undo();
await controller.undo();
await controller.undo();

Next Steps