D45Hub
3 months ago
19 changed files with 6396 additions and 904 deletions
-
5993package-lock.json
-
5package.json
-
63src/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
5993
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