/ scripts.js
scripts.js
1 async function main() { 2 const existed = await Dexie.exists('symptoms_database') 3 4 const db = new Dexie("symptoms_database"); 5 db.version(1).stores({ 6 symptoms: 'timestamp,symptom' 7 }); 8 9 if(!existed) { 10 console.log("Database did not exist, adding sample data..."); 11 writeFakeData(db.symptoms, [3, 4, 7, 5, 6, 8, 1].reverse()); 12 } 13 14 let recognizer; 15 let listening = false; 16 17 // Loads the model and starts the prediction. 18 async function predict() { 19 recognizer = speechCommands.create('BROWSER_FFT'); 20 await recognizer.ensureModelLoaded(); 21 22 // Array of words that the recognizer is trained to recognize. 23 const symptoms = recognizer.wordLabels(); 24 recognizer.listen(({scores}) => { 25 // Turn scores into a list of (score,word) pairs. 26 scores = Array.from(scores).map((s, i) => ({score: s, word: symptoms[i]})); 27 // Find the most probable word. 28 scores.sort((s1, s2) => s2.score - s1.score); 29 30 if(scores[0].word == "three") { 31 symptomDetected("cough") 32 } 33 }, {probabilityThreshold: 0.75}); 34 35 document.querySelector('#status').className = 'listening'; 36 listening = true; 37 } 38 39 function stopPredicting() { 40 recognizer.stopListening(); 41 document.querySelector('#status').className = 'stopped'; 42 listening = false; 43 } 44 45 // Callback called everytime a symptom is detected. 46 async function symptomDetected(symptom) { 47 await db.symptoms.add({ 48 timestamp: +moment(), 49 symptom: symptom, 50 }) 51 52 redraw() 53 } 54 55 const chart = document.getElementById('chart'); 56 Chart.defaults.global.defaultFontFamily = 'Nunito, sans-serif'; 57 var myChart = new Chart(chart, { 58 plugins: [ChartDataLabels], 59 type: 'line', 60 data: { 61 datasets:[{ 62 data: [], 63 fill: false, 64 borderColor: '#E0FCFF', 65 pointBackgroundColor: '#85BAC0', 66 pointBorderColor: '#85BAC0', 67 pointHoverBackgroundColor: '#85BAC0', 68 pointHoverBorderColor: '#85BAC0', 69 pointRadius: 10, 70 pointHoverRadius: 10, 71 borderWidth: 7, 72 hoverBorderWidth: 7, 73 }] 74 }, 75 options: { 76 legend: {display: false}, 77 scales: { 78 xAxes: [{ 79 gridLines: { 80 color: '#91837A', 81 lineWidth: 2, 82 drawBorder: false, 83 zeroLineWidth: 0, 84 fontColor: '#fff', 85 }, 86 ticks: { 87 fontColor: '#fff', 88 fontSize: 24, 89 padding: 30, 90 }, 91 type: 'time', 92 time: { 93 unit: 'day', 94 } 95 }], 96 yAxes: [{ 97 gridLines: { 98 color: '#91837A', 99 lineWidth: 2, 100 drawBorder: false, 101 zeroLineWidth: 0, 102 }, 103 ticks: { 104 beginAtZero: true, 105 precision: 0, 106 stepSize: 10, 107 max: 80, 108 fontColor: '#fff', 109 fontSize: 16, 110 padding: 30, 111 }, 112 }] 113 }, 114 hover: { 115 animationDuration: 100, 116 }, 117 tooltips: { 118 enabled: false, 119 xPadding: 10, 120 yPadding: 10, 121 bodyFontSize: 20, 122 bodyFontColor: '#3E3E3E', 123 bodyFontStyle: 'bold', 124 titleFontSize: 0, 125 backgroundColor: '#fff', 126 displayColors: false, 127 callbacks: { 128 title: function() { 129 return ''; 130 } 131 } 132 }, 133 plugins: { 134 // Change options for ALL labels of THIS CHART 135 datalabels: { 136 opacity: 0.9, 137 color: '#36A2EB', 138 anchor: 'end', 139 align: 'end', 140 backgroundColor: '#fff', 141 borderRadius: 3, 142 color: '#3E3E3E', 143 offset: 10, 144 font: { 145 size: 14, 146 }, 147 formatter: function(value, context) { 148 return ''+value.y; 149 } 150 } 151 } 152 } 153 }); 154 155 // To be called everytime an event happened. 156 async function redraw() { 157 const bounds = lastNDays(7) 158 159 const data = [] 160 let sum = 0 161 for(const i in bounds) { 162 [start, end] = bounds[i] 163 const coughs = await db.symptoms.where('timestamp').between(+start, +end).count() 164 data.push({x: start.toDate(), y: coughs}) 165 sum += coughs 166 } 167 168 const last = data[1].y 169 const diff = last - data[2].y 170 const today = data[0].y 171 const worst = Math.max(last, today) 172 const average = sum/7 173 174 if(worst < 10 && diff < 5 && worst <= average) { 175 document.querySelector('#health').className = 'good'; 176 } else if(worst < 20 && diff < 10) { 177 document.querySelector('#health').className = 'warn'; 178 } else { 179 document.querySelector('#health').className = 'bad'; 180 } 181 182 myChart.data.datasets[0].data = data 183 myChart.update(); 184 } 185 186 // Call redraw() to draw the initial state from the database. 187 redraw() 188 189 // Start predicting by default. 190 predict(); 191 192 onClick('status-listening', function() { 193 if(listening) { 194 stopPredicting(); 195 } 196 }) 197 198 onClick('status-stopped', function() { 199 if(!listening) { 200 predict(); 201 } 202 }) 203 204 onClick('bsp1', function() { 205 db.symptoms.clear(); 206 writeFakeData(db.symptoms, [3, 4, 7, 5, 6, 4, 1].reverse()); 207 redraw(); 208 }); 209 210 onClick('bsp2', function() { 211 db.symptoms.clear(); 212 writeFakeData(db.symptoms, [3, 9, 11, 20, 25, 19, 15].reverse()); 213 redraw(); 214 }); 215 216 onClick('bsp3', function() { 217 db.symptoms.clear(); 218 writeFakeData(db.symptoms, [3, 9, 22, 37, 45, 64, 65].reverse()); 219 redraw(); 220 }); 221 222 onClick('random', function() { 223 db.symptoms.clear(); 224 writeFakeData(db.symptoms, [...Array(7).keys()].map(i => Math.floor(Math.random() * 60))) 225 redraw(); 226 }) 227 228 onClick('reset', function() { 229 db.symptoms.clear(); 230 redraw(); 231 }) 232 233 document.addEventListener('keydown', function(event) { 234 const key = event.key; // "a", "1", "Shift", etc. 235 if(key == "h") { 236 db.symptoms.add({ 237 timestamp: +moment(), 238 symptom: "cough" 239 }); 240 redraw(); 241 } 242 }); 243 244 } 245 246 main(); 247 248 function onClick(id, f) { 249 document.getElementById(id).addEventListener('click', function() { 250 f(); 251 return false; 252 }); 253 } 254 255 async function writeFakeData(table, fakes) { 256 const bounds = lastNDays(fakes.length) 257 const data = [] 258 for(const i in bounds) { 259 const [start, end] = bounds[i]; 260 const count = fakes[i] 261 for(let j = 0; j < count; j++) { 262 data.push({ 263 timestamp: +start+j, 264 symptom: "cough", 265 }) 266 } 267 } 268 await table.bulkAdd(data) 269 }