GridGlyph - A Small Helper Tool

Go with the flow
Go with the flow

For part three of my Data Governance series, I created a diagram using draw.io to show how the different building blocks of Data Governance interact with each other. I started mapping all the blocks and connecting them to show what data is exchanged where. And then I thought about how it might be useful to annotate the connections. So I gave them an ID and started adding a table listing the data exchanged for each ID. And then the whole diagram started looking really messy. So I figured, that having a flat image as diagram is really old school and that having an interactive diagram would be much cooler.

I did some researching and did not find the solution I was looking for, so I decided to build my own. It is very limited (a one trick pony you could say) and can only display one type of diagram: a grid of (nested) blocks that can be connected to each other. To do so, it reads a configuration file describing the grid and the connections and then renders them into an interactive diagram where you can click the blocks and the connectors to get more info on what they do. So without further ado, here is a description of what I built. If you want to dive directly into the code it is published unter MIT License and, there is a link to the GitHub-Repo at the end of this article.

The Data

The diagram to be rendered is described in a .json-file. It contains all information to render the diagram, such as definition of rows and columns, blocks and hierarchies of blocks (including their placement) and connections between the blocks. Each element has a cssClass attribute so their look and feel can be adjusted in the CSS file.

rows

Horizontal swimlanes, rendered top to bottom in array order.

{
    "id": "row-1",
    "label": "Structure & Design",
    "description": "Shown in the detail overlay when the header is clicked.",
    "cssClass": "optional-extra-class"
}

columns

Vertical slices, rendered left to right in array order.

{
    "id": "col-2",
    "label": "Modeling",
    "cssClass": "optional-extra-class"
}

blocks

Blocks come in two kinds: leaf blocks that sit in a specific grid cell, and parent blocks that visually span the cells their children occupy. Leaf block — must have row, column, and optionally parentId:

{
    "id": "b-datamodelingdesign",
    "label": "Data Modeling & Design",
    "description": "Shown when the block is clicked.",
    "row": "row-1",
    "column": "col-2",
    "parentId": "b-datawarehouse",
    "cssClass": "leaf"
}

Parent block — omit row and column; the renderer calculates the bounding box from the children:

{
    "id": "b-datawarehouse",
    "label": "Data Warehouse & BI",
    "description": "Optional detail text.",
    "parentId": null,
    "cssClass": "parent-block"
}

Multiple parent blocks whose children share the same column range are placed in separate sub-rows to prevent visual overlap.

connections

Directed edges between blocks.

{
    "id": "conn-1",
    "label": "",
    "description": "Shown when the arrow is clicked.",
    "sourceBlockId": "b-dataarchitecture",
    "targetBlockId": "b-datamodelingdesign",
    "cssClass": "data-flow"
}

The flow for rendering the diagram is

  1. Clear the container
  2. Create the grid
  3. Append the elements
  4. Render the connections
  5. Setup the overlay.

Rendering the Diagram

When creating the grid, it is important to note, that if blocks occupy the same cell, they are stacked vertically, so a “row” can actually contain multiple sub-rows. So, for each row, we can calculate the height be counting the number of blocks in a cell of said row (see getRowHeight(row)). Then the blocks are assigned to the rows and the sub-rows subsequently.

Next, to populate the grid, for each block, its span is calculated and for each cell the blocks in the cell are calculated. This includes spanning blocks (parents with cells in multiple columns) and leaf blocks (blocks that go directly into the cell) (see renderRow(grid, row).

Finally, the connections between the blocks are rendered. For connections between rows, a routing logic is applied:

Source and target cell are on same row (routing: 'same'): cubic Bézier between the left/right edges of the blocks. Forward and return connections between the same pair are offset ±15 px vertically so they don’t overlap.

Source cell is in row above target cell (connection going down) (routing: 'right'): elbow path that exits the source’s right edge, travels through a right-hand corridor (inside the container’s right padding), turns downward, then enters the target’s right edge. Multiple downward connections are spread across evenly spaced vertical lanes in the corridor.

Source cell is in row below target cell (connectino going up) (routing: 'left'): 10-segment path with rounded corners that exits the source’s top, dips into the row gap above the source row, travels horizontally to a lane in the left-hand corridor (inside the container’s left padding), runs vertically up or down to the row gap above the target row, then enters the target from above.

All connections in the diagram are interactive: hovering turns them orange with a drop shadow; clicking opens the detail overlay with the connection’s description.

Overlays and interactivity

The overlay surfaces contextual details for headers, blocks and connections. During render, setupOverlay() finds #overlay and its controls, attaches named handlers for the close button, backdrop clicks (only when clicking the overlay itself) and Escape, and should be guarded so missing elements don’t throw. showOverlay(title, description) fills title/description, toggles title visibility, reveals the dialog, moves focus into it, and sets ARIA attributes (role="dialog", aria-labelledby/aria-describedby, mark background aria-hidden="true"). hideOverlay() hides the dialog, restores ARIA state and returns focus to the triggering element.

Make interactive affordances clear: headers and blocks with descriptions get cursor: pointer and click handlers (use e.stopPropagation() on block clicks when needed). For connections, add a wider invisible SVG hit-path so thin lines are easy to tap; both hit-path and labels open the overlay. Increase hit targets for touch. Manage lifecycle: store and remove listeners on destroy (or before reinit), prefer delegation to reduce handlers, trap focus while open, and respect prefers-reduced-motion for animations.

That’s all. For more details, check the source code and feel free to use and/or expand it as needed for your purpose.

See it in action

View the code on GitHub

Photo: