Thanks! Option 2 seems like a better solution. Just to clarify, that example you provided goes into my tabulator function (which I posted)? Is the replaceData done by default? Because that's what is happening now.
Adding that code actually breaks the table (the constant declaration alone already breaks it).
Could you take a look if I did it correctly please:
<script type="text/javascript" src="https://cdn.jsdelivr.net/npm/luxon@3.0.4/build/global/luxon.min.js"></script>
<div>
<button class="copy" id="copy"> ■ </button>
</div>
<div id="screenerF"></div>
<script>
var screenerTableF = new Tabulator("#screenerF", {
index:"name",
clipboardCopySelector:"selected",
rowHeader:{formatter:"rowSelection", titleFormatter:"rowSelection", width:41, headerSort:false, resizable: false, frozen:true, cssClass:"selectbox", headerHozAlign:"center", hozAlign:"center"},
clipboard:"copy",
clipboardCopyStyled:false,
clipboardCopyConfig:{
columnHeaders:false,
formatCells:false
},
height:900,
headerSortElement:"",
layout:"fitColumns",
columns:[ //Define Table Columns
{title:"Ticker", field:"ticker", visible:false, clipboard:true, hozAlign:"center", width:65, accessorClipboard:stockAccessor, accessorClipboardParams:{}},
{title:"Stock", field:"name", clipboard:false, hozAlign:"center", formatter:link, width:65},
{title:"Gap", field:"gap", clipboard:false, hozAlign:"right", width:65,
formatter: function(cell, formatterParams){var value = cell.getValue(); if(value == null) {return ""}; if (value !== null) {if(value >= 15){cell.getElement().style.color ='#609f70'} else if(value >= 0 && value < 15) {cell.getElement().style.color ='#728faa'} else {cell.getElement().style.color ='#ff4a68'} return value +'%';}}},
{title:"Price", field:"close", clipboard:false, hozAlign:"right", width:55},
{title:"Change", field:"change", clipboard:false, hozAlign:"right", width:65,
formatter: function(cell, formatterParams){var value = cell.getValue(); if(value == null) {return ""}; if (value !== null) {if(value >= 15){cell.getElement().style.color ='#609f70'} else if(value >= 0 && value < 15) {cell.getElement().style.color ='#728faa'} else {cell.getElement().style.color ='#ff4a68'} return value +'%';}}},
{title:"PreM", field:"premarket_change", clipboard:false, hozAlign:"right", width:55,
formatter: function(cell, formatterParams){var value = cell.getValue(); if(value == null) {return ""}; if (value !== null) {if(value >= 15){cell.getElement().style.color ='#609f70'} else if(value >= 0 && value < 15) {cell.getElement().style.color ='#728faa'} else {cell.getElement().style.color ='#ff4a68'} return value +'%';}}},
{title:"rVol", field:"relative_volume", clipboard:false, hozAlign:"right", width:60,
formatter: function(cell, formatterParams){var value = cell.getValue(); if(value == null) {return ""}; if (value !== null) {if(value >= 10){cell.getElement().style.color ='#609f70'} else if(value >= 0 && value < 15) {cell.getElement().style.color ='#728faa'} else {cell.getElement().style.color ='#ff4a68'} return value +'%';}}},
{title:"rVol D|5", field:"relative_volume_intraday|5", clipboard:false, hozAlign:"right", width:66,
formatter: function(cell, formatterParams){var value = cell.getValue(); if(value == null) {return ""}; if (value !== null) {if(value >= 10){cell.getElement().style.color ='#609f70'} else if(value >= 0 && value < 15) {cell.getElement().style.color ='#728faa'} else {cell.getElement().style.color ='#ff4a68'} return value +'%';}}},
{title:"rVol 10D", field:"relative_volume_10d_calc", clipboard:false, hozAlign:"right", width:65,
formatter: function(cell, formatterParams){var value = cell.getValue(); if(value == null) {return ""}; if (value !== null) {if(value >= 10){cell.getElement().style.color ='#609f70'} else if(value >= 0 && value < 15) {cell.getElement().style.color ='#728faa'} else {cell.getElement().style.color ='#ff4a68'} return value +'%';}}},
{title:"Int", field:"open_interest", clipboard:false, hozAlign:"right", width:55},
// {title:"rVol 10D|5", field:"relative_volume_10d_calc|5", clipboard:false, hozAlign:"right", width:75,
// formatter: function(cell, formatterParams){var value = cell.getValue(); if(value == null) {return ""}; if (value !== null) {if(value >= 10){cell.getElement().style.color ='#609f70'} else if(value >= 0 && value < 15) {cell.getElement().style.color ='#728faa'} else {cell.getElement().style.color ='#ff4a68'} return value +'%';}}},
{title:"Vol", field:"volume", clipboard:false, hozAlign:"right", width:90, formatter:"money", formatterParams:{precision:0}},
{title:"Float", field:"float_shares_outstanding", clipboard:false, hozAlign:"right", width:90, formatter:"money", formatterParams:{precision:0}}
],
});
const tableData = screenerTableF.getData();
// newData is the new record set
for (let i = 0 ; i < newData.length ; i++)
{
// Check if the new row exists in the table
if (tableData.findIndex((row)=>row.name == newData[i].name ) < 0) // not found
$scope.send({payload:newData[i],topic:"New name"});
}
screenerTableF.replaceData(newData);
document.getElementById("copy").addEventListener("click", function(){
screenerTableF.copyToClipboard("selected");
});
var stockAccessor = function(value, data, type, params, column, row){
return value +",";
}
function link(cell, formatterParams){
var url = cell.getValue();
return "<a href='https://stockanalysis.com/stocks/"+url+"' target='_blank'>"+url+" </a>";
}
(function(scope){
scope.$watch('msg', function(msg){
screenerTableF.setData(msg.payload, true);
});
screenerTableF.on("cellClick", function(e, cell){
scope.send({topic:cell.getValue(), payload:cell.getData()});
});
})(scope);
</script>
You need to plug my code inside your msg listener, as follows:
var $scope = this; // save the template's 'this' for use inside scoped functions
(function(scope){
scope.$watch('msg', function(msg) {
if (!msg)
return; // due to dashboard-1.0 bug, sometimes you get bad messages
let tableData = screenerTableF.getData();
let newData = msg.payload;
for (let i = 0 ; i < newData.length ; i++) // Check every new row if it exists in the table
{
if (tableData.findIndex((row)=> row.name == newData[i].name ) < 0) // not found
$scope.send({payload:newData[i],topic:"New name"}); // Format the notification to your needs
}
screenerTableF.setData(newData, true); // or replaceData
});
setData
and replaceData
are mostly equivalent, you can use either.
Sorry, I am not sure what the msg listener is in my config. It's in my tabulator template ?
This is my cache function, which in fact sets the table ... rows get added/updated/deleted (or entire table replaced - does this happen when it's an array?). ... so this is where a determination is made if a row already exists based on some index (I don't understand how the comparison is made) ... seems this is where the solution lies.
var status = {fill:"red",shape:"ring",text:"an error occured"};
var success = (msg.topic && msg.topic==="success") || false;
var tableData = flow.get("tableData");
if (tableData === undefined) {
tableData = [];
flow.set("tableData",tableData);
}
// find the index for a row in tableData for a given index (id)
function checkIndex(id) {
let matchRow=-1
tableData.forEach(function (row,index){
if (row.id === id){
matchRow=index;
return matchRow;
}
})
return matchRow;
}
// flat merge one row
function mergeRow(dest,source) {
Object.keys(source).forEach(function(key) {
dest[key]=source[key];
})
}
//merge or add one or many rows into tableData
function mergeData(newData,toTop) {
newData.forEach(function (item,index) {
node.warn(["findIndex",item]);
let row=checkIndex(item.id);
if (row<0) { // row do not existst in tableData
if (toTop) {
tableData.push(item);
status.text+="newRow @ top";
} else {
tableData.unshift(item);
status.text+="newRow @ bottom";
}
return;
} else { // row exists so update
mergeRow(tableData[row],item);
status.text+="row updated";
return;
}
if (status.text!=="") node.status(status);
});
}
switch (typeof msg.payload){
case "string":
node.warn(["[table recorder] ",(typeof msg.payload),msg.payload]);
switch (msg.payload){
case "change":
status={fill:"green",shape:"dot",text:"table restored "+tableData.length+" rows"};
msg.payload=tableData;
break;
}
break;
case "object":
node.warn(["[table recorder] ",(typeof msg.payload),msg.payload]);
if (Array.isArray(msg.payload)) { // replace all tableData
status={fill:"green",shape:"dot",text:"table replaced "+msg.payload.length+" rows"};
tableData=RED.util.cloneMessage(msg.payload);
} else {
switch (msg.payload.command) { // clearData does not return a promise!
case "clearData":
status={fill:"green",shape:"dot",text:"clearData: done"};
tableData=[];
flow.set("lastId",0);
break;
}
}
break;
default: // likely a msg fom a ui-table command or callback
if (msg.hasOwnProperty("topic")&&
msg.hasOwnProperty("ui_control") &&
msg.ui_control.hasOwnProperty("callback") &&
msg.hasOwnProperty("return")) { // message originates from a ui-table callback
if (success) {
switch(msg.return.command) {
case "addRow":
status.text="addRow: ";
mergeData(msg.return.arguments[0],msg.return.arguments[1]);
status.shape="dot";
break;
case "updateOrAddData":
status.text="updateOrAddData: ";
mergeData(msg.return.arguments[0]);
break;
case "deleteRow":
let row=checkIndex(msg.return.arguments[0]);
tableData.splice(row,1);
status.shape="dot";
status.text="deleteRow: "+row+" deleted";
break;
default:
status={fill:"yellow",shape:"dot",text:msg.return.command + " unknown!"};
break;
}
} else {
status.text=msg.topic+" "+msg.error;
}
}
break;
}
if (success) status.fill="green";
flow.set("tableData",tableData);
node.status(status);
return msg;
Yes, I am referring to the table template, not the cache function.
As I understand, your cache function processes the incoming data and sends it as an array of rows (in msg.payload
) to the template, which uses setData
to update the table.
In your template, the function $watch
is a msg listener, i.e. it receives incoming messages and acts upon them (i.e. updates the table).
What I proposed was to add my comparison code inside the msg listener (before setData
) which would then:
- Read the current table data
- Compare it to the incoming
msg.payload
and identify new rows - Send notifications for these new rows
- Proceed to updating the table.
Below is the fixed code segment (which I tested with fabricated data on your actual table). Just replace the (function(scope) {...}) (scope);
segment in your template code with the below:
(function(scope){
scope.$watch('msg', function(msg) { // This is the message listener
if (!msg)
return; // dashboard-1.0 bug sometimes sends bad messages
const tableData = screenerTableF.getData();
const newData = msg.payload;
for (let i = 0 ; i < newData.length ; i++) // Check every new row if it exists in the table
{
if (tableData.findIndex((row)=> row.name == newData[i].name ) < 0) // not found
scope.send({payload:newData[i],topic:"New name"}); // Send notification - in any format you wish
}
screenerTableF.setData(newData, true); // or use replaceData
});
screenerTableF.on("cellClick", function(e, cell){
scope.send({topic:cell.getValue(), payload:cell.getData()});
});
})(scope);
BTW, I would recommend setting any event listeners (in your case, cellClick
) inside the tableBuilt
event handler, to ensure it is set after the table has completed initializing.
Unfortunately (while the table does load) it flashes an "error" message on the dashboard and is triggering an infinite loop of some kind. Is it possible to do this within the Cache function?
Strange, it works well on my machine. Maybe you are sending some invalid data.
It is possible to identify & notify new records in the cache function. I'll take a look.
You cache function is rather complex, I'm not sure I'm able follow the various payload transformations & code paths.
To do the check & notifications in the cache function, you can do the following:
- Add another output port to the cache node, where port 0 continues to return the messages as before, and port 1 outputs notifications
- In the appropriate place in your code, compare the table data and the new data (the same way we did before in the template), when detecting a record with a new 'name' property, send a notification through output port 1.
For example:
...
var tableData = flow.get("tableData");
...
//^^^^^^^ added code: check & notify for new names ^^^^^^^^^
// Assuming tableData holds the current table data, and msg.payload has the new recordset
for (let i = 0 ; i < msg.payload.length ; i++)
{
if (tableData.findIndex((row) => row.name == msg.payload[i].name) < 0) // not found
node.send([null, { payload: msg.payload[i],topic:"New name notification"}]);
}
//^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
And in the end of the code:
//^^^^^^^^^^
//return msg;
return ([msg,null]);
//^^^^^^^^^^
I added a port and added the code, and tested by manually injecting a record with a new name. It's working!
Curious, I am seeing this type of (warn) being generated by the cache function ..
The computer does what we tell it to do...
switch (typeof msg.payload){
case "string":
node.warn(["[table recorder] ",(typeof msg.payload),msg.payload]);
I would suggest that you streamline your cache code, as it seems (to me) to be too complex, and will be hard for you to maintain in the future.
For example, I would not make heuristic assumptions based on the type of msg.payload (string, object) etc. but would rather set a dedicated property identifying the msg (incoming new data, table callback etc.).
Wish you success with your project (going on vacation tomorrow).
Those individual letters actually spell "c h a n g e"; the only place where that word exists in the function is:
wmsg.payload),msg.payload]);
switch (msg.payload){
case "change":
status={fill:"green",shape:"dot",text:"table restored "+tableData.length+" rows"};
msg.payload=tableData;
break;
(and it's the name of one the fields in the table, but I doubt it's related);
The newly added code is generating it (you can see by your topic) and it's sending it regardless whether this a new record or not, just every time it reloads; unfortunately it's triggering something else, so I need to find a way to solve it.
Thanks for your help.
This one was too bizarre to figure out, so I just blocked that output using the switch node. Thanks again for your help.
This topic was automatically closed 14 days after the last reply. New replies are no longer allowed.