org_chart 4.1.0+1 copy "org_chart: ^4.1.0+1" to clipboard
org_chart: ^4.1.0+1 copied to clipboard

A flutter orgranizational chart with drag and drop, zoom and pan, search, collapse, expand, and easy customizations!

example/lib/main.dart

import 'package:flutter/material.dart';
import 'package:org_chart/org_chart.dart';
import 'widgets/org_chart_node.dart';
import 'models/node_data.dart';

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

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Organization Chart Example',
      theme: ThemeData(
        useMaterial3: true,
        colorSchemeSeed: Colors.blue,
      ),
      home: const Example2(),
    );
  }
}

class Example2 extends StatefulWidget {
  const Example2({super.key});

  @override
  State<Example2> createState() => _Example2State();
}

class _Example2State extends State<Example2> {
  late final OrgChartController<NodeData> orgChartController;

  @override
  void initState() {
    super.initState();
    orgChartController = OrgChartController<NodeData>(
      boxSize: const Size(150, 100),
      items: _initialNodes,
      idProvider: (data) => data.id,
      toProvider: (data) => data.parentId,
      toSetter: _updateNodeParent,
    );
  }

  List<NodeData> get _initialNodes => [
        NodeData(id: '0', text: 'Main Block'),
        NodeData(id: '1', text: 'Block 2', parentId: '0'),
        NodeData(id: '2', text: 'Block 3', parentId: '0'),
        NodeData(id: '3', text: 'Block 4', parentId: '1'),
      ];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Stack(
        children: [
          DecoratedBox(
            decoration: _buildBackgroundGradient(),
            child: Stack(
              children: [
                Center(child: _buildOrgChart()),
                const _InfoOverlay(),
              ],
            ),
          ),
        ],
      ),
      floatingActionButton: _buildOrientationButton(),
    );
  }

  Widget _buildOrgChart() {
    return OrgChart<NodeData>(
      controller: orgChartController,
      arrowStyle: DashedGraphArrow(pattern: [20, 10, 5, 10]),
      cornerRadius: 10,
      isDraggable: true,
      linePaint: _buildArrowPaint(),
      builder: (details) => OrgChartNode(
        details: details,
        onAddNode: () => _handleAddNode(details.item.id),
        onEditText: () => _handleEditText(details.item),
        onToggleNodes: details.hideNodes,
      ),
      optionsBuilder: _buildOptionsMenu,
      onOptionSelect: _handleOptionSelect,
      onDrop: _handleNodeDrop,
    );
  }

  Widget _buildOrientationButton() {
    return FloatingActionButton.extended(
      label: const Text('Change Orientation'),
      icon: const Icon(Icons.rotate_90_degrees_ccw),
      onPressed: () => orgChartController.switchOrientation(),
    );
  }

  BoxDecoration _buildBackgroundGradient() {
    return const BoxDecoration(
      gradient: LinearGradient(
        colors: [Color(0xFFE3F2FD), Color(0xFFBBDEFB)],
        begin: Alignment.bottomLeft,
        end: Alignment.topRight,
      ),
    );
  }

  Paint _buildArrowPaint() {
    return Paint()
      ..color = Colors.grey.shade600
      ..strokeWidth = 5
      ..style = PaintingStyle.stroke
      ..strokeCap = StrokeCap.round;
  }

  List<PopupMenuEntry<String>> _buildOptionsMenu(NodeData item) {
    // Don't allow removing the root node
    if (item.parentId == null) return const [];

    return [
      const PopupMenuItem(
        value: 'remove',
        child: ListTile(
          leading: Icon(Icons.remove_circle_outline),
          title: Text('Remove Node'),
        ),
      ),
      const PopupMenuItem(
        value: 'edit',
        child: ListTile(
          leading: Icon(Icons.edit),
          title: Text('Edit Node'),
        ),
      )
    ];
  }

  void _handleOptionSelect(NodeData item, dynamic value) {
    if (value == 'remove') {
      _removeNode(item);
    } else if (value == 'edit') {
      _handleEditText(item);
    }
  }

  void _removeNode(NodeData item) {
    try {
      orgChartController.removeItem(item.id, ActionOnNodeRemoval.unlink);
    } catch (e) {
      _showError('Failed to remove node: ${e.toString()}');
    }
  }

  void _updateNodeParent(NodeData data, String? newParentId) {
    data.parentId = newParentId;
  }

  void _handleAddNode(String parentId) {
    try {
      final newNode = NodeData(
        id: orgChartController.uniqueNodeId,
        text: 'New Block',
        parentId: parentId,
      );
      orgChartController.addItem(newNode);
    } catch (e) {
      _showError('Failed to add node: ${e.toString()}');
    }
  }

  Future<void> _handleEditText(NodeData item) async {
    try {
      final newText = await _showTextEditDialog(item);
      if (newText == null) return;

      final index = orgChartController.items.indexOf(item);
      if (index == -1) return;
      setState(() {
        item.text = newText;
      });
    } catch (e) {
      _showError('Failed to edit node text: ${e.toString()}');
    }
  }

  void _handleNodeDrop(
      NodeData dragged, NodeData target, bool isTargetSubnode) {
    try {
      if (isTargetSubnode) {
        _showError('Cannot drop a node onto its own child');
        orgChartController.calculatePosition();
        return;
      }

      if (dragged.parentId == target.id) {
        orgChartController.calculatePosition();
        return;
      }
      dragged.parentId = target.id;
      orgChartController.calculatePosition();
    } catch (e) {
      _showError('Failed to move node: ${e.toString()}');
      orgChartController.calculatePosition();
    }
  }

  Future<String?> _showTextEditDialog(NodeData item) {
    return showDialog<String>(
      context: context,
      builder: (context) => _EditNodeDialog(initialText: item.text),
    );
  }

  void _showError(String message) {
    showDialog<void>(
      context: context,
      builder: (context) => AlertDialog(
        title: const Text('Error'),
        content: Text(message),
        actions: [
          TextButton(
            onPressed: () => Navigator.of(context).pop(),
            child: const Text('OK'),
          ),
        ],
      ),
    );
  }
}

class _InfoOverlay extends StatelessWidget {
  const _InfoOverlay();

  @override
  Widget build(BuildContext context) {
    final textTheme = Theme.of(context).textTheme;
    return Positioned(
      bottom: 20,
      left: 20,
      child: Card(
        child: Padding(
          padding: const EdgeInsets.all(16.0),
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              Text('Instructions:', style: textTheme.titleSmall),
              const SizedBox(height: 8),
              ...[
                'Tap to add a child node',
                'Double tap to edit text',
                'Drag to rearrange nodes',
                'Right click/long press to remove',
                'Drag in empty space to pan',
                'Pinch to zoom'
              ].map(
                (text) => Padding(
                  padding: const EdgeInsets.only(bottom: 4),
                  child: Text('• $text', style: textTheme.bodySmall),
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

class _EditNodeDialog extends StatefulWidget {
  final String initialText;

  const _EditNodeDialog({required this.initialText});

  @override
  State<_EditNodeDialog> createState() => _EditNodeDialogState();
}

class _EditNodeDialogState extends State<_EditNodeDialog> {
  late final TextEditingController _controller;

  @override
  void initState() {
    super.initState();
    _controller = TextEditingController(text: widget.initialText);
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return AlertDialog(
      title: const Text('Edit Node Text'),
      content: TextField(
        controller: _controller,
        autofocus: true,
        decoration: const InputDecoration(
          labelText: 'Node Text',
          border: OutlineInputBorder(),
        ),
      ),
      actions: [
        TextButton(
          onPressed: () => Navigator.of(context).pop(),
          child: const Text('Cancel'),
        ),
        FilledButton(
          onPressed: () => Navigator.of(context).pop(_controller.text),
          child: const Text('Save'),
        ),
      ],
    );
  }
}
38
likes
0
points
905
downloads

Publisher

verified publisherali-hnaineh.dev

Weekly Downloads

A flutter orgranizational chart with drag and drop, zoom and pan, search, collapse, expand, and easy customizations!

Homepage
Repository (GitHub)
View/report issues

License

unknown (license)

Dependencies

flutter

More

Packages that depend on org_chart