From f96a621636ea70ef561479fd981658b8e0f917e5 Mon Sep 17 00:00:00 2001 From: StevenBtw Date: Sun, 15 Mar 2026 21:25:57 +0100 Subject: [PATCH 1/4] bug fix demo --- src/anywidget_archimate/parser.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/anywidget_archimate/parser.py b/src/anywidget_archimate/parser.py index 38ef229..8b59250 100644 --- a/src/anywidget_archimate/parser.py +++ b/src/anywidget_archimate/parser.py @@ -63,12 +63,16 @@ def parse_xml(source: str | Path) -> tuple[list[dict], list[dict]]: Returns: Tuple of (elements, relationships) as lists of dicts. """ - source_path = Path(source) if not isinstance(source, Path) else source - if source_path.exists(): - tree = ET.parse(source_path) - root = tree.getroot() + source_str = str(source) + if not source_str.lstrip().startswith("<"): + source_path = Path(source) if not isinstance(source, Path) else source + if source_path.exists(): + tree = ET.parse(source_path) + root = tree.getroot() + else: + root = ET.fromstring(source_str) else: - root = ET.fromstring(str(source)) + root = ET.fromstring(source_str) ns = _detect_namespace(root) From 670ac0c6821117c0be8f91b2bf8c98fd0a24f8e7 Mon Sep 17 00:00:00 2001 From: StevenBtw Date: Sun, 15 Mar 2026 21:31:15 +0100 Subject: [PATCH 2/4] upload import functionality added --- src/anywidget_archimate/ui/index.js | 76 +++++++++++++++++++++++++++++ src/anywidget_archimate/widget.py | 12 +++++ 2 files changed, 88 insertions(+) diff --git a/src/anywidget_archimate/ui/index.js b/src/anywidget_archimate/ui/index.js index f2de3dd..e3bbc0f 100644 --- a/src/anywidget_archimate/ui/index.js +++ b/src/anywidget_archimate/ui/index.js @@ -1021,6 +1021,15 @@ function svgPoint(svg, event) { // Utilities // ============================================================================ +function downloadBlob(blob, filename) { + const a = document.createElement("a"); + a.href = URL.createObjectURL(blob); + a.download = filename; + document.body.appendChild(a); + a.click(); + setTimeout(() => { URL.revokeObjectURL(a.href); a.remove(); }, 100); +} + function truncate(s, n) { return !s ? "" : s.length > n ? s.slice(0, n - 1) + "\u2026" : s; } function formatType(type) { @@ -1055,6 +1064,10 @@ function render({ model, el }) { Application Technology + + + + `; @@ -1111,6 +1124,69 @@ function render({ model, el }) { }); toolbar.querySelector(".aam-btn-dark").addEventListener("click", () => { model.set("dark_mode", !model.get("dark_mode")); model.save_changes(); }); + // Upload + const fileInput = toolbar.querySelector(".aam-file-input"); + toolbar.querySelector(".aam-btn-upload").addEventListener("click", () => { fileInput.value = ""; fileInput.click(); }); + fileInput.addEventListener("change", () => { + const file = fileInput.files[0]; + if (!file) return; + const reader = new FileReader(); + reader.onload = () => { model.set("_upload_xml", reader.result); model.save_changes(); }; + reader.readAsText(file); + }); + + // Export SVG + toolbar.querySelector(".aam-btn-export-svg").addEventListener("click", () => { + const svg = graphContainer.querySelector("svg"); + if (!svg) return; + const clone = svg.cloneNode(true); + // Inline key styles so the SVG is self-contained + const style = document.createElementNS("http://www.w3.org/2000/svg", "style"); + style.textContent = ` + .aam-node-label { font: 600 11px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; } + .aam-node-type { font: 8px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; } + .aam-layer-label { font: 700 12px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; text-transform: uppercase; letter-spacing: 0.5px; opacity: 0.7; } + `; + clone.insertBefore(style, clone.firstChild); + // Set explicit dimensions from viewBox + const vb = clone.getAttribute("viewBox"); + if (vb) { const [,,w,h] = vb.split(" "); clone.setAttribute("width", w); clone.setAttribute("height", h); } + const blob = new Blob([new XMLSerializer().serializeToString(clone)], { type: "image/svg+xml" }); + downloadBlob(blob, "archimate-diagram.svg"); + }); + + // Export PNG + toolbar.querySelector(".aam-btn-export-png").addEventListener("click", () => { + const svg = graphContainer.querySelector("svg"); + if (!svg) return; + const clone = svg.cloneNode(true); + const style = document.createElementNS("http://www.w3.org/2000/svg", "style"); + style.textContent = ` + .aam-node-label { font: 600 11px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; } + .aam-node-type { font: 8px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; } + .aam-layer-label { font: 700 12px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; text-transform: uppercase; letter-spacing: 0.5px; opacity: 0.7; } + `; + clone.insertBefore(style, clone.firstChild); + const vb = clone.getAttribute("viewBox"); + let w = 1200, h = 800; + if (vb) { const parts = vb.split(" "); w = Math.ceil(parts[2] * 2); h = Math.ceil(parts[3] * 2); } + clone.setAttribute("width", w); clone.setAttribute("height", h); + const blob = new Blob([new XMLSerializer().serializeToString(clone)], { type: "image/svg+xml" }); + const url = URL.createObjectURL(blob); + const img = new Image(); + img.onload = () => { + const canvas = document.createElement("canvas"); + canvas.width = w; canvas.height = h; + const ctx = canvas.getContext("2d"); + ctx.fillStyle = model.get("dark_mode") ? "#1e1e3a" : "#fdfdfd"; + ctx.fillRect(0, 0, w, h); + ctx.drawImage(img, 0, 0, w, h); + URL.revokeObjectURL(url); + canvas.toBlob((b) => { if (b) downloadBlob(b, "archimate-diagram.png"); }, "image/png"); + }; + img.src = url; + }); + model.on("change:elements", rebuildDiagram); model.on("change:relationships", rebuildDiagram); model.on("change:dark_mode", rebuildDiagram); diff --git a/src/anywidget_archimate/widget.py b/src/anywidget_archimate/widget.py index 5a2305b..840a803 100644 --- a/src/anywidget_archimate/widget.py +++ b/src/anywidget_archimate/widget.py @@ -36,6 +36,18 @@ class ArchiMate(anywidget.AnyWidget): # Interaction state selected_element = traitlets.Dict(allow_none=True, default_value=None).tag(sync=True) + # File upload (browser → Python) + _upload_xml = traitlets.Unicode(default_value="").tag(sync=True) + + @traitlets.observe("_upload_xml") + def _on_upload_xml(self, change): + xml = change["new"] + if xml: + elements, relationships = parse_xml(xml) + self.elements = elements + self.relationships = relationships + self._upload_xml = "" # reset + @classmethod def from_xml(cls, source: str | Path, **kwargs) -> ArchiMate: """Create widget from ArchiMate XML file or string. From 7481f224b7e93a6a776a065c45bfe72339bdf95d Mon Sep 17 00:00:00 2001 From: StevenBtw Date: Sun, 15 Mar 2026 21:32:01 +0100 Subject: [PATCH 3/4] updated CHANGELOG.md --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0404c45..62adf63 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## [0.1.3] - (2026-03-15) + +### New Features + +- **Upload button**: Toolbar button to load `.xml` or `.archimate` files directly from the browser — no Python code needed to switch models +- **Export to SVG**: Download the current diagram as a self-contained SVG file with inlined fonts +- **Export to PNG**: Download the current diagram as a high-resolution (2x) PNG with correct light/dark background + ## [0.1.2] - (2026-03-15) ### New Features From 2c49042b0d1f40aa005d447ae37b183b5b673fcd Mon Sep 17 00:00:00 2001 From: StevenBtw Date: Sun, 15 Mar 2026 21:35:14 +0100 Subject: [PATCH 4/4] bumperino --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 8a340c5..45f78fe 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "anywidget-archimate" -version = "0.1.1" +version = "0.1.3" description = "Interactive ArchiMate model viewer widget for Jupyter, Marimo, and VS Code notebooks" readme = "README.md" license = "Apache-2.0"