interactive_timeline 0.2.0
interactive_timeline: ^0.2.0 copied to clipboard
A performant, reusable horizontal timeline widget with LOD ticks, panning, anchored zoom, and customizable markers/ticks.
interactive_timeline #
A performant, reusable horizontal timeline widget for Flutter with:
- Anchored zoom (mouse wheel, trackpad, Magic Mouse, pinch) around the cursor/focal point
- Smooth horizontal panning
- Auto-LOD ticks (hours → months → years → decades → centuries → millennia)
- Double-tap to center on events midpoint (or initial center)
- Event markers with tap callback and customizable widget/shape
- Parent scroll suppression (prevents ancestor scrollables from hijacking gestures)
Published on pub.dev.
Screenshot #
[Interactive timeline demo]
Installation (from pub.dev) #
Add to your app's pubspec.yaml:
dependencies:
interactive_timeline: ^0.1.0
Installation (local path in a monorepo) #
Add a path dependency in your app's pubspec.yaml:
dependencies:
interactive_timeline:
path: packages/interactive_timeline
Import it:
import 'package:interactive_timeline/interactive_timeline.dart';
Quick start #
final now = DateTime.now().toUtc();
final events = <TimelineEvent>[
TimelineEvent(date: now.subtract(const Duration(days: 365 * 2)), title: 'Two years ago'),
TimelineEvent(date: now.subtract(const Duration(days: 30)), title: 'Last month'),
TimelineEvent(date: now, title: 'Today'),
TimelineEvent(date: now.add(const Duration(days: 30)), title: 'Next month'),
TimelineEvent(date: now.add(const Duration(days: 365)), title: 'Next year'),
];
SizedBox(
height: 140,
child: TimelineWidget(
height: 120,
events: events,
minZoomLOD: TimeScaleLOD.month,
maxZoomLOD: TimeScaleLOD.century,
tickLabelColor: const Color(0xFF444444),
axisThickness: 2,
majorTickThickness: 2,
minorTickThickness: 1,
minorTickColor: Colors.grey,
labelStride: 1,
labelStyleByLOD: const {
TimeScaleLOD.year: TextStyle(fontSize: 12, fontWeight: FontWeight.w600),
TimeScaleLOD.decade: TextStyle(fontSize: 12),
},
onZoomChanged: (z) => debugPrint('zoom: $z'),
onEventTap: (e) => debugPrint('Tapped: ${e.title}'),
),
)
API (selected) #
- Data:
events(TimelineEvent) - Size:
height - Zoom/scale:
initialZoom,minZoom,maxZoom,minZoomLOD,maxZoomLOD,basePixelsPerMillisecond - Styling:
timelineColor,eventColor,backgroundColor,tickLabelColor,axisThickness,majorTickThickness,minorTickThickness,minorTickColor,labelStride,labelStyleByLOD,tickLabelStyle,tickLabelFontFamily - Callbacks:
onZoomChanged(double),onEventTap(TimelineEvent) - Behavior: anchored zoom, double-tap to center, suppress ancestor pointer events
Event markers
- Per-event overrides on
TimelineEvent:markerOffset,markerScale. - Defaults on
TimelineWidget:eventMarkerOffset,eventMarkerScale. - Custom marker as widget:
TimelineWidget(
events: events,
showDefaultEventMarker: false,
eventMarkerOffset: const Offset(0, -12),
eventMarkerScale: 1.0,
eventMarkerBuilder: (context, event, info) {
return DecoratedBox(
decoration: BoxDecoration(
color: Colors.red,
borderRadius: BorderRadius.circular(8),
),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 3),
child: Text(event.title, style: const TextStyle(color: Colors.white, fontSize: 10)),
),
);
},
onEventTap: (e) => debugPrint('Tap ${e.title}'),
)
- Custom marker as shape (canvas painter):
TimelineWidget(
events: events,
eventMarkerPainter: (canvas, event, info) {
final p = Paint()..color = Colors.purple;
canvas.drawRRect(
RRect.fromRectAndRadius(
Rect.fromCenter(center: info.position, width: 12 * info.markerScale, height: 12 * info.markerScale),
const Radius.circular(3),
),
p,
);
},
)
Ticks
- Custom tick painter and transforms:
TimelineWidget(
tickOffset: const Offset(0, 0),
tickScale: 1.0,
tickPainter: (canvas, tick, ctx) {
final paint = Paint()
..color = tick.isMajor ? ctx.axisColor : ctx.minorColor
..strokeWidth = tick.isMajor ? 2 : 1;
if (!tick.vertical) {
final h = tick.height * ctx.tickScale;
final x = tick.positionMainAxis + ctx.tickOffset.dx;
final y = tick.centerCrossAxis + ctx.tickOffset.dy;
canvas.drawLine(Offset(x, y - h), Offset(x, y + h), paint);
} else {
final h = tick.height * ctx.tickScale;
final x = tick.centerCrossAxis + ctx.tickOffset.dx;
final y = tick.positionMainAxis + ctx.tickOffset.dy;
canvas.drawLine(Offset(x - h, y), Offset(x + h, y), paint);
}
},
tickLabelStyle: const TextStyle(fontSize: 11),
tickLabelFontFamily: 'monospace',
)
Example app (in this repo) #
An example is included at packages/interactive_timeline/example. Run it directly:
cd example
flutter run
The example demonstrates:
- Anchored zoom via wheel/trackpad/pinch
- Horizontal panning
- Double-tap to center on events midpoint
- Event tap callback (shows a SnackBar)
Publish to pub.dev #
- Ensure
pubspec.yamlhasname,description,version,homepage,repository,issue_trackerand a proper SDK/Flutter constraint. - Include a
LICENSEandCHANGELOG.md(both are present in this repo). - Add screenshots/GIFs to your README (optional but recommended).
- Run:
Fix any issues it reports.flutter pub publish --dry-run - Publish:
flutter pub publish - Consumers can depend on it with:
dependencies: interactive_timeline: ^0.1.0
Notes #
- Anchored zoom keeps content under pointer fixed while zooming
- Pooled tick manager for performance
- Deep-time beyond
DateTimepossible with a custom epoch in future
Contributing #
- Fork the repo and create a feature branch from
main. - Development setup:
- Flutter SDK 3.16+ (Dart 3)
- Format and analyze before committing:
dart format .flutter analyze
- Run tests (add more as you contribute):
flutter test
- Example app for manual testing:
cd example && flutter run
- Coding style: keep code clear and well-named; prefer small, readable functions. Follow the included
flutter_lintsrules. - Commits: conventional messages are appreciated (feat:, fix:, docs:, chore:, refactor:, test:).
- Pull Requests: include a brief description, screenshots/GIFs if UI changes, and a changelog entry suggestion.
Releasing and Publishing (pub.dev) #
- Update
CHANGELOG.mdwith a new entry. - Bump
version:inpubspec.yaml(semver). - Verify
README.md,LICENSE, andpubspec.yamlmetadata (homepage,repository,issue_tracker,topics). - Ensure screenshots referenced in the README (e.g.,
demo1.png) are checked in. - Dry run:
dart format . flutter pub get flutter pub publish --dry-run - If clean, publish:
flutter pub publish - Tag the release (optional but recommended):
git tag v<version> git push --tags
Republishing / Hotfixes #
pub.dev does not allow re-uploading the same version. For any fix, bump the version (usually patch):
- Update
CHANGELOG.md(e.g., “0.1.1 – Fix label style precedence”). - Bump
version:inpubspec.yamlto the next patch/minor. - Re-run the publish steps above (dry-run, then publish).
- If you accidentally published a broken version, you can retract it from the pub.dev UI (Manage Versions → Retract). Consumers on that exact version will be warned.
GitHub Pages website #
- A minimal site is available under
doc/and can be enabled via GitHub Pages (branch:main, folder:/doc). - After it’s live, consider setting
homepage:inpubspec.yamlto the Pages URL. - If you prefer, move
demo1.pngintodoc/(e.g.,doc/assets/) and update links accordingly. Keeping it at the repo root also works for pub.dev README rendering.