diff --git a/implot.cpp b/implot.cpp index 242f152..f1dff5c 100644 --- a/implot.cpp +++ b/implot.cpp @@ -501,7 +501,7 @@ void LabelTickDefault(ImPlotTick& tick, ImGuiTextBuffer& buffer) { char temp[32]; if (tick.ShowLabel) { tick.BufferOffset = buffer.size(); - sprintf(temp, "%.10g", tick.PlotPos); + snprintf(temp, 32, "%.10g", tick.PlotPos); buffer.append(temp, temp + strlen(temp) + 1); tick.LabelSize = ImGui::CalcTextSize(buffer.Buf.Data + tick.BufferOffset); } @@ -511,7 +511,17 @@ void LabelTickScientific(ImPlotTick& tick, ImGuiTextBuffer& buffer) { char temp[32]; if (tick.ShowLabel) { tick.BufferOffset = buffer.size(); - sprintf(temp, "%.0E", tick.PlotPos); + snprintf(temp, 32, "%.0E", tick.PlotPos); + buffer.append(temp, temp + strlen(temp) + 1); + tick.LabelSize = ImGui::CalcTextSize(buffer.Buf.Data + tick.BufferOffset); + } +} + +void LabelTickTime(ImPlotTick& tick, ImGuiTextBuffer& buffer, ImPlotTimeFmt fmt) { + char temp[16]; + if (tick.ShowLabel) { + tick.BufferOffset = buffer.size(); + FormatTime(tick.PlotPos, temp, 16, fmt); buffer.append(temp, temp + strlen(temp) + 1); tick.LabelSize = ImGui::CalcTextSize(buffer.Buf.Data + tick.BufferOffset); } @@ -565,6 +575,17 @@ void AddTicksLogarithmic(const ImPlotRange& range, int nMajor, ImPlotTickCollect } } +void AddTicksTime(const ImPlotRange& range, ImPlotTickCollection& ticks) { + ImPlotTimeUnit unit_range = GetUnitForRange(range.Min, range.Max); + ImPlotTimeUnit unit_ticks = unit_range == 0 ? ImPlotTimeUnit_Us : unit_range - 1; + double t = FloorTime(range.Min, unit_range); + while (t < range.Max) { + t = AddTime(t, unit_ticks, 1); + ImPlotTick tick(t,false,true); + LabelTickTime(tick,ticks.Labels,unit_ticks); + ticks.AddTick(tick); + } +} void AddTicksCustom(const double* values, const char** labels, int n, ImPlotTickCollection& ticks) { for (int i = 0; i < n; ++i) { @@ -755,6 +776,8 @@ bool BeginPlot(const char* title, const char* x_label, const char* y_label, cons if (gp.RenderX && gp.NextPlotData.ShowDefaultTicksX) { if (ImHasFlag(plot.XAxis.Flags, ImPlotAxisFlags_LogScale)) AddTicksLogarithmic(plot.XAxis.Range, (int)(gp.BB_Canvas.GetWidth() * 0.01f), gp.XTicks); + else if (gp.X.IsTime) + AddTicksTime(plot.XAxis.Range, gp.XTicks); else AddTicksDefault(plot.XAxis.Range, ImMax(2, (int)IM_ROUND(0.003 * gp.BB_Canvas.GetWidth())), IMPLOT_SUB_DIV, gp.XTicks); } @@ -768,10 +791,14 @@ bool BeginPlot(const char* title, const char* x_label, const char* y_label, cons } // plot bb + const ImVec2 title_size = ImGui::CalcTextSize(title, NULL, true); const float txt_height = ImGui::GetTextLineHeight(); + const float pad_top = title_size.x > 0.0f ? txt_height + gp.Style.LabelPadding.y : 0; - const float pad_bot = (gp.X.HasLabels ? txt_height + gp.Style.LabelPadding.y : 0) + (x_label ? txt_height + gp.Style.LabelPadding.y : 0); + const float pad_bot = (gp.X.HasLabels ? txt_height + gp.Style.LabelPadding.y : 0) + + (x_label ? txt_height + gp.Style.LabelPadding.y : 0) + + (gp.X.IsTime ? txt_height + gp.Style.LabelPadding.y : 0); const float pad_left = (y_label ? txt_height + gp.Style.LabelPadding.x : 0) + (gp.Y[0].HasLabels ? gp.YTicks[0].MaxWidth + gp.Style.LabelPadding.x : 0); const float pad_right = ((gp.Y[1].Present && gp.Y[1].HasLabels) ? gp.YTicks[1].MaxWidth + gp.Style.LabelPadding.x : 0) diff --git a/implot.h b/implot.h index 634a22e..eda8d3d 100644 --- a/implot.h +++ b/implot.h @@ -76,6 +76,7 @@ enum ImPlotAxisFlags_ { ImPlotAxisFlags_LockMin = 1 << 4, // the axis minimum value will be locked when panning/zooming ImPlotAxisFlags_LockMax = 1 << 5, // the axis maximum value will be locked when panning/zooming ImPlotAxisFlags_LogScale = 1 << 6, // a logartithmic (base 10) axis scale will be used + ImPlotAxisFlags_Time = 1 << 7, // axis will display data/time formatted labels ImPlotAxisFlags_Default = ImPlotAxisFlags_GridLines | ImPlotAxisFlags_TickMarks | ImPlotAxisFlags_TickLabels, ImPlotAxisFlags_Auxiliary = ImPlotAxisFlags_TickMarks | ImPlotAxisFlags_TickLabels, }; diff --git a/implot_demo.cpp b/implot_demo.cpp index 36cbe6e..2ed3a81 100644 --- a/implot_demo.cpp +++ b/implot_demo.cpp @@ -591,6 +591,13 @@ void ShowDemoWindow(bool* p_open) { ImPlot::EndPlot(); } } + if (ImGui::CollapsingHeader("Time Formatting")) { + ImPlot::SetNextPlotLimits(1599106881,1599106881+1000000,0,1); + if (ImPlot::BeginPlot("UTC Time", "Date-Time", "Y-Axis", ImVec2(-1,0), ImPlotFlags_Default, ImPlotAxisFlags_Default | ImPlotAxisFlags_Time)) { + + ImPlot::EndPlot(); + } + } //------------------------------------------------------------------------- if (ImGui::CollapsingHeader("Multiple Y-Axes")) { diff --git a/implot_internal.h b/implot_internal.h index 1f4969d..934b546 100644 --- a/implot_internal.h +++ b/implot_internal.h @@ -35,6 +35,7 @@ #define IMGUI_DEFINE_MATH_OPERATORS #endif +#include #include "imgui_internal.h" #ifndef IMPLOT_VERSION @@ -147,7 +148,9 @@ struct ImPlotPointArray { // [SECTION] ImPlot Enums //----------------------------------------------------------------------------- -typedef int ImPlotScale; // -> enum ImPlotScale_ +typedef int ImPlotScale; // -> enum ImPlotScale_ +typedef int ImPlotTimeUnit; // -> enum ImPlotTimeUnit_ +typedef int ImPlotTimeFmt; // -> enum ImPlotTimeFmt_ // XY axes scaling combinations enum ImPlotScale_ { @@ -157,6 +160,29 @@ enum ImPlotScale_ { ImPlotScale_LogLog // log x, log y }; +enum ImPlotTimeUnit_ { + ImPlotTimeUnit_Us, // microsecond + ImPlotTimeUnit_Ms, // millisecond + ImPlotTimeUnit_S, // second + ImPlotTimeUnit_Min, // minute + ImPlotTimeUnit_Hr, // hour + ImPlotTimeUnit_Day, // day + ImPlotTimeUnit_Mo, // month + ImPlotTimeUnit_Yr, // year + ImPlotTimeUnit_COUNT +}; + +enum ImPlotTimeFmt_ { + ImPlotTimeFmt_SUs, // :29.428 552 + ImPlotTimeFmt_SMs, // :29.428 + ImPlotTimeFmt_S, // :29 + ImPlotTimeFmt_HrMin, // 7:21pm + ImPlotTimeFmt_Hr, // 7pm + ImPlotTimeFmt_MoDay, // 10/3 + ImPlotTimeFmt_Mo, // Oct + ImPlotTimeFmt_Yr // 1991 +}; + //----------------------------------------------------------------------------- // [SECTION] ImPlot Structs //----------------------------------------------------------------------------- @@ -272,6 +298,7 @@ struct ImPlotAxisState bool LockMin; bool LockMax; bool Lock; + bool IsTime; ImPlotAxisState(ImPlotAxis* axis, bool has_range, ImGuiCond range_cond, bool present) { Axis = axis; @@ -283,6 +310,7 @@ struct ImPlotAxisState LockMin = ImHasFlag(Axis->Flags, ImPlotAxisFlags_LockMin) || (HasRange && RangeCond == ImGuiCond_Always); LockMax = ImHasFlag(Axis->Flags, ImPlotAxisFlags_LockMax) || (HasRange && RangeCond == ImGuiCond_Always); Lock = !Present || ((LockMin && LockMax) || (HasRange && RangeCond == ImGuiCond_Always)); + IsTime = ImHasFlag(Axis->Flags, ImPlotAxisFlags_Time); } ImPlotAxisState() { } @@ -456,6 +484,9 @@ struct ImPlotContext { int ColormapSize; ImVector ColormapModifiers; + // Time + tm Tm; + // Misc int VisibleItemCount; int DigitalPlotItemCnt; @@ -560,15 +591,19 @@ const char* GetLegendLabel(int i); // [SECTION] Tick Utils //----------------------------------------------------------------------------- -// Label a tick with default formatting +// Label a tick with default formatting. void LabelTickDefault(ImPlotTick& tick, ImGuiTextBuffer& buffer); -// Label a tick with scientific formating +// Label a tick with scientific formating. void LabelTickScientific(ImPlotTick& tick, ImGuiTextBuffer& buffer); +// Label a tick with time formatting. +void LabelTickTime(ImPlotTick& tick, ImGuiTextBuffer& buffer, ImPlotTimeFmt fmt); // Populates a list of ImPlotTicks with normal spaced and formatted ticks void AddTicksDefault(const ImPlotRange& range, int nMajor, int nMinor, ImPlotTickCollection& ticks); // Populates a list of ImPlotTicks with logarithmic space and formatted ticks void AddTicksLogarithmic(const ImPlotRange& range, int nMajor, ImPlotTickCollection& ticks); +// Populates a list of ImPlotTicks with time formatted ticks. +void AddTicksTime(const ImPlotRange& range, int nMajor, ImPlotTickCollection& ticks); // Populates a list of ImPlotTicks with custom spaced and labeled ticks void AddTicksCustom(const double* values, const char** labels, int n, ImPlotTickCollection& ticks); @@ -649,6 +684,144 @@ inline T OffsetAndStride(const T* data, int idx, int count, int offset, int stri return *(const T*)(const void*)((const unsigned char*)data + (size_t)idx * stride); } +//----------------------------------------------------------------------------- +// [SECTION] Time Utils +//----------------------------------------------------------------------------- + +static const int DaysInMonth[12] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; +static const double ImPlotTimeUnitSpans[ImPlotTimeUnit_COUNT] = {0.000001, 0.001, 1, 60, 3600, 86400, 2629800, 31557600}; + +inline ImPlotTimeUnit GetUnitForRange(double smin, double smax) { + double range = smax - smin; + for (int i = 0; i < ImPlotTimeUnit_COUNT; ++i) { + if (range <= ImPlotTimeUnitSpans[i]) + return (ImPlotTimeUnit)i; + } + return ImPlotTimeUnit_Yr; +} + +// Returns true if year is leap year (366 days long) +inline bool IsLeapYear(int year) { + if (year % 4 != 0) return false; + if (year % 400 == 0) return true; + if (year % 100 == 0) return false; + return true; +} + +// Returns the number of days in a month, accounting for Feb. leap years. +inline int GetDaysInMonth(int year, int month) { + return DaysInMonth[month] + (int)(month == 1 && IsLeapYear(year)); +} + +inline time_t MkGmTime(const struct tm *ptm) { + time_t secs = 0; + int year = ptm->tm_year + 1900; + for (int y = 1970; y < year; ++y) { + secs += (IsLeapYear(y)? 366: 365) * 86400; + } + for (int m = 0; m < ptm->tm_mon; ++m) { + secs += DaysInMonth[m] * 86400; + if (m == 1 && IsLeapYear(year)) secs += 86400; + } + secs += (ptm->tm_mday - 1) * 86400; + secs += ptm->tm_hour * 3600; + secs += ptm->tm_min * 60; + secs += ptm->tm_sec; + return secs; +} + +inline tm* GmTime(const time_t* time, tm* tm) +{ +#ifdef _MSC_VER + if (gmtime_s(tm, time) == 0) + return tm; + else + return NULL; +#else + return gmtime_r(time, tm); +#endif +} + +inline double AddTime(double t, ImPlotTimeUnit unit, int count) { + switch(unit) { + case ImPlotTimeUnit_Us: return t + count * 0.000001; + case ImPlotTimeUnit_Ms: return t + count * 0.001; + case ImPlotTimeUnit_S: return t + count; + case ImPlotTimeUnit_Min: return t + count * 60; + case ImPlotTimeUnit_Hr: return t + count * 3600; + case ImPlotTimeUnit_Day: return t + count * 86400; + case ImPlotTimeUnit_Yr: count *= 12; // fall-through + case ImPlotTimeUnit_Mo: for (int i = 0; i < count; ++i) { + time_t s = (time_t)t; + GmTime(&s, &GImPlot->Tm); + int days = GetDaysInMonth(GImPlot->Tm.tm_year, GImPlot->Tm.tm_mon); + t = AddTime(t, ImPlotTimeUnit_Day, days); + } + return t; + default: return t; + } +} + +inline double FloorTime(double t, ImPlotTimeUnit unit) { + time_t s = (time_t)t; + GmTime(&s, &GImPlot->Tm); + GImPlot->Tm.tm_isdst = -1; + switch (unit) { + case ImPlotTimeUnit_S: return (double)s; + case ImPlotTimeUnit_Ms: return floor(t * 1000) / 1000; + case ImPlotTimeUnit_Us: return floor(t * 1000000) / 1000000; + case ImPlotTimeUnit_Yr: GImPlot->Tm.tm_mon = 0; // fall-through + case ImPlotTimeUnit_Mo: GImPlot->Tm.tm_mday = 1; // fall-through + case ImPlotTimeUnit_Day: GImPlot->Tm.tm_hour = 0; // fall-through + case ImPlotTimeUnit_Hr: GImPlot->Tm.tm_min = 0; // fall-through + case ImPlotTimeUnit_Min: GImPlot->Tm.tm_sec = 0; break; + default: return t; + } + s = MkGmTime(&GImPlot->Tm); + return (double)s; +} + +inline double CeilTime(double t, ImPlotTimeUnit unit) { + return AddTime(FloorTime(t, unit), unit, 1); +} + +inline void FormatTime(double t, char* buffer, int size, ImPlotTimeFmt fmt) { + time_t s = (time_t)t; + int ms = (int)(t * 1000 - floor(t) * 1000); + int us = (int)(t * 1000000 - floor(t) * 1000000); + tm& Tm = GImPlot->Tm; + GmTime(&s, &Tm); + switch(fmt) { + case ImPlotTimeFmt_Yr: strftime(buffer, size, "%Y", &Tm); break; + case ImPlotTimeFmt_Mo: strftime(buffer, size, "%b", &Tm); break; + case ImPlotTimeFmt_MoDay: strftime(buffer, size, "%m/%d", &Tm); break; + case ImPlotTimeFmt_Hr: + if (Tm.tm_hour == 0) + snprintf(buffer, size, "12am"); + else if (Tm.tm_hour == 12) + snprintf(buffer, size, "12pm"); + else if (Tm.tm_hour < 12) + snprintf(buffer, size, "%uam", Tm.tm_hour); + else if (Tm.tm_hour > 12) + snprintf(buffer, size, "%upm", Tm.tm_hour - 12); + break; + case ImPlotTimeFmt_HrMin: + if (Tm.tm_hour == 0) + snprintf(buffer, size, "12:%02dam", Tm.tm_min); + else if (Tm.tm_hour == 12) + snprintf(buffer, size, "12%02dpm", Tm.tm_min); + else if (Tm.tm_hour < 12) + snprintf(buffer, size, "%u:%02dam", Tm.tm_hour, Tm.tm_min); + else if (Tm.tm_hour > 12) + snprintf(buffer, size, "%u:%02dpm", Tm.tm_hour - 12, Tm.tm_min); + break; + case ImPlotTimeFmt_S: strftime(buffer, size, ":%S", &Tm); + case ImPlotTimeFmt_SMs: snprintf(buffer, size, ":%02d.%d", Tm.tm_sec, ms); + case ImPlotTimeFmt_SUs: snprintf(buffer, size, ":%02d.%d", Tm.tm_sec, us); + default: break; + } +} + //----------------------------------------------------------------------------- // [SECTION] Internal / Experimental Plotters // No guarantee of forward compatibility here!