Dynamically create charts / front end elements with Ui-Builder based on size of array?

Hi guys,

so here's a "requirement" I would like to implement with UI-Builder: Imagine in one of your flows you've prepared an array like below and you wanna display that in the frontend. The problem here is that the size of the array may vary dynamically in the flow where data is assembled:

series [200]
 0: object
     name: chartForSeries1
     data: array[4711]   // contains timeseries data
 1: object
     name: chartForSeries2
     data: array[4711]   // contains timeseries data
 ...
 199: object
     name: chartForSeries200
     data: array[4711]   // contains timeseries data

Now with UI-Builder in src/index.js we usually define options and a series arrays like this:

var app1 = new Vue({
    el: '#app',
    data: {
        optionsForSeries1: { chart: { id: 'series1', height: 300, type: 'bar' }, /* other chart options... */ },
        timeseries0: [ /* will be updated dynamically */ ]
       ...
        optionsForSeries200: { chart: { id: 'series200', height: 300, type: 'bar' }, /* other chart options... */ },
        timeseries200: [ /* will be updated dynamically */ ]

You can already see that this is seriously tedious much work (and I have more than just 200 charts).
And thinking of the update mechanisms, well that should be easier to dynamically fill..

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

        // Process new messages from Node-RED
        uibuilder.onChange('msg', function (newMsg) {
            if (newMsg.update === "updateAllSeries") {
                vueApp.timeseries1 = newMsg.series[0];
                ...
                vueApp.timeseries200 = newMsg.series[199];
            }

Via update we fill those arrays which are then rendered in (e.g.) predefined Apex charts that are wrapped in nice looking bootstrap-vue elements (e.g. b-card) in the index.html:

<!doctype html>
...
<head>...</head>
<body>
    <div id="app">
        <b-container id="app_container">
<b-row>
   <b-card col class="w-100"><apexchart width="95%" :options="optionsForSeries1" :series="timeseries1"></apexchart></b-card>
</b-row>
...
<b-row>
   <b-card col class="w-100"><apexchart width="95%" :options="optionsForSeries200" :series="timeseries200"></apexchart></b-card>
</b-row>

</b-container>
</div>
</body></html>

If I have an unknown amount of series can you think of a way to create the number of frontend elements dynamically?

Best regards from Germany,
Marcel

Hi @CreativeWarlock ..

surely there is a way to make the creation of the charts dynamic
possibly using vue's v-for loop around your apexchart component.

but i wanted to ask .. 200 charts on the same page ? its a bit heavy :wink:
.. i would create a dropdown menu with bootstrap vue's Form select component and request data only for the selected chart.

can you share some sample data to play around with ?
no need for all 200 since each has 4711 datapoints :wink:
5-6 series would be enough

1 Like

Hi mate,

there will be many small charts (minimum ~60), but to demonstrate the difficulty with scaling I just threw in a high number of 200.

I like your idea with vue's v-for loop and the request of data via a form select, that gives me a very good starting point to further research and try out stuff! :slight_smile:

About the data:
Sure, for playing around I attached a small example array below.

The number of resulting arrays is coming from a cross product of sets (setA x setB) and with setB even comprising of subsets.

  • setA = ['any', 'none', 'either',..] -> this came from supervisors who approve / don't approve data)
  • setB = ['ETF', 'INDEX', 'STOCK', ...]
  • timeseries values are found in property 'changeAtDay5'.
  • Property sector = ['SECTOR_FINANCIAL', 'SECTOR_HEALTHCARE', ... ] may work as a later filter for displaying charts of a certain sector only.

Edit2: Uploaded json-Data here: Index of /uploads

you can try to save the data in a .json or .txt file and try to upload with the Upload icon image
but i dont know if there are any limitations for that option

I've uploaded the data on my webserver: Index of /uploads

1 Like

so the structure of the data is this

you have 4 array elements in series which later will be the 4 charts
each element is an object that has nested data array
the y value of the timeseries chart will be changeAtDay5 ?
what about x-axis value ?
shouldnt we have in that object the datetime when the value was changed in order to plot x-y ?

did you have a working example for at least one chart ?

1 Like

Obbala! I forgot to query the timestamp :joy:

So it would be another property then:
"timestampOfDay5"

You might notice the complexity of data .. we may even pick 'price change' and timestamp at any variable point in time (after a trade signal occurs). The basic idea behind this is to visualize the quality of price changes after e.g. X=5,10,15,... days

I'm currently querying all combinations of approval modes with asset classes (should be around 56 or so data arrays). It's still running. Once that's done (<1 hour I hope), I'll prepare the JSON-Array and upload it.

did you have a working example for at least one chart ?
I have many charts running on my dashboard. Since I found it tedious to hardwire each chart series/options, html section and whatnot, I was wondering about letting this the machine do dynamically. I'm right now working with the templates and try with the for-loop. This looks very promising! :sunglasses:

cool ..

there is also this video tutorial for v-for that covers the basics.

the tricky part will be restructuring the data with javascript to send it to the correct format for the apexchart
and dynamically passing the :options and :series prop data for it in the v-for loop.

1 Like

I have updated the file in case you wanna play around with the data (36 arrays "only" as not every asset class doesn't have data yet): Index of /uploads

(timestamps are now included :wink: )

For some reason it wouldn't wanna render this example code :thinking:

index.js

Vue.component('apexchart', VueApexCharts)
Vue.component('chart-item', {
    props: ['chart'],
     // can I split 'thisVeryLongString into 'this' + 'Very' + 'Long' + 'String' ?
    template:   '<b-row><b-card title="{{chart.title}}" col class="w-100" header="" border-variant="default" header-bg-variant="default" header-text-variant="white" align="center"><apexchart width="95%" type="bar" :options="{{chart.options}}" :series="{{chart.series}}"></apexchart></b-card></b-row>'
})
Vue.component('todo-item', { // works
    props: ['todo'],
    template: '<li>{{ todo.text }}</li>'
  })

var app1 = new Vue({
    el: '#app',
    data: {
        groceryList: [ // renders fine in frontend
            { id: 0, text: 'Vegetables' },
            { id: 1, text: 'Cheese' },
            { id: 2, text: 'Whatever else humans are supposed to eat' }
        ],

        chartsList: [ // not rendered in frontend! 
             //just exemplary data. Will later be updated via onChange mechanism
            { id: 0,
                title: 'Approval: Any - Asset class: Index',
                options: 'Hello', /* { chart: { id: 'chartOptions-PCAND-Any-INDEX' } }, */
                series: 'Yeah!' /* [{  name: 'chartSeries-PCAND-Any-INDEX', data: [] }] */
            },
            { id: 1,
                title: 'Approval: Any - Asset class: ETF',
                options: 'Hello', /* { chart: { id: 'chartOptions-PCAND-Any-ETF' } }, */
                series: 'Yeah' /* [{  name: 'chartSeries-PCAND-Any-ETF', data: [] }] */
            },
            { id: 2,
                title: 'Approval: Any - Asset class: Stock',
                options: 'Hello', /* { chart: { id: 'chartOptions-PCAND-Any-STOCK' } }, */
                series: 'Yeah' /* [{  name: 'chartSeries-PCAND-Any-STOCK', data: [] }] */
            },
        ],

index.html:

<div id="app">
    <b-container id="app_container">
        <ol>
            <todo-item
                  v-for="item in groceryList"
                  v-bind:todo="item"
                  v-bind:key="item.id"
                ></todo-item>
            </ol>
            <ol>
                <chart-item
                    v-for="item in chartsList"
                    v-bind:chart="item"
                    v-bind:key="item.id"></chart-item>
            </ol>
  ...
        </b-container>
    </div>

I don't see any errors in the console nor in the Node Red backend. Any ideas what I'm doing wrong here?

Dunno .. i was trying to get an example working with the apexcharts

index.html

<body>
      <div id="app">
        <b-container id="app_container">
          <b-row v-for="(chart,index) in chartData">
            <b-card col class="w-75 mt-3"><apexchart width="95%" :options="chart.chartOptions" :series="chart.chartSeries"></apexchart></b-card>
          </b-row>
        </b-container>
      </div>
    </body>

index.js

/* jshint browser: true, esversion: 5, asi: true */
/*globals Vue, uibuilder */
// @ts-nocheck
/*
  Copyright (c) 2021 Julian Knight (Totally Information)

  Licensed under the Apache License, Version 2.0 (the "License");
  you may not use this file except in compliance with the License.
  You may obtain a copy of the License at

  http://www.apache.org/licenses/LICENSE-2.0

  Unless required by applicable law or agreed to in writing, software
  distributed under the License is distributed on an "AS IS" BASIS,
  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  See the License for the specific language governing permissions and
  limitations under the License.
*/
"use strict";

/** @see https://totallyinformation.github.io/node-red-contrib-uibuilder/#/front-end-library */

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

var app1 = new Vue({
  el: "#app",
  data: {
    // Data for apex charts
    chartData: [
      // chart1 data
      {
        chartOptions: {
          chart: {
            id: "vuechart-example1",
          },
          xaxis: {
            type: "datetime",
          },
        },
        chartSeries: [
          {
            name: "series-1",
            data: [
              [1324508400000, 34],
              [1324594800000, 54],
              [1326236400000, 43],
            ],
          },
        ],
      },

      // chart2 data
      {
        chartOptions: {
          chart: {
            id: "vuechart-example2",
          },
          xaxis: {
            type: "datetime",
          },
        },
        chartSeries: [
          {
            name: "series-1",
            data: [
              [1224508400000, 24],
              [1224594800000, 31],
              [1226236400000, 53],
            ],
          },
        ],
      },
    ],
  }, // --- 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(msg) {
      if (msg) {
       console.log(msg)
      }
    });
  }, // --- End of mounted hook --- //
}); // --- End of app1 --- //

// EOF

now we need to find a way to restucture your real data to the same format as the above example

Filling the data to the bar chart is not difficult. Did this many times. Probably the best approach is I prepare it properly when the data is queried from MongoDB so that the final series object (or one of its lower property keys is ready to pass along its value (containing timestamps and values).

Usually I work with ultra small examples and try to get them to work first before I work with my data :slight_smile:


Just figured out why that Vue.component('chart-item', ...) does not render as shown in my above post.
When I change the working 'todo-item' component to simply display the element like this:

Vue.component('todo-item', {
    props: ['todo'],
    /* template: '<li>{{ todo.text }}</li>' */
    template:   '<b-row><b-card title="{{todo.text}}" col class="w-100" header="" border-variant="default" header-bg-variant="default" header-text-variant="white" align="center">{{todo.text}}</b-card></b-row>'
  })

the frontend will render the groceryList as follows:

So it now becomes clear that it is not possible to assign the text (be it veggies or my series/options 'labels') to that title via title ="{{todo.text}}"

The same assumption will probably be true for setting the options and series property of the apex-chart element:
<apexchart height="500" :options="{{todo.text}}" :series="{{todo.text}}"></apexchart>

So this is where my problem resides :slight_smile: Will have to learn more from bootstrap-Vue it seems.

Have a nice evening, mate

Cheers,
Marcel

you are right .. lose the double curly brackets and try v-bind:title="todo.text"
or :title ="todo.text" ( : is shorthand for v-bind)

i demonstrated above a working example how to pass the props dynamically :wink:
but ok .. no pain no gain

2 Likes

Oha, I totally missed that! Typical when you're new to something it's easy to miss important details - doh!

Thanks for your help, mate! I'll implement it tomorrow and look forward to that dynamical scaling!

Cheers,
Marcel

hello .. i worked a little bit on the restructuring of the data.
it needs a lot of processing and i dont know how easy it is to do it straight from Mongodb queries.
if you are interested to see the javascript ...

Test Flow:
flows.json (65.0 KB)

Some problem i noticed was that not all objects had data so those had to filtered

Added vue v-if v-else conditional rendering of a warning msg
when no data has been received from Node-red

<body>
      <div id="app">
        <b-container id="app_container" fluid class="ml-5">
          <h1 class="mt-5">Vue-apexcharts in Uibuilder</h1>
          <b-row v-if="chartData.length > 0" >
            <b-card col class="mt-3 m-3" style="width: 40%;" v-for="(chart,index) in chartData" :key="index"
              ><apexchart width="95%" :options="chart.chartOptions" :series="chart.chartSeries"></apexchart
            ></b-card>
          </b-row>
          <b-row v-else class="ml-2 mt-5"><b-alert show variant="warning" class="w-75">No data received from NR yet</b-alert></b-row>
        </b-container>
      </div>
    </body>

index.js

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

var app1 = new Vue({
  el: "#app",
  data: {
    // Data for apex charts
    chartData: [],
  }, // --- End of data --- //
  computed: {}, // --- End of computed --- //
  methods: {}, // --- End of methods --- //

  // Available hooks: init,mounted,updated,destroyed
  mounted: function() {
    uibuilder.start();

    var vueApp = this;

    // Process new messages from Node-RED
    uibuilder.onChange("msg", function(msg) {
      if (msg) {
        console.table(msg.payload[0].chartSeries[0].data);
        console.log(msg.payload);
        vueApp.chartData = msg.payload;
      }
    });
  }, // --- End of mounted hook --- //
}); // --- End of app1 --- //

// EOF

2 Likes

Wow, this is very kind of you! Big Thanks, mate!

Haven't started with the implementation yet, but will do so after lunch and then check out your flow and start with the implementation.

Yes, not all combinations (approval x asset class) had data, so that had to be checked at some point,

Best regards,
Marcel

P.S.: The sorting of timestamps as it is now appears random because the whole set of data was generated via this set of "for each" loops (approval mode -> asset class -> symbols -> signals (with price changes and timestamps).

yes .. i noticed that also .. i used the sort javascript method to handle that .. i think it was affecting the chart when it was set as a line chart.

1 Like

Yeah it would be rendered in a mess. :slight_smile: Next to the scatter chart I'm pretty sure the bar chart works fine, too.


But what about the template mechanism?
I tested the template without curly brackets and the title is still not being rendered. It seems I lack an important puzzle in this mechanism of Vue.. could you point me to what I'm doing wrong?

index.js:

Vue.component('chart-item', {
    props: ['chart'],
    template:   '<b-row><b-card title="chart.title" col class="w-100" header="" border-variant="default" header-bg-variant="default" header-text-variant="white" align="center">{{chart.title}}</b-card></b-row>'
  })

var app1 = new Vue({
    el: '#app',
    data: {
        chartData: [ // Will be updated via onChange mechanism
            { id: 0,
                title: 'Approval: Any - Asset class: Index',
                options: 'Hello', /* { chart: { id: 'chartOptions-PCAND-Any-INDEX' } }, */
                series: 'Yeah!' /* [{  name: 'chartSeries-PCAND-Any-INDEX', data: [ /*...*/ ] }] */
            },
            ...
        ],

index.html:

            <ol>
                <chart-item
                    v-for="(chart, index) in chartData"
                    v-bind:chart="chart"
                    v-bind:key="chart.id"></chart-item>
            </ol>

My initial goal/hope was to define a template in the index.js and use it dynamical create b-rows with apexChart elements in it. Or should I discard that path?

try with a colon in front of title
:title="chart.title"

in order to use a Vue data value in an html attribute .. you need to v-bind it otherwise vue doesnt know about it and will be considered as string.

its good for testing but for the final project i think it is unnecessary to create a new vue component just to wrap the b-card and b-rows inside. besides apexchart is a component itself.

working from my example

<b-card col class="mt-3 m-3" style="width: 40%;" :title="chart.chartOptions.title.text" v-for="(chart,index) in chartData" :key="index"
              ><apexchart width="95%" :options="chart.chartOptions" :series="chart.chartSeries"></apexchart
            ></b-card>
1 Like

Your suggestion works like a charm!
Yep, indeed there is no need for a dedicated template.

Thanks so much, Andy.

Best regards
Marcel

1 Like