D45Hub
3 months ago
commit
5baaef455b
42 changed files with 26801 additions and 0 deletions
-
23.gitignore
-
12README.md
-
23936package-lock.json
-
55package.json
-
BINpublic/favicon.ico
-
43public/index.html
-
BINpublic/logo192.png
-
BINpublic/logo512.png
-
25public/manifest.json
-
3public/robots.txt
-
38src/App.css
-
253src/App.js
-
8src/App.test.js
-
205src/charts/QuestionScoreBarChart.jsx
-
73src/charts/demographics/AgeBoxPlot.jsx
-
86src/charts/demographics/AgeHistogram.jsx
-
55src/charts/demographics/AudioExamplesWordCloud.jsx
-
61src/charts/demographics/AudioTurnedOnChart.jsx
-
58src/charts/demographics/GenderBarChart.jsx
-
64src/charts/end/EndQuestionnairePreferredSite.jsx
-
56src/charts/end/EndSentimentWordcloud.jsx
-
56src/charts/end/EndUsagesWordcloud.jsx
-
115src/charts/imi/IMIAnalysis.jsx
-
58src/charts/imi/IMIBarChart.jsx
-
112src/charts/imi/IMIBoxPlot.jsx
-
165src/charts/imi/IMIBoxPlotComp.jsx
-
85src/charts/imi/IMIDeviationPlot.jsx
-
69src/charts/imi/IMIScatterPlot.jsx
-
120src/charts/sus/SUSAnalysis.jsx
-
105src/charts/sus/SUSBarChart.jsx
-
105src/charts/sus/SUSBoxPlot.jsx
-
165src/charts/sus/SUSBoxPlotComp.jsx
-
85src/charts/sus/SUSDeviationPlot.jsx
-
69src/charts/sus/SUSScatterPlot.jsx
-
1src/core/Constants.jsx
-
19src/index.css
-
17src/index.js
-
1src/logo.svg
-
13src/reportWebVitals.js
-
BINsrc/server/participant_db.db
-
382src/server/server.js
-
5src/setupTests.js
@ -0,0 +1,23 @@ |
|||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. |
|||
|
|||
# dependencies |
|||
/node_modules |
|||
/.pnp |
|||
.pnp.js |
|||
|
|||
# testing |
|||
/coverage |
|||
|
|||
# production |
|||
/build |
|||
|
|||
# misc |
|||
.DS_Store |
|||
.env.local |
|||
.env.development.local |
|||
.env.test.local |
|||
.env.production.local |
|||
|
|||
npm-debug.log* |
|||
yarn-debug.log* |
|||
yarn-error.log* |
@ -0,0 +1,12 @@ |
|||
# Getting Started with Create React App |
|||
|
|||
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). |
|||
|
|||
A modified version of the dashboard, with anonymized participant responses. (No open-text answers) |
|||
|
|||
## How to run: |
|||
|
|||
(Run all commands in the root directory) |
|||
- Install all packages. (via "npm install") |
|||
- Run "npm start". This will open up the API, running on port 5000 alongside the dashboard running on port 3000. |
|||
- Open "localhost:3000" and enjoy. :) |
23936
package-lock.json
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
@ -0,0 +1,55 @@ |
|||
{ |
|||
"name": "master-thesis-data-analysis", |
|||
"version": "0.1.0", |
|||
"private": true, |
|||
"dependencies": { |
|||
"@geist-ui/core": "^2.3.8", |
|||
"@testing-library/jest-dom": "^5.17.0", |
|||
"@testing-library/react": "^13.4.0", |
|||
"@testing-library/user-event": "^13.5.0", |
|||
"cors": "^2.8.5", |
|||
"express": "^4.19.2", |
|||
"geist": "^1.3.1", |
|||
"geist-ui": "^0.0.102", |
|||
"highcharts": "^11.4.6", |
|||
"highcharts-more": "^0.1.7", |
|||
"highcharts-react-official": "^3.2.1", |
|||
"mathjs": "^13.0.2", |
|||
"react": "^18.3.1", |
|||
"react-dom": "^18.3.1", |
|||
"react-jsx-highcharts": "^5.0.1", |
|||
"react-scripts": "5.0.1", |
|||
"react-tabs": "^6.0.2", |
|||
"simple-statistics": "^7.8.3", |
|||
"sqlite3": "^5.1.7", |
|||
"web-vitals": "^2.1.4" |
|||
}, |
|||
"scripts": { |
|||
"start": "concurrently \"npm run server\" \"react-scripts start\"", |
|||
"server": "node ./src/server/server.js", |
|||
"build": "react-scripts build", |
|||
"test": "react-scripts test", |
|||
"eject": "react-scripts eject" |
|||
}, |
|||
"eslintConfig": { |
|||
"extends": [ |
|||
"react-app", |
|||
"react-app/jest" |
|||
] |
|||
}, |
|||
"browserslist": { |
|||
"production": [ |
|||
">0.2%", |
|||
"not dead", |
|||
"not op_mini all" |
|||
], |
|||
"development": [ |
|||
"last 1 chrome version", |
|||
"last 1 firefox version", |
|||
"last 1 safari version" |
|||
] |
|||
}, |
|||
"devDependencies": { |
|||
"concurrently": "^8.2.2" |
|||
} |
|||
} |
@ -0,0 +1,43 @@ |
|||
<!DOCTYPE html> |
|||
<html lang="en"> |
|||
<head> |
|||
<meta charset="utf-8" /> |
|||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" /> |
|||
<meta name="viewport" content="width=device-width, initial-scale=1" /> |
|||
<meta name="theme-color" content="#000000" /> |
|||
<meta |
|||
name="description" |
|||
content="Web site created using create-react-app" |
|||
/> |
|||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" /> |
|||
<!-- |
|||
manifest.json provides metadata used when your web app is installed on a |
|||
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/ |
|||
--> |
|||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" /> |
|||
<!-- |
|||
Notice the use of %PUBLIC_URL% in the tags above. |
|||
It will be replaced with the URL of the `public` folder during the build. |
|||
Only files inside the `public` folder can be referenced from the HTML. |
|||
|
|||
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will |
|||
work correctly both with client-side routing and a non-root public URL. |
|||
Learn how to configure a non-root public URL by running `npm run build`. |
|||
--> |
|||
<title>React App</title> |
|||
</head> |
|||
<body> |
|||
<noscript>You need to enable JavaScript to run this app.</noscript> |
|||
<div id="root"></div> |
|||
<!-- |
|||
This HTML file is a template. |
|||
If you open it directly in the browser, you will see an empty page. |
|||
|
|||
You can add webfonts, meta tags, or analytics to this file. |
|||
The build step will place the bundled scripts into the <body> tag. |
|||
|
|||
To begin the development, run `npm start` or `yarn start`. |
|||
To create a production bundle, use `npm run build` or `yarn build`. |
|||
--> |
|||
</body> |
|||
</html> |
After Width: 192 | Height: 192 | Size: 5.2 KiB |
After Width: 512 | Height: 512 | Size: 9.4 KiB |
@ -0,0 +1,25 @@ |
|||
{ |
|||
"short_name": "React App", |
|||
"name": "Create React App Sample", |
|||
"icons": [ |
|||
{ |
|||
"src": "favicon.ico", |
|||
"sizes": "64x64 32x32 24x24 16x16", |
|||
"type": "image/x-icon" |
|||
}, |
|||
{ |
|||
"src": "logo192.png", |
|||
"type": "image/png", |
|||
"sizes": "192x192" |
|||
}, |
|||
{ |
|||
"src": "logo512.png", |
|||
"type": "image/png", |
|||
"sizes": "512x512" |
|||
} |
|||
], |
|||
"start_url": ".", |
|||
"display": "standalone", |
|||
"theme_color": "#000000", |
|||
"background_color": "#ffffff" |
|||
} |
@ -0,0 +1,3 @@ |
|||
# https://www.robotstxt.org/robotstxt.html |
|||
User-agent: * |
|||
Disallow: |
@ -0,0 +1,38 @@ |
|||
.App { |
|||
text-align: center; |
|||
} |
|||
|
|||
.App-logo { |
|||
height: 40vmin; |
|||
pointer-events: none; |
|||
} |
|||
|
|||
@media (prefers-reduced-motion: no-preference) { |
|||
.App-logo { |
|||
animation: App-logo-spin infinite 20s linear; |
|||
} |
|||
} |
|||
|
|||
.App-header { |
|||
background-color: #282c34; |
|||
min-height: 100vh; |
|||
display: flex; |
|||
flex-direction: column; |
|||
align-items: center; |
|||
justify-content: center; |
|||
font-size: calc(10px + 2vmin); |
|||
color: white; |
|||
} |
|||
|
|||
.App-link { |
|||
color: #61dafb; |
|||
} |
|||
|
|||
@keyframes App-logo-spin { |
|||
from { |
|||
transform: rotate(0deg); |
|||
} |
|||
to { |
|||
transform: rotate(360deg); |
|||
} |
|||
} |
@ -0,0 +1,253 @@ |
|||
import React, { useState } from "react"; |
|||
import "./App.css"; |
|||
import AgeHistogram from "./charts/demographics/AgeHistogram"; |
|||
import GenderBarChart from "./charts/demographics/GenderBarChart"; |
|||
import AgeBoxPlot from "./charts/demographics/AgeBoxPlot"; |
|||
import AudioTurnedOnChart from "./charts/demographics/AudioTurnedOnChart"; |
|||
import AudioExamplesWordCloud from "./charts/demographics/AudioExamplesWordCloud"; |
|||
import IMIBarChart from "./charts/imi/IMIBarChart"; |
|||
|
|||
import { Tab, Tabs, TabList, TabPanel } from "react-tabs"; |
|||
import "react-tabs/style/react-tabs.css"; |
|||
import SUSBarChart from "./charts/sus/SUSBarChart"; |
|||
import IMIScatterPlot from "./charts/imi/IMIScatterPlot"; |
|||
import IMIBoxPlot from "./charts/imi/IMIBoxPlot"; |
|||
import IMIDeviationPlot from "./charts/imi/IMIDeviationPlot"; |
|||
import SUSScatterPlot from "./charts/sus/SUSScatterPlot"; |
|||
import SUSBoxPlot from "./charts/sus/SUSBoxPlot"; |
|||
import SUSDeviationPlot from "./charts/sus/SUSDeviationPlot"; |
|||
import IMIAnalysis from "./charts/imi/IMIAnalysis"; |
|||
import SUSAnalysis from "./charts/sus/SUSAnalysis"; |
|||
import IMIBoxPlotComp from "./charts/imi/IMIBoxPlotComp"; |
|||
import SUSBoxPlotComp from "./charts/sus/SUSBoxPlotComp"; |
|||
import EndUsagesWordcloud from "./charts/end/EndUsagesWordcloud"; |
|||
import EndSentimentWordcloud from "./charts/end/EndSentimentWordcloud"; |
|||
import QuestionScoreBarChart from "./charts/QuestionScoreBarChart"; |
|||
import EndQuestionnairePreferredSite from "./charts/end/EndQuestionnairePreferredSite"; |
|||
|
|||
function App() { |
|||
const [ageRange, setAgeRange] = useState({ min: "", max: "" }); |
|||
const [gender, setGender] = useState(""); |
|||
const [audio, setAudio] = useState(""); |
|||
const [submittedAgeRange, setSubmittedAgeRange] = useState({ |
|||
min: "", |
|||
max: "", |
|||
}); |
|||
const [submittedGender, setSubmittedGender] = useState(""); |
|||
const [submittedAudio, setSubmittedAudio] = useState(""); |
|||
const [error, setError] = useState(""); |
|||
|
|||
const handleInputChange = (e) => { |
|||
const { name, value } = e.target; |
|||
if (value === "" || /^\d*$/.test(value)) { |
|||
setAgeRange({ ...ageRange, [name]: value }); |
|||
} |
|||
}; |
|||
|
|||
const handleGenderChange = (e) => { |
|||
setGender(e.target.value); |
|||
}; |
|||
|
|||
const handleAudioChange = (e) => { |
|||
const value = e.target.value; |
|||
if (value === "" || /^\d*$/.test(value)) { |
|||
setAudio(value); |
|||
} |
|||
}; |
|||
|
|||
// TODOs...
|
|||
// Optional: Can be done later: Deploy.
|
|||
|
|||
const handleSubmit = () => { |
|||
if ( |
|||
ageRange.min !== "" && |
|||
ageRange.max !== "" && |
|||
parseInt(ageRange.min) > parseInt(ageRange.max) |
|||
) { |
|||
setError("Max age must be greater than or equal to min age"); |
|||
} else if (audio && (isNaN(audio) || audio < 1 || audio > 7)) { |
|||
setError("Set audio turned on value to a number between 1 and 7."); |
|||
} else { |
|||
setError(""); |
|||
setSubmittedAgeRange({ ...ageRange }); |
|||
setSubmittedGender(gender); |
|||
setSubmittedAudio(audio); |
|||
} |
|||
}; |
|||
|
|||
return ( |
|||
<div className="App"> |
|||
<h1>Questionnaire Data Visualizations</h1> |
|||
<div> |
|||
<p> |
|||
<label> |
|||
Min Age:{" "} |
|||
<input |
|||
type="number" |
|||
name="min" |
|||
value={ageRange.min} |
|||
onChange={handleInputChange} |
|||
/> |
|||
</label> |
|||
<p /> |
|||
<label> |
|||
Max Age:{" "} |
|||
<input |
|||
type="number" |
|||
name="max" |
|||
value={ageRange.max} |
|||
onChange={handleInputChange} |
|||
/> |
|||
</label> |
|||
</p> |
|||
<p> |
|||
<label> |
|||
Gender:{" "} |
|||
<select value={gender} onChange={handleGenderChange}> |
|||
<option value="">Select Gender</option> |
|||
<option value="Male">Male</option> |
|||
<option value="Female">Female</option> |
|||
</select> |
|||
</label> |
|||
</p> |
|||
<p> |
|||
<label> |
|||
Audio Turned On:{" "} |
|||
<input type="text" value={audio} onChange={handleAudioChange} /> |
|||
</label> |
|||
</p> |
|||
<button onClick={handleSubmit}>Update</button> |
|||
{error && <p style={{ color: "red" }}>{error}</p>} |
|||
</div> |
|||
<br /> |
|||
<Tabs> |
|||
<TabList> |
|||
<Tab>Demographics</Tab> |
|||
<Tab>IMI Questionnaire</Tab> |
|||
<Tab>SUS Questionnaire</Tab> |
|||
<Tab>End Questionnaire Opinions</Tab> |
|||
</TabList> |
|||
<TabPanel> |
|||
<AgeHistogram |
|||
ageRange={submittedAgeRange} |
|||
gender={submittedGender} |
|||
audio={submittedAudio} |
|||
/> |
|||
<AgeBoxPlot |
|||
ageRange={submittedAgeRange} |
|||
gender={submittedGender} |
|||
audio={submittedAudio} |
|||
/> |
|||
<GenderBarChart |
|||
ageRange={submittedAgeRange} |
|||
gender={submittedGender} |
|||
audio={submittedAudio} |
|||
/> |
|||
<AudioTurnedOnChart |
|||
ageRange={submittedAgeRange} |
|||
gender={submittedGender} |
|||
audio={submittedAudio} |
|||
/> |
|||
<AudioExamplesWordCloud |
|||
ageRange={submittedAgeRange} |
|||
gender={submittedGender} |
|||
audio={submittedAudio} |
|||
/> |
|||
</TabPanel> |
|||
<TabPanel> |
|||
<IMIAnalysis |
|||
ageRange={submittedAgeRange} |
|||
gender={submittedGender} |
|||
audio={submittedAudio} |
|||
/> |
|||
<IMIBarChart |
|||
ageRange={submittedAgeRange} |
|||
gender={submittedGender} |
|||
audio={submittedAudio} |
|||
/> |
|||
<IMIScatterPlot |
|||
ageRange={submittedAgeRange} |
|||
gender={submittedGender} |
|||
audio={submittedAudio} |
|||
/> |
|||
<IMIBoxPlot |
|||
ageRange={submittedAgeRange} |
|||
gender={submittedGender} |
|||
audio={submittedAudio} |
|||
/> |
|||
<IMIDeviationPlot |
|||
ageRange={submittedAgeRange} |
|||
gender={submittedGender} |
|||
audio={submittedAudio} |
|||
/> |
|||
<IMIBoxPlotComp |
|||
ageRange={submittedAgeRange} |
|||
gender={submittedGender} |
|||
/> |
|||
<QuestionScoreBarChart |
|||
type="IMI" |
|||
ageRange={submittedAgeRange} |
|||
gender={submittedGender} |
|||
audio={submittedAudio} |
|||
/> |
|||
</TabPanel> |
|||
<TabPanel> |
|||
<SUSAnalysis |
|||
ageRange={submittedAgeRange} |
|||
gender={submittedGender} |
|||
audio={submittedAudio} |
|||
/> |
|||
<SUSBarChart |
|||
ageRange={submittedAgeRange} |
|||
gender={submittedGender} |
|||
audio={submittedAudio} |
|||
/> |
|||
<SUSScatterPlot |
|||
ageRange={submittedAgeRange} |
|||
gender={submittedGender} |
|||
audio={submittedAudio} |
|||
/> |
|||
<SUSBoxPlot |
|||
ageRange={submittedAgeRange} |
|||
gender={submittedGender} |
|||
audio={submittedAudio} |
|||
/> |
|||
<SUSDeviationPlot |
|||
ageRange={submittedAgeRange} |
|||
gender={submittedGender} |
|||
audio={submittedAudio} |
|||
/> |
|||
<SUSBoxPlotComp |
|||
ageRange={submittedAgeRange} |
|||
gender={submittedGender} |
|||
/> |
|||
<QuestionScoreBarChart |
|||
type="SUS" |
|||
ageRange={submittedAgeRange} |
|||
gender={submittedGender} |
|||
audio={submittedAudio} |
|||
/> |
|||
</TabPanel> |
|||
<TabPanel> |
|||
<EndQuestionnairePreferredSite |
|||
ageRange={submittedAgeRange} |
|||
gender={submittedGender} |
|||
audio={submittedAudio} |
|||
/> |
|||
<EndUsagesWordcloud |
|||
ageRange={submittedAgeRange} |
|||
gender={submittedGender} |
|||
audio={submittedAudio} |
|||
/> |
|||
<EndSentimentWordcloud |
|||
ageRange={submittedAgeRange} |
|||
gender={submittedGender} |
|||
audio={submittedAudio} |
|||
/> |
|||
</TabPanel> |
|||
</Tabs> |
|||
</div> |
|||
); |
|||
} |
|||
|
|||
export default App; |
@ -0,0 +1,8 @@ |
|||
import { render, screen } from '@testing-library/react'; |
|||
import App from './App'; |
|||
|
|||
test('renders learn react link', () => { |
|||
render(<App />); |
|||
const linkElement = screen.getByText(/learn react/i); |
|||
expect(linkElement).toBeInTheDocument(); |
|||
}); |
@ -0,0 +1,205 @@ |
|||
import React, { useEffect, useState } from "react"; |
|||
import Highcharts from "highcharts"; |
|||
import HighchartsReact from "highcharts-react-official"; |
|||
|
|||
const QuestionScoreBarChart = ({ ageRange, gender, audio, type }) => { |
|||
const [categories, setCategories] = useState([]); |
|||
const [seriesData, setSeriesData] = useState([]); |
|||
const [chartsData, setChartsData] = useState([]); |
|||
const [categoriesLabels, setCategoryLabels] = useState([]); |
|||
|
|||
useEffect(() => { |
|||
if (type === "IMI") { |
|||
setCategoryLabels([ |
|||
"Strongly Disagree", |
|||
"Disagree", |
|||
"Somewhat Disagree", |
|||
"Neutral", |
|||
"Agree", |
|||
"Somewhat Agree", |
|||
"Strongly Agree", |
|||
]); |
|||
} else if (type === "SUS") { |
|||
setCategoryLabels([ |
|||
"Strongly Disagree", |
|||
"Disagree", |
|||
"Neutral", |
|||
"Agree", |
|||
"Strongly Agree", |
|||
]); |
|||
} |
|||
}, [type]); |
|||
|
|||
useEffect(() => { |
|||
const fetchDataForWebpageID = async (webpageID) => { |
|||
const queryParams = new URLSearchParams(); |
|||
if (ageRange.min) queryParams.append("age_min", ageRange.min); |
|||
if (ageRange.max) queryParams.append("age_max", ageRange.max); |
|||
if (gender) queryParams.append("gender", gender); |
|||
if (audio) queryParams.append("audio", audio); |
|||
queryParams.append("WebpageID", webpageID); |
|||
|
|||
var endpoint; |
|||
var numQuestions; |
|||
var scores; |
|||
|
|||
if (type === "IMI") { |
|||
endpoint = `http://localhost:5000/api/imi_values?${queryParams.toString()}`; |
|||
numQuestions = 7; |
|||
} else if (type === "SUS") { |
|||
endpoint = `http://localhost:5000/api/sus_values?${queryParams.toString()}`; |
|||
numQuestions = 10; |
|||
} |
|||
|
|||
const response = await fetch(endpoint); |
|||
const data = await response.json(); |
|||
scores = data.filter((dataVal) => dataVal.WebpageID === webpageID); |
|||
|
|||
const processData = (data, numQuestions) => { |
|||
const questionCounts = Array.from({ length: numQuestions }, () => |
|||
Array(type === "SUS" ? 5 : 7).fill(0) |
|||
); |
|||
|
|||
data.forEach((participant) => { |
|||
for (let i = 0; i < numQuestions; i++) { |
|||
const score = participant[`Q${i + 1}`]; |
|||
questionCounts[i][score - 1] += 1; |
|||
} |
|||
}); |
|||
|
|||
const questionPercentages = questionCounts.map((counts) => { |
|||
const total = counts.reduce((sum, count) => sum + count, 0); |
|||
return counts.map((count) => (count / total) * 100); |
|||
}); |
|||
|
|||
const seriesData = categoriesLabels.map((label, index) => ({ |
|||
name: label, |
|||
// Switch for absolute/relative responses |
|||
//data: questionCounts.map((counts) => counts[index]), |
|||
data: questionPercentages.map((percentages) => percentages[index]), |
|||
})); |
|||
|
|||
return { |
|||
categories: Array.from( |
|||
{ length: numQuestions }, |
|||
(_, i) => |
|||
`Q${i + 1} ${type === "IMI" && (i + 1 === 3 || i + 1 === 4) ? " - (R)" : ""}` |
|||
), |
|||
seriesData: seriesData, |
|||
}; |
|||
}; |
|||
|
|||
const processedData = processData(scores, numQuestions); |
|||
return { webpageID, ...processedData }; |
|||
}; |
|||
|
|||
const fetchAllData = async () => { |
|||
const results = await Promise.all( |
|||
[1, 2, 3, 4, 5, 6].map((webpageID) => fetchDataForWebpageID(webpageID)) |
|||
); |
|||
setChartsData(results); |
|||
}; |
|||
|
|||
fetchAllData(); |
|||
}, [ageRange, gender, audio, type, categoriesLabels]); |
|||
|
|||
// Absolute Responses |
|||
const options = (categories, seriesData, webpageID) => ({ |
|||
chart: { |
|||
type: "bar", |
|||
}, |
|||
title: { |
|||
text: `${type} Question Responses Distribution for Webpage ${webpageID}`, |
|||
}, |
|||
xAxis: { |
|||
categories: categories, |
|||
title: { |
|||
text: "Questions", |
|||
}, |
|||
}, |
|||
yAxis: { |
|||
min: 0, |
|||
title: { |
|||
text: "Number of Responses", |
|||
align: "high", |
|||
}, |
|||
labels: { |
|||
overflow: "justify", |
|||
}, |
|||
}, |
|||
plotOptions: { |
|||
bar: { |
|||
dataLabels: { |
|||
enabled: true, |
|||
}, |
|||
}, |
|||
}, |
|||
legend: { |
|||
reversed: true, |
|||
}, |
|||
credits: { |
|||
enabled: false, |
|||
}, |
|||
series: seriesData, |
|||
}); |
|||
|
|||
// Relative responses |
|||
const options2 = (categories, seriesData, webpageID) => ({ |
|||
chart: { |
|||
type: "bar", |
|||
}, |
|||
title: { |
|||
text: `${type} Question Responses Distribution for Webpage ${webpageID}`, |
|||
}, |
|||
xAxis: { |
|||
categories: categories, |
|||
title: { |
|||
text: "Questions", |
|||
}, |
|||
}, |
|||
yAxis: { |
|||
min: 0, |
|||
max: 100, |
|||
title: { |
|||
text: "Percentage of Responses", |
|||
align: "high", |
|||
}, |
|||
labels: { |
|||
format: "{value}%", |
|||
}, |
|||
}, |
|||
plotOptions: { |
|||
series: { |
|||
stacking: "percent", |
|||
dataLabels: { |
|||
enabled: true, |
|||
format: "{point.percentage:.1f}%", |
|||
}, |
|||
}, |
|||
}, |
|||
credits: { |
|||
enabled: false, |
|||
}, |
|||
series: seriesData, |
|||
}); |
|||
|
|||
return ( |
|||
<div |
|||
style={{ |
|||
display: "grid", |
|||
gridTemplateColumns: "1fr 1fr", |
|||
gridTemplateRows: "1fr 1fr 1fr", |
|||
}} |
|||
> |
|||
{chartsData.map(({ webpageID, categories, seriesData }) => ( |
|||
<HighchartsReact |
|||
key={webpageID} |
|||
highcharts={Highcharts} |
|||
options={options2(categories, seriesData, webpageID)} |
|||
/> |
|||
))} |
|||
</div> |
|||
); |
|||
}; |
|||
|
|||
export default QuestionScoreBarChart; |
@ -0,0 +1,73 @@ |
|||
import React, { useEffect, useState } from "react"; |
|||
import Highcharts from "highcharts"; |
|||
import HighchartsReact from "highcharts-react-official"; |
|||
import HighchartsMore from "highcharts/highcharts-more"; // Required for the boxplot series type |
|||
|
|||
HighchartsMore(Highcharts); // Initialize the module |
|||
|
|||
const AgeBoxPlot = ({ ageRange, gender, audio }) => { |
|||
const [data, setData] = useState([]); |
|||
|
|||
useEffect(() => { |
|||
const queryParams = new URLSearchParams(); |
|||
if (ageRange.min) queryParams.append("age_min", ageRange.min); |
|||
if (ageRange.max) queryParams.append("age_max", ageRange.max); |
|||
if (gender) queryParams.append("gender", gender); |
|||
if (audio) queryParams.append("audio", audio); |
|||
fetch(`http://localhost:5000/api/demographics?${queryParams.toString()}`) |
|||
.then((response) => response.json()) |
|||
.then((data) => { |
|||
const ages = data.map((item) => item.ParticipantAge); |
|||
setData(ages); |
|||
}); |
|||
}, [ageRange, gender, audio]); |
|||
|
|||
const calculateBoxPlotData = (ages) => { |
|||
const sortedAges = ages.sort((a, b) => a - b); |
|||
const min = sortedAges[0]; |
|||
const max = sortedAges[sortedAges.length - 1]; |
|||
const median = sortedAges[Math.floor(sortedAges.length / 2)]; |
|||
const q1 = sortedAges[Math.floor(sortedAges.length / 4)]; |
|||
const q3 = sortedAges[Math.floor((sortedAges.length * 3) / 4)]; |
|||
|
|||
return [min, q1, median, q3, max]; |
|||
}; |
|||
|
|||
const options = { |
|||
chart: { |
|||
type: "boxplot", |
|||
}, |
|||
title: { |
|||
text: "Participant Age Distribution", |
|||
}, |
|||
xAxis: { |
|||
categories: ["Ages"], |
|||
title: { |
|||
text: "Age", |
|||
}, |
|||
}, |
|||
yAxis: { |
|||
title: { |
|||
text: "Frequency", |
|||
}, |
|||
}, |
|||
plotOptions: { |
|||
boxplot: { |
|||
pointWidth: 50, |
|||
}, |
|||
}, |
|||
series: [ |
|||
{ |
|||
name: "Ages", |
|||
data: [calculateBoxPlotData(data)], |
|||
tooltip: { |
|||
headerFormat: "<em>Experiment No {point.key}</em><br/>", |
|||
}, |
|||
}, |
|||
], |
|||
}; |
|||
|
|||
return <HighchartsReact highcharts={Highcharts} options={options} />; |
|||
}; |
|||
|
|||
export default AgeBoxPlot; |
@ -0,0 +1,86 @@ |
|||
import React, { useEffect, useState } from "react"; |
|||
import Highcharts from "highcharts"; |
|||
import HighchartsReact from "highcharts-react-official"; |
|||
|
|||
const getCompleteAgesSeries = function (data) { |
|||
const agesMap = data.reduce((acc, item) => { |
|||
acc[item.ParticipantAge] = (acc[item.ParticipantAge] || 0) + 1; |
|||
return acc; |
|||
}, {}); |
|||
|
|||
const minAge = 20; |
|||
const maxAge = Math.max(...data.map((item) => item.ParticipantAge)); |
|||
|
|||
const filledAges = []; |
|||
|
|||
for (let age = minAge; age <= maxAge; age++) { |
|||
filledAges.push({ |
|||
name: age.toString(), |
|||
y: agesMap[age] || 0, |
|||
}); |
|||
} |
|||
|
|||
return filledAges; |
|||
}; |
|||
|
|||
const getCompressedAgesSeries = function (data) { |
|||
const agesMap = data.reduce((acc, item) => { |
|||
acc[item.ParticipantAge] = (acc[item.ParticipantAge] || 0) + 1; |
|||
return acc; |
|||
}, {}); |
|||
|
|||
const ages = Object.keys(agesMap).map((age) => ({ |
|||
name: age, |
|||
y: agesMap[age], |
|||
})); |
|||
|
|||
return ages; |
|||
}; |
|||
|
|||
const AgeHistogram = ({ ageRange, gender, audio }) => { |
|||
const [data, setData] = useState([]); |
|||
|
|||
useEffect(() => { |
|||
const queryParams = new URLSearchParams(); |
|||
if (ageRange.min) queryParams.append("age_min", ageRange.min); |
|||
if (ageRange.max) queryParams.append("age_max", ageRange.max); |
|||
if (gender) queryParams.append("gender", gender); |
|||
if (audio) queryParams.append("audio", audio); |
|||
fetch(`http://localhost:5000/api/demographics?${queryParams.toString()}`) |
|||
.then((response) => response.json()) |
|||
.then((data) => { |
|||
setData(getCompleteAgesSeries(data)); |
|||
}); |
|||
}, [ageRange, gender, audio]); |
|||
|
|||
const options = { |
|||
chart: { |
|||
type: "column", |
|||
}, |
|||
title: { |
|||
text: "Participant Age Distribution", |
|||
}, |
|||
xAxis: { |
|||
title: { |
|||
text: "Age", |
|||
}, |
|||
categories: ["20", "21", "22", "23", "24", "25", "26", "27", "28", "29", "30", "31", "32", "33", "34", "35", "36", "37"], |
|||
}, |
|||
yAxis: { |
|||
title: { |
|||
text: "Frequency", |
|||
}, |
|||
}, |
|||
series: [ |
|||
{ |
|||
name: "Age", |
|||
data: data, |
|||
binWidth: 5, |
|||
}, |
|||
], |
|||
}; |
|||
|
|||
return <HighchartsReact highcharts={Highcharts} options={options} />; |
|||
}; |
|||
|
|||
export default AgeHistogram; |
@ -0,0 +1,55 @@ |
|||
import React, { useEffect, useState } from "react"; |
|||
import Highcharts from "highcharts"; |
|||
import HighchartsReact from "highcharts-react-official"; |
|||
import wordcloud from "highcharts/modules/wordcloud"; |
|||
|
|||
wordcloud(Highcharts); |
|||
|
|||
const AudioExamplesWordCloud = ({ ageRange, gender, audio }) => { |
|||
const [data, setData] = useState([]); |
|||
|
|||
useEffect(() => { |
|||
const queryParams = new URLSearchParams(); |
|||
if (ageRange.min) queryParams.append("age_min", ageRange.min); |
|||
if (ageRange.max) queryParams.append("age_max", ageRange.max); |
|||
if (gender) queryParams.append("gender", gender); |
|||
if (audio) queryParams.append("audio", audio); |
|||
fetch( |
|||
`http://localhost:5000/api/end_questionnaire_analysed_data?${queryParams.toString()}` |
|||
) |
|||
.then((response) => response.json()) |
|||
.then((data) => { |
|||
const wordCounts = data.reduce((acc, item) => { |
|||
const words = item.AudioUsage.split(" "); |
|||
words.forEach((word) => { |
|||
acc[word] = (acc[word] || 0) + 1; |
|||
}); |
|||
return acc; |
|||
}, {}); |
|||
|
|||
const formattedWords = Object.keys(wordCounts).map((word) => ({ |
|||
name: word, |
|||
weight: wordCounts[word], |
|||
})); |
|||
|
|||
setData(formattedWords); |
|||
}); |
|||
}, [ageRange, gender, audio]); |
|||
|
|||
const options = { |
|||
series: [ |
|||
{ |
|||
type: "wordcloud", |
|||
data: data, |
|||
name: "Occurrences", |
|||
}, |
|||
], |
|||
title: { |
|||
text: "Sonification Usages Wordcloud", |
|||
}, |
|||
}; |
|||
|
|||
return <HighchartsReact highcharts={Highcharts} options={options} />; |
|||
}; |
|||
|
|||
export default AudioExamplesWordCloud; |
@ -0,0 +1,61 @@ |
|||
import React, { useEffect, useState } from "react"; |
|||
import Highcharts from "highcharts"; |
|||
import HighchartsReact from "highcharts-react-official"; |
|||
|
|||
const AudioTurnedOnChart = ({ ageRange, gender, audio }) => { |
|||
const [data, setData] = useState([]); |
|||
|
|||
useEffect(() => { |
|||
const queryParams = new URLSearchParams(); |
|||
if (ageRange.min) queryParams.append("age_min", ageRange.min); |
|||
if (ageRange.max) queryParams.append("age_max", ageRange.max); |
|||
if (gender) queryParams.append("gender", gender); |
|||
if (audio) queryParams.append("audio", audio); |
|||
fetch(`http://localhost:5000/api/demographics?${queryParams.toString()}`) |
|||
.then((response) => response.json()) |
|||
.then((data) => { |
|||
const scale = Array.from({ length: 7 }, (_, i) => i + 1); |
|||
const audioCounts = data.reduce((acc, item) => { |
|||
acc[item.AudioTurnedOn] = (acc[item.AudioTurnedOn] || 0) + 1; |
|||
return acc; |
|||
}, {}); |
|||
|
|||
const formattedData = scale.map((key) => ({ |
|||
name: key.toString(), |
|||
y: audioCounts[key] || 0, |
|||
})); |
|||
|
|||
setData(formattedData); |
|||
}); |
|||
}, [ageRange, gender, audio]); |
|||
|
|||
const options = { |
|||
chart: { |
|||
type: "bar", |
|||
}, |
|||
title: { |
|||
text: "Audio Turned On Distribution", |
|||
}, |
|||
xAxis: { |
|||
categories: data.map((item) => item.name), |
|||
title: { |
|||
text: "Audio Scale", |
|||
}, |
|||
}, |
|||
yAxis: { |
|||
title: { |
|||
text: "Count", |
|||
}, |
|||
}, |
|||
series: [ |
|||
{ |
|||
name: "Audio Scale", |
|||
data: data, |
|||
}, |
|||
], |
|||
}; |
|||
|
|||
return <HighchartsReact highcharts={Highcharts} options={options} />; |
|||
}; |
|||
|
|||
export default AudioTurnedOnChart; |
@ -0,0 +1,58 @@ |
|||
import React, { useEffect, useState } from "react"; |
|||
import Highcharts from "highcharts"; |
|||
import HighchartsReact from "highcharts-react-official"; |
|||
|
|||
const GenderBarChart = ({ ageRange, gender, audio }) => { |
|||
const [data, setData] = useState([]); |
|||
|
|||
useEffect(() => { |
|||
const queryParams = new URLSearchParams(); |
|||
if (ageRange.min) queryParams.append("age_min", ageRange.min); |
|||
if (ageRange.max) queryParams.append("age_max", ageRange.max); |
|||
if (gender) queryParams.append("gender", gender); |
|||
if (audio) queryParams.append("audio", audio); |
|||
fetch(`http://localhost:5000/api/demographics?${queryParams.toString()}`) |
|||
.then((response) => response.json()) |
|||
.then((data) => { |
|||
const genderCounts = data.reduce((acc, item) => { |
|||
acc[item.ParticipantGender] = (acc[item.ParticipantGender] || 0) + 1; |
|||
return acc; |
|||
}, {}); |
|||
const formattedData = Object.keys(genderCounts).map((gender) => ({ |
|||
name: gender, |
|||
y: genderCounts[gender], |
|||
})); |
|||
setData(formattedData); |
|||
}); |
|||
}, [ageRange, gender, audio]); |
|||
|
|||
const options = { |
|||
chart: { |
|||
type: "bar", |
|||
}, |
|||
title: { |
|||
text: "Participant Gender Distribution", |
|||
}, |
|||
xAxis: { |
|||
categories: data.map((item) => item.name), |
|||
title: { |
|||
text: "Gender", |
|||
}, |
|||
}, |
|||
yAxis: { |
|||
title: { |
|||
text: "Count", |
|||
}, |
|||
}, |
|||
series: [ |
|||
{ |
|||
name: "Gender", |
|||
data: data, |
|||
}, |
|||
], |
|||
}; |
|||
|
|||
return <HighchartsReact highcharts={Highcharts} options={options} />; |
|||
}; |
|||
|
|||
export default GenderBarChart; |
@ -0,0 +1,64 @@ |
|||
import React, { useEffect, useState } from "react"; |
|||
import Highcharts from "highcharts"; |
|||
import HighchartsReact from "highcharts-react-official"; |
|||
|
|||
const EndQuestionnairePreferredSite = ({ ageRange, gender, audio }) => { |
|||
const [data, setData] = useState([]); |
|||
|
|||
useEffect(() => { |
|||
const queryParams = new URLSearchParams(); |
|||
if (ageRange.min) queryParams.append("age_min", ageRange.min); |
|||
if (ageRange.max) queryParams.append("age_max", ageRange.max); |
|||
if (gender) queryParams.append("gender", gender); |
|||
if (audio) queryParams.append("audio", audio); |
|||
fetch( |
|||
`http://localhost:5000/api/end_questionnaire_analysed_data?${queryParams.toString()}` |
|||
) |
|||
.then((response) => response.json()) |
|||
.then((data) => { |
|||
const scale = Array.from({ length: 7 }, (_, i) => i + 1); |
|||
const versionCounts = data.reduce((acc, item) => { |
|||
acc[item.MostLikedSite] = (acc[item.MostLikedSite] || 0) + 1; |
|||
return acc; |
|||
}, {}); |
|||
|
|||
const formattedData = scale.map((key) => ({ |
|||
name: key.toString(), |
|||
y: versionCounts[key] || 0, |
|||
})); |
|||
formattedData.pop(); |
|||
setData(formattedData); |
|||
}); |
|||
}, [ageRange, gender, audio]); |
|||
|
|||
const options = { |
|||
chart: { |
|||
type: "column", |
|||
}, |
|||
title: { |
|||
text: "Distribution of Preferred Conditions", |
|||
}, |
|||
xAxis: { |
|||
title: { |
|||
text: "Condition", |
|||
}, |
|||
categories: ["1", "2", "3", "4", "5", "6"], |
|||
}, |
|||
yAxis: { |
|||
title: { |
|||
text: "Amount of Persons", |
|||
}, |
|||
}, |
|||
series: [ |
|||
{ |
|||
name: "Webpage", |
|||
data: data, |
|||
binWidth: 5, |
|||
}, |
|||
], |
|||
}; |
|||
|
|||
return <HighchartsReact highcharts={Highcharts} options={options} />; |
|||
}; |
|||
|
|||
export default EndQuestionnairePreferredSite; |
@ -0,0 +1,56 @@ |
|||
import React, { useEffect, useState } from "react"; |
|||
import Highcharts from "highcharts"; |
|||
import HighchartsReact from "highcharts-react-official"; |
|||
import wordcloud from "highcharts/modules/wordcloud"; |
|||
|
|||
wordcloud(Highcharts); |
|||
|
|||
const EndSentimentWordcloud = ({ ageRange, gender, audio }) => { |
|||
const [data, setData] = useState([]); |
|||
|
|||
useEffect(() => { |
|||
const queryParams = new URLSearchParams(); |
|||
if (ageRange.min) queryParams.append("age_min", ageRange.min); |
|||
if (ageRange.max) queryParams.append("age_max", ageRange.max); |
|||
if (gender) queryParams.append("gender", gender); |
|||
if (audio) queryParams.append("audio", audio); |
|||
fetch( |
|||
`http://localhost:5000/api/end_questionnaire_analysed_data?${queryParams.toString()}` |
|||
) |
|||
.then((response) => response.json()) |
|||
.then((data) => { |
|||
const wordCounts = data.reduce((acc, item) => { |
|||
console.log(item); |
|||
const words = item.SoundSentiment.split(" "); |
|||
words.forEach((word) => { |
|||
acc[word] = (acc[word] || 0) + 1; |
|||
}); |
|||
return acc; |
|||
}, {}); |
|||
|
|||
const formattedWords = Object.keys(wordCounts).map((word) => ({ |
|||
name: word, |
|||
weight: wordCounts[word], |
|||
})); |
|||
|
|||
setData(formattedWords); |
|||
}); |
|||
}, [ageRange, gender, audio]); |
|||
|
|||
const options = { |
|||
series: [ |
|||
{ |
|||
type: "wordcloud", |
|||
data: data, |
|||
name: "Occurrences", |
|||
}, |
|||
], |
|||
title: { |
|||
text: "Sonification Sentiment Wordcloud", |
|||
}, |
|||
}; |
|||
|
|||
return <HighchartsReact highcharts={Highcharts} options={options} />; |
|||
}; |
|||
|
|||
export default EndSentimentWordcloud; |
@ -0,0 +1,56 @@ |
|||
import React, { useEffect, useState } from "react"; |
|||
import Highcharts from "highcharts"; |
|||
import HighchartsReact from "highcharts-react-official"; |
|||
import wordcloud from "highcharts/modules/wordcloud"; |
|||
|
|||
wordcloud(Highcharts); |
|||
|
|||
const EndUsagesWordcloud = ({ ageRange, gender, audio }) => { |
|||
const [data, setData] = useState([]); |
|||
|
|||
useEffect(() => { |
|||
const queryParams = new URLSearchParams(); |
|||
if (ageRange.min) queryParams.append("age_min", ageRange.min); |
|||
if (ageRange.max) queryParams.append("age_max", ageRange.max); |
|||
if (gender) queryParams.append("gender", gender); |
|||
if (audio) queryParams.append("audio", audio); |
|||
fetch( |
|||
`http://localhost:5000/api/end_questionnaire_analysed_data?${queryParams.toString()}` |
|||
) |
|||
.then((response) => response.json()) |
|||
.then((data) => { |
|||
const wordCounts = data.reduce((acc, item) => { |
|||
console.log(item); |
|||
const words = item.RealLifeApplicationIdea.split(" "); |
|||
words.forEach((word) => { |
|||
acc[word] = (acc[word] || 0) + 1; |
|||
}); |
|||
return acc; |
|||
}, {}); |
|||
|
|||
const formattedWords = Object.keys(wordCounts).map((word) => ({ |
|||
name: word, |
|||
weight: wordCounts[word], |
|||
})); |
|||
|
|||
setData(formattedWords); |
|||
}); |
|||
}, [ageRange, gender, audio]); |
|||
|
|||
const options = { |
|||
series: [ |
|||
{ |
|||
type: "wordcloud", |
|||
data: data, |
|||
name: "Occurrences", |
|||
}, |
|||
], |
|||
title: { |
|||
text: "Sonification Usages Wordcloud", |
|||
}, |
|||
}; |
|||
|
|||
return <HighchartsReact highcharts={Highcharts} options={options} />; |
|||
}; |
|||
|
|||
export default EndUsagesWordcloud; |
@ -0,0 +1,115 @@ |
|||
import React, { useEffect, useState } from "react"; |
|||
import { std, variance } from "mathjs"; |
|||
|
|||
const IMIAnalysis = ({ ageRange, gender, audio }) => { |
|||
const [descriptiveStats, setDescriptiveStats] = useState({}); |
|||
const [cronbachAlpha, setCronbachAlpha] = useState(null); |
|||
const [alphasIfDeleted, setAlphasIfDeleted] = useState([]); |
|||
|
|||
useEffect(() => { |
|||
const queryParams = new URLSearchParams(); |
|||
if (ageRange.min) queryParams.append("age_min", ageRange.min); |
|||
if (ageRange.max) queryParams.append("age_max", ageRange.max); |
|||
if (gender) queryParams.append("gender", gender); |
|||
if (audio) queryParams.append("audio", audio); |
|||
fetch(`http://localhost:5000/api/imi_scores?${queryParams.toString()}`) |
|||
.then((response) => response.json()) |
|||
.then((data) => { |
|||
calculateDescriptiveStats(data); |
|||
}); |
|||
|
|||
fetch(`http://localhost:5000/api/imi_values?${queryParams.toString()}`) |
|||
.then((response) => response.json()) |
|||
.then((data) => { |
|||
const scores = data.map((row) => [ |
|||
row.Q1, |
|||
row.Q2, |
|||
8 - row.Q3, |
|||
8 - row.Q4, |
|||
row.Q5, |
|||
row.Q6, |
|||
row.Q7, |
|||
]); |
|||
setCronbachAlpha(calcAlpha(scores)); |
|||
const alphas = calculateAlphaIfDeleted(scores); |
|||
setAlphasIfDeleted(alphas); |
|||
}); |
|||
}, [ageRange, gender, audio]); |
|||
|
|||
const calculateDescriptiveStats = (data) => { |
|||
if (data.length === 0) return; |
|||
const scores = data.map((d) => d.TotalIMIScore); |
|||
const mean = scores.reduce((a, b) => a + b, 0) / scores.length; |
|||
const stdDev = std(scores); |
|||
const stats = { |
|||
mean, |
|||
stdDev, |
|||
min: Math.min(...scores), |
|||
max: Math.max(...scores), |
|||
}; |
|||
setDescriptiveStats(stats); |
|||
}; |
|||
|
|||
const calcAlpha = (scores) => { |
|||
const variances = scores[0].map((_, colIndex) => { |
|||
const col = scores.map((row) => row[colIndex]); |
|||
return variance(col); |
|||
}); |
|||
|
|||
const totalScores = scores.map((row) => row.reduce((a, b) => a + b, 0)); |
|||
const totalVariance = variance(totalScores); |
|||
|
|||
const numItems = scores[0].length; |
|||
return ( |
|||
(numItems / (numItems - 1)) * |
|||
(1 - variances.reduce((a, b) => a + b, 0) / totalVariance) |
|||
); |
|||
}; |
|||
|
|||
const calculateAlphaIfDeleted = (scores) => { |
|||
const itemVars = scores[0].map((_, colIndex) => { |
|||
const col = scores.map((row) => row[colIndex]); |
|||
return variance(col); |
|||
}); |
|||
|
|||
const totalScores = scores.map((row) => row.reduce((a, b) => a + b, 0)); |
|||
const totalVariance = variance(totalScores); |
|||
|
|||
const numItems = scores[0].length; |
|||
|
|||
const alphas = scores[0].map((_, colIndex) => { |
|||
const remainingVars = itemVars.filter((_, i) => i !== colIndex); |
|||
const alpha = |
|||
((numItems - 1) / (numItems - 2)) * |
|||
(1 - remainingVars.reduce((a, b) => a + b, 0) / totalVariance); |
|||
return alpha; |
|||
}); |
|||
|
|||
return alphas; |
|||
}; |
|||
|
|||
return ( |
|||
<div> |
|||
<h1>IMI Analysis</h1> |
|||
<h2>Descriptive Statistics</h2> |
|||
<p>Mean: {descriptiveStats.mean}</p> |
|||
<p>Standard Deviation: {descriptiveStats.stdDev}</p> |
|||
<p>Min: {descriptiveStats.min}</p> |
|||
<p>Max: {descriptiveStats.max}</p> |
|||
|
|||
<h2>Cronbach's Alpha</h2> |
|||
<p>{cronbachAlpha}</p> |
|||
|
|||
<h2>Cronbach's Alpha if Items Deleted</h2> |
|||
<ul> |
|||
{alphasIfDeleted.map((alpha, index) => ( |
|||
<li key={index}> |
|||
Alpha if Q{index + 1} deleted: {alpha} |
|||
</li> |
|||
))} |
|||
</ul> |
|||
</div> |
|||
); |
|||
}; |
|||
|
|||
export default IMIAnalysis; |
@ -0,0 +1,58 @@ |
|||
import React, { useEffect, useState } from "react"; |
|||
import Highcharts from "highcharts"; |
|||
import HighchartsReact from "highcharts-react-official"; |
|||
|
|||
const IMIBarChart = ({ ageRange, gender, audio }) => { |
|||
const [data, setData] = useState([]); |
|||
|
|||
useEffect(() => { |
|||
const queryParams = new URLSearchParams(); |
|||
if (ageRange.min) queryParams.append("age_min", ageRange.min); |
|||
if (ageRange.max) queryParams.append("age_max", ageRange.max); |
|||
if (gender) queryParams.append("gender", gender); |
|||
if (audio) queryParams.append("audio", audio); |
|||
fetch(`http://localhost:5000/api/imi_average?${queryParams.toString()}`) |
|||
.then((response) => response.json()) |
|||
.then((data) => { |
|||
const imiData = []; |
|||
for (const item of data) { |
|||
imiData.push({ |
|||
name: item.WebpageID, |
|||
y: item.Avg_TotalIMIScore, |
|||
}); |
|||
} |
|||
setData(imiData); |
|||
}); |
|||
}, [ageRange, gender, audio]); |
|||
|
|||
const options = { |
|||
chart: { |
|||
type: "column", |
|||
}, |
|||
title: { |
|||
text: "Participant IMI Score Distribution", |
|||
}, |
|||
xAxis: { |
|||
title: { |
|||
text: "Webpage", |
|||
}, |
|||
categories: ["1", "2", "3", "4", "5", "6"], |
|||
}, |
|||
yAxis: { |
|||
title: { |
|||
text: "IMI Score", |
|||
}, |
|||
}, |
|||
series: [ |
|||
{ |
|||
name: "Webpage", |
|||
data: data, |
|||
binWidth: 5, |
|||
}, |
|||
], |
|||
}; |
|||
|
|||
return <HighchartsReact highcharts={Highcharts} options={options} />; |
|||
}; |
|||
|
|||
export default IMIBarChart; |
@ -0,0 +1,112 @@ |
|||
import React, { useEffect, useState } from "react"; |
|||
import Highcharts from "highcharts"; |
|||
import HighchartsReact from "highcharts-react-official"; |
|||
import HighchartsMore from "highcharts/highcharts-more"; // Required for the boxplot series type |
|||
|
|||
HighchartsMore(Highcharts); // Initialize the module |
|||
|
|||
const IMIBoxPlot = ({ ageRange, gender, audio }) => { |
|||
const [data, setData] = useState([]); |
|||
|
|||
useEffect(() => { |
|||
const fetchData = async () => { |
|||
const queryParams = new URLSearchParams(); |
|||
if (ageRange.min) queryParams.append("age_min", ageRange.min); |
|||
if (ageRange.max) queryParams.append("age_max", ageRange.max); |
|||
if (gender) queryParams.append("gender", gender); |
|||
if (audio) queryParams.append("audio", audio); |
|||
const response = await fetch( |
|||
`http://localhost:5000/api/imi_scores?${queryParams.toString()}` |
|||
); |
|||
const result = await response.json(); |
|||
|
|||
const groupedData = result.reduce((acc, item) => { |
|||
acc[item.WebpageID] = acc[item.WebpageID] || []; |
|||
acc[item.WebpageID].push(item.TotalIMIScore); |
|||
return acc; |
|||
}, {}); |
|||
|
|||
const formattedData = Object.keys(groupedData).map((key) => { |
|||
const scores = groupedData[key].sort((a, b) => a - b); |
|||
const q1 = scores[Math.floor(scores.length / 4)]; |
|||
const median = scores[Math.floor(scores.length / 2)]; |
|||
const q3 = scores[Math.floor((3 * scores.length) / 4)]; |
|||
const min = scores[0]; |
|||
const max = scores[scores.length - 1]; |
|||
const mean = |
|||
scores.reduce((sum, score) => sum + score, 0) / scores.length; |
|||
const variance = |
|||
scores.reduce((sum, score) => sum + Math.pow(score - mean, 2), 0) / |
|||
scores.length; |
|||
const stdDev = Math.sqrt(variance); |
|||
|
|||
return { |
|||
x: parseInt(key - 1), |
|||
low: min, |
|||
q1: q1, |
|||
median: median, |
|||
q3: q3, |
|||
high: max, |
|||
mean: mean.toFixed(3), |
|||
stdDev: stdDev.toFixed(3), |
|||
variance: variance.toFixed(3), |
|||
}; |
|||
}); |
|||
|
|||
setData(formattedData); |
|||
}; |
|||
|
|||
fetchData(); |
|||
}, [ageRange, gender, audio]); |
|||
|
|||
const options = { |
|||
chart: { |
|||
type: "boxplot", |
|||
}, |
|||
title: { |
|||
text: "IMI Score Distribution by Webpage Version", |
|||
}, |
|||
xAxis: { |
|||
title: { |
|||
text: "Webpage Version", |
|||
}, |
|||
categories: [ |
|||
"1 - BudgetBird", |
|||
"2 - Hotel", |
|||
"3 - UVV", |
|||
"4 - Iceland", |
|||
"5 - Rental", |
|||
"6 - QuickDeliver", |
|||
], |
|||
tickInterval: 1, |
|||
}, |
|||
yAxis: { |
|||
title: { |
|||
text: "IMI Score", |
|||
}, |
|||
}, |
|||
series: [ |
|||
{ |
|||
name: "IMI Scores", |
|||
data: data, |
|||
tooltip: { |
|||
pointFormat: ` |
|||
<b>Min:</b> {point.low}<br/> |
|||
<b>Q1:</b> {point.q1}<br/> |
|||
<b>Median:</b> {point.median}<br/> |
|||
<b>Q3:</b> {point.q3}<br/> |
|||
<b>Max:</b> {point.high}<br/> |
|||
<b>Mean:</b> {point.mean}<br/> |
|||
<b>Std Dev:</b> {point.stdDev}<br/> |
|||
<b>Variance:</b> {point.variance}<br/> |
|||
`, |
|||
headerFormat: "<em>Webpage Version {point.key}</em><br/>", |
|||
}, |
|||
}, |
|||
], |
|||
}; |
|||
|
|||
return <HighchartsReact highcharts={Highcharts} options={options} />; |
|||
}; |
|||
|
|||
export default IMIBoxPlot; |
@ -0,0 +1,165 @@ |
|||
import React, { useEffect, useState } from "react"; |
|||
import Highcharts from "highcharts"; |
|||
import HighchartsReact from "highcharts-react-official"; |
|||
import { Slider } from "@geist-ui/core"; |
|||
|
|||
const IMIBoxPlotComp = ({ ageRange, gender }) => { |
|||
const [audio, setAudio] = useState(4); // Default slider value |
|||
const [dataLower, setDataLower] = useState([]); |
|||
const [dataHigher, setDataHigher] = useState([]); |
|||
|
|||
useEffect(() => { |
|||
const fetchIMIData = async () => { |
|||
const queryParamsLower = new URLSearchParams(); |
|||
const queryParamsHigher = new URLSearchParams(); |
|||
|
|||
if (ageRange.min) { |
|||
queryParamsLower.append("age_min", ageRange.min); |
|||
queryParamsHigher.append("age_min", ageRange.min); |
|||
} |
|||
if (ageRange.max) { |
|||
queryParamsLower.append("age_max", ageRange.max); |
|||
queryParamsHigher.append("age_max", ageRange.max); |
|||
} |
|||
if (gender) { |
|||
queryParamsLower.append("gender", gender); |
|||
queryParamsHigher.append("gender", gender); |
|||
} |
|||
queryParamsLower.append("audioLowerThan", audio + 1); |
|||
queryParamsHigher.append("audioHigherThan", audio); |
|||
|
|||
const [responseLower, responseHigher] = await Promise.all([ |
|||
fetch( |
|||
`http://localhost:5000/api/imi_scores?${queryParamsLower.toString()}` |
|||
), |
|||
fetch( |
|||
`http://localhost:5000/api/imi_scores?${queryParamsHigher.toString()}` |
|||
), |
|||
]); |
|||
|
|||
const dataLower = await responseLower.json(); |
|||
const dataHigher = await responseHigher.json(); |
|||
|
|||
const groupedDataLower = dataLower.reduce((acc, item) => { |
|||
acc[item.WebpageID] = acc[item.WebpageID] || []; |
|||
acc[item.WebpageID].push(item.TotalIMIScore); |
|||
return acc; |
|||
}, {}); |
|||
|
|||
const formattedDataLower = Object.keys(groupedDataLower).map((key) => { |
|||
const scores = groupedDataLower[key].sort((a, b) => a - b); |
|||
const q1 = scores[Math.floor(scores.length / 4)]; |
|||
const median = scores[Math.floor(scores.length / 2)]; |
|||
const q3 = scores[Math.floor((3 * scores.length) / 4)]; |
|||
const min = scores[0]; |
|||
const max = scores[scores.length - 1]; |
|||
return [parseInt(key - 1), min, q1, median, q3, max]; |
|||
}); |
|||
|
|||
const groupedDataHigher = dataHigher.reduce((acc, item) => { |
|||
acc[item.WebpageID] = acc[item.WebpageID] || []; |
|||
acc[item.WebpageID].push(item.TotalIMIScore); |
|||
return acc; |
|||
}, {}); |
|||
|
|||
const formattedDataHigher = Object.keys(groupedDataHigher).map((key) => { |
|||
const scores = groupedDataHigher[key].sort((a, b) => a - b); |
|||
const q1 = scores[Math.floor(scores.length / 4)]; |
|||
const median = scores[Math.floor(scores.length / 2)]; |
|||
const q3 = scores[Math.floor((3 * scores.length) / 4)]; |
|||
const min = scores[0]; |
|||
const max = scores[scores.length - 1]; |
|||
return [parseInt(key - 1), min, q1, median, q3, max]; |
|||
}); |
|||
|
|||
setDataLower(formattedDataLower); |
|||
setDataHigher(formattedDataHigher); |
|||
}; |
|||
|
|||
fetchIMIData(); |
|||
}, [ageRange, gender, audio]); |
|||
|
|||
const options1 = { |
|||
chart: { |
|||
type: "boxplot", |
|||
}, |
|||
title: { |
|||
text: "Participant Average IMI Score Distribution (Audio <= " + audio + " )", |
|||
}, |
|||
xAxis: { |
|||
title: { |
|||
text: "Webpage", |
|||
}, |
|||
categories: ["1", "2", "3", "4", "5", "6"], |
|||
}, |
|||
yAxis: { |
|||
title: { |
|||
text: "IMI Score", |
|||
}, |
|||
}, |
|||
series: [ |
|||
{ |
|||
name: "Webpage", |
|||
data: dataLower, |
|||
binWidth: 5, |
|||
}, |
|||
], |
|||
}; |
|||
|
|||
const options2 = { |
|||
chart: { |
|||
type: "boxplot", |
|||
}, |
|||
title: { |
|||
text: "Participant Average IMI Score Distribution (Audio > " + audio + " )", |
|||
}, |
|||
xAxis: { |
|||
title: { |
|||
text: "Webpage", |
|||
}, |
|||
categories: ["1", "2", "3", "4", "5", "6"], |
|||
}, |
|||
yAxis: { |
|||
title: { |
|||
text: "IMI Score", |
|||
}, |
|||
}, |
|||
series: [ |
|||
{ |
|||
name: "Webpage", |
|||
data: dataHigher, |
|||
binWidth: 5, |
|||
}, |
|||
], |
|||
}; |
|||
|
|||
return ( |
|||
<div style={{ marginTop: "32px", marginBottom: "32px" }}> |
|||
<div |
|||
style={{ |
|||
display: "grid", |
|||
gridTemplateColumns: "1fr 1fr", |
|||
gridTemplateRows: "1fr", |
|||
gap: "0px 0px", |
|||
gridTemplateAreas: ". .", |
|||
}} |
|||
> |
|||
<HighchartsReact highcharts={Highcharts} options={options1} /> |
|||
<HighchartsReact highcharts={Highcharts} options={options2} /> |
|||
</div> |
|||
<Slider |
|||
value={audio} |
|||
onChange={(val) => setAudio(val)} |
|||
min={2} |
|||
max={6} |
|||
step={1} |
|||
initialValue={4} |
|||
showMarkers |
|||
width="75%" |
|||
style={{margin: "16px auto 0 auto"}} |
|||
/> |
|||
</div> |
|||
); |
|||
}; |
|||
|
|||
export default IMIBoxPlotComp; |
@ -0,0 +1,85 @@ |
|||
import React, { useState, useEffect } from "react"; |
|||
import Highcharts from "highcharts"; |
|||
import HighchartsReact from "highcharts-react-official"; |
|||
|
|||
import bellCurve from "highcharts/modules/histogram-bellcurve"; |
|||
bellCurve(Highcharts); |
|||
|
|||
function IMIDeviationPlot({ ageRange, gender, audio }) { |
|||
const [data, setData] = useState([]); |
|||
|
|||
useEffect(() => { |
|||
const fetchData = async () => { |
|||
const queryParams = new URLSearchParams(); |
|||
if (ageRange.min) queryParams.append("age_min", ageRange.min); |
|||
if (ageRange.max) queryParams.append("age_max", ageRange.max); |
|||
if (gender) queryParams.append("gender", gender); |
|||
if (audio) queryParams.append("audio", audio); |
|||
const response = await fetch( |
|||
`http://localhost:5000/api/imi_scores?${queryParams.toString()}` |
|||
); |
|||
const result = await response.json(); |
|||
setData(result); |
|||
}; |
|||
|
|||
fetchData(); |
|||
}, [ageRange, gender, audio]); |
|||
|
|||
const processIMIData = (data) => { |
|||
const scoresByWebpage = {}; |
|||
data.forEach(({ WebpageID, TotalIMIScore }) => { |
|||
if (!scoresByWebpage[WebpageID]) { |
|||
scoresByWebpage[WebpageID] = []; |
|||
} |
|||
scoresByWebpage[WebpageID].push(TotalIMIScore); |
|||
}); |
|||
|
|||
const seriesData = Object.entries(scoresByWebpage).map( |
|||
([webpage, scores]) => { |
|||
const frequencies = {}; |
|||
scores.forEach((score) => { |
|||
//const roundedScore = (Math.round(score * 10) / 10).toFixed(1); |
|||
const roundedScore = (Math.round(score * 2) / 2).toFixed(1); |
|||
frequencies[roundedScore] = (frequencies[roundedScore] || 0) + 1; |
|||
}); |
|||
|
|||
const totalScores = scores.length; |
|||
const percentages = Object.entries(frequencies) |
|||
.map(([score, count]) => ({ |
|||
x: parseFloat(score), |
|||
y: (count / totalScores) * 100, |
|||
})) |
|||
.sort((a, b) => a.x - b.x); |
|||
|
|||
return { |
|||
name: `Webpage ${webpage}`, |
|||
type: "column", |
|||
data: percentages, |
|||
}; |
|||
} |
|||
); |
|||
|
|||
return seriesData; |
|||
}; |
|||
|
|||
const options = { |
|||
title: { |
|||
text: "IMI Score Distribution by Webpage Version", |
|||
}, |
|||
xAxis: { |
|||
title: { text: "IMI Score" }, |
|||
tickInterval: 0.5, //0.3 |
|||
}, |
|||
yAxis: { |
|||
title: { text: "Percentage" }, |
|||
labels: { |
|||
format: "{value}%", |
|||
}, |
|||
}, |
|||
series: processIMIData(data), |
|||
}; |
|||
|
|||
return <HighchartsReact highcharts={Highcharts} options={options} />; |
|||
} |
|||
|
|||
export default IMIDeviationPlot; |
@ -0,0 +1,69 @@ |
|||
import React, { useEffect, useState } from "react"; |
|||
import Highcharts from "highcharts"; |
|||
import HighchartsReact from "highcharts-react-official"; |
|||
|
|||
const IMIScatterPlot = ({ ageRange, gender, audio }) => { |
|||
const [data, setData] = useState([]); |
|||
|
|||
useEffect(() => { |
|||
const fetchData = async () => { |
|||
const queryParams = new URLSearchParams(); |
|||
if (ageRange.min) queryParams.append("age_min", ageRange.min); |
|||
if (ageRange.max) queryParams.append("age_max", ageRange.max); |
|||
if (gender) queryParams.append("gender", gender); |
|||
if (audio) queryParams.append("audio", audio); |
|||
const response = await fetch( |
|||
`http://localhost:5000/api/imi_scores?${queryParams.toString()}` |
|||
); |
|||
const result = await response.json(); |
|||
|
|||
const formattedData = result.map((item) => ({ |
|||
x: item.WebpageID - 1, |
|||
y: item.TotalIMIScore, |
|||
name: `Participant ${item.ParticipantID}`, |
|||
})); |
|||
|
|||
setData(formattedData); |
|||
}; |
|||
|
|||
fetchData(); |
|||
}, [ageRange, gender, audio]); |
|||
|
|||
const options = { |
|||
chart: { |
|||
type: "scatter", |
|||
jitter: { |
|||
x: 0.24, |
|||
}, |
|||
zoomType: "xy", |
|||
}, |
|||
title: { |
|||
text: "IMI Scores by Webpage Version", |
|||
}, |
|||
xAxis: { |
|||
title: { |
|||
text: "Webpage Version", |
|||
}, |
|||
categories: ["1", "2", "3", "4", "5", "6"], |
|||
tickInterval: 1, |
|||
}, |
|||
yAxis: { |
|||
title: { |
|||
text: "IMI Score", |
|||
}, |
|||
}, |
|||
series: [ |
|||
{ |
|||
name: "IMI Scores", |
|||
data: data, |
|||
tooltip: { |
|||
pointFormat: "{point.name}<br/>IMI Score: {point.y}", |
|||
}, |
|||
}, |
|||
], |
|||
}; |
|||
|
|||
return <HighchartsReact highcharts={Highcharts} options={options} />; |
|||
}; |
|||
|
|||
export default IMIScatterPlot; |
@ -0,0 +1,120 @@ |
|||
import React, { useEffect, useState } from "react"; |
|||
import { std, variance } from "mathjs"; |
|||
|
|||
const SUSAnalysis = ({ ageRange, gender, audio }) => { |
|||
const [descriptiveStats, setDescriptiveStats] = useState({}); |
|||
const [cronbachAlpha, setCronbachAlpha] = useState(null); |
|||
const [alphasIfDeleted, setAlphasIfDeleted] = useState([]); |
|||
|
|||
useEffect(() => { |
|||
const queryParams = new URLSearchParams(); |
|||
if (ageRange.min) queryParams.append("age_min", ageRange.min); |
|||
if (ageRange.max) queryParams.append("age_max", ageRange.max); |
|||
if (gender) queryParams.append("gender", gender); |
|||
if (audio) queryParams.append("audio", audio); |
|||
fetch(`http://localhost:5000/api/sus_scores?${queryParams.toString()}`) |
|||
.then((response) => response.json()) |
|||
.then((data) => { |
|||
calculateDescriptiveStats(data); |
|||
}); |
|||
|
|||
// Honestly... Not quite sure how to rate these or the IMI one... At least with the formula it should be rated in an inverted manner... |
|||
// Same with the reverse scores of the IMI... |
|||
fetch(`http://localhost:5000/api/sus_values?${queryParams.toString()}`) |
|||
.then((response) => response.json()) |
|||
.then((data) => { |
|||
const scores = data.map((row) => [ |
|||
row.Q1, |
|||
-1 * row.Q2, |
|||
row.Q3, |
|||
-1 * row.Q4, |
|||
row.Q5, |
|||
-1 * row.Q6, |
|||
row.Q7, |
|||
-1 * row.Q8, |
|||
row.Q9, |
|||
-1 * row.Q10, |
|||
]); |
|||
setCronbachAlpha(calcAlpha(scores)); |
|||
const alphas = calculateAlphaIfDeleted(scores); |
|||
setAlphasIfDeleted(alphas); |
|||
}); |
|||
}, [ageRange, gender, audio]); |
|||
|
|||
const calculateDescriptiveStats = (data) => { |
|||
if (data.length === 0) return; |
|||
const scores = data.map((d) => d.TotalSUSScore); |
|||
const mean = scores.reduce((a, b) => a + b, 0) / scores.length; |
|||
const stdDev = std(scores); |
|||
const stats = { |
|||
mean, |
|||
stdDev, |
|||
min: Math.min(...scores), |
|||
max: Math.max(...scores), |
|||
}; |
|||
setDescriptiveStats(stats); |
|||
}; |
|||
|
|||
const calcAlpha = (scores) => { |
|||
const variances = scores[0].map((_, colIndex) => { |
|||
const col = scores.map((row) => row[colIndex]); |
|||
return variance(col); |
|||
}); |
|||
|
|||
const totalScores = scores.map((row) => row.reduce((a, b) => a + b, 0)); |
|||
const totalVariance = variance(totalScores); |
|||
|
|||
const numItems = scores[0].length; |
|||
return ( |
|||
(numItems / (numItems - 1)) * |
|||
(1 - variances.reduce((a, b) => a + b, 0) / totalVariance) |
|||
); |
|||
}; |
|||
|
|||
const calculateAlphaIfDeleted = (scores) => { |
|||
const itemVars = scores[0].map((_, colIndex) => { |
|||
const col = scores.map((row) => row[colIndex]); |
|||
return variance(col); |
|||
}); |
|||
|
|||
const totalScores = scores.map((row) => row.reduce((a, b) => a + b, 0)); |
|||
const totalVariance = variance(totalScores); |
|||
|
|||
const numItems = scores[0].length; |
|||
|
|||
const alphas = scores[0].map((_, colIndex) => { |
|||
const remainingVars = itemVars.filter((_, i) => i !== colIndex); |
|||
const alpha = |
|||
((numItems - 1) / (numItems - 2)) * |
|||
(1 - remainingVars.reduce((a, b) => a + b, 0) / totalVariance); |
|||
return alpha; |
|||
}); |
|||
|
|||
return alphas; |
|||
}; |
|||
|
|||
return ( |
|||
<div> |
|||
<h1>SUS Analysis</h1> |
|||
<h2>Descriptive Statistics</h2> |
|||
<p>Mean: {descriptiveStats.mean}</p> |
|||
<p>Standard Deviation: {descriptiveStats.stdDev}</p> |
|||
<p>Min: {descriptiveStats.min}</p> |
|||
<p>Max: {descriptiveStats.max}</p> |
|||
|
|||
<h2>Cronbach's Alpha</h2> |
|||
<p>{cronbachAlpha}</p> |
|||
|
|||
<h2>Cronbach's Alpha if Items Deleted</h2> |
|||
<ul> |
|||
{alphasIfDeleted.map((alpha, index) => ( |
|||
<li key={index}> |
|||
Alpha if Q{index + 1} deleted: {alpha} |
|||
</li> |
|||
))} |
|||
</ul> |
|||
</div> |
|||
); |
|||
}; |
|||
|
|||
export default SUSAnalysis; |
@ -0,0 +1,105 @@ |
|||
import React, { useEffect, useState } from "react"; |
|||
import Highcharts from "highcharts"; |
|||
import HighchartsReact from "highcharts-react-official"; |
|||
|
|||
const SUSBarChart = ({ ageRange, audio, gender }) => { |
|||
const [data, setData] = useState([]); |
|||
|
|||
useEffect(() => { |
|||
const queryParams = new URLSearchParams(); |
|||
if (ageRange.min) queryParams.append("age_min", ageRange.min); |
|||
if (ageRange.max) queryParams.append("age_max", ageRange.max); |
|||
if (gender) queryParams.append("gender", gender); |
|||
if (audio) queryParams.append("audio", audio); |
|||
fetch(`http://localhost:5000/api/sus_average?${queryParams.toString()}`) |
|||
.then((response) => response.json()) |
|||
.then((data) => { |
|||
const susData = []; |
|||
for (const item of data) { |
|||
const { grade, color } = getLetterGrade(item.Avg_TotalSUSScore); |
|||
susData.push({ |
|||
name: item.WebpageID, |
|||
y: item.Avg_TotalSUSScore, |
|||
color: color, |
|||
grade: grade, |
|||
}); |
|||
} |
|||
setData(susData); |
|||
}); |
|||
}, [ageRange, gender, audio]); |
|||
|
|||
const getLetterGrade = (score) => { |
|||
if (score >= 84.1) return { grade: "A+", color: "#00FF00" }; |
|||
if (score >= 80.8) return { grade: "A", color: "#32CD32" }; |
|||
if (score >= 78.9) return { grade: "A-", color: "#7FFF00" }; |
|||
if (score >= 77.2) return { grade: "B+", color: "#ADFF2F" }; |
|||
if (score >= 74.1) return { grade: "B", color: "#FFFF00" }; |
|||
if (score >= 72.6) return { grade: "B-", color: "#FFD700" }; |
|||
if (score >= 71.1) return { grade: "C+", color: "#FFA500" }; |
|||
if (score >= 65.0) return { grade: "C", color: "#FF8C00" }; |
|||
if (score >= 62.7) return { grade: "C-", color: "#FF4500" }; |
|||
if (score >= 51.7) return { grade: "D", color: "#FF0000" }; |
|||
return { grade: "F", color: "#8B0000" }; |
|||
}; |
|||
|
|||
const options = { |
|||
chart: { |
|||
type: "column", |
|||
}, |
|||
title: { |
|||
text: "Participant Average SUS Score Distribution", |
|||
}, |
|||
xAxis: { |
|||
title: { |
|||
text: "Webpage", |
|||
}, |
|||
categories: ["1", "2", "3", "4", "5", "6"], |
|||
}, |
|||
yAxis: { |
|||
title: { |
|||
text: "SUS Score", |
|||
}, |
|||
}, |
|||
plotOptions: { |
|||
column: { |
|||
dataLabels: { |
|||
enabled: true, |
|||
formatter: function () { |
|||
return this.point.grade; |
|||
}, |
|||
style: { |
|||
color: "black", |
|||
fontSize: "12px", |
|||
}, |
|||
}, |
|||
}, |
|||
}, |
|||
series: [ |
|||
{ |
|||
name: "Webpage", |
|||
data: data, |
|||
binWidth: 5, |
|||
dataLabels: { |
|||
enabled: true, |
|||
inside: true, |
|||
formatter: function () { |
|||
return this.point.grade; |
|||
}, |
|||
style: { |
|||
color: "black", |
|||
fontSize: "12px", |
|||
}, |
|||
}, |
|||
}, |
|||
], |
|||
}; |
|||
|
|||
return ( |
|||
<div> |
|||
<HighchartsReact highcharts={Highcharts} options={options} /> |
|||
<p>Based on <a href="https://quod.lib.umich.edu/w/weave/12535642.0001.602?view=text;rgn=main">this ranking</a>.</p> |
|||
</div> |
|||
); |
|||
}; |
|||
|
|||
export default SUSBarChart; |
@ -0,0 +1,105 @@ |
|||
import React, { useEffect, useState } from "react"; |
|||
import Highcharts from "highcharts"; |
|||
import HighchartsReact from "highcharts-react-official"; |
|||
import HighchartsMore from "highcharts/highcharts-more"; |
|||
|
|||
HighchartsMore(Highcharts); |
|||
|
|||
const SUSBoxPlot = ({ ageRange, gender, audio }) => { |
|||
const [data, setData] = useState([]); |
|||
|
|||
useEffect(() => { |
|||
const fetchData = async () => { |
|||
const queryParams = new URLSearchParams(); |
|||
if (ageRange.min) queryParams.append("age_min", ageRange.min); |
|||
if (ageRange.max) queryParams.append("age_max", ageRange.max); |
|||
if (gender) queryParams.append("gender", gender); |
|||
if (audio) queryParams.append("audio", audio); |
|||
const response = await fetch( |
|||
`http://localhost:5000/api/sus_scores?${queryParams.toString()}` |
|||
); |
|||
const result = await response.json(); |
|||
|
|||
const groupedData = result.reduce((acc, item) => { |
|||
acc[item.WebpageID] = acc[item.WebpageID] || []; |
|||
acc[item.WebpageID].push(item.TotalSUSScore); |
|||
return acc; |
|||
}, {}); |
|||
|
|||
const formattedData = Object.keys(groupedData).map((key) => { |
|||
const scores = groupedData[key].sort((a, b) => a - b); |
|||
const q1 = scores[Math.floor(scores.length / 4)]; |
|||
const median = scores[Math.floor(scores.length / 2)]; |
|||
const q3 = scores[Math.floor((3 * scores.length) / 4)]; |
|||
const min = scores[0]; |
|||
const max = scores[scores.length - 1]; |
|||
const mean = |
|||
scores.reduce((sum, score) => sum + score, 0) / scores.length; |
|||
const variance = |
|||
scores.reduce((sum, score) => sum + Math.pow(score - mean, 2), 0) / |
|||
scores.length; |
|||
const stdDev = Math.sqrt(variance); |
|||
|
|||
return { |
|||
x: parseInt(key - 1), |
|||
low: min, |
|||
q1: q1, |
|||
median: median, |
|||
q3: q3, |
|||
high: max, |
|||
mean: mean.toFixed(3), |
|||
stdDev: stdDev.toFixed(3), |
|||
variance: variance.toFixed(3), |
|||
}; |
|||
}); |
|||
|
|||
setData(formattedData); |
|||
}; |
|||
|
|||
fetchData(); |
|||
}, [ageRange, gender, audio]); |
|||
|
|||
const options = { |
|||
chart: { |
|||
type: "boxplot", |
|||
}, |
|||
title: { |
|||
text: "SUS Score Distribution by Webpage Version", |
|||
}, |
|||
xAxis: { |
|||
title: { |
|||
text: "Webpage Version", |
|||
}, |
|||
categories: ["1 - BudgetBird", "2 - Hotel", "3 - UVV", "4 - Iceland", "5 - Rental", "6 - QuickDeliver"], |
|||
tickInterval: 1, |
|||
}, |
|||
yAxis: { |
|||
title: { |
|||
text: "SUS Score", |
|||
}, |
|||
}, |
|||
series: [ |
|||
{ |
|||
name: "SUS Scores", |
|||
data: data, |
|||
tooltip: { |
|||
pointFormat: ` |
|||
<b>Min:</b> {point.low}<br/> |
|||
<b>Q1:</b> {point.q1}<br/> |
|||
<b>Median:</b> {point.median}<br/> |
|||
<b>Q3:</b> {point.q3}<br/> |
|||
<b>Max:</b> {point.high}<br/> |
|||
<b>Mean:</b> {point.mean}<br/> |
|||
<b>Std Dev:</b> {point.stdDev}<br/> |
|||
<b>Variance:</b> {point.variance}<br/> |
|||
`, |
|||
headerFormat: "<em>Webpage Version {point.key}</em><br/>", |
|||
}, |
|||
}, |
|||
], |
|||
}; |
|||
|
|||
return <HighchartsReact highcharts={Highcharts} options={options} />; |
|||
}; |
|||
|
|||
export default SUSBoxPlot; |
@ -0,0 +1,165 @@ |
|||
import React, { useEffect, useState } from "react"; |
|||
import Highcharts from "highcharts"; |
|||
import HighchartsReact from "highcharts-react-official"; |
|||
import { Slider } from "@geist-ui/core"; |
|||
|
|||
const SUSBoxPlotComp = ({ ageRange, gender }) => { |
|||
const [audio, setAudio] = useState(4); // Default slider value |
|||
const [dataLower, setDataLower] = useState([]); |
|||
const [dataHigher, setDataHigher] = useState([]); |
|||
|
|||
useEffect(() => { |
|||
const fetchIMIData = async () => { |
|||
const queryParamsLower = new URLSearchParams(); |
|||
const queryParamsHigher = new URLSearchParams(); |
|||
|
|||
if (ageRange.min) { |
|||
queryParamsLower.append("age_min", ageRange.min); |
|||
queryParamsHigher.append("age_min", ageRange.min); |
|||
} |
|||
if (ageRange.max) { |
|||
queryParamsLower.append("age_max", ageRange.max); |
|||
queryParamsHigher.append("age_max", ageRange.max); |
|||
} |
|||
if (gender) { |
|||
queryParamsLower.append("gender", gender); |
|||
queryParamsHigher.append("gender", gender); |
|||
} |
|||
queryParamsLower.append("audioLowerThan", audio + 1); |
|||
queryParamsHigher.append("audioHigherThan", audio); |
|||
|
|||
const [responseLower, responseHigher] = await Promise.all([ |
|||
fetch( |
|||
`http://localhost:5000/api/sus_scores?${queryParamsLower.toString()}` |
|||
), |
|||
fetch( |
|||
`http://localhost:5000/api/sus_scores?${queryParamsHigher.toString()}` |
|||
), |
|||
]); |
|||
|
|||
const dataLower = await responseLower.json(); |
|||
const dataHigher = await responseHigher.json(); |
|||
|
|||
const groupedDataLower = dataLower.reduce((acc, item) => { |
|||
acc[item.WebpageID] = acc[item.WebpageID] || []; |
|||
acc[item.WebpageID].push(item.TotalSUSScore); |
|||
return acc; |
|||
}, {}); |
|||
|
|||
const formattedDataLower = Object.keys(groupedDataLower).map((key) => { |
|||
const scores = groupedDataLower[key].sort((a, b) => a - b); |
|||
const q1 = scores[Math.floor(scores.length / 4)]; |
|||
const median = scores[Math.floor(scores.length / 2)]; |
|||
const q3 = scores[Math.floor((3 * scores.length) / 4)]; |
|||
const min = scores[0]; |
|||
const max = scores[scores.length - 1]; |
|||
return [parseInt(key - 1), min, q1, median, q3, max]; |
|||
}); |
|||
|
|||
const groupedDataHigher = dataHigher.reduce((acc, item) => { |
|||
acc[item.WebpageID] = acc[item.WebpageID] || []; |
|||
acc[item.WebpageID].push(item.TotalSUSScore); |
|||
return acc; |
|||
}, {}); |
|||
|
|||
const formattedDataHigher = Object.keys(groupedDataHigher).map((key) => { |
|||
const scores = groupedDataHigher[key].sort((a, b) => a - b); |
|||
const q1 = scores[Math.floor(scores.length / 4)]; |
|||
const median = scores[Math.floor(scores.length / 2)]; |
|||
const q3 = scores[Math.floor((3 * scores.length) / 4)]; |
|||
const min = scores[0]; |
|||
const max = scores[scores.length - 1]; |
|||
return [parseInt(key - 1), min, q1, median, q3, max]; |
|||
}); |
|||
|
|||
setDataLower(formattedDataLower); |
|||
setDataHigher(formattedDataHigher); |
|||
}; |
|||
|
|||
fetchIMIData(); |
|||
}, [ageRange, gender, audio]); |
|||
|
|||
const options1 = { |
|||
chart: { |
|||
type: "boxplot", |
|||
}, |
|||
title: { |
|||
text: "Participant Average SUS Score Distribution (Audio <= " + audio + " )", |
|||
}, |
|||
xAxis: { |
|||
title: { |
|||
text: "Webpage", |
|||
}, |
|||
categories: ["1", "2", "3", "4", "5", "6"], |
|||
}, |
|||
yAxis: { |
|||
title: { |
|||
text: "SUS Score", |
|||
}, |
|||
}, |
|||
series: [ |
|||
{ |
|||
name: "Webpage", |
|||
data: dataLower, |
|||
binWidth: 5, |
|||
}, |
|||
], |
|||
}; |
|||
|
|||
const options2 = { |
|||
chart: { |
|||
type: "boxplot", |
|||
}, |
|||
title: { |
|||
text: "Participant Average SUS Score Distribution (Audio > " + audio + " )", |
|||
}, |
|||
xAxis: { |
|||
title: { |
|||
text: "Webpage", |
|||
}, |
|||
categories: ["1", "2", "3", "4", "5", "6"], |
|||
}, |
|||
yAxis: { |
|||
title: { |
|||
text: "SUS Score", |
|||
}, |
|||
}, |
|||
series: [ |
|||
{ |
|||
name: "Webpage", |
|||
data: dataHigher, |
|||
binWidth: 5, |
|||
}, |
|||
], |
|||
}; |
|||
|
|||
return ( |
|||
<div style={{ marginTop: "32px", marginBottom: "32px" }}> |
|||
<div |
|||
style={{ |
|||
display: "grid", |
|||
gridTemplateColumns: "1fr 1fr", |
|||
gridTemplateRows: "1fr", |
|||
gap: "0px 0px", |
|||
gridTemplateAreas: ". .", |
|||
}} |
|||
> |
|||
<HighchartsReact highcharts={Highcharts} options={options1} /> |
|||
<HighchartsReact highcharts={Highcharts} options={options2} /> |
|||
</div> |
|||
<Slider |
|||
value={audio} |
|||
onChange={(val) => setAudio(val)} |
|||
min={2} |
|||
max={6} |
|||
step={1} |
|||
initialValue={4} |
|||
showMarkers |
|||
width="75%" |
|||
style={{margin: "16px auto 0 auto"}} |
|||
/> |
|||
</div> |
|||
); |
|||
}; |
|||
|
|||
export default SUSBoxPlotComp; |
@ -0,0 +1,85 @@ |
|||
import React, { useState, useEffect } from "react"; |
|||
import Highcharts from "highcharts"; |
|||
import HighchartsReact from "highcharts-react-official"; |
|||
|
|||
import bellCurve from "highcharts/modules/histogram-bellcurve"; |
|||
bellCurve(Highcharts); |
|||
|
|||
function SUSDeviationPlot({ ageRange, gender, audio }) { |
|||
const [data, setData] = useState([]); |
|||
|
|||
useEffect(() => { |
|||
const fetchData = async () => { |
|||
const queryParams = new URLSearchParams(); |
|||
if (ageRange.min) queryParams.append("age_min", ageRange.min); |
|||
if (ageRange.max) queryParams.append("age_max", ageRange.max); |
|||
if (gender) queryParams.append("gender", gender); |
|||
if (audio) queryParams.append("audio", audio); |
|||
const response = await fetch( |
|||
`http://localhost:5000/api/sus_scores?${queryParams.toString()}` |
|||
); |
|||
const result = await response.json(); |
|||
setData(result); |
|||
}; |
|||
|
|||
fetchData(); |
|||
}, [ageRange, gender, audio]); |
|||
|
|||
const processSUSData = (data) => { |
|||
const scoresByWebpage = {}; |
|||
data.forEach(({ WebpageID, TotalSUSScore }) => { |
|||
if (!scoresByWebpage[WebpageID]) { |
|||
scoresByWebpage[WebpageID] = []; |
|||
} |
|||
scoresByWebpage[WebpageID].push(TotalSUSScore); |
|||
}); |
|||
|
|||
const seriesData = Object.entries(scoresByWebpage).map( |
|||
([webpage, scores]) => { |
|||
const frequencies = {}; |
|||
scores.forEach((score) => { |
|||
//const roundedScore = (Math.round(score * 10) / 10).toFixed(1); |
|||
const roundedScore = (Math.round(score * 2) / 2).toFixed(1); |
|||
frequencies[roundedScore] = (frequencies[roundedScore] || 0) + 1; |
|||
}); |
|||
|
|||
const totalScores = scores.length; |
|||
const percentages = Object.entries(frequencies) |
|||
.map(([score, count]) => ({ |
|||
x: parseFloat(score), |
|||
y: (count / totalScores) * 100, |
|||
})) |
|||
.sort((a, b) => a.x - b.x); |
|||
|
|||
return { |
|||
name: `Webpage ${webpage}`, |
|||
type: "column", //line |
|||
data: percentages, |
|||
}; |
|||
} |
|||
); |
|||
|
|||
return seriesData; |
|||
}; |
|||
|
|||
const options = { |
|||
title: { |
|||
text: "SUS Score Distribution by Webpage Version", |
|||
}, |
|||
xAxis: { |
|||
title: { text: "SUS Score" }, |
|||
tickInterval: 0.5, //0.3 |
|||
}, |
|||
yAxis: { |
|||
title: { text: "Percentage" }, |
|||
labels: { |
|||
format: "{value}%", |
|||
}, |
|||
}, |
|||
series: processSUSData(data), |
|||
}; |
|||
|
|||
return <HighchartsReact highcharts={Highcharts} options={options} />; |
|||
} |
|||
|
|||
export default SUSDeviationPlot; |
@ -0,0 +1,69 @@ |
|||
import React, { useEffect, useState } from "react"; |
|||
import Highcharts from "highcharts"; |
|||
import HighchartsReact from "highcharts-react-official"; |
|||
|
|||
const SUSScatterPlot = ({ ageRange, gender, audio }) => { |
|||
const [data, setData] = useState([]); |
|||
|
|||
useEffect(() => { |
|||
const fetchData = async () => { |
|||
const queryParams = new URLSearchParams(); |
|||
if (ageRange.min) queryParams.append("age_min", ageRange.min); |
|||
if (ageRange.max) queryParams.append("age_max", ageRange.max); |
|||
if (gender) queryParams.append("gender", gender); |
|||
if (audio) queryParams.append("audio", audio); |
|||
const response = await fetch( |
|||
`http://localhost:5000/api/sus_scores?${queryParams.toString()}` |
|||
); |
|||
const result = await response.json(); |
|||
|
|||
const formattedData = result.map((item) => ({ |
|||
x: item.WebpageID - 1, |
|||
y: item.TotalSUSScore, |
|||
name: `Participant ${item.ParticipantID}`, |
|||
})); |
|||
|
|||
setData(formattedData); |
|||
}; |
|||
|
|||
fetchData(); |
|||
}, [ageRange, gender, audio]); |
|||
|
|||
const options = { |
|||
chart: { |
|||
type: "scatter", |
|||
jitter: { |
|||
x: 0.24, |
|||
}, |
|||
zoomType: "xy", |
|||
}, |
|||
title: { |
|||
text: "SUS Scores by Webpage Version", |
|||
}, |
|||
xAxis: { |
|||
title: { |
|||
text: "Webpage Version", |
|||
}, |
|||
categories: ["1", "2", "3", "4", "5", "6"], |
|||
tickInterval: 1, |
|||
}, |
|||
yAxis: { |
|||
title: { |
|||
text: "SUS Score", |
|||
}, |
|||
}, |
|||
series: [ |
|||
{ |
|||
name: "SUS Scores", |
|||
data: data, |
|||
tooltip: { |
|||
pointFormat: "{point.name}<br/>SUS Score: {point.y}", |
|||
}, |
|||
}, |
|||
], |
|||
}; |
|||
|
|||
return <HighchartsReact highcharts={Highcharts} options={options} />; |
|||
}; |
|||
|
|||
export default SUSScatterPlot; |
@ -0,0 +1 @@ |
|||
export const DB_PATH = "./participant_db.db"; |
@ -0,0 +1,19 @@ |
|||
body { |
|||
margin: 0; |
|||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', |
|||
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', |
|||
sans-serif; |
|||
-webkit-font-smoothing: antialiased; |
|||
-moz-osx-font-smoothing: grayscale; |
|||
} |
|||
|
|||
code { |
|||
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', |
|||
monospace; |
|||
} |
|||
|
|||
#responseTable, #responseTable th, #responseTable td { |
|||
border: 1px solid black; |
|||
border-collapse: collapse; |
|||
padding: 2px; |
|||
} |
@ -0,0 +1,17 @@ |
|||
import React from 'react'; |
|||
import ReactDOM from 'react-dom/client'; |
|||
import './index.css'; |
|||
import App from './App'; |
|||
import reportWebVitals from './reportWebVitals'; |
|||
|
|||
const root = ReactDOM.createRoot(document.getElementById('root')); |
|||
root.render( |
|||
<React.StrictMode> |
|||
<App /> |
|||
</React.StrictMode> |
|||
); |
|||
|
|||
// If you want to start measuring performance in your app, pass a function
|
|||
// to log results (for example: reportWebVitals(console.log))
|
|||
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
|
|||
reportWebVitals(); |
@ -0,0 +1 @@ |
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3"><g fill="#61DAFB"><path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/><circle cx="420.9" cy="296.5" r="45.7"/><path d="M520.5 78.1z"/></g></svg> |
@ -0,0 +1,13 @@ |
|||
const reportWebVitals = onPerfEntry => { |
|||
if (onPerfEntry && onPerfEntry instanceof Function) { |
|||
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { |
|||
getCLS(onPerfEntry); |
|||
getFID(onPerfEntry); |
|||
getFCP(onPerfEntry); |
|||
getLCP(onPerfEntry); |
|||
getTTFB(onPerfEntry); |
|||
}); |
|||
} |
|||
}; |
|||
|
|||
export default reportWebVitals; |
@ -0,0 +1,382 @@ |
|||
const express = require("express"); |
|||
const sqlite3 = require("sqlite3").verbose(); |
|||
const cors = require("cors"); |
|||
//const { DB_PATH } = require("../core/Constants");
|
|||
const app = express(); |
|||
const port = 5000; |
|||
|
|||
app.use(cors()); |
|||
|
|||
const db = new sqlite3.Database("./src/server/participant_db.db"); |
|||
|
|||
app.get("/api/demographics", (req, res) => { |
|||
let sql = "SELECT * FROM DemographicsData"; |
|||
const params = []; |
|||
|
|||
if ( |
|||
req.query.age_min || |
|||
req.query.age_max || |
|||
req.query.gender || |
|||
req.query.audio |
|||
) { |
|||
const conditions = []; |
|||
if (req.query.age_min) { |
|||
conditions.push("ParticipantAge >= ?"); |
|||
params.push(req.query.age_min); |
|||
} |
|||
if (req.query.age_max) { |
|||
conditions.push("ParticipantAge <= ?"); |
|||
params.push(req.query.age_max); |
|||
} |
|||
if (req.query.gender) { |
|||
conditions.push("ParticipantGender = ?"); |
|||
params.push(req.query.gender.toString()); |
|||
} |
|||
if (req.query.audio) { |
|||
conditions.push("AudioTurnedOn = ?"); |
|||
params.push(req.query.audio); |
|||
} |
|||
sql += " WHERE " + conditions.join(" AND "); |
|||
} |
|||
|
|||
sql += ";"; |
|||
|
|||
db.all(sql, params, (err, rows) => { |
|||
if (err) { |
|||
res.status(400).json({ error: err.message }); |
|||
return; |
|||
} |
|||
res.json(rows); |
|||
}); |
|||
}); |
|||
|
|||
app.get("/api/imi_average", (req, res) => { |
|||
let sql = |
|||
"SELECT WebpageID, AVG((Q1 + Q2 + (8 - Q3) + (8 - Q4) + Q5 + Q6 + Q7) / 7.0) AS Avg_TotalIMIScore FROM QuestionnaireDataIMI"; |
|||
const params = []; |
|||
|
|||
if ( |
|||
req.query.age_min || |
|||
req.query.age_max || |
|||
req.query.gender || |
|||
req.query.audio |
|||
) { |
|||
sql += |
|||
" INNER JOIN DemographicsData ON DemographicsData.ParticipantID = QuestionnaireDataIMI.ParticipantID"; |
|||
const conditions = []; |
|||
if (req.query.age_min) { |
|||
conditions.push("DemographicsData.ParticipantAge >= ?"); |
|||
params.push(req.query.age_min); |
|||
} |
|||
if (req.query.age_max) { |
|||
conditions.push("DemographicsData.ParticipantAge <= ?"); |
|||
params.push(req.query.age_max); |
|||
} |
|||
if (req.query.gender) { |
|||
conditions.push("DemographicsData.ParticipantGender = ?"); |
|||
params.push(req.query.gender.toString()); |
|||
} |
|||
if (req.query.audio) { |
|||
conditions.push("DemographicsData.AudioTurnedOn = ?"); |
|||
params.push(req.query.audio); |
|||
} |
|||
sql += " WHERE " + conditions.join(" AND "); |
|||
} |
|||
|
|||
sql += " GROUP BY WebpageID;"; |
|||
|
|||
db.all(sql, params, (err, rows) => { |
|||
if (err) { |
|||
res.status(400).json({ error: err.message }); |
|||
return; |
|||
} |
|||
res.json(rows); |
|||
}); |
|||
}); |
|||
|
|||
app.get("/api/sus_average", (req, res) => { |
|||
let sql = |
|||
"SELECT WebpageID, AVG(((Q1 - 1) + (5 - Q2) + (Q3 - 1) + (5 - Q4) + (Q5 - 1) + (5 - Q6) + (Q7 - 1) + (5 - Q8) + (Q9 - 1) + (5 - Q10)) * 2.5) AS Avg_TotalSUSScore FROM QuestionnaireDataSUS"; |
|||
const params = []; |
|||
|
|||
if ( |
|||
req.query.age_min || |
|||
req.query.age_max || |
|||
req.query.gender || |
|||
req.query.audio |
|||
) { |
|||
sql += |
|||
" INNER JOIN DemographicsData ON DemographicsData.ParticipantID = QuestionnaireDataSUS.ParticipantID"; |
|||
const conditions = []; |
|||
if (req.query.age_min) { |
|||
conditions.push("DemographicsData.ParticipantAge >= ?"); |
|||
params.push(req.query.age_min); |
|||
} |
|||
if (req.query.age_max) { |
|||
conditions.push("DemographicsData.ParticipantAge <= ?"); |
|||
params.push(req.query.age_max); |
|||
} |
|||
if (req.query.gender) { |
|||
conditions.push("DemographicsData.ParticipantGender = ?"); |
|||
params.push(req.query.gender.toString()); |
|||
} |
|||
if (req.query.audio) { |
|||
conditions.push("DemographicsData.AudioTurnedOn = ?"); |
|||
params.push(req.query.audio); |
|||
} |
|||
sql += " WHERE " + conditions.join(" AND "); |
|||
} |
|||
|
|||
sql += " GROUP BY WebpageID;"; |
|||
|
|||
db.all(sql, params, (err, rows) => { |
|||
if (err) { |
|||
res.status(400).json({ error: err.message }); |
|||
return; |
|||
} |
|||
res.json(rows); |
|||
}); |
|||
}); |
|||
|
|||
app.get("/api/imi_scores", (req, res) => { |
|||
let sql = |
|||
"SELECT QuestionnaireDataIMI.ParticipantID, WebpageID, ((Q1 + Q2 + (8 - Q3) + (8 - Q4) + Q5 + Q6 + Q7) / 7.0) AS TotalIMIScore FROM QuestionnaireDataIMI"; |
|||
|
|||
const params = []; |
|||
|
|||
if ( |
|||
req.query.age_min || |
|||
req.query.age_max || |
|||
req.query.gender || |
|||
req.query.audio || |
|||
req.query.audioLowerThan || |
|||
req.query.audioHigherThan |
|||
) { |
|||
sql += |
|||
" INNER JOIN DemographicsData ON DemographicsData.ParticipantID = QuestionnaireDataIMI.ParticipantID"; |
|||
const conditions = []; |
|||
if (req.query.age_min) { |
|||
conditions.push("DemographicsData.ParticipantAge >= ?"); |
|||
params.push(req.query.age_min); |
|||
} |
|||
if (req.query.age_max) { |
|||
conditions.push("DemographicsData.ParticipantAge <= ?"); |
|||
params.push(req.query.age_max); |
|||
} |
|||
if (req.query.gender) { |
|||
conditions.push("DemographicsData.ParticipantGender = ?"); |
|||
params.push(req.query.gender.toString()); |
|||
} |
|||
if (req.query.audio) { |
|||
conditions.push("DemographicsData.AudioTurnedOn = ?"); |
|||
params.push(req.query.audio); |
|||
} |
|||
if (req.query.audioLowerThan) { |
|||
conditions.push("DemographicsData.AudioTurnedOn < ?"); |
|||
params.push(req.query.audioLowerThan); |
|||
} |
|||
if (req.query.audioHigherThan) { |
|||
conditions.push("DemographicsData.AudioTurnedOn > ?"); |
|||
params.push(req.query.audioHigherThan); |
|||
} |
|||
sql += " WHERE " + conditions.join(" AND ") + ";"; |
|||
} |
|||
|
|||
db.all(sql, params, (err, rows) => { |
|||
if (err) { |
|||
res.status(400).json({ error: err.message }); |
|||
return; |
|||
} |
|||
res.json(rows); |
|||
}); |
|||
}); |
|||
|
|||
app.get("/api/imi_values", (req, res) => { |
|||
let sql = |
|||
"SELECT QuestionnaireDataIMI.ParticipantID, WebpageID, Q1, Q2, Q3, Q4, Q5, Q6, Q7 FROM QuestionnaireDataIMI"; |
|||
|
|||
const params = []; |
|||
|
|||
if ( |
|||
req.query.age_min || |
|||
req.query.age_max || |
|||
req.query.gender || |
|||
req.query.audio |
|||
) { |
|||
sql += |
|||
" INNER JOIN DemographicsData ON DemographicsData.ParticipantID = QuestionnaireDataIMI.ParticipantID"; |
|||
const conditions = []; |
|||
if (req.query.age_min) { |
|||
conditions.push("DemographicsData.ParticipantAge >= ?"); |
|||
params.push(req.query.age_min); |
|||
} |
|||
if (req.query.age_max) { |
|||
conditions.push("DemographicsData.ParticipantAge <= ?"); |
|||
params.push(req.query.age_max); |
|||
} |
|||
if (req.query.gender) { |
|||
conditions.push("DemographicsData.ParticipantGender = ?"); |
|||
params.push(req.query.gender.toString()); |
|||
} |
|||
if (req.query.audio) { |
|||
conditions.push("DemographicsData.AudioTurnedOn = ?"); |
|||
params.push(req.query.audio); |
|||
} |
|||
sql += " WHERE " + conditions.join(" AND ") + ";"; |
|||
} |
|||
|
|||
db.all(sql, params, (err, rows) => { |
|||
if (err) { |
|||
res.status(400).json({ error: err.message }); |
|||
return; |
|||
} |
|||
res.json(rows); |
|||
}); |
|||
}); |
|||
|
|||
app.get("/api/sus_values", (req, res) => { |
|||
let sql = |
|||
"SELECT QuestionnaireDataSUS.ParticipantID, WebpageID, Q1, Q2, Q3, Q4, Q5, Q6, Q7, Q8, Q9, Q10 FROM QuestionnaireDataSUS"; |
|||
|
|||
const params = []; |
|||
|
|||
if ( |
|||
req.query.age_min || |
|||
req.query.age_max || |
|||
req.query.gender || |
|||
req.query.audio |
|||
) { |
|||
sql += |
|||
" INNER JOIN DemographicsData ON DemographicsData.ParticipantID = QuestionnaireDataSUS.ParticipantID"; |
|||
const conditions = []; |
|||
if (req.query.age_min) { |
|||
conditions.push("DemographicsData.ParticipantAge >= ?"); |
|||
params.push(req.query.age_min); |
|||
} |
|||
if (req.query.age_max) { |
|||
conditions.push("DemographicsData.ParticipantAge <= ?"); |
|||
params.push(req.query.age_max); |
|||
} |
|||
if (req.query.gender) { |
|||
conditions.push("DemographicsData.ParticipantGender = ?"); |
|||
params.push(req.query.gender.toString()); |
|||
} |
|||
if (req.query.audio) { |
|||
conditions.push("DemographicsData.AudioTurnedOn = ?"); |
|||
params.push(req.query.audio); |
|||
} |
|||
sql += " WHERE " + conditions.join(" AND ") + ";"; |
|||
} |
|||
|
|||
db.all(sql, params, (err, rows) => { |
|||
if (err) { |
|||
res.status(400).json({ error: err.message }); |
|||
return; |
|||
} |
|||
res.json(rows); |
|||
}); |
|||
}); |
|||
|
|||
app.get("/api/sus_scores", (req, res) => { |
|||
let sql = |
|||
"SELECT QuestionnaireDataSUS.ParticipantID, WebpageID, (((Q1 - 1) + (5 - Q2) + (Q3 - 1) + (5 - Q4) + (Q5 - 1) + (5 - Q6) + (Q7 - 1) + (5 - Q8) + (Q9 - 1) + (5 - Q10)) * 2.5) AS TotalSUSScore FROM QuestionnaireDataSUS"; |
|||
|
|||
const params = []; |
|||
|
|||
if ( |
|||
req.query.age_min || |
|||
req.query.age_max || |
|||
req.query.gender || |
|||
req.query.audio || |
|||
req.query.audioLowerThan || |
|||
req.query.audioHigherThan |
|||
) { |
|||
sql += |
|||
" INNER JOIN DemographicsData ON DemographicsData.ParticipantID = QuestionnaireDataSUS.ParticipantID"; |
|||
const conditions = []; |
|||
if (req.query.age_min) { |
|||
conditions.push("DemographicsData.ParticipantAge >= ?"); |
|||
params.push(req.query.age_min); |
|||
} |
|||
if (req.query.age_max) { |
|||
conditions.push("DemographicsData.ParticipantAge <= ?"); |
|||
params.push(req.query.age_max); |
|||
} |
|||
if (req.query.gender) { |
|||
conditions.push("DemographicsData.ParticipantGender = ?"); |
|||
params.push(req.query.gender.toString()); |
|||
} |
|||
if (req.query.audio) { |
|||
conditions.push("DemographicsData.AudioTurnedOn = ?"); |
|||
params.push(req.query.audio); |
|||
} |
|||
if (req.query.audioLowerThan) { |
|||
conditions.push("DemographicsData.AudioTurnedOn < ?"); |
|||
params.push(req.query.audioLowerThan); |
|||
} |
|||
if (req.query.audioHigherThan) { |
|||
conditions.push("DemographicsData.AudioTurnedOn > ?"); |
|||
params.push(req.query.audioHigherThan); |
|||
} |
|||
sql += " WHERE " + conditions.join(" AND "); |
|||
} |
|||
|
|||
sql += ";"; |
|||
|
|||
db.all(sql, params, (err, rows) => { |
|||
if (err) { |
|||
res.status(400).json({ error: err.message }); |
|||
return; |
|||
} |
|||
res.json(rows); |
|||
}); |
|||
}); |
|||
|
|||
app.get("/api/end_questionnaire_analysed_data", (req, res) => { |
|||
let sql = |
|||
"SELECT * FROM EndQuestionnaireAnalysedData"; |
|||
|
|||
const params = []; |
|||
|
|||
if ( |
|||
req.query.age_min || |
|||
req.query.age_max || |
|||
req.query.gender || |
|||
req.query.audio |
|||
) { |
|||
sql += |
|||
" INNER JOIN DemographicsData ON DemographicsData.ParticipantID = EndQuestionnaireAnalysedData.ParticipantID"; |
|||
const conditions = []; |
|||
if (req.query.age_min) { |
|||
conditions.push("DemographicsData.ParticipantAge >= ?"); |
|||
params.push(req.query.age_min); |
|||
} |
|||
if (req.query.age_max) { |
|||
conditions.push("DemographicsData.ParticipantAge <= ?"); |
|||
params.push(req.query.age_max); |
|||
} |
|||
if (req.query.gender) { |
|||
conditions.push("DemographicsData.ParticipantGender = ?"); |
|||
params.push(req.query.gender.toString()); |
|||
} |
|||
if (req.query.audio) { |
|||
conditions.push("DemographicsData.AudioTurnedOn = ?"); |
|||
params.push(req.query.audio); |
|||
} |
|||
sql += " WHERE " + conditions.join(" AND "); |
|||
} |
|||
|
|||
sql += ";"; |
|||
|
|||
db.all(sql, params, (err, rows) => { |
|||
if (err) { |
|||
res.status(400).json({ error: err.message }); |
|||
return; |
|||
} |
|||
res.json(rows); |
|||
}); |
|||
}); |
|||
|
|||
app.listen(port, () => { |
|||
console.log(`Server is running on http://localhost:${port}`); |
|||
}); |
@ -0,0 +1,5 @@ |
|||
// jest-dom adds custom jest matchers for asserting on DOM nodes.
|
|||
// allows you to do things like:
|
|||
// expect(element).toHaveTextContent(/react/i)
|
|||
// learn more: https://github.com/testing-library/jest-dom
|
|||
import '@testing-library/jest-dom'; |
Write
Preview
Loading…
Cancel
Save
Reference in new issue