Skip to content

Trend Widget

The Trend widget is a scrolling min/max-bar or stepped-line chart backed by an app-owned ring buffer of buckets — a patient-monitor trend, a fuel or energy history, any "what happened over the last N minutes" readout. set_buffer() links the ring and set_timebase() sets each bucket's duration; push() feeds one raw sample at a time (aggregated live into the current bucket) while push_bucket() replays already-summarised history in one call — the same API serves a live feed and a stored-data review screen. set_on_cursor() reports the bucket under a tap/drag (age, min, max) so the app can route it to any readout; set_zoom() widens or narrows the visible window without touching the buffer.

Every example below is complete — the file-scope storage, the callbacks and the build calls — beside the exact frame it renders on a 480×320 panel. Each compiles against nothing but the released library: drop it into the Application Skeleton and it runs as-is. The icons come from inc/icons/, the lexend fonts from inc/fonts/ — both ship with the library.

Examples

Patient HR Trend (live feed + cursor)

A trend chart pre-filled with a synthetic "past" via push_bucket(), then advancing LIVE — the demo feed pushes one HR sample at a time and the widget aggregates 2 s buckets, so a new bar lands every 2 s. set_on_cursor() routes a tapped bucket's min/max + age into a TILE readout beside the chart.

Patient HR Trend (live feed + cursor)

/* --- widget IDs: only need to be unique within the screen (10+ leaves
 *     room for the IDs your app already uses, e.g. the root's) --- */
enum {
    WDGT_ID_DEMO = 10, WDGT_ID_READOUT
};

/* --- fonts: each is DEFINED in inc/fonts/<name>.h — #include that
 *     header in exactly one .c of your app; elsewhere just: --- */
extern const ugui_font_t lexend_14pt_2bpp;
extern const ugui_font_t lexend_num_40pt_2bpp;

/* --- file-scope storage --- */
#if defined(UGUI_WIDGET_ENABLE_TREND)
static ugui_screen_t  s_screen;
static ugui_widget_t   s_root;
static ugui_widget_t       s_trend;
static ugui_trend_ext_t    s_trend_ext;
static ugui_trend_bucket_t s_ring[150];     /* 150 × 2 s = 5 min on screen */
#endif
#if defined(UGUI_WIDGET_ENABLE_TREND)
static ugui_widget_t   s_read;
static ugui_tile_ext_t s_read_ext;
#endif
#if defined(UGUI_WIDGET_ENABLE_TREND)
static ugui_text_cfg_t s_read_label;
static ugui_text_cfg_t s_read_sub;
static char            s_read_sub_str[24];
#endif
#if defined(UGUI_WIDGET_ENABLE_TREND)
#define PT_HR_COL    ugui_color_hex(0x00E676u)
#define PT_BG        ugui_color_hex(0x06080Au)
#define PT_CARD      ugui_color_hex(0x11151Bu)
#define PT_GRID      ugui_color_hex(0x223041u)
#define PT_MUTED     ugui_color_hex(0x6B7787u)
#define PT_TREND_X   10
#define PT_TREND_Y   38
#define PT_TREND_W   (UGUI_SCREEN_W - 20 - 150 - 8)
#define PT_TREND_H   (UGUI_SCREEN_H - PT_TREND_Y - 34)
#define PT_TILE_X    (PT_TREND_X + PT_TREND_W + 8)
#define PT_TILE_W    150
#endif

/* --- local helpers & event callbacks --- */
static void on_cursor(ugui_widget_t* w, int16_t age, int16_t bmin,
                      int16_t bmax, void* ctx)
{
    (void)w; (void)ctx;
#ifdef UGUI_WIDGET_ENABLE_TILE
    if (age < 0) {
        ugui_widget_tile_set_value_invalid(&s_read);
#ifdef UGUI_ENABLE_FONT
        (void)snprintf(s_read_sub_str, sizeof(s_read_sub_str), "tap chart");
        ugui_widget_invalidate(&s_read);
#endif
    } else {
        uint32_t back_s = (uint32_t)age * 2u;       /* 2 s buckets */
        ugui_widget_tile_set_value(&s_read, (int32_t)bmax);
#ifdef UGUI_ENABLE_FONT
        if (back_s < 120u) {
            (void)snprintf(s_read_sub_str, sizeof(s_read_sub_str),
                           "min %d   -%us", (int)bmin, (unsigned)back_s);
        } else {
            (void)snprintf(s_read_sub_str, sizeof(s_read_sub_str),
                           "min %d   -%um", (int)bmin,
                           (unsigned)(back_s / 60u));
        }
        ugui_widget_invalidate(&s_read);
#endif
    }
#else
    printf("[trend.patient] cursor age=%d min=%d max=%d\n",
           (int)age, (int)bmin, (int)bmax);
#endif
}

static void prefill_history(void)
{
    uint16_t i;
    for (i = 0u; i < 100u; i++) {
        int16_t v;
        if (i < 45u)       { v = (int16_t)(70 + (int16_t)((i * 5u) % 9u) - 4); }
        else if (i < 60u)  { v = (int16_t)(74 + (int16_t)((i - 45u) * 4u)); }
        else if (i < 75u)  { v = (int16_t)(134 - (int16_t)((i - 60u) * 4u)); }
        else               { v = (int16_t)(72 + (int16_t)((i * 7u) % 9u) - 4); }
        ugui_widget_trend_push_bucket(&s_trend,
                                      (int16_t)(v - 3 - (int16_t)(i % 3u)),
                                      (int16_t)(v + 4 + (int16_t)(i % 4u)));
    }
}

/* --- build it (in your screen's init) --- */
ugui_widget_trend_init(&s_root, &s_trend, &s_trend_ext);
ugui_widget_set_area(&s_trend, PT_TREND_X, PT_TREND_Y,
                     (int16_t)PT_TREND_W, (int16_t)PT_TREND_H);
ugui_widget_set_id(&s_trend, WDGT_ID_DEMO);
ugui_widget_trend_set_buffer(&s_trend, s_ring,
                             (uint16_t)(sizeof(s_ring) / sizeof(s_ring[0])));
ugui_widget_trend_set_range(&s_trend, 40, 160);
ugui_widget_trend_set_timebase(&s_trend, 2u);     /* 2 s buckets: live  */
ugui_widget_trend_set_colors(&s_trend, PT_HR_COL, PT_CARD, PT_GRID,
                             UGUI_COLOR_WHITE);
#ifdef UGUI_ENABLE_FONT
ugui_widget_trend_set_axis_font(&s_trend, &lexend_14pt_2bpp, PT_MUTED, 0u);
#endif
ugui_widget_trend_set_on_cursor(&s_trend, on_cursor, NULL);
prefill_history();
ugui_screen_add_widget(&s_screen, &s_trend);
#if UGUI_ENABLE_KEYPAD
ugui_screen_add_focus(&s_screen, &s_trend);
#endif

#ifdef UGUI_WIDGET_ENABLE_TILE
/* Cursor readout tile — fed exclusively by on_cursor. */
ugui_widget_tile_init(&s_root, &s_read, &s_read_ext);
ugui_widget_set_area(&s_read, (int16_t)PT_TILE_X, PT_TREND_Y,
                     (int16_t)PT_TILE_W, 110);
ugui_widget_set_id(&s_read, WDGT_ID_READOUT);
ugui_widget_tile_set_style(&s_read, PT_CARD, PT_GRID, PT_BG, 1u, 8u);
#ifdef UGUI_ENABLE_FONT
ugui_widget_tile_set_value_format(&s_read, &lexend_num_40pt_2bpp,
                                  PT_HR_COL, 0u);
ugui_text_cfg_init(&s_read_label, "HR @ cursor", &lexend_14pt_2bpp,
                   PT_HR_COL, UGUI_ALIGN_TOP | UGUI_ALIGN_H_CENTER,
                   (ugui_point_t){ 0, 4 }, 1u);
(void)ugui_widget_tile_add_text(&s_read, &s_read_label);
(void)snprintf(s_read_sub_str, sizeof(s_read_sub_str), "tap chart");
ugui_text_cfg_init(&s_read_sub, s_read_sub_str, &lexend_14pt_2bpp,
                   PT_MUTED, UGUI_ALIGN_BOTTOM | UGUI_ALIGN_H_CENTER,
                   (ugui_point_t){ 0, -4 }, 1u);
(void)ugui_widget_tile_add_text(&s_read, &s_read_sub);
#endif
ugui_screen_add_widget(&s_screen, &s_read);
#endif /* UGUI_WIDGET_ENABLE_TILE */

EV Energy History (stepped line + zoom)

The other face of the same widget: a 2 h consumption history in 1-minute buckets replayed on init through push_bucket() — no live feed, a stored-data pattern. Regen braking goes negative (the stepped line dips below zero); set_zoom() starts the view on the last 60 minutes; tapping the chart routes the cursor's bucket into a plain label readout.

EV Energy History (stepped line + zoom)

/* --- widget IDs: only need to be unique within the screen (10+ leaves
 *     room for the IDs your app already uses, e.g. the root's) --- */
enum {
    WDGT_ID_DEMO = 10
};

/* --- fonts: each is DEFINED in inc/fonts/<name>.h — #include that
 *     header in exactly one .c of your app; elsewhere just: --- */
extern const ugui_font_t lexend_14pt_2bpp;

#include "ugui_font.h"

/* --- file-scope storage --- */
#if defined(UGUI_WIDGET_ENABLE_TREND)
static ugui_screen_t  s_screen;
static ugui_widget_t   s_root;
static ugui_widget_t       s_trend;
static ugui_trend_ext_t    s_trend_ext;
static ugui_trend_bucket_t s_ring[120];     /* 120 × 60 s = 2 h            */
static ugui_widget_t   s_readout;
static ugui_box_ext_t  s_readout_ext;
static ugui_text_cfg_t s_readout_text;
static char            s_readout_str[64];
#define EV_LINE_COL  ugui_color_hex(0x00C8FFu)
#define EV_BG        ugui_color_hex(0x000000u)
#define EV_CARD      ugui_color_hex(0x0E1116u)
#define EV_GRID      ugui_color_hex(0x1E2833u)
#define EV_MUTED     ugui_color_hex(0x5C6673u)
#define EV_TREND_X   10
#define EV_TREND_Y   38
#define EV_TREND_W   (UGUI_SCREEN_W - 20)
#define EV_TREND_H   (UGUI_SCREEN_H - EV_TREND_Y - 60)
#endif

/* --- local helpers & event callbacks --- */
static void on_cursor(ugui_widget_t* w, int16_t age, int16_t bmin,
                      int16_t bmax, void* ctx)
{
    (void)w; (void)ctx;
    if (age < 0) {
        (void)snprintf(s_readout_str, sizeof(s_readout_str),
                       "tap the chart for a readout");
    } else {
        /* ugui_format_fixed handles the sign of sub-1.0 negatives right. */
        char lo[17];
        char hi[17];
        (void)ugui_format_fixed(lo, (int32_t)bmin, 1u);
        (void)ugui_format_fixed(hi, (int32_t)bmax, 1u);
        (void)snprintf(s_readout_str, sizeof(s_readout_str),
                       "-%um: %s to %s kW", (unsigned)age, lo, hi);
    }
    ugui_widget_invalidate(&s_readout);
}

static void prefill_history(void)
{
    uint16_t i;
    for (i = 0u; i < 120u; i++) {
        int16_t v;                                 /* 0.1 kW units */
        if (i < 30u)       { v = (int16_t)(45 + (int16_t)((i * 9u) % 31u)); }
        else if (i < 70u)  { v = (int16_t)(95 + (int16_t)((i * 5u) % 17u)); }
        else if (i < 90u)  { v = (int16_t)(-25 + (int16_t)((i * 3u) % 19u)); }
        else               { v = (int16_t)(2 + (int16_t)(i % 3u)); }
        ugui_widget_trend_push_bucket(&s_trend,
                                      (int16_t)(v - 4 - (int16_t)(i % 5u)),
                                      (int16_t)(v + 5 + (int16_t)(i % 4u)));
    }
}

/* --- build it (in your screen's init) --- */
ugui_widget_trend_init(&s_root, &s_trend, &s_trend_ext);
ugui_widget_set_area(&s_trend, EV_TREND_X, EV_TREND_Y,
                     (int16_t)EV_TREND_W, (int16_t)EV_TREND_H);
ugui_widget_set_id(&s_trend, WDGT_ID_DEMO);
ugui_widget_trend_set_buffer(&s_trend, s_ring,
                             (uint16_t)(sizeof(s_ring) / sizeof(s_ring[0])));
ugui_widget_trend_set_range(&s_trend, -40, 120);  /* 0.1 kW units       */
ugui_widget_trend_set_timebase(&s_trend, 60u);    /* 1 min buckets      */
ugui_widget_trend_set_style(&s_trend, UGUI_TREND_STYLE_LINE);
ugui_widget_trend_set_grid(&s_trend, 4u, 6u);
ugui_widget_trend_set_colors(&s_trend, EV_LINE_COL, EV_CARD, EV_GRID,
                             UGUI_COLOR_WHITE);
#ifdef UGUI_ENABLE_FONT
ugui_widget_trend_set_axis_font(&s_trend, &lexend_14pt_2bpp, EV_MUTED, 1u);
#endif
ugui_widget_trend_set_on_cursor(&s_trend, on_cursor, NULL);
prefill_history();
ugui_widget_trend_set_zoom(&s_trend, 60u);        /* last hour first    */
ugui_screen_add_widget(&s_screen, &s_trend);
#if UGUI_ENABLE_KEYPAD
ugui_screen_add_focus(&s_screen, &s_trend);
#endif

/* Cursor readout — a plain label this time (10.1 uses a tile). */
(void)snprintf(s_readout_str, sizeof(s_readout_str),
               "tap the chart for a readout");
ugui_widget_box_init(&s_root, &s_readout, &s_readout_ext);
ugui_widget_set_area(&s_readout, EV_TREND_X,
                     (int16_t)(EV_TREND_Y + EV_TREND_H + 4),
                     (int16_t)EV_TREND_W, 24);
ugui_widget_box_set_style(&s_readout, true, 0u, 0u, EV_BG, EV_BG);
ugui_widget_set_enabled(&s_readout, false);
ugui_text_cfg_init(&s_readout_text, s_readout_str, &lexend_14pt_2bpp,
                   EV_LINE_COL, UGUI_ALIGN_CENTER, (ugui_point_t){ 0, 0 }, 1u);
ugui_widget_box_set_text(&s_readout, &s_readout_text);
ugui_screen_add_widget(&s_screen, &s_readout);


From this widget to a working app

Everything above is the widget-specific code. The scaffolding it drops into — storage, HAL, main loop, display-mode bring-up — is the same for every app: copy it from the Application Skeleton.