@tic-nova/ngx-interactive-org-chart 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,1171 @@
1
+ # ngx-interactive-org-chart
2
+
3
+ > Modern Angular organizational chart component with interactive pan/zoom functionality
4
+
5
+ [![npm version](https://img.shields.io/npm/v/ngx-interactive-org-chart)](https://www.npmjs.com/package/ngx-interactive-org-chart)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
7
+ [![Downloads](https://img.shields.io/npm/dm/ngx-interactive-org-chart.svg)](https://www.npmjs.com/package/ngx-interactive-org-chart)
8
+
9
+ A beautiful, interactive organizational chart component for Angular applications. Built with modern Angular features and designed for ease of use and customization.
10
+
11
+ ## ✨ Features
12
+
13
+ - 🎯 **Interactive Pan & Zoom** - Smooth navigation with mouse/touch
14
+ - 🗺️ **Mini Map Navigation** - Bird's-eye view with drag navigation and real-time viewport tracking
15
+ - 🌳 **Hierarchical Layout** - Perfect for organizational structures
16
+ - 🎨 **Fully Themable** - Complete theme system including mini map customization with CSS variable support
17
+ - 📱 **Mobile Friendly** - Touch gestures support
18
+ - ⚡ **High Performance** - Optimized rendering with canvas-based mini map
19
+ - 🔍 **Searchable Nodes** - Easily find nodes in large charts
20
+ - 🧭 **Focus & Highlight** - Quickly navigate to specific nodes
21
+ - 📊 **Custom Node Templates** - Use Angular templates for nodes
22
+ - 🖱️ **Drag & Drop** - Reorganize nodes with drag and drop support
23
+ - 🎯 **Custom Drag Handles** - Use custom templates for drag handles
24
+ - 📈 **Dynamic Data Binding** - Reactive updates with Angular signals
25
+ - 📦 **Tree Shakable** - Import only what you need
26
+ - 🔄 **Collapsible Nodes** - Expand/collapse functionality
27
+ - 🌐 **RTL Support** - Right-to-left text direction
28
+ - 🌓 **Dark Mode Ready** - Automatic theme detection and CSS variable resolution
29
+ - 🧩 **Modular Design** - Standalone component for easy integration
30
+ - 🔧 **TypeScript Support** - Full type definitions included
31
+ - 🛠️ **Easy Setup** - Minimal configuration required
32
+ - 🎪 **Angular 20+** - Built with latest Angular features
33
+ - 🆓 **100% Free** - Open source MIT license
34
+
35
+ ## 📋 Version Compatibility
36
+
37
+ | ngx-interactive-org-chart | Angular Version | Notes |
38
+ | ------------------------- | --------------- | -------------------------------- |
39
+ | 1.1.4 | Angular 19 | Stable release |
40
+ | 1.2.x | Angular 20+ | Drag & drop, RTL support |
41
+ | 1.3.x | Angular 20+ | Mini map, dark mode, performance |
42
+
43
+ ## 🚀 Installation
44
+
45
+ ```bash
46
+ npm install ngx-interactive-org-chart
47
+ ```
48
+
49
+ ### Setup Angular Animations
50
+
51
+ The component uses Angular animations for smooth transitions. Add the animations module to your `main.ts`:
52
+
53
+ ```typescript
54
+ import { bootstrapApplication } from '@angular/platform-browser';
55
+ import { provideAnimations } from '@angular/platform-browser/animations';
56
+ import { AppComponent } from './app/app.component';
57
+
58
+ bootstrapApplication(AppComponent, {
59
+ providers: [
60
+ provideAnimations(), // Required for ngx-interactive-org-chart
61
+ // ... your other providers
62
+ ],
63
+ });
64
+ ```
65
+
66
+ Or if you're using NgModules:
67
+
68
+ ```typescript
69
+ import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
70
+
71
+ @NgModule({
72
+ imports: [
73
+ BrowserModule,
74
+ BrowserAnimationsModule, // Required for ngx-interactive-org-chart
75
+ // ... your other modules
76
+ ],
77
+ // ...
78
+ })
79
+ export class AppModule {}
80
+ ```
81
+
82
+ ## 📖 Usage
83
+
84
+ ### Basic Example
85
+
86
+ ```typescript
87
+ import { Component } from '@angular/core';
88
+ import {
89
+ NgxInteractiveOrgChart,
90
+ OrgChartNode,
91
+ } from 'ngx-interactive-org-chart';
92
+
93
+ @Component({
94
+ selector: 'app-demo',
95
+ standalone: true,
96
+ imports: [NgxInteractiveOrgChart],
97
+ template: `
98
+ <ngx-interactive-org-chart [data]="orgData" [themeOptions]="themeOptions" />
99
+ `,
100
+ })
101
+ export class DemoComponent {
102
+ orgData: OrgChartNode = {
103
+ id: 'ceo', // auto generated if not provided
104
+ name: 'John Smith',
105
+ data: {
106
+ // add any additional data properties here to customize the node and use it for displaying different types of nodes
107
+ },
108
+ children: [
109
+ {
110
+ id: 'cto',
111
+ name: 'Jane Doe',
112
+ children: [
113
+ {
114
+ id: 'dev1',
115
+ name: 'Mike Johnson',
116
+ },
117
+ ],
118
+ },
119
+ {
120
+ id: 'cfo',
121
+ name: 'Sarah Wilson',
122
+ },
123
+ ],
124
+ };
125
+ }
126
+ ```
127
+
128
+ ### Data Structure
129
+
130
+ The component expects hierarchical data in the following format:
131
+
132
+ ```typescript
133
+ interface OrgChartNode<T = any> {
134
+ id?: string;
135
+ name?: string;
136
+ data?: T;
137
+ children?: OrgChartNode<T>[];
138
+ collapsed?: boolean;
139
+ hidden?: boolean;
140
+ nodeClass?: string;
141
+ }
142
+ ```
143
+
144
+ ### Component Options
145
+
146
+ ```typescript
147
+ interface NgxInteractiveOrgChartTheme {
148
+ node?: {
149
+ background?: string;
150
+ color?: string;
151
+ shadow?: string;
152
+ outlineColor?: string;
153
+ outlineWidth?: string;
154
+ activeOutlineColor?: string;
155
+ highlightShadowColor?: string;
156
+ padding?: string;
157
+ borderRadius?: string;
158
+ activeColor?: string;
159
+ containerSpacing?: string;
160
+ maxWidth?: string;
161
+ minWidth?: string;
162
+ maxHeight?: string;
163
+ minHeight?: string;
164
+ dragOverOutlineColor?: string;
165
+ };
166
+ connector?: {
167
+ color?: string;
168
+ activeColor?: string;
169
+ borderRadius?: string;
170
+ width?: string;
171
+ };
172
+ collapseButton?: {
173
+ size?: string;
174
+ borderColor?: string;
175
+ borderRadius?: string;
176
+ color?: string;
177
+ background?: string;
178
+ hoverColor?: string;
179
+ hoverBackground?: string;
180
+ hoverShadow?: string;
181
+ hoverTransformScale?: string;
182
+ focusOutline?: string;
183
+ countFontSize?: string;
184
+ };
185
+ container?: {
186
+ background?: string;
187
+ border?: string;
188
+ };
189
+ }
190
+ ```
191
+
192
+ ### 🎯 Smart Zoom & Highlighting
193
+
194
+ The component features intelligent zoom calculation that automatically adjusts to provide optimal viewing of highlighted nodes:
195
+
196
+ ```typescript
197
+ // Configure dynamic zoom behavior
198
+ <ngx-interactive-org-chart
199
+ [data]="orgData"
200
+ [highlightZoomNodeWidthRatio]="0.4" // Node takes 40% of container width
201
+ [highlightZoomNodeHeightRatio]="0.5" // Node takes 50% of container height
202
+ [highlightZoomMinimum]="1.0" // Never zoom below 100%
203
+ />
204
+
205
+ // Programmatically highlight nodes
206
+ @ViewChild(NgxInteractiveOrgChart) orgChart!: NgxInteractiveOrgChart;
207
+
208
+ highlightManager() {
209
+ this.orgChart.highlightNode('cto'); // Automatically zooms to optimal level
210
+ }
211
+ ```
212
+
213
+ ## 📐 Layout Options
214
+
215
+ The component supports both vertical and horizontal layout orientations:
216
+
217
+ ```typescript
218
+ // Vertical layout (default)
219
+ <ngx-interactive-org-chart
220
+ [data]="orgData"
221
+ layout="vertical"
222
+ />
223
+
224
+ // Horizontal layout
225
+ <ngx-interactive-org-chart
226
+ [data]="orgData"
227
+ layout="horizontal"
228
+ />
229
+ ```
230
+
231
+ ## 🖱️ Pan Functionality
232
+
233
+ The component includes built-in pan functionality that allows users to navigate large organizational charts:
234
+
235
+ ```typescript
236
+ // Pan functionality is enabled by default
237
+ // Users can click and drag to pan around the chart
238
+ // Touch gestures are supported on mobile devices
239
+
240
+ @ViewChild(NgxInteractiveOrgChart) orgChart!: NgxInteractiveOrgChart;
241
+
242
+ // Programmatically control panning
243
+ panToSpecificLocation() {
244
+ // Pan to specific coordinates (x, y, smooth)
245
+ this.orgChart.pan(100, 200, true); // Pans to x: 100, y: 200 with smooth animation
246
+ }
247
+
248
+ // Reset pan to center
249
+ resetPanning() {
250
+ this.orgChart.resetPan(); // Centers the chart
251
+ }
252
+
253
+ // Reset both pan and zoom
254
+ resetView() {
255
+ this.orgChart.resetPanAndZoom(); // Centers and fits the chart
256
+ }
257
+ ```
258
+
259
+ **Pan Features:**
260
+
261
+ - **Mouse Support:** Click and drag to pan around the chart
262
+ - **Touch Support:** Touch and drag gestures on mobile devices
263
+ - **Smooth Animation:** Animated transitions when panning programmatically
264
+ - **Momentum:** Natural momentum-based panning for smooth user experience
265
+
266
+ ## 📋 Component Properties
267
+
268
+ | Property | Type | Default | Description |
269
+ | ------------------------------ | ------------------------------ | ------------ | ------------------------------------------------------------------ |
270
+ | `data` | `OrgChartNode` | required | The organizational data to display |
271
+ | `collapsible` | `boolean` | `true` | Enable/disable node collapsing |
272
+ | `layout` | `'vertical' \| 'horizontal'` | `'vertical'` | Chart layout orientation |
273
+ | `themeOptions` | `NgxInteractiveOrgChartTheme` | `{}` | Theme configuration options for styling |
274
+ | `nodeClass` | `string` | `undefined` | Custom CSS class applied to all nodes |
275
+ | `initialZoom` | `number` | `undefined` | Initial zoom level |
276
+ | `minZoom` | `number` | `0.1` | Minimum zoom level |
277
+ | `maxZoom` | `number` | `5` | Maximum zoom level |
278
+ | `zoomSpeed` | `number` | `1` | Zoom speed multiplier |
279
+ | `zoomDoubleClickSpeed` | `number` | `2` | Double-click zoom speed multiplier |
280
+ | `initialCollapsed` | `boolean` | `false` | Initial collapsed state for all nodes |
281
+ | `isRtl` | `boolean` | `false` | Right-to-left text direction support |
282
+ | `displayChildrenCount` | `boolean` | `true` | Show children count on collapse buttons |
283
+ | `highlightZoomNodeWidthRatio` | `number` | `0.3` | Node width ratio relative to viewport when highlighting (0.1-1.0) |
284
+ | `highlightZoomNodeHeightRatio` | `number` | `0.4` | Node height ratio relative to viewport when highlighting (0.1-1.0) |
285
+ | `highlightZoomMinimum` | `number` | `0.8` | Minimum zoom level when highlighting a node |
286
+ | `draggable` | `boolean` | `false` | Enable drag and drop functionality for nodes |
287
+ | `canDragNode` | `(node) => boolean` | `undefined` | Predicate function to determine if a node can be dragged |
288
+ | `canDropNode` | `(dragged, target) => boolean` | `undefined` | Predicate function to validate drop operations |
289
+ | `dragEdgeThreshold` | `number` | `0.1` | Auto-pan threshold is calculated as 10% of container dimensions |
290
+ | `dragAutoPanSpeed` | `number` | `15` | Speed of auto-panning in pixels per frame during drag |
291
+
292
+ ### Component Events
293
+
294
+ | Event | Type | Description |
295
+ | --------------- | --------------------------------------------------------------- | ------------------------------------------- |
296
+ | `nodeDrop` | `{ draggedNode: OrgChartNode<T>, targetNode: OrgChartNode<T> }` | Emitted when a node is dropped onto another |
297
+ | `nodeDragStart` | `OrgChartNode<T>` | Emitted when a node drag operation starts |
298
+ | `nodeDragEnd` | `OrgChartNode<T>` | Emitted when a node drag operation ends |
299
+
300
+ ### Component Methods
301
+
302
+ The component exposes several useful methods that can be called using a template reference:
303
+
304
+ ```typescript
305
+ @Component({
306
+ template: `
307
+ <ngx-interactive-org-chart #orgChart [data]="orgData" />
308
+ <button (click)="orgChart.zoomIn({ by: 10, relative: true })">Zoom In</button>
309
+ <button (click)="orgChart.zoomOut({ by: 10, relative: true })">Zoom Out</button>
310
+ <!-- Reset zoom and pan takes padding param for outer container -->
311
+ <button (click)="orgChart.resetPanAndZoom(50)">Reset</button>
312
+ <button (click)="orgChart.resetPan()">Reset Pan</button>
313
+ <button (click)="orgChart.resetZoom()">Reset Zoom</button>
314
+ <!-- Highlight a specific node by node.id - if you want to get node id by searching for a node use orgChart.flattenedNodes() it returns a signal of all nodes flattened -->
315
+ <button (click)="orgChart.highlightNode('cto')">Highlight CTO</button>
316
+ `
317
+ })
318
+ ```
319
+
320
+ | Method | Description |
321
+ | ------------------------------ | ---------------------------------------- |
322
+ | `zoomIn(options?)` | Zooms in the chart |
323
+ | `zoomOut(options?)` | Zooms out the chart |
324
+ | `resetZoom(padding?)` | Resets zoom to fit content |
325
+ | `resetPan()` | Resets pan position to center |
326
+ | `resetPanAndZoom(padding?)` | Resets both pan and zoom |
327
+ | `highlightNode(nodeId)` | Highlights and focuses a specific node |
328
+ | `toggleCollapseAll(collapse?)` | Collapses or expands all nodes |
329
+ | `getScale()` | Returns current zoom scale as percentage |
330
+
331
+ ### Dynamic Zoom Configuration
332
+
333
+ The component supports dynamic zoom calculation when highlighting nodes. This ensures optimal zoom levels based on the node size and viewport dimensions:
334
+
335
+ ```typescript
336
+ @Component({
337
+ template: `
338
+ <ngx-interactive-org-chart
339
+ [data]="orgData"
340
+ [highlightZoomNodeWidthRatio]="0.4"
341
+ [highlightZoomNodeHeightRatio]="0.5"
342
+ [highlightZoomMinimum]="1.0"
343
+ />
344
+ `
345
+ })
346
+ ```
347
+
348
+ **Configuration Options:**
349
+
350
+ - `highlightZoomNodeWidthRatio` (0.1-1.0): How much of the viewport width the highlighted node should occupy
351
+ - `highlightZoomNodeHeightRatio` (0.1-1.0): How much of the viewport height the highlighted node should occupy
352
+ - `highlightZoomMinimum`: Minimum zoom level when highlighting (prevents over-zooming out)
353
+
354
+ **Examples:**
355
+
356
+ - Small nodes: Use higher ratios (0.4-0.6) for better visibility
357
+ - Large nodes: Use lower ratios (0.2-0.3) to avoid excessive zoom
358
+ - Mobile devices: Consider using higher minimum zoom for readability
359
+
360
+ ### Custom Node Templates
361
+
362
+ You can customize how nodes are displayed by providing your own template. Use the `#nodeTemplate` template reference to override the default node appearance:
363
+
364
+ ```typescript
365
+ enum TypeEnum {
366
+ Employee = 'employee',
367
+ Contractor = 'contractor',
368
+ Department = 'department',
369
+ }
370
+
371
+ interface ApiResponse {
372
+ readonly id: number;
373
+ readonly name: string;
374
+ readonly title?: string;
375
+ readonly thumbnail?: string;
376
+ readonly type: TypeEnum;
377
+ readonly children?: ApiResponse[];
378
+ }
379
+
380
+ @Component({
381
+ selector: 'app-custom-org-chart',
382
+ standalone: true,
383
+ imports: [NgxInteractiveOrgChart],
384
+ template: `
385
+ <ngx-interactive-org-chart
386
+ [data]="orgChartData() ?? {}"
387
+ [themeOptions]="themeOptions"
388
+ [displayChildrenCount]="false"
389
+ >
390
+ <ng-template #nodeTemplate let-node="node">
391
+ @let nodeData = node?.data;
392
+
393
+ @switch (true) {
394
+ @case (
395
+ nodeData.type === dataTypeEnum.Employee ||
396
+ nodeData.type === dataTypeEnum.Contractor
397
+ ) {
398
+ @let isContractor = nodeData.type === dataTypeEnum.Contractor;
399
+
400
+ <section class="demo__employee">
401
+ <section class="demo__employee-thumbnail">
402
+ <img [src]="nodeData?.thumbnail" />
403
+ </section>
404
+ <section class="demo__employee-details">
405
+ <span class="demo__employee-details-name">{{
406
+ nodeData?.name
407
+ }}</span>
408
+ <span class="demo__employee-details-position">{{
409
+ nodeData?.title
410
+ }}</span>
411
+ @if (isContractor) {
412
+ <small class="demo__employee-details-type">Contractor</small>
413
+ }
414
+ </section>
415
+ </section>
416
+ }
417
+
418
+ @case (nodeData.type === dataTypeEnum.Department) {
419
+ <section class="demo__department">
420
+ <section class="demo__department-details">
421
+ <span class="demo__department-details-name">{{
422
+ nodeData?.name
423
+ }}</span>
424
+ <span class="demo__department-details-description">
425
+ {{ node?.descendantsCount }} Members
426
+ </span>
427
+ </section>
428
+ </section>
429
+ }
430
+ }
431
+ </ng-template>
432
+ </ngx-interactive-org-chart>
433
+ `,
434
+ styles: [
435
+ `
436
+ .demo {
437
+ &__employee {
438
+ display: flex;
439
+ gap: 1rem;
440
+ align-items: center;
441
+
442
+ &-thumbnail {
443
+ img {
444
+ border-radius: 50%;
445
+ width: 3rem;
446
+ height: 3rem;
447
+ object-fit: cover;
448
+ box-shadow: 0 0 0.25rem rgba(0, 0, 0, 0.3);
449
+ }
450
+ }
451
+
452
+ &-details {
453
+ display: flex;
454
+ flex-direction: column;
455
+ gap: 0.25rem;
456
+ align-items: flex-start;
457
+
458
+ &-name {
459
+ color: var(--text-primary);
460
+ font-weight: 600;
461
+ font-size: 0.875rem;
462
+ }
463
+
464
+ &-position {
465
+ font-size: 0.75rem;
466
+ color: #6c757d;
467
+ }
468
+
469
+ &-type {
470
+ font-size: 0.5rem;
471
+ background-color: rgb(203, 225, 232);
472
+ padding: 0.125rem 0.25rem;
473
+ border-radius: 0.25rem;
474
+ }
475
+ }
476
+ }
477
+
478
+ &__department {
479
+ display: flex;
480
+ gap: 1rem;
481
+ align-items: center;
482
+
483
+ &-details {
484
+ display: flex;
485
+ flex-direction: column;
486
+ gap: 0.25rem;
487
+ align-items: flex-start;
488
+
489
+ &-name {
490
+ font-weight: 600;
491
+ font-size: 0.875rem;
492
+ }
493
+
494
+ &-description {
495
+ font-size: 0.75rem;
496
+ }
497
+ }
498
+
499
+ &-name {
500
+ font-weight: 600;
501
+ font-size: 0.875rem;
502
+ }
503
+ }
504
+ }
505
+ `,
506
+ ],
507
+ })
508
+ export class CustomOrgChartComponent {
509
+ data: ApiResponse = {
510
+ id: 1,
511
+ name: 'Company',
512
+ type: TypeEnum.Department,
513
+ children: [
514
+ {
515
+ id: 2,
516
+ name: 'Engineering',
517
+ type: TypeEnum.Department,
518
+ children: [
519
+ {
520
+ id: 3,
521
+ name: 'Alice Johnson',
522
+ title: 'Software Engineer',
523
+ thumbnail: 'https://randomuser.me/api/portraits/women/21.jpg',
524
+ type: TypeEnum.Employee,
525
+ },
526
+ {
527
+ id: 4,
528
+ name: 'Bob Smith',
529
+ title: 'Senior Developer',
530
+ thumbnail: 'https://randomuser.me/api/portraits/men/21.jpg',
531
+ type: TypeEnum.Contractor,
532
+ },
533
+ ],
534
+ },
535
+ {
536
+ id: 5,
537
+ name: 'Marketing',
538
+ type: TypeEnum.Department,
539
+ children: [
540
+ {
541
+ id: 6,
542
+ name: 'Carol White',
543
+ title: 'Marketing Manager',
544
+ thumbnail: 'https://randomuser.me/api/portraits/women/21.jpg',
545
+ type: TypeEnum.Employee,
546
+ },
547
+ ],
548
+ },
549
+ ],
550
+ };
551
+
552
+ protected readonly orgChartData = signal<OrgChartNode<ApiResponse> | null>(
553
+ null
554
+ );
555
+
556
+ readonly #setOrgChartData = effect(() => {
557
+ this.orgChartData.set(this.mapDataToOrgChartNode(this.data));
558
+ });
559
+
560
+ protected readonly dataTypeEnum = TypeEnum;
561
+
562
+ protected readonly themeOptions: NgxInteractiveOrgChartTheme = {
563
+ node: {
564
+ background: 'white',
565
+ color: 'black',
566
+ shadow: '0 2px 8px rgba(0, 0, 0, 0.1)',
567
+ borderRadius: '8px',
568
+ outlineColor: '#e0e0e0',
569
+ activeOutlineColor: '#1976d2',
570
+ },
571
+ };
572
+
573
+ private mapDataToOrgChartNode({
574
+ children,
575
+ ...data
576
+ }: ApiResponse): OrgChartNode<ApiResponse> {
577
+ return {
578
+ id: data.id.toString(),
579
+ name: data.name, // for search purposes
580
+ collapsed: data.type === TypeEnum.Department, // collapse departments by default
581
+ style: {
582
+ // Apply any conditional styles here: For example, different background colors based on type
583
+ background: data.type === TypeEnum.Department ? '#e3f2fd' : '#f1f1f1',
584
+ color: data.type === TypeEnum.Department ? '#1976d2' : '#333',
585
+ // or you can just use predefined css variables (preferable)
586
+ '--node-background':
587
+ data.type === TypeEnum.Department ? '#e3f2fd' : '#f1f1f1',
588
+ '--node-color': data.type === TypeEnum.Department ? '#1976d2' : '#333',
589
+ },
590
+ // you can also set a custom class for each node, but make sure you apply this class in ng-deep
591
+ nodeClass:
592
+ data.type === TypeEnum.Department ? 'department-node' : 'employee-node',
593
+ data: {
594
+ ...data,
595
+ },
596
+ children: children?.map(child => this.mapDataToOrgChartNode(child)) || [],
597
+ };
598
+ }
599
+ }
600
+ ```
601
+
602
+ The custom template receives the node data through the `let-node="node"` directive. You can access:
603
+
604
+ - `node.name` - The node name
605
+ - `node.data` - Custom data object with any properties you define
606
+ - `node.id` - Unique node identifier
607
+ - `node.children` - Array of child nodes
608
+ - `node.collapsed` - Current collapsed state
609
+ - `node.descendantsCount` - Total number of descendants (useful for displaying counts)
610
+
611
+ ## 🖱️ Drag & Drop
612
+
613
+ The component supports drag and drop functionality, allowing users to reorganize the organizational chart dynamically. The library provides events and helper functions to handle the data restructuring.
614
+
615
+ ### Basic Drag & Drop Setup
616
+
617
+ ```typescript
618
+ import { Component, signal } from '@angular/core';
619
+ import {
620
+ NgxInteractiveOrgChart,
621
+ OrgChartNode,
622
+ moveNode,
623
+ } from 'ngx-interactive-org-chart';
624
+
625
+ @Component({
626
+ selector: 'app-drag-drop-demo',
627
+ standalone: true,
628
+ imports: [NgxInteractiveOrgChart],
629
+ template: `
630
+ <ngx-interactive-org-chart
631
+ [data]="orgData()"
632
+ [draggable]="true"
633
+ (nodeDrop)="onNodeDrop($event)"
634
+ (nodeDragStart)="onDragStart($event)"
635
+ (nodeDragEnd)="onDragEnd($event)"
636
+ />
637
+ `,
638
+ })
639
+ export class DragDropDemoComponent {
640
+ orgData = signal<OrgChartNode>({
641
+ id: '1',
642
+ name: 'CEO',
643
+ children: [
644
+ { id: '2', name: 'CTO', children: [] },
645
+ { id: '3', name: 'CFO', children: [] },
646
+ ],
647
+ });
648
+
649
+ /**
650
+ * Handle node drop event.
651
+ * IMPORTANT: The library does NOT modify your data automatically.
652
+ * You must handle the data restructuring yourself.
653
+ */
654
+ onNodeDrop(event: { draggedNode: OrgChartNode; targetNode: OrgChartNode }) {
655
+ const currentData = this.orgData();
656
+
657
+ // Option 1: Use the built-in helper function (recommended)
658
+ const updatedData = moveNode(
659
+ currentData,
660
+ event.draggedNode.id,
661
+ event.targetNode.id
662
+ );
663
+
664
+ if (updatedData) {
665
+ this.orgData.set(updatedData);
666
+ // Optionally save to backend
667
+ // this.api.updateOrgStructure(updatedData);
668
+ } else {
669
+ alert('Cannot move node: Invalid operation');
670
+ }
671
+
672
+ // Option 2: Implement your own custom logic
673
+ // const updatedData = this.customMoveLogic(currentData, event);
674
+ // this.orgData.set(updatedData);
675
+ }
676
+
677
+ onDragStart(node: OrgChartNode) {
678
+ console.log('Drag started:', node.name);
679
+ }
680
+
681
+ onDragEnd(node: OrgChartNode) {
682
+ console.log('Drag ended:', node.name);
683
+ }
684
+ }
685
+ ```
686
+
687
+ ### Custom Drag Handle
688
+
689
+ By default, the entire node is draggable. You can provide a custom drag handle template to specify which part of the node should be used for dragging:
690
+
691
+ ```typescript
692
+ @Component({
693
+ template: `
694
+ <ngx-interactive-org-chart
695
+ [data]="orgData()"
696
+ [draggable]="true"
697
+ (nodeDrop)="onNodeDrop($event)"
698
+ >
699
+ <!-- Custom node template -->
700
+ <ng-template #nodeTemplate let-node="node">
701
+ <div class="custom-node">
702
+ <h3>{{ node.name }}</h3>
703
+ <p>{{ node.data?.title }}</p>
704
+ </div>
705
+ </ng-template>
706
+
707
+ <!-- Custom drag handle template -->
708
+ <ng-template #dragHandleTemplate let-node="node">
709
+ <button class="drag-handle" title="Drag to move">
710
+ ⋮⋮
711
+ </button>
712
+ </ng-template>
713
+ </ngx-interactive-org-chart>
714
+ `,
715
+ styles: [`
716
+ .drag-handle {
717
+ cursor: move;
718
+ padding: 4px 8px;
719
+ background: #f0f0f0;
720
+ border: 1px solid #ccc;
721
+ border-radius: 4px;
722
+ user-select: none;
723
+ }
724
+
725
+ .drag-handle:hover {
726
+ background: #e0e0e0;
727
+ }
728
+ `]
729
+ })
730
+ ```
731
+
732
+ ### Helper Functions
733
+
734
+ The library provides several helper functions for common tree operations:
735
+
736
+ ```typescript
737
+ import {
738
+ moveNode,
739
+ findNode,
740
+ removeNode,
741
+ addNodeToParent,
742
+ isNodeDescendant,
743
+ } from 'ngx-interactive-org-chart';
744
+
745
+ // Move a node to a new parent
746
+ const updatedTree = moveNode(tree, draggedNodeId, targetParentId);
747
+
748
+ // Find a specific node by ID
749
+ const node = findNode(tree, nodeId);
750
+
751
+ // Remove a node from the tree
752
+ const treeWithoutNode = removeNode(tree, nodeId);
753
+
754
+ // Add a node to a specific parent
755
+ const treeWithNewNode = addNodeToParent(tree, parentId, newNode);
756
+
757
+ // Check if a node is a descendant of another
758
+ const isDescendant = isNodeDescendant(ancestorNode, descendantId);
759
+ ```
760
+
761
+ ### Drag & Drop Features
762
+
763
+ - **Auto-panning**: Automatically pans the view when dragging near viewport edges (configurable threshold and speed)
764
+ - **Visual Feedback**: Shows drag-over state on target nodes with color hints
765
+ - **Drag Constraints**: Use `canDragNode` and `canDropNode` predicates to control what can be dragged and where
766
+ - **ESC to Cancel**: Press ESC key during drag to cancel the operation
767
+ - **Validation**: Prevents dropping nodes on themselves or their descendants
768
+ - **Custom Handles**: Optional custom drag handle templates
769
+ - **Events**: Full control with dragStart, dragEnd, and drop events
770
+ - **Helper Functions**: Built-in utilities for tree manipulation
771
+ - **Pure Functions**: All helpers are immutable and return new tree structures
772
+ - **Touch Screen Support**: Full drag and drop support on mobile devices and tablets
773
+
774
+ ### Touch Screen Support
775
+
776
+ The drag and drop functionality works seamlessly on touch-enabled devices (smartphones and tablets):
777
+
778
+ **Features:**
779
+
780
+ - **Touch Gestures**: Long press or drag to initiate drag operation
781
+ - **Visual Ghost Element**: A semi-transparent copy of the node follows your finger during drag
782
+ - **Auto-panning**: Works with touch gestures when dragging near screen edges
783
+ - **Drop Zones**: Visual feedback shows valid/invalid drop targets
784
+ - **Smooth Performance**: Optimized for 60fps touch interactions
785
+ - **Hybrid Support**: Works on devices with both touch and mouse input
786
+
787
+ **How it works:**
788
+
789
+ 1. Touch and hold a draggable node
790
+ 2. Start moving your finger - a ghost element appears after a small movement threshold (10px)
791
+ 3. The org chart auto-pans when you drag near the edges
792
+ 4. Valid drop targets show visual feedback (dashed outline)
793
+ 5. Invalid targets show a "not-allowed" indicator
794
+ 6. Release to drop, or drag outside to cancel
795
+
796
+ **No configuration needed** - touch support is automatically enabled when `draggable` is set to `true`. All drag constraints (`canDragNode`, `canDropNode`) work identically for both mouse and touch input.
797
+
798
+ **Example:**
799
+
800
+ ```typescript
801
+ @Component({
802
+ template: `
803
+ <ngx-interactive-org-chart
804
+ [data]="orgData()"
805
+ [draggable]="true"
806
+ (nodeDrop)="onNodeDrop($event)"
807
+ />
808
+ `,
809
+ })
810
+ export class MyComponent {
811
+ // Works with both mouse and touch
812
+ onNodeDrop(event: { draggedNode: OrgChartNode; targetNode: OrgChartNode }) {
813
+ const updated = moveNode(
814
+ this.orgData(),
815
+ event.draggedNode.id,
816
+ event.targetNode.id
817
+ );
818
+ if (updated) {
819
+ this.orgData.set(updated);
820
+ }
821
+ }
822
+ }
823
+ ```
824
+
825
+ ### Drag Constraints & Validation
826
+
827
+ Control which nodes can be dragged and where they can be dropped using predicate functions:
828
+
829
+ ```typescript
830
+ @Component({
831
+ template: `
832
+ <ngx-interactive-org-chart
833
+ [data]="orgData()"
834
+ [draggable]="true"
835
+ [canDragNode]="canDragNode"
836
+ [canDropNode]="canDropNode"
837
+ [dragAutoPanSpeed]="20"
838
+ (nodeDrop)="onNodeDrop($event)"
839
+ />
840
+ `,
841
+ })
842
+ export class MyComponent {
843
+ // Prevent specific nodes from being dragged
844
+ canDragNode = (node: OrgChartNode) => {
845
+ // Example: CEO can't be moved
846
+ return node.data?.role !== 'CEO';
847
+ };
848
+
849
+ // Control where nodes can be dropped
850
+ canDropNode = (draggedNode: OrgChartNode, targetNode: OrgChartNode) => {
851
+ // Example: Only departments can have children
852
+ if (targetNode.data?.type !== 'Department') {
853
+ return false;
854
+ }
855
+
856
+ // Example: Managers can't report to employees
857
+ if (
858
+ draggedNode.data?.role === 'Manager' &&
859
+ targetNode.data?.role === 'Employee'
860
+ ) {
861
+ return false;
862
+ }
863
+
864
+ return true;
865
+ };
866
+
867
+ onNodeDrop(event: { draggedNode: OrgChartNode; targetNode: OrgChartNode }) {
868
+ // Use built-in helper to move nodes
869
+ const updated = moveNode(
870
+ this.orgData(),
871
+ event.draggedNode.id,
872
+ event.targetNode.id
873
+ );
874
+
875
+ if (updated) {
876
+ this.orgData.set(updated);
877
+ // Optionally sync with backend
878
+ this.api.updateOrgChart(updated);
879
+ }
880
+ }
881
+ }
882
+ ```
883
+
884
+ **Visual Feedback:**
885
+
886
+ - Valid drop targets show a **dashed outline with subtle background tint**
887
+ - Invalid drop targets show **reduced opacity and not-allowed cursor**
888
+ - Press **ESC** to cancel drag operation at any time
889
+
890
+ **Auto-panning Configuration:**
891
+
892
+ The auto-pan threshold is **automatically calculated as 10% of the container dimensions**, making it responsive across all screen sizes:
893
+
894
+ - **Small screens (mobile)**: Smaller activation zone prevents accidental auto-panning
895
+ - **Large screens (desktop)**: Larger comfortable edge zones
896
+ - **No configuration needed**: Works perfectly out of the box
897
+
898
+ You can still customize the panning speed:
899
+
900
+ - `dragAutoPanSpeed` (default: 15): Panning speed in pixels per frame
901
+
902
+ > **Note**: The `dragEdgeThreshold` property is deprecated. The threshold is now dynamically calculated for optimal UX.
903
+
904
+ ### Data Handling Pattern
905
+
906
+ **Important**: The library follows a controlled component pattern and does NOT modify your data automatically. This design gives you:
907
+
908
+ - ✅ **Full control** over validation logic
909
+ - ✅ **Backend synchronization** capabilities
910
+ - ✅ **Custom business rules** implementation
911
+ - ✅ **Undo/redo** functionality support
912
+ - ✅ **Optimistic updates** with rollback
913
+
914
+ **Pattern:**
915
+
916
+ 1. User drags and drops a node
917
+ 2. Library emits `nodeDrop` event with source and target nodes
918
+ 3. You handle the event and restructure your data
919
+ 4. Update your data signal/input
920
+ 5. Library automatically re-renders the updated structure
921
+
922
+ ```typescript
923
+ onNodeDrop(event: { draggedNode: OrgChartNode; targetNode: OrgChartNode }) {
924
+ // 1. Get current data
925
+ const currentData = this.orgData();
926
+
927
+ // 2. Validate the operation (optional)
928
+ if (!this.canMove(event.draggedNode, event.targetNode)) {
929
+ this.showError('Cannot move this node');
930
+ return;
931
+ }
932
+
933
+ // 3. Update the data structure
934
+ const updatedData = moveNode(
935
+ currentData,
936
+ event.draggedNode.id,
937
+ event.targetNode.id
938
+ );
939
+
940
+ if (!updatedData) return;
941
+
942
+ // 4. Update state (with rollback capability)
943
+ const previousData = currentData;
944
+ this.orgData.set(updatedData);
945
+
946
+ // 5. Sync with backend
947
+ this.api.updateOrgChart(updatedData).subscribe({
948
+ error: () => {
949
+ // Rollback on error
950
+ this.orgData.set(previousData);
951
+ this.showError('Failed to update');
952
+ }
953
+ });
954
+ }
955
+ ```
956
+
957
+ ## 🗺️ Mini Map Navigation
958
+
959
+ The component includes a built-in mini map feature for easy navigation of large organizational charts. The mini map provides a bird's-eye view of the entire chart with a viewport indicator showing your current position.
960
+
961
+ ### Basic Mini Map Setup
962
+
963
+ ```typescript
964
+ import { Component } from '@angular/core';
965
+ import { NgxInteractiveOrgChart } from 'ngx-interactive-org-chart';
966
+
967
+ @Component({
968
+ selector: 'app-minimap-demo',
969
+ standalone: true,
970
+ imports: [NgxInteractiveOrgChart],
971
+ template: `
972
+ <ngx-interactive-org-chart
973
+ [data]="orgData"
974
+ [showMiniMap]="true"
975
+ [miniMapPosition]="'bottom-right'"
976
+ [miniMapWidth]="200"
977
+ [miniMapHeight]="150"
978
+ />
979
+ `,
980
+ })
981
+ export class MiniMapDemoComponent {
982
+ orgData = {
983
+ id: '1',
984
+ name: 'CEO',
985
+ children: [
986
+ /* ... your org chart data ... */
987
+ ],
988
+ };
989
+ }
990
+ ```
991
+
992
+ ### Mini Map Configuration
993
+
994
+ | Input | Type | Default | Description |
995
+ | ----------------- | -------------------------------------------------------------- | ---------------- | ---------------------------------- |
996
+ | `showMiniMap` | `boolean` | `false` | Enable/disable the mini map |
997
+ | `miniMapPosition` | `'top-left' \| 'top-right' \| 'bottom-left' \| 'bottom-right'` | `'bottom-right'` | Position of the mini map on screen |
998
+ | `miniMapWidth` | `number` | `200` | Width of the mini map in pixels |
999
+ | `miniMapHeight` | `number` | `150` | Height of the mini map in pixels |
1000
+
1001
+ ### Mini Map Features
1002
+
1003
+ - **📊 Visual Overview**: Shows a simplified view of the entire organizational chart
1004
+ - **🎯 Viewport Indicator**: Blue rectangle shows your current visible area
1005
+ - **� Drag Navigation**: Drag the viewport indicator to smoothly pan the main chart
1006
+ - **🔄 Auto-Update**: Automatically updates when the chart structure changes
1007
+ - **🎨 Fully Themable**: Customizable through theme options with 8 configurable properties
1008
+ - **⚡ High Performance**: Optimized rendering with debounced updates
1009
+
1010
+ ### Example with All Options
1011
+
1012
+ ```typescript
1013
+ @Component({
1014
+ selector: 'app-advanced-minimap',
1015
+ standalone: true,
1016
+ imports: [NgxInteractiveOrgChart],
1017
+ template: `
1018
+ <ngx-interactive-org-chart
1019
+ [data]="largeOrgData"
1020
+ [showMiniMap]="miniMapVisible()"
1021
+ [miniMapPosition]="miniMapPosition()"
1022
+ [miniMapWidth]="250"
1023
+ [miniMapHeight]="180"
1024
+ [themeOptions]="themeOptions"
1025
+ />
1026
+
1027
+ <!-- Toggle button -->
1028
+ <button (click)="toggleMiniMap()">
1029
+ {{ miniMapVisible() ? 'Hide' : 'Show' }} Mini Map
1030
+ </button>
1031
+ `,
1032
+ })
1033
+ export class AdvancedMiniMapComponent {
1034
+ miniMapVisible = signal(true);
1035
+ miniMapPosition = signal<
1036
+ 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right'
1037
+ >('bottom-right');
1038
+
1039
+ largeOrgData = {
1040
+ // ... large organizational chart data
1041
+ };
1042
+
1043
+ themeOptions = {
1044
+ // ... your theme options
1045
+ };
1046
+
1047
+ toggleMiniMap() {
1048
+ this.miniMapVisible.update(v => !v);
1049
+ }
1050
+ }
1051
+ ```
1052
+
1053
+ ### Mini Map Styling
1054
+
1055
+ The mini map is fully themable through the `themeOptions` input. It automatically inherits and adapts to your chart's theme configuration:
1056
+
1057
+ ```typescript
1058
+ import { NgxInteractiveOrgChartTheme } from 'ngx-interactive-org-chart';
1059
+
1060
+ const customTheme: NgxInteractiveOrgChartTheme = {
1061
+ // ... other theme options
1062
+ miniMap: {
1063
+ background: 'rgba(255, 255, 255, 0.95)',
1064
+ borderColor: 'rgba(0, 0, 0, 0.15)',
1065
+ borderRadius: '8px',
1066
+ shadow: '0 4px 6px -1px rgba(0, 0, 0, 0.1)',
1067
+ nodeColor: 'rgba(0, 0, 0, 0.6)',
1068
+ viewportBackground: 'rgba(59, 130, 246, 0.2)',
1069
+ viewportBorderColor: 'rgb(59, 130, 246)',
1070
+ viewportBorderWidth: '2px',
1071
+ },
1072
+ };
1073
+ ```
1074
+
1075
+ #### Mini Map Theme Options
1076
+
1077
+ | Property | Type | Default | Description |
1078
+ | --------------------- | -------- | --------------------------- | --------------------------------- |
1079
+ | `background` | `string` | `rgba(255, 255, 255, 0.95)` | Background color of the mini map |
1080
+ | `borderColor` | `string` | `rgba(0, 0, 0, 0.15)` | Border color of the mini map |
1081
+ | `borderRadius` | `string` | `8px` | Border radius of the mini map |
1082
+ | `shadow` | `string` | `0 4px 6px -1px rgba(...)` | Box shadow of the mini map |
1083
+ | `nodeColor` | `string` | `rgba(0, 0, 0, 0.6)` | Color of nodes in the mini map |
1084
+ | `viewportBackground` | `string` | `rgba(59, 130, 246, 0.2)` | Background color of viewport area |
1085
+ | `viewportBorderColor` | `string` | `rgb(59, 130, 246)` | Border color of viewport area |
1086
+ | `viewportBorderWidth` | `string` | `2px` | Border width of viewport area |
1087
+
1088
+ You can also use CSS custom properties for additional customization:
1089
+
1090
+ ```scss
1091
+ ::ng-deep ngx-org-chart-mini-map {
1092
+ .mini-map-container {
1093
+ // Override theme values with custom CSS
1094
+ border-radius: 12px !important;
1095
+ box-shadow: 0 8px 16px rgba(0, 0, 0, 0.2) !important;
1096
+ }
1097
+
1098
+ .viewport-indicator {
1099
+ // Custom styling for the viewport indicator
1100
+ border-width: 3px !important;
1101
+ }
1102
+ }
1103
+ ```
1104
+
1105
+ ### Performance Considerations
1106
+
1107
+ The mini map is optimized for performance:
1108
+
1109
+ - Uses HTML Canvas for efficient rendering
1110
+ - Debounced updates (100ms) to prevent excessive redraws
1111
+ - MutationObserver for smart DOM change detection
1112
+ - RequestAnimationFrame for smooth viewport tracking
1113
+ - Only redraws when necessary (chart changes, pan, zoom)
1114
+
1115
+ ### Use Cases
1116
+
1117
+ The mini map is particularly useful for:
1118
+
1119
+ - **Large Organizations**: Navigate charts with 50+ nodes
1120
+ - **Deep Hierarchies**: Quickly jump between different levels
1121
+ - **Complex Structures**: Overview of multi-department organizations
1122
+ - **Presentations**: Show context while focusing on specific areas
1123
+ - **User Onboarding**: Help users understand chart structure
1124
+
1125
+ ## 🎨 Styling
1126
+
1127
+ You can add a custom class to each node that will be applied separately or use the `nodeClass` input that will be applied to all nodes or you can use the `themeOptions` input to define global styles for nodes, connectors, and the chart container.
1128
+
1129
+ ## 📊 Live Demo
1130
+
1131
+ Check out the interactive demo to see the component in action:
1132
+
1133
+ **[View Live Demo →](https://zeyadelshaf3y.github.io/ngx-interactive-org-chart)**
1134
+
1135
+ ## 🤝 Contributing
1136
+
1137
+ Contributions are welcome! Please feel free to submit a Pull Request. For major changes, please open an issue first to discuss what you would like to change.
1138
+
1139
+ 1. Fork the project
1140
+ 2. Create your feature branch (`git checkout -b feature/amazing-feature`)
1141
+ 3. Commit your changes (`git commit -m 'Add some amazing-feature'`)
1142
+ 4. Push to the branch (`git push origin feature/amazing-feature`)
1143
+ 5. Open a Pull Request
1144
+
1145
+ ## 🤖 Issues & Support
1146
+
1147
+ If you encounter any issues or have questions:
1148
+
1149
+ 1. Check the [GitHub Issues](https://github.com/zeyadelshaf3y/ngx-interactive-org-chart/issues)
1150
+ 2. Create a new issue with a detailed description
1151
+ 3. Include your Angular version and reproduction steps
1152
+
1153
+ ## 💝 Support the Project
1154
+
1155
+ If this library helps you, consider supporting its development:
1156
+
1157
+ - ⭐ Star the repository on GitHub
1158
+ - 🐛 Report bugs and suggest features
1159
+ - 💝 [Buy me a coffee](https://buymeacoffee.com/zeyadalshafey)
1160
+ - 💖 [GitHub Sponsors](https://github.com/sponsors/zeyadelshaf3y)
1161
+
1162
+ ## 📄 License
1163
+
1164
+ MIT © [Zeyad Alshafey](https://github.com/zeyadelshaf3y)
1165
+
1166
+ ## 🔗 Links
1167
+
1168
+ - [GitHub Repository](https://github.com/zeyadelshaf3y/ngx-interactive-org-chart)
1169
+ - [NPM Package](https://www.npmjs.com/package/ngx-interactive-org-chart)
1170
+ - [Live Demo](https://zeyadelshaf3y.github.io/ngx-interactive-org-chart)
1171
+ - [Issues](https://github.com/zeyadelshaf3y/ngx-interactive-org-chart/issues)