ecg_paper 0.1.0
ecg_paper: ^0.1.0 copied to clipboard
Scrollable ECG "paper" widget (25 mm/s, 10 mm/mV) for Flutter. Feed mV samples and sample rate to render clinical-style grids and trace.
import 'dart:io';
import 'dart:math' as math;
import 'package:flutter/material.dart';
import 'package:path_provider/path_provider.dart';
import 'package:screenshot/screenshot.dart';
import 'package:ecg_paper/ecg_paper.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(home: DemoPage());
}
}
class DemoPage extends StatefulWidget {
const DemoPage({super.key});
@override
State<DemoPage> createState() => _DemoPageState();
}
class _DemoPageState extends State<DemoPage> {
final fs = 512.0;
final seconds = 8.0;
late final List<double> samplesMv;
final sc = ScreenshotController();
bool fixedY = false;
bool decimation = true;
@override
void initState() {
super.initState();
final n = (fs * seconds).toInt();
samplesMv = List<double>.generate(n, (i) {
final t = i / fs;
// crude pseudo-ECG shape: base sine + small spikes
final s = math.sin(2 * math.pi * 1.3 * t);
final qrs = (i % 60 == 0) ? 1.8 : 0.0; // sparse spike
final noise = 0.05 * math.sin(2 * math.pi * 50 * t);
return 0.7 * s + qrs + noise; // ~±1 mV
});
}
Future<void> _savePng() async {
final bytes = await sc.capture(pixelRatio: 2.0);
if (bytes == null) return;
final dir = await getApplicationDocumentsDirectory();
final file = File('${dir.path}/ecg_${DateTime.now().millisecondsSinceEpoch}.png');
await file.writeAsBytes(bytes);
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Saved: ${file.path}')),
);
}
@override
Widget build(BuildContext context) {
final style = EcgStyle(
height: 220,
fixedYRangeMv: fixedY ? 2.0 : null, // toggle lock ±2 mV
);
final widgetECG = EcgScrollableWidget(
samples: samplesMv,
sampleRateHz: fs,
durationSeconds: seconds,
style: style,
enableDecimation: decimation,
autoDetectUnits: true,
);
return Scaffold(
appBar: AppBar(title: const Text('ECG Paper Demo')),
body: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Screenshot(controller: sc, child: widgetECG),
const SizedBox(height: 12),
Wrap(
spacing: 12,
children: [
ElevatedButton(
onPressed: _savePng,
child: const Text('Save PNG'),
),
FilterChip(
label: const Text('Fixed ±2 mV'),
selected: fixedY,
onSelected: (v) => setState(() => fixedY = v),
),
FilterChip(
label: const Text('Decimation'),
selected: decimation,
onSelected: (v) => setState(() => decimation = v),
),
],
),
],
),
),
);
}
}