Uibuilder v2 Charting Examples

Hi all, I had a query today regarding how to integrate vue-chartjs into uibuilder. Unfortunately, it is rather poorly documented and I didn't manage to make it work without going through a build step.

However, that prompted me to build a couple of examples using alternate chart libraries and I've published these to the WIKI.

  1. Using ApexCharts - this seems like an easy to use library.

    Integrating ApexCharts with VueJS · TotallyInformation/node-red-contrib-uibuilder Wiki · GitHub

  2. Using Vue-ECharts - this seems like a much more complex library but possibly has more interactivity?

    Integrating Vue ECharts · TotallyInformation/node-red-contrib-uibuilder Wiki · GitHub

Both examples include a flow with the following features:

  • Data sent from Node-RED to the front-end dynamically updates the chart
  • The flow includes the html and javascript in comment nodes so that you can easily use the uibuilder editor to change the front-end code. Just copy and paste.
  • Demonstrates how easy it is to get a nice layout and typography with minimal boilerplate front-end code. Thanks for vue-bootstrap.
1 Like

Another example added to the WIKI.

This time using Vue-Chartkick which is a really nice library that lets you make use of any of Chart.js, Google Charts and Highcharts.

This example shows off several charts including Google geomap and timeline and a timeseries chart similar to the Node-RED Dashboard line chart that will keep adding entries every 10s until it reaches a 1000 points after which it drops old points when adding new ones.

And another. This time, a nice gauge using vue-svg-gauge.

image

I integrated ECharts in my last proper Vue app back in March. I’ll post sample code here later. It was a bit of a challenge to get it the way I wanted to, but I’m actually using it to plot time series of data. It was a 10 minute server polling of votes in a March Madness bracket poll as a proof of concept. Each round lasting a couple days, it would get the new amount of votes per bracket and total every 10 minutes and store those in a database, with this monitoring page in Vue to show how the brackets were doing.

ECharts comes with a few nice tricks for zooming the axes especially with large amounts of data. I made a component specifically for my charts, with vue-ECharts used internally.

You’ll only truly realise how powerful ECharts is after reading through the not-always-perfectly-translated documentation and api reference, then diving into the Chinese user gallery page. I used both Plotly and Bokeh for my html-plotting needs in the past, but have switched more or less fully to ECharts since March.

Yes, I had another look at echarts and it does look amazingly powerful. It has some great features - I will investigate more at some point.

Anyway, I've scratched an itch and produced some examples that hopefully people will find useful. I need to get some gardening done now :frowning:

I just realised how this is probably hard to read as this was my typescript experimenting project... So this is a Vue component file which is loaded by the components that put the charts with number of votes into the viewport. And a lot hadn't made it to the configuration yet so it's partially hard coded and not in possession of the best documentation yet...

<template>
  <v-chart :options="options" ref="votesChart"></v-chart>
</template>

<style>
  .echarts {
    width: 100%;
    height: 100%;
  }
</style>

<script lang="ts">
  import ECharts from 'vue-echarts';
  import {EChartOption} from 'echarts';
  import {Component, Prop, Watch, Vue} from 'vue-property-decorator';

  import 'echarts/lib/chart/line';

  @Component({
    components: {
      'v-chart': ECharts,
    },
  })
  export default class ChartVotes extends Vue {
    @Prop(String) title!: string;
    @Prop([Object, Array]) votes!: object[];
    @Prop(Array) items!: string[];
    @Prop({type: Boolean, default: false}) logScale!: boolean;

    private seriesVotes: object[] = [];
    private scaleType: string = 'value';

    protected options: EChartOption = {};

    public legend: object = {
      type: 'scroll',
      orient: 'vertical',
      width: '20%',
      top: '15%',
      height: '70%',
      right: 0,
    };

    public mounted() {
      // noinspection TypeScriptUnresolvedFunction
      this.$refs.votesChart.showLoading();
      this.seriesVotes = this.computedVotesData;

      this.init();
      // noinspection TypeScriptUnresolvedFunction
      this.$refs.votesChart.hideLoading();
    }

    private init() {
      this.options = {
        title: {
          text: this.title,
          left: 'center',
        },
        grid: [
          {
            left: '5%',
            width: '70%',
            right: '25%',
          }
        ],
        dataZoom: [
          {
            id: 'dataZoomX',
            type: 'slider',
            xAxisIndex: [0],
            filterMode: 'filter',
            start: 70,
            end: 100,
            minValueSpan: 6 * 10 * 60 * 1000,  // 60 minutes
          },
        ],
        xAxis: {
          type: 'time',
          gridIndex: 0,
          minInterval: 10 * 60 * 1000,  // 10 minutes aka polling time interval
        },
        yAxis: {
          type: this.scaleType,
          gridIndex: 0,
          name: 'Votes',
          nameLocation: 'end',
        },
        tooltip: {
          trigger: 'axis',
        },
        legend: this.legend,
        series: this.seriesVotes,
      }
    }

    get computedVotesData(): object[] {
      interface ISeriesData {
        type: string,
        symbol: string,
        name: string,
        data: (string | number)[][],
      }


      const seriesVotes = [];
      for (const [pairing, votesData] of Object.entries(this.votes)) {
        // Votes
        const seriesData: ISeriesData = {
          type: 'line',
          symbol: 'none',
          name: pairing,
          data: [],
        };

        for (const [timestamp, votes] of Object.entries(votesData)) {
          const tmp: (string | number)[] = [timestamp, votes];
          seriesData.data.push(tmp);
        }
        seriesVotes.push(seriesData);
      }
      return seriesVotes;
    }

    @Watch('logScale')
    public onLayoutChanged(val: boolean, oldVal: boolean) {
      if (this.logScale) {
        this.scaleType = 'log'
      }
      else {
        this.scaleType = 'value'
      }
    }

    @Watch('votes')
    public onVotesChanged(val: object[], oldVal: object[]) {
      this.seriesVotes = this.computedVotesData;
      this.options.series = this.computedVotesData;

      // noinspection TypeScriptUnresolvedFunction
      this.$refs.votesChart.refresh();
    }

    @Watch('title')
    public onTitleChanged(val: string, oldVal: string) {
      // noinspection TypeScriptUnresolvedFunction
      this.options.title!.text = val;

      // noinspection TypeScriptUnresolvedFunction
      this.$refs.votesChart.refresh();
    }
  }
</script>

I'll write a proper example of this setup at a later time, when I can actually keep my head upright for more than five minutes

2 Likes

Hope it's ok to revive this topic. I am working with your examples at :https://github.com/TotallyInformation/node-red-contrib-uibuilder/wiki/Integrating-ApexCharts-with-VueJS
I don't have any javascript chops so this has been trial and error. My goal is to create a line chart - time on the x and temperature on the y axis. I am able to put the data pairs directly into the index.js and it looks good. I need some direction, however, on how to load this data into Uibuilder from an inject node for starters with then I can load the data from a file once it's clear how it works. Here is what I have so far:

index.html

<!doctype html><html lang="en"><head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, minimum-scale=1, initial-scale=1, user-scalable=yes">

    <title>ApexChart/VueJS Example - Node-RED UI Builder</title>
    <meta name="description" content="Node-RED UI Builder - ApexChart/VueJS Example">

    <link rel="icon" href="./images/node-blue.ico">

    <link type="text/css" rel="stylesheet" href="../uibuilder/vendor/bootstrap/dist/css/bootstrap.min.css" />
    <link type="text/css" rel="stylesheet" href="../uibuilder/vendor/bootstrap-vue/dist/bootstrap-vue.css" />

    <link rel="stylesheet" href="./index.css" media="all">
</head><body>
    <div id="app">
        <b-container id="app_container">



                <b-card col class="w-80" header="Line Chart" border-variant="primary" header-bg-variant="primary" header-text-variant="white" align="center">
                        <apexchart width="100%" type="line" :options="options" :series="series"></apexchart>
                </b-card>



        </b-container>
    </div>

    <script src="../uibuilder/vendor/socket.io/socket.io.js"></script>
    <script src="../uibuilder/vendor/vue/dist/vue.min.js"></script>
    <script src="../uibuilder/vendor/bootstrap-vue/dist/bootstrap-vue.js"></script>

    <!-- Loading from CDN, use uibuilder to install packages and change to vendor links -->
    <script src="https://cdn.jsdelivr.net/npm/apexcharts"></script>
    <script src="https://cdn.jsdelivr.net/npm/vue-apexcharts"></script>

    <script src="./uibuilderfe.min.js"></script> <!--    //prod version -->
    <script src="./index.js"></script>

</body></html>

index.js


'use strict'

/** @see https://github.com/TotallyInformation/node-red-contrib-uibuilder/wiki/Front-End-Library---available-properties-and-methods */

/** Reference the apexchart component (removes need for a build step) */
Vue.component('apexchart', VueApexCharts)

// eslint-disable-next-line no-unused-vars
var app1 = new Vue({
    el: '#app',
    data: {
        startMsg    : 'Vue has started, waiting for messages',

        // Data for bar chart
        options: {
            chart: {
                id: 'vuechart-example'
            },

            //xaxis: {
            //    categories: [1991, 1992, 1993, 1994, 1995, 1996, 1997, 1998]
            //}
            xaxis: {
            type: 'datetime'
            }

        },
        series: [{
            name: 'series-1',
            //data: [30, 40, 45, 50, 49, 60, 70, 91]
            data: [[1582672682720,3.3],[1582672743197,3.4],[1582672802656,3.4],[1582672863262,3.5],[1582672922642,3.6],[1582672983224,3.6],[1582673042637,3.6],[1582673103127,3.6],[1582674603470,3.4],[1582674662717,3.3],[1582674723201,3.2],[1582674782700,3.1],[1582694883073,-3.9],[1582698482591,-4],[1582705322673,-4.1],[1582708923245,-0.6],[1582712522456,-3.2],[1582716123025,-2.4],[1582719722594,-3.6],[1582723322980,-4.5],[1582726923580,-3.4],[1582751943244,10.8],[1582773543768,-2.4],[1582795142888,-1.6],[1582816743323,-5.2],[1582838342726,9.6],[1582860063475,2.4],[1582881662632,-2.4],[1582903262894,-1.9],[1582924983168,11.9],[1582946583208,0.8],[1582968183465,-2.8],[1582989782780,-1.6],[1583011383481,0.7],[1583119382606,-1.8],[1583140983079,1.3],[1583162583504,2.1],[1583184183360,4.7],[1583205783112,-2],[1583227383258,-0.8],[1583248982931,-0.8],[1583270582910,5.4],[1583292182760,0.6],[1583313783483,-1.8],[1583335382559,-1.4],[1583356982766,3.1],[1583378583180,-3.1],[1583400182937,-10.3],[1583421783210,-5],[1583443383273,9.2],[1583464982974,-4.9],[1583486582796,-6.7],[1583508183368,-3.6],[1583529783257,-1.6],[1583551382677,-9.5],[1583572982935,-13.9],[1583594583061,-14.4],[1583616183303,-8.6],[1583637783453,-13.4],[1583659382530,-16.4],[1583680982771,-16.1],[1583702583576,-1.9],[1583724183393,-13.2],[1583745783507,-15.8],[1583767383297,-10.5],[1583790362859,2],[1583811963131,-8.5],[1583833563017,-12.7],[1583855163075,-4.4],[1583876762680,4.9]]

        }],

        // Data for donut chart
        //doptions: {},
        //dseries: [44, 55, 41, 17, 15]

    }, // --- End of data --- //
    computed: {
    }, // --- End of computed --- //
    methods: {
    }, // --- End of methods --- //

    // Available hooks: init,mounted,updated,destroyed
    mounted: function(){
        /** **REQUIRED** Start uibuilder comms with Node-RED @since v2.0.0-dev3
         * Pass the namespace and ioPath variables if hosting page is not in the instance root folder
         * e.g. If you get continual `uibuilderfe:ioSetup: SOCKET CONNECT ERROR` error messages.
         * e.g. uibuilder.start('/nr/uib', '/nr/uibuilder/vendor/socket.io') // change to use your paths/names
         */
        uibuilder.start()

        var vueApp = this

        // Process new messages from Node-RED
        uibuilder.onChange('msg', function (newVal) {
            if ( typeof newVal.payload === 'number' ){
                // Add new element
                vueApp.series.push(newVal.payload)
                // Lose the first element
                vueApp.series.shift()
                //console.log(vueApp.dseries)
            }
        })

    } // --- End of mounted hook --- //

}) // --- End of app1 --- //

// EOF

The way you have it set up at the moment, you can only push a new complete series to the chart from Node-RED and when you do, you remove the previous series so you only keep 1 series ever - is that what you want?

If so, at first sight, that looks fine. You can take the vueApp.series data and paste it into the inject node as JSON. You will have to remove the comment and you have to use double quotes. This is what you will end up with:

[{
    name: "series-1",
    data: [[1582672682720,3.3],[1582672743197,3.4],[1582672802656,3.4],[1582672863262,3.5],[1582672922642,3.6],[1582672983224,3.6],[1582673042637,3.6],[1582673103127,3.6],[1582674603470,3.4],[1582674662717,3.3],[1582674723201,3.2],[1582674782700,3.1],[1582694883073,-3.9],[1582698482591,-4],[1582705322673,-4.1],[1582708923245,-0.6],[1582712522456,-3.2],[1582716123025,-2.4],[1582719722594,-3.6],[1582723322980,-4.5],[1582726923580,-3.4],[1582751943244,10.8],[1582773543768,-2.4],[1582795142888,-1.6],[1582816743323,-5.2],[1582838342726,9.6],[1582860063475,2.4],[1582881662632,-2.4],[1582903262894,-1.9],[1582924983168,11.9],[1582946583208,0.8],[1582968183465,-2.8],[1582989782780,-1.6],[1583011383481,0.7],[1583119382606,-1.8],[1583140983079,1.3],[1583162583504,2.1],[1583184183360,4.7],[1583205783112,-2],[1583227383258,-0.8],[1583248982931,-0.8],[1583270582910,5.4],[1583292182760,0.6],[1583313783483,-1.8],[1583335382559,-1.4],[1583356982766,3.1],[1583378583180,-3.1],[1583400182937,-10.3],[1583421783210,-5],[1583443383273,9.2],[1583464982974,-4.9],[1583486582796,-6.7],[1583508183368,-3.6],[1583529783257,-1.6],[1583551382677,-9.5],[1583572982935,-13.9],[1583594583061,-14.4],[1583616183303,-8.6],[1583637783453,-13.4],[1583659382530,-16.4],[1583680982771,-16.1],[1583702583576,-1.9],[1583724183393,-13.2],[1583745783507,-15.8],[1583767383297,-10.5],[1583790362859,2],[1583811963131,-8.5],[1583833563017,-12.7],[1583855163075,-4.4],[1583876762680,4.9]]
}]

I need the charts zoom feature so I'm not sure if that can be done with dynamic updating. Mainly I need to view and zoom historical data but, if the chart can be loaded with historical data and then dynamically update with with newer data - wonderful. It's probably best for me to crawl before running :slightly_smiling_face:

Not sure about the zoom, it may reset itself when it receives new data, not sure.

But either way, it is also easy enough to adjust the data you send so that you can add new data points to the current series.

Or indeed, by lets say using the msg.topic, you could easily allow uibuilder to update either the series or add to the data points.

If you want data to be available when you first connect the client, you will need to use a caching node. There is an example or 2 on the WIK of how to do that with using a function node. There is also an example in the flows site and some additional examples here in the Forum.

Just note that when a client (browser tab) first connects. 2 or 3 messages are sent out of the control port (#2 output on your uibuilder node). Those messages have a property that tells you what they mean and a "from" property that tells you whether it is the client or the node that is sending the msg. Attach a debug node and you will easily see the msgs and their data (set the debug to show the whole msg not just the payload). So you can feed one of those messages back to your function node (actually just send them all and filter them in the function is the easiest way). Your function node will also sit in front of uibuilder and record any incoming msgs to a context/flow/global variable. When you detect a control msg, you then send out a new msg with all of the recorded data so that a new client connection gets that data and can show a chart straight away rather than having to wait for new data to come along.

Hope that gives you some ideas. Give it a try and if you get stuck, feel free to create a new thread in the forum.

Nice to see someone dipping their toes in and using Node-RED and uibuilder to help learn data driven web design :grinning:

1 Like

Hi!,
First of all thanks so much for all of work on this as the uibuilder has really opened node red to a hugely extensive and flexible dashboard/graphing options !
I've been playing around with it for couple days and most of things works ok - the only thing i'm struggling with is how to store a multiple data set/data points in memory so i can inject them into the uibuilder once page loads for line charts.
My use case is simple - store last 24 hours of data and graph it on webpage.
Do you have any pointers on how to store a time-series under one variable in node red without any databse so i can inject it into the uibuilder node for graphing ? I'm ok with loosing the data after reboot, just don`t wan to write data into SD card.

Thanks for your kind words. Glad it is being useful.

Most if not all of the charting libraries use array's for their input data. So all you need to do is pre-define an empty array in your vue app's data section (so that it is responsive to data changes) and then push new entries to it. Push will only add a single new entry, if you are sending a batch of entries, you will need to do some searching to find the most efficient JavaScript approach.

The only other thing you might want to do is to limit the number of entries in the array because if you let it get too big, you will eventually crash the browser tab by running out of memory. There is no point in keeping more horizontal data points that there will be pixels in the display. JavaScript has a function to pop an array entry off the front of an array so that your chart will continue to shift itself right-wards and keep showing the latest data. Just use that as new data comes in and once the count of entries exceeds the required number of points. JavaScript has quite a few array specific functions and you should become familiar with them in order to best handle your data.

If you need to keep the start of the horizontal axis at a fixed point, this is a bit more complex because you will need a method to start dropping intermediate entries. That is more of a math exercise and depends on the data and what you are trying to convey using the chart.

Remember that all of that chart data is in the browser and not in Node-RED. So if you reload the page, open a new tab to the page or connect a new browser window, there wont be any data. If that is an issue, you can use one of the caching examples to cache the data in Node-RED as well as the front-end. uibuilder issues control messages when a page is (re)loaded and you can use that to trigger a cache replay.

Alternatively, you could use local browser storage to keep the data in the front-end and write some front-end code to reload the data from the storage at (re)load.

Thanks !
I was able to store the cached data via node-red cache node. The thing is it cashes only one value per topic so it will by default store only the latest data from the sensor. I got aorund it by using timestamp as a topic what then gives me a json of a timeseries like this:
{"1585404029129":305.83,"1585404034130":304.17,"1585404039131":301.67,"1585404044132":300.83,"1585404049132":304.17,"1585404054134":302.5}
The only thing i'm struggling now is how to format this and push into the array for charts.
The JSON node is not converting this into anything and just pases as json even if i set it to convert into JS object.
In general all mechanisms of caching etc are well documented when it comes to s single value, but most of them don`t have a good example of locally storing cached time series and then pushing this into graphs via node red :frowning:

Yes, I'm aware of that for uibuilder and I was trying to build something better but every time I try, I begin to find more edge cases that need to be handled. Who knew that caching data was so complex :sunglasses:

Because the chart libraries want data in array format, it is best to build an array for your cache rather than an object. Objects are preferred where you need to reference a specific entry.

So, just as I suggested processing single incoming data in the front-end, you can do that in your caching function node as well. with each new msg doing a push into the cache array. Again, you will want to limit the max size of the cache though.

Then, on a cache replay event, you can either loop through the cache array and send out a series of msgs, or send out the whole array and process it in the front-end.

Thanks so much for all your replies so far !
I'll play around with it some more base don your suggestions. My JS knowledge as you are probably already aware is very limited hence it's a learning curve for me :wink:
Once i get this to work i`ll report a full working example.
Thanks again and have a great weekend !

1 Like

So i was able to generate a line chart with dynamic data via apexchart and vue but there is one thing that bothers me - i'm sure it's but i just don`t know JS enough.

Everytime i update the series in line chart the graph is not getting re-renderd/updated. The series values themselves are getting updated as i also log them to console and can see that data goes there and bseries[0].data have the new data in them but graph just stands still.

I read that this is because in Vue nested arrays are not reactive and series data in apexcharts are exactly this - a nested array inside main vue data definition:

   bseries: [{
        name: 'series',
        data: []
    }],

This makes this thing dificult because all other graphs ( bar, donut etc ) in apex at least have the series data one layer above what makes them reactive by default but not this.
A VERY VERY ugly workaround i have for this is to send a window.resize event every data update inside mount so it triggers a built-in apex redraw function making chart to update - but this is VERY bad

            // Add new element
            vueApp.bseries[0].data.push( new Array( (new Date(newVal.payload[0])), newVal.payload[1] ) )
            window.dispatchEvent(new Event('resize'));

I've tried different things ( like vue.set based on https://vuejs.org/v2/guide/reactivity.html) but whatever i do the graph doesn`t update without this ugly thing.

Do you have an example set of data to save me having to build one?

The clue was in the link that you've given. The trick is this (I'm replacing the whole series here):

Vue.set(vueApp.series, 0, {'data': msg.payload})

The reason for this is that it is the series data property that is an array first, so you have to make a reactive change to that and not to the inner series[0].data.

Here is a complete example:

[{"id":"496ec69.fc6d138","type":"debug","z":"ef122f0d.d488b","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","x":530,"y":2300,"wires":[]},{"id":"d2b25ddf.9b4ed","type":"debug","z":"ef122f0d.d488b","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","x":510,"y":2340,"wires":[]},{"id":"1a697799.80f098","type":"uibuilder","z":"ef122f0d.d488b","name":"","topic":"","url":"apexchart","fwdInMessages":false,"allowScripts":false,"allowStyles":false,"copyIndex":true,"showfolder":false,"x":380,"y":2320,"wires":[["496ec69.fc6d138"],["d2b25ddf.9b4ed"]]},{"id":"32616b77.cc0834","type":"inject","z":"ef122f0d.d488b","name":"repeat every 10s","topic":"","payload":"[{\"x\":\"2020-02-12\",\"y\":76},{\"x\":\"2020-02-13\",\"y\":57},{\"x\":\"2020-02-14\",\"y\":64}]","payloadType":"json","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":160,"y":2320,"wires":[["1a697799.80f098"]]}]

html

<!doctype html><html lang="en"><head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, minimum-scale=1, initial-scale=1, user-scalable=yes">

    <title>ApexChart/VueJS Example - Node-RED UI Builder</title>
    <meta name="description" content="Node-RED UI Builder - ApexChart/VueJS Example">

    <link rel="icon" href="./images/node-blue.ico">

    <link type="text/css" rel="stylesheet" href="../uibuilder/vendor/bootstrap/dist/css/bootstrap.min.css" />
    <link type="text/css" rel="stylesheet" href="../uibuilder/vendor/bootstrap-vue/dist/bootstrap-vue.css" />

    <link rel="stylesheet" href="./index.css" media="all">
</head><body>
    <div id="app">
        <b-container id="app_container">

            <h1>
                Examples of using <a href="https://apexcharts.com/docs/vue-charts/" target="_blank">ApexChart</a>
                with uibuilder, VueJS and bootstrap-vue
            </h1>

            <b-row>
                <b-card col class="w-50" header="Bar Chart" border-variant="primary" header-bg-variant="primary" header-text-variant="white" align="center">
                    <apexchart width="100%" type="line" :options="options" :series="series"></apexchart>
                </b-card>
            </b-row>

        </b-container>
    </div>

    <script src="../uibuilder/vendor/socket.io/socket.io.js"></script>
    <script src="../uibuilder/vendor/vue/dist/vue.min.js"></script>
    <script src="../uibuilder/vendor/bootstrap-vue/dist/bootstrap-vue.js"></script>

    <!-- Loading from CDN, use uibuilder to install packages and change to vendor links -->
    <script src="https://cdn.jsdelivr.net/npm/apexcharts"></script>
    <script src="https://cdn.jsdelivr.net/npm/vue-apexcharts"></script>

    <script src="./uibuilderfe.min.js"></script> <!--    //prod version -->
    <script src="./index.js"></script>

</body></html>

javascript

/* jshint browser: true, esversion: 5 */
/* globals document,Vue,window,uibuilder,VueApexCharts */
'use strict'

/** Reference the apexchart component (removes need for a build step) */
Vue.component('apexchart', VueApexCharts)

// eslint-disable-next-line no-unused-vars
var app1 = new Vue({
    el: '#app',
    data: {
        startMsg    : 'Vue has started, waiting for messages',

        options: {
            chart: {
                id: 'vuechart-example'
            },
    
            xaxis: {
                type: 'datetime'
            }
        },
        
        series: [{
            data: [],
        }], 

    }, // --- End of data --- //
    computed: {
    }, // --- End of computed --- //
    methods: {
    }, // --- End of methods --- //

    mounted: function(){
        uibuilder.start()
        var vueApp = this

        uibuilder.onChange('msg', function (msg) {
            Vue.set(vueApp.series, 0, {'data': msg.payload})
            //vueApp.series[0].data = msg.payload
            console.log('SERIES: ', vueApp.series)
        })

    } // --- End of mounted hook --- //

}) // --- End of app1 --- //

// EOF

And because I was bored, here is an extended example that lets you send replacement/new series and/or add to existing series. :sunglasses:

[{"id":"496ec69.fc6d138","type":"debug","z":"ef122f0d.d488b","name":"","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","x":630,"y":2340,"wires":[]},{"id":"d2b25ddf.9b4ed","type":"debug","z":"ef122f0d.d488b","name":"","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","x":610,"y":2380,"wires":[]},{"id":"1a697799.80f098","type":"uibuilder","z":"ef122f0d.d488b","name":"","topic":"","url":"apexchart","fwdInMessages":false,"allowScripts":false,"allowStyles":false,"copyIndex":true,"showfolder":false,"x":460,"y":2360,"wires":[["496ec69.fc6d138"],["d2b25ddf.9b4ed"]]},{"id":"32616b77.cc0834","type":"inject","z":"ef122f0d.d488b","name":"Series 0 (whole set)","topic":"series/0","payload":"[{\"x\":\"2020-02-12\",\"y\":76},{\"x\":\"2020-02-13\",\"y\":57},{\"x\":\"2020-02-14\",\"y\":64}]","payloadType":"json","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":170,"y":2320,"wires":[["1a697799.80f098"]]},{"id":"23300c2f.0646d4","type":"inject","z":"ef122f0d.d488b","name":"Series 1 (whole set)","topic":"series/1","payload":"[{\"x\":\"2020-02-12\",\"y\":176},{\"x\":\"2020-02-13\",\"y\":157},{\"x\":\"2020-02-14\",\"y\":164}]","payloadType":"json","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":170,"y":2360,"wires":[["1a697799.80f098"]]},{"id":"36fb9c2.da48264","type":"inject","z":"ef122f0d.d488b","name":"Series 1 (add new entry)","topic":"entry/1","payload":"{\"x\":\"2020-02-20\",\"y\":134}","payloadType":"json","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":190,"y":2460,"wires":[["1a697799.80f098"]]},{"id":"104d6cbb.926473","type":"inject","z":"ef122f0d.d488b","name":"Series 0 (add new entry)","topic":"entry/0","payload":"{\"x\":\"2020-02-20\",\"y\":34}","payloadType":"json","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":190,"y":2420,"wires":[["1a697799.80f098"]]}]

The html is the same. So is the JavaScript except for the onchange function which you replace with:

            
            // Lets allow for several differnt types of data input
            let seriesNum = 0, fullSeries = true
            if (msg.topic) {
                let splitTopic = msg.topic.split('/')
                if (splitTopic.length === 2) {
                    seriesNum = splitTopic[1]
                    if (splitTopic[0].toLowerCase() === 'series') {
                        // If topic is like "series/0" then replace (or add) the whole series
                        fullSeries = true
                    } else {
                        // otherwise, if it like 'entry/0', we will add a new entry to the give series index
                        fullSeries = false
                    }
                } else {
                    // otherwise, we will add a new entry to series 0
                    fullSeries = false
                }
            } else {
                // otherwise, we will add a new entry to series 0
                fullSeries = false
            }
            
            // Because JS isn't good at detecting reactive changes to arrays and nested objects
            // we have to use Vue.set here so that Vue knows that the data has changed.
            if (fullSeries) {
                // replace the whole series[x] data array
                Vue.set(vueApp.series, seriesNum, {'data': msg.payload})
            } else {
                // Add a new entry to the end of the series[x] data array - have to update the whole thing because of reactivity
                let d = vueApp.series[seriesNum].data // copy the current data array for the series
                //console.log('SERIES: ', seriesNum, vueApp.series[seriesNum], msg.payload)
                let newLen = d.push(msg.payload) // add the new entry to the end of the array
                //console.log('DATA:Updated: ', seriesNum, d)
                Vue.set(vueApp.series, seriesNum, {'data': d}) // put the updated array into place
            }
            
            //console.log('SERIES: ', vueApp.series)
            

Have fun.

Aaagghhh - i knew i messed something up with syntax of vie.set ..
I also used simillar logic as in your second example in order to log past data into node red memory persistence and i push the entire table into the front end once user connects.
This as dicussed above gives me ability to store the past data to some extent in memory and push it into the user without wearing an SD card.
Now onto the buttons and control messages ! :slight_smile:
THANK YOU ! Your rock !

1 Like