node-red-contrib-prib-functions
Version:
Node-RED added node functions.
907 lines (886 loc) • 33.6 kB
JavaScript
const path = require('path')
require("../lib/objectExtensions")
const Format=require('./Format')
const Table=require('../lib/Table')
const { action, children } = require('./defs')
const { table } = require('console')
const timePrecision=(time,minutes,hours,days)=>{
if(Math.floor(time/60000)>0) return minutes()
if(Math.floor(time/60)>0) return hours()
if(Math.floor(time/24)>0) return days()
}
const timePeriodsMS={
second:1000,
minute:60*1000,
hour:60*60*1000,
day:24*60*60*1000,
week:7*24*60*60*1000,
fortnight:2*7*24*60*60*1000,
month:30*24*60*60*1000,
year:365*24*60*60*1000,
}
const timeUnitRange=(t,second=p=>r,minute=p=>r,hour=p=>r,day=p=>r,week=p=>r,fortnight=p=>r,month=p=>r,year=p=>r)=>{
let duration=Math.floor(t/1000)
if(duration==0) return seconds(duration)
duration=Math.floor(duration/60)
if(duration==0) return minutes(duration)
duration=Math.floor(duration/60)
if(duration==0) return hours(duration)
duration=Math.floor(duration/60)
if(duration==0) return days(duration)
if(Math.floor(duration/7)==0) return weeks(duration)
if(Math.floor(duration/14)==0) return fortnights(duration)
if(Math.floor(duration/30)==0) return months(duration)
return years(Math.floor(duration/365))
}
const timeTickPoints=(timeUnit="second",min=0,max=100)=>{
const period=timePeriodsMS[second]
const results=[]
const rangeRatio=Math.floor((max-min)/period);
for(let j=min; j<max; j++)
Math.floor(rangeRatio*j)
}
const circle={
arcPath:(v)=>" A "+v.rx+" "+v.ry+","+(v.xAxisRotation??0)+","+(v.largeArcFlag??0)+","+(v.sweep-flag??0)+","+v.x+" "+v.y,
basePoints:(degrees=6,circumference=2*Math.PI)=>{
const points=[],increment=circle.toRadians(degrees)
for(let i=0; i<circumference; i+increment)
points.push([Math.cos(i),Math.sin(i)])
},
getPieSlicePath:(to,from=0,radius=100)=>{
const fromRadians=circle.toRadians(from),toRadians=circle.toRadians(to)
const flip180=(to-from)>180?",0,0,0":",0,1,1,"
return " M "+radius*Math.cos(fromRadians+" "+radians*Math.cos(fromRadians) +
" A "+radius+" "+radius + flip180 + radius*Math.cos(toRadians)+" "+radius*Math.cos(toRadians) +
" Z"
},
getPieSliceRatioPath:(to,from=0,radius=100)=>{
const fromRadians=from*circle.pi2,toRadians=to*circle.pi2
const flip180=(to-from)>.5?",0,0,0":",0,1,1,"
return " M "+radius*Math.cos(fromRadians+" "+radians*Math.cos(fromRadians) +
" A "+radius+" "+radius + flip180 + radius*Math.cos(toRadians)+" "+radius*Math.cos(toRadians) +
" Z"
},
pi2:2 * Math.PI,
pi2DegreeFactor:180 / Math.PI,
point:(degrees=6,radius=1)=>{
const radians=circle.toRadians(degrees)
return [radius*Math.cos(radians),radius*Math.sin(radians)]
},
points:(degrees=6,radius=1)=>circle.circumferenceBasePoints(degrees).map(c=>[radius*c[0],radius*c[1]]),
pieSlicesDegrees:(degrees,radius=100)=>{
const pieces=degrees.reduce((a,c,i)=>{
a.push({action:"path",id:"pieslice"+i,d:circle.getPieSlicePath(f,t,radius)})
},[])
return {action:g,id:"pie",children=pieces}
},
toDegrees:(radians)=> radians * circle.pi2DegreeFactor,
toRadians:(degrees)=> degrees / circle.pi2DegreeFactor
}
const stackBlocks=(data, colors=getColors(data[0].length), barWidth, yStart=0, yEnd=this.data[0].length-1 )=>{
let xPos=0, yPos=0
const results=[]
for(let i= 0; i < data.length; i++) {
const row=data[i]
xPos=Math.floor((i+0.5)*barWidth);
yPos=Math.sum(...row)
row.map((y,i)=>({action:"rect",fill:colors[j],x:xPos,y:yPos,width:barWidth,height:y})
for(let j=0; j<row.length; j++) {
if (row[j]==null) continue
const y=row[j]
results.push({action:"rect",fill:colors[j],x:xPos,y:yPos,width:barWidth,height:y})
yPos+=y;
}
}
return results
}
const getPushline=(data,j,tickIncrement,barWidth)=>{
const points = [];
for(let i= 0; i <data.length; i++) {
const row=data[i]
if(row[j]==null) continue;
xPos=Math.floor(i*tickIncrement)+(j-1)*barWidth;
points=[xPos,row[j]]
}
return {action:"polyline",points:points}
}
const getCircleTicks=(radius,degrees=6,strokeWidth=1,size=2)=>{
const ri=radius-size/2, ro=radius+size/2
return {action:"path",d:circle.basePoints(degrees).reduce((a,c)=>" M "+ri*c[0]+","+ri*c[1]+" L "+ro*c[0]+","+ro*c[1],"")}
}
function chart() {
Object.assign(this,{
bubbleRatio:0.2,
type:"line",
width:100,
height:100,
colour:"black",
disableLeftMenu:false,
grouping:null,
highlight:'',
legendDisplay:'show',
legendPositionX:60,
legendPositionY:0,
lineWidth:1,
outline:'black',
showOptions:true,
showPoints:false,
slices:'row',
tickIncrement:5
})
this.sliceByRow=this.slices=='row'
this.setAxis("x").setAxis("y").setAxis("z")
if(this.table == null) throw Error("Cannot render chart as table not specified")
this.processProperties();
}
chart.prototype.setAxis= function(dimension){
const axis=this.axis[dimension]
axis={columns:null,position:0,bound:{upper:null,lower:null}}
axis.columnArray=axis.columns.toLowerCase().split(',')
if(axis.columnArray.length==0) throw axis+" axis has no valid values for the database version, "+axis+"Axis: "+axis.columnArray;
if(axis.bound.upper!=null) axis.bound.upper=parseInt(axis.bound.upper,10);
if(axis.bound.lower!=null) axis.bound.lower=parseInt(axis.bound.lower,10);
axis.normaliser=1
return this
}
chart.prototype.errorFunction=function(msg){
throw typeof msg == "string"? Error(msg) : msg
}
chart.prototype.getPies=function (sliceByRow=this.sliceByRow,height=this.height) {
let xPos=0, yPos=0,pies=[]
const piesCount=(sliceByRow?this.columnIndexDetails.y.size+1:this.data.length)
const piesPerRow=Math.ceil(Math.sqrt(piesCount))
for(let i=(sliceByRow?0:0); i < piesCount; i++) {
pies.push(this.drawPie(xPos,yPos,this.height,i))
if(i%piesPerRow) continue
xPos=0;
yPos+=this.height;
}
return pies
}
chart.prototype.getPie=function(radius,i,dataset,sliceByRow=this.sliceByRow,label=isSliceRow)?this.label[i]:dataset[i][this.columnIndex[0]]) {
const circumference=radius * Math.PI+2
const total=sliceByRow?dataset.reduce((a,row)=>a+row[i],0):dataset[i].reduce((a,c)=>a+c,0)
if (total==0) return []
let angleStart=0, angleEnd=0, slices=[]
const unitDegreesRatio = 2 * Math.PI/total;
for(let row= (sliceByRow?0:1); row < arrayLength; row++) {
const value=(sliceByRow?this.data[row][i]:this.data[i][row])
if(value==null) continue;
angleStart=angleEnd;
angleEnd+=unitDegreesRatio*value;
const dash=(circumference*ratio)
const gap=circumference-dash
slices.push({action:"circle",fill:"transparent",cx:radius,cy:radius,r:radius,stroke:this.colour[row],"stroke-width":radius,
"stroke-dasharray": dash+","+gap,
transform:{rotate:{angle:angleStart,x:this.radius,y:this.radius}}}
)
slices.push({action:"text",x:x,y:y+10,children:[label]})
}
return {action:"g",children:[slices]}
}
chart.prototype.checkExists=function(value,errorMessage="value is null",error=this.errorFunction) {
if(value==null) error(errorMessage)
}
chart.prototype.processProperties=function(error=(msg)=>{throw Error(msg)}) {
if(this.grouping!=null) this.grouping = this.grouping.toUpperCase();
else if(this.baseTableData.primaryKeys.length>0)
this.grouping = this.baseTableData.primaryKeys.toString();
if(!['row',"column"].includes(this.slices)) error("parameter slices can only equal row or column")
this.sliceByRow=this.slices=="row"
if(['bubble','bubbleandline'].includes(this.type))
this.checkExists(this.axis.z.columns,'Missing z axis columns for '+this.type,error);
if(['bubble','bubbleandline','line','pushline','setline'].includes(this.type))
this.checkExists(this.axis.x.columns,'Missing x axis columns for '+this.type,error);
if(['bubble','bubbleandline','line','pushline','setline','stack','bar','pie','events'].includes(this.type))
this.checkExists(this.axis.y.columns,'Missing y axis columns for '+this.type,error);
if(['pie'].includes(this.type)) {
this.grouping=null;
}
}
chart.prototype.setParameter=function(name,value) {
this[name]=value
return this
}
chart.prototype.getMenuOption=function(label,value,property){
return '<tr><td>'+label+'</td><td>'
+'<input type="button" value="'+value+'" onclick="this.value='+this.callBackText+'.setParameter(\\\''+property+'\\\',this.value)"/>'
+'</td></tr>';
}
chart.prototype.setMenuOptions=function(menuArray) {
if(!this.showOptions) return null;
if(menuArray==null) menuArray=[];
if(this.optionsDialog==null) {
this.optionsDialog = new floatingPanel(this.elementUniqueID + '_optionsDialog', 'RAW', "", this.elementUniqueID + '_optionsDialog_button', false, false);
this.parentPanel.registerNestedObject(this.elementUniqueID + '_optionsDialog', this.optionsDialog);
}
const thisObject = this;
if(!this.disableLeftMenu)
if(this.baseTableData.localLeftMenu!=null)
menuArray=menuArray.concat(this.baseTableData.localLeftMenu);
options='';
switch (this.type) {
case 'pie':
options+=this.getMenuOption('Slice',(this.slices=='row'?'column':'row'),'slices')
break;
case 'bubble':
case 'bubbleandline':
case 'line':
break;
case 'pushline':
case 'setline':
options+=this.getMenuOption('Chart Type',this.type=='setline'?'pushline':'setline'),'type')
break;
case 'bar':
case 'stack':
options+=this.getMenuOption('Chart Type',(this.type=='bar'?'stack':'bar'),'type')
break;
}
options+=this.getMenuOption('Legend',(this.legendDisplay=="hide"?"show":"hide"),'legendDisplay')
+'<tr><td>Max. rows</td><td>'
+' <input type="text" value="'+this.baseTableData.maxResultsToFetch+'" onchange="'+this.callBackText+'.setParameter('+"'baseTableData.maxResultsToFetch'"+',this.value)"/>'
+'</td></tr>';
this.optionsDialog.draw();
this.optionsDialog.setContent('<table>'+options+this.getMetricColumnsOptions()+'</table>','Options', null, null, null)
}
chart.prototype.getBar=function (tickIncrement=this.tickIncrement,barWidth=Math.floor(this.tickIncrement/this.columnIndexDetails.y.size)) {
const group=[]
let xPos=0, yPos=0
for(let i= 0; i < this.data.length; i++) {
const row=this.data[i]
for(let j=0; j<this.table.data.length; j++) {
if (row[j]==null) continue;
xPos=Math.floor((i+0.5)*tickIncrement)+(j-1)*barWidth;
yPos=row[j]
group.push({action:"rect", x:xPos,y:yPos,width:this.barWidth,height:this.height-yPos,fill:this.colour[j],stroke:this.outline})
}
}
return result
}
chart.prototype.getBubble=function () {
const zDetails=this.axis.z;
const results=[]
for( let i= 0; i < this.data.length; i++) {
for( let j=0; j<this.data.length; j++) {
const row = this.data[i]
const zData = zDetails.dataStore
const y=row[j];
if(y==null || isNaN(y)) continue
xPos=row[0]
radius=Math.abs(zData[i][j]/2);
if(radius==0) continue
results.push({action:"circle",fill:this.colour[j],cx:xPos,cy:y,r:radius,stroke:this.outline,"stroke-width":1, opacity:0.5})
}
}
return results
}
chart.prototype.getBubbleAndLine=function () {
return [this.getLine(),this.getBubble()]
}
chart.prototype.getEvents=function(data) {
let dataX, plotX, lines=[]
for(let j=0 ; j<data.length; j++) {
dataX=data[j][0];
if (dataX==null || isNaN(dataX) ) continue;
lines.push({action:"line",x1:dataX,y1:0,x1:dataX,y1:this.height,stroke:"red","stroke-width":this.lineWidth,"stroke-opacity":0.8})
}
return lines
}
chart.prototype.getLine=function () {
let errors,points;
for(let j=0; j<this.data.length; j++)
try {
this.plot(j, 1, this.data)
} catch(e) {
errors += 'error plotting '+ this.label[i] + ' ' +e +'\n';
}
if(errors) this.errorFunction(errors)
return {action:'polyline',points:"100,100 150,25 150,75 200,0",fill:"none",stroke="red","stroke-width":this.lineWidth,"stroke-opacity":0.8}
},
drawChart_pushline: function() {
const errors=[],result="",yDetails=this.columnIndexDetails.y
this.barWidth=Math.floor(this.tickIncrement/yDetailssize);
for(let j=yDetails.start; j<yDetails.end; j++)
try {
result+=this.plotSet(j,this.data,this.colour[j], this.barWidth);
} catch(e) {
errors += 'error plotting '+ this.label[i] + ' ' +e +'\n';
}
if (errors.length) throw errors;
},
drawChart_setline: function () {return this.drawChart_pushline();},
drawChart_stack: function(data,tickIncrement) {
const y=this.columnIndexDetails.y
return stackBlock(data, this.colour, tickIncrement)
},
chart.prototype.getChart=function () {
let errors;
const colourCnt = this.type=='pie'&& this.sliceByRow? ? this.data.length : this.columnIndexDetails.y.size+1
if(colourCnt>this.colour.length)
this.color=getColors(colourPallet.length/colourCnt)
this.ctx.clearRect(0,0,this.width,this.height);
this.ctx.strokeStyle = "black";
if(['bar','bubble','bubbleandline','line','pushline','setline','stack'].includes[this.type])
this.drawLineAxis();
try { this["drawChart_"+this.type]();} catch(e) {throw "drawing " + this.type + "\n" + e.toString(); };
while (this.legend.rows.length> 0) this.legend.deleteRow(0);
switch (this.type) {
case 'pie':
if(this.sliceByRow) {
for(let i = 1; i < this.data.length; i++)
this.legend+= "<tr><a style='color:" + this.colour[i] + "' >" + this.data[i][0] + "</a></tr>";
} else
for(let i=0; i<this.label.length; i++)
this.legend+="<tr><a style='color:" + this.colour[i] + "' >" + this.label[i] + "</a></tr>";
break;
default:
this.legend+="x axis: " + this.label[0];
for(var i=0; i<this.table.data.length; i++) {
this.legend.+= "<tr><a style='color:" + this.colour[i] + "' >" + this.label[i]
+ (this.zAxis?" z is " +this.axis.z.label[i]:"")
+ "</a></tr>";
}
}
},
const coords={
events:(chart,data,xPos,yPos)=>{
const xPlot=xScaleRange(xPos,chart.xRatio)
for(let i=0; i<data.length;i++) {
const v=data[i][0]
if (v< xPlot.min || v> xPlot.max) continue;
let details="event:";
for(let i= 0; i < this.table.data.length; i++)
details+=" "+this.dataToString(i,v>xPlot.max);
XYRow.insertCell(-1).innerHTML=details;
var XYRow=chart.detailXY.insertRow(-1);
}
},
bubble:(chart,data,xPos,yPos)=>{
const xPlot=xScaleRange(xPos,chart.xRatio)
const yPlot=yScaleRange(yPos,chart.yRatio)
const text=[{action:'text',children:["x: "+this.dataToString(0,xPlot.value) + " "+"y: "+this.dataToString(1,yPlot.value)]}]
const zText=chart.zAxis?
(chart,row,colum)=>{
const zDetails=chart.columnIndexDetails.z
const zData = zDetails.dataStore
return "z: "+chart.dataToString(i,zData[row][column]);
}:
()=>""
for(let row= 0; row < this.data.length; row++) {
const dataRow=data[row]
const x=dataRow[0]
const xText=" x: "+this.dataToString(0,x)
if (v< xPlot.min || v> xPlot.max) continue;
for(let i= 0; i < this.table.data.length; i++) {
const y=this.dataToString(i,dataRow[i])
if(isNaN(y) || y< yPlot.min || y> yPlot.max) continue;
text.push({action:'text',children:[chart.label[i]+xText+" y: "+y+zText(chart,row,i)]})
}
}
}
}
coords.bubbleandline=coords.bubble
coords.line=coords.bubble
function getDisplay(chart,xPos,yPos)
canvasCoordsDetails: function (chart,xPos,yPos) {
var XYRow=chart.detailXY.insertRow(-1);
switch (this.type) {
case 'bubble':
case 'bubbleandline':
case 'line':
case 'pushline':
case 'setline':
break;
case 'bar':
case 'stack':
var x=Math.floor((xPos)/this.tickIncrement-0.5);
XYRow.insertCell(-1).innerHTML="x:";
switch (this.dataType[0]) {
case 'timestamp' :
case 'date' :
case 'datetime' :
XYRow.insertCell(-1).innerHTML=this.dataToString(0,(xPos -this.xOffset)/this.xRatio);
break;
default:
XYRow.insertCell(-1).innerHTML=this.data[x][0];
}
XYRow=chart.detailXY.insertRow(-1);
XYRow.insertCell(-1).innerHTML= "y:";
XYRow.insertCell(-1).innerHTML=this.dataToString(1,(this.yOffset - yPos)/this.yRatio);
break;
default:
break;
}
},
chart.prototype.plotSet=function (y,data,colour,tickIncrement,barWidth) {
const points=[]
const group=[]
for(let i= 0; i < data.length; i++) {
const dataiy=data[i][y]
if(dataiy==null) continue;
const xPos=Math.floor(i*this.tickIncrement)+(y-1)*barWidth
points.push([xPos,dataiy])
}
group.push({action:"polyline",points:points})
return {action:"g",stroke:color,children:[group,this.getPoints(points)]}
},
chart.prototype.getPoints=function(points,call=(x,y)=>({action:"circle",cx:x,cy:y,r:3})) {
if(!this.showPoints) return ""
return points.reduce((a,p)=>{
a.push(call(p.x,py))
return a
},[])
}
chart.prototype.getPointsRect=function(points,call=(x,y)=>({action:"rect",x:x-3,y:y-3,height:6,width:6})) {
this.getPoints(points,call)
}
plot: function (y, offset, data) {
if(data.length==0) return;
const points=[]
const row0=this.data[0]
if(data.length==1 || this.highlight=='first') {
const dataX=row0[0];
const dataY=row0[y];
if (dataX==null || dataY==null) return;
points=[dataX,dataY]
if(data.length==1) return;
}
result=this.showPoints?this.getPoints(points):this.getPointsRect(points)
const colour=this.colour[y * offset]
return {action:"g",stroke:colour,children:result}
var errors;
let linePointCnt=0;
const rowPoints=[]
for(let row=0; row < data.length; row++) {
try {
const rowData=this.data[row]
const dataX=rowData[0];
const dataY=rowData[y];
if (dataX==null || dataY==null || isNaN(dataX) || isNaN(dataY)) {
if(linePointCnt>0) {
if(linePointCnt==1 && !this.showPoints)
rowPoints.push([this.data[row-1][0],data[row-1][y]])
linePointCnt=0;
}
continue;
}
plotX=dataX
plotY=dataY
if(++linePointCnt>1) {
this.ctx.lineTo(plotX,plotY);
continue;
}
this.getPoints(points)
} catch (e) {
errors += " x: " + dataX + " ( " + plotX + " ) " + " y: " + dataY + " ( " + plotY + " ) " + "\n" + e + "\n";
linePointCnt=0;
}
}
if(linePointCnt>0) {
if(( linePointCnt==1 || this.highlight=='last' ) && !this.showPoints)
rowPoints.push([dataX,dataY])
}
this.getPointsRect(rowPoints)
if(this.showPoints) {
const points=[]
for(let row= 0; row < data.length; row++) {
const dataRow=data[row]
const dataY=dataRow[y]
if(dataY==null) continue;
points.push([dataRow[0],dataY])
}
this.getPoints(points)
}
if (errors!=null) throw errors;
},
dataToString: function(i,value) {
return Format.toString(this.dataType[i],value)
}
drawLineAxis: function() {
x+-0.5
const ticks=[[0,this.height]]
return {action:"g",id:"axis",stroke:"black","stroke-width":this.axis.X.LineWidth,children:result}
{action:"line",x1:0,y1:this.height,x2:0,y2:0}
switch (this.type) {
case 'bubble':
case 'bubbleandline':
case 'line':
if (this.xTicks.length>1) {
this.ctx.textAlign='left';
this.ctx.fillText(Format.toString(this.dataType[0],parseInt(this.xTicks[0])),0,this.height-this.axisOffset/2);
}
for(let i = 0; i< this.xTicks.length; i++) {
const pos=this.xTicks[i]
this.drawXAxisTick(pos);
this.ctx.textAlign='center';
this.ctx.fillText(this.dataToString(0,this.xTicks[i],true), pos,this.height-this.axisOffset/2);
}
break;
case 'bar':
case 'pushline':
case 'setline':
case 'stack':
if (this.xTicks.length==0) this.tickIncrement=this.xMax;
else this.tickIncrement=Math.floor(this.xMax/(this.xTicks.length+1));
var k = Math.ceil((50/this.tickIncrement));
for(let i = 0; i< this.xTicks.length; i+= k) {
var pos=this.axisOffset + this.tickIncrement*(i+1);
if(this.type == 'setline')
pos -= this.tickIncrement;
this.drawXAxisTick(pos);
this.ctx.textAlign='center';
this.ctx.fillText(Format.toString(this.dataType[0],this.data[i][0]), pos,this.height-this.axisOffset/2);
}
break;
default:
}
// y axis
this.ctx.beginPath();
this.ctx.moveTo(0,0.5);
this.ctx.lineTo(this.width-( this.type == 'setline' ? this.tickIncrement : 0 ),0.5);
this.ctx.stroke();
// y ticks
for(let i = 0; i< this.yTicks.length; i++) {
var pos=this.yTicks[i]
this.drawYAxisTick(pos);
this.ctx.textAlign='left';
this.ctx.fillText(this.dataToString(1,this.yTicks[i]),0 , pos+3);
}
},
calculateTicks: function(yMax,max,min,type,metric) {
let tickPosition = [];
if (max==null || yMax==null ) return tickPosition;
const tickCount = Math.floor(yMax/20);
if(tickCount < 1) tickCount = 1;
let i=0,k=0;
let duration=max-min;
if(column.isTime()) {
this.precision[metric]=1;
const minTS= new Date();
minTS.setTime(parseInt(min));
const maxTS= new Date();
maxTS.setTime(parseInt(max));
timePrecision(duration,{
minutes:()=>{
this.precision[metric]=60;
minTS.setMilliseconds(0);
minTS.setSeconds(0);
},
hours:()=>{
this.precision[metric]=360;
minTS.setMinutes(0);
},
days:()=>{
this.precision[metric]=1440;
minTS.setHours(0);
}
})
const getTicks=(max,min,step)
for(let i = minTS.getTime() ; i < max ; i=i+duration )
if (i>min && i<=max) tickPosition[k++]=i;
)
const hour=60*60*1000
duration=Math.floor(duration/60000); // minutes?
if (duration > 0) {
duration=Math.floor(duration/60); // hours?
if (duration > 0) {
duration=Math.floor(duration/24); // days?
if (duration > 0) {
for(let j=1; j<this.height ; j++)
if (duration<j*5+1) break;
duration=j*24*hour;
} else {
duration=Math.floor((max-min)/hour);
duration=(Math.floor(duration/5)+1)*hour;
}
} else {
duration=Math.floor((max-min)/(60*1000)); // minutes
if (duration<5)
duration=60*1000; minute==60000
else if (duration<25)
duration=5*60*1000;
else
duration=10*60*1000;
}
for(let i = minTS.getTime() ; i < max ; i=i+duration )
if (i>min && i<=max) tickPosition[k++]=i;
break;
}
duration=max-min;
} else {
}
if(column.isMetric()) {
const min = parseFloat(min)
const max = parseFloat(max)
const tickSpan = Math.floor((max-min)/tickCount);
var tickTotal = Math.min(tickCount+10, Math.floor(Number.MAX_VALUE / tickSpan));
for(let j=0;j<=tickTotal;j++) {
tickPosition[j]= min + j*tickSpan;
if( tickPosition[j] > max) break;
}
} else throw 'Unknown type: "'+type+'" for axis tick creation, check types are numeric, label: '+this.label[metric];
return tickPosition;
},
resize: function() {
switch (this.dataType[0]) {
case "int":
case "real":
case "number":
case "date":
case "time":
case "datetime":
case "timestamp":
if(this.columnIndex[0]==null) {
this.dataMin[0]=0.5;
this.dataMax[0]=1.5;
this.xMax = this.width
break; // xTicks built whilst building data
}
const dataMin0=this.dataMin[0]
const dataMax0=this.dataMax[0]
this.xMax = this.width-6;
if(dataMin0==dataMax0) {
this.dataMin[0]=dataMax0-1;
this.dataMax[0]=dataMax0+1;
}
this.xRatio = this.xMax/(this.dataMax[0]-this.dataMin[0]);
this.xOffset= this.dataMin[0]
this.xTicks = this.calculateTicks(this.dataMax[0],this.dataMin[0],this.dataType[0],0);
break;
default:
}
this.yDataMin = this.yAxisLowerBound;
this.yDataMax = this.yAxisUpperBound;
switch (this.type) {
case 'stack':
this.yDataMin= this.yDataMin ?? 0
this.yDataMax= this.yDataMax ?? 0
for( var i=0; i < this.table.data.length; i++) {
if (this.dataMax[i]==null) continue;
this.yDataMax+=this.dataMax[i];
}
if(this.yAxisUpperBound==null)
this.yDataMax=this.yDataMax*1.01;
break;
case 'bar':
case 'pushline':
case 'setline':
this.yDataMin= this.yDataMin??0
this.yDataMax= this.yDataMax??0;
for(var i =0; i < this.table.data.length; i++)
if (this.yDataMax<this.dataMax[i]) this.yDataMax=this.dataMax[i];
if(this.yAxisUpperBound==null)
this.yDataMax=this.yDataMax*1.1;
break;
default:
this.yDataMax= this.yDataMax??this.dataMax[1];
this.yDataMin= this.yDataMin??this.dataMin[1];
for(var i = 0; i < this.table.data.length; i++) {
if (this.yDataMax<this.dataMax[i]) this.yDataMax=this.dataMax[i];
if (this.yDataMin>this.dataMin[i]) this.yDataMin=this.dataMin[i];
}
this.yDataMax= this.yDataMax?? 1
this.yDataMin= this.yDataMin?? 0;
if(this.yDataMin==this.yDataMax)
if(this.yDataMax==0)
this.yDataMax=1;
if(this.yAxisLowerBound==null)
this.yDataMin=this.yDataMin*0.99;
if(this.yAxisUpperBound==null)
this.yDataMax=this.yDataMax*1.01;
break;
}
if(this.zAxis) {
var zDetails=this.axis.z;
this.zDataMax=null;
this.zDataMin=null;
this.zRatioColumns=[];
for (let i=0;i<zDetails.dataMin.length;i++) {
if(this.zDataMax<zDetails.dataMax[i]) this.zDataMax=zDetails.dataMax[i];
if(this.zDataMin>zDetails.dataMin[i]) this.zDataMin=zDetails.dataMin[i];
this.zRatioColumns[i]={};
var ratio=this.zRatioColumns[i];
rangeZ=zDetails.dataMax[i]-zDetails.dataMin[i];
if(rangeZ==0) {
rangeZ=zDetails.dataMin[i];
ratio.zRatioOffset=0;
} else
ratio.zRatioOffset=zDetails.dataMin[i];
ratio.zRatio=this.bubbleRatio*(Math.min(this.yMax,this.xMax)/rangeZ );
if(ratio.zRatio==Infinity) this.zRatio=this.bubbleRatio;
if (isNaN(ratio.zRatio)) throw "z ratio calculation error, charting width:"+ this.yMax + " max:"+ zDetails.dataMax[i] + " min:"+ zDetails.dataMin[i];
}
rangeZ=this.zDataMax-this.zDataMin;
if(rangeZ==0) {
rangeZ=this.zDataMin;
this.zRatioOffset==0;
} else
this.zRatioOffset=this.zDataMin;
this.zRatio=this.bubbleRatio*(Math.min(this.yMax,this.xMax)/rangeZ);
if(this.zRatio==Infinity) this.zRatio=this.bubbleRatio;
if (isNaN(this.zRatio)) throw "z ratio calculation error, charting width:"+ this.yMax + " max:"+ Math.max.apply(Math,this.axis.z.dataMax) + " min:"+ Math.min.apply(Math,this.axis.z.dataMin);
}
},
dataConversion: function(i,value) {
if (value==null) return null;
if(dataConversionFunc[this.dataType[i]]==null) return value;
try{
return dataConversionFunc[this.dataType[i]](value);
} catch(e) {
throw "data conversion error data type: " +dataType[i] + ' value: "'+ value +'"';
}
},
buildDataSet: function(dataset) {
if (dataset.length <1) this.errorFunction('No data to chart')
delete this.columnIndexDetails;
this.columnIndex=[];
this.data=[];
this.dataType=[];
this.dataMax=[];
this.dataMin=[];
this.group=[];
this.groupIndex;
this.groupingValue="";
this.groupValue=[];
this.precision=[];
this.label=[];
this.xTicks=[];
this.yTicks=[];
// this.table.setDelta([])
this.setColumnDetails(0,this.axis.x.columns);
let x = -1
switch (this.baseTableData.columnsInfo.type[this.columnIndex[0]]) {
case 'timestamp':
this.xNormliseFactor=this.deltaNormaliser*1000;
break;
default:
this.xNormliseFactor=this.deltaNormaliser;
}
this.axis.x.normaliser=1;
var xCol=this.columnIndex[0];
const group=table.list.group.names?
(row)=>this.setData(++x):
(row)=>{
if (x==-1) {
this.setData(++x);
} else if (dataset[row][xCol] != dataset[row-1][xCol]) {
this.setData(++x);
}
var newGroupingValue='';
for(let i = 0; i < this.groupIndex.length; i++)
newGroupingValue+=' '+ dataset[row][this.groupIndex[i]];
if (this.groupingValue!=newGroupingValue) {
this.groupingValue=newGroupingValue;
this.group[0]=this.groupingValue;
const i=this.groupValue.find(v=>this.groupingValue==v)
if (i<0) {
this.groupValue.push(this.groupingValue)
this.setColumnIndexDetails("y");
}
}
}
table.forEachRow(row=>{
group(row)
})
for(let row = 0; row < dataset.length; row++) {
if (this.grouping==null) {
this.setData(++x);
} else {
if (x==-1) {
this.setData(++x);
} else if (dataset[row][xCol] != dataset[row-1][xCol]) {
this.setData(++x);
}
let newGroupingValue='';
for( i = 0; i < this.groupIndex.length; i++)
newGroupingValue+=' '+ dataset[row][this.groupIndex[i]];
if (this.groupingValue!=newGroupingValue) {
this.groupingValue=newGroupingValue;
this.group[0]=this.groupingValue;
const i=this.groupValue.find(v=>this.groupingValue==v)
if (i <0) {
this.groupValue.push(this.groupingValue)
this.setColumnIndexDetails("y");
}
}
}
this.xTicks[x]=row;
for(var i = 0; i < this.columnIndex.length; i++) {
if (this.group[i]!=this.groupingValue) continue;
var value = (this.columnIndex[i]==null?x+1:this.dataConversion(i,dataset[row][this.columnIndex[i]]));
this.data[x][i] = value;
this.setMaxMin(this, i, value);
if(this.zAxis) this.setDataAxis('z',dataset[row],x,i);
}
}
},
setMaxMin: (base,i,value)=>{
if (base.dataMin[i]==null) {
base.dataMin[i]=value;
base.dataMax[i]=value;
} else if (base.dataMin[i]>value)
base.dataMin[i]=value;
else if (base.dataMax[i]<value)
base.dataMax[i]=value;
},
setData: function(x) {
this.data[x]=[];
this.deltaData[x]=[];
},
setDataAxis: function(axis,row,x,i) {
var colDetails=this.columnIndexDetails[axis];
var value = (row==null?null:this.dataConversion(i,row[colDetails.columnIndex[i]]));
if(colDetails.dataStore[x]==null) {
colDetails.dataStore[x]=[];
colDetails.deltaData[x]=[];
}
colDetails.dataStore[x][i]=value;
this.setMaxMin(colDetails, i, value);
},
setColumnDetails: function(index,columnName) {
if(columnName==null) {
this.columnIndex[index]=null;
this.dataType[index]='int';
this.label[index]=this.groupingValue;
this.group[index]=this.groupingValue;
return;
}
const i = this.findColumnIndex(columnName);
this.columnIndex[index]=i;
this.dataType[index]=this.baseTableData.columnsInfo.type[i];
this.label[index]=this.groupingValue + " " + this.table.getColumn(columnName).title
this.group[index]=this.groupingValue;
},
setColumnDetailsAxis: function(columnDetails,columnName) {
if(columnName==null) throw "Column name not specified"
const i = this.findColumnIndex(columnName);
const index=columnDetails.columnIndex.length;
columnDetails.columnIndex[index]=i;
columnDetails.dataType[index]=this.baseTableData.columnsInfo.type[i];
columnDetails.label[index]=this.groupingValue + " " + this.table.getColumn(columnName).title
columnDetails.group[index]=this.groupingValue;
},
setListProperty: function(select,property,list) {
this[property]=[];
const p=this[property]
for (let i = 0; i < select.options.length; i++)
if (select.options[i].selected)
p.push(select.options[i].value)
this[list]=p.join();
},
setMetrics: function(select) {
this.setListProperty(select,"ySeries","yAxis")
},
setGrouping: function(select) {
this.setListProperty(select,"groupingArray","grouping")
}
getMetricColumnsOptions=function() {
const option='<tr><td>Y Metrics</td><td>'
+this.table.getSelectHML(this.ySeries,this.callBackText+".setMetrics(this)",(column)=>column.isNumeric())
+'</td></tr>'
+'<tr><td>Grouping</td><td>'
+this.table.getSelectHML(this.grouping,this.callBackText+".setGrouping(this)",(column)=>column.type=="string")
+'</td></tr>'
return option
}
}