gchart_demo.tc¶
gchart_demo.tc — Google-Charts-style dual-axis dashboard on LVGL (P4 playground)
// gchart_demo.tc — Google-Charts-style dual-axis dashboard on LVGL (P4 playground)
//
// v3 (firmware ABI >= 15): two time series per chart, each on its own y-axis —
// - LINE : temp (blue, left scale) + humidity (red, RIGHT scale), each a lvglLinePoly
// - COLUMN : energy (blue, left scale) + cost (green, RIGHT scale), grouped bars
// - GAUGE : zoned semicircle (3 lvglLinePoly arcs) + needle
// - shared gridlines; left axis gray, right axis coloured to its series; legend per card
// - left y-axis title rotated 270° via lvglRotate
// Portrait 800x1280: white cards on a black screen. Needs USE_TINYC_LVGL ABI>=15.
#define ST_RADIUS 120
#define ST_BORDER 56
#define ST_BCOLOR 57
#define ST_BOPA 58
#define AL_CENTER 9
#define PI 3.14159265
#define C_BG 0x000000
#define C_CARD 0xFFFFFF
#define C_CARDB 0xDADCE0
#define C_GRID 0xBDC1C6 // medium grey gridlines, visible on the light grey plot
#define C_BASE 0x9AA0A6 // baseline / axis line (darker still)
#define C_PLOT 0xF1F3F4 // light grey plot-area background
#define C_AXIS 0x5F6368
#define C_TITLE 0x202124
#define C_BLUE 0x4285F4
#define C_GREEN 0x34A853
#define C_AMBER 0xFBBC04
#define C_RED 0xEA4335
int gdat[32]; // data for whichever series is being drawn
int px[40]; int py[40]; // polyline scratch (lvglLinePoly copies it)
int g_needle = 0; int g_value = 0; // dynamic gauge parts (updated live)
int g_cx; int g_cy; int g_r; int g_pmax; // stored gauge geometry for the updater
int ymap(int v, int vmin, int vmax, int y0, int h) {
return y0 + h - ((v - vmin) * h) / (vmax - vmin);
}
void card(int x, int y, int w, int h) {
int c = lvglObj(0);
lvglSetPos(c, x, y);
lvglSetSize(c, w, h);
lvglSetBgColor(c, C_CARD);
lvglSetStyleInt(c, ST_RADIUS, 12);
lvglSetStyleInt(c, ST_BORDER, 1);
lvglSetStyleInt(c, ST_BOPA, 255);
lvglSetStyleInt(c, ST_BCOLOR, C_CARDB);
}
// light grey plot-area panel (draw BEFORE the grid + data so they sit on top)
void plotBg(int x, int y, int w, int h) {
int p = lvglObj(0);
lvglSetStyleInt(p, ST_BORDER, 0);
lvglSetSize(p, w, h);
lvglSetStyleInt(p, ST_RADIUS, 0);
lvglSetBgColor(p, C_PLOT);
lvglSetPos(p, x, y);
}
// shared gridlines: ndiv horizontal rows (baseline darker) + nxdiv vertical columns
void drawGrid(int x0, int y0, int w, int h, int ndiv, int nxdiv) {
int i = 0;
while (i <= ndiv) {
int gy = y0 + (h * i) / ndiv;
int gl = lvglLine(0);
if (i == ndiv) { lvglLineStyle(gl, C_BASE, 1); } else { lvglLineStyle(gl, C_GRID, 1); }
lvglLinePoints(gl, x0, gy, x0 + w, gy);
i = i + 1;
}
if (nxdiv >= 1) {
i = 0;
while (i <= nxdiv) {
int gx = x0 + (w * i) / nxdiv;
int vl = lvglLine(0);
lvglLineStyle(vl, C_GRID, 1);
lvglLinePoints(vl, gx, y0, gx, y0 + h);
i = i + 1;
}
}
}
// y-axis value labels at the gridline rows, anchored at x (left edge), coloured
void yAxis(int x, int y0, int h, int vmin, int vmax, int ndiv, int color) {
int i = 0;
while (i <= ndiv) {
int gy = y0 + (h * i) / ndiv;
int val = vmax - ((vmax - vmin) * i) / ndiv;
char lb[12];
sprintf(lb, "%d", val);
int t = lvglLabel(0);
lvglSetText(t, lb);
lvglSetTextColor(t, color);
lvglSetFont(t, 14);
lvglSetPos(t, x, gy - 11);
i = i + 1;
}
}
// one line series (reads gdat) as a single polyline, on its own scale; markers optional
void drawLine(int n, int x0, int y0, int w, int h, int vmin, int vmax, int color, int markers) {
int step = w / (n - 1);
int i = 0;
while (i < n) {
px[i] = x0 + step * i;
py[i] = ymap(gdat[i], vmin, vmax, y0, h);
i = i + 1;
}
int ln = lvglLine(0);
lvglLineStyle(ln, color, 4);
lvglLinePoly(ln, px, py, n);
if (markers) {
i = 0;
while (i < n) {
int m = lvglObj(0);
lvglSetStyleInt(m, ST_BORDER, 0);
lvglSetSize(m, 9, 9);
lvglSetStyleInt(m, ST_RADIUS, 5);
lvglSetBgColor(m, color);
lvglSetPos(m, px[i] - 4, py[i] - 4);
i = i + 1;
}
}
}
// one grouped-column series (reads gdat); sidx in 0..nser-1 places the sub-bar
void drawCols(int n, int x0, int y0, int w, int h, int vmin, int vmax, int color, int sidx, int nser) {
int slot = w / n;
int gw = (slot * 8) / 10; // group = 80% of the slot
int sub = gw / nser;
int bw = sub - 4;
if (bw < 2) { bw = 2; }
int i = 0;
while (i < n) {
int top = ymap(gdat[i], vmin, vmax, y0, h);
int bh = (y0 + h) - top;
if (bh < 2) { bh = 2; }
int bx = x0 + slot * i + (slot - gw) / 2 + sidx * sub;
int b = lvglObj(0);
lvglSetStyleInt(b, ST_BORDER, 0);
lvglSetSize(b, bw, bh);
lvglSetStyleInt(b, ST_RADIUS, 3);
lvglSetBgColor(b, color);
lvglSetPos(b, bx, top);
i = i + 1;
}
}
void lineXLabels(int x0, int w, int n, int every, int hour0, int hourStep, int y) {
int step = w / (n - 1);
int i = 0;
while (i < n) {
if ((i % every) == 0) {
char lb[8];
sprintf(lb, "%d", hour0 + i * hourStep);
int t = lvglLabel(0);
lvglSetText(t, lb);
lvglSetTextColor(t, C_AXIS);
lvglSetFont(t, 14);
lvglSetPos(t, x0 + step * i - 8, y);
}
i = i + 1;
}
}
void colXLabels(int x0, int w, int n, int hour0, int hourStep, int y) {
int slot = w / n;
int i = 0;
while (i < n) {
char lb[8];
sprintf(lb, "%d", hour0 + i * hourStep);
int t = lvglLabel(0);
lvglSetText(t, lb);
lvglSetTextColor(t, C_AXIS);
lvglSetFont(t, 14);
lvglSetPos(t, x0 + slot * i + slot / 2 - 8, y);
i = i + 1;
}
}
// small legend colour swatch (the text label is set inline by the caller)
void legSquare(int x, int y, int color) {
int s = lvglObj(0);
lvglSetStyleInt(s, ST_BORDER, 0);
lvglSetSize(s, 12, 12);
lvglSetStyleInt(s, ST_RADIUS, 2);
lvglSetBgColor(s, color);
lvglSetPos(s, x, y);
}
void zoneArc(int cx, int cy, int r, int p0, int p1, int color, int thick) {
int N = 14;
int i = 0;
float pi = PI;
while (i <= N) {
float f = ((float)p0 + (float)(p1 - p0) * ((float)i / (float)N)) / 100.0;
float a = pi - pi * f;
px[i] = cx + (int)((float)r * cos(a));
py[i] = cy - (int)((float)r * sin(a));
i = i + 1;
}
int arc = lvglLine(0);
lvglLineStyle(arc, color, thick);
lvglLinePoly(arc, px, py, N + 1);
}
// set the needle endpoints for a value (uses the stored gauge geometry)
void needlePoint(int value) {
float pi = PI;
float fv = (float)value / (float)g_pmax;
if (fv < 0.0) { fv = 0.0; }
if (fv > 1.0) { fv = 1.0; }
float av = pi - pi * fv;
int nx = g_cx + (int)((float)(g_r - 22) * cos(av));
int ny = g_cy - (int)((float)(g_r - 22) * sin(av));
lvglLinePoints(g_needle, g_cx, g_cy, nx, ny);
}
void gauge(int cx, int cy, int r, int value, int vmax) {
int thick = 18;
zoneArc(cx, cy, r, 0, 60, C_GREEN, thick);
zoneArc(cx, cy, r, 60, 85, C_AMBER, thick);
zoneArc(cx, cy, r, 85, 100, C_RED, thick);
g_cx = cx; g_cy = cy; g_r = r; g_pmax = vmax;
g_needle = lvglLine(0); // kept global so the loop can move it
lvglLineStyle(g_needle, C_TITLE, 5);
needlePoint(value);
int hub = lvglObj(0);
lvglSetStyleInt(hub, ST_BORDER, 0);
lvglSetSize(hub, 20, 20);
lvglSetStyleInt(hub, ST_RADIUS, 10);
lvglSetBgColor(hub, C_TITLE);
lvglSetPos(hub, cx - 10, cy - 10);
}
// realtime: move the needle + refresh the centre readout to a new value (tenths)
void updateGauge(int value) {
needlePoint(value);
char b[12];
sprintf(b, "%d.%d", value / 10, value - (value / 10) * 10);
lvglSetText(g_value, b);
}
// numbered outer scale around the gauge arc: nticks intervals -> nticks+1 labels (0..vmax)
void gaugeScale(int cx, int cy, int r, int vmax, int nticks) {
float pi = PI;
int i = 0;
while (i <= nticks) {
float f = (float)i / (float)nticks;
float a = pi - pi * f;
int lx = cx + (int)((float)(r + 24) * cos(a));
int ly = cy - (int)((float)(r + 24) * sin(a));
int val = (vmax * i) / nticks;
char lb[8];
sprintf(lb, "%d", val);
int t = lvglLabel(0);
lvglSetText(t, lb);
lvglSetTextColor(t, C_AXIS);
lvglSetFont(t, 14);
lvglSetPos(t, lx - 7, ly - 9);
i = i + 1;
}
}
int main() {
lvglInit();
lvglClean(0);
lvglSetBgColor(0, C_BG);
// ===== LINE — temp (left, blue) + humidity (right, red) =====
card(40, 28, 720, 410);
int t1 = lvglLabel(0);
lvglSetText(t1, "Temperature & humidity - 24 h");
lvglSetTextColor(t1, C_TITLE);
lvglSetFont(t1, 20);
lvglSetPos(t1, 64, 46);
legSquare(540, 50, C_BLUE);
int l1 = lvglLabel(0); lvglSetText(l1, "temp"); lvglSetTextColor(l1, C_AXIS); lvglSetFont(l1, 14); lvglSetPos(l1, 558, 47);
legSquare(636, 50, C_RED);
int l2 = lvglLabel(0); lvglSetText(l2, "hum"); lvglSetTextColor(l2, C_AXIS); lvglSetFont(l2, 14); lvglSetPos(l2, 654, 47);
plotBg(96, 92, 608, 308);
drawGrid(100, 96, 600, 300, 4, 4); // 4 rows + 4 columns (x grid)
yAxis(56, 96, 300, 10, 30, 4, C_AXIS); // left: temp scale (gray)
yAxis(706, 96, 300, 0, 100, 4, C_RED); // right: humidity scale (red)
gdat[0]=14; gdat[1]=13; gdat[2]=13; gdat[3]=15; gdat[4]=18; gdat[5]=21; gdat[6]=24;
gdat[7]=26; gdat[8]=27; gdat[9]=25; gdat[10]=22; gdat[11]=19; gdat[12]=16;
drawLine(13, 100, 96, 600, 300, 10, 30, C_BLUE, 1); // primary series: markers
gdat[0]=70; gdat[1]=72; gdat[2]=71; gdat[3]=65; gdat[4]=58; gdat[5]=50; gdat[6]=44;
gdat[7]=40; gdat[8]=38; gdat[9]=45; gdat[10]=55; gdat[11]=62; gdat[12]=68;
drawLine(13, 100, 96, 600, 300, 0, 100, C_RED, 0); // secondary: no markers
lineXLabels(100, 600, 13, 3, 0, 2, 408);
int yt = lvglLabel(0);
lvglSetText(yt, "deg C");
lvglSetTextColor(yt, C_AXIS);
lvglSetFont(yt, 14);
lvglSetPos(yt, 24, 237);
lvglRotate(yt, 2700);
// ===== COLUMN — energy (left, blue) + cost (right, green), grouped =====
card(40, 476, 720, 416);
int t2 = lvglLabel(0);
lvglSetText(t2, "Energy & cost - by hour");
lvglSetTextColor(t2, C_TITLE);
lvglSetFont(t2, 20);
lvglSetPos(t2, 64, 494);
legSquare(520, 498, C_BLUE);
int l3 = lvglLabel(0); lvglSetText(l3, "kWh"); lvglSetTextColor(l3, C_AXIS); lvglSetFont(l3, 14); lvglSetPos(l3, 538, 495);
legSquare(626, 498, C_GREEN);
int l4 = lvglLabel(0); lvglSetText(l4, "ct"); lvglSetTextColor(l4, C_AXIS); lvglSetFont(l4, 14); lvglSetPos(l4, 644, 495);
plotBg(96, 552, 608, 308);
drawGrid(100, 556, 600, 300, 4, 0); // rows only (bars give the vertical structure)
yAxis(56, 556, 300, 0, 8, 4, C_AXIS); // left: energy (gray)
yAxis(706, 556, 300, 0, 40, 4, C_GREEN); // right: cost (green)
gdat[0]=2; gdat[1]=3; gdat[2]=4; gdat[3]=5; gdat[4]=6; gdat[5]=5; gdat[6]=3; gdat[7]=2;
drawCols(8, 100, 556, 600, 300, 0, 8, C_BLUE, 0, 2);
gdat[0]=12; gdat[1]=18; gdat[2]=22; gdat[3]=28; gdat[4]=33; gdat[5]=26; gdat[6]=17; gdat[7]=13;
drawCols(8, 100, 556, 600, 300, 0, 40, C_GREEN, 1, 2);
colXLabels(100, 600, 8, 0, 3, 868);
// ===== GAUGE — power, zoned semicircle =====
card(40, 912, 720, 300);
int t3 = lvglLabel(0);
lvglSetText(t3, "Power");
lvglSetTextColor(t3, C_TITLE);
lvglSetFont(t3, 20);
lvglSetPos(t3, 64, 930);
int pval = 44; int pmax = 100;
gauge(400, 1100, 130, pval, pmax);
char pb[12];
sprintf(pb, "%d.%d", pval / 10, pval - (pval / 10) * 10);
g_value = lvglLabel(0);
lvglSetText(g_value, pb);
lvglSetTextColor(g_value, C_TITLE);
lvglSetFont(g_value, 28);
lvglAlign(g_value, AL_CENTER, 0, 395);
int gu = lvglLabel(0);
lvglSetText(gu, "kW");
lvglSetTextColor(gu, C_AXIS);
lvglSetFont(gu, 14);
lvglAlign(gu, AL_CENTER, 0, 432);
gaugeScale(400, 1100, 130, 10, 5); // outer scale 0,2,4,6,8,10
// ===== real-time gauge: sweep the needle ~16 fps. Swap the demo sweep for a live
// reading, e.g. int v = (int)(sensorGet("ENERGY#Power") * 10.0); (0.1 kW units) =====
float ph = 0.0;
while (1) {
ph = ph + 0.08;
if (ph > 6.2832) { ph = ph - 6.2832; }
int v = 50 + (int)(45.0 * sin(ph)); // demo sweep 5..95 (= 0.5..9.5 kW)
updateGauge(v);
delay(60);
}
return 0;
}