diff --git a/apps/docs/src/components/angular/minimap/diagram.component.scss b/apps/docs/src/components/angular/minimap/diagram.component.scss
new file mode 100644
index 000000000..08c961298
--- /dev/null
+++ b/apps/docs/src/components/angular/minimap/diagram.component.scss
@@ -0,0 +1,12 @@
+:host {
+ position: relative;
+ display: flex;
+}
+
+.diagram {
+ position: relative;
+ display: flex;
+ width: 100%;
+ height: var(--ng-diagram-height);
+ border: var(--ng-diagram-border);
+}
diff --git a/apps/docs/src/components/angular/minimap/diagram.component.ts b/apps/docs/src/components/angular/minimap/diagram.component.ts
new file mode 100644
index 000000000..5003d761f
--- /dev/null
+++ b/apps/docs/src/components/angular/minimap/diagram.component.ts
@@ -0,0 +1,60 @@
+import '@angular/compiler';
+
+import { Component } from '@angular/core';
+import {
+ initializeModel,
+ NgDiagramBackgroundComponent,
+ NgDiagramComponent,
+ NgDiagramMinimapComponent,
+ provideNgDiagram,
+ type NgDiagramConfig,
+} from 'ng-diagram';
+
+@Component({
+ imports: [
+ NgDiagramComponent,
+ NgDiagramBackgroundComponent,
+ NgDiagramMinimapComponent,
+ ],
+ providers: [provideNgDiagram()],
+ templateUrl: './diagram.component.html',
+ styleUrls: ['./diagram.component.scss'],
+})
+export class DiagramComponent {
+ config = {
+ zoom: {
+ zoomToFit: {
+ onInit: true,
+ padding: [50, 250, 50, 50],
+ },
+ },
+ } satisfies NgDiagramConfig;
+
+ model = initializeModel({
+ nodes: [
+ { id: '1', position: { x: 0, y: 0 }, data: { label: 'Node 1' } },
+ { id: '2', position: { x: 300, y: 0 }, data: { label: 'Node 2' } },
+ { id: '3', position: { x: 150, y: 150 }, data: { label: 'Node 3' } },
+ { id: '4', position: { x: 0, y: 300 }, data: { label: 'Node 4' } },
+ { id: '5', position: { x: 300, y: 300 }, data: { label: 'Node 5' } },
+ ],
+ edges: [
+ {
+ id: 'e1',
+ source: '1',
+ sourcePort: 'port-right',
+ target: '2',
+ targetPort: 'port-left',
+ data: {},
+ },
+ {
+ id: 'e2',
+ source: '4',
+ sourcePort: 'port-right',
+ target: '5',
+ targetPort: 'port-left',
+ data: {},
+ },
+ ],
+ });
+}
diff --git a/apps/docs/src/components/angular/minimap/minimap.astro b/apps/docs/src/components/angular/minimap/minimap.astro
new file mode 100644
index 000000000..3df7e17e2
--- /dev/null
+++ b/apps/docs/src/components/angular/minimap/minimap.astro
@@ -0,0 +1,5 @@
+---
+import { DiagramComponent } from './diagram.component';
+---
+
+
diff --git a/apps/docs/src/content/docs/api/Components/NgDiagramMinimapComponent.md b/apps/docs/src/content/docs/api/Components/NgDiagramMinimapComponent.md
new file mode 100644
index 000000000..c60e455e2
--- /dev/null
+++ b/apps/docs/src/content/docs/api/Components/NgDiagramMinimapComponent.md
@@ -0,0 +1,91 @@
+---
+version: "since v1.0.0"
+editUrl: false
+next: false
+prev: false
+title: "NgDiagramMinimapComponent"
+---
+
+A minimap component that displays a bird's-eye view of the diagram.
+
+Shows all nodes as small rectangles and a viewport rectangle indicating
+the currently visible area. The minimap updates reactively when the
+diagram viewport changes (pan/zoom) or when nodes are added/removed/updated.
+
+The minimap also supports navigation: click and drag on the minimap to pan
+the diagram viewport to different areas.
+
+## Implements
+
+- `AfterViewInit`
+
+## Properties
+
+### height
+
+> **height**: `InputSignal`\<`number`\>
+
+Height of the minimap in pixels.
+
+***
+
+### minimapNodeTemplateMap
+
+> **minimapNodeTemplateMap**: `InputSignal`\<[`NgDiagramMinimapNodeTemplateMap`](/docs/api/types/minimap/ngdiagramminimapnodetemplatemap/)\>
+
+Optional template map for complete control over node rendering per node type.
+Components registered in the map should render SVG elements.
+
+#### Example
+
+```typescript
+const minimapTemplateMap = new NgDiagramMinimapNodeTemplateMap([
+ ['database', DatabaseMinimapNodeComponent],
+ ['api', ApiMinimapNodeComponent],
+]);
+
+// Usage:
+
+```
+
+***
+
+### nodeStyle
+
+> **nodeStyle**: `InputSignal`\<`undefined` \| [`MinimapNodeStyleFn`](/docs/api/types/minimap/minimapnodestylefn/)\>
+
+Optional callback function to customize node styling.
+Return style properties to override defaults, or null/undefined to use CSS defaults.
+
+#### Example
+
+```typescript
+nodeStyle = (node: Node) => ({
+ fill: node.type === 'database' ? '#4CAF50' : '#9E9E9E',
+ opacity: node.selected ? 1 : 0.6,
+});
+```
+
+***
+
+### position
+
+> **position**: `InputSignal`\<[`NgDiagramPanelPosition`](/docs/api/types/ngdiagrampanelposition/)\>
+
+Position of the minimap panel within the diagram container.
+
+***
+
+### showZoomControls
+
+> **showZoomControls**: `InputSignal`\<`boolean`\>
+
+Whether to show zoom controls in the minimap footer.
+
+***
+
+### width
+
+> **width**: `InputSignal`\<`number`\>
+
+Width of the minimap in pixels.
diff --git a/apps/docs/src/content/docs/api/Directives/NgDiagramMinimapNavigationDirective.md b/apps/docs/src/content/docs/api/Directives/NgDiagramMinimapNavigationDirective.md
new file mode 100644
index 000000000..fea23affb
--- /dev/null
+++ b/apps/docs/src/content/docs/api/Directives/NgDiagramMinimapNavigationDirective.md
@@ -0,0 +1,34 @@
+---
+version: "since v1.0.0"
+editUrl: false
+next: false
+prev: false
+title: "NgDiagramMinimapNavigationDirective"
+---
+
+Directive that enables drag navigation on the minimap.
+Users can drag on the minimap to move the diagram viewport.
+
+Supports both mouse and touch input.
+Uses pointer capture for reliable touch tracking on mobile devices.
+
+## Implements
+
+- `OnDestroy`
+
+## Methods
+
+### ngOnDestroy()
+
+> **ngOnDestroy**(): `void`
+
+A callback method that performs custom clean-up, invoked immediately
+before a directive, pipe, or service instance is destroyed.
+
+#### Returns
+
+`void`
+
+#### Implementation of
+
+`OnDestroy.ngOnDestroy`
diff --git a/apps/docs/src/content/docs/api/Services/NgDiagramViewportService.md b/apps/docs/src/content/docs/api/Services/NgDiagramViewportService.md
index a6d64b765..e0e3f0a5c 100644
--- a/apps/docs/src/content/docs/api/Services/NgDiagramViewportService.md
+++ b/apps/docs/src/content/docs/api/Services/NgDiagramViewportService.md
@@ -25,6 +25,22 @@ this.viewportService.zoom(1.2);
## Properties
+### canZoomIn
+
+> **canZoomIn**: `Signal`\<`boolean`\>
+
+Returns true if the current zoom level is below the maximum and can be increased.
+
+***
+
+### canZoomOut
+
+> **canZoomOut**: `Signal`\<`boolean`\>
+
+Returns true if the current zoom level is above the minimum and can be decreased.
+
+***
+
### scale
> **scale**: `Signal`\<`number`\>
@@ -39,6 +55,34 @@ Returns a computed signal for the scale that safely handles uninitialized state.
Returns a computed signal for the viewport that safely handles uninitialized state.
+## Accessors
+
+### maxZoom
+
+#### Get Signature
+
+> **get** **maxZoom**(): `number`
+
+Returns the maximum zoom scale from the diagram configuration.
+
+##### Returns
+
+`number`
+
+***
+
+### minZoom
+
+#### Get Signature
+
+> **get** **minZoom**(): `number`
+
+Returns the minimum zoom scale from the diagram configuration.
+
+##### Returns
+
+`number`
+
## Methods
### centerOnNode()
@@ -222,7 +266,7 @@ Zooms the viewport by the specified factor.
`number`
-The factor to zoom by.
+The factor to zoom by (e.g., 1.1 for 10% zoom in, 0.9 for 10% zoom out).
##### center?
diff --git a/apps/docs/src/content/docs/api/Types/Minimap/MinimapNodeShape.md b/apps/docs/src/content/docs/api/Types/Minimap/MinimapNodeShape.md
new file mode 100644
index 000000000..00b0f93c8
--- /dev/null
+++ b/apps/docs/src/content/docs/api/Types/Minimap/MinimapNodeShape.md
@@ -0,0 +1,11 @@
+---
+version: "since v1.0.0"
+editUrl: false
+next: false
+prev: false
+title: "MinimapNodeShape"
+---
+
+> **MinimapNodeShape** = `"rect"` \| `"circle"` \| `"ellipse"`
+
+Available shapes for minimap node rendering.
diff --git a/apps/docs/src/content/docs/api/Types/Minimap/MinimapNodeStyle.md b/apps/docs/src/content/docs/api/Types/Minimap/MinimapNodeStyle.md
new file mode 100644
index 000000000..82441499a
--- /dev/null
+++ b/apps/docs/src/content/docs/api/Types/Minimap/MinimapNodeStyle.md
@@ -0,0 +1,58 @@
+---
+version: "since v1.0.0"
+editUrl: false
+next: false
+prev: false
+title: "MinimapNodeStyle"
+---
+
+Style properties that can be applied to minimap nodes.
+All properties are optional - unset properties use CSS defaults.
+
+## Properties
+
+### cssClass?
+
+> `optional` **cssClass**: `string`
+
+CSS class to apply to the node
+
+***
+
+### fill?
+
+> `optional` **fill**: `string`
+
+Fill color for the node
+
+***
+
+### opacity?
+
+> `optional` **opacity**: `number`
+
+Opacity from 0 to 1
+
+***
+
+### shape?
+
+> `optional` **shape**: [`MinimapNodeShape`](/docs/api/types/minimap/minimapnodeshape/)
+
+Shape of the node in the minimap. Defaults to 'rect'.
+
+***
+
+### stroke?
+
+> `optional` **stroke**: `string`
+
+Stroke color for the node
+
+***
+
+### strokeWidth?
+
+> `optional` **strokeWidth**: `number`
+
+Stroke width in pixels
diff --git a/apps/docs/src/content/docs/api/Types/Minimap/MinimapNodeStyleFn.md b/apps/docs/src/content/docs/api/Types/Minimap/MinimapNodeStyleFn.md
new file mode 100644
index 000000000..167bd7f67
--- /dev/null
+++ b/apps/docs/src/content/docs/api/Types/Minimap/MinimapNodeStyleFn.md
@@ -0,0 +1,31 @@
+---
+version: "since v1.0.0"
+editUrl: false
+next: false
+prev: false
+title: "MinimapNodeStyleFn"
+---
+
+> **MinimapNodeStyleFn** = (`node`) => [`MinimapNodeStyle`](/docs/api/types/minimap/minimapnodestyle/) \| `null` \| `undefined`
+
+Function signature for the nodeStyle callback.
+Return style properties to override defaults, or null/undefined to use defaults.
+
+## Parameters
+
+### node
+
+[`Node`](/docs/api/types/model/node/)
+
+## Returns
+
+[`MinimapNodeStyle`](/docs/api/types/minimap/minimapnodestyle/) \| `null` \| `undefined`
+
+## Example
+
+```typescript
+const nodeStyle: MinimapNodeStyleFn = (node) => ({
+ fill: node.type === 'database' ? '#4CAF50' : '#9E9E9E',
+ opacity: node.selected ? 1 : 0.6,
+});
+```
diff --git a/apps/docs/src/content/docs/api/Types/Minimap/NgDiagramMinimapNodeTemplate.md b/apps/docs/src/content/docs/api/Types/Minimap/NgDiagramMinimapNodeTemplate.md
new file mode 100644
index 000000000..a74a7139e
--- /dev/null
+++ b/apps/docs/src/content/docs/api/Types/Minimap/NgDiagramMinimapNodeTemplate.md
@@ -0,0 +1,49 @@
+---
+version: "since v1.0.0"
+editUrl: false
+next: false
+prev: false
+title: "NgDiagramMinimapNodeTemplate"
+---
+
+Interface for custom minimap node components.
+Components implementing this interface can be registered in NgDiagramMinimapNodeTemplateMap
+to customize how specific node types are rendered in the minimap.
+
+Custom templates are rendered inside a foreignObject that handles positioning and sizing,
+so the component only needs to render content that fills its container.
+
+## Example
+
+```typescript
+@Component({
+ selector: 'my-minimap-node',
+ standalone: true,
+ template: `
+
+ {{ node().type }}
+
+ `,
+ styles: [`.minimap-icon { width: 100%; height: 100%; }`]
+})
+export class MyMinimapNodeComponent implements NgDiagramMinimapNodeTemplate {
+ node = input.required();
+ nodeStyle = input(); // Required by interface, can be ignored if not needed
+}
+```
+
+## Properties
+
+### node
+
+> **node**: `InputSignal`\<[`Node`](/docs/api/types/model/node/)\>
+
+Input signal containing the original Node object for accessing node data, type, etc.
+
+***
+
+### nodeStyle
+
+> **nodeStyle**: `InputSignal`\<`undefined` \| [`MinimapNodeStyle`](/docs/api/types/minimap/minimapnodestyle/)\>
+
+Input signal for style overrides computed by nodeStyle callback. Can be ignored if not needed.
diff --git a/apps/docs/src/content/docs/api/Types/Minimap/NgDiagramMinimapNodeTemplateMap.md b/apps/docs/src/content/docs/api/Types/Minimap/NgDiagramMinimapNodeTemplateMap.md
new file mode 100644
index 000000000..1e46dac41
--- /dev/null
+++ b/apps/docs/src/content/docs/api/Types/Minimap/NgDiagramMinimapNodeTemplateMap.md
@@ -0,0 +1,224 @@
+---
+version: "since v1.0.0"
+editUrl: false
+next: false
+prev: false
+title: "NgDiagramMinimapNodeTemplateMap"
+---
+
+Map that associates node type names with their corresponding minimap Angular component classes.
+Used by ng-diagram-minimap to determine which custom component to render based on node type.
+
+## Example
+
+```typescript
+const minimapTemplateMap = new NgDiagramMinimapNodeTemplateMap([
+ ['database', DatabaseMinimapNodeComponent],
+ ['api', ApiMinimapNodeComponent],
+]);
+
+// Usage in template:
+
+```
+
+## Extends
+
+- `Map`\<`string`, `Type`\<[`NgDiagramMinimapNodeTemplate`](/docs/api/types/minimap/ngdiagramminimapnodetemplate/)\>\>
+
+## Properties
+
+### size
+
+> `readonly` **size**: `number`
+
+#### Returns
+
+the number of elements in the Map.
+
+#### Inherited from
+
+`Map.size`
+
+## Methods
+
+### \[iterator\]()
+
+> **\[iterator\]**(): `MapIterator`\<\[`string`, `Type$1`\<[`NgDiagramMinimapNodeTemplate`](/docs/api/types/minimap/ngdiagramminimapnodetemplate/)\>\]\>
+
+Returns an iterable of entries in the map.
+
+#### Returns
+
+`MapIterator`\<\[`string`, `Type$1`\<[`NgDiagramMinimapNodeTemplate`](/docs/api/types/minimap/ngdiagramminimapnodetemplate/)\>\]\>
+
+#### Inherited from
+
+`Map.[iterator]`
+
+***
+
+### delete()
+
+> **delete**(`key`): `boolean`
+
+#### Parameters
+
+##### key
+
+`string`
+
+#### Returns
+
+`boolean`
+
+true if an element in the Map existed and has been removed, or false if the element does not exist.
+
+#### Inherited from
+
+`Map.delete`
+
+***
+
+### entries()
+
+> **entries**(): `MapIterator`\<\[`string`, `Type$1`\<[`NgDiagramMinimapNodeTemplate`](/docs/api/types/minimap/ngdiagramminimapnodetemplate/)\>\]\>
+
+Returns an iterable of key, value pairs for every entry in the map.
+
+#### Returns
+
+`MapIterator`\<\[`string`, `Type$1`\<[`NgDiagramMinimapNodeTemplate`](/docs/api/types/minimap/ngdiagramminimapnodetemplate/)\>\]\>
+
+#### Inherited from
+
+`Map.entries`
+
+***
+
+### forEach()
+
+> **forEach**(`callbackfn`, `thisArg?`): `void`
+
+Executes a provided function once per each key/value pair in the Map, in insertion order.
+
+#### Parameters
+
+##### callbackfn
+
+(`value`, `key`, `map`) => `void`
+
+##### thisArg?
+
+`any`
+
+#### Returns
+
+`void`
+
+#### Inherited from
+
+`Map.forEach`
+
+***
+
+### get()
+
+> **get**(`key`): `undefined` \| `Type$1`\<[`NgDiagramMinimapNodeTemplate`](/docs/api/types/minimap/ngdiagramminimapnodetemplate/)\>
+
+Returns a specified element from the Map object. If the value that is associated to the provided key is an object, then you will get a reference to that object and any change made to that object will effectively modify it inside the Map.
+
+#### Parameters
+
+##### key
+
+`string`
+
+#### Returns
+
+`undefined` \| `Type$1`\<[`NgDiagramMinimapNodeTemplate`](/docs/api/types/minimap/ngdiagramminimapnodetemplate/)\>
+
+Returns the element associated with the specified key. If no element is associated with the specified key, undefined is returned.
+
+#### Inherited from
+
+`Map.get`
+
+***
+
+### has()
+
+> **has**(`key`): `boolean`
+
+#### Parameters
+
+##### key
+
+`string`
+
+#### Returns
+
+`boolean`
+
+boolean indicating whether an element with the specified key exists or not.
+
+#### Inherited from
+
+`Map.has`
+
+***
+
+### keys()
+
+> **keys**(): `MapIterator`\<`string`\>
+
+Returns an iterable of keys in the map
+
+#### Returns
+
+`MapIterator`\<`string`\>
+
+#### Inherited from
+
+`Map.keys`
+
+***
+
+### set()
+
+> **set**(`key`, `value`): `this`
+
+Adds a new element with a specified key and value to the Map. If an element with the same key already exists, the element will be updated.
+
+#### Parameters
+
+##### key
+
+`string`
+
+##### value
+
+`Type$1`
+
+#### Returns
+
+`this`
+
+#### Inherited from
+
+`Map.set`
+
+***
+
+### values()
+
+> **values**(): `MapIterator`\<`Type$1`\<[`NgDiagramMinimapNodeTemplate`](/docs/api/types/minimap/ngdiagramminimapnodetemplate/)\>\>
+
+Returns an iterable of values in the map
+
+#### Returns
+
+`MapIterator`\<`Type$1`\<[`NgDiagramMinimapNodeTemplate`](/docs/api/types/minimap/ngdiagramminimapnodetemplate/)\>\>
+
+#### Inherited from
+
+`Map.values`
diff --git a/apps/docs/src/content/docs/api/Types/NgDiagramPanelPosition.md b/apps/docs/src/content/docs/api/Types/NgDiagramPanelPosition.md
new file mode 100644
index 000000000..c6c472bac
--- /dev/null
+++ b/apps/docs/src/content/docs/api/Types/NgDiagramPanelPosition.md
@@ -0,0 +1,11 @@
+---
+version: "since v1.0.0"
+editUrl: false
+next: false
+prev: false
+title: "NgDiagramPanelPosition"
+---
+
+> **NgDiagramPanelPosition** = `"top-left"` \| `"top-right"` \| `"bottom-left"` \| `"bottom-right"`
+
+Position for diagram overlay panels (minimap, watermark, etc.).
diff --git a/apps/docs/src/content/docs/api/_readme.md b/apps/docs/src/content/docs/api/_readme.md
index da8bc80ab..e4607b270 100644
--- a/apps/docs/src/content/docs/api/_readme.md
+++ b/apps/docs/src/content/docs/api/_readme.md
@@ -13,6 +13,7 @@ title: "ng-diagram"
- [NgDiagramBaseNodeTemplateComponent](/docs/api/components/ngdiagrambasenodetemplatecomponent/)
- [NgDiagramComponent](/docs/api/components/ngdiagramcomponent/)
- [NgDiagramMarkerComponent](/docs/api/components/ngdiagrammarkercomponent/)
+- [NgDiagramMinimapComponent](/docs/api/components/ngdiagramminimapcomponent/)
- [NgDiagramNodeResizeAdornmentComponent](/docs/api/components/ngdiagramnoderesizeadornmentcomponent/)
- [NgDiagramNodeRotateAdornmentComponent](/docs/api/components/ngdiagramnoderotateadornmentcomponent/)
- [NgDiagramPaletteItemComponent](/docs/api/components/ngdiagrampaletteitemcomponent/)
@@ -22,6 +23,7 @@ title: "ng-diagram"
## Directives
- [NgDiagramGroupHighlightedDirective](/docs/api/directives/ngdiagramgrouphighlighteddirective/)
+- [NgDiagramMinimapNavigationDirective](/docs/api/directives/ngdiagramminimapnavigationdirective/)
- [NgDiagramNodeSelectedDirective](/docs/api/directives/ngdiagramnodeselecteddirective/)
## Internals
@@ -51,6 +53,10 @@ title: "ng-diagram"
- [NgDiagramService](/docs/api/services/ngdiagramservice/)
- [NgDiagramViewportService](/docs/api/services/ngdiagramviewportservice/)
+## Types
+
+- [NgDiagramPanelPosition](/docs/api/types/ngdiagrampanelposition/)
+
## Types/Configuration
- [FlowConfig](/docs/api/types/configuration/flowconfig/)
@@ -119,6 +125,14 @@ title: "ng-diagram"
- [ModelActionType](/docs/api/types/middleware/modelactiontype/)
- [ModelActionTypes](/docs/api/types/middleware/modelactiontypes/)
+## Types/Minimap
+
+- [NgDiagramMinimapNodeTemplateMap](/docs/api/types/minimap/ngdiagramminimapnodetemplatemap/)
+- [MinimapNodeStyle](/docs/api/types/minimap/minimapnodestyle/)
+- [NgDiagramMinimapNodeTemplate](/docs/api/types/minimap/ngdiagramminimapnodetemplate/)
+- [MinimapNodeShape](/docs/api/types/minimap/minimapnodeshape/)
+- [MinimapNodeStyleFn](/docs/api/types/minimap/minimapnodestylefn/)
+
## Types/Model
- [Edge](/docs/api/types/model/edge/)
diff --git a/apps/docs/src/content/docs/guides/minimap.mdx b/apps/docs/src/content/docs/guides/minimap.mdx
new file mode 100644
index 000000000..a441ce284
--- /dev/null
+++ b/apps/docs/src/content/docs/guides/minimap.mdx
@@ -0,0 +1,241 @@
+---
+version: 'since v1.0.0'
+title: Minimap
+description: How to use and customize the minimap component in ngDiagram
+sidebar:
+ label: Minimap
+ badge: New
+---
+
+import CodeViewer from '@components/code-viewer/code-viewer.astro';
+import Minimap from '../../../components/angular/minimap/minimap.astro';
+
+The [``](/docs/api/components/ngdiagramminimapcomponent/) component provides a bird's-eye view of your diagram, showing all nodes and the current viewport position.
+It supports click-and-drag navigation to quickly pan to different areas of your diagram.
+
+:::note
+The minimap displays only nodes. Edges are not rendered in the minimap view.
+:::
+
+## Basic Usage
+
+Add the [``](/docs/api/components/ngdiagramminimapcomponent/) component inside your diagram container:
+
+```html
+
+
+
+
+
+
+```
+
+:::note[Service Access]
+The minimap can be placed anywhere in your template, but it requires access to ngDiagram services.
+Make sure the component is within a context where [`provideNgDiagram()`](/docs/api/utilities/providengdiagram/) has been provided.
+:::
+
+:::note[Container Positioning]
+The minimap uses `position: absolute` for corner placement via the `position` input.
+Ensure the parent container has `position: relative` for proper positioning:
+
+```css
+.diagram-container {
+ position: relative;
+}
+```
+
+:::
+
+## Configuration
+
+### Position and Size
+
+The minimap can be positioned in any corner using the [`position`](/docs/api/components/ngdiagramminimapcomponent/#position) input and sized with [`width`](/docs/api/components/ngdiagramminimapcomponent/#width) and [`height`](/docs/api/components/ngdiagramminimapcomponent/#height):
+
+```html
+
+```
+
+See [`NgDiagramPanelPosition`](/docs/api/types/ngdiagrampanelposition/) for available position values.
+
+### Zoom Controls
+
+By default, zoom controls are displayed below the minimap. To hide them:
+
+```html
+
+```
+
+## Customization via CSS Variables
+
+:::tip[Theming]
+Some CSS variables reference design tokens to support light/dark themes.
+For proper multi-theme customization, see the [Styling guide](/docs/intro/styling/) to understand how tokens and primitives work.
+:::
+
+### General Appearance
+
+You can customize the minimap container appearance using CSS variables:
+
+```css
+:root {
+ /* Container styling */
+ --ngd-minimap-background: ...;
+ --ngd-minimap-border-color: ...;
+ --ngd-minimap-border-radius: 1rem;
+ --ngd-minimap-padding: 0.5rem;
+ --ngd-minimap-margin: 1rem;
+ --ngd-minimap-shadow-color: ...;
+
+ /* Viewport indicator */
+ --ngd-minimap-viewport-stroke-color: ...;
+ --ngd-minimap-viewport-stroke-width: 1;
+}
+```
+
+### Node Styling
+
+Default node appearance in the minimap can be controlled via CSS variables:
+
+```css
+:root {
+ --ngd-minimap-node-color: ...;
+ --ngd-minimap-node-opacity: 0.8;
+}
+```
+
+### Zoom Controls
+
+The zoom controls appearance can be customized with these CSS variables:
+
+```css
+:root {
+ /* Zoom controls text */
+ --ngd-zoom-controls-font-size: 0.8125rem;
+ --ngd-zoom-controls-font-weight: 500;
+ --ngd-zoom-controls-color: ...;
+
+ /* Navigation buttons (zoom in/out) */
+ --ngd-nav-button-color: ...;
+ --ngd-nav-button-size: 1.25rem;
+ --ngd-nav-button-border-radius: 0.5rem;
+ --ngd-nav-button-padding: 0.6875rem;
+ --ngd-nav-button-background-color-hover: ...;
+ --ngd-nav-button-color-active: ...;
+ --ngd-nav-button-color-disabled: ...;
+}
+```
+
+## Customization via Style Function
+
+For dynamic node styling based on [`Node`](/docs/api/types/model/node/) properties, use the [`nodeStyle`](/docs/api/components/ngdiagramminimapcomponent/#nodestyle) input.
+This accepts a [`MinimapNodeStyleFn`](/docs/api/types/minimap/minimapnodestylefn/) callback:
+
+```typescript
+import { MinimapNodeStyle, Node } from 'ng-diagram';
+
+nodeStyle = (node: Node): MinimapNodeStyle => {
+ const style: MinimapNodeStyle = {};
+
+ // Different shape for specific nodes
+ if (node.type === 'database') {
+ style.shape = 'circle';
+ }
+
+ // Highlight selected nodes
+ if (node.selected) {
+ style.stroke = '#2196F3';
+ style.strokeWidth = 2;
+ }
+
+ // Custom fill based on node data
+ if (node.data?.status === 'error') {
+ style.fill = '#f44336';
+ }
+
+ return style;
+};
+```
+
+```html
+
+```
+
+See [`MinimapNodeStyle`](/docs/api/types/minimap/minimapnodestyle/) for all available style properties.
+
+## Customization via Templates
+
+For complete control over node rendering, you can provide custom Angular components per node type using [`minimapNodeTemplateMap`](/docs/api/components/ngdiagramminimapcomponent/#minimapnodetemplatemap).
+This is useful when you need to display icons, images, or complex visuals in the minimap.
+
+:::caution[Performance Consideration]
+Custom templates use `foreignObject` and Angular components, which can impact performance with large diagrams.
+Use templates sparingly and only for node types that truly require custom rendering.
+For simple styling changes, prefer the `nodeStyle` function or CSS variables.
+:::
+
+### Creating a Custom Template
+
+Your component must implement the [`NgDiagramMinimapNodeTemplate`](/docs/api/types/minimap/ngdiagramminimapnodetemplate/) interface:
+
+```typescript
+import { ChangeDetectionStrategy, Component, input } from '@angular/core';
+import { MinimapNodeStyle, NgDiagramMinimapNodeTemplate, Node } from 'ng-diagram';
+
+@Component({
+ selector: 'app-image-minimap-node',
+ standalone: true,
+ template: ``,
+ styles: [
+ `
+ :host {
+ display: contents;
+
+ img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ display: block;
+ }
+ }
+ `,
+ ],
+ changeDetection: ChangeDetectionStrategy.OnPush,
+})
+export class ImageMinimapNodeComponent implements NgDiagramMinimapNodeTemplate {
+ node = input.required();
+ nodeStyle = input();
+
+ get imageUrl(): string {
+ const data = this.node().data as { imageUrl?: string };
+ return data?.imageUrl ?? 'placeholder.png';
+ }
+}
+```
+
+### Registering Templates
+
+Create a [`NgDiagramMinimapNodeTemplateMap`](/docs/api/types/minimap/ngdiagramminimapnodetemplatemap/) and pass it to the minimap:
+
+```typescript
+import { NgDiagramMinimapNodeTemplateMap } from 'ng-diagram';
+
+minimapNodeTemplateMap = new NgDiagramMinimapNodeTemplateMap([
+ ['image', ImageMinimapNodeComponent],
+ ['custom-type', CustomMinimapNodeComponent],
+]);
+```
+
+```html
+
+```
+
+Nodes with types not in the map will use the default rectangle rendering.
+
+## Example
+
+
+
+
+---
diff --git a/apps/docs/src/content/docs/intro/styling.mdx b/apps/docs/src/content/docs/intro/styling.mdx
index 98187e403..0644da364 100644
--- a/apps/docs/src/content/docs/intro/styling.mdx
+++ b/apps/docs/src/content/docs/intro/styling.mdx
@@ -31,18 +31,29 @@ Base color values and fundamental design tokens defined in `primitives`. These a
```css
:root {
+ /* Gray scale */
--ngd-colors-gray-100: /* white */;
+ --ngd-colors-gray-200: /* near white */;
--ngd-colors-gray-300: /* light gray */;
--ngd-colors-gray-400: /* medium light gray */;
+ --ngd-colors-gray-450: /* medium gray light */;
--ngd-colors-gray-500: /* medium gray */;
--ngd-colors-gray-600: /* medium dark gray */;
--ngd-colors-gray-650: /* dark gray */;
--ngd-colors-gray-700: /* darker gray */;
--ngd-colors-gray-800: /* darkest gray */;
+ --ngd-colors-gray-900-5: /* near black with 5% opacity */;
+ --ngd-colors-gray-900-20: /* near black with 20% opacity */;
+ --ngd-colors-gray-900-50: /* near black with 50% opacity */;
+
+ /* Primary accent */
--ngd-colors-acc1-400: /* primary accent light */;
--ngd-colors-acc1-500: /* primary accent */;
--ngd-colors-acc1-500-40: /* primary accent with 40% opacity */;
--ngd-colors-acc1-500-50: /* primary accent with 50% opacity */;
+ --ngd-colors-acc1-600: /* primary accent dark */;
+
+ /* Secondary accent */
--ngd-colors-acc4-500: /* secondary accent */;
}
```
diff --git a/packages/ng-diagram/api-report/ng-diagram.api.md b/packages/ng-diagram/api-report/ng-diagram.api.md
index 23d016b78..af26e810e 100644
--- a/packages/ng-diagram/api-report/ng-diagram.api.md
+++ b/packages/ng-diagram/api-report/ng-diagram.api.md
@@ -506,6 +506,22 @@ export interface MiddlewareHistoryUpdate {
stateUpdate: FlowStateUpdate;
}
+// @public
+export type MinimapNodeShape = 'rect' | 'circle' | 'ellipse';
+
+// @public
+export interface MinimapNodeStyle {
+ cssClass?: string;
+ fill?: string;
+ opacity?: number;
+ shape?: MinimapNodeShape;
+ stroke?: string;
+ strokeWidth?: number;
+}
+
+// @public
+export type MinimapNodeStyleFn = (node: Node_2) => MinimapNodeStyle | null | undefined;
+
// @public (undocumented)
export class MobileBoxSelectionDirective {
// (undocumented)
@@ -821,6 +837,64 @@ export const NgDiagramMath: {
calculateEdgePanningForce: (containerBox: Rect, clientPosition: Point, detectionThreshold: number, forceMultiplier: number) => Point | null;
};
+// @public
+export class NgDiagramMinimapComponent implements AfterViewInit {
+ // (undocumented)
+ hasValidViewport: Signal;
+ height: InputSignal;
+ // (undocumented)
+ isDiagramInitialized: WritableSignal;
+ // @internal
+ protected minimapNodes: Signal;
+ minimapNodeTemplateMap: InputSignal;
+ // (undocumented)
+ ngAfterViewInit(): void;
+ // (undocumented)
+ nodes: WritableSignal;
+ // @internal
+ protected nodesGroupTransform: Signal;
+ nodeStyle: InputSignal;
+ position: InputSignal;
+ showZoomControls: InputSignal;
+ // @internal
+ protected transform: Signal;
+ // (undocumented)
+ viewport: WritableSignal;
+ // (undocumented)
+ viewportRect: Signal;
+ width: InputSignal;
+ // (undocumented)
+ static ɵcmp: i0.ɵɵComponentDeclaration;
+ // (undocumented)
+ static ɵfac: i0.ɵɵFactoryDeclaration;
+}
+
+// @public
+export class NgDiagramMinimapNavigationDirective implements OnDestroy {
+ // (undocumented)
+ ngOnDestroy(): void;
+ // (undocumented)
+ onPointerDown(event: PointerEvent): void;
+ // (undocumented)
+ transform: InputSignal;
+ // (undocumented)
+ viewport: InputSignal;
+ // (undocumented)
+ static ɵdir: i0.ɵɵDirectiveDeclaration;
+ // (undocumented)
+ static ɵfac: i0.ɵɵFactoryDeclaration;
+}
+
+// @public
+export interface NgDiagramMinimapNodeTemplate {
+ node: InputSignal;
+ nodeStyle: InputSignal;
+}
+
+// @public
+export class NgDiagramMinimapNodeTemplateMap extends Map> {
+}
+
// @public
export class NgDiagramModelService extends NgDiagramBaseService implements OnDestroy {
constructor();
@@ -983,6 +1057,9 @@ export class NgDiagramPaletteItemPreviewComponent {
static ɵfac: i0.ɵɵFactoryDeclaration;
}
+// @public
+export type NgDiagramPanelPosition = 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
+
// @public
export class NgDiagramPortComponent extends NodeContextGuardBase implements OnInit, OnDestroy, AfterContentInit {
constructor();
@@ -1074,11 +1151,15 @@ export class NgDiagramServicesAvailabilityCheckerDirective {
// @public
export class NgDiagramViewportService extends NgDiagramBaseService {
+ canZoomIn: Signal;
+ canZoomOut: Signal;
centerOnNode(nodeOrId: string | Node_2): void;
centerOnRect(rect: Rect): void;
clientToFlowPosition(clientPosition: Point): Point;
clientToFlowViewportPosition(clientPosition: Point): Point;
flowToClientPosition(flowPosition: Point): Point;
+ get maxZoom(): number;
+ get minZoom(): number;
moveViewport(x: number, y: number): void;
moveViewportBy(dx: number, dy: number): void;
scale: Signal;
diff --git a/packages/ng-diagram/projects/ng-diagram/src/lib/components/diagram/ng-diagram.component.spec.ts b/packages/ng-diagram/projects/ng-diagram/src/lib/components/diagram/ng-diagram.component.spec.ts
index 92afd6cab..b21170b38 100644
--- a/packages/ng-diagram/projects/ng-diagram/src/lib/components/diagram/ng-diagram.component.spec.ts
+++ b/packages/ng-diagram/projects/ng-diagram/src/lib/components/diagram/ng-diagram.component.spec.ts
@@ -13,6 +13,7 @@ import {
import { CursorPositionTrackerService } from '../../services/cursor-position-tracker/cursor-position-tracker.service';
import { InputEventsRouterService } from '../../services/input-events/input-events-router.service';
import { MarkerRegistryService } from '../../services/marker-registry/marker-registry.service';
+import { PanelRegistryService } from '../../services/panel-registry/panel-registry.service';
import { TemplateProviderService } from '../../services/template-provider/template-provider.service';
import { NgDiagramComponent } from './ng-diagram.component';
@@ -112,6 +113,7 @@ describe('AngularAdapterDiagramComponent', () => {
CursorPositionTrackerService,
InputEventsRouterService,
MarkerRegistryService,
+ PanelRegistryService,
],
}).compileComponents();
diff --git a/packages/ng-diagram/projects/ng-diagram/src/lib/components/minimap/default-node/ng-diagram-default-minimap-node.component.html b/packages/ng-diagram/projects/ng-diagram/src/lib/components/minimap/default-node/ng-diagram-default-minimap-node.component.html
new file mode 100644
index 000000000..9964aeb42
--- /dev/null
+++ b/packages/ng-diagram/projects/ng-diagram/src/lib/components/minimap/default-node/ng-diagram-default-minimap-node.component.html
@@ -0,0 +1,16 @@
+@switch (shape()) {
+ @case ('circle') {
+
+ }
+ @case ('ellipse') {
+
+ }
+ @default {
+
+ }
+}
diff --git a/packages/ng-diagram/projects/ng-diagram/src/lib/components/minimap/default-node/ng-diagram-default-minimap-node.component.scss b/packages/ng-diagram/projects/ng-diagram/src/lib/components/minimap/default-node/ng-diagram-default-minimap-node.component.scss
new file mode 100644
index 000000000..09a410c7b
--- /dev/null
+++ b/packages/ng-diagram/projects/ng-diagram/src/lib/components/minimap/default-node/ng-diagram-default-minimap-node.component.scss
@@ -0,0 +1,4 @@
+:host {
+ fill: var(--ngd-minimap-node-color);
+ opacity: var(--ngd-minimap-node-opacity);
+}
diff --git a/packages/ng-diagram/projects/ng-diagram/src/lib/components/minimap/default-node/ng-diagram-default-minimap-node.component.ts b/packages/ng-diagram/projects/ng-diagram/src/lib/components/minimap/default-node/ng-diagram-default-minimap-node.component.ts
new file mode 100644
index 000000000..7bad259f9
--- /dev/null
+++ b/packages/ng-diagram/projects/ng-diagram/src/lib/components/minimap/default-node/ng-diagram-default-minimap-node.component.ts
@@ -0,0 +1,46 @@
+/* eslint-disable @angular-eslint/component-selector */
+import { ChangeDetectionStrategy, Component, computed, input } from '@angular/core';
+import { MinimapBounds, MinimapNodeStyle } from '../ng-diagram-minimap.types';
+
+/**
+ * Internal default component for rendering minimap nodes as SVG shapes.
+ * Renders directly in SVG context (not inside foreignObject) using
+ * pre-transformed minimap coordinates.
+ *
+ * @internal
+ */
+
+@Component({
+ selector: 'g[ng-diagram-default-minimap-node]',
+ standalone: true,
+ templateUrl: './ng-diagram-default-minimap-node.component.html',
+ styleUrl: './ng-diagram-default-minimap-node.component.scss',
+ host: {
+ '[class]': 'hostClass()',
+ '[attr.transform]': 'bounds().transform',
+ '[style.fill]': 'computedStyle().fill',
+ '[style.stroke]': 'computedStyle().stroke',
+ '[style.stroke-width.px]': 'computedStyle().strokeWidth',
+ '[style.opacity]': 'computedStyle().opacity',
+ },
+ changeDetection: ChangeDetectionStrategy.OnPush,
+})
+export class NgDiagramDefaultMinimapNodeComponent {
+ bounds = input.required();
+ nodeStyle = input();
+
+ computedStyle = computed(() => this.nodeStyle() ?? {});
+ shape = computed(() => this.computedStyle().shape ?? 'rect');
+
+ /** Host class combining base class with optional custom cssClass. */
+ hostClass = computed(() => {
+ const cssClass = this.computedStyle().cssClass;
+ return cssClass ? `minimap-node ${cssClass}` : 'minimap-node';
+ });
+
+ cx = computed(() => this.bounds().x + this.bounds().width / 2);
+ cy = computed(() => this.bounds().y + this.bounds().height / 2);
+ radius = computed(() => Math.min(this.bounds().width, this.bounds().height) / 2);
+ rx = computed(() => this.bounds().width / 2);
+ ry = computed(() => this.bounds().height / 2);
+}
diff --git a/packages/ng-diagram/projects/ng-diagram/src/lib/components/minimap/ng-diagram-minimap-navigation.directive.ts b/packages/ng-diagram/projects/ng-diagram/src/lib/components/minimap/ng-diagram-minimap-navigation.directive.ts
new file mode 100644
index 000000000..1af55786a
--- /dev/null
+++ b/packages/ng-diagram/projects/ng-diagram/src/lib/components/minimap/ng-diagram-minimap-navigation.directive.ts
@@ -0,0 +1,128 @@
+import { Directive, inject, input, OnDestroy } from '@angular/core';
+import { Viewport } from '../../../core/src';
+import { NgDiagramViewportService } from '../../public-services/ng-diagram-viewport.service';
+import { MinimapTransform } from './ng-diagram-minimap.types';
+
+interface Point {
+ x: number;
+ y: number;
+}
+
+interface DragState {
+ isDragging: boolean;
+ lastPosition: Point;
+ pointerId: number | null;
+}
+
+/**
+ * Directive that enables drag navigation on the minimap.
+ * Users can drag on the minimap to move the diagram viewport.
+ *
+ * Supports both mouse and touch input.
+ * Uses pointer capture for reliable touch tracking on mobile devices.
+ *
+ * @public
+ * @since 1.0.0
+ * @category Directives
+ */
+@Directive({
+ selector: '[ngDiagramMinimapNavigation]',
+ standalone: true,
+ host: {
+ '(pointerdown)': 'onPointerDown($event)',
+ },
+})
+export class NgDiagramMinimapNavigationDirective implements OnDestroy {
+ private readonly viewportService = inject(NgDiagramViewportService);
+
+ transform = input.required();
+ viewport = input.required();
+
+ private dragState: DragState = {
+ isDragging: false,
+ lastPosition: { x: 0, y: 0 },
+ pointerId: null,
+ };
+
+ ngOnDestroy(): void {
+ this.removeDocumentListeners();
+ }
+
+ onPointerDown(event: PointerEvent): void {
+ if (event.button !== 0) {
+ return;
+ }
+
+ event.preventDefault();
+
+ this.capturePointer(event);
+ this.dragState.isDragging = true;
+ this.dragState.lastPosition = { x: event.clientX, y: event.clientY };
+ this.attachDocumentListeners();
+ }
+
+ private onPointerMove = (event: PointerEvent): void => {
+ if (!this.dragState.isDragging) {
+ return;
+ }
+
+ const delta = this.calculateClientDelta(event);
+ this.dragState.lastPosition = { x: event.clientX, y: event.clientY };
+
+ const viewportDelta = this.calculateViewportDelta(delta);
+ this.viewportService.moveViewportBy(viewportDelta.x, viewportDelta.y);
+ };
+
+ private onPointerUp = (event: PointerEvent): void => {
+ this.dragState.isDragging = false;
+ this.releasePointer(event);
+ this.removeDocumentListeners();
+ };
+
+ private capturePointer(event: PointerEvent): void {
+ const target = event.target as Element;
+ target.setPointerCapture(event.pointerId);
+ this.dragState.pointerId = event.pointerId;
+ }
+
+ private releasePointer(event: PointerEvent): void {
+ if (this.dragState.pointerId === null) {
+ return;
+ }
+
+ const target = event.target as Element;
+ if (target.hasPointerCapture(this.dragState.pointerId)) {
+ target.releasePointerCapture(this.dragState.pointerId);
+ }
+ this.dragState.pointerId = null;
+ }
+
+ private attachDocumentListeners(): void {
+ document.addEventListener('pointermove', this.onPointerMove);
+ document.addEventListener('pointerup', this.onPointerUp);
+ document.addEventListener('pointercancel', this.onPointerUp);
+ }
+
+ private removeDocumentListeners(): void {
+ document.removeEventListener('pointermove', this.onPointerMove);
+ document.removeEventListener('pointerup', this.onPointerUp);
+ document.removeEventListener('pointercancel', this.onPointerUp);
+ }
+
+ private calculateClientDelta(event: PointerEvent): Point {
+ return {
+ x: event.clientX - this.dragState.lastPosition.x,
+ y: event.clientY - this.dragState.lastPosition.y,
+ };
+ }
+
+ private calculateViewportDelta(clientDelta: Point): Point {
+ const { scale: minimapScale } = this.transform();
+ const { scale: viewportScale } = this.viewport();
+
+ return {
+ x: -(clientDelta.x / minimapScale) * viewportScale,
+ y: -(clientDelta.y / minimapScale) * viewportScale,
+ };
+ }
+}
diff --git a/packages/ng-diagram/projects/ng-diagram/src/lib/components/minimap/ng-diagram-minimap.calculations.ts b/packages/ng-diagram/projects/ng-diagram/src/lib/components/minimap/ng-diagram-minimap.calculations.ts
new file mode 100644
index 000000000..82bb90b7c
--- /dev/null
+++ b/packages/ng-diagram/projects/ng-diagram/src/lib/components/minimap/ng-diagram-minimap.calculations.ts
@@ -0,0 +1,142 @@
+import { Node, Rect, unionRect } from '../../../core/src';
+import { MinimapBounds, MinimapTransform, MinimapViewportRect } from './ng-diagram-minimap.types';
+
+/**
+ * Converts viewport position and scale to a bounding rectangle in diagram coordinate space.
+ * This represents the visible area of the diagram.
+ */
+export const convertViewportToDiagramBounds = (viewport: {
+ x: number;
+ y: number;
+ scale: number;
+ width?: number;
+ height?: number;
+}): Rect => {
+ const containerWidth = viewport.width ?? 0;
+ const containerHeight = viewport.height ?? 0;
+
+ return {
+ x: -viewport.x / viewport.scale,
+ y: -viewport.y / viewport.scale,
+ width: containerWidth / viewport.scale,
+ height: containerHeight / viewport.scale,
+ };
+};
+
+/**
+ * Creates a union of diagram content bounds and viewport bounds.
+ * This ensures the minimap shows both the diagram content and the current view position.
+ */
+export const combineDiagramAndViewportBounds = (diagramBounds: Rect, viewportBounds: Rect): Rect => {
+ if (viewportBounds.width === 0 || viewportBounds.height === 0) {
+ return diagramBounds;
+ }
+ return unionRect([diagramBounds, viewportBounds]);
+};
+
+/**
+ * Calculates the scale factor needed to fit bounds within the available minimap space.
+ */
+const calculateScaleToFitBounds = (
+ bounds: Rect,
+ minimapWidth: number,
+ minimapHeight: number,
+ padding: number
+): number => {
+ const availableWidth = minimapWidth - padding * 2;
+ const availableHeight = minimapHeight - padding * 2;
+
+ if (bounds.width === 0 || bounds.height === 0) {
+ return 1;
+ }
+
+ return Math.min(availableWidth / bounds.width, availableHeight / bounds.height);
+};
+
+/**
+ * Calculates the X and Y offsets needed to center the scaled content within the minimap.
+ */
+const calculateCenteringOffset = (
+ bounds: Rect,
+ minimapWidth: number,
+ minimapHeight: number,
+ scale: number
+): { offsetX: number; offsetY: number } => {
+ const scaledWidth = bounds.width * scale;
+ const scaledHeight = bounds.height * scale;
+
+ return {
+ offsetX: (minimapWidth - scaledWidth) / 2 - bounds.x * scale,
+ offsetY: (minimapHeight - scaledHeight) / 2 - bounds.y * scale,
+ };
+};
+
+/**
+ * Computes the complete minimap transform (scale and offset) to fit bounds within minimap dimensions.
+ */
+export const calculateMinimapTransform = (
+ bounds: Rect,
+ minimapWidth: number,
+ minimapHeight: number,
+ padding: number
+): MinimapTransform => {
+ const scale = calculateScaleToFitBounds(bounds, minimapWidth, minimapHeight, padding);
+ const { offsetX, offsetY } = calculateCenteringOffset(bounds, minimapWidth, minimapHeight, scale);
+ return { scale, offsetX, offsetY };
+};
+
+/** Default node size when not specified. */
+const DEFAULT_NODE_SIZE = { width: 100, height: 50 };
+
+/**
+ * Extracts node bounds in diagram coordinate space.
+ * Used with SVG group transform - the group applies scale/offset,
+ * so individual nodes stay in their original diagram coordinates.
+ *
+ * @param node - The diagram node
+ * @param defaultSize - Optional default size for nodes without explicit size
+ */
+export const extractNodeBounds = (
+ node: Node,
+ defaultSize: { width: number; height: number } = DEFAULT_NODE_SIZE
+): MinimapBounds => {
+ const size = node.size ?? defaultSize;
+ const angle = node.angle ?? 0;
+
+ // Calculate rotation center in diagram space
+ const centerX = node.position.x + size.width / 2;
+ const centerY = node.position.y + size.height / 2;
+
+ return {
+ id: node.id,
+ x: node.position.x,
+ y: node.position.y,
+ width: size.width,
+ height: size.height,
+ angle,
+ transform: `rotate(${angle} ${centerX} ${centerY})`,
+ };
+};
+
+/**
+ * Transforms the viewport rectangle to minimap coordinate space.
+ */
+export const transformViewportToMinimapSpace = (
+ viewport: { x: number; y: number; scale: number; width?: number; height?: number },
+ transform: MinimapTransform
+): MinimapViewportRect => {
+ const containerWidth = viewport.width ?? 0;
+ const containerHeight = viewport.height ?? 0;
+
+ const visibleX = -viewport.x / viewport.scale;
+ const visibleY = -viewport.y / viewport.scale;
+ const visibleWidth = containerWidth / viewport.scale;
+ const visibleHeight = containerHeight / viewport.scale;
+
+ return {
+ x: visibleX * transform.scale + transform.offsetX,
+ y: visibleY * transform.scale + transform.offsetY,
+ width: visibleWidth * transform.scale,
+ height: visibleHeight * transform.scale,
+ };
+};
diff --git a/packages/ng-diagram/projects/ng-diagram/src/lib/components/minimap/ng-diagram-minimap.component.html b/packages/ng-diagram/projects/ng-diagram/src/lib/components/minimap/ng-diagram-minimap.component.html
new file mode 100644
index 000000000..189b61907
--- /dev/null
+++ b/packages/ng-diagram/projects/ng-diagram/src/lib/components/minimap/ng-diagram-minimap.component.html
@@ -0,0 +1,46 @@
+
+
+ @if (showZoomControls()) {
+
+ }
+
diff --git a/packages/ng-diagram/projects/ng-diagram/src/lib/components/minimap/ng-diagram-minimap.component.scss b/packages/ng-diagram/projects/ng-diagram/src/lib/components/minimap/ng-diagram-minimap.component.scss
new file mode 100644
index 000000000..1b113413b
--- /dev/null
+++ b/packages/ng-diagram/projects/ng-diagram/src/lib/components/minimap/ng-diagram-minimap.component.scss
@@ -0,0 +1,49 @@
+:host {
+ display: block;
+ position: absolute;
+ pointer-events: auto;
+ z-index: 10;
+
+ &.bottom-right {
+ bottom: var(--ngd-minimap-margin);
+ right: var(--ngd-minimap-margin);
+ }
+
+ &.bottom-left {
+ bottom: var(--ngd-minimap-margin);
+ left: var(--ngd-minimap-margin);
+ }
+
+ &.top-right {
+ top: var(--ngd-minimap-margin);
+ right: var(--ngd-minimap-margin);
+ }
+
+ &.top-left {
+ top: var(--ngd-minimap-margin);
+ left: var(--ngd-minimap-margin);
+ }
+}
+
+.minimap-svg {
+ display: block;
+ cursor: pointer;
+ touch-action: none;
+}
+
+.minimap-background {
+ fill: transparent;
+}
+
+.minimap-viewport {
+ stroke: var(--ngd-minimap-viewport-stroke-color);
+ stroke-width: var(--ngd-minimap-viewport-stroke-width);
+ fill: transparent;
+ pointer-events: none;
+}
+
+.minimap-footer {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+}
diff --git a/packages/ng-diagram/projects/ng-diagram/src/lib/components/minimap/ng-diagram-minimap.component.ts b/packages/ng-diagram/projects/ng-diagram/src/lib/components/minimap/ng-diagram-minimap.component.ts
new file mode 100644
index 000000000..5fe185f6c
--- /dev/null
+++ b/packages/ng-diagram/projects/ng-diagram/src/lib/components/minimap/ng-diagram-minimap.component.ts
@@ -0,0 +1,194 @@
+import { CommonModule } from '@angular/common';
+import {
+ AfterViewInit,
+ ChangeDetectionStrategy,
+ Component,
+ computed,
+ ElementRef,
+ inject,
+ input,
+ signal,
+} from '@angular/core';
+import { NgDiagramModelService } from '../../public-services/ng-diagram-model.service';
+import { RendererService } from '../../services/renderer/renderer.service';
+import { NgDiagramPanelPosition } from '../../types/panel-position';
+import { NgDiagramPanelComponent } from '../panel/ng-diagram-panel.component';
+import { NgDiagramZoomControlsComponent } from '../zoom-controls/ng-diagram-zoom-controls.component';
+import { NgDiagramDefaultMinimapNodeComponent } from './default-node/ng-diagram-default-minimap-node.component';
+import { NgDiagramMinimapNavigationDirective } from './ng-diagram-minimap-navigation.directive';
+import {
+ calculateMinimapTransform,
+ combineDiagramAndViewportBounds,
+ convertViewportToDiagramBounds,
+ extractNodeBounds,
+ transformViewportToMinimapSpace,
+} from './ng-diagram-minimap.calculations';
+import { MinimapNodeData, MinimapNodeStyleFn, NgDiagramMinimapNodeTemplateMap } from './ng-diagram-minimap.types';
+
+/**
+ * A minimap component that displays a bird's-eye view of the diagram.
+ *
+ * Shows all nodes as small rectangles and a viewport rectangle indicating
+ * the currently visible area. The minimap updates reactively when the
+ * diagram viewport changes (pan/zoom) or when nodes are added/removed/updated.
+ *
+ * The minimap also supports navigation: click and drag on the minimap to pan
+ * the diagram viewport to different areas.
+ *
+ * @public
+ * @since 1.0.0
+ * @category Components
+ */
+@Component({
+ selector: 'ng-diagram-minimap',
+ standalone: true,
+ imports: [
+ NgDiagramMinimapNavigationDirective,
+ CommonModule,
+ NgDiagramDefaultMinimapNodeComponent,
+ NgDiagramZoomControlsComponent,
+ NgDiagramPanelComponent,
+
+ NgDiagramMinimapNavigationDirective,
+ CommonModule,
+ NgDiagramDefaultMinimapNodeComponent,
+ NgDiagramZoomControlsComponent,
+ NgDiagramPanelComponent,
+ ],
+ templateUrl: './ng-diagram-minimap.component.html',
+ styleUrls: ['./ng-diagram-minimap.component.scss'],
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ host: {
+ '[class]': 'position()',
+ },
+})
+export class NgDiagramMinimapComponent implements AfterViewInit {
+ private readonly VIEWPORT_STROKE_WIDTH_CSS_VAR = '--ngd-minimap-viewport-stroke-width';
+
+ private readonly modelService = inject(NgDiagramModelService);
+ private readonly renderer = inject(RendererService);
+ private readonly elementRef = inject(ElementRef);
+
+ /** Cached stroke padding to avoid layout thrashing from repeated getComputedStyle calls. */
+ private strokePadding = signal(1);
+
+ /** Position of the minimap panel within the diagram container. */
+ position = input('bottom-right');
+
+ /** Width of the minimap in pixels. */
+ width = input(200);
+
+ /** Height of the minimap in pixels. */
+ height = input(150);
+
+ /** Whether to show zoom controls in the minimap footer. */
+ showZoomControls = input(true);
+
+ /**
+ * Optional callback function to customize node styling.
+ * Return style properties to override defaults, or null/undefined to use CSS defaults.
+ *
+ * @example
+ * ```typescript
+ * nodeStyle = (node: Node) => ({
+ * fill: node.type === 'database' ? '#4CAF50' : '#9E9E9E',
+ * opacity: node.selected ? 1 : 0.6,
+ * });
+ * ```
+ */
+ nodeStyle = input();
+
+ /**
+ * Optional template map for complete control over node rendering per node type.
+ * Components registered in the map should render SVG elements.
+ *
+ * @example
+ * ```typescript
+ * const minimapTemplateMap = new NgDiagramMinimapNodeTemplateMap([
+ * ['database', DatabaseMinimapNodeComponent],
+ * ['api', ApiMinimapNodeComponent],
+ * ]);
+ *
+ * // Usage:
+ *
+ * ```
+ */
+ minimapNodeTemplateMap = input(new NgDiagramMinimapNodeTemplateMap());
+
+ /** @ignore */
+ ngAfterViewInit(): void {
+ // Cache stroke padding after view init to avoid layout thrashing
+ this.strokePadding.set(this.getStrokePaddingFromCss());
+ }
+
+ isDiagramInitialized = this.renderer.isInitialized;
+ nodes = this.renderer.nodes;
+ viewport = this.renderer.viewport;
+
+ hasValidViewport = computed(() => {
+ const viewport = this.viewport();
+ return !!viewport.width && !!viewport.height && viewport.width > 0 && viewport.height > 0;
+ });
+
+ viewportRect = computed(() => transformViewportToMinimapSpace(this.viewport(), this.transform()));
+
+ /**
+ * @internal
+ * Main transform for minimap - updates when viewport or diagram bounds change.
+ * Used for SVG group transform and viewport rect calculation.
+ */
+ protected transform = computed(() =>
+ calculateMinimapTransform(this.combinedBounds(), this.width(), this.height(), this.strokePadding())
+ );
+
+ /**
+ * @internal
+ * SVG transform attribute for the nodes group.
+ * Converts diagram coordinates to minimap coordinates.
+ */
+ protected nodesGroupTransform = computed(() => {
+ const t = this.transform();
+ return `translate(${t.offsetX}, ${t.offsetY}) scale(${t.scale})`;
+ });
+
+ /**
+ * @internal
+ * Pre-computed minimap node data in DIAGRAM coordinates (not minimap coordinates).
+ * The SVG group transform handles the coordinate conversion, so nodes only recalculate
+ * when diagram content changes, NOT during pan/zoom.
+ */
+ protected minimapNodes = computed((): MinimapNodeData[] => {
+ const nodes = this.nodes();
+ const styleFn = this.nodeStyle();
+ const templateMap = this.minimapNodeTemplateMap();
+
+ return nodes.map((node) => ({
+ bounds: extractNodeBounds(node),
+ diagramNode: node,
+ nodeStyle: styleFn?.(node) ?? {},
+ template: node.type ? (templateMap.get(node.type) ?? null) : null,
+ }));
+ });
+
+ private diagramBounds = computed(() => {
+ const nodes = this.nodes();
+ return this.modelService.computePartsBounds(nodes, []);
+ });
+
+ private viewportBoundsInDiagramSpace = computed(() => convertViewportToDiagramBounds(this.viewport()));
+
+ private combinedBounds = computed(() =>
+ combineDiagramAndViewportBounds(this.diagramBounds(), this.viewportBoundsInDiagramSpace())
+ );
+
+ /**
+ * Reads viewport stroke width from CSS to use as internal padding.
+ * This prevents the viewport rectangle stroke from being clipped at minimap edges.
+ */
+ private getStrokePaddingFromCss(): number {
+ const style = getComputedStyle(this.elementRef.nativeElement);
+ const strokeWidth = style.getPropertyValue(this.VIEWPORT_STROKE_WIDTH_CSS_VAR).trim();
+
+ return parseFloat(strokeWidth) || 1;
+ }
+}
diff --git a/packages/ng-diagram/projects/ng-diagram/src/lib/components/minimap/ng-diagram-minimap.types.ts b/packages/ng-diagram/projects/ng-diagram/src/lib/components/minimap/ng-diagram-minimap.types.ts
new file mode 100644
index 000000000..2f2649b60
--- /dev/null
+++ b/packages/ng-diagram/projects/ng-diagram/src/lib/components/minimap/ng-diagram-minimap.types.ts
@@ -0,0 +1,157 @@
+import { InputSignal, Type } from '@angular/core';
+import { Node } from '../../../core/src';
+
+/**
+ * Represents the calculated transform data for minimap rendering.
+ */
+export interface MinimapTransform {
+ scale: number;
+ offsetX: number;
+ offsetY: number;
+}
+
+/**
+ * Pre-computed node data for minimap rendering.
+ * Contains transformed bounds, original node reference, styling, and optional custom template.
+ */
+export interface MinimapNodeData {
+ bounds: MinimapBounds;
+ diagramNode: Node;
+ nodeStyle: MinimapNodeStyle;
+ template: Type | null;
+}
+
+/**
+ * Bounding box and transform for a node in minimap coordinate space.
+ *
+ * @public
+ * @since 1.0.0
+ * @category Types/Minimap
+ */
+export interface MinimapBounds {
+ id: string;
+ x: number;
+ y: number;
+ width: number;
+ height: number;
+ angle: number;
+ transform: string;
+}
+
+/**
+ * Represents the viewport rectangle in minimap space.
+ */
+export interface MinimapViewportRect {
+ x: number;
+ y: number;
+ width: number;
+ height: number;
+}
+
+/**
+ * Available shapes for minimap node rendering.
+ *
+ * @public
+ * @since 1.0.0
+ * @category Types/Minimap
+ */
+export type MinimapNodeShape = 'rect' | 'circle' | 'ellipse';
+
+/**
+ * Style properties that can be applied to minimap nodes.
+ * All properties are optional - unset properties use CSS defaults.
+ *
+ * @public
+ * @since 1.0.0
+ * @category Types/Minimap
+ */
+export interface MinimapNodeStyle {
+ /** Shape of the node in the minimap. Defaults to 'rect'. */
+ shape?: MinimapNodeShape;
+ /** Fill color for the node */
+ fill?: string;
+ /** Stroke color for the node */
+ stroke?: string;
+ /** Stroke width in pixels */
+ strokeWidth?: number;
+ /** Opacity from 0 to 1 */
+ opacity?: number;
+ /** CSS class to apply to the node */
+ cssClass?: string;
+}
+
+/**
+ * Function signature for the nodeStyle callback.
+ * Return style properties to override defaults, or null/undefined to use defaults.
+ *
+ * @public
+ * @since 1.0.0
+ * @category Types/Minimap
+ *
+ * @example
+ * ```typescript
+ * const nodeStyle: MinimapNodeStyleFn = (node) => ({
+ * fill: node.type === 'database' ? '#4CAF50' : '#9E9E9E',
+ * opacity: node.selected ? 1 : 0.6,
+ * });
+ * ```
+ */
+export type MinimapNodeStyleFn = (node: Node) => MinimapNodeStyle | null | undefined;
+
+/**
+ * Interface for custom minimap node components.
+ * Components implementing this interface can be registered in NgDiagramMinimapNodeTemplateMap
+ * to customize how specific node types are rendered in the minimap.
+ *
+ * Custom templates are rendered inside a foreignObject that handles positioning and sizing,
+ * so the component only needs to render content that fills its container.
+ *
+ * @public
+ * @since 1.0.0
+ * @category Types/Minimap
+ *
+ * @example
+ * ```typescript
+ * @Component({
+ * selector: 'my-minimap-node',
+ * standalone: true,
+ * template: `
+ *
+ * {{ node().type }}
+ *
+ * `,
+ * styles: [`.minimap-icon { width: 100%; height: 100%; }`]
+ * })
+ * export class MyMinimapNodeComponent implements NgDiagramMinimapNodeTemplate {
+ * node = input.required();
+ * nodeStyle = input(); // Required by interface, can be ignored if not needed
+ * }
+ * ```
+ */
+export interface NgDiagramMinimapNodeTemplate {
+ /** Input signal containing the original Node object for accessing node data, type, etc. */
+ node: InputSignal;
+ /** Input signal for style overrides computed by nodeStyle callback. Can be ignored if not needed. */
+ nodeStyle: InputSignal;
+}
+
+/**
+ * Map that associates node type names with their corresponding minimap Angular component classes.
+ * Used by ng-diagram-minimap to determine which custom component to render based on node type.
+ *
+ * @public
+ * @since 1.0.0
+ * @category Types/Minimap
+ *
+ * @example
+ * ```typescript
+ * const minimapTemplateMap = new NgDiagramMinimapNodeTemplateMap([
+ * ['database', DatabaseMinimapNodeComponent],
+ * ['api', ApiMinimapNodeComponent],
+ * ]);
+ *
+ * // Usage in template:
+ *
+ * ```
+ */
+export class NgDiagramMinimapNodeTemplateMap extends Map> {}
diff --git a/packages/ng-diagram/projects/ng-diagram/src/lib/components/panel/ng-diagram-panel.component.scss b/packages/ng-diagram/projects/ng-diagram/src/lib/components/panel/ng-diagram-panel.component.scss
new file mode 100644
index 000000000..aac47e382
--- /dev/null
+++ b/packages/ng-diagram/projects/ng-diagram/src/lib/components/panel/ng-diagram-panel.component.scss
@@ -0,0 +1,11 @@
+:host {
+ overflow: hidden;
+ border-radius: var(--ngd-minimap-border-radius);
+ padding: var(--ngd-minimap-padding);
+ background-color: var(--ngd-minimap-background);
+ box-shadow: 0 8px 16px -4px var(--ngd-minimap-shadow-color);
+ border: 1px solid var(--ngd-minimap-border-color);
+ display: flex;
+ flex-direction: column;
+ gap: var(--ngd-minimap-padding);
+}
diff --git a/packages/ng-diagram/projects/ng-diagram/src/lib/components/panel/ng-diagram-panel.component.ts b/packages/ng-diagram/projects/ng-diagram/src/lib/components/panel/ng-diagram-panel.component.ts
new file mode 100644
index 000000000..9b6bcb728
--- /dev/null
+++ b/packages/ng-diagram/projects/ng-diagram/src/lib/components/panel/ng-diagram-panel.component.ts
@@ -0,0 +1,33 @@
+import { AfterViewInit, ChangeDetectionStrategy, Component, inject, input, OnDestroy } from '@angular/core';
+import { PanelRegistryService } from '../../services/panel-registry/panel-registry.service';
+import { NgDiagramPanelPosition } from '../../types/panel-position';
+
+/**
+ * A generic panel container component that registers itself with the panel registry.
+ * Used internally to wrap overlay content and coordinate positioning with other components.
+ * @internal
+ */
+@Component({
+ selector: 'ng-diagram-panel',
+ standalone: true,
+ template: '',
+ styleUrls: ['./ng-diagram-panel.component.scss'],
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ host: {
+ '[class]': 'position()',
+ },
+})
+export class NgDiagramPanelComponent implements AfterViewInit, OnDestroy {
+ private readonly panelRegistry = inject(PanelRegistryService);
+
+ /** Position of the panel within the diagram container. */
+ position = input('bottom-right');
+
+ ngAfterViewInit(): void {
+ this.panelRegistry?.register(this);
+ }
+
+ ngOnDestroy(): void {
+ this.panelRegistry?.unregister();
+ }
+}
diff --git a/packages/ng-diagram/projects/ng-diagram/src/lib/components/watermark/watermark.component.scss b/packages/ng-diagram/projects/ng-diagram/src/lib/components/watermark/watermark.component.scss
index 0e4e265d4..fac9cbede 100644
--- a/packages/ng-diagram/projects/ng-diagram/src/lib/components/watermark/watermark.component.scss
+++ b/packages/ng-diagram/projects/ng-diagram/src/lib/components/watermark/watermark.component.scss
@@ -1,9 +1,27 @@
:host {
position: absolute;
- bottom: 1rem;
- right: 1rem;
width: var(--watermark-size);
+ &.bottom-right {
+ bottom: 1rem;
+ right: 1rem;
+ }
+
+ &.top-right {
+ top: 1rem;
+ right: 1rem;
+ }
+
+ &.bottom-left {
+ bottom: 1rem;
+ left: 1rem;
+ }
+
+ &.top-left {
+ top: 1rem;
+ left: 1rem;
+ }
+
svg {
opacity: 0.7;
height: 100%;
diff --git a/packages/ng-diagram/projects/ng-diagram/src/lib/components/watermark/watermark.component.ts b/packages/ng-diagram/projects/ng-diagram/src/lib/components/watermark/watermark.component.ts
index 188e9f128..c73fa4b7b 100644
--- a/packages/ng-diagram/projects/ng-diagram/src/lib/components/watermark/watermark.component.ts
+++ b/packages/ng-diagram/projects/ng-diagram/src/lib/components/watermark/watermark.component.ts
@@ -1,9 +1,31 @@
-import { Component } from '@angular/core';
+import { Component, computed, inject } from '@angular/core';
+import { PanelRegistryService } from '../../services/panel-registry/panel-registry.service';
+import { NgDiagramPanelPosition } from '../../types/panel-position';
@Component({
selector: 'ng-diagram-watermark',
standalone: true,
templateUrl: './watermark.component.html',
styleUrls: ['./watermark.component.scss'],
+ host: {
+ '[class]': 'position()',
+ },
})
-export class NgDiagramWatermarkComponent {}
+export class NgDiagramWatermarkComponent {
+ private readonly DEFAULT_POSITION: NgDiagramPanelPosition = 'bottom-right';
+ private readonly ALTERNATIVE_POSITION: NgDiagramPanelPosition = 'top-right';
+
+ private readonly panelRegistry = inject(PanelRegistryService);
+
+ /**
+ * Computes the watermark position based on panel position.
+ * If panel is at the same position, watermark moves to avoid overlap.
+ */
+ position = computed(() => {
+ if (this.panelRegistry.position() === this.DEFAULT_POSITION) {
+ return this.ALTERNATIVE_POSITION;
+ }
+
+ return this.DEFAULT_POSITION;
+ });
+}
diff --git a/packages/ng-diagram/projects/ng-diagram/src/lib/components/zoom-controls/ng-diagram-zoom-controls.component.scss b/packages/ng-diagram/projects/ng-diagram/src/lib/components/zoom-controls/ng-diagram-zoom-controls.component.scss
new file mode 100644
index 000000000..9d895de14
--- /dev/null
+++ b/packages/ng-diagram/projects/ng-diagram/src/lib/components/zoom-controls/ng-diagram-zoom-controls.component.scss
@@ -0,0 +1,40 @@
+.zoom-controls-container {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+
+ .label {
+ text-align: center;
+ font-size: var(--ngd-zoom-controls-font-size);
+ font-weight: var(--ngd-zoom-controls-font-weight);
+ color: var(--ngd-zoom-controls-color);
+ user-select: none;
+ }
+}
+
+.nav-button {
+ width: var(--ngd-nav-button-size);
+ height: var(--ngd-nav-button-size);
+ color: var(--ngd-nav-button-color);
+ border-radius: var(--ngd-nav-button-border-radius);
+ padding: var(--ngd-nav-button-padding);
+ border: none;
+ background: none;
+ box-sizing: content-box;
+ cursor: pointer;
+ font-size: var(--ngd-nav-button-size);
+ line-height: var(--ngd-nav-button-size);
+
+ &:hover:not(:disabled) {
+ background-color: var(--ngd-nav-button-background-color-hover);
+ }
+
+ &:active {
+ color: var(--ngd-nav-button-color-active);
+ }
+
+ &:disabled {
+ color: var(--ngd-nav-button-color-disabled);
+ cursor: default;
+ }
+}
diff --git a/packages/ng-diagram/projects/ng-diagram/src/lib/components/zoom-controls/ng-diagram-zoom-controls.component.ts b/packages/ng-diagram/projects/ng-diagram/src/lib/components/zoom-controls/ng-diagram-zoom-controls.component.ts
new file mode 100644
index 000000000..929a3a079
--- /dev/null
+++ b/packages/ng-diagram/projects/ng-diagram/src/lib/components/zoom-controls/ng-diagram-zoom-controls.component.ts
@@ -0,0 +1,48 @@
+import { ChangeDetectionStrategy, Component, computed, inject, input } from '@angular/core';
+import { NgDiagramViewportService } from '../../public-services/ng-diagram-viewport.service';
+
+/**
+ * A zoom controls component that displays zoom in/out buttons and current zoom level.
+ */
+@Component({
+ selector: 'ng-diagram-zoom-controls',
+ standalone: true,
+ template: `
+
+
+ {{ zoomPercentage() }}%
+
+
+ `,
+ styleUrls: ['./ng-diagram-zoom-controls.component.scss'],
+ changeDetection: ChangeDetectionStrategy.OnPush,
+})
+export class NgDiagramZoomControlsComponent {
+ private readonly viewportService = inject(NgDiagramViewportService);
+
+ step = input(0.1);
+
+ zoomPercentage = computed(() => Math.round(this.viewportService.scale() * 100));
+
+ canZoomIn = computed(() => {
+ const newScale = this.viewportService.scale() + this.step();
+ return newScale <= this.viewportService.maxZoom;
+ });
+
+ canZoomOut = computed(() => {
+ const newScale = this.viewportService.scale() - this.step();
+ return newScale >= this.viewportService.minZoom;
+ });
+
+ zoomIn(): void {
+ const currentScale = this.viewportService.scale();
+ const factor = (currentScale + this.step()) / currentScale;
+ this.viewportService.zoom(factor);
+ }
+
+ zoomOut(): void {
+ const currentScale = this.viewportService.scale();
+ const factor = (currentScale - this.step()) / currentScale;
+ this.viewportService.zoom(factor);
+ }
+}
diff --git a/packages/ng-diagram/projects/ng-diagram/src/lib/providers/ng-diagram.providers.ts b/packages/ng-diagram/projects/ng-diagram/src/lib/providers/ng-diagram.providers.ts
index 25c5b76ea..e7120247b 100644
--- a/packages/ng-diagram/projects/ng-diagram/src/lib/providers/ng-diagram.providers.ts
+++ b/packages/ng-diagram/projects/ng-diagram/src/lib/providers/ng-diagram.providers.ts
@@ -14,6 +14,7 @@ import { InputEventsRouterService } from '../services/input-events/input-events-
import { LinkingEventService } from '../services/input-events/linking-event.service';
import { ManualLinkingService } from '../services/input-events/manual-linking.service';
import { MarkerRegistryService } from '../services/marker-registry/marker-registry.service';
+import { PanelRegistryService } from '../services/panel-registry/panel-registry.service';
import { PaletteService } from '../services/palette/palette.service';
import { RendererService } from '../services/renderer/renderer.service';
import { TemplateProviderService } from '../services/template-provider/template-provider.service';
@@ -69,5 +70,6 @@ export function provideNgDiagram(): Provider[] {
ManualLinkingService,
TemplateProviderService,
MarkerRegistryService,
+ PanelRegistryService,
];
}
diff --git a/packages/ng-diagram/projects/ng-diagram/src/lib/public-services/ng-diagram-viewport.service.ts b/packages/ng-diagram/projects/ng-diagram/src/lib/public-services/ng-diagram-viewport.service.ts
index d49ddb559..859f24be9 100644
--- a/packages/ng-diagram/projects/ng-diagram/src/lib/public-services/ng-diagram-viewport.service.ts
+++ b/packages/ng-diagram/projects/ng-diagram/src/lib/public-services/ng-diagram-viewport.service.ts
@@ -38,6 +38,30 @@ export class NgDiagramViewportService extends NgDiagramBaseService {
*/
scale = computed(() => this.renderer.viewport().scale || 1);
+ /**
+ * Returns the minimum zoom scale from the diagram configuration.
+ */
+ get minZoom(): number {
+ return this.flowCore.config.zoom.min;
+ }
+
+ /**
+ * Returns the maximum zoom scale from the diagram configuration.
+ */
+ get maxZoom(): number {
+ return this.flowCore.config.zoom.max;
+ }
+
+ /**
+ * Returns true if the current zoom level is below the maximum and can be increased.
+ */
+ canZoomIn = computed(() => this.scale() < this.maxZoom);
+
+ /**
+ * Returns true if the current zoom level is above the minimum and can be decreased.
+ */
+ canZoomOut = computed(() => this.scale() > this.minZoom);
+
// ===================
// POSITION CONVERSION METHODS
// ===================
@@ -97,13 +121,15 @@ export class NgDiagramViewportService extends NgDiagramBaseService {
/**
* Zooms the viewport by the specified factor.
- * @param factor The factor to zoom by.
+ * @param factor The factor to zoom by (e.g., 1.1 for 10% zoom in, 0.9 for 10% zoom out).
* @param center The center point to zoom towards.
*/
zoom(factor: number, center?: Point | undefined) {
- const x = center?.x || this.viewport().x;
- const y = center?.y || this.viewport().y;
- this.flowCore.commandHandler.emit('zoom', { scale: factor, x, y });
+ const currentScale = this.scale();
+ const newScale = currentScale * factor;
+ const x = center?.x ?? this.viewport().x;
+ const y = center?.y ?? this.viewport().y;
+ this.flowCore.commandHandler.emit('zoom', { scale: newScale, x, y });
}
/**
diff --git a/packages/ng-diagram/projects/ng-diagram/src/lib/services/panel-registry/panel-registry.service.ts b/packages/ng-diagram/projects/ng-diagram/src/lib/services/panel-registry/panel-registry.service.ts
new file mode 100644
index 000000000..7aea4e683
--- /dev/null
+++ b/packages/ng-diagram/projects/ng-diagram/src/lib/services/panel-registry/panel-registry.service.ts
@@ -0,0 +1,31 @@
+import { computed, Injectable, Signal, signal } from '@angular/core';
+import { NgDiagramPanelPosition } from '../../types/panel-position';
+
+/**
+ * Interface for panel components that can register with the panel registry.
+ * @internal
+ */
+export interface RegisterablePanel {
+ position: Signal;
+}
+
+/**
+ * Service that tracks registered panel components.
+ * Used by other components (like watermark) to coordinate their positioning
+ * and avoid overlapping with panels.
+ * @internal
+ */
+@Injectable()
+export class PanelRegistryService {
+ private readonly panel = signal(null);
+
+ position = computed(() => this.panel()?.position());
+
+ register(component: RegisterablePanel): void {
+ this.panel.set(component);
+ }
+
+ unregister(): void {
+ this.panel.set(null);
+ }
+}
diff --git a/packages/ng-diagram/projects/ng-diagram/src/lib/styles/primitives.css b/packages/ng-diagram/projects/ng-diagram/src/lib/styles/primitives.css
index dff957d17..8e29ed09c 100644
--- a/packages/ng-diagram/projects/ng-diagram/src/lib/styles/primitives.css
+++ b/packages/ng-diagram/projects/ng-diagram/src/lib/styles/primitives.css
@@ -13,9 +13,13 @@
--ngd-colors-gray-650: #383a40;
--ngd-colors-gray-700: #27282b;
--ngd-colors-gray-800: #151516;
+ --ngd-colors-gray-900-5: rgba(7, 7, 8, 0.05);
+ --ngd-colors-gray-900-20: rgba(7, 7, 8, 0.2);
+ --ngd-colors-gray-900-50: rgba(7, 7, 8, 0.5);
--ngd-colors-acc1-400: #a977ff;
--ngd-colors-acc1-500: #9140ff;
--ngd-colors-acc1-500-40: rgba(145, 64, 255, 0.4);
--ngd-colors-acc1-500-50: rgba(145, 64, 255, 0.5);
+ --ngd-colors-acc1-600: #891aff;
--ngd-colors-acc4-500: #1096e7;
}
diff --git a/packages/ng-diagram/projects/ng-diagram/src/lib/styles/styles.css b/packages/ng-diagram/projects/ng-diagram/src/lib/styles/styles.css
index 5ddf65896..83a872131 100644
--- a/packages/ng-diagram/projects/ng-diagram/src/lib/styles/styles.css
+++ b/packages/ng-diagram/projects/ng-diagram/src/lib/styles/styles.css
@@ -64,6 +64,31 @@
--ngd-background-line-major-width: 1;
--ngd-background-line-minor-opacity: 0.5;
--ngd-background-line-major-opacity: 0.6;
+
+ --ngd-minimap-background: var(--ngd-minimap-bg-primary-default);
+ --ngd-minimap-border-color: var(--ngd-minimap-stroke-primary-default);
+ --ngd-minimap-shadow-color: var(--ngd-shadow);
+ --ngd-minimap-border-radius: 1rem;
+ --ngd-minimap-padding: 0.5rem;
+ --ngd-minimap-margin: 1rem;
+
+ --ngd-minimap-node-color: var(--ngd-colors-gray-400);
+ --ngd-minimap-node-opacity: 0.8;
+
+ --ngd-minimap-viewport-stroke-color: var(--ngd-colors-gray-400);
+ --ngd-minimap-viewport-stroke-width: 1;
+
+ --ngd-nav-button-color: var(--ngd-nav-button-primary-default);
+ --ngd-nav-button-size: 1.25rem;
+ --ngd-nav-button-border-radius: 0.5rem;
+ --ngd-nav-button-padding: 0.6875rem;
+ --ngd-nav-button-background-color-hover: var(--ngd-nav-button-primary-hover);
+ --ngd-nav-button-color-active: var(--ngd-nav-button-primary-pressed);
+ --ngd-nav-button-color-disabled: var(--ngd-nav-button-primary-disabled);
+
+ --ngd-zoom-controls-font-size: 0.8125rem;
+ --ngd-zoom-controls-font-weight: 500;
+ --ngd-zoom-controls-color: var(--ngd-nav-button-primary-default);
}
.ng-diagram-port-hoverable .ng-diagram-port:hover,
diff --git a/packages/ng-diagram/projects/ng-diagram/src/lib/styles/tokens.css b/packages/ng-diagram/projects/ng-diagram/src/lib/styles/tokens.css
index ef98e54b6..e46718977 100644
--- a/packages/ng-diagram/projects/ng-diagram/src/lib/styles/tokens.css
+++ b/packages/ng-diagram/projects/ng-diagram/src/lib/styles/tokens.css
@@ -2,7 +2,12 @@
/* DEFAULT LIGHT THEME */
:root {
+ --ngd-shadow: var(--ngd-colors-gray-900-20);
--ngd-txt-primary-default: var(--ngd-colors-gray-800);
+ --ngd-nav-button-primary-default: var(--ngd-colors-gray-600);
+ --ngd-nav-button-primary-pressed: var(--ngd-colors-acc1-600);
+ --ngd-nav-button-primary-hover: var(--ngd-colors-gray-900-5);
+ --ngd-nav-button-primary-disabled: var(--ngd-colors-gray-450);
--ngd-diagram-background-color: var(--ngd-colors-gray-200);
--ngd-ui-bg-primary-default: var(--ngd-colors-gray-100);
--ngd-ui-bg-tertiary-default: var(--ngd-colors-gray-200);
@@ -24,11 +29,18 @@
--ngd-background-line-minor-color: var(--ngd-colors-gray-450);
--ngd-background-line-major-color: var(--ngd-colors-gray-500);
--ngd-ui-border-color: var(--ngd-colors-gray-300);
+ --ngd-minimap-bg-primary-default: var(--ngd-colors-gray-100);
+ --ngd-minimap-stroke-primary-default: var(--ngd-colors-gray-400);
}
/* OPTIONAL DARK THEME */
html[data-theme='dark'] {
+ --ngd-shadow: var(--ngd-colors-gray-900-50);
--ngd-txt-primary-default: var(--ngd-colors-gray-100);
+ --ngd-nav-button-primary-default: var(--ngd-colors-gray-400);
+ --ngd-nav-button-primary-pressed: var(--ngd-colors-acc1-500);
+ --ngd-nav-button-primary-hover: var(--ngd-colors-gray-900-5);
+ --ngd-nav-button-primary-disabled: var(--ngd-colors-gray-600);
--ngd-diagram-background-color: var(--ngd-colors-gray-800);
--ngd-ui-bg-primary-default: var(--ngd-colors-gray-700);
--ngd-ui-bg-tertiary-default: var(--ngd-colors-gray-800);
@@ -50,4 +62,6 @@ html[data-theme='dark'] {
--ngd-background-line-minor-color: var(--ngd-colors-gray-700);
--ngd-background-line-major-color: var(--ngd-colors-gray-600);
--ngd-ui-border-color: var(--ngd-colors-gray-700);
+ --ngd-minimap-bg-primary-default: var(--ngd-colors-gray-700);
+ --ngd-minimap-stroke-primary-default: var(--ngd-colors-gray-600);
}
diff --git a/packages/ng-diagram/projects/ng-diagram/src/lib/types/panel-position.ts b/packages/ng-diagram/projects/ng-diagram/src/lib/types/panel-position.ts
new file mode 100644
index 000000000..695bb26ff
--- /dev/null
+++ b/packages/ng-diagram/projects/ng-diagram/src/lib/types/panel-position.ts
@@ -0,0 +1,8 @@
+/**
+ * Position for diagram overlay panels (minimap, watermark, etc.).
+ *
+ * @public
+ * @since 1.0.0
+ * @category Types
+ */
+export type NgDiagramPanelPosition = 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
diff --git a/packages/ng-diagram/projects/ng-diagram/src/public-api.ts b/packages/ng-diagram/projects/ng-diagram/src/public-api.ts
index 6fc0bfc9e..55ada9db0 100644
--- a/packages/ng-diagram/projects/ng-diagram/src/public-api.ts
+++ b/packages/ng-diagram/projects/ng-diagram/src/public-api.ts
@@ -11,6 +11,7 @@ export {
} from './lib/components/edge-label/base-edge-label/base-edge-label.component';
export { NgDiagramBaseEdgeComponent } from './lib/components/edge/base-edge/base-edge.component';
export { NgDiagramMarkerComponent } from './lib/components/marker/ng-diagram-marker.component';
+export { NgDiagramMinimapComponent } from './lib/components/minimap/ng-diagram-minimap.component';
export { NgDiagramBaseNodeTemplateComponent } from './lib/components/node/base-node-template/ng-diagram-base-node-template.component';
export { NgDiagramNodeResizeAdornmentComponent } from './lib/components/node/resize/ng-diagram-node-resize-adornment.component';
export { NgDiagramNodeRotateAdornmentComponent } from './lib/components/node/rotate/ng-diagram-node-rotate-adornment.component';
@@ -41,6 +42,7 @@ export { ViewportDirective } from './lib/directives/viewport/viewport.directive'
export { ZIndexDirective } from './lib/directives/z-index/z-index.directive';
// Public directives
+export { NgDiagramMinimapNavigationDirective } from './lib/components/minimap/ng-diagram-minimap-navigation.directive';
export { NgDiagramGroupHighlightedDirective } from './lib/directives/group-highlighted/ng-diagram-group-highlighted.directive';
export { NgDiagramNodeSelectedDirective } from './lib/directives/node-selected/ng-diagram-node-selected.directive';
@@ -55,6 +57,7 @@ export { NgDiagramService } from './lib/public-services/ng-diagram.service';
// Configuration helpers
export { configureShortcuts } from './core/src';
+export { NgDiagramMinimapNodeTemplateMap } from './lib/components/minimap/ng-diagram-minimap.types';
export { initializeModel } from './lib/model/initialize-model';
export { provideNgDiagram } from './lib/providers/ng-diagram.providers';
export { NgDiagramEdgeTemplateMap } from './lib/types/edge-template-map';
@@ -62,11 +65,18 @@ export { NgDiagramNodeTemplateMap } from './lib/types/node-template-map';
export { createMiddlewares } from './lib/utils/create-middlewares';
// Types
+export type {
+ MinimapNodeShape,
+ MinimapNodeStyle,
+ MinimapNodeStyleFn,
+ NgDiagramMinimapNodeTemplate,
+} from './lib/components/minimap/ng-diagram-minimap.types';
export type { NgDiagramConfig } from './lib/types/config';
export type { NgDiagramEdgeTemplate } from './lib/types/edge-template-map';
export type { PointerInputEvent } from './lib/types/event';
export type { NgDiagramGroupNodeTemplate, NgDiagramNodeTemplate } from './lib/types/node-template-map';
export type { BasePaletteItemData, GroupNodeData, NgDiagramPaletteItem, SimpleNodeData } from './lib/types/palette';
+export type { NgDiagramPanelPosition } from './lib/types/panel-position';
export type { AppMiddlewares } from './lib/utils/create-middlewares';
// Core types re-export