Add quantity for expense sublist on line commit - suitescript2.0

I have an expense report transaction where I am trying to sum the quantity field on the sublist and update a custom field (i.e. basically a running total that is updated every time a line is committed)
This is a part of the client script that I have so far:
function sublistChanged(context) {
var currentRecord = context.currentRecord;
var sublistName = context.sublistId;
var sublistFieldName = context.fieldId;
var op = context.operation;
totalExpQuantity = 0;
if (sublistName === "expense") {
for (var i = 0; i < sublistName.length; i++) {
var quantity = currentRecord.getCurrentSublistValue({
sublistId: "expense",
fieldId: "quantity",
});
log.debug("quantity", quantity);
}
totalExpQuantity += parseInt(quantity);
var lineCount = currentRecord.getLineCount({
sublistId: "expense",
});
console.log(lineCount);
currentRecord.setValue({
fieldId: "custbody_mileage_exp_report",
value:
"Total has changed to " +
currentRecord.getValue({
fieldId: "amount",
}) +
" with operation: " +
op +
" total quantity: " +
totalExpQuantity,
});
}
}
Every time a line is committed with a quantity value, I want the total to increment by the value of the line that was committed
i.e. here, after every line is committed, the total should go from 10,000, to 10,500 to 15,500
I have tried a variation of this code where the following line is part of the 'for loop':
totalExpQuantity += parseInt(quantity);
i.e.
for (var i = 0; i < sublistName.length; i++) {
var quantity = currentRecord.getCurrentSublistValue({
sublistId: "expense",
fieldId: "quantity",
});
totalExpQuantity += parseInt(quantity);
log.debug("quantity", quantity);
}
I get the following result:
Is what I am trying to do possible?
Where am I going wrong in the code? Should it be a different entry point?
I have also tried postsourcing though it didn't retrieve the quantity field and returned a blank
Found the answer. This is the working version of the script:
/**
*#NApiVersion 2.x
*#NScriptType ClientScript
*/
define(["N/search"], function (search) {
function fieldChanged(context) {
try {
var recordObj = context.currentRecord;
var employeeObj = parseInt(
recordObj.getValue({
fieldId: "entity",
})
);
if (context.fieldId == "entity") {
var employeeName = parseInt(employeeObj);
log.debug("employee", employeeName);
var expensereportSearchObj = search.create({
type: "expensereport",
filters: [
["type", "anyof", "ExpRept"],
"AND",
["expensecategory", "anyof", "8"],
"AND",
["expensedate", "within", "thisfiscalyear"],
"AND",
["employee", "anyof", employeeObj],
],
columns: [
search.createColumn({
name: "entityid",
join: "employee",
label: "Name",
}),
search.createColumn({ name: "tranid", label: "Document Number" }),
search.createColumn({
name: "expensecategory",
label: "Expense Category",
}),
search.createColumn({ name: "expensedate", label: "Expense Date" }),
search.createColumn({ name: "currency", label: "Currency" }),
search.createColumn({
name: "quantity",
join: "expenseDetail",
sort: search.Sort.ASC,
label: "Quantity",
}),
],
});
var searchResult = expensereportSearchObj
.run()
.getRange({ start: 0, end: 1000 });
log.debug("result", JSON.stringify(searchResult));
var searchResultCount = expensereportSearchObj.runPaged().count;
log.debug("expensereportSearchObj result count", searchResultCount);
let q = 0;
for (var i = 0; i < searchResult.length; i++) {
var quantity = searchResult[i].getValue(searchResult[i].columns[5]);
log.debug("quantity", quantity);
q += parseInt(quantity);
log.debug("q", q);
}
recordObj.setValue({
fieldId: "custbody_employee_mileage_ytd",
value: q,
});
}
//loop through all results add +- to 0. see video for sub;ists
} catch (error) {
log.debug(
error.name,
"recordObjId: " +
recordObj +
", employee:" +
employeeName +
", message: " +
error.message +
", cause: " +
error.cause
);
}
}
function sublistChanged(context) {
var recordObj = context.currentRecord;
var sublistName = recordObj.getSublist({ sublistId: "expense" });
var lineCount = recordObj.getLineCount({ sublistId: "expense" });
var totalQuantity = 0;
for (i = 0; i < lineCount; i++) {
var expenseCategory = recordObj.getSublistValue({
sublistId: "expense",
fieldId: "category",
line: i,
});
log.debug("expenseCategory", expenseCategory);
var expenseQuantity = recordObj.getSublistValue({
sublistId: "expense",
fieldId: "quantity",
line: i,
});
if (expenseCategory == 8) {
totalQuantity += expenseQuantity;
}
recordObj.setValue({
fieldId: "custbody_mileage_exp_report",
value: totalQuantity,
});
}
}
return {
fieldChanged: fieldChanged,
sublistChanged: sublistChanged,
};
});

Related

Suitescript 2.0-Why is a new line inserted into a sublist on record edit

I have the following client script deployed onto a vendor bill
/**
*#NApiVersion 2.x
*#NScriptType ClientScript
*/
define(["N/record"], function (record) {
/**
* #param {ClientScriptContext.fieldChanged} context
*/
function fieldChanged(context) {
//the if condition ensures the field changed trigger does not execute on changes in sublist. setting this to
//!context.sublistid ensures changes in department field and location field for the sublist do not trigger the script
if (
!context.sublistId &&
(context.fieldId == "department" || context.fieldId == "location")
) {
log.debug("sublistId", context.sublistId)
log.debug("fieldId", context.fieldId)
var recordObj = context.currentRecord
var departmentMain = recordObj.getValue({ fieldId: "department" })
var locationMain = recordObj.getValue({ fieldId: "location" })
var expenseSublist = recordObj.getSublist({ sublistId: "expense" })
var expenseSublistLength = recordObj.getLineCount({
sublistId: "expense",
})
log.debug("department", departmentMain)
log.debug("location", locationMain)
log.debug("expenseSublist.length", expenseSublistLength)
for (var i = 0; i < expenseSublistLength; i++) {
recordObj.selectLine({
sublistId: "expense",
line: i,
})
var departmentLine = recordObj.setCurrentSublistValue({
sublistId: "expense",
fieldId: "department",
value: departmentMain,
ignoreFieldChange: true,
})
var locationLine = recordObj.setCurrentSublistValue({
sublistId: "expense",
fieldId: "location",
value: locationMain,
ignoreFieldChange: true,
})
recordObj.commitLine({ sublistId: "expense" })
log.debug("department", departmentLine)
log.debug("location", locationLine)
}
}
}
/**
* #param {ClientScriptContext.postSourcing} context
*/
function postSourcing(context) {
var currentRecord = context.currentRecord
var sublistName = context.sublistId
var sublistFieldName = context.fieldId
var line = context.line
var departmentMain = currentRecord.getValue({ fieldId: "department" })
var locationMain = currentRecord.getValue({ fieldId: "location" })
if (sublistName === "expense" && sublistFieldName === "account") {
var expenseSublist = currentRecord.getSublist({ sublistId: "expense" })
var expenseSublistLength = currentRecord.getLineCount({
sublistId: "expense",
})
log.debug("department-main", departmentMain)
log.debug("location-main", locationMain)
currentRecord.setCurrentSublistValue({
sublistId: "expense",
fieldId: "department",
value: departmentMain,
ignoreFieldChange: false,
})
currentRecord.setCurrentSublistValue({
sublistId: "expense",
fieldId: "location",
value: locationMain,
ignoreFieldChange: false,
})
}
}
return {
fieldChanged: fieldChanged,
postSourcing: postSourcing,
}
})
When I edit an existing vendor bill record and attempt to save, I get the following prompt
It looks like placing the record into edit mode has also inserted a new blank line into the existing sublist despite no fields being clicked
Why would that be the case? Based on the above script, my expectation is that the postsourcing and field changed functions should only occur if the relevant fields (department and location for field changed and account for post sourcing) are clicked.
Is there something my script is missing?

SuiteScript Workflow Action error SSS_USAGE_LIMIT_EXCEEDED and email not attaching to related records

I am receiving the following error when I try to process more than approx 15 transactions:
Error: SSS_USAGE_LIMIT_EXCEEDED
{"type":"error.SuiteScriptError","name":"SSS_USAGE_LIMIT_EXCEEDED","message":"Script Execution Usage Limit Exceeded","stack":["createError(N/error)","onAction(/SuiteScripts/sdf_ignore/Send Remittance PFA Workflow Action.js:84)"],"cause":{"type":"internal error","code":"SSS_USAGE_LIMIT_EXCEEDED","details":"Script Execution Usage Limit Exceeded","userEvent":null,"stackTrace":["createError(N/error)","onAction(/SuiteScripts/sdf_ignore/Send Remittance PFA Workflow Action.js:84)"],"notifyOff":false},"id":"","notifyOff":false,"userFacing":false}
The script is a workflow action script that is triggered when the user clicks on a button to send email remittance advice.
This works for the most part unless I exceed a certain number of transactions.
Do I need to use another script type?
Or can I modify the following script to reduce its governance usage?
/**
*#NApiVersion 2.x
*#NScriptType WorkflowActionScript
*/
define([
"N/search",
"N/record",
"N/render",
"N/file",
"N/xml",
"N/email",
], function (search, record, render, file, xml, email) {
function onAction(context) {
var fileObj = [];
var record = context.newRecord;
log.debug("record", record);
var batchId = record.getValue({ fieldId: "name" });
var id = record.id;
log.debug("recordid", record.id);
var vendorpaymentSearchObj = search.create({
type: "vendorpayment",
filters: [
["type", "anyof", "VendPymt"],
"AND",
["custbody_9997_pfa_record", "anyof", id],
],
columns: [
search.createColumn({
name: "transactionnumber",
summary: "GROUP",
label: "Transaction Number",
}),
search.createColumn({
name: "formulatext",
summary: "GROUP",
formula: "{entity}",
label: "Vendor",
}),
search.createColumn({
name: "formulatext",
summary: "GROUP",
formula:
"CASE WHEN {vendor.custentity_2663_email_address_notif} IS NULL THEN {vendor.email} ELSE {vendor.custentity_2663_email_address_notif} END",
label: "Email",
}),
search.createColumn({
name: "total",
summary: "SUM",
label: "Amount (Transaction Total)",
}),
search.createColumn({
name: "currency",
summary: "GROUP",
label: "Currency",
}),
search.createColumn({
name: "trandate",
summary: "GROUP",
sort: search.Sort.ASC,
label: "Date",
}),
search.createColumn({
name: "internalid",
summary: "GROUP",
label: "internalid",
}),
search.createColumn({
name: "internalid",
join: "vendor",
summary: "GROUP",
label: "Internal ID",
}),
search.createColumn({
name: "internalid",
summary: "GROUP",
label: "Internal ID",
}),
],
});
var searchResultCount = vendorpaymentSearchObj.runPaged().count;
log.debug("vendorpaymentSearchObj result count", searchResultCount);
vendorpaymentSearchObj.run().each(function (result) {
var emailAddress = result.getValue(result.columns[2]);
var transactionNumber = result.getValue(result.columns[0]);
var amount = result.getValue(result.columns[3]);
var date = result.getValue(result.columns[5]);
var vendor = result.getValue(result.columns[1]);
var resultId = result.getValue(result.columns[6]);
var vendorId = result.getValue(result.columns[7]);
var transactionId = result.getValue(result.columns[8]);
log.debug(
"emailAddress: ",
emailAddress +
" transaction bumber: " +
transactionNumber +
" amount: " +
amount +
" date: " +
date +
" vendor: " +
vendor +
" resultId " +
resultId +
" transactionId " +
transactionId
);
var pdfFile = render.transaction({
entityId: parseInt(resultId),
printMode: render.PrintMode.PDF,
formId: 109,
inCustLocale: true,
});
pdfFile.folder = 1351;
var fileId = pdfFile.save();
var pdffile2 = file.load({ id: fileId });
// var fileObj = file.load({ id: parseInt(fileId) });
var mergeResult = render.mergeEmail({
templateId: 8,
// entity: {
// type: "employee",
// id: parseInt(recipient),
// },
entity: {
type: "vendor",
id: parseInt(vendorId),
},
recipient: {
type: "vendor",
id: parseInt(vendorId),
},
supportCaseId: null,
transactionId: parseInt(resultId),
customRecord: null,
// {
// type: "customrecord_2663_entity_bank_details",
// id: parseInt(bankDetailsId),
// },
});
var emailSubject = mergeResult.subject;
var emailBody = mergeResult.body;
//create a placeholder in the original HTML with an element called NLID. This will replace that with a value that is part of the script
emailSubject = emailSubject.replace("NLVENDOR", vendor);
// emailBody = emailBody.replace("NLDOCNUMBER", bankDetailsId);
var emailString = JSON.stringify(emailAddress);
email.send({
author: -5,
// recipients: 2020,
recipients: emailAddress,
subject: emailSubject,
body: emailBody,
attachments: [pdffile2],
relatedRecords: {
entity: parseInt(vendorId),
customRecord: {
id: parseInt(id),
recordType: 'customrecord_2663_file_admin', //an integer value
},
// transactionId: 38326,
},
});
return true;
});
/*
vendorpaymentSearchObj.id="customsearch1658554258593";
vendorpaymentSearchObj.title="Bill Payments in a Payment Batch (copy)";
var newSearchId = vendorpaymentSearchObj.save();
*/
}
return {
onAction: onAction,
};
});
Another issue I am having with this script is the email.send method doesn't throw an error for the custom record but doesn't actually attach the email messages to the stated transaction type either. It doesn't allow me to attach the emails to a transaction at all (I get an 'unexpected error' if I do)
I finally got a map reduce working for this:
/**
*#NApiVersion 2.x
*#NScriptType MapReduceScript
*/
define([
"N/search",
"N/record",
"N/render",
"N/file",
"N/xml",
"N/email",
"N/runtime",
], function (search, record, render, file, xml, email, runtime) {
function getInputData(context) {
var scriptObj = runtime.getCurrentScript();
var recordId = scriptObj.getParameter("custscript_recordid");
// var record = context.newRecord;
// log.debug("record", record);
// var batchId = record.getValue({ fieldId: "name" });
var id = recordId;
log.debug("recordid", recordId);
var vendorpaymentSearchObj = search.create({
type: "vendorpayment",
filters: [
["type", "anyof", "VendPymt"],
"AND",
["custbody_9997_pfa_record", "anyof", id],
],
columns: [
//0
search.createColumn({
name: "transactionnumber",
summary: "GROUP",
label: "Transaction Number",
}),
//1
search.createColumn({
name: "formulatext",
summary: "GROUP",
formula: "{entity}",
label: "Vendor",
}),
//2
search.createColumn({
name: "formulatext",
summary: "GROUP",
formula:
"CASE WHEN {vendor.custentity_2663_email_address_notif} IS NULL THEN {vendor.email} ELSE {vendor.custentity_2663_email_address_notif} END",
label: "Email",
}),
//3
search.createColumn({
name: "total",
summary: "SUM",
label: "Amount (Transaction Total)",
}),
//4
search.createColumn({
name: "currency",
summary: "GROUP",
label: "Currency",
}),
//5
search.createColumn({
name: "trandate",
summary: "GROUP",
sort: search.Sort.ASC,
label: "Date",
}),
//6
search.createColumn({
name: "internalid",
join: "vendor",
summary: "GROUP",
label: "vendorId",
}),
//7
search.createColumn({
name: "internalid",
summary: "GROUP",
label: "searchResultId",
}),
],
});
log.debug(
"vendorpaymentSearchObj result count",
vendorpaymentSearchObj.runPaged().count
);
var vendorPayments = [];
vendorpaymentSearchObj.run().each(function (result) {
vendorPayments.push({
emailAddress: result.getValue(result.columns[2]),
transactionNumber: result.getValue(result.columns[0]),
amount: result.getValue(result.columns[3]),
date: result.getValue(result.columns[5]),
vendor: result.getValue(result.columns[1]),
resultId: result.getValue(result.columns[7]),
vendorId: result.getValue(result.columns[6]),
id: id,
// transactionId: result.getValue(result.columns[8]),
});
return true;
});
return vendorPayments;
}
/**
* #param {MapReduceContext.map} context
*/
function map(context) {
try {
log.debug("context", context);
const result = JSON.parse(context.value);
log.debug("result", result);
var emailAddress = result.emailAddress;
var transactionNumber = result.transactionNumber;
var amount = result.amount;
var date = result.date;
var vendor = result.vendor;
var resultId = result.resultId;
var vendorId = result.vendorId;
var id = result.id;
// var transactionId = result.transactionId;
log.debug(
"emailAddress: ",
emailAddress +
" transaction bumber: " +
transactionNumber +
" amount: " +
amount +
" date: " +
date +
" vendor: " +
vendor +
" resultId " +
resultId +
// " transactionId " +
// transactionId +
"vendorId " +
vendorId
);
// for (var i = 0; i < context.value.length; i++) {
var pdfFile = render.transaction({
entityId: parseInt(resultId),
printMode: render.PrintMode.PDF,
formId: 109,
});
pdfFile.folder = 1351;
var fileId = pdfFile.save();
var pdffile2 = file.load({ id: fileId });
// context.write({
// key: context.value[i],
// value: [
// [pdffile2],
// pdfFile,
// fileId,
// emailAddress,
// transactionNumber,
// amount,
// date,
// vendor,
// resultId,
// vendorId,
// transactionId,
// ],
// });
log.debug("fileid: " + fileId + pdfFile + pdffile2);
context.write({
key: resultId,
value: JSON.stringify({
vendorId: vendorId,
vendor: vendor,
fileId: fileId,
emailAddress: emailAddress,
id: id,
}),
});
// }
} catch (ex) {
log.error("Error on map", ex.message + ex.error);
}
}
// var fileObj = file.load({ id: parseInt(fileId) });
/**
* #param {MapReduceContext.reduce} context
*/
function reduce(context) {
try {
var reduceResults = context.values; //note: context.values and not context.value unlike the earlier stages. Also, this is not JSON.Parse
log.debug("reduceResults", reduceResults);
var pdffile2 = [];
for (var i = 0; i < reduceResults.length; i++) {
//note: this is context.value(S)
log.debug("vendorId", JSON.parse(reduceResults[i]).vendorId);
log.debug("key", context.key);
if (reduceResults) {
var mergeResult = render.mergeEmail({
templateId: 8,
// entity: {
// type: "employee",
// id: parseInt(recipient),
// },
entity: {
type: "vendor",
id: parseInt(JSON.parse(reduceResults[i]).vendorId),
},
recipient: {
type: "vendor",
id: parseInt(JSON.parse(reduceResults[i]).vendorId),
},
supportCaseId: null,
transactionId: parseInt(context.key),
customRecord: null,
});
log.debug("mergeResult", mergeResult);
var emailSubject = mergeResult.subject;
var emailBody = mergeResult.body;
log.debug("email body", emailBody);
var pdf = file.load({
id: JSON.parse(reduceResults[i]).fileId,
});
var vendorName = JSON.parse(reduceResults[i]).vendor;
log.debug("vendorname", vendorName);
pdffile2.push(pdf);
//create a placeholder in the original HTML with an element called NLVENDOR. This will replace that with a value that is part of the script
var emailSubjectNew = emailSubject.replace("NLVENDOR", vendorName);
log.debug("email subject", emailSubjectNew);
var emailString = JSON.parse(reduceResults[i]).emailAddress;
log.debug("emailstring", emailString);
email.send({
author: -5,
recipients: JSON.parse(reduceResults[i]).emailAddress,
subject: emailSubjectNew,
body: emailBody,
attachments: pdffile2,
relatedRecords: {
entity: parseInt(JSON.parse(reduceResults[i]).vendorId),
transactionId: parseInt(context.key),
},
});
}
}
} catch (ex) {
log.error("Error on reduce", ex.message + "" + ex.name);
}
}
/**
* #param {MapReduceContext.summarize} context
*/
function summarize(summary) {
log.debug("context", summary);
summary.output.iterator().each(function (key, value) {
contents += key + " " + value + "\n";
return true;
});
}
return {
getInputData: getInputData,
map: map,
reduce: reduce,
summarize: summarize,
};
});
To me this operation seems more suitable for a Map/Reduce if you don't know how many results the search will have. Meaning if it always rendered one PDF and sent it so no need for a MR but if the amount is unknown so a Map/Reduce is the way to go.
If you still want to try and reduce usage on this script you can try:
Not saving and loading the PDF. Instead just generate it and send it. (assuming this is not a requirement)
You can try adding search criteria to narrow down the query
I'm doubtful that either of these will reduce the usage enough to make a real difference. I would look into the N/task module.

How to write SuiteScript for map reduce CSV file or JSON data as an input process it and Create a customer Record

Map Reduce script for Csv file or JSON Data as input process it and create a customer record
Suite Answer 43795 has an excellent "Processing Invoices Example" sample script that you can work from, it is also pasted below. Suite Answer 43795, also has additional information for each map/reduce stage.
/**
* #NApiVersion 2.x
* #NScriptType MapReduceScript
*/
define(['N/search', 'N/record', 'N/email', 'N/runtime', 'N/error'],
function(search, record, email, runtime, error){
function handleErrorAndSendNotification(e, stage){
log.error('Stage: ' + stage + ' failed', e);
var author = -5;
var recipients = 'notify#xxxxxx.com';
var subject = 'Map/Reduce script ' + runtime.getCurrentScript().id + ' failed for stage: ' + stage;
var body = 'An error occurred with the following information:\n' + 'Error code: ' + e.name + '\n' + 'Error msg: ' + e.message;
email.send({
author: author,
recipients: recipients,
subject: subject,
body: body
});
}
function handleErrorIfAny(summary){
var inputSummary = summary.inputSummary;
var mapSummary = summary.mapSummary;
var reduceSummary = summary.reduceSummary;
if (inputSummary.error)
{
var e = error.create({
name: 'INPUT_STAGE_FAILED',
message: inputSummary.error
});
handleErrorAndSendNotification(e, 'getInputData');
}
handleErrorInStage('map', mapSummary);
handleErrorInStage('reduce', reduceSummary);
}
function handleErrorInStage(stage, summary){
var errorMsg = [];
summary.errors.iterator().each(function(key, value){
var msg = 'Failure to accept payment from customer id: ' + key + '. Error was: ' + JSON.parse(value).message + '\n';
errorMsg.push(msg);
return true;
});
if (errorMsg.length > 0)
{
var e = error.create({
name: 'RECORD_TRANSFORM_FAILED',
message: JSON.stringify(errorMsg)
});
handleErrorAndSendNotification(e, stage);
}
}
function createSummaryRecord(summary){
try{
var seconds = summary.seconds;
var usage = summary.usage;
var yields = summary.yields;
var rec = record.create({
type: 'customrecord_summary',
});
rec.setValue({
fieldId : 'name',
value: 'Summary for M/R script: ' + runtime.getCurrentScript().id
});
rec.setValue({
fieldId: 'custrecord_time',
value: seconds
});
rec.setValue({
fieldId: 'custrecord_usage',
value: usage
});
rec.setValue({
fieldId: 'custrecord_yields',
value: yields
});
rec.save();
} catch(e){
handleErrorAndSendNotification(e, 'summarize');
}
}
function applyLocationDiscountToInvoice(recordId){
var invoice = record.load({
type: record.Type.INVOICE,
id: recordId,
isDynamic: true
});
var location = invoice.getText({
fieldId: 'location'
});
var discount;
if (location === 'East Coast')
discount = 'Eight Percent';
else if (location === 'West Coast')
discount = 'Five Percent';
else if (location === 'United Kingdom')
discount = 'Nine Percent';
else
discount = '';
invoice.setText({
fieldId: 'discountitem',
text: discount,
ignoreFieldChange : false
});
log.debug(recordId + ' has been updated with location-based discount.');
invoice.save();
}
function getInputData(){
return search.create({
type: record.Type.INVOICE,
filters: [['status', search.Operator.IS, 'open']],
columns: ['entity'],
title: 'Open Invoice Search'
});
}
function map(context){
var searchResult = JSON.parse(context.value);
var invoiceId = searchResult.id;
var entityId = searchResult.values.entity.value;
applyLocationDiscountToInvoice(invoiceId);
context.write({
key: entityId,
value: invoiceId
});
}
function reduce(context){
var customerId = context.key;
var custPayment = record.transform({
fromType: record.Type.CUSTOMER,
fromId: customerId,
toType: record.Type.CUSTOMER_PAYMENT,
isDynamic: true
});
var lineCount = custPayment.getLineCount('apply');
for (var j = 0; j < lineCount; j++){
custPayment.selectLine({
sublistId: 'apply',
line: j
});
custPayment.setCurrentSublistValue({
sublistId: 'apply',
fieldId: 'apply',
value: true
});
}
var custPaymentId = custPayment.save();
context.write({
key: custPaymentId
});
}
function summarize(summary){
handleErrorIfAny(summary);
createSummaryRecord(summary);
}
return {
getInputData: getInputData,
map: map,
reduce: reduce,
summarize: summarize
};
});

Dynamically update lines in Highcharts time series chart

Here I'm working on Highcharts time series chart with live streaming data, based on the sample jsfiddle. In the fiddle there shows 4 lines named as input1, input2, input3, & input 4 and it is updated with live random data but in my actual project the input values are updated via MQTT. In actual project, sometimes, when we push streaming data, there will be increase or decrease in no: of inputs (such as input5, input6 like wise). So how can we add new line or remove line dynamically in time series chart with streaming data.
javascript code :
$(function() {
$(document).ready(function() {
Highcharts.setOptions({
global: {
useUTC: false
}
});
$('#container').highcharts({
chart: {
type: 'spline',
animation: Highcharts.svg, // don't animate in old IE
marginRight: 10,
events: {
load: function() {
// set up the updating of the chart each second
var series = this.series;
var length = series.length;
setInterval(function() {
var x = (new Date()).getTime(), // current time
a0 = Math.random();
a1 = Math.random();
a2 = Math.random();
series[0].addPoint([x, Math.random()], true, true);
for (var i = 1; i < length; i++) {
series[i].addPoint([x, Math.random()], false, true);
}
}, 1000);
}
}
},
title: {
text: 'Live random data'
},
legend: {
enabled: true
},
xAxis: {
type: 'datetime',
tickPixelInterval: 150
},
yAxis: {
title: {
text: 'Value'
},
plotLines: [{
value: 0,
width: 1,
color: '#808080'
}]
},
tooltip: {
formatter: function() {
return '<b>' + this.series.name + '</b><br/>' +
Highcharts.dateFormat('%Y-%m-%d %H:%M:%S', this.x) + '<br/>' +
Highcharts.numberFormat(this.y, 2);
}
},
exporting: {
enabled: false
},
series: [{
name: 'input1',
data: (function() {
// generate an array of random data
var data = [],
time = (new Date()).getTime(),
i;
for (i = -19; i <= 0; i += 1) {
data.push({
x: time + i * 1000,
y: Math.random()
});
}
return data;
}())
}, {
name: 'input2',
data: (function() {
// generate an array of random data
var data = [],
time = (new Date()).getTime(),
i;
for (i = -19; i <= 0; i += 1) {
data.push({
x: time + i * 1000,
y: Math.random()
});
}
return data;
}())
}, {
name: 'input3',
data: (function() {
// generate an array of random data
var data = [],
time = (new Date()).getTime(),
i;
for (i = -19; i <= 0; i += 1) {
data.push({
x: time + i * 1000,
y: Math.random()
});
}
return data;
}())
}, {
name: 'input4',
data: (function() {
// generate an array of random data
var data = [],
time = (new Date()).getTime(),
i;
for (i = -19; i <= 0; i += 1) {
data.push({
x: time + i * 1000,
y: Math.random()
});
}
return data;
}())
}]
});
});
});

Highcharts - Lazy loading combined with SQL queries

Based on the Lazy Loading Example I wanted to create the similar function for my Highcharts.StockChart. But with the aid of Ajax.
Some words about the database structure:
Tables contains the values of 1 month
Column 'tijd' contains a timestamp
Values are posted every minute to the database, resolution = 1 minute
Please focus on why the zoom function isn't updating the chart. This drives me #!#
For the first time generation of the chart I use this code, which works fine
JAVASCRIPT:
function generateChart(param, options)
{
// EERST DE GESELECTEERDE GEGEVENS GAAN OPVRAGEN
data = param.join();
console.log(data);
t_min = new Date(2015, 0, 1, 0, 0, 0, 0);
t_max = Date.now();
console.log(t_min, t_max);
console.log((t_max - t_min) / 1000);
$.ajax({
url: url,
data: { "data": data, "db": klantnummer, "t_min": t_min, "t_max": t_max },
type: "POST",
async: false,
cache: false,
success:
function (gegevens)
{
// GEGEVENS OPSLAAN IN DE OPTIE.SERIES
console.log('Gegevens:');
console.log(gegevens); //werkt!
options.series = gegevens;
}
});
//EFFECTIEF AANMAKEN VAN DE GRAFIEK
chart = new Highcharts.StockChart(options);
}
PHP:
// READ THE COLUMN NAMES
$data_in = $_POST['data'];
// DATA_IN to array
$name = explode(",", $data_in);
// count the amount of columns
$c_name = count($name);
$name_sql = implode(", ", $name);
//Connection with database
$klantnummer = $_POST['db'];
require 'connectie.php';
//ZOOM: min and max values are converterd to PHP UNIX EPOCH time
$t_min = ($_POST['t_min'])/1000;
$t_max = ($_POST['t_max'])/1000;
$range = $t_max - $t_min;
//Doesn't work, don't need it to load the chart
//$startTime = $t_min->format('m-d-Y H:i:s');
//$stopTime = $t_max->format('m-d-Y H:i:s');
//we only ask a query if there are enough columns
if ($c_name > 0) {
//Ask the names of the tables from the database
$sql = "SELECT TABLE_NAME FROM information_schema.tables WHERE TABLE_NAME LIKE 'T%' ORDER BY TABLE_NAME DESC";
$tableName = sqlsrv_query($conn, $sql);
if ( $tableName === false ) {
die(print_r(sqlsrv_errors(), true));
};
$t = array();
while ($row = sqlsrv_fetch_array($tableName)) {
array_push($t, $row['TABLE_NAME']);
};
// So we know the range, which filter should we apply?
if ($range < 14 * 24 *3600) { //2 weeks
$tijdspanne = 2; // load 2 tables
$filter = 1; // show every minute
} elseif ($range < 28 * 24 *3600) { //4 weeks
$tijdspanne = 2; // load 2 tables
$filter = 5; // show every 5 minutes
} elseif ($range < 3 * 28 * 24 *3600) { //3 months
$tijdspanne = 4; // load 4 months
$filter = 15; // show every 15 minutes
} else {
$tijdspanne = count($t); // load every table
$filter = 60; // show every 60 minutes
}
//Namen zijn gekend, nu in query steken om daarna op te vragen
$sql = "select tijd,".$name_sql." FROM ".$t[0]." where datepart(mi,tijd) % ".$filter." = 0";// AND tijd BETWEEN ".$startTime." AND ".$stopTime;
for ($x = 1; $x < count($t) and $x < $tijdspanne ; ++$x ) {
$sql .= " union all select tijd,".$name_sql." FROM ".$t[$x]." where datepart(mi,tijd) % ".$filter." = 0";// AND tijd BETWEEN ".$startTime." AND ".$stopTime;
};
$sql .= " ORDER BY tijd";
//DATA from MS SQL server, save to arrays
$stmt = sqlsrv_query($conn, $sql);
if ( $stmt === false ) {
die(print_r(sqlsrv_errors(), true));
}
while ($row = sqlsrv_fetch_array($stmt)) {
$tijd_U = $row["0"]->format("U");
$tijd = ($tijd_U) * 1000; //date format to javascript
for ($i = 0; $i < $c_name; ++$i) {
$j = $i +1;
$data[$i][]= array($tijd, $row[$j]);
}
}
// prepare JSON format
$data_output = array();
for ($i = 0; $i < $c_name; ++$i) {
$data_output[$i] = array('name' => $name[$i], 'data' => $data[$i]);
}
}
// admit we use JSON
header('Content-Type: application/json');
// send the data to the user
echo json_encode($data_output, JSON_NUMERIC_CHECK);
So this code works, but when the user zooms in. The graph doens't update.
JAVASCRIPT:
function afterSetExtremes(e)
{
console.log(Math.round(e.min));
chart.showLoading('Loading data from server...');
$.ajax({
url: url,
data: { "data": data, "db": klantnummer, "t_min": Math.round(e.min), "t_max": Math.round(e.max) },
type: "POST",
async: false,
cache: false,
success:
function (gegevens)
{
// GEGEVENS OPSLAAN IN DE OPTIE.SERIES
console.log('Gegevens:');
console.log(gegevens); //werkt!
options.series = gegevens;
}
});
chart.hideLoading();
}
With this set of options:
function generateChartOptions(div, ymax, ymin)
{
var options = {
navigator: {
adaptToUpdatedData: false,
series: {
includeInCSVExport: false
}
},
chart: {
renderTo: div,
type: 'line',
zoomType: 'x'
},
rangeSelector: {
buttons: [{
type: 'day',
count: 1,
text: '1d'
}, {
type: 'week',
count: 1,
text: '1w'
}, {
type: 'week',
count: 2,
text: '2w'
}, {
type: 'all',
text: 'All'
}],
selected: 4,
inputBoxWidth: 150,
inputDateFormat: '%d-%m-%Y %H:%M',
inputEditDateFormat: '%d-%m-%Y %H:%M'
},
legend: {
enabled: true
},
xAxis: {
type: 'datetime',
events: {
afterSetExtremes: afterSetExtremes,
setExtremes: function (e)
{
$('#testveld').html('<b>Set extremes:</b> e.min: ' + Highcharts.dateFormat(null, e.min) +
' | e.max: ' + Highcharts.dateFormat(null, e.max) + ' | e.trigger: ' + e.trigger);
}
},
minRange: 60 * 1000 //1 minuut
},
yAxis: {
opposite: false,
max: ymax,
min: ymin
},
plotOptions: {
line: {
tooltip: {
valueDecimals: 1
}
}
},
scrollbar: {
liveRedraw: false
},
series: [{}]
};
return options;
}