Docs & Code Guide

Embed a diagram in a Jupyter notebook: URL images, base64 attachments, and what GitHub shows

By the engineer who builds Datadef, from client work on data platforms · Reviewed August 21, 2026

A notebook is a JSON file in git that renders in at least three places: JupyterLab, the GitHub file view, and whatever HTML your team exports. Each one treats an image differently, and the choice you make in the markdown cell decides how big the file gets, how readable the diff is, and whether the picture is still true six months later. Three storage paths exist and only one of them keeps the notebook light and the diagram current.

7 min readFor data teams whose architecture notes live in notebooks

See it as a diagram

Everything below, as a diagram you can edit. Describe yours and see it in seconds.

142/20003 credits left
Try:

No account needed · Editable canvas, not a picture

Three ways to put a picture in a notebook, and where each one stores it

The first is a markdown cell with an image address. Nothing is stored in the notebook except the URL, and the renderer fetches it every time the cell displays. The picture can change without the notebook changing.

The second is dragging a file into a markdown cell. Jupyter encodes it as base64 and writes it into the notebook JSON as an attachment, referenced through the attachment scheme. The image now travels with the file and never updates again.

The third is code: IPython.display.Image with a url argument emits an img tag pointing at the address, while the same class with a filename argument reads the bytes and stores them in the cell output, which then gets saved into the notebook. Two very similar lines with opposite consequences.

from IPython.display import Image, IFrame

# Points at the address, re-fetched on every render
Image(url="https://datadef.io/api/embed/my-platform-a1b2c3d4")

# Pan and zoom inside JupyterLab
IFrame("https://datadef.io/embed/my-platform-a1b2c3d4", width="100%", height=600)

Why base64 attachments hurt in review

A notebook is JSON under version control, and an inlined PNG lands as one enormous line of base64. Every re-save rewrites it, the diff view gives up, and reviewers scroll past a wall of characters looking for the two lines of code that actually changed. Teams that have adopted nbdime or a stripping pre-commit hook know this pain already.

An address costs about eighty characters, produces a readable diff, and keeps the repository small. For a diagram that is regenerated regularly, it also means the notebook does not need to be touched when the architecture moves.

The trade is real and worth naming: an attachment renders with no network, and an address does not. For a notebook shipped to an air-gapped environment or archived for compliance, the frozen copy is the correct choice.

What GitHub does with a committed notebook

GitHub renders .ipynb files, and external images inside them are routed through the same anonymizing image proxy as a README, so caching behaves the way it does there: the proxy fetches from the origin and honours the origin cache headers. The embed endpoint sends max-age 300, so an edited diagram shows up in the rendered notebook within minutes.

HTML output is sanitized, which is where the two paths diverge. An IFrame call gives you a pannable diagram in JupyterLab and shows nothing in the GitHub file view, because the frame is stripped. If the notebook is meant to be read on GitHub, put the image in a markdown cell and keep the frame for interactive work.

Attachment images are stored in the notebook and render on GitHub, at the cost of the diff problem above. There is no option that is simultaneously light, interactive and offline.

Freezing on purpose for a handout

When a notebook becomes a deliverable, the network dependency stops being a feature. nbconvert can inline every image as a data URI while converting to HTML, with the embed-images flag, so the exported file opens anywhere with no requests going out.

That gives a clean split: the working notebook in the repository carries addresses and stays current, the exported artifact carries bytes and stays fixed. Nobody has to choose one policy for both.

jupyter nbconvert --to html --embed-images analysis.ipynb

The diagram a data notebook needs

Notebook readers are usually asking where the data came from. That makes the useful picture a pipeline or a lineage view rather than a service map: sources, ingestion, the warehouse layers, and the specific tables this notebook reads. Datadef draws column-level lineage as first-class relationships, which is closer to the question than boxes and arrows. See lineage.

Keep the diagram in one place and reference it from every notebook that needs it. Ten notebooks each carrying their own base64 copy of last year architecture is the exact failure this page exists to prevent.

Legibility and completeness are handled separately, which matters for an audience that will notice a missing table. Repeated minor resources of the same type inside a module collapse into one node carrying its count and its members, labelled like Storage credentials ×2 with external and root named underneath, and the tail that still does not fit becomes a single supporting-resources node. Nothing is discarded in the process: the counts ride on the canvas and the full inventory lands in the module reference table of the generated architecture.md, which is the document to open when the picture has deliberately compressed something you were looking for.

Keeping it current

A connected repository regenerates the diagram and an architecture.md on a daily sync from GitHub, GitLab or Azure DevOps, so the address in your markdown cell keeps returning something true. See repo to diagram.

FAQ

How do I show an image from a URL in a Jupyter notebook?

Use standard markdown image syntax in a markdown cell, or IPython.display.Image with the url argument in a code cell. Both store only the address, so the picture is fetched at render time and reflects whatever the URL returns then. Watch the argument name: the same class called with filename instead of url reads the bytes and saves them into the cell output, which is the opposite outcome.

Why do images disappear when my notebook is viewed on GitHub?

GitHub sanitizes HTML output in rendered notebooks, so iframes and most raw HTML are removed. Markdown cell images survive and are routed through the GitHub image proxy. Local file paths also fail there, since the file is not part of the rendered context.

Should I drag an image into a notebook or link it?

Dragging encodes the file as base64 inside the .ipynb, which inflates the file and produces unreadable diffs, and the copy never updates. Link it when the notebook lives in version control and the picture should stay current; embed it when the notebook must render offline or be archived unchanged.

Can I get a pannable diagram inside JupyterLab?

Yes, with IPython.display.IFrame pointing at an embeddable diagram page. It works in JupyterLab and in most local notebook interfaces, and it will not render in the GitHub file view because the frame is stripped there. A notebook meant to be read on GitHub should therefore carry a markdown cell image, keeping the frame for interactive work in the lab.

How do I export a notebook with the images frozen?

Convert to HTML with the nbconvert embed-images flag, which inlines every image as a data URI. The command is jupyter nbconvert --to html --embed-images. The exported file then opens with no network access, while the source notebook keeps its addresses and stays current, which is the right split for a deliverable: the repository copy tracks the system, the handout does not move.