@valcri/spc-sd
Version:
Detects SPC signals in datasets.
350 lines (315 loc) • 11.8 kB
JavaScript
var d3 = require('d3');
module.exports.getSignals = function(data, properties) {
module.exports.configureProperties(properties, data);
properties.processes = [];
if (properties.manualProcesses.length == 0 || properties.autoDetectProcess) {
module.exports.createProcess(properties.processes, 0, properties.signalDescriptors);
} else {
var prev = 0;
for (let i = 0; i < properties.manualProcesses.length; ++i) {
var p = properties.manualProcesses[i];
module.exports.createProcess(properties.processes, prev, properties.signalDescriptors, p-1);
if (i == properties.manualProcesses.length - 1 ) {
module.exports.createProcess(properties.processes, p, properties.signalDescriptors, data.length-1);
}
prev = p;
}
}
module.exports.calculateSignals(data, properties.processes, 0, properties.autoDetectProcess, properties.autoDetectUntil, properties.datesToExclude, properties.signalDescriptors);
}
module.exports.configureProperties = function (properties, data) {
if (!("signalDescriptors" in properties)) {
properties.signalDescriptors = module.exports.SIGNALS;
}
if (!("processes" in properties)) {
properties.processes = [];
}
if (!("autoDetectProcess" in properties)) {
properties.autoDetectProcess = false;
}
if ("manualProcesses" in properties) {
properties.manualProcesses.sort(function(a, b) {
return a - b;
});
} else {
properties.manualProcesses = [];
}
if (!("datesToExclude" in properties)) {
properties.datesToExclude = {};
}
if (!("autoDetectUntil" in properties)) {
properties.autoDetectUntil = d3.max(data, function(d) { return d.Date});
}
properties.dates = [];
data.forEach(function(d) {
properties.dates.push(d.Date);
})
}
module.exports.calculateSignals = function(data, processes, pIndex, autoDetectProcess, autoDetectUntil,
datesToExclude, signalDescriptors, iterate = true) {
var mean = function(data) {
return d3.mean(data, function(d) {
return d.Count;
});
}
var sd = function(data) {
return d3.deviation(data, function(d) {
return d.Count;
});
}
var clearSignal = function(signals, signalTracker, signalType, signalDescriptors) {
addSignalToTracker(signals, signalTracker, signalType, signalDescriptors);
signalTracker[signalType.id] = [];
}
var addSignalToTracker = function(signals, signalTracker, signalType, signalDescriptors) {
if (signalTracker[signalType.id].length >= signalType.length) {
signalTracker[signalType.id].forEach(function (d) {
if ((d in signals && signalType.length < signalDescriptors[signals[d]].length) || !(d in signals)) {
signals[d] = signalType.id;
}
});
}
}
var incrementSignal = function(signalTracker, signalType, id, processes, index, autoDetectProcess, autoDetectUntil, signalDescriptors) {
signalTracker[signalType.id].push(id);
if ((signalType.id == signalDescriptors.EIGHT_OVER_MEAN.id || signalType.id == signalDescriptors.EIGHT_UNDER_MEAN.id) &&
signalTracker[signalType.id].length == signalDescriptors.EIGHT_OVER_MEAN.length && autoDetectProcess && new Date(id) < new Date(autoDetectUntil)) {
module.exports.createProcess(processes, index, signalDescriptors);
return true;
}
return false;
}
var process = processes[pIndex];
if (process.endIndex >= data.length) {
process.endIndex = data.length - 1;
} else {
process.endIndex--;
}
var processFound = false;
while (!processFound && process.endIndex < data.length -1 && (process.cap == -1 || process.endIndex < process.cap) ) {
process.endIndex++;
process.signals = {};
var signalTracker = {};
for (let i in signalDescriptors) {
signalTracker[signalDescriptors[i].id] = [];
};
if (module.exports.isEmpty(datesToExclude)) {
process.mean = mean(data.slice(process.startIndex, process.endIndex+1));
process.sd = sd(data.slice(process.startIndex, process.endIndex+1));
} else {
var tmpData = [];
for (let i = process.startIndex; i < process.endIndex; i++) {
if (!(data[i].Date in datesToExclude)) {
tmpData.push(data[i]);
}
process.mean = mean(tmpData);
process.sd = sd(tmpData);
}
}
var j = 0;
for (j = process.endIndex; j >= process.startIndex; --j) {
var d = data[j];
if (!(d.Date in datesToExclude)) {
for (let i in signalDescriptors) {
var sig = signalDescriptors[i];
if (module.exports.rules[sig.rule.func](d.Count, process.mean, process.sd, sig.rule.cl)) {
processFound = incrementSignal(signalTracker, sig, d.Date, processes, j, j == process.startIndex ? false : autoDetectProcess, autoDetectUntil, signalDescriptors);
} else {
clearSignal(process.signals, signalTracker, sig, signalDescriptors);
}
if (processFound) {
break;
}
}
if (processFound) {
break;
}
}
}
if (processFound) {
if (module.exports.isEmpty(datesToExclude)) {
process.mean = mean(data.slice(process.startIndex, process.endIndex+1));
process.sd = sd(data.slice(process.startIndex, process.endIndex+1));
} else {
var tmpData = [];
for (let i = process.startIndex; i < process.endIndex; i++) {
if (!(data[i].Date in datesToExclude)) {
tmpData.push(data[i]);
}
process.mean = mean(tmpData);
process.sd = sd(tmpData);
}
}
}
}
for (let i in signalDescriptors) {
var val = signalDescriptors[i];
addSignalToTracker(process.signals, signalTracker, val, signalDescriptors);
};
if (iterate && (processFound || pIndex < processes.length - 1)) {
if (processFound) {
processes[pIndex].signals = {};
processes[pIndex].cap = processes[pIndex].endIndex;
processes[pIndex].endIndex = 0;
module.exports.calculateSignals(data, processes, pIndex, false, autoDetectUntil, datesToExclude, signalDescriptors, false);
}
module.exports.calculateSignals(data, processes, pIndex + 1, autoDetectProcess, autoDetectUntil, datesToExclude, signalDescriptors);
}
};
module.exports.createProcess = function(processes, index, signalDescriptors, cap = -1) {
var endIndex = index + signalDescriptors.EIGHT_OVER_MEAN.length;
if (endIndex > cap) {
endIndex = cap;
}
processes.push({
"startIndex" : index,
"endIndex" : endIndex,
"cap" : cap
});
if (processes.length > 1 && cap == -1) {
processes[processes.length - 2].endIndex = index - 1;
}
}
module.exports.isEmpty = function(obj) {
for(let prop in obj) {
if(obj.hasOwnProperty(prop))
return false;
}
return true;
}
module.exports.signalIsBelow = function(d) {
let r = module.exports.SIGNALS[d].rule;
return module.exports.rules[r.func](0, 1, 0, r.cl);
}
module.exports.SIGNALS = {
EIGHT_OVER_MEAN : {
id: "EIGHT_OVER_MEAN",
length: 8,
index: 4,
rule: {func: "over", cl: 0},
shape: {func: "cross", class: "spc__overSignal_fill spc__eight"}
},
EIGHT_UNDER_MEAN : {
id: "EIGHT_UNDER_MEAN",
length: 8,
index: 3,
rule: {func: "under", cl: 0},
shape : {func: "cross", class: "spc__underSignal_fill spc__eight"}
},
TWO_OVER_TWO : {
id : "TWO_OVER_TWO",
length : 2,
index : 6,
rule: {func: "over", cl: 2},
shape: {func: "cross", class: "spc__overSignal_fill spc__two"}
},
TWO_UNDER_TWO : {
id: "TWO_UNDER_TWO",
length: 2,
index: 1,
rule: {func: "under", cl: 2},
shape: {func: "cross", class: "spc__underSignal_fill spc__two"}
},
THREE_OVER_ONE_FIVE : {
id: "THREE_OVER_ONE_FIVE",
length: 3,
index: 5,
rule: {func: "over", cl: 1.5},
shape: {func: "cross", class: "spc__overSignal_fill spc__three"}
},
THREE_UNDER_ONE_FIVE : {
id: "THREE_UNDER_ONE_FIVE",
length: 3,
index: 2,
rule: {func: "under", cl: 1.5},
shape: {func: "cross", class: "spc__underSignal_fill spc__three"}
},
ONE_OVER_THREE : {
id: "ONE_OVER_THREE",
length: 1,
index: 7,
rule: {func: "over", cl: 3},
shape: {func: "cross", class: "spc__overSignal_fill spc__one"}
},
ONE_UNDER_THREE: {
id: "ONE_UNDER_THREE",
length: 1,
index: 0,
rule: {func: "under", cl: 3},
shape: {func: "cross", class: "spc__underSignal_fill spc__one"}
}
}
module.exports.rules = {
over: function(v, mean, sd, cl) {
if (v > mean + sd*cl) return true;
return false;
},
under: function(v, mean, sd, cl) {
if (v < mean - sd * cl) return true;
return false;
}
}
/**
* Drawing functions
**/
module.exports.icons = {
circle: function(size, x, y, container, classed, colour = "") {
container.append("circle")
.attr("cx", function(d) {
return x;
})
.attr("cy", function(d) {
return y;
})
.attr("r", size / 2)
.classed(classed, true)
.attr("fill", colour !== "" ? colour : null);
},
diamond: function(size, x, y, container, classed, colour = "") {
var r = size / 2;
container.append('polyline')
.attr('points', function(d) {
return (-r+x) + " " + y
+ " " + x + " " + (-r + y)
+ " " + (r+x) + " " + y
+ " " + x + " " + (r+y)
+ " " + (-r+x) + " " + y;
} )
.classed(classed, true)
.attr("fill", colour !== "" ? colour : null);
},
triangle: function(size, x, y, container, classed, colour = "") {
var r = size / 2;
container.append('polyline')
.attr('points', function(d) {
return (-r+x) + " " + (r+y)
+ " " + (r+x) + " " + (r+y)
+ " " + x + " " + (-r+y)
+ " " + (-r+x) + " " + (r+y);
})
.classed(classed, true)
.attr("fill", colour !== "" ? colour : null);
},
cross: function(size, x, y, container, classed, colour = "") {
var s = 1.0 / size * (size / 2.5);
var r = size / 2;
container.append("polyline")
.attr('points', function(d) {
return (x - r) + " " + (y - (1-s) * r) // left , above middle
+ " " + (x - (1-s)*r) + " " + (y - r) // near left , top
+ " " + x + " " + (y - s * r) // middle , above middle
+ " " + (x + ((1-s)* r)) + " " + (y - r) // near right, top
+ " " + (x + r) + " " + (y - r + (s * r)) // right, near top
+ " " + (x + s * r) + " " + y // right of middle, middle
+ " " + (x + r) + " " + (y + r - (s * r)) // right, near bottom
+ " " + (x + ((1-s)* r)) + " " + (y + r) // near right, bottom
+ " " + x + " " + (y + s * r) // middle, below middle
+ " " + (x - (1-s)*r) + " " + (y + r) // near left, bottom
+ " " + (x - r) + " " + (y + (1-s) * r) // left, near bottom
+ " " + (x - s * r) + " " + y // left of middle, middle
+ " " + (x - r) + " " + (y - (1-s) * r); // left , near top
} )
.classed(classed, true)
.attr("fill", colour !== "" ? colour : null);
}
}