Visual apperance
This commit is contained in:
49
src/App.tsx
49
src/App.tsx
@@ -1,5 +1,7 @@
|
|||||||
import * as d3 from "d3";
|
import * as d3 from "d3";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
import Legend from "./components/Legend";
|
||||||
|
import QRCode from "./components/QRCode";
|
||||||
import { QuestionGroupChart } from "./components/QuestionGroupChart";
|
import { QuestionGroupChart } from "./components/QuestionGroupChart";
|
||||||
import { config } from "./config";
|
import { config } from "./config";
|
||||||
import { getSampleData } from "./lib/data";
|
import { getSampleData } from "./lib/data";
|
||||||
@@ -8,6 +10,7 @@ import { ResponseData } from "./lib/parser";
|
|||||||
function App() {
|
function App() {
|
||||||
const [data, setData] = useState<ResponseData[]>([]);
|
const [data, setData] = useState<ResponseData[]>([]);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
// fetchGoogleSheet().then(setData);
|
||||||
setData(getSampleData());
|
setData(getSampleData());
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -20,27 +23,7 @@ function App() {
|
|||||||
|
|
||||||
// Get unique response categories (sorted for consistent ordering)
|
// Get unique response categories (sorted for consistent ordering)
|
||||||
const allResponses = [...new Set(data.map((d) => d.response))];
|
const allResponses = [...new Set(data.map((d) => d.response))];
|
||||||
const responseOrder = [
|
const sortedResponses = allResponses.sort((a, b) => a - b);
|
||||||
"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
|
// Create scales
|
||||||
const xScale = d3
|
const xScale = d3
|
||||||
@@ -51,15 +34,21 @@ function App() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{questionGroups.map(([question, groupData]) => (
|
<div className="chart-container">
|
||||||
<QuestionGroupChart
|
<div className="charts">
|
||||||
key={question}
|
{questionGroups.map(([question, groupData]) => (
|
||||||
question={question}
|
<QuestionGroupChart
|
||||||
groupData={groupData}
|
key={question}
|
||||||
responses={sortedResponses}
|
question={question}
|
||||||
xScale={xScale}
|
groupData={groupData}
|
||||||
/>
|
responses={sortedResponses}
|
||||||
))}
|
xScale={xScale}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<Legend />
|
||||||
|
</div>
|
||||||
|
<QRCode />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
30
src/components/Chart.css
Normal file
30
src/components/Chart.css
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
.dot {
|
||||||
|
stroke: #fff;
|
||||||
|
stroke-width: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected {
|
||||||
|
fill: rgb(223 110 38);
|
||||||
|
}
|
||||||
|
|
||||||
|
.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;
|
||||||
|
}
|
||||||
19
src/components/Legend.css
Normal file
19
src/components/Legend.css
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
.legend {
|
||||||
|
.box {
|
||||||
|
display: inline-block;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-right: 8px;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
ul {
|
||||||
|
list-style: none;
|
||||||
|
margin: 16px;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
28
src/components/Legend.tsx
Normal file
28
src/components/Legend.tsx
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import { colorScheme } from "../config";
|
||||||
|
import "./Legend.css";
|
||||||
|
|
||||||
|
const labels = {
|
||||||
|
0: "Keine Erfahrung",
|
||||||
|
1: "Grundkenntnisse",
|
||||||
|
2: "Geübte Anwendung",
|
||||||
|
3: "Sichere Praxisanwendung",
|
||||||
|
4: "Fachwissen und Erfahrung",
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function Legend() {
|
||||||
|
return (
|
||||||
|
<div className="legend">
|
||||||
|
<ul>
|
||||||
|
{Object.entries(labels).map(([level, label]) => (
|
||||||
|
<li key={level}>
|
||||||
|
<span
|
||||||
|
className="box"
|
||||||
|
style={{ backgroundColor: `${colorScheme[level]}` }}
|
||||||
|
></span>
|
||||||
|
<span className="label">{label}</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
22
src/components/QRCode.css
Normal file
22
src/components/QRCode.css
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
.qr-code-container {
|
||||||
|
background-color: rgb(223 110 37);
|
||||||
|
flex-basis: 20%;
|
||||||
|
flex-shrink: 0;
|
||||||
|
padding: 16px;
|
||||||
|
border-radius: 16px;
|
||||||
|
|
||||||
|
filter: drop-shadow(0 8px 8px hsl(202.5deg 20% 76.5%));
|
||||||
|
|
||||||
|
color: #fff;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
img {
|
||||||
|
background-color: #fff;
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 16px;
|
||||||
|
width: 50%;
|
||||||
|
}
|
||||||
|
}
|
||||||
16
src/components/QRCode.tsx
Normal file
16
src/components/QRCode.tsx
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import "./QRCode.css";
|
||||||
|
|
||||||
|
export default function QRCode() {
|
||||||
|
return (
|
||||||
|
<div className="qr-code-container">
|
||||||
|
<h1>Scan me!</h1>
|
||||||
|
<p>
|
||||||
|
Scanne den Code und zeige, welche KI-Skills du mitbringst — ganz
|
||||||
|
egal, auf welchem Level du bist.
|
||||||
|
</p>
|
||||||
|
<div className="qr-code bracket-frame">
|
||||||
|
<img src="https://upload.wikimedia.org/wikipedia/commons/4/41/QR_Code_Example.svg" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -2,6 +2,8 @@ import * as d3 from "d3";
|
|||||||
import { useEffect, useRef } from "react";
|
import { useEffect, useRef } from "react";
|
||||||
import { colorScheme, config } from "../config";
|
import { colorScheme, config } from "../config";
|
||||||
|
|
||||||
|
import "./Chart.css";
|
||||||
|
|
||||||
interface QuestionGroupChartProps {
|
interface QuestionGroupChartProps {
|
||||||
question: string;
|
question: string;
|
||||||
groupData: { response: string }[];
|
groupData: { response: string }[];
|
||||||
@@ -9,6 +11,31 @@ interface QuestionGroupChartProps {
|
|||||||
xScale: d3.ScaleBand<string>;
|
xScale: d3.ScaleBand<string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function makeDot(
|
||||||
|
shape: "rect" | "circle",
|
||||||
|
g: d3.Selection<SVGGElement, unknown, null, undefined>,
|
||||||
|
dotX: number,
|
||||||
|
dotY: number
|
||||||
|
) {
|
||||||
|
if (shape === "circle") {
|
||||||
|
return g
|
||||||
|
.append("circle")
|
||||||
|
.attr("class", "dot")
|
||||||
|
.attr("cx", dotX)
|
||||||
|
.attr("cy", dotY)
|
||||||
|
.attr("r", config.dotRadius);
|
||||||
|
} else if (shape === "rect") {
|
||||||
|
return g
|
||||||
|
.append("rect")
|
||||||
|
.attr("class", "dot")
|
||||||
|
.attr("x", dotX - config.dotRadius)
|
||||||
|
.attr("y", dotY - config.dotRadius)
|
||||||
|
.attr("width", config.dotRadius * 2)
|
||||||
|
.attr("height", config.dotRadius * 2);
|
||||||
|
}
|
||||||
|
throw new Error(`Unsupported shape: ${shape}`);
|
||||||
|
}
|
||||||
|
|
||||||
export function QuestionGroupChart({
|
export function QuestionGroupChart({
|
||||||
question,
|
question,
|
||||||
groupData,
|
groupData,
|
||||||
@@ -23,27 +50,14 @@ export function QuestionGroupChart({
|
|||||||
|
|
||||||
// Group responses by category (within this group only)
|
// Group responses by category (within this group only)
|
||||||
const responseGroups = d3.group(groupData, (d) => d.response);
|
const responseGroups = d3.group(groupData, (d) => d.response);
|
||||||
const maxCount =
|
const chartHeight = 200;
|
||||||
d3.max(Array.from(responseGroups.values(), (values) => values.length)) ||
|
const chartWidth = xScale.range()[1];
|
||||||
0;
|
|
||||||
const maxRows = Math.ceil(maxCount / config.columnsPerGroup);
|
|
||||||
|
|
||||||
const chartHeight =
|
|
||||||
maxRows * (config.dotRadius * 2 + config.dotSpacing) +
|
|
||||||
config.margin.bottom;
|
|
||||||
const chartWidth =
|
|
||||||
xScale.range()[1] + config.margin.left + config.margin.right;
|
|
||||||
|
|
||||||
const svg = d3
|
const svg = d3
|
||||||
.select(svgRef.current)
|
.select(svgRef.current)
|
||||||
.attr("width", chartWidth)
|
.attr("width", chartWidth)
|
||||||
.attr("height", chartHeight);
|
.attr("height", chartHeight);
|
||||||
const g = svg
|
const g = svg.append("g");
|
||||||
.append("g")
|
|
||||||
.attr(
|
|
||||||
"transform",
|
|
||||||
`translate(${config.margin.left},${config.margin.top})`
|
|
||||||
);
|
|
||||||
|
|
||||||
// Dots
|
// Dots
|
||||||
responses.forEach((response) => {
|
responses.forEach((response) => {
|
||||||
@@ -58,36 +72,25 @@ export function QuestionGroupChart({
|
|||||||
(col - (config.columnsPerGroup - 1) / 2) *
|
(col - (config.columnsPerGroup - 1) / 2) *
|
||||||
(config.dotRadius * 2 + config.dotSpacing);
|
(config.dotRadius * 2 + config.dotSpacing);
|
||||||
const dotY =
|
const dotY =
|
||||||
chartHeight -
|
chartHeight - (row + 1) * (config.dotRadius * 2 + config.dotSpacing);
|
||||||
config.margin.top -
|
|
||||||
config.margin.bottom -
|
|
||||||
(row + 1) * (config.dotRadius * 2 + config.dotSpacing);
|
|
||||||
|
|
||||||
g.append("circle")
|
makeDot("rect", g, dotX, dotY).attr(
|
||||||
.attr("class", "dot")
|
"fill",
|
||||||
.attr("cx", dotX)
|
colorScheme[response] || "#666"
|
||||||
.attr("cy", dotY)
|
);
|
||||||
.attr("r", config.dotRadius)
|
|
||||||
.attr("fill", colorScheme[response] || "#666");
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}, [groupData, responses, xScale, question]);
|
}, [groupData, responses, xScale, question]);
|
||||||
|
|
||||||
/*
|
|
||||||
// Calculate height for container
|
|
||||||
const responseGroups = d3.group(groupData, (d) => d.response);
|
|
||||||
const maxCount =
|
|
||||||
d3.max(Array.from(responseGroups.values(), (values) => values.length)) || 0;
|
|
||||||
const maxRows = Math.ceil(maxCount / config.columnsPerGroup);
|
|
||||||
const chartHeight =
|
|
||||||
maxRows * (config.dotRadius * 2 + config.dotSpacing) +
|
|
||||||
config.margin.top +
|
|
||||||
config.margin.bottom;
|
|
||||||
*/
|
|
||||||
return (
|
return (
|
||||||
<div className="question-group">
|
<div className="question-group">
|
||||||
<div className="question-title">{question}</div>
|
|
||||||
<svg ref={svgRef}></svg>
|
<svg ref={svgRef}></svg>
|
||||||
|
<div className="question-title">{question}</div>
|
||||||
|
<p>
|
||||||
|
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod
|
||||||
|
tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim
|
||||||
|
veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea
|
||||||
|
commodo.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,24 +1,40 @@
|
|||||||
|
const dotRadius = 8;
|
||||||
|
const dotSpacing = 2;
|
||||||
|
const columnsPerGroup = 3;
|
||||||
|
const groupGap = 8;
|
||||||
|
|
||||||
export const config = {
|
export const config = {
|
||||||
dotRadius: 5,
|
dotRadius,
|
||||||
dotSpacing: 2,
|
dotSpacing,
|
||||||
columnsPerGroup: 3,
|
columnsPerGroup,
|
||||||
groupSpacing: 40,
|
groupSpacing: (2 * dotRadius + dotSpacing) * columnsPerGroup + groupGap,
|
||||||
margin: { top: 20, right: 30, bottom: 80, left: 120 },
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Color scheme for Likert scale responses
|
// Color scheme for Likert scale responses
|
||||||
type ColorScheme = {
|
const aaiColors = [
|
||||||
[key: string | number]: string;
|
"rgb(205 208 219)",
|
||||||
|
"rgb(177 214 217)",
|
||||||
|
"rgb(116 178 183)",
|
||||||
|
"rgb(81 137 141)",
|
||||||
|
"rgb(73 101 104)",
|
||||||
|
];
|
||||||
|
|
||||||
|
export const greens = {
|
||||||
|
0: "#ECEFF2",
|
||||||
|
1: "#DDFDFD",
|
||||||
|
2: "#7FD0D0",
|
||||||
|
3: "#18A5A7",
|
||||||
|
4: "#184F57",
|
||||||
};
|
};
|
||||||
export const colorScheme: ColorScheme = {
|
|
||||||
"Strongly Disagree": "#d32f2f",
|
export const oranges = {
|
||||||
Disagree: "#f57c00",
|
0: "#ECEFF2",
|
||||||
Neutral: "#fbc02d",
|
1: "#FFEBD7",
|
||||||
Agree: "#689f38",
|
2: "#F8C096",
|
||||||
"Strongly Agree": "#388e3c",
|
3: "#F47533",
|
||||||
1: "#d32f2f",
|
4: "#9D3A10",
|
||||||
2: "#f57c00",
|
|
||||||
3: "#fbc02d",
|
|
||||||
4: "#689f38",
|
|
||||||
5: "#388e3c",
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const colorScheme = Object.fromEntries(
|
||||||
|
aaiColors.map((color, index) => [index, color])
|
||||||
|
);
|
||||||
|
|||||||
@@ -36,21 +36,50 @@ body {
|
|||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
.container {
|
#root {
|
||||||
max-width: 1200px;
|
display: flex;
|
||||||
margin: 0 auto;
|
gap: 16px;
|
||||||
background: white;
|
margin: 16px;
|
||||||
padding: 20px;
|
flex-direction: row;
|
||||||
border-radius: 8px;
|
align-items: start;
|
||||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.chart-container {
|
.chart-container {
|
||||||
margin-top: 30px;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
background-color: #ffffff;
|
||||||
|
border-radius: 16px;
|
||||||
|
|
||||||
|
filter: drop-shadow(0 8px 8px hsl(202.5deg 20% 76.5%));
|
||||||
|
}
|
||||||
|
|
||||||
|
.charts {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 16px;
|
||||||
|
padding: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.question-group {
|
.question-group {
|
||||||
margin-bottom: 40px;
|
background-color: #ffffff;
|
||||||
|
padding: 16px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 8px;
|
||||||
|
|
||||||
|
filter: drop-shadow(0 2px 2px hsl(202.5deg 20% 76.5%));
|
||||||
|
}
|
||||||
|
|
||||||
|
.question-group svg {
|
||||||
|
height: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.question-group p {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #666;
|
||||||
|
margin-top: 10px;
|
||||||
|
line-height: 1.4;
|
||||||
|
max-width: 40ch;
|
||||||
}
|
}
|
||||||
|
|
||||||
.question-title {
|
.question-title {
|
||||||
@@ -59,40 +88,6 @@ body {
|
|||||||
margin-bottom: 15px;
|
margin-bottom: 15px;
|
||||||
color: #333;
|
color: #333;
|
||||||
}
|
}
|
||||||
|
.question-title::before {
|
||||||
.dot {
|
content: "💻 ";
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*@media (prefers-color-scheme: dark) {
|
|
||||||
:root {
|
|
||||||
color: rgba(255, 255, 255, 0.87);
|
|
||||||
background-color: #242424;
|
|
||||||
}
|
|
||||||
a:hover {
|
|
||||||
color: #535bf2;
|
|
||||||
}
|
|
||||||
}*/
|
|
||||||
|
|||||||
@@ -6,22 +6,19 @@ export function getSampleData(): ResponseData[] {
|
|||||||
"Value for Money",
|
"Value for Money",
|
||||||
"Ease of Use",
|
"Ease of Use",
|
||||||
"Recommendation",
|
"Recommendation",
|
||||||
];
|
"Overall Satisfaction",
|
||||||
const responses = [
|
"Customer Support",
|
||||||
"Strongly Disagree",
|
"Product Features",
|
||||||
"Disagree",
|
|
||||||
"Neutral",
|
|
||||||
"Agree",
|
|
||||||
"Strongly Agree",
|
|
||||||
];
|
];
|
||||||
const sampleData: ResponseData[] = [];
|
const sampleData: ResponseData[] = [];
|
||||||
let id = 1;
|
let id = 1;
|
||||||
questions.forEach((question) => {
|
questions.forEach((question) => {
|
||||||
const numResponses = Math.floor(Math.random() * 50) + 30;
|
const numResponses = Math.floor(Math.random() * 50) + 30;
|
||||||
for (let i = 0; i < numResponses; i++) {
|
for (let i = 0; i < numResponses; i++) {
|
||||||
const response = responses[Math.floor(Math.random() * responses.length)];
|
const response = Math.floor(Math.random() * 5);
|
||||||
sampleData.push({
|
sampleData.push({
|
||||||
id: id++,
|
timestamp: id++,
|
||||||
|
position: "",
|
||||||
question: question,
|
question: question,
|
||||||
response: response,
|
response: response,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,11 +1,59 @@
|
|||||||
import * as d3 from "d3";
|
import * as d3 from "d3";
|
||||||
|
|
||||||
export interface ResponseData {
|
function mapSkillToNumber(skill: string): number {
|
||||||
id: number;
|
const skills: { [key: string]: number } = {
|
||||||
question: string;
|
"Gar nicht qualifiziert": 0,
|
||||||
response: string;
|
"Leicht qualifiziert": 1,
|
||||||
|
"Mäßig qualifiziert": 2,
|
||||||
|
"Sehr qualifiziert": 3,
|
||||||
|
"Äußerst qualifiziert": 4,
|
||||||
|
};
|
||||||
|
return skills[skill] !== undefined ? skills[skill] : -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function parseCSV(csv: string): d3.DSVRowArray<string> {
|
const sheet_id = "12pGfvJx0SQmb6mnnVygmZsEeLZ6bFrpZvq8GYw2oX9E";
|
||||||
return d3.csvParse(csv);
|
const sheet_name = "Responses";
|
||||||
|
const url = `https://docs.google.com/spreadsheets/d/${sheet_id}/gviz/tq?tqx=out:csv&sheet=${sheet_name}`;
|
||||||
|
|
||||||
|
export interface ResponseData {
|
||||||
|
timestamp: number;
|
||||||
|
position: string;
|
||||||
|
question: string;
|
||||||
|
response: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseCSV(csv: string): ResponseData[] {
|
||||||
|
const rowConverter = (row: d3.DSVRowString): ResponseData[] => {
|
||||||
|
const responses: { [key: string]: number } = {};
|
||||||
|
Object.keys(row).forEach((key) => {
|
||||||
|
// Likert scale responses have their group name in square brackets at the end of the column name
|
||||||
|
const m = key.match(/\[([^\]]+)\]$/);
|
||||||
|
if (m) {
|
||||||
|
const category = m[1];
|
||||||
|
responses[category] = mapSkillToNumber(row[key]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return Object.entries(responses).flatMap(([category, response]) => ({
|
||||||
|
timestamp: new Date(row["Timestamp"]).getTime(),
|
||||||
|
position: row["Position"],
|
||||||
|
question: category,
|
||||||
|
response: response,
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
const rows = d3.csvParse(csv, rowConverter);
|
||||||
|
console.log("Parsed rows:", rows);
|
||||||
|
return rows.flatMap((row) => row);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchGoogleSheet() {
|
||||||
|
return fetch(url)
|
||||||
|
.then((response) => {
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
return response.text();
|
||||||
|
})
|
||||||
|
.then((csv) => {
|
||||||
|
return parseCSV(csv);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user