Download Image
This example demonstrates how to export the current flow as an image using Angular features and the html-to-image
library.
Download Image Overview
Section titled “Download Image Overview”The download functionality allows users to export the flow as a image, capturing the current state and layout.
Key Features
Section titled “Key Features”- Export: Downloads the flow as a PNG image.
- Bounding Box Calculation: Ensures the exported image includes all nodes with proper margins.
Implementation Details
Section titled “Implementation Details”Generate Image Service
Section titled “Generate Image Service”The GenerateImageService
handles the logic for exporting the flow as an image. It uses the html-to-image
library and calculates the bounding box to ensure all nodes are included:
7 collapsed lines
import { Injectable } from '@angular/core';import { toPng } from 'html-to-image';import type { Options } from 'html-to-image/lib/types';import { type Node } from 'ng-diagram';import { calculateBoundingBox } from './generate-image.helper';
@Injectable()export class GenerateImageService { private IMAGE_MARGIN: number = 50;
async generateImageFile(nodes: Node[], element: HTMLElement) { const size = calculateBoundingBox(nodes, this.IMAGE_MARGIN); const backgroundColor = getComputedStyle(element).backgroundColor || '#fff';
const options: Options = { backgroundColor, width: size.width, height: size.height, cacheBust: false, skipFonts: false, pixelRatio: 2, fetchRequestInit: { mode: 'cors' as RequestMode }, style: { transform: `translate(${Math.abs(size.left)}px, ${Math.abs(size.top)}px) scale(1)`, }, };
const diagramCanvasElement = element.getElementsByTagName( 'ng-diagram-canvas' )[0] as HTMLElement;
return await toPng(diagramCanvasElement, options); }}
Helper Functions
Section titled “Helper Functions”Helper functions manage the download process and calculate the bounding box for the flow:
29 collapsed lines
import { type Node } from 'ng-diagram';
export const downloadImage = (data: string | Blob, fileName: string) => { try { const anchor = document.createElement('a'); let url: string | null = null;
if (typeof data === 'string') { anchor.href = data; } else if (data instanceof Blob) { url = URL.createObjectURL(data); anchor.href = url; } else { throw new Error('Unsupported data type for download'); }
anchor.download = fileName; document.body.appendChild(anchor); anchor.click(); document.body.removeChild(anchor);
if (url) { URL.revokeObjectURL(url); } } catch (e) { console.error(e); }};
export const calculateBoundingBox = (nodes: Node[], margin: number) => { if (nodes.length === 0) { return { width: 0, height: 0, top: 0, left: 0, }; } let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
for (const node of nodes) { const nodeWidth = node.size?.width || 0; const nodeHeight = node.size?.height || 0;
minX = Math.min(minX, node.position.x); minY = Math.min(minY, node.position.y); maxX = Math.max(maxX, node.position.x + nodeWidth); maxY = Math.max(maxY, node.position.y + nodeHeight); }
return { width: maxX - minX + margin * 2, height: maxY - minY + margin * 2, left: minX - margin, top: minY - margin, };};
NavBar Component
Section titled “NavBar Component”The navigation bar provides a button to trigger the download action.
To accurately calculate the bounding box for the entire flow, all nodes are pulled and passed to the calculation. Also it passes the flow element reference to the service.
5 collapsed lines
import { Component, ElementRef, inject, input } from '@angular/core';import { NgDiagramModelService } from 'ng-diagram';import { downloadImage } from '../generate-image.helper';import { GenerateImageService } from '../generate-image.service';
@Component({ selector: 'nav-bar', template: ` <div class="nav-bar"> <button (click)="download()">Download</button> </div> `, styleUrls: ['./nav-bar-component.scss'],})export class NavBarComponent { private readonly generateImageService = inject(GenerateImageService); private readonly modelService = inject(NgDiagramModelService);
elementRef = input<ElementRef>();
async download(): Promise<void> { if (this.elementRef()) { const file = await this.generateImageService.generateImageFile( this.modelService.getModel().getNodes(), this.elementRef()?.nativeElement );
downloadImage(file, `flow-diagram-${Date.now()}.png`); } }}
Actions
Section titled “Actions”- Download: Exports the current flow as a PNG image using the bounding box and margin settings.
import '@angular/compiler';import { Component } from '@angular/core';import { provideNgDiagram } from 'ng-diagram';import { DownloadImageComponent } from './download-image.component';import { GenerateImageService } from './generate-image.service';
@Component({ selector: 'download-image-wrapper', imports: [DownloadImageComponent], template: ` <download-image></download-image> `, styles: [ ` :host { flex: 1; display: flex; position: relative; flex-direction: column; } `, ], providers: [GenerateImageService, provideNgDiagram()],})export class DownloadImageWrapperComponent {}
import '@angular/compiler';import { Component, ElementRef, viewChild, type Signal } from '@angular/core';import { initializeModel, NgDiagramComponent, NgDiagramModelService, type NgDiagramConfig,} from 'ng-diagram';import { NavBarComponent } from './nav-bar/nav-bar.component';
@Component({ selector: 'download-image', imports: [NgDiagramComponent, NavBarComponent], template: ` <div> <nav-bar [elementRef]="ngDiagram.elementRef"></nav-bar> <div class="not-content diagram"> <ng-diagram #ngDiagram [model]="model" [config]="config" /> </div> </div> `, styleUrl: './download-image.component.scss', providers: [NgDiagramModelService],})export class DownloadImageComponent { ngDiagram: Signal<ElementRef<HTMLElement> | undefined> = viewChild('ngDiagram');
config = { zoom: { max: 3, }, } satisfies NgDiagramConfig;
model = initializeModel({ nodes: [ { id: 'MAIN ROOT', position: { x: 500, y: 50 }, data: {}, size: { width: 140, height: 50 }, }, { id: 'child1', position: { x: 250, y: 100 }, data: {}, size: { width: 120, height: 44 }, }, { id: 'child2', position: { x: 250, y: 0 }, data: {}, size: { width: 120, height: 44 }, }, { id: 'child3', position: { x: 750, y: 0 }, data: {}, size: { width: 120, height: 44 }, }, { id: 'leaf1', position: { x: 0, y: 150 }, data: {}, size: { width: 100, height: 38 }, }, { id: 'leaf2', position: { x: 0, y: 50 }, data: {}, size: { width: 100, height: 38 }, }, { id: 'leaf3', position: { x: 1000, y: 50 }, data: {}, size: { width: 100, height: 38 }, }, { id: 'leaf4', position: { x: 1000, y: -50 }, data: {}, size: { width: 100, height: 38 }, }, { id: 'leaf5', position: { x: 1000, y: -150 }, data: {}, size: { width: 100, height: 38 }, }, { id: 'leaf6', position: { x: 1000, y: 150 }, data: {}, size: { width: 100, height: 38 }, }, ], edges: [ { id: 'e1', data: {}, source: 'MAIN ROOT', target: 'child1', sourcePort: 'port-left', targetPort: 'port-right', }, { id: 'e2', data: {}, source: 'MAIN ROOT', target: 'child2', sourcePort: 'port-left', targetPort: 'port-right', }, { id: 'e3', data: {}, source: 'MAIN ROOT', target: 'child3', sourcePort: 'port-right', targetPort: 'port-left', }, { id: 'e4', data: {}, source: 'child1', target: 'leaf1', sourcePort: 'port-left', targetPort: 'port-right', }, { id: 'e5', data: {}, source: 'child1', target: 'leaf2', sourcePort: 'port-left', targetPort: 'port-right', }, { id: 'e6', data: {}, source: 'child3', target: 'leaf3', sourcePort: 'port-right', targetPort: 'port-left', }, { id: 'e7', data: {}, source: 'child3', target: 'leaf4', sourcePort: 'port-right', targetPort: 'port-left', }, { id: 'e8', data: {}, source: 'child3', target: 'leaf5', sourcePort: 'port-right', targetPort: 'port-left', }, { id: 'e9', data: {}, source: 'child3', target: 'leaf6', sourcePort: 'port-right', targetPort: 'port-left', }, ], metadata: { viewport: { x: -100, y: 100, scale: 0.75 } }, });}
import { type Node } from 'ng-diagram';
export const downloadImage = (data: string | Blob, fileName: string) => { try { const anchor = document.createElement('a'); let url: string | null = null;
if (typeof data === 'string') { anchor.href = data; } else if (data instanceof Blob) { url = URL.createObjectURL(data); anchor.href = url; } else { throw new Error('Unsupported data type for download'); }
anchor.download = fileName; document.body.appendChild(anchor); anchor.click(); document.body.removeChild(anchor);
if (url) { URL.revokeObjectURL(url); } } catch (e) { console.error(e); }};
export const calculateBoundingBox = (nodes: Node[], margin: number) => { if (nodes.length === 0) { return { width: 0, height: 0, top: 0, left: 0, }; } let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
for (const node of nodes) { const nodeWidth = node.size?.width || 0; const nodeHeight = node.size?.height || 0;
minX = Math.min(minX, node.position.x); minY = Math.min(minY, node.position.y); maxX = Math.max(maxX, node.position.x + nodeWidth); maxY = Math.max(maxY, node.position.y + nodeHeight); }
return { width: maxX - minX + margin * 2, height: maxY - minY + margin * 2, left: minX - margin, top: minY - margin, };};
import { Injectable } from '@angular/core';import { toPng } from 'html-to-image';import type { Options } from 'html-to-image/lib/types';import { type Node } from 'ng-diagram';import { calculateBoundingBox } from './generate-image.helper';
@Injectable()export class GenerateImageService { private IMAGE_MARGIN: number = 50;
async generateImageFile(nodes: Node[], element: HTMLElement) { const size = calculateBoundingBox(nodes, this.IMAGE_MARGIN); const backgroundColor = getComputedStyle(element).backgroundColor || '#fff';
const options: Options = { backgroundColor, width: size.width, height: size.height, cacheBust: false, skipFonts: false, pixelRatio: 2, fetchRequestInit: { mode: 'cors' as RequestMode }, style: { transform: `translate(${Math.abs(size.left)}px, ${Math.abs(size.top)}px) scale(1)`, }, };
const diagramCanvasElement = element.getElementsByTagName( 'ng-diagram-canvas' )[0] as HTMLElement;
return await toPng(diagramCanvasElement, options); }}
import { Component, ElementRef, inject, input } from '@angular/core';import { NgDiagramModelService } from 'ng-diagram';import { downloadImage } from '../generate-image.helper';import { GenerateImageService } from '../generate-image.service';
@Component({ selector: 'nav-bar', template: ` <div class="nav-bar"> <button (click)="download()">Download</button> </div> `, styleUrls: ['./nav-bar-component.scss'],})export class NavBarComponent { private readonly generateImageService = inject(GenerateImageService); private readonly modelService = inject(NgDiagramModelService);
elementRef = input<ElementRef>();
async download(): Promise<void> { if (this.elementRef()) { const file = await this.generateImageService.generateImageFile( this.modelService.getModel().getNodes(), this.elementRef()?.nativeElement );
downloadImage(file, `flow-diagram-${Date.now()}.png`); } }}
:host { width: 100%; height: 100%;}
.diagram { flex: 1; display: flex; height: 24rem; font-family: 'Poppins', sans-serif;}
.nav-bar { display: flex; gap: 8px; align-items: flex-end; justify-content: end; padding: 2px 16px; background-color: var(--ngd-node-stroke-primary-defaultx);
& > button { cursor: pointer; border: none;
&:disabled { cursor: not-allowed;
&.clear { background: none; } } }}