Skip to content

Feat: Simplify PlotSurface API #135

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 15 commits into
base: main
Choose a base branch
from
Open

Conversation

ACvanWyk
Copy link
Contributor

@ACvanWyk ACvanWyk commented Jul 14, 2025

Closes #110

Add what is supposed to be a "simplified" :o version of the PlotSurface which can take in the a N*M array and plot the data. The major and the minor parameters need to be specified instead of the x/y values so that we have a bit more control. Can also specify which axis is the data axis and the major axis. By default the data axis is Z-Axis and major axis is Y-Axis.

There are a couple of extra minor and major parameters that can be used with the plot:

  1. Stride: Set the stride of the plot data.
  2. Offset: Add an offset for the minor/major values
  3. Boundary: Set the boundary that the major and minor axis need to use. Will plot the data in the specified boundaries. This value can also be used to flip around an axis.

For simplicity the client can do ImPlot3D::PlotSurface("Plot", z_values, num_x, num_y) which will plot the z_values with dimensions num_x*num_y.

@brenocq : Let me know what you think. I hope you don't mind me looking at this issue. I saw this issue and was interested at taking a whack at it.
The function might be a bit overcomplicated but does allow for a lot of control. Could maybe simplify it?

Some things that still need to be fixed.

  • Simplify function and finalize parameters to use for the function call
  • Add some comments to explain the getter and indexer
  • Potentially change the names of the new getter/indexer. I have no idea what to call them?
  • Fix demo
  • Fix the X/Y-Axis value axis is selected there seems to be still some issues.
  • The RendererSurfaceFill(Maybe other Rendererxx) is still very dependent on Z-Values and does not necessarily understand the concept of axis values that are not z-axis values. I might take a look at this so that the getter maybe potentially reports this to the Renderer?

ACvanWyk added 2 commits July 14, 2025 23:20
- The minor and the major parameters for the rest of the plot needs to be specified. These parameters include stride, offset and the boundary to use
- By default using  `ImPlot3D::PlotSurface("Plot", z_values, num_x, num_y)` should work perfectly to produce a z-plot with just z_values
- Most of the parameters can be left as default but does allow for more control if desired
Copy link
Contributor Author

@ACvanWyk ACvanWyk left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@brenocq : When you get a chance can you please go through some of the comments that I have left.
I am hoping this is going in the right direction but feel free to provide feedback/thoughts/questions.

@@ -37,6 +37,8 @@
#include "imgui.h"
#ifndef IMGUI_DISABLE

#include <limits.h>
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Need this for INT_MAX used in IMPLOT3D_DEFAULT_MAJOR_STRIDE. Cannot use sizeof(T), -1(i/uint8) or 0 which are all potentially valid values.
I cannot think of a good value to make IMPLOT3D_DEFAULT_MAJOR_STRIDE other than INT_MAX. Maybe somebody has a better suggestion?

@@ -453,6 +456,10 @@ IMPLOT3D_TMP void PlotQuad(const char* label_id, const T* xs, const T* ys, const
// to a predefined range
IMPLOT3D_TMP void PlotSurface(const char* label_id, const T* xs, const T* ys, const T* zs, int x_count, int y_count, double scale_min = 0.0,
double scale_max = 0.0, ImPlot3DSurfaceFlags flags = 0, int offset = 0, int stride = sizeof(T));
IMPLOT3D_TMP void PlotSurface(const char* label_id, const T* values, int minor_count, int major_count, double scale_min = 0.0, double scale_max = 0.0,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could maybe simplify this. Having all these parameters allows for a lot of control.

if (ImGui::Combo("Values Axis", &values_axis, "X-Axis\0Y-Axis\0Z-Axis\0")) {
// The major and value axis cannot be the same
if (major_axis == values_axis) {
major_axis = (major_axis + 1) % ImAxis3D_COUNT;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Values-Axis and the Major-Axis cannot have the same value. Change them if they are the same.

case 3: return ImPlot3DPoint(major_value, value, minor_value); // Y-Values + X-Major
case 2: return ImPlot3DPoint(value, minor_value, major_value); // X-Values + Z-Major
case 1: return ImPlot3DPoint(value, major_value, minor_value); // X-Values + Y-Major
case 8: // Z-Values + Z-Major. Not valid. Maybe assert here?
Copy link
Contributor Author

@ACvanWyk ACvanWyk Jul 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should not support the value axis and major axis being the same type. There is a IM_ASSERT_USER_ERROR(values_axis != major_axis, "The values axis and major axis needs to be two different values"); in PlotSurface for this.

@@ -917,6 +917,50 @@ template <typename T> struct IndexerIdx {
int Stride;
};

template <typename T> struct IndexerIdxMajorMinor {
Copy link
Contributor Author

@ACvanWyk ACvanWyk Jul 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure what to call the indexer+getter. Ended up with this name but open to better suggestions?
Could maybe document how they work a bit better.
Added GetSurfaceValue function to help with determining the values for the surface to use and depending on what the data axis is set to will return different values.

const int updated_num_major = major_stride == 0 ? N : (N / abs(major_stride) + (N % major_stride == 0 ? 0 : 1));

// Add an offset to the array if either the major or the minor stride is negative
const int array_offset = (major_stride < 0 ? (M * (N - 1)) : 0) + (minor_stride < 0 ? M : 1) - 1;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Need to add an offset to the array since we are now striding backwards. I added support for negative strides if needed. I don't know if it necessary?

@@ -873,6 +962,7 @@ void ShowAllDemos() {
DemoHeader("Triangle Plots", DemoTrianglePlots);
DemoHeader("Quad Plots", DemoQuadPlots);
DemoHeader("Surface Plots", DemoSurfacePlots);
DemoHeader("Simplified Surface Plots with Offset and Stride", DemoSimplifiedSurfacePlotsOffsetStride);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure what to call this or where to put this. There is also nothing really simple about it.
Could maybe split it into two functions to show new API and then another for more complicated usage?

Copy link
Contributor Author

@ACvanWyk ACvanWyk Jul 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I split this into two separate demos:

  1. To show the simplified surface plot API which just takes in Z-Values and plots the N*M surface plot
  2. Added a section similar to ImPlot -> Tools, where the stride/offset/axis flag combinations can be tested and added a demo that tests different combinations of values for these values

Did not know where to put the axis, offset and stride demo. I followed the example of ImPlot. Let me know if we should put it somewhere else?


// TODO: I am pretty sure that the way that PlotSurfaceEx works is that it takes in the minor and the major count and thus it first iterates over
// the major then minor values. I need to confirm this first though
return PlotSurfaceEx(label_id, getter, minor_count, major_count, scale_min, scale_max, flags);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@brenocq: As far as I can tell PlotSurfaceEx does work in this way with major/minor being specified in this order since it just uses the getter and iterates over the major and then minor indexes.
I am hoping that we don't have to change anything in PlotSurfaceEx but let me know if you think I missed something?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did some further investigation and other than the Point.z in RendererSurfaceFill (added function Getter.GetSurfaceValue) I cannot see any specific things that depends on x, y or z values being in a certain order.
The function PlotSurfaceEx by default takes in x_count + y_count but this has always been minor and major since x has always been minor and y has been major. If the getter and indexer works correctly we can pass in the minor_count and major_count to this function. Could maybe rename the variables to minor_count and major_count instead of x_count and y_count since the new API can use different axis for different things?
It is very possible that I might have missed something.
Please @brenocq let me know if I missed anything?

…ing on what the data axis was set to

- Don't know if we are keeping this. Might remove it depending on feeback
ACvanWyk added 8 commits July 15, 2025 20:59
… surface plot and allows for even more control

Default to `ImAxis3D_COUNT` which the plot will then use with `values_axis` but can change the value if desired
1. A simplified example where the x and y values do not have to be specified but only the z_values.
2. Simillar to ImPlot add a section `Tools` where we can show an example where the axis, stride and offset of the surface plot is changed
- The plot works with plots that use x-axis or y-axis information
- As far as I can tell PlotSurfaceEx does not care what the values axis and minor/major values are as long as they are provided.
- If the value axis and the major axis is the same then do not have a case for it.
- There are fewer case statements in the switch
…jor_count` since that matches how they are used in the new `PlotSurface` function.

- Up to this point they have been used as x and y count but with the new function the major axis can be set
…inorRef` to apply to the major and minor when creating the `Getter` instead of determining it in the for loop each time
… minor values if they are already available.

- Call and pass in values to the operator in the `Getter` function if the minor and major values are already known.
- Default back to using the operator which does not take in the minor and major stride if it is not available.
Copy link
Contributor Author

@ACvanWyk ACvanWyk left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@brenocq I am quite happy with the implementation as it is. It does need some comments but I'll add them once I know whether I have gone in the correct direction.
When you get a chance please provide some feedback and let me know what you think


// TODO: I am pretty sure that the way that PlotSurfaceEx works is that it takes in the minor and the major count and thus it first iterates over
// the major then minor values. I need to confirm this first though
return PlotSurfaceEx(label_id, getter, minor_count, major_count, scale_min, scale_max, flags);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did some further investigation and other than the Point.z in RendererSurfaceFill (added function Getter.GetSurfaceValue) I cannot see any specific things that depends on x, y or z values being in a certain order.
The function PlotSurfaceEx by default takes in x_count + y_count but this has always been minor and major since x has always been minor and y has been major. If the getter and indexer works correctly we can pass in the minor_count and major_count to this function. Could maybe rename the variables to minor_count and major_count instead of x_count and y_count since the new API can use different axis for different things?
It is very possible that I might have missed something.
Please @brenocq let me know if I missed anything?

p_plot[1] = Getter(x + 1 + y * XCount);
p_plot[2] = Getter(x + 1 + (y + 1) * XCount);
p_plot[3] = Getter(x + (y + 1) * XCount);
p_plot[0] = Getter(minor + major * MinorCount, minor, major);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added the operator()(I idx, int minor, int major) function in the Getters for the GetterMinorMajor so that we don't have to determine the values again in the Getter with the operator()(I idx) function. Not sure if this is required but I thought it might be a nice little optimisation.
In the GetterXYZ the operator()(I idx, int minor, int major) just calls into the operator()(I idx) so it does not do anything.

RendererSurfaceFill(const _Getter& getter, int x_count, int y_count, ImU32 col, double scale_min, double scale_max)
: RendererBase((x_count - 1) * (y_count - 1), 6, 4), Getter(getter), XCount(x_count), YCount(y_count), Col(col), ScaleMin(scale_min),
ScaleMax(scale_max) {}
RendererSurfaceFill(const _Getter& getter, int minor_count, int major_count, ImU32 col, double scale_min, double scale_max)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Change to using minor_count and major_count instead of x_count and y_count since it makes more sense in the context of the new function. Up to this point it has always been the case that x and y axis has been the minor and major axis but with the new function the user can specify the major axis and the minor axis(indirectly).

@ACvanWyk ACvanWyk marked this pull request as ready for review July 16, 2025 17:50
@ACvanWyk ACvanWyk requested a review from brenocq as a code owner July 16, 2025 17:50
…et` to `MajorValueRef`, `MajorValueOffset`, `MinorValueRef` and `MinorValueOffset`

- This is mainly distinguish it from the major and minor offset we add to the data pointer
@brenocq brenocq added type:feat New feature or request prio:medium Medium priority status:review The task is under review labels Jul 21, 2025
@brenocq
Copy link
Owner

brenocq commented Jul 22, 2025

It would be nice to have a simplified API with as few arguments as possible. Some people may get "scared" just by the number of arguments.

IMPLOT3D_TMP void PlotSurface(const char* label_id, const T* values, int minor_count, int major_count, double scale_min = 0.0, double scale_max = 0.0, ImPlot3DSurfaceFlags flags = 0, int offset = 0, int stride = sizeof(T)))

Note that the offset/stride is for the const T* values. I don't think we should have an offset per major/minor. Having minor_stride and major_stride does make sense for performance reasons, but maybe we should offload this work for the user for the simplified SurfacePlot API.

I like the idea of choosing custom major/minor/data axes! We could potentially add new flags to the ImPlot3DSurfaceFlags to be able to use different major/minor axes (this would work with both the previous PlotSurface and the new simplified version)

  • ImPlot3DSurfaceFlags_XY - This is the default, X minor and Y major
  • ImPlot3DSurfaceFlags_XZ
  • ImPlot3DSurfaceFlags_YX
  • ImPlot3DSurfaceFlags_YZ
  • ImPlot3DSurfaceFlags_ZX
  • ImPlot3DSurfaceFlags_ZY

The flags above are a bit confusing, so we could also do the below:

  • ImPlot3DSurfaceFlags_PlaneXY - Default, X minor, Y major
  • ImPlot3DSurfaceFlags_PlaneXZ - X minor, Z major
  • ImPlot3DSurfaceFlags_PlaneYZ - Y minor, Z major
  • ImPlot3DSurfaceFlags_SwapAxes - This would swap the minor and major axes

Let me know what you think! These are just suggestions, nothing set in stone 😄

@ACvanWyk
Copy link
Contributor Author

ACvanWyk commented Aug 3, 2025

Hi @brenocq . Thank you for the constructive feedback.
Sorry I have been quite busy with other things and have not gotten a chance to take a look at this again.

Just a couple of thoughts that I have based on your feedback:

  1. Offset/Stride: Thank you for pointing out the stride/offset minor issue I used it just as a minor offset/minor stride. I'll fix this to be instead in terms of all the data instead of the minor values. I'll rename the minor offset + minor stride to just offset and stride and fix the calculations. As I currently implemented the minor stride is the same a stride and I am pretty sure is working as expected. But the minor offset -> offset is wrong and I'll change it so that it offsets all the data. I cannot imagine the user wanting to shift the entire minor column values by an offset and thus I'll remove it.
  2. Major stride/Major Offset: I think both of these parameters are quite important and I don't think we should leave it out just to simplify the the API. I believe we can save the the user from doing these things manually with these two parameters. If the user does not want to use it then they don't have to and they can leave it as default?
    2.1 Major stride: We can certainly make the user deal with this value but having this simplifies using the stride by quite a bit if the user does not specify the stride as a value of num_minor % _stride != 0(where _stride excludes the sizeof(T)) which causes weird artifacts as shown below. There is 20 minor elements and the stride is set to 3 (20 % 3 == 2 != 0). The 1st image has no stride, 2nd image a stride of 3 with no major stride and 3rd image with major stride specified. The 2nd image does not look at all like the original image with no stride while the 3rd one does and seems like it is working. I feel like users are going to use random strides and could run into this issue quite easily. Otherwise we need to make it clear to the user that they can only use a stride that does num_minor % _stride != 0?
    1_Stride3_Stride3_Stride_Major
    This will also allow the user to specify the major stride to use together with the stride if they want to:
    imageimage
    2.2 Major offset: Talking from personal experience with waterfall plots, I create one big memory location and just add new data into a certain memory location as new data becomes available and would like to update a offset value instead of having to be copying around memory constantly or having to draw multiple plots.
  3. Minor/Major boundary: From the original issue 📸 Gallery: Post your screenshots here #3 , the user was trying to plot a log scale plot. Having the user being able to specify the boundary of the plot is extremely useful and would allow the ticks to match where the data points are. We can always move this to the back of the parameters so that the defaults are {-1,1} but I do think this is quite useful to the user if they want to plot data at a certain location. I am not sure of using ImVec2 to represent these values though?
  4. Plane flags: Adding this to the ImPlot3DSurfaceFlags makes a lot more sense. I can change the API to do this instead of having to specify the major/minor in the API.

I do agree that a simplified API would be better but I do think more control is sometimes better. Maybe we can rearrange the arguments so that the the arguments commonly used are at the front while arguments less used are at the back.
Because of the above reasons I think the API should be changed to something like:

void PlotSurface(const char* label_id, const T* values, int minor_count, int major_count, double scale_min = 0.0, double scale_max = 0.0, ImPlot3DSurfaceFlags flags = 0, const ImVec2& minor_bounds = ImVec2(-1, 1), const ImVec2& major_bounds = ImVec2(-1, 1), int offset = 0, int stride = sizeof(T),  int major_offset = 0, int major_stride = IMPLOT3D_DEFAULT_MAJOR_STRIDE);

Let me know what you think? I am open to better suggestions.

- Rearange parameters for the `PlotSurface` function call and get rid of some of the parameters not needed.
- Added flags to indicate what the plane axis is and use them accordingly
- Fix demo to take into account some of the changes
- Use data `stride` and `offset` instead of `minor stride` and `minor offset`. For the most part the `minor stride` is the same as `stride` but there is quite a big difference between `minor offset` and `offset`.
- Added helper object `SurfacePlotPlaneGetter` which get the plane value from the 3D point  based on which plane was set
@ACvanWyk
Copy link
Contributor Author

ACvanWyk commented Aug 3, 2025

@brenocq . I updated the API a bit keeping in mind some of the changes I previously mentioned. Let me know what you think?

…comments

- Depending to what the `ImPlot3DSurfaceFlags_Plane(XY/YZ/XZ)` flag is set to, the minor/major count values changes based on the flag set. Rename so that there is no potential confusion
- Added some much needed comments for the new flags
- If neither the `ImPlot3DSurfaceFlags_PlaneXZ` and the `ImPlot3DSurfaceFlags_PlaneYZ` flag is specified then use `ImPlot3DSurfaceFlags_PlaneXY`.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
prio:medium Medium priority status:review The task is under review type:feat New feature or request
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[Feature] Simplify PlotSurface API
2 participants