Initial revision
This commit is contained in:
341
main.html
Normal file
341
main.html
Normal file
@@ -0,0 +1,341 @@
|
|||||||
|
<!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>
|
||||||
Reference in New Issue
Block a user