Tables
Many application areas require a presentation of data where the nodes of a diagram are organized in a tabular way, i.e., where each node is associated to a specific row and column in the grid-like structure of a table. Swimlane layouts are a popular example of such presentations.
The following figure shows samples of tabular presentations of diagrams:
The yFiles for HTML diagramming library contains comprehensive support for tabular data presentation, which builds on the general concept of grouped graphs. The provided functionality covers the “look” as well as the “feel,” i.e., table structures with rows and columns can be both rendered and also interactively edited. Rows and columns can be:
- added,
- removed,
- resized, and also
- reordered.
Furthermore, the hierarchical layout style, more precisely class HierarchicLayout, provides advanced support for automatic tabular layout.
Concepts
The presentation of a diagram in a tabular way needs an additional element, namely a table structure to encompass the proper nodes of the diagram. This table structure is backed by a group node, and the diagram’s proper nodes, i.e., the actual content nodes, need to be set up so that they are child nodes of this group node.
Using a group node to hold the content nodes brings all advantages of this concept as described in Grouping Nodes. For example, when the group node is moved, the child nodes move accordingly, thus maintaining the visual clue that they are contained in the table group node.
The table structure, which typically shows some rows and columns within which the content nodes lie, directly results from the table model which actually defines these rows and columns.
The group node that is “behind” the table structure uses a special node style implementation to govern the actual rendering. The visualization of individual rows and columns can be conveniently achieved by using special style implementations.
User interaction with the table structure is supported by a specialized input mode, which makes available support for resizing rows and columns, re-parenting them, or editing their labels, for example.
It is crucial to understand, however, that the content nodes of a diagram and the table structure are only loosely coupled. More precisely, the association of a content node to a row and a column is only done on a geometric basis, i.e., the node’s center coordinates determine the row (column) that it belongs to.
This also means that when a content node is moved, for example, the representation of the table does not change in any way.
Table Model
Basically, the table model represents the blueprint for the rendering of the table structure. The table model defines the rows and columns of a table, their sizes, and in particular their nesting structure, i.e., rows (columns) may have so-called child rows (columns) to support nested row (column) structures.
Table structure with nested rows and columns shows a table structure with nested rows and columns. The table has two top-level rows where one consists of two child rows. Similarly, there are two top-level columns where one consists of two child columns.
Note that there are no content nodes.
The table model is constituted by the types listed in Table model types.
The ITable implementation provides the virtual roots of the row and column hierarchies of a table model. These allow to add top-level rows and columns, to which in turn child rows and child columns can be added to.
The area that belongs to a row (column) is determined by its height (width) as well as by its position with respect to the order of rows (columns). A row (column) spans all columns (rows) of the table structure, i.e., its width (height) is the accumulated width (height) of all columns (rows).
While the width of a row (height of a column) is determined implicitly, its height (width) can be set. This size, however, is restricted by the row’s (column’s) minimum size. In other words, a row (column) cannot be made smaller than its minimum size.
- setSize(stripe: IStripe, size: number): void
- Sets the preferred size for a given IStripe, i.e., the height of a row or the width of a column.
Also, if the row (column) contains any nested child rows (columns), setting the size does not take effect, since it is implicitly constrained to the accumulated sizes of the nested rows (columns).
For the same reason the actual bounds, or layout, of a row (column) also cannot be set explicitly. Instead, they are determined as a result of the table structure, which means they are calculated anew through the row’s (column’s) layout property whenever necessary.
From a geometric point of view, a table consists of its content area, which is determined by the geometry of its rows and columns, and the surrounding border area. The border area is determined by the table insets, and may be used to display a table description, for example.
- insets
- Sets the table’s insets. The insets define a border around the rows and columns of a table structure.
Similarly, rows and columns also have a content area and a border area. The content area contains the content nodes of the row (column), respectively the content area of nested rows (columns).
The border area which is determined by corresponding row (column) insets, however, denotes both extra space outside of a row’s (column’s) content area as well as some padding inside the content area.
- setStripeInsets(stripe: IStripe, insets: Insets): void
- Sets insets for a given IStripe, i.e., a row or column. The insets denote extra space to the left and right of a row’s content area, respectively at the top and bottom of a column’s content area.The top and bottom values (row) as well as the left and right values (column) denote padding space at the inside of the content area.
In a parent row (column), i.e., a row (column) with nested child rows (columns), the content area of the child rows (columns) lies completely within the content area of the parent row (column).
The border area of the parent row (column) is to the left and right of its nested child rows (at the top and bottom of its nested child columns).
The actual insets and the actual size of a parent row (column) where nested child rows (columns) affect the parent row’s (column’s) preferred insets and size can be queried using the following methods:
- actualInsets: Insets
- actualSize: number
- Convenience (read only) properties for interface IStripe to get the actual insets and the actual size of a row or column.
Unless explicitly set, insets for new rows and columns are adopted from the default values as specified by the rowDefaults/columnDefaults provided by the ITable instance:
- rowDefaults
- columnDefaults
- Properties to manage the IStripeDefaults instances that hold default values for the rows and columns of a table structure. Among other things, the default values also include the default insets that are used for newly created rows and columns.
Working with the Table Model
Creating a table model that uses the default ITable implementation class Table goes as follows:
// Creating a new table model using the default ITable implementation.
const table = new Table()
To actually get a table model rendered, it needs yet two other things: a group node, to which the table model needs to be bound to, and an appropriate node style implementation that can handle table models and that is set as the group node’s style.
Binding the table model to a group node means making the table model available in the group node’s look-up, so that it can be queried using the look-up mechanism:
const table = groupNode.lookup(ITable.$class)
This is important for properly handling user interaction with a table structure, for example.
Using class TableNodeStyle, the predefined node style which support tables, as the node style for a group node conveniently binds a table model to the group node as a side effect:
const graph = getMyGraph()
const table = getMyTable()
const tableStyle = new TableNodeStyle(table)
// Create a top-level group node and bind, via the TableNodeStyle, the table to
// it.
graph.createGroupNode(null, table.layout.toRect(), tableStyle)
Upon initialization, a Table instance holds an empty model that has no rows or columns. Rows and columns can be created using the following Table methods:
- createChildRow(owner: IRow, height: number, minHeight: number, insets: Insets, style: IStripeStyle, tag: Object, index: number): IRow
- createChildColumn(owner: IColumn, width: number, minWidth: number, insets: Insets, style: IStripeStyle, tag: Object, index: number): IColumn
- Creates a new row (column) as a child of the given IRow (IColumn).
Adding rows and columns illustrates a simple tabular presentation with two rows and columns together with its setup in code.
const graph = getMyGraph()
// Create a new table model and set insets on the table so that the rows and
// columns stand out.
const table = new Table()
table.insets = new Insets(10, 10, 10, 10)
const rowStyle = new ShapeNodeStyle({ fill: Fill.TRANSPARENT })
table.rowDefaults.style = new NodeStyleStripeStyleAdapter(rowStyle)
const columnStyle = new ShapeNodeStyle({
fill: new SolidColorFill(new Color(139, 162, 220))
})
table.columnDefaults.style = new NodeStyleStripeStyleAdapter(columnStyle)
// Using no insets prevents column/row headers.
const insets = new Insets(0)
table.columnDefaults.insets = insets
table.rowDefaults.insets = insets
table.columnDefaults.size = 30
table.rowDefaults.size = 30
// Add top-level rows and columns. The table's size is then 2x2.
table.createRow()
table.createRow()
table.createColumn()
table.createColumn()
// Using TableNodeStyle to render the table structure.
const tableStyle = new TableNodeStyle(table)
const backgroundStyle = new ShapeNodeStyle({
fill: new SolidColorFill(new Color(248, 236, 201))
})
tableStyle.backgroundStyle = backgroundStyle
// Create a top-level group node and bind, via the TableNodeStyle, the table to
// it.
graph.createGroupNode(null, table.layout.toRect(), tableStyle)
Rows and columns can be deleted using the following method defined by the ITable interface:
- remove(stripe: IStripe): void
- Removes a given IStripe, i.e., a row or column.
The following convenience methods support recursively deleting rows or columns and enable resizing of the remaining rows or columns:
- removeWithResize(stripe: IStripe): void
- removeRecursively(stripe: IStripe): void
- removeRecursivelyWithResize(stripe: IStripe): void
- Convenience (extension) methods for interface ITable to remove rows or columns.
Undo/Redo Support
Support for Undo/Redo operations for the tables in a diagram can be easily enabled using the functionality in class Table which is the default implementation of interface ITable:
- installStaticUndoSupport(graph: IGraph): void
- Enables Undo/Redo support for tables. Installs the undo support instance which is valid on the graph at the time this method is called.
- installDynamicUndoSupport(graph: IGraph): void
- Enables Undo/Redo support for tables. Registers the graph from which the undo support will be queried.
const graph = getMyGraph()
if (graph != null) {
// Enabling general undo support.
graph.undoEngineEnabled = true
// Using the undo support from the graph also for all future table instances.
Table.installStaticUndoSupport(graph)
}
Visual Representation
A table model’s visual representation is provided by a special node style implementation that can handle table models, class TableNodeStyle.
Class TableNodeStyle can be set as the style for a group node as follows:
const graph = getMyGraph()
const table = getMyTable()
const tableStyle = new TableNodeStyle(table)
// Create a top-level group node and bind, via the TableNodeStyle, the table to it.
graph.createGroupNode(null, table.layout.toRect(), tableStyle)
The ITable instance that is set with the TableNodeStyle instance defines the style’s table model. When the TableNodeStyle instance is set as the style of a group node, this table model is used to render a table structure that has the group node’s dimensions and location.
As a side effect of setting the TableNodeStyle instance as the style for a group node, the table model is also conveniently bound to the group node. This means that it is made available in the group node’s look-up, so that it can be queried using the look-up mechanism:
const table = groupNode.lookup(ITable.$class)
This is important for properly handling user interaction with a table structure, for example.
Note that because of its table model, class TableNodeStyle does not support style sharing among multiple group nodes.
TableNodeStyle governs the overall rendering of a table structure. It uses a separate node style implementation to render the background behind all rows and columns:
- backgroundStyle
- Sets the node style that is used to render the background behind all rows and columns of a table structure.
Additionally, class TableNodeStyle defines the following property to determine the rendering order of rows and columns:
- tableRenderingOrder
- Sets the rendering order of the rows and column of a table structure. By default, columns are rendered first, then rows.
The rendering order also affects the fill colors for rows and columns. Using the default rendering order, for example, neither column fill colors nor border lines can be seen for non-transparent row fill colors.
The rendering order is also taken into account for hit-testing.
Rows and Columns
The visual representation of rows and columns of a table structure is conveniently provided by implementations. Stripe styles can be associated with IStripe instances either at creation time or using the following method defined in interface ITable:
- setStyle(stripe: IStripe, style: IStripeStyle): void
- Sets the style for an IStripe instance, i.e., the row or column of a table structure.
The TableNodeStyle that is set with the group node, delegates the rendering of rows and columns to these row and column styles.
Stripe Styles
Stripe styles are responsible for the graphical rendering of rows and columns of a table. Interface IStripeStyle is the common base type for actual implementations.
Predefined stripe style implementations lists the predefined stripe style implementations present in yFiles.
Type Name | Description |
---|---|
The following figures show custom IStripeStyle implementations that provide specialized rendering for rows and columns of table structures:
Rows and columns of a table structure can have labels whose position is determined by label models. The following method defined in ITable can be used to add labels to IStripe instances:
- addLabel(owner: IStripe, text: string, layoutParameter: ILabelModelParameter, style: ILabelStyle, preferredSize: Size, tag: Object): ILabel
- Adds a label to an IStripe instance, i.e., to a row or column of a table structure.
StretchStripeLabelModel and StripeLabelModel are specifically tailored to support positions for the labels of rows and columns in a table structure. It is used as the default label model when adding labels to an IStripe.
The following figure illustrates labels in a tabular presentation with a single row and a single column together with their setup in code:
const graph = getMyGraph()
const table = getMyTable()
// Setup of row and column default insets.
table.rowDefaults.insets = new Insets(30, 5, 30, 5)
table.columnDefaults.insets = new Insets(5, 30, 5, 30)
// Create a single column.
const column = table.createColumn(100)
// Add and configure two labels for the column.
table.addLabel(column, 'Column North')
table.addLabel(column, 'Column South', StretchStripeLabelModel.SOUTH)
// Create a single row.
const row = table.createRow(100)
// Add and configure two labels for the row.
table.addLabel(row, 'Row West')
table.addLabel(row, 'Row East', StretchStripeLabelModel.EAST)
// Using TableNodeStyle to render the table structure.
const tableStyle = new TableNodeStyle(table)
const backgroundStyle = new ShapeNodeStyle({
fill: new SolidColorFill(new Color(248, 236, 201))
})
tableStyle.backgroundStyle = backgroundStyle
// Create a top-level group node and bind, via the TableNodeStyle, the table to
// it.
graph.createGroupNode(null, table.layout.toRect(), tableStyle)
Note that row labels that use WEST
are automatically
rotated 90 degrees counterclockwise while row labels that use EAST
are rotated 90 degrees clockwise.
NodeLabelModelStripeLabelModelAdapter can be used to use a node label model (e.g. FreeNodeLabelModel) as if the row or column were a node. This allows for positions where the dedicated stripe label models mentioned above do not suffice.
CSS Styling of Indicators
The different interactive editing gestures on the table provide visual indicators for better user feedback. Those indicators can either be styled by changing the template that is associated with the resource key (see DefaultStripeInputVisualizationHelper and Customizing String Resources) or through the CSS classes that are provided by the default templates (see Styling of Table Indicators).
User Interaction
TableEditorInputMode is a specialized input mode that brings additional support for handling mouse gestures and keyboard interaction specific to the tabular representation of a diagram.
The additional mouse gesture support covers, for example:
- Selecting rows and columns,
- resizing them, and
- changing their position in the order of rows (columns).
TableEditorInputMode can be used in conjunction with GraphEditorInputMode } as shown in the following code snippet, or can be used on its own.
const geim = getMyGraphEditorInputMode()
geim.add(new TableEditorInputMode())
Upon initialization, TableEditorInputMode creates and installs the input modes listed in the following table as concurrent input modes.
Type Name | Description |
---|---|
If used in conjunction with GraphEditorInputMode, the ClickInputMode, KeyboardInputMode, and TextEditorInputMode child input modes of TableEditorInputMode are disabled and the respective GraphEditorInputMode counterparts are enabled.
Each of the input modes can be obtained or replaced using a like-named property defined by TableEditorInputMode.
In order for TableEditorInputMode and its child input modes to properly recognize the connection between a table structure/table model and its group node, they rely on the table model being bound to the group node. The input modes query the group node’s look-up and expect to find a table model:
const table = groupNode.lookup(ITable.$class)
Interaction Customization
Class TableEditorInputMode provides properties and callbacks that allow for fine-grained control over the support for user interaction.
TableEditorInputMode customization lists the customization properties of class TableEditorInputMode. Most of the properties support that combinations of stripe types can be specified using the constants defined in the StripeTypes enumeration type.
Name | Purpose |
---|---|
Hit-testing
Hit-testing support in a table structure takes into account the overall rendering order of rows and columns (as defined by class TableNodeStyle) as well as the complexities introduced by nested rows and columns.
By default, the specific needs for hit-testing support in a table structure are provided by the StripeHitTester class. The actual instance that is used is also available in the group node’s look-up.
The following convenience methods in TableEditorInputMode help in finding which part(s) of a table structure is (are) underneath a given location. The StripeSubregion class encodes both the stripe, i.e., row or column, and the actual sub region within the stripe.
- findStripe
- Depending on the rendering order of rows and columns, returns the sub region of either a row or a column that is underneath the given location.
- findStripes
- Returns an ordered list of sub regions of (nested) rows and columns that are underneath the given location. The ordering takes into account the rendering order of rows and columns and also the nesting of child rows (columns).
The StripeSubregionTypes enumeration defines constants for the different sub regions within a stripe. The figure below depicts the default definitions of these sub regions together with their corresponding constants for the column of the table structure in Row and column labels.
The stripe sub regions for a row are defined analogously.
The hit-testing for the sub regions can be handled through dedicated IHitTestable implementations. The IStripeHitTestHelper interface bundles all stripe-related IHitTestable instances and makes the default implementations conveniently accessible.
Tutorial Demo Code
The Table Editor demo demonstrates user interaction and shows how to customize it. The demo also presents a way to create new rows and columns via drag’n’drop gestures.
Automatic Layout
Automatic layout for a diagram containing table structures that are modeled using ITable implementations can be conveniently invoked using LayoutExecutor which performs all necessary setup. Internally, LayoutExecutor uses the TableLayoutConfigurator class.
const graphComponent = getMyGraphComponent()
// Configure HierarchicLayout.
const hl = new HierarchicLayout()
hl.componentLayoutEnabled = false
hl.layoutOrientation = LayoutOrientation.LEFT_TO_RIGHT
hl.orthogonalRouting = true
hl.recursiveGroupLayering = false
// Start layout.
try {
const layoutExecutor = new LayoutExecutor({
graphComponent,
layout: hl,
duration: '0.5s',
updateContentRect: true
})
layoutExecutor.tableLayoutConfigurator.compaction = false
layoutExecutor.start()
} catch (e) {
console.log('Layout failed')
}
TableLayoutConfigurator converts relevant parts of the table structure into PartitionGrid-based information that is understood by algorithms which support automatic tabular layout. In particular, this information includes:
- the geometric information of all rows and columns, i.e., the top and bottom borders, left and right borders, and insets, and also the
- row and column that each content node belongs to.
Note that the row and column is determined geometrically, by the node’s center coordinates.
Tutorial Demo Code
The following tutorial demo applications show how to create table models using the Table class and how to use the TableNodeStyle node style implementation for tabular data representation:
- The Table Editor demo demonstrates user interaction and shows how to customize it. The demo also presents a way to create new rows and columns via drag’n’drop gestures.