UI Tabular Table Sorting

That would be something I would go after just as a matter of housekeeping. Now that you're using multiple tables, you're going to need to keep things separated.

What does the developer's console show you? And what does your flow look like so far now that you have multiple tables? If you post what you have in your templates, maybe we can spot the conflict or whatever is causing the second table to not load. It sounds like it's right there and just needs something small to break it loose.

On the console the only table-related message is a warning: "Invalid table constructor option: textSize". Here is the final first table config. The second is the flow you sent over. These are actually two independent flows (different tabs, different data sources).

<script type="text/javascript" src="https://cdn.jsdelivr.net/npm/luxon@3.0.4/build/global/luxon.min.js"></script>
<div id="screener"></div>
<script>
    //Copy/paste Tabulator examples directly from the web page here.
    //You will need example data to use them directly.
    //Otherwise copy the examples and alter to your needs.
    //Most of the following is directly from the quickstart page.
    
    var tabledata = [
//   	{id:1, name:"Oli Bob", age:"12", col:"red", dob:""}
  //  	{id:2, name:"Mary May", age:"1", col:"blue", dob:"14/05/1982"},
 //   	{id:3, name:"Christine Lobowski", age:"42", col:"green", dob:"22/05/1982"},
 //   	{id:4, name:"Brendon Philips", age:"125", col:"orange", dob:"01/08/1980"},
//    	{id:5, name:"Margret Marmajuke", age:"16", col:"yellow", dob:"31/01/1999"},
    ];
    
    var table = new Tabulator("#screener", {
     	height:900, 
     	textSize : 10,
     	data:tabledata, //assign data to table
     	layout:"fitColumns", //fit columns to width of table (optional)
     	columns:[ //Define Table Columns
    	 	{title:"I", field:"note", width:"1%"},
    	 	{title:"Coin", field:"currency", hozAlign:"center", width:66},
    	 	{title:"Exchange", field:"exchange", hozAlign:"center", width:70},
    	 	{title:"UP", field:"percent", hozAlign:"center", width:50,
    	 	 formatter: function(cell, formatterParams){var value = cell.getValue(); if(value == null) {return ""}; if (value !== null) {if(value >= 0){cell.getElement().style.color ='#609f70'} else {cell.getElement().style.color ='#ff4a68'} return value +'%';}}    
    	 	},
     	 	{title:"W", field:"window", hozAlign:"center", width:"20"	 	    
    	 	},
    	 	{title:"24Hr", field:"change", hozAlign:"center", width:60, formatterParams:{precision:0},
             formatter: function(cell, formatterParams){var value = cell.getValue(); if(value == null) {return ""}; if (value !== null) {if(value >= 0){cell.getElement().style.color ='#609f70'} else {cell.getElement().style.color ='#ff4a68'} return value +'%';}}	    
    	 	},
    	 	{title:"B", field:"b_coin", hozAlign:"center", width:20, formatter:function(cell, formatterParams, onRendered) {var value = cell.getValue(); if(value == null) {return ""}; return "<span style='color:#2962ff; font-weight:bold;'>" + cell.getValue() + "</span>";}},  
    	 	{title:"F", field:"f_coin", hozAlign:"center", width:20},
    	 	{title:"G", field:"g_coin", hozAlign:"center", width:20},
    	 	{title:"Volume", field:"volume", hozAlign:"right", width:100,formatter:"money", formatterParams:{thousand:",", precision:0}},
    	 	{title:"Price", field:"price", hozAlign:"right", width:70, formatter:"money", formatterParams:{thousand:","}},
    	 	{title:"Time", field:"time", hozAlign:"center", width:100}
     	],
    });
    
    table.on("rowClick", function(e, row){ 
    	var fetchOptions = {
      	    method:'POST',
            headers:{'Content-Type':'application/json'},
            body:JSON.stringify(table.getData())
        };
        fetch("https://red.interscope.link/table-out", fetchOptions) //Change address here
            .catch(function(error){alert(error)});
    });
    
    //This is needed to interact with Node-Red when a message is sent to the ui-template.
    (function(scope){
        scope.$watch('msg', function(msg){
            if(msg){
                table.addData([msg.payload], true); //Your new reaction code
            }
        });
    })(scope);
</script>

So here is the summary of symptoms.

First table works both by itself and with the second. Second table doesn't render with the first being active. When I try to inject (a refresh) on the second it triggers empty row being added on the first.

If I disable the first table, rows can be added to the second via the inject, however they are empty. Also, the second table is supposed to be fixed 6 columns x 4 rows with data being refreshed at a regular interval.

I can see that data is properly coming out of the function node; this the fixed column configuration (5 columns x 5 rows). So the problem must be with the template. I am also seeing an error indicator on the template node itself "invalid properties - width" (there is no "width" referenced in template). And there is definitely some interference taking place as I described.

<div id="screener2"></div>
<script>
    var table = new Tabulator("#screener2", {
     	height:800, 
   //  	textSize : 10,
     	layout:"fitColumns",
     	columns:[
    		{title:"Number",field:"Number",hozAlign:"left",frozen:true},
    		{title:"Binance",field:"Binance",hozAlign:"center"},
    		{title:"FTX Bots",field:"FTX Bots",hozAlign:"center"},
    		{title:"FTX Trade",field:"FTX Trade",hozAlign:"center"},
    		{title:"Gate",field:"Gate",hozAlign:"center"},
    		{title:"TOTAL",field:"TOTAL",hozAlign:"center"}
    	]
    });
    
    table.on("rowClick", function(e, row){ 
    //
    });
    
    (function(scope){
        scope.$watch('msg', function(msg){
            if(msg){
                table.addData([msg.payload], true);
            }
        });
    })(scope);
</script>

I've commented out the config in the new code. You shouldn't see a difference if it was an invalid config anyways. We can always figure out what the actual config should be if you need it to be a certain way.

Normally that would keep them isolated, true. But the template node just puts up whatever is in the template. It exists on the page by itself and can therefore be accessible by any tab and any flow in Node-Red. It doesn't care. Essentially the ui-template makes a global variable/function accessible by anything written in any template node in your instance of Node-Red. Hence why you have to isolate it.

Yes. The first table gets the variable declaration and all references point to it. The function in the second ui-template referenced table.addData(), which would have been a reference to the first table. Which is why the first table got the extra row.

This might actually be caused by how the data is being passed into the tables themselves. If the first table is having an individual row object being passed in, the object is being converted into an array inside the addData() function. If you have an array being passed in (like is what's happening in the second table data inject), that is also getting turned into an array. So it's an array of arrays, which is not something that Tabulator knows what to do with at this point. It knows what to do about an array of objects, but not an array of arrays.

This will be determined outside of your ui-template node by whatever does the injection of the data. Possibly an inject node firing into a function node. Whatever is sending the data into the second ui-template node will dictate when the data is updated.

So, here's what I think might help. Table 1:

<script type="text/javascript" src="https://cdn.jsdelivr.net/npm/luxon@3.0.4/build/global/luxon.min.js"></script>
<div id="screener"></div>
<script>
    var screenerTable = new Tabulator("#screener", {
     	height:900, 
     	//textSize : 10,
     	layout:"fitColumns",
     	columns:[ //Define Table Columns
    	 	{title:"I", field:"note", width:"1%"},
    	 	{title:"Coin", field:"currency", hozAlign:"center", width:66},
    	 	{title:"Exchange", field:"exchange", hozAlign:"center", width:70},
    	 	{title:"UP", field:"percent", hozAlign:"center", width:50,
    	 	 formatter: function(cell, formatterParams){var value = cell.getValue(); if(value == null) {return ""}; if (value !== null) {if(value >= 0){cell.getElement().style.color ='#609f70'} else {cell.getElement().style.color ='#ff4a68'} return value +'%';}}    
    	 	},
     	 	{title:"W", field:"window", hozAlign:"center", width:"20"	 	    
    	 	},
    	 	{title:"24Hr", field:"change", hozAlign:"center", width:60, formatterParams:{precision:0},
             formatter: function(cell, formatterParams){var value = cell.getValue(); if(value == null) {return ""}; if (value !== null) {if(value >= 0){cell.getElement().style.color ='#609f70'} else {cell.getElement().style.color ='#ff4a68'} return value +'%';}}	    
    	 	},
    	 	{title:"B", field:"b_coin", hozAlign:"center", width:20, formatter:function(cell, formatterParams, onRendered) {var value = cell.getValue(); if(value == null) {return ""}; return "<span style='color:#2962ff; font-weight:bold;'>" + cell.getValue() + "</span>";}},  
    	 	{title:"F", field:"f_coin", hozAlign:"center", width:20},
    	 	{title:"G", field:"g_coin", hozAlign:"center", width:20},
    	 	{title:"Volume", field:"volume", hozAlign:"right", width:100,formatter:"money", formatterParams:{thousand:",", precision:0}},
    	 	{title:"Price", field:"price", hozAlign:"right", width:70, formatter:"money", formatterParams:{thousand:","}},
    	 	{title:"Time", field:"time", hozAlign:"center", width:100}
     	],
    });
    
    screenerTable.on("rowClick", function(e, row){ 
    	var fetchOptions = {
      	    method:'POST',
            headers:{'Content-Type':'application/json'},
            body:JSON.stringify(screenerTable.getData())
        };
        fetch("https://red.interscope.link/table-out", fetchOptions)
            .catch(function(error){alert(error)});
    });
    
    (function(scope){
        scope.$watch('msg', function(msg){
            if(msg){
                screenerTable.addData([msg.payload], true);
            }
        });
    })(scope);
</script>

Table 2:

<div id="screener2"></div>
<script>
    var screener2Table = new Tabulator("#screener2", {
     	height:800, 
	    //textSize : 10,
     	layout:"fitColumns",
     	columns:[
    		{title:"Number",field:"Number",hozAlign:"left",frozen:true},
    		{title:"Binance",field:"Binance",hozAlign:"center"},
    		//{title:"FTX Bots",field:"FTX Bots",hozAlign:"center"}, //Change this field to "FTX"
    		//{title:"FTX Trade",field:"FTX Trade",hozAlign:"center"}, //Or change this field
    		{title:"Gate",field:"Gate",hozAlign:"center"},
    		{title:"TOTAL",field:"TOTAL",hozAlign:"center"}
    	]
    });
    
    /*screener2Table.on("rowClick", function(e, row){ 
        //Uncomment this whole event to use it
    });*/
    
    (function(scope){
        scope.$watch('msg', function(msg){
            if(msg){
                screener2Table.setData(msg.payload, true);
            }
        });
    })(scope);
</script>

I've gone through and changed some stuff in your table code to isolate the two tables and their functions/events. Each table now has its own name, namely screenerTable and screener2Table. This should make one respond to its own set of data and the other respond to its own. Now when you send data to a table, it should react.

The next thing I noticed is you're sending a key into the second table (FTX) that isn't being accounted for in the column definitions. I've commented out the column definitions that would pertain to it. Uncomment the one that applies and change the field value to "FTX" so it will pull that field.

Third, I've changed your addData on the second table to setData. Since you're passing the entire array every time and not adding to the table, setData() applies. Also, since you're passing in the array already, I took out the brackets that would have changed it into an array of arrays.

Lastly, I've commented out the whole rowClick event on the second table. If it's not being used yet, it doesn't need to be in the code. You can always uncomment it and add stuff to it or change it to a different type of listener. But as it stands, these two table setups should work, based off what I know is coming into them.

There will be some formatting that needs to be done as well as other stuff to make it pretty, but this should hopefully make your data display and add like it's supposed to. Let me know what you run up against.

Well looks like all the table issues are resolved - can't thank you enough for all the time and help!!!

I ended up using a time format node to deal with the date/time formatting problem - there could be an issue with sorting (if it's not done in Tabulator) but not a big deal since it comes in the proper sequence. Let me know if you ever get this "sorted out" :slight_smile:

Final question. I have a scenario (pertaining to the first table) where a msg is coming into the tabulator template which I don't want to add. I have a flow.variable flag to check against. It got complicated trying to filter it inside function that's generating the msg (prevent the output). I tried checking the flag in the tabulator template to stop the row from being added but the template doesn't work like a regular node (in terms of passing variables, etc) - is there a simple way to do it? I can also put a function or change node before tabulator input to block the msg.

What's the best way to do it?

You'll have to show me what you mean. What is an example of what you're blocking against and how are you checking it?

I have function that's sending out msg to add the next row:

msgScreener.payload = {"currency": currency, "exchange": exchange, "percent": percent, "window": window, "change": change, "b_coin": b_coin, "f_coin": f_coin, "g_coin": g_coin, "price": price, "volume": volume, "time": msg.time, "symbol":symbol, "note":note};

image

The last variable "note" is a flag to indicate whether this particular rows needs be skipped. It got tricky filtering it out in the function because I got multiple outputs (I think because if one of the msg is blocked it causes issues). Couldn't figure out how to do it in the Tabulator template to prevent row from being added. Other option is to put a node in between the two to check if (note == "no" {don't send msg to tabulator).

Modify your input function in the template nodes to look like this:

(function(scope){
        scope.$watch('msg', function(msg){
            if(msg && msg.payload.note != "no"){
                screenerTable.addData([msg.payload], true);
            }
        });
    })(scope);

If the function receives a message AND the message isn't empty AND the note item in payload isn't "no", add the data.

Looks good, but not working ....

Are you getting any errors or is the unwanted data just showing up?

No errors, just no effect - the rows are still getting added despite the note = no

So what if you did this:

msgScreener.payload = {"currency": currency, "exchange": exchange, "percent": percent, "window": window, "change": change, "b_coin": b_coin, "f_coin": f_coin, "g_coin": g_coin, "price": price, "volume": volume, "time": msg.time, "symbol":symbol};
msgScreener.note = note;

Not only that, but instead of using "yes" or "no", use true or false. With true or false, you can actually use the element itself in the if statement instead of comparing it against something else, like this:

if(msg.note){
}
//or
if(!msg.note){
}

That also makes sure you don't have something that is assigning "No" to the variable when you're expecting "no". Something to try, and also good coding practice if you're expecting to use the data for logic operations.

Ok, just confirmed that it's reading the flag, just need to get the logic right.

Nice. As long as it's reading, logic can be worked out. But that sounds like the last hurdle before everything is working as it should.

Hi again, quick question concerning the second table (defined matrix config). How would I apply formatting, in this case to the rows?

It depends on the formatting you want to apply to your rows. In most of the cases, you'll want to add the rowFormatter option to your table definition. You'll find the example here. In the example, you'll notice the table is declared and the only option is the rowFormatter. Your table has several other options declared, so you'll just add this into the mix. Then you'll define what you want the formatting to be in the function that comes after. You're having a row component passed in to the function, so you just have to get the cell value you want to trigger off of and define the parameter you want to format your row with (i.e. background color). Make sense?

So I specify the specific row number (?) directly in the declaration? And if I want to format multiple rows differently?

Lets say I want to apply this formatting (similar to what I have in the other table):

formatter: function(cell, formatterParams){var value = cell.getValue(); if(value == null) {return ""}; if (value !== null) {if(value >= 0){cell.getElement().style.color ='#609f70'} else {cell.getElement().style.color ='#ff4a68'} return value +'%';}}

EXAMPLE:

var table = new Tabulator("#example-table", {
rowFormatter:function(row){
//row - row component

    var data = row.getData();
    
    if(data.col == "blue"){
        row.getElement().style.backgroundColor = "#1e3b20";
    }
},

});

No. How it works is you apply the formatting formula. Then Tabulator iterates through the rows, extracts the row or cell data you're specifying as the variable and then applies the formatting based on the value. What I should have said is what you have passed in is the row data Tabulator will use to make the determination. So you just have to build it knowing that will be your information.

Make sense?

And as far as the actual formatting will go, yes the formatting will be very similar. Just make sure you're pointing to the right component and don't pass in formatterParams. For instance, the formatting example you gave passes in the cell. So if you want to trigger off a cell value, you're going to need to get the cell from the row and then get there value from the cell, all inside the function. It's simple to do. Otherwise you can just use the row value for something like alternating colors or something.

Unfortunately still confused :frowning:

Here is the table function:

msgAcct = {}; 


var binance = Number(msg.payload.all[1].usd_amount);
var binance_day = Number(msg.payload.all[1].day_profit_usd);
var binance_dc = (Number(msg.payload.all[1].day_profit_usd_percentage)).toFixed(2);

var ftxtrade = Number(msg.payload.all[5].usd_amount);
var ftxtrade_day = Number(msg.payload.all[5].day_profit_usd);
var ftxtrade_dc = (Number(msg.payload.all[5].day_profit_usd_percentage)).toFixed(2);

var gate = Number(msg.payload.all[7].usd_amount);
var gate_day = Number(msg.payload.all[7].day_profit_usd);
var gate_dc = (Number(msg.payload.all[7].day_profit_usd_percentage)).toFixed(2);

var paper = Number(msg.payload.allp[0].usd_amount);
var paper_day = Number(msg.payload.allp[0].day_profit_usd);
var paper_dc = (Number(msg.payload.allp[0].day_profit_usd_percentage)).toFixed(2);

var trades_b = msg.payload.b.data.active_smart_trades_count;
var trades_ft = msg.payload.ft.data.active_smart_trades_count;
var trades_g = msg.payload.g.data.active_smart_trades_count;
var trades_p = msg.payload.p.data.active_smart_trades_count;

var bots_b = msg.payload.b.data.active_bots_count;
var bots_ft = msg.payload.ft.data.active_bots_count;
var bots_g = msg.payload.g.data.active_bots_count;
var bots_p = msg.payload.p.data.active_bots_count;

var total = (binance+ftxtrade+gate);
var total_day = (gate_day+binance_day+ftxtrade_day)


msgAcct.payload = [
    {
        "Number": "Balance",
        "Binance": "$ "+binance.toFixed(0),
        "FTX":"$ "+ftxtrade.toFixed(0),
        "Gate": "$ "+gate.toFixed(0),
        "Paper": "$ "+paper.toFixed(0),
        "TOTAL": "$ "+total.toFixed(0)
        
    },
    {
        "Number": "24HR",
        "Binance": binance_dc+" %",
        "FTX": ftxtrade_dc+" %",
        "Gate":  gate_dc+" %",
        "Paper":  paper_dc+" %",
        "TOTAL": " "
    },    
    {
        "Number": "Day",
        "Binance": "$ "+binance_day.toFixed(0),
        "FTX":  "$ "+ftxtrade_day.toFixed(0),
        "Gate":  "$ "+gate_day.toFixed(0),
        "Paper":  "$ "+paper_day.toFixed(0),
        "TOTAL": "$ "+total_day.toFixed(0)
    },
    {
        "Number": "Trades",
        "Binance": trades_b,
        "FTX":  trades_ft,
        "Gate":  trades_g,
        "Paper":  trades_p,
        "TOTAL": " "
    },
    {
        "Number": "Bots",
        "Binance": bots_b,
        "FTX":  bots_ft,
        "Gate":  bots_g,
        "Paper":  bots_p,
        "TOTAL": " "
    },
    {
        "Number": "ROI",
        "Binance": (((binance - 1016)/1015)*100).toFixed(2)+" %",
        "FTX": (((ftxtrade - 1001)/1001)*100).toFixed(2)+" %",
        "Gate":  (((gate - 1075)/1075)*100).toFixed(2)+" %",
        "Paper":  (((paper - 222495)/222495)*100).toFixed(2)+" %",
        "TOTAL": (((total - 3107)/3107)*100).toFixed(2)+" %"
    }
]


return msgAcct 

Lets say I want to format the "24HR" row -- red if negative, green if positive -- what do I reference?

Is this how I add the declaration?

<div id="screener2"></div>
<script>
    var screener2Table = new Tabulator("#screener2", {

    rowFormatter:function(row){
        //row - row component
        
        var data = row.getData();
        
        if(data.col == ""){
            row.getElement().style.backgroundColor = "#1e3b20";
        }
    },


     	height:800, 
	    //textSize : 10,
     	layout:"fitColumns",
     	columns:[
    		{title:"",field:"Number",hozAlign:"center",frozen:true},
    		{title:"Binance",field:"Binance",hozAlign:"center"},
    		{title:"FTX",field:"FTX",hozAlign:"center"},
    		{title:"Gate",field:"Gate",hozAlign:"center"},
    		{title:"Paper",field:"Paper",hozAlign:"center"},    		
    		{title:"TOTAL",field:"TOTAL",hozAlign:"center"}
    	]
    });
    
    /*screener2Table.on("rowClick", function(e, row){ 
        //Uncomment this whole event to use it
    });*/
    
    (function(scope){
        scope.$watch('msg', function(msg){
            if(msg){
                screener2Table.setData(msg.payload, true);
            }
        });
    })(scope);
</script>