Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 61

Responsive Visualization Design

Recommended resources

Hello! Let's get started on the next step of building our telemetry engine's visualizer.

In the last lesson, we brought our custom plot to life by handling mouse input for panning, zooming, and displaying tooltips. Our plotter is now interactive, but it has a critical weakness: it doesn't behave correctly when you resize the window. The plot might draw outside its intended boundaries, or the mouse interactions might become misaligned.

Today, we will fix this. This lesson addresses the final piece of our custom visualization primitive: ensuring it correctly and robustly adapts to window resizing and other viewport updates. By the end, you'll have a plotting widget that's as flexible as it is interactive.

The Problem: A Static View in a Dynamic World

The issue with our current code lies in how we define the drawing canvas. We calculate the canvas corners, canvas_p0 and canvas_p1, once at the beginning of our plotting function. These values are then used for all coordinate transformations and drawing operations in that frame.

If the user resizes the native application window, the ImGui window containing our plot might also change size. However, our code is still using the old, now-incorrect canvas dimensions, leading to visual glitches and broken interactions.

The solution is to make our canvas definition dynamic, querying the available drawing space on every single frame.


Step 1: Making the Native Window Resizable

Before our ImGui code can adapt, the underlying OS window (created with a library like SDL, GLFW, or SFML) must be configured to be resizable in the first place. This is typically done by setting a flag during window creation.

GUI Development with C++, SDL2, and Dear ImGui

Let's look at how to create a resizable window using SDL2, as shown in an article by Martin Fieber. This is the foundational step required for any resizable UI.

In the 'Creating the window' section, find the code block where SDL_CreateWindow is called. Note the use of the SDL_WINDOW_RESIZABLE flag. If you are using a different windowing library, look for an equivalent option. This ensures the OS allows the user to resize your application window.

Once the native window can be resized, the ImGui viewport within it will automatically adjust. Our next task is to make our custom plot aware of this change.


Step 2: Dynamically Sizing the Canvas

ImGui provides a simple way to determine the available space for drawing within a window. Instead of calculating a static canvas size, we can ask ImGui: "How much space is left in this window?"

The key functions are:

  • ImGui::GetCursorScreenPos(): Gives the top-left corner of where the next widget or drawing command will be placed. This will be our canvas_p0.
  • ImGui::GetContentRegionAvail(): Returns the width and height of the remaining drawable area in the window. We can add this to canvas_p0 to find our canvas_p1.

This approach ensures our canvas always fills the entire client area of the ImGui window it's in, no matter how it's resized.

To protect our drawing from spilling outside these dynamic boundaries, we must also use clipping.

  • draw_list->PushClipRect(canvas_p0, canvas_p1, true): Tells the renderer to discard any pixels drawn outside the specified rectangle.
  • draw_list->PopClipRect(): Restores the previous clipping rectangle.

All your custom drawing commands for the plot must be placed between these two calls.

imgui_demo.cpp - Example: Custom rendering

The official ImGui demo code is the best reference for idiomatic usage. Let's examine the custom rendering example, which perfectly demonstrates this dynamic canvas and clipping technique.

Please read the code for the function ShowExampleAppCustomRendering. This is the pattern we will adopt. Pay special attention to: ImVec2 canvas_pos = ImGui::GetCursorScreenPos(); to get the starting corner. ImVec2 canvas_size = ImGui::GetContentRegionAvail(); to get the available dimensions. The use of draw_list->PushClipRect(...) and draw_list->PopClipRect() to wrap all the AddLine and AddRect calls.


Step 3: Updating Your Plotting Code

Now, let's apply this pattern to your PlotMyData function. You will replace your previous static calculation of canvas_p0 and canvas_p1 with this new dynamic approach.

Here's how your function's structure should look. Note how the canvas definition and clipping are now central to the implementation.

void PlotMyData(
    /* your parameters: pan_offset, zoom_level, etc. */
)
{
    ImGui::Begin("My Plot");

    // 1. Dynamically calculate canvas boundaries every frame
    ImVec2 canvas_p0 = ImGui::GetCursorScreenPos();      // Top-left corner of the drawing area
    ImVec2 canvas_size = ImGui::GetContentRegionAvail(); // Size of the drawing area
    if (canvas_size.x < 50.0f) canvas_size.x = 50.0f;    // Ensure canvas is not too small
    if (canvas_size.y < 50.0f) canvas_size.y = 50.0f;
    ImVec2 canvas_p1 = ImVec2(canvas_p0.x + canvas_size.x, canvas_p0.y + canvas_size.y); // Bottom-right corner

    ImGuiIO& io = ImGui::GetIO();
    ImDrawList* draw_list = ImGui::GetWindowDrawList();

    // Draw the plot background
    draw_list->AddRectFilled(canvas_p0, canvas_p1, IM_COL32(50, 50, 50, 255));
    
    // 2. Push a clipping rectangle to ensure all drawing stays within the canvas
    draw_list->PushClipRect(canvas_p0, canvas_p1, true);

    // --- All your drawing and interaction logic goes here ---
    // Your coordinate transformation functions (WorldToScreen, ScreenToWorld)
    // will now use these dynamic canvas_p0 and canvas_p1 values.
    
    // Example: Drawing grid lines
    // ... your grid line drawing code ...

    // Example: Drawing data
    // ... your data plotting code ...
    
    // Example: Mouse interaction handling (panning, zooming, tooltips)
    // This logic remains the same, but it will now operate with the correct,
    // up-to-date canvas boundaries.
    // ... your input handling code ...

    // --- End of drawing and interaction logic ---

    // 3. Pop the clipping rectangle
    draw_list->PopClipRect();

    // Draw the border on top of the clipped content
    draw_list->AddRect(canvas_p0, canvas_p1, IM_COL32(255, 255, 255, 255));

    ImGui::End();
}

By making this change, your WorldToScreen and ScreenToWorld functions will automatically use the correct canvas dimensions each frame, ensuring that all calculations for zooming, panning, and coordinate display remain accurate as the window is resized.

Test your understanding!

What would happen if you implemented the dynamic canvas sizing with GetContentRegionAvail() but forgot to wrap your drawing commands with PushClipRect and PopClipRect? What visual artifact would you expect to see if you made the window smaller?

Show answer

If you didn't use PushClipRect, your drawing commands would not be constrained to the canvas. When you made the window smaller, parts of your plot (like grid lines or data lines that extend to the old, larger boundary) would be drawn outside the new, smaller ImGui window border, potentially overlapping with other UI elements or rendering in unintended areas of the screen. PushClipRect is the essential safety measure that prevents this.


A Note on Viewports and High-DPI Scaling

Adapting to the "viewport" means more than just handling size changes; it also means handling pixel density. Modern displays, like Apple's Retina screens, have a high density of physical pixels for each "logical" pixel used for layout. If not handled, your application's UI can appear tiny.

Your windowing library (like SDL2) and ImGui can work together to handle this. The general idea is to calculate a scaling factor and have ImGui use it to render its fonts and widgets at the correct physical size.

GUI Development with C++, SDL2, and Dear ImGui

A robust application must account for High-DPI scaling. Let's revisit the article by Martin Fieber, which provides a clear example of how to handle this.

Read the section 'Handling high DPI displays'. The key is to get the actual render output size and compare it to the logical window size to find the scale factor. This ensures UI elements are comfortably sized on all displays. While your custom drawing code uses raw pixels and might not be directly affected, this is crucial for the readability of ImGui's built-in widgets like your tooltip and any labels.

This is an important concept for building professional-grade applications, ensuring a consistent user experience across different hardware.


Conclusion

Congratulations! You have now built a complete, robust, and interactive custom plotting widget. It can transform data coordinates, respond to user input for navigation, and correctly adapt to any window size. This is a foundational component for our telemetry engine's visualizer.

Key Takeaways:

  • A resizable UI starts with a resizable native window (e.g., using SDL_WINDOW_RESIZABLE).
  • To create a resizable canvas in ImGui, you must query for the available space every frame using ImGui::GetContentRegionAvail().
  • All custom drawing should be wrapped in draw_list->PushClipRect() and draw_list->PopClipRect() to prevent rendering outside the canvas boundaries.
  • Robust viewport handling also includes accounting for High-DPI scaling to ensure the UI is legible on all displays.

What's Next?

Our plotting widget is functionally complete, but it has a major performance limitation: it's not designed to render millions of data points. If we tried to plot a few minutes of high-frequency telemetry data, the application would grind to a halt.

In the next module, Building the High-Performance LOD Plotter, we will solve this exact problem. You will learn how to use data decimation and aggregation techniques (specifically, a min-max tree) to render enormous datasets at interactive frame rates, decoupling rendering performance from data volume.