Skip to content

Commit a4d2d2f

Browse files
committed
added support for offset-plus-delta ticking
1 parent d364e31 commit a4d2d2f

3 files changed

Lines changed: 192 additions & 65 deletions

File tree

implot.cpp

Lines changed: 168 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -750,14 +750,68 @@ bool ShowLegendEntries(ImPlotItemGroup& items, const ImRect& legend_bb, bool hov
750750
return hovered && !any_item_hovered;
751751
}
752752

753+
754+
755+
//-----------------------------------------------------------------------------
756+
// Offset plus delta formatter
757+
//-----------------------------------------------------------------------------
758+
int FormatOffsetPlusDelta(double value, char* buff, int size, char* fmt, int decimal_shift, double offset) {
759+
return ImFormatString(buff, size, fmt, (value - offset) * ImPow(10., (double) -decimal_shift));
760+
}
761+
bool UseOffset(ImPlotRange Range) {
762+
if((Range.Max > 0) != (Range.Min > 0)) {
763+
return false;
764+
}
765+
int range_most_sig_place = (int) floor(ImLog10(Range.Max - Range.Min));
766+
int extremum_most_sig_place = (int) floor(ImLog10(ImMax(ImAbs(Range.Max),ImAbs(Range.Min))));
767+
// present the ticks as a delta relative to an offset if the most significant digit of the
768+
// range is at least 3 digits to the right of the most significant digit of the extremum
769+
return range_most_sig_place - extremum_most_sig_place <= -3;
770+
}
771+
772+
int GetDecimalShift(ImPlotRange Range) {
773+
int decimal_shift;
774+
int n_axis_unit_thresh_log10 = 1;
775+
if(UseOffset(Range)) {
776+
// if the range is not between 0.01 and 10 user units, then scale the labels by a factor
777+
// 10^(3n) to ensure the range is between 0.01 and 10 label units. here, a user unit is a
778+
// change of 1 in the units employed by the user, and a label unit is a change of 1 in the
779+
// units used for the tick labeling
780+
decimal_shift = ((int)((ImLog10(Range.Max - Range.Min) - 1)/3)) * 3;
781+
} else {
782+
// if the abs of the most extreme value is not between 0.1 and 100 user units, then scale
783+
// the labels by a factor 10^(3n) to ensure the abs of the most extreme label value is
784+
// between 0.1 and 100 label units
785+
decimal_shift = ((int)((ImLog10(ImMax(ImAbs(Range.Max),ImAbs(Range.Min))) - 2)/3)) * 3;
786+
}
787+
return decimal_shift;
788+
}
789+
790+
double GetOffset(ImPlotRange Range) {
791+
double offset;
792+
if(UseOffset(Range)) {
793+
int range_most_sig_place = (int) floor(ImLog10(Range.Max - Range.Min));
794+
double minabs = (Range.Max > 0) ? Range.Min : Range.Max;
795+
// how often should the offset change as the user pans across the axis? the two lines
796+
// below set the axis offset to the range min (positive values) or the range max
797+
// (negative values) truncated to 2 digits to the left of the range_most_sig_place.
798+
// the place value of this place is between 10 and 100 times the axis range
799+
float ten_base = pow(10.,-range_most_sig_place - 2);
800+
offset = (minabs > 0) ? floor(minabs*ten_base) / ten_base : ceil(minabs*ten_base)/ten_base;
801+
} else {
802+
offset = 0.;
803+
}
804+
return offset;
805+
}
806+
753807
//-----------------------------------------------------------------------------
754808
// Locators
755809
//-----------------------------------------------------------------------------
756810

757811
constexpr float TICK_FILL_X = 0.8f;
758812
constexpr float TICK_FILL_Y = 1.0f;
759813

760-
void Locator_Default(ImPlotTicker& ticker, const ImPlotRange& range, float pixels, bool vertical, ImPlotFormatter formatter, void* formatter_data, double* offset, int* log10_multiplier) {
814+
void Locator_Default(ImPlotTicker& ticker, const ImPlotRange& range, float pixels, bool vertical, ImPlotFormatter formatter, void* formatter_data) {
761815
if (range.Min == range.Max)
762816
return;
763817
const int nMinor = 10;
@@ -768,29 +822,18 @@ void Locator_Default(ImPlotTicker& ticker, const ImPlotRange& range, float pixel
768822
const double graphmax = ceil(range.Max / interval) * interval;
769823
bool first_major_set = false;
770824
int first_major_idx = 0;
771-
bool delta_mode = false;
772-
if((range.Max > 0) == (range.Min > 0)) {
773-
double min_abs = ImMin(ImAbs(range.Min),ImAbs(range.Max));
774-
*log10_multiplier = (int)IM_ROUND(ImLog10((range.Max - range.Min)));
775-
if(*log10_multiplier < 0) {
776-
*log10_multiplier = *log10_multiplier - 1;
777-
}
778-
*log10_multiplier = ImSign(static_cast<double>(*log10_multiplier)) * IM_TRUNC(ImAbs(static_cast<double>(*log10_multiplier)/3.)) * 3;
779-
780-
delta_mode = !(*log10_multiplier==0);
781-
782-
if(delta_mode) {
783-
double ten_base = ImPow(10.,-static_cast<double>(*log10_multiplier) + 2 * ((*log10_multiplier < 0) ? -1 : 1));
784825

785-
*offset = (range.Max > 0) ? floor(graphmin*ten_base) / ten_base : ceil(graphmax*ten_base) / ten_base;
786-
787-
} else {
788-
*offset = 0;
789-
}
790-
791-
}
792826
const int idx0 = ticker.TickCount(); // ticker may have user custom ticks
793827
ImVec2 total_size(0,0);
828+
Formatter_Offset_Plus_Delta_Data fopdd;
829+
if(formatter == Formatter_Offset_Plus_Delta) {
830+
ticker.LabelDecimalShift = GetDecimalShift(range);
831+
ticker.LabelOffset = GetOffset(range);
832+
fopdd.decimal_shift = ticker.LabelDecimalShift;
833+
fopdd.offset = ticker.LabelOffset;
834+
fopdd.fmt = (char *) formatter_data;
835+
formatter_data = &fopdd;
836+
}
794837
for (double major = graphmin; major < graphmax + 0.5 * interval; major += interval) {
795838
// is this zero? combat zero formatting issues
796839
if (major-interval < 0 && major+interval > 0)
@@ -800,18 +843,12 @@ void Locator_Default(ImPlotTicker& ticker, const ImPlotRange& range, float pixel
800843
first_major_idx = ticker.TickCount();
801844
first_major_set = true;
802845
}
803-
char label[16];
804-
sprintf(label, (char *) formatter_data, (major - (delta_mode ? *offset : 0.))* (delta_mode ? pow(10,-*log10_multiplier) : 1));
805-
total_size += ticker.AddTick(major, true, 0, true, label).LabelSize;
806-
// total_size += ticker.AddTick(major, true, 0, true, formatter, formatter_data).LabelSize;
846+
total_size += ticker.AddTick(major, true, 0, true, formatter, formatter_data).LabelSize;
807847
}
808848
for (int i = 1; i < nMinor; ++i) {
809849
double minor = major + i * interval / nMinor;
810850
if (range.Contains(minor)) {
811-
char label[16];
812-
sprintf(label, (char *) formatter_data, (minor - (delta_mode ? *offset : 0.))* (delta_mode ? pow(10,-*log10_multiplier) : 1));
813-
total_size += ticker.AddTick(minor, false, 0, true, label).LabelSize;
814-
// total_size += ticker.AddTick(minor, false, 0, true, formatter, formatter_data).LabelSize;
851+
total_size += ticker.AddTick(minor, false, 0, true, formatter, formatter_data).LabelSize;
815852
}
816853
}
817854
}
@@ -822,6 +859,10 @@ void Locator_Default(ImPlotTicker& ticker, const ImPlotRange& range, float pixel
822859
for (int i = first_major_idx+1; i < ticker.TickCount(); i += 2)
823860
ticker.Ticks[i].ShowLabel = false;
824861
}
862+
if(formatter == Formatter_Offset_Plus_Delta) {
863+
// reset formatter_data so that it doesn't point to fopdd when fopdd goes out of scope
864+
formatter_data = fopdd.fmt;
865+
}
825866
}
826867

827868
bool CalcLogarithmicExponents(const ImPlotRange& range, float pix, bool vertical, int& exp_min, int& exp_max, int& exp_step) {
@@ -864,7 +905,7 @@ void AddTicksLogarithmic(const ImPlotRange& range, int exp_min, int exp_max, int
864905
}
865906
}
866907

867-
void Locator_Log10(ImPlotTicker& ticker, const ImPlotRange& range, float pixels, bool vertical, ImPlotFormatter formatter, void* formatter_data, double* offset, int* log10_multiplier) {
908+
void Locator_Log10(ImPlotTicker& ticker, const ImPlotRange& range, float pixels, bool vertical, ImPlotFormatter formatter, void* formatter_data) {
868909
int exp_min, exp_max, exp_step;
869910
if (CalcLogarithmicExponents(range, pixels, vertical, exp_min, exp_max, exp_step))
870911
AddTicksLogarithmic(range, exp_min, exp_max, exp_step, ticker, formatter, formatter_data);
@@ -881,7 +922,7 @@ float CalcSymLogPixel(double plt, const ImPlotRange& range, float pixels) {
881922
return (float)(0 + scaleToPixels * (plt - range.Min));
882923
}
883924

884-
void Locator_SymLog(ImPlotTicker& ticker, const ImPlotRange& range, float pixels, bool vertical, ImPlotFormatter formatter, void* formatter_data, double* offset, int* log10_multiplier) {
925+
void Locator_SymLog(ImPlotTicker& ticker, const ImPlotRange& range, float pixels, bool vertical, ImPlotFormatter formatter, void* formatter_data) {
885926
if (range.Min >= -1 && range.Max <= 1) {
886927
Locator_Default(ticker, range, pixels, vertical, formatter, formatter_data);
887928
}
@@ -1284,7 +1325,7 @@ inline ImPlotDateTimeSpec GetDateTimeFmt(const ImPlotDateTimeSpec* ctx, ImPlotTi
12841325
return fmt;
12851326
}
12861327

1287-
void Locator_Time(ImPlotTicker& ticker, const ImPlotRange& range, float pixels, bool vertical, ImPlotFormatter formatter, void* formatter_data, double* offset, int* log10_multiplier) {
1328+
void Locator_Time(ImPlotTicker& ticker, const ImPlotRange& range, float pixels, bool vertical, ImPlotFormatter formatter, void* formatter_data) {
12881329
IM_ASSERT_USER_ERROR(vertical == false, "Cannot locate Time ticks on vertical axis!");
12891330
(void)vertical;
12901331
// get units for level 0 and level 1 labels
@@ -1675,7 +1716,11 @@ void LabelAxisValue(const ImPlotAxis& axis, double value, char* buff, int size,
16751716
else {
16761717
if (round)
16771718
value = RoundAxisValue(axis, value);
1678-
axis.Formatter(value, buff, size, axis.FormatterData);
1719+
if(axis.Formatter == Formatter_Offset_Plus_Delta) {
1720+
Formatter_Default(value, buff, size, axis.FormatterData);
1721+
} else {
1722+
axis.Formatter(value, buff, size, axis.FormatterData);
1723+
}
16791724
}
16801725
}
16811726

@@ -2540,6 +2585,59 @@ bool BeginPlot(const char* title_id, const ImVec2& size, ImPlotFlags flags) {
25402585
// SetupFinish
25412586
//-----------------------------------------------------------------------------
25422587

2588+
void AxisLabelWithOffsetAndDeltaDecorations(char* label, ImPlotAxis ax) {
2589+
ImPlotPlot &plot = *GImPlot->CurrentPlot;
2590+
const char* base_label = plot.GetAxisLabel(ax);
2591+
if(ax.Formatter == Formatter_Offset_Plus_Delta) {
2592+
char offset_label[IMPLOT_LABEL_MAX_SIZE];
2593+
char decimal_shift_label[IMPLOT_LABEL_MAX_SIZE];
2594+
if(ax.Ticker.LabelDecimalShift != 0) {
2595+
ImFormatString(decimal_shift_label, IMPLOT_LABEL_MAX_SIZE, " x 10^%d", -ax.Ticker.LabelDecimalShift);
2596+
} else {
2597+
decimal_shift_label[0] = '\0';
2598+
}
2599+
if(ax.Ticker.LabelOffset != 0) {
2600+
ImFormatString(offset_label, IMPLOT_LABEL_MAX_SIZE, " %c %g",
2601+
ax.Ticker.LabelOffset > 0 ? '-' : '+',
2602+
ImAbs(ax.Ticker.LabelOffset));
2603+
} else {
2604+
offset_label[0] = '\0';
2605+
}
2606+
2607+
bool both_on = (ax.Ticker.LabelDecimalShift != 0) && (ax.Ticker.LabelOffset != 0);
2608+
ImFormatString(label, IMPLOT_LABEL_MAX_SIZE, "%s%s%s%s%s",
2609+
both_on ? "(" : "",
2610+
base_label,
2611+
offset_label,
2612+
both_on ? ")" : "",
2613+
decimal_shift_label);
2614+
} else {
2615+
ImFormatString(label, IMPLOT_LABEL_MAX_SIZE, "%s", base_label);
2616+
}
2617+
}
2618+
2619+
void OffsetAndDeltaLabel(char* label, ImPlotAxis ax) {
2620+
ImPlotPlot &plot = *GImPlot->CurrentPlot;
2621+
char offset_label[IMPLOT_LABEL_MAX_SIZE];
2622+
char decimal_shift_label[IMPLOT_LABEL_MAX_SIZE];
2623+
if(ax.Ticker.LabelDecimalShift != 0) {
2624+
ImFormatString(decimal_shift_label, IMPLOT_LABEL_MAX_SIZE, "x10^%d", ax.Ticker.LabelDecimalShift);
2625+
} else {
2626+
decimal_shift_label[0] = '\0';
2627+
}
2628+
if(ax.Ticker.LabelOffset != 0) {
2629+
ImFormatString(offset_label, IMPLOT_LABEL_MAX_SIZE, "%c %g",
2630+
ax.Ticker.LabelOffset > 0 ? '+' : '-',
2631+
ImAbs(ax.Ticker.LabelOffset));
2632+
} else {
2633+
offset_label[0] = '\0';
2634+
}
2635+
ImFormatString(label, IMPLOT_LABEL_MAX_SIZE, "%s%s%s",
2636+
decimal_shift_label,
2637+
(ax.Ticker.LabelDecimalShift != 0) && (ax.Ticker.LabelOffset != 0) ? " " : "",
2638+
offset_label);
2639+
}
2640+
25432641
void SetupFinish() {
25442642
IM_ASSERT_USER_ERROR(GImPlot != nullptr, "No current context. Did you call ImPlot::CreateContext() or ImPlot::SetCurrentContext()?");
25452643
ImPlotContext& gp = *GImPlot;
@@ -2569,9 +2667,12 @@ void SetupFinish() {
25692667
else
25702668
axis.FormatterData = (void*)IMPLOT_LABEL_FORMAT;
25712669
}
2670+
25722671
if (axis.Locator == nullptr) {
25732672
axis.Locator = Locator_Default;
25742673
}
2674+
2675+
IM_ASSERT_USER_ERROR((axis.Formatter != Formatter_Offset_Plus_Delta) || (axis.Locator == Locator_Default), "The Offset_Plus_Delta tick formatter can only be used with the default tick locator");
25752676
}
25762677

25772678
// setup nullptr orthogonal axes
@@ -2645,12 +2746,10 @@ void SetupFinish() {
26452746
const float plot_height = plot.CanvasRect.GetHeight() - pad_top - pad_bot;
26462747

26472748
// (2) get y tick labels (needed for left/right pad)
2648-
double y_offset = 0.;
2649-
int y_log10_multiplier = 0.;
26502749
for (int i = 0; i < IMPLOT_NUM_Y_AXES; i++) {
26512750
ImPlotAxis& axis = plot.YAxis(i);
26522751
if (axis.WillRender() && axis.ShowDefaultTicks && plot_height > 0) {
2653-
axis.Locator(axis.Ticker, axis.Range, plot_height, true, axis.Formatter, axis.FormatterData, &y_offset, &y_log10_multiplier);
2752+
axis.Locator(axis.Ticker, axis.Range, plot_height, true, axis.Formatter, axis.FormatterData);
26542753
}
26552754
}
26562755

@@ -2660,12 +2759,10 @@ void SetupFinish() {
26602759
const float plot_width = plot.CanvasRect.GetWidth() - pad_left - pad_right;
26612760

26622761
// (4) get x ticks
2663-
double x_offset = 0.;
2664-
int x_log10_multiplier = 0.;
26652762
for (int i = 0; i < IMPLOT_NUM_X_AXES; i++) {
26662763
ImPlotAxis& axis = plot.XAxis(i);
26672764
if (axis.WillRender() && axis.ShowDefaultTicks && plot_width > 0) {
2668-
axis.Locator(axis.Ticker, axis.Range, plot_width, false, axis.Formatter, axis.FormatterData, &x_offset, &x_log10_multiplier);
2765+
axis.Locator(axis.Ticker, axis.Range, plot_width, false, axis.Formatter, axis.FormatterData);
26692766
}
26702767
}
26712768

@@ -2785,28 +2882,12 @@ void SetupFinish() {
27852882
const ImPlotTicker& tkr = ax.Ticker;
27862883
const bool opp = ax.IsOpposite();
27872884
if (ax.HasLabel()) {
2788-
const char* base_label = plot.GetAxisLabel(ax);
2789-
char label[36];
2790-
if(x_offset==0. && x_log10_multiplier==0) {
2791-
strcpy(label, base_label);
2885+
char label[IMPLOT_LABEL_MAX_SIZE];
2886+
if(ax.Formatter == Formatter_Offset_Plus_Delta && ImHasFlag(ax.Flags, ImPlotAxisFlags_CompactOffsetAndDelta)) {
2887+
AxisLabelWithOffsetAndDeltaDecorations(label, ax);
27922888
} else {
2793-
strcpy(label, "(");
2794-
strcat(label, base_label);
2795-
char label_offset_label[12];
2796-
sprintf(label_offset_label, (char *)ax.FormatterData, ImAbs(x_offset));
2797-
if(x_offset>0) {
2798-
strcat(label," - ");
2799-
} else if (x_offset < 0){
2800-
strcat(label," + ");
2801-
}
2802-
char label_log10_label[12];
2803-
sprintf(label_log10_label, ") x 10^%d", -x_log10_multiplier);
2804-
if(x_offset!=0) {
2805-
strcat(label, label_offset_label);
2806-
}
2807-
strcat(label, label_log10_label);
2889+
ImFormatString(label, IMPLOT_LABEL_MAX_SIZE, "%s", plot.GetAxisLabel(ax));
28082890
}
2809-
28102891
const ImVec2 label_size = ImGui::CalcTextSize(label);
28112892
const float label_offset = (ax.HasTickLabels() ? tkr.MaxSize.y + gp.Style.LabelPadding.y : 0.0f)
28122893
+ (tkr.Levels - 1) * (txt_height + gp.Style.LabelPadding.y)
@@ -2827,6 +2908,20 @@ void SetupFinish() {
28272908
}
28282909
}
28292910
}
2911+
2912+
if (ax.Formatter == Formatter_Offset_Plus_Delta && !ImHasFlag(ax.Flags, ImPlotAxisFlags_CompactOffsetAndDelta)) {
2913+
char offset_delta_label[IMPLOT_LABEL_MAX_SIZE];
2914+
OffsetAndDeltaLabel(offset_delta_label, ax);
2915+
const ImVec2 label_size = ImGui::CalcTextSize(offset_delta_label);
2916+
const float label_offset = (ax.HasTickLabels() ? tkr.MaxSize.y + gp.Style.LabelPadding.y : 0.0f)
2917+
+ (tkr.Levels - 1) * (txt_height + gp.Style.LabelPadding.y);
2918+
const ImVec2 label_pos(plot.PlotRect.Max.x - label_size.x,
2919+
opp ? ax.Datum1 - label_offset - label_size.y : ax.Datum1 + label_offset);
2920+
DrawList.AddText(label_pos, ax.ColorTxt, offset_delta_label);
2921+
}
2922+
//
2923+
2924+
28302925
}
28312926

28322927
// render y axis button, label, tick labels
@@ -2846,7 +2941,12 @@ void SetupFinish() {
28462941
const ImPlotTicker& tkr = ax.Ticker;
28472942
const bool opp = ax.IsOpposite();
28482943
if (ax.HasLabel()) {
2849-
const char* label = plot.GetAxisLabel(ax);
2944+
char label[IMPLOT_LABEL_MAX_SIZE];
2945+
if(ax.Formatter == Formatter_Offset_Plus_Delta && ImHasFlag(ax.Flags, ImPlotAxisFlags_CompactOffsetAndDelta)) {
2946+
AxisLabelWithOffsetAndDeltaDecorations(label, ax);
2947+
} else {
2948+
ImFormatString(label, IMPLOT_LABEL_MAX_SIZE, "%s", plot.GetAxisLabel(ax));
2949+
}
28502950
const ImVec2 label_size = CalcTextSizeVertical(label);
28512951
const float label_offset = (ax.HasTickLabels() ? tkr.MaxSize.x + gp.Style.LabelPadding.x : 0.0f)
28522952
+ gp.Style.LabelPadding.x;
@@ -2864,6 +2964,14 @@ void SetupFinish() {
28642964
}
28652965
}
28662966
}
2967+
if (ax.Formatter == Formatter_Offset_Plus_Delta && !ImHasFlag(ax.Flags, ImPlotAxisFlags_CompactOffsetAndDelta)) {
2968+
char offset_delta_label[IMPLOT_LABEL_MAX_SIZE];
2969+
OffsetAndDeltaLabel(offset_delta_label, ax);
2970+
const ImVec2 label_size = ImGui::CalcTextSize(offset_delta_label);
2971+
const ImVec2 label_pos(plot.PlotRect.Min.x,
2972+
plot.PlotRect.Min.y - label_size.y);
2973+
DrawList.AddText(label_pos, ax.ColorTxt, offset_delta_label);
2974+
}
28672975
}
28682976

28692977

implot.h

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,7 @@ enum ImPlotAxisFlags_ {
190190
ImPlotAxisFlags_PanStretch = 1 << 13, // panning in a locked or constrained state will cause the axis to stretch if possible
191191
ImPlotAxisFlags_LockMin = 1 << 14, // the axis minimum value will be locked when panning/zooming
192192
ImPlotAxisFlags_LockMax = 1 << 15, // the axis maximum value will be locked when panning/zooming
193+
ImPlotAxisFlags_CompactOffsetAndDelta = 1 << 16,
193194
ImPlotAxisFlags_Lock = ImPlotAxisFlags_LockMin | ImPlotAxisFlags_LockMax,
194195
ImPlotAxisFlags_NoDecorations = ImPlotAxisFlags_NoLabel | ImPlotAxisFlags_NoGridLines | ImPlotAxisFlags_NoTickMarks | ImPlotAxisFlags_NoTickLabels,
195196
ImPlotAxisFlags_AuxDefault = ImPlotAxisFlags_NoGridLines | ImPlotAxisFlags_Opposite
@@ -1354,6 +1355,19 @@ IMPLOT_API void ShowUserGuide();
13541355
// Shows ImPlot metrics/debug information window.
13551356
IMPLOT_API void ShowMetricsWindow(bool* p_popen = nullptr);
13561357

1358+
struct Formatter_Offset_Plus_Delta_Data {
1359+
int decimal_shift;
1360+
double offset;
1361+
char* fmt;
1362+
};
1363+
1364+
int FormatOffsetPlusDelta(double value, char* buffer, int size, char * fmt, int decimal_shift, double offset);
1365+
1366+
IMPLOT_API inline int Formatter_Offset_Plus_Delta(double value, char* buff, int size, void* data) {
1367+
Formatter_Offset_Plus_Delta_Data* fopdd = (Formatter_Offset_Plus_Delta_Data*)data;
1368+
return FormatOffsetPlusDelta(value, buff, size, fopdd->fmt, fopdd->decimal_shift, fopdd->offset);
1369+
}
1370+
13571371
//-----------------------------------------------------------------------------
13581372
// [SECTION] Demo
13591373
//-----------------------------------------------------------------------------

0 commit comments

Comments
 (0)