The theme editor¶
The themes screen (/themes, static/pages/themes.js and its modules in
static/pages/themes/, listed in The web interface) edits the project’s
theme tree (The theme tree) at any depth, one to four levels. It shows one
tree in three views kept in sync, with a side panel, and every change goes
through the operations of POST /api/themes/ops; nothing reaches
decisions/themes.json before someone saves.
Layout¶
part |
what it shows |
keyboard |
|---|---|---|
outline (left) |
the tree at any depth, each node with its keywords (under it), its share of the use, its keywords on demand; a search over every keyword and node name; the « Set aside » tray and the « To check » queue as tabs |
arrows, Home, End, PageUp, PageDown, Right and Left to open and close, letters for type-ahead, Enter to open in the panel, F2 to rename, Delete, Shift+F10 or the Menu key for the actions, Space and Shift for ranges of keywords, Ctrl+A for a node’s keywords |
treemap (centre) |
nested rectangles sized by the use of the keywords counting toward each node, the top level by hue family, a hatched part for a node’s own keywords; zoom into a node and back |
arrows choose, Enter zooms in, Escape zooms out |
map (centre, a tab) |
the people and keywords of |
arrows move, + and − zoom, 0 fits |
side panel (right) |
the selected node (its keywords with their use, the people who weigh most on it, its names per language, what it had in the saved version and on the map), keywords (their use, place, review and attribution) or person |
Escape goes back to the outline |
Page keys: Ctrl+Z undo, Ctrl+Shift+Z or Ctrl+Y redo, Ctrl+S save, / search.
Selecting in one view selects in the others: a node chosen in the treemap or
the map opens its path in the outline.
Actions¶
Every action has a menu item (the context menu, the panel’s buttons, the toolbar’s « Levels » and « More ») and a key; drag and drop is only a shortcut: keywords or a node dropped on a row of the outline or a rectangle of the treemap (keywords onto any node; a node under a node of the level above).
action |
where |
operation |
|---|---|---|
rename (per language) |
F2, menu |
|
move to… |
menu, drag |
|
merge with… |
menu |
|
split |
menu: tick keywords and child nodes, name the new node |
|
create |
toolbar, menu (inside, beside) |
|
delete |
Delete, menu (empty nodes only) |
|
set aside, with a reason |
Delete on keywords, menu |
|
put back |
the tray’s menu (where it came from, or into…) |
|
attribution |
menu: its own level, down to a level, nowhere |
|
insert or remove a level, rename a level |
« Levels » |
|
accept, move or set aside what a rebase added |
the « To check » queue (A, M, S; J and K) |
|
New nodes get their ids from the browser (n<k>, the next free number, as the
server would): the operations of the undo list are self-contained, so they can
be applied again to another tree (below).
Nothing is lost¶
Undo and redo of every operation, unlimited within the visit. An entry holds the operations, their descriptions (from
POST /api/themes/ops, shown in the interface language with the nodes’ names at that time: « Merge “Tidal flats” into “Estuaries” ») and a patch of what changed: undo and redo need no call, and a long session costs little memory.The draft is kept in the browser’s local storage, per project (
cartolex.themes-draft/1:<project id>): the tree, the version of the saved tree it started from, and the undo list. It is written right after each change is on screen and whenever the tab is hidden or closed, so it survives a reload and a crash of the tab or of the browser; the page restores it with a notice (« Your unsaved changes … were restored »). Why the browser and not a draft route: a draft is personal and unfinished. A file in the project would be written at every operation, into a folder that other people of a hosted project see and that file sync copies around; the project’s decisions stay what people saved. The cost: a draft stays in the browser it was made in, and a private window loses it when closed.Leaving with unsaved changes asks in the page (« Leave, keep the draft » or « Stay »), never with the browser’s
confirm(); closing the tab asks the browser’s own question.Save writes a new version with
If-Match(the version the edits started from); its action names the steps since the last save. Saving removes the empty nodes and says which; that removal is a step of the undo list too.Reload and merge. A save refused because someone saved meanwhile (412), or a draft made on an older version, re-applies the draft’s operations to the newest tree (
POST /api/themes/opswithlenient); what no longer applies is listed with the reason, and nothing is saved until the person saves.Versions: the list (when, by which action; the nodes an action names are named as in the versions around it, so a node merged away since keeps its name), a version opened read-only, compared with the current tree (keywords added, removed and moved, nodes renamed, added, removed, moved), restored (a restore is a new version).
Review and the vocabulary¶
The « To check » queue lists the keywords a rebase added, each with its proposed place; accepting, moving or setting one aside marks it
reviewed, saved with the tree.A banner says when the tree is based on another vocabulary than the current one, with « Rebase onto the current vocabulary » (
POST /api/themes/rebase); its new keywords then wait in the queue.A clustering-only change (the grouping ran again with other parameters on the same keywords) leaves the tree as it is: a banner offers the new proposal as a comparison with the tree, then « Adopt the proposal » or « Keep my tree », once (
POST /api/themes/proposal). An apply made while the question waits does not drop it silently: it keeps the tree over the proposal as a version of its own (« keep the curated tree over the proposal … at an apply », in the versions list), and a one-line notice says so. A rebase agrees with the grouping it placed the new keywords from: the question comes back only if the grouping runs again after it.
Borderline keywords and suggested places¶
The « Borderline » tab lists the placed keywords nearest the border between their node and another (
POST /api/themes/borderline, paged, smallest margin first), each with the other node and its margin; a « Compare at » choice compares every keyword at one level (through its node’s ancestor there) instead of its own node’s. Its keys: A keeps a keyword here (set_reviewkept: reviewed, and it leaves the list; a review in the « To check » queue leaves it listed), O moves it to the other node (move_keywords), S sets it aside, J and K move; the same actions are in its context menu.Suggested places: a keyword set aside or « to check » shows its three nearest nodes in the side panel (
POST /api/themes/suggestions), and the first in its row of the tray or the queue; a button, or 1, 2 or 3 in the tray or the queue, puts it there (put_back, ormove_keywordsandset_reviewfrom the queue): one step of the undo list.
The measure (cartolex.lexicon.theme_fit): the margin of a keyword is
cos(k, own) − cos(k, other) in the keywords’ space (themes.space), where
own is the centroid of the other keywords of its node (the keyword left
out) and other the nearest centroid of another node of the same level; a
centroid is the renormalised mean of the L2-normalised vectors of the
keywords on or under the node. Why: the grouping cut the same space with Ward’s
method on these normalised vectors and centroids, so the margin says how near
the keyword came to being grouped elsewhere; leaving it out of its own node
removes its pull on a small node; one level at a time keeps a node from
competing with its parent; a silhouette over every pair of keywords would say
much the same at a cost that grows with the square of the vocabulary. A
suggestion’s score is the cosine to the centroid of the keywords placed on a
node. Both read the tree being edited, so they follow each change; they are
asked only while shown, a quarter of a second after the last change, and cost
a few milliseconds on S.
Playground¶
The centre column’s third tab, « Playground » (playground/: panel.js,
controls.js, store.js, icicle.js, balance.js, adopt.js), lets the
curator try the grouping’s settings and see the tree before adopting it or
sending it to AI. While it is shown it takes the outline’s and the side
panel’s room.
Controls along the top, each the shared
ParamControlof its parameter: the levels (1 to 4), the keywords per topic and the top themes, or the size of each level by hand; the comb and its θ (« Calibrated », with the θ the calibration kept); the space (people or texts). They start from the project’s parameters (GET /api/params, read when the tab opens) and stay for the visit; a save of the « Tune » panel shows here, and the other way round.The tree as an icicle, one column per level, aligned: a node’s children sit beside it in the next column, in one group whose total height is the parent’s. A block’s height follows its keyword count, never below the room of its label; the node’s own keywords show as a hatched band at its foot and as « N its own ». The top level is in its hue family, the levels below in tints of it. A parent of more than 20 children (the top level: 60) shows its largest and « + N more » inside its own span. The whole icicle scrolls as one, the levels’ names staying in view; hovering or focusing a node highlights its path (ancestors and descendants). A block opens its most used keywords in a popover. Keys: one tab stop, ↑ ↓ within a level, ← → to the parent and the first child, Enter or Space, Escape.
The side panel: the balance across levels (per level, the share of the keywords on its nodes, and the keywords on each node, median and middle half, against the target: few keywords on the top level, about as many on each node of a level); against the editor’s tree (themes split and merged, keywords that would move to another theme, be set aside or placed); then « Adopt as my draft » and « Send to AI ».
A change regroups in the background (POST /api/themes/playground, a job
of the preview group), 0.4 s after the last change, cached by the settings
and the runs it read; a newer change supersedes the job (the request names it,
the server cancels it) and the newest settings are asked for when it ends.
Nothing is saved. The preview runs the build’s own runners, themes.group’s
and, when the space’s unit changes, themes.space’s, into a scratch folder
(cartolex.app.playground): it is what a build with those settings proposes.
Stability per node (the copilot kit’s measure) is not shown: it regroups a
space fitted again three times, many times a preview’s cost.
« Adopt as my draft » writes the settings to params.json as the Tune
panel’s Save does, builds themes.group (a build that would also run other
stages goes to the pre-flight sheet instead), carries the curator’s work onto
its proposal (POST /api/themes/carry, cartolex.project.themes_carry:
names, set-asides, attributions and reviews kept wherever a node continues,
a node of the proposal holding more than half of an old node’s keywords and
taking more than half of its own from it) and puts it in place of the
editor’s tree as one step of the undo list: the draft, saved by Save as any
other edit; the « new grouping » question does not come back for it.
« Send to AI » adopts, then opens the copilot’s dialog on the draft (the
theme curation has no API route).
Apply¶
« Save and apply » saves if needed and starts POST /api/themes/apply in the
background: a banner and the header’s Activity indicator follow the job, the
editor stays usable, and a failure shows an error card with its code and what
to do. What depends on people is not recomputed while editing: people’s
shares (the side panel’s « people who weigh most »), their colours and the
map itself refresh when the apply ends, and the editor says so under the map
and in the side panel. The keywords’ places, their colours on the map and
the treemap’s areas follow each edit at once.
Curate with AI¶
« Curate with AI », in the editor’s header, opens the copilot’s dialog
(The AI copilot): the tree being edited, unsaved edits included, goes into a
bundle an assistant that runs code works from on its own, with the tree’s
levels and measures, the curator’s standing rules and cartolex’s kit. Its
result.json comes back through the same dialog (or a result imported before
is opened again), and is shown as a list: each change with the assistant’s
reason, those that cannot apply to the tree with why. The person chooses,
previews the chosen ones on the tree (read-only), and applies them: they are
saved at once, as a version whose action starts with ai-copilot:.
Earlier versions also offered a handoff (instructions and the tree as text to paste in a chat, the answer pasted back); the answers they imported stay readable (The app and its API), and open in the same review.
Components¶
The screen uses the component library and adds three components to it:
component |
what it does |
|---|---|
|
a virtualised tree (the rows in view only) with the tree pattern’s keyboard, single and range selection, type-ahead, context menu, drag and drop, and a caller’s own keys (the queue’s A, M, S) |
|
squarified nested rectangles by weight, hue families from |
|
points on a canvas: a scene (layers of points, a palette of colours or tokens, a highlight), a view (pan, zoom, fit), hit testing through a grid, resize, theme changes, and a renderer interface ( |
Measures¶
On the L demo world (depth 2: 2 955 keywords, 163 nodes; the app on the same
computer). The browser check keeps measuring the page’s readiness and the
search (tests/browser/test_theme_editor.py, in
.cache/check/ui-measures.json); the other lines were measured while the
screen was built, with the same harness:
measure |
budget |
measured |
|---|---|---|
page ready |
< 1 s |
0.24 s |
search over every keyword, until the outline shows the matches |
< 100 ms |
4 to 18 ms |
an operation’s feedback (a rename, from Enter to the outline) |
< 100 ms |
21 to 43 ms |
undo |
< 100 ms |
20 to 23 ms |
map pan, 10⁴ points |
60 frames a second |
a frame every 16.7 ms (median and 95th percentile) |
ten visits of the editor and back |
no leak |
DOM nodes +0, listeners +0 |
A rename, a move, a set aside or a review shows at once in the tree (the browser applies it by the same rules), then the server’s tree, the one kept, replaces it; merges, splits and levels wait for the server (about 0.1 s on L, with a spinner beside the status meanwhile). The first read after the app starts loads the scientific libraries for the keywords’ use (about half a second): the tree shows first, sized by its keyword counts, then by use.