leeoniya / uPlot

📈 A small, fast chart for time series, lines, areas, ohlc & bars
MIT License
8.76k stars 385 forks source link

Help needed with uPlot running on ESP32 microcontroller #699

Closed happytm closed 1 month ago

happytm commented 2 years ago

@leeoniya Hi Leon,

Thank you for such a powerful graph software which can run on microcontroller where webserver itself is hosted.

With your help I was able to implement graphs for my home automation project. In my opinion it is easiest and fastest way to visualize sensor data of whole network with very little effort. Everything is working fine for me.

Now I wanted to add some filters to load graph according to three dropdown menu at the top and I am not sure it is even possible.

I have shared relevant code from my project with mock up of three drop down menus. If it is possible to load graph according to selection at top can you please show me how. My code is below: Thanks.

My data.json structure is : [12,"Epoch","Location","Voltage","SSID","T1","S1","T2","S2","T3","S3","T4","S4"]

<html> 
<head> 
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> 
<title>Proberequest, TinyMQTT, Websockets, Asyncwebserver, LittleFS Editor & uPlot</title> 

<link rel="stylesheet" href="uPlot.min.css">

<style>
body {margin: 0;}
.u-legend.u-inline .u-value {width: 150px;text-align: left;}

#container { width: 100%; height: 49vh; background-color: #333; display: flex; align-items: center; justify-content: center; overflow: hidden; border-radius: 7px; touch-action: none; } 
#item { width: 100px; height: 100px; background-color: rgb(245, 230, 99); border: 10px solid rgba(136, 136, 136, .5); border-radius: 50%; touch-action: none; user-select: none; } 
#item:active { background-color: rgba(168, 218, 220, 1.00); } 
#item:hover { cursor: pointer; border-width: 20px; } 
#area { position: fixed; right: 33.33%; top: 50vh; } 
#stream-container{ height: 49vh; padding:0; margin:0; margin-block-start:0; margin-block-end:0; margin-inline-start:0; margin-inline-end:0 } 
#stream-container img{ display:block; max-width:100%; min-height:49vh; border-radius:4px; margin-top:8px } 
</style> 

</head> 

<body>
<script type="text/javascript" src="uPlot.iife.min.js"></script>

<h2>Proberequest, TinyMQTT, Asyncwebserver, LittleFS Editor & uPlot</h2>

<SELECT class="combine" id ="period" name = "period">
    <option value="all">Graph by Period</option>
    <option value="1">Last Hour</option>
    <option value="2">Today</option>
    <option value="3">This Week</option>
    <option value="4">This Month</option>
    <option value="5">This Year</option>
</SELECT>

<SELECT class="combine" id ="deviceid" name = "deviceid">
    <option value="all">Graph by Device</option>
    <option value="1">Gateway</option>
    <option value="6">Livingroom</option>
    <option value="16">Kitchen</option>
    <option value="26">Bedroom1</option>
    <option value="36">Bedroom2</option>
    <option value="46">Bathroom1</option>
    <option value="56">Bathroom2</option>
    <option value="66">Laundry</option>
    <option value="76">Solar Tracker</option>
    <option value="86">Water Tank</option>
    <option value="96">Weather Station</option>
    <option value="106">Greenhouse</option>
</SELECT>

<SELECT class="combine" id ="sensortype" name = "sensortype">
    <option value="all">Graph by Sensor Type</option>
    <option value="100">Voltage</option>
    <option value="200">RSSI</option>
    <option value="1">Temperature</option>
    <option value="2">Humidity</option>
    <option value="3">Pressure</option>
    <option value="4">Light</option>
    <option value="5">Level</option>
    <option value="6">Switch Status</option>
    <option value="7">Motion</option>
    <option value="8">Azimuth</option>
    <option value="9">Elevation</option>
    <option value="10">Soil Moisture</option>
    <option value="11">Wind Speed</option>
    <option value="12">Wind Direction</option>
    <option value="13">Rainfall</option>
    <option value="14">Air Quality</option>
</SELECT>
<script>

var sensor1,sensor2,sensor3,sensor4,sensor5,sensor6,sensor7,sensor8,sensor9,sensor10,sensor11,sensor12,sensor13,sensor14;

// converts the legend into a simple tooltip
function legendAsTooltipPlugin({ className, style = { backgroundColor:"rgba(255, 249, 196, 0.92)", color: "black" } } = {}) {
                let legendEl;

                function init(u, opts) {
                    legendEl = u.root.querySelector(".u-legend");

                    legendEl.classList.remove("u-inline");
                    className && legendEl.classList.add(className);

                    uPlot.assign(legendEl.style, {
                        textAlign: "left",
                        pointerEvents: "",
                        display: "none",
                        position: "absolute",
                        left: 0,
                        top: 0,
                        zIndex: 100,
                        boxShadow: "2px 2px 10px rgba(0,0,0,0.5)",
                        ...style
                    });

                    // hide series color markers
                    const idents = legendEl.querySelectorAll(".u-marker");

                    for (let i = 0; i < idents.length; i++)
                        idents[i].style.display = "";

                    const overEl = u.root.querySelector(".u-over");
                    overEl.style.overflow = "visible";

                    // move legend into plot bounds
                    overEl.appendChild(legendEl);

                    // show/hide tooltip on enter/exit
                    overEl.addEventListener("mouseenter", () => {legendEl.style.display = null;});
                    overEl.addEventListener("mouseleave", () => {legendEl.style.display = "none";});

                    // let tooltip exit plot
            //  overEl.style.overflow = "visible";
                }

                function update(u) {
                    const { left, top } = u.cursor;
                    legendEl.style.transform = "translate(" + left + "px, " + top + "px)";
                }

                return {
                    hooks: {
                        init: init,
                        setCursor: update,
                    }
                };
            }

            function prepData(packed) {
                console.time("prep");

                // epoch,location,voltage,rssi,temperature,humidity,pressure,light

                const numFields = packed[0];
                console.log(numFields); //12
                packed = packed.slice(numFields + 1);
                console.log(packed);   //single array of all sensor data.

                let data = [
                    Array(packed.length/numFields), // Time
                    Array(packed.length/numFields), // Location
          Array(packed.length/numFields), // Voltage
                    Array(packed.length/numFields), // RSSI
                    Array(packed.length/numFields), // Temperature
                    Array(packed.length/numFields), // Humidity
          Array(packed.length/numFields), // Pressure
                    Array(packed.length/numFields), // Light
                    Array(packed.length/numFields), // Level
                    Array(packed.length/numFields), // Switch Status
                    Array(packed.length/numFields), // Motion
                    Array(packed.length/numFields), // Azimuth
                    Array(packed.length/numFields), // Elevation
                    Array(packed.length/numFields), // Soil Moisture
                    Array(packed.length/numFields), // Wind Speed
                    Array(packed.length/numFields), // Wind Direction
                    Array(packed.length/numFields), // Rainfall
                    Array(packed.length/numFields), // Air Quality
                ];

              console.log(data); // (12)arrays of values by field

                for (let i = 0, j = 0; i < packed.length; i += numFields, j++) {

                    data[0][j] = packed[i+0];
                  data[1][j] = packed[i+1];
                    data[2][j] = packed[i+2];
                    data[3][j] = packed[i+3];

                           if (packed[i+4] == 1) {data[4][j] = packed[i+5]; sensor1   = "Temperature";
                    } else if (packed[i+4] == 2) {data[5][j] = packed[i+5]; sensor2   = "Humidity";
                    } else if (packed[i+4] == 3) {data[6][j] = packed[i+5] * 4; sensor3   = "Pressure";
                    } else if (packed[i+4] == 4) {data[7][j] = packed[i+5]; sensor4   = "Light";
                  } else if (packed[i+4] == 5) {data[8][j] = packed[i+5]; sensor5   = "Level";
                    } else if (packed[i+4] == 6) {data[9][j] = packed[i+5]; sensor6   = "Switch Status";
                    } else if (packed[i+4] == 7) {data[10][j] = packed[i+5]; sensor7  = "Motion";
                  } else if (packed[i+4] == 8) {data[11][j] = packed[i+5]; sensor8  = "Azimuth";
                  } else if (packed[i+4] == 9) {data[12][j] = packed[i+5]; sensor9  = "Elevation";
                    } else if (packed[i+4] == 10) {data[13][j] = packed[i+5];sensor10 = "Soil Moisture";
                  } else if (packed[i+4] == 11) {data[14][j] = packed[i+5];sensor11 = "Wind Speed";
                    } else if (packed[i+4] == 12) {data[15][j] = packed[i+5];sensor12 = "Wind Direction";
                    } else if (packed[i+4] == 13) {data[16][j] = packed[i+5];sensor13 = "Rainfall";
                    } else if (packed[i+4] == 14) {data[17][j] = packed[i+5];sensor14 = "Air Quality";}

                           if (packed[i+6] == 1) {data[4][j] = packed[i+7]; sensor1   = "Temperature";
                    } else if (packed[i+6] == 2) {data[5][j] = packed[i+7]; sensor2   = "Humidity";
                    } else if (packed[i+6] == 3) {data[6][j] = packed[i+7] * 4; sensor3   = "Pressure";
                    } else if (packed[i+6] == 4) {data[7][j] = packed[i+7]; sensor4   = "Light";
                  } else if (packed[i+6] == 5) {data[8][j] = packed[i+7]; sensor5   = "Level";
                    } else if (packed[i+6] == 6) {data[9][j] = packed[i+7]; sensor6   = "Switch Status";
                    } else if (packed[i+6] == 7) {data[10][j] = packed[i+7]; sensor7  = "Motion";
                  } else if (packed[i+6] == 8) {data[11][j] = packed[i+7]; sensor8  = "Azimuth";
                  } else if (packed[i+6] == 9) {data[12][j] = packed[i+7]; sensor9  = "Elevation";
                    } else if (packed[i+6] == 10) {data[13][j] = packed[i+7];sensor10 = "Soil Moisture";
                  } else if (packed[i+6] == 11) {data[14][j] = packed[i+7];sensor11 = "Wind Speed";
                    } else if (packed[i+6] == 12) {data[15][j] = packed[i+7];sensor12 = "Wind Direction";
                    } else if (packed[i+6] == 13) {data[16][j] = packed[i+7];sensor13 = "Rainfall";
                    } else if (packed[i+6] == 14) {data[17][j] = packed[i+7];sensor14 = "Air Quality";}

                           if (packed[i+8] == 1) {data[4][j] = packed[i+9]; sensor1   = "Temperature";
                    } else if (packed[i+8] == 2) {data[5][j] = packed[i+9]; sensor2   = "Humidity";
                    } else if (packed[i+8] == 3) {data[6][j] = packed[i+9] * 4; sensor3   = "Pressure";
                    } else if (packed[i+8] == 4) {data[7][j] = packed[i+9]; sensor4   = "Light";
                  } else if (packed[i+8] == 5) {data[8][j] = packed[i+9]; sensor5   = "Level";
                    } else if (packed[i+8] == 6) {data[9][j] = packed[i+9]; sensor6   = "Switch Status";
                    } else if (packed[i+8] == 7) {data[10][j] = packed[i+9]; sensor7  = "Motion";
                  } else if (packed[i+8] == 8) {data[11][j] = packed[i+9]; sensor8  = "Azimuth";
                  } else if (packed[i+8] == 9) {data[12][j] = packed[i+9]; sensor9  = "Elevation";
                    } else if (packed[i+8] == 10) {data[13][j] = packed[i+9];sensor10 = "Soil Moisture";
                  } else if (packed[i+8] == 11) {data[14][j] = packed[i+9];sensor11 = "Wind Speed";
                    } else if (packed[i+8] == 12) {data[15][j] = packed[i+9];sensor12 = "Wind Direction";
                    } else if (packed[i+8] == 13) {data[16][j] = packed[i+9];sensor13 = "Rainfall";
                    } else if (packed[i+8] == 14) {data[17][j] = packed[i+9];sensor14 = "Air Quality";}

                           if (packed[i+10] == 1) {data[4][j] = packed[i+11]; sensor1   = "Temperature";
                    } else if (packed[i+10] == 2) {data[5][j] = packed[i+11]; sensor2   = "Humidity";
                    } else if (packed[i+10] == 3) {data[6][j] = packed[i+11] * 4; sensor3   = "Pressure";
                    } else if (packed[i+10] == 4) {data[7][j] = packed[i+11]; sensor4   = "Light";
                  } else if (packed[i+10] == 5) {data[8][j] = packed[i+11]; sensor5   = "Level";
                    } else if (packed[i+10] == 6) {data[9][j] = packed[i+11]; sensor6   = "Switch Status";
                    } else if (packed[i+10] == 7) {data[10][j] = packed[i+11]; sensor7  = "Motion";
                  } else if (packed[i+10] == 8) {data[11][j] = packed[i+11]; sensor8  = "Azimuth";
                  } else if (packed[i+10] == 9) {data[12][j] = packed[i+11]; sensor9  = "Elevation";
                    } else if (packed[i+10] == 10) {data[13][j] = packed[i+11];sensor10 = "Soil Moisture";
                  } else if (packed[i+10] == 11) {data[14][j] = packed[i+11];sensor11 = "Wind Speed";
                    } else if (packed[i+10] == 12) {data[15][j] = packed[i+11];sensor12 = "Wind Direction";
                    } else if (packed[i+10] == 13) {data[16][j] = packed[i+11];sensor13 = "Rainfall";
                    } else if (packed[i+10] == 14) {data[17][j] = packed[i+11];sensor14 = "Air Quality";}                   

            }

                console.timeEnd("prep");

                return data;

            }

            function makeChart(data) {
                console.time("chart");
                console.log(data[4][1]); // SensorType1

        var title = "Environment data from sensor network";
        const opts = {
                    title: title,

                    width: 480,
                    height:600,
                //  ms:     1,
                //  cursor: {
                //      x: false,
                //      y: false,
                //  },
                    series: [
                        {},

                        {
                            label: "Device",
                            scale: "Left",
                            value: (u, v) => v == null ? "-" : v.toFixed(1) + " ",
                            stroke: "teal",
                            width: 1/devicePixelRatio,
                        },
                        {
                            label: "Voltage",
                            scale: "Left",
                            value: (u, v) => v == null ? "-" : v.toFixed(1) + " V",
                            stroke: "green",
                            width: 1/devicePixelRatio,
                        },
                        {
                            label: "RSSI",
                            scale: "Right",
                            value: (u, v) => v == null ? "-" : v.toFixed(2) + " %",
                            stroke: "maroon",
                            width: 1/devicePixelRatio,
                        },

                        {
                            label: sensor1,
                            scale: "Left",
                            value: (u, v) => v == null ? "-" : v.toFixed(1) + " F",
                            stroke: "lime",
                            width: 1/devicePixelRatio,
                        },

                        {
                            label: sensor2,
                            scale: "Left",
                            value: (u, v) => v == null ? "-" : v.toFixed(1) + " %",
                            stroke: "olive",
                            width: 1/devicePixelRatio,
                        },

                        {
                            label: sensor3,
                            scale: "Right",
                            value: (u, v) => v == null ? "-" : v.toFixed(2) + " mb",
                            stroke: "blue",
                            width: 1/devicePixelRatio,
                        },

                        {
                            label: sensor4,
                            scale: "Left",
                            value: (u, v) => v == null ? "-" : v.toFixed(1) + " %",
                            stroke: "orange",
                            width: 1/devicePixelRatio,
                        },

            {
                            label: "Level",
                            scale: "Left",
                            value: (u, v) => v == null ? "-" : v.toFixed(1) + " ",
                            stroke: "crimson",
                            width: 1/devicePixelRatio,
                        },
                        {
                            label: "Switch Status",
                            scale: "Left",
                            value: (u, v) => v == null ? "-" : v.toFixed(1) + " V",
                            stroke: "deeppink",
                            width: 1/devicePixelRatio,
                        },
                        {
                            label: "Motion",
                            scale: "Right",
                            value: (u, v) => v == null ? "-" : v.toFixed(2) + " %",
                            stroke: "tomato",
                            width: 1/devicePixelRatio,
                        },
                        {
                            label: "Azimuth",
                            scale: "Left",
                            value: (u, v) => v == null ? "-" : v.toFixed(1) + " V",
                            stroke: "yellow",
                            width: 1/devicePixelRatio,
                        },
                        {
                            label: "Elevation",
                            scale: "Right",
                            value: (u, v) => v == null ? "-" : v.toFixed(2) + " %",
                            stroke: "magenta",
                            width: 1/devicePixelRatio,
                        },
                        {
                            label: "Soil Moisture",
                            scale: "Left",
                            value: (u, v) => v == null ? "-" : v.toFixed(1) + " V",
                            stroke: "indigo",
                            width: 1/devicePixelRatio,
                        },
                        {
                            label: "Wind Speed",
                            scale: "Right",
                            value: (u, v) => v == null ? "-" : v.toFixed(2) + " %",
                            stroke: "springgreen",
                            width: 1/devicePixelRatio,
                        },
                        {
                            label: "Wind Direction",
                            scale: "Left",
                            value: (u, v) => v == null ? "-" : v.toFixed(1) + " V",
                            stroke: "cyan",
                            width: 1/devicePixelRatio,
                        },
                        {
                            label: "Rainfall",
                            scale: "Right",
                            value: (u, v) => v == null ? "-" : v.toFixed(2) + " %",
                            stroke: "navy",
                            width: 1/devicePixelRatio,
                        },
                        {
                            label: "Air Quality",
                            scale: "Left",
                            value: (u, v) => v == null ? "-" : v.toFixed(1) + " V",
                            stroke: "brown",
                            width: 1/devicePixelRatio,
                        }

            ],

                    plugins: [

                    legendAsTooltipPlugin()

                ],

                    axes: [
                        {},

                        {
                            scale: "Left",
                            values: (u, vals, space) => vals.map(v => +v.toFixed(1) + " "),
                        },
                        {
                            side: 1,
                            scale: "Right",
                            size: 60,
                            values: (u, vals, space) => vals.map(v => +v.toFixed(2) + ""),
                            grid: {show: false},
                        },
                    ],
                };

                let uplot = new uPlot(opts, data, document.body);

                Promise.resolve().then(() => {

                    console.timeEnd("chart");
                });
            }

            fetch("data.json").then(r => r.json()).then(packed => {

                let data = prepData(packed);
                setTimeout(() => makeChart(data), 0);
            });
</script>

</body>
</html>   
happytm commented 2 years ago

Sorry I forgot to include data.json sample above.

[12,"Epoch","Location","Voltage","SSID","T1","S1","T2","S2","T3","S3","T4","S4", 1653449039,6,2.30,-48,1,73,2,54,3,252,4,68, 1653449040,16,2.30,-48,2,52,3,242,4,67,5,78, 1653449100,26,2.30,-48,3,241,4,50,5,1,6,0, 1653449160,36,2.30,-46,4,70,5,0,6,0,7,1, 1653449220,46,2.30,-47,5,1,6,1,7,0,8,77, 1653449280,56,2.30,-47,6,73,7,1,8,78,9,44, 1653449280,66,2.30,-48,7,0,8,79,9,45,10,60, 1653449340,76,2.30,-46,8,80,9,46,10,61,11,5, 1653449400,86,2.30,-47,9,47,10,62,11,7,12,23, 1653449400,96,2.30,-47,10,63,11,4,12,24,13,2, 1653449460,106,2.30,-45,11,6,12,23,13,2,14,49]

Thanks.