Shahathir (•◡•)

24 · Batu Caves, Selangor, Malaysia · 🇲🇾

professionally distracted

My Career Journey

  1. Financial Risk Group logo

    Financial Risk Group

    1yrs 3mos

    Assistant Software Developer

    Jun 2025 – Present

  2. Estee Lauder Companies logo

    Estee Lauder Companies

    6mos

    Software Engineer Intern

    Sep 2024 – Mar 2025

Tools & Platforms

TypeScript
JavaScript
Java
Python
PHP
Go
HTML5
CSS3
React
Next.js
Vite
Angular
Redux
React Router
Tailwind CSS
shadcn/ui
Material UI
Sass
Bootstrap
React Native
Node.js
TypeScript
JavaScript
Java
Python
PHP
Go
HTML5
CSS3
React
Next.js
Vite
Angular
Redux
React Router
Tailwind CSS
shadcn/ui
Material UI
Sass
Bootstrap
React Native
Node.js
Bun
Django
FastAPI
Spring Boot
Java Servlets
Express
PostgreSQL
MySQL
MS SQL Server
SQLite
Appwrite
Docker
AWS
Cloudflare
Nginx
Vercel
Netlify
DigitalOcean
Git
DBeaver
Postman
Bun
Django
FastAPI
Spring Boot
Java Servlets
Express
PostgreSQL
MySQL
MS SQL Server
SQLite
Appwrite
Docker
AWS
Cloudflare
Nginx
Vercel
Netlify
DigitalOcean
Git
DBeaver
Postman

Words I Live By

Shahathir is currently not listening to anything
Shahathir is currently not listening to anything

2026 © shahathir.me

Changelogs · Old site

  • AboutAbout
  • ThoughtsThoughts
  • TILTIL
  • BookmarksBookmarks
  • ExperienceExperience
  • ProjectsProjects
  • AccoladesAccolades
  • PhotographyPhotography
  • SongsSongs
  • StatsStats
  • UsesUses
  • ChatChat
  • Resume BuilderResume Builder
  • AboutAbout
  • ThoughtsThoughts
  • TILTIL
  • BookmarksBookmarks
  • ExperienceExperience
  • ProjectsProjects
  • AccoladesAccolades
  • PhotographyPhotography
  • SongsSongs
  • StatsStats
  • UsesUses
  • ChatChat
  • Resume BuilderResume Builder
March 3, 2026

A hidden graph node needs its edges gone too (and stop regexing SVG)

Hiding nodes in a generated workflow diagram with regex left arrows pointing into empty space and dead click-targets. Parsing the SVG instead — and treating a node and its edges as one unit — fixed both.

We render a workflow as a Graphviz-generated SVG — nodes, edges, and cluster boxes — shown as a background-image data URL. Some nodes are conditionally skipped at runtime and have to be hidden from the diagram.

The original code hid them by running regex string-replacements over the raw SVG markup. It worked until the generated structure shifted slightly, and then the regexes drifted in two nasty ways: a node's box got removed but its incoming edge didn't — so arrows pointed into empty space — and a leftover clickable overlay sat on top of nothing, firing a backend request that errored when someone clicked it.

Both bugs have the same root cause: treating structured markup as a flat string. The fix is to parse it into a real document, mutate the tree, and serialize back:

const doc = new DOMParser().parseFromString(svgMarkup, "image/svg+xml");
 
function hideNode(doc: Document, nodeId: string) {
  // A Graphviz node is <g class="node"><title>{id}</title>…</g>
  doc.querySelectorAll("g.node").forEach((g) => {
    if (g.querySelector("title")?.textContent?.trim() === nodeId) g.remove();
  });
  doc.querySelector(`g#cluster_${nodeId}.cluster`)?.remove();
 
  // CRUCIAL: also remove every edge whose destination is this node,
  // or its arrow is left dangling into empty space.
  doc.querySelectorAll("g.edge").forEach((edge) => {
    const raw = edge.querySelector("title")?.textContent?.trim() ?? "";
    const [, dst] = raw.replace(/&#45;&gt;/g, "->").split("->").map((s) => s.trim());
    if (dst === nodeId) edge.remove();
  });
}
 
const svg = new XMLSerializer().serializeToString(doc);

The line that actually fixed the "arrows into nowhere" bug is the edge loop. A node and its incident edges are one unit — remove the node without removing the edges pointing at it and you get a diagram that's subtly, visibly broken. The same principle killed the dead click-target: there's a parallel data array driving the clickable overlays, and hiding a node has to remove its entry too, or a blank click-target outlives the thing it belonged to.

You can toggle exactly that below. Hide the node and its incident edges are left dangling into empty space; flip on the cascade and they go with it:

3 orphaned edges
Press enter or space to select a node. You can then use the arrow keys to move the node around. Press delete to remove it and escape to cancel.
Press enter or space to select an edge. You can then press delete to remove it or escape to cancel.

Hide the node without cascading and its incident edges (red) dangle into empty space. A node and its edges are one unit. (The real fix parsed a Graphviz SVG; this shows the principle.)

A couple of details that make this kind of SVG surgery work:

  • Graphviz encodes the edge arrow in the <title> as HTML entities (A&#45;&gt;B). If you split("->") without normalizing first, every edge match silently fails and you're back to dangling arrows — but now confusingly, because the code looks right.
  • Order of operations matters. Do the DOM surgery on the parsed document first; do any color-swaps and whitespace cleanup on the string; do URL-encoding for the data URL last. The old regex version smeared all three together, which is a big part of why it was so fragile.

The transferable lesson, and one I'll carry into any language: regex over HTML/SVG/XML is a smell. A parser with querySelectorAll keyed on the semantics (g.node, g.edge, <title>) survives structural churn that string rewrites can't. And any hide/filter over a graph has to cascade — to the edges, and to whatever overlay or interaction model you've layered on top — because in a graph, removing one thing quietly invalidates its relationships.