-
Notifications
You must be signed in to change notification settings - Fork 0
Code
Om goed met de data te kunnen werken, was het nodig zelf een API op te zetten, omdat de data tot nog toe enkel met SQL op te halen is. Zodoende hebben wij in onze app.js een route toegevoegd, genaamd data. Dit endpoint gebruiken wij om verschillende typen data op te halen in .json formaat en client side onze grafieken mee te renderen. In routes/data.js worden de verschillende endpoints gedefinieerd. Zodoende zijn er 5 verschillende endpoints:
- /user, om de gebruikersgegevens op te halen als het ID en het ID van de beweegmeter.
- /daily, om de data over hoe het kind zich dagelijks voelt op te halen.
- /trophy, om de data over de behaalde trofeeën op te halen.
- /pam, om de data van de beweegmeter op te halen.
- /all, om in 1 keer alle bovenstaande data voor een gebruiker op te halen.
In het bestand modules/data.js wordt per endpoint de query gedefinieerd, de data opgehaald en als .json aan de route aangeboden. Voor het ophalen van alle data en de userdata ziet dat er bijvoorbeeld als volgt uit:
async getAll() {
const allData = await Api('SELECT * FROM pam_data')
return allData
},
async getUserData(userID) {
const userQuery = `SELECT * FROM simbapam.pa_users WHERE username = "${userID}";`
const userData = await Api(userQuery)
return userData
},De logica waarmee de SQL database wordt benaderd staat gedefinieerd in modules/api.js. Eerst wordt daar bovenin een connectie gedefinieerd, waarbij de nodige variabelen worden opgehaald uit onze .env:
const con = mysql.createConnection({
host: process.env.HOSTNAME,
user: process.env.DBUSERNAME,
password: process.env.PASSWORD,
database: process.env.DATABASE,
port: process.env.HOSTPORT
})Daarna wordt connectie gemaakt met de database en wordt de query waarmee de functie wordt aangeroepen gebruikt om de nodige data op te halen:
async function apiData (query){
con.connect((err) => {
if(err){
console.log('Error connecting to Db or a handshake is already enqued')
return
}
console.log('Connection established')
})
let promise = await new Promise((resolve) => {
con.query(query, function (error, results) {
if (error) throw error
resolve(results)
})})
return await promise
}Om samen tot een dezelfde stijl te komen hebben wij een header gemaakt die wij in elke pagina hebben gezet, dit gaf al een klein idee van de stijl die wij aan moesten houden. Ook hebben wij gebruik gemaakt van custom variabelen binnen css
:root {
--font-main: 'Roboto', sans-serif;
--font-weight-light: 300;
--font-weight-regular: 400;
--font-weight-bold: 500;
--c-blue-dark: rgb(43, 116, 122);
--c-blue: #3CC3B8;
--c-blue-light: #9DD3CF;
--c-blue-lightest: #DEF5F7;
--c-white: white;
}De code voor deze grafiek is te vinden in public/scripts/feeling.js.
Om de grafiek makkelijk aanpasbaar te maken heb ik bovenin een settings object toegevoegd. Hierin zijn de marges, de container waarin de grafiek geladen moet worden, de afstand tussen de grafieken en de labels voor de dagen aan te passen. Dit object heeft dan ook de volgende structuur.
const settings = {,
api: {
baseUrl: window.location.protocol + '//' + window.location.host,
endpoint: '/data/movement/',
userID: 'PA043F3',
},
container: {
id: 'graph-container',
},
margins: {
left:100,
right:50,
top:40,
bottom: 40
},
spaceBetweenGraphs: 0,
dayLabels: ['Zondag', 'Maandag', 'Dinsdag', 'Woensdag', 'Donderdag', 'Vrijdag', 'Zaterdag']
}Daarnaast was het nodig om bij te houden welke intensiteitscategorieën zichtbaar gemaakt worden in de grafiek. Daarom hebben wij een states object aangemaakt waarin deze categorieën getoggled kunnen worden. Dit object heeft de volgende structuur:
const states = {
visibleIntensities: {
light: true,
medium: true,
heavy: true
}
}Om op basis van de marges die in het settings object staan opgeslagen de breedtes en hoogtes voor de grafieken te berekenen, heb ik nog een object genaamd getters aangemaakt, waarmee gemakkelijk deze variabelen opgevraagd kunnen worden.
De data die ik verkrijg wordt met een getData() opgehaald en daarna met een transformData() aangepast. De tranformed data heb ik nodig om een stacked bar chart te maken.
function transformData(rawData) {
const data = []
let week = []
let count = 1
rawData.data.pamData.forEach((item, i) => {
let activity = {
date: item.date,
total: (item.light_activity + item.medium_activity + item.heavy_activity),
light: item.light_activity,
medium: item.medium_activity,
heavy: item.heavy_activity
}
if (week.length >= 7) {
data.push({
week: count,
group: week
})
count = count + 1
week = []
week.push(activity)
return
} else {
week.push(activity)
}
})
return data
}Om de filters toe te passen moet ik de data filteren. Omdat ik de originele data niet wil aanpassen moet ik eerst een copy maken en die opsturen voor het gebruik met de filters. Dit doe ik met een filterData() functie. Ik kan hierbij geen spread operator gebruiken [...data], omdat die geen deep copy maakt. Hiervoor moet ik eerst alles omzetten naar een json string en daarna weer parsen.
function filterData(data, id, val) {
const deepCopy = JSON.parse(JSON.stringify(data))
let week = deepCopy[val].group
week.forEach((d) => {
d[id] = 0
})
return deepCopy
}De data die wij verkrijgen wordt in een andere notatie opgestuurd dan dat wij gewend zijn om te lezen. Hiervoor heb ik een getDay() en getDate() functie geschreven. Bij de getDay() functie word het omgezet naar een dag en bij de getDate() wordt het omgezet naar een datum.
function getDate(d) {
let date = new Date(d)
return date.toLocaleDateString('nl-NL')
}'
function getDay(d) {
const days = ['Zondag', 'Maandag', 'Dinsdag', 'Woensdag', 'Donderdag', 'Vrijdag', 'Zaterdag']
let date = new Date(d)
let day = days[date.getDay()]
return day
}Vervolgens wordt de data opgehaald door de functie getData(). Deze functie haalt de nodige data op van onze API met het endpoint /data/movement. Ook het endpoint, de baseurl en het userID zijn in het settings object aan te passen. Zo wordt het makkelijk om deze grafiek voor meerdere gebruikers te implementeren of in het eigen dashboard op te nemen.
Wanneer de data is opgehaald, wordt deze gefilterd op de meest recente week en wordt daarmee de grafiek geladen. Ook worden op dit moment de data gekoppeld aan het select element, zodat daarmee de geselecteerde week aangepast kan worden. Het select element krijgt een event-listener, zodat wanneer de geselecteerde week wordt aangepast, de grafiek wordt geupdate met de data die daarbij hoort. Zo hoeft voor het selecteren van een andere week niet elke keer een call naar de api te worden gedaan.
De grafiek wordt opgebouwd met hulp van de d3.js library. Omdat deze pagina in totaal uit twee grafieken bestaat, heb ik een overkoepelende functie, genaamd createGraphs. Deze creëert de svg in de container die gedefinieerd wordt in het settings object met de hoogtes, breedtes en marges die daar staan ingesteld.
Vervolgens zijn er twee functies om beide grafieken op te bouwen: createMovementGraph() en createFeelingGraph(). Deze functies bouwen enkel het assenstelsel op. Dat is nodig, zodat de code om de data in de grafiek te laden gescheiden kan blijven en enkel die functie aangeroepen kan worden om de grafiek met nieuwe data te vullen, zodat niet bij het wisselen van weken de complete grafiek opnieuw geladen hoeft te worden, maar enkel de waarden van de al aanwezige staven kunnen worden geupdate. Dat gebeurt in de twee functies updateMovementGraph() en updateFeelingGraph.
De code voor deze grafiek is te vinden in public/scripts/planets.js.
Op de pagina voor de kinderen worden de planeten ingeladen op het moment dat de gebruiker op de pagina komt. Hier wordt vervolgens gekeken hoeveel weken al behaald zijn en op basis hiervan worden ingekleurde of grijze planeten en wel of geen trofeeën weergegeven.
Object.keys(trophy).forEach((singleTrophy, i) => {
...
collectedTrophySource = false / behaalde trofee bv. 'gold.svg'
className = full / outline
...
wrapper.insertAdjacentHTML('beforeend', `<div>${collectedTrophySource === false ? '' : `<img class="trophy" src="${collectedTrophy + collectedTrophySource}" />`}<img class="${className}" data-weekNumber="${i + 1}" data-obtained="${collectedTrophySource}" data-trophy="${singleTrophy}" src="${source + sourceVar}" /></div>`)
})Vervolgens wordt er een click event aan elke planeet toegewezen die de grafiek van die week zal laten zien
planet.addEventListener('click', function (e) {
addOverlayData(this)
})Hierna wordt er op basis van de datasets, die hierboven eerder zijn toegewezen, de juiste data opgehaald en weergegeven in de grafiek. Deze grafiek wordt aangemaakt door middel van de d3.js library.
Op zowel de planeten pagina als op de grafiek pagina worden random sterren gegenereerd om het zo iets speelser te maken voor de kinderen. Op de planeten pagina vliegen de sterren achter de raket aan, op de grafiek pagina verschijnen deze sterren achter de beker. Voor beide sterren hebben wij een recursion functie aangemaakt, waarbij je bij het aanroepen van de functie bepaald hoeveel sterren er gegenereerd moeten worden.
Een voorbeeld van de grafiek pagina recursion:
function generateTrophyStars(noS, index) {
let newStar = document.createElement('img')
const randomTop = Math.floor(Math.random() * 7)
const randomLeft = Math.floor(Math.random() * 10)
const plusOrMinus = Math.random() >= 0.5 ? '' : '-'
newStar.classList.add('star')
newStar.src = '/images/planets/star.svg'
newStar.style.width = 5 + Math.floor(Math.random() * 20) + 'px'
newStar.style.transform = 'rotate(' + Math.floor(Math.random() * 360) + 'deg' + ')'
newStar.style.top = 21 + '%'
newStar.style.left = 85 + '%'
document.querySelector('#graph-container').appendChild(newStar)
setTimeout(() => {
if(plusOrMinus === '-') {
newStar.style.top = 21 - randomTop + '%'
newStar.style.left = 85 - randomLeft + '%'
} else {
newStar.style.top = 21 + randomTop + '%'
newStar.style.left = 85 + randomLeft + '%'
}
}, 10);
setTimeout(() => {
newStar.classList.add('fade')
setTimeout(() => {
newStar.parentNode.removeChild(newStar)
}, 200);
}, 340);
setTimeout(() => {
console.log(noS, index)
if(noS > index) {console.log(index);index++; generateTrophyStars(noS, index)}
}, 50)
}