Migrate to React

This commit is contained in:
Adrian Rumpold
2025-07-09 06:51:16 +02:00
parent 50d2fa8bfe
commit 5dc583e387
23 changed files with 4791 additions and 341 deletions

341
main.html
View File

@@ -1,341 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Likert Survey Visualization</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.8.5/d3.min.js"></script>
<style>
body {
font-family: Arial, sans-serif;
margin: 20px;
background-color: #f5f5f5;
}
.container {
max-width: 1200px;
margin: 0 auto;
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}
h1 {
text-align: center;
color: #333;
margin-bottom: 30px;
}
.file-input-section {
margin-bottom: 30px;
padding: 20px;
background: #f8f9fa;
border-radius: 5px;
border: 2px dashed #dee2e6;
}
.file-input-section input[type="file"] {
margin-bottom: 10px;
}
.sample-button {
background: #007bff;
color: white;
padding: 8px 16px;
border: none;
border-radius: 4px;
cursor: pointer;
margin-left: 10px;
}
.sample-button:hover {
background: #0056b3;
}
.chart-container {
margin-top: 30px;
}
.question-group {
margin-bottom: 40px;
}
.question-title {
font-size: 16px;
font-weight: bold;
margin-bottom: 15px;
color: #333;
}
.dot {
stroke: #fff;
stroke-width: 1;
}
.axis-label {
font-size: 12px;
fill: #666;
}
.axis text {
font-size: 11px;
fill: #666;
}
.axis path,
.axis line {
fill: none;
stroke: #ddd;
shape-rendering: crispEdges;
}
.grid line {
stroke: #e0e0e0;
stroke-dasharray: 2, 2;
}
.tooltip {
position: absolute;
background: rgba(0, 0, 0, 0.8);
color: white;
padding: 8px;
border-radius: 4px;
font-size: 12px;
pointer-events: none;
z-index: 1000;
}
</style>
</head>
<body>
<div class="container">
<h1>Likert Survey Response Visualization</h1>
<div class="file-input-section">
<p>
<strong>Upload CSV File:</strong> Expected format:
id,question,response
</p>
<input type="file" id="csvFile" accept=".csv" />
<button class="sample-button" onclick="loadSampleData()">
Load Sample Data
</button>
</div>
<div class="chart-container" id="chartContainer"></div>
</div>
<script>
// Color scheme for Likert scale responses
const colorScheme = {
"Strongly Disagree": "#d32f2f",
Disagree: "#f57c00",
Neutral: "#fbc02d",
Agree: "#689f38",
"Strongly Agree": "#388e3c",
1: "#d32f2f",
2: "#f57c00",
3: "#fbc02d",
4: "#689f38",
5: "#388e3c",
};
// Configuration
const config = {
dotRadius: 4,
dotSpacing: 2,
columnsPerGroup: 4,
groupSpacing: 60,
questionSpacing: 80,
margin: { top: 20, right: 30, bottom: 80, left: 120 },
};
// Create tooltip
const tooltip = d3
.select("body")
.append("div")
.attr("class", "tooltip")
.style("opacity", 0);
// Sample data generator
function loadSampleData() {
const questions = [
"Service Quality",
"Value for Money",
"Ease of Use",
"Recommendation",
];
const responses = [
"Strongly Disagree",
"Disagree",
"Neutral",
"Agree",
"Strongly Agree",
];
let sampleData = [];
let id = 1;
questions.forEach((question) => {
// Generate random responses for each question
const numResponses = Math.floor(Math.random() * 50) + 30; // 30-80 responses per question
for (let i = 0; i < numResponses; i++) {
const response =
responses[Math.floor(Math.random() * responses.length)];
sampleData.push({
id: id++,
question: question,
response: response,
});
}
});
visualizeData(sampleData);
}
// File input handler
document
.getElementById("csvFile")
.addEventListener("change", function (event) {
const file = event.target.files[0];
if (file) {
const reader = new FileReader();
reader.onload = function (e) {
const csv = e.target.result;
const data = d3.csvParse(csv);
visualizeData(data);
};
reader.readAsText(file);
}
});
function visualizeData(data) {
// Clear existing chart
d3.select("#chartContainer").selectAll("*").remove();
// Group data by question
const questionGroups = d3.group(data, (d) => d.question);
// Get unique response categories (sorted for consistent ordering)
const allResponses = [...new Set(data.map((d) => d.response))];
const responseOrder = [
"Strongly Disagree",
"Disagree",
"Neutral",
"Agree",
"Strongly Agree",
"1",
"2",
"3",
"4",
"5",
];
const sortedResponses = allResponses.sort((a, b) => {
const aIndex = responseOrder.indexOf(a);
const bIndex = responseOrder.indexOf(b);
if (aIndex !== -1 && bIndex !== -1) return aIndex - bIndex;
if (aIndex !== -1) return -1;
if (bIndex !== -1) return 1;
return a.localeCompare(b);
});
// Create scales
const xScale = d3
.scaleBand()
.domain(sortedResponses)
.range([0, sortedResponses.length * config.groupSpacing])
.padding(0.1);
// Create visualization for each question
questionGroups.forEach((questionData, question) => {
createQuestionChart(questionData, question, sortedResponses, xScale);
});
}
function createQuestionChart(data, question, responses, xScale) {
const container = d3.select("#chartContainer");
// Group responses by category
const responseGroups = d3.group(data, (d) => d.response);
// Calculate maximum dots in any group for height calculation
const maxCount = d3.max(
responseGroups,
([key, values]) => values.length
);
const maxRows = Math.ceil(maxCount / config.columnsPerGroup);
const chartHeight =
maxRows * (config.dotRadius * 2 + config.dotSpacing) +
config.margin.top +
config.margin.bottom;
// Create SVG for this question
const svg = container
.append("div")
.attr("class", "question-group")
.append("svg")
.attr(
"width",
xScale.range()[1] + config.margin.left + config.margin.right
)
.attr("height", chartHeight);
const g = svg
.append("g")
.attr(
"transform",
`translate(${config.margin.left},${config.margin.top})`
);
// Create dots for each response group
responses.forEach((response) => {
const responseData = responseGroups.get(response) || [];
const x = xScale(response);
responseData.forEach((d, index) => {
const row = Math.floor(index / config.columnsPerGroup);
const col = index % config.columnsPerGroup;
const dotX =
x +
xScale.bandwidth() / 2 +
(col - (config.columnsPerGroup - 1) / 2) *
(config.dotRadius * 2 + config.dotSpacing);
const dotY =
chartHeight -
config.margin.top -
config.margin.bottom -
(row + 1) * (config.dotRadius * 2 + config.dotSpacing);
g.append("circle")
.attr("class", "dot")
.attr("cx", dotX)
.attr("cy", dotY)
.attr("r", config.dotRadius)
.attr("fill", colorScheme[response] || "#666")
.on("mouseover", function (event) {
tooltip.transition().duration(200).style("opacity", 0.9);
tooltip
.html(
"Question: " +
question +
"<br/>Response: " +
response +
"<br/>ID: " +
d.id
)
.style("left", event.pageX + 10 + "px")
.style("top", event.pageY - 28 + "px");
})
.on("mouseout", function () {
tooltip.transition().duration(500).style("opacity", 0);
});
});
});
}
// Load sample data on page load
loadSampleData();
</script>
</body>
</html>