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:
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:
initializeChartSystem();
ChartDataSourceRegistry().register(
ChartDataSourceType.entity,
MyEntityChartDataSource(),
);| Parameter | Purpose |
|---|---|
mapping | Field mapping configuration (label, value, series fields) |
filters | Implementation-specific filters (e.g., endpoint URL, date range) |
entityType | Optional entity type for entity-based data sources |
Implementing a Custom Data Source
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.
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
| Dimension | Field | Example (LMS) |
|---|---|---|
| Dimension 1 | label | Course name, month, trainer name |
| Dimension 2 | value | Enrollment count, score, completion rate |
| Dimension 3 | series | Course 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:
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
// 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:
labelFieldmust not be emptyvalueFieldmust not be emptylabelFieldandvalueFieldmust 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.
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 Key | Required | Description |
|---|---|---|
endpoint | Yes | Full URL or relative path to fetch |
dataArrayPath | No | Path to the data array in the response JSON |
Nested JSON Traversal
The ApiChartDataSource supports dot notation and array flattening for nested JSON structures:
// 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:
- If the root is an array, use it directly
- Try common keys:
data,items,results - Try any array value in the root object
Nested Field Extraction
Field names in ChartFieldMapping also support dot notation:
final mapping = ChartFieldMapping(
labelField: 'course.name', // Nested object field
valueField: 'stats.enrollments', // Nested numeric field
);LMS Example: Course Completion API
// 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:
class ChartData {
final List<DataPoint> data;
final ChartType type;
ChartData({required this.data, required this.type});
}Validation Rules
| Chart Type | Requirements |
|---|---|
pie, donut | Each point must have both label and value |
gauge | Exactly one point with a value |
groupedBar, multiLine, stackedBar | Each point must have a series field |
bar, line, area, sparkline | Each 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.
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
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.
abstract class ChartFieldProvider {
String get displayName;
String get description;
List<String> getFieldsForEntity(String entityType);
List<String> getNumericFieldsForEntity(String entityType);
}LMS Implementation
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
- Chart System -- Understand chart renderers and types
- Data Binding Guide -- Connect components to live data
- LMS Dashboard Example -- Complete working example