documentationfor yFiles for HTML 2.6

Using Projections

A GraphComponent displays a section of the graph defined by the viewport. Per default this viewport is defined by a combination of translation (the viewPoint) and scaling (the zoom).

In addition it is possible to set a further affine transformation that is applied to the rendering of the GraphComponent. Typically, this is used for visually representing graph elements as three-dimensional objects, for example in isometric renderings. Using projections is fully compatible with most other functionality of yFiles for HTML, such as user interaction (including orthogonal edge editing and snapping) or layout calculation.

After setting a projection, graph elements will appear to be flatly projected onto the resulting plane. To use this feature in combination with a custom style, see the Isometric Drawing demo on how to render nodes to appear three-dimensional.

Isometric projection using a custom isometric node style, as well as a grid
p isometricsamplestyle

The additional transformation is simply set as CanvasComponent.projection property, e.g. to the predefined Matrix.ISOMETRIC projection.

// Setting the projection to one of the standard projections
graphComponent.projection = Matrix.ISOMETRIC

The projection can be removed by setting it to IDENTITY again:

// Resetting the projection
graphComponent.projection = Matrix.IDENTITY

You can also set a custom projection

// Setting a custom projection
const projection = new Matrix()
projection.scale(1, 0.577)
projection.rotate(Math.PI / 4)
graphComponent.projection = projection

Understanding Projections

yFiles for HTML uses three distinct coordinate systems.

World coordinate system
This is the coordinate system in which the position and size of graph objects are specified.
Intermediate coordinate system
This is the coordinate system in which view point and zoom have been applied. This coordinate system is useful for drawing decorations independent of zoom level. Selection and highlight decorations are drawn in this coordinate system, for example.
View coordinate system
This is the coordinate system in which view point, zoom, and projection have been applied. The point (0,0) in this coordinate system lies in the upper left corner of the control and lengths correspond to distances in pixels on the screen. Typically, a watermark or legend would be drawn in this coordinate system.

By default, the projection is set to IDENTITY, so the intermediate and view coordinate systems are identical. Once a different projection has been set, this is no longer the case.

Coordinate system comparison for an isometric projection
World coordinate system, with node at (100,100)
View coordinate system, with node at roughly (width*0.5, height*0.8) in view coordinates

To transform a point between those coordinate systems, the following methods are available:

To transform directly from world to view coordinates and vice versa, use the more common toWorldCoordinates(Point) and toViewCoordinates(Point) on CanvasComponent, as described in Managing the View

In addition, the transformation matrices and a subset of above transformation methods are also available on the IRenderContext instances. When implementing visualizations, the methods on IRenderContext must be used, if possible.

Drawing Elements in World Coordinates

Drawing in world coordinates is the default behaviour and requires no further setup. See Basic Style Implementation for a general introduction into drawing with yFiles for HTML.

Drawing Elements in Intermediate Coordinates

Drawing in intermediate coordinates can be useful to render visualizations that must be zoom-independent. A common example for this kind of drawing are handles. Handles are anchored to a location in the world coordinate system (e.g. a bend on an edge, or a corner of a node), but zooming does not change their size. yFiles for HTML handles this internally, but it is also possible to draw arbitrary visuals independent of the zoom level.

In the following example a line is drawn with constant thickness, independent of the current zoom level:

Zoom-Independant drawing of Visuals
Zoomed out
Zoomed in, note that unlike the rulers, the blue line is drawn in intermediate coordinates.

Note that the start and end point are transformed into the correct coordinate system. Without this conversion, the line would move when the viewport is moved since the Intermediate coordinate system already includes the viewport transformation.

/**
 * @param {!IRenderContext} context
 * @returns {!SvgVisualGroup}
 */
createVisual(context) {
  // create a visual group
  const zoomIndependentGroup = new SvgVisualGroup()
  // configure the visual group to be rendered in intermediate coordinates
  zoomIndependentGroup.transform = context.intermediateTransform

  // create a line from world point (20,20) to world point (75,20)
  // since the group is rendered in intermediate coordinates, start and end of the line have to be transformed
  const start = context.canvasComponent.worldToIntermediateCoordinates(new Point(20, 20))
  const end = context.canvasComponent.worldToIntermediateCoordinates(new Point(75, 20))
  const line = document.createElementNS('http://www.w3.org/2000/svg', 'line')
  line.x1.baseVal.value = start.x
  line.y1.baseVal.value = start.y
  line.x2.baseVal.value = end.x
  line.y2.baseVal.value = end.y
  Stroke.setStroke(Stroke.DODGER_BLUE, line, context)
  zoomIndependentGroup.add(new SvgVisual(line))
  return zoomIndependentGroup
}
createVisual(context: IRenderContext): SvgVisualGroup {
  // create a visual group
  const zoomIndependentGroup = new SvgVisualGroup()
  // configure the visual group to be rendered in intermediate coordinates
  zoomIndependentGroup.transform = context.intermediateTransform

  // create a line from world point (20,20) to world point (75,20)
  // since the group is rendered in intermediate coordinates, start and end of the line have to be transformed
  const start = context.canvasComponent!.worldToIntermediateCoordinates(new Point(20, 20))
  const end = context.canvasComponent!.worldToIntermediateCoordinates(new Point(75, 20))
  const line = document.createElementNS('http://www.w3.org/2000/svg', 'line')
  line.x1.baseVal.value = start.x
  line.y1.baseVal.value = start.y
  line.x2.baseVal.value = end.x
  line.y2.baseVal.value = end.y
  Stroke.setStroke(Stroke.DODGER_BLUE, line, context)
  zoomIndependentGroup.add(new SvgVisual(line))
  return zoomIndependentGroup
}

Drawing Elements in View Coordinates

Drawing Custom Visuals in View Coordinates describes rendering arbitrary visualizations in view coordinates but there are already several default user interface visuals that can be configured to be drawn in view or intermediate coordinates:

Marquee Rectangle

Drawing the marquee rectangle in view coordinates is the typical experience users expect. Still, it can be beneficial to use intermediate coordinates either, for example when selecting rows in a hierarchically layouted graph. Use the useViewCoordinates property to switch between the two behaviors.

// gim is a GraphEditorInputMode or GraphViewerInputMode
gim.marqueeSelectionInputMode.useViewCoordinates = false
Using view vs. intermediate coordinates for the selection rectangle
Intermediate coordinates
View coordinates (default)

Edge Selection

The default edge selection uses a checkboard pattern for the edge path as well as little diamonds for the bends. Therefore using intermediate or view coordinates not only differs in how the pattern of the edge path looks like but also in the projection of the bend markers. Use the useViewCoordinates property to switch between the two behaviors.

const viewEdgeSelection = new EdgeSelectionIndicatorInstaller()
viewEdgeSelection.useViewCoordinates = true
graphComponent.graph.decorator.edgeDecorator.selectionDecorator.setImplementation(viewEdgeSelection)
Using intermediate vs. view coordinates for selection decoration
Intermediate coordinates
View coordinates (default)

Handles

Handles are drawn in intermediate coordinates by default, to make them look as if they reside in the same plane as the graph elements. They can be drawn in view coordinates as well with the HandleInputMode.useViewCoordinates property. This is useful for projections that include more extreme scaling factors, making handles hard to see.

// geim is a GraphEditorInputMode
geim.handleInputMode.useViewCoordinates = true
Using intermediate vs. view coordinates for handles
Intermediate coordinates (default)
View coordinates

Port Candidates

The DefaultPortCandidateDescriptor for the port candidates in general and for the closest port can be set separately:

// geim is a GraphEditorInputMode
const candidateDescriptor = new DefaultPortCandidateDescriptor()
candidateDescriptor.useViewCoordinates = true
geim.createEdgeInputMode.candidateDescriptor = candidateDescriptor

const closestCandidateDescriptor = new DefaultPortCandidateDescriptor()
closestCandidateDescriptor.currentCandidate = true
closestCandidateDescriptor.size = 5
closestCandidateDescriptor.useViewCoordinates = true
geim.createEdgeInputMode.closestCandidateDescriptor = closestCandidateDescriptor
Using intermediate vs. view coordinates for port candidates
Intermediate coordinates (default)
View coordinates

Arbitrary Rectangles

The RectangleIndicatorInstaller can be used in combination with Decorators to easily draw focus, selection, or highlight rectangles independent of the current projection.

graphComponent.graph.decorator.nodeDecorator.selectionDecorator.setFactory((node) => {
  const rectangleIndicatorInstaller = new RectangleIndicatorInstaller(node.layout)
  rectangleIndicatorInstaller.useViewCoordinates = true
  return rectangleIndicatorInstaller
})
Using intermediate vs. view coordinates for the node’s selection rectangle
p rectangle indicator

Drawing Custom Visuals in View Coordinates

Arbitrary visuals can be drawn onto the GraphComponent in view coordinates as well. Setting the viewTransform as SvgVisualGroup.transform will cause it to be drawn as if hovering above the Control, much like a watermark. In this coordinate system the point (0,0) is the upper left corner and (width,height) is the lower right corner. Note that the circle itself is completely unaffected by the transform as well — it is not deformed by the projection.

/**
 * @param {!IRenderContext} context
 * @returns {!SvgVisualGroup}
 */
createVisual(context) {
  const viewGroup = new SvgVisualGroup()
  // set the VisualGroup's transform to ViewCoordinates
  viewGroup.transform = context.viewTransform

  // create a circle and set its position to the lower right of the control
  const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle')
  Fill.setFill(Fill.DODGER_BLUE, circle, context)
  const position = new Point(context.canvasComponent.size.width - 20, context.canvasComponent.size.height - 20)
  circle.cx.baseVal.value = position.x
  circle.cy.baseVal.value = position.x
  circle.r.baseVal.value = 20
  viewGroup.add(new SvgVisual(circle))
  return viewGroup
}
createVisual(context: IRenderContext): SvgVisualGroup {
  const viewGroup = new SvgVisualGroup()
  // set the VisualGroup's transform to ViewCoordinates
  viewGroup.transform = context.viewTransform

  // create a circle and set its position to the lower right of the control
  const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle')
  Fill.setFill(Fill.DODGER_BLUE, circle, context)
  const position = new Point(context.canvasComponent!.size.width - 20, context.canvasComponent!.size.height - 20)
  circle.cx.baseVal.value = position.x
  circle.cy.baseVal.value = position.x
  circle.r.baseVal.value = 20
  viewGroup.add(new SvgVisual(circle))
  return viewGroup
}
Drawing in View Coordinates
The blue circle is always drawn in the lower right corner, irrespective of viewport movement or projection

Interacting with projected Graphs

Since the standard events are using world coordinates, it generally "just works". However, depending on the projection used, some considerations need to be taken into account.

Keyboard Navigation

Shift ⇧+arrow keys and Ctrl+arrow keys focus and select a node in the respective direction. Which node is considered to be the next node in this direction can be determined either in world coordinates (default) or in view coordinates. For example, if the projection is horizontally mirroring the graph, keyboard navigation would seem to be horizontally inverted in world coordinates.

// gim is a GraphEditorInputMode or GraphViewerInputMode
// set keyboard navigation to consider where nodes appear on screen instead of
// what their location in world coordinates is
gim.navigationInputMode.useViewCoordinates = true
Keyboard Navigation
In world coordinates, the blue node is to the right of the orange node. In view coordinates, it is the green one.

Scrollbars and Content Fitting

Without a projection, the contentRect property, a rectangle in world coordinates, is used for scrollbar calculations as well as adjusting the viewport via fitContent and fitGraphBounds. When a projection is set, the scrollable area on the other hand is computed automatically from all visible graph elements in view coordinates and can not be set manually.

fitContent will cause the viewport to enclose the bounding box of the contentRect. This area may be substatially larger than the graph, depending on how it appears in the projection. Any contentMargins will be applied to this bounding box. To fit the graph tightly to the visible area, use fitGraphBounds instead.

Updates of the scrollable area are triggered whenever the content rectangle is updated, such as when graph elements are added interactively. When programmatically adding or removing elements updateContentRect needs to be called to update the scrollable area as well.

Content rectangle and scrollable area
The content rectangle (blue), it’s bounding rectangle in view coordinates (green), and the visible area used for scrollbar calculations (red)

Image Export

Image export works as described in section Image Export. With a projection set, however, the worldBounds rectangle is no longer parallel to the image’s coordinate system axes. In this case a bounding rectangle is chosen to completely encompass the worldBounds and the entire content is guaranteed to be visible. However, this may create large portions of unnecessary whitespace.

Exporting images with world bounds set
World bounds given in world coordinates
The exported image

An axis-parallel rectangle in view coordinates is no longer a rectangle in world coordinates. Rather, it is a polygon whose points can be set by worldPoints. The following example shows how the marquee rectangle can be computed to set the world points:

// Create a new marquee input mode (despite the name it will only allow to drag a marquee, not select anything)
const marqueeMode = new MarqueeSelectionInputMode({
  // Ensure that the marquee is rectangular in view coordinates
  useViewCoordinates: true
})
// Add to the GraphEditorInputMode instance
graphEditorInputMode.add(marqueeMode)

marqueeMode.addDragFinishedListener(async (sender, evt) => {
  // Collect the corner points of the rectangle in world coordinates
  const points = []
  for (const cursor = evt.path.createCursor(); cursor.moveNext(); ) {
    if (cursor.pathType != PathType.CLOSE) {
      points.push(cursor.currentEndPoint)
    }
  }

  // Pass the points along with the correct projection to the SVG Export
  const svgExport = new SvgExport({
    worldBounds: Rect.EMPTY, // This is just a dummy rectangle that's overwritten by the following points anyway
    worldPoints: points,
    projection: graphComponent.projection
  })

  // Export the image
  const svg = await svgExport.exportSvgAsync(graphComponent)
  downloadFile(SvgExport.exportSvgString(svg), 'image.svg')
})

// Create a new marquee input mode (despite the name it will only allow to drag a marquee, not select anything)
const marqueeMode = new MarqueeSelectionInputMode({
  // Ensure that the marquee is rectangular in view coordinates
  useViewCoordinates: true
})
// Add to the GraphEditorInputMode instance
graphEditorInputMode.add(marqueeMode)

marqueeMode.addDragFinishedListener(async (sender, evt) => {
  // Collect the corner points of the rectangle in world coordinates
  const points: Point[] = []
  for (const cursor = evt.path.createCursor(); cursor.moveNext(); ) {
    if (cursor.pathType != PathType.CLOSE) {
      points.push(cursor.currentEndPoint)
    }
  }

  // Pass the points along with the correct projection to the SVG Export
  const svgExport = new SvgExport({
    worldBounds: Rect.EMPTY, // This is just a dummy rectangle that's overwritten by the following points anyway
    worldPoints: points,
    projection: graphComponent.projection
  })

  // Export the image
  const svg = await svgExport.exportSvgAsync(graphComponent)
  downloadFile(SvgExport.exportSvgString(svg), 'image.svg')
})

Exporting images with world points set
The world points set in world coordinates form a rectangle in screen coordinates
The exported image

Printing

Printing works as described in section Printing. With a projection set, however, the print rectangle is no longer axis parallel to the image’s coordinate system. In this case a bounding rectangle is chosen to completely encompass the print rectangle so that the entire content is guaranteed to be visible. However, this may create large portions of unnecessary whitespace.

Printing the graph with world bounds set
The world bounds in the canvas
The printed image

An axis-parallel rectangle in view coordinates is no longer a rectangle in world coordinates. Rather, it is a polygon whose points can be set by world points. The following example shows how the smallest possible bounds around the contents can be computed to set the world points:

// start with an empty rectangle as bounds
let bounds = Rect.EMPTY

// transform the coordinates into a coordinate system
// which is axis-parallel to the printing paper
const matrix = graphComponent.projection.clone()

for (const co of graphComponent.getCanvasObjects(graphComponent.contentGroup)) {
  // for each canvas object get the bounds
  const b = co.descriptor.getBoundsProvider(co.userObject).getBounds(graphComponent.canvasContext)
  if (b.isFinite) {
    // transform all corners into projected coordinates
    // and add to the bounds
    bounds = bounds.add(matrix.transform(b.topLeft))
    bounds = bounds.add(matrix.transform(b.topRight))
    bounds = bounds.add(matrix.transform(b.bottomLeft))
    bounds = bounds.add(matrix.transform(b.bottomRight))
  }
}
// now bounds is the smallest rectangle which encloses all objects
// and which is axis parallel to the paper
matrix.invert()
// now transform the corners of the bounds back into the world coordinate system
const points = [
  matrix.transform(bounds.topLeft),
  matrix.transform(bounds.topRight),
  matrix.transform(bounds.bottomRight),
  matrix.transform(bounds.bottomLeft)
]

const printSupport = new PrintingSupport()
printSupport.print(graphComponent, points)

Printing only the contents

Note that the PrintingSupport class used in the Printing demo already uses the smallest possible area of the canvas.

Unsupported Functionality