Hello! Welcome back to the project.
In our last lesson, we put the finishing touches on our MinMaxTree by implementing its powerful query function. We now have a data structure that can give us the minimum and maximum values for any slice of our data in logarithmic time. This is a critical building block for our LOD plotter.
But a data structure is only useful if we know what to ask it. A user interacts with a plot not by typing in data indices like [1024, 2048], but by intuitively panning and zooming with their mouse. Today's lesson is about bridging that gap. We will figure out how to translate the user's pan and zoom actions into a precise data range that we can feed into our MinMaxTree.
This lesson focuses on the learning outcome: Calculate the visible data range based on the current zoom and pan state.
By the end of this session, you'll understand the mathematics of 2D camera transformations and be able to convert screen coordinates into data coordinates, which is the key to making our plotter interactive.
World Space vs. Screen Space: The Two Realities
First, we need to establish a core concept in any kind of rendering or visualization: the distinction between two coordinate systems.
- World Space (or Data Space): This is the coordinate system of your entire dataset. For our plotter, the x-axis represents the sample index (or timestamp), stretching from 0 to potentially millions. The y-axis represents the measured value. This is the "world" that our data lives in.
- Screen Space (or Viewport Space): This is the pixel-based coordinate system of the canvas or window where you are drawing. The x-axis goes from 0 to the canvas width, and the y-axis from 0 to the canvas height. This is the "camera lens" through which we view the world.
Our task is to define a mathematical mapping between these two spaces. Panning and zooming are simply operations that modify this mapping.

To get a clearer sense of these concepts, let's start with a foundational article.
This article, 'Panning and zooming' from sunshine2k.de, provides a great introduction to the concepts of global (world) space and screen space.
Read the first section, 'Description of panning and zooming'. Focus on how it defines the 'view port' (screen space) as a cutout of the 'global space' (world space).
The Mathematics of Transformation
To manage the mapping between world and screen space, we need two key state variables:
pan(oroffset): An(x, y)vector that represents how much we've shifted our view. This corresponds to the top-left corner of our view in world coordinates.zoom(orscale): An(x, y)scalar or vector that controls the magnification level. A larger scale value means we're more "zoomed in".
With these, we can define two fundamental transformation functions:
- World-to-Screen:
screen_pos = (world_pos - pan) * zoom - Screen-to-World:
world_pos = (screen_pos / zoom) + pan
These equations are the heart of our interactive plot. The world-to-screen function will be used for drawing, while the screen-to-world function is what we'll use today to determine what is visible. The article you just read summarizes these equations nicely.
Let's revisit the 'Panning and zooming' article for a concise summary of the transformation formulas.
Read the 'Summary' section. These two sets of equations are the most important takeaway. We will be implementing them in our code.
Implementing Panning and Zooming
Now let's see how to apply these concepts in practice. A fantastic video by javidx9 walks through building exactly this kind of system from scratch. We will use it as our guide.
First, watch this segment where the presenter introduces the concepts and implements basic panning.
This video, 'Programming Panning & Zooming' by javidx9, is a masterclass in implementing 2D camera controls. We'll start with the basics of setting up the transformation functions and handling panning.
Watch from 02:04 to 11:09. The presenter will derive the simple transformation equations for panning and then implement the logic to drag the view with the mouse. Notice how the mouse's delta (change in position) is used to update the offset.
As you saw, panning is relatively straightforward: the change in mouse position on the screen is used to update the world offset.
Zooming, however, introduces a classic problem. If you simply change the zoom variable, the view appears to "shoot away" from the mouse cursor, zooming towards the world origin (0,0). This is disorienting and not what users expect.
The Key: Mouse-Centered Zoom
To fix this, we need to ensure that the point in the world that is under the mouse cursor remains under the mouse cursor after the zoom. This creates a stable anchor point for the user.
The algorithm to achieve this is clever and essential:
- Get the current mouse position in screen space.
- Use our
screenToWorldfunction to find out which world coordinate is under the mouse before we zoom. Let's call thisworld_pos_before. - Apply the zoom: update the
zoomvariable based on the mouse wheel scroll (zoom *= zoom_factor). - Now that
zoomhas changed, ourscreenToWorldfunction will give a different result. Use it again with the same screen position to find the new world coordinate under the mouse after the zoom. Let's call thisworld_pos_after. - The view has drifted. The world point that was at our cursor is now somewhere else. The difference,
drift = world_pos_before - world_pos_after, tells us exactly how much we need to adjust ourpanto counteract this drift. - Update the pan:
pan += drift.
This sequence ensures the world "expands" or "contracts" around your mouse cursor. The following video segment explains and implements this perfectly.
This is the most important part of the lesson. The presenter explains the 'shooting off' problem with naive zooming and then walks through the logic and code for implementing correct, mouse-centered zooming.
Watch from 11:09 to 18:08. Pay very close attention to the sequence of getting the world coordinate before and after the zoom, and using their difference to correct the offset.
To see this exact logic implemented in C++ using ImGui (which you'll be using), a Stack Overflow answer provides a beautifully concise example.
Zoom in and out centered about the mouse cursor in 2D ...
This Stack Overflow post provides a direct, practical C++ implementation of mouse-centered zoom within an ImGui context. It perfectly mirrors the algorithm from the video.
Read the accepted answer. The first code block shows the corrected 'if' statement for handling the mouse wheel. Compare it to the algorithm we just discussed: mpos1 is world_pos_before, mpos2 is world_pos_after, and the pan is adjusted by their difference.
Finally: Calculating the Visible Data Range
Now that we have a robust transformation system, we can answer our original question: which data indices are visible on screen?
The visible data indices are simply the world-space x-coordinates that correspond to the left and right edges of our screen canvas. We can find them by using our screenToWorld function on the screen-space coordinates of the canvas corners.
Let's say our ImGui canvas starts at screen coordinate canvas_pos and has size canvas_size.
- Find the world coordinate of the left edge:
world_pos_left = screenToWorld(canvas_pos) - Find the world coordinate of the right edge:
world_pos_right = screenToWorld({canvas_pos.x + canvas_size.x, canvas_pos.y})
The x-components of these two vectors, world_pos_left.x and world_pos_right.x, define the continuous range of data indices visible on screen.
To turn this into the discrete indices l and r for our MinMaxTree query, we do the following:
// Assume data_size is the total number of points in our dataset
float visible_start_f = world_pos_left.x;
float visible_end_f = world_pos_right.x;
// Convert to integer indices and clamp to the valid range of our data array
size_t l = std::max(0, static_cast<int>(floor(visible_start_f)));
size_t r = std::min(data_size - 1, static_cast<size_t>(ceil(visible_end_f)));
// Now we can query our tree!
if (l <= r) {
MinMaxNode visible_range_data = min_max_tree.getMinMaxInRange(l, r);
// ... use this to draw the plot
}
This final step connects our interactive controls directly to our optimized data structure.
Test your understanding!
Imagine your plot canvas is 800 pixels wide and is located at screen position (100, 50).
Your current state is:
pan = { x: 5000.0, y: 10.0 }zoom = { x: 2.0, y: 1.0 }(meaning 1 world unit takes up 2 screen pixels horizontally)
Your screenToWorld function for the x-coordinate is world_x = (screen_x / zoom.x) + pan.x.
- What is the screen-space x-coordinate of the left edge of the canvas?
- What is the screen-space x-coordinate of the right edge of the canvas?
- What are the world-space x-coordinates corresponding to the left and right edges?
- What would the integer indices
landrbe for yourMinMaxTreequery?
Show answer
- The left edge is at
screen_x = 100. - The right edge is at
screen_x = 100 + 800 = 900. - Left edge world-x:
(100 / 2.0) + 5000.0 = 50.0 + 5000.0 = 5050.0
Right edge world-x:(900 / 2.0) + 5000.0 = 450.0 + 5000.0 = 5450.0 - The visible data range is
[5050.0, 5450.0].l = floor(5050.0) = 5050r = ceil(5450.0) = 5450
Your query would begetMinMaxInRange(5050, 5450).
Conclusion
Great job! This lesson was heavy on concepts and mathematics, but you've navigated it well. This is the logic that powers everything from 2D game cameras to CAD software to high-performance plotting tools.
Key Takeaways:
- Visualizations operate in two coordinate systems: World Space (for the data) and Screen Space (for the pixels).
- The transformation between them is controlled by
pan(offset) andzoom(scale). screen_pos = (world_pos - pan) * zoomworld_pos = (screen_pos / zoom) + pan- Mouse-centered zoom is achieved by calculating the world-space "drift" caused by a change in zoom and correcting the pan accordingly.
- The visible data range
[l, r]is found by applying thescreenToWorldtransformation to the left and right edges of your canvas.
What's Next?
We've come a long way. We have our MinMaxTree to query data efficiently, and now we know which slice of data to query based on user input.
But there's one final piece to the puzzle. If the visible range [l, r] still contains 10,000 data points but the canvas is only 800 pixels wide, we still can't just draw 10,000 points. In the next lesson, we will solve this. We'll learn how to implement a decimation algorithm to select representative points based on screen resolution. This algorithm will use our MinMaxTree to intelligently reduce the number of points we draw while preserving the visual characteristics of the data.