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
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions implot3d.h
Original file line number Diff line number Diff line change
Expand Up @@ -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?


//-----------------------------------------------------------------------------
// [SECTION] Macros and Defines
//-----------------------------------------------------------------------------
Expand All @@ -50,6 +52,7 @@
#define IMPLOT3D_AUTO -1 // Deduce variable automatically
#define IMPLOT3D_AUTO_COL ImVec4(0, 0, 0, -1) // Deduce color automatically
#define IMPLOT3D_TMP template <typename T> IMPLOT3D_API
#define IMPLOT3D_DEFAULT_MAJOR_STRIDE INT_MAX

//-----------------------------------------------------------------------------
// [SECTION] Forward declarations and basic types
Expand Down Expand Up @@ -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.

const ImVec2& minor_bounds = ImVec2(-1, 1), const ImVec2& major_bounds = ImVec2(-1, 1), ImPlot3DSurfaceFlags flags = 0,
ImAxis3D values_axis = ImAxis3D_Z, ImAxis3D major_axis = ImAxis3D_Y, int minor_offset = 0, int major_offset = 0,
int minor_stride = sizeof(T), int major_stride = IMPLOT3D_DEFAULT_MAJOR_STRIDE);

IMPLOT3D_API void PlotMesh(const char* label_id, const ImPlot3DPoint* vtx, const unsigned int* idx, int vtx_count, int idx_count,
ImPlot3DMeshFlags flags = 0);
Expand Down
90 changes: 90 additions & 0 deletions implot3d_demo.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -397,6 +397,95 @@ void DemoSurfacePlots() {
ImPlot3D::PopColormap();
}

void DemoSimplifiedSurfacePlotsOffsetStride() {
constexpr int N = 10;
constexpr int M = 20;
static float values[N * M];
static float t = 0.0f;
// t += ImGui::GetIO().DeltaTime;

// Define the range for X and Y
constexpr float min_val = -1.0f;
constexpr float max_val = 1.0f;
constexpr float x_step = (max_val - min_val) / (N - 1);
constexpr float y_step = (max_val - min_val) / (M - 1);

// Populate the values array
for (int i = 0; i < N; i++) {
for (int j = 0; j < M; j++) {
int idx = i * M + j;
float x = min_val + j * x_step;
float y = min_val + i * y_step;
values[idx] = ImSin(2 * t + ImSqrt((x * x + y * y))); // value = sin(2t + sqrt(x^2 + y^2))
}
}

// Determine the
static ImAxis3D values_axis = ImAxis3D_Z;
static ImAxis3D major_axis = ImAxis3D_Y;
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.

}
}
if (ImGui::Combo("Major Axis", &major_axis, "X-Axis\0Y-Axis\0Z-Axis\0")) {
if (major_axis == values_axis) {
values_axis = (values_axis + 1) % ImAxis3D_COUNT;
}
}
// Add offset and stride
static int minor_offset = 0;
static int major_offset = 0;
static int minor_stride = 1;
static int major_stride = 1;

ImGui::SliderInt("Minor Offset", &minor_offset, -20, 20);
ImGui::SliderInt("Major Offset", &major_offset, -20, 20);
ImGui::SliderInt("Minor Stride", &minor_stride, -7, 7);
ImGui::SliderInt("Major Stride", &major_stride, -7, 7);

// Select flags
static ImPlot3DSurfaceFlags flags = ImPlot3DSurfaceFlags_NoMarkers;
CHECKBOX_FLAG(flags, ImPlot3DSurfaceFlags_NoLines);
CHECKBOX_FLAG(flags, ImPlot3DSurfaceFlags_NoFill);
CHECKBOX_FLAG(flags, ImPlot3DSurfaceFlags_NoMarkers);

// Begin the plot
ImPlot3D::PushColormap("Jet");
if (ImPlot3D::BeginPlot("Surface Plots", ImVec2(-1, 400), ImPlot3DFlags_NoClip)) {
ImPlot3D::SetupAxes("x", "y", "z");
ImPlot3D::SetupAxesLimits(-1, 1, -1, 1, -1.5, 1.5);

// Set fill style
ImPlot3D::PushStyleVar(ImPlot3DStyleVar_FillAlpha, 0.8f);

// Set line style
ImPlot3D::SetNextLineStyle(ImPlot3D::GetColormapColor(1));

// Set marker style
ImPlot3D::SetNextMarkerStyle(ImPlot3DMarker_Square, IMPLOT3D_AUTO, ImPlot3D::GetColormapColor(2));

// Update the number of minor and major items depending on the stride that was selected
const int updated_num_minor = minor_stride == 0 ? M : (M / abs(minor_stride) + (M % minor_stride == 0 ? 0 : 1));
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?


// Plot the surface
// The surface is flipped around the minor axis by specifying ImVec2(1, -1) for the minor bounds
ImPlot3D::PlotSurface("Wave Surface", &values[array_offset], updated_num_minor, updated_num_major, 0.0, 0.0, ImVec2(1, -1), ImVec2(-1, 1),
flags, values_axis, major_axis, minor_offset, major_offset, minor_stride * sizeof(float),
major_stride * M * sizeof(float));

// End the plot
ImPlot3D::PopStyleVar();
ImPlot3D::EndPlot();
}
ImPlot3D::PopColormap();
}

void DemoMeshPlots() {
static int mesh_id = 0;
ImGui::Combo("Mesh", &mesh_id, "Duck\0Sphere\0Cube\0\0");
Expand Down Expand Up @@ -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?

DemoHeader("Mesh Plots", DemoMeshPlots);
DemoHeader("Realtime Plots", DemoRealtimePlots);
DemoHeader("Image Plots", DemoImagePlots);
Expand Down
2 changes: 2 additions & 0 deletions implot3d_internal.h
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ template <typename TSet, typename TFlag> static inline bool ImHasFlag(TSet set,
// Flips a flag in a flagset
template <typename TSet, typename TFlag> static inline void ImFlipFlag(TSet& set, TFlag flag) { ImHasFlag(set, flag) ? set &= ~flag : set |= flag; }
template <typename T> static inline T ImRemap01(T x, T x0, T x1) { return (x1 - x0) ? ((x - x0) / (x1 - x0)) : 0; }
// Returns always positive modulo (assumes r != 0)
static inline int ImPosMod(int l, int r) { return (l % r + r) % r; }
// Returns true if val is NAN
static inline bool ImNan(float val) { return isnan(val); }
// Returns true if val is NAN or INFINITY
Expand Down
99 changes: 98 additions & 1 deletion implot3d_items.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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.

IndexerIdxMajorMinor(const T* data, int num_major, int num_minor, int major_offset, int minor_offset, int major_stride, int minor_stride)
: Data(data),
// If the stride does not have to be applied then use the major offset index as the number of major columns to shift by instead of applying
// the shift for the indexes It is a bit simpler to only have to append a constant to the index instead of having to add a constant to first
// determine the rows and then column offsets
MajorOffset((major_stride == int(sizeof(T)) * num_minor && minor_stride > 0) ? ImPosMod(num_minor * major_offset, num_minor * num_major)
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.

Added a little optimization here which makes it a bit more difficult to follow what is going on but I think worth it. I am hoping it is clear enough?

: ImPosMod(major_offset, num_major)),
MinorOffset(ImPosMod(minor_offset, num_minor)), MajorStride(major_stride), MinorStride(minor_stride),
Type(((MinorOffset == 0) << 0) | ((MajorOffset == 0) << 1) | ((MinorStride == int(sizeof(T)) && MajorStride > 0) << 2) |
((MajorStride == int(sizeof(T)) * num_minor && MinorStride > 0) << 3)) {}

template <typename I> IMPLOT3D_INLINE double operator()(I idx, int count, int major, int minor, int num_major, int num_minor) const {
return (double)GetData(idx, count, major, minor, num_major, num_minor);
}

template <typename I> IMPLOT3D_INLINE T GetData(I idx, int count, int major, int minor, int num_major, int num_minor) const {
// clang-format off
// Get the data based based on the type
switch (Type) {
case 15: return Data[idx]; // No offset or stride
case 14: return Data[(((minor + MinorOffset) < num_minor ? MinorOffset : (MinorOffset - num_minor)) + idx) % count]; // Minor offset
case 13: return Data[(MajorOffset + idx) % count]; // Major offset
case 12: return Data[(MajorOffset + ((minor + MinorOffset) < num_minor ? MinorOffset : (MinorOffset - num_minor)) + idx) % count]; // Major+minor offset
case 11: return *(const T*)(const void*)((const unsigned char*)Data + (size_t)((idx)) * MinorStride); // Minor stride
case 10: return *(const T*)(const void*)((const unsigned char*)Data + (size_t)((((minor + MinorOffset) < num_minor ? MinorOffset : (MinorOffset - num_minor)) + idx) % count) * MinorStride); // Minor stride and minor offset
case 9: return *(const T*)(const void*)((const unsigned char*)Data + (size_t)((MajorOffset + idx) % count) * MinorStride); // Minor stride and major offset
case 8: return *(const T*)(const void*)((const unsigned char*)Data + (size_t)((MajorOffset + ((minor + MinorOffset) < num_minor ? MinorOffset : (MinorOffset - num_minor)) + idx) % count) * MinorStride); // Minor stride and major + minor offset
case 7: return *(const T*)(const void*)((const unsigned char*)Data + (size_t)((major)) * MajorStride + minor * sizeof(T)); // Major stride
case 6: return *(const T*)(const void*)((const unsigned char*)Data + (size_t)((major)) * MajorStride + ((minor + MinorOffset) % num_minor) * sizeof(T)); // Major stride and minor offset
case 5: return *(const T*)(const void*)((const unsigned char*)Data + (size_t)((major + MajorOffset) % num_major) * MajorStride + minor * sizeof(T)); // Major stride and major offset
case 4: return *(const T*)(const void*)((const unsigned char*)Data + (size_t)((major + MajorOffset) % num_major) * MajorStride + ((minor + MinorOffset) % num_minor) * sizeof(T)); // Major stride and major+minor offset
case 3: return *(const T*)(const void*)((const unsigned char*)Data + (size_t)((major)) * MajorStride + (size_t)((minor)) * MinorStride); // Major+minor stride
case 2: return *(const T*)(const void*)((const unsigned char*)Data + (size_t)((major)) * MajorStride + (size_t)((minor + MinorOffset) % num_minor) * MinorStride); // Major+minor stride and minor offset
case 1: return *(const T*)(const void*)((const unsigned char*)Data + (size_t)((major + MajorOffset) % num_major) * MajorStride + (size_t)((minor)) * MinorStride); // Major+minor stride and major offset
case 0: return *(const T*)(const void*)((const unsigned char*)Data + (size_t)((major + MajorOffset) % num_major) * MajorStride + (size_t)((minor + MinorOffset) % num_minor) * MinorStride); // Major+minor stride and major+minor offset
default: return T(0);
}
// clang-format on
}
const T* const Data;
const int MajorOffset, MinorOffset, MajorStride, MinorStride, Type;
};

//-----------------------------------------------------------------------------
// [SECTION] Getters
//-----------------------------------------------------------------------------
Expand All @@ -932,6 +976,36 @@ template <typename _IndexerX, typename _IndexerY, typename _IndexerZ> struct Get
const int Count;
};

template <typename _Indexer> struct GetterMinorMajor {
GetterMinorMajor(const _Indexer& indexer, int num_major, int num_minor, int count, const ImVec2& major_bounds, const ImVec2& minor_bounds,
ImAxis3D values_axis, ImAxis3D major_axis)
: Indexer(indexer), NumMajor(num_major), NumMinor(num_minor), Count(count), MajorRef(major_bounds.y - major_bounds.x),
MajorOffset(major_bounds.x), MinorRef(minor_bounds.y - minor_bounds.x), MinorOffset(minor_bounds.x), Type(values_axis * 3 + major_axis) {}
template <typename I> IMPLOT3D_INLINE ImPlot3DPoint operator()(I idx) const {
const int major = idx / NumMinor;
const int minor = idx % NumMinor;
const float major_value = (major / float(NumMajor - 1.0f)) * MajorRef + MajorOffset;
const float minor_value = (minor / float(NumMinor - 1.0f)) * MinorRef + MinorOffset;
const float value = (float)Indexer(idx, Count, major, minor, NumMajor, NumMinor);
switch (Type) {
case 7: return ImPlot3DPoint(minor_value, major_value, value); // Z-Values + Y-Major
case 6: return ImPlot3DPoint(major_value, minor_value, value); // Z-Values + X-Major
case 5: return ImPlot3DPoint(minor_value, value, major_value); // Y-Values + Z-Major
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.

case 4: // Y-Values + Y-Major. Not valid. Maybe assert here?
case 0: // X-Values + X-Major. Not Valid. Maybe assert here?
default: return ImPlot3DPoint(0, 0, 0);
}
}
const _Indexer& Indexer;
const int NumMajor, NumMinor, Count;
const float MajorRef, MajorOffset, MinorRef, MinorOffset;
const int Type;
};

template <typename _Getter> struct GetterLoop {
GetterLoop(_Getter getter) : Getter(getter), Count(getter.Count + 1) {}
template <typename I> IMPLOT3D_INLINE ImPlot3DPoint operator()(I idx) const {
Expand Down Expand Up @@ -1364,9 +1438,32 @@ IMPLOT3D_TMP void PlotSurface(const char* label_id, const T* xs, const T* ys, co
return PlotSurfaceEx(label_id, getter, x_count, y_count, scale_min, scale_max, flags);
}

IMPLOT3D_TMP void PlotSurface(const char* label_id, const T* values, int minor_count, int major_count, double scale_min, double scale_max,
const ImVec2& minor_bounds, const ImVec2& major_bounds, ImPlot3DSurfaceFlags flags, ImAxis3D values_axis,
ImAxis3D major_axis, int minor_offset, int major_offset, int minor_stride, int major_stride) {
IM_ASSERT_USER_ERROR(values_axis != major_axis, "The values axis and major axis needs to be two different values");
// IM_ASSERT_USER_ERROR(values_axis == ImAxis3D_Z, "Only support Z-Axis at the moment. Need to change PlotSurfaceEx for this to work with anything
// other than z-axis");
int count = major_count * minor_count;
if (count < 4)
return;
GetterMinorMajor<IndexerIdxMajorMinor<T>> getter(
IndexerIdxMajorMinor<T>(values, major_count, minor_count, major_offset, minor_offset,
(major_stride == IMPLOT3D_DEFAULT_MAJOR_STRIDE ? (sizeof(T) * minor_count) : major_stride), minor_stride),
major_count, minor_count, count, major_bounds, minor_bounds, values_axis, major_axis);

// 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?

}

#define INSTANTIATE_MACRO(T) \
template IMPLOT3D_API void PlotSurface<T>(const char* label_id, const T* xs, const T* ys, const T* zs, int x_count, int y_count, \
double scale_min, double scale_max, ImPlot3DSurfaceFlags flags, int offset, int stride);
double scale_min, double scale_max, ImPlot3DSurfaceFlags flags, int offset, int stride); \
template IMPLOT3D_API void PlotSurface<T>(const char* label_id, const T* values, int minor_count, int major_count, double scale_min, \
double scale_max, const ImVec2& minor_bounds, const ImVec2& major_bounds, ImPlot3DSurfaceFlags flags, \
ImAxis3D values_axis, ImAxis3D major_axis, int minor_offset, int major_offset, int minor_stride, \
int major_stride);
CALL_INSTANTIATE_FOR_NUMERIC_TYPES()
#undef INSTANTIATE_MACRO

Expand Down
Loading