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 ​