D45Hub
3 months ago
19 changed files with 6396 additions and 904 deletions
-
23081package-lock.json
-
5package.json
-
53src/App.js
-
205src/charts/QuestionScoreBarChart.jsx
-
3src/charts/demographics/AgeHistogram.jsx
-
10src/charts/demographics/AudioExamplesWordCloud.jsx
-
5src/charts/end/EndQuestionnaireBoxPlot.jsx
-
63src/charts/end/EndQuestionnairePreferredSite.jsx
-
56src/charts/end/EndSentimentWordcloud.jsx
-
56src/charts/end/EndUsagesWordcloud.jsx
-
115src/charts/imi/IMIAnalysis.jsx
-
1src/charts/imi/IMIBarChart.jsx
-
38src/charts/imi/IMIBoxPlot.jsx
-
165src/charts/imi/IMIBoxPlotComp.jsx
-
120src/charts/sus/SUSAnalysis.jsx
-
52src/charts/sus/SUSBarChart.jsx
-
31src/charts/sus/SUSBoxPlot.jsx
-
165src/charts/sus/SUSBoxPlotComp.jsx
-
154src/server/server.js
23081
package-lock.json
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
@ -0,0 +1,205 @@ |
|||||
|
import React, { useEffect, useState } from "react"; |
||||
|
import Highcharts from "highcharts"; |
||||
|
import HighchartsReact from "highcharts-react-official"; |
||||
|
|
||||
|
const QuestionScoreBarChart = ({ ageRange, gender, audio, type }) => { |
||||
|
const [categories, setCategories] = useState([]); |
||||
|
const [seriesData, setSeriesData] = useState([]); |
||||
|
const [chartsData, setChartsData] = useState([]); |
||||
|
const [categoriesLabels, setCategoryLabels] = useState([]); |
||||
|
|
||||
|
useEffect(() => { |
||||
|
if (type === "IMI") { |
||||
|
setCategoryLabels([ |
||||
|
"Strongly Disagree", |
||||
|
"Disagree", |
||||
|
"Somewhat Disagree", |
||||
|
"Neutral", |
||||
|
"Agree", |
||||
|
"Somewhat Agree", |
||||
|
"Strongly Agree", |
||||
|
]); |
||||
|
} else if (type === "SUS") { |
||||
|
setCategoryLabels([ |
||||
|
"Strongly Disagree", |
||||
|
"Disagree", |
||||
|
"Neutral", |
||||
|
"Agree", |
||||
|
"Strongly Agree", |
||||
|
]); |
||||
|
} |
||||
|
}, [type]); |
||||
|
|
||||
|
useEffect(() => { |
||||
|
const fetchDataForWebpageID = async (webpageID) => { |
||||
|
const queryParams = new URLSearchParams(); |
||||
|
if (ageRange.min) queryParams.append("age_min", ageRange.min); |
||||
|
if (ageRange.max) queryParams.append("age_max", ageRange.max); |
||||
|
if (gender) queryParams.append("gender", gender); |
||||
|
if (audio) queryParams.append("audio", audio); |
||||
|
queryParams.append("WebpageID", webpageID); |
||||
|
|
||||
|
var endpoint; |
||||
|
var numQuestions; |
||||
|
var scores; |
||||
|
|
||||
|
if (type === "IMI") { |
||||
|
endpoint = `http://localhost:5000/api/imi_values?${queryParams.toString()}`; |
||||
|
numQuestions = 7; |
||||
|
} else if (type === "SUS") { |
||||
|
endpoint = `http://localhost:5000/api/sus_values?${queryParams.toString()}`; |
||||
|
numQuestions = 10; |
||||
|
} |
||||
|
|
||||
|
const response = await fetch(endpoint); |
||||
|
const data = await response.json(); |
||||
|
scores = data.filter((dataVal) => dataVal.WebpageID === webpageID); |
||||
|
|
||||
|
const processData = (data, numQuestions) => { |
||||
|
const questionCounts = Array.from({ length: numQuestions }, () => |
||||
|
Array(type === "SUS" ? 5 : 7).fill(0) |
||||
|
); |
||||
|
|
||||
|
data.forEach((participant) => { |
||||
|
for (let i = 0; i < numQuestions; i++) { |
||||
|
const score = participant[`Q${i + 1}`]; |
||||
|
questionCounts[i][score - 1] += 1; |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
const questionPercentages = questionCounts.map((counts) => { |
||||
|
const total = counts.reduce((sum, count) => sum + count, 0); |
||||
|
return counts.map((count) => (count / total) * 100); |
||||
|
}); |
||||
|
|
||||
|
const seriesData = categoriesLabels.map((label, index) => ({ |
||||
|
name: label, |
||||
|
// Switch for absolute/relative responses |
||||
|
//data: questionCounts.map((counts) => counts[index]), |
||||
|
data: questionPercentages.map((percentages) => percentages[index]), |
||||
|
})); |
||||
|
|
||||
|
return { |
||||
|
categories: Array.from( |
||||
|
{ length: numQuestions }, |
||||
|
(_, i) => |
||||
|
`Q${i + 1} ${type === "IMI" && (i + 1 === 3 || i + 1 === 4) ? " - (R)" : ""}` |
||||
|
), |
||||
|
seriesData: seriesData, |
||||
|
}; |
||||
|
}; |
||||
|
|
||||
|
const processedData = processData(scores, numQuestions); |
||||
|
return { webpageID, ...processedData }; |
||||
|
}; |
||||
|
|
||||
|
const fetchAllData = async () => { |
||||
|
const results = await Promise.all( |
||||
|
[1, 2, 3, 4, 5, 6].map((webpageID) => fetchDataForWebpageID(webpageID)) |
||||
|
); |
||||
|
setChartsData(results); |
||||
|
}; |
||||
|
|
||||
|
fetchAllData(); |
||||
|
}, [ageRange, gender, audio, type, categoriesLabels]); |
||||
|
|
||||
|
// Absolute Responses |
||||
|
const options = (categories, seriesData, webpageID) => ({ |
||||
|
chart: { |
||||
|
type: "bar", |
||||
|
}, |
||||
|
title: { |
||||
|
text: `${type} Question Responses Distribution for Webpage ${webpageID}`, |
||||
|
}, |
||||
|
xAxis: { |
||||
|
categories: categories, |
||||
|
title: { |
||||
|
text: "Questions", |
||||
|
}, |
||||
|
}, |
||||
|
yAxis: { |
||||
|
min: 0, |
||||
|
title: { |
||||
|
text: "Number of Responses", |
||||
|
align: "high", |
||||
|
}, |
||||
|
labels: { |
||||
|
overflow: "justify", |
||||
|
}, |
||||
|
}, |
||||
|
plotOptions: { |
||||
|
bar: { |
||||
|
dataLabels: { |
||||
|
enabled: true, |
||||
|
}, |
||||
|
}, |
||||
|
}, |
||||
|
legend: { |
||||
|
reversed: true, |
||||
|
}, |
||||
|
credits: { |
||||
|
enabled: false, |
||||
|
}, |
||||
|
series: seriesData, |
||||
|
}); |
||||
|
|
||||
|
// Relative responses |
||||
|
const options2 = (categories, seriesData, webpageID) => ({ |
||||
|
chart: { |
||||
|
type: "bar", |
||||
|
}, |
||||
|
title: { |
||||
|
text: `${type} Question Responses Distribution for Webpage ${webpageID}`, |
||||
|
}, |
||||
|
xAxis: { |
||||
|
categories: categories, |
||||
|
title: { |
||||
|
text: "Questions", |
||||
|
}, |
||||
|
}, |
||||
|
yAxis: { |
||||
|
min: 0, |
||||
|
max: 100, |
||||
|
title: { |
||||
|
text: "Percentage of Responses", |
||||
|
align: "high", |
||||
|
}, |
||||
|
labels: { |
||||
|
format: "{value}%", |
||||
|
}, |
||||
|
}, |
||||
|
plotOptions: { |
||||
|
series: { |
||||
|
stacking: "percent", |
||||
|
dataLabels: { |
||||
|
enabled: true, |
||||
|
format: "{point.percentage:.1f}%", |
||||
|
}, |
||||
|
}, |
||||
|
}, |
||||
|
credits: { |
||||
|
enabled: false, |
||||
|
}, |
||||
|
series: seriesData, |
||||
|
}); |
||||
|
|
||||
|
return ( |
||||
|
<div |
||||
|
style={{ |
||||
|
display: "grid", |
||||
|
gridTemplateColumns: "1fr 1fr", |
||||
|
gridTemplateRows: "1fr 1fr 1fr", |
||||
|
}} |
||||
|
> |
||||
|
{chartsData.map(({ webpageID, categories, seriesData }) => ( |
||||
|
<HighchartsReact |
||||
|
key={webpageID} |
||||
|
highcharts={Highcharts} |
||||
|
options={options2(categories, seriesData, webpageID)} |
||||
|
/> |
||||
|
))} |
||||
|
</div> |
||||
|
); |
||||
|
}; |
||||
|
|
||||
|
export default QuestionScoreBarChart; |
@ -0,0 +1,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; |
@ -0,0 +1,56 @@ |
|||||
|
import React, { useEffect, useState } from "react"; |
||||
|
import Highcharts from "highcharts"; |
||||
|
import HighchartsReact from "highcharts-react-official"; |
||||
|
import wordcloud from "highcharts/modules/wordcloud"; |
||||
|
|
||||
|
wordcloud(Highcharts); |
||||
|
|
||||
|
const EndSentimentWordcloud = ({ ageRange, gender, audio }) => { |
||||
|
const [data, setData] = useState([]); |
||||
|
|
||||
|
useEffect(() => { |
||||
|
const queryParams = new URLSearchParams(); |
||||
|
if (ageRange.min) queryParams.append("age_min", ageRange.min); |
||||
|
if (ageRange.max) queryParams.append("age_max", ageRange.max); |
||||
|
if (gender) queryParams.append("gender", gender); |
||||
|
if (audio) queryParams.append("audio", audio); |
||||
|
fetch( |
||||
|
`http://localhost:5000/api/end_questionnaire_analysed_data?${queryParams.toString()}` |
||||
|
) |
||||
|
.then((response) => response.json()) |
||||
|
.then((data) => { |
||||
|
const wordCounts = data.reduce((acc, item) => { |
||||
|
console.log(item); |
||||
|
const words = item.SoundSentiment.split(" "); |
||||
|
words.forEach((word) => { |
||||
|
acc[word] = (acc[word] || 0) + 1; |
||||
|
}); |
||||
|
return acc; |
||||
|
}, {}); |
||||
|
|
||||
|
const formattedWords = Object.keys(wordCounts).map((word) => ({ |
||||
|
name: word, |
||||
|
weight: wordCounts[word], |
||||
|
})); |
||||
|
|
||||
|
setData(formattedWords); |
||||
|
}); |
||||
|
}, [ageRange, gender, audio]); |
||||
|
|
||||
|
const options = { |
||||
|
series: [ |
||||
|
{ |
||||
|
type: "wordcloud", |
||||
|
data: data, |
||||
|
name: "Occurrences", |
||||
|
}, |
||||
|
], |
||||
|
title: { |
||||
|
text: "Sonification Sentiment Wordcloud", |
||||
|
}, |
||||
|
}; |
||||
|
|
||||
|
return <HighchartsReact highcharts={Highcharts} options={options} />; |
||||
|
}; |
||||
|
|
||||
|
export default EndSentimentWordcloud; |
@ -0,0 +1,56 @@ |
|||||
|
import React, { useEffect, useState } from "react"; |
||||
|
import Highcharts from "highcharts"; |
||||
|
import HighchartsReact from "highcharts-react-official"; |
||||
|
import wordcloud from "highcharts/modules/wordcloud"; |
||||
|
|
||||
|
wordcloud(Highcharts); |
||||
|
|
||||
|
const EndUsagesWordcloud = ({ ageRange, gender, audio }) => { |
||||
|
const [data, setData] = useState([]); |
||||
|
|
||||
|
useEffect(() => { |
||||
|
const queryParams = new URLSearchParams(); |
||||
|
if (ageRange.min) queryParams.append("age_min", ageRange.min); |
||||
|
if (ageRange.max) queryParams.append("age_max", ageRange.max); |
||||
|
if (gender) queryParams.append("gender", gender); |
||||
|
if (audio) queryParams.append("audio", audio); |
||||
|
fetch( |
||||
|
`http://localhost:5000/api/end_questionnaire_analysed_data?${queryParams.toString()}` |
||||
|
) |
||||
|
.then((response) => response.json()) |
||||
|
.then((data) => { |
||||
|
const wordCounts = data.reduce((acc, item) => { |
||||
|
console.log(item); |
||||
|
const words = item.RealLifeApplicationIdea.split(" "); |
||||
|
words.forEach((word) => { |
||||
|
acc[word] = (acc[word] || 0) + 1; |
||||
|
}); |
||||
|
return acc; |
||||
|
}, {}); |
||||
|
|
||||
|
const formattedWords = Object.keys(wordCounts).map((word) => ({ |
||||
|
name: word, |
||||
|
weight: wordCounts[word], |
||||
|
})); |
||||
|
|
||||
|
setData(formattedWords); |
||||
|
}); |
||||
|
}, [ageRange, gender, audio]); |
||||
|
|
||||
|
const options = { |
||||
|
series: [ |
||||
|
{ |
||||
|
type: "wordcloud", |
||||
|
data: data, |
||||
|
name: "Occurrences", |
||||
|
}, |
||||
|
], |
||||
|
title: { |
||||
|
text: "Sonification Usages Wordcloud", |
||||
|
}, |
||||
|
}; |
||||
|
|
||||
|
return <HighchartsReact highcharts={Highcharts} options={options} />; |
||||
|
}; |
||||
|
|
||||
|
export default EndUsagesWordcloud; |
@ -0,0 +1,115 @@ |
|||||
|
import React, { useEffect, useState } from "react"; |
||||
|
import { std, variance } from "mathjs"; |
||||
|
|
||||
|
const IMIAnalysis = ({ ageRange, gender, audio }) => { |
||||
|
const [descriptiveStats, setDescriptiveStats] = useState({}); |
||||
|
const [cronbachAlpha, setCronbachAlpha] = useState(null); |
||||
|
const [alphasIfDeleted, setAlphasIfDeleted] = useState([]); |
||||
|
|
||||
|
useEffect(() => { |
||||
|
const queryParams = new URLSearchParams(); |
||||
|
if (ageRange.min) queryParams.append("age_min", ageRange.min); |
||||
|
if (ageRange.max) queryParams.append("age_max", ageRange.max); |
||||
|
if (gender) queryParams.append("gender", gender); |
||||
|
if (audio) queryParams.append("audio", audio); |
||||
|
fetch(`http://localhost:5000/api/imi_scores?${queryParams.toString()}`) |
||||
|
.then((response) => response.json()) |
||||
|
.then((data) => { |
||||
|
calculateDescriptiveStats(data); |
||||
|
}); |
||||
|
|
||||
|
fetch(`http://localhost:5000/api/imi_values?${queryParams.toString()}`) |
||||
|
.then((response) => response.json()) |
||||
|
.then((data) => { |
||||
|
const scores = data.map((row) => [ |
||||
|
row.Q1, |
||||
|
row.Q2, |
||||
|
8 - row.Q3, |
||||
|
8 - row.Q4, |
||||
|
row.Q5, |
||||
|
row.Q6, |
||||
|
row.Q7, |
||||
|
]); |
||||
|
setCronbachAlpha(calcAlpha(scores)); |
||||
|
const alphas = calculateAlphaIfDeleted(scores); |
||||
|
setAlphasIfDeleted(alphas); |
||||
|
}); |
||||
|
}, [ageRange, gender, audio]); |
||||
|
|
||||
|
const calculateDescriptiveStats = (data) => { |
||||
|
if (data.length === 0) return; |
||||
|
const scores = data.map((d) => d.TotalIMIScore); |
||||
|
const mean = scores.reduce((a, b) => a + b, 0) / scores.length; |
||||
|
const stdDev = std(scores); |
||||
|
const stats = { |
||||
|
mean, |
||||
|
stdDev, |
||||
|
min: Math.min(...scores), |
||||
|
max: Math.max(...scores), |
||||
|
}; |
||||
|
setDescriptiveStats(stats); |
||||
|
}; |
||||
|
|
||||
|
const calcAlpha = (scores) => { |
||||
|
const variances = scores[0].map((_, colIndex) => { |
||||
|
const col = scores.map((row) => row[colIndex]); |
||||
|
return variance(col); |
||||
|
}); |
||||
|
|
||||
|
const totalScores = scores.map((row) => row.reduce((a, b) => a + b, 0)); |
||||
|
const totalVariance = variance(totalScores); |
||||
|
|
||||
|
const numItems = scores[0].length; |
||||
|
return ( |
||||
|
(numItems / (numItems - 1)) * |
||||
|
(1 - variances.reduce((a, b) => a + b, 0) / totalVariance) |
||||
|
); |
||||
|
}; |
||||
|
|
||||
|
const calculateAlphaIfDeleted = (scores) => { |
||||
|
const itemVars = scores[0].map((_, colIndex) => { |
||||
|
const col = scores.map((row) => row[colIndex]); |
||||
|
return variance(col); |
||||
|
}); |
||||
|
|
||||
|
const totalScores = scores.map((row) => row.reduce((a, b) => a + b, 0)); |
||||
|
const totalVariance = variance(totalScores); |
||||
|
|
||||
|
const numItems = scores[0].length; |
||||
|
|
||||
|
const alphas = scores[0].map((_, colIndex) => { |
||||
|
const remainingVars = itemVars.filter((_, i) => i !== colIndex); |
||||
|
const alpha = |
||||
|
((numItems - 1) / (numItems - 2)) * |
||||
|
(1 - remainingVars.reduce((a, b) => a + b, 0) / totalVariance); |
||||
|
return alpha; |
||||
|
}); |
||||
|
|
||||
|
return alphas; |
||||
|
}; |
||||
|
|
||||
|
return ( |
||||
|
<div> |
||||
|
<h1>IMI Analysis</h1> |
||||
|
<h2>Descriptive Statistics</h2> |
||||
|
<p>Mean: {descriptiveStats.mean}</p> |
||||
|
<p>Standard Deviation: {descriptiveStats.stdDev}</p> |
||||
|
<p>Min: {descriptiveStats.min}</p> |
||||
|
<p>Max: {descriptiveStats.max}</p> |
||||
|
|
||||
|
<h2>Cronbach's Alpha</h2> |
||||
|
<p>{cronbachAlpha}</p> |
||||
|
|
||||
|
<h2>Cronbach's Alpha if Items Deleted</h2> |
||||
|
<ul> |
||||
|
{alphasIfDeleted.map((alpha, index) => ( |
||||
|
<li key={index}> |
||||
|
Alpha if Q{index + 1} deleted: {alpha} |
||||
|
</li> |
||||
|
))} |
||||
|
</ul> |
||||
|
</div> |
||||
|
); |
||||
|
}; |
||||
|
|
||||
|
export default IMIAnalysis; |
@ -0,0 +1,165 @@ |
|||||
|
import React, { useEffect, useState } from "react"; |
||||
|
import Highcharts from "highcharts"; |
||||
|
import HighchartsReact from "highcharts-react-official"; |
||||
|
import { Slider } from "@geist-ui/core"; |
||||
|
|
||||
|
const IMIBoxPlotComp = ({ ageRange, gender }) => { |
||||
|
const [audio, setAudio] = useState(4); // Default slider value |
||||
|
const [dataLower, setDataLower] = useState([]); |
||||
|
const [dataHigher, setDataHigher] = useState([]); |
||||
|
|
||||
|
useEffect(() => { |
||||
|
const fetchIMIData = async () => { |
||||
|
const queryParamsLower = new URLSearchParams(); |
||||
|
const queryParamsHigher = new URLSearchParams(); |
||||
|
|
||||
|
if (ageRange.min) { |
||||
|
queryParamsLower.append("age_min", ageRange.min); |
||||
|
queryParamsHigher.append("age_min", ageRange.min); |
||||
|
} |
||||
|
if (ageRange.max) { |
||||
|
queryParamsLower.append("age_max", ageRange.max); |
||||
|
queryParamsHigher.append("age_max", ageRange.max); |
||||
|
} |
||||
|
if (gender) { |
||||
|
queryParamsLower.append("gender", gender); |
||||
|
queryParamsHigher.append("gender", gender); |
||||
|
} |
||||
|
queryParamsLower.append("audioLowerThan", audio + 1); |
||||
|
queryParamsHigher.append("audioHigherThan", audio); |
||||
|
|
||||
|
const [responseLower, responseHigher] = await Promise.all([ |
||||
|
fetch( |
||||
|
`http://localhost:5000/api/imi_scores?${queryParamsLower.toString()}` |
||||
|
), |
||||
|
fetch( |
||||
|
`http://localhost:5000/api/imi_scores?${queryParamsHigher.toString()}` |
||||
|
), |
||||
|
]); |
||||
|
|
||||
|
const dataLower = await responseLower.json(); |
||||
|
const dataHigher = await responseHigher.json(); |
||||
|
|
||||
|
const groupedDataLower = dataLower.reduce((acc, item) => { |
||||
|
acc[item.WebpageID] = acc[item.WebpageID] || []; |
||||
|
acc[item.WebpageID].push(item.TotalIMIScore); |
||||
|
return acc; |
||||
|
}, {}); |
||||
|
|
||||
|
const formattedDataLower = Object.keys(groupedDataLower).map((key) => { |
||||
|
const scores = groupedDataLower[key].sort((a, b) => a - b); |
||||
|
const q1 = scores[Math.floor(scores.length / 4)]; |
||||
|
const median = scores[Math.floor(scores.length / 2)]; |
||||
|
const q3 = scores[Math.floor((3 * scores.length) / 4)]; |
||||
|
const min = scores[0]; |
||||
|
const max = scores[scores.length - 1]; |
||||
|
return [parseInt(key - 1), min, q1, median, q3, max]; |
||||
|
}); |
||||
|
|
||||
|
const groupedDataHigher = dataHigher.reduce((acc, item) => { |
||||
|
acc[item.WebpageID] = acc[item.WebpageID] || []; |
||||
|
acc[item.WebpageID].push(item.TotalIMIScore); |
||||
|
return acc; |
||||
|
}, {}); |
||||
|
|
||||
|
const formattedDataHigher = Object.keys(groupedDataHigher).map((key) => { |
||||
|
const scores = groupedDataHigher[key].sort((a, b) => a - b); |
||||
|
const q1 = scores[Math.floor(scores.length / 4)]; |
||||
|
const median = scores[Math.floor(scores.length / 2)]; |
||||
|
const q3 = scores[Math.floor((3 * scores.length) / 4)]; |
||||
|
const min = scores[0]; |
||||
|
const max = scores[scores.length - 1]; |
||||
|
return [parseInt(key - 1), min, q1, median, q3, max]; |
||||
|
}); |
||||
|
|
||||
|
setDataLower(formattedDataLower); |
||||
|
setDataHigher(formattedDataHigher); |
||||
|
}; |
||||
|
|
||||
|
fetchIMIData(); |
||||
|
}, [ageRange, gender, audio]); |
||||
|
|
||||
|
const options1 = { |
||||
|
chart: { |
||||
|
type: "boxplot", |
||||
|
}, |
||||
|
title: { |
||||
|
text: "Participant Average IMI Score Distribution (Audio <= " + audio + " )", |
||||
|
}, |
||||
|
xAxis: { |
||||
|
title: { |
||||
|
text: "Webpage", |
||||
|
}, |
||||
|
categories: ["1", "2", "3", "4", "5", "6"], |
||||
|
}, |
||||
|
yAxis: { |
||||
|
title: { |
||||
|
text: "IMI Score", |
||||
|
}, |
||||
|
}, |
||||
|
series: [ |
||||
|
{ |
||||
|
name: "Webpage", |
||||
|
data: dataLower, |
||||
|
binWidth: 5, |
||||
|
}, |
||||
|
], |
||||
|
}; |
||||
|
|
||||
|
const options2 = { |
||||
|
chart: { |
||||
|
type: "boxplot", |
||||
|
}, |
||||
|
title: { |
||||
|
text: "Participant Average IMI Score Distribution (Audio > " + audio + " )", |
||||
|
}, |
||||
|
xAxis: { |
||||
|
title: { |
||||
|
text: "Webpage", |
||||
|
}, |
||||
|
categories: ["1", "2", "3", "4", "5", "6"], |
||||
|
}, |
||||
|
yAxis: { |
||||
|
title: { |
||||
|
text: "IMI Score", |
||||
|
}, |
||||
|
}, |
||||
|
series: [ |
||||
|
{ |
||||
|
name: "Webpage", |
||||
|
data: dataHigher, |
||||
|
binWidth: 5, |
||||
|
}, |
||||
|
], |
||||
|
}; |
||||
|
|
||||
|
return ( |
||||
|
<div style={{ marginTop: "32px", marginBottom: "32px" }}> |
||||
|
<div |
||||
|
style={{ |
||||
|
display: "grid", |
||||
|
gridTemplateColumns: "1fr 1fr", |
||||
|
gridTemplateRows: "1fr", |
||||
|
gap: "0px 0px", |
||||
|
gridTemplateAreas: ". .", |
||||
|
}} |
||||
|
> |
||||
|
<HighchartsReact highcharts={Highcharts} options={options1} /> |
||||
|
<HighchartsReact highcharts={Highcharts} options={options2} /> |
||||
|
</div> |
||||
|
<Slider |
||||
|
value={audio} |
||||
|
onChange={(val) => setAudio(val)} |
||||
|
min={2} |
||||
|
max={6} |
||||
|
step={1} |
||||
|
initialValue={4} |
||||
|
showMarkers |
||||
|
width="75%" |
||||
|
style={{margin: "16px auto 0 auto"}} |
||||
|
/> |
||||
|
</div> |
||||
|
); |
||||
|
}; |
||||
|
|
||||
|
export default IMIBoxPlotComp; |
@ -0,0 +1,120 @@ |
|||||
|
import React, { useEffect, useState } from "react"; |
||||
|
import { std, variance } from "mathjs"; |
||||
|
|
||||
|
const SUSAnalysis = ({ ageRange, gender, audio }) => { |
||||
|
const [descriptiveStats, setDescriptiveStats] = useState({}); |
||||
|
const [cronbachAlpha, setCronbachAlpha] = useState(null); |
||||
|
const [alphasIfDeleted, setAlphasIfDeleted] = useState([]); |
||||
|
|
||||
|
useEffect(() => { |
||||
|
const queryParams = new URLSearchParams(); |
||||
|
if (ageRange.min) queryParams.append("age_min", ageRange.min); |
||||
|
if (ageRange.max) queryParams.append("age_max", ageRange.max); |
||||
|
if (gender) queryParams.append("gender", gender); |
||||
|
if (audio) queryParams.append("audio", audio); |
||||
|
fetch(`http://localhost:5000/api/sus_scores?${queryParams.toString()}`) |
||||
|
.then((response) => response.json()) |
||||
|
.then((data) => { |
||||
|
calculateDescriptiveStats(data); |
||||
|
}); |
||||
|
|
||||
|
// Honestly... Not quite sure how to rate these or the IMI one... At least with the formula it should be rated in an inverted manner... |
||||
|
// Same with the reverse scores of the IMI... |
||||
|
fetch(`http://localhost:5000/api/sus_values?${queryParams.toString()}`) |
||||
|
.then((response) => response.json()) |
||||
|
.then((data) => { |
||||
|
const scores = data.map((row) => [ |
||||
|
row.Q1, |
||||
|
-1 * row.Q2, |
||||
|
row.Q3, |
||||
|
-1 * row.Q4, |
||||
|
row.Q5, |
||||
|
-1 * row.Q6, |
||||
|
row.Q7, |
||||
|
-1 * row.Q8, |
||||
|
row.Q9, |
||||
|
-1 * row.Q10, |
||||
|
]); |
||||
|
setCronbachAlpha(calcAlpha(scores)); |
||||
|
const alphas = calculateAlphaIfDeleted(scores); |
||||
|
setAlphasIfDeleted(alphas); |
||||
|
}); |
||||
|
}, [ageRange, gender, audio]); |
||||
|
|
||||
|
const calculateDescriptiveStats = (data) => { |
||||
|
if (data.length === 0) return; |
||||
|
const scores = data.map((d) => d.TotalSUSScore); |
||||
|
const mean = scores.reduce((a, b) => a + b, 0) / scores.length; |
||||
|
const stdDev = std(scores); |
||||
|
const stats = { |
||||
|
mean, |
||||
|
stdDev, |
||||
|
min: Math.min(...scores), |
||||
|
max: Math.max(...scores), |
||||
|
}; |
||||
|
setDescriptiveStats(stats); |
||||
|
}; |
||||
|
|
||||
|
const calcAlpha = (scores) => { |
||||
|
const variances = scores[0].map((_, colIndex) => { |
||||
|
const col = scores.map((row) => row[colIndex]); |
||||
|
return variance(col); |
||||
|
}); |
||||
|
|
||||
|
const totalScores = scores.map((row) => row.reduce((a, b) => a + b, 0)); |
||||
|
const totalVariance = variance(totalScores); |
||||
|
|
||||
|
const numItems = scores[0].length; |
||||
|
return ( |
||||
|
(numItems / (numItems - 1)) * |
||||
|
(1 - variances.reduce((a, b) => a + b, 0) / totalVariance) |
||||
|
); |
||||
|
}; |
||||
|
|
||||
|
const calculateAlphaIfDeleted = (scores) => { |
||||
|
const itemVars = scores[0].map((_, colIndex) => { |
||||
|
const col = scores.map((row) => row[colIndex]); |
||||
|
return variance(col); |
||||
|
}); |
||||
|
|
||||
|
const totalScores = scores.map((row) => row.reduce((a, b) => a + b, 0)); |
||||
|
const totalVariance = variance(totalScores); |
||||
|
|
||||
|
const numItems = scores[0].length; |
||||
|
|
||||
|
const alphas = scores[0].map((_, colIndex) => { |
||||
|
const remainingVars = itemVars.filter((_, i) => i !== colIndex); |
||||
|
const alpha = |
||||
|
((numItems - 1) / (numItems - 2)) * |
||||
|
(1 - remainingVars.reduce((a, b) => a + b, 0) / totalVariance); |
||||
|
return alpha; |
||||
|
}); |
||||
|
|
||||
|
return alphas; |
||||
|
}; |
||||
|
|
||||
|
return ( |
||||
|
<div> |
||||
|
<h1>SUS Analysis</h1> |
||||
|
<h2>Descriptive Statistics</h2> |
||||
|
<p>Mean: {descriptiveStats.mean}</p> |
||||
|
<p>Standard Deviation: {descriptiveStats.stdDev}</p> |
||||
|
<p>Min: {descriptiveStats.min}</p> |
||||
|
<p>Max: {descriptiveStats.max}</p> |
||||
|
|
||||
|
<h2>Cronbach's Alpha</h2> |
||||
|
<p>{cronbachAlpha}</p> |
||||
|
|
||||
|
<h2>Cronbach's Alpha if Items Deleted</h2> |
||||
|
<ul> |
||||
|
{alphasIfDeleted.map((alpha, index) => ( |
||||
|
<li key={index}> |
||||
|
Alpha if Q{index + 1} deleted: {alpha} |
||||
|
</li> |
||||
|
))} |
||||
|
</ul> |
||||
|
</div> |
||||
|
); |
||||
|
}; |
||||
|
|
||||
|
export default SUSAnalysis; |
@ -0,0 +1,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; |
Write
Preview
Loading…
Cancel
Save
Reference in new issue