This commit is contained in:
2025-01-22 12:25:18 +02:00
commit 2a434fa84b
55 changed files with 56968 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
node_modules/
responses.json
.env

55
README.md Normal file
View File

@@ -0,0 +1,55 @@
# local mini-VLAT
This project is a simple web application designed to collect responses from users through a randomized quiz format. The application consists of several pages, including a consent page, a quiz page, a questionnaire, and a results page.
## Project Structure
```
.
├── public
│ ├── index.html # Main entry point for the web application
│ ├── consent.html # Consent page for users
│ ├── quiz.html # Quiz page that loads questions dynamically
│ ├── questionnaire.html # Questionnaire for additional participant information
│ └── results.html # Results page displaying quiz outcomes
├── src
│ ├── js
│ │ ├── app.js # Main JavaScript file for app initialization and routing
│ │ ├── consent.js # JavaScript logic for the consent page
│ │ ├── quiz.js # JavaScript for managing quiz functionality
│ │ ├── questionnaire.js # Logic for the questionnaire
│ │ └── results.js # Processing and displaying quiz results
│ ├── css
│ │ └── styles.css # Styles for the web application
│ └── data
│ └── questions.json # Questions and answers in nested JSON format
├── package.json # npm configuration file
├── server.js # Simple backend server for handling requests
└── README.md # Documentation for the project
```
## Features
- **Consent Page**: Users can read consent information and proceed to the quiz.
- **Randomized Quiz**: Users are randomly assigned one of four quiz versions.
- **Dynamic Question Loading**: Questions are loaded from a JSON file based on the assigned quiz version.
- **Questionnaire**: A follow-up questionnaire collects additional information from participants.
- **Results Display**: Users can view their quiz results, including correct and incorrect answers.
## Setup Instructions
1. Clone the repository to your local machine.
2. Navigate to the project directory.
3. Install the necessary dependencies using npm:
```
npm install
```
4. Start the server:
```
node server.js
```
5. Open your web browser and go to `http://localhost:3000` to access the application.
## Contributing
Feel free to submit issues or pull requests for any improvements or bug fixes.

33
docker-compose.yml Normal file
View File

@@ -0,0 +1,33 @@
version: '3.8'
services:
web:
container_name: mini-vlat-adaptation
image: node:14
working_dir: /app
volumes:
- .:/app
- ./data:/app/data
ports:
- "3000:3000"
environment:
- PORT=3000
env_file:
- .env
command: sh -c "npm install && npm start"
traefik:
image: traefik:v2.5
container_name: traefik
ports:
- "80:80"
- "8080:8080"
command:
- "--api.insecure=true"
- "--providers.docker=true"
- "--entrypoints.web.address=:80"
volumes:
- /var/run/docker.sock:/var/run/docker.sock
labels:
- "traefik.http.routers.web.rule=Host(`${YOUR_DOMAIN}`)"
- "traefik.http.services.web.loadbalancer.server.port=3000"

582
package-lock.json generated Normal file
View File

@@ -0,0 +1,582 @@
{
"name": "simple-web-app",
"version": "1.0.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
"accepts": {
"version": "1.3.8",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
"integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
"requires": {
"mime-types": "~2.1.34",
"negotiator": "0.6.3"
}
},
"array-flatten": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg=="
},
"asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
},
"axios": {
"version": "1.7.9",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.7.9.tgz",
"integrity": "sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw==",
"requires": {
"follow-redirects": "^1.15.6",
"form-data": "^4.0.0",
"proxy-from-env": "^1.1.0"
}
},
"body-parser": {
"version": "1.20.3",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz",
"integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==",
"requires": {
"bytes": "3.1.2",
"content-type": "~1.0.5",
"debug": "2.6.9",
"depd": "2.0.0",
"destroy": "1.2.0",
"http-errors": "2.0.0",
"iconv-lite": "0.4.24",
"on-finished": "2.4.1",
"qs": "6.13.0",
"raw-body": "2.5.2",
"type-is": "~1.6.18",
"unpipe": "1.0.0"
}
},
"bytes": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
"integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="
},
"call-bind-apply-helpers": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.1.tgz",
"integrity": "sha512-BhYE+WDaywFg2TBWYNXAE+8B1ATnThNBqXHP5nQu0jWJdVvY2hvkpyB3qOmtmDePiS5/BDQ8wASEWGMWRG148g==",
"requires": {
"es-errors": "^1.3.0",
"function-bind": "^1.1.2"
}
},
"call-bound": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.3.tgz",
"integrity": "sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA==",
"requires": {
"call-bind-apply-helpers": "^1.0.1",
"get-intrinsic": "^1.2.6"
}
},
"combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"requires": {
"delayed-stream": "~1.0.0"
}
},
"content-disposition": {
"version": "0.5.4",
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
"integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
"requires": {
"safe-buffer": "5.2.1"
}
},
"content-type": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
"integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="
},
"cookie": {
"version": "0.7.1",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz",
"integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w=="
},
"cookie-signature": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
"integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ=="
},
"debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"requires": {
"ms": "2.0.0"
}
},
"delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="
},
"depd": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
"integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="
},
"destroy": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
"integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg=="
},
"dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
"requires": {
"call-bind-apply-helpers": "^1.0.1",
"es-errors": "^1.3.0",
"gopd": "^1.2.0"
}
},
"ee-first": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="
},
"encodeurl": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
"integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="
},
"es-define-property": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="
},
"es-errors": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="
},
"es-object-atoms": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
"requires": {
"es-errors": "^1.3.0"
}
},
"escape-html": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
"integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="
},
"etag": {
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
"integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="
},
"express": {
"version": "4.21.2",
"resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz",
"integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==",
"requires": {
"accepts": "~1.3.8",
"array-flatten": "1.1.1",
"body-parser": "1.20.3",
"content-disposition": "0.5.4",
"content-type": "~1.0.4",
"cookie": "0.7.1",
"cookie-signature": "1.0.6",
"debug": "2.6.9",
"depd": "2.0.0",
"encodeurl": "~2.0.0",
"escape-html": "~1.0.3",
"etag": "~1.8.1",
"finalhandler": "1.3.1",
"fresh": "0.5.2",
"http-errors": "2.0.0",
"merge-descriptors": "1.0.3",
"methods": "~1.1.2",
"on-finished": "2.4.1",
"parseurl": "~1.3.3",
"path-to-regexp": "0.1.12",
"proxy-addr": "~2.0.7",
"qs": "6.13.0",
"range-parser": "~1.2.1",
"safe-buffer": "5.2.1",
"send": "0.19.0",
"serve-static": "1.16.2",
"setprototypeof": "1.2.0",
"statuses": "2.0.1",
"type-is": "~1.6.18",
"utils-merge": "1.0.1",
"vary": "~1.1.2"
}
},
"finalhandler": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz",
"integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==",
"requires": {
"debug": "2.6.9",
"encodeurl": "~2.0.0",
"escape-html": "~1.0.3",
"on-finished": "2.4.1",
"parseurl": "~1.3.3",
"statuses": "2.0.1",
"unpipe": "~1.0.0"
}
},
"follow-redirects": {
"version": "1.15.9",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz",
"integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ=="
},
"form-data": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz",
"integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==",
"requires": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"mime-types": "^2.1.12"
}
},
"forwarded": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
"integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="
},
"fresh": {
"version": "0.5.2",
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
"integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q=="
},
"function-bind": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="
},
"get-intrinsic": {
"version": "1.2.7",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.7.tgz",
"integrity": "sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA==",
"requires": {
"call-bind-apply-helpers": "^1.0.1",
"es-define-property": "^1.0.1",
"es-errors": "^1.3.0",
"es-object-atoms": "^1.0.0",
"function-bind": "^1.1.2",
"get-proto": "^1.0.0",
"gopd": "^1.2.0",
"has-symbols": "^1.1.0",
"hasown": "^2.0.2",
"math-intrinsics": "^1.1.0"
}
},
"get-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
"requires": {
"dunder-proto": "^1.0.1",
"es-object-atoms": "^1.0.0"
}
},
"gopd": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="
},
"has-symbols": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="
},
"hasown": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
"requires": {
"function-bind": "^1.1.2"
}
},
"http-errors": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
"integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
"requires": {
"depd": "2.0.0",
"inherits": "2.0.4",
"setprototypeof": "1.2.0",
"statuses": "2.0.1",
"toidentifier": "1.0.1"
}
},
"iconv-lite": {
"version": "0.4.24",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
"integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
"requires": {
"safer-buffer": ">= 2.1.2 < 3"
}
},
"inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
},
"ipaddr.js": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
"integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="
},
"math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="
},
"media-typer": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
"integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ=="
},
"merge-descriptors": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
"integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ=="
},
"methods": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
"integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w=="
},
"mime": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
"integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="
},
"mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="
},
"mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"requires": {
"mime-db": "1.52.0"
}
},
"ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
},
"negotiator": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
"integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="
},
"object-inspect": {
"version": "1.13.3",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.3.tgz",
"integrity": "sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA=="
},
"on-finished": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
"integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
"requires": {
"ee-first": "1.1.1"
}
},
"parseurl": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
"integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="
},
"path-to-regexp": {
"version": "0.1.12",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
"integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ=="
},
"proxy-addr": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
"integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
"requires": {
"forwarded": "0.2.0",
"ipaddr.js": "1.9.1"
}
},
"proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
},
"qs": {
"version": "6.13.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz",
"integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==",
"requires": {
"side-channel": "^1.0.6"
}
},
"range-parser": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
"integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="
},
"raw-body": {
"version": "2.5.2",
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz",
"integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==",
"requires": {
"bytes": "3.1.2",
"http-errors": "2.0.0",
"iconv-lite": "0.4.24",
"unpipe": "1.0.0"
}
},
"safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="
},
"safer-buffer": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
},
"send": {
"version": "0.19.0",
"resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz",
"integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==",
"requires": {
"debug": "2.6.9",
"depd": "2.0.0",
"destroy": "1.2.0",
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"etag": "~1.8.1",
"fresh": "0.5.2",
"http-errors": "2.0.0",
"mime": "1.6.0",
"ms": "2.1.3",
"on-finished": "2.4.1",
"range-parser": "~1.2.1",
"statuses": "2.0.1"
},
"dependencies": {
"encodeurl": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
"integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w=="
},
"ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
}
}
},
"serve-static": {
"version": "1.16.2",
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz",
"integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==",
"requires": {
"encodeurl": "~2.0.0",
"escape-html": "~1.0.3",
"parseurl": "~1.3.3",
"send": "0.19.0"
}
},
"setprototypeof": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="
},
"side-channel": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
"integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
"requires": {
"es-errors": "^1.3.0",
"object-inspect": "^1.13.3",
"side-channel-list": "^1.0.0",
"side-channel-map": "^1.0.1",
"side-channel-weakmap": "^1.0.2"
}
},
"side-channel-list": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
"integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
"requires": {
"es-errors": "^1.3.0",
"object-inspect": "^1.13.3"
}
},
"side-channel-map": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
"integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
"requires": {
"call-bound": "^1.0.2",
"es-errors": "^1.3.0",
"get-intrinsic": "^1.2.5",
"object-inspect": "^1.13.3"
}
},
"side-channel-weakmap": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
"integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
"requires": {
"call-bound": "^1.0.2",
"es-errors": "^1.3.0",
"get-intrinsic": "^1.2.5",
"object-inspect": "^1.13.3",
"side-channel-map": "^1.0.1"
}
},
"statuses": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
"integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ=="
},
"toidentifier": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
"integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="
},
"type-is": {
"version": "1.6.18",
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
"integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
"requires": {
"media-typer": "0.3.0",
"mime-types": "~2.1.24"
}
},
"unpipe": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
"integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="
},
"utils-merge": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
"integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA=="
},
"vary": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
"integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="
}
}
}

17
package.json Normal file
View File

@@ -0,0 +1,17 @@
{
"name": "simple-web-app",
"version": "1.0.0",
"description": "A simple web app for collecting responses through a quiz and storing results.",
"main": "server.js",
"scripts": {
"start": "node server.js",
"test": "echo \"Error: no test specified\" && exit 1"
},
"dependencies": {
"axios": "^1.7.9",
"body-parser": "^1.19.0",
"express": "^4.17.1"
},
"author": "",
"license": "ISC"
}

18
public/consent.html Normal file
View File

@@ -0,0 +1,18 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Consent Page</title>
<link rel="stylesheet" href="css/styles.css">
</head>
<body>
<div class="container">
<h1>Consent Form</h1>
<p>Please read the following consent information carefully before proceeding:</p>
<p>Your participation in this quiz is voluntary, and you can withdraw at any time. Your responses will be kept confidential.</p>
<button id="consentButton">I Consent to Participate</button>
</div>
<script src="js/consent.js"></script>
</body>
</html>

36
public/index.html Normal file
View File

@@ -0,0 +1,36 @@
<!DOCTYPE html>
<html lang="uk">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Consent Page</title>
<link rel="stylesheet" href="css/styles.css">
<script src="js/app.js" defer></script>
<script src="js/consent.js" defer></script>
</head>
<body>
<div class="container">
<h1>Згода на участь в дослідженні</h1>
<p>
Запрошуємо взяти участь в дослідженні з адаптації інструменту оцінки грамотності в візуалізації даних, яке проводять викладачі та студенти Київської школи економіки.
</p>
<p>
Мета дослідження полягає в адаптації інструменту, який вимірює, наскільки вдало люди можуть розуміти, інтерпретувати та використовувати дані, що містяться в візуалізаціях (графіки, діаграми тощо), для вирішення повсякденних проблем.
</p>
<p>
Вам буде запропоновано 12 різних типів візуалізацій даних. Кожен з них буде супроводжений запитанням, відповідь на яке потрібно дати, засновуючись на інформації що отримана із візуалізації. Мова опитування буде обрана випадковим чином із двох: українська або англійська. Оберіть відповідь, яку вважаєте найточнішою. Якщо ви не впевнені, оберіть “пропустити” замість того щоб вгадувати. На кожне запитання ви маєте 25 секунд для відповіді. Якщо ви не встигли відповісти, тест автоматично перейде до наступного запитання.
</p>
<p>
Після надання відповідей ми попросимо вас також заповнити коротку демографічну анкету. Дані, що ми зберемо протягом опитування, є анонімними та не зможуть бути використані для ідентифікації вашої особи. Вони будуть використані в поточному та подальших дослідженнях грамотності в сфері візуалізації даних. Ці дані можуть бути передані іншим дослідникам/цям, що проводять дослідження в цій або суміжних сферах. Також вони будуть опубліковані в репозиторіях для ширшого доступу наукової спільноти.
</p>
<p>
Якщо ви маєте будь-які питання щодо змісту та деталей дослідження, звʼяжіться з Олегом Омельченком o_omelchenko@kse.org.ua.
</p>
<p>
Дякуємо за допомогу в проведенні дослідження!
</p>
<button id="consentButton">Погоджуюсь з умовами, розпочати тест</button>
</div>
</body>
</html>

101
public/questionnaire.html Normal file
View File

@@ -0,0 +1,101 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Questionnaire</title>
<link rel="stylesheet" href="css/styles.css">
</head>
<body>
<div class="container">
<h1>Анкета учасника(ці)</h1>
<div id="error-message" style="display: none; color: red; margin-bottom: 1rem;"></div>
<form id="questionnaire-form">
<label for="age">Вік:</label>
<input type="number" id="age" name="age" required min="10" max="100">
<label for="gender">Ґендер:</label>
<select id="gender" name="gender" required>
<option value="" selected disabled></option>
<option value="male">Чоловік</option>
<option value="female">Жінка</option>
<option value="other">Небінарна персона / Інше</option>
<option value="prefer_not_to_say">Не бажаю відповідати</option>
</select>
<label for="education">Найвищий здобутий рівень освіти:</label>
<select id="education" name="education" required>
<option value="" selected disabled></option>
<option value="highschool">Середня школа</option>
<option value="bachelors">Бакалавр</option>
<option value="masters">Магістр</option>
<option value="doctorate">Доктор наук</option>
</select>
<label for="color-blind">Чи маєте ви дальтонізм (кольорову сліпоту)?</label>
<select id="color-blind" name="color-blind" required>
<option value="" selected disabled></option>
<option value="yes">Так</option>
<option value="no">Ні</option>
<option value="maybe">Не бажаю відповідати</option>
</select>
<label for="familiarity">Ваш досвід роботи з візуалізацією даних:</label>
<select id="familiarity" name="familiarity" required>
<option value="" selected disabled></option>
<option value="not_familiar">Я ніколи не працював професійно з візуалізацією даних</option>
<option value="somewhat">Я дещо знайомий з візуалізацією даних</option>
<option value="very_familiar">Я сам створював візуалізації даних</option>
</select>
<label for="english-level">Рівень англійської мови:</label>
<select id="english-level" name="english-level" required>
<option value="" selected disabled></option>
<option value="beginner">Початковий</option>
<option value="intermediate">Середній</option>
<option value="advanced">Високий</option>
<option value="native">Рідна мова</option>
</select>
<label for="ukrainian-level">Рівень української мови:</label>
<select id="ukrainian-level" name="ukrainian-level" required>
<option value="" selected disabled></option>
<option value="beginner">Початковий</option>
<option value="intermediate">Середній</option>
<option value="advanced">Високий</option>
<option value="native">Рідна мова</option>
</select>
<label for="visualization-difficulty">Наскільки складно вам було сприймати зміст візуалізацій (графіків):</label>
<select id="visualization-difficulty" name="visualization-difficulty" required>
<option value="" selected disabled></option>
<option value="easy">Легко</option>
<option value="moderate">Помірно</option>
<option value="difficult">Важко</option>
</select>
<label for="quiz-difficulty">Наскільки складно вам було сприймати зміст питань тесту:</label>
<select id="quiz-difficulty" name="quiz-difficulty" required>
<option value="" selected disabled></option>
<option value="easy">Легко</option>
<option value="moderate">Помірно</option>
<option value="difficult">Важко</option>
</select>
<label for="kse-affiliation">Ви студент(ка), викладач(ка)/співробітник(ця) Київської школи економіки?</label>
<select id="kse-affiliation" name="kse-affiliation" required>
<option value="" selected disabled></option>
<option value="neither">Ні</option>
<option value="student">Студент(ка)</option>
<option value="teacher_employee">Викладач(ка)/Співробітник(ця)</option>
</select>
<label for="comments">Додаткові коментарі:</label>
<textarea id="comments" name="comments" rows="4"></textarea>
<button type="submit">Надіслати</button>
</form>
</div>
<script src="js/questionnaire.js"></script>
</body>
</html>

27
public/quiz.html Normal file
View File

@@ -0,0 +1,27 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Quiz</title>
<link rel="stylesheet" href="css/styles.css">
<script src="js/quiz.js" defer></script>
<script src="https://cdn.jsdelivr.net/npm/vega@5"></script>
<script src="https://cdn.jsdelivr.net/npm/vega-lite@5"></script>
<script src="https://cdn.jsdelivr.net/npm/vega-embed@6"></script>
</head>
<body>
<div id="quiz-container">
<div id="timer">Time remaining: <span id="time">25</span>s</div>
<div id="quiz" style="display: flex;">
<div id="chart" style="flex: 1; display: flex; justify-content: center; align-items: center;"></div>
<div style="flex: 1; display: flex; flex-direction: column; justify-content: center; align-items: center;">
<div id="question-text"></div>
<div id="options"></div>
<!-- Removed submit button -->
</div>
</div>
<div id="results"></div>
</div>
</body>
</html>

30
public/results.html Normal file
View File

@@ -0,0 +1,30 @@
<!DOCTYPE html>
<html lang="uk">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Результати тесту</title>
<link rel="stylesheet" href="css/styles.css">
</head>
<body>
<div class="container">
<h1 id="results-header"></h1>
<div class="results">
<table>
<thead>
<tr>
<th>Питання</th>
<th>Тип діаграми</th>
<th>Коректність</th>
</tr>
</thead>
<tbody id="results-table-body">
<!-- Dynamic content will be inserted here -->
</tbody>
</table>
</div>
<button onclick="window.location.href='index.html'">Повернутися на головну</button>
</div>
<script src="js/results.js"></script>
</body>
</html>

55
server.js Normal file
View File

@@ -0,0 +1,55 @@
const express = require('express');
const bodyParser = require('body-parser');
const fs = require('fs');
const path = require('path');
const axios = require('axios');
const app = express();
const PORT = process.env.PORT || 3000;
const WEBHOOK_URL = process.env.WEBHOOK_URL;
app.use(bodyParser.json());
app.use(express.static(path.join(__dirname, 'public')));
app.use(express.static(path.join(__dirname, 'src')));
let responses = [];
// Endpoint to store responses
app.post('/api/responses', (req, res) => {
const response = req.body;
responses.push(response);
fs.writeFileSync('data/responses.json', JSON.stringify(responses, null, 2));
console.log('Received response:', response);
// Redirect payload to webhook
if (WEBHOOK_URL) {
axios.post(WEBHOOK_URL, JSON.stringify(response), {
headers: {
'Content-Type': 'application/json'
}
})
.then(() => {
console.log('Payload sent to webhook successfully');
})
.catch((error) => {
console.error('Error sending payload to webhook:', error);
});
}
res.status(201).json({ success: true, message: 'Response stored successfully' });
});
// Endpoint to retrieve questions
app.get('/api/questions', (req, res) => {
const questionsPath = path.join(__dirname, 'src', 'data', 'questions.json');
fs.readFile(questionsPath, 'utf8', (err, data) => {
if (err) {
return res.status(500).send('Error reading questions');
}
res.json(JSON.parse(data));
});
});
app.listen(PORT, () => {
console.log(`Server is running on http://localhost:${PORT}`);
});

View File

@@ -0,0 +1,160 @@
{
"$schema": "https://vega.github.io/schema/vega-lite/v5.json",
"title": {
"text": "Robusta Coffee Price",
"fontSize": 16,
"fontWeight": "bold"
},
"width": 500,
"height": 300,
"data": {
"values": [
{
"month": "2018-01",
"price": 0.8865
},
{
"month": "2018-02",
"price": 0.8924
},
{
"month": "2018-03",
"price": 0.8818
},
{
"month": "2018-04",
"price": 0.8831
},
{
"month": "2018-05",
"price": 0.8874
},
{
"month": "2018-06",
"price": 0.8607
},
{
"month": "2018-07",
"price": 0.8442
},
{
"month": "2018-08",
"price": 0.8074
},
{
"month": "2018-09",
"price": 0.7670
},
{
"month": "2018-10",
"price": 0.8532
},
{
"month": "2018-11",
"price": 0.8352
},
{
"month": "2018-12",
"price": 0.7757
},
{
"month": "2019-01",
"price": 0.7824
},
{
"month": "2019-02",
"price": 0.7865
},
{
"month": "2019-03",
"price": 0.7696
},
{
"month": "2019-04",
"price": 0.7328
},
{
"month": "2019-05",
"price": 0.7112
},
{
"month": "2019-06",
"price": 0.7402
},
{
"month": "2019-07",
"price": 0.7393
},
{
"month": "2019-08",
"price": 0.7078
},
{
"month": "2019-09",
"price": 0.7064
},
{
"month": "2019-10",
"price": 0.6863
},
{
"month": "2019-11",
"price": 0.7328
},
{
"month": "2019-12",
"price": 0.7322
}
]
},
"mark": {
"type": "area",
"color": "#3182bd",
"opacity": 0.6,
"line": true,
"strokeWidth": 1.5
},
"encoding": {
"x": {
"field": "month",
"type": "temporal",
"title": null,
"axis": {
"grid": true
}
},
"y": {
"field": "price",
"type": "quantitative",
"title": "Coffee Price ($/lb)",
"axis": {
"titleFontWeight": "bold",
"grid": true
}
},
"tooltip": [
{
"field": "month",
"type": "temporal",
"title": "Month"
},
{
"field": "price",
"type": "quantitative",
"title": "Price",
"format": "$.3f"
}
]
},
"config": {
"axis": {
"gridColor": "#ddd",
"gridOpacity": 0.5,
"labelFontSize": 11,
"titleFontSize": 12
},
"view": {
"stroke": null
}
}
}

View File

@@ -0,0 +1,160 @@
{
"$schema": "https://vega.github.io/schema/vega-lite/v5.json",
"title": {
"text": "Ціна кави Робуста",
"fontSize": 16,
"fontWeight": "bold"
},
"width": 500,
"height": 300,
"data": {
"values": [
{
"month": "2018-01",
"price": 0.8865
},
{
"month": "2018-02",
"price": 0.8924
},
{
"month": "2018-03",
"price": 0.8818
},
{
"month": "2018-04",
"price": 0.8831
},
{
"month": "2018-05",
"price": 0.8874
},
{
"month": "2018-06",
"price": 0.8607
},
{
"month": "2018-07",
"price": 0.8442
},
{
"month": "2018-08",
"price": 0.8074
},
{
"month": "2018-09",
"price": 0.7670
},
{
"month": "2018-10",
"price": 0.8532
},
{
"month": "2018-11",
"price": 0.8352
},
{
"month": "2018-12",
"price": 0.7757
},
{
"month": "2019-01",
"price": 0.7824
},
{
"month": "2019-02",
"price": 0.7865
},
{
"month": "2019-03",
"price": 0.7696
},
{
"month": "2019-04",
"price": 0.7328
},
{
"month": "2019-05",
"price": 0.7112
},
{
"month": "2019-06",
"price": 0.7402
},
{
"month": "2019-07",
"price": 0.7393
},
{
"month": "2019-08",
"price": 0.7078
},
{
"month": "2019-09",
"price": 0.7064
},
{
"month": "2019-10",
"price": 0.6863
},
{
"month": "2019-11",
"price": 0.7328
},
{
"month": "2019-12",
"price": 0.7322
}
]
},
"mark": {
"type": "area",
"color": "#3182bd",
"opacity": 0.6,
"line": true,
"strokeWidth": 1.5
},
"encoding": {
"x": {
"field": "month",
"type": "temporal",
"title": null,
"axis": {
"grid": true
}
},
"y": {
"field": "price",
"type": "quantitative",
"title": "Ціна кави ($/фунт)",
"axis": {
"titleFontWeight": "bold",
"grid": true
}
},
"tooltip": [
{
"field": "month",
"type": "temporal",
"title": "Month"
},
{
"field": "price",
"type": "quantitative",
"title": "Price",
"format": "$.3f"
}
]
},
"config": {
"axis": {
"gridColor": "#ddd",
"gridOpacity": 0.5,
"labelFontSize": 11,
"titleFontSize": 12
},
"view": {
"stroke": null
}
}
}

View File

@@ -0,0 +1,37 @@
{
"$schema": "https://vega.github.io/schema/vega-lite/v5.json",
"title": {
"text": "Global Internet Speed (Mbps)",
"fontSize": 16,
"fontWeight": "bold"
},
"data": {
"url": "data/barchart.csv", "format": {"type": "csv"}
},
"width": "container",
"height": "container",
"mark": {
"type": "bar"
},
"encoding": {
"x": {
"field": "Country",
"type": "nominal",
"axis": {
"labelAngle": 40,
"labelAlign": "left",
"title": null,
"labelPadding": 10
}
},
"y": {
"field": "Speed",
"type": "quantitative",
"axis": {
"title": "Internet Speed (Mbps)",
"titleFontWeight": "bold",
"grid": true
}
}
}
}

View File

@@ -0,0 +1,38 @@
{
"$schema": "https://vega.github.io/schema/vega-lite/v5.json",
"title": {
"text": "Світова швидкість інтернету (МБіт/с)",
"fontSize": 16,
"fontWeight": "bold"
},
"data": {
"url": "data/barchart.csv", "format": {"type": "csv"}
},
"width": "container",
"height": "container",
"mark": {
"type": "bar"
},
"encoding": {
"x": {
"field": "Country_UK",
"type": "nominal",
"title": "Країна",
"axis": {
"labelAngle": 40,
"labelAlign": "left",
"title": null,
"labelPadding": 10
}
},
"y": {
"field": "Speed",
"type": "quantitative",
"axis": {
"title": "Швидкість інтернету (МБіт/с)",
"titleFontWeight": "bold",
"grid": true
}
}
}
}

View File

@@ -0,0 +1,82 @@
{
"$schema": "https://vega.github.io/schema/vega-lite/v5.json",
"title": {
"text": "Metro Systems of the World",
"fontSize": 16,
"fontWeight": "bold"
},
"width": 400,
"height": 400,
"data": {
"url": "data/bubblechart.csv"
},
"layer": [
{
"mark": {
"type": "circle",
"opacity": 0.7,
"stroke": "black",
"strokeWidth": 1,
"fill": "#3182bd"
},
"encoding": {
"x": {
"field": "NumberofStations",
"type": "quantitative",
"scale": {"domain": [150, 800]},
"axis": {
"title": "Number of Stations",
"titleFontWeight": "bold",
"grid": true
}
},
"y": {
"field": "Length",
"type": "quantitative",
"scale": {"domain": [150, 500]},
"axis": {
"title": "Total System Length (Km)",
"titleFontWeight": "bold",
"grid": true
}
},
"size": {
"field": "Ridership",
"type": "quantitative",
"scale": {
"domain": [0, 5],
"range": [0, 1500]
},
"legend": {
"title": "Ridership (bn per year)",
"orient": "right",
"symbolType": "circle",
"values": [1.5, 2.5, 3.5]
}
}
}
},
{
"mark": {
"type": "text",
"fontSize": 11,
"dx": 15,
"align": "left"
},
"encoding": {
"x": {"field": "NumberofStations", "type": "quantitative"},
"y": {"field": "Length", "type": "quantitative"},
"text": {"field": "City", "type": "nominal"}
}
}
],
"config": {
"axis": {
"gridColor": "#ddd",
"gridDash": [1, 1],
"labelFontSize": 11,
"titleFontSize": 12
},
"view": {"stroke": null}
}
}

View File

@@ -0,0 +1,82 @@
{
"$schema": "https://vega.github.io/schema/vega-lite/v5.json",
"title": {
"text": "Системи метро світу",
"fontSize": 16,
"fontWeight": "bold"
},
"width": 400,
"height": 400,
"data": {
"url": "data/bubblechart.csv"
},
"layer": [
{
"mark": {
"type": "circle",
"opacity": 0.7,
"stroke": "black",
"strokeWidth": 1,
"fill": "#3182bd"
},
"encoding": {
"x": {
"field": "NumberofStations",
"type": "quantitative",
"scale": {"domain": [150, 800]},
"axis": {
"title": "Кількість станцій",
"titleFontWeight": "bold",
"grid": true
}
},
"y": {
"field": "Length",
"type": "quantitative",
"scale": {"domain": [150, 500]},
"axis": {
"title": "Загальна довжина системи (км)",
"titleFontWeight": "bold",
"grid": true
}
},
"size": {
"field": "Ridership",
"type": "quantitative",
"scale": {
"domain": [0, 5],
"range": [0, 1500]
},
"legend": {
"title": "Пасажиропотік (млрд на рік)",
"orient": "right",
"symbolType": "circle",
"values": [1.5, 2.5, 3.5]
}
}
}
},
{
"mark": {
"type": "text",
"fontSize": 11,
"dx": 15,
"align": "left"
},
"encoding": {
"x": {"field": "NumberofStations", "type": "quantitative"},
"y": {"field": "Length", "type": "quantitative"},
"text": {"field": "CityUK", "type": "nominal"}
}
}
],
"config": {
"axis": {
"gridColor": "#ddd",
"gridDash": [1, 1],
"labelFontSize": 11,
"titleFontSize": 12
},
"view": {"stroke": null}
}
}

View File

@@ -0,0 +1,113 @@
{
"$schema": "https://vega.github.io/schema/vega-lite/v5.json",
"title": {
"text": "Unemployment Rates for States in 2020",
"fontSize": 16,
"fontWeight": "bold"
},
"width": 500,
"height": 500,
"projection": {
"type": "albersUsa"
},
"layer": [
{
"data": {
"url": "data/topo-USA.json",
"format": {
"type": "topojson",
"feature": "states"
}
},
"transform": [
{
"lookup": "id",
"from": {
"data": {
"url": "data/choropleth.csv"
},
"key": "id",
"fields": ["rate"]
}
}
],
"mark": {
"type": "geoshape",
"stroke": "#fff",
"strokeWidth": 0.5
},
"encoding": {
"color": {
"field": "rate",
"type": "quantitative",
"title": "Unemployment Rate (%)",
"scale": {
"scheme": "blues"
},
"legend": {
"title": "Unemployment Rate (%)",
"orient": "top",
"direction": "horizontal"
}
}
}
},
{
"data": {
"url": "data/topo-USA.json",
"format": {
"type": "topojson",
"feature": "states"
}
},
"mark": {
"type": "geoshape",
"filled": false,
"stroke": "#ccc",
"strokeWidth": 0.5
}
},
{
"data": {
"url": "data/choropleth.csv"
},
"transform": [
{
"lookup": "code",
"from": {
"data": {
"url": "data/state-coordinates.json"
},
"key": "code",
"fields": ["latitude", "longitude"]
}
}
],
"mark": {
"type": "text",
"color": "black",
"fontSize": 10,
"fontWeight": "bold"
},
"encoding": {
"longitude": {
"field": "longitude",
"type": "quantitative"
},
"latitude": {
"field": "latitude",
"type": "quantitative"
},
"text": {
"field": "code",
"type": "nominal"
}
}
}
],
"config": {
"view": {
"stroke": null
}
}
}

View File

@@ -0,0 +1,308 @@
{
"$schema": "https://vega.github.io/schema/vega-lite/v5.json",
"height": 300,
"width": 600,
"title": "Кількість шлюбів в першому півріччі 2024 р.",
"config": {
"padding": 0,
"legend": {
"orient": "top",
"padding": 20,
"gradientLength": 300,
"titleFontWeight": "normal",
"titleAlign": "center",
"titleAnchor": "middle",
"titleLimit": 1000
}
},
"layer": [
{
"data": {
"url": "https://raw.githubusercontent.com/org-scn-design-studio-community/sdkcommunitymaps/master/geojson/Europe/Ukraine-regions.json",
"format": {
"feature": "UKR_adm1",
"type": "topojson"
}
},
"mark": {
"type": "geoshape",
"stroke": "white",
"strokeOpacity": 0.3,
"strokeWidth": 0.5
},
"encoding": {
"color": {
"field": "value",
"legend": {
"title": null
},
"scale": {
"scheme": "blues",
"reverse": false
},
"type": "quantitative"
}
},
"projection": {
"type": "mercator"
},
"transform": [
{
"lookup": "properties.ID_1",
"from": {
"data": {
"name": "referendum"
},
"key": "id",
"fields": [
"value"
]
}
}
]
},
{
"data": {
"name": "referendum"
},
"mark": {
"type": "text",
"color": "#111"
},
"encoding": {
"latitude": {
"field": "lat",
"type": "quantitative"
},
"longitude": {
"field": "lon",
"type": "quantitative"
},
"text": {
"field": "short_region",
"type": "ordinal"
}
}
}
],
"datasets": {
"referendum": [
{
"region": "Львівська область",
"short_region": "Льв",
"id": 3150,
"value": 4616,
"lat": 49.8397,
"lon": 24.0297
},
{
"region": "Івано-Франківська область",
"short_region": "ІвФ",
"id": 3142,
"value": 2333,
"lat": 48.7226,
"lon": 24.7105
},
{
"region": "Тернопільська область",
"short_region": "Тер",
"id": 3157,
"value": "1512",
"lat": 49.3535,
"lon": 25.5948
},
{
"region": "м.Київ",
"short_region": "м.К",
"id": 3147,
"value": 9392,
"lat": 50.4501,
"lon": 30.5234
},
{
"region": "Волинська область",
"short_region": "Вол",
"id": 3160,
"value": 1791,
"lat": 51.3424,
"lon": 25
},
{
"region": "Рівненська область",
"short_region": "Рів",
"id": 3154,
"value": 2682,
"lat": 51,
"lon": 26.8
},
{
"region": "Закарпатська область",
"short_region": "Зак",
"id": 3158,
"value": 1859,
"lat": 48.4,
"lon": 23
},
{
"region": "Чернівецька область",
"short_region": "Чнц",
"id": 3138,
"value": 1376,
"lat": 48.2915,
"lon": 25.9403
},
{
"region": "Київська область",
"short_region": "К.о",
"id": 3146,
"value": 4848,
"lat": 49.92,
"lon": 30.57
},
{
"region": "Харківська область",
"short_region": "Хар",
"id": 3143,
"value": 4148,
"lat": 49.5935,
"lon": 36.5304
},
{
"region": "Черкаська область",
"short_region": "Чрк",
"id": 3136,
"value": 3001,
"lat": 49.2,
"lon": 31.4
},
{
"region": "Дніпропетровська область",
"short_region": "Дні",
"id": 3140,
"value": 7076,
"lat": 48.4647,
"lon": 35.0462
},
{
"region": "Сумська область",
"short_region": "Сум",
"id": 3156,
"value": 1670,
"lat": 50.8077,
"lon": 34.4981
},
{
"region": "Полтавська область",
"short_region": "Пол",
"id": 3153,
"value": 3065,
"lat": 49.5883,
"lon": 34.0514
},
{
"region": "Запорізька область",
"short_region": "Зап",
"id": 3161,
"value": 2205,
"lat": 47.1388,
"lon": 35.5396
},
{
"region": "Херсонська область",
"short_region": "Хер",
"id": 3144,
"value": 323,
"lat": 46.6354,
"lon": 33.5169
},
{
"region": "Вінницька область",
"short_region": "Він",
"id": 3159,
"value": 3362,
"lat": 49.0331,
"lon": 28.6682
},
{
"region": "Житомирська область",
"short_region": "Жит",
"id": 3162,
"value": 2717,
"lat": 50.2649,
"lon": 28.6767
},
{
"region": "Хмельницька область",
"short_region": "Хме",
"id": 3145,
"value": 2925,
"lat": 49.4228,
"lon": 26.9871
},
{
"region": "Одеська область",
"short_region": "Оде",
"id": 3152,
"value": 4896,
"lat": 46.9825,
"lon": 30.3233
},
{
"region": "Кіровоградська область",
"short_region": "Кір",
"id": 3148,
"value": 1801,
"lat": 48.5079,
"lon": 32.2623
},
{
"region": "м.Севастополь",
"short_region": "Сев",
"id": 3155,
"value": 0,
"lat": 44.58,
"lon": 33.58
},
{
"region": "Чернігівська область",
"short_region": "Чнг",
"id": 3137,
"value": 1776,
"lat": 51.2982,
"lon": 31.8893
},
{
"region": "Донецька область",
"short_region": "Дон",
"id": 3141,
"value": 933,
"lat": 48.0159,
"lon": 37.8028
},
{
"region": "Миколаївська область",
"short_region": "Мик",
"id": 3151,
"value": 2459,
"lat": 47.275,
"lon": 31.9946
},
{
"region": "Луганська область",
"short_region": "Луг",
"id": 3149,
"value": 0,
"lat": 48.974,
"lon": 39.0078
},
{
"region": "Автономна Республіка Крим",
"short_region": "АРК",
"id": 3139,
"value": 0,
"lat": 45.3657,
"lon": 34.4708
}
]
}
}

View File

@@ -0,0 +1,54 @@
{
"$schema": "https://vega.github.io/schema/vega-lite/v5.json",
"title": {
"text": "Trip Distance and Customers",
"fontSize": 16,
"fontWeight": "bold"
},
"width": 500,
"height": 300,
"data": {
"url": "data/histogram.csv"
},
"mark": {
"type": "bar",
"fill": "#3182bd",
"tooltip": true
},
"encoding": {
"x": {
"field": "Trip_Distance",
"type": "quantitative",
"bin": {
"step": 10
},
"axis": {
"title": "Distance (in Km)",
"titleFontWeight": "bold",
"grid": false
}
},
"y": {
"aggregate": "count",
"axis": {
"title": "Number of Customers",
"titleFontWeight": "bold",
"grid": true
}
}
},
"config": {
"axis": {
"grid": true,
"gridColor": "#ddd",
"labelFontSize": 11,
"titleFontSize": 12
},
"view": {
"stroke": null
},
"bar": {
"binSpacing": 1
}
}
}

View File

@@ -0,0 +1,54 @@
{
"$schema": "https://vega.github.io/schema/vega-lite/v5.json",
"title": {
"text": "Відстань поїздки та клієнти",
"fontSize": 16,
"fontWeight": "bold"
},
"width": 500,
"height": 300,
"data": {
"url": "data/histogram.csv"
},
"mark": {
"type": "bar",
"fill": "#3182bd",
"tooltip": true
},
"encoding": {
"x": {
"field": "Trip_Distance",
"type": "quantitative",
"bin": {
"step": 10
},
"axis": {
"title": "Відстань (км)",
"titleFontWeight": "bold",
"grid": false
}
},
"y": {
"aggregate": "count",
"axis": {
"title": "Кількість клієнтів",
"titleFontWeight": "bold",
"grid": true
}
}
},
"config": {
"axis": {
"grid": true,
"gridColor": "#ddd",
"labelFontSize": 11,
"titleFontSize": 12
},
"view": {
"stroke": null
},
"bar": {
"binSpacing": 1
}
}
}

View File

@@ -0,0 +1,35 @@
{
"$schema": "https://vega.github.io/schema/vega-lite/v5.json",
"width": 600,
"height": 400,
"title": "Oil Prices",
"data": {
"url": "data/linechart.json"
},
"mark": {
"type": "line",
"point": true
},
"encoding": {
"x": {
"field": "date",
"type": "temporal",
"title": "Month",
"axis": {
"format": "%b %Y",
"labelOverlap": false,
"labelAngle": -25,
"labelAlign": "right",
"tickCount": "month"
}
},
"y": {
"field": "value",
"type": "quantitative",
"title": "Oil Price (USD)",
"scale": {
"zero": true
}
}
}
}

View File

@@ -0,0 +1,35 @@
{
"$schema": "https://vega.github.io/schema/vega-lite/v5.json",
"width": 600,
"height": 400,
"title": "Ціни на нафту",
"data": {
"url": "data/linechart.json"
},
"mark": {
"type": "line",
"point": true
},
"encoding": {
"x": {
"field": "date",
"type": "temporal",
"title": "Місяць",
"axis": {
"format": "%b %Y",
"labelOverlap": false,
"labelAngle": -25,
"labelAlign": "right",
"tickCount": "month"
}
},
"y": {
"field": "value",
"type": "quantitative",
"title": "Ціна за барель нафти ($)",
"scale": {
"zero": true
}
}
}
}

View File

@@ -0,0 +1,39 @@
{
"$schema": "https://vega.github.io/schema/vega-lite/v5.json",
"title": {
"text": "Global Smartphone Market Share in 2021",
"fontSize": 16,
"fontWeight": "bold"
},
"config": {
"legend": {
"labelFontSize": 11,
"titleFontSize": 12,
"titleFontWeight": "bold"
}
},
"width": 500, "height": 300,
"data": {
"values": [
{"brand": "Samsung", "share": 25},
{"brand": "Xiaomi", "share": 16},
{"brand": "Apple", "share": 15},
{"brand": "Oppo", "share": 10},
{"brand": "Vivo", "share": 10},
{"brand": "Others", "share": 24}
]
},
"encoding": {
"theta": {"field": "share", "type": "quantitative", "stack": true},
"color": {"field": "brand", "type": "nominal", "legend": null}
},
"layer": [{
"mark": {"type": "arc", "outerRadius": 120}
}, {
"mark": {"type": "text", "radius": 90},
"encoding": {
"text": {"field": "brand", "type": "nominal"},
"color": {"value": "black"}
}
}]
}

View File

@@ -0,0 +1,39 @@
{
"$schema": "https://vega.github.io/schema/vega-lite/v5.json",
"title": {
"text": "Розподіл світового ринку смартфонів у 2021 році",
"fontSize": 16,
"fontWeight": "bold"
},
"config": {
"legend": {
"labelFontSize": 11,
"titleFontSize": 12,
"titleFontWeight": "bold"
}
},
"width": 500, "height": 300,
"data": {
"values": [
{"brand": "Samsung", "share": 25},
{"brand": "Xiaomi", "share": 16},
{"brand": "Apple", "share": 15},
{"brand": "Oppo", "share": 10},
{"brand": "Vivo", "share": 10},
{"brand": "Інші", "share": 24}
]
},
"encoding": {
"theta": {"field": "share", "type": "quantitative", "stack": true},
"color": {"field": "brand", "type": "nominal", "legend": null}
},
"layer": [{
"mark": {"type": "arc", "outerRadius": 120}
}, {
"mark": {"type": "text", "radius": 90},
"encoding": {
"text": {"field": "brand", "type": "nominal"},
"color": {"value": "black"}
}
}]
}

View File

@@ -0,0 +1,64 @@
{
"$schema": "https://vega.github.io/schema/vega-lite/v5.json",
"title": {
"text": "Weight and Height of 85 Individuals",
"fontSize": 16,
"fontWeight": "bold"
},
"width": 400,
"height": 400,
"data": {
"url": "data/scatterplot.csv"
},
"mark": {
"type": "circle",
"size": 40,
"fill": "#3182bd",
"opacity": 1
},
"encoding": {
"x": {
"field": "Height",
"type": "quantitative",
"scale": {
"domain": [155, 190]
},
"axis": {
"title": "Height (cm)",
"titleFontWeight": "bold",
"grid": true,
"gridDash": [1, 1],
"ticks": 5
}
},
"y": {
"field": "Weight",
"type": "quantitative",
"scale": {
"domain": [40, 75]
},
"axis": {
"title": "Weight (kg)",
"titleFontWeight": "bold",
"grid": true,
"gridDash": [1, 1],
"ticks": 8
}
},
"tooltip": [
{"field": "Height", "title": "Height", "format": ".1f"},
{"field": "Weight", "title": "Weight", "format": ".1f"}
]
},
"config": {
"axis": {
"gridColor": "#ddd",
"gridOpacity": 0.5,
"labelFontSize": 11,
"titleFontSize": 12
},
"view": {
"stroke": null
}
}
}

View File

@@ -0,0 +1,64 @@
{
"$schema": "https://vega.github.io/schema/vega-lite/v5.json",
"title": {
"text": "Вага та зріст 85 осіб",
"fontSize": 16,
"fontWeight": "bold"
},
"width": 400,
"height": 400,
"data": {
"url": "data/scatterplot.csv"
},
"mark": {
"type": "circle",
"size": 40,
"fill": "#3182bd",
"opacity": 1
},
"encoding": {
"x": {
"field": "Height",
"type": "quantitative",
"scale": {
"domain": [155, 190]
},
"axis": {
"title": "Зріст (см)",
"titleFontWeight": "bold",
"grid": true,
"gridDash": [1, 1],
"ticks": 5
}
},
"y": {
"field": "Weight",
"type": "quantitative",
"scale": {
"domain": [40, 75]
},
"axis": {
"title": "Вага (кг)",
"titleFontWeight": "bold",
"grid": true,
"gridDash": [1, 1],
"ticks": 8
}
},
"tooltip": [
{"field": "Height", "title": "Зріст", "format": ".1f"},
{"field": "Weight", "title": "Вага", "format": ".1f"}
]
},
"config": {
"axis": {
"gridColor": "#ddd",
"gridOpacity": 0.5,
"labelFontSize": 11,
"titleFontSize": 12
},
"view": {
"stroke": null
}
}
}

View File

@@ -0,0 +1,82 @@
{
"$schema": "https://vega.github.io/schema/vega-lite/v5.json",
"title": {
"text": "Olympic Medal Distribution by Country",
"fontSize": 16,
"fontWeight": "bold"
},
"data": {
"url": "data/stacked100.csv"
},
"width": 500,
"height": 300,
"encoding": {
"y": {
"field": "value",
"type": "quantitative",
"axis": {
"title": "Percentage",
"format": ".0%",
"grid": true
},
"stack": "normalize"
},
"x": {
"field": "Countries",
"type": "nominal",
"axis": {
"title": null,
"labelAngle": 40,
"titleFontWeight": "bold"
}
},
"color": {
"field": "category",
"type": "nominal",
"scale": {
"domain": [
"Gold",
"Silver",
"Bronze"
],
"range": [
"#FFD700",
"#C0C0C0",
"#CD7F32"
]
},
"legend": {
"title": "Medals",
"orient": "right"
}
}
},
"mark": "bar",
"transform": [
{
"fold": [
"Gold",
"Silver",
"Bronze"
],
"as": [
"category",
"value"
]
},
{
"sort": [
{"field": "category", "order": "descending"}
]
}
],
"config": {
"axis": {
"grid": true,
"gridColor": "#ddd"
},
"view": {
"stroke": null
}
}
}

View File

@@ -0,0 +1,86 @@
{
"$schema": "https://vega.github.io/schema/vega-lite/v5.json",
"title": {
"text": "Розподіл олімпійських медалей за країною",
"fontSize": 16,
"fontWeight": "bold"
},
"data": {
"url": "data/stacked100.csv"
},
"width": 500,
"height": 300,
"encoding": {
"y": {
"field": "value",
"type": "quantitative",
"axis": {
"title": "Відсоток",
"format": ".0%",
"grid": true
},
"stack": "normalize"
},
"x": {
"field": "CountriesUK",
"type": "nominal",
"axis": {
"title": null,
"labelAngle": 40,
"titleFontWeight": "bold"
}
},
"color": {
"field": "category",
"type": "nominal",
"scale": {
"domain": [
"Золото",
"Срібло",
"Бронза"
],
"range": [
"#FFD700",
"#C0C0C0",
"#CD7F32"
]
},
"legend": {
"title": "Медалі",
"orient": "right"
}
}
},
"mark": "bar",
"transform": [
{
"fold": [
"Gold",
"Silver",
"Bronze"
],
"as": [
"category",
"value"
]
},
{
"calculate": "datum.category === 'Gold' ? 'Золото' : datum.category === 'Silver' ? 'Срібло' : 'Бронза'",
"as": "category"
},
{
"sort": [
{"field": "category", "order": "descending"}
]
}
],
"config": {
"axis": {
"grid": true,
"gridColor": "#ddd"
},
"view": {
"stroke": null
}
}
}

View File

@@ -0,0 +1,122 @@
{
"$schema": "https://vega.github.io/schema/vega-lite/v5.json",
"title": {
"text": "Popular Girls' names in the UK",
"fontSize": 16,
"fontWeight": "bold"
},
"width": 400,
"height": 400,
"data": {
"values": [
{
"Year": 2009,
"Amelia": 3625,
"Isla": 1908,
"Olivia": 5201
},
{
"Year": 2010,
"Amelia": 4227,
"Isla": 2384,
"Olivia": 5279
},
{
"Year": 2011,
"Amelia": 5054,
"Isla": 2849,
"Olivia": 4938
},
{
"Year": 2012,
"Amelia": 7061,
"Isla": 3501,
"Olivia": 4585
},
{
"Year": 2013,
"Amelia": 5570,
"Isla": 3526,
"Olivia": 4598
},
{
"Year": 2014,
"Amelia": 5327,
"Isla": 4012,
"Olivia": 4724
}
]
},
"encoding": {
"x": {
"field": "Year",
"type": "quantitative",
"axis": {
"title": "Year",
"titleFontWeight": "bold",
"grid": true
}
},
"y": {
"field": "value",
"type": "quantitative",
"axis": {
"title": "Number of Girls",
"titleFontWeight": "bold",
"grid": true
}
},
"color": {
"field": "name",
"type": "nominal",
"scale": {
"domain": [
"Amelia",
"Isla",
"Olivia"
],
"range": [
"#3182bd",
"#9ecae1",
"#deebf7"
]
},
"legend": {
"title": null,
"orient": "right"
}
}
},
"layer": [
{
"mark": {
"type": "area",
"opacity": 0.7
},
"transform": [
{
"fold": [
"Amelia",
"Isla",
"Olivia"
],
"as": [
"name",
"value"
]
}
]
}
],
"config": {
"axis": {
"gridColor": "#ddd",
"gridOpacity": 0.5,
"labelFontSize": 11,
"titleFontSize": 12
},
"view": {
"stroke": null
}
}
}

View File

@@ -0,0 +1,122 @@
{
"$schema": "https://vega.github.io/schema/vega-lite/v5.json",
"title": {
"text": "Популярність виробництва сортів меду",
"fontSize": 16,
"fontWeight": "bold"
},
"width": 400,
"height": 400,
"data": {
"values": [
{
"Year": 2009,
"Липовий": 3625,
"Гречаний": 1908,
"Акацієвий": 5201
},
{
"Year": 2010,
"Липовий": 4227,
"Гречаний": 2384,
"Акацієвий": 5279
},
{
"Year": 2011,
"Липовий": 5054,
"Гречаний": 2849,
"Акацієвий": 4938
},
{
"Year": 2012,
"Липовий": 7061,
"Гречаний": 3501,
"Акацієвий": 4585
},
{
"Year": 2013,
"Липовий": 5570,
"Гречаний": 3526,
"Акацієвий": 4598
},
{
"Year": 2014,
"Липовий": 5327,
"Гречаний": 4012,
"Акацієвий": 4724
}
]
},
"encoding": {
"x": {
"field": "Year",
"type": "quantitative",
"axis": {
"title": "Рік",
"titleFontWeight": "bold",
"grid": true
}
},
"y": {
"field": "value",
"type": "quantitative",
"axis": {
"title": "Кількість тонн",
"titleFontWeight": "bold",
"grid": true
}
},
"color": {
"field": "name",
"type": "nominal",
"scale": {
"domain": [
"Липовий",
"Гречаний",
"Акацієвий"
],
"range": [
"#3182bd",
"#9ecae1",
"#deebf7"
]
},
"legend": {
"title": null,
"orient": "right"
}
}
},
"layer": [
{
"mark": {
"type": "area",
"opacity": 0.7
},
"transform": [
{
"fold": [
"Липовий",
"Гречаний",
"Акацієвий"
],
"as": [
"name",
"value"
]
}
]
}
],
"config": {
"axis": {
"gridColor": "#ddd",
"gridOpacity": 0.5,
"labelFontSize": 11,
"titleFontSize": 12
},
"view": {
"stroke": null
}
}
}

View File

@@ -0,0 +1,86 @@
{
"$schema": "https://vega.github.io/schema/vega-lite/v5.json",
"title": {
"text": "Room Service Prices"
},
"data": {
"url": "data/stackedbarchart.csv"
},
"width": 500,
"height": 300,
"encoding": {
"x": {
"field": "City",
"type": "nominal",
"axis": {
"title": "Cities",
"labelAngle": 40,
"titleFontWeight": "bold"
}
},
"y": {
"field": "value",
"type": "quantitative",
"axis": {
"title": "Cost ($)",
"titleFontWeight": "bold",
"grid": true
},
"scale": {
"domain": [
0,
70
]
}
},
"color": {
"field": "category",
"type": "nominal",
"scale": {
"domain": [
"Sandwich",
"Water",
"Peanut",
"Soda",
"Vodka"
],
"range": [
"#7fc97f",
"#beaed4",
"#fdc086",
"#fb9a99",
"#386cb0"
]
},
"legend": {
"title": null,
"orient": "right"
}
}
},
"mark": "bar",
"transform": [
{
"fold": [
"Sandwich",
"Water",
"Peanut",
"Soda",
"Vodka"
],
"as": [
"category",
"value"
]
}
],
"config": {
"axis": {
"grid": true,
"gridColor": "#ddd"
},
"view": {
"stroke": null
}
}
}

View File

@@ -0,0 +1,87 @@
{
"$schema": "https://vega.github.io/schema/vega-lite/v5.json",
"title": {
"text": "Ціни на обслуговування в номері"
},
"data": {
"url": "data/stackedbarchart.csv"
},
"width": 500,
"height": 300,
"encoding": {
"x": {
"field": "CityUK",
"type": "nominal",
"axis": {
"title": "Місто",
"labelAngle": 40,
"titleFontWeight": "bold"
}
},
"y": {
"field": "value",
"type": "quantitative",
"axis": {
"title": "Ціна ($)",
"titleFontWeight": "bold",
"grid": true
},
"scale": {
"domain": [
0,
70
]
}
},
"color": {
"field": "category",
"type": "nominal",
"scale": {
"domain": [
"Sandwich",
"Water",
"Peanut",
"Soda",
"Vodka"
],
"range": [
"#7fc97f",
"#beaed4",
"#fdc086",
"#fb9a99",
"#386cb0"
]
},
"legend": {
"title": null,
"orient": "right",
"labelExpr": "datum.label == 'Sandwich' ? 'Сендвіч' : datum.label == 'Water' ? 'Вода' : datum.label == 'Peanut' ? 'Арахіс' : datum.label == 'Soda' ? 'Газована вода' : 'Горілка'"
}
}
},
"mark": "bar",
"transform": [
{
"fold": [
"Sandwich",
"Water",
"Peanut",
"Soda",
"Vodka"
],
"as": [
"category",
"value"
]
}
],
"config": {
"axis": {
"grid": true,
"gridColor": "#ddd"
},
"view": {
"stroke": null
}
}
}

View File

@@ -0,0 +1,38 @@
{
"$schema": "https://vega.github.io/schema/vega/v5.json",
"width": 500,
"height": 500,
"marks": [
{
"type": "image",
"encode": {
"enter": {
"url": {
"value": "data/treemap-uk.png"
},
"x": {
"value": 0
},
"y": {
"value": 0
},
"width": {
"value": 500
},
"height": {
"value": 500
},
"aspect": {
"value": true
},
"align": {
"value": "left"
},
"baseline": {
"value": "top"
}
}
}
}
]
}

View File

@@ -0,0 +1,38 @@
{
"$schema": "https://vega.github.io/schema/vega/v5.json",
"width": 500,
"height": 500,
"marks": [
{
"type": "image",
"encode": {
"enter": {
"url": {
"value": "data/treemap-uk.png"
},
"x": {
"value": 0
},
"y": {
"value": 0
},
"width": {
"value": 500
},
"height": {
"value": 500
},
"aspect": {
"value": true
},
"align": {
"value": "left"
},
"baseline": {
"value": "top"
}
}
}
}
]
}

130
src/css/styles.css Normal file
View File

@@ -0,0 +1,130 @@
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 0;
background-color: #ffffff;
}
h1, h2, h3 {
color: #333;
}
.container {
width: 80%;
margin: auto;
overflow: hidden;
}
button {
background-color: #5cb85c;
color: white;
border: none;
padding: 10px 20px;
cursor: pointer;
border-radius: 5px;
}
button:hover {
background-color: #4cae4c;
}
.quiz-question {
margin: 20px 0;
}
.questionnaire {
display: none;
background-color: white;
padding: 20px;
border-radius: 5px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
}
.results {
margin: 20px 0;
padding: 20px;
background-color: white;
border-radius: 5px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
}
form {
display: flex;
flex-direction: column;
}
form label {
margin-top: 10px;
margin-bottom: 5px;
}
form input, form select, form textarea {
margin-bottom: 15px;
padding: 10px;
border: 1px solid #ccc;
border-radius: 5px;
}
form button {
align-self: flex-start;
margin-top: 20px;
}
#quiz {
display: flex;
}
#chart {
flex: 1;
display: flex;
justify-content: center;
align-items: center;
}
#question-text, #options {
margin-bottom: 20px;
}
#options {
display: flex;
flex-direction: column;
align-items: center;
}
.option-button {
margin-bottom: 10px;
width: 100%;
max-width: 300px;
}
#options label {
display: block;
margin-bottom: 10px;
}
#submit {
margin-top: 20px;
}
table {
width: 100%;
border-collapse: collapse;
margin-top: 20px;
}
table, th, td {
border: 1px solid #ddd;
}
th, td {
padding: 10px;
text-align: left;
}
th {
background-color: #f2f2f2;
}
td {
text-align: center;
}

15
src/data/barchart.csv Normal file
View File

@@ -0,0 +1,15 @@
Country,Speed,Country_UK
Australia,79.24,Австралія
China,78.61,Китай
Hong Kong,43.88,Гонконг
India,13.45,Індія
Indonesia,16.16,Індонезія
Japan,40.51,Японія
Malaysia,23.74,Малайзія
New Zealand,92.05,Нова Зеландія
Singapore,68.32,Сінгапур
South Korea,98.93,Південна Корея
Sri Lanka,12.60,Шрі-Ланка
Taiwan,51.67,Тайвань
Thailand,31.38,Таїланд
Vietnam,35.33,В'єтнам
1 Country Speed Country_UK
2 Australia 79.24 Австралія
3 China 78.61 Китай
4 Hong Kong 43.88 Гонконг
5 India 13.45 Індія
6 Indonesia 16.16 Індонезія
7 Japan 40.51 Японія
8 Malaysia 23.74 Малайзія
9 New Zealand 92.05 Нова Зеландія
10 Singapore 68.32 Сінгапур
11 South Korea 98.93 Південна Корея
12 Sri Lanka 12.60 Шрі-Ланка
13 Taiwan 51.67 Тайвань
14 Thailand 31.38 Таїланд
15 Vietnam 35.33 В'єтнам

12
src/data/bubblechart.csv Normal file
View File

@@ -0,0 +1,12 @@
City,Length,NumberofStations,Ridership,CityUK
Delhi,230,348.12,1.7,Делі
Guangzhow,247,531.1,2.4,Гуанчжоу
Tokyo,249,316.3,3.6,Токіо
Mexico City,163,200.9,0.93,Мехіко
Moscow,198,412.1,1.6,Москва
London,317,439.2,0.335,Лондон
Seoul,436,547.9,2.7,Сеул
Paris,304,219.9,2.3,Париж
Beijing,342,727,0.75,Пекін
Shanghai,369,743,2.8,Шанхай
N.Y.C.,458,443.7,0.67,Нью-Йорк
1 City Length NumberofStations Ridership CityUK
2 Delhi 230 348.12 1.7 Делі
3 Guangzhow 247 531.1 2.4 Гуанчжоу
4 Tokyo 249 316.3 3.6 Токіо
5 Mexico City 163 200.9 0.93 Мехіко
6 Moscow 198 412.1 1.6 Москва
7 London 317 439.2 0.335 Лондон
8 Seoul 436 547.9 2.7 Сеул
9 Paris 304 219.9 2.3 Париж
10 Beijing 342 727 0.75 Пекін
11 Shanghai 369 743 2.8 Шанхай
12 N.Y.C. 458 443.7 0.67 Нью-Йорк

52
src/data/choropleth.csv Normal file
View File

@@ -0,0 +1,52 @@
state,rate,code,id
Nebraska,4.2,NE,31
South Dakota,4.6,SD,46
Utah,4.7,UT,49
North Dakota,5.1,ND,38
Iowa,5.3,IA,19
Idaho,5.4,ID,16
Maine,5.4,ME,23
Vermont,5.6,VT,50
Wyoming,5.8,WY,56
Alabama,5.9,AL,01
Kansas,5.9,KS,20
Montana,5.9,MT,30
Arkansas,6.1,AR,05
Missouri,6.1,MO,29
Oklahoma,6.1,OK,40
Minnesota,6.2,MN,27
South Carolina,6.2,SC,45
Virginia,6.2,VA,51
Wisconsin,6.3,WI,55
Georgia,6.5,GA,13
Kentucky,6.6,KY,21
New Hampshire,6.7,NH,33
Maryland,6.8,MD,24
Indiana,7.1,IN,18
Colorado,7.3,CO,08
North Carolina,7.3,NC,37
Tennessee,7.5,TN,47
Oregon,7.6,OR,41
Texas,7.6,TX,48
Florida,7.7,FL,12
Alaska,7.8,AK,02
Delaware,7.8,DE,10
Arizona,7.9,AZ,04
Connecticut,7.9,CT,09
District of Columbia,8,DC,11
Mississippi,8.1,MS,28
Ohio,8.1,OH,39
Louisiana,8.3,LA,22
West Virginia,8.3,WV,54
New Mexico,8.4,NM,35
Washington,8.4,WA,53
Massachusetts,8.9,MA,25
Pennsylvania,9.1,PN,42
Rhode Island,9.4,RI,44
Illinois,9.5,IL,17
New Jersey,9.8,NJ,34
Michigan,9.9,MI,26
New York,10,NY,36
California,10.1,CA,06
Hawaii,11.6,HI,15
Nevada,12.8,NV,32
1 state rate code id
2 Nebraska 4.2 NE 31
3 South Dakota 4.6 SD 46
4 Utah 4.7 UT 49
5 North Dakota 5.1 ND 38
6 Iowa 5.3 IA 19
7 Idaho 5.4 ID 16
8 Maine 5.4 ME 23
9 Vermont 5.6 VT 50
10 Wyoming 5.8 WY 56
11 Alabama 5.9 AL 01
12 Kansas 5.9 KS 20
13 Montana 5.9 MT 30
14 Arkansas 6.1 AR 05
15 Missouri 6.1 MO 29
16 Oklahoma 6.1 OK 40
17 Minnesota 6.2 MN 27
18 South Carolina 6.2 SC 45
19 Virginia 6.2 VA 51
20 Wisconsin 6.3 WI 55
21 Georgia 6.5 GA 13
22 Kentucky 6.6 KY 21
23 New Hampshire 6.7 NH 33
24 Maryland 6.8 MD 24
25 Indiana 7.1 IN 18
26 Colorado 7.3 CO 08
27 North Carolina 7.3 NC 37
28 Tennessee 7.5 TN 47
29 Oregon 7.6 OR 41
30 Texas 7.6 TX 48
31 Florida 7.7 FL 12
32 Alaska 7.8 AK 02
33 Delaware 7.8 DE 10
34 Arizona 7.9 AZ 04
35 Connecticut 7.9 CT 09
36 District of Columbia 8 DC 11
37 Mississippi 8.1 MS 28
38 Ohio 8.1 OH 39
39 Louisiana 8.3 LA 22
40 West Virginia 8.3 WV 54
41 New Mexico 8.4 NM 35
42 Washington 8.4 WA 53
43 Massachusetts 8.9 MA 25
44 Pennsylvania 9.1 PN 42
45 Rhode Island 9.4 RI 44
46 Illinois 9.5 IL 17
47 New Jersey 9.8 NJ 34
48 Michigan 9.9 MI 26
49 New York 10 NY 36
50 California 10.1 CA 06
51 Hawaii 11.6 HI 15
52 Nevada 12.8 NV 32

1374
src/data/histogram.csv Normal file

File diff suppressed because it is too large Load Diff

50
src/data/linechart.json Normal file
View File

@@ -0,0 +1,50 @@
[
{
"date": "2020-01-01",
"value": 57.52
},
{
"date": "2020-02-01",
"value": 50.54
},
{
"date": "2020-03-01",
"value": 29.21
},
{
"date": "2020-04-01",
"value": 16.55
},
{
"date": "2020-05-01",
"value": 28.56
},
{
"date": "2020-06-01",
"value": 38.31
},
{
"date": "2020-07-01",
"value": 40.71
},
{
"date": "2020-08-01",
"value": 42.34
},
{
"date": "2020-09-01",
"value": 39.63
},
{
"date": "2020-10-01",
"value": 39.40
},
{
"date": "2020-11-01",
"value": 40.94
},
{
"date": "2020-12-01",
"value": 47.02
}
]

7
src/data/piechart.csv Normal file
View File

@@ -0,0 +1,7 @@
Brand,Share
Samsung,25
Xiaomi,16
Apple,15
Oppo,10
Vivo,10
Others,24
1 Brand Share
2 Samsung 25
3 Xiaomi 16
4 Apple 15
5 Oppo 10
6 Vivo 10
7 Others 24

312
src/data/questions.json Normal file
View File

@@ -0,0 +1,312 @@
{
"quizzes": {
"original": {
"questions": [
{
"chart": "linechart",
"chart_uk": "лінійний графік",
"question": "What was the price of a barrel of oil in February 2020?",
"options": [
"$50.54",
"$47.02",
"$42.34",
"$42.34",
"Skip"
],
"answer": "$50.54"
},
{
"chart": "barchart",
"chart_uk": "стовпчаста діаграма",
"question": "What is the average internet speed in Japan?",
"options": [
"42.30 Mbps",
"40.51 Mbps",
"35.25 Mbps",
"16.16 Mbps",
"Skip"
],
"answer": "40.51 Mbps"
},
{
"chart": "stackedbarchart",
"chart_uk": "накопичувана стовпчаста діаграма",
"question": "What is the cost of peanuts in Seoul?",
"options": [
"$6.1",
"$5.2",
"$7.5",
"$4.5",
"Skip"
],
"answer": "$6.1"
},
{
"chart": "stacked100",
"chart_uk": "накопичувана стовпчаста діаграма 100%",
"question": "Which country has the lowest proportion of Gold medals?",
"options": [
"Great Britain",
"U.S.A.",
"Japan",
"Australia",
"Skip"
],
"answer": "Great Britain"
},
{
"chart": "piechart",
"chart_uk": "кругова діаграма",
"question": "What is the approximate global smartphone market share of Samsung?",
"options": [
"17.6%",
"25.3%",
"10.9%",
"35.2%",
"Skip"
],
"answer": "17.6%"
},
{
"chart": "histogram",
"chart_uk": "гістограма",
"question": "What distance have customers traveled in the taxi the most?",
"options": [
"60 - 70 Km",
"30 - 40 Km",
"20 - 30 Km",
"50 - 60 Km",
"Skip"
],
"answer": "30 - 40 Km"
},
{
"chart": "scatterplot",
"chart_uk": "точкова діаграма",
"question": "There is a negative linear relationship between the height and the weight of the 85 males.",
"options": [
"True",
"False",
"Skip"
],
"answer": "False"
},
{
"chart": "areachart",
"chart_uk": "площева діаграма",
"question": "What was the average price of a pound of coffee beans in October 2019?",
"options": [
"$0.71",
"$0.90",
"$0.80",
"$0.63",
"Skip"
],
"answer": "$0.71"
},
{
"chart": "stackedareachart",
"chart_uk": "накопичувана площева діаграма",
"question": "What was the ratio of girls named 'Isla' to girls named 'Amelia' in 2012 in the UK?",
"options": [
"1 to 1",
"1 to 2",
"1 to 3",
"1 to 4",
"Skip"
],
"answer": "1 to 2"
},
{
"chart": "bubblechart",
"chart_uk": "бульбашкова діаграма",
"question": "Which city's metro system has the largest number of stations?",
"options": [
"Beijing",
"Shanghai",
"London",
"Seoul",
"Skip"
],
"answer": "Shanghai"
},
{
"chart": "choropleth",
"chart_uk": "хороплетна карта",
"question": "In 2020, the unemployment rate for Washington (WA) was higher than that of Wisconsin (WI).",
"options": [
"True",
"False",
"Skip"
],
"answer": "True"
},
{
"chart": "treemap",
"chart_uk": "деревоподібна діаграма",
"question": "eBay is nested in the Software category.",
"options": [
"True",
"False",
"Skip"
],
"answer": "False"
}
]
},
"ukrainian": {
"questions": [
{
"chart": "linechart",
"chart_uk": "лінійний графік",
"question": "Яка була ціна за барель нафти у лютому 2020 року?",
"options": [
"$50.54",
"$47.02",
"$42.34",
"$42.34",
"Пропустити"
],
"answer": "$50.54"
},
{
"chart": "barchart",
"chart_uk": "стовпчаста діаграма",
"question": "Яка середня швидкість інтернету в Японії?",
"options": [
"42.30 Мбіт/с",
"40.51 Мбіт/с",
"35.25 Мбіт/с",
"16.16 Мбіт/с",
"Пропустити"
],
"answer": "40.51 Мбіт/с"
},
{
"chart": "stackedbarchart",
"chart_uk": "накопичувана стовпчаста діаграма",
"question": "Яка вартість арахісу в Сеулі?",
"options": [
"$6.1",
"$5.2",
"$7.5",
"$4.5",
"Пропустити"
],
"answer": "$6.1"
},
{
"chart": "stacked100",
"chart_uk": "накопичувана стовпчаста діаграма 100%",
"question": "Яка країна має найменшу частку золотих медалей?",
"options": [
"Велика Британія",
"США",
"Японія",
"Австралія",
"Пропустити"
],
"answer": "Велика Британія"
},
{
"chart": "piechart",
"chart_uk": "кругова діаграма",
"question": "Яка приблизна глобальна частка ринку смартфонів Samsung?",
"options": [
"17.6%",
"25.3%",
"10.9%",
"35.2%",
"Пропустити"
],
"answer": "17.6%"
},
{
"chart": "histogram",
"chart_uk": "гістограма",
"question": "Яку відстань клієнти найчастіше подорожували на таксі?",
"options": [
"60 - 70 км",
"30 - 40 км",
"20 - 30 км",
"50 - 60 км",
"Пропустити"
],
"answer": "30 - 40 км"
},
{
"chart": "scatterplot",
"chart_uk": "точкова діаграма",
"question": "Існує негативний лінійний зв'язок між зростом та вагою 85 чоловіків.",
"options": [
"Правда",
"Неправда",
"Пропустити"
],
"answer": "Неправда"
},
{
"chart": "areachart",
"chart_uk": "площева діаграма",
"question": "Яка була середня ціна фунта кавових зерен у жовтні 2019 року?",
"options": [
"$0.71",
"$0.90",
"$0.80",
"$0.63",
"Пропустити"
],
"answer": "$0.71"
},
{
"chart": "stackedareachart",
"chart_uk": "накопичувана площева діаграма",
"question": "Яке було співвідношення гречаного меду до липового у 2012 році?",
"options": [
"1 до 1",
"1 до 2",
"1 до 3",
"1 до 4",
"Пропустити"
],
"answer": "1 до 2"
},
{
"chart": "bubblechart",
"chart_uk": "бульбашкова діаграма",
"question": "Метро якого міста має найбільшу кількість станцій?",
"options": [
"Пекін",
"Шанхай",
"Лондон",
"Сеул",
"Пропустити"
],
"answer": "Шанхай"
},
{
"chart": "choropleth",
"chart_uk": "хороплетна карта",
"question": "У першому півріччі 2024 р. кількість шлюбів в Харківській області була більшою, ніж у Волинській області.",
"options": [
"Правда",
"Неправда",
"Пропустити"
],
"answer": "Правда"
},
{
"chart": "treemap",
"chart_uk": "деревоподібна діаграма",
"question": "eBay вкладено в категорію \"програмне забезпечення\".",
"options": [
"Правда",
"Неправда",
"Пропустити"
],
"answer": "Неправда"
}
]
}
}
}

86
src/data/scatterplot.csv Normal file
View File

@@ -0,0 +1,86 @@
Height,Weight
167.0812,51.25136008
181.6608,61.91077208
176.276,69.41318376
173.2788,64.56428528
172.1866,65.4533256
174.498,55.9278936
177.292,64.17873208
177.8254,61.89716432
172.466,50.97013304
169.6212,54.73494664
168.8846,57.8103004
171.7548,51.77299088
173.482,56.97569112
170.4848,55.54687632
173.4312,52.65749528
180.5686,63.50288
168.8084,58.740164
174.371,64.85004824
180.9242,62.5503368
170.5102,56.26355168
172.2882,64.08347776
174.9552,65.10859568
161.2392,44.4066568
173.7868,58.740164
171.7802,64.3420252
170.7134,58.83995424
179.9336,64.60057264
171.4246,59.6700276
168.9862,49.13762136
166.2176,51.65959288
176.5808,46.8560536
167.1574,54.771234
172.2628,57.05733768
179.324,61.78830224
182.372,63.5482392
175.7934,58.39997
169.672,64.3193456
171.8564,54.98895816
172.2374,59.5793092
162.687,48.40280232
174.1678,56.40870112
165.5572,56.63549712
176.9364,63.35319464
172.6438,62.30993304
167.5892,48.2848684
174.4218,58.40450592
169.8752,66.07928256
171.958,52.98861744
177.3428,65.14488304
175.4886,61.20316856
177.5714,66.68709584
171.0182,57.30227736
178.4858,56.91672416
175.514,52.48513032
166.0652,56.01407608
178.2572,67.08172088
178.8414,70.7149928
169.0116,58.09152744
168.5544,54.14527704
171.5516,60.69514552
168.91,58.39089816
175.26,62.3915796
173.482,58.85809792
170.2054,58.43172144
179.8574,61.38006944
173.2788,49.71821912
175.4124,64.62325224
172.0342,60.214338
170.7388,73.96037976
171.1198,56.57653016
165.7858,58.65398152
179.9336,60.79039984
177.5968,63.6843168
163.2966,46.64740128
173.355,58.29564384
168.5544,54.5671176
173.6344,62.8678512
166.3192,60.30959232
177.0888,52.44430704
172.0342,55.57409184
174.3456,61.06709096
169.6212,55.2928648
177.927,70.47912496
168.3512,58.48615248
175.768,58.5587272
1 Height Weight
2 167.0812 51.25136008
3 181.6608 61.91077208
4 176.276 69.41318376
5 173.2788 64.56428528
6 172.1866 65.4533256
7 174.498 55.9278936
8 177.292 64.17873208
9 177.8254 61.89716432
10 172.466 50.97013304
11 169.6212 54.73494664
12 168.8846 57.8103004
13 171.7548 51.77299088
14 173.482 56.97569112
15 170.4848 55.54687632
16 173.4312 52.65749528
17 180.5686 63.50288
18 168.8084 58.740164
19 174.371 64.85004824
20 180.9242 62.5503368
21 170.5102 56.26355168
22 172.2882 64.08347776
23 174.9552 65.10859568
24 161.2392 44.4066568
25 173.7868 58.740164
26 171.7802 64.3420252
27 170.7134 58.83995424
28 179.9336 64.60057264
29 171.4246 59.6700276
30 168.9862 49.13762136
31 166.2176 51.65959288
32 176.5808 46.8560536
33 167.1574 54.771234
34 172.2628 57.05733768
35 179.324 61.78830224
36 182.372 63.5482392
37 175.7934 58.39997
38 169.672 64.3193456
39 171.8564 54.98895816
40 172.2374 59.5793092
41 162.687 48.40280232
42 174.1678 56.40870112
43 165.5572 56.63549712
44 176.9364 63.35319464
45 172.6438 62.30993304
46 167.5892 48.2848684
47 174.4218 58.40450592
48 169.8752 66.07928256
49 171.958 52.98861744
50 177.3428 65.14488304
51 175.4886 61.20316856
52 177.5714 66.68709584
53 171.0182 57.30227736
54 178.4858 56.91672416
55 175.514 52.48513032
56 166.0652 56.01407608
57 178.2572 67.08172088
58 178.8414 70.7149928
59 169.0116 58.09152744
60 168.5544 54.14527704
61 171.5516 60.69514552
62 168.91 58.39089816
63 175.26 62.3915796
64 173.482 58.85809792
65 170.2054 58.43172144
66 179.8574 61.38006944
67 173.2788 49.71821912
68 175.4124 64.62325224
69 172.0342 60.214338
70 170.7388 73.96037976
71 171.1198 56.57653016
72 165.7858 58.65398152
73 179.9336 60.79039984
74 177.5968 63.6843168
75 163.2966 46.64740128
76 173.355 58.29564384
77 168.5544 54.5671176
78 173.6344 62.8678512
79 166.3192 60.30959232
80 177.0888 52.44430704
81 172.0342 55.57409184
82 174.3456 61.06709096
83 169.6212 55.2928648
84 177.927 70.47912496
85 168.3512 58.48615248
86 175.768 58.5587272

5
src/data/stacked100.csv Normal file
View File

@@ -0,0 +1,5 @@
Countries,CountriesUK,Gold,Silver,Bronze
U.S.A.,США,34.5,36.3,29.2
Great Britain,Велика Британія,33.8,32.3,33.9
Japan,Японія,46.6,24.1,29.3
Australia,Австралія,37,15.2,47.8
1 Countries CountriesUK Gold Silver Bronze
2 U.S.A. США 34.5 36.3 29.2
3 Great Britain Велика Британія 33.8 32.3 33.9
4 Japan Японія 46.6 24.1 29.3
5 Australia Австралія 37 15.2 47.8

View File

@@ -0,0 +1,11 @@
City,CityUK,Sandwich,Water,Peanut,Soda,Vodka
Helsinki,Гельсінкі,38.2,6.4,12.1,6.4,5.5
Oslo,Осло,29.4,7.4,15.8,7.1,5.1
Seoul,Сеул,27.4,7.4,6.1,7.9,9.9
Zurich,Цюрих,31.3,5.9,10.8,5.6,4.3
Stockholm,Стокгольм,28.4,4.1,11.3,4.9,5
Paris,Париж,23.6,6.8,12.6,6.8,7.0
N.Y.C.,Нью-Йорк,24.2,3.9,16.8,3.9,7.1
Singapore,Сінгапур,19.4,5.2,14.8,6.6,6.2
Toronto,Торонто,20.8,5.4,18.2,3.4,8.0
Copenhagen,Копенгаген,24.3,2.8,10.8,5.0,3.7
1 City CityUK Sandwich Water Peanut Soda Vodka
2 Helsinki Гельсінкі 38.2 6.4 12.1 6.4 5.5
3 Oslo Осло 29.4 7.4 15.8 7.1 5.1
4 Seoul Сеул 27.4 7.4 6.1 7.9 9.9
5 Zurich Цюрих 31.3 5.9 10.8 5.6 4.3
6 Stockholm Стокгольм 28.4 4.1 11.3 4.9 5
7 Paris Париж 23.6 6.8 12.6 6.8 7.0
8 N.Y.C. Нью-Йорк 24.2 3.9 16.8 3.9 7.1
9 Singapore Сінгапур 19.4 5.2 14.8 6.6 6.2
10 Toronto Торонто 20.8 5.4 18.2 3.4 8.0
11 Copenhagen Копенгаген 24.3 2.8 10.8 5.0 3.7

View File

@@ -0,0 +1,53 @@
[
{ "code": "NE", "latitude": 41.5, "longitude": -99.68 },
{ "code": "SD", "latitude": 44.5, "longitude": -100.23 },
{ "code": "UT", "latitude": 39.32, "longitude": -111.09 },
{ "code": "ND", "latitude": 47.5, "longitude": -100.47 },
{ "code": "IA", "latitude": 42.07, "longitude": -93.5 },
{ "code": "ID", "latitude": 44.24, "longitude": -114.47 },
{ "code": "ME", "latitude": 45.37, "longitude": -69.24 },
{ "code": "VT", "latitude": 44.07, "longitude": -72.67 },
{ "code": "WY", "latitude": 43.07, "longitude": -107.29 },
{ "code": "AL", "latitude": 32.8, "longitude": -86.79 },
{ "code": "KS", "latitude": 38.5, "longitude": -98.38 },
{ "code": "MT", "latitude": 46.92, "longitude": -110.45 },
{ "code": "AR", "latitude": 34.75, "longitude": -92.29 },
{ "code": "MO", "latitude": 38.57, "longitude": -92.6 },
{ "code": "OK", "latitude": 35.47, "longitude": -97.52 },
{ "code": "MN", "latitude": 46.39, "longitude": -94.64 },
{ "code": "SC", "latitude": 33.84, "longitude": -81.16 },
{ "code": "VA", "latitude": 37.77, "longitude": -78.17 },
{ "code": "WI", "latitude": 44.5, "longitude": -89.5 },
{ "code": "GA", "latitude": 32.64, "longitude": -83.44 },
{ "code": "KY", "latitude": 37.53, "longitude": -85.3 },
{ "code": "NH", "latitude": 43.68, "longitude": -71.58 },
{ "code": "MD", "latitude": 39.05, "longitude": -76.64 },
{ "code": "IN", "latitude": 40.27, "longitude": -86.13 },
{ "code": "CO", "latitude": 39.11, "longitude": -105.36 },
{ "code": "NC", "latitude": 35.78, "longitude": -80.79 },
{ "code": "TN", "latitude": 35.86, "longitude": -86.66 },
{ "code": "OR", "latitude": 43.93, "longitude": -120.55 },
{ "code": "TX", "latitude": 31.47, "longitude": -99.33 },
{ "code": "FL", "latitude": 27.99, "longitude": -81.76 },
{ "code": "AK", "latitude": 64.2, "longitude": -149.49 },
{ "code": "DE", "latitude": 38.99, "longitude": -75.51 },
{ "code": "AZ", "latitude": 34.05, "longitude": -111.09 },
{ "code": "CT", "latitude": 41.6, "longitude": -72.76 },
{ "code": "DC", "latitude": 38.91, "longitude": -77.01 },
{ "code": "MS", "latitude": 32.74, "longitude": -89.67 },
{ "code": "OH", "latitude": 40.19, "longitude": -82.67 },
{ "code": "LA", "latitude": 30.98, "longitude": -91.96 },
{ "code": "WV", "latitude": 38.64, "longitude": -80.62 },
{ "code": "NM", "latitude": 34.52, "longitude": -106.1 },
{ "code": "WA", "latitude": 47.39, "longitude": -121.49 },
{ "code": "MA", "latitude": 42.24, "longitude": -71.53 },
{ "code": "PA", "latitude": 40.88, "longitude": -77.79 },
{ "code": "RI", "latitude": 41.68, "longitude": -71.51 },
{ "code": "IL", "latitude": 40.34, "longitude": -89.0 },
{ "code": "NJ", "latitude": 40.14, "longitude": -74.67 },
{ "code": "MI", "latitude": 44.18, "longitude": -84.51 },
{ "code": "NY", "latitude": 42.95, "longitude": -75.53 },
{ "code": "CA", "latitude": 37.25, "longitude": -119.61 },
{ "code": "HI", "latitude": 20.79, "longitude": -156.33 },
{ "code": "NV", "latitude": 38.5, "longitude": -117.02 }
]

51502
src/data/topo-USA.json Normal file

File diff suppressed because it is too large Load Diff

BIN
src/data/treemap-en.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

BIN
src/data/treemap-uk.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

55
src/js/app.js Normal file
View File

@@ -0,0 +1,55 @@
// src/js/app.js
document.addEventListener('DOMContentLoaded', function() {
const currentPage = window.location.pathname.split('/').pop();
if (currentPage === 'consent.html') {
initConsentPage();
} else if (currentPage === 'quiz.html') {
initQuizPage();
} else if (currentPage === 'questionnaire.html') {
initQuestionnairePage();
} else if (currentPage === 'results.html') {
initResultsPage();
}
});
function initConsentPage() {
const consentButton = document.getElementById('consent-button');
consentButton.addEventListener('click', function() {
fetch('../data/questions.json')
.then(response => response.json())
.then(data => {
const quizVersions = Object.keys(data.quizzes);
const randomVersion = quizVersions[Math.floor(Math.random() * quizVersions.length)];
localStorage.setItem('quizVersion', randomVersion);
window.location.href = 'quiz.html';
});
});
}
function initQuizPage() {
const version = localStorage.getItem('quizVersion');
loadQuestions(version);
}
function initQuestionnairePage() {
// Logic for initializing the questionnaire page
}
function initResultsPage() {
// Logic for displaying results
}
function loadQuestions(version) {
fetch('../data/questions.json')
.then(response => response.json())
.then(data => {
const questions = data.quizzes[version];
displayQuestions(questions);
});
}
function displayQuestions(questions) {
// Logic to display questions on the quiz page
}

14
src/js/consent.js Normal file
View File

@@ -0,0 +1,14 @@
document.addEventListener('DOMContentLoaded', function() {
const consentButton = document.getElementById('consentButton');
consentButton.addEventListener('click', function() {
fetch('../data/questions.json')
.then(response => response.json())
.then(data => {
const quizVersions = Object.keys(data.quizzes);
const randomVersion = quizVersions[Math.floor(Math.random() * quizVersions.length)];
localStorage.setItem('quizVersion', randomVersion);
window.location.href = 'quiz.html';
});
});
});

79
src/js/questionnaire.js Normal file
View File

@@ -0,0 +1,79 @@
// This file handles the logic for the questionnaire, including collecting additional participant information after the quiz.
document.addEventListener('DOMContentLoaded', function() {
const questionnaireForm = document.getElementById('questionnaire-form');
questionnaireForm.addEventListener('submit', function(event) {
event.preventDefault();
// Form validation
const age = document.getElementById('age').value;
if (age < 0 || age > 120) {
showError('Please enter a valid age between 0 and 120');
return;
}
const participantData = {
age: age,
gender: document.getElementById('gender').value,
education: document.getElementById('education').value,
colorBlind: document.getElementById('color-blind').value,
familiarity: document.getElementById('familiarity').value,
englishLevel: document.getElementById('english-level').value,
ukrainianLevel: document.getElementById('ukrainian-level').value,
visualizationDifficulty: document.getElementById('visualization-difficulty').value,
quizDifficulty: document.getElementById('quiz-difficulty').value,
kseAffiliation: document.getElementById('kse-affiliation').value,
comments: document.getElementById('comments').value
};
// Disable form while submitting
const submitButton = document.querySelector('button[type="submit"]');
submitButton.disabled = true;
// Store participant data in local storage
localStorage.setItem('participantData', JSON.stringify(participantData));
// Retrieve existing quiz data
const allQuizzes = JSON.parse(localStorage.getItem('allQuizzes') || '{}');
const quizId = 'quiz';
if (allQuizzes[quizId]) {
allQuizzes[quizId].participantData = participantData;
localStorage.setItem('allQuizzes', JSON.stringify(allQuizzes));
}
// Send data to the backend
fetch('/api/responses', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(allQuizzes)
})
.then(response => {
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.json();
})
.then(data => {
if (data.success) {
console.log('Success:', data);
window.location.href = 'results.html';
} else {
throw new Error(data.message || 'Failed to submit the form');
}
})
.catch((error) => {
console.error('Error:', error);
showError('Failed to submit the form. Please try again.');
submitButton.disabled = false;
});
});
function showError(message) {
const errorDiv = document.getElementById('error-message');
errorDiv.textContent = message;
errorDiv.style.display = 'block';
}
});

199
src/js/quiz.js Normal file
View File

@@ -0,0 +1,199 @@
// This file manages the quiz functionality, including loading questions from the JSON file, displaying them to the user, and collecting answers.
document.addEventListener('DOMContentLoaded', function () {
const quizContainer = document.getElementById('quiz');
const resultsContainer = document.getElementById('results');
const timerDisplay = document.getElementById('time');
const version = localStorage.getItem('quizVersion');
const quizId = 'quiz';
let questions = [];
let currentQuestionIndex = 0;
let score = 0;
let quizResults = [];
let timeLeft = 25;
let timer;
let startTime = new Date().toISOString();
// Generate and store browser ID
function generateBrowserId() {
return 'xxxx-xxxx-4xxx-yxxx-xxxx-xxxx-xxxx-xxxx'.replace(/[xy]/g, function(c) {
const r = Math.random() * 16 | 0, v = c === 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16);
});
}
const browserId = localStorage.getItem('browserId') || generateBrowserId();
localStorage.setItem('browserId', browserId);
const browserInfo = {
userAgent: navigator.userAgent,
screenResolution: `${window.screen.width}x${window.screen.height}`,
operatingSystem: navigator.platform
};
// Check if there is a completed quiz
const allQuizzes = JSON.parse(localStorage.getItem('allQuizzes') || '{}');
if (allQuizzes[quizId] && allQuizzes[quizId].completed) {
alert("You have already completed the quiz. Redirecting to the questionnaire.");
window.location.href = 'index.html';
return;
}
fetch('data/questions.json')
.then(response => response.json())
.then(data => {
if (!data.quizzes || !data.quizzes[version] || !data.quizzes[version].questions) {
throw new Error('Invalid quiz data or version');
}
questions = data.quizzes[version].questions;
displayQuestion();
})
.catch(error => {
console.error('Error loading quiz:', error);
quizContainer.innerHTML = '<p>Error loading quiz. Please try again later.</p>';
});
function displayQuestion() {
if (!questions || currentQuestionIndex >= questions.length) {
showResults();
return;
}
// Complete timer reset
stopTimer();
timeLeft = 25;
timerDisplay.textContent = timeLeft;
const question = questions[currentQuestionIndex];
const chartFile = `charts/${question.chart}-${version}.vl.json`;
// Embed the Vega-Lite chart
const embedOptions = {
actions: false, // Disable kebab menu
tooltip: false // Disable tooltips
};
if (version === 'ukrainian') {
embedOptions.timeFormatLocale = {
dateTime: "%A, %e %B %Y р. %X",
date: "%d.%m.%Y",
time: "%H:%M:%S",
periods: ["", ""],
days: ["неділя", "понеділок", "вівторок", "середа", "четвер", "п'ятниця", "субота"],
shortDays: ["нд", "пн", "вт", "ср", "чт", "пт", "сб"],
months: ["січень", "лютий", "березень", "квітень", "травень", "червень", "липень", "серпень", "вересень", "жовтень", "листопад", "грудень"],
shortMonths: ["січ", "лют", "бер", "кві", "тра", "чер", "лип", "сер", "вер", "жов", "лис", "гру"]
};
}
vegaEmbed('#chart', chartFile, embedOptions).catch(console.error);
// Update question and options
document.getElementById('question-text').innerHTML = question.question;
document.getElementById('options').innerHTML = question.options.map((option, index) => `
<button class="option-button" data-value="${option}">${option}</button>
`).join('');
// Start fresh timer
startTimer();
// Add event listeners to option buttons
document.querySelectorAll('.option-button').forEach(button => {
button.addEventListener('click', () => submitAnswer(button.dataset.value));
});
}
function stopTimer() {
if (timer) {
clearInterval(timer);
timer = null;
}
}
function startTimer() {
// Clear any existing timer
stopTimer();
// Ensure display shows starting value
timerDisplay.textContent = timeLeft;
timer = setInterval(() => {
timeLeft--;
timerDisplay.textContent = timeLeft;
if (timeLeft <= 0) {
stopTimer();
alert("Time's up! Moving to the next question.");
submitAnswer(true);
}
}, 1000);
}
function storeQuizProgress() {
const currentProgress = {
quizId: quizId,
version: version,
score: score,
currentQuestionIndex: currentQuestionIndex,
totalQuestions: questions.length,
answers: quizResults,
lastUpdated: new Date().toISOString(),
participantData: JSON.parse(localStorage.getItem('participantData') || '{}'),
startTime: startTime, // Store start time
browserId: browserId, // Store browser ID
browserInfo: browserInfo // Store browser info
};
const allQuizzes = JSON.parse(localStorage.getItem('allQuizzes') || '{}');
allQuizzes[quizId] = currentProgress;
localStorage.setItem('allQuizzes', JSON.stringify(allQuizzes));
}
function submitAnswer(selectedAnswer = null) {
// Stop the timer first
stopTimer();
if (!selectedAnswer) {
alert('Please select an answer before proceeding.');
startTimer(); // Restart timer if no answer selected
return;
}
const currentQuestion = questions[currentQuestionIndex];
const isCorrect = selectedAnswer === currentQuestion.answer;
if (isCorrect) {
score++;
}
// Store the question result
quizResults.push({
question: currentQuestion.question,
selectedAnswer: selectedAnswer,
correctAnswer: currentQuestion.answer,
isCorrect: isCorrect,
questionIndex: currentQuestionIndex,
timestamp: new Date().toISOString(),
timeSpent: 25 - timeLeft,
chartType: currentQuestion.chart,
chartTypeUk: currentQuestion.chart_uk // Include chart_uk type
});
// Store progress after each answer
storeQuizProgress();
currentQuestionIndex++;
// Reset timer state before showing next question
timeLeft = 25;
displayQuestion();
}
function showResults() {
// Mark quiz as completed
const allQuizzes = JSON.parse(localStorage.getItem('allQuizzes') || '{}');
allQuizzes[quizId].completed = true;
allQuizzes[quizId].completedAt = new Date().toISOString();
localStorage.setItem('allQuizzes', JSON.stringify(allQuizzes));
// Redirect to the questionnaire page
window.location.href = 'questionnaire.html';
}
});

30
src/js/results.js Normal file
View File

@@ -0,0 +1,30 @@
const resultsContainer = document.getElementById('results');
const resultsHeader = document.getElementById('results-header');
const resultsTableBody = document.getElementById('results-table-body');
const allQuizzes = JSON.parse(localStorage.getItem('allQuizzes') || '{}');
const quizData = allQuizzes['quiz'];
function displayResults() {
const totalQuestions = quizData.totalQuestions;
const correctAnswers = quizData.score;
const percentage = ((correctAnswers / totalQuestions) * 100).toFixed(2);
resultsHeader.textContent = `Результати тесту: ${correctAnswers}/${totalQuestions}, ${percentage}%`;
let resultsHTML = '';
quizData.answers.forEach(answer => {
resultsHTML += `
<tr>
<td>${answer.question}</td>
<td>${answer.chartTypeUk}</td>
<td>${answer.isCorrect ? '✅' : '❌'}</td>
</tr>
`;
});
resultsTableBody.innerHTML = resultsHTML;
}
window.onload = displayResults;