Compare commits

...

4 Commits

Author SHA1 Message Date
Adrian Rumpold
3af9f42e66 Better configurability 2025-07-10 12:47:35 +02:00
Adrian Rumpold
7fb60ad2ed Align legend with survey response options 2025-07-10 12:44:54 +02:00
Adrian Rumpold
5151fd8b73 Make response data refresh interval configurable 2025-07-10 12:35:05 +02:00
Adrian Rumpold
176f68e32d Generate realistic sample data 2025-07-10 12:32:31 +02:00
7 changed files with 47 additions and 63 deletions

View File

@@ -8,7 +8,8 @@ import Legend from "./components/Legend";
import QRCode from "./components/QRCode";
import { WaffleChart } from "./components/WaffleChart";
import { fetchGoogleSheet } from "./lib/parser";
import { config } from "./config";
import { getSampleData } from "./lib/data";
import "./styles/App.scss";
function App() {
@@ -18,9 +19,9 @@ function App() {
});
const responseQuery = useQuery({
queryKey: ["responses"],
queryFn: fetchGoogleSheet,
// queryFn: getSampleData,
refetchInterval: 5 * 1000, // Refresh every 5 seconds
// queryFn: fetchGoogleSheet,
queryFn: getSampleData,
refetchInterval: config.refreshIntervalSeconds * 1000,
});
if (metadataQuery.isPending || responseQuery.isPending)
@@ -29,19 +30,16 @@ function App() {
return <div>Error loading data</div>;
// Sort responses by timestamp to easily find the latest response
const responses = responseQuery.data.sort(
const responses = [...responseQuery.data].sort(
(a, b) => a.timestamp - b.timestamp
);
const categoryMetadata = metadataQuery.data;
if (!responses.length) return null;
// Group data by question (outside the component)
const questionGroups = Array.from(
d3.group(responses, (d) => d.question).entries()
);
// Create scales
return (
<div className="layout">
<div className="chart-container">

View File

@@ -1,19 +1,12 @@
import { colorScheme } from "../config";
import { skills } from "../lib/parser";
import "../styles/Legend.scss";
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]) => (
{Object.entries(skills).map(([label, level]) => (
<li key={level}>
<span
className="box"

View File

@@ -11,6 +11,7 @@ export const config = {
chartHeight: 50,
dotShape: "rect", // "circle" or "rect"
renderXAxis: true, // Whether to render the x-axis
refreshIntervalSeconds: 1, // Refresh interval for response data in seconds
};
// Color scheme for Likert scale responses
@@ -27,3 +28,6 @@ export const colorScheme = Object.fromEntries(
export const categoryMetadataUrl =
"https://docs.google.com/spreadsheets/d/e/2PACX-1vT6FQoV_8ET_pmEB5LGlI_ST9AAhsfiZrWydFwIB80G0Lr_kGwVJUzjM6fRPP9Yrx6iVZYMVAPTnLKq/pub?gid=0&single=true&output=csv";
export const responsesSheetId = "12pGfvJx0SQmb6mnnVygmZsEeLZ6bFrpZvq8GYw2oX9E";
export const responsesSheetName = "Responses";

View File

@@ -1,31 +1,26 @@
import { fetchCategoryMetadata } from "./metadata";
import { ResponseData } from "./parser";
export function getSampleData(): Promise<ResponseData[]> {
const questions = [
"Service Quality",
"Value for Money",
"Ease of Use",
"Recommendation",
"Overall Satisfaction",
"Customer Support",
"Product Features",
];
function randInt(min: number, max: number): number {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
export async function getSampleData(): Promise<ResponseData[]> {
// Use the actual categories
const questions = (await fetchCategoryMetadata()).map(
(metadata) => metadata.category
);
const sampleData: ResponseData[] = [];
let id = 1;
questions.forEach((question) => {
const numResponses = Math.floor(Math.random() * 50) + 30;
const numResponses = randInt(10, 20);
for (let i = 0; i < numResponses; i++) {
const response = Math.floor(Math.random() * 5);
questions.forEach((question) => {
const response = randInt(0, 4); // Likert scale response (0-4)
sampleData.push({
timestamp: id++,
position: "",
timestamp: i, // Group all responses by the same timestamp to mimic Google Forms behavior
question: question,
response: response,
});
});
}
});
// Simulate a delay to mimic fetching actual data
return new Promise<ResponseData[]>((resolve) => {
setTimeout(() => resolve(sampleData), 500);
});
return Promise.resolve(sampleData);
}

View File

@@ -7,7 +7,7 @@ export interface CategoryMetadata {
}
const sampleMetadataCsv = `title,text,icon
Allgemeines KI-Wissen,Grundlegendes Wissen über Künstliche Intelligenz und deren Anwendung in Organisationen,school
Generelles KI-Wissen,Grundlegendes Wissen über Künstliche Intelligenz und deren Anwendung in Organisationen,school
KI-Innovation,"Fähigkeiten zur Entwicklung, Bewertung und Förderung von KI-Innovationen im Unternehmen",science
KI-Geschäftsstrategie,"Verstehen, wie KI strategisch in Geschäftsmodelle integriert und eingesetzt werden kann",business
Stakeholder-Landschaft,"Fähigkeit, relevante Stakeholder für KI-Initiativen zu identifizieren, einzubinden und zu koordinieren",people_alt
@@ -18,7 +18,7 @@ Python-Programmierung,Grundlegende Programmier-kenntnisse zur Umsetzung und Anpa
Software Design,"Gestaltung robuster, skalierbarer und wartbarer Softwarelösungen mit KI-Komponenten",code
Maschinelles Lernen,Kenntnisse in maschinellem Lernen zur Entwicklung datengetriebener Modelle,model_training
MLOps / Infrastruktur,Fähigkeiten zum produktiven Einsatz und Betrieb von KI-Systemen in Unternehmen,all_inclusive
GenAI-Kenntnisse,Verständnis generativer KI-Modelle (z. B. Large Language Models) und ihrer praktischen Nutzung,auto_awesome`;
Generative KI,Verständnis generativer KI-Modelle (z. B. Large Language Models) und ihrer praktischen Nutzung,auto_awesome`;
export function fetchCategoryMetadata(): Promise<CategoryMetadata[]> {
const parseCsv = (csv: string): CategoryMetadata[] => {

View File

@@ -1,28 +1,20 @@
import * as d3 from "d3";
import { responsesSheetId, responsesSheetName } from "../config";
function mapSkillToNumber(skill: string): number {
const skills: { [key: string]: number } = {
"Gar nicht qualifiziert": 0,
"Leicht qualifiziert": 1,
"Mäßig qualifiziert": 2,
"Sehr qualifiziert": 3,
"Äußerst qualifiziert": 4,
export const skills: { [key: string]: number } = {
"Keine Kenntnisse": 0,
"Geringe Kenntnisse": 1,
"Grundlegende Kenntnisse": 2,
"Gute Kenntnisse": 3,
"Sehr fundierte Kenntnisse": 4,
};
};
function mapSkillToNumber(skill: string): number {
return skills[skill] !== undefined ? skills[skill] : -1;
}
const sheet_id = "12pGfvJx0SQmb6mnnVygmZsEeLZ6bFrpZvq8GYw2oX9E";
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;
}
@@ -40,7 +32,6 @@ export function parseCSV(csv: string): ResponseData[] {
});
return Object.entries(responses).flatMap(([category, response]) => ({
timestamp: new Date(row["Timestamp"]).getTime(),
position: row["Position"],
question: category,
response: response,
}));
@@ -50,6 +41,7 @@ export function parseCSV(csv: string): ResponseData[] {
}
export function fetchGoogleSheet() {
const url = `https://docs.google.com/spreadsheets/d/${responsesSheetId}/gviz/tq?tqx=out:csv&sheet=${responsesSheetName}`;
return fetch(url)
.then((response) => {
if (!response.ok) {

View File

@@ -1,9 +1,11 @@
{
"compilerOptions": {
"composite": true,
"module": "nodenext",
"moduleResolution": "nodenext",
"allowSyntheticDefaultImports": true
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true,
"strict": true
},
"include": ["vite.config.ts"]
}