UNPKG

@valcri/spc-sd

Version:

Detects SPC signals in datasets.

350 lines (315 loc) 11.8 kB
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); } }