Skip to content

Data Sources

The data source system connects dashboard components to live data. It defines interfaces for fetching data, transforming it into chart-ready format, and keeping it up to date with polling.

Overview

ChartDataSource Interface

The ChartDataSource interface defines how data is fetched and transformed into DataPoint lists:

dart
abstract class ChartDataSource {
  String get displayName;
  String get description;

  Future<List<DataPoint>> fetchData({
    required ChartFieldMapping mapping,
    Map<String, dynamic>? filters,
    String? entityType,
  });
}

ChartDataSourceRegistry is a singleton for chart data source implementations. It registers ChartDataSourceType.static and ChartDataSourceType.api by default; apps can register ChartDataSourceType.entity or replace any built-in source:

dart
initializeChartSystem();

ChartDataSourceRegistry().register(
  ChartDataSourceType.entity,
  MyEntityChartDataSource(),
);
ParameterPurpose
mappingField mapping configuration (label, value, series fields)
filtersImplementation-specific filters (e.g., endpoint URL, date range)
entityTypeOptional entity type for entity-based data sources

Implementing a Custom Data Source

dart
class LmsEnrollmentDataSource implements ChartDataSource {
  @override
  String get displayName => 'LMS Enrollments';

  @override
  String get description => 'Fetch enrollment data from LMS API';

  @override
  Future<List<DataPoint>> fetchData({
    required ChartFieldMapping mapping,
    Map<String, dynamic>? filters,
    String? entityType,
  }) async {
    final response = await lmsApi.getEnrollments(
      courseLevel: filters?['level'] as String?,
    );

    return response.map((enrollment) => DataPoint(
      label: enrollment[mapping.labelField] as String,
      value: enrollment[mapping.valueField] as num,
      series: mapping.seriesField != null
        ? enrollment[mapping.seriesField!] as String?
        : null,
    )).toList();
  }
}

DataPoint Model

The universal data model for all chart types. Supports 3D data (label, value, series) which covers the vast majority of dashboard use cases.

dart
class DataPoint {
  final String? label;    // Category / x-axis (Dimension 1)
  final num? value;       // Primary value / y-axis (Dimension 2)
  final String? series;   // Grouping / secondary category (Dimension 3)

  DataPoint({this.label, this.value, this.series});

  // Smart defaults factory
  factory DataPoint.withDefaults({
    String? label,
    num? value,
    String? series,
    int? index,
  });
}

When to Use Each Dimension

DimensionFieldExample (LMS)
Dimension 1labelCourse name, month, trainer name
Dimension 2valueEnrollment count, score, completion rate
Dimension 3seriesCourse level (beginner/intermediate/advanced)

The series field is required for multi-series chart types (groupedBar, multiLine, stackedBar) and optional for single-series charts.

ChartFieldMapping

Maps source data fields to chart dimensions:

dart
class ChartFieldMapping {
  final String labelField;     // Maps to DataPoint.label
  final String valueField;     // Maps to DataPoint.value
  final String? seriesField;   // Maps to DataPoint.series (optional)

  // Smart defaults: labelField='name', valueField='value'
  factory ChartFieldMapping.withDefaults({...});
}

LMS Example

dart
// Map enrollment API response to chart data
final enrollmentMapping = ChartFieldMapping(
  labelField: 'courseName',      // Course name on x-axis
  valueField: 'enrollmentCount', // Enrollment count on y-axis
  seriesField: 'level',          // Group by course level
);

Validation

ChartFieldMapping validates its inputs on construction:

  • labelField must not be empty
  • valueField must not be empty
  • labelField and valueField must be different

ApiChartDataSource

The built-in data source for HTTP API endpoints. It fetches JSON, extracts the data array, and transforms it to DataPoint instances using the field mapping.

dart
class ApiChartDataSource implements ChartDataSource {
  @override
  Future<List<DataPoint>> fetchData({
    required ChartFieldMapping mapping,
    Map<String, dynamic>? filters,
    String? entityType,
  }) async {
    // filters['endpoint'] — required API URL
    // filters['dataArrayPath'] — optional path to data array in JSON
  }
}

Configuration via Filters

Filter KeyRequiredDescription
endpointYesFull URL or relative path to fetch
dataArrayPathNoPath to the data array in the response JSON

Nested JSON Traversal

The ApiChartDataSource supports dot notation and array flattening for nested JSON structures:

dart
// Simple path: extract "items" from root
filters: {'endpoint': url, 'dataArrayPath': 'items'}

// Dot notation: extract nested data
filters: {'endpoint': url, 'dataArrayPath': 'data.enrollments'}

// Array flattening: flatten nested arrays
// Given: { "courses": [{ "modules": [...] }] }
filters: {'endpoint': url, 'dataArrayPath': 'courses[].modules'}

Auto-Detection

When dataArrayPath is not specified, the data source auto-detects the data array by:

  1. If the root is an array, use it directly
  2. Try common keys: data, items, results
  3. Try any array value in the root object

Nested Field Extraction

Field names in ChartFieldMapping also support dot notation:

dart
final mapping = ChartFieldMapping(
  labelField: 'course.name',        // Nested object field
  valueField: 'stats.enrollments',  // Nested numeric field
);

LMS Example: Course Completion API

dart
// API response:
// {
//   "data": {
//     "completions": [
//       {"courseName": "Dart Basics", "completionRate": 87, "level": "beginner"},
//       {"courseName": "Flutter Advanced", "completionRate": 62, "level": "advanced"}
//     ]
//   }
// }

final dataSource = ApiChartDataSource();
final data = await dataSource.fetchData(
  mapping: ChartFieldMapping(
    labelField: 'courseName',
    valueField: 'completionRate',
    seriesField: 'level',
  ),
  filters: {
    'endpoint': 'https://api.lms.example.com/completions',
    'dataArrayPath': 'data.completions',
  },
);
// Result: [DataPoint(label: 'Dart Basics', value: 87, series: 'beginner'), ...]

ChartData

The ChartData class combines a list of DataPoint instances with a ChartType. It validates the data on construction based on the chart type requirements:

dart
class ChartData {
  final List<DataPoint> data;
  final ChartType type;

  ChartData({required this.data, required this.type});
}

Validation Rules

Chart TypeRequirements
pie, donutEach point must have both label and value
gaugeExactly one point with a value
groupedBar, multiLine, stackedBarEach point must have a series field
bar, line, area, sparklineEach point must have a value

DashboardDataStore

The DashboardDataStore is a MobX store for managing component-level data fetching and polling. It handles API and static data sources, validates response format through a ComponentDataFormat, and supports auto-refresh.

Unlike ApiChartDataSource, DashboardDataStore uses ApiDataSource, which sends requests through vyuh.network with the current auth token. If a server response uses the canonical {success, data, error} API envelope, ApiDataSource unwraps the inner data value before validation.

dart
class DashboardDataStore {
  final String storeKey;
  final ApiDataSource apiDataSource;
  final ComponentDataFormat dataFormat;

  // Observable state
  @observable dynamic data;
  @observable bool isLoading;
  @observable String? error;
  @observable DateTime? lastFetch;
  @observable bool isPolling;

  // Fetch methods
  Future<void> fetchStatic(dynamic staticData);
  Future<void> fetchFromApi(String endpoint, {String method = 'GET'});

  // Polling
  void startPolling({
    required String sourceType,
    required dynamic sourceData,
    required int intervalSeconds,
  });
  void stopPolling();

  // Manual refresh
  Future<void> refresh({required String sourceType, required dynamic sourceData});

  // Cleanup
  void clear();
}

Polling Example

dart
class EnrollmentSummaryFormat implements ComponentDataFormat {
  @override
  String get displayName => 'Enrollment Summary';

  @override
  String get description => 'Enrollment summary envelope';

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

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

final store = DashboardDataStore(
  storeKey: 'enrollment-stats',
  apiDataSource: ApiDataSource(baseUrl: 'https://api.lms.example.com'),
  dataFormat: EnrollmentSummaryFormat(),
);

// Start polling every 30 seconds
store.startPolling(
  sourceType: 'api',
  sourceData: {'endpoint': '/api/enrollments/summary'},
  intervalSeconds: 30,
);

// React to data changes in UI
Observer(
  builder: (_) {
    if (store.isLoading) return const CircularProgressIndicator();
    if (store.error != null) return Text('Error: ${store.error}');
    return buildDashboard(store.data);
  },
)

ChartFieldProvider

An optional interface for providing field suggestions in the config panel. Apps can implement this to offer autocomplete when users configure field mappings.

dart
abstract class ChartFieldProvider {
  String get displayName;
  String get description;

  List<String> getFieldsForEntity(String entityType);
  List<String> getNumericFieldsForEntity(String entityType);
}

LMS Implementation

dart
class LmsFieldProvider implements ChartFieldProvider {
  @override
  String get displayName => 'LMS Entities';

  @override
  String get description => 'Field suggestions for LMS entities';

  @override
  List<String> getFieldsForEntity(String entityType) {
    return switch (entityType) {
      'courses' => ['name', 'level', 'status', 'instructor', 'durationMinutes'],
      'enrollments' => ['studentName', 'courseName', 'enrollDate', 'status', 'score'],
      'trainers' => ['name', 'department', 'rating', 'coursesCount'],
      _ => [],
    };
  }

  @override
  List<String> getNumericFieldsForEntity(String entityType) {
    return switch (entityType) {
      'courses' => ['durationMinutes'],
      'enrollments' => ['score'],
      'trainers' => ['rating', 'coursesCount'],
      _ => [],
    };
  }
}

Next Steps