Skip to content

Stats Extension

The Stats extension provides reactive access to graph statistics. All properties are MobX observables, so UI components automatically update when the graph changes.

Quick Start

Stats is included by default and available via controller.stats:

dart
// Access stats via controller
final nodeCount = controller.stats?.nodeCount ?? 0;
final connectionCount = controller.stats?.connectionCount ?? 0;
final zoomPercent = controller.stats?.zoomPercent ?? 100;

Reactive UI

Wrap stats access in an Observer for reactive updates:

dart
import 'package:flutter_mobx/flutter_mobx.dart';

Observer(
  builder: (_) {
    final stats = controller.stats;
    if (stats == null) return const SizedBox.shrink();

    return Text('${stats.nodeCount} nodes, ${stats.connectionCount} connections');
  },
)

Node Statistics

PropertyTypeDescription
nodeCountintTotal number of nodes
visibleNodeCountintNon-hidden nodes
lockedNodeCountintLocked (non-draggable) nodes
groupCountintNumber of GroupNode instances
commentCountintNumber of CommentNode instances
regularNodeCountintNodes excluding groups and comments
nodesByTypeMap<String, int>Breakdown by node type

Node Type Breakdown

dart
Observer(
  builder: (_) {
    final nodesByType = controller.stats?.nodesByType ?? {};

    return Column(
      children: nodesByType.entries.map((entry) {
        return Text('${entry.key}: ${entry.value}');
      }).toList(),
    );
  },
)

// Output might be:
// process: 5
// decision: 3
// start: 1
// end: 2

Connection Statistics

PropertyTypeDescription
connectionCountintTotal connections
labeledConnectionCountintConnections with labels
avgConnectionsPerNodedoubleAverage connections per node
dart
Observer(
  builder: (_) {
    final stats = controller.stats;
    if (stats == null) return const SizedBox.shrink();

    return Column(
      children: [
        Text('${stats.connectionCount} connections'),
        Text('${stats.labeledConnectionCount} with labels'),
        Text('Avg: ${stats.avgConnectionsPerNode.toStringAsFixed(1)} per node'),
      ],
    );
  },
)

Selection Statistics

PropertyTypeDescription
selectedNodeCountintNumber of selected nodes
selectedConnectionCountintNumber of selected connections
selectedCountintTotal selected (nodes + connections)
hasSelectionboolWhether anything is selected
isMultiSelectionboolWhether multiple items are selected

Selection-Aware UI

dart
Observer(
  builder: (_) {
    final stats = controller.stats;
    if (stats == null) return const SizedBox.shrink();

    if (!stats.hasSelection) {
      return const Text('Nothing selected');
    }

    return Column(
      children: [
        if (stats.selectedNodeCount > 0)
          Text('${stats.selectedNodeCount} nodes'),
        if (stats.selectedConnectionCount > 0)
          Text('${stats.selectedConnectionCount} connections'),
        if (stats.isMultiSelection)
          const Text('(multi-selection)'),
      ],
    );
  },
)

Viewport Statistics

PropertyTypeDescription
viewportObservable<GraphViewport>Raw viewport observable
zoomdoubleCurrent zoom level (e.g., 1.0, 0.5, 2.0)
zoomPercentintZoom as percentage (e.g., 100, 50, 200)
panOffsetCurrent pan offset in graph coordinates
lodLevelStringCurrent LOD level: 'minimal', 'standard', or 'full'

Zoom Display

dart
Observer(
  builder: (_) {
    final zoomPercent = controller.stats?.zoomPercent ?? 100;
    return Text('$zoomPercent%');
  },
)

Viewport Coordinates

dart
Observer(
  builder: (_) {
    final pan = controller.stats?.pan ?? Offset.zero;
    return Text('Position: (${pan.dx.toInt()}, ${pan.dy.toInt()})');
  },
)

Bounds Statistics

PropertyTypeDescription
boundsRectBounding rectangle of all nodes
boundsWidthdoubleWidth of node bounds
boundsHeightdoubleHeight of node bounds
boundsCenterOffsetCenter point of the graph
boundsAreadoubleTotal area of the bounds
dart
Observer(
  builder: (_) {
    final stats = controller.stats;
    if (stats == null) return const SizedBox.shrink();

    return Text(
      'Canvas: ${stats.boundsWidth.toInt()} × ${stats.boundsHeight.toInt()} px'
    );
  },
)

Performance Statistics

PropertyTypeDescription
nodesInViewportintNodes currently visible in viewport
isLargeGraphboolWhether graph has > 100 nodes
densitydoubleNodes per million square units

Performance Indicators

dart
Observer(
  builder: (_) {
    final stats = controller.stats;
    if (stats == null) return const SizedBox.shrink();

    return Column(
      children: [
        Text('Visible: ${stats.nodesInViewport}/${stats.nodeCount}'),
        if (stats.isLargeGraph)
          const Text('Large graph - LOD recommended'),
      ],
    );
  },
)

Summary Helpers

Pre-formatted strings for common displays:

PropertyExample Output
summary"25 nodes, 40 connections"
selectionSummary"3 nodes, 2 connections selected" or "Nothing selected"
viewportSummary"100% at (0, 0)"
boundsSummary"2400 × 1800 px"

Status Bar Example

dart
Observer(
  builder: (_) {
    final stats = controller.stats;
    if (stats == null) return const SizedBox.shrink();

    return Row(
      mainAxisAlignment: MainAxisAlignment.spaceBetween,
      children: [
        Text(stats.summary),
        Text(stats.viewportSummary),
        Text(stats.selectionSummary),
      ],
    );
  },
)

// Output: "25 nodes, 40 connections | 100% at (0, 0) | 3 nodes selected"

Observable Collections

For fine-grained reactivity, access raw observables:

dart
// Direct observable access
final nodes = controller.stats?.nodes;           // ObservableMap<String, Node>
final connections = controller.stats?.connections;  // ObservableList<Connection>
final selectedNodeIds = controller.stats?.selectedNodeIds;     // ObservableSet<String>
final selectedConnectionIds = controller.stats?.selectedConnectionIds; // ObservableSet<String>
final viewport = controller.stats?.viewport;      // Observable<GraphViewport>

Granular Observers

Each stat can have its own Observer for minimal rebuilds:

dart
Row(
  children: [
    // Only rebuilds when node count changes
    Observer(
      builder: (_) => Text('${controller.stats?.nodeCount ?? 0} nodes'),
    ),
    const SizedBox(width: 16),
    // Only rebuilds when zoom changes
    Observer(
      builder: (_) => Text('${controller.stats?.zoomPercent ?? 100}%'),
    ),
    const SizedBox(width: 16),
    // Only rebuilds when selection changes
    Observer(
      builder: (_) => Text(controller.stats?.selectionSummary ?? ''),
    ),
  ],
)

Complete Example

dart
class GraphStatusBar extends StatelessWidget {
  final NodeFlowController controller;

  const GraphStatusBar({super.key, required this.controller});

  @override
  Widget build(BuildContext context) {
    return Container(
      padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
      color: Theme.of(context).colorScheme.surfaceVariant,
      child: Observer(
        builder: (_) {
          final stats = controller.stats;
          if (stats == null) return const SizedBox.shrink();

          return Row(
            children: [
              // Graph summary
              _StatChip(
                icon: Icons.circle,
                label: '${stats.nodeCount} nodes',
              ),
              const SizedBox(width: 12),
              _StatChip(
                icon: Icons.arrow_forward,
                label: '${stats.connectionCount} connections',
              ),

              const Spacer(),

              // Viewport info
              _StatChip(
                icon: Icons.zoom_in,
                label: '${stats.zoomPercent}%',
              ),
              const SizedBox(width: 12),

              // Selection info
              if (stats.hasSelection)
                _StatChip(
                  icon: Icons.select_all,
                  label: stats.selectionSummary,
                ),

              // Performance warning
              if (stats.isLargeGraph)
                const Padding(
                  padding: EdgeInsets.only(left: 12),
                  child: Tooltip(
                    message: 'Large graph - consider enabling LOD',
                    child: Icon(Icons.warning, size: 16),
                  ),
                ),
            ],
          );
        },
      ),
    );
  }
}

class _StatChip extends StatelessWidget {
  final IconData icon;
  final String label;

  const _StatChip({required this.icon, required this.label});

  @override
  Widget build(BuildContext context) {
    return Row(
      mainAxisSize: MainAxisSize.min,
      children: [
        Icon(icon, size: 14),
        const SizedBox(width: 4),
        Text(label),
      ],
    );
  }
}

API Reference

StatsExtension Properties

PropertyTypeReactiveDescription
nodesObservableMap<String, Node>YesAll nodes
connectionsObservableList<Connection>YesAll connections
selectedNodeIdsObservableSet<String>YesSelected node IDs
selectedConnectionIdsObservableSet<String>YesSelected connection IDs
viewportObservable<GraphViewport>YesViewport state
nodeCountintDerivedTotal nodes
visibleNodeCountintDerivedNon-hidden nodes
lockedNodeCountintDerivedLocked nodes
groupCountintDerivedGroup nodes
commentCountintDerivedComment nodes
regularNodeCountintDerivedRegular nodes
nodesByTypeMap<String, int>DerivedType breakdown
connectionCountintDerivedTotal connections
labeledConnectionCountintDerivedLabeled connections
avgConnectionsPerNodedoubleDerivedAverage per node
selectedNodeCountintDerivedSelected nodes
selectedConnectionCountintDerivedSelected connections
selectedCountintDerivedTotal selected
hasSelectionboolDerivedAny selection
isMultiSelectionboolDerivedMultiple selected
zoomdoubleDerivedZoom level
zoomPercentintDerivedZoom percentage
panOffsetDerivedPan offset
lodLevelStringDerivedLOD level name
boundsRectDerivedNode bounds
boundsWidthdoubleDerivedBounds width
boundsHeightdoubleDerivedBounds height
boundsCenterOffsetDerivedBounds center
boundsAreadoubleDerivedBounds area
nodesInViewportintDerivedVisible nodes
isLargeGraphboolDerived> 100 nodes
densitydoubleDerivedNodes per area
summaryStringDerivedGraph summary
selectionSummaryStringDerivedSelection summary
viewportSummaryStringDerivedViewport summary
boundsSummaryStringDerivedBounds summary

See Also