From 036b4acaab875b0c8fbbca177b3e7c8ee5f21aa8 Mon Sep 17 00:00:00 2001 From: ozlb Date: Mon, 11 May 2020 13:12:22 +0200 Subject: [PATCH] PlotDigital updates on ImPlot v0.2 --- implot.cpp | 1292 +++++++++++++++++++++++++++++++++++----------------- 1 file changed, 862 insertions(+), 430 deletions(-) diff --git a/implot.cpp b/implot.cpp index 0a89831..b457616 100644 --- a/implot.cpp +++ b/implot.cpp @@ -20,14 +20,38 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. -// ImPlot v0.1 WIP +// ImPlot v0.2 WIP + +/* + +API BREAKING CHANGES +==================== +Occasionally introducing changes that are breaking the API. We try to make the breakage minor and easy to fix. +Below is a change-log of API breaking changes only. If you are using one of the functions listed, expect to have to fix some code. +When you are not sure about a old symbol or function name, try using the Search/Find function of your IDE to look for comments or references in all implot files. +You can read releases logs https://github.com/epezent/implot/releases for more details. + +- 2020/05/10 (0.2) - The following function/struct names were changes: + - ImPlotRange -> ImPlotLimits + - GetPlotRange() -> GetPlotLimits() + - SetNextPlotRange -> SetNextPlotLimits + - SetNextPlotRangeX -> SetNextPlotLimitsX + - SetNextPlotRangeY -> SetNextPlotLimitsY +- 2020/05/10 (0.2) - Plot queries are pixel based by default. Query rects that maintain relative plot position have been removed. This was done to support multi-y-axis. + +*/ + +#ifdef _MSC_VER +#pragma warning (disable: 4996) // 'This function or variable may be unsafe': strcpy, strdup, sprintf, vsnprintf, sscanf, fopen +#endif #ifndef IMGUI_DEFINE_MATH_OPERATORS #define IMGUI_DEFINE_MATH_OPERATORS #endif -#include -#include + +#include "implot.h" +#include "imgui_internal.h" #define IM_NORMALIZE2F_OVER_ZERO(VX, VY) \ { \ @@ -41,6 +65,8 @@ // Special Color used to specific that a plot item color should set determined automatically. #define IM_COL_AUTO ImVec4(0,0,0,-1) +// The maximum number of support y-axes +#define MAX_Y_AXES 3 ImPlotStyle::ImPlotStyle() { LineWeight = 1; @@ -50,7 +76,6 @@ ImPlotStyle::ImPlotStyle() { ErrorBarSize = 5; ErrorBarWeight = 1.5; DigitalBitHeight = 8; - DigitalBitGap = 4; Colors[ImPlotCol_Line] = IM_COL_AUTO; Colors[ImPlotCol_Fill] = IM_COL_AUTO; @@ -62,16 +87,27 @@ ImPlotStyle::ImPlotStyle() { Colors[ImPlotCol_PlotBorder] = IM_COL_AUTO; Colors[ImPlotCol_XAxis] = IM_COL_AUTO; Colors[ImPlotCol_YAxis] = IM_COL_AUTO; + Colors[ImPlotCol_YAxis2] = IM_COL_AUTO; + Colors[ImPlotCol_YAxis3] = IM_COL_AUTO; Colors[ImPlotCol_Selection] = ImVec4(1,1,0,1); Colors[ImPlotCol_Query] = ImVec4(0,1,0,1); + Colors[ImPlotCol_QueryX] = ImVec4(1,0,0,1); } -ImPlotRange::ImPlotRange() { - XMin = XMax = YMin = YMax = NAN; +ImPlotRange::ImPlotRange() : Min(NAN), Max(NAN) {} + +bool ImPlotRange::Contains(float v) const { + return v >= Min && v <= Max; } -bool ImPlotRange::Contains(const ImVec2& p) { - return p.x >= XMin && p.x <= XMax && p.y >= YMin && p.y <= YMax; +float ImPlotRange::Size() const { + return Max - Min; +} + +ImPlotLimits::ImPlotLimits() {} + +bool ImPlotLimits::Contains(const ImVec2& p) const { + return X.Contains(p.x) && Y.Contains(p.y); } namespace ImGui { @@ -101,7 +137,7 @@ inline float Remap(float x, float x0, float x1, float y0, float y1) { /// Turns NANs to 0s inline float ConstrainNan(float val) { - return val == NAN || val == -NAN ? 0 : val; + return isnan(val) ? 0 : val; } /// Turns INFINITYs to FLT_MAXs @@ -116,7 +152,7 @@ inline float ConstrainLog(float val) { /// Returns true if val is NAN or INFINITY inline bool NanOrInf(float val) { - return val == INFINITY || val == NAN || val == -INFINITY || val == -NAN; + return val == INFINITY || val == -INFINITY || isnan(val); } /// Utility function to that rounds x to powers of 2,5 and 10 for generating axis labels @@ -223,19 +259,17 @@ struct ImPlotItem { struct ImPlotAxis { ImPlotAxis() { Dragging = false; - Min = 0; - Max = 1; + Range.Min = 0; + Range.Max = 1; Divisions = 3; Subdivisions = 10; - Flags = ImAxisFlags_Default; + Flags = PreviousFlags = ImAxisFlags_Default; } - float Range() { return Max - Min; } bool Dragging; - float Min; - float Max; + ImPlotRange Range; int Divisions; int Subdivisions; - ImAxisFlags Flags; + ImAxisFlags Flags, PreviousFlags; }; /// Holds Plot state information that must persist between frames @@ -243,8 +277,10 @@ struct ImPlot { ImPlot() { Selecting = Querying = Queried = DraggingQuery = false; SelectStart = QueryStart = ImVec2(0,0); - Flags = ImPlotFlags_Default; + Flags = PreviousFlags = ImPlotFlags_Default; + DraggingQueryX[0] = DraggingQueryX[1] = false; ColorIdx = 0; + CurrentYAxis = 0; } ImPool Items; @@ -256,33 +292,35 @@ struct ImPlot { ImVec2 QueryStart; ImRect QueryRect; // relative to BB_grid!! bool DraggingQuery; - ImPlotRange QueryRange; + + bool DraggingQueryX[2]; + ImRect QueryRectX[2]; // relative to BB_grid!! + ImPlotLimits QueryRangeX; ImPlotAxis XAxis; - ImPlotAxis YAxis; - inline ImPlotAxis& Axis(int idx) { return (&XAxis)[idx]; } + ImPlotAxis YAxis[MAX_Y_AXES]; - ImPlotFlags Flags; + + ImPlotFlags Flags, PreviousFlags; int ColorIdx; + int CurrentYAxis; }; struct ImNextPlotData { - ImNextPlotData() { - HasXRange = false; - HasYRange = false; - } + ImNextPlotData() : HasXRange{}, HasYRange{} {} ImGuiCond XRangeCond; - ImGuiCond YRangeCond; + ImGuiCond YRangeCond[MAX_Y_AXES]; bool HasXRange; - bool HasYRange; - float XMin, XMax, YMin, YMax; + bool HasYRange[MAX_Y_AXES]; + ImPlotRange X; + ImPlotRange Y[MAX_Y_AXES]; }; /// Holds Plot state information that must persist only between calls to BeginPlot()/EndPlot() struct ImPlotContext { - ImPlotContext() { + ImPlotContext() : RenderX(), RenderY() { CurrentPlot = NULL; - FitThisFrame = FitX = FitY = false; + FitThisFrame = FitX = false; RestorePlotPalette(); } @@ -305,24 +343,37 @@ struct ImPlotContext { Col_Txt, Col_TxtDis, Col_SlctBg, Col_SlctBd, Col_QryBg, Col_QryBd, - Col_XMajor, Col_XMinor, Col_XTxt, - Col_YMajor, Col_YMinor, Col_YTxt; - // Tick marks - ImVector XTicks, YTicks; - ImGuiTextBuffer XTickLabels, YTickLabels; + Col_QryX; + struct AxisColor { + AxisColor() : Major(), Minor(), Txt() {} + ImU32 Major, Minor, Txt; + }; + AxisColor Col_X; + AxisColor Col_Y[MAX_Y_AXES]; + // Tick marks + ImVector XTicks, YTicks[MAX_Y_AXES]; + ImGuiTextBuffer XTickLabels, YTickLabels[MAX_Y_AXES]; + float AxisLabelReference[MAX_Y_AXES]; // Transformation cache - ImRect PixelRange; - ImVec2 M; // linear scale (slope) - ImVec2 LogDen; // log scale denominator + ImRect PixelRange[MAX_Y_AXES]; + // linear scale (slope) + float Mx; + float My[MAX_Y_AXES]; + // log scale denominator + float LogDenX; + float LogDenY[MAX_Y_AXES]; // Data extents - ImRect Extents; - bool FitThisFrame; bool FitX; bool FitY; + ImPlotRange ExtentsX; + ImPlotRange ExtentsY[MAX_Y_AXES]; + + bool FitThisFrame; bool FitX; + bool FitY[MAX_Y_AXES] = {}; int VisibleItemCount; // Render flags - bool RenderX, RenderY; + bool RenderX, RenderY[MAX_Y_AXES]; // Mouse pos - ImVec2 LastMousePos; + ImVec2 LastMousePos[MAX_Y_AXES]; // Style ImVector ColorMap; ImPlotStyle Style; @@ -343,19 +394,21 @@ static ImPlotContext gp; /// Returns the next unused default plot color ImVec4 NextColor() { - auto col = gp.ColorMap[gp.CurrentPlot->ColorIdx % gp.ColorMap.size()]; + ImVec4 col = gp.ColorMap[gp.CurrentPlot->ColorIdx % gp.ColorMap.size()]; gp.CurrentPlot->ColorIdx++; return col; } inline void FitPoint(const ImVec2& p) { + ImPlotRange* extents_x = &gp.ExtentsX; + ImPlotRange* extents_y = &gp.ExtentsY[gp.CurrentPlot->CurrentYAxis]; if (!NanOrInf(p.x)) { - gp.Extents.Min.x = p.x < gp.Extents.Min.x ? p.x : gp.Extents.Min.x; - gp.Extents.Max.x = p.x > gp.Extents.Max.x ? p.x : gp.Extents.Max.x; + extents_x->Min = p.x < extents_x->Min ? p.x : extents_x->Min; + extents_x->Max = p.x > extents_x->Max ? p.x : extents_x->Max; } if (!NanOrInf(p.y)) { - gp.Extents.Min.y = p.y < gp.Extents.Min.y ? p.y : gp.Extents.Min.y; - gp.Extents.Max.y = p.y > gp.Extents.Max.y ? p.y : gp.Extents.Max.y; + extents_y->Min = p.y < extents_y->Min ? p.y : extents_y->Min; + extents_y->Max = p.y > extents_y->Max ? p.y : extents_y->Max; } } @@ -365,112 +418,118 @@ inline void FitPoint(const ImVec2& p) { inline void UpdateTransformCache() { // get pixels for transforms - gp.PixelRange = ImRect(HasFlag(gp.CurrentPlot->XAxis.Flags, ImAxisFlags_Invert) ? gp.BB_Grid.Max.x : gp.BB_Grid.Min.x, - HasFlag(gp.CurrentPlot->YAxis.Flags, ImAxisFlags_Invert) ? gp.BB_Grid.Min.y : gp.BB_Grid.Max.y, - HasFlag(gp.CurrentPlot->XAxis.Flags, ImAxisFlags_Invert) ? gp.BB_Grid.Min.x : gp.BB_Grid.Max.x, - HasFlag(gp.CurrentPlot->YAxis.Flags, ImAxisFlags_Invert) ? gp.BB_Grid.Max.y : gp.BB_Grid.Min.y); - gp.M.x = (gp.PixelRange.Max.x - gp.PixelRange.Min.x) / (gp.CurrentPlot->XAxis.Max - gp.CurrentPlot->XAxis.Min); - gp.M.y = (gp.PixelRange.Max.y - gp.PixelRange.Min.y) / (gp.CurrentPlot->YAxis.Max - gp.CurrentPlot->YAxis.Min); - gp.LogDen.x = log10(gp.CurrentPlot->XAxis.Max / gp.CurrentPlot->XAxis.Min); - gp.LogDen.y = log10(gp.CurrentPlot->YAxis.Max / gp.CurrentPlot->YAxis.Min); + for (int i = 0; i < MAX_Y_AXES; i++) { + gp.PixelRange[i] = ImRect(HasFlag(gp.CurrentPlot->XAxis.Flags, ImAxisFlags_Invert) ? gp.BB_Grid.Max.x : gp.BB_Grid.Min.x, + HasFlag(gp.CurrentPlot->YAxis[i].Flags, ImAxisFlags_Invert) ? gp.BB_Grid.Min.y : gp.BB_Grid.Max.y, + HasFlag(gp.CurrentPlot->XAxis.Flags, ImAxisFlags_Invert) ? gp.BB_Grid.Min.x : gp.BB_Grid.Max.x, + HasFlag(gp.CurrentPlot->YAxis[i].Flags, ImAxisFlags_Invert) ? gp.BB_Grid.Max.y : gp.BB_Grid.Min.y); + + gp.My[i] = (gp.PixelRange[i].Max.y - gp.PixelRange[i].Min.y) / gp.CurrentPlot->YAxis[i].Range.Size(); + } + gp.LogDenX = log10(gp.CurrentPlot->XAxis.Range.Max / gp.CurrentPlot->XAxis.Range.Min); + for (int i = 0; i < MAX_Y_AXES; i++) { + gp.LogDenY[i] = log10(gp.CurrentPlot->YAxis[i].Range.Max / gp.CurrentPlot->YAxis[i].Range.Min); + } + gp.Mx = (gp.PixelRange[0].Max.x - gp.PixelRange[0].Min.x) / gp.CurrentPlot->XAxis.Range.Size(); } -inline ImVec2 PixelsToPlot(float x, float y) { +inline ImVec2 PixelsToPlot(float x, float y, int y_axis_in = -1) { IM_ASSERT_USER_ERROR(gp.CurrentPlot != NULL, "PixelsToPlot() Needs to be called between BeginPlot() and EndPlot()!"); + const int y_axis = y_axis_in >= 0 ? y_axis_in : gp.CurrentPlot->CurrentYAxis; ImVec2 plt; - plt.x = (x - gp.PixelRange.Min.x) / gp.M.x + gp.CurrentPlot->XAxis.Min; - plt.y = (y - gp.PixelRange.Min.y) / gp.M.y + gp.CurrentPlot->YAxis.Min; + plt.x = (x - gp.PixelRange[y_axis].Min.x) / gp.Mx + gp.CurrentPlot->XAxis.Range.Min; + plt.y = (y - gp.PixelRange[y_axis].Min.y) / gp.My[y_axis] + gp.CurrentPlot->YAxis[y_axis].Range.Min; if (HasFlag(gp.CurrentPlot->XAxis.Flags, ImAxisFlags_LogScale)) { - float t = (plt.x - gp.CurrentPlot->XAxis.Min) / (gp.CurrentPlot->XAxis.Max - gp.CurrentPlot->XAxis.Min); - plt.x = pow(10.0f, t * gp.LogDen.x) * gp.CurrentPlot->XAxis.Min; + float t = (plt.x - gp.CurrentPlot->XAxis.Range.Min) / gp.CurrentPlot->XAxis.Range.Size(); + plt.x = pow(10.0f, t * gp.LogDenX) * gp.CurrentPlot->XAxis.Range.Min; } - if (HasFlag(gp.CurrentPlot->YAxis.Flags, ImAxisFlags_LogScale)) { - float t = (plt.y - gp.CurrentPlot->YAxis.Min) / (gp.CurrentPlot->YAxis.Max - gp.CurrentPlot->YAxis.Min); - plt.y = pow(10.0f, t * gp.LogDen.y) * gp.CurrentPlot->YAxis.Min; + if (HasFlag(gp.CurrentPlot->YAxis[y_axis].Flags, ImAxisFlags_LogScale)) { + float t = (plt.y - gp.CurrentPlot->YAxis[y_axis].Range.Min) / gp.CurrentPlot->YAxis[y_axis].Range.Size(); + plt.y = pow(10.0f, t * gp.LogDenY[y_axis]) * gp.CurrentPlot->YAxis[y_axis].Range.Min; } return plt; } -inline ImVec2 PlotToPixels(float x, float y) { +inline ImVec2 PlotToPixels(float x, float y, int y_axis_in = -1) { IM_ASSERT_USER_ERROR(gp.CurrentPlot != NULL, "PlotToPixels() Needs to be called between BeginPlot() and EndPlot()!"); + const int y_axis = y_axis_in >= 0 ? y_axis_in : gp.CurrentPlot->CurrentYAxis; ImVec2 pix; if (HasFlag(gp.CurrentPlot->XAxis.Flags, ImAxisFlags_LogScale)) { - float t = log10(x / gp.CurrentPlot->XAxis.Min) / gp.LogDen.x; - x = ImLerp(gp.CurrentPlot->XAxis.Min, gp.CurrentPlot->XAxis.Max, t); + float t = log10(x / gp.CurrentPlot->XAxis.Range.Min) / gp.LogDenX; + x = ImLerp(gp.CurrentPlot->XAxis.Range.Min, gp.CurrentPlot->XAxis.Range.Max, t); } - if (HasFlag(gp.CurrentPlot->YAxis.Flags, ImAxisFlags_LogScale)) { - float t = log10(y / gp.CurrentPlot->YAxis.Min) / gp.LogDen.y; - y = ImLerp(gp.CurrentPlot->YAxis.Min, gp.CurrentPlot->YAxis.Max, t); + if (HasFlag(gp.CurrentPlot->YAxis[y_axis].Flags, ImAxisFlags_LogScale)) { + float t = log10(y / gp.CurrentPlot->YAxis[y_axis].Range.Min) / gp.LogDenY[y_axis]; + y = ImLerp(gp.CurrentPlot->YAxis[y_axis].Range.Min, gp.CurrentPlot->YAxis[y_axis].Range.Max, t); } - pix.x = gp.PixelRange.Min.x + gp.M.x * (x - gp.CurrentPlot->XAxis.Min); - pix.y = gp.PixelRange.Min.y + gp.M.y * (y - gp.CurrentPlot->YAxis.Min); + pix.x = gp.PixelRange[y_axis].Min.x + gp.Mx * (x - gp.CurrentPlot->XAxis.Range.Min); + pix.y = gp.PixelRange[y_axis].Min.y + gp.My[y_axis] * (y - gp.CurrentPlot->YAxis[y_axis].Range.Min); return pix; } -ImVec2 PixelsToPlot(const ImVec2& pix) { - return PixelsToPlot(pix.x, pix.y); +ImVec2 PixelsToPlot(const ImVec2& pix, int y_axis) { + return PixelsToPlot(pix.x, pix.y, y_axis); } -ImVec2 PlotToPixels(const ImVec2& plt) { - return PlotToPixels(plt.x, plt.y); +ImVec2 PlotToPixels(const ImVec2& plt, int y_axis) { + return PlotToPixels(plt.x, plt.y, y_axis); } struct Plt2PixLinLin { - static inline ImVec2 Transform(const ImVec2& plt) { return Transform(plt.x, plt.y); } - static inline ImVec2 Transform(float x, float y) { - return { gp.PixelRange.Min.x + gp.M.x * (x - gp.CurrentPlot->XAxis.Min), - gp.PixelRange.Min.y + gp.M.y * (y - gp.CurrentPlot->YAxis.Min) }; - } -}; + Plt2PixLinLin(int y_axis_in) : y_axis(y_axis_in) {} -struct Plt2PixLinLinObj { - Plt2PixLinLinObj() { - a = gp.PixelRange.Min.x; - b = gp.M.x; - c = gp.CurrentPlot->XAxis.Min; - d = gp.PixelRange.Min.y; - e = gp.M.y; - f = gp.CurrentPlot->YAxis.Min; + ImVec2 operator()(const ImVec2& plt) { return (*this)(plt.x, plt.y); } + ImVec2 operator()(float x, float y) { + return { gp.PixelRange[y_axis].Min.x + gp.Mx * (x - gp.CurrentPlot->XAxis.Range.Min), + gp.PixelRange[y_axis].Min.y + gp.My[y_axis] * (y - gp.CurrentPlot->YAxis[y_axis].Range.Min) }; } - inline ImVec2 Transform(const ImVec2& plt) { return Transform(plt.x, plt.y); } - inline ImVec2 Transform(float x, float y) { - return { a + b * (x - c), d + e * (y - f) }; - } - float a, b, c, d, e, f; -}; + int y_axis; +}; struct Plt2PixLogLin { - static inline ImVec2 Transform(const ImVec2& plt) { return Transform(plt.x, plt.y); } - static inline ImVec2 Transform(float x, float y) { - float t = log10(x / gp.CurrentPlot->XAxis.Min) / gp.LogDen.x; - x = ImLerp(gp.CurrentPlot->XAxis.Min, gp.CurrentPlot->XAxis.Max, t); - return { gp.PixelRange.Min.x + gp.M.x * (x - gp.CurrentPlot->XAxis.Min), - gp.PixelRange.Min.y + gp.M.y * (y - gp.CurrentPlot->YAxis.Min) }; + Plt2PixLogLin(int y_axis_in) : y_axis(y_axis_in) {} + + ImVec2 operator()(const ImVec2& plt) { return (*this)(plt.x, plt.y); } + ImVec2 operator()(float x, float y) { + float t = log10(x / gp.CurrentPlot->XAxis.Range.Min) / gp.LogDenX; + x = ImLerp(gp.CurrentPlot->XAxis.Range.Min, gp.CurrentPlot->XAxis.Range.Max, t); + return { gp.PixelRange[y_axis].Min.x + gp.Mx * (x - gp.CurrentPlot->XAxis.Range.Min), + gp.PixelRange[y_axis].Min.y + gp.My[y_axis] * (y - gp.CurrentPlot->YAxis[y_axis].Range.Min) }; } + + int y_axis; }; struct Plt2PixLinLog { - static inline ImVec2 Transform(const ImVec2& plt) { return Transform(plt.x, plt.y); } - static inline ImVec2 Transform(float x, float y) { - float t = log10(y / gp.CurrentPlot->YAxis.Min) / gp.LogDen.y; - y = ImLerp(gp.CurrentPlot->YAxis.Min, gp.CurrentPlot->YAxis.Max, t); - return { gp.PixelRange.Min.x + gp.M.x * (x - gp.CurrentPlot->XAxis.Min), - gp.PixelRange.Min.y + gp.M.y * (y - gp.CurrentPlot->YAxis.Min) }; + Plt2PixLinLog(int y_axis_in) : y_axis(y_axis_in) {} + + ImVec2 operator()(const ImVec2& plt) { return (*this)(plt.x, plt.y); } + ImVec2 operator()(float x, float y) { + float t = log10(y / gp.CurrentPlot->YAxis[y_axis].Range.Min) / gp.LogDenY[y_axis]; + y = ImLerp(gp.CurrentPlot->YAxis[y_axis].Range.Min, gp.CurrentPlot->YAxis[y_axis].Range.Max, t); + return { gp.PixelRange[y_axis].Min.x + gp.Mx * (x - gp.CurrentPlot->XAxis.Range.Min), + gp.PixelRange[y_axis].Min.y + gp.My[y_axis] * (y - gp.CurrentPlot->YAxis[y_axis].Range.Min) }; } + + int y_axis; }; struct Plt2PixLogLog { - static inline ImVec2 Transform(const ImVec2& plt) { return Transform(plt.x, plt.y); } - static inline ImVec2 Transform(float x, float y) { - float t = log10(x / gp.CurrentPlot->XAxis.Min) / gp.LogDen.x; - x = ImLerp(gp.CurrentPlot->XAxis.Min, gp.CurrentPlot->XAxis.Max, t); - t = log10(y / gp.CurrentPlot->YAxis.Min) / gp.LogDen.y; - y = ImLerp(gp.CurrentPlot->YAxis.Min, gp.CurrentPlot->YAxis.Max, t); - return { gp.PixelRange.Min.x + gp.M.x * (x - gp.CurrentPlot->XAxis.Min), - gp.PixelRange.Min.y + gp.M.y * (y - gp.CurrentPlot->YAxis.Min) }; + Plt2PixLogLog(int y_axis_in) : y_axis(y_axis_in) {} + + ImVec2 operator()(const ImVec2& plt) { return (*this)(plt.x, plt.y); } + ImVec2 operator()(float x, float y) { + float t = log10(x / gp.CurrentPlot->XAxis.Range.Min) / gp.LogDenX; + x = ImLerp(gp.CurrentPlot->XAxis.Range.Min, gp.CurrentPlot->XAxis.Range.Max, t); + t = log10(y / gp.CurrentPlot->YAxis[y_axis].Range.Min) / gp.LogDenY[y_axis]; + y = ImLerp(gp.CurrentPlot->YAxis[y_axis].Range.Min, gp.CurrentPlot->YAxis[y_axis].Range.Max, t); + return { gp.PixelRange[y_axis].Min.x + gp.Mx * (x - gp.CurrentPlot->XAxis.Range.Min), + gp.PixelRange[y_axis].Min.y + gp.My[y_axis] * (y - gp.CurrentPlot->YAxis[y_axis].Range.Min) }; } + + int y_axis; }; //----------------------------------------------------------------------------- @@ -508,37 +567,37 @@ const char* GetLegendLabel(int i) { // Tick Utils //----------------------------------------------------------------------------- -inline void GetTicks(float tMin, float tMax, int nMajor, int nMinor, bool logscale, ImVector &out) { +inline void GetTicks(const ImPlotRange& scale, int nMajor, int nMinor, bool logscale, ImVector &out) { out.shrink(0); if (logscale) { - if (tMin <= 0 || tMax <= 0) + if (scale.Min <= 0 || scale.Max <= 0) return; - int exp_min = (int)(ImFloor(log10(tMin))); - int exp_max = (int)(ImCeil(log10(tMax))); + int exp_min = (int)(ImFloor(log10(scale.Min))); + int exp_max = (int)(ImCeil(log10(scale.Max))); for (int e = exp_min - 1; e < exp_max + 1; ++e) { double major1 = ImPow(10, (double)(e)); double major2 = ImPow(10, (double)(e + 1)); double interval = (major2 - major1) / 9; - if (major1 >= (tMin - FLT_EPSILON) && major1 <= (tMax + FLT_EPSILON)) + if (major1 >= (scale.Min - FLT_EPSILON) && major1 <= (scale.Max + FLT_EPSILON)) out.push_back(ImTick(major1, true)); for (int i = 1; i < 9; ++i) { double minor = major1 + i * interval; - if (minor >= (tMin - FLT_EPSILON) && minor <= (tMax + FLT_EPSILON)) + if (minor >= (scale.Min - FLT_EPSILON) && minor <= (scale.Max + FLT_EPSILON)) out.push_back(ImTick(minor, false, false)); } } } else { - const double range = NiceNum(tMax - tMin, 0); + const double range = NiceNum(scale.Max - scale.Min, 0); const double interval = NiceNum(range / (nMajor - 1), 1); - const double graphmin = floor(tMin / interval) * interval; - const double graphmax = ceil(tMax / interval) * interval; + const double graphmin = floor(scale.Min / interval) * interval; + const double graphmax = ceil(scale.Max / interval) * interval; for (double major = graphmin; major < graphmax + 0.5 * interval; major += interval) { - if (major >= tMin && major <= tMax) + if (major >= scale.Min && major <= scale.Max) out.push_back(ImTick(major, true)); for (int i = 1; i < nMinor; ++i) { double minor = major + i * interval / nMinor; - if (minor >= tMin && minor <= tMax) + if (minor >= scale.Min && minor <= scale.Max) out.push_back(ImTick(minor, false)); } } @@ -548,7 +607,7 @@ inline void GetTicks(float tMin, float tMax, int nMajor, int nMinor, bool logsca inline void LabelTicks(ImVector &ticks, bool scientific, ImGuiTextBuffer& buffer) { buffer.Buf.resize(0); char temp[32]; - for (auto &tk : ticks) { + for (ImTick &tk : ticks) { if (tk.RenderLabel) { tk.TextOffset = buffer.size(); if (scientific) @@ -561,11 +620,88 @@ inline void LabelTicks(ImVector &ticks, bool scientific, ImGuiTextBuffer } } +namespace { +struct AxisState { + ImPlotAxis* axis; + bool has_range; + ImGuiCond range_cond; + bool present; + int present_so_far; + bool flip; + bool lock_min; + bool lock_max; + bool lock; + + AxisState(ImPlotAxis& axis_in, bool has_range_in, ImGuiCond range_cond_in, + bool present_in, int previous_present) + : axis(&axis_in), + has_range(has_range_in), + range_cond(range_cond_in), + present(present_in), + present_so_far(previous_present + (present ? 1 : 0)), + flip(HasFlag(axis->Flags, ImAxisFlags_Invert)), + lock_min(HasFlag(axis->Flags, ImAxisFlags_LockMin)), + lock_max(HasFlag(axis->Flags, ImAxisFlags_LockMax)), + lock(present && ((lock_min && lock_max) || (has_range && range_cond == ImGuiCond_Always))) {} + + AxisState() + : axis(), + has_range(), + range_cond(), + present(), + present_so_far(), + flip(), + lock_min(), + lock_max(), + lock() {} +}; + +void UpdateAxisColor(int axis_flag, ImPlotContext::AxisColor* col) { + const ImVec4 col_Axis = gp.Style.Colors[axis_flag].w == -1 ? ImGui::GetStyle().Colors[ImGuiCol_Text] * ImVec4(1, 1, 1, 0.25f) : gp.Style.Colors[axis_flag]; + col->Major = GetColorU32(col_Axis); + col->Minor = GetColorU32(col_Axis * ImVec4(1, 1, 1, 0.25f)); + col->Txt = GetColorU32({col_Axis.x, col_Axis.y, col_Axis.z, 1}); +} + +ImRect GetAxisScale(int y_axis, float tx, float ty, float zoom_rate) { + return ImRect( + PixelsToPlot(gp.BB_Grid.Min - gp.BB_Grid.GetSize() * ImVec2(tx * zoom_rate, ty * zoom_rate), y_axis), + PixelsToPlot(gp.BB_Grid.Max + gp.BB_Grid.GetSize() * ImVec2((1 - tx) * zoom_rate, (1 - ty) * zoom_rate), y_axis)); +} + +class YPadCalculator { + public: + YPadCalculator(const AxisState* axis_states, const float* max_label_widths, float txt_off) + : AxisStates(axis_states), MaxLabelWidths(max_label_widths), TxtOff(txt_off) {} + + float operator()(int y_axis) { + ImPlot& plot = *gp.CurrentPlot; + if (!AxisStates[y_axis].present) { return 0; } + // If we have more than 1 axis present before us, then we need + // extra space to account for our tick bar. + float pad_result = 0; + if (AxisStates[y_axis].present_so_far >= 3) { + pad_result += 6.0f; + } + if (!HasFlag(plot.YAxis[y_axis].Flags, ImAxisFlags_TickLabels)) { + return pad_result; + } + pad_result += MaxLabelWidths[y_axis] + TxtOff; + return pad_result; + } + + private: + const AxisState* const AxisStates; + const float* const MaxLabelWidths; + const float TxtOff; +}; +} // namespace + //----------------------------------------------------------------------------- // BeginPlot() //----------------------------------------------------------------------------- -bool BeginPlot(const char* title, const char* x_label, const char* y_label, const ImVec2& size, ImPlotFlags flags, ImAxisFlags x_flags, ImAxisFlags y_flags) { +bool BeginPlot(const char* title, const char* x_label, const char* y_label, const ImVec2& size, ImPlotFlags flags, ImAxisFlags x_flags, ImAxisFlags y_flags, ImAxisFlags y2_flags, ImAxisFlags y3_flags) { IM_ASSERT_USER_ERROR(gp.CurrentPlot == NULL, "Mismatched BeginPlot()/EndPlot()!"); @@ -586,11 +722,33 @@ bool BeginPlot(const char* title, const char* x_label, const char* y_label, cons gp.CurrentPlot = gp.Plots.GetOrAddByKey(ID); ImPlot &plot = *gp.CurrentPlot; + plot.CurrentYAxis = 0; + if (just_created) { - plot.Flags = flags; - plot.XAxis.Flags = x_flags; - plot.YAxis.Flags = y_flags; + plot.Flags = flags; + plot.XAxis.Flags = x_flags; + plot.YAxis[0].Flags = y_flags; + plot.YAxis[1].Flags = y2_flags; + plot.YAxis[2].Flags = y3_flags; } + else { + // TODO: Check which individual flags changed, and only reset those! + // There's probably an easy bit mask trick I'm not aware of. + if (flags != plot.PreviousFlags) + plot.Flags = flags; + if (y_flags != plot.YAxis[0].PreviousFlags) + plot.YAxis[0].PreviousFlags = y_flags; + if (y2_flags != plot.YAxis[1].PreviousFlags) + plot.YAxis[1].PreviousFlags = y2_flags; + if (y3_flags != plot.YAxis[2].PreviousFlags) + plot.YAxis[2].PreviousFlags = y3_flags; + } + + plot.PreviousFlags = flags; + plot.XAxis.PreviousFlags = x_flags; + plot.YAxis[0].PreviousFlags = y_flags; + plot.YAxis[1].PreviousFlags = y2_flags; + plot.YAxis[2].PreviousFlags = y3_flags; // capture scroll with a child region if (!HasFlag(plot.Flags, ImPlotFlags_NoChild)) { @@ -606,53 +764,56 @@ bool BeginPlot(const char* title, const char* x_label, const char* y_label, cons if (gp.NextPlotData.HasXRange) { if (just_created || gp.NextPlotData.XRangeCond == ImGuiCond_Always) { - plot.XAxis.Min = gp.NextPlotData.XMin; - plot.XAxis.Max = gp.NextPlotData.XMax; + plot.XAxis.Range = gp.NextPlotData.X; } } - if (gp.NextPlotData.HasYRange) { - if (just_created || gp.NextPlotData.YRangeCond == ImGuiCond_Always) - { - plot.YAxis.Min = gp.NextPlotData.YMin; - plot.YAxis.Max = gp.NextPlotData.YMax; + for (int i = 0; i < MAX_Y_AXES; i++) { + if (gp.NextPlotData.HasYRange[i]) { + if (just_created || gp.NextPlotData.YRangeCond[i] == ImGuiCond_Always) + { + plot.YAxis[i].Range = gp.NextPlotData.Y[i]; + } } } // AXIS STATES ------------------------------------------------------------ + AxisState x(plot.XAxis, gp.NextPlotData.HasXRange, gp.NextPlotData.XRangeCond, true, 0); + AxisState y[MAX_Y_AXES]; + y[0] = AxisState(plot.YAxis[0], gp.NextPlotData.HasYRange[0], gp.NextPlotData.YRangeCond[0], true, 0); + y[1] = AxisState(plot.YAxis[1], gp.NextPlotData.HasYRange[1], gp.NextPlotData.YRangeCond[1], + HasFlag(plot.Flags, ImPlotFlags_YAxis2), y[0].present_so_far); + y[2] = AxisState(plot.YAxis[2], gp.NextPlotData.HasYRange[2], gp.NextPlotData.YRangeCond[2], + HasFlag(plot.Flags, ImPlotFlags_YAxis3), y[1].present_so_far); - const bool flip_x = HasFlag(plot.XAxis.Flags, ImAxisFlags_Invert); - const bool lock_x_min = HasFlag(plot.XAxis.Flags, ImAxisFlags_LockMin); - const bool lock_x_max = HasFlag(plot.XAxis.Flags, ImAxisFlags_LockMax); - const bool lock_x = (lock_x_min && lock_x_max) || (gp.NextPlotData.HasXRange && gp.NextPlotData.XRangeCond == ImGuiCond_Always); + const bool lock_plot = x.lock && y[0].lock && y[1].lock && y[2].lock; - const bool flip_y = HasFlag(plot.YAxis.Flags, ImAxisFlags_Invert); - const bool lock_y_min = HasFlag(plot.YAxis.Flags, ImAxisFlags_LockMin); - const bool lock_y_max = HasFlag(plot.YAxis.Flags, ImAxisFlags_LockMax); - const bool lock_y = (lock_y_min && lock_y_max) || (gp.NextPlotData.HasYRange && gp.NextPlotData.YRangeCond == ImGuiCond_Always); - - const bool lock_plot = lock_x && lock_y; - // CONSTRAINTS ------------------------------------------------------------ - plot.XAxis.Min = ConstrainNan(ConstrainInf(plot.XAxis.Min)); - plot.XAxis.Max = ConstrainNan(ConstrainInf(plot.XAxis.Max)); - plot.YAxis.Min = ConstrainNan(ConstrainInf(plot.YAxis.Min)); - plot.YAxis.Max = ConstrainNan(ConstrainInf(plot.YAxis.Max)); + plot.XAxis.Range.Min = ConstrainNan(ConstrainInf(plot.XAxis.Range.Min)); + plot.XAxis.Range.Max = ConstrainNan(ConstrainInf(plot.XAxis.Range.Max)); + for (int i = 0; i < MAX_Y_AXES; i++) { + plot.YAxis[i].Range.Min = ConstrainNan(ConstrainInf(plot.YAxis[i].Range.Min)); + plot.YAxis[i].Range.Max = ConstrainNan(ConstrainInf(plot.YAxis[i].Range.Max)); + } if (HasFlag(plot.XAxis.Flags, ImAxisFlags_LogScale)) - plot.XAxis.Min = ConstrainLog(plot.XAxis.Min); + plot.XAxis.Range.Min = ConstrainLog(plot.XAxis.Range.Min); if (HasFlag(plot.XAxis.Flags, ImAxisFlags_LogScale)) - plot.XAxis.Max = ConstrainLog(plot.XAxis.Max); - if (HasFlag(plot.YAxis.Flags, ImAxisFlags_LogScale)) - plot.YAxis.Min = ConstrainLog(plot.YAxis.Min); - if (HasFlag(plot.YAxis.Flags, ImAxisFlags_LogScale)) - plot.YAxis.Max = ConstrainLog(plot.YAxis.Max); + plot.XAxis.Range.Max = ConstrainLog(plot.XAxis.Range.Max); + for (int i = 0; i < MAX_Y_AXES; i++) { + if (HasFlag(plot.YAxis[i].Flags, ImAxisFlags_LogScale)) + plot.YAxis[i].Range.Min = ConstrainLog(plot.YAxis[i].Range.Min); + if (HasFlag(plot.YAxis[i].Flags, ImAxisFlags_LogScale)) + plot.YAxis[i].Range.Max = ConstrainLog(plot.YAxis[i].Range.Max); + } - if (plot.XAxis.Max <= plot.XAxis.Min) - plot.XAxis.Max = plot.XAxis.Min + FLT_EPSILON; - if (plot.YAxis.Max <= plot.YAxis.Min) - plot.YAxis.Max = plot.YAxis.Min + FLT_EPSILON; + if (plot.XAxis.Range.Max <= plot.XAxis.Range.Min) + plot.XAxis.Range.Max = plot.XAxis.Range.Min + FLT_EPSILON; + for (int i = 0; i < MAX_Y_AXES; i++) { + if (plot.YAxis[i].Range.Max <= plot.YAxis[i].Range.Min) + plot.YAxis[i].Range.Max = plot.YAxis[i].Range.Min + FLT_EPSILON; + } // adaptive divisions if (HasFlag(plot.XAxis.Flags, ImAxisFlags_Adaptive)) { @@ -660,10 +821,12 @@ bool BeginPlot(const char* title, const char* x_label, const char* y_label, cons if (plot.XAxis.Divisions < 2) plot.XAxis.Divisions = 2; } - if (HasFlag(plot.YAxis.Flags, ImAxisFlags_Adaptive)) { - plot.YAxis.Divisions = (int)IM_ROUND(0.003 * gp.BB_Canvas.GetHeight()); - if (plot.YAxis.Divisions < 2) - plot.YAxis.Divisions = 2; + for (int i = 0; i < MAX_Y_AXES; i++) { + if (HasFlag(plot.YAxis[i].Flags, ImAxisFlags_Adaptive)) { + plot.YAxis[i].Divisions = (int)IM_ROUND(0.003 * gp.BB_Canvas.GetHeight()); + if (plot.YAxis[i].Divisions < 2) + plot.YAxis[i].Divisions = 2; + } } // COLORS ----------------------------------------------------------------- @@ -672,15 +835,10 @@ bool BeginPlot(const char* title, const char* x_label, const char* y_label, cons gp.Col_Bg = gp.Style.Colors[ImPlotCol_PlotBg].w == -1 ? GetColorU32(ImGuiCol_WindowBg) : GetColorU32(gp.Style.Colors[ImPlotCol_PlotBg]); gp.Col_Border = gp.Style.Colors[ImPlotCol_PlotBorder].w == -1 ? GetColorU32(ImGuiCol_Text, 0.5f) : GetColorU32(gp.Style.Colors[ImPlotCol_PlotBorder]); - const ImVec4 col_xAxis = gp.Style.Colors[ImPlotCol_XAxis].w == -1 ? ImGui::GetStyle().Colors[ImGuiCol_Text] * ImVec4(1, 1, 1, 0.25f) : gp.Style.Colors[ImPlotCol_XAxis]; - gp.Col_XMajor = GetColorU32(col_xAxis); - gp.Col_XMinor = GetColorU32(col_xAxis * ImVec4(1, 1, 1, 0.25f)); - gp.Col_XTxt = GetColorU32({col_xAxis.x, col_xAxis.y, col_xAxis.z, 1}); - - const ImVec4 col_yAxis = gp.Style.Colors[ImPlotCol_YAxis].w == -1 ? ImGui::GetStyle().Colors[ImGuiCol_Text] * ImVec4(1, 1, 1, 0.25f) : gp.Style.Colors[ImPlotCol_YAxis]; - gp.Col_YMajor = GetColorU32(col_yAxis); - gp.Col_YMinor = GetColorU32(col_yAxis * ImVec4(1, 1, 1, 0.25f)); - gp.Col_YTxt = GetColorU32({col_yAxis.x, col_yAxis.y, col_yAxis.z, 1}); + UpdateAxisColor(ImPlotCol_XAxis, &gp.Col_X); + UpdateAxisColor(ImPlotCol_YAxis, &gp.Col_Y[0]); + UpdateAxisColor(ImPlotCol_YAxis2, &gp.Col_Y[1]); + UpdateAxisColor(ImPlotCol_YAxis3, &gp.Col_Y[2]); gp.Col_Txt = GetColorU32(ImGuiCol_Text); gp.Col_TxtDis = GetColorU32(ImGuiCol_TextDisabled); @@ -689,6 +847,8 @@ bool BeginPlot(const char* title, const char* x_label, const char* y_label, cons gp.Col_QryBg = GetColorU32(gp.Style.Colors[ImPlotCol_Query] * ImVec4(1,1,1,0.25f)); gp.Col_QryBd = GetColorU32(gp.Style.Colors[ImPlotCol_Query]); + gp.Col_QryX = GetColorU32(gp.Style.Colors[ImPlotCol_QueryX]); + // BB AND HOVER ----------------------------------------------------------- // frame @@ -708,28 +868,38 @@ bool BeginPlot(const char* title, const char* x_label, const char* y_label, cons // canvas bb gp.BB_Canvas = ImRect(gp.BB_Frame.Min + Style.WindowPadding, gp.BB_Frame.Max - Style.WindowPadding); - gp.RenderX = (HasFlag(plot.XAxis.Flags, ImAxisFlags_GridLines) || - HasFlag(plot.XAxis.Flags, ImAxisFlags_TickMarks) || + gp.RenderX = (HasFlag(plot.XAxis.Flags, ImAxisFlags_GridLines) || + HasFlag(plot.XAxis.Flags, ImAxisFlags_TickMarks) || HasFlag(plot.XAxis.Flags, ImAxisFlags_TickLabels)) && plot.XAxis.Divisions > 1; - gp.RenderY = (HasFlag(plot.YAxis.Flags, ImAxisFlags_GridLines) || - HasFlag(plot.YAxis.Flags, ImAxisFlags_TickMarks) || - HasFlag(plot.YAxis.Flags, ImAxisFlags_TickLabels)) && plot.YAxis.Divisions > 1; + for (int i = 0; i < MAX_Y_AXES; i++) { + gp.RenderY[i] = + y[i].present && + (HasFlag(plot.YAxis[i].Flags, ImAxisFlags_GridLines) || + HasFlag(plot.YAxis[i].Flags, ImAxisFlags_TickMarks) || + HasFlag(plot.YAxis[i].Flags, ImAxisFlags_TickLabels)) && plot.YAxis[i].Divisions > 1; + } // get ticks if (gp.RenderX) - GetTicks(plot.XAxis.Min, plot.XAxis.Max, plot.XAxis.Divisions, plot.XAxis.Subdivisions, HasFlag(plot.XAxis.Flags, ImAxisFlags_LogScale), gp.XTicks); - if (gp.RenderY) - GetTicks(plot.YAxis.Min, plot.YAxis.Max, plot.YAxis.Divisions, plot.YAxis.Subdivisions, HasFlag(plot.YAxis.Flags, ImAxisFlags_LogScale), gp.YTicks); + GetTicks(plot.XAxis.Range, plot.XAxis.Divisions, plot.XAxis.Subdivisions, HasFlag(plot.XAxis.Flags, ImAxisFlags_LogScale), gp.XTicks); + for (int i = 0; i < MAX_Y_AXES; i++) { + if (gp.RenderY[i]) { + GetTicks(plot.YAxis[i].Range, plot.YAxis[i].Divisions, plot.YAxis[i].Subdivisions, HasFlag(plot.YAxis[i].Flags, ImAxisFlags_LogScale), gp.YTicks[i]); + } + } // label ticks if (HasFlag(plot.XAxis.Flags, ImAxisFlags_TickLabels)) LabelTicks(gp.XTicks, HasFlag(plot.XAxis.Flags, ImAxisFlags_Scientific), gp.XTickLabels); - float max_label_width = 0; - if (HasFlag(plot.YAxis.Flags, ImAxisFlags_TickLabels)) { - LabelTicks(gp.YTicks, HasFlag(plot.YAxis.Flags, ImAxisFlags_Scientific), gp.YTickLabels); - for (auto &yt : gp.YTicks) - max_label_width = yt.Size.x > max_label_width ? yt.Size.x : max_label_width; + float max_label_width[MAX_Y_AXES] = {}; + for (int i = 0; i < MAX_Y_AXES; i++) { + if (y[i].present && HasFlag(plot.YAxis[i].Flags, ImAxisFlags_TickLabels)) { + LabelTicks(gp.YTicks[i], HasFlag(plot.YAxis[i].Flags, ImAxisFlags_Scientific), gp.YTickLabels[i]); + for (ImTick &yt : gp.YTicks[i]) { + max_label_width[i] = yt.Size.x > max_label_width[i] ? yt.Size.x : max_label_width[i]; + } + } } // grid bb @@ -738,34 +908,56 @@ bool BeginPlot(const char* title, const char* x_label, const char* y_label, cons const float txt_height = GetTextLineHeight(); const float pad_top = title_size.x > 0.0f ? txt_height + txt_off : 0; const float pad_bot = (HasFlag(plot.XAxis.Flags, ImAxisFlags_TickLabels) ? txt_height + txt_off : 0) + (x_label ? txt_height + txt_off : 0); - const float pad_left = (HasFlag(plot.YAxis.Flags, ImAxisFlags_TickLabels) ? max_label_width + txt_off : 0) + (y_label ? txt_height + txt_off : 0); - gp.BB_Grid = ImRect(gp.BB_Canvas.Min + ImVec2(pad_left, pad_top), gp.BB_Canvas.Max - ImVec2(0, pad_bot)); + YPadCalculator y_axis_pad(y, max_label_width, txt_off); + const float pad_left = y_axis_pad(0) + (y_label ? txt_height + txt_off : 0); + const float pad_right = y_axis_pad(1) + y_axis_pad(2); + gp.BB_Grid = ImRect(gp.BB_Canvas.Min + ImVec2(pad_left, pad_top), gp.BB_Canvas.Max - ImVec2(pad_right, pad_bot)); gp.Hov_Grid = gp.BB_Grid.Contains(IO.MousePos); // axis region bbs - const ImRect xAxisRegion_bb(gp.BB_Grid.Min + ImVec2(10, 0), {gp.BB_Grid.Max.x, gp.BB_Frame.Max.y}); + const ImRect xAxisRegion_bb(gp.BB_Grid.Min + ImVec2(10, 0), ImVec2(gp.BB_Grid.Max.x, gp.BB_Frame.Max.y) - ImVec2(10, 0)); const bool hov_x_axis_region = xAxisRegion_bb.Contains(IO.MousePos); - const ImRect yAxisRegion_bb({gp.BB_Frame.Min.x, gp.BB_Grid.Min.y}, gp.BB_Grid.Max - ImVec2(0, 10)); - const bool hov_y_axis_region = yAxisRegion_bb.Contains(IO.MousePos); + + // The left labels are referenced to the left of the bounding box. + gp.AxisLabelReference[0] = gp.BB_Grid.Min.x; + // If Y axis 1 is present, its labels will be referenced to the + // right of the bounding box. + gp.AxisLabelReference[1] = gp.BB_Grid.Max.x; + // The third axis may be either referenced to the right of the + // bounding box, or 6 pixels further past the end of the 2nd axis. + gp.AxisLabelReference[2] = + !y[1].present ? + gp.BB_Grid.Max.x : + (gp.AxisLabelReference[1] + y_axis_pad(1) + 6); + + ImRect yAxisRegion_bb[MAX_Y_AXES]; + yAxisRegion_bb[0] = ImRect({gp.BB_Frame.Min.x, gp.BB_Grid.Min.y}, {gp.BB_Grid.Min.x + 6, gp.BB_Grid.Max.y - 10}); + // The auxiliary y axes are off to the right of the BB grid. + yAxisRegion_bb[1] = ImRect({gp.BB_Grid.Max.x - 6, gp.BB_Grid.Min.y}, + gp.BB_Grid.Max + ImVec2(y_axis_pad(1), 0)); + yAxisRegion_bb[2] = ImRect({gp.AxisLabelReference[2] - 6, gp.BB_Grid.Min.y}, + yAxisRegion_bb[1].Max + ImVec2(y_axis_pad(2), 0)); + + ImRect centralRegion({gp.BB_Grid.Min.x + 6, gp.BB_Grid.Min.y}, + {gp.BB_Grid.Max.x - 6, gp.BB_Grid.Max.y}); + + const bool hov_y_axis_region[MAX_Y_AXES] = { + y[0].present && (yAxisRegion_bb[0].Contains(IO.MousePos) || centralRegion.Contains(IO.MousePos)), + y[1].present && (yAxisRegion_bb[1].Contains(IO.MousePos) || centralRegion.Contains(IO.MousePos)), + y[2].present && (yAxisRegion_bb[2].Contains(IO.MousePos) || centralRegion.Contains(IO.MousePos)), + }; + const bool any_hov_y_axis_region = hov_y_axis_region[0] || hov_y_axis_region[1] || hov_y_axis_region[2]; // legend hovered from last frame const bool hov_legend = HasFlag(plot.Flags, ImPlotFlags_Legend) ? gp.Hov_Frame && plot.BB_Legend.Contains(IO.MousePos) : false; bool hov_query = false; - if (plot.Queried && !plot.Querying) { - ImRect bb_query; - if (HasFlag(plot.Flags, ImPlotFlags_PixelQuery)) { - bb_query = plot.QueryRect; - bb_query.Min += gp.BB_Grid.Min; - bb_query.Max += gp.BB_Grid.Min; - } - else { - UpdateTransformCache(); - ImVec2 p1 = PlotToPixels(plot.QueryRange.XMin, plot.QueryRange.YMin); - ImVec2 p2 = PlotToPixels(plot.QueryRange.XMax, plot.QueryRange.YMax); - bb_query.Min = ImVec2(ImMin(p1.x,p2.x), ImMin(p1.y,p2.y)); - bb_query.Max = ImVec2(ImMax(p1.x,p2.x), ImMax(p1.y,p2.y)); - } + if (gp.Hov_Frame && gp.Hov_Grid && plot.Queried && !plot.Querying) { + ImRect bb_query = plot.QueryRect; + + bb_query.Min += gp.BB_Grid.Min; + bb_query.Max += gp.BB_Grid.Min; + hov_query = bb_query.Contains(IO.MousePos); } @@ -775,97 +967,169 @@ bool BeginPlot(const char* title, const char* x_label, const char* y_label, cons } if (plot.DraggingQuery) { SetMouseCursor(ImGuiMouseCursor_ResizeAll); - if (!HasFlag(plot.Flags, ImPlotFlags_PixelQuery)) { - ImVec2 p1 = PlotToPixels(plot.QueryRange.XMin, plot.QueryRange.YMin); - ImVec2 p2 = PlotToPixels(plot.QueryRange.XMax, plot.QueryRange.YMax); - plot.QueryRect.Min = ImVec2(ImMin(p1.x,p2.x), ImMin(p1.y,p2.y)) + IO.MouseDelta; - plot.QueryRect.Max = ImVec2(ImMax(p1.x,p2.x), ImMax(p1.y,p2.y)) + IO.MouseDelta; - p1 = PixelsToPlot(plot.QueryRect.Min); - p2 = PixelsToPlot(plot.QueryRect.Max); - plot.QueryRect.Min -= gp.BB_Grid.Min; - plot.QueryRect.Max -= gp.BB_Grid.Min; - plot.QueryRange.XMin = ImMin(p1.x, p2.x); - plot.QueryRange.XMax = ImMax(p1.x, p2.x); - plot.QueryRange.YMin = ImMin(p1.y, p2.y); - plot.QueryRange.YMax = ImMax(p1.y, p2.y); - } - else { - plot.QueryRect.Min += IO.MouseDelta; - plot.QueryRect.Max += IO.MouseDelta; - } + plot.QueryRect.Min += IO.MouseDelta; + plot.QueryRect.Max += IO.MouseDelta; } - if (gp.Hov_Frame && hov_query && !plot.DraggingQuery && !plot.Selecting && !hov_legend) { + if (gp.Hov_Frame && gp.Hov_Grid && hov_query && !plot.DraggingQuery && !plot.Selecting && !hov_legend) { SetMouseCursor(ImGuiMouseCursor_ResizeAll); - if (IO.MouseDown[0] && !plot.XAxis.Dragging && !plot.YAxis.Dragging) { + const bool any_y_dragging = plot.YAxis[0].Dragging || plot.YAxis[1].Dragging || plot.YAxis[2].Dragging; + if (IO.MouseDown[0] && !plot.XAxis.Dragging && !any_y_dragging) { plot.DraggingQuery = true; } } + //QUERY X + bool hov_queryX[2]; + for (size_t i = 0; i < 2; i++) + { + hov_queryX[i] = plot.QueryRectX[i].Contains(IO.MousePos); + //x limits + bool xAtMax = false; + if (plot.QueryRectX[i].Min.x <= gp.BB_Grid.Min.x) { + plot.QueryRectX[i].Min.x = gp.BB_Grid.Min.x; + } + if (plot.QueryRectX[i].Max.x >= (gp.BB_Grid.Max.x)) { + plot.QueryRectX[i].Max.x = gp.BB_Grid.Max.x; + xAtMax = true; + } + //min cursor "line" width + if ((plot.QueryRectX[i].Max.x - plot.QueryRectX[i].Min.x) < 3) { + if (xAtMax) + plot.QueryRectX[i].Min.x = plot.QueryRectX[i].Max.x - 3; + else + plot.QueryRectX[i].Max.x = plot.QueryRectX[i].Min.x + 3; + } + //y locked to min max grid + plot.QueryRectX[i].Min.y = gp.BB_Grid.Min.y; + plot.QueryRectX[i].Max.y = gp.BB_Grid.Max.y; + // CURSOR1 DRAG ------------------------------------------------------------- + if (plot.DraggingQueryX[i] && (IO.MouseReleased[0] || !IO.MouseDown[0])) { + plot.DraggingQueryX[i] = false; + } + if (plot.DraggingQueryX[i]) { + ImGui::SetMouseCursor(ImGuiMouseCursor_ResizeEW); + plot.QueryRectX[i].Min += IO.MouseDelta; + plot.QueryRectX[i].Max += IO.MouseDelta; + //x limits + //if (plot.QueryRectX[i].Min.x < gp.BB_Grid.Min.x) plot.QueryRectX[i].Min.x = gp.BB_Grid.Min.x; + //if (plot.QueryRectX[i].Max.x > (gp.BB_Grid.Max.x - 10)) plot.QueryRectX[i].Max.x = gp.BB_Grid.Max.x - 10; + } + if (gp.Hov_Frame && hov_queryX[i] && !plot.DraggingQueryX[i] && !plot.Selecting && !plot.DraggingQuery && !hov_legend) { + ImGui::SetMouseCursor(ImGuiMouseCursor_ResizeEW); + if (IO.MouseDown[0] && !plot.XAxis.Dragging && !(plot.YAxis[0].Dragging || plot.YAxis[1].Dragging || plot.YAxis[2].Dragging)) { + //allow only one cursor dragging per time + if (i==0) + plot.DraggingQueryX[i] = !plot.DraggingQueryX[1]; + else + plot.DraggingQueryX[i] = !plot.DraggingQueryX[0]; + } + } + } + // DRAG INPUT ------------------------------------------------------------- + // end drags if (plot.XAxis.Dragging && (IO.MouseReleased[0] || !IO.MouseDown[0])) { plot.XAxis.Dragging = false; G.IO.MouseDragMaxDistanceSqr[0] = 0; } - if (plot.YAxis.Dragging && (IO.MouseReleased[0] || !IO.MouseDown[0])) { - plot.YAxis.Dragging = false; - G.IO.MouseDragMaxDistanceSqr[0] = 0; + for (int i = 0; i < MAX_Y_AXES; i++) { + if (plot.YAxis[i].Dragging && (IO.MouseReleased[0] || !IO.MouseDown[0])) { + plot.YAxis[i].Dragging = false; + G.IO.MouseDragMaxDistanceSqr[0] = 0; + } } + const bool any_y_dragging = plot.YAxis[0].Dragging || plot.YAxis[1].Dragging || plot.YAxis[2].Dragging; + const bool any_queryX_dragging = plot.DraggingQueryX[0] || plot.DraggingQueryX[1]; + bool drag_in_progress = plot.XAxis.Dragging || any_y_dragging || any_queryX_dragging; // do drag - if (plot.XAxis.Dragging || plot.YAxis.Dragging) { + if (drag_in_progress) { UpdateTransformCache(); - ImVec2 plot_tl = PixelsToPlot(gp.BB_Grid.Min - IO.MouseDelta); - ImVec2 plot_br = PixelsToPlot(gp.BB_Grid.Max - IO.MouseDelta); - if (!lock_x && plot.XAxis.Dragging) { - if (!lock_x_min) - plot.XAxis.Min = flip_x ? plot_br.x : plot_tl.x; - if (!lock_x_max) - plot.XAxis.Max = flip_x ? plot_tl.x : plot_br.x; + if (!x.lock && plot.XAxis.Dragging) { + ImVec2 plot_tl = PixelsToPlot(gp.BB_Grid.Min - IO.MouseDelta, 0); + ImVec2 plot_br = PixelsToPlot(gp.BB_Grid.Max - IO.MouseDelta, 0); + if (!x.lock_min) + plot.XAxis.Range.Min = x.flip ? plot_br.x : plot_tl.x; + if (!x.lock_max) + plot.XAxis.Range.Max = x.flip ? plot_tl.x : plot_br.x; } - if (!lock_y && plot.YAxis.Dragging) { - if (!lock_y_min) - plot.YAxis.Min = flip_y ? plot_tl.y : plot_br.y; - if (!lock_y_max) - plot.YAxis.Max = flip_y ? plot_br.y : plot_tl.y; + for (int i = 0; i < MAX_Y_AXES; i++) { + if (!y[i].lock && plot.YAxis[i].Dragging) { + ImVec2 plot_tl = PixelsToPlot(gp.BB_Grid.Min - IO.MouseDelta, i); + ImVec2 plot_br = PixelsToPlot(gp.BB_Grid.Max - IO.MouseDelta, i); + + if (!y[i].lock_min) + plot.YAxis[i].Range.Min = y[i].flip ? plot_tl.y : plot_br.y; + if (!y[i].lock_max) + plot.YAxis[i].Range.Max = y[i].flip ? plot_br.y : plot_tl.y; + } } - if ((lock_x && lock_y) || (lock_x && plot.XAxis.Dragging && !plot.YAxis.Dragging) || (lock_y && plot.YAxis.Dragging && !plot.XAxis.Dragging)) + // Set the mouse cursor based on which axes are moving. + int direction = 0; + if (!x.lock && plot.XAxis.Dragging) { + direction |= (1 << 1); + } + for (int i = 0; i < MAX_Y_AXES; i++) { + if (!y[i].present) { continue; } + if (!y[i].lock && plot.YAxis[i].Dragging) { + direction |= (1 << 2); + break; + } + } + + if (direction == 0) { ImGui::SetMouseCursor(ImGuiMouseCursor_NotAllowed); - else if (lock_x || (!plot.XAxis.Dragging && plot.YAxis.Dragging)) - ImGui::SetMouseCursor(ImGuiMouseCursor_ResizeNS); - else if (lock_y || (!plot.YAxis.Dragging && plot.XAxis.Dragging)) + } else if (direction == (1 << 1)) { ImGui::SetMouseCursor(ImGuiMouseCursor_ResizeEW); - else + } else if (direction == (1 << 2)) { + ImGui::SetMouseCursor(ImGuiMouseCursor_ResizeNS); + } else { ImGui::SetMouseCursor(ImGuiMouseCursor_ResizeAll); + } } // start drag - if (gp.Hov_Frame && hov_x_axis_region && IO.MouseDragMaxDistanceSqr[0] > 5 && !plot.Selecting && !hov_legend && !hov_query && !plot.DraggingQuery) - plot.XAxis.Dragging = true; - if (gp.Hov_Frame && hov_y_axis_region && IO.MouseDragMaxDistanceSqr[0] > 5 && !plot.Selecting && !hov_legend && !hov_query && !plot.DraggingQuery) - plot.YAxis.Dragging = true; + if (!drag_in_progress && gp.Hov_Frame && IO.MouseDragMaxDistanceSqr[0] > 5 && !plot.Selecting && !hov_legend && !hov_query && !plot.DraggingQuery) { + if (hov_x_axis_region) { + plot.XAxis.Dragging = true; + } + for (int i = 0; i < MAX_Y_AXES; i++) { + if (hov_y_axis_region[i]) { + plot.YAxis[i].Dragging = true; + } + } + } // SCROLL INPUT ----------------------------------------------------------- - if (gp.Hov_Frame && (hov_x_axis_region || hov_y_axis_region) && IO.MouseWheel != 0) { + if (gp.Hov_Frame && (hov_x_axis_region || any_hov_y_axis_region) && IO.MouseWheel != 0) { UpdateTransformCache(); float zoom_rate = 0.1f; if (IO.MouseWheel > 0) zoom_rate = (-zoom_rate) / (1.0f + (2.0f * zoom_rate)); float tx = Remap(IO.MousePos.x, gp.BB_Grid.Min.x, gp.BB_Grid.Max.x, 0, 1); float ty = Remap(IO.MousePos.y, gp.BB_Grid.Min.y, gp.BB_Grid.Max.y, 0, 1); - ImVec2 plot_tl = PixelsToPlot(gp.BB_Grid.Min - gp.BB_Grid.GetSize() * ImVec2(tx * zoom_rate, ty * zoom_rate)); - ImVec2 plot_br = PixelsToPlot(gp.BB_Grid.Max + gp.BB_Grid.GetSize() * ImVec2((1 - tx) * zoom_rate, (1 - ty) * zoom_rate)); - if (hov_x_axis_region && !lock_x) { - if (!lock_x_min) - plot.XAxis.Min = flip_x ? plot_br.x : plot_tl.x; - if (!lock_x_max) - plot.XAxis.Max = flip_x ? plot_tl.x : plot_br.x; - } - if (hov_y_axis_region && !lock_y) { - if (!lock_y_min) - plot.YAxis.Min = flip_y ? plot_tl.y : plot_br.y; - if (!lock_y_max) - plot.YAxis.Max = flip_y ? plot_br.y : plot_tl.y; + if (hov_x_axis_region && !x.lock) { + ImRect axis_scale = GetAxisScale(0, tx, ty, zoom_rate); + const ImVec2& plot_tl = axis_scale.Min; + const ImVec2& plot_br = axis_scale.Max; + + if (!x.lock_min) + plot.XAxis.Range.Min = x.flip ? plot_br.x : plot_tl.x; + if (!x.lock_max) + plot.XAxis.Range.Max = x.flip ? plot_tl.x : plot_br.x; + } + for (int i = 0; i < MAX_Y_AXES; i++) { + if (hov_y_axis_region[i] && !y[i].lock) { + ImRect axis_scale = GetAxisScale(i, tx, ty, zoom_rate); + const ImVec2& plot_tl = axis_scale.Min; + const ImVec2& plot_br = axis_scale.Max; + + if (!y[i].lock_min) + plot.YAxis[i].Range.Min = y[i].flip ? plot_tl.y : plot_br.y; + if (!y[i].lock_max) + plot.YAxis[i].Range.Max = y[i].flip ? plot_br.y : plot_tl.y; + } } } @@ -878,14 +1142,18 @@ bool BeginPlot(const char* title, const char* x_label, const char* y_label, cons if (HasFlag(plot.Flags, ImPlotFlags_Selection) && ImFabs(select_size.x) > 2 && ImFabs(select_size.y) > 2) { ImVec2 p1 = PixelsToPlot(plot.SelectStart); ImVec2 p2 = PixelsToPlot(IO.MousePos); - if (!lock_x_min && !IO.KeyAlt) - plot.XAxis.Min = ImMin(p1.x, p2.x); - if (!lock_x_max && !IO.KeyAlt) - plot.XAxis.Max = ImMax(p1.x, p2.x); - if (!lock_y_min && !IO.KeyShift) - plot.YAxis.Min = ImMin(p1.y, p2.y); - if (!lock_y_max && !IO.KeyShift) - plot.YAxis.Max = ImMax(p1.y, p2.y); + if (!x.lock_min && !IO.KeyAlt) + plot.XAxis.Range.Min = ImMin(p1.x, p2.x); + if (!x.lock_max && !IO.KeyAlt) + plot.XAxis.Range.Max = ImMax(p1.x, p2.x); + for (int i = 0; i < MAX_Y_AXES; i++) { + p1 = PixelsToPlot(plot.SelectStart, i); + p2 = PixelsToPlot(IO.MousePos, i); + if (!y[i].lock_min && !IO.KeyShift) + plot.YAxis[i].Range.Min = ImMin(p1.y, p2.y); + if (!y[i].lock_max && !IO.KeyShift) + plot.YAxis[i].Range.Max = ImMax(p1.y, p2.y); + } } plot.Selecting = false; } @@ -909,14 +1177,9 @@ bool BeginPlot(const char* title, const char* x_label, const char* y_label, cons plot.QueryRect.Max.x = IO.KeyAlt ? gp.BB_Grid.Max.x : ImMax(plot.QueryStart.x, IO.MousePos.x); plot.QueryRect.Min.y = IO.KeyShift ? gp.BB_Grid.Min.y : ImMin(plot.QueryStart.y, IO.MousePos.y); plot.QueryRect.Max.y = IO.KeyShift ? gp.BB_Grid.Max.y : ImMax(plot.QueryStart.y, IO.MousePos.y); - ImVec2 p1 = PixelsToPlot(plot.QueryRect.Min); - ImVec2 p2 = PixelsToPlot(plot.QueryRect.Max); + plot.QueryRect.Min -= gp.BB_Grid.Min; plot.QueryRect.Max -= gp.BB_Grid.Min; - plot.QueryRange.XMin = ImMin(p1.x, p2.x); - plot.QueryRange.XMax = ImMax(p1.x, p2.x); - plot.QueryRange.YMin = ImMin(p1.y, p2.y); - plot.QueryRange.YMax = ImMax(p1.y, p2.y); } // end query if (plot.Querying && (IO.MouseReleased[2] || IO.MouseReleased[1])) { @@ -926,45 +1189,45 @@ bool BeginPlot(const char* title, const char* x_label, const char* y_label, cons } else { plot.Queried = false; - plot.QueryRange = ImPlotRange(); } } // begin query - if ((gp.Hov_Frame && gp.Hov_Grid && IO.MouseClicked[2])) { + if (HasFlag(plot.Flags, ImPlotFlags_Query) && (gp.Hov_Frame && gp.Hov_Grid && IO.MouseClicked[2])) { plot.QueryRect = ImRect(0,0,0,0); - plot.QueryRange = ImPlotRange(); plot.Querying = true; plot.Queried = true; plot.QueryStart = IO.MousePos; } // toggle between select/query - if (plot.Selecting && IO.KeyCtrl) { + if (HasFlag(plot.Flags, ImPlotFlags_Query) && plot.Selecting && IO.KeyCtrl) { plot.Selecting = false; plot.QueryRect = ImRect(0,0,0,0); - plot.QueryRange = ImPlotRange(); plot.Querying = true; plot.Queried = true; plot.QueryStart = plot.SelectStart; } - if (plot.Querying && !IO.KeyCtrl && !IO.MouseDown[2]) { + if (HasFlag(plot.Flags, ImPlotFlags_Selection) && plot.Querying && !IO.KeyCtrl && !IO.MouseDown[2]) { plot.Selecting = true; plot.Querying = false; plot.Queried = false; plot.QueryRect = ImRect(0,0,0,0); - plot.QueryRange = ImPlotRange(); } // DOUBLE CLICK ----------------------------------------------------------- - if ( IO.MouseDoubleClicked[0] && gp.Hov_Frame && (hov_x_axis_region || hov_y_axis_region) && !hov_legend && !hov_query) { + if ( IO.MouseDoubleClicked[0] && gp.Hov_Frame && (hov_x_axis_region || any_hov_y_axis_region) && !hov_legend && !hov_query) { gp.FitThisFrame = true; gp.FitX = hov_x_axis_region; - gp.FitY = hov_y_axis_region; + for (int i = 0; i < MAX_Y_AXES; i++) { + gp.FitY[i] = hov_y_axis_region[i]; + } } else { gp.FitThisFrame = false; gp.FitX = false; - gp.FitY = false; + for (int i = 0; i < MAX_Y_AXES; i++) { + gp.FitY[i] = false; + } } // FOCUS ------------------------------------------------------------------ @@ -976,7 +1239,9 @@ bool BeginPlot(const char* title, const char* x_label, const char* y_label, cons UpdateTransformCache(); // set mouse position - gp.LastMousePos = PixelsToPlot(IO.MousePos); + for (int i = 0; i < MAX_Y_AXES; i++) { + gp.LastMousePos[i] = PixelsToPlot(IO.MousePos, i); + } // RENDER ----------------------------------------------------------------- @@ -988,23 +1253,27 @@ bool BeginPlot(const char* title, const char* x_label, const char* y_label, cons // transform ticks if (gp.RenderX) { - for (auto& xt : gp.XTicks) - xt.PixelPos = PlotToPixels((float)xt.PlotPos, 0).x; + for (ImTick& xt : gp.XTicks) + xt.PixelPos = PlotToPixels((float)xt.PlotPos, 0, 0).x; } - if (gp.RenderY) { - for (auto& yt : gp.YTicks) - yt.PixelPos = PlotToPixels(0, (float)yt.PlotPos).y; + for (int i = 0; i < MAX_Y_AXES; i++) { + if (gp.RenderY[i]) { + for (ImTick& yt : gp.YTicks[i]) + yt.PixelPos = PlotToPixels(0, (float)yt.PlotPos, i).y; + } } // render grid if (HasFlag(plot.XAxis.Flags, ImAxisFlags_GridLines)) { - for (auto &xt : gp.XTicks) - DrawList.AddLine({xt.PixelPos, gp.BB_Grid.Min.y}, {xt.PixelPos, gp.BB_Grid.Max.y}, xt.Major ? gp.Col_XMajor : gp.Col_XMinor, 1); + for (ImTick &xt : gp.XTicks) + DrawList.AddLine({xt.PixelPos, gp.BB_Grid.Min.y}, {xt.PixelPos, gp.BB_Grid.Max.y}, xt.Major ? gp.Col_X.Major : gp.Col_X.Minor, 1); } - if (HasFlag(plot.YAxis.Flags, ImAxisFlags_GridLines)) { - for (auto &yt : gp.YTicks) - DrawList.AddLine({gp.BB_Grid.Min.x, yt.PixelPos}, {gp.BB_Grid.Max.x, yt.PixelPos}, yt.Major ? gp.Col_YMajor : gp.Col_YMinor, 1); + for (int i = 0; i < MAX_Y_AXES; i++) { + if (y[i].present && HasFlag(plot.YAxis[i].Flags, ImAxisFlags_GridLines)) { + for (ImTick &yt : gp.YTicks[i]) + DrawList.AddLine({gp.BB_Grid.Min.x, yt.PixelPos}, {gp.BB_Grid.Max.x, yt.PixelPos}, yt.Major ? gp.Col_Y[i].Major : gp.Col_Y[i].Minor, 1); + } } PopPlotClipRect(); @@ -1017,9 +1286,9 @@ bool BeginPlot(const char* title, const char* x_label, const char* y_label, cons // render labels if (HasFlag(plot.XAxis.Flags, ImAxisFlags_TickLabels)) { PushClipRect(gp.BB_Frame.Min, gp.BB_Frame.Max, true); - for (auto &xt : gp.XTicks) { + for (ImTick &xt : gp.XTicks) { if (xt.RenderLabel && xt.PixelPos >= gp.BB_Grid.Min.x - 1 && xt.PixelPos <= gp.BB_Grid.Max.x + 1) - DrawList.AddText({xt.PixelPos - xt.Size.x * 0.5f, gp.BB_Grid.Max.y + txt_off}, gp.Col_XTxt, gp.XTickLabels.Buf.Data + xt.TextOffset); + DrawList.AddText({xt.PixelPos - xt.Size.x * 0.5f, gp.BB_Grid.Max.y + txt_off}, gp.Col_X.Txt, gp.XTickLabels.Buf.Data + xt.TextOffset); } ImGui::PopClipRect(); } @@ -1027,20 +1296,29 @@ bool BeginPlot(const char* title, const char* x_label, const char* y_label, cons const ImVec2 xLabel_size = CalcTextSize(x_label); const ImVec2 xLabel_pos(gp.BB_Grid.GetCenter().x - xLabel_size.x * 0.5f, gp.BB_Canvas.Max.y - txt_height); - DrawList.AddText(xLabel_pos, gp.Col_XTxt, x_label); + DrawList.AddText(xLabel_pos, gp.Col_X.Txt, x_label); } - if (HasFlag(plot.YAxis.Flags, ImAxisFlags_TickLabels)) { - PushClipRect(gp.BB_Frame.Min, gp.BB_Frame.Max, true); - for (auto &yt : gp.YTicks) { - if (yt.RenderLabel && yt.PixelPos >= gp.BB_Grid.Min.y - 1 && yt.PixelPos <= gp.BB_Grid.Max.y + 1) - DrawList.AddText({gp.BB_Grid.Min.x - txt_off - yt.Size.x, yt.PixelPos - 0.5f * yt.Size.y}, gp.Col_YTxt, gp.YTickLabels.Buf.Data + yt.TextOffset); + PushClipRect(gp.BB_Frame.Min, gp.BB_Frame.Max, true); + for (int i = 0; i < MAX_Y_AXES; i++) { + if (y[i].present && HasFlag(plot.YAxis[i].Flags, ImAxisFlags_TickLabels)) { + const float x_start = + gp.AxisLabelReference[i] + + ((i == 0) ? + (-txt_off - max_label_width[0]) : + txt_off); + for (ImTick &yt : gp.YTicks[i]) { + if (yt.RenderLabel && yt.PixelPos >= gp.BB_Grid.Min.y - 1 && yt.PixelPos <= gp.BB_Grid.Max.y + 1) { + ImVec2 start(x_start, yt.PixelPos - 0.5f * yt.Size.y); + DrawList.AddText(start, gp.Col_Y[i].Txt, gp.YTickLabels[i].Buf.Data + yt.TextOffset); + } + } } - ImGui::PopClipRect(); } + ImGui::PopClipRect(); if (y_label) { const ImVec2 yLabel_size = CalcTextSizeVertical(y_label); const ImVec2 yLabel_pos(gp.BB_Canvas.Min.x, gp.BB_Grid.GetCenter().y + yLabel_size.y * 0.5f); - AddTextVertical(&DrawList, y_label, yLabel_pos, gp.Col_YTxt); + AddTextVertical(&DrawList, y_label, yLabel_pos, gp.Col_Y[0].Txt); } // PREP ------------------------------------------------------------------- @@ -1050,10 +1328,12 @@ bool BeginPlot(const char* title, const char* x_label, const char* y_label, cons // reset items count gp.VisibleItemCount = 0; // reset extents - gp.Extents.Min.x = INFINITY; - gp.Extents.Min.y = INFINITY; - gp.Extents.Max.x = -INFINITY; - gp.Extents.Max.y = -INFINITY; + gp.ExtentsX.Min = INFINITY; + gp.ExtentsX.Max = -INFINITY; + for (int i = 0; i < MAX_Y_AXES; i++) { + gp.ExtentsY[i].Min = INFINITY; + gp.ExtentsY[i].Max = -INFINITY; + } // clear item names gp.LegendLabels.Buf.resize(0); // reset digital plot items count @@ -1082,7 +1362,7 @@ inline void AxisMenu(ImPlotAxis& Axis) { ImGui::PushItemFlag(ImGuiItemFlags_Disabled, true); ImGui::PushStyleVar(ImGuiStyleVar_Alpha, ImGui::GetStyle().Alpha * 0.25f); } - ImGui::DragFloat("Min", &Axis.Min, 0.01f + 0.01f * (Axis.Max - Axis.Min), -INFINITY, Axis.Max - FLT_EPSILON); + ImGui::DragFloat("Min", &Axis.Range.Min, 0.01f + 0.01f * (Axis.Range.Size()), -INFINITY, Axis.Range.Max - FLT_EPSILON); if (lock_min) { ImGui::PopItemFlag(); ImGui::PopStyleVar(); } @@ -1093,7 +1373,7 @@ inline void AxisMenu(ImPlotAxis& Axis) { if (lock_max) { ImGui::PushItemFlag(ImGuiItemFlags_Disabled, true); ImGui::PushStyleVar(ImGuiStyleVar_Alpha, ImGui::GetStyle().Alpha * 0.25f); } - ImGui::DragFloat("Max", &Axis.Max, 0.01f + 0.01f * (Axis.Max - Axis.Min), Axis.Min + FLT_EPSILON, INFINITY); + ImGui::DragFloat("Max", &Axis.Range.Max, 0.01f + 0.01f * (Axis.Range.Size()), Axis.Range.Min + FLT_EPSILON, INFINITY); if (lock_max) { ImGui::PopItemFlag(); ImGui::PopStyleVar(); @@ -1119,20 +1399,32 @@ void PlotContextMenu(ImPlot& plot) { PopID(); ImGui::EndMenu(); } - if (ImGui::BeginMenu("Y-Axis")) { - PushID("Y"); - AxisMenu(plot.YAxis); - PopID(); - ImGui::EndMenu(); + for (int i = 0; i < MAX_Y_AXES; i++) { + if (i == 1 && !HasFlag(plot.Flags, ImPlotFlags_YAxis2)) { + continue; + } + if (i == 2 && !HasFlag(plot.Flags, ImPlotFlags_YAxis3)) { + continue; + } + char buf[10] = {}; + if (i == 0) { + snprintf(buf, sizeof(buf) - 1, "Y-Axis"); + } else { + snprintf(buf, sizeof(buf) - 1, "Y-Axis %d", i + 1); + } + if (ImGui::BeginMenu(buf)) { + PushID(i); + AxisMenu(plot.YAxis[i]); + PopID(); + ImGui::EndMenu(); + } } + ImGui::Separator(); if ((ImGui::BeginMenu("Settings"))) { if (ImGui::MenuItem("Box Select",NULL,HasFlag(plot.Flags, ImPlotFlags_Selection))) { FlipFlag(plot.Flags, ImPlotFlags_Selection); } - if (ImGui::MenuItem("Pixel Query",NULL,HasFlag(plot.Flags, ImPlotFlags_PixelQuery))) { - FlipFlag(plot.Flags, ImPlotFlags_PixelQuery); - } if (ImGui::MenuItem("Crosshairs",NULL,HasFlag(plot.Flags, ImPlotFlags_Crosshairs))) { FlipFlag(plot.Flags, ImPlotFlags_Crosshairs); } @@ -1163,6 +1455,30 @@ void PlotContextMenu(ImPlot& plot) { } +namespace { +class BufferWriter { + public: + BufferWriter(char* buffer, size_t size) : Buffer(buffer), Pos(0), Size(size) {} + + void Write(const char* fmt, ...) IM_FMTARGS(2) { + va_list argp; + va_start(argp, fmt); + VWrite(fmt, argp); + va_end(argp); + } + + private: + void VWrite(const char* fmt, va_list argp) { + const int written = ::vsnprintf(&Buffer[Pos], Size - Pos - 1, fmt, argp); + Pos += written; + } + + char* const Buffer; + size_t Pos; + const size_t Size; +}; +} + //----------------------------------------------------------------------------- // EndPlot() //----------------------------------------------------------------------------- @@ -1179,30 +1495,63 @@ void EndPlot() { // AXIS STATES ------------------------------------------------------------ - const bool lock_x_min = HasFlag(plot.XAxis.Flags, ImAxisFlags_LockMin); - const bool lock_x_max = HasFlag(plot.XAxis.Flags, ImAxisFlags_LockMax); - const bool lock_x = (lock_x_min && lock_x_max) || (gp.NextPlotData.HasXRange && gp.NextPlotData.XRangeCond == ImGuiCond_Always); + AxisState x(plot.XAxis, gp.NextPlotData.HasXRange, gp.NextPlotData.XRangeCond, true, 0); + AxisState y[MAX_Y_AXES]; + y[0] = AxisState(plot.YAxis[0], gp.NextPlotData.HasYRange[0], gp.NextPlotData.YRangeCond[0], true, 0); + y[1] = AxisState(plot.YAxis[1], gp.NextPlotData.HasYRange[1], gp.NextPlotData.YRangeCond[1], + HasFlag(plot.Flags, ImPlotFlags_YAxis2), y[0].present_so_far); + y[2] = AxisState(plot.YAxis[2], gp.NextPlotData.HasYRange[2], gp.NextPlotData.YRangeCond[2], + HasFlag(plot.Flags, ImPlotFlags_YAxis3), y[1].present_so_far); - const bool lock_y_min = HasFlag(plot.YAxis.Flags, ImAxisFlags_LockMin); - const bool lock_y_max = HasFlag(plot.YAxis.Flags, ImAxisFlags_LockMax); - const bool lock_y = (lock_y_min && lock_y_max) || (gp.NextPlotData.HasYRange && gp.NextPlotData.YRangeCond == ImGuiCond_Always); - - const bool lock_plot = lock_x && lock_y; + const bool lock_plot = x.lock && y[0].lock && y[1].lock && y[2].lock; + const bool any_y_locked = y[0].lock || y[1].lock || y[2].lock; + const bool any_y_dragging = plot.YAxis[0].Dragging || plot.YAxis[1].Dragging || plot.YAxis[2].Dragging; // FINAL RENDER ----------------------------------------------------------- - PushPlotClipRect(); + PushClipRect(gp.BB_Grid.Min, gp.BB_Frame.Max, true); // render ticks + PushPlotClipRect(); if (HasFlag(plot.XAxis.Flags, ImAxisFlags_TickMarks)) { - for (auto &xt : gp.XTicks) + for (ImTick &xt : gp.XTicks) DrawList.AddLine({xt.PixelPos, gp.BB_Grid.Max.y},{xt.PixelPos, gp.BB_Grid.Max.y - (xt.Major ? 10.0f : 5.0f)}, gp.Col_Border, 1); } - if (HasFlag(plot.YAxis.Flags, ImAxisFlags_TickMarks)) { - for (auto &yt : gp.YTicks) - DrawList.AddLine({gp.BB_Grid.Min.x, yt.PixelPos}, {gp.BB_Grid.Min.x + (yt.Major ? 10.0f : 5.0f), yt.PixelPos}, gp.Col_Border, 1); + PopPlotClipRect(); + + PushClipRect(gp.BB_Grid.Min, {gp.BB_Frame.Max.x, gp.BB_Grid.Max.y}, true); + int axis_count = 0; + for (int i = 0; i < MAX_Y_AXES; i++) { + if (!y[i].present) { continue; } + axis_count++; + + if (!HasFlag(plot.YAxis[i].Flags, ImAxisFlags_TickMarks)) { continue; } + + float x_start = gp.AxisLabelReference[i]; + float direction = (i == 0) ? 1.0f : -1.0f; + bool no_major = axis_count >= 3; + + for (ImTick &yt : gp.YTicks[i]) { + ImVec2 start = ImVec2(x_start, yt.PixelPos); + + DrawList.AddLine( + start, + start + ImVec2(direction * ((!no_major && yt.Major) ? 10.0f : 5.0f), 0), + gp.Col_Border, 1); + } + + if (axis_count >= 3) { + // Draw a bar next to the ticks to act as a visual separator. + DrawList.AddLine( + ImVec2(x_start, gp.BB_Grid.Min.y), + ImVec2(x_start, gp.BB_Grid.Max.y), + gp.Col_Border, 1); + } } + PopClipRect(); + + PushPlotClipRect(); // render selection/query if (plot.Selecting) { ImRect select_bb(ImMin(IO.MousePos, plot.SelectStart), ImMax(IO.MousePos, plot.SelectStart)); @@ -1211,11 +1560,11 @@ void EndPlot() { DrawList.AddRectFilled(gp.BB_Grid.Min, gp.BB_Grid.Max, gp.Col_SlctBg); DrawList.AddRect( gp.BB_Grid.Min, gp.BB_Grid.Max, gp.Col_SlctBd); } - else if ((lock_x || IO.KeyAlt) && select_bb.GetHeight() > 2) { + else if ((x.lock || IO.KeyAlt) && select_bb.GetHeight() > 2) { DrawList.AddRectFilled(ImVec2(gp.BB_Grid.Min.x, select_bb.Min.y), ImVec2(gp.BB_Grid.Max.x, select_bb.Max.y), gp.Col_SlctBg); DrawList.AddRect( ImVec2(gp.BB_Grid.Min.x, select_bb.Min.y), ImVec2(gp.BB_Grid.Max.x, select_bb.Max.y), gp.Col_SlctBd); } - else if ((lock_y || IO.KeyShift) && select_bb.GetWidth() > 2) { + else if ((any_y_locked || IO.KeyShift) && select_bb.GetWidth() > 2) { DrawList.AddRectFilled(ImVec2(select_bb.Min.x, gp.BB_Grid.Min.y), ImVec2(select_bb.Max.x, gp.BB_Grid.Max.y), gp.Col_SlctBg); DrawList.AddRect( ImVec2(select_bb.Min.x, gp.BB_Grid.Min.y), ImVec2(select_bb.Max.x, gp.BB_Grid.Max.y), gp.Col_SlctBd); } @@ -1226,19 +1575,59 @@ void EndPlot() { } } - if (plot.Querying || (HasFlag(plot.Flags, ImPlotFlags_PixelQuery) && plot.Queried)) { + if (plot.Querying || plot.Queried) { if (plot.QueryRect.GetWidth() > 2 && plot.QueryRect.GetHeight() > 2) { DrawList.AddRectFilled(plot.QueryRect.Min + gp.BB_Grid.Min, plot.QueryRect.Max + gp.BB_Grid.Min, gp.Col_QryBg); DrawList.AddRect( plot.QueryRect.Min + gp.BB_Grid.Min, plot.QueryRect.Max + gp.BB_Grid.Min, gp.Col_QryBd); } } else if (plot.Queried) { - ImVec2 p1 = PlotToPixels(plot.QueryRange.XMin, plot.QueryRange.YMin); - ImVec2 p2 = PlotToPixels(plot.QueryRange.XMax, plot.QueryRange.YMax); - ImVec2 Min(ImMin(p1.x,p2.x), ImMin(p1.y,p2.y)); - ImVec2 Max(ImMax(p1.x,p2.x), ImMax(p1.y,p2.y)); - DrawList.AddRectFilled(Min, Max, gp.Col_QryBg); - DrawList.AddRect( Min, Max, gp.Col_QryBd); + ImRect bb_query = plot.QueryRect; + + bb_query.Min += gp.BB_Grid.Min; + bb_query.Max += gp.BB_Grid.Min; + + DrawList.AddRectFilled(bb_query.Min, bb_query.Max, gp.Col_QryBg); + DrawList.AddRect( bb_query.Min, bb_query.Max, gp.Col_QryBd); + } + + //render query x-axis + if (HasFlag(plot.Flags, ImPlotFlags_QueryX)) { + //render x1 + const int lineOffset = -1; + ImVec2 p1, p2; + p1.x = plot.QueryRectX[0].Min.x; + p1.y = plot.QueryRectX[0].Min.y; + p2 = p1 + ImVec2(3,10); + DrawList.AddRectFilled(p1, p2, gp.Col_QryX);//upper rectangle + p1.x = plot.QueryRectX[0].Min.x; + p1.y = plot.QueryRectX[0].Max.y; + p2 = p1 + ImVec2(3,-10); + DrawList.AddRectFilled(p1, p2, gp.Col_QryX);//lower rectangle + p1.x = plot.QueryRectX[0].Min.x; + p1.y = plot.QueryRectX[0].Min.y; + p1 += ImVec2(1, 10 -lineOffset); + p2.x = plot.QueryRectX[0].Min.x; + p2.y = plot.QueryRectX[0].Max.y; + p2 += ImVec2(1, -10 +lineOffset); + DrawList.AddLine(p1, p2, gp.Col_QryX);//line + + //render x2 + p1.x = plot.QueryRectX[1].Min.x; + p1.y = plot.QueryRectX[1].Min.y; + p2 = p1 + ImVec2(3,10); + DrawList.AddRectFilled(p1, p2, gp.Col_QryX);//upper rectangle + p1.x = plot.QueryRectX[1].Min.x; + p1.y = plot.QueryRectX[1].Max.y; + p2 = p1 + ImVec2(3,-10); + DrawList.AddRectFilled(p1, p2, gp.Col_QryX);//lower rectangle + p1.x = plot.QueryRectX[1].Min.x; + p1.y = plot.QueryRectX[1].Min.y; + p1 += ImVec2(1, 10 -lineOffset); + p2.x = plot.QueryRectX[1].Min.x; + p2.y = plot.QueryRectX[1].Max.y; + p2 += ImVec2(1, -10 +lineOffset); + DrawList.AddLine(p1, p2, gp.Col_QryX);//line } // render legend @@ -1254,7 +1643,7 @@ void EndPlot() { float max_label_width = 0; for (int i = 0; i < nItems; ++i) { const char* label = GetLegendLabel(i); - auto labelWidth = CalcTextSize(label, NULL, true); + ImVec2 labelWidth = CalcTextSize(label, NULL, true); max_label_width = labelWidth.x > max_label_width ? labelWidth.x : max_label_width; } legend_content_bb = ImRect(gp.BB_Grid.Min + legend_offset, gp.BB_Grid.Min + legend_offset + ImVec2(max_label_width, nItems * txt_ht)); @@ -1281,7 +1670,7 @@ void EndPlot() { item->Highlight = false; ImU32 iconColor; if (hov_legend && icon_bb.Contains(IO.MousePos)) { - auto colAlpha = item->Color; + ImVec4 colAlpha = item->Color; colAlpha.w = 0.5f; iconColor = item->Show ? GetColorU32(colAlpha) : GetColorU32(ImGuiCol_TextDisabled, 0.5f); @@ -1301,7 +1690,7 @@ void EndPlot() { // render crosshairs if (HasFlag(plot.Flags, ImPlotFlags_Crosshairs) && gp.Hov_Grid && gp.Hov_Frame && - !(plot.XAxis.Dragging || plot.YAxis.Dragging) && !plot.Selecting && !plot.Querying && !hov_legend) { + !(plot.XAxis.Dragging || any_y_dragging) && !plot.Selecting && !plot.Querying && !hov_legend) { ImGui::SetMouseCursor(ImGuiMouseCursor_None); ImVec2 xy = IO.MousePos; ImVec2 h1(gp.BB_Grid.Min.x, xy.y); @@ -1320,8 +1709,16 @@ void EndPlot() { // render mouse pos if (HasFlag(plot.Flags, ImPlotFlags_MousePos) && gp.Hov_Grid) { - static char buffer[128]; - sprintf(buffer, "%.2f,%.2f", gp.LastMousePos.x, gp.LastMousePos.y); + static char buffer[128] = {}; + BufferWriter writer(buffer, sizeof(buffer)); + + writer.Write("%.2f,%.2f", gp.LastMousePos[0].x, gp.LastMousePos[0].y); + if (HasFlag(plot.Flags, ImPlotFlags_YAxis2)) { + writer.Write(",(%.2f)", gp.LastMousePos[1].y); + } + if (HasFlag(plot.Flags, ImPlotFlags_YAxis3)) { + writer.Write(",(%.2f)", gp.LastMousePos[2].y); + } ImVec2 size = CalcTextSize(buffer); ImVec2 pos = gp.BB_Grid.Max - size - ImVec2(5, 5); DrawList.AddText(pos, gp.Col_Txt, buffer); @@ -1335,14 +1732,20 @@ void EndPlot() { // FIT DATA -------------------------------------------------------------- if (gp.FitThisFrame && (gp.VisibleItemCount > 0 || plot.Queried)) { - if (gp.FitX && !HasFlag(plot.XAxis.Flags, ImAxisFlags_LockMin) && !NanOrInf(gp.Extents.Min.x)) - plot.XAxis.Min = gp.Extents.Min.x; - if (gp.FitX && !HasFlag(plot.XAxis.Flags, ImAxisFlags_LockMax) && !NanOrInf(gp.Extents.Max.x)) - plot.XAxis.Max = gp.Extents.Max.x; - if (gp.FitY && !HasFlag(plot.YAxis.Flags, ImAxisFlags_LockMin) && !NanOrInf(gp.Extents.Min.y)) - plot.YAxis.Min = gp.Extents.Min.y; - if (gp.FitY && !HasFlag(plot.YAxis.Flags, ImAxisFlags_LockMax) && !NanOrInf(gp.Extents.Max.y)) - plot.YAxis.Max = gp.Extents.Max.y; + if (gp.FitX && !HasFlag(plot.XAxis.Flags, ImAxisFlags_LockMin) && !NanOrInf(gp.ExtentsX.Min)) { + plot.XAxis.Range.Min = gp.ExtentsX.Min; + } + if (gp.FitX && !HasFlag(plot.XAxis.Flags, ImAxisFlags_LockMax) && !NanOrInf(gp.ExtentsX.Max)) { + plot.XAxis.Range.Max = gp.ExtentsX.Max; + } + for (int i = 0; i < MAX_Y_AXES; i++) { + if (gp.FitY[i] && !HasFlag(plot.YAxis[i].Flags, ImAxisFlags_LockMin) && !NanOrInf(gp.ExtentsY[i].Min)) { + plot.YAxis[i].Range.Min = gp.ExtentsY[i].Min; + } + if (gp.FitY[i] && !HasFlag(plot.YAxis[i].Flags, ImAxisFlags_LockMax) && !NanOrInf(gp.ExtentsY[i].Max)) { + plot.YAxis[i].Range.Max = gp.ExtentsY[i].Max; + } + } } // CONTEXT MENU ----------------------------------------------------------- @@ -1372,25 +1775,32 @@ void EndPlot() { // MISC API //----------------------------------------------------------------------------- -void SetNextPlotRange(float x_min, float x_max, float y_min, float y_max, ImGuiCond cond) { - SetNextPlotRangeX(x_min, x_max, cond); - SetNextPlotRangeY(y_min, y_max, cond); +void SetNextPlotLimits(float x_min, float x_max, float y_min, float y_max, ImGuiCond cond) { + SetNextPlotLimitsX(x_min, x_max, cond); + SetNextPlotLimitsY(y_min, y_max, cond); } -void SetNextPlotRangeX(float x_min, float x_max, ImGuiCond cond) { +void SetNextPlotLimitsX(float x_min, float x_max, ImGuiCond cond) { IM_ASSERT(cond == 0 || ImIsPowerOfTwo(cond)); // Make sure the user doesn't attempt to combine multiple condition flags. gp.NextPlotData.HasXRange = true; gp.NextPlotData.XRangeCond = cond; - gp.NextPlotData.XMin = x_min; - gp.NextPlotData.XMax = x_max; + gp.NextPlotData.X.Min = x_min; + gp.NextPlotData.X.Max = x_max; } -void SetNextPlotRangeY(float y_min, float y_max, ImGuiCond cond) { +void SetNextPlotLimitsY(float y_min, float y_max, ImGuiCond cond, int y_axis) { + IM_ASSERT_USER_ERROR(y_axis >= 0 && y_axis < MAX_Y_AXES, "y_axis Needs to be between 0 and MAX_Y_AXES"); IM_ASSERT(cond == 0 || ImIsPowerOfTwo(cond)); // Make sure the user doesn't attempt to combine multiple condition flags. - gp.NextPlotData.HasYRange = true; - gp.NextPlotData.YRangeCond = cond; - gp.NextPlotData.YMin = y_min; - gp.NextPlotData.YMax = y_max; + gp.NextPlotData.HasYRange[y_axis] = true; + gp.NextPlotData.YRangeCond[y_axis] = cond; + gp.NextPlotData.Y[y_axis].Min = y_min; + gp.NextPlotData.Y[y_axis].Max = y_max; +} + +void SetPlotYAxis(int y_axis) { + IM_ASSERT_USER_ERROR(y_axis >= 0 && y_axis < MAX_Y_AXES, "y_axis Needs to be between 0 and MAX_Y_AXES"); + IM_ASSERT_USER_ERROR(gp.CurrentPlot != NULL, "SetPlotYAxis() Needs to be called between BeginPlot() and EndPlot()!"); + gp.CurrentPlot->CurrentYAxis = y_axis; } ImVec2 GetPlotPos() { @@ -1416,21 +1826,24 @@ bool IsPlotHovered() { IM_ASSERT_USER_ERROR(gp.CurrentPlot != NULL, "IsPlotHovered() Needs to be called between BeginPlot() and EndPlot()!"); return gp.Hov_Grid; } -ImVec2 GetPlotMousePos() { +ImVec2 GetPlotMousePos(int y_axis_in) { + IM_ASSERT_USER_ERROR(y_axis_in >= -1 && y_axis_in < MAX_Y_AXES, "y_axis needs to between -1 and MAX_Y_AXES"); IM_ASSERT_USER_ERROR(gp.CurrentPlot != NULL, "GetPlotMousePos() Needs to be called between BeginPlot() and EndPlot()!"); - return gp.LastMousePos; + const int y_axis = y_axis_in >= 0 ? y_axis_in : gp.CurrentPlot->CurrentYAxis; + return gp.LastMousePos[y_axis]; } -ImPlotRange GetPlotRange() { - IM_ASSERT_USER_ERROR(gp.CurrentPlot != NULL, "GetPlotRange() Needs to be called between BeginPlot() and EndPlot()!"); +ImPlotLimits GetPlotLimits(int y_axis_in) { + IM_ASSERT_USER_ERROR(y_axis_in >= -1 && y_axis_in < MAX_Y_AXES, "y_axis needs to between -1 and MAX_Y_AXES"); + IM_ASSERT_USER_ERROR(gp.CurrentPlot != NULL, "GetPlotLimits() Needs to be called between BeginPlot() and EndPlot()!"); + const int y_axis = y_axis_in >= 0 ? y_axis_in : gp.CurrentPlot->CurrentYAxis; + ImPlot& plot = *gp.CurrentPlot; - ImPlotRange range; - range.XMin = plot.XAxis.Min; - range.XMax = plot.XAxis.Max; - range.YMin = plot.YAxis.Min; - range.YMax = plot.YAxis.Max; - return range; + ImPlotLimits limits; + limits.X = plot.XAxis.Range; + limits.Y = plot.YAxis[y_axis].Range; + return limits; } bool IsPlotQueried() { @@ -1438,21 +1851,35 @@ bool IsPlotQueried() { return gp.CurrentPlot->Queried; } -ImPlotRange GetPlotQuery() { +ImPlotLimits GetPlotQuery(int y_axis_in) { + IM_ASSERT_USER_ERROR(y_axis_in >= -1 && y_axis_in < MAX_Y_AXES, "y_axis needs to between -1 and MAX_Y_AXES"); IM_ASSERT_USER_ERROR(gp.CurrentPlot != NULL, "GetPlotQuery() Needs to be called between BeginPlot() and EndPlot()!"); ImPlot& plot = *gp.CurrentPlot; - if (HasFlag(plot.Flags, ImPlotFlags_PixelQuery)) { - UpdateTransformCache(); - ImVec2 p1 = PixelsToPlot(plot.QueryRect.Min + gp.BB_Grid.Min); - ImVec2 p2 = PixelsToPlot(plot.QueryRect.Max + gp.BB_Grid.Min); - plot.QueryRange.XMin = ImMin(p1.x, p2.x); - plot.QueryRange.XMax = ImMax(p1.x, p2.x); - plot.QueryRange.YMin = ImMin(p1.y, p2.y); - plot.QueryRange.YMax = ImMax(p1.y, p2.y); - } - return plot.QueryRange; + const int y_axis = y_axis_in >= 0 ? y_axis_in : gp.CurrentPlot->CurrentYAxis; + + UpdateTransformCache(); + ImVec2 p1 = PixelsToPlot(plot.QueryRect.Min + gp.BB_Grid.Min, y_axis); + ImVec2 p2 = PixelsToPlot(plot.QueryRect.Max + gp.BB_Grid.Min, y_axis); + + ImPlotLimits result; + result.X.Min = ImMin(p1.x, p2.x); + result.X.Max = ImMax(p1.x, p2.x); + result.Y.Min = ImMin(p1.y, p2.y); + result.Y.Max = ImMax(p1.y, p2.y); + return result; } +ImPlotLimits GetPlotQueryX() { + IM_ASSERT_USER_ERROR(gp.CurrentPlot != NULL, "GetPlotQueryX() Needs to be called between BeginPlot() and EndPlot()!"); + ImPlot& plot = *gp.CurrentPlot; + ImVec2 x1 = plot.QueryRectX[0].Min; + ImVec2 x2 = plot.QueryRectX[1].Min; + x1.x += 1; + x2.x += 1; + plot.QueryRangeX.X.Min = PixelsToPlot(x1).x; + plot.QueryRangeX.X.Max = PixelsToPlot(x2).x; + return plot.QueryRangeX; +} //----------------------------------------------------------------------------- // STYLING //----------------------------------------------------------------------------- @@ -1610,7 +2037,7 @@ inline void MarkerGeneral(ImDrawList& DrawList, ImVec2* points, int n, const ImV } } -inline void MakerCircle(ImDrawList& DrawList, const ImVec2& c, float s, bool outline, ImU32 col_outline, bool fill, ImU32 col_fill, float weight) { +inline void MarkerCircle(ImDrawList& DrawList, const ImVec2& c, float s, bool outline, ImU32 col_outline, bool fill, ImU32 col_fill, float weight) { ImVec2 marker[10] = {{1.0f, 0.0f}, {0.809017f, 0.58778524f}, {0.30901697f, 0.95105654f}, @@ -1677,16 +2104,16 @@ inline void MarkerCross(ImDrawList& DrawList, const ImVec2& c, float s, bool out } template -inline void RenderMarkers(ImDrawList& DrawList, Getter getter, int count, int offset, bool rend_mk_line, ImU32 col_mk_line, bool rend_mk_fill, ImU32 col_mk_fill, bool cull) { +inline void RenderMarkers(Transformer transformer, ImDrawList& DrawList, Getter getter, int count, int offset, bool rend_mk_line, ImU32 col_mk_line, bool rend_mk_fill, ImU32 col_mk_fill, bool cull) { int idx = offset; for (int i = 0; i < count; ++i) { ImVec2 c; - c = Transformer::Transform(getter(idx)); + c = transformer(getter(idx)); idx = (idx + 1) % count; if (!cull || gp.BB_Grid.Contains(c)) { // TODO: Optimize the loop and if statements, this is atrocious if (HasFlag(gp.Style.Marker, ImMarker_Circle)) - MakerCircle(DrawList, c, gp.Style.MarkerSize, rend_mk_line, col_mk_line, rend_mk_fill, col_mk_fill, gp.Style.MarkerWeight); + MarkerCircle(DrawList, c, gp.Style.MarkerSize, rend_mk_line, col_mk_line, rend_mk_fill, col_mk_fill, gp.Style.MarkerWeight); if (HasFlag(gp.Style.Marker, ImMarker_Square)) MarkerSquare(DrawList, c, gp.Style.MarkerSize, rend_mk_line, col_mk_line, rend_mk_fill, col_mk_fill, gp.Style.MarkerWeight); if (HasFlag(gp.Style.Marker, ImMarker_Diamond)) @@ -1748,7 +2175,7 @@ inline void RenderLineAA(ImDrawList& DrawList, const ImVec2& p1, const ImVec2& p } template -inline void RenderLines(ImDrawList& DrawList, Getter getter, int count, int offset, float line_weight, ImU32 col_line, bool cull) { +inline void RenderLines(Transformer transformer, ImDrawList& DrawList, Getter getter, int count, int offset, float line_weight, ImU32 col_line, bool cull) { // render line segments const int segments = count - 1; int i1 = offset; @@ -1756,8 +2183,8 @@ inline void RenderLines(ImDrawList& DrawList, Getter getter, int count, int offs if (HasFlag(gp.CurrentPlot->Flags, ImPlotFlags_AntiAliased)) { for (int s = 0; s < segments; ++s) { const int i2 = (i1 + 1) % count; - p1 = Transformer::Transform(getter(i1)); - p2 = Transformer::Transform(getter(i2)); + p1 = transformer(getter(i1)); + p2 = transformer(getter(i2)); i1 = i2; if (!cull || gp.BB_Grid.Contains(p1) || gp.BB_Grid.Contains(p2)) RenderLineAA(DrawList, p1, p2, line_weight, col_line); @@ -1769,8 +2196,8 @@ inline void RenderLines(ImDrawList& DrawList, Getter getter, int count, int offs int segments_culled = 0; for (int s = 0; s < segments; ++s) { const int i2 = (i1 + 1) % count; - p1 = Transformer::Transform(getter(i1)); - p2 = Transformer::Transform(getter(i2)); + p1 = transformer(getter(i1)); + p2 = transformer(getter(i2)); i1 = i2; if (!cull || gp.BB_Grid.Contains(p1) || gp.BB_Grid.Contains(p2)) RenderLine(DrawList, p1, p2, line_weight, col_line, uv); @@ -1838,6 +2265,8 @@ inline void PlotEx(const char* label_id, Getter getter, int count, int offset) { IM_ASSERT_USER_ERROR(gp.CurrentPlot != NULL, "Plot() Needs to be called between BeginPlot() and EndPlot()!"); + ImPlot* plot = gp.CurrentPlot; + const int y_axis = plot->CurrentYAxis; ImPlotItem* item = RegisterItem(label_id); if (!item->Show) return; @@ -1857,7 +2286,7 @@ inline void PlotEx(const char* label_id, Getter getter, int count, int offset) if (gp.Style.Colors[ImPlotCol_Line].w != -1) item->Color = gp.Style.Colors[ImPlotCol_Line]; - bool cull = HasFlag(gp.CurrentPlot->Flags, ImPlotFlags_CullData); + bool cull = HasFlag(plot->Flags, ImPlotFlags_CullData); // find data extents if (gp.FitThisFrame) { @@ -1868,25 +2297,25 @@ inline void PlotEx(const char* label_id, Getter getter, int count, int offset) } PushPlotClipRect(); if (count > 1 && rend_line) { - if (HasFlag(gp.CurrentPlot->XAxis.Flags, ImAxisFlags_LogScale) && HasFlag(gp.CurrentPlot->YAxis.Flags, ImAxisFlags_LogScale)) - RenderLines(DrawList, getter, count, offset, line_weight, col_line, cull); - else if (HasFlag(gp.CurrentPlot->XAxis.Flags, ImAxisFlags_LogScale)) - RenderLines(DrawList, getter, count, offset, line_weight, col_line, cull); - else if (HasFlag(gp.CurrentPlot->YAxis.Flags, ImAxisFlags_LogScale)) - RenderLines(DrawList, getter, count, offset, line_weight, col_line, cull); + if (HasFlag(plot->XAxis.Flags, ImAxisFlags_LogScale) && HasFlag(plot->YAxis[y_axis].Flags, ImAxisFlags_LogScale)) + RenderLines(Plt2PixLogLog(y_axis), DrawList, getter, count, offset, line_weight, col_line, cull); + else if (HasFlag(plot->XAxis.Flags, ImAxisFlags_LogScale)) + RenderLines(Plt2PixLogLin(y_axis), DrawList, getter, count, offset, line_weight, col_line, cull); + else if (HasFlag(plot->YAxis[y_axis].Flags, ImAxisFlags_LogScale)) + RenderLines(Plt2PixLinLog(y_axis), DrawList, getter, count, offset, line_weight, col_line, cull); else - RenderLines(DrawList, getter, count, offset, line_weight, col_line, cull); + RenderLines(Plt2PixLinLin(y_axis), DrawList, getter, count, offset, line_weight, col_line, cull); } // render markers if (gp.Style.Marker != ImMarker_None) { - if (HasFlag(gp.CurrentPlot->XAxis.Flags, ImAxisFlags_LogScale) && HasFlag(gp.CurrentPlot->YAxis.Flags, ImAxisFlags_LogScale)) - RenderMarkers(DrawList, getter, count, offset, rend_mk_line, col_mk_line, rend_mk_fill, col_mk_fill, cull); - else if (HasFlag(gp.CurrentPlot->XAxis.Flags, ImAxisFlags_LogScale)) - RenderMarkers(DrawList, getter, count, offset, rend_mk_line, col_mk_line, rend_mk_fill, col_mk_fill, cull); - else if (HasFlag(gp.CurrentPlot->YAxis.Flags, ImAxisFlags_LogScale)) - RenderMarkers(DrawList, getter, count, offset, rend_mk_line, col_mk_line, rend_mk_fill, col_mk_fill, cull); + if (HasFlag(plot->XAxis.Flags, ImAxisFlags_LogScale) && HasFlag(plot->YAxis[y_axis].Flags, ImAxisFlags_LogScale)) + RenderMarkers(Plt2PixLogLog(y_axis), DrawList, getter, count, offset, rend_mk_line, col_mk_line, rend_mk_fill, col_mk_fill, cull); + else if (HasFlag(plot->XAxis.Flags, ImAxisFlags_LogScale)) + RenderMarkers(Plt2PixLogLin(y_axis), DrawList, getter, count, offset, rend_mk_line, col_mk_line, rend_mk_fill, col_mk_fill, cull); + else if (HasFlag(plot->YAxis[y_axis].Flags, ImAxisFlags_LogScale)) + RenderMarkers(Plt2PixLinLog(y_axis), DrawList, getter, count, offset, rend_mk_line, col_mk_line, rend_mk_fill, col_mk_fill, cull); else - RenderMarkers(DrawList, getter, count, offset, rend_mk_line, col_mk_line, rend_mk_fill, col_mk_fill, cull); + RenderMarkers(Plt2PixLinLin(y_axis), DrawList, getter, count, offset, rend_mk_line, col_mk_line, rend_mk_fill, col_mk_fill, cull); } PopPlotClipRect(); } @@ -2240,10 +2669,13 @@ inline void PlotDigitalEx(const char* label_id, Getter getter, int count, int of bool cull = HasFlag(gp.CurrentPlot->Flags, ImPlotFlags_CullData); const float line_weight = item->Highlight ? gp.Style.LineWeight * 2 : gp.Style.LineWeight; + + const int ax = gp.CurrentPlot->CurrentYAxis; + // render digital signals as "pixel bases" rectangles if (count > 1 && rend_line) { // - const float mx = (gp.PixelRange.Max.x - gp.PixelRange.Min.x) / (gp.CurrentPlot->XAxis.Max - gp.CurrentPlot->XAxis.Min); + const float mx = (gp.PixelRange[ax].Max.x - gp.PixelRange[ax].Min.x) / gp.CurrentPlot->XAxis.Range.Size(); const int segments = count - 1; int i1 = offset; int pixYMax = 0; @@ -2257,28 +2689,28 @@ inline void PlotDigitalEx(const char* label_id, Getter getter, int count, int of int pixY_chPosOffset = ImMax((int)gp.Style.DigitalBitHeight, pixY_1) + gp.Style.DigitalBitGap; pixYMax = ImMax(pixYMax, pixY_chPosOffset); ImVec2 pMin, pMax; - pMin.x = gp.PixelRange.Min.x + mx * (itemData1.x - gp.CurrentPlot->XAxis.Min); - pMax.x = gp.PixelRange.Min.x + mx * (itemData2.x - gp.CurrentPlot->XAxis.Min); + pMin.x = gp.PixelRange[ax].Min.x + mx * (itemData1.x - gp.CurrentPlot->XAxis.Range.Min); + pMax.x = gp.PixelRange[ax].Min.x + mx * (itemData2.x - gp.CurrentPlot->XAxis.Range.Min); int pixY_Offset = 20;//20 pixel from bottom due to mouse cursor label - pMin.y = (gp.PixelRange.Min.y) + ((-gp.DigitalPlotOffset) - pixY_Offset); - pMax.y = (gp.PixelRange.Min.y) + ((-gp.DigitalPlotOffset) - pixY_0 - pixY_1 - pixY_Offset); + pMin.y = (gp.PixelRange[ax].Min.y) + ((-gp.DigitalPlotOffset) - pixY_Offset); + pMax.y = (gp.PixelRange[ax].Min.y) + ((-gp.DigitalPlotOffset) - pixY_0 - pixY_1 - pixY_Offset); //plot only one rectangle for same digital state while (((s+2) < segments) && (itemData1.y == itemData2.y)) { const int i2 = (i1 + 1) % count; itemData2 = getter(i2); - pMax.x = gp.PixelRange.Min.x + mx * (itemData2.x - gp.CurrentPlot->XAxis.Min); + pMax.x = gp.PixelRange[ax].Min.x + mx * (itemData2.x - gp.CurrentPlot->XAxis.Range.Min); i1 = i2; s++; } //do not extend plot outside plot range - if (pMin.x < gp.PixelRange.Min.x) pMin.x = gp.PixelRange.Min.x; - if (pMax.x < gp.PixelRange.Min.x) pMax.x = gp.PixelRange.Min.x; - if (pMin.x > gp.PixelRange.Max.x) pMin.x = gp.PixelRange.Max.x; - if (pMax.x > gp.PixelRange.Max.x) pMax.x = gp.PixelRange.Max.x; + if (pMin.x < gp.PixelRange[ax].Min.x) pMin.x = gp.PixelRange[ax].Min.x; + if (pMax.x < gp.PixelRange[ax].Min.x) pMax.x = gp.PixelRange[ax].Min.x; + if (pMin.x > gp.PixelRange[ax].Max.x) pMin.x = gp.PixelRange[ax].Max.x; + if (pMax.x > gp.PixelRange[ax].Max.x) pMax.x = gp.PixelRange[ax].Max.x; //plot a rectangle that extends up to x2 with y1 height if ((pMax.x > pMin.x) && (!cull || gp.BB_Grid.Contains(pMin) || gp.BB_Grid.Contains(pMax))) { - auto colAlpha = item->Color; - colAlpha.w = item->Highlight ? 1.0 : 0.9; + ImVec4 colAlpha = item->Color; + colAlpha.w = item->Highlight ? 1.0f : 0.9f; DrawList.AddRectFilled(pMin, pMax, GetColorU32(colAlpha)); } }