diff --git a/implot.cpp b/implot.cpp index 61850c8..91fb296 100644 --- a/implot.cpp +++ b/implot.cpp @@ -575,82 +575,121 @@ void AddTicksLogarithmic(const ImPlotRange& range, int nMajor, ImPlotTickCollect } } -// splits -// mo: 6 3 2 -// day: -// hr: 12, 6, 3, 2, 1 -// min: 30: 15, 10, 5, 1 +inline int LowerBoundStep(int max_divs, const int* divs, const int* step, int size) { + if (max_divs < divs[0]) + return 0; + for (int i = 1; i < size; ++i) { + if (max_divs < divs[i]) + return step[i-1]; + } + return step[size-1]; +} + +inline int GetTimeStep(int max_divs, ImPlotTimeUnit unit) { + if (unit == ImPlotTimeUnit_Ms || unit == ImPlotTimeUnit_Us) { + static const int step[] = {500,250,200,100,50,25,20,10,5}; + static const int divs[] = {2,4,5,10,20,40,50,100,200}; + return LowerBoundStep(max_divs, divs, step, 9); + } + if (unit == ImPlotTimeUnit_S || unit == ImPlotTimeUnit_Min) { + static const int step[] = {30,15,10,5,1}; + static const int divs[] = {2,4,6,12,60}; + return LowerBoundStep(max_divs, divs, step, 5); + } + else if (unit == ImPlotTimeUnit_Hr) { + static const int step[] = {12,6,3,2,1}; + static const int divs[] = {2,4,8,12,24}; + return LowerBoundStep(max_divs, divs, step, 5); + } + else if (unit == ImPlotTimeUnit_Day) { + static const int step[] = {14,7,2,1}; + static const int divs[] = {2,4,14,28}; + return LowerBoundStep(max_divs, divs, step, 4); + } + else if (unit == ImPlotTimeUnit_Mo) { + static const int step[] = {6,3,2,1}; + static const int divs[] = {2,4,6,12}; + return LowerBoundStep(max_divs, divs, step, 4); + } + return 0; +} + + void AddTicksTime(const ImPlotRange& range, float plot_width, ImPlotTickCollection& ticks) { + // get units for level 0 and level 1 labels const ImPlotTimeUnit unit0 = GetUnitForRange(range.Size() / (plot_width / 100)); // level = 0 (top) const ImPlotTimeUnit unit1 = unit0 + 1; // level = 1 (bottom) // maximum allowable density of labels const float max_density = 0.5f; - // pixels per major (level 1) division - const float pix_per_major_div = plot_width / (float)(range.Size() / TimeUnitSpans[unit1]); - // nominal pixels taken up by minor (level 0) label - const float minor_label_width = GetTimeLabelWidth(TimeFormatLevel0[unit0]); - // the maximum number of minor (level 0) labels that can fit between major (level 1) divisions - const int minor_per_major = (int)(max_density * pix_per_major_div / minor_label_width); - if (unit1 != ImPlotTimeUnit_COUNT) { - double t = FloorTime(range.Min, unit1); - while (t < range.Max) { - if (range.Contains(t)) { - ImPlotTick tick_maj(t,true,true); + if (unit0 != ImPlotTimeUnit_Yr) { + // pixels per major (level 1) division + const float pix_per_major_div = plot_width / (float)(range.Size() / TimeUnitSpans[unit1]); + // nominal pixels taken up by minor (level 0) label + const float minor_label_width = GetTimeLabelWidth(TimeFormatLevel0[unit0]); + // the maximum number of minor (level 0) labels that can fit between major (level 1) divisions + const int minor_per_major = (int)(max_density * pix_per_major_div / minor_label_width); + // the minor step size (level 0) + const int step = GetTimeStep(minor_per_major, unit0); + // generate ticks + double t1 = FloorTime(range.Min, unit1); + while (t1 < range.Max) { + if (range.Contains(t1)) { + ImPlotTick tick_maj(t1,true,true); tick_maj.Level = 1; LabelTickTime(tick_maj,ticks.Labels,TimeFormatLevel1[unit1]); ticks.AddTick(tick_maj); - ImPlotTick tick_min(t,true,true); + ImPlotTick tick_min(t1,true,true); tick_min.Level = 0; LabelTickTime(tick_min,ticks.Labels,TimeFormatLevel0[unit0]); ticks.AddTick(tick_min); } // add minor ticks up until next major if (minor_per_major > 1) { - double t2 = AddTime(t, unit1, 1); - double inc = (t2 - t) / minor_per_major; - for (int i = 1; i < minor_per_major; ++i) { - double t3 = t + i * inc; - if (range.Contains(t3)) { - ImPlotTick tick(t3,false,true); + double t2 = AddTime(t1, unit1, 1); + double t12 = AddTime(t1, unit0, step); + while (t12 < t2) { + float px_to_t2 = (float)((t2 - t12)/range.Size()) * plot_width; + if (range.Contains(t12)) { + ImPlotTick tick(t12,false,px_to_t2 >= minor_label_width); tick.Level = 0; LabelTickTime(tick,ticks.Labels,TimeFormatLevel0[unit0]); ticks.AddTick(tick); } - } - - if (unit0 == ImPlotTimeUnit_Us) { - - } - else if (unit0 == ImPlotTimeUnit_Ms) { - - } - else if (unit0 == ImPlotTimeUnit_S) { - - } - else if (unit0 == ImPlotTimeUnit_Min) { - - } - else if (unit0 == ImPlotTimeUnit_Hr) { - - } - else if (unit0 == ImPlotTimeUnit_Day) { - - } - else if (unit0 == ImPlotTimeUnit_Mo) { - - } + t12 = AddTime(t12, unit0, step); + } } - t = AddTime(t, unit1, 1); + t1 = AddTime(t1, unit1, 1); } } else { + // nominal pixels taken up by year label + const float label_width = GetTimeLabelWidth(TimeFormatLevel0[ImPlotTimeUnit_Yr]); + // maximum number of labels we can display + const int max_labels = (int)(max_density * plot_width / label_width); + + const int year_min = GetYear(range.Min); + const int year_max = GetYear(CeilTime(range.Max, ImPlotTimeUnit_Yr)); + const double nice_range = NiceNum((year_max - year_min)*0.99,false); + const double interval = NiceNum(nice_range / (max_labels - 1), true); + const int graphmin = (int)(floor(year_min / interval) * interval); + + double t1 = MakeYear(graphmin); + while (t1 < range.Max) { + if (range.Contains(t1)) { + ImPlotTick tick(t1, true, true); + tick.Level = 0; + LabelTickTime(tick, ticks.Labels, TimeFormatLevel0[ImPlotTimeUnit_Yr]); + ticks.AddTick(tick); + } + t1 = AddTime(t1, ImPlotTimeUnit_Yr, (int)interval); + } } - // printf("%d\n",minor_per_major); + // printf("%d , %d\n",minor_per_major,step); } void AddTicksCustom(const double* values, const char** labels, int n, ImPlotTickCollection& ticks) { diff --git a/implot_demo.cpp b/implot_demo.cpp index 2ed3a81..4eff8b8 100644 --- a/implot_demo.cpp +++ b/implot_demo.cpp @@ -592,8 +592,23 @@ void ShowDemoWindow(bool* p_open) { } } 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)) { + static double min = 1599242863*0.5; + static double max = 1599242863*1.5; + static bool zooming = false; + ImGuiCond cond = ImGuiCond_Once; + if (ImGui::Button("Zoom")) { + zooming = true; + } + if (zooming) { + cond = ImGuiCond_Always; + double range = max - min; + min += range * 0.005; + max -= range * 0.005; + if (range < 0.005) + zooming = false; + } + ImPlot::SetNextPlotLimits(min,max,0,1,cond); + if (ImPlot::BeginPlot("##Time", "UTC Time", "Y-Axis", ImVec2(-1,0), ImPlotFlags_Default, ImPlotAxisFlags_Default | ImPlotAxisFlags_Time)) { ImPlot::EndPlot(); } diff --git a/implot_internal.h b/implot_internal.h index 6bd32da..cd6d80a 100644 --- a/implot_internal.h +++ b/implot_internal.h @@ -745,7 +745,7 @@ inline int GetDaysInMonth(int year, int month) { return DaysInMonth[month] + (int)(month == 1 && IsLeapYear(year)); } -inline time_t MkGmTime(const struct tm *ptm) { +inline time_t MakeGmTime(const struct tm *ptm) { time_t secs = 0; int year = ptm->tm_year + 1900; for (int y = 1970; y < year; ++y) { @@ -762,7 +762,7 @@ inline time_t MkGmTime(const struct tm *ptm) { return secs; } -inline tm* GmTime(const time_t* time, tm* tm) +inline tm* GetGmTime(const time_t* time, tm* tm) { #ifdef _MSC_VER if (gmtime_s(tm, time) == 0) @@ -785,7 +785,7 @@ inline double AddTime(double t, ImPlotTimeUnit unit, int count) { 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); + GetGmTime(&s, &GImPlot->Tm); int days = GetDaysInMonth(GImPlot->Tm.tm_year, GImPlot->Tm.tm_mon); t = AddTime(t, ImPlotTimeUnit_Day, days); } @@ -794,9 +794,29 @@ inline double AddTime(double t, ImPlotTimeUnit unit, int count) { } } +inline int GetYear(double t) { + time_t s = (time_t)t; + tm& Tm = GImPlot->Tm; + IM_ASSERT(GetGmTime(&s, &Tm) != NULL); + return Tm.tm_year + 1900; +} + +inline double MakeYear(int year) { + int yr = year - 1900; + if (yr < 0) + yr = 0; + GImPlot->Tm = tm(); + GImPlot->Tm.tm_year = yr; + GImPlot->Tm.tm_sec = 1; + time_t s = MakeGmTime(&GImPlot->Tm); + if (s < 0) + s = 0; + return (double)s; +} + inline double FloorTime(double t, ImPlotTimeUnit unit) { time_t s = (time_t)t; - GmTime(&s, &GImPlot->Tm); + GetGmTime(&s, &GImPlot->Tm); GImPlot->Tm.tm_isdst = -1; switch (unit) { case ImPlotTimeUnit_S: return (double)s; @@ -809,7 +829,7 @@ inline double FloorTime(double t, ImPlotTimeUnit unit) { case ImPlotTimeUnit_Min: GImPlot->Tm.tm_sec = 0; break; default: return t; } - s = MkGmTime(&GImPlot->Tm); + s = MakeGmTime(&GImPlot->Tm); return (double)s; } @@ -820,9 +840,11 @@ inline double CeilTime(double t, ImPlotTimeUnit unit) { 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); + ms = ms % 10 == 9 ? ms + 1 : ms; int us = (int)(t * 1000000 - floor(t) * 1000000); + us = us % 10 == 9 ? us + 1 : us; tm& Tm = GImPlot->Tm; - IM_ASSERT(GmTime(&s, &Tm) != NULL); + IM_ASSERT(GetGmTime(&s, &Tm) != NULL); switch(fmt) { case ImPlotTimeFmt_Yr: strftime(buffer, size, "%Y", &Tm); break; case ImPlotTimeFmt_Mo: strftime(buffer, size, "%b", &Tm); break; @@ -831,7 +853,7 @@ inline void FormatTime(double t, char* buffer, int size, ImPlotTimeFmt fmt) { if (Tm.tm_hour == 0) snprintf(buffer, size, "%d/%d 12:%02dam", Tm.tm_mon + 1, Tm.tm_mday, Tm.tm_min); else if (Tm.tm_hour == 12) - snprintf(buffer, size, "%d/%d 12%02dpm", Tm.tm_mon + 1, Tm.tm_mday, Tm.tm_min); + snprintf(buffer, size, "%d/%d 12:%02dpm", Tm.tm_mon + 1, Tm.tm_mday, Tm.tm_min); else if (Tm.tm_hour < 12) snprintf(buffer, size, "%d/%d %u:%02dam", Tm.tm_mon + 1, Tm.tm_mday, Tm.tm_hour, Tm.tm_min); else if (Tm.tm_hour > 12)