import { toPng, toSvg } from 'html-to-image';
import { getNodesBounds } from 'reactflow';
import type { Node } from 'reactflow';
/**
* Graph Export Utilities
*
* Based on React Flow's official download-image example:
* https://reactflow.dev/examples/misc/download-image
*
* Uses html-to-image@1.11.11 (newer versions have export issues)
*/
export interface ExportOptions {
/** Background color for the exported image */
backgroundColor?: string;
/** Padding around the graph content (in pixels) */
padding?: number;
/** Image quality/scale multiplier (1-4) */
quality?: number;
/** File name (without extension) */
fileName?: string;
}
const DEFAULT_OPTIONS: Required> = {
backgroundColor: '#ffffff',
padding: 10,
quality: 4, // ~300 DPI for standard screen-to-print conversion
};
/**
* Downloads a file from a data URL
*/
function downloadImage(dataUrl: string, fileName: string, extension: string) {
const a = document.createElement('a');
a.setAttribute('download', `${fileName}.${extension}`);
a.setAttribute('href', dataUrl);
a.click();
}
/**
* Calculate the viewport bounds for capturing the entire graph
*
* This creates a tight crop around all nodes with minimal padding.
*
* @param nodes - Array of nodes in the graph
* @param padding - Padding around the content
* @returns Dimensions and transform for the export
*/
function calculateImageBounds(nodes: Node[], padding: number) {
// Get the bounding box that contains all nodes
const nodesBounds = getNodesBounds(nodes);
console.log('Node bounds:', nodesBounds);
console.log('Number of nodes:', nodes.length);
console.log('Nodes:', nodes.map(n => ({ id: n.id, position: n.position, width: n.width, height: n.height })));
// Calculate image dimensions with padding
const imageWidth = nodesBounds.width + padding * 2;
const imageHeight = nodesBounds.height + padding * 2;
// Calculate transform to position the content
// We want to translate the viewport so that the top-left of nodesBounds
// is at position (padding, padding) in the export
const transform = {
x: -nodesBounds.x + padding,
y: -nodesBounds.y + padding,
zoom: 1,
};
console.log('Calculated dimensions:', { width: imageWidth, height: imageHeight });
console.log('Transform:', transform);
return {
width: imageWidth,
height: imageHeight,
transform,
};
}
/**
* Export React Flow graph as PNG
*
* @param viewportElement - The .react-flow__viewport DOM element
* @param nodes - Array of nodes in the graph
* @param options - Export options
*/
export async function exportGraphAsPNG(
viewportElement: HTMLElement,
nodes: Node[],
options: ExportOptions = {}
): Promise {
const {
backgroundColor,
padding,
quality,
fileName = 'constellation-graph',
} = { ...DEFAULT_OPTIONS, ...options };
if (nodes.length === 0) {
throw new Error('Cannot export empty graph');
}
const { width, height, transform } = calculateImageBounds(nodes, padding);
try {
const dataUrl = await toPng(viewportElement, {
backgroundColor,
width: width,
height: height,
pixelRatio: quality,
cacheBust: true,
style: {
width: `${width}px`,
height: `${height}px`,
transform: `translate(${transform.x}px, ${transform.y}px) scale(${transform.zoom})`,
},
});
downloadImage(dataUrl, fileName, 'png');
} catch (error) {
console.error('PNG export failed:', error);
throw new Error('Failed to export graph as PNG');
}
}
/**
* Export React Flow graph as SVG
*
* @param viewportElement - The .react-flow__viewport DOM element
* @param nodes - Array of nodes in the graph
* @param options - Export options
*/
export async function exportGraphAsSVG(
viewportElement: HTMLElement,
nodes: Node[],
options: ExportOptions = {}
): Promise {
const {
backgroundColor,
padding,
fileName = 'constellation-graph',
} = { ...DEFAULT_OPTIONS, ...options };
if (nodes.length === 0) {
throw new Error('Cannot export empty graph');
}
const { width, height, transform } = calculateImageBounds(nodes, padding);
try {
const dataUrl = await toSvg(viewportElement, {
backgroundColor,
width,
height,
style: {
width: `${width}px`,
height: `${height}px`,
transform: `translate(${transform.x}px, ${transform.y}px) scale(${transform.zoom})`,
},
});
downloadImage(dataUrl, fileName, 'svg');
} catch (error) {
console.error('SVG export failed:', error);
throw new Error('Failed to export graph as SVG');
}
}