Skip to content

Graph Model

David Benson edited this page Oct 7, 2026 · 1 revision

This page describes the programming model behind a draw.io diagram: the cells, their geometry, transactions, events, undo, the view, selection, labels and pages. It is for plugin authors and forkers who read or change diagrams in code. For the on-disk XML see File format; for style strings see Styles.

All examples run inside a plugin (see Plugins):

Draw.loadPlugin(function(ui)
{
	var graph = ui.editor.graph;
	var model = graph.getModel();
	// ...
});

Where the objects live

Object Class Source
ui App (extends EditorUi) diagramly/App.js, grapheditor/EditorUi.js, diagramly/EditorUi.js
ui.editor Editor grapheditor/Editor.js, diagramly/Editor.js
ui.editor.graph Graph (extends mxGraph) grapheditor/Graph.js, mxgraph/src/view/mxGraph.js
graph.model / graph.getModel() mxGraphModel mxgraph/src/model/
graph.view / graph.getView() mxGraphView mxgraph/src/view/mxGraphView.js
graph.getStylesheet() mxStylesheet see Styles
graph.getSelectionModel() mxGraphSelectionModel mxgraph/src/view/
ui.editor.undoManager mxUndoManager created in Editor.prototype.createUndoManager
ui.pages, ui.currentPage DiagramPage diagramly/Pages.js

One Graph instance shows one page at a time. The model holds the cells of the current page only; the other pages are kept in ui.pages (see Pages).

The cell hierarchy

Every page has the same three-level structure:

root cell (id "0")                 model.getRoot()
└── layer (id "1", the default)    model.getChildAt(root, 0), graph.getDefaultParent()
    ├── vertex
    │   └── child vertex           (a group, container or relative child)
    └── edge
        └── edge label             (a vertex with a relative geometry)
  • The root has no geometry. Its children are the layers; model.isLayer(cell) is true when the parent is the root.
  • graph.getDefaultParent() returns the layer that new cells go into (the current layer in the Layers window). Add a layer with graph.addCell(new mxCell('Name'), model.root).
  • A hidden layer has visible = false; a locked layer has locked=1 in its style.
  • Vertices can contain vertices and edges (groups, containers, tables, swimlanes). Edges can contain vertices (edge labels).
  • The model moves an edge into the nearest common ancestor of its terminals (mxGraphModel.prototype.maintainEdgeParent, default true).

mxCell

mxCell is a plain data object. Read its fields directly, but change them only through the model (model.setValue, model.setStyle, ...). The setters on the cell itself (cell.setStyle, cell.setAttribute, ...) bypass the transaction, so there is no undo, no event and no repaint.

Field Accessor Meaning
id cell.getId() String, unique within the page. Created by the model if missing.
value model.getValue(cell) A string (the label) or an XML element (a user object, see Values and labels).
style model.getStyle(cell) The style string, see Styles.
vertex, edge model.isVertex(cell), model.isEdge(cell) At most one is true. Root and layers have neither.
parent model.getParent(cell) Containing cell.
children model.getChildCount(cell), model.getChildAt(cell, i), model.getChildren(cell) Child cells in z-order (first is at the back).
source, target model.getTerminal(edge, isSource) Connected vertices (or edges) of an edge, or null.
edges model.getEdgeCount(cell), model.getEdgeAt(cell, i), model.getEdges(cell) Edges connected to a cell.
geometry model.getGeometry(cell) mxGeometry, see below.
connectable model.isConnectable(cell) Default true. Graph.isCellConnectable also checks the connectable style.
visible model.isVisible(cell) Default true.
collapsed model.isCollapsed(cell) Default false. A collapsed container swaps its geometry with geometry.alternateBounds.

Other useful lookups: model.getCell(id), model.getDescendants(cell), model.filterDescendants(filter, parent), model.getTopmostCells(cells), model.isAncestor(parent, child), model.getEdgesBetween(a, b, directed).

mxGeometry

mxGeometry extends mxRectangle. Coordinates are in model units (pixels at 100% zoom) and relative to the origin of the parent cell (a layer's origin is 0,0).

Field Vertices Edges
x, y, width, height Bounds in the parent. x, y place the edge's own label (see relative); width/height are ignored.
relative If true, x and y are fractions (0..1) of the parent's size and offset is added in pixels. Used for ports and decorations attached to a parent. Always true for edges created by graph.createEdge. x is the position along the edge from -1 (source) to 1 (target), 0 is the centre; y is the orthogonal distance in pixels.
offset For relative geometries the pixel offset of the origin, otherwise the label offset. Pixel offset of the label.
points Not used. Array of mxPoint waypoints in parent coordinates. Interpreted by the edge style.
sourcePoint, targetPoint Not used. End points used when the edge has no source or target terminal.
alternateBounds Bounds to swap in when the cell is collapsed or expanded. Not used.

Edge labels added in the editor are child vertices of the edge with a relative geometry (x along the edge, y orthogonal, plus offset) and the edgeLabel style.

Geometries are shared with the undo history. Never modify a geometry in place; clone it, change the clone and set it:

var geo = model.getGeometry(cell).clone();
geo.x += 20;
geo.points = [new mxPoint(100, 40)];   // edges only
model.setGeometry(cell, geo);

Transactions

All changes go through the model and are grouped into transactions. A transaction is one undo step and one change event:

model.beginUpdate();
try
{
	// any number of changes
}
finally
{
	model.endUpdate();
}

Transactions nest: only the outermost endUpdate fires the change and undo events. Always use try/finally, otherwise an exception leaves the model in an open transaction and later changes are never shown or saved.

Model-level changes (each creates a change object, see below): model.add(parent, child, index), model.remove(cell), model.setValue, model.setStyle, model.setGeometry, model.setTerminal(edge, terminal, isSource), model.setTerminals(edge, source, target), model.setVisible, model.setCollapsed, model.setRoot and model.execute(change). A single call outside beginUpdate/endUpdate is its own transaction.

The mxGraph methods (insertVertex, addCells, moveCells, resizeCells, removeCells, setCellStyles, groupCells, labelChanged, ...) open their own transaction and also apply the graph's rules (constraints, layouts, container resizing). Prefer them to raw model calls when there is one.

Changes, events and undo

Each atomic change is an object with an execute() method; executing it again undoes it. After execute(), the fields describe the current state and previous holds the state before:

Change Fields
mxChildChange child, parent (null when the cell was removed), previous (null when it was added), index
mxTerminalChange cell, terminal, previous, source (boolean)
mxValueChange cell, value, previous
mxStyleChange cell, style, previous
mxGeometryChange cell, geometry, previous
mxCollapseChange, mxVisibleChange cell, collapsed / visible, previous
mxRootChange root, previous

Page changes in Pages.js (SelectPage, ChangePage, RenamePage, MovePage, ...) use the same mechanism.

When the outermost endUpdate runs, the model fires, in order:

Event Property Use
mxEvent.CHANGE edit (an mxUndoableEdit; edit.changes is the array of changes) React to any change. Also fires on undo and redo.
mxEvent.NOTIFY edit Same as CHANGE; intended for syncing with a remote model.
mxEvent.UNDO edit Fired once per new transaction (not on undo/redo). The undo manager listens to this.

Other model events: mxEvent.BEGIN_UPDATE, mxEvent.END_UPDATE, mxEvent.EXECUTE / mxEvent.EXECUTED (per change, while the transaction is open). The graph fires higher-level events such as mxEvent.CELLS_ADDED, mxEvent.MOVE_CELLS, mxEvent.LABEL_CHANGED and mxEvent.ROOT (the root, i.e. the page, was replaced).

Editor.prototype.createUndoManager creates ui.editor.undoManager and adds every mxEvent.UNDO edit of the model and the view to it. To undo or redo in the app, call ui.undo() / ui.redo() (or run the undo / redo actions) rather than ui.editor.undoManager.undo(): the EditorUi versions also handle in-place text editing and repair the page after the step.

The view

The model is pure data. mxGraphView keeps an mxCellState for each visible cell of the current page, with the resolved style, the absolute position and the DOM shapes.

var state = graph.view.getState(cell);   // null if the cell is not rendered

if (state != null)
{
	state.style;              // resolved style object (see Styles)
	state.x; state.y;         // screen position: scale and translate applied
	state.width; state.height;
	state.getCellBounds();    // absolute bounds in model units
	state.absolutePoints;     // edges: screen points of the route (cellPoints in model units)
	state.shape; state.text;  // mxShape instances that paint the cell and its label
}

graph.view.scale is the zoom factor and graph.view.translate the pan offset in model units; a screen coordinate is scale * (modelCoordinate + translate). Read or write the model, not the state: states are rebuilt when the model changes, and cells in collapsed or hidden containers and on other pages have none. graph.getCellGeometry(cell) returns the geometry; graph.getBoundingBox(cells) and graph.getGraphBounds() return screen bounds.

Selection

Call Purpose
graph.getSelectionCells() / graph.getSelectionCell() Current selection (array / first cell).
graph.setSelectionCells(cells) / graph.setSelectionCell(cell) Replace the selection.
graph.addSelectionCells(cells), graph.removeSelectionCells(cells), graph.clearSelection() Modify it.
graph.getSelectionCount(), graph.isSelectionEmpty(), graph.isCellSelected(cell) Query it.
graph.selectAll(parent, descendants) Select the children of a parent.

Listen to graph.getSelectionModel() for mxEvent.CHANGE. Its added and removed properties are swapped for historical reasons: added holds the cells that were deselected and removed the cells that were selected. Usually it is simpler to call graph.getSelectionCells() in the listener.

Inserting cells

From mxGraph.js:

graph.insertVertex(parent, id, value, x, y, width, height, style, relative)  // returns the vertex
graph.insertEdge(parent, id, value, source, target, style)                   // returns the edge
graph.addCell(cell, parent, index, source, target)                           // returns the cell
graph.addCells(cells, parent, index, source, target, absolute)               // returns the cells
  • parent null means graph.getDefaultParent(). id null lets the model create one. In draw.io, generated IDs are a random prefix plus a counter (model.prefix = Editor.guid() + '-', reset when a file is opened and when a page is selected), so never derive meaning from them; pass your own id if you need a stable one, and check it is free with model.getCell(id).
  • value is a label string or an XML element (see below). style is a style string; with no style the stylesheet defaults apply, which differ from the defaults the sidebar uses (see Styles).
  • graph.createVertex(...) and graph.createEdge(...) take the same arguments and return a cell that is not yet in the model.

To remove cells use graph.removeCells(cells, includeEdges) or, as the Delete key does, graph.deleteCells(cells, includeEdges).

Values, user objects and labels

A cell value is either a string or an XML element, called a user object. User objects carry the label plus any number of custom attributes (Edit Data in the editor, Ctrl+M or Cmd+M). In the file they wrap the mxCell (see File format).

Call Purpose
graph.convertValueToString(cell) The label text: the value, or the label attribute of a user object (or the attribute named by placeholder).
graph.getLabel(cell) The label as rendered, with %placeholders% replaced.
graph.labelChanged(cell, text) Set the label (undoable). Updates the label attribute of a user object.
graph.getAttributeForCell(cell, name, defaultValue) Read an attribute of a user object.
graph.setAttributeForCell(cell, name, value) Set (or, with null, remove) an attribute. Undoable; clones the value and converts a string value into a UserObject element first.
cell.getAttribute(name, defaultValue), cell.hasAttribute(name) Read directly from the user object.

Attributes with a meaning in draw.io:

Attribute Meaning
label The label.
tooltip Tooltip text (graph.getTooltipForCell). Without it, the tooltip lists the other attributes.
note Markdown note (Edit Note; graph.getNoteForCell, graph.setNoteForCell).
link, linkTarget Cell link (graph.getLinkForCell, graph.setLinkForCell). data:page/id,<pageId> links to a page.
placeholders 1 replaces %name% in the label and tooltip with the value of attribute name on the cell or its nearest ancestor (up to the root cell), then the global variables.
placeholder The label is the value of the named attribute on the cell or its nearest ancestor.
tags Space-separated tags (graph.getTagsForCell, graph.addTagsForCells).

Attributes on the user object of the root cell (Edit Data with nothing selected) are therefore page-wide variables. See Placeholders and Predefined placeholders.

Labels are plain text unless the cell style has html=1; then the label is HTML. Labels are stored raw and sanitised only when rendered (cellRenderer.getLabelValue in Graph.js calls Graph.sanitizeHtml). If your code puts a label into the DOM itself, sanitise it with Graph.sanitizeHtml or write it as text with mxUtils.write; see Security.

Reading and writing XML

Call Returns / does
new mxCodec(mxUtils.createXmlDocument()).encode(model) <mxGraphModel> element of a model, cells only.
new mxCodec(doc).decode(node, model) Decodes an <mxGraphModel> element into a model.
ui.editor.getGraphXml() <mxGraphModel> element of the current page, including the page settings (grid, page size, background, ...).
graph.encodeCells(cells) <mxGraphModel> element with clones of the given cells.
ui.getXmlFileData() <mxfile> element with all pages.
ui.getFileData(true) The whole file as an XML string, as it would be saved.
ui.importXml(xml, dx, dy) Inserts copies of the cells (or extra pages) of an <mxGraphModel> or <mxfile> string into the diagram; undoable; returns the new cells.
ui.replaceDiagramData(xml) Replaces the current page with an <mxGraphModel> string; undoable.
mxUtils.parseXml(text), mxUtils.getXml(node), mxUtils.getPrettyXml(node) Parse and serialise.

The codec registry (mxgraph/src/io/) maps class names to codecs. mxCellCodec writes a user object as the outer element with the mxCell inside it, and mxModelCodec writes the <root> (and <defs> with style compression).

Pages

A file has one or more pages. ui.pages is an array of DiagramPage objects; ui.currentPage is the one in the graph.

DiagramPage member Meaning
getId(), getName(), setName() Page ID and name (attributes of the <diagram> node).
node The <diagram> XML node; holds the page content while the page is not current.
root The decoded root cell. Null until the page has been shown or decoded; call ui.updatePageRoot(page) to decode it.
viewState Grid, page format, background, scale and other page settings.

Switching pages saves the current root and view state into the page, then gives the graph the new page's root (graph.model.rootChanged). The graph fires mxEvent.ROOT and ui.editor fires pageSelected. Cell objects and IDs of other pages are therefore not in graph.model; use ui.updatePageRoot(page).root to read them, and select a page before changing it.

Call Purpose
ui.selectPage(page), ui.selectNextPage(forward) Show a page.
ui.insertPage(page, index) Insert (and show) a page; with no arguments creates an empty page at the end. Returns the page.
ui.createPage(name, id) New DiagramPage that is not yet in the file; ui.createPageId() returns a free ID.
ui.removePage(page), ui.duplicatePage(page, name), ui.movePage(oldIndex, newIndex) Edit the page list.
ui.getPageById(id), ui.getPageIndex(page), ui.getSelectedPageIndex() Look up pages.
graph.model.execute(new RenamePage(ui, page, name)) Rename without the dialog (ui.renamePage(page) opens it).

Inserting, removing, duplicating, moving and renaming pages are executed as changes on graph.model, so they are undoable and mark the file as modified. Selecting a page is also recorded in the undo history but does not modify the file.

Recipes

Add a vertex and an edge

var parent = graph.getDefaultParent();
var v1, v2, e1;

model.beginUpdate();
try
{
	v1 = graph.insertVertex(parent, null, 'Start', 40, 40, 120, 60,
		'rounded=1;whiteSpace=wrap;html=1;');
	v2 = graph.insertVertex(parent, null, 'End', 240, 40, 120, 60,
		'ellipse;whiteSpace=wrap;html=1;');
	e1 = graph.insertEdge(parent, null, 'next', v1, v2,
		'edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;endArrow=block;');
}
finally
{
	model.endUpdate();
}

graph.setSelectionCells([v1, v2, e1]);

Change a style on the selection

var cells = graph.getSelectionCells();

model.beginUpdate();
try
{
	graph.setCellStyles('fillColor', '#dae8fc', cells);
	graph.setCellStyles('strokeColor', '#6c8ebf', cells);
	graph.setCellStyles('dashed', null, cells);   // null removes the key
}
finally
{
	model.endUpdate();
}

Iterate all cells

// Current page: vertices and edges in z-order
var cells = model.filterDescendants(function(cell)
{
	return model.isVertex(cell) || model.isEdge(cell);
});

// All pages (read only)
for (var i = 0; i < ui.pages.length; i++)
{
	var root = ui.updatePageRoot(ui.pages[i]).root;
	var stack = [root];

	while (stack.length > 0)
	{
		var cell = stack.pop();
		// ... cell.vertex, cell.edge, cell.value, cell.style
		for (var j = 0; j < cell.getChildCount(); j++)
		{
			stack.push(cell.getChildAt(j));
		}
	}
}

Find cells by attribute

function findByAttribute(name, value)
{
	return model.filterDescendants(function(cell)
	{
		return mxUtils.isNode(cell.value) && cell.getAttribute(name) == value;
	});
}

graph.setSelectionCells(findByAttribute('owner', 'ops'));

Store custom data on a cell

var doc = mxUtils.createXmlDocument();
var obj = doc.createElement('object');
obj.setAttribute('label', 'web-01');
obj.setAttribute('owner', 'ops');

var v = graph.insertVertex(graph.getDefaultParent(), null, obj,
	40, 160, 120, 60, 'whiteSpace=wrap;html=1;');

graph.setAttributeForCell(v, 'ip', '10.0.0.1');   // undoable
graph.getAttributeForCell(v, 'ip');               // '10.0.0.1'

Listen for changes

model.addListener(mxEvent.CHANGE, function(sender, evt)
{
	var changes = evt.getProperty('edit').changes;

	for (var i = 0; i < changes.length; i++)
	{
		var change = changes[i];

		if (change instanceof mxChildChange && change.previous == null)
		{
			console.log('added', change.child.id);
		}
		else if (change instanceof mxChildChange && change.parent == null)
		{
			console.log('removed', change.child.id);
		}
		else if (change instanceof mxValueChange || change instanceof mxStyleChange)
		{
			console.log('changed', change.cell.id);
		}
	}
});

graph.getSelectionModel().addListener(mxEvent.CHANGE, function()
{
	console.log('selected', graph.getSelectionCount());
});

Group and ungroup

// null creates the group cell; edges between the cells move into the group
var group = graph.groupCells(null, 0, graph.getSelectionCells());
graph.setSelectionCell(group);

// Later: moves the children to the group's parent and removes the empty group
graph.setSelectionCells(graph.ungroupCells([group]));

Read and write the current XML

// Current page as an <mxGraphModel> string
var xml = mxUtils.getXml(ui.editor.getGraphXml());

// Whole file (all pages) as it would be saved
var file = ui.getFileData(true);

// Replace the current page (undoable)
ui.replaceDiagramData(xml);

// Insert cells from XML, offset by 20px (undoable)
var inserted = ui.importXml('<mxGraphModel><root><mxCell id="0"/>' +
	'<mxCell id="1" parent="0"/><mxCell id="a" value="Imported" ' +
	'style="whiteSpace=wrap;html=1;" vertex="1" parent="1">' +
	'<mxGeometry x="0" y="0" width="120" height="60" as="geometry"/>' +
	'</mxCell></root></mxGraphModel>', 20, 20);

importXml inserts copies with new IDs (links between the imported cells are updated). replaceDiagramData decodes new cell objects, so look up cells again with model.getCell(id) afterwards.

Add a page

var page = ui.insertPage(ui.createPage('Details', ui.createPageId()));

// The new page is now current: graph.model holds its (empty) root
graph.insertVertex(graph.getDefaultParent(), null, 'On page 2', 40, 40, 120, 60);

ui.selectPage(ui.pages[0]);

Move and resize

graph.moveCells(graph.getSelectionCells(), 20, 0);   // dx, dy

var cell = graph.getSelectionCell();
var geo = graph.getCellGeometry(cell).clone();
geo.width = 200;
graph.resizeCells([cell], [geo]);                    // fires RESIZE_CELLS, which layouts react to

See also

Clone this wiki locally