UI Tabular Table Sorting

Yes. It would sit right there and you would have to do a little data manipulation to get what you want.

<div id="screener2"></div>
<script>
    var screener2Table = new Tabulator("#screener2", {
        rowFormatter:function(row){
            //row - row component
            var rowM = row.getCell("Number");
            var rowMarker = rowM.getValue();
            var rowT = row.getCell("TOTAL");
            var rowTrigger = rowT.getValue();
            if(cellData == "24HR"){
                if(rowTrigger > 0){
                    row.getElement().style.backgroundColor = "#00ff00";
                }
                if(rowTrigger < 0){
                    row.getElement().style.backgroundColor = "#ff0000";
                }
            }
        },
     	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>

You'll notice I did a few things in your rowFormatter function. First, I used the row component and extracted the cell component I was looking for into a variable. I then extracted the value of the cell component and placed it into another variable I could check against. I did this both to find the row I wanted to look at as well as the value I wanted to check against for the formatting, hence the two different cells and values. Once I had the values, I checked to see if the current row had a cell value in the Number column of "24HR". If it did, I checked to see whether TOTAL was less than or greater than 0 and applied the background colors accordingly. I picked the TOTAL column since I figured that held the number you were comparing against. If it is a different column, just change TOTAL to whatever it is.

Let me know if there is still any confusion.

Hi, actually there is no comparison (calculations) needed - the values come in neg/pos already. I tried to remove the comparison part but something is not right (no formatting applied). P.S. Wonder if the background color is being prevented from changing since it's defined in CSS? However, I was looking to change font not background.

<div id="screener2"></div>
<script>
    var screener2Table = new Tabulator("#screener2", {
        rowFormatter:function(row){
            //row - row component
            var rowM = row.getCell("ROI");
            var rowMarker = rowM.getValue();
//            var rowT = row.getCell("TOTAL");
//            var rowTrigger = rowT.getValue();
            if(cellData == "ROI"){
                if(rowMarker > 0){
                    row.getElement().style.backgroundColor = "#00ff00";
                }
                if(rowMarker < 0){
                    row.getElement().style.backgroundColor = "#ff0000";
                }
            }
        },
     	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>

I'll take some blame for that. I marked the problem line in what you have. You're referencing cellData to determine if you need to check the row marker or not. I messed you up in my post because I changed some variable names without changing that one to match. But you also deleted the part that would make it work. Here's what it should be.

rowFormatter:function(row){
            //row - row component
            var rowM = row.getCell("Number");
            var rowMarker = rowM.getValue();
            var rowT = row.getCell("TOTAL");
            var rowTrigger = rowT.getValue();
            if(rowMarker == "ROI"){
                if(rowTrigger > 0){
                    row.getElement().style.backgroundColor = "#00ff00";
                }
                if(rowTrigger < 0){
                    row.getElement().style.backgroundColor = "#ff0000";
                }
            }
        }

The part I messed up on was I forgot to change cellData to rowMarker. rowMarker is the variable used to see if it's the row you want to apply the formatting to or not. Since you're looking for a row with a "Number" column value of the string "ROI", you want to make sure you can get that variable. "Number" only holds the string value of "ROI" when that's true, so we need another variable that holds the positive or negative value we want to compare against. This is where I was referencing TOTAL. Getting the cell that contains TOTAL and extracting it's value gives us something to compare against for positive and negative values. Again, change that to whatever field you want to compare against to get your positive or negative value. But you'll want to keep both variables in play.

Make a little more sense now?

Can't really compare some of these values ((like 24HR) with anything at this point (at the table) since they are coming in via webhook from an external source. Are you saying there is no way to recognize if they are pos/neg? If so, I suppose some variable can be created in the input function and passed on to the table just for formatting purposes.

I'm going to take a guess on where we're getting confused. My guess is that off of your webhook you're getting something like this:

"ROI": -0.45

That's my guess. Hence why you're saying the value coming in is already neg/pos. And you would be correct. However in your script, you split that up.

    {
        "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)+" %"
    }

Notice how "ROI" becomes the value of the field "Number"? It no longer has a value itself. That value is being thrown into the calculations that come before this point and in the rest of the object setup. "ROI" itself no longer is a value holder and is instead a value itself. It's been split up, and that's fine. You have to in order to get the table to work. All the stuff is still there, since you're keeping the data with the label. But the one part where I think the understanding is breaking down is that the label may come into the function with a value, but it's leaving as a value to the cell it belongs to, while the value it was associated with has been broken off to go to another cell. So in the example I gave above, I'm looking for the label initially. When I find the label, I look for the value associated with the same row it's on.

The only thing different is if you're trying to change the color of EVERY row in that table based on if it's positive or negative. If that's the case, then you can leave out the if statement and associated cell variables that look for the label and go right into checking the value.

Hopefully I explained it a little better that time.

In case of ROI I am calculating it, but in case of 24HR that's coming via the webhook already pos/neg

Ok. Then you'll need to use where you're sending in the value to the table. This is what the table is seeing for that row:

    {
        "Number": "24HR",
        "Binance": binance_dc+" %",
        "FTX": ftxtrade_dc+" %",
        "Gate":  gate_dc+" %",
        "Paper":  paper_dc+" %",
        "TOTAL": " "
    }

It's still the same setup. Your passing in "24HR" as a cell value in the column "Number". You'll need to figure out the determining cell in order to make the formatting work.

If none of those values are the one's you're triggering on, you can add a property to the object for "24HR" that holds whether it was positive or negative. Since that field won't be defined in your column definition, it will be ignored and not displayed. But you can use it in your rowFormatter to trigger your row coloring.

In all cases, you're going to have to find where the value is you're sending in to determine positive or negative. Right now, you're not sending it in the way you're thinking you are. What you are sending in is a group of properties with values. You just have to pick out the one's that mean something and use them.

Hello Again!

Have been trying to figure out why click-row wasn't producing the expected results and just realized that the output is the entire table (which grows as the table grows) rather than the single row. Does that make sense?

<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:"", field:"flag", width:"1%"},
    	 	{title:"Coin", field:"currency", hozAlign:"center", width:70},
    	 	{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:"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:",", precision:3}},
    	 	{title:"Time", field:"time", hozAlign:"center", width:90}
     	],
    });
    
    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 && msg.flag) {
                screenerTable.addData([msg.payload], true);
            }
        });
    })(scope);
</script>

If you're just trying to output only the row data, simply change your rowClicked event to this:

    screenerTable.on("rowClick", function(e, row){ 
    	var fetchOptions = {
      	    method:'POST',
            headers:{'Content-Type':'application/json'},
            body:JSON.stringify(row.getData())
        };
        fetch("https://red.interscope.link/table-out", fetchOptions)
            .catch(function(error){alert(error)});
    });

All you have to do is change the data variable from the one that points to the whole table (screenerTable) to the one that is passed into the function holding everything that is the row. That will pass out the data for the row instead of the whole table. Simple change.

Thank you!

The impermanent nature of this table config is causing issues - constantly loosing data on application restart, browser refresh, etc. Wondering if it can be addressed with a different approach? The second table does not loose content on browser refresh - what make it different? What if the table wasn't adding "new rows" but was defined as having "10 rows" and we were just updating them (obviously each incoming set would still have to fill in the top row with the rest being pushed down)?

The easiest way to fix this is to place a function between the data and the table that holds the data you want to have in the table. Then you just send the whole array to the table. The function will hold the data and display it. That's the difference between the tables. Your second one is having all the data processed and sent in all at once, so it looks like it loses nothing. The first table is having rows added individually so when that data is lost, it has to rebuild. So storing the data elsewhere makes it so the table can pull everything that's stored and also make it look like it lost nothing.

I see. But the function would still need to rebuild the table in a way so that new data fills in top row and the rest are pushed down?

In the start tab of the function:

var scanArray = [];
context.set("scanArray", scanArray);

In the function itself:

var temp = context.get("scanArray");
temp.unshift(msg.payload);
if(temp.length == 11){ //Whatever is one greater than the length of table you want
    temp.pop();
}
context.set("scanArray", temp);
msg.payload = temp;
return msg;

Unshift pushes something to the top of the stack while pop removes it from the bottom. That's how you push to the top, chop off the bottom and keep it the length you want. Context.set() saves the variable for reference anytime the function is called so you don't lose you're data so long as Node-Red is running.

Function between data out function and tabulator template? I see new rows generated but they blank. Or is this incomplete example?

Yes, it will go between the data out and the template. You will have to adjust things accordingly to match whatever you have. The example is complete for a simple setup, but will need to be customized for anything else. If you've ever used the context tab in Node-Red, it will tell you what the variable values are as they're being processed. That way you can see if they're updating appropriately.

Hmm, so example is complete - the rows are blank. Am I looking for the "scanArray" in the context tab? Not seeing anything getting saved. Does the tabulator function need to adjusted? This is what see as output of the function

image

Try

temp.unshift(msg.payload[0]);

Change it where, in the function? That kills the output (becomes undefined).

I changed it in the Tabulator (see below) and started generating the rows. But if I refresh the browser the table disappears again (exactly same as before).

    (function(scope){
        scope.$watch('msg', function(msg){
            if(!msg.payload.note) {
                screenerTable.addData([msg.payload[0]], true);
            }
        });
    })(scope);
</script>

Also note that nothing was shown in context tab as getting saved. When I changed it from context.set to flow.set it started getting saved but it didn't affect the table issue.

This is what the multi row output looks like

Yes. You'll change your unshift in the function from pushing into temp the whole array in msg.payload to just pushing the first object into temp. Remember, Tabulator is looking for an array one level deep. If you have an array object inside another array object, it can't read it. If you have an individual object, it can't read it. You have to have a one level array for it to read it. Check your table code. If you're receiving something as an [array] and you're sending an [array], you'll end up with an [[array]], which Tabulator can't read. You have to take the receiving brackets off. Also, if you're passing the msg to an addRow or something instead of a setData, it won't work. You have to pass a one level [array] to scannerTable.setData(msg.payload). Make sense?

I found if I manually refresh the data while the flow is running that values will show up under context. Sometimes they won't show up until a manual refresh.