ESPHome  2024.12.2
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 #include <sstream>
8 #include <iostream> // std::cout, std::fixed
9 #include <iomanip>
10 namespace esphome {
11 namespace graph {
12 
13 using namespace display;
14 
15 static const char *const TAG = "graph";
16 static const char *const TAGL = "graphlegend";
17 
19  this->length_ = length;
20  this->samples_.resize(length, NAN);
21  this->last_sample_ = millis();
22 }
23 
24 void HistoryData::take_sample(float data) {
25  uint32_t tm = millis();
26  uint32_t dt = tm - last_sample_;
27  last_sample_ = tm;
28 
29  // Step data based on time
30  this->period_ += dt;
31  while (this->period_ >= this->update_time_) {
32  this->samples_[this->count_] = data;
33  this->period_ -= this->update_time_;
34  this->count_ = (this->count_ + 1) % this->length_;
35  ESP_LOGV(TAG, "Updating trace with value: %f", data);
36  }
37  if (!std::isnan(data)) {
38  // Recalc recent max/min
39  this->recent_min_ = data;
40  this->recent_max_ = data;
41  for (int i = 0; i < this->length_; i++) {
42  if (!std::isnan(this->samples_[i])) {
43  if (this->recent_max_ < this->samples_[i])
44  this->recent_max_ = this->samples_[i];
45  if (this->recent_min_ > this->samples_[i])
46  this->recent_min_ = this->samples_[i];
47  }
48  }
49  }
50 }
51 
53  ESP_LOGI(TAG, "Init trace for sensor %s", this->get_name().c_str());
54  this->data_.init(g->get_width());
55  sensor_->add_on_state_callback([this](float state) { this->data_.take_sample(state); });
56  this->data_.set_update_time_ms(g->get_duration() * 1000 / g->get_width());
57 }
58 
59 void Graph::draw(Display *buff, uint16_t x_offset, uint16_t y_offset, Color color) {
61  if (this->border_) {
62  buff->horizontal_line(x_offset, y_offset, this->width_, color);
63  buff->horizontal_line(x_offset, y_offset + this->height_ - 1, this->width_, color);
64  buff->vertical_line(x_offset, y_offset, this->height_, color);
65  buff->vertical_line(x_offset + this->width_ - 1, y_offset, this->height_, color);
66  }
68  float ymin = NAN;
69  float ymax = NAN;
70  for (auto *trace : traces_) {
71  float mx = trace->get_tracedata()->get_recent_max();
72  float mn = trace->get_tracedata()->get_recent_min();
73  if (std::isnan(ymax) || (ymax < mx))
74  ymax = mx;
75  if (std::isnan(ymin) || (ymin > mn))
76  ymin = mn;
77  }
78  // Adjust if manually overridden
79  if (!std::isnan(this->min_value_))
80  ymin = this->min_value_;
81  if (!std::isnan(this->max_value_))
82  ymax = this->max_value_;
83 
84  float yrange = ymax - ymin;
85  if (yrange > this->max_range_) {
86  // Look back in trace data to best-fit into local range
87  float mx = NAN;
88  float mn = NAN;
89  for (uint32_t i = 0; i < this->width_; i++) {
90  for (auto *trace : traces_) {
91  float v = trace->get_tracedata()->get_value(i);
92  if (!std::isnan(v)) {
93  if ((v - mn) > this->max_range_)
94  break;
95  if ((mx - v) > this->max_range_)
96  break;
97  if (std::isnan(mx) || (v > mx))
98  mx = v;
99  if (std::isnan(mn) || (v < mn))
100  mn = v;
101  }
102  }
103  }
104  yrange = this->max_range_;
105  if (!std::isnan(mn)) {
106  ymin = mn;
107  ymax = ymin + this->max_range_;
108  }
109  ESP_LOGV(TAG, "Graphing at max_range. Using local min %f, max %f", mn, mx);
110  }
111 
112  float y_per_div = this->min_range_;
113  if (!std::isnan(this->gridspacing_y_)) {
114  y_per_div = this->gridspacing_y_;
115  }
116  // Restrict drawing too many gridlines
117  if (yrange > 10 * y_per_div) {
118  while (yrange > 10 * y_per_div) {
119  y_per_div *= 2;
120  }
121  ESP_LOGW(TAG, "Graphing reducing y-scale to prevent too many gridlines");
122  }
123 
124  // Adjust limits to nice y_per_div boundaries
125  int yn = 0;
126  int ym = 1;
127  if (!std::isnan(ymin) && !std::isnan(ymax)) {
128  yn = (int) floorf(ymin / y_per_div);
129  ym = (int) ceilf(ymax / y_per_div);
130  if (yn == ym) {
131  ym++;
132  }
133  ymin = yn * y_per_div;
134  ymax = ym * y_per_div;
135  yrange = ymax - ymin;
136  }
137 
139  if (!std::isnan(this->gridspacing_y_)) {
140  for (int y = yn; y <= ym; y++) {
141  int16_t py = (int16_t) roundf((this->height_ - 1) * (1.0 - (float) (y - yn) / (ym - yn)));
142  for (uint32_t x = 0; x < this->width_; x += 2) {
143  buff->draw_pixel_at(x_offset + x, y_offset + py, color);
144  }
145  }
146  }
147  if (!std::isnan(this->gridspacing_x_) && (this->gridspacing_x_ > 0)) {
148  int n = this->duration_ / this->gridspacing_x_;
149  // Restrict drawing too many gridlines
150  if (n > 20) {
151  while (n > 20) {
152  n /= 2;
153  }
154  ESP_LOGW(TAG, "Graphing reducing x-scale to prevent too many gridlines");
155  }
156  for (int i = 0; i <= n; i++) {
157  for (uint32_t y = 0; y < this->height_; y += 2) {
158  buff->draw_pixel_at(x_offset + i * (this->width_ - 1) / n, y_offset + y, color);
159  }
160  }
161  }
162 
164  ESP_LOGV(TAG, "Updating graph. ymin %f, ymax %f", ymin, ymax);
165  for (auto *trace : traces_) {
166  Color c = trace->get_line_color();
167  int16_t thick = trace->get_line_thickness();
168  bool continuous = trace->get_continuous();
169  bool has_prev = false;
170  bool prev_b = false;
171  int16_t prev_y = 0;
172  for (uint32_t i = 0; i < this->width_; i++) {
173  float v = (trace->get_tracedata()->get_value(i) - ymin) / yrange;
174  if (!std::isnan(v) && (thick > 0)) {
175  int16_t x = this->width_ - 1 - i + x_offset;
176  uint8_t bit = 1 << ((i % (thick * LineType::PATTERN_LENGTH)) / thick);
177  bool b = (trace->get_line_type() & bit) == bit;
178  if (b) {
179  int16_t y = (int16_t) roundf((this->height_ - 1) * (1.0 - v)) - thick / 2 + y_offset;
180  auto draw_pixel_at = [&buff, c, y_offset, this](int16_t x, int16_t y) {
181  if (y >= y_offset && y < y_offset + this->height_)
182  buff->draw_pixel_at(x, y, c);
183  };
184  if (!continuous || !has_prev || !prev_b || (abs(y - prev_y) <= thick)) {
185  for (int16_t t = 0; t < thick; t++) {
186  draw_pixel_at(x, y + t);
187  }
188  } else {
189  int16_t mid_y = (y + prev_y + thick) / 2;
190  if (y > prev_y) {
191  for (int16_t t = prev_y + thick; t <= mid_y; t++)
192  draw_pixel_at(x + 1, t);
193  for (int16_t t = mid_y + 1; t < y + thick; t++)
194  draw_pixel_at(x, t);
195  } else {
196  for (int16_t t = prev_y - 1; t >= mid_y; t--)
197  draw_pixel_at(x + 1, t);
198  for (int16_t t = mid_y - 1; t >= y; t--)
199  draw_pixel_at(x, t);
200  }
201  }
202  prev_y = y;
203  }
204  prev_b = b;
205  has_prev = true;
206  } else {
207  has_prev = false;
208  }
209  }
210  }
211 }
212 
215  parent_ = g;
216 
217  // Determine maximum expected text and value width / height
218  int txtw = 0, txth = 0;
219  int valw = 0, valh = 0;
220  int lt = 0;
221  for (auto *trace : g->traces_) {
222  std::string txtstr = trace->get_name();
223  int fw, fos, fbl, fh;
224  this->font_label_->measure(txtstr.c_str(), &fw, &fos, &fbl, &fh);
225  if (fw > txtw)
226  txtw = fw;
227  if (fh > txth)
228  txth = fh;
229  if (trace->get_line_thickness() > lt)
230  lt = trace->get_line_thickness();
231  ESP_LOGI(TAGL, " %s %d %d", txtstr.c_str(), fw, fh);
232 
233  if (this->values_ != VALUE_POSITION_TYPE_NONE) {
234  std::stringstream ss;
235  ss << std::fixed << std::setprecision(trace->sensor_->get_accuracy_decimals()) << trace->sensor_->get_state();
236  std::string valstr = ss.str();
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_) {
258  valpos = VALUE_POSITION_TYPE_NONE;
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?
272  valpos = VALUE_POSITION_TYPE_BELOW;
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 
335 void 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 
368  if (legend_->values_ != VALUE_POSITION_TYPE_NONE) {
369  int xv = x + legend_->xv_;
370  int yv = y + legend_->yv_;
371  std::stringstream ss;
372  ss << std::fixed << std::setprecision(trace->sensor_->get_accuracy_decimals()) << trace->sensor_->get_state();
373  std::string valstr = ss.str();
374  if (legend_->units_) {
375  valstr += trace->sensor_->get_unit_of_measurement();
376  }
377  buff->printf(xv, yv, legend_->font_value_, trace->get_line_color(), TextAlign::TOP_CENTER, "%s", valstr.c_str());
378  ESP_LOGV(TAG, " value: %s", valstr.c_str());
379  }
380  x += legend_->xs_;
381  y += legend_->ys_;
382  }
383 }
384 
385 void Graph::setup() {
386  for (auto *trace : traces_) {
387  trace->init(this);
388  }
389 }
390 
392  for (auto *trace : traces_) {
393  ESP_LOGCONFIG(TAG, "Graph for sensor %s", trace->get_name().c_str());
394  }
395 }
396 
397 } // namespace graph
398 } // 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
void take_sample(float data)
Definition: graph.cpp:24
uint16_t x
Definition: tt21100.cpp:17
uint8_t h
Definition: bl0906.h:209
void setup() override
Definition: graph.cpp:385
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:391
void draw_legend(display::Display *buff, uint16_t x_offset, uint16_t y_offset, Color color)
Definition: graph.cpp:335
void init(int length)
Definition: graph.cpp:18
void init(Graph *g)
Determine the best coordinates of drawing text + lines.
Definition: graph.cpp:214
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:52
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:594
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:59
bool state
Definition: fan.h:34