interactive_timeline 0.3.0 copy "interactive_timeline: ^0.3.0" to clipboard
interactive_timeline: ^0.3.0 copied to clipboard

A performant, reusable horizontal timeline widget with LOD ticks, panning, anchored zoom, and customizable markers/ticks.

example/lib/main.dart

import 'dart:math';
import 'package:flutter/material.dart';
import 'package:interactive_timeline/interactive_timeline.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        // This is the theme of your application.
        //
        // TRY THIS: Try running your application with "flutter run". You'll see
        // the application has a purple toolbar. Then, without quitting the app,
        // try changing the seedColor in the colorScheme below to Colors.green
        // and then invoke "hot reload" (save your changes or press the "hot
        // reload" button in a Flutter-supported IDE, or press "r" if you used
        // the command line to start the app).
        //
        // Notice that the counter didn't reset back to zero; the application
        // state is not lost during the reload. To reset the state, use hot
        // restart instead.
        //
        // This works for code too, not just values: Most code changes can be
        // tested with just a hot reload.
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
      ),
      home: const MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatelessWidget {
  const MyHomePage({super.key, required this.title});
  final String title;

  @override
  Widget build(BuildContext context) {
    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',
      ),
      // Ranged event with sticky right alignment and a pole
      TimelineEvent(
        date: now.subtract(const Duration(days: 132)),
        endDate: now.add(const Duration(days: 120)),
        title: 'Sprint 42',
        labelAlign: EventLabelAlign.right,
        stickyLabel: true,
        showPole: true,
        importance: 5,
        spanColor: Colors.orangeAccent,
      ),
      TimelineEvent(date: now, title: 'Today', importance: 10),
      TimelineEvent(
        date: now.add(const Duration(days: 30)),
        title: 'Next month',
        importance: 2,
      ),
      TimelineEvent(
        date: now.add(const Duration(days: 365)),
        title: 'Next year',
      ),
      // Dense cluster to demonstrate stacking and fading
      ...List.generate(2, (idx) {
        final i = idx + 1;
        final random = Random();
        const min = 1;
        const max = 10;
        final startDays = random.nextInt(max - min + 1) + min;
        final endDays = random.nextInt(max - min + 1) + min;
        return TimelineEvent(
          date: now.subtract(Duration(days: startDays)),
          endDate: now.add(Duration(days: endDays)),
          title: 'Cluster $i',
          importance: i == 1 ? 3 : 1,
        );
      }),
    ];

    return Scaffold(
      appBar: AppBar(title: Text(title)),
      body: Padding(
        padding: const EdgeInsets.all(12),
        child: SingleChildScrollView(
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              const Text('Interactive Timeline Demo'),
              const SizedBox(height: 12),
              // Horizontal timeline (default)
              SizedBox(
                height: 160,
                child: TimelineWidget(
                  height: 120,
                  debugMode: false,
                  events: events,
                  showEventSpans: true,
                  showSpanEndPoles: true,
                  spanEndPoleThickness: 1.5,
                  showEventPole: true,
                  markerMaxStackLayers: 5,
                  markerClusterPx: 40,
                  minZoomLOD: TimeScaleLOD.month,
                  maxZoomLOD: TimeScaleLOD.century,
                  enableFisheye: true,
                  fisheyeIntensity: 1.8,
                  fisheyeRadiusPx: 140,
                  fisheyeHardness: 2.7,
                  fisheyeScaleTicks: true,
                  fisheyeScaleMarkers: true,
                  fisheyeScaleLabels: true,
                  fisheyeEnterMs: 140,
                  fisheyeExitMs: 160,
                  fisheyeFollowAlpha: 0.03,
                  fisheyeActivateOnHover: true,
                  fisheyeActivateOnLongPress: true,
                  fisheyeShowIndicator: true,
                  fisheyeEdgeFeatherOpacity: 0.3,
                  fisheyeColor: const Color.fromARGB(255, 0, 164, 246),
                  fisheyeGlowEnabled: false,
                  fisheyeGlowColor: const Color.fromARGB(255, 132, 255, 0),
                  fisheyeGlowOpacity: 0.06,
                  fisheyeGlowRadiusMultiplier: 0.4,
                  fisheyeGlowBlurSigma: 14.0,
                  fisheyeBlendMode: BlendMode.colorBurn,
                  fisheyeGlowBlendMode: BlendMode.screen,
                  tickLabelColor: const Color(0xFF444444),
                  axisThickness: 2,
                  majorTickThickness: 2,
                  minorTickThickness: 1,
                  minorTickColor: Colors.grey.shade500,
                  labelStride: 1,
                  tickLabelStyle: const TextStyle(fontSize: 11),
                  tickLabelFontFamily: 'monospace',
                  labelStyleByLOD: const {
                    TimeScaleLOD.all: TextStyle(
                      fontWeight: FontWeight.w600,
                      color: Colors.grey,
                    ),
                    TimeScaleLOD.year: TextStyle(fontSize: 14),
                  },
                  // Custom tick appearance via painter and offset/scale
                  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,
                      );
                    }
                  },
                  // Event markers as widgets with offset and scale
                  eventMarkerOffset: const Offset(0, -12),
                  eventMarkerScale: 1.0,
                  showDefaultEventMarker: true,
                  eventMarkerBuilder: (ctx, event, info) {
                    return Container(
                      padding: const EdgeInsets.symmetric(
                        horizontal: 6,
                        vertical: 3,
                      ),
                      decoration: BoxDecoration(
                        color: Colors.red.shade400,
                        borderRadius: BorderRadius.circular(6),
                        boxShadow: const [
                          BoxShadow(
                            color: Colors.black26,
                            blurRadius: 4,
                            offset: Offset(0, 2),
                          ),
                        ],
                      ),
                      child: Row(
                        mainAxisSize: MainAxisSize.min,
                        children: [
                          const Icon(
                            Icons.place,
                            size: 12,
                            color: Colors.white,
                          ),
                          const SizedBox(width: 4),
                          Text(
                            event.title,
                            style: const TextStyle(
                              fontSize: 10,
                              color: Colors.white,
                              fontWeight: FontWeight.w600,
                            ),
                          ),
                        ],
                      ),
                    );
                  },
                  onZoomChanged: (z) => debugPrint('zoom: $z'),
                  onEventTap: (e) => ScaffoldMessenger.of(context).showSnackBar(
                    SnackBar(
                      content: Text(
                        'Tapped: ${e.title} @ ${e.date.toIso8601String()}',
                      ),
                    ),
                  ),
                ),
              ),
              const SizedBox(height: 24),
              const Text('Vertical Timeline Demo'),
              const SizedBox(height: 12),
              Row(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  SizedBox(
                    width: 140, // constrain cross-axis thickness
                    height: 300, // desired main-axis extent
                    child: TimelineWidget(
                      height: 120, // cross-axis thickness used by painter
                      orientation: Axis.vertical,
                      debugMode: false,
                      events: events,
                      minZoomLOD: TimeScaleLOD.month,
                      maxZoomLOD: TimeScaleLOD.century,
                      enableFisheye: true,
                      fisheyeIntensity: 1.8,
                      fisheyeRadiusPx: 140,
                      fisheyeHardness: 2.0,
                      fisheyeScaleTicks: true,
                      fisheyeScaleMarkers: true,
                      fisheyeScaleLabels: true,
                      fisheyeEnterMs: 140,
                      fisheyeExitMs: 160,
                      fisheyeFollowAlpha: 0.06,
                      fisheyeActivateOnHover: true,
                      fisheyeActivateOnLongPress: true,
                      fisheyeShowIndicator: true,
                      fisheyeEdgeFeatherOpacity: 0.03,
                      fisheyeColor: Colors.teal,
                      fisheyeGlowEnabled: true,
                      fisheyeGlowColor: Colors.teal,
                      fisheyeGlowOpacity: 0.06,
                      fisheyeGlowRadiusMultiplier: 0.4,
                      fisheyeGlowBlurSigma: 14.0,
                      fisheyeBlendMode: BlendMode.plus,
                      fisheyeGlowBlendMode: BlendMode.screen,
                      tickLabelColor: const Color(0xFF444444),
                      axisThickness: 2,
                      majorTickThickness: 2,
                      minorTickThickness: 1,
                      minorTickColor: Colors.grey.shade500,
                      labelStride: 1,
                      labelStyleByLOD: const {
                        TimeScaleLOD.all: TextStyle(
                          fontSize: 12,
                          fontWeight: FontWeight.w600,
                          color: Colors.grey,
                        ),
                      },
                      onZoomChanged: (z) => debugPrint('zoom: $z'),
                      onEventTap: (e) =>
                          ScaffoldMessenger.of(context).showSnackBar(
                        SnackBar(
                          content: Text(
                            'Tapped: ${e.title} @ ${e.date.toIso8601String()}',
                          ),
                        ),
                      ),
                    ),
                  ),
                  const SizedBox(width: 16),
                  const Expanded(
                    child: Text(
                      'Tips:\n- Drag up/down to pan.\n- Use trackpad/mouse wheel to zoom anchored under the cursor.\n- Tap markers to show a SnackBar.',
                    ),
                  ),
                ],
              ),
              const SizedBox(height: 24),
              const Text('Gestures:'),
              const Text(' - Mouse wheel/trackpad: zoom anchored under cursor'),
              const Text(' - Drag: pan along the axis'),
              const Text(' - Double-tap: center on events midpoint'),
            ],
          ),
        ),
      ),
    );
  }
}
3
likes
140
points
40
downloads

Publisher

unverified uploader

Weekly Downloads

A performant, reusable horizontal timeline widget with LOD ticks, panning, anchored zoom, and customizable markers/ticks.

Homepage
Repository (GitHub)
View/report issues

Topics

#timeline #widget #zoom #visualization #chart

Documentation

API reference

License

MIT (license)

Dependencies

flutter

More

Packages that depend on interactive_timeline