The Canvas Is SDUI Too
Even the hardest surface in the product — a gigapixel whole-slide annotation canvas — is just four sections in a list. No bespoke canvas screen; SDUI all the way down.
The hardest screen is still just a list
Server-Driven UI (SDUI) means the server sends an ordered list of typed section descriptions as JSON, and the client is a generic renderer that draws each one by looking its id up in a registry. The obvious worry is that some screen is too special for that — surely a zoomable medical image canvas needs its own hand-built screen. It does not. Even here, the page is nothing but a list of sections the backend hands over.
A quick gloss of the domain, then we leave it alone: a whole-slide image (WSI) is a digital scan of a glass microscope slide; at full magnification it is gigapixel (on the order of 100,000 × 100,000 pixels), far too big to load like a normal photo; so it is served as DZI tiles — the image pre-cut into small square tiles, streamed a screenful at a time. That tiling, and the smart rendering on top of it, is a deep topic of its own. This day is not about that. It is about how the page is composed: four sections, added one at a time.
What the canvas has to put on screen
Four regions make up the working canvas, and each one is a single section. The toolbar is the tool rail (select, pen, shapes, pan, AI, zoom). The viewer is the gigapixel image itself. The annotations panel is the list of classes the AI has marked on the tissue, each with a live count — an annotation class is a labelled category like "Tumour" or "Plasma" that groups the marks. The image_list is the strip of other slides in the project.
Nothing about this layout is written as a canvas screen. Each region is a section the backend can add, remove, or reorder — and the page reveals them one at a time.
canvas — four regions, one section each ┌───────────────────────────────┐ │ toolbar ▸ select pen … zoom │ ├──────────────────────┬────────┤ │ │ Tumour │ │ viewer │ Eos │ │ (gigapixel WSI) │ Plasma │ │ │ ROIs │ ├──────────────────────┴────────┤ │ image_list ▸ ▢ ▢ ▢ ▢ ▢ │ └───────────────────────────────┘
Start from an empty page
Every page in this system starts the same way: an ordered list of sections that begins empty. The backend owns the list; the client owns only how each section looks. An empty list is a blank workspace, and the frontend wrote no canvas code to get there.
{ "page": "canvas", "sections": [] }
From here the canvas is built additively — the backend appends one section object at a time, and each new object makes a region appear. The client never changes.
Add the toolbar
The first section is the DynamicSection shape you already know: an id the client looks up in its mapper to pick a lazy React component, an actions map of the URLs this section may call, and any section-specific fields it needs. The id toolbar resolves to the tool-rail component; its tools array names the tools to show; actions.load is the href the client follows to fetch the section.
{
"id": "toolbar",
"tools": ["select", "pen", "shapes", "pan", "ai", "zoom"],
"actions": { "load": { "url": "/canvas/9/sections/toolbar", "method": "GET" } }
}
Append that object and the tool rail appears along the top. One list entry, one region.
Drop in the gigapixel viewer
The second section is the viewer, and this is where the "keep it a list" discipline pays off. The section object only names the region (id: viewer) and points at its data source (source: dzi_tiles — the tile pyramid the backend prepared). Everything hard — streaming only the visible tiles, adapting detail to zoom, keeping panning smooth on a tablet — lives inside the viewer component, not in the page.
{
"id": "viewer",
"source": "dzi_tiles",
"actions": { "load": { "url": "/canvas/9/sections/viewer", "method": "GET" } }
}
This is the single-responsibility boundary that makes SDUI hold up on a hard surface: the page composes, the section contains. The page stays a plain list even though the section it names is doing gigapixel work.
Switch on the annotation classes
The third section is the annotations panel, bound to the AI's output. Its data field, ai_classes, tells the component which dataset to read — the classes the model marked on this slide, each with a running count.
{
"id": "annotations",
"data": "ai_classes",
"actions": { "load": { "url": "/canvas/9/sections/annotations", "method": "GET" } }
}
Append it and the class list switches on beside the viewer. Same shape as every other section: an id, a data binding, and an actions map — no new page machinery.
Dock the image strip — the canvas is complete
The fourth and last section is the project's slide strip, bound to project_slides. Append it and the strip docks along the bottom; now all four regions are present and the canvas is whole.
{
"id": "image_list",
"data": "project_slides",
"actions": { "load": { "url": "/canvas/9/sections/images", "method": "GET" } }
}
Four objects in a list — toolbar, viewer, annotations, image_list — and the hardest screen in the product is finished. The progressive build looks like this end to end:
Add a section, the region appears
Because the page is a list, changing what the canvas shows is a backend edit, not a frontend release. The backend decides when a region appears: append image_list and the strip docks; drop it from the list and the strip is gone; reorder the list and the regions rearrange. Through all of it the toolbar stays put — it is its own section, untouched by what happens to the others. There is no canvas screen to redeploy, because there is no canvas screen.
Two properties carry over from every other page and matter most here. First, one render path draws all four sections, so a fix to the renderer fixes the canvas and the projects list at once. Second, if the backend ever sends a section id the client does not know, the renderer falls back safely — it skips that one section rather than crashing the whole canvas. The canvas earns no exception from the SDUI rules; it is just another list, composed one section at a time.
Key takeaways
- Even a gigapixel whole-slide-image annotation canvas is not a bespoke screen — it is four sections in a backend-owned list:
toolbar,viewer,annotations,image_list. - The page is built additively: start from
{ "page": "canvas", "sections": [] }and append one section object at a time; each object makes a region appear, and the client writes no canvas code. - Every section is the same DynamicSection shape — an
idthe client maps to a component, a data binding (tools,source: dzi_tiles,data: ai_classes,data: project_slides), and anactionsmap of hrefs to follow. - The page composes, the section contains: all the hard tiling and adaptive-detail work lives inside the
viewercomponent, so the page stays a plain list (single responsibility). - The backend decides when a region appears; adding or removing a section is a JSON edit, not a client redeploy, and the toolbar stays put through it.
- One render path draws every section, and an unknown section id falls back safely instead of crashing — the same guarantees as any other SDUI page.
Checklist
- [ ] I can explain why a gigapixel canvas still fits the "page is an ordered list of sections" model.
- [ ] I can name the four canvas sections and what each one shows.
- [ ] I can build the canvas additively from an empty
sections: []by appending one section object at a time. - [ ] I can read a canvas section JSON and point to its
id, its data binding, and itsactions.loadhref. - [ ] I can explain the "page composes, section contains" boundary and why the hard tiling work lives inside the
viewersection. - [ ] I can explain how adding, removing, or reordering a section changes the canvas without a client redeploy.
- [ ] I can state the two guarantees that carry over: one shared render path, and a safe fallback for unknown section ids.