Skip to content

Commit eb2a500

Browse files
committed
feat(visualizer): add export to png
1 parent d1d04b1 commit eb2a500

File tree

3 files changed

+68
-3
lines changed

3 files changed

+68
-3
lines changed

packages/json-table-schema-visualizer/src/components/DiagramViewer/DiagramWrapper.tsx

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,45 @@ const DiagramWrapper = ({ children }: DiagramWrapperProps) => {
208208
};
209209
}, []);
210210

211+
const onDownload = () => {
212+
if (stageRef.current == null) return;
213+
const stage = stageRef.current;
214+
215+
// Save current stage state
216+
const originalScale = stage.scaleX();
217+
const originalPosition = { ...stage.position() };
218+
219+
// Reset stage to scale 1 and position 0,0 to get actual content bounds
220+
stage.scale({ x: 1, y: 1 });
221+
stage.position({ x: 0, y: 0 });
222+
223+
const contentBounds = stage.getClientRect({ relativeTo: stage });
224+
225+
// Calculate square dimensions (use the larger dimension)
226+
const maxDimension = Math.max(contentBounds.width, contentBounds.height);
227+
228+
// Center the content in the square
229+
const offsetX = (maxDimension - contentBounds.width) / 2;
230+
const offsetY = (maxDimension - contentBounds.height) / 2;
231+
232+
const data = stage.toDataURL({
233+
x: contentBounds.x - offsetX,
234+
y: contentBounds.y - offsetY,
235+
width: maxDimension,
236+
height: maxDimension,
237+
pixelRatio: 2,
238+
});
239+
240+
// Restore original stage state
241+
stage.scale({ x: originalScale, y: originalScale });
242+
stage.position(originalPosition);
243+
244+
const link = document.createElement("a");
245+
link.href = data;
246+
link.download = `diagram-${Date.now()}.png`;
247+
link.click();
248+
};
249+
211250
return (
212251
<>
213252
<Stage
@@ -229,7 +268,7 @@ const DiagramWrapper = ({ children }: DiagramWrapperProps) => {
229268
</Layer>
230269
</Stage>
231270

232-
<Toolbar onFitToView={fitToView} />
271+
<Toolbar onFitToView={fitToView} onDownload={onDownload} />
233272
</>
234273
);
235274
};
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { DownloadIcon } from "lucide-react";
2+
3+
import ToolbarButton from "../Button";
4+
5+
interface ExportButtonProps {
6+
onDownload: () => void;
7+
}
8+
9+
const ExportButton = ({ onDownload }: ExportButtonProps) => {
10+
return (
11+
<ToolbarButton onClick={onDownload} title="Export">
12+
<DownloadIcon />
13+
</ToolbarButton>
14+
);
15+
};
16+
17+
export default ExportButton;

packages/json-table-schema-visualizer/src/components/Toolbar/Toolbar.tsx

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,23 @@ import AutoArrangeTableButton from "./AutoArrage/AutoArrangeTables";
44
import ThemeToggler from "./ThemeToggler/ThemeToggler";
55
import DetailLevelToggle from "./DetailLevelToggle/DetailLevelToggle";
66
import FitToViewButton from "./FitToView/FitToView";
7+
import ExportButton from "./Export/Export";
78

8-
const Toolbar = ({ onFitToView }: { onFitToView: () => void }) => {
9+
const Toolbar = ({
10+
onFitToView,
11+
onDownload,
12+
}: {
13+
onFitToView: () => void;
14+
onDownload: () => void;
15+
}) => {
916
return (
1017
<div className="flex absolute [&_svg]:w-5 [&_svg]:h-5 px-6 py-1 bottom-14 text-sm bg-gray-100 dark:bg-gray-700 shadow-lg rounded-2xl">
1118
<AutoArrangeTableButton />
1219
<DetailLevelToggle />
1320
<FitToViewButton onClick={onFitToView} />
14-
<hr className="w-px h-6 mx-4 my-1 bg-gray-300" />
21+
<hr className="mx-4 my-1 w-px h-6 bg-gray-300" />
22+
<ExportButton onDownload={onDownload} />
23+
<hr className="mx-4 my-1 w-px h-6 bg-gray-300" />
1524
<ThemeToggler />
1625
</div>
1726
);

0 commit comments

Comments
 (0)