Skip to content
  • Categories
  • Recent
  • Tags
  • Popular
  • Users
  • Groups
Skins
  • Light
  • Cerulean
  • Cosmo
  • Flatly
  • Journal
  • Litera
  • Lumen
  • Lux
  • Materia
  • Minty
  • Morph
  • Pulse
  • Sandstone
  • Simplex
  • Sketchy
  • Spacelab
  • United
  • Yeti
  • Zephyr
  • Dark
  • Cyborg
  • Darkly
  • Quartz
  • Slate
  • Solar
  • Superhero
  • Vapor

  • Default (Cyborg)
  • No Skin
Collapse
Diagram Community Logo
W

Wojtek Krzesaj

@Wojtek Krzesaj
About
Posts
4
Topics
3
Groups
0
Followers
0
Following
0

Posts

Recent Best Controversial

  • Text that overflows its container - solving CSS spacing issue
    W Wojtek Krzesaj

    A subtle and often overlooked issue can arise when setting word-spacing or letter-spacing on the <canvas> element itself or one of its parent elements. This can cause the text drawn on the diagram to appear unexpectedly larger than intended.

    In this post, we'll explore this bug, its root cause, and how to work around it.

    The Bug: Growing Text on the Diagram

    You might encounter this issue when applying CSS styles like this:

    canvas {
      letter-spacing: 4px;
    }
    

    Or if a parent element of the <canvas> applies such styles:

    .parent-container {
      word-spacing: 4px;
    }
    

    Expected Behavior

    Normally, setting letter-spacing or word-spacing affects only regular HTML text elements (e.g., <p>, <span>, <div>). Since text inside a diagram or rather a <canvas> is drawn using JavaScript, we wouldn't expect it to be affected by CSS styles.

    Observed Behavior

    When letter-spacing or word-spacing is applied to the <canvas> or its parent, any text appears larger than expected. The scaling effect varies based on the spacing value and seems to grow proportionally.

    Why Does This Happen?

    The root cause of this issue lies in how the browser applies CSS styles before rendering. Although the <canvas> itself does not render text via the DOM, the browser includes inherited styles when computing layout.

    How to Reproduce the Bug

    To see this in action, try the following code:

    <!DOCTYPE html>
    <html lang="en">
      <body>
        <script src="https://cdn.jsdelivr.net/npm/gojs@3.0.19/release/go.js"></script>
        <div
          style="
            width: 100%;
            height: 100%;
            overflow: hidden;
            letter-spacing: 4px;
            padding: 0;
          "
        >
          <div
            id="diagram"
            style="
              border: solid 1px black;
              width: calc(100vw - 20px);
              height: calc(100vh - 20px);
            "
          ></div>
        </div>
    
        <script id="code">
          const diagram = new go.Diagram("diagram");
          const $ = go.GraphObject.make;
    
          diagram.nodeTemplate = $(
            go.Node,
            go.Node.Auto,
            $(
              go.Shape,
              {
                figure: "RoundedRectangle",
                strokeWidth: 0,
                fill: "black",
              },
              new go.Binding("fill", "color")
            ),
            $(
              go.TextBlock,
              {
                margin: 8,
                font: "bold 14px sans-serif",
                stroke: "#333",
                background: "yellow",
              },
              new go.Binding("text")
            )
          );
    
          diagram.model = new go.GraphLinksModel(
            [{ key: 1, text: "Alpha Text Simple Example", color: "lightblue" }],
            []
          );
        </script>
      </body>
    </html>
    

    The example was made using https://gojs.net/latest/samples/minimal.html as a starting point. You will notice that the text appears larger than expected. The picture below demonstrates the outcome.

    spacing-bug-img1.png

    Workarounds and Fixes

    To prevent this issue, you need to reset properties on the <canvas> or on the diagram container. If styles are applied at a higher level, override them specifically for the canvas or the diagram container:

     <div
            id="diagram"
            style="
              ...
              letter-spacing: normal;
              word-spacing: normal;
            "
          ></div>
    

    Conclusion

    While HTML <canvas> text rendering is generally isolated from CSS styles, certain properties like letter-spacing and word-spacing can unexpectedly affect it, leading to enlarged text. The issue can be detected in Chromium-based browsers, and if you attempt to run it on Firefox, everything appears flawless. Please make sure to check your solution for the most commonly used browsers 😄

    spacing-bug-img2-firefox.png

    Have you encountered this bug before? Let us know in the comments how you handled it!


  • Leveraging AbortController for GoJS Integration
    W Wojtek Krzesaj

    Have you ever forgotten to remove an event listener, or accidentally removed the wrong one? Maybe you’ve used an arrow function when registering a listener, only to realize later that you need the exact same reference to unregister it. These common pitfalls can lead to memory leaks and unexpected behaviour in your applications.

    Thankfully, AbortController provides a clean and efficient way to manage event listeners and cancel async operations. While it’s often associated with aborting fetch requests [1], it’s also a powerful tool for handling event listeners in a more controlled and maintainable way. Let’s explore how this concept can help us write better, cleaner code.

    ⚾ Web requests and Fetch

    Since AbortController is often introduced as a way to cancel web requests, let’s start with how it works alongside fetch.
    Typically, when making a request, you might write:

    const response = await fetch(url);
    

    However, this approach doesn’t provide a way to cancel the request if it’s no longer needed. For example, if a user navigates away or clicks a "Cancel" button, the request will still complete, potentially wasting resources.

    But here is where AbortController comes to the rescue. We can modify our code to be cancellable.

    const controller = new AbortController();
    const signal = controller.signal;
    const response = await fetch(url, { signal });
    

    Now, if we decide to cancel the request at any point, we can simply call:

    controller.abort();
    

    When abort() is called, the fetch request is immediately terminated, and the promise rejects with an AbortError. This is particularly useful in scenarios where a user cancels an action, switches pages, or when we want to prevent unnecessary network requests.

    ⚛️ Event listeners in React

    Another powerful use case for AbortController is managing event listeners more efficiently. You might find yourself often writing React code like this:

    useEffect(() => {
      window.addEventListener('resize', handleResize)
      window.addEventListener('hashchange', handleHashChange)
      window.addEventListener('storage', handleStorageChange)
    
      return () => {
        window.removeEventListener('resize', handleResize)
        window.removeEventListener('hashchange', handleHashChange)
        window.removeEventListener('storage', handleStorageChange)
      }
    }, [])
    

    While this approach works, manually removing each event listener can become tedious and error-prone, especially as the number of event listeners grows.
    Instead, we can simplify our cleanup logic by leveraging AbortController. Since addEventListener supports passing an AbortSignal as an option, we can register multiple event listeners and remove them all at once in the cleanup function:

    useEffect(() => {
      const controller = new AbortController();
      const { signal } = controller;
    
      window.addEventListener('resize', handleResize, { signal });
      window.addEventListener('hashchange', handleHashChange, { signal });
      window.addEventListener('storage', handleStorageChange, { signal });
    
      return () => controller.abort(); // Removes all listeners at once
    }, []);
    

    This small change makes event handling in React applications more manageable, especially when dealing with multiple listeners in components that frequently mount and unmount [2].

    🎉 AbortController with GoJS

    Managing window event listeners is one use case, but the same concept can be applied to the GoJS library. According to its documentation, we can register listeners for multiple different events in a similar way to how we handle window events [3].

    const listener = (e: go.DiagramEvent) => {
      const part = e.subject.part;
      if (!(part instanceof go.Link)) {
        console.log('Clicked on ' + part.data.text);
      }
    };
    diagram.addDiagramListener('ObjectSingleClicked', listener);
    

    In this example, clicking on a node logs a message to the console. To remove the event listener, we simply call:

    diagram.removeDiagramListener('ObjectSingleClicked', listener);
    

    However, when dealing with multiple diagram events and model change listeners, managing their removal can quickly become complex and difficult to track. Unfortunately, we can’t simply pass an AbortController signal as a third parameter like we do with window.addEventListener. However, we can adjust how we register our listeners to leverage the same technique. The key is to listen for the 'abort' event whenever we register a listener, allowing us to clean them up efficiently. Here's how it can be done:

    function makeAddDiagramEventListener(diagram: go.Diagram) {
      return (
        eventName: go.DiagramEventName,
        callback: go.DiagramEventHandler,
        options: { signal?: AbortSignal } = {}
      ) => {
        options.signal?.addEventListener("abort", () => {
          diagram.removeDiagramListener(eventName, callback);
        });
        return diagram.addDiagramListener(eventName, callback);
      };
    }
    

    Here, we define a higher-order function that first takes the diagram we’re working with. Inside, we register the event listener while also checking if an AbortSignal is provided. If a signal is available, we attach a listener to its abort event to ensure the diagram event listener is properly removed when aborted. Using this function, we can register a 'ObjectSingleClicked' event on our diagram like this:

    const controller = new AbortController();
    makeAddDiagramEventListener(diagram)('ObjectSingleClicked', listener, { signal: controller.signal });
    

    This way we can add multiple event listeners and be sure we remove them all by:

    controller.abort();
    

    The same logic can be applied when working with GoJS model change listeners.
    The higher-order method for mode changed listener could look like this:

    function makeAddModelChangedListener(diagram: go.Diagram) {
      return (
        callback: go.ChangedEventHandler,
        options: { signal?: AbortSignal } = {}
      ) => {
        options.signal?.addEventListener("abort", () => {
          diagram.removeModelChangedListener(callback);
        });
        return diagram.addModelChangedListener(callback);
      };
    }
    

    It's worth noticing that even if we add both diagram event listeners and model changed listeners we still remove them all by calling abort method on the Abort Controller.

    🎯 Conclusion

    By integrating AbortController, we can make our code more maintainable, scalable, and efficient, especially when dealing with multiple event listeners. Instead of manually tracking and removing listeners, we can leverage a single abort() call to clean everything up effortlessly.

    If you're working with GoJS, consider adopting AbortController to keep your codebase clean and prevent potential memory leaks. Small optimizations like this can significantly improve the maintainability and reliability of your applications.

    Happy coding! 🚀

    📃 Source

    1. https://developer.mozilla.org/en-/docs/Web/API/AbortController
    2. https://kettanaito.com/blog/dont-sleep-on-abort-controller
    3. https://gojs.net/latest/intro/events.html
    4. https://youtu.be/2sdXSczmvNc?feature=shared
    5. https://betterstack.com/community/guides/scaling-/understanding-abortcontroller/

  • Panel Auto truncating content
    W Wojtek Krzesaj

    🐛 Problem

    One of the most commonly used panel types in GoJS is the Auto panel. It adjusts its size according to the content.
    Adding go.Shape as first element to an Auto panel will result in the shape being used as a background for other elements added to the panel after it.
    Depending on the shape’s figure we use for the background, by setting the figure property, it changed the properties responsible for panel Auto positioning. These properties are spot1 and spot2, whose default values are the top left corner for spot1 and bottom right corner for spot2.
    An example of shape that would change these properties is RoundedRectangle, where the spot1 and spot2 properties will be compensated by the value parameter1 * 0.3 where parameter1 is equal to the border radius of the RoundedRectangle shape. This behaviour prevents content from extending beyond the shape's outline.
    Unfortunately, this leads to the situation of cutting of the content of the Auto panel, which tries to use all its space (responsiveness).

    💭 Example

    Let’s look at the example of such behaviour.
    A panel of type Auto with the light blue, rounded at the top, rectangle background, and inside a panel of type Table, with stretch set to go.Stretch.Horizontal, positioning the text. The Auto panel is placed inside a GoJS Node with desiredSize property defined.
    p0a.png
    At the picture above we can see a red parent Auto panel, blue background shape and a TextBlock with yellow background inside a Table panel.
    Since we set stretch to go.Stretch.Horizontal in order to stretch the Table panel to always use the entire available width of the parent panel we encounter a situation where our text is cut off.
    p1a.png
    And without extra background colours:
    p1b.png
    This is because the Table panel will have the same width as the parent panel (Auto), which in turn has compensated spot1 and spot2 properties by which the extended content of the Table panel will be truncated.

    🎯 Solution

    In order to fix this problem, we need to override the values of the properties responsible for content positioning in the Auto panel. In the properties of the panels background shape (go.Shape) we set values for spot1 and spot2 as follows:

    spot1: go.Spot.TopLeft,
    spot2: go.Spot.BottomRight,
    

    The result will be as follows:
    p2a.png
    p2b.png
    As we can see, the text is no longer cut off, however it sticks out beyond the rounded corners of the panels Auto background shape. Because of the setting spot1 and spot2 properties back to uncompensated values, we have to take care of the correct positioning of the panel content ourselves so that it does not stick out beyond the parent panel. To do this, we can use margins. We add a margin with the value new go.Margin(0, 10) to the Table panel to achieve the following result.
    p3a.png
    p3b.png
    This way we fixed the truncated text, and our Table panel is still responsive without the need to calculate its width manually 🎉


  • Introduce yourself
    W Wojtek Krzesaj

    Hello 👋

    I'm Wojtek and I'm a Software Developer at Synergy Codes with almost 4 years of experience in diagramming and GoJS.
    I'm here to connect, share insights, and collaborate so If you have ideas or need support in GoJS or diagramming in general, feel free to reach out or drop a message!

    Looking forward to connecting with you all! 😀

  • Login

  • Don't have an account? Register

Powered by NodeBB Contributors
  • First post
    Last post
0
  • Categories
  • Recent
  • Tags
  • Popular
  • Users
  • Groups