ESPHome  2025.2.0
All Data Structures Namespaces Files Functions Variables Typedefs Enumerations Enumerator Friends Modules Pages
graph.cpp
Go to the documentation of this file.
1 #include "graph.h"
3 #include "esphome/core/color.h"
4 #include "esphome/core/log.h"
5 #include "esphome/core/hal.h"
6 #include <algorithm>
7 namespace esphome {
8 namespace graph {
9 
10 using namespace display;
11 
12 static const char *const TAG = "graph";
13 static const char *const TAGL = "graphlegend";
14 
16  this->length_ = length;
17  this->samples_.resize(length, NAN);
18  this->last_sample_ = millis();
19 }
20 
21 void 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])) {
40  if (this->recent_max_ < 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 
56 void 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 
136  if (!std::isnan(this->gridspacing_y_)) {
137  for (int y = yn; y <= ym; y++) {
138  int16_t py = (int16_t) roundf((this->height_ - 1) * (1.0 - (float) (y - yn) / (ym - yn)));
139  for (uint32_t x = 0; x < this->width_; x += 2) {
140  buff->draw_pixel_at(x_offset + x, y_offset + py, color);
141  }
142  }
143  }
144  if (!std::isnan(this->gridspacing_x_) && (this->gridspacing_x_ > 0)) {
145  int n = this->duration_ / this->gridspacing_x_;
146  // Restrict drawing too many gridlines
147  if (n > 20) {
148  while (n > 20) {
149  n /= 2;
150  }
151  ESP_LOGW(TAG, "Graphing reducing x-scale to prevent too many gridlines");
152  }
153  for (int i = 0; i <= n; i++) {
154  for (uint32_t y = 0; y < this->height_; y += 2) {
155  buff->draw_pixel_at(x_offset + i * (this->width_ - 1) / n, y_offset + y, color);
156  }
157  }
158  }
159 
161  ESP_LOGV(TAG, "Updating graph. ymin %f, ymax %f", ymin, ymax);
162  for (auto *trace : traces_) {
163  Color c = trace->get_line_color();
164  int16_t thick = trace->get_line_thickness();
165  bool continuous = trace->get_continuous();
166  bool has_prev = false;
167  bool prev_b = false;
168  int16_t prev_y = 0;
169  for (uint32_t i = 0; i < this->width_; i++) {
170  float v = (trace->get_tracedata()->get_value(i) - ymin) / yrange;
171  if (!std::isnan(v) && (thick > 0)) {
172  int16_t x = this->width_ - 1 - i + x_offset;
173  uint8_t bit = 1 << ((i % (thick * LineType::PATTERN_LENGTH)) / thick);
174  bool b = (trace->get_line_type() & bit) == bit;
175  if (b) {
176  int16_t y = (int16_t) roundf((this->height_ - 1) * (1.0 - v)) - thick / 2 + y_offset;
177  auto draw_pixel_at = [&buff, c, y_offset, this](int16_t x, int16_t y) {
178  if (y >= y_offset && y < y_offset + this->height_)
179  buff->draw_pixel_at(x, y, c);
180  };
181  if (!continuous || !has_prev || !prev_b || (abs(y - prev_y) <= thick)) {
182  for (int16_t t = 0; t < thick; t++) {
183  draw_pixel_at(x, y + t);
184  }
185  } else {
186  int16_t mid_y = (y + prev_y + thick) / 2;
187  if (y > prev_y) {
188  for (int16_t t = prev_y + thick; t <= mid_y; t++)
189  draw_pixel_at(x + 1, t);
190  for (int16_t t = mid_y + 1; t < y + thick; t++)
191  draw_pixel_at(x, t);
192  } else {
193  for (int16_t t = prev_y - 1; t >= mid_y; t--)
194  draw_pixel_at(x + 1, t);
195  for (int16_t t = mid_y - 1; t >= y; t--)
196  draw_pixel_at(x, t);
197  }
198  }
199  prev_y = y;
200  }
201  prev_b = b;
202  has_prev = true;
203  } else {
204  has_prev = false;
205  }
206  }
207  }
208 }
209 
212  parent_ = g;
213 
214  // Determine maximum expected text and value width / height
215  int txtw = 0, txth = 0;
216  int valw = 0, valh = 0;
217  int lt = 0;
218  for (auto *trace : g->traces_) {
219  std::string txtstr = trace->get_name();
220  int fw, fos, fbl, fh;
221  this->font_label_->measure(txtstr.c_str(), &fw, &fos, &fbl, &fh);
222  if (fw > txtw)
223  txtw = fw;
224  if (fh > txth)
225  txth = fh;
226  if (trace->get_line_thickness() > lt)
227  lt = trace->get_line_thickness();
228  ESP_LOGI(TAGL, " %s %d %d", txtstr.c_str(), fw, fh);
229 
230  if (this->values_ != VALUE_POSITION_TYPE_NONE) {
231  std::string valstr =
232  value_accuracy_to_string(trace->sensor_->get_state(), trace->sensor_->get_accuracy_decimals());
233  if (this->units_) {
234  valstr += trace->sensor_->get_unit_of_measurement();
235  }
236  this->font_value_->measure(valstr.c_str(), &fw, &fos, &fbl, &fh);
237  if (fw > valw)
238  valw = fw;
239  if (fh > valh)
240  valh = fh;
241  ESP_LOGI(TAGL, " %s %d %d", valstr.c_str(), fw, fh);
242  }
243  }
244  // Add extra margin
245  txtw *= 1.2;
246  valw *= 1.2;
247 
248  uint8_t n = g->traces_.size();
249  uint16_t w = this->width_;
250  uint16_t h = this->height_;
251  DirectionType dir = this->direction_;
252  ValuePositionType valpos = this->values_;
253  if (!this->font_value_) {
254  valpos = VALUE_POSITION_TYPE_NONE;
255  }
256  // Line sample always goes below text for compactness
257  this->yl_ = txth + (txth / 4) + lt / 2;
258 
259  if (dir == DIRECTION_TYPE_AUTO) {
260  dir = DIRECTION_TYPE_HORIZONTAL; // as default
261  if (h > 0) {
263  }
264  }
265 
266  if (valpos == VALUE_POSITION_TYPE_AUTO) {
267  // TODO: do something smarter?? - fit to w and h?
268  valpos = VALUE_POSITION_TYPE_BELOW;
269  }
270 
271  if (valpos == VALUE_POSITION_TYPE_BELOW) {
272  this->yv_ = txth + (txth / 4);
273  if (this->lines_)
274  this->yv_ += txth / 4 + lt;
275  } else if (valpos == VALUE_POSITION_TYPE_BESIDE) {
276  this->xv_ = (txtw + valw) / 2;
277  }
278 
279  // If width or height is specified we divide evenly within, else we do tight-fit
280  if (w == 0) {
281  this->x0_ = txtw / 2;
282  this->xs_ = txtw;
283  if (valpos == VALUE_POSITION_TYPE_BELOW) {
284  this->xs_ = std::max(txtw, valw);
285  ;
286  this->x0_ = this->xs_ / 2;
287  } else if (valpos == VALUE_POSITION_TYPE_BESIDE) {
288  this->xs_ = txtw + valw;
289  }
290  if (dir == DIRECTION_TYPE_VERTICAL) {
291  this->width_ = this->xs_;
292  } else {
293  this->width_ = this->xs_ * n;
294  }
295  } else {
296  this->xs_ = w / n;
297  this->x0_ = this->xs_ / 2;
298  }
299 
300  if (h == 0) {
301  this->ys_ = txth;
302  if (valpos == VALUE_POSITION_TYPE_BELOW) {
303  this->ys_ = txth + txth / 2 + valh;
304  if (this->lines_) {
305  this->ys_ += lt;
306  }
307  } else if (valpos == VALUE_POSITION_TYPE_BESIDE) {
308  if (this->lines_) {
309  this->ys_ = std::max(txth + txth / 4 + lt + txth / 4, valh + valh / 4);
310  } else {
311  this->ys_ = std::max(txth + txth / 4, valh + valh / 4);
312  }
313  this->height_ = this->ys_ * n;
314  }
315  if (dir == DIRECTION_TYPE_HORIZONTAL) {
316  this->height_ = this->ys_;
317  } else {
318  this->height_ = this->ys_ * n;
319  }
320  } else {
321  this->ys_ = h / n;
322  }
323 
324  if (dir == DIRECTION_TYPE_HORIZONTAL) {
325  this->ys_ = 0;
326  } else {
327  this->xs_ = 0;
328  }
329 }
330 
331 void Graph::draw_legend(display::Display *buff, uint16_t x_offset, uint16_t y_offset, Color color) {
332  if (!legend_)
333  return;
334 
336  if (this->border_) {
337  int w = legend_->width_;
338  int h = legend_->height_;
339  buff->horizontal_line(x_offset, y_offset, w, color);
340  buff->horizontal_line(x_offset, y_offset + h - 1, w, color);
341  buff->vertical_line(x_offset, y_offset, h, color);
342  buff->vertical_line(x_offset + w - 1, y_offset, h, color);
343  }
344 
345  int x = x_offset + legend_->x0_;
346  int y = y_offset;
347  for (auto *trace : traces_) {
348  std::string txtstr = trace->get_name();
349  ESP_LOGV(TAG, " %s", txtstr.c_str());
350 
351  buff->printf(x, y, legend_->font_label_, trace->get_line_color(), TextAlign::TOP_CENTER, "%s", txtstr.c_str());
352 
353  if (legend_->lines_) {
354  uint16_t thick = trace->get_line_thickness();
355  for (int i = 0; i < legend_->x0_ * 4 / 3; i++) {
356  uint8_t b = (i % (thick * LineType::PATTERN_LENGTH)) / thick;
357  if (((uint8_t) trace->get_line_type() & (1 << b)) == (1 << b)) {
358  buff->vertical_line(x - legend_->x0_ * 2 / 3 + i, y + legend_->yl_ - thick / 2, thick,
359  trace->get_line_color());
360  }
361  }
362  }
363 
364  if (legend_->values_ != VALUE_POSITION_TYPE_NONE) {
365  int xv = x + legend_->xv_;
366  int yv = y + legend_->yv_;
367  std::string valstr =
368  value_accuracy_to_string(trace->sensor_->get_state(), trace->sensor_->get_accuracy_decimals());
369  if (legend_->units_) {
370  valstr += trace->sensor_->get_unit_of_measurement();
371  }
372  buff->printf(xv, yv, legend_->font_value_, trace->get_line_color(), TextAlign::TOP_CENTER, "%s", valstr.c_str());
373  ESP_LOGV(TAG, " value: %s", valstr.c_str());
374  }
375  x += legend_->xs_;
376  y += legend_->ys_;
377  }
378 }
379 
380 void Graph::setup() {
381  for (auto *trace : traces_) {
382  trace->init(this);
383  }
384 }
385 
387  for (auto *trace : traces_) {
388  ESP_LOGCONFIG(TAG, "Graph for sensor %s", trace->get_name().c_str());
389  }
390 }
391 
392 } // namespace graph
393 } // namespace esphome
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
uint32_t get_width()
Definition: graph.h:162
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:436
void take_sample(float data)
Definition: graph.cpp:21
uint16_t x
Definition: tt21100.cpp:17
uint8_t h
Definition: bl0906.h:209
void setup() override
Definition: graph.cpp:380
uint32_t IRAM_ATTR HOT millis()
Definition: core.cpp:25
uint16_t y
Definition: tt21100.cpp:18
std::vector< GraphTrace * > traces_
Definition: graph.h:176
void dump_config() override
Definition: graph.cpp:386
void draw_legend(display::Display *buff, uint16_t x_offset, uint16_t y_offset, Color color)
Definition: graph.cpp:331
void init(int length)
Definition: graph.cpp:15
void init(Graph *g)
Determine the best coordinates of drawing text + lines.
Definition: graph.cpp:211
uint32_t get_duration()
Definition: graph.h:161
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
ValuePositionType
Definition: graph.h:38
void init(Graph *g)
Definition: graph.cpp:49
uint16_t length
Definition: tt21100.cpp:12
void draw_pixel_at(int x, int y)
Set a single pixel at the specified coordinates to default color.
Definition: display.h:226
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,y] with font.
Definition: display.cpp:595
Implementation of SPI Controller mode.
Definition: a01nyub.cpp:7
void draw(display::Display *buff, uint16_t x_offset, uint16_t y_offset, Color color)
Definition: graph.cpp:56
bool state
Definition: fan.h:34