An interactive, responsive trivia data dashboard that turns raw API responses into a clean, explorable visualization — built with React, TypeScript, and Recharts.
This project fetches 50 live trivia questions from the Open Trivia DB API and presents the resulting dataset as an interactive bar chart. You can filter the questions by category and switch views between distribution by category and distribution by difficulty.
It was built as a self-contained frontend prototype focused on typed data models, a clean component structure, async API handling, data aggregation, and a responsive UI. The app is deployed as a static site on GitHub Pages.
Live demo: jslusark.github.io/mini-visualization-tool
The dashboard loads data from the live Open Trivia DB API, so an internet connection is required for the chart to populate.
- What This Is
- Features
- Tech Stack
- How It Is Built
- Application Data Flow
- Project Structure
- Getting Started
- Deployment
- Configuration
- Design Decisions & Trade-offs
- Known Issues
- Roadmap & Future Improvements
The Mini Visualization Tool is a single-page application that answers one simple question: "What does this trivia dataset look like?" Instead of dumping raw JSON on screen, it:
- Requests trivia questions from a public REST API.
- Decodes the URL-encoded response into a typed data model.
- Aggregates the data into a grouped distribution.
- Renders that distribution as an interactive, responsive bar chart.
- Lets the user refine the view by category and metric.
| Feature | Description |
|---|---|
| Live API integration | Fetches 50 questions from Open Trivia DB with encode=url3986 and decodes all text fields. |
| Category filter | A dropdown lists the unique categories found in the fetched data, plus an "All categories" option. |
| View toggle | Switch between aggregating by category or difficulty with a radio toggle. |
| Interactive chart | Hover tooltips, animated bars, hover highlighting, and a legend that describes the current view. |
| Responsive labels | X-axis labels rotate, shorten, and (on very narrow screens) skip every other tick when they no longer fit. |
| Request states | Dedicated loading, error, and empty-state components. |
| Styled UI | Built with DaisyUI + Tailwind CSS using a custom DaisyUI theme. |
| Static deployment | Automatic deploys to GitHub Pages via GitHub Actions, plus a manual gh-pages script. |
| Layer | Technology | Purpose |
|---|---|---|
| Language | TypeScript | Typed data models, component props, and shared interfaces. |
| UI library | React 19 | Component-based UI with hooks. |
| Build tool | Vite 7 | Fast dev server and optimized production builds. |
| HTTP client | Axios | Fetching the trivia API data. |
| Charting | Recharts 3 | Declarative, responsive bar charts. |
| Styling | Tailwind CSS 4 + DaisyUI 5 | Utility-first styling and prebuilt UI components/theme. |
| Linting | ESLint 9 | Code quality via typescript-eslint and React hooks/refresh plugins. |
| Deployment | GitHub Actions / gh-pages |
Static hosting on GitHub Pages. |
The project follows a component-driven architecture with clear separation of concerns:
flowchart LR
A[Open Trivia DB API] -->|Axios GET| B[useTriviaData hook]
B -->|"TriviaQuestion list"| C[DataContainer]
C -->|data + state| D[FilterPanel]
D -->|selectedCategory / activeFilter| C
C -->|data + selection| E[Chart]
E -->|calls| F[filterData util]
F -->|"{ value, count } list"| E
- Data layer —
hooks/useTriviaData.tsxowns the network request and its lifecycle. - State layer —
components/DataContainer.tsxholds the active view and the selected category, and picks which request state to render. - UI layer —
components/filterPanel/*andcomponents/chart/*are driven by props. - Logic layer —
utils/filterData.tsxandutils/colorUtils.tsxhold pure, reusable functions. - Types layer —
types.tsdefines the shared TypeScript interfaces.
The orchestration component. It:
- Calls
useTriviaData(50)to kick off the fetch. - Holds two pieces of UI state:
activeFilter(categoryordifficulty) andselectedCategory. - Renders one of: loading spinner → error message → empty state → chart.
- Passes props down to
FilterPanelandChart, keeping data flow one-directional.
A custom hook encapsulating the API call:
- Uses
useStatefordata,error, andisLoading. - Fetches with Axios and decodes
category,question,correct_answer, andincorrect_answersusingdecodeURIComponent(because the request usesencode=url3986). - Waits 5 seconds before sending the request so the loading state is visible during development.
- Clears its timer on unmount.
Lays out the category dropdown and the view toggle side by side (stacked on small screens).
- Derives a unique, alphabetically sorted list of categories from the data using
Setandsort(). - Prepends an "All categories" option and hides the currently selected entry from the list.
- Renders a DaisyUI hover dropdown; selecting "All categories" resets the filter to
null.
- Maps the two view options (
category,difficulty) to radio inputs. - Highlights the active option and calls
onChangeto lift the selection up toDataContainer.
The visualization centerpiece:
- Computes chart data via
filterDatafor the current category and view. - Tracks viewport width with a
resizelistener to decide whether to rotate and truncate labels. - Uses a linear scaling rule: labels rotate when the label count exceeds
5 * (width / 640). Below 500px wide, rotated labels show every other tick. - Shortens category prefixes to three letters (e.g.
Entertainment: Video Games→Ent: Video Games) and trims labels with an ellipsis (17 characters when rotated, 25 otherwise). - Colors bars and ticks differently depending on the active view via
customiseColor. - Shows the tooltip only while the pointer is over a bar.
Small presentational components for each request state.
A simple footer with author credits and a link to the repository.
The core data transformation:
- Optionally filters questions by
selectedCategory. - Counts occurrences of the chosen field (
categoryordifficulty). - Returns
{ value, count }pairs sorted alphabetically withlocaleCompare(case-insensitive).
colorSchememirrors the DaisyUI theme tokens asoklch()values so Recharts can use the same palette.customiseColormaps the active view to idle/active colors (secondary for category, accent for difficulty).
TriviaQuestion— the shape of one decoded API record.FilteredItem— the shape of aggregated chart data.FilterToggleProps— the props contract for the toggle component.
- Fetch —
useTriviaData(50)requests 50 URL-encoded questions from Open Trivia DB. - Normalize — Text fields are decoded and stored as
TriviaQuestionobjects. - Store —
DataContainerholds the questions, the active view, and the selected category. - Aggregate —
ChartcallsfilterData, which narrows the questions and counts matching values. - Render —
Chartdraws the bar chart; the filter panel lets the user change the view.
mini-visualization-tool/
├── .github/workflows/
│ └── deploy.yml # Build + deploy to GitHub Pages
├── public/ # Static assets served as-is
├── src/
│ ├── assets/
│ │ ├── mockData.json # Sample API response (not used by the app)
│ │ └── preview.gif # README preview
│ ├── components/
│ │ ├── chart/
│ │ │ └── Chart.tsx # Responsive Recharts bar chart
│ │ ├── filterPanel/
│ │ │ ├── CategorySelect.tsx # Category dropdown filter
│ │ │ ├── FilterPanel.tsx # Combines select + toggle
│ │ │ └── FIlterToggle.tsx # Category/difficulty view toggle
│ │ ├── layout/
│ │ │ └── footer.tsx # Author + repo footer
│ │ ├── loading/
│ │ │ ├── EmptyState.tsx # No-data state
│ │ │ ├── ErrorMessage.tsx # Error state
│ │ │ └── LoadingSpinner.tsx # Loading state
│ │ └── DataContainer.tsx # Orchestrates data + state
│ ├── constants/
│ │ └── constants.ts # API URLs, view options, max questions
│ ├── hooks/
│ │ └── useTriviaData.tsx # Custom data-fetching hook
│ ├── styles/
│ │ ├── App.css
│ │ └── index.css # Tailwind + DaisyUI theme
│ ├── utils/
│ │ ├── colorUtils.tsx # Theme colors + chart color mapping
│ │ └── filterData.tsx # Filter + aggregate logic
│ ├── App.tsx # App shell + layout
│ ├── main.tsx # React entry point
│ └── types.ts # Shared TypeScript interfaces
├── index.html
├── package.json
├── vite.config.ts # Vite + base path config
├── tailwind.config.js
├── postcss.config.cjs
├── tsconfig*.json
└── eslint.config.js
- Node.js 20.19+ or 22.12+ (required by Vite 7)
- npm
npm install
npm run devVite prints the local URL (usually http://localhost:5173/mini-visualization-tool). Open it to see the app with hot reload.
| Command | What it does |
|---|---|
npm run lint |
Runs ESLint. |
npm run build |
Type-checks with tsc -b, then builds to dist/ with Vite. |
npm run preview |
Serves the production build locally. |
npm run deploy |
Publishes dist/ to the gh-pages branch. |
There are two ways the site gets deployed:
- GitHub Actions (automatic) —
.github/workflows/deploy.ymlbuilds and deploys to GitHub Pages on every push tomainormini_visualization_tool, and can also be run manually from the Actions tab. This requires the repository's Pages source to be set to GitHub Actions. gh-pages(manual) —npm run deploypushes the currentdist/folder to thegh-pagesbranch. It does not build first, so runnpm run buildbefore it.
- Vite base path —
vite.config.tssetsbase: "/mini-visualization-tool"so asset paths resolve under the GitHub Pages sub-path. Change this if you rename the repository. - TypeScript — split into
tsconfig.json,tsconfig.app.json, andtsconfig.node.json, withverbatimModuleSyntaxenabled so types are imported withimport type. - Styling — Tailwind CSS 4 runs through
postcss.config.cjs(@tailwindcss/postcss), with the DaisyUI 5 plugin and a custommythemetheme defined insrc/styles/index.css. - Linting —
eslint.config.jsuses@eslint/js,typescript-eslint,eslint-plugin-react-hooks, andeslint-plugin-react-refresh.
- One-directional data flow — Shared state lives in
DataContainer; children receive props and report changes through callbacks. - Pure utility functions —
filterDataand the color helpers have no side effects, which makes them easy to unit test later. - Computed label rotation — Rotation is calculated from
window.innerWidthand the number of labels with a linear rule instead of fixed CSS breakpoints. This adapts to the data but needs a resize listener. - Visible loading state — The hook deliberately delays the request so the loading UI can be seen and checked.
- Typed API records — The decoded response is typed as
TriviaQuestion, so components get compile-time checks on the fields they use.
- Constants not fully used —
API_BASE_URLandMAX_QUESTIONSexist inconstants.ts, but the hook andDataContainerstill hard-code the URL and50.API_CATEGORY_URLis unused. - Open Trivia DB rate limits — the API allows one request per IP every 5 seconds, so reloading quickly can return an empty or error response.
From the TODOs in the code:
- Local-storage caching to reduce repeated API calls and support offline use.
- Automated tests — unit tests for
filterData, integration tests for the API hook. - Loading more than 50 questions — the API caps a single request at 50, so this needs multiple requests.
- Smaller batches for very large datasets (1000+ items).
Other ideas:
- Runtime validation of the API response (e.g. with Zod).
- Lint and test steps in the GitHub Actions workflow.
