Implementing Dual-Line Real-Time Charts with LVGL for Data Visualization

This guide demonstrates how to create a dual-line real-time refreshing chart using LVGL version 8.3, suitable for data collection applications. While we won't cover basic LVGL concepts here, you'll need to include the "lvgl" header file in your project.

Creating the Chart Container First, we establish the main chart container with appropriate dimensions for a 480x320 display:

chart_container = lv_obj_create(lv_scr_act());
lv_obj_set_size(chart_container, 480, 320);
lv_obj_set_scrollbar_mode(chart_container, LV_SCROLLBAR_MODE_OFF);

lv_obj_set_style_bg_opa(chart_container, 255, LV_PART_MAIN|LV_STATE_DEFAULT);
lv_obj_set_style_bg_color(chart_container, lv_color_hex(0xE8E8E8), LV_PART_MAIN|LV_STATE_DEFAULT);
lv_obj_set_style_bg_grad_dir(chart_container, LV_GRAD_DIR_NONE, LV_PART_MAIN|LV_STATE_DEFAULT);

main_chart = lv_chart_create(chart_container);
lv_chart_set_zoom_x(main_chart, 256);
lv_chart_set_zoom_y(main_chart, 256);
lv_obj_set_pos(main_chart, 20, -15);
lv_obj_set_size(main_chart, 400, 290);

Configuring Chart Structure Next, we configure the internal chart structure, including axis settings, point count, and series creation:

lv_chart_set_type(main_chart, LV_CHART_TYPE_LINE);
lv_obj_set_style_size(main_chart, 0, LV_PART_INDICATOR);

lv_chart_set_div_line_count(main_chart, 5, 11);
lv_chart_set_point_count(main_chart, 50);
lv_chart_set_axis_tick(main_chart, LV_CHART_AXIS_PRIMARY_X, 5, 3, 11, 5, true, 40);

lv_chart_set_range(main_chart, LV_CHART_AXIS_PRIMARY_Y, 0, 100);
lv_chart_set_axis_tick(main_chart, LV_CHART_AXIS_PRIMARY_Y, 5, 3, 5, 5, true, 40);
lv_chart_set_range(main_chart, LV_CHART_AXIS_SECONDARY_Y, 0, 100);
lv_chart_set_axis_tick(main_chart, LV_CHART_AXIS_SECONDARY_Y, 5, 3, 5, 5, true, 40);

series_primary = lv_chart_add_series(main_chart, lv_color_hex(0x000000), LV_CHART_AXIS_PRIMARY_Y);
series_secondary = lv_chart_add_series(main_chart, lv_color_hex(0xff0000), LV_CHART_AXIS_SECONDARY_Y);

Styling the Chart We apply styling to enhance the visual appearance of our chart:

lv_obj_set_style_bg_opa(main_chart, 255, LV_PART_MAIN|LV_STATE_DEFAULT);
lv_obj_set_style_bg_color(main_chart, lv_color_hex(0xffffff), LV_PART_MAIN|LV_STATE_DEFAULT);
lv_obj_set_style_bg_grad_dir(main_chart, LV_GRAD_DIR_NONE, LV_PART_MAIN|LV_STATE_DEFAULT);
lv_obj_set_style_border_width(main_chart, 0, LV_PART_MAIN|LV_STATE_DEFAULT);
lv_obj_set_style_border_opa(main_chart, 255, LV_PART_MAIN|LV_STATE_DEFAULT);
lv_obj_set_style_border_color(main_chart, lv_color_hex(0xe8e8e8), LV_PART_MAIN|LV_STATE_DEFAULT);
lv_obj_set_style_border_side(main_chart, LV_BORDER_SIDE_FULL, LV_PART_MAIN|LV_STATE_DEFAULT);
lv_obj_set_style_radius(main_chart, 0, LV_PART_MAIN|LV_STATE_DEFAULT);
lv_obj_set_style_line_width(main_chart, 2, LV_PART_MAIN|LV_STATE_DEFAULT);
lv_obj_set_style_line_color(main_chart, lv_color_hex(0xe8e8e8), LV_PART_MAIN|LV_STATE_DEFAULT);
lv_obj_set_style_line_opa(main_chart, 255, LV_PART_MAIN|LV_STATE_DEFAULT);
lv_obj_set_style_shadow_width(main_chart, 0, LV_PART_MAIN|LV_STATE_DEFAULT);

lv_obj_set_style_text_color(main_chart, lv_color_hex(0x000000), LV_PART_TICKS|LV_STATE_DEFAULT);
lv_obj_set_style_text_font(main_chart, &lv_font_montserrat_16, LV_PART_TICKS|LV_STATE_DEFAULT);
lv_obj_set_style_text_opa(main_chart, 255, LV_PART_TICKS|LV_STATE_DEFAULT);
lv_obj_set_style_line_width(main_chart, 2, LV_PART_TICKS|LV_STATE_DEFAULT);
lv_obj_set_style_line_color(main_chart, lv_color_hex(0x000000), LV_PART_TICKS|LV_STATE_DEFAULT);
lv_obj_set_style_line_opa(main_chart, 255, LV_PART_TICKS|LV_STATE_DEFAULT);

Implementing Custom Styling with Callback We create a callback function to customize the appearance of axis labels and lines:

static void customize_chart_appearance(lv_event_t* e)
{
    lv_obj_draw_part_dsc_t* draw_desc = lv_event_get_draw_part_dsc(e);
    if (!lv_obj_draw_part_check_type(draw_desc, &lv_chart_class, LV_CHART_DRAW_PART_TICK_LABEL)) return;

    if (draw_desc->id == LV_CHART_AXIS_PRIMARY_Y && draw_desc->text)
    {
        draw_desc->label_dsc->color = lv_color_hex(0x000000);
        draw_desc->line_dsc->color = lv_color_hex(0x000000);
    }
    else if (draw_desc->id == LV_CHART_AXIS_SECONDARY_Y && draw_desc->text)
    {
        draw_desc->label_dsc->color = lv_color_hex(0xff0000);
        draw_desc->line_dsc->color = lv_color_hex(0xff0000);
    }
    else if (draw_desc->id == LV_CHART_AXIS_PRIMARY_X && draw_desc->text)
    {
        draw_desc->label_dsc->color = lv_color_hex(0x00ff00);
        draw_desc->line_dsc->color = lv_color_hex(0x00ff00);
    }
}

Setting Up Real-Time Data Updates For real-time data visualization, we implement a circular udpate mode with a timer. Note that while high-performance systems might support 10ms refresh intervals, embedded systems typically benefit from longer intervals to avoid visual artifacts:

lv_obj_add_event_cb(main_chart, customize_chart_appearance, LV_EVENT_DRAW_PART_BEGIN, NULL);
lv_chart_set_update_mode(main_chart, LV_CHART_UPDATE_MODE_CIRCULAR);
data_timer = lv_timer_create(update_chart_data, 1000, main_chart);

The data update function handles both data clearing and new data insertion:

static void update_chart_data(lv_timer_t * t)
{
    lv_obj_t * chart = t->user_data;
    static data_counter = 0;

    if(data_counter++ == 50)
    {
        lv_chart_set_all_value(chart, series_primary, LV_CHART_POINT_NONE);
        lv_chart_set_all_value(chart, series_secondary, LV_CHART_POINT_NONE);
        data_counter = 0;
    }

    // Using random values for demonstration; replace with actual sensor data
    lv_chart_set_next_value(chart, series_primary, lv_rand(0, 100));
    lv_chart_set_next_value(chart, series_secondary, lv_rand(0, 100));
}

Complete Implementation Here's the complete implementation for reference:

#include "lvgl.h"

lv_timer_t * data_timer;
lv_obj_t *chart_container;
lv_chart_series_t *main_chart;
lv_chart_series_t *series_primary;
lv_chart_series_t *series_secondary;
int data_counter;

static void customize_chart_appearance(lv_event_t* e)
{
    lv_obj_draw_part_dsc_t* draw_desc = lv_event_get_draw_part_dsc(e);
    if (!lv_obj_draw_part_check_type(draw_desc, &lv_chart_class, LV_CHART_DRAW_PART_TICK_LABEL)) return;

    if (draw_desc->id == LV_CHART_AXIS_PRIMARY_Y && draw_desc->text)
    {
        draw_desc->label_dsc->color = lv_color_hex(0x000000);
        draw_desc->line_dsc->color = lv_color_hex(0x000000);
    }
    else if (draw_desc->id == LV_CHART_AXIS_SECONDARY_Y && draw_desc->text)
    {
        draw_desc->label_dsc->color = lv_color_hex(0xff0000);
        draw_desc->line_dsc->color = lv_color_hex(0xff0000);
    }
    else if (draw_desc->id == LV_CHART_AXIS_PRIMARY_X && draw_desc->text)
    {
        draw_desc->label_dsc->color = lv_color_hex(0x00ff00);
        draw_desc->line_dsc->color = lv_color_hex(0x00ff00);
    }
}

static void update_chart_data(lv_timer_t * t)
{
    lv_obj_t * chart = t->user_data;
    static data_counter = 0;

    if(data_counter++ == 50)
    {
        lv_chart_set_all_value(chart, series_primary, LV_CHART_POINT_NONE);
        lv_chart_set_all_value(chart, series_secondary, LV_CHART_POINT_NONE);
        data_counter = 0;
    }

    lv_chart_set_next_value(chart, series_primary, lv_rand(0, 100));
    lv_chart_set_next_value(chart, series_secondary, lv_rand(0, 100));
}

void initialize_dual_line_chart()
{
    chart_container = lv_obj_create(lv_scr_act());
    lv_obj_set_size(chart_container, 480, 320);
    lv_obj_set_scrollbar_mode(chart_container, LV_SCROLLBAR_MODE_OFF);

    lv_obj_set_style_bg_opa(chart_container, 255, LV_PART_MAIN|LV_STATE_DEFAULT);
    lv_obj_set_style_bg_color(chart_container, lv_color_hex(0xE8E8E8), LV_PART_MAIN|LV_STATE_DEFAULT);
    lv_obj_set_style_bg_grad_dir(chart_container, LV_GRAD_DIR_NONE, LV_PART_MAIN|LV_STATE_DEFAULT);

    main_chart = lv_chart_create(chart_container);
    lv_chart_set_zoom_x(main_chart, 256);
    lv_chart_set_zoom_y(main_chart, 256);
    lv_obj_set_pos(main_chart, 20, -15);
    lv_obj_set_size(main_chart, 400, 290);

    lv_chart_set_type(main_chart, LV_CHART_TYPE_LINE);
    lv_obj_set_style_size(main_chart, 0, LV_PART_INDICATOR);

    lv_chart_set_div_line_count(main_chart, 5, 11);
    lv_chart_set_point_count(main_chart, 50);
    lv_chart_set_axis_tick(main_chart, LV_CHART_AXIS_PRIMARY_X, 5, 3, 11, 5, true, 40);

    lv_chart_set_range(main_chart, LV_CHART_AXIS_PRIMARY_Y, 0, 100);
    lv_chart_set_axis_tick(main_chart, LV_CHART_AXIS_PRIMARY_Y, 5, 3, 5, 5, true, 40);
    lv_chart_set_range(main_chart, LV_CHART_AXIS_SECONDARY_Y, 0, 100);
    lv_chart_set_axis_tick(main_chart, LV_CHART_AXIS_SECONDARY_Y, 5, 3, 5, 5, true, 40);

    series_primary = lv_chart_add_series(main_chart, lv_color_hex(0x000000), LV_CHART_AXIS_PRIMARY_Y);
    series_secondary = lv_chart_add_series(main_chart, lv_color_hex(0xff0000), LV_CHART_AXIS_SECONDARY_Y);

    lv_obj_set_style_bg_opa(main_chart, 255, LV_PART_MAIN|LV_STATE_DEFAULT);
    lv_obj_set_style_bg_color(main_chart, lv_color_hex(0xffffff), LV_PART_MAIN|LV_STATE_DEFAULT);
    lv_obj_set_style_bg_grad_dir(main_chart, LV_GRAD_DIR_NONE, LV_PART_MAIN|LV_STATE_DEFAULT);
    lv_obj_set_style_border_width(main_chart, 0, LV_PART_MAIN|LV_STATE_DEFAULT);
    lv_obj_set_style_border_opa(main_chart, 255, LV_PART_MAIN|LV_STATE_DEFAULT);
    lv_obj_set_style_border_color(main_chart, lv_color_hex(0xe8e8e8), LV_PART_MAIN|LV_STATE_DEFAULT);
    lv_obj_set_style_border_side(main_chart, LV_BORDER_SIDE_FULL, LV_PART_MAIN|LV_STATE_DEFAULT);
    lv_obj_set_style_radius(main_chart, 0, LV_PART_MAIN|LV_STATE_DEFAULT);
    lv_obj_set_style_line_width(main_chart, 2, LV_PART_MAIN|LV_STATE_DEFAULT);
    lv_obj_set_style_line_color(main_chart, lv_color_hex(0xe8e8e8), LV_PART_MAIN|LV_STATE_DEFAULT);
    lv_obj_set_style_line_opa(main_chart, 255, LV_PART_MAIN|LV_STATE_DEFAULT);
    lv_obj_set_style_shadow_width(main_chart, 0, LV_PART_MAIN|LV_STATE_DEFAULT);

    lv_obj_set_style_text_color(main_chart, lv_color_hex(0x000000), LV_PART_TICKS|LV_STATE_DEFAULT);
    lv_obj_set_style_text_font(main_chart, &lv_font_montserrat_16, LV_PART_TICKS|LV_STATE_DEFAULT);
    lv_obj_set_style_text_opa(main_chart, 255, LV_PART_TICKS|LV_STATE_DEFAULT);
    lv_obj_set_style_line_width(main_chart, 2, LV_PART_TICKS|LV_STATE_DEFAULT);
    lv_obj_set_style_line_color(main_chart, lv_color_hex(0x000000), LV_PART_TICKS|LV_STATE_DEFAULT);
    lv_obj_set_style_line_opa(main_chart, 255, LV_PART_TICKS|LV_STATE_DEFAULT);

    lv_obj_add_event_cb(main_chart, customize_chart_appearance, LV_EVENT_DRAW_PART_BEGIN, NULL);
    lv_chart_set_update_mode(main_chart, LV_CHART_UPDATE_MODE_CIRCULAR);
    data_timer = lv_timer_create(update_chart_data, 1000, main_chart);
}

This implementation provides a complete solution for dual-line real-time data visualization with LVGL, suitable for monitoring applications and data collecsion systems.

Tags: LVGL data-visualization embedded-ui real-time-charts

Posted on Thu, 15 Oct 2026 16:33:15 +0000 by tekrscom