Browse Source

feat: Added truncated visualizer tool

master
D45Hub 2 months ago
commit
5baaef455b
  1. 23
      .gitignore
  2. 12
      README.md
  3. 23936
      package-lock.json
  4. 55
      package.json
  5. BIN
      public/favicon.ico
  6. 43
      public/index.html
  7. BIN
      public/logo192.png
  8. BIN
      public/logo512.png
  9. 25
      public/manifest.json
  10. 3
      public/robots.txt
  11. 38
      src/App.css
  12. 253
      src/App.js
  13. 8
      src/App.test.js
  14. 205
      src/charts/QuestionScoreBarChart.jsx
  15. 73
      src/charts/demographics/AgeBoxPlot.jsx
  16. 86
      src/charts/demographics/AgeHistogram.jsx
  17. 55
      src/charts/demographics/AudioExamplesWordCloud.jsx
  18. 61
      src/charts/demographics/AudioTurnedOnChart.jsx
  19. 58
      src/charts/demographics/GenderBarChart.jsx
  20. 64
      src/charts/end/EndQuestionnairePreferredSite.jsx
  21. 56
      src/charts/end/EndSentimentWordcloud.jsx
  22. 56
      src/charts/end/EndUsagesWordcloud.jsx
  23. 115
      src/charts/imi/IMIAnalysis.jsx
  24. 58
      src/charts/imi/IMIBarChart.jsx
  25. 112
      src/charts/imi/IMIBoxPlot.jsx
  26. 165
      src/charts/imi/IMIBoxPlotComp.jsx
  27. 85
      src/charts/imi/IMIDeviationPlot.jsx
  28. 69
      src/charts/imi/IMIScatterPlot.jsx
  29. 120
      src/charts/sus/SUSAnalysis.jsx
  30. 105
      src/charts/sus/SUSBarChart.jsx
  31. 105
      src/charts/sus/SUSBoxPlot.jsx
  32. 165
      src/charts/sus/SUSBoxPlotComp.jsx
  33. 85
      src/charts/sus/SUSDeviationPlot.jsx
  34. 69
      src/charts/sus/SUSScatterPlot.jsx
  35. 1
      src/core/Constants.jsx
  36. 19
      src/index.css
  37. 17
      src/index.js
  38. 1
      src/logo.svg
  39. 13
      src/reportWebVitals.js
  40. BIN
      src/server/participant_db.db
  41. 382
      src/server/server.js
  42. 5
      src/setupTests.js

23
.gitignore

@ -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*

12
README.md

@ -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

55
package.json

@ -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"
}
}

BIN
public/favicon.ico

43
public/index.html

@ -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>

BIN
public/logo192.png

After

Width: 192  |  Height: 192  |  Size: 5.2 KiB

BIN
public/logo512.png

After

Width: 512  |  Height: 512  |  Size: 9.4 KiB

25
public/manifest.json

@ -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"
}

3
public/robots.txt

@ -0,0 +1,3 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:

38
src/App.css

@ -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);
}
}

253
src/App.js

@ -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;

8
src/App.test.js

@ -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();
});

205
src/charts/QuestionScoreBarChart.jsx

@ -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;

73
src/charts/demographics/AgeBoxPlot.jsx

@ -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;

86
src/charts/demographics/AgeHistogram.jsx

@ -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;

55
src/charts/demographics/AudioExamplesWordCloud.jsx

@ -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;

61
src/charts/demographics/AudioTurnedOnChart.jsx

@ -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;

58
src/charts/demographics/GenderBarChart.jsx

@ -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;

64
src/charts/end/EndQuestionnairePreferredSite.jsx

@ -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;

56
src/charts/end/EndSentimentWordcloud.jsx

@ -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;

56
src/charts/end/EndUsagesWordcloud.jsx

@ -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;

115
src/charts/imi/IMIAnalysis.jsx

@ -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;

58
src/charts/imi/IMIBarChart.jsx

@ -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;

112
src/charts/imi/IMIBoxPlot.jsx

@ -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;

165
src/charts/imi/IMIBoxPlotComp.jsx

@ -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;

85
src/charts/imi/IMIDeviationPlot.jsx

@ -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;

69
src/charts/imi/IMIScatterPlot.jsx

@ -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;

120
src/charts/sus/SUSAnalysis.jsx

@ -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;

105
src/charts/sus/SUSBarChart.jsx

@ -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;

105
src/charts/sus/SUSBoxPlot.jsx

@ -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;

165
src/charts/sus/SUSBoxPlotComp.jsx

@ -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;

85
src/charts/sus/SUSDeviationPlot.jsx

@ -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;

69
src/charts/sus/SUSScatterPlot.jsx

@ -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;

1
src/core/Constants.jsx

@ -0,0 +1 @@
export const DB_PATH = "./participant_db.db";

19
src/index.css

@ -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;
}

17
src/index.js

@ -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();

1
src/logo.svg

@ -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>

13
src/reportWebVitals.js

@ -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;

BIN
src/server/participant_db.db

382
src/server/server.js

@ -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}`);
});

5
src/setupTests.js

@ -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';
Loading…
Cancel
Save