Coordinate systems
A diagram needs to both store information about itself, and then display it to the user. These needs correspond to the concept of a document and viewport.
The document contains the (document) coordinates of every GraphObject and is represented by a rectangle that contains all Nodes and Links, plus some padding. The document's bounds only change when the Diagram content changes.
The viewport is effectively the canvas that we view the document (and thus, diagram) through. It contains the coordinates of every GraphObject relative to the top left corner of the canvas. These coordinates are called viewport coordinates, and a node's viewport coordinates change when a user scrolls or zooms into or out of a diagram.
This example shows the relationship between the document and the viewport. Pan or zoom the diagram on the right and see the Diagram.viewportBounds, represented by the red rectangle, update on the left. The dashed border represents the Diagram.documentBounds. All coordinates at the top pertain to the diagram on the right.
This example does not use Overviews. For a more robust version of this example, see Overviews.
Here are a few things to try:
- When panning while the viewport is smaller than the document, the viewport cannot leave the document bounds, and vice versa when the viewport is larger.
- Zoom in or out very far and move your cursor slowly on the main diagram, and note the differing rates at which the viewport and document coordinates change. This is because zooming alters Diagram.scale, meaning that one "pixel" of the document may be represented by several pixels in the viewport, and vice versa.
This example illustrates a few important points:
- The viewport's (0, 0) is always the top-left of the canvas. The document's (0, 0) is usually close to the top-left-most node, but all sorts of things, like Diagram.padding, can change that.
- The top-left of the viewport is always at Diagram.position in document coordinates. But, the top-left of the document has no special relationship to the viewport. This lends to the idea that the document "owns" the viewport.
- Viewport coordinates are determined entirely by your cursor's position, Diagram.position, and Diagram.scale.
As well as a few less important but still notable points:
- If you align the viewport with the top left of the document bounds, you'll see that the Diagram.position is (-5, -5). This is because of the default Diagram.padding.
- Each of these (and all) coordinate systems use Points with increasing values of X going rightwards and of Y going downwards.
- The viewport does include the areas occupied by the diagram's scrollbars.
- To hide scrollbars, set Diagram.hasHorizontalScrollbar and/or Diagram.hasVerticalScrollbar to false.
To "move" a node one must change its GraphObject.position or Part.location in document coordinates. To "scroll" a diagram one must change the Diagram.position. Either way will cause a node to appear at a different point in the viewport.
If you want to easily programmatically convert between these systems, you can use Diagram.transformDocToView and Diagram.transformViewToDoc. But, most coordinates are in document or Panel Coordinates; Panels inside Parts have their own coordinate systems that their elements use.
Document bounds
In the previous example, the document bounds are represented by a dashed rectangle.
All of the Parts of a diagram have positions and sizes (their GraphObject.actualBounds) in document coordinates. The union of all of those parts' actualBounds constitutes the Diagram.documentBounds. If all of the parts are close together, the document bounds might be small. If some or all of the parts are far apart from each other, the document bounds might be large, even if there are only two parts or if there is just one really large part.
The Diagram.documentBounds value is independent of the Diagram.viewportBounds. The former only depends on the bounds of the parts; the latter only depends on the size of the canvas and the diagram's position and scale.
Diagram.computeBounds, which is responsible for the bounds computation, also adds the Diagram.padding Margin, so that no Parts appear directly up against the edge of the diagram when scrolled to that side. You may want to keep some parts, particularly background decorations, from being included in the document bounds computation. Just set Part.isInDocumentBounds to false for such parts.
The diagram does not compute a new value for Diagram.documentBounds immediately upon any change to any part or the addition or removal of a part. Thus the Diagram.documentBounds property value may not be up-to-date until after a transaction completes.
The relative sizes of the Diagram.documentBounds and Diagram.viewportBounds control whether or not scrollbars are needed. You can set Diagram.hasHorizontalScrollbar and/or Diagram.hasVerticalScrollbar to false to make sure no scrollbar appears even when needed. Or set Diagram.scrollMode to ScrollMode.Infinite.
If you do not want the Diagram.documentBounds to always reflect the sizes and locations of all of the nodes and links, you can set the Diagram.fixedBounds property. However if there are any nodes that are located beyond the fixedBounds, the user may be unable to scroll the diagram to see them.
If you want to be notified whenever the document bounds changes, you can register a "DocumentBoundsChanged" DiagramEvent listener.
Viewport bounds
In the previous example, the viewport bounds of the right diagram is represented by the red rectangle. The left diagram's viewport bounds is not labeled, but is the entire white rectangle.
The Diagram.viewportBounds always has x and y values that are given by Diagram.position. It always has width and height values that are computed from the canvas size and the Diagram.scale. In the previous example, you can see the viewportBounds' width and height by hovering your mouse over the bottom-right corner of the viewport (the red rectangle).
Users can scroll the document contents using keyboard commands, scrollbars or panning. Programmatically, you can scroll using several means:
Diagram.position = PointDiagram.scrollToRect(Rect)Diagram.centerRect(Rect)Diagram.scroll(unit, dir)Diagram.alignDocument(documentSpot, viewportSpot)Diagram.contentAlignment = SpotCommandHandler.scrollToPart(part)
Furthermore, scrolling may happen automatically as nodes or links are added to or removed from or change visibility in the diagram. Also, zooming will typically result in scrolling as well.
When scrolling, the Diagram.position normally will be limited to the range specified by the Diagram.documentBounds. The short or "line" scrolling distance is controlled by Diagram.scrollHorizontalLineChange and Diagram.scrollVerticalLineChange. The long or "page" scrolling distance is controlled by the size of the viewport. If you want to control the precise values that the Diagram.position may have, you can specify a Diagram.positionComputation function. See the example below.
A user can zoom in or out using keyboard commands, mouse wheel, or pinching. Programmatically, you can zoom using several means:
Diagram.scale = numberDiagram.zoomToFit()Diagram.zoomToRect(Rect)Diagram.autoScale = AutoScaleCommandHandler.decreaseZoom()CommandHandler.increaseZoom()CommandHandler.resetZoom()CommandHandler.zoomToFit()
When zooming in or out, the Diagram.scale normally will be limited to the range given by Diagram.minScale and Diagram.maxScale. If you want to control the precise values that the Diagram.scale may have, you can specify a Diagram.scaleComputation function. See the example below.
If you want to be notified whenever the viewport bounds changes, you can register a "ViewportBoundsChanged" DiagramEvent listener.
If you want a Part to always be displayed within the viewport, no matter how the user scrolls or zooms, you can add them to a Layer that has Layer.isViewportAligned set to true, such as the "ViewportBackground" layer. Such Parts are positioned by the GraphObject.alignment and GraphObject.alignmentFocus properties rather than by the GraphObject.position or Part.location properties. Read more at Static Parts.
Scroll margin
Diagram.scrollMargin allows the user to scroll into empty space at the edges of the viewport, beyond the document bounds (including its Diagram.padding margin) when it is greater than the viewport bounds. This can be useful when users need extra space at the edges of a Diagram, for instance to have an area to create new nodes with the ClickCreatingTool.
Diagram.padding is added as if part of the document bounds, whereas Diagram.scrollMargin makes sure you can scroll to empty space beyond the document bounds. Because of this, Diagram.scrollMargin does not create additional scrollable empty space if none is needed to scroll the margin distance beyond, such as when the document bounds are very small in the viewport.
Below is a Diagram with Diagram.scrollMargin set to 100.
As you drag to the boundary, you will find the additional space created by the margin.
This example also uses the Diagram.nodeTemplateMap for multiple node templates in one diagram.
Scrolling modes
Diagram.scrollMode allows the user to either scroll to document bound borders with ScrollMode.Document (the default), or scroll endlessly with ScrollMode.Infinite.
Diagram.positionComputation and Diagram.scaleComputation allow you to determine what positions and scales are acceptable to be scrolled to or zoomed to. For instance, you could allow only integer position values, or only allow scaling to the values of 0.5, 1, or 2.
The Scroll Modes sample displays all the code for the example below, which lets you toggle these three properties.
This example uses GraphObject.bindObject for link colors that are based off their Link.fromNode.
Panel coordinates
A GraphObject that is not a Part but is an element of a Panel has measurements that are in panel coordinates, not in document coordinates. That means that GraphObject.position, GraphObject.actualBounds, GraphObject.maxSize, GraphObject.minSize, GraphObject.measuredBounds, GraphObject.margin, and RowColumnDefinition properties apply to all elements of a panel using the same coordinate system.
Some GraphObject properties use units that have values before they are transformed for use by the containing Panel's coordinate system. In particular, GraphObject.desiredSize (which means GraphObject.width and GraphObject.height), GraphObject.naturalBounds, Shape.geometry, and Shape.strokeWidth are in "local" coordinates, before the object is scaled and rotated by the value of GraphObject.scale and GraphObject.angle.
GraphObject.actualBounds will tell you the position and size of an element within its panel. If you want to get the document position of some object that is within a Node, call GraphObject.getDocumentPoint.
For examples of the sizes of elements in a panel, see Sizing GraphObjects.
Nested Panel coordinates
The TextBlock that is "Bottom" has the default GraphObject.angle of zero, so that the text is drawn upright. But that TextBlock is an element in the green "Spot" Panel whose GraphObject.angle to 30, so it and its text should appear somewhat tilted. However the blue "Vertical" Panel itself has an GraphObject.angle of 165. Because each Panel has its own coordinate system and because transformations on nested elements are compounded, the effective angle for the green Panel is 195 degrees, the sum of those individual angles (30 + 165), which is nearly upside down.
The GraphObject.scale property also affects how an object is sized in its container Panel. The brown "Position" Panel has a scale of 0.8 relative to its container. But because the "Vertical" Panel has a scale of 1.5, its effective scale is 1.2 overall, the product of those individual scales (0.8 x 1.5).