Browse Source

feat: Final version of data analyser.

master
D45Hub 3 months ago
parent
commit
4b72fba98d
  1. 5993
      package-lock.json
  2. 5
      package.json
  3. 63
      src/App.js
  4. 205
      src/charts/QuestionScoreBarChart.jsx
  5. 3
      src/charts/demographics/AgeHistogram.jsx
  6. 10
      src/charts/demographics/AudioExamplesWordCloud.jsx
  7. 5
      src/charts/end/EndQuestionnaireBoxPlot.jsx
  8. 63
      src/charts/end/EndQuestionnairePreferredSite.jsx
  9. 56
      src/charts/end/EndSentimentWordcloud.jsx
  10. 56
      src/charts/end/EndUsagesWordcloud.jsx
  11. 115
      src/charts/imi/IMIAnalysis.jsx
  12. 1
      src/charts/imi/IMIBarChart.jsx
  13. 38
      src/charts/imi/IMIBoxPlot.jsx
  14. 165
      src/charts/imi/IMIBoxPlotComp.jsx
  15. 120
      src/charts/sus/SUSAnalysis.jsx
  16. 52
      src/charts/sus/SUSBarChart.jsx
  17. 31
      src/charts/sus/SUSBoxPlot.jsx
  18. 165
      src/charts/sus/SUSBoxPlotComp.jsx
  19. 154
      src/server/server.js

5993
package-lock.json
File diff suppressed because it is too large
View File

5
package.json

@ -3,19 +3,24 @@
"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"
},

63
src/App.js

@ -18,6 +18,14 @@ import SUSBoxPlot from "./charts/sus/SUSBoxPlot";
import SUSDeviationPlot from "./charts/sus/SUSDeviationPlot";
import EndQuestionnaireResponses from "./charts/end/EndQuestionnaireResponses";
import EndQuestionnaireBoxPlot from "./charts/end/EndQuestionnaireBoxPlot";
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: "" });
@ -75,7 +83,7 @@ function App() {
<div>
<p>
<label>
Min Age: {" "}
Min Age:{" "}
<input
type="number"
name="min"
@ -85,7 +93,7 @@ function App() {
</label>
<p />
<label>
Max Age: {" "}
Max Age:{" "}
<input
type="number"
name="max"
@ -96,7 +104,7 @@ function App() {
</p>
<p>
<label>
Gender: {" "}
Gender:{" "}
<select value={gender} onChange={handleGenderChange}>
<option value="">Select Gender</option>
<option value="Male">Male</option>
@ -106,14 +114,14 @@ function App() {
</p>
<p>
<label>
Audio Turned On: {" "}
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 />
<br />
<Tabs>
<TabList>
<Tab>Demographics</Tab>
@ -150,6 +158,11 @@ function App() {
/>
</TabPanel>
<TabPanel>
<IMIAnalysis
ageRange={submittedAgeRange}
gender={submittedGender}
audio={submittedAudio}
/>
<IMIBarChart
ageRange={submittedAgeRange}
gender={submittedGender}
@ -170,8 +183,23 @@ function App() {
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}
@ -192,6 +220,16 @@ function App() {
gender={submittedGender}
audio={submittedAudio}
/>
<SUSBoxPlotComp
ageRange={submittedAgeRange}
gender={submittedGender}
/>
<QuestionScoreBarChart
type="SUS"
ageRange={submittedAgeRange}
gender={submittedGender}
audio={submittedAudio}
/>
</TabPanel>
<TabPanel>
<EndQuestionnaireResponses
@ -206,6 +244,21 @@ function App() {
gender={submittedGender}
audio={submittedAudio}
/>
<EndQuestionnairePreferredSite
ageRange={submittedAgeRange}
gender={submittedGender}
audio={submittedAudio}
/>
<EndUsagesWordcloud
ageRange={submittedAgeRange}
gender={submittedGender}
audio={submittedAudio}
/>
<EndSentimentWordcloud
ageRange={submittedAgeRange}
gender={submittedGender}
audio={submittedAudio}
/>
</TabPanel>
</Tabs>
</div>

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;

3
src/charts/demographics/AgeHistogram.jsx

@ -8,7 +8,7 @@ const getCompleteAgesSeries = function (data) {
return acc;
}, {});
const minAge = 0;
const minAge = 20;
const maxAge = Math.max(...data.map((item) => item.ParticipantAge));
const filledAges = [];
@ -64,6 +64,7 @@ const AgeHistogram = ({ ageRange, gender, audio }) => {
title: {
text: "Age",
},
categories: ["20", "21", "22", "23", "24", "25", "26", "27", "28", "29", "30", "31", "32", "33", "34", "35", "36", "37"],
},
yAxis: {
title: {

10
src/charts/demographics/AudioExamplesWordCloud.jsx

@ -3,7 +3,7 @@ import Highcharts from "highcharts";
import HighchartsReact from "highcharts-react-official";
import wordcloud from "highcharts/modules/wordcloud";
wordcloud(Highcharts); // Initialize the word cloud module
wordcloud(Highcharts);
const AudioExamplesWordCloud = ({ ageRange, gender, audio }) => {
const [data, setData] = useState([]);
@ -14,11 +14,13 @@ const AudioExamplesWordCloud = ({ ageRange, gender, audio }) => {
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()}`)
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.AudioExamples.split(" ");
const words = item.AudioUsage.split(" ");
words.forEach((word) => {
acc[word] = (acc[word] || 0) + 1;
});
@ -43,7 +45,7 @@ const AudioExamplesWordCloud = ({ ageRange, gender, audio }) => {
},
],
title: {
text: "Audio Examples Word Cloud",
text: "Sonification Usages Wordcloud",
},
};

5
src/charts/end/EndQuestionnaireBoxPlot.jsx

@ -27,8 +27,7 @@ const EndQuestionnaireBoxPlot = ({ ageRange, gender, audio }) => {
const processQuestionnaireData = (data) => {
const categories = ["Iceland", "Hotel", "BudgetBird", "UVV"];
const boxData = [[], [], [], []]; // Four categories
console.log(data);
const boxData = [[], [], [], []];
data.forEach(({ L1_Iceland, L2_Hotel, L3_BudgetBird, L4_UVV }) => {
boxData[0].push(L1_Iceland);
@ -57,7 +56,7 @@ const EndQuestionnaireBoxPlot = ({ ageRange, gender, audio }) => {
type: "boxplot",
},
title: {
text: "End Questionnaire Box Plot",
text: "Sentiment of Sonified Conditions",
},
xAxis: {
categories: categories,

63
src/charts/end/EndQuestionnairePreferredSite.jsx

@ -0,0 +1,63 @@
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,
}));
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;

1
src/charts/imi/IMIBarChart.jsx

@ -36,6 +36,7 @@ const IMIBarChart = ({ ageRange, gender, audio }) => {
title: {
text: "Webpage",
},
categories: ["1", "2", "3", "4", "5", "6"],
},
yAxis: {
title: {

38
src/charts/imi/IMIBoxPlot.jsx

@ -33,7 +33,24 @@ const IMIBoxPlot = ({ ageRange, gender, audio }) => {
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 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);
@ -53,7 +70,14 @@ const IMIBoxPlot = ({ ageRange, gender, audio }) => {
title: {
text: "Webpage Version",
},
categories: ["1", "2", "3", "4", "5", "6"],
categories: [
"1 - BudgetBird",
"2 - Hotel",
"3 - UVV",
"4 - Iceland",
"5 - Rental",
"6 - QuickDeliver",
],
tickInterval: 1,
},
yAxis: {
@ -66,6 +90,16 @@ const IMIBoxPlot = ({ ageRange, gender, audio }) => {
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/>",
},
},

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;

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;

52
src/charts/sus/SUSBarChart.jsx

@ -16,42 +16,90 @@ const SUSBarChart = ({ ageRange, audio, gender }) => {
.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 SUS Score Distribution",
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 <HighchartsReact highcharts={Highcharts} options={options} />;
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;

31
src/charts/sus/SUSBoxPlot.jsx

@ -33,7 +33,24 @@ const SUSBoxPlot = ({ ageRange, gender, audio }) => {
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 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);
@ -53,7 +70,7 @@ const SUSBoxPlot = ({ ageRange, gender, audio }) => {
title: {
text: "Webpage Version",
},
categories: ["1", "2", "3", "4", "5", "6"],
categories: ["1 - BudgetBird", "2 - Hotel", "3 - UVV", "4 - Iceland", "5 - Rental", "6 - QuickDeliver"],
tickInterval: 1,
},
yAxis: {
@ -66,6 +83,16 @@ const SUSBoxPlot = ({ ageRange, gender, audio }) => {
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/>",
},
},

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;

154
src/server/server.js

@ -20,7 +20,6 @@ app.get("/api/demographics", (req, res) => {
req.query.audio
) {
const conditions = [];
console.log(req.query);
if (req.query.age_min) {
conditions.push("ParticipantAge >= ?");
params.push(req.query.age_min);
@ -145,6 +144,59 @@ app.get("/api/imi_scores", (req, res) => {
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 ||
@ -182,6 +234,49 @@ app.get("/api/imi_scores", (req, res) => {
});
});
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";
@ -192,7 +287,9 @@ app.get("/api/sus_scores", (req, res) => {
req.query.age_min ||
req.query.age_max ||
req.query.gender ||
req.query.audio
req.query.audio ||
req.query.audioLowerThan ||
req.query.audioHigherThan
) {
sql +=
" INNER JOIN DemographicsData ON DemographicsData.ParticipantID = QuestionnaireDataSUS.ParticipantID";
@ -213,6 +310,14 @@ app.get("/api/sus_scores", (req, res) => {
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 ");
}
@ -317,6 +422,51 @@ app.get("/api/end_questionnaire_opinions", (req, res) => {
});
});
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}`);
});
Loading…
Cancel
Save