}).map(function(quarter, ix, all) { var bgValues = quarter.records.map(function(record) { return record.sgv; }); quarter.standardDeviation = ss.standard_deviation(bgValues); quarter.average = bgValues.length > 0? (sum(bgValues) / bgValues.length): 'N/A'; quarter.lowerQuartile = ss.quantile(bgValues, 0.25); quarter.upperQuartile = ss.quantile(bgValues, 0.75); quarter.numberLow = bgValues.filter(function(bg) { return bg < low; }).length; quarter.numberHigh = bgValues.filter(function(bg) { return bg >= high; }).length; quarter.numberInRange = bgValues.length - (quarter.numberHigh + quarter.numberLow); quarter.percentLow = (quarter.numberLow / bgValues.length) * 100; quarter.percentInRange = (quarter.numberInRange / bgValues.length) * 100; quarter.percentHigh = (quarter.numberHigh / bgValues.length) * 100; averages.percentLow += quarter.percentLow / all.length; averages.percentInRange += quarter.percentInRange / all.length; averages.percentHigh += quarter.percentHigh / all.length; averages.lowerQuartile += quarter.lowerQuartile / all.length; averages.upperQuartile += quarter.upperQuartile / all.length; averages.average += quarter.average / all.length; averages.standardDeviation += quarter.standardDeviation / all.length; return quarter; });
function toStats (sample) { return { "84th": ss.quantile(sample, 0.84), "98th": ss.quantile(sample, 0.98), "mean": ss.mean(sample), "standardDeviation": ss.standardDeviation(sample), "max": ss.max(sample), "min": ss.min(sample) }; }
module.exports = function(fc, z, classification, numBreaks, colors){ // JENKS if(classification === 'jenks'){ var vals = _.chain(fc.features) .pluck('properties') .pluck(z) .value() var breaks = ss.jenks(vals, numBreaks) var normals = normalize(breaks,1) fc = colorize(fc, z, colors, breaks, normals) return fc } // QUANTILE else if(classification === 'quantile'){ var vals = _.chain(fc.features) .pluck('properties') .pluck(z) .value() var min = ss.min(vals) var max = ss.max(vals) var interval = 1 / numBreaks var quants = [0] var currentBreak = 0 for(var i=0;i<numBreaks;i++){ currentBreak += interval quants.push(currentBreak) } var breaks = ss.quantile(vals, quants) var normals = normalize(breaks,1) fc = colorize(fc, z, colors, breaks, normals) return fc } // EQUAL INTERVAL else if(classification === 'interval'){ var vals = _.chain(fc.features) .pluck('properties') .pluck(z) .value() var min = ss.min(vals) var max = ss.max(vals) var interval = (max - min) / numBreaks var breaks = [0] var currentBreak = 0 for(var i=0;i<=numBreaks;i++){ currentBreak += interval breaks.push(currentBreak) } var normals = normalize(breaks,1) fc = colorize(fc, z, colors, breaks, normals) return fc } // UNKOWN else{ return new Error('unsupported classification: ' + z) } }
_.each(stat, function(s, field) { var stepRange; if (!s.values) { console.log('Issue with stat for group: ' + statGroup); } stats[statGroup].count = s.values.length; //stats[statGroup][field].sum = ss.sum(s.values); //stats[statGroup][field].min = ss.min(s.values); //stats[statGroup][field].max = ss.max(s.values); stats[statGroup][field].mean = ss.mean(s.values); stats[statGroup][field].median = ss.median(s.values); //stats[statGroup][field].mode = ss.mode(s.values); //stats[statGroup][field].variance = ss.variance(s.values); //stats[statGroup][field].standard_deviation = ss.standard_deviation(s.values); stats[statGroup][field].q25 = ss.quantile(s.values, 0.25); stats[statGroup][field].q75 = ss.quantile(s.values, 0.75); stepRange = outlierRange(s.values, stats[statGroup][field].q25, stats[statGroup][field].q75, stats[statGroup][field].median); stats[statGroup][field].stepL = stepRange[0]; stats[statGroup][field].stepU = stepRange[1]; });
quantiles: function(fc, z, numBreaks, colors, style){ var vals = _.chain(fc.features) .pluck('properties') .pluck(z) .value() var min = ss.min(vals) var max = ss.max(vals) var interval = 1 / numBreaks var quants = [0] var currentBreak = 0 for(var i=0;i<numBreaks;i++){ currentBreak += interval quants.push(currentBreak) } var breaks = ss.quantile(vals, quants) var normals = normalize(breaks.length) fc = colorize(fc, z, colors, breaks, normals) fc = applyStyle(fc, style) return fc },
/** * Builds and returns an array describing the legend colors. * Each element is an object with keys "color" and "upperBound", eg. * [ { color: [r, g, b, a], upperBound: 20 } , { color: [r, g, b, a]: upperBound: 80 } ] * @private * @param {LegendHelper} legendHelper The legend helper. * @param {Integer|Number[]} colorBins The number of color bins to use, or the boundaries to use. * @return {Array} Array of objects with keys "color" and "upperBound". */ function buildBinColors(legendHelper, colorBins) { var tableColumn = legendHelper.tableColumn; var tableColumnStyle = legendHelper.tableColumnStyle; var colorGradient = legendHelper._colorGradient; // If colorBins is an array, just return it in the right format. var extremes = getExtremes(tableColumn, tableColumnStyle); if (Array.isArray(colorBins) && defined(extremes.minimum) && defined(extremes.maximum)) { // If the max value is beyond the range, add it to the end. // Do this to be symmetric with min and max. if (colorBins[colorBins.length - 1] < extremes.maximum) { colorBins = colorBins.concat(extremes.maximum); } var numberOfColorBins = colorBins.length; var filteredBins = colorBins.filter(function(bound, i) { // By cutting off all bins equal to or lower than the min value, // the min value will be added as a titleBelow instead of titleAbove. // Since any bins wholy below the min are removed, do the same with max. return (bound > extremes.minimum) && (i === 0 || colorBins[i - 1] < extremes.maximum); }); // Offset to make sure that the correct color is used when the legend is truncated var binOffset = colorBins.indexOf(filteredBins[0]); return filteredBins.map(function(bound, i) { return { // Just use the provided bound, but cap it at the max value. upperBound: Math.min(bound, extremes.maximum), colorArray: getColorArrayFromColorGradient(colorGradient, (binOffset + i) / (numberOfColorBins - 1)) }; }); } if (colorBins <= 0 || tableColumnStyle.colorBinMethod.match(/none/i)) { return undefined; } var binColors = []; var i; var numericalValues = tableColumn.numericalValues; if (numericalValues.length === 0) { return []; } // Must ask for fewer clusters than the number of items. var binCount = Math.min(colorBins, numericalValues.length); var method = tableColumnStyle.colorBinMethod.toLowerCase(); if (method === 'auto') { if (numericalValues.length > 1000) { // The quantile method is simpler and less accurate, but faster for large datasets. method = 'quantile'; } else { method = 'ckmeans'; } } if (method === 'quantile') { // One issue is we don't check to see if any values actually lie within a given quantile, so it's bad for small datasets. for (i = 0; i < binCount; i++) { binColors.push({ upperBound: simplestats.quantile(numericalValues, (i + 1) / binCount), colorArray: getColorArrayFromColorGradient(colorGradient, i / (binCount - 1)) }); } } else if (method === 'ckmeans') { var clusters = simplestats.ckmeans(numericalValues, binCount); // Convert the ckmeans format [ [5, 20], [65, 80] ] into our format. for (i = 0; i < clusters.length; i++) { if (i > 0 && clusters[i].length === 1 && clusters[i][0] === clusters[i - 1][clusters[i - 1].length - 1]) { // When there are few unique values, we can end up with clusters like [1], [2],[2],[2],[3]. Let's avoid that. continue; } binColors.push({ upperBound: clusters[i][clusters[i].length - 1], }); } if (binColors.length > 1) { for (i = 0; i < binColors.length; i++) { binColors[i].colorArray = getColorArrayFromColorGradient(colorGradient, i / (binColors.length - 1)); } } else { // only one binColor, pick the middle of the color gradient. binColors[0].colorArray = getColorArrayFromColorGradient(colorGradient, 0.5); } } return binColors; }
glucosedistribution.report = function report_glucosedistribution(datastorage, sorteddaystoshow, options) { var Nightscout = window.Nightscout; var client = Nightscout.client; var translate = client.translate; var ss = require('simple-statistics'); var colors = ['#f88', '#8f8', '#ff8']; var tablecolors = { Low: '#f88', Normal: '#8f8', High: '#ff8' }; var enabledHours = [true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true]; var report = $('#glucosedistribution-report'); report.empty(); var stability = $('#glucosedistribution-stability'); stability.empty(); var stats = []; var table = $('<table class="centeraligned">'); var thead = $('<tr/>'); $('<th>' + translate('Range') + '</th>').appendTo(thead); $('<th>' + translate('% of Readings') + '</th>').appendTo(thead); $('<th>' + translate('# of Readings') + '</th>').appendTo(thead); $('<th>' + translate('Average') + '</th>').appendTo(thead); $('<th>' + translate('Median') + '</th>').appendTo(thead); $('<th>' + translate('Standard Deviation') + '</th>').appendTo(thead); $('<th>' + translate('A1c estimation*') + '</th>').appendTo(thead); thead.appendTo(table); var data = datastorage.allstatsrecords; var days = datastorage.alldays; $('#glucosedistribution-days').text(days + ' ' + translate('days total')); for (var i = 0; i < 23; i++) { $('#glucosedistribution-' + i).unbind('click').click(onClick); enabledHours[i] = $('#glucosedistribution-' + i).is(':checked'); } //console.log(enabledHours); var result = {}; // Filter data for noise var glucose_data = [data[0]]; // data cleaning pass 0 - remove duplicates and sort var seen = {}; data = data.filter(function(item) { return seen.hasOwnProperty(item.displayTime) ? false : (seen[item.displayTime] = true); }); data.sort(function(a,b){ return a.displayTime.getTime()-b.displayTime.getTime(); }); // data cleaning pass 1 - add interpolated missing points for (var i = 0; i < data.length - 2; i++) { var entry = data[i]; var nextEntry = data[i + 1]; var timeDelta = nextEntry.displayTime.getTime() - entry.displayTime.getTime(); if (timeDelta < 9 * 60 * 1000 || timeDelta > 25 * 60 * 1000) { glucose_data.push(entry); continue; } var missingRecords = Math.floor(timeDelta / (5 * 60 * 990)) -1; var timePatch = Math.floor(timeDelta / (missingRecords + 1)); var bgDelta = (nextEntry.bgValue - entry.bgValue) / (missingRecords + 1); glucose_data.push(entry); for (var j = 1; j <= missingRecords; j++) { var bg = Math.floor(entry.bgValue + bgDelta * j); var t = new Date(entry.displayTime.getTime() + j * timePatch); var newEntry = { bgValue: bg, displayTime: t }; glucose_data.push(newEntry); } } // data cleaning pass 2 - replace single jumpy measures with interpolated values var glucose_data2 = [glucose_data[0]]; var prevEntry = glucose_data[0]; for (var i = 1; i < glucose_data.length-2; i++) { // var prevEntry = glucose_data[i-1]; var entry = glucose_data[i]; var nextEntry = glucose_data[i+1]; var timeDelta = nextEntry.displayTime.getTime() - entry.displayTime.getTime(); var timeDelta2 = entry.displayTime.getTime() - nextEntry.displayTime.getTime(); var maxGap = (5 * 60 * 1000) + 10000; if (timeDelta > maxGap || timeDelta2 > maxGap ) { glucose_data2.push(entry); prevEntry = entry; continue; } var delta1 = entry.bgValue - prevEntry.bgValue; var delta2 = nextEntry.bgValue - entry.bgValue; if (delta1 <= 8 && delta2 <= 8) { glucose_data2.push(entry); prevEntry = entry; continue; } if ((delta1 > 0 && delta2 <0) || (delta1 < 0 && delta2 > 0)) { var d = (nextEntry.bgValue - prevEntry.bgValue) / 2; var newEntry = { bgValue: prevEntry.bgValue + d, displayTime: entry.displayTime }; glucose_data2.push(newEntry); prevEntry = newEntry; continue; } glucose_data2.push(entry); prevEntry = entry; } glucose_data = data = glucose_data2.filter(function(r) { return enabledHours[new Date(r.displayTime).getHours()] }); ['Low', 'Normal', 'High'].forEach(function(range) { result[range] = {}; var r = result[range]; r.rangeRecords = glucose_data.filter(function(r) { if (range === 'Low') { return r.sgv > 0 && r.sgv < options.targetLow; } else if (range === 'Normal') { return r.sgv >= options.targetLow && r.sgv < options.targetHigh; } else { return r.sgv >= options.targetHigh; } }); stats.push(r.rangeRecords.length); r.rangeRecords.sort(function(a, b) { return a.sgv - b.sgv; }); r.localBgs = r.rangeRecords.map(function(r) { return r.sgv; }).filter(function(bg) { return !!bg; }); r.midpoint = Math.floor(r.rangeRecords.length / 2); r.readingspct = (100 * r.rangeRecords.length / data.length).toFixed(1); if (r.rangeRecords.length > 0) { r.mean = Math.floor(10 * ss.mean(r.localBgs)) / 10; r.median = r.rangeRecords[r.midpoint].sgv; r.stddev = Math.floor(ss.standard_deviation(r.localBgs) * 10) / 10; } }); // make sure we have total 100% result.Normal.readingspct = (100 - result.Low.readingspct - result.High.readingspct).toFixed(1); ['Low', 'Normal', 'High'].forEach(function(range) { var tr = $('<tr>'); var r = result[range]; var rangeExp = ''; if (range == 'Low') { rangeExp = ' (<' + options.targetLow + ')'; } if (range == 'High') { rangeExp = ' (>=' + options.targetHigh + ')'; } $('<td class="tdborder" style="background-color:' + tablecolors[range] + '"><strong>' + translate(range) + rangeExp + ': </strong></td>').appendTo(tr); $('<td class="tdborder">' + r.readingspct + '%</td>').appendTo(tr); $('<td class="tdborder">' + r.rangeRecords.length + '</td>').appendTo(tr); if (r.rangeRecords.length > 0) { $('<td class="tdborder">' + r.mean.toFixed(1) + '</td>').appendTo(tr); $('<td class="tdborder">' + r.median.toFixed(1) + '</td>').appendTo(tr); $('<td class="tdborder">' + r.stddev.toFixed(1) + '</td>').appendTo(tr); $('<td> </td>').appendTo(tr); } else { $('<td class="tdborder">N/A</td>').appendTo(tr); $('<td class="tdborder">N/A</td>').appendTo(tr); $('<td class="tdborder">N/A</td>').appendTo(tr); $('<td class="tdborder"> </td>').appendTo(tr); } table.append(tr); }); var tr = $('<tr>'); $('<td class="tdborder"><strong>' + translate('Overall') + ': </strong></td>').appendTo(tr); $('<td> </td>').appendTo(tr); $('<td class="tdborder">' + glucose_data.length + '</td>').appendTo(tr); if (glucose_data.length > 0) { var localBgs = glucose_data.map(function(r) { return r.sgv; }).filter(function(bg) { return !!bg; }); var mgDlBgs = glucose_data.map(function(r) { return r.bgValue; }).filter(function(bg) { return !!bg; }); $('<td class="tdborder">' + (Math.round(10 * ss.mean(localBgs)) / 10).toFixed(1) + '</td>').appendTo(tr); $('<td class="tdborder">' + (Math.round(10 * ss.quantile(localBgs, 0.5)) / 10).toFixed(1) + '</td>').appendTo(tr); $('<td class="tdborder">' + (Math.round(ss.standard_deviation(localBgs) * 10) / 10).toFixed(1) + '</td>').appendTo(tr); $('<td class="tdborder"><center>' + (Math.round(10 * (ss.mean(mgDlBgs) + 46.7) / 28.7) / 10).toFixed(1) + '%<sub>DCCT</sub> | ' + Math.round(((ss.mean(mgDlBgs) + 46.7) / 28.7 - 2.15) * 10.929) + '<sub>IFCC</sub></center></td>').appendTo(tr); } else { $('<td class="tdborder">N/A</td>').appendTo(tr); $('<td class="tdborder">N/A</td>').appendTo(tr); $('<td class="tdborder">N/A</td>').appendTo(tr); $('<td class="tdborder">N/A</td>').appendTo(tr); } table.append(tr); report.append(table); // Stability var t1 = 6; var t2 = 11; var t1count = 0; var t2count = 0; var total = 0; var events = 0; var GVITotal = 0; var GVIIdeal = 0; var RMSTotal = 0; var usedRecords = 0; var glucoseTotal = 0; var deltaTotal = 0; for (var i = 0; i < glucose_data.length - 2; i++) { var entry = glucose_data[i]; var nextEntry = glucose_data[i + 1]; var timeDelta = nextEntry.displayTime.getTime() - entry.displayTime.getTime(); if (timeDelta > 6 * 60 * 1000) { // console.log("Record skipped"); continue; } usedRecords += 1; var delta = Math.abs(nextEntry.bgValue - entry.bgValue); deltaTotal += delta; total += delta; events += 1; if (delta >= t1) { t1count += 1; } if (delta >= t2) { t2count += 1; } GVITotal += Math.sqrt(25 + Math.pow(delta, 2)); glucoseTotal += entry.bgValue; if (entry.bgValue < options.targetLow) { RMSTotal += Math.pow(options.targetLow - entry.bgValue, 2); } if (entry.bgValue > options.targetHigh) { RMSTotal += Math.pow(entry.bgValue - options.targetHigh, 2); } } var GVIDelta = Math.floor(glucose_data[0].bgValue,glucose_data[glucose_data.length-1].bgValue); GVIIdeal = Math.sqrt(Math.pow(usedRecords*5,2) + Math.pow(GVIDelta,2)); var GVI = Math.round(GVITotal / GVIIdeal * 100) / 100; console.log('GVI',GVI,'GVIIdeal',GVIIdeal,'GVITotal',GVITotal); var glucoseMean = Math.floor(glucoseTotal / usedRecords); var tirMultiplier = result.Normal.readingspct / 100.0; var PGS = Math.round(GVI * glucoseMean * (1-tirMultiplier) * 100) / 100; console.log('glucoseMean', glucoseMean,'tirMultiplier',tirMultiplier, 'PGS',PGS); var days = (glucose_data[glucose_data.length-1].displayTime.getTime() - glucose_data[0].displayTime.getTime()) / (24*60*60*1000.0); var TDC = deltaTotal / days; var TDCHourly = TDC / 24.0; var RMS = Math.sqrt(RMSTotal / events); // console.log('TADC',TDC,'days',days); var timeInT1 = Math.round(100 * t1count / events).toFixed(1); var timeInT2 = Math.round(100 * t2count / events).toFixed(1); var mac = (total / events).toFixed(1); var unitString = ' mg/dl'; if (client.settings.units == 'mmol') { mac = (total / events / 18.0).toFixed(2); TDC = TDC / 18.0; TDCHourly = TDCHourly / 18.0; unitString = ' mmol/L'; RMS = Math.sqrt(RMSTotal / events) / 18; } TDC = Math.round(TDC * 100) / 100; TDCHourly = Math.round(TDCHourly * 100) / 100; var stabilitytable = $('<table style="width: 100%;">'); var t1exp = '>5 mg/dl/5m'; var t2exp = '>10 mg/dl/5m'; if (client.settings.units == 'mmol') { t1exp = '>0.27 mmol/l/5m'; t2exp = '>0.55 mmol/l/5m'; } $('<tr><th>' + translate('Mean Total Daily Change') + '</th><th>' + translate('Time in fluctuation') + '<br>(' + t1exp + ')</th><th>' + translate('Time in rapid fluctuation') + '<br>(' + t2exp + ')</th></tr>').appendTo(stabilitytable); $('<tr><td class="tdborder">' + TDC + unitString + '</td><td class="tdborder">' + timeInT1 + '%</td><td class="tdborder">' + timeInT2 + '%</td></tr>').appendTo(stabilitytable); $('<tr><th>' + translate('Mean Hourly Change') + '</th><th>GVI</th><th>PGS</th></tr>').appendTo(stabilitytable); $('<tr><td class="tdborder">' + TDCHourly + unitString + '</td><td class="tdborder">' + GVI + '</td><td class="tdborder">' + PGS + '</td></tr>').appendTo(stabilitytable); // $('<tr><th>Out of Range RMS</th></tr>').appendTo(stabilitytable); // $('<tr><td class="tdborder">' + Math.round(RMS * 100) / 100 + unitString + '</td></tr>').appendTo(stabilitytable); stabilitytable.appendTo(stability); setTimeout(function() { $.plot( '#glucosedistribution-overviewchart', stats, { series: { pie: { show: true } }, colors: colors } ); }); function onClick() { report_glucosedistribution(datastorage, sorteddaystoshow, options); } };
var UnicodeMap = require('./'); var ss = require('simple-statistics'); var map = new UnicodeMap(document.getElementById('map')); var data = []; var scale = [ '#21313E', '#324D60', '#436C83', '#538DA8', '#62AFCE', '#70D3F4'].reverse(); var values = []; for (var k in all) { data.push([+k, all[k]]); values.push(all[k]); } var quantiles = ss.quantile(values, [0, 0.2, 0.4, 0.6, 0.8, 1]); for (var i = 0; i < data.length; i++) { for (var j = 0; j < scale.length; j++) { if (data[i][1] <= quantiles[j]) data[i][1] = scale[j]; } } map.draw(data);
_.each(percentiles, function(percentile){ quantiles.push(ss.quantile(vals, percentile * .01)) })
sorteddaystoshow.forEach(function (day) { var tr = $('<tr>'); var daysRecords = datastorage[day].statsrecords; if (daysRecords.length === 0) { $('<td/>').appendTo(tr); $('<td class=\"tdborder\" style=\"width:160px\">' + report_plugins.utils.localeDate(day) + '</td>').appendTo(tr); $('<td class=\"tdborder\"colspan="10">'+translate('No data available')+'</td>').appendTo(tr); table.append(tr); return;; } minForDay = daysRecords[0].sgv; maxForDay = daysRecords[0].sgv; sum = 0; var stats = daysRecords.reduce(function(out, record) { record.sgv = parseFloat(record.sgv); if (record.sgv < options.targetLow) { out.lows++; } else if (record.sgv < options.targetHigh) { out.normal++; } else { out.highs++; } if (minForDay > record.sgv) { minForDay = record.sgv; } if (maxForDay < record.sgv) { maxForDay = record.sgv; } sum += record.sgv; return out; }, { lows: 0, normal: 0, highs: 0 }); var average = sum / daysRecords.length; var bgValues = daysRecords.map(function(r) { return r.sgv; }); $('<td><div id=\"dailystat-chart-' + day.toString() + '\" class=\"inlinepiechart\"></div></td>').appendTo(tr); $('<td class=\"tdborder\" style=\"width:160px\">' + report_plugins.utils.localeDate(day) + '</td>').appendTo(tr); $('<td class=\"tdborder\">' + Math.round((100 * stats.lows) / daysRecords.length) + '%</td>').appendTo(tr); $('<td class=\"tdborder\">' + Math.round((100 * stats.normal) / daysRecords.length) + '%</td>').appendTo(tr); $('<td class=\"tdborder\">' + Math.round((100 * stats.highs) / daysRecords.length) + '%</td>').appendTo(tr); $('<td class=\"tdborder\">' + daysRecords.length +'</td>').appendTo(tr); $('<td class=\"tdborder\">' + minForDay +'</td>').appendTo(tr); $('<td class=\"tdborder\">' + maxForDay +'</td>').appendTo(tr); $('<td class=\"tdborder\">' + average.toFixed(1) +'</td>').appendTo(tr); $('<td class=\"tdborder\">' + ss.standard_deviation(bgValues).toFixed(1) + '</td>').appendTo(tr); $('<td class=\"tdborder\">' + ss.quantile(bgValues, 0.25).toFixed(1) + '</td>').appendTo(tr); $('<td class=\"tdborder\">' + ss.quantile(bgValues, 0.5).toFixed(1) + '</td>').appendTo(tr); $('<td class=\"tdborder\">' + ss.quantile(bgValues, 0.75).toFixed(1) + '</td>').appendTo(tr); table.append(tr); var inrange = [ { label: translate('Low'), data: Math.round(stats.lows * 1000 / daysRecords.length) / 10 }, { label: translate('In Range'), data: Math.round(stats.normal * 1000 / daysRecords.length) / 10 }, { label: translate('High'), data: Math.round(stats.highs * 1000 / daysRecords.length) / 10 } ]; $.plot( '#dailystat-chart-' + day.toString(), inrange, { series: { pie: { show: true } }, colors: ['#f88', '#8f8', '#ff8'] } ); });
percentiles.forEach(function(percentile) { quantiles.push(ss.quantile(vals, percentile * 0.01)); });
/** * Builds and returns an array describing the legend colors. * Each element is an object with keys "color" and "upperBound", eg. * [ { color: [r, g, b, a], upperBound: 20 } , { color: [r, g, b, a]: upperBound: 80 } ] * * @param {LegendHelper} legendHelper The legend helper. * @param {Integer} [colorBins] The number of color bins to use; defaults to legendHelper.tableColumnStyle.colorBins. * @return {Array} Array of objects with keys "color" and "upperBound". */ function buildBinColors(legendHelper, colorBins) { var tableColumn = legendHelper.tableColumn; var tableColumnStyle = legendHelper.tableColumnStyle; var colorGradient = legendHelper._colorGradient; var regionProvider = legendHelper._regionProvider; if (!defined(colorBins)) { colorBins = tableColumnStyle.colorBins; } if (colorBins <= 0 || tableColumnStyle.colorBinMethod.match(/none/i)) { return undefined; } var binColors = []; var i; var numericalValues; var usesIndicesIntoUniqueValues; if (!defined(tableColumn) || !defined(tableColumn.values)) { usesIndicesIntoUniqueValues = false; // There is no tableColumn. // Number by the index into regions instead, if it's region mapped; otherwise return undefined. if (regionProvider) { numericalValues = regionProvider.regions.map(function(region, index) { return index; }); } else { return undefined; } } else { usesIndicesIntoUniqueValues = tableColumn.usesIndicesIntoUniqueValues; numericalValues = tableColumn.indicesOrNumericalValues; } // Must ask for fewer clusters than the number of items. var binCount = Math.min(colorBins, numericalValues.length); // Convert the output formats of two binning methods into our format. if (tableColumnStyle.colorBinMethod === 'quantile' || tableColumnStyle.colorBinMethod === 'auto' && numericalValues.length > 1000 && (!usesIndicesIntoUniqueValues)) { // the quantile method is simpler, less accurate, but faster for large datasets. One issue is we don't check to see if any // values actually lie within a given quantile, so it's bad for small datasets. for (i = 0; i < binCount; i++) { binColors.push({ upperBound: simplestats.quantile(numericalValues, (i + 1) / binCount), colorArray: getColorArrayFromColorGradient(colorGradient, i / (binCount - 1)) }); } } else { var clusters = simplestats.ckmeans(numericalValues, binCount); // Convert the ckmeans format [ [5, 20], [65, 80] ] into our format. for (i = 0; i < clusters.length; i++) { if (i > 0 && clusters[i].length === 1 && clusters[i][0] === clusters[i - 1][clusters[i - 1].length - 1]) { // When there are few unique values, we can end up with clusters like [1], [2],[2],[2],[3]. Let's avoid that. continue; } binColors.push({ upperBound: clusters[i][clusters[i].length - 1], }); } if (binColors.length > 1) { for (i = 0; i < binColors.length; i++) { binColors[i].colorArray = getColorArrayFromColorGradient(colorGradient, i / (binColors.length - 1)); } } else { // only one binColor, pick the middle of the color gradient. binColors[0].colorArray = getColorArrayFromColorGradient(colorGradient, 0.5); } } return binColors; }
var dat90 = bins.map(function(bin) { return [bin[0], ss.quantile(bin[1], 0.9)]; });