Skip to content

Commit 889173d

Browse files
committed
added support for offset-plus-delta ticking
1 parent 524f9fc commit 889173d

3 files changed

Lines changed: 183 additions & 3 deletions

File tree

implot.cpp

Lines changed: 164 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -750,6 +750,60 @@ 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
//-----------------------------------------------------------------------------
@@ -768,8 +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;
825+
771826
const int idx0 = ticker.TickCount(); // ticker may have user custom ticks
772827
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+
}
773837
for (double major = graphmin; major < graphmax + 0.5 * interval; major += interval) {
774838
// is this zero? combat zero formatting issues
775839
if (major-interval < 0 && major+interval > 0)
@@ -795,6 +859,10 @@ void Locator_Default(ImPlotTicker& ticker, const ImPlotRange& range, float pixel
795859
for (int i = first_major_idx+1; i < ticker.TickCount(); i += 2)
796860
ticker.Ticks[i].ShowLabel = false;
797861
}
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+
}
798866
}
799867

800868
bool CalcLogarithmicExponents(const ImPlotRange& range, float pix, bool vertical, int& exp_min, int& exp_max, int& exp_step) {
@@ -1648,7 +1716,11 @@ void LabelAxisValue(const ImPlotAxis& axis, double value, char* buff, int size,
16481716
else {
16491717
if (round)
16501718
value = RoundAxisValue(axis, value);
1651-
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+
}
16521724
}
16531725
}
16541726

@@ -2513,6 +2585,59 @@ bool BeginPlot(const char* title_id, const ImVec2& size, ImPlotFlags flags) {
25132585
// SetupFinish
25142586
//-----------------------------------------------------------------------------
25152587

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+
25162641
void SetupFinish() {
25172642
IM_ASSERT_USER_ERROR(GImPlot != nullptr, "No current context. Did you call ImPlot::CreateContext() or ImPlot::SetCurrentContext()?");
25182643
ImPlotContext& gp = *GImPlot;
@@ -2542,9 +2667,12 @@ void SetupFinish() {
25422667
else
25432668
axis.FormatterData = (void*)IMPLOT_LABEL_FORMAT;
25442669
}
2670+
25452671
if (axis.Locator == nullptr) {
25462672
axis.Locator = Locator_Default;
25472673
}
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");
25482676
}
25492677

25502678
// setup nullptr orthogonal axes
@@ -2754,7 +2882,12 @@ void SetupFinish() {
27542882
const ImPlotTicker& tkr = ax.Ticker;
27552883
const bool opp = ax.IsOpposite();
27562884
if (ax.HasLabel()) {
2757-
const char* label = plot.GetAxisLabel(ax);
2885+
char label[IMPLOT_LABEL_MAX_SIZE];
2886+
if(ax.Formatter == Formatter_Offset_Plus_Delta && ImHasFlag(ax.Flags, ImPlotAxisFlags_CompactOffsetAndDelta)) {
2887+
AxisLabelWithOffsetAndDeltaDecorations(label, ax);
2888+
} else {
2889+
ImFormatString(label, IMPLOT_LABEL_MAX_SIZE, "%s", plot.GetAxisLabel(ax));
2890+
}
27582891
const ImVec2 label_size = ImGui::CalcTextSize(label);
27592892
const float label_offset = (ax.HasTickLabels() ? tkr.MaxSize.y + gp.Style.LabelPadding.y : 0.0f)
27602893
+ (tkr.Levels - 1) * (txt_height + gp.Style.LabelPadding.y)
@@ -2763,6 +2896,7 @@ void SetupFinish() {
27632896
opp ? ax.Datum1 - label_offset - label_size.y : ax.Datum1 + label_offset);
27642897
DrawList.AddText(label_pos, ax.ColorTxt, label);
27652898
}
2899+
27662900
if (ax.HasTickLabels()) {
27672901
for (int j = 0; j < tkr.TickCount(); ++j) {
27682902
const ImPlotTick& tk = tkr.Ticks[j];
@@ -2774,6 +2908,20 @@ void SetupFinish() {
27742908
}
27752909
}
27762910
}
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+
27772925
}
27782926

27792927
// render y axis button, label, tick labels
@@ -2793,7 +2941,12 @@ void SetupFinish() {
27932941
const ImPlotTicker& tkr = ax.Ticker;
27942942
const bool opp = ax.IsOpposite();
27952943
if (ax.HasLabel()) {
2796-
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+
}
27972950
const ImVec2 label_size = CalcTextSizeVertical(label);
27982951
const float label_offset = (ax.HasTickLabels() ? tkr.MaxSize.x + gp.Style.LabelPadding.x : 0.0f)
27992952
+ gp.Style.LabelPadding.x;
@@ -2811,6 +2964,14 @@ void SetupFinish() {
28112964
}
28122965
}
28132966
}
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+
}
28142975
}
28152976

28162977

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

implot_internal.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -588,6 +588,8 @@ struct ImPlotTicker {
588588
ImVec2 MaxSize;
589589
ImVec2 LateSize;
590590
int Levels;
591+
double LabelOffset;
592+
int LabelDecimalShift;
591593

592594
ImPlotTicker() {
593595
Reset();
@@ -644,6 +646,8 @@ struct ImPlotTicker {
644646
MaxSize = LateSize;
645647
LateSize = ImVec2(0,0);
646648
Levels = 1;
649+
LabelOffset = 0.;
650+
LabelDecimalShift = 0;
647651
}
648652

649653
int TickCount() const {
@@ -1146,6 +1150,7 @@ struct ImPlotPlot
11461150
}
11471151

11481152
inline const char* GetAxisLabel(const ImPlotAxis& axis) const { return TextBuffer.Buf.Data + axis.LabelOffset; }
1153+
11491154
};
11501155

11511156
// Holds subplot data that must persist after EndSubplot

0 commit comments

Comments
 (0)