/ 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  }