├── CMakeLists.txt ├── LICENSE ├── include └── imgui_plot.h ├── README.md └── src └── imgui_plot.cpp /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # This is only really useable inside another project with imgui already in 2 | 3 | set (srcs 4 | src/imgui_plot.cpp 5 | ) 6 | 7 | set(hdrs 8 | include/imgui_plot.h 9 | ) 10 | 11 | set(IMGUI_INCLUDE_DIR ${CMAKE_SOURCE_DIR}/imgui CACHE PATH "Path to Dear ImGui headers") 12 | 13 | add_library(imgui_plot ${srcs} ${hdrs}) 14 | target_include_directories(imgui_plot PUBLIC 15 | $ 16 | $ 17 | ) 18 | 19 | target_include_directories(imgui_plot PRIVATE 20 | ${IMGUI_INCLUDE_DIR} 21 | ) 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Anton Lobashev 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /include/imgui_plot.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #ifndef IMGUI_DEFINE_MATH_OPERATORS 4 | #define IMGUI_DEFINE_MATH_OPERATORS 5 | #endif 6 | #include 7 | 8 | namespace ImGui { 9 | // Use this structure to pass the plot data and settings into the Plot function 10 | struct PlotConfig { 11 | struct Values { 12 | // if necessary, you can provide x-axis values 13 | const float *xs = nullptr; 14 | // array of y values. If null, use ys_list (below) 15 | const float *ys = nullptr; 16 | // the number of values in each array 17 | int count; 18 | // at which offset to start plotting. 19 | // Warning: count+offset must be <= length of array! 20 | int offset = 0; 21 | // Plot color. If 0, use ImGuiCol_PlotLines. 22 | ImU32 color = 0; 23 | 24 | // in case you need to draw multiple plots at once, use this instead of ys 25 | const float **ys_list = nullptr; 26 | // the number of plots to draw 27 | int ys_count = 0; 28 | // colors for each plot 29 | const ImU32* colors = nullptr; 30 | } values; 31 | struct Scale { 32 | // Minimum plot value 33 | float min; 34 | // Maximum plot value 35 | float max; 36 | enum Type { 37 | Linear, 38 | Log10, 39 | }; 40 | // How to scale the x-axis 41 | Type type = Linear; 42 | } scale; 43 | struct Tooltip { 44 | bool show = false; 45 | const char* format = "%g: %8.4g"; 46 | } tooltip; 47 | struct Grid { 48 | bool show = false; 49 | float size = 100; // at which intervals to draw the grid 50 | int subticks = 10; // how many subticks in each tick 51 | } grid_x, grid_y; 52 | struct Selection { 53 | bool show = false; 54 | uint32_t* start = nullptr; 55 | uint32_t* length = nullptr; 56 | // "Sanitize" function. Give it selection length, and it will return 57 | // the "allowed" length. Useful for FFT, where selection must be 58 | // of power of two 59 | uint32_t(*sanitize_fn)(uint32_t) = nullptr; 60 | } selection; 61 | struct VerticalLines { 62 | bool show = false; 63 | const size_t* indices = nullptr; // at which indices to draw the lines 64 | size_t count = 0; 65 | } v_lines; 66 | ImVec2 frame_size = ImVec2(0.f, 0.f); 67 | float line_thickness = 1.f; 68 | bool skip_small_lines = true; 69 | const char* overlay_text = nullptr; 70 | }; 71 | 72 | enum class PlotStatus { 73 | nothing, 74 | selection_updated, 75 | }; 76 | 77 | IMGUI_API PlotStatus Plot(const char* label, const PlotConfig& conf); 78 | } 79 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # imgui-plot 2 | An improved plot widget for [Dear ImGui](https://github.com/ocornut/imgui/), aimed at displaying audio data 3 | 4 | ## TOC 5 | 1. [Screenshots](#screenshots) 6 | 2. [Rationale](#rationale) 7 | 3. [Usage](#usage) 8 | 4. [Installation](#installation) 9 | 5. [FAQ](#faq) 10 | 11 | ## Screenshots 12 | Displaying waveform and spectrum: 13 | ![Displaying Waveform and Spectrum](https://linuxoids.net/static/imgui-plot/waveform_and_spectrum.png) 14 | 15 | Custom tooltip: 16 | ![Custom Tooltip](https://linuxoids.net/static/imgui-plot/custom_tooltip.png) 17 | 18 | Selection example: 19 | ![Selection Example](https://linuxoids.net/static/imgui-plot/selection_example.gif) 20 | 21 | ## Rationale 22 | The `PlotLines()` function in Dear ImGui is nice and simple, but it does lack some basic features, such as grids, logarithmic scaling, custom tooltips etc. 23 | 24 | My work involves handling lots of waveforms and their spectrums, so I decided to extend `PlotLines()` with these features to display this data in a nice(r) way. 25 | 26 | ## Usage 27 | Instead of feeding all the parameters into plot function via its arguments, I decided that, with all the configurability, it would be cleaner to have a struct `PlotConfig` with all the neccessary stuff in it. See `imgui_plot.h` for its description. 28 | 29 | Simple usecase: 30 | ```cpp 31 | ImGui::PlotConfig conf; 32 | conf.values.xs = x_data; // this line is optional 33 | conf.values.ys = y_data; 34 | conf.values.count = data_count; 35 | conf.scale.min = -1; 36 | conf.scale.max = 1; 37 | conf.tooltip.show = true; 38 | conf.tooltip.format = "x=%.2f, y=%.2f"; 39 | conf.grid_x.show = true; 40 | conf.grid_y.show = true; 41 | conf.frame_size = ImVec2(400, 400); 42 | conf.line_thickness = 2.f; 43 | 44 | ImGui::Plot("plot", conf); 45 | ``` 46 | 47 | Selection example (gif above): 48 | ```cpp 49 | constexpr size_t buf_size = 512; 50 | static float x_data[buf_size]; 51 | static float y_data1[buf_size]; 52 | static float y_data2[buf_size]; 53 | static float y_data3[buf_size]; 54 | 55 | void generate_data() { 56 | constexpr float sampling_freq = 44100; 57 | constexpr float freq = 500; 58 | for (size_t i = 0; i < buf_size; ++i) { 59 | const float t = i / sampling_freq; 60 | x_data[i] = t; 61 | const float arg = 2 * M_PI * freq * t; 62 | y_data1[i] = sin(arg); 63 | y_data2[i] = y_data1[i] * -0.6 + sin(2 * arg) * 0.4; 64 | y_data3[i] = y_data2[i] * -0.6 + sin(3 * arg) * 0.4; 65 | } 66 | } 67 | 68 | void draw_multi_plot() { 69 | static const float* y_data[] = { y_data1, y_data2, y_data3 }; 70 | static ImU32 colors[3] = { ImColor(0, 255, 0), ImColor(255, 0, 0), ImColor(0, 0, 255) }; 71 | static uint32_t selection_start = 0, selection_length = 0; 72 | 73 | ImGui::Begin("Example plot", nullptr, ImGuiWindowFlags_AlwaysAutoResize); 74 | // Draw first plot with multiple sources 75 | ImGui::PlotConfig conf; 76 | conf.values.xs = x_data; 77 | conf.values.count = buf_size; 78 | conf.values.ys_list = y_data; // use ys_list to draw several lines simultaneously 79 | conf.values.ys_count = 3; 80 | conf.values.colors = colors; 81 | conf.scale.min = -1; 82 | conf.scale.max = 1; 83 | conf.tooltip.show = true; 84 | conf.grid_x.show = true; 85 | conf.grid_x.size = 128; 86 | conf.grid_x.subticks = 4; 87 | conf.grid_y.show = true; 88 | conf.grid_y.size = 0.5f; 89 | conf.grid_y.subticks = 5; 90 | conf.selection.show = true; 91 | conf.selection.start = &selection_start; 92 | conf.selection.length = &selection_length; 93 | conf.frame_size = ImVec2(buf_size, 200); 94 | ImGui::Plot("plot1", conf); 95 | 96 | // Draw second plot with the selection 97 | // reset previous values 98 | conf.values.ys_list = nullptr; 99 | conf.selection.show = false; 100 | // set new ones 101 | conf.values.ys = y_data3; 102 | conf.values.offset = selection_start; 103 | conf.values.count = selection_length; 104 | conf.line_thickness = 2.f; 105 | ImGui::Plot("plot2", conf); 106 | 107 | ImGui::End(); 108 | } 109 | 110 | ``` 111 | 112 | ## Installation 113 | Just copy `include/imgui_plot.h` and `src/imgui_plot.cpp` to where your imgui is, and it should work like that. 114 | 115 | ### CMake 116 | Alternatively, you can use [FetchContent](https://cmake.org/cmake/help/latest/module/FetchContent.html) like this: 117 | 118 | ```cmake 119 | include(FetchContent) 120 | # Optional: Set IMGUI_INCLUDE_DIR to the path of imgui sources, if different from the default. 121 | # set(IMGUI_INCLUDE_DIR ${CMAKE_SOURCE_DIR}/imgui) 122 | FetchContent_Declare( 123 | imgui_plot 124 | GIT_REPOSITORY https://github.com/soulthreads/imgui-plot.git 125 | GIT_TAG v0.1.0 126 | EXCLUDE_FROM_ALL 127 | ) 128 | FetchContent_MakeAvailable(imgui_plot) 129 | # ... 130 | target_link_libraries(${PROJECT_NAME} PRIVATE imgui_plot) 131 | ``` 132 | 133 | ## FAQ 134 | ### How do I do _x_? 135 | If something isn't obvious or your think my design is bad, please file away an issue, I'll take a look at it. 136 | 137 | If you want to have some new feature, issues and PRs are welcome too. 138 | -------------------------------------------------------------------------------- /src/imgui_plot.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | namespace ImGui { 6 | // [0..1] -> [0..1] 7 | static float rescale(float t, float min, float max, PlotConfig::Scale::Type type) { 8 | switch (type) { 9 | case PlotConfig::Scale::Linear: 10 | return t; 11 | case PlotConfig::Scale::Log10: 12 | return log10(ImLerp(min, max, t) / min) / log10(max / min); 13 | } 14 | return 0; 15 | } 16 | 17 | // [0..1] -> [0..1] 18 | static float rescale_inv(float t, float min, float max, PlotConfig::Scale::Type type) { 19 | switch (type) { 20 | case PlotConfig::Scale::Linear: 21 | return t; 22 | case PlotConfig::Scale::Log10: 23 | return (pow(max/min, t) * min - min) / (max - min); 24 | } 25 | return 0; 26 | } 27 | 28 | static int cursor_to_idx(const ImVec2& pos, const ImRect& bb, const PlotConfig& conf, float x_min, float x_max) { 29 | const float t = ImClamp((pos.x - bb.Min.x) / (bb.Max.x - bb.Min.x), 0.0f, 0.9999f); 30 | const int v_idx = (int)(rescale_inv(t, x_min, x_max, conf.scale.type) * (conf.values.count - 1)); 31 | IM_ASSERT(v_idx >= 0 && v_idx < conf.values.count); 32 | 33 | return v_idx; 34 | } 35 | 36 | PlotStatus Plot(const char* label, const PlotConfig& conf) { 37 | PlotStatus status = PlotStatus::nothing; 38 | 39 | ImGuiWindow* window = GetCurrentWindow(); 40 | if (window->SkipItems) 41 | return status; 42 | 43 | const float* const* ys_list = conf.values.ys_list; 44 | int ys_count = conf.values.ys_count; 45 | const ImU32* colors = conf.values.colors; 46 | if (conf.values.ys != nullptr) { // draw only a single plot 47 | ys_list = &conf.values.ys; 48 | ys_count = 1; 49 | colors = &conf.values.color; 50 | } 51 | 52 | ImGuiContext& g = *GImGui; 53 | const ImGuiStyle& style = g.Style; 54 | const ImGuiID id = window->GetID(label); 55 | 56 | const ImRect frame_bb( 57 | window->DC.CursorPos, 58 | window->DC.CursorPos + conf.frame_size); 59 | const ImRect inner_bb( 60 | frame_bb.Min + style.FramePadding, 61 | frame_bb.Max - style.FramePadding); 62 | const ImRect total_bb = frame_bb; 63 | ItemSize(total_bb, style.FramePadding.y); 64 | if (!ItemAdd(total_bb, 0, &frame_bb)) 65 | return status; 66 | const bool hovered = ItemHoverable(frame_bb, id, ImGuiItemFlags_None); 67 | 68 | RenderFrame( 69 | frame_bb.Min, 70 | frame_bb.Max, 71 | GetColorU32(ImGuiCol_FrameBg), 72 | true, 73 | style.FrameRounding); 74 | 75 | if (conf.values.count > 0) { 76 | int res_w; 77 | if (conf.skip_small_lines) 78 | res_w = ImMin((int)conf.frame_size.x, conf.values.count); 79 | else 80 | res_w = conf.values.count; 81 | res_w -= 1; 82 | int item_count = conf.values.count - 1; 83 | 84 | float x_min = conf.values.offset; 85 | float x_max = conf.values.offset + conf.values.count - 1; 86 | if (conf.values.xs) { 87 | x_min = conf.values.xs[size_t(x_min)]; 88 | x_max = conf.values.xs[size_t(x_max)]; 89 | } 90 | 91 | // Tooltip on hover 92 | int v_hovered = -1; 93 | if (conf.tooltip.show && hovered && inner_bb.Contains(g.IO.MousePos)) { 94 | const int v_idx = cursor_to_idx(g.IO.MousePos, inner_bb, conf, x_min, x_max); 95 | const size_t data_idx = conf.values.offset + (v_idx % conf.values.count); 96 | const float x0 = conf.values.xs ? conf.values.xs[data_idx] : v_idx; 97 | const float y0 = ys_list[0][data_idx]; // TODO: tooltip is only shown for the first y-value! 98 | SetTooltip(conf.tooltip.format, x0, y0); 99 | v_hovered = v_idx; 100 | } 101 | 102 | const float t_step = 1.0f / (float)res_w; 103 | const float inv_scale = (conf.scale.min == conf.scale.max) ? 104 | 0.0f : (1.0f / (conf.scale.max - conf.scale.min)); 105 | 106 | if (conf.grid_x.show) { 107 | int y0 = inner_bb.Min.y; 108 | int y1 = inner_bb.Max.y; 109 | switch (conf.scale.type) { 110 | case PlotConfig::Scale::Linear: { 111 | float cnt = conf.values.count / (conf.grid_x.size / conf.grid_x.subticks); 112 | float inc = 1.f / cnt; 113 | for (int i = 0; i <= cnt; ++i) { 114 | int x0 = ImLerp(inner_bb.Min.x, inner_bb.Max.x, i * inc); 115 | window->DrawList->AddLine( 116 | ImVec2(x0, y0), 117 | ImVec2(x0, y1), 118 | IM_COL32(200, 200, 200, (i % conf.grid_x.subticks) ? 128 : 255)); 119 | } 120 | break; 121 | } 122 | case PlotConfig::Scale::Log10: { 123 | float start = 1.f; 124 | while (start < x_max) { 125 | for (int i = 1; i < 10; ++i) { 126 | float x = start * i; 127 | if (x < x_min) continue; 128 | if (x > x_max) break; 129 | float t = log10(x / x_min) / log10(x_max / x_min); 130 | int x0 = ImLerp(inner_bb.Min.x, inner_bb.Max.x, t); 131 | window->DrawList->AddLine( 132 | ImVec2(x0, y0), 133 | ImVec2(x0, y1), 134 | IM_COL32(200, 200, 200, (i > 1) ? 128 : 255)); 135 | } 136 | start *= 10.f; 137 | } 138 | break; 139 | } 140 | } 141 | } 142 | if (conf.grid_y.show) { 143 | int x0 = inner_bb.Min.x; 144 | int x1 = inner_bb.Max.x; 145 | float cnt = (conf.scale.max - conf.scale.min) / (conf.grid_y.size / conf.grid_y.subticks); 146 | float inc = 1.f / cnt; 147 | for (int i = 0; i <= cnt; ++i) { 148 | int y0 = ImLerp(inner_bb.Min.y, inner_bb.Max.y, i * inc); 149 | window->DrawList->AddLine( 150 | ImVec2(x0, y0), 151 | ImVec2(x1, y0), 152 | IM_COL32(0, 0, 0, (i % conf.grid_y.subticks) ? 16 : 64)); 153 | } 154 | } 155 | 156 | const ImU32 col_hovered = GetColorU32(ImGuiCol_PlotLinesHovered); 157 | ImU32 col_base = GetColorU32(ImGuiCol_PlotLines); 158 | 159 | for (int i = 0; i < ys_count; ++i) { 160 | if (colors) { 161 | if (colors[i]) col_base = colors[i]; 162 | else col_base = GetColorU32(ImGuiCol_PlotLines); 163 | } 164 | float v0 = ys_list[i][conf.values.offset]; 165 | float t0 = 0.0f; 166 | // Point in the normalized space of our target rectangle 167 | ImVec2 tp0 = ImVec2(t0, 1.0f - ImSaturate((v0 - conf.scale.min) * inv_scale)); 168 | 169 | for (int n = 0; n < res_w; n++) 170 | { 171 | const float t1 = t0 + t_step; 172 | const int v1_idx = (int)(t0 * item_count + 0.5f); 173 | IM_ASSERT(v1_idx >= 0 && v1_idx < conf.values.count); 174 | const float v1 = ys_list[i][conf.values.offset + (v1_idx + 1) % conf.values.count]; 175 | const ImVec2 tp1 = ImVec2( 176 | rescale(t1, x_min, x_max, conf.scale.type), 177 | 1.0f - ImSaturate((v1 - conf.scale.min) * inv_scale)); 178 | 179 | // NB: Draw calls are merged together by the DrawList system. Still, we should render our batch are lower level to save a bit of CPU. 180 | ImVec2 pos0 = ImLerp(inner_bb.Min, inner_bb.Max, tp0); 181 | ImVec2 pos1 = ImLerp(inner_bb.Min, inner_bb.Max, tp1); 182 | 183 | if (v1_idx == v_hovered) { 184 | window->DrawList->AddCircleFilled(pos0, 3, col_hovered); 185 | } 186 | 187 | window->DrawList->AddLine( 188 | pos0, 189 | pos1, 190 | col_base, 191 | conf.line_thickness); 192 | 193 | t0 = t1; 194 | tp0 = tp1; 195 | } 196 | } 197 | 198 | if (conf.v_lines.show) { 199 | for (size_t i = 0; i < conf.v_lines.count; ++i) { 200 | const size_t idx = conf.v_lines.indices[i]; 201 | const float t1 = rescale(idx * t_step, x_min, x_max, conf.scale.type); 202 | ImVec2 pos0 = ImLerp(inner_bb.Min, inner_bb.Max, ImVec2(t1, 0.f)); 203 | ImVec2 pos1 = ImLerp(inner_bb.Min, inner_bb.Max, ImVec2(t1, 1.f)); 204 | window->DrawList->AddLine(pos0, pos1, IM_COL32(0xff, 0, 0, 0x88)); 205 | } 206 | } 207 | 208 | if (conf.selection.show) { 209 | if (hovered) { 210 | if (g.IO.MouseClicked[0]) { 211 | SetActiveID(id, window); 212 | FocusWindow(window); 213 | 214 | const int v_idx = cursor_to_idx(g.IO.MousePos, inner_bb, conf, x_min, x_max); 215 | uint32_t start = conf.values.offset + (v_idx % conf.values.count); 216 | uint32_t end = start; 217 | if (conf.selection.sanitize_fn) 218 | end = conf.selection.sanitize_fn(end - start) + start; 219 | if (end < conf.values.offset + conf.values.count) { 220 | *conf.selection.start = start; 221 | *conf.selection.length = end - start; 222 | status = PlotStatus::selection_updated; 223 | } 224 | } 225 | } 226 | 227 | if (g.ActiveId == id) { 228 | if (g.IO.MouseDown[0]) { 229 | const int v_idx = cursor_to_idx(g.IO.MousePos, inner_bb, conf, x_min, x_max); 230 | const uint32_t start = *conf.selection.start; 231 | uint32_t end = conf.values.offset + (v_idx % conf.values.count); 232 | if (end > start) { 233 | if (conf.selection.sanitize_fn) 234 | end = conf.selection.sanitize_fn(end - start) + start; 235 | if (end < conf.values.offset + conf.values.count) { 236 | *conf.selection.length = end - start; 237 | status = PlotStatus::selection_updated; 238 | } 239 | } 240 | } else { 241 | ClearActiveID(); 242 | } 243 | } 244 | float fSelectionStep = 1.0 / item_count; 245 | ImVec2 pos0 = ImLerp(inner_bb.Min, inner_bb.Max, 246 | ImVec2(fSelectionStep * *conf.selection.start, 0.f)); 247 | ImVec2 pos1 = ImLerp(inner_bb.Min, inner_bb.Max, 248 | ImVec2(fSelectionStep * (*conf.selection.start + *conf.selection.length), 1.f)); 249 | window->DrawList->AddRectFilled(pos0, pos1, IM_COL32(128, 128, 128, 32)); 250 | window->DrawList->AddRect(pos0, pos1, IM_COL32(128, 128, 128, 128)); 251 | } 252 | } 253 | 254 | // Text overlay 255 | if (conf.overlay_text) 256 | RenderTextClipped(ImVec2(frame_bb.Min.x, frame_bb.Min.y + style.FramePadding.y), frame_bb.Max, conf.overlay_text, NULL, NULL, ImVec2(0.5f,0.0f)); 257 | 258 | return status; 259 | } 260 | } 261 | --------------------------------------------------------------------------------