Link here

Ether/WiFi Basic Data Visualization in C
Demo programs for web data visualization.


A challenging part of providing a web visualization of measured data is simply converting the measured data into valid HTML to send to a user's web browser. Several demo programs are provided here that do just that – they produce valid HTML5 that browsers display as data rich graphics.

 

New HTML5 features support data visualization

The HTML5 standard now offers several new methods for rendering graphics in web pages, including:

These are powerful tools for data visualization, allowing an intuitive display of measurements from a web-enabled instrument.

 

Data visualization demo programs

C-generated HTML bar graphs

Of course, standard HTML provides simple table cells and div elements useful for displaying simple tables of data. It is these simpler HTML elements, rather than the HTML5-specific technologies mentioned above, that the following demo program uses.

This demo provides a simple example of producing valid HTML based on measured data from a microcontroller-based single board computer such as the QCard Controller and PDQ Board.

Download Data Visualization Demo Program
Click on this zip file to download the demo. The zip file contains everything needed to load this example on Mosaic's QCard or QScreen by sending the loader file, and also contains a Mosaic IDE Plus CBP project file that will produce a download file for the PDQ Board simply by clicking Build menu →  Build.

The C code and HTML boilerplate for this demo are displayed below. After loading the demo onto your controller and typing main, you should be able to browse to the IP address displayed in Mosaic Terminal and see a page that looks like this, displaying measured voltages from the built-in analog to digital converter.


Basic Web Bar Chart Code

// this demonstration code is provided in source form.
// Top level functions:
//  Ether_Web_Demo( )  // no input parameters; runs web pages, enables email, tunneling;
//  WiFi_Web_Demo( )  // no input parameters; runs web pages, enables email, tunneling;
// You may edit main() to call the version of the web demo for your Wildcard.
 
// Make sure to edit the E_MODULENUM constant to match your hardware jumper settings.
// For graphical controllers (i.e. QScreen), E_MODULENUM must not be 0!
 
// ************** #includes **************
 
 
#include <mosaic/allqed.h> // include all of the qed and C utilities
 
#ifdef __GNUC__
// For PDQ line platforms, the driver is enabled by simply including
// the header file below.
#include "wwifi.h"
#else  // __GNUC__
// For the Q-line platforms, we include the kernel extension manager
// generated library.c.  We assume that it is present in this directory.
#include "packages/library.h"
#include "packages/library.c"
#endif  // __GNUC__
 
// For PDQ line platforms, Mosaic IDE Plus specifically compiles files
// in the web_resources folder.  For Q-line platforms, we assume you have
// manually put the IMAGECONVERTER.EXE program outputs in web_resources.
#include "web_resources/image_headers.h"
 
 
 
// ************** USEFUL MACRO FOR STRINGS IN V4.xx C COMPILER **************
 
// the TO_XADDR defined in /mosaic/include/types.h
// transforms a separate 16-bit addr, page into a 32-bit xaddress:
// #define TO_XADDR(address,page)     ((xaddr) (((page)<<16)+ (0xFFFF & (address))))
 
// We want to substitute THIS_PAGE (also defined in types.h) for the page,
// as the V4.xx C compiler replicates the strings on each page;
// therefore, in most cases, the calling function's page is the same as the string page:
 
#ifndef STRING_XADDR
  #define STRING_XADDR(str_addr) TO_XADDR(((xaddr) str_addr), ((xaddr) THIS_PAGE))
#endif
 
// This is a generalized method of casting a pointer
// to common memory to a 32-bit XADDR with zero for page.
#ifndef COMMON_XADDR
  #define COMMON_XADDR(A) ((xaddr)((unsigned)(A)))
#endif
 
 
 
// ************** USEFUL CONSTANTS **************
 
// #define TASK_SIZE 0x400   // 0x400 = decimal 1024 = 1Kbyte = task size
 
 
 
// *************** IMPORTANT: SET E_MODULENUM TO MATCH HARDWARE JUMPERS ******************
// *************** FOR GRAPHICAL CONTROLLERS, E_MODULENUM MUST NOT BE 0! *****************
 
#define E_MODULENUM 1
 
// This specifies the modulenum for the high level functions in this file
// except for Ether_Monitor_Demo (see its comments).
// If EtherSmart is installed on module bus 0, E_MODULENUM = 0, 1, 2, or 3
// If EtherSmart is installed on module bus 1, E_MODULENUM = 4, 5, 6, or 7
// That is, the bus specifies the top bit of a 3-bit modulenum,
// and [J2, J1] specifies the remaining 2 bits of the modulenum.
// Example: On module bus 0, if neither jumper cap is installed: E_MODULENUM = 0
// Example: On module bus 0, if both jumper caps are installed: E_MODULENUM = 3
// Example: On module bus 1, if J2 is installed but J1 is not: E_MODULENUM = 6
// The GUI Toolkit for graphical controllers (i.e. QScreen) uses module number 0
// to access the display, so module number 0 must not be assigned to wildcards
// when used with a graphical controller.
 
 
 
// ************** DEFAULT IP ADDRESS SELECTION **************
 
// Uncomment all of these to use a static IP address, i.e. 10.0.1.169 .
// Otherwise a dynamic DHCP-assigned address will be requested in main().
// #define IP_OCTET_1 10
// #define IP_OCTET_2 0
// #define IP_OCTET_3 1
// #define IP_OCTET_4 169
 
 
 
// ***************** INTERACTIVE CONFIGURATION AND REPORTING ************
 
// These functions allow setting the IP address on the command line.
 
_Q int Ether_Set_Defaults( void )
// works for xport or wiport (ethernet/wifi)
// call this AFTER calling main() or Ether_Web_Demo or Ether_Task_Setup_Default()
// or WiFi_Web_Demo or WiFi_Task_Setup_Default.
// sets mosaic factory defaults; returns error code
// sets local IP and gateway to 0.0.0.0 = unassigned, so IP address
// gets assigned via DHCP (Dynamic Host Configuration Protocol) by the LAN's gateway.
// see user guide for more information.
{
    printf( "\r\nSetting defaults, requesting DHCP IP address...\r\n" );
    Ether_XPort_Defaults( E_MODULENUM ); // works for xport or wiport (ethernet/wifi)
    Ether_IP_Info_Report( E_MODULENUM );
    return( (int) Ether_Await_Response( E_MODULENUM ) ); // error code is in lsword
}
 
_Q int Ether_Set_Local_IP( int my_ip1, int my_ip2, int my_ip3, int my_ip4 )
// call this AFTER calling main() or Ether_Web_Demo or Ether_Task_Setup_Default()
// or WiFi_Web_Demo or WiFi_Task_Setup_Default.
// sets the IP address of the EtherSmart Wildcard specified by E_MODULENUM as:
//   ip1.ip2.ip3.ip4
// For example, to set the IP address to 10.0.1.22, pass to this function the parameters:
//   10 0 1 22
// returns error code
// NOTES: type DECIMAL at the monitor before invoking this function interactively!
//        The input types are declared as int to simplify interactive calling,
//        as the interactive debugger would require char specifiers before each input
//        parameter if the char type were used.
// NOTE: assigning a nonzero IP address disables DHCP!
{
    int retval;
    printf( "\r\nSetting local IP address to %d.%d.%d.%d ...\r\n",
            my_ip1, my_ip2, my_ip3, my_ip4 );
    Ether_Local_IP( (uchar) my_ip1, (uchar) my_ip2,
                    (uchar) my_ip3, (uchar) my_ip4, E_MODULENUM );
    Ether_XPort_Update( E_MODULENUM );  // works for xport or wiport (ethernet/wifi)
    retval = (int) Ether_Await_Response( E_MODULENUM ); // error code is in lsword
    if( retval ) printf( "Setting local IP address failed with error: %d\r\n", retval );
    else Ether_IP_Info_Report( E_MODULENUM );
    return retval;
}
 
_Q void Ether_IP_Report( void )
// call this AFTER calling main() or Ether_Web_Demo or Ether_Task_Setup_Default()
// or WiFi_Web_Demo or WiFi_Task_Setup_Default.
// takes 7 seconds to execute, so be patient.
// Report is of the form:
// IP 010.000.001.019 GW 010.000.001.022 Mask 255.255.255.000
// which summarizes the IP address, gateway address, and netmask, respectively.
{
    Ether_IP_Info_Report( E_MODULENUM );
}
 
_Q void Ether_Ping( int remote_ip1, int remote_ip2, int remote_ip3,  int remote_ip4 )
// call this AFTER calling main() or Ether_Web_Demo or Ether_Task_Setup_Default()
// or WiFi_Web_Demo or WiFi_Task_Setup_Default.
// on error, prints " Couldn't enter monitor mode!" or " No response from remote".
// takes thirteen seconds to execute, so be patient.
// Report is of the form (summarizes response time from specified remote host):
// Seq 001 time 10ms
// Seq 002 time 10ms
// Seq 003 time 10ms
// Seq 004 time 10ms
// Seq 005 time 10ms
// Seq 006 time 10ms
// NOTES: type DECIMAL at the monitor before invoking this function interactively!
//        The input types are declared as int to simplify interactive calling,
//        as the interactive debugger would require char specifiers before each input
//        parameter if the char type were used.
{
    Ether_Ping_Report( (uchar) remote_ip1, (uchar) remote_ip2,
                       (uchar) remote_ip3, (uchar) remote_ip4, E_MODULENUM );
}
 
 
 
// ****************** DATA ACQUISITION *******************
 
// Total number of inputs for which to report voltage.
#define INPUT_COUNT 8
#define VOLTAGE_FULL_SCALE 5.0f
#define COUNTS_FULL_SCALE 65536.0f
 
float input_voltages[ INPUT_COUNT ];
 
void Read_Inputs( void )
{
    unsigned i, sample;
    float voltage;
 
#ifdef __GNUC__
    unsigned* atd_results;
    // PDQ-Line HCS12 ADC can convert multiple channels at once.
    // Convert INPUT_COUNT channels, starting at channel 0,
    // and return a pointer to the static array of results.
    atd_results = ATDSample( 0, INPUT_COUNT );
#endif
 
    for( i = 0; i < INPUT_COUNT; ++i )
    {
#ifdef __GNUC__
        // Copy previously-converted PDQ HCS12 result out of the static array.
        sample = atd_results[i];
#else
        // "Fast" version of Q-Line HC11 AD8Sample may be used if only one task uses ADC.
        // Shift the result into upper byte to match PDQ HCS12 left-justified result.
        sample = FastAD8Sample( i ) << 8;
#endif
        voltage = VOLTAGE_FULL_SCALE * ( (float)sample / COUNTS_FULL_SCALE );
        // This function disables interrupts while copying the 4-byte float
        // to ensure integrity of data shared between this task and the web task.
        StoreFloatProtected( voltage, COMMON_XADDR( &input_voltages[i] ) );
    }
}
 
#ifdef __GNUC__
#define TICKS_PER_SECOND 977
#else
#define TICKS_PER_SECOND 200
#endif
 
void Read_Inputs_Loop( void )
{
    unsigned long last_timestamp, this_timestamp;
 
    last_timestamp = FetchLongProtected( COMMON_XADDR( &TIMESLICE_COUNT ) );
 
    for(;;)
    {
        this_timestamp = FetchLongProtected( COMMON_XADDR( &TIMESLICE_COUNT ) );
        // Update readings only once per second.
        if( this_timestamp - last_timestamp > TICKS_PER_SECOND )
        {
            Read_Inputs();
            last_timestamp = this_timestamp;
        }
        Pause();
    }
}
 
 
 
// *********************** WEBSERVER DEMO (Ethersmart Web Pages) ***************
 
// These support functions produce dynamic content
// based on the values of global variables.
 
// Height in pixels of the area containing a bar in the chart to be displayed.
#define CHART_BAR_MAX 320
 
// Statically allocated buffer area for sprintf to create lines of dynamic content.
// This must only be used in one task, specifically ether_control_task by handlers.
#define LINEBUF_SIZE 128
char html_line_buffer[ LINEBUF_SIZE ];
 
void Put_Chart_Header( unsigned colspan, const char* caption, int modulenum )
{
    static const char header_end[] = "</th></tr><tr>\r\n";
    unsigned line_length;
 
    // First string fragment is here; formatting %u is safely five characters max.
    line_length =
        sprintf( html_line_buffer,
                 "<table id=\"chart\"><tr><th colspan=\"%u\">", colspan );
 
    // Compiler in Mosaic IDE for Q-line products does not include snprintf(),
    // strncat(), etc, so make an effort at avoiding buffer overruns this way.
    line_length += strlen( caption ) + strlen( header_end );
    if( line_length >= LINEBUF_SIZE ) SysAbort();
 
    strcat( html_line_buffer, caption );
    strcat( html_line_buffer, header_end );
 
    HTTP_Send_Buffer( COMMON_XADDR( html_line_buffer ), line_length, modulenum );
}
 
void Put_Chart_Bar( float value, float total, const char* color, int modulenum )
{
    // String fragments defined here to add up line_length before concatenation.
    static const char bar_post_color[] = ";\">";
    static const char bar_post_value[] = "</div></td>\r\n";
    unsigned bar_height, line_length;
    char* fp_string;
 
    bar_height = (unsigned)( (float)(CHART_BAR_MAX) * ( value / total ) + 0.5 );
    // In case user passes value > total, clamp bar_height .
    if( bar_height > CHART_BAR_MAX ) bar_height = CHART_BAR_MAX;
 
    // QED-Forth system floating point formatting is a safer alternative to sprintf().
    // See initialization settings in Put_Chart().  Formatted number string placed in a
    // task-specific buffer, valid until another FPtoString() or PrintFP() in this task.
    fp_string = FPtoString( value );
 
    // First string fragment is here; formatting %u is safely five characters max.
    line_length =
        sprintf( html_line_buffer,
                 "  <td class=\"chart-column\"><div class=\"chart-bar\" "
                 "style=\"height: %upx; background: ", bar_height );
 
    // Now check whether full concatenated string will be too long.
    line_length += strlen( color ) + strlen( bar_post_color ) +
                   strlen( fp_string ) + strlen( bar_post_value );
    if( line_length >= LINEBUF_SIZE ) SysAbort();
 
    strcat( html_line_buffer, color );
    strcat( html_line_buffer, bar_post_color );
    strcat( html_line_buffer, fp_string );
    strcat( html_line_buffer, bar_post_value );
 
    HTTP_Send_Buffer( COMMON_XADDR( html_line_buffer ), line_length, modulenum );
}
 
void Put_Chart_New_Row( int modulenum )
{
    // This simply sends the end of a table row and beginning of the next.
    static const char row_divider[] = "</tr><tr>\r\n";
    HTTP_Send_Buffer( COMMON_XADDR( row_divider ), strlen( row_divider ), modulenum );
}
 
void Put_Chart_Label( char* label, int modulenum )
{
    static const char label_begin[] = "  <td class=\"chart-label\">";
    static const char label_end[]   = "</td>\r\n";
    unsigned line_length;
 
    line_length = strlen( label_begin ) + strlen( label_end ) + strlen( label );
    if( line_length >= LINEBUF_SIZE ) SysAbort();
 
    strcpy( html_line_buffer, label_begin );
    strcat( html_line_buffer, label );
    strcat( html_line_buffer, label_end );
 
    // html_line_buffer is an array in common memory, so it may be simply cast to xaddr.
    HTTP_Send_Buffer( COMMON_XADDR( html_line_buffer ), line_length, modulenum );
}
 
void Put_Chart_Footer( int modulenum )
{
    // This simply sends the end of a table row and end of table.
    static const char chart_footer[] = "</tr></table>\r\n";
    HTTP_Send_Buffer( COMMON_XADDR( chart_footer ), strlen( chart_footer ), modulenum );
}
 
// Voltage thresholds for low and critical.
#define VOLTAGE_LOW 2.0f
#define VOLTAGE_CRITICAL 1.0f
 
void Put_Chart( int modulenum )
{
    // CSS colors to be used in rendering.
    static const char color_normal[]   = "green";
    static const char color_low[]      = "yellow";
    static const char color_critical[] = "red";
    unsigned i;
#ifdef __GNUC__
    const char* this_color;  // Pointer modifiable, data pointed to is const.
#else
    char* this_color;
#endif
    float this_voltage;
 
    // Initialize QED-Forth system floating point formatting.
    // See its usage in Put_Chart_Bar() as a safer alternative to sprintf().
    FIXED();                    // Fixed-point rathar than SCIENTIFIC() or FLOATING()
    LEFT_PLACES = 1;            // Will be printing voltages in the range 0-5V
    RIGHT_PLACES = 2;           // ~0.02V resolution on HC11, ~0.005V resolution on HCS12
    TRAILING_ZEROS = TRUE;      // Trailing zeros are not removed, for consistency
    // FILL_FIELD and NO_SPACES irrelevant for generating HTML.
 
    // Number of columns and table caption.
    Put_Chart_Header( INPUT_COUNT, "Inputs (Volts)", modulenum );
 
    // Chart bars are drawn in a loop to iterate over input_voltages[].
    for( i = 0; i < INPUT_COUNT; ++i )
    {
        // This function disables interrupts in copying a 4-byte float
        // to ensure integrity of data shared between this task and ADC task.
        this_voltage = FetchFloatProtected( COMMON_XADDR( &input_voltages[i] ) );
 
        if( this_voltage < VOLTAGE_CRITICAL ) this_color = color_critical;
        else if( this_voltage < VOLTAGE_LOW ) this_color = color_low;
        else this_color = color_normal;
 
        Put_Chart_Bar( this_voltage, VOLTAGE_FULL_SCALE, this_color, modulenum );
    }
 
    // Divider between table rows.
    Put_Chart_New_Row( modulenum );
 
    // Chart labels are simply enumerated here.
    Put_Chart_Label( "Sensor A", modulenum );
    Put_Chart_Label( "Sensor B", modulenum );
    Put_Chart_Label( "Sensor C", modulenum );
    Put_Chart_Label( "Sensor D", modulenum );
    Put_Chart_Label( "Sensor E", modulenum );
    Put_Chart_Label( "Sensor F", modulenum );
    Put_Chart_Label( "Sensor G", modulenum );
    Put_Chart_Label( "Sensor H", modulenum );
 
    // End of table.
    Put_Chart_Footer( modulenum );
}
 
void Put_Page_End( int modulenum )
{
    // This simply sends the end of body and html elements.
    static const char page_end[] = "</body></html>\r\n";
    HTTP_Send_Buffer( COMMON_XADDR( page_end ), strlen( page_end ), modulenum );
}
 
 
 
void Home_Page( int modulenum )
// this is the handler function for the sole "/" URL.  Sends the http header
// with dynamic (because of changing time stamp) text/html content type, followed
// by html boilerplate, the chart, and the client time when the page was loaded.
{
    xaddr http_outbuf_base = HTTP_Outbuf(modulenum);
    uint http_outbuf_size =  HTTP_Outbufsize(modulenum);
    HTTP_Put_Headers(http_outbuf_base, http_outbuf_size,
                     TRUE, TRUE, HTTP_TEXT_HTML_CONTENT);
            // params: xlbuf,maxbufsize,ok?,dynamic?,content_type
            // dynamic text/html content ->outbuf;header is done
    HTTP_Send_LBuffer(http_outbuf_base, modulenum); // send out header, ignore #bytes_sent
    HTTP_Send_Buffer(BASIC_HTML_BAR_CHART_HTML_XADDR, BASIC_HTML_BAR_CHART_HTML_SIZE, modulenum);
 
    Put_Chart( modulenum );
 
    Put_Page_End( modulenum );
}
 
 
 
#ifdef __GNUC__
// For PDQ line platforms (running the V6.xx kernel and using GNU C compiler),
// use this syntax to declare a 4-byte pointer to each handler function.
// It must be initialized at runtime, generally in main().
 
xaddr home_page_ptr;
 
#else  // __GNUC__
 
// For Q line platforms (running the V4.xx kernel and using Fabius C compiler),
// use this syntax to properly initialize a 4-byte pointer to each handler function:
 
#pragma option init=.doubleword    // declare 32-bit function pointers in code area
#include </mosaic/gui_tk/to_large.h>
 
xaddr (*home_page_ptr)(void) = Home_Page;
 
#include </mosaic/gui_tk/fr_large.h>
#pragma option init=.init       // return the initialized variable area to RAM;
 
#endif  // __GNUC__
 
 
 
// Allocate 1K task areas.
TASK ether_control_task;
TASK read_inputs_task;
 
int main( void )
{
    int retval;
 
    printf( "\r\nDelaying six seconds for XPort init...\r\n" );
    for( retval = 0; retval < 96; ++retval ) MicrosecDelay( 62500u );
    printf( "Initializing memory and inputs...\r\n" );
 
#ifdef __GNUC__
    // For the PDQ line products, handler pointer must be initialized at runtime.
    home_page_ptr = (xaddr) Home_Page;
    // Initialize PDQ line HCS12 analog to digital converter, channels 0-7.
    ATDOn(0);
#else
    // Initialize Q-line HC11 analog to digital converter, all eight channels.
    AD8On();
#endif
 
    // Take one initial set of voltage readings.
    Read_Inputs();
 
    // Ensures task loop initialized to empty.
    NEXT_TASK = TASKBASE;
 
    // Allocate and initialize ~3Kbyte ram buffers, and start timeslicer.
    // See the glossary entries for these functions for more details.
    // Comment in the correct version for your Wildcard:
    Ether_Setup_Default( E_MODULENUM );
    //WiFi_Setup_Default( E_MODULENUM );
 
    printf( "Starting tasks...\r\n" );
 
    // Build and activate data acquisition task.
    BUILD_C_TASK( 0, 0, &read_inputs_task );
    ACTIVATE( Read_Inputs_Loop, &read_inputs_task );
 
    // Build and activate web service task.
    // Ether_Service_Loop is a function internal to the Ethersmart/Wifi Wildcard
    // driver that awaits incoming connections and calls the defined handlers.
    // See the glossary entry for more details.
    BUILD_C_TASK( 0, 0, &ether_control_task );
    ACTIVATE( Ether_Service_Loop, &ether_control_task );
 
    // Add a single handler for the empty ("/") URL.
    retval = HTTP_Add_Handler( STRING_XADDR( "/" ), 1, home_page_ptr, E_MODULENUM );
 
    if( retval )
    {
        printf( "Adding handler failed with error number: %d\r\n", retval );
    }
    else
    {
        printf( "Configuring network interface...\r\n" );
        // Set IP address if specified, or request DHCP-assigned address.
#if defined IP_OCTET_1 && defined IP_OCTET_2 && defined IP_OCTET_3 && defined IP_OCTET_4
        // Static IP address specified.
        Ether_Local_IP( IP_OCTET_1, IP_OCTET_2, IP_OCTET_3, IP_OCTET_4 );
        Ether_XPort_Update( E_MODULENUM );  // works for xport or wiport (ethernet/wifi)
#else
        // No static IP address specified; request dynamic address with DHCP.
        Ether_XPort_Defaults( E_MODULENUM ); // works for xport or wiport (ethernet/wifi)
#endif
        retval = (int) Ether_Await_Response( E_MODULENUM ); // error code is in lsword
        if( retval ) printf( "Configuration failed with error number: %d\r\n", retval );
        else Ether_IP_Info_Report( E_MODULENUM );
    }
 
    return retval;
}

Basic Web Bar Chart Boilerplate

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta http-equiv="refresh" content="60" />
<title>Basic HTML Bar Chart Demo</title>
 
<!-- Prevent browser from requesting favicon.ico; which adds an additional 2 second delay to loading the HTML View.
     A favicon may be encoded in base64 to be included after the comma below using the following code in Python 3:
>>> from base64 import b64encode
>>> with open( r'c:\users\you\desktop\favicon.png', 'rb' ) as f:
...   print( b64encode( f.read() ).decode() )
-->
<link rel="icon" href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAACXBIWXMAAA7EAAAOxAGVKw4bAAAARElEQVR42mNgoA743wDBMDbxgAm3gfQF6F4g3jtMxBk+sF7A5R0In4l0i2jjLWK9gMkm4AXGhoFOSITVkBAL6N6hkvcAKa86aKbjO6UAAAAASUVORK5CYII=" />
 
<style type="text/css">
html { background: #ADAFB2; }
h1 { text-align: center; }
#chart { background: #CC7D00; margin-left: auto; margin-right: auto; vertical-align: middle; border-spacing: 20px; }
#chart th { background: white; text-align: center; }
#chart td.chart-column { background: #303050; vertical-align: bottom; height: 320px; width: 60px; border: 3px solid black; padding: 0px; }
#chart td.chart-label { background: white; text-align:center; vertical-align:top; }
#chart div.chart-bar { width: 60px; vertical-align: bottom; text-align: center; }
p#timestamp { text-align: center; font-style: italic; }
</style>
 
</head>
<body>
<h1>Basic HTML Bar Chart Demo</h1>
 
<!-- Remainder of the page to be generated by controller. -->
 
This page is about: C Programs Generate HTML Data Tables for Displaying Embedded System Measurements in Web Browser – A challenging part of providing web visualization of measured data is simply converting measured data into valid HTML to send to users web browser. Several demo programs are provided here that do just that - they produce valid HTML5 that browsers display …
 
 
Navigation