Updating an HTML table in d3.js using a reusable chart - charts

I have this reusable pattern to create a table, inspired by http://bl.ocks.org/3687826, and I have two questions about it.
This is the function:
d3.table = function(config) {
var columns = [];
var tbl = function(selection) {
if (columns.length == 0) columns = d3.keys(selection.data()[0][0]);
console.log(columns)
// Creating the table
var table = selection.append("table");
var thead = table.append("thead");
var tbody = table.append("tbody");
// appending the header row
var th = thead.selectAll("th")
.data(columns)
th.enter().append("th");
th.text(function(d) { return d });
th.exit().remove()
// creating a row for each object in the data
var rows = tbody.selectAll('tr')
.data(function(d) { return d; })
rows.enter().append("tr");
rows.attr('data-row',function(d,i){return i});
rows.exit().remove();
// creating a cell for each column in the rows
var cells = rows.selectAll("td")
.data(function(row) {
return columns.map(function(key) {
return {key:key, value:row[key]};
});
})
cells.enter().append("td");
cells.text(function(d) { return d.value; })
.attr('data-col',function(d,i){return i})
.attr('data-key',function(d,i){return d.key});
cells.exit().remove();
return tbl;
};
tbl.columns = function(_) {
if (!arguments.length) return columns;
columns = _;
return this;
};
return tbl;
};
This table can be called as follows:
/// new table
var t = d3.table();
/// loading data
d3.csv('reusable.csv', function(error,data) {
d3.select("body")
.datum(data.filter(function(d){return d.price<850})) /// filter on lines
.call(t)
});
where the reusable.csv file is something like this:
date,price
Jan 2000,1394.46
Feb 2000,1366.42
Mar 2000,1498.58
Apr 2000,1452.43
May 2000,1420.6
Jun 2000,1454.6
Jul 2000,1430.83
Aug 2000,1517.68
Sep 2000,1436.51
and the number of columns can be updated by
t.columns(["price"]);
d3.select("body").call(t);
The problem is that another table with thead and tbody is created while updating, because the creation of the table is inside the function.
How can I say "create the table only one time, then update"?
Another question is: how can I filter the lines using a method inside the function?

The problem is with these three lines of code:
// Creating the table
var table = selection.append("table");
var thead = table.append("thead");
var tbody = table.append("tbody");
which always appends new table, thead, and tbody elements to your document. Here's how you can do this conditionally, only when these elements don't already exist (the example you cite creates its div.header element similarly):
selection.selectAll('table').data([0]).enter().append('table');
var table = selection.select('table');
table.selectAll('thead').data([0]).enter().append('thead');
var thead = table.select('thead');
table.selectAll('tbody').data([0]).enter().append('tbody');
var tbody = table.select('tbody');
The selectAll().data([0]).enter().append() pattern conditionally creates a single element if it isn't found. The cited example used data([true]) but any array with a single element will do.
To filter the nested data from within your function, change your call to data() and pass a filtered subset of the selection data like this:
var rows = tbody.selectAll('tr').data(tbody.data()[0].filter(function(d) {
return d.price > 1400;
}));
Good luck!

Related

Send single email containing a table based on a condition to the recipients when the names are repetitive using google app script

This is the extended version of my previous question.
I want to send email once in a week to the recipients based on the status column.
Sheet Link: https://docs.google.com/spreadsheets/d/1GC59976VwB2qC-LEO2sH3o2xJaMeXfXLKdfOjRAQoiI/edit#gid=1546237372
The previous code is attached in the sheet.
From the sheet, When the Status column will be new and ongoing, a table will be generated with column Title, Link and due date and send a single email to the recipients even they are repeated.
In the sheet, For resource Anushka, Status New appeared twice and Ongoing once. The table will be like-
Anushka || New || 10/25/2022
Anushka || New || 10/25/2022
Anushka || Ongoing || 10/25/2022
And after creating it, it will send single email to each recipients though they have appeared several times.
I have done it for getting multiple emails whatever the status is with the help of another commenter from stackflow but I want to modify it and change it. The code for this one is a bit longer as I have two helper gs file, html table code and the main one. That's why I am not writing all the codes here. But in the sheet from the extension, one can see my code.
If anyone give me suggestions how to change or modify the logic, it will be appreciated.
Code
function macro(){
// get range of cell with data from A1 to any cell near having value (call data region)
var table = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Sheet3").getRange("A1").getDataRegion();
var header=table.getValues()
var resource=header[0][1]
var status_r=header[0][3]
var due_date_r=header[0][5]
var link_r=header[0][10]
// create custom table filtered by the column having header "State" where the value match "New"
var filterTable = new TableWithHeaderHelper(table)
.getTableWhereColumn("Status").matchValueRegex(/(New)/);
// for each row matching the criteria
for(var i=0; i< filterTable.length() ; i++){
// Get cell range value at column Mail
var mail = filterTable.getWithinColumn("Email").cellAtRow(i).getValue();
// Any other value of column Target Value
var resource_col = filterTable.getWithinColumn("Resource").cellAtRow(i).getValue();
var status_col = filterTable.getWithinColumn("Status").cellAtRow(i).getValue();
var due_date_col = filterTable.getWithinColumn("Due Date").cellAtRow(i).getValue();
var link_col = filterTable.getWithinColumn("Link").cellAtRow(i).getValue();
var new_data=[[resource_col,status_col,due_date_col,link_col]]
var htmltemplate=HtmlService.createTemplateFromFile("email")
htmltemplate.resource=resource
htmltemplate.status_r=status_r
htmltemplate.due_date_r=due_date_r
htmltemplate.link_r=link_r
htmltemplate.new_data=new_data
var htmlformail=htmltemplate.evaluate().getContent()
var subjectMail = "Automation Support Services Actions Items";
var dt1 = new Date()
var dt2 = due_date_col
// get milliseconds
var t1 = dt1.getTime()
var t2 = dt2.getTime()
var diffInDays = Math.floor((t1-t2)/(24*3600*1000));
// 24*3600*1000 is milliseconds in a day
console.log(diffInDays);
// Send email
MailApp.sendEmail({
to:mail ,
subject: subjectMail,
htmlBody:htmlformail,
});
}
}```
2 loops can make the job.
// get range of cell with data from A1 to any cell near having value (call data region)
var table = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Sheet3").getRange("A1").getDataRegion();
// init html header data
var header=table.getValues()
var resource=header[0][1]
var status_r=header[0][3]
var due_date_r=header[0][5]
var link_r=header[0][10]
var listOfEmails = [];
var tableWithHeader = new TableWithHeaderHelper(table)
// get all email
for(var i=0; i< tableWithHeader.length() ; i++){
var mail = tableWithHeader.getWithinColumn("Email").cellAtRow(i).getValue();
listOfEmails.push(mail)
}
// filter all email to get unique liste of email
var uniqueMailList = listOfEmails.filter((c, index) => {
return listOfEmails.indexOf(c) === index;
});
for(var i=0; i< uniqueMailList.length; i++){
// get mail of target i
var mail = uniqueMailList[i]
// filter table using mail of target i and status
var mailTable = new TableWithHeaderHelper(table)
.getTableWhereColumn("Status").matchValueRegex(/(New)/)
.getTableWhereColumn("Email").matchValue(mail);
// initialise html template
var htmltemplate=HtmlService.createTemplateFromFile("email")
htmltemplate.resource=resource
htmltemplate.status_r=status_r
htmltemplate.due_date_r=due_date_r
htmltemplate.link_r=link_r
var new_data = []
var htmlformail
// loop into the filtered table of target i only
for(var j=0; j< mailTable.length() ; j++){
// Any other value of column Target Value
var resource_col = mailTable.getWithinColumn("Resource").cellAtRow(j).getValue();
var status_col = mailTable.getWithinColumn("Status").cellAtRow(j).getValue();
var due_date_col = mailTable.getWithinColumn("Due Date").cellAtRow(j).getValue();
var link_col = mailTable.getWithinColumn("Link").cellAtRow(j).getValue();
new_data.push([resource_col,status_col,due_date_col,link_col])
}
htmltemplate.new_data=new_data
htmlformail=htmltemplate.evaluate().getContent()
var subjectMail = "Automation Support Services Actions Items";
var dt1 = new Date()
var dt2 = due_date_col
// get milliseconds
var t1 = dt1.getTime()
var t2 = dt2.getTime()
var diffInDays = Math.floor((t1-t2)/(24*3600*1000));
// 24*3600*1000 is milliseconds in a day
console.log(diffInDays);
// Send email
MailApp.sendEmail({
to:mail ,
subject: subjectMail,
htmlBody:htmlformail,
});
}
I'm not confident on the new_data.push([resource_col,status_col,due_date_col,link_col]), it's seems to be corect but I have no no way to verify that
Anyway thanks for using the utils script at https://github.com/SolannP/UtilsAppSsript, glad to see it help 👍

Add future date in Google script and Goggle Sheet

I use Google sheet and Cryptofinance.ai to retreive cryptocurrency prices. I have one script that works well : it appends data periodically from one row in tab A to save them in a tab B so I can make graphs and charts.
Now I'd like to add date + 1 year in the next row. Idealy, each time the script is triggered, it make two new rows : The one with the data as it is now and the one with the date + 1 year.
If you curious and want to know why I want to do that is is to make projection price using this
formula in another tab : =TREND(filter(B2:B,B2:B<>""),filter(A2:A,B2:B<>""),filter(A2:A,(N(B2:B)=0)*(A2:A>0)))
Here is my script now:
// [START modifiable parameters]
var rangeToLog = 'Portefeuille!A28:L28';
var sheetToLogTo = 'Archive BTC/USD KRAKEN';
// [END modifiable parameters]
////////////////////////////////
/**
* Appends a range of values to the end of an archive sheet.
* A timestamp is inserted in column A of each row on the archive sheet.
* All values in rangeToLog go to one row on the archive sheet.
*
* #OnlyCurrentDoc
*/
function appendValuesToArchiveBTCUSDKRAKENSheet() {
// version 1.4, written by --Hyde, 30 January 2020
// - use Array.prototype.some() to skip empty rows when concating
// - see https://support.google.com/docs/thread/27095918?msgid=27148911
// version 1.3, written by --Hyde, 26 January 2020
// - see https://support.google.com/docs/thread/26760916
var ss = SpreadsheetApp.getActive();
var valuesToLog = ss.getRange(rangeToLog).getValues();
var logSheet = ss.getSheetByName(sheetToLogTo);
if (!logSheet) {
logSheet = ss.insertSheet(sheetToLogTo);
logSheet.appendRow(['Date time', 'Data']);
}
var rowToAppend = [new Date()].concat(
valuesToLog.reduce(function concatArrays_(left, right) {
var arrayContainsData = right.some(function isNonBlanky_(element, index, array) {
return element !== null && element !== undefined && element !== '';
});
return arrayContainsData ? left.concat(right) : left;
})
);
logSheet.appendRow(rowToAppend);
}
NOW :
What I want to do:
The easy fix is to simply add another appendRow() call at the end of your function with the one year from now value.
function appendValuesToArchiveBTCUSDKRAKENSheet() {
// ...
logSheet.appendRow(rowToAppend);
logSheet.appendRow([new Date(new Date().setFullYear(new Date().getFullYear() + 1))]);
}
A more complex solution, but with better execution time, would have you print both rows in a single setValues() call. This follows the best practice of using batch operations, but I suspect that the easier solution above is adequate for your purpose. I do encourage you, however, to try implementing the batch operation if you want to improve your apps script skills.
Finally, after some research I came to this :
function expCalc(){
delLastNRows();
appendValuesToArchiveBTCUSDKRAKENSheet();
}
function delLastNRows(n){
var n=n || 1;//allows you to delete last three rows without passing function a parameter.
var ss=SpreadsheetApp.getActive();
var sh=ss.getSheetByName('Archive BTC/USD KRAKEN');
var lr=sh.getLastRow();
if(lr>=n){
for(var i=0;i<n;i++){
sh.deleteRow(sh.getLastRow());
}
}
}
// [START modifiable parameters]
var rangeToLog = 'Portefeuille!A28:L28';
var sheetToLogTo = 'Archive BTC/USD KRAKEN';
// [END modifiable parameters]
////////////////////////////////
/**
* Appends a range of values to the end of an archive sheet.
* A timestamp is inserted in column A of each row on the archive sheet.
* All values in rangeToLog go to one row on the archive sheet.
*
* #OnlyCurrentDoc
*/
function appendValuesToArchiveBTCUSDKRAKENSheet() {
// version 1.4, written by --Hyde, 30 January 2020
// - use Array.prototype.some() to skip empty rows when concating
// - see https://support.google.com/docs/thread/27095918?msgid=27148911
// version 1.3, written by --Hyde, 26 January 2020
// - see https://support.google.com/docs/thread/26760916
var ss = SpreadsheetApp.getActive();
var valuesToLog = ss.getRange(rangeToLog).getValues();
var logSheet = ss.getSheetByName(sheetToLogTo);
var sheet = ss.getSheets()[0]
if (!logSheet) {
logSheet = ss.insertSheet(sheetToLogTo);
logSheet.appendRow(['Date time', 'Data']);
}
var rowToAppend = [new Date()].concat(
valuesToLog.reduce(function concatArrays_(left, right) {
var arrayContainsData = right.some(function isNonBlanky_(element, index, array) {
return element !== null && element !== undefined && element !== '';
});
return arrayContainsData ? left.concat(right) : left;
})
);
logSheet.appendRow(rowToAppend);
logSheet.appendRow([new Date(new Date().setFullYear(new Date().getFullYear() + 1))]);
}
Works like a charm !

How to return all entries from specific date in log.nsf

I need to return all entries (collection) from a specific date from the miscellaneus view in log.nsf using SSJS.
The views first category is "text" and the second category is a "date".
I tried to use the methods getAllEntriesByKey(vector) or createViewNavFromCategory(vector) but I got kind of stuck as the categorized columns contain different data types.
how can I do that?
Here is one thing I tried
var logdb = sessionAsSigner.getDatabase("domino01/....","log.nsf");
var logView = logdb.getView("MiscEvents")
var v = new java.util.Vector()
var nav = logView.createViewNavFromCategory("domino01/...\\2019-02-15")
return nav.getCount()
and here is another
var logdb = sessionAsSigner.getDatabase("domino01/...","log.nsf");
var logView = logdb.getView("MiscEvents")
var v = new java.util.Vector()
v.add("domino01/...")
v.add(session.createDateTime("Today").getDateOnly())
var nav = logView.getAllEntriesByKey(v)
return nav.getCount()
Just remove the getDateOnly call from your 2nd example code.
v.add(session.createDateTime("Today"))

BIRT - creating a temporary table

I am building a BIRT report based on a dynamic table. I have a function that needs to return a table with variable number of columns of different types, so we have decided that this function will create a temporary table and just return its name.
So in the BIRT beforeFactory I am running the query that calls this function and then I am trying to read from the table of returned name and create a report table dynamically based on the number of columns and types. Two questions:
1. Is there a better way to return a table with variable number of columns? (this cannot be all the possible columns that I later filter as that would exceed maximum number of allowed columns)?
2. How to make Birt to see my temporary table? Here is the code, that I run in beforeFactory. Apparenty the table does not exists when calling second query.
importPackage(Packages.java.lang);
importPackage(Packages.org.eclipse.birt.report.data.adapter);
importPackage(Packages.org.eclipse.birt.report.data.adapter.api);
importPackage(Packages.org.eclipse.birt.report.data.adapter.impl);
importPackage(Packages.org.eclipse.birt.report.model.api);
importPackage(Packages.org.eclipse.birt.data.engine.api.querydefn);
//Get Data Source
var dataSource =
reportContext.getDesignHandle().getDesign().findDataSource("mydb");
//Create Data Set for data table name
var elementFactory =
reportContext.getReportRunnable().designHandle.getElementFactory();
var dataSet = elementFactory.newOdaDataSet("tableName",
"org.eclipse.birt.report.data.oda.jdbc.JdbcSelectDataSet");
dataSet.setDataSource(dataSource.getName());
dataSet.setQueryText("select table_name from my_export_function('str1',
'str2');");
reportContext.getDesignHandle().getDataSets( ).add(dataSet);
//Create Data Session
var myconfig =
reportContext.getReportRunnable().getReportEngine().getConfig();
var des = DataRequestSession.newSession(myconfig, new DataSessionContext(3));
var dset = reportContext.getDesignHandle().findDataSet("tableName");
des.defineDataSource(des.getModelAdaptor()
.adaptDataSource(reportContext.getDesig nHandle()
.findDataSource("mydb")));
des.defineDataSet(des.getModelAdaptor()
.adaptDataSet(reportContext.getDesignHandle()
.findDataSet("tableName")));
//Query Definition
queryDefinition = new QueryDefinition();
queryDefinition.setDataSetName(dataSet.getName());
queryDefinition.setAutoBinding(true);
var pq = des.prepare(queryDefinition);
var qr = pq.execute(null);
var ri = qr.getResultIterator();
var tableName = "";
while (ri.next()) {
tableName = ri.getString("table_name");
}
var dataSet2 = elementFactory.newOdaDataSet("reportData",
"org.eclipse.birt.report.data.oda.jdbc.JdbcSelectDataSet");
dataSet2.setDataSource(dataSource.getName());
dataSet2.setQueryText("select * from " + tableName + ";");
System.out.println("TTTT: " + dataSet2.getQueryText());
reportContext.getDesignHandle().getDataSets( ).add(dataSet2);
//Query Definition
queryDefinition2 = new QueryDefinition();
queryDefinition2.setDataSetName(dataSet2.getName());
queryDefinition2.setAutoBinding(true);
var pq2 = des.prepare(queryDefinition2);
var qr2 = pq2.execute(null);
var ri2 = qr2.getResultIterator( );
var cc = ri2.getResultMetaData().getColumnCount();
System.out.println("col_01_name: " +
ri2.getResultMetaData().getColumnLabel(1));
System.out.println("Count: " + cc);
while (ri2.next()) {
System.out.println("Table: "+ ri2);
System.out.println("col_01: "+ ri2.getValue("col_01"));
}
ri.close();
qr.close();
ri2.close();
qr2.close();
des.close();
I managed to make the code work. Following lines solved the problem with second query:
var des2 = DataRequestSession.newSession(myconfig, new DataSessionContext(3));
des2.defineDataSource(des2.getModelAdaptor()
.adaptDataSource(reportContext.getDesig nHandle()
.findDataSource("mydb")));
des2.defineDataSet(des2.getModelAdaptor()
.adaptDataSet(reportContext.getDesignHandle()
.findDataSet("tableName")));
and then:
var pq2 = des2.prepare(queryDefinition2);
Also we have changed the function to return the query instead of creating a temporary table.

How can I delete a row from a Table in SAPUI5 Application when I used Model as XMLModel?

I have created SAPUI5 application, in that I have loaded data from external .xml file into a table, it was fine. Now, I am trying to delete a specific row from that table.
For this purpose, I use this code:
var oModel = new sap.ui.model.xml.XMLModel();
oModel.loadData("Deployments.xml", "", false);
sap.ui.getCore().setModel(oModel);
oTable.bindRows("/service"); // here "service" is the root element of xml file
var oTable = new sap.ui.commons.Button({
text: "Delete Service",
press: function() {
var idx = oTable.getSelectedIndex();
if (idx !== -1) {
var m = oTable.getModel();
var data = m.getData();
var removed = data.splice(idx, 1); // error showing at this line
m.setData(data);
sap.m.MessageToast.show(JSON.stringify(removed[0]) + 'is removed');
} else {
sap.m.MessageToast.show('Please select a row');
}
}
});
But, I am getting error at the line: var removed = data.splice(idx, 1);. However, the same code is good for when model is JSON. How can I delete a specific row from a table when model XMLModel?
It is a lot easier an more reliable to use a Bindings BindingPath to manipulate data belonging to a particular binding. Here is your adapted sample for a XMLModel:
press: function() {
var iIdx = oTable.getSelectedIndex();
var sPath = oTable.getContextByIndex(iIdx).getPath();
var oObj = oTable.getModel().getObject(sPath);
oObj.remove();
oTable.getModel().refresh();
}
This way you save the hazzle of dealing with the XML structure and furthermore this will scale with any change in the binding path you might introduce in the future.
BR
Chris
var data = m.getData();
data is not an Array. It is a XML document.
To remove an entry from the document:
var root = data.childNodes[0];
var aEntry = root.getElementsByTagName("entry");
root.removeChild(aEntry[idx]);