Question regarding Graph plot

Hello to everyone!
I am designing a weather station, for wind reading purposes, I would like to do a graph as shown below:


The idea is to plot the 5 min wind average in the center and then the wind readings deviation to the sides,

What kind of display do you recommend for doing this? Is there any special library for doing graphs?
is arduino uno right tool for this?

thanks

fede

Probably not.

To get a graph of that resolution you need to store a lot of pixels in the processor's SRAM memory. In a Uno this is limited to just 2K, so if you use all of it that just leaves you with a 45 by 45 display to work with, assuming one byte per pixel. For a 1 bit per pixel you could go up to 361 square, but that is using all your SRAM.

Better get something with a bit more memory like a Mega.

But before you decide first find a display to use.
It is likely it will be an LCD display because really available OLED displays do not have that much resolution.

Of course @Grumpy_Mike is right regarding the RAM of Arduino UNO!

If - for example - you take only 4 samples per minute and display the data of the last five minutes, you end up with 20 data. That would be easy to handle. If you want to have a resolution per second, 5 minutes would require 5 x 60 = 300 data.

So it would be helpful if you could elaborate on

  • What is the resolution in time for the data?
  • What is the maximum number of data to be stored/displayed?

GLCD and OLED tend to be 128x64.
Common TFT are 320x240 or 480x320.

You can use hardware to scroll most of these displays. e.g. for fast moving complex data. LCD and OLED hardware scrolls in short direction. TFT scrolls in long direction. So you would choose time as short side for LCD or long side for TFT.

Since you appear to have about 4 plots a minute the data is neither fast or complex. So you don't need speed. You can just re-draw every 15 seconds or whatever.

I suggest that you look at existing Weather Stations. Think carefully about graph axes and resolution. If you are not in the middle of a hurricane or tornado the average wind speed does not change every few seconds.
In practice it is average that you want to see and not the instantaneous speed.

A Uno is fine if you use the display hardware to scroll.

David.

Mi idea is to take 70 readings in 5 minutes, a reading every 5 seconds.
Maximum ammount of data displayed would be those 70 readings in the 5 minutes period of time.
Theres no need to store older values, however maybe in a future would be nice to store those values on a SD card.

Thanks

Fede

Hello David! Thanks for your message. This weather station it’s intended for sailboat racing, where 5 degrees of windshift (that happens really often due to turbulence) can make the difference for a boat to arrive before another one,
Unfortunately, precision is one of my biggest challenge into this build.
And I am not taking into consideration my inexperience in programming as well! Lol

Thanks

Fede

The following sketch may give you some testing ability in case you have an Adafruit_ILI9341 display at hand:


#include "SPI.h"
#include "Adafruit_GFX.h"
#include "Adafruit_ILI9341.h"

// For the Adafruit shield, these are the default.
#define TFT_DC 9
#define TFT_CS 10

const int Xoff = 32;
const int Yoff = 32;

const byte HalfMarkerLength = 4;
const byte TopOffset = 18;
const byte DotRadius  = 2;

const unsigned long SampleInterval = 10000; // [msec]
const int maxData = 70;                     // No of Samples

int data[maxData];
int actData = 0;
int AverageLine = 0;
float StepPerDeviationMark;  // 10 units from mark to mark


// Use hardware SPI (on Uno, #13, #12, #11) and the above for CS/DC
Adafruit_ILI9341 tft = Adafruit_ILI9341(TFT_CS, TFT_DC);
// If using the breakout, change pins as desired
//Adafruit_ILI9341 tft = Adafruit_ILI9341(TFT_CS, TFT_DC, TFT_MOSI, TFT_CLK, TFT_RST, TFT_MISO);

void setup() {
  Serial.begin(9600);
  Serial.println("Test"); 
 
  tft.begin();
  tft.setRotation(1);
  CoordinateSystem();
  CreateTestData();
}

void loop(void) {
  static unsigned long lastMillis = 0;
  if (millis()-lastMillis > SampleInterval){
    lastMillis = millis();
    data[actData] = random(-50,51);
    PlotData(ILI9341_RED);
    actData++;
    if (actData >= maxData) RefreshField();
    //if (actData >= maxData) RefreshPlots();
  }
}

void CreateTestData(){
  for (int i = 0;i < maxData-1;i++){
    data[actData] = random(-50,51);
    PlotData(ILI9341_RED);
    actData++;
  }
}

void RefreshField(){
  Serial.println("Refresh");
  int x = Xoff+ HalfMarkerLength;
  int y = TopOffset;
  tft.fillRect(x,y,tft.width()-x, tft.height()-y-Yoff-HalfMarkerLength, ILI9341_BLACK);
  delay(300); // Give some time for erasing the data
  // Reprint the average line
  PlotFromTo(-HalfMarkerLength,AverageLine,tft.width(),AverageLine,ILI9341_YELLOW, false );
  // Move all data one field to the "left"
  for (int i = 1; i < maxData;i++) data[i-1] = data[i];
  // Replot the "old" data
  for (int i = 0; i < maxData-1;i++){ 
    actData = i;
    PlotData(ILI9341_RED);
  }
  actData = maxData-1;
}


void RefreshPlots(){
  Serial.println("Refresh");
  for (int i = 0; i < maxData;i++){ 
    actData = i;
    PlotData(ILI9341_BLACK);
  }
  for (int i = 1; i < maxData;i++) data[i-1] = data[i];
  PlotFromTo(-HalfMarkerLength,AverageLine,tft.width(),AverageLine,ILI9341_YELLOW, false );
  for (int i = 0; i < maxData-1;i++){ 
    actData = i;
    PlotData(ILI9341_RED);
  }
  actData = maxData-1;
}

void CoordinateSystem() {
 
  int           x1, y1, x2, y2,
                w = tft.width(),
                h = tft.height();
  tft.fillScreen(ILI9341_BLACK);
  tft.setCursor(0, 0);
  tft.setTextColor(ILI9341_WHITE);  
  tft.setTextSize(2);
  tft.println("Wind Graph");
  // x axis 
  tft.drawLine(0,h-Yoff,w,h-Yoff,ILI9341_WHITE);  
  int StepPerMinute = (w - Xoff) / 6;
  for (int i = 1; i < 6; i++) {
    PlotFromTo(i*StepPerMinute,-HalfMarkerLength,i*StepPerMinute, HalfMarkerLength,ILI9341_WHITE, false );
    tft.setCursor(i*StepPerMinute+Xoff-4, h-Yoff+HalfMarkerLength*2);
    tft.print(i);
  }  
  // y axis
  tft.drawLine(Xoff,TopOffset,Xoff,w,ILI9341_WHITE);
  StepPerDeviationMark = (h - Yoff) / 12;
  tft.setTextSize(1);
  int value = 50;
  for (int i = 1; i < 12; i++) {
    if (value == 0){
      AverageLine = i*StepPerDeviationMark;
      PlotFromTo(-HalfMarkerLength,AverageLine,w,AverageLine,ILI9341_YELLOW, false );
    } 
    PlotFromTo(-HalfMarkerLength,i*StepPerDeviationMark,HalfMarkerLength,i*StepPerDeviationMark,ILI9341_WHITE, false );
    tft.setCursor(4,i*StepPerDeviationMark);
    tft.print(value);
    value -= 10;
  }  

}

void PlotFromTo(int x1,int y1, int x2, int y2, uint16_t color, boolean MarkDot){
  x1 = x1+Xoff;
  y1 = tft.height()-(y1+Yoff);
  x2 = x2+Xoff;
  y2 = tft.height()-(y2+Yoff);
  tft.drawLine(x1,y1,x2,y2,color);
  if (MarkDot) {
    tft.fillCircle(x1,y1, DotRadius, color);
    tft.fillCircle(x2,y2, DotRadius, color);
  }  
}

void PlotData(uint16_t color){
  //if (actData >= maxData) Refresh();
  float StepPerData = (tft.width() -Xoff) / maxData;
  int x = (actData+1) * StepPerData + HalfMarkerLength;
  int y = (data[actData] * StepPerDeviationMark)/10 + AverageLine;
  if (actData > 0) {
    int x0 = (actData) * StepPerData + HalfMarkerLength;
    int y0 = (data[actData-1]*StepPerDeviationMark)/10 + AverageLine;
    PlotFromTo(x0,y0,x,y,color,true);
    Serial.println(String(actData)+"->\t"+String(x)+"\t:\t"+String(data[actData]));
   
  } else tft.fillCircle(x+Xoff,tft.height()-y-Yoff, DotRadius, color);

}

It has been written more or less "quick and dirty" so there will surely be some room for improvement.

However it shows the possibilities of

  • Drawing the coordinate system including marks/tics and some numbers and text
  • Erasing the plots by a black rectangle (see RefreshField() )
    or erasing the plots by redrawing them with black color (see RefreshPlots() )
  • Using an array to store the data and re-plot them after maximum data have been sampled

If more data have to be plotted RefreshField() becomes quicker than RefreshPlots().

Who wants may also check it out on Wokwi:

https://wokwi.com/projects/335069279045026388

P.S.: And this one "moves" the points from right to left stepwise ... :wink:


#include "SPI.h"
#include "Adafruit_GFX.h"
#include "Adafruit_ILI9341.h"

// For the Adafruit shield, these are the default.
#define TFT_DC 9
#define TFT_CS 10

const int Xoff = 32;
const int Yoff = 32;

const byte HalfMarkerLength = 4;
const byte TopOffset = 18;
const byte DotRadius  = 2;

const unsigned long SampleInterval = 10000; // [msec]
const int maxData = 70;                     // No of Samples

int data[maxData];
int actData = 0;
int AverageLine = 0;
float StepPerDeviationMark;  // 10 units from mark to mark


// Use hardware SPI (on Uno, #13, #12, #11) and the above for CS/DC
Adafruit_ILI9341 tft = Adafruit_ILI9341(TFT_CS, TFT_DC);
// If using the breakout, change pins as desired
//Adafruit_ILI9341 tft = Adafruit_ILI9341(TFT_CS, TFT_DC, TFT_MOSI, TFT_CLK, TFT_RST, TFT_MISO);

void setup() {
  Serial.begin(9600);
  Serial.println("Test"); 
 
  tft.begin();
  tft.setRotation(1);
  CoordinateSystem();
  CreateTestData();
}

void loop(void) {
  static unsigned long lastMillis = 0;
  if (millis()-lastMillis > SampleInterval){
    lastMillis = millis();
    data[actData] = random(-50,51);
    PlotData(data,ILI9341_RED);
    actData++;
    //if (actData >= maxData) RefreshField();
    //if (actData >= maxData) RefreshPlots();
    if (actData >= maxData) RefreshPlotsStepWise();
  }
}

void CreateTestData(){
  for (int i = 0;i < maxData-1;i++){
    data[actData] = random(-50,51);
    PlotData(data,ILI9341_RED);
    actData++;
  }
}

void RefreshField(){
  Serial.println("Refresh");
  int x = Xoff+ HalfMarkerLength;
  int y = TopOffset;
  tft.fillRect(x,y,tft.width()-x, tft.height()-y-Yoff-HalfMarkerLength, ILI9341_BLACK);
  delay(300); // Give some time for erasing the data
  // Reprint the average line
  PlotFromTo(-HalfMarkerLength,AverageLine,tft.width(),AverageLine,ILI9341_YELLOW, false );
  // Move all data one field to the "left"
  for (int i = 1; i < maxData;i++) data[i-1] = data[i];
  // Replot the "old" data
  for (int i = 0; i < maxData-1;i++){ 
    actData = i;
    PlotData(data,ILI9341_RED);
  }
  actData = maxData-1;
}


void RefreshPlots(){
  Serial.println("Refresh");
  for (int i = 0; i < maxData;i++){ 
    actData = i;
    PlotData(data,ILI9341_BLACK);
  }
  for (int i = 1; i < maxData;i++) data[i-1] = data[i];
  PlotFromTo(-HalfMarkerLength,AverageLine,tft.width(),AverageLine,ILI9341_YELLOW, false );
  for (int i = 0; i < maxData-1;i++){ 
    actData = i;
    PlotData(data,ILI9341_RED);
  }
  actData = maxData-1;
}

void RefreshPlotsStepWise(){
  int olddata[maxData];
  for (int i = 0; i < maxData;i++) olddata[i] = data[i];
  for (int i = 1; i < maxData;i++) data[i-1] = data[i];
  Serial.println("Refresh");
  for (int i = 0; i < maxData;i++){ 
    actData = i;
    PlotData(olddata,ILI9341_BLACK);
    if (actData < maxData-1) PlotData(data,ILI9341_RED);
  }
  PlotFromTo(-HalfMarkerLength,AverageLine,tft.width(),AverageLine,ILI9341_YELLOW, false );
  actData = maxData-1;
}

void CoordinateSystem() {
 
  int           x1, y1, x2, y2,
                w = tft.width(),
                h = tft.height();
  tft.fillScreen(ILI9341_BLACK);
  tft.setCursor(0, 0);
  tft.setTextColor(ILI9341_WHITE);  
  tft.setTextSize(2);
  tft.println("Wind Graph");
  // x axis 
  tft.drawLine(0,h-Yoff,w,h-Yoff,ILI9341_WHITE);  
  int StepPerMinute = (w - Xoff) / 6;
  for (int i = 1; i < 6; i++) {
    PlotFromTo(i*StepPerMinute,-HalfMarkerLength,i*StepPerMinute, HalfMarkerLength,ILI9341_WHITE, false );
    tft.setCursor(i*StepPerMinute+Xoff-4, h-Yoff+HalfMarkerLength*2);
    tft.print(i);
  }  
  // y axis
  tft.drawLine(Xoff,TopOffset,Xoff,w,ILI9341_WHITE);
  StepPerDeviationMark = (h - Yoff) / 12;
  tft.setTextSize(1);
  int value = 50;
  for (int i = 1; i < 12; i++) {
    if (value == 0){
      AverageLine = i*StepPerDeviationMark;
      PlotFromTo(-HalfMarkerLength,AverageLine,w,AverageLine,ILI9341_YELLOW, false );
    } 
    PlotFromTo(-HalfMarkerLength,i*StepPerDeviationMark,HalfMarkerLength,i*StepPerDeviationMark,ILI9341_WHITE, false );
    tft.setCursor(4,i*StepPerDeviationMark);
    tft.print(value);
    value -= 10;
  }  

}

void PlotFromTo(int x1,int y1, int x2, int y2, uint16_t color, boolean MarkDot){
  x1 = x1+Xoff;
  y1 = tft.height()-(y1+Yoff);
  x2 = x2+Xoff;
  y2 = tft.height()-(y2+Yoff);
  tft.drawLine(x1,y1,x2,y2,color);
  if (MarkDot) {
    tft.fillCircle(x1,y1, DotRadius, color);
    tft.fillCircle(x2,y2, DotRadius, color);
  }  
}

void PlotData(int myData[], uint16_t color){
  //if (actData >= maxData) Refresh();
  float StepPerData = (tft.width() -Xoff) / maxData;
  int x = (actData+1) * StepPerData + HalfMarkerLength;
  int y = (myData[actData] * StepPerDeviationMark)/10 + AverageLine;
  if (actData > 0) {
    int x0 = (actData) * StepPerData + HalfMarkerLength;
    int y0 = (myData[actData-1]*StepPerDeviationMark)/10 + AverageLine;
    PlotFromTo(x0,y0,x,y,color,true);
    Serial.println(String(actData)+"->\t"+String(x)+"\t:\t"+String(myData[actData]));
   
  } else tft.fillCircle(x+Xoff,tft.height()-y-Yoff, DotRadius, color);

}

See https://wokwi.com/projects/335081139056149076

sounds great! I'll chase one of those displays on Amazon.
Is this I2C connection?
I already have a couple of sensors, so I dont want to run out of pins

thanks

fede

The one simulated at Wokwi uses SPI:

image

And this is how the software looks like on a real hardware :slight_smile:

P.S.: If you do not get the same/similar display as the sketch refers to, you will quite likely have to make some changes to the code, concerning pins and initialization of the display. This can usually be derived from the delivered examples (and a little bit of trial and error :wink: ).

Wow! This looks great! This is what I am looking for. I already purchased the display from Amazon and would try it asap.
Is there any possibility of getting the wind axes in the center of the graph?
I also could flip mechanically the screen, but sometimes I prefer not to be so technician on my upgrades , lol

Thanks

Fede

Is there any possibility of getting the wind axes in the center of the graph?

Do you mean this?

image

I mean like my graph on top where the wind average is the central axis and the readings are to the left or to the right,
that way it's more intuitive for the sailor recognizing if the wind shifted left or right,

thanks

Fede

Would it be ok if the x-axis is at the right hand side if you mount the display in an upright position like this?

image

Here the sketch:


#include "SPI.h"
#include "Adafruit_GFX.h"
#include "Adafruit_ILI9341.h"

// For the Adafruit shield, these are the default.
#define TFT_DC 9
#define TFT_CS 10

const int Xoff = 32;
const int Yoff = 32;

const byte HalfMarkerLength = 4;
const byte TopOffset = 18;
const byte DotRadius  = 2;

const unsigned long SampleInterval = 10000; // [msec]
const int maxData = 70;                     // No of Samples

int data[maxData];
int actData = 0;
int AverageLine = 0;
float StepPerDeviationMark;  // 10 units from mark to mark


// Use hardware SPI (on Uno, #13, #12, #11) and the above for CS/DC
Adafruit_ILI9341 tft = Adafruit_ILI9341(TFT_CS, TFT_DC);
// If using the breakout, change pins as desired
//Adafruit_ILI9341 tft = Adafruit_ILI9341(TFT_CS, TFT_DC, TFT_MOSI, TFT_CLK, TFT_RST, TFT_MISO);

void setup() {
  Serial.begin(9600);
  Serial.println("Test"); 
 
  tft.begin();
  tft.setRotation(1);
  CoordinateSystem();
  CreateTestData();
}

void loop(void) {
  static unsigned long lastMillis = 0;
  if (millis()-lastMillis > SampleInterval){
    lastMillis = millis();
    data[actData] = random(-50,51);
    PlotData(data,ILI9341_RED);
    actData++;
    //if (actData >= maxData) RefreshField();
    //if (actData >= maxData) RefreshPlots();
    if (actData >= maxData) RefreshPlotsStepWise();
  }
}

void CreateTestData(){
  for (int i = 0;i < maxData-1;i++){
    data[actData] = random(-50,51);
    PlotData(data,ILI9341_RED);
    actData++;
  }
}

void RefreshField(){
  Serial.println("Refresh");
  int x = Xoff+ HalfMarkerLength;
  int y = TopOffset;
  tft.fillRect(x,y,tft.width()-x, tft.height()-y-Yoff-HalfMarkerLength, ILI9341_BLACK);
  delay(300); // Give some time for erasing the data
  // Reprint the average line
  PlotFromTo(-HalfMarkerLength,AverageLine,tft.width(),AverageLine,ILI9341_YELLOW, false );
  // Move all data one field to the "left"
  for (int i = 1; i < maxData;i++) data[i-1] = data[i];
  // Replot the "old" data
  for (int i = 0; i < maxData-1;i++){ 
    actData = i;
    PlotData(data,ILI9341_RED);
  }
  actData = maxData-1;
}


void RefreshPlots(){
  Serial.println("Refresh");
  for (int i = 0; i < maxData;i++){ 
    actData = i;
    PlotData(data,ILI9341_BLACK);
  }
  for (int i = 1; i < maxData;i++) data[i-1] = data[i];
  PlotFromTo(-HalfMarkerLength,AverageLine,tft.width(),AverageLine,ILI9341_YELLOW, false );
  for (int i = 0; i < maxData-1;i++){ 
    actData = i;
    PlotData(data,ILI9341_RED);
  }
  actData = maxData-1;
}

void RefreshPlotsStepWise(){
  int olddata[maxData];
  for (int i = 0; i < maxData;i++) olddata[i] = data[i];
  for (int i = 1; i < maxData;i++) data[i-1] = data[i];
  Serial.println("Refresh");
  for (int i = 0; i < maxData;i++){ 
    actData = i;
    PlotData(olddata,ILI9341_BLACK);
    if (actData < maxData-1) PlotData(data,ILI9341_RED);
  }
  PlotFromTo(-HalfMarkerLength,AverageLine,tft.width(),AverageLine,ILI9341_YELLOW, false );
  actData = maxData-1;
}

void CoordinateSystem() {
 
  int           x1, y1, x2, y2,
                w = tft.width(),
                h = tft.height();
  tft.fillScreen(ILI9341_BLACK);
  tft.setCursor(0, 0);
  tft.setTextColor(ILI9341_WHITE);  
  tft.setTextSize(2);
  tft.println("Wind Graph");
  // x axis 
  tft.drawLine(0,h-Yoff,w,h-Yoff,ILI9341_WHITE);  
  int StepPerMinute = (w - Xoff) / 6;
  for (int i = 1; i < 6; i++) {
    PlotFromTo(i*StepPerMinute,-HalfMarkerLength,i*StepPerMinute, HalfMarkerLength,ILI9341_WHITE, false );
    tft.setCursor(i*StepPerMinute+Xoff-4, h-Yoff+HalfMarkerLength*2);
    tft.print(i);
  }  
  // y axis
  tft.drawLine(Xoff,TopOffset,Xoff,w,ILI9341_WHITE);
  StepPerDeviationMark = (h - Yoff) / 12;
  tft.setTextSize(1);
  int value = -50;
  for (int i = 1; i < 12; i++) {
    if (value == 0){
      AverageLine = i*StepPerDeviationMark;
      PlotFromTo(-HalfMarkerLength,AverageLine,w,AverageLine,ILI9341_YELLOW, false );
    } 
    PlotFromTo(-HalfMarkerLength,i*StepPerDeviationMark,HalfMarkerLength,i*StepPerDeviationMark,ILI9341_WHITE, false );
    tft.setCursor(4,i*StepPerDeviationMark);
    if (value == 0) tft.print(" ");
    if (value >  0) tft.print("+");
    tft.print(value);
    value += 10;
  }  

}

void PlotFromTo(int x1,int y1, int x2, int y2, uint16_t color, boolean MarkDot){
  x1 = x1+Xoff;
  y1 = tft.height()-(y1+Yoff);
  x2 = x2+Xoff;
  y2 = tft.height()-(y2+Yoff);
  tft.drawLine(x1,y1,x2,y2,color);
  if (MarkDot) {
    tft.fillCircle(x1,y1, DotRadius, color);
    tft.fillCircle(x2,y2, DotRadius, color);
  }  
}

void PlotData(int myData[], uint16_t color){
  //if (actData >= maxData) Refresh();
  float StepPerData = (tft.width() -Xoff) / maxData;
  int x = (actData+1) * StepPerData + HalfMarkerLength;
  int y = -(myData[actData] * StepPerDeviationMark)/10 + AverageLine;
  Serial.println(String(actData)+"->\t"+String(myData[actData]));
  if (actData > 0) {
    int x0 = (actData) * StepPerData + HalfMarkerLength;
    int y0 = -(myData[actData-1]*StepPerDeviationMark)/10 + AverageLine;
    PlotFromTo(x0,y0,x,y,color,true);
   
  } else tft.fillCircle(x+Xoff,tft.height()-y-Yoff, DotRadius, color);

}

To test on Wokwi at https://wokwi.com/projects/335095650511225426

Yeah at first I thought the same, buy maybe there was a way through coding

thanks a lot

fede

This topic was automatically closed 180 days after the last reply. New replies are no longer allowed.