ESPHome 2025.5.0
Loading...
Searching...
No Matches
graph.cpp
Go to the documentation of this file.
1#include "graph.h"
4#include "esphome/core/log.h"
5#include "esphome/core/hal.h"
6#include <algorithm>
7namespace esphome {
8namespace graph {
9
10using namespace display;
11
12static const char *const TAG = "graph";
13static const char *const TAGL = "graphlegend";
14
16 this->length_ = length;
17 this->samples_.resize(length, NAN);
18 this->last_sample_ = millis();
19}
20
21void HistoryData::take_sample(float data) {
22 uint32_t tm = millis();
23 uint32_t dt = tm - last_sample_;
24 last_sample_ = tm;
25
26 // Step data based on time
27 this->period_ += dt;
28 while (this->period_ >= this->update_time_) {
29 this->samples_[this->count_] = data;
30 this->period_ -= this->update_time_;
31 this->count_ = (this->count_ + 1) % this->length_;
32 ESP_LOGV(TAG, "Updating trace with value: %f", data);
33 }
34 if (!std::isnan(data)) {
35 // Recalc recent max/min
36 this->recent_min_ = data;
37 this->recent_max_ = data;
38 for (int i = 0; i < this->length_; i++) {
39 if (!std::isnan(this->samples_[i])) {
41 this->recent_max_ = this->samples_[i];
42 if (this->recent_min_ > this->samples_[i])
43 this->recent_min_ = this->samples_[i];
44 }
45 }
46 }
47}
48
50 ESP_LOGI(TAG, "Init trace for sensor %s", this->get_name().c_str());
51 this->data_.init(g->get_width());
52 sensor_->add_on_state_callback([this](float state) { this->data_.take_sample(state); });
53 this->data_.set_update_time_ms(g->get_duration() * 1000 / g->get_width());
54}
55
56void Graph::draw(Display *buff, uint16_t x_offset, uint16_t y_offset, Color color) {
58 if (this->border_) {
59 buff->horizontal_line(x_offset, y_offset, this->width_, color);
60 buff->horizontal_line(x_offset, y_offset + this->height_ - 1, this->width_, color);
61 buff->vertical_line(x_offset, y_offset, this->height_, color);
62 buff->vertical_line(x_offset + this->width_ - 1, y_offset, this->height_, color);
63 }
65 float ymin = NAN;
66 float ymax = NAN;
67 for (auto *trace : traces_) {
68 float mx = trace->get_tracedata()->get_recent_max();
69 float mn = trace->get_tracedata()->get_recent_min();
70 if (std::isnan(ymax) || (ymax < mx))
71 ymax = mx;
72 if (std::isnan(ymin) || (ymin > mn))
73 ymin = mn;
74 }
75 // Adjust if manually overridden
76 if (!std::isnan(this->min_value_))
77 ymin = this->min_value_;
78 if (!std::isnan(this->max_value_))
79 ymax = this->max_value_;
80
81 float yrange = ymax - ymin;
82 if (yrange > this->max_range_) {
83 // Look back in trace data to best-fit into local range
84 float mx = NAN;
85 float mn = NAN;
86 for (uint32_t i = 0; i < this->width_; i++) {
87 for (auto *trace : traces_) {
88 float v = trace->get_tracedata()->get_value(i);
89 if (!std::isnan(v)) {
90 if ((v - mn) > this->max_range_)
91 break;
92 if ((mx - v) > this->max_range_)
93 break;
94 if (std::isnan(mx) || (v > mx))
95 mx = v;
96 if (std::isnan(mn) || (v < mn))
97 mn = v;
98 }
99 }
100 }
101 yrange = this->max_range_;
102 if (!std::isnan(mn)) {
103 ymin = mn;
104 ymax = ymin + this->max_range_;
105 }
106 ESP_LOGV(TAG, "Graphing at max_range. Using local min %f, max %f", mn, mx);
107 }
108
109 float y_per_div = this->min_range_;
110 if (!std::isnan(this->gridspacing_y_)) {
111 y_per_div = this->gridspacing_y_;
112 }
113 // Restrict drawing too many gridlines
114 if (yrange > 10 * y_per_div) {
115 while (yrange > 10 * y_per_div) {
116 y_per_div *= 2;
117 }
118 ESP_LOGW(TAG, "Graphing reducing y-scale to prevent too many gridlines");
119 }
120
121 // Adjust limits to nice y_per_div boundaries
122 int yn = 0;
123 int ym = 1;
124 if (!std::isnan(ymin) && !std::isnan(ymax)) {
125 yn = (int) floorf(ymin / y_per_div);
126 ym = (int) ceilf(ymax / y_per_div);
127 if (yn == ym) {
128 ym++;
129 }
130 ymin = yn * y_per_div;
131 ymax = ym * y_per_div;
132 yrange = ymax - ymin;
133 }
134
135 // Store graph limts
136 this->graph_limit_max_ = ymax;
137 this->graph_limit_min_ = ymin;
138
140 if (!std::isnan(this->gridspacing_y_)) {
141 for (int y = yn; y <= ym; y++) {
142 int16_t py = (int16_t) roundf((this->height_ - 1) * (1.0 - (float) (y - yn) / (ym - yn)));
143 for (uint32_t x = 0; x < this->width_; x += 2) {
144 buff->draw_pixel_at(x_offset + x, y_offset + py, color);
145 }
146 }
147 }
148 if (!std::isnan(this->gridspacing_x_) && (this->gridspacing_x_ > 0)) {
149 int n = this->duration_ / this->gridspacing_x_;
150 // Restrict drawing too many gridlines
151 if (n > 20) {
152 while (n > 20) {
153 n /= 2;
154 }
155 ESP_LOGW(TAG, "Graphing reducing x-scale to prevent too many gridlines");
156 }
157 for (int i = 0; i <= n; i++) {
158 for (uint32_t y = 0; y < this->height_; y += 2) {
159 buff->draw_pixel_at(x_offset + i * (this->width_ - 1) / n, y_offset + y, color);
160 }
161 }
162 }
163
165 ESP_LOGV(TAG, "Updating graph. ymin %f, ymax %f", ymin, ymax);
166 for (auto *trace : traces_) {
167 Color c = trace->get_line_color();
168 int16_t thick = trace->get_line_thickness();
169 bool continuous = trace->get_continuous();
170 bool has_prev = false;
171 bool prev_b = false;
172 int16_t prev_y = 0;
173 for (uint32_t i = 0; i < this->width_; i++) {
174 float v = (trace->get_tracedata()->get_value(i) - ymin) / yrange;
175 if (!std::isnan(v) && (thick > 0)) {
176 int16_t x = this->width_ - 1 - i + x_offset;
177 uint8_t bit = 1 << ((i % (thick * LineType::PATTERN_LENGTH)) / thick);
178 bool b = (trace->get_line_type() & bit) == bit;
179 if (b) {
180 int16_t y = (int16_t) roundf((this->height_ - 1) * (1.0 - v)) - thick / 2 + y_offset;
181 auto draw_pixel_at = [&buff, c, y_offset, this](int16_t x, int16_t y) {
182 if (y >= y_offset && y < y_offset + this->height_)
183 buff->draw_pixel_at(x, y, c);
184 };
185 if (!continuous || !has_prev || !prev_b || (abs(y - prev_y) <= thick)) {
186 for (int16_t t = 0; t < thick; t++) {
187 draw_pixel_at(x, y + t);
188 }
189 } else {
190 int16_t mid_y = (y + prev_y + thick) / 2;
191 if (y > prev_y) {
192 for (int16_t t = prev_y + thick; t <= mid_y; t++)
193 draw_pixel_at(x + 1, t);
194 for (int16_t t = mid_y + 1; t < y + thick; t++)
195 draw_pixel_at(x, t);
196 } else {
197 for (int16_t t = prev_y - 1; t >= mid_y; t--)
198 draw_pixel_at(x + 1, t);
199 for (int16_t t = mid_y - 1; t >= y; t--)
200 draw_pixel_at(x, t);
201 }
202 }
203 prev_y = y;
204 }
205 prev_b = b;
206 has_prev = true;
207 } else {
208 has_prev = false;
209 }
210 }
211 }
212}
213
216 parent_ = g;
217
218 // Determine maximum expected text and value width / height
219 int txtw = 0, txth = 0;
220 int valw = 0, valh = 0;
221 int lt = 0;
222 for (auto *trace : g->traces_) {
223 std::string txtstr = trace->get_name();
224 int fw, fos, fbl, fh;
225 this->font_label_->measure(txtstr.c_str(), &fw, &fos, &fbl, &fh);
226 if (fw > txtw)
227 txtw = fw;
228 if (fh > txth)
229 txth = fh;
230 if (trace->get_line_thickness() > lt)
231 lt = trace->get_line_thickness();
232 ESP_LOGI(TAGL, " %s %d %d", txtstr.c_str(), fw, fh);
233
234 if (this->values_ != VALUE_POSITION_TYPE_NONE) {
235 std::string valstr =
236 value_accuracy_to_string(trace->sensor_->get_state(), trace->sensor_->get_accuracy_decimals());
237 if (this->units_) {
238 valstr += trace->sensor_->get_unit_of_measurement();
239 }
240 this->font_value_->measure(valstr.c_str(), &fw, &fos, &fbl, &fh);
241 if (fw > valw)
242 valw = fw;
243 if (fh > valh)
244 valh = fh;
245 ESP_LOGI(TAGL, " %s %d %d", valstr.c_str(), fw, fh);
246 }
247 }
248 // Add extra margin
249 txtw *= 1.2;
250 valw *= 1.2;
251
252 uint8_t n = g->traces_.size();
253 uint16_t w = this->width_;
254 uint16_t h = this->height_;
255 DirectionType dir = this->direction_;
256 ValuePositionType valpos = this->values_;
257 if (!this->font_value_) {
259 }
260 // Line sample always goes below text for compactness
261 this->yl_ = txth + (txth / 4) + lt / 2;
262
263 if (dir == DIRECTION_TYPE_AUTO) {
264 dir = DIRECTION_TYPE_HORIZONTAL; // as default
265 if (h > 0) {
267 }
268 }
269
270 if (valpos == VALUE_POSITION_TYPE_AUTO) {
271 // TODO: do something smarter?? - fit to w and h?
273 }
274
275 if (valpos == VALUE_POSITION_TYPE_BELOW) {
276 this->yv_ = txth + (txth / 4);
277 if (this->lines_)
278 this->yv_ += txth / 4 + lt;
279 } else if (valpos == VALUE_POSITION_TYPE_BESIDE) {
280 this->xv_ = (txtw + valw) / 2;
281 }
282
283 // If width or height is specified we divide evenly within, else we do tight-fit
284 if (w == 0) {
285 this->x0_ = txtw / 2;
286 this->xs_ = txtw;
287 if (valpos == VALUE_POSITION_TYPE_BELOW) {
288 this->xs_ = std::max(txtw, valw);
289 ;
290 this->x0_ = this->xs_ / 2;
291 } else if (valpos == VALUE_POSITION_TYPE_BESIDE) {
292 this->xs_ = txtw + valw;
293 }
294 if (dir == DIRECTION_TYPE_VERTICAL) {
295 this->width_ = this->xs_;
296 } else {
297 this->width_ = this->xs_ * n;
298 }
299 } else {
300 this->xs_ = w / n;
301 this->x0_ = this->xs_ / 2;
302 }
303
304 if (h == 0) {
305 this->ys_ = txth;
306 if (valpos == VALUE_POSITION_TYPE_BELOW) {
307 this->ys_ = txth + txth / 2 + valh;
308 if (this->lines_) {
309 this->ys_ += lt;
310 }
311 } else if (valpos == VALUE_POSITION_TYPE_BESIDE) {
312 if (this->lines_) {
313 this->ys_ = std::max(txth + txth / 4 + lt + txth / 4, valh + valh / 4);
314 } else {
315 this->ys_ = std::max(txth + txth / 4, valh + valh / 4);
316 }
317 this->height_ = this->ys_ * n;
318 }
319 if (dir == DIRECTION_TYPE_HORIZONTAL) {
320 this->height_ = this->ys_;
321 } else {
322 this->height_ = this->ys_ * n;
323 }
324 } else {
325 this->ys_ = h / n;
326 }
327
328 if (dir == DIRECTION_TYPE_HORIZONTAL) {
329 this->ys_ = 0;
330 } else {
331 this->xs_ = 0;
332 }
333}
334
335void Graph::draw_legend(display::Display *buff, uint16_t x_offset, uint16_t y_offset, Color color) {
336 if (!legend_)
337 return;
338
340 if (this->border_) {
341 int w = legend_->width_;
342 int h = legend_->height_;
343 buff->horizontal_line(x_offset, y_offset, w, color);
344 buff->horizontal_line(x_offset, y_offset + h - 1, w, color);
345 buff->vertical_line(x_offset, y_offset, h, color);
346 buff->vertical_line(x_offset + w - 1, y_offset, h, color);
347 }
348
349 int x = x_offset + legend_->x0_;
350 int y = y_offset;
351 for (auto *trace : traces_) {
352 std::string txtstr = trace->get_name();
353 ESP_LOGV(TAG, " %s", txtstr.c_str());
354
355 buff->printf(x, y, legend_->font_label_, trace->get_line_color(), TextAlign::TOP_CENTER, "%s", txtstr.c_str());
356
357 if (legend_->lines_) {
358 uint16_t thick = trace->get_line_thickness();
359 for (int i = 0; i < legend_->x0_ * 4 / 3; i++) {
360 uint8_t b = (i % (thick * LineType::PATTERN_LENGTH)) / thick;
361 if (((uint8_t) trace->get_line_type() & (1 << b)) == (1 << b)) {
362 buff->vertical_line(x - legend_->x0_ * 2 / 3 + i, y + legend_->yl_ - thick / 2, thick,
363 trace->get_line_color());
364 }
365 }
366 }
367
369 int xv = x + legend_->xv_;
370 int yv = y + legend_->yv_;
371 std::string valstr =
372 value_accuracy_to_string(trace->sensor_->get_state(), trace->sensor_->get_accuracy_decimals());
373 if (legend_->units_) {
374 valstr += trace->sensor_->get_unit_of_measurement();
375 }
376 buff->printf(xv, yv, legend_->font_value_, trace->get_line_color(), TextAlign::TOP_CENTER, "%s", valstr.c_str());
377 ESP_LOGV(TAG, " value: %s", valstr.c_str());
378 }
379 x += legend_->xs_;
380 y += legend_->ys_;
381 }
382}
383
385 for (auto *trace : traces_) {
386 trace->init(this);
387 }
388}
389
391 for (auto *trace : traces_) {
392 ESP_LOGCONFIG(TAG, "Graph for sensor %s", trace->get_name().c_str());
393 }
394}
395
396} // namespace graph
397} // namespace esphome
uint8_t h
Definition bl0906.h:2
virtual void measure(const char *str, int *width, int *x_offset, int *baseline, int *height)=0
void horizontal_line(int x, int y, int width, Color color=COLOR_ON)
Draw a horizontal line from the point [x,y] to [x+width,y] with the given color.
Definition display.cpp:88
void printf(int x, int y, BaseFont *font, Color color, Color background, TextAlign align, const char *format,...) __attribute__((format(printf
Evaluate the printf-format format and print the result with the anchor point at [x,...
Definition display.cpp:595
void draw_pixel_at(int x, int y)
Set a single pixel at the specified coordinates to default color.
Definition display.h:226
void vertical_line(int x, int y, int height, Color color=COLOR_ON)
Draw a vertical line from the point [x,y] to [x,y+width] with the given color.
Definition display.cpp:93
void dump_config() override
Definition graph.cpp:390
void setup() override
Definition graph.cpp:384
GraphLegend * legend_
Definition graph.h:181
float graph_limit_min_
in pixels
Definition graph.h:171
void draw(display::Display *buff, uint16_t x_offset, uint16_t y_offset, Color color)
Definition graph.cpp:56
std::vector< GraphTrace * > traces_
Definition graph.h:180
uint32_t width_
in seconds
Definition graph.h:169
void draw_legend(display::Display *buff, uint16_t x_offset, uint16_t y_offset, Color color)
Definition graph.cpp:335
uint32_t get_duration()
Definition graph.h:161
uint32_t get_width()
Definition graph.h:162
uint32_t duration_
Definition graph.h:168
uint32_t height_
in pixels
Definition graph.h:170
display::BaseFont * font_label_
Definition graph.h:66
display::BaseFont * font_value_
Definition graph.h:67
DirectionType direction_
Definition graph.h:65
void init(Graph *g)
Determine the best coordinates of drawing text + lines.
Definition graph.cpp:215
ValuePositionType values_
Definition graph.h:63
sensor::Sensor * sensor_
Definition graph.h:125
std::string get_name()
Definition graph.h:121
void init(Graph *g)
Definition graph.cpp:49
void init(int length)
Definition graph.cpp:15
void set_update_time_ms(uint32_t update_time_ms)
Definition graph.h:90
void take_sample(float data)
Definition graph.cpp:21
std::vector< float > samples_
Definition graph.h:105
uint32_t update_time_
in ms
Definition graph.h:100
void add_on_state_callback(std::function< void(float)> &&callback)
Add a callback that will be called every time a filtered value arrives.
Definition sensor.cpp:52
bool state
Definition fan.h:0
@ VALUE_POSITION_TYPE_NONE
Definition graph.h:39
@ VALUE_POSITION_TYPE_AUTO
Definition graph.h:40
@ VALUE_POSITION_TYPE_BELOW
Definition graph.h:42
@ VALUE_POSITION_TYPE_BESIDE
Definition graph.h:41
@ PATTERN_LENGTH
Definition graph.h:29
@ DIRECTION_TYPE_HORIZONTAL
Definition graph.h:34
@ DIRECTION_TYPE_AUTO
Definition graph.h:33
@ DIRECTION_TYPE_VERTICAL
Definition graph.h:35
Providing packet encoding functions for exchanging data with a remote host.
Definition a01nyub.cpp:7
std::string value_accuracy_to_string(float value, int8_t accuracy_decimals)
Create a string from a value and an accuracy in decimals.
Definition helpers.cpp:435
uint32_t IRAM_ATTR HOT millis()
Definition core.cpp:27
uint16_t length
Definition tt21100.cpp:0
uint16_t x
Definition tt21100.cpp:5
uint16_t y
Definition tt21100.cpp:6