Skip to content
  • Text that overflows its container - solving CSS spacing issue

    GoJS
    1
    0 Votes
    1 Posts
    35 Views
    No one has replied
  • Accessibility in diagrams (GoJS)

    GoJS
    2
    1 Votes
    2 Posts
    43 Views
    D
    Navigating a Diagram Using the Keyboard ⌨️

    In the previous post, we were able to add nodes to the diagram using the keyboard. Now, we will focus on navigating the diagram using the keyboard to move elements and switch between them efficiently.

    Built-in Solution 🎉

    GoJS provides a built-in solution for navigating between diagram elements, including a tree graph approach where you can move between parents, siblings, and children: AriaCommandHandler.

    This is an extension of CommandHandler that captures keyboard events and modifies selection accordingly.

    Custom Solution and Explanation 🤓 Introduction

    For one of my applications, the built-in solution was not sufficient or perfectly suited, so I decided to write my own "AriaCommandHandler" with custom navigation logic, taking inspiration from GoJS's approach (another reason was educational—I wanted to understand how it works 😉). I will explain what happens and how, so you can also tailor navigation to your needs.

    Implementation

    First, we need to determine which keys will be used for navigating between nodes and which for moving elements.

    Let's establish that selection changes will occur using the WSAD keys, and movement will require holding the alt key.

    type Direction = 0 | 90 | 180 | 270; export class AriaCommandHandler extends CommandHandler { // Define keys with direction which will be used later private keyDirectionMap: Record<string, Direction> = { D: 0, S: 90, A: 180, W: 270, }; doKeyDown(): void { const e: go.InputEvent = this.diagram.lastInput; // If one of WSAD clicked, go to custom logic. if (e.key in this.keyDirectionMap) { // If alt key is held, move element if (e.alt) { this._moveSelection(e.key as keyof typeof this.keyDirectionMap); } else { // Else, change selection this._changeFocus(e.key as keyof typeof this.keyDirectionMap); } } else { // Otherwise, fall back to base implementation super.doKeyDown(); } } }

    In the above implementation, the appropriate keys are already handled, so now we just need to write the logic.
    Let's start with moving elements, as this part is simpler.

    _moveSelection(key: keyof typeof this.keyDirectionMap) { const direction = this.keyDirectionMap[key]; const originalPart = this.diagram.selection.first(); if (!originalPart) { return; } const { x, y } = originalPart.location; const { width, height } = this.diagram.grid.gridCellSize; // Depending on direction, move element by grid cell size if (direction === 0) { originalPart.moveTo(x + width, y); } else if (direction === 90) { originalPart.moveTo(x, y + height); } else if (direction === 180) { originalPart.moveTo(x - width, y); } else if (direction === 270) { originalPart.moveTo(x, y - height); } }

    The above logic simply moves the node by the appropriate distance in the correct direction. Additionally, it uses a grid, so the movement aligns with the grid cell size.

    Now, let's move on to changing the selection.

    _changeFocus(key: keyof typeof this.keyDirectionMap) { const direction = this.keyDirectionMap[key]; const closestNode = this._findClosestNode(direction); closestNode && this.diagram.select(closestNode); }

    The _changeFocus method retrieves the direction and searches for the nearest node. If it finds one, it selects it using this.diagram.select. Now, let's look at the _findClosestNode method, which is key to determining which node to select next.

    _findClosestNode(direction: Direction) { const originalPart = this.diagram.selection.first(); // If no nodes are selected, skip searching if (originalPart === null) { return null; } const allParts: go.Part[] = []; // Extract all visible nodes and convert them to an array this.diagram.nodes.each((x) => x.isVisible() && allParts.push(x)); // The point from which we search for the nearest node is the center of the current node const originalPoint = originalPart.actualBounds.center; let closestDistance = Infinity; let closest = originalPart; // Iterate through all nodes for (const nextPart of allParts) { // Skip the currently selected node and those that cannot be selected if (nextPart === originalPart || !nextPart.canSelect()) { continue; } // Use the center point of other nodes to determine distances const nextPoint = nextPart.actualBounds.center; const distance = this._calculateDistance(originalPoint, nextPoint, direction); // If the distance is smaller than the last smallest, define a new target node if (distance < closestDistance) { closestDistance = distance; closest = nextPart; } } // If the closest is still the current node, return null return closest === originalPart ? null : closest; }

    This logic is fairly straightforward. However, the key part is the _calculateDistance method.
    At first glance, calculating the distance between two points may seem simple, but in this case, it's more complex. If there are three nodes in a line (First, Second, and Third), where First is far to the left of Second (which is selected), and Third is far to the right, pressing A should select First, even though it may not be the closest node. This is because we are considering direction.

    To handle this, we use Direction, which we compare with the angles between nodes.
    We use directionPoint to get the angle between two points on a coordinate plane.

    _calculateDistance(originalPoint: go.Point, nextPoint: go.Point, direction: Direction) { const angleDiff = this._angleCloseness( originalPoint.directionPoint(nextPoint), direction ); ... } _angleCloseness(angle: number, direction: number) { // Return the actual difference between angles return Math.min( Math.abs(direction - angle), Math.min( Math.abs(direction + 360 - angle), Math.abs(direction - 360 - angle) ) ); }

    Using _angleCloseness, we get the difference in angles between two points.
    Now, we need to calculate the actual distance and combine it with angle difference information.
    We will use distanceSquaredPoint, which returns the squared distance between two points.
    To determine the next node, we apply a penalty based on the angle difference—the greater the difference, the greater the penalty.
    We also set a maximum angle of 60 degrees. If the angle difference exceeds this, the penalty is Infinity, and the node is skipped.

    _calculateDistance(originalPoint: go.Point, nextPoint: go.Point, direction: Direction) { const angleDiff = this._angleCloseness(originalPoint.directionPoint(nextPoint), direction); const penalty = angleDiff > 60 ? Infinity : angleDiff / 8; return ( originalPoint.distanceSquaredPoint(nextPoint) * (1 + Math.sin((angleDiff * Math.PI) / 180) * penalty) ); }

    Thanks to this logic, we can accurately determine which node the user wants to navigate to using WSAD. Some values result from experimenting with different options.

    Summary

    This solution is just one of many ways to implement keyboard navigation for a diagram. The best approach depends on project needs and diagram structure. If nodes vary in size, choosing their center points may not be ideal. Additionally, selecting links may be a factor.

    A useful addition would be Tab and Shift+Tab navigation to cycle through nodes, as WSAD navigation might sometimes be blocked by other nodes.

  • Workflow Builder

    Projects
    3
    0 Votes
    3 Posts
    89 Views
    PMarcinP

    In last week we released 1.1 version.
    Direct link: https://app.workflowbuilder.io/

  • Proper Copy-Pasting of Disconnected Links in GoJS

    GoJS
    1
    2 Votes
    1 Posts
    38 Views
    No one has replied
  • Leveraging AbortController for GoJS Integration

    GoJS
    1
    1 Votes
    1 Posts
    14 Views
    No one has replied
  • The ultimate guide to optimize React Flow project performance

    ReactFlow
    1
    3 Votes
    1 Posts
    681 Views
    No one has replied
  • Panel Auto truncating content

    GoJS
    1
    4 Votes
    1 Posts
    37 Views
    No one has replied
  • Introduce yourself

    Community
    9
    0 Votes
    9 Posts
    156 Views
    D

    Hi all! 👋
    My name is Mateusz, and I’m a Software Developer with over 6 years of experience working in data visualization and diagramming at Synergy Codes.
    Feel free to reach out if you have questions, need advice, or want to collaborate. 😁

  • Report: AI in data visualization

    Machine Learning
    1
    0 Votes
    1 Posts
    56 Views
    No one has replied
  • GoJS E-book

    Pinned GoJS
    1
    0 Votes
    1 Posts
    32 Views
    No one has replied
  • GoJS Course thread

    GoJS Course
    1
    0 Votes
    1 Posts
    32 Views
    No one has replied
  • Feedback and Questions

    Community
    1
    0 Votes
    1 Posts
    26 Views
    No one has replied
  • Off Topic

    Community
    1
    0 Votes
    1 Posts
    17 Views
    No one has replied
  • Homework PR

    GoJS Course
    1
    0 Votes
    1 Posts
    21 Views
    No one has replied
  • GoJS Chrome Extension

    Pinned Moved GoJS
    1
    0 Votes
    1 Posts
    39 Views
    No one has replied
  • Vislabs

    Projects
    1
    0 Votes
    1 Posts
    30 Views
    No one has replied
  • dAIgram App

    Projects
    1
    0 Votes
    1 Posts
    24 Views
    No one has replied
  • Graph Editor

    Projects
    2
    0 Votes
    2 Posts
    21 Views
    No one has replied
  • Code of Conduct and Community Rules

    Moved Community
    1
    0 Votes
    1 Posts
    38 Views
    No one has replied