A comprehensive data visualization dashboard built with Next.js 15 that showcases and compares multiple JavaScript charting libraries by fetching and displaying data from various public APIs.
This project serves as both a functional dashboard and a demonstration of integrating multiple charting solutions into a single application. It fetches real-time data from public APIs and visualizes it using six different charting libraries, allowing for side-by-side comparison of their capabilities.
- Nivo - Bar, Heatmap, Line, Pie, and Area (Bump) charts
- ECharts - Pie, Donut, Line, Bar, Scatter, and Radar charts
- Recharts - Line, Bar, Area, Pie, and Radar charts
- Chart.js - Bar, Pie, Doughnut, and Radar charts
- Plotly.js - Line, Bar, Pie, and Scatter charts
- D3.js - Area, Line, and Bar charts
The dashboard uses a hybrid data architecture combining internal API routes and external APIs:
Internal API Routes (Next.js API routes under /api/charts/):
- Products - Price/rating, categories, brand counts, low stock, discounts, price distribution
- Users - Gender distribution, age distribution, blood type distribution
- Recipes - Ratings, difficulty levels, cooking times
- Todos - Completion status
- Posts - Reaction counts
- Carts - Shopping cart totals
- Quotes - Author distribution
- Heatmap - Server-generated sample data
External APIs (proxied for CORS):
- DummyJSON - Products, users, recipes, todos, posts, carts, quotes
- Open-Meteo - Weather forecasts (London)
- OpenBrewery DB - Brewery information by state
- Open Library - Book and subject data
- PokéAPI - Pokemon statistics
- SpaceX API - Launch history
- Interactive Chart Selection - Toggle individual charts on/off via sidebar
- URL State Persistence - Selected charts are saved in URL query parameters
- Optimized Data Fetching - TanStack Query with 5-minute caching and parallel requests
- Responsive Design - Mobile-friendly grid layout
- Refresh Functionality - Manually refresh all chart data
- CORS Proxy - Built-in API proxy to handle cross-origin requests
- Code Splitting - Dynamic imports for optimal bundle size
- Type Safety - Full TypeScript implementation
- Next.js 15 - React framework with App Router
- React 19.2 - UI library
- TypeScript 5 - Type safety
- Zustand 5 - Lightweight state management
- TanStack Query 5 - Server state management and caching
- Tailwind CSS 4 - Utility-first CSS framework
- Radix UI - Accessible component primitives (Dialog, Checkbox, Tabs, Slot)
- Lucide React - Icon library
- class-variance-authority - Component variant handling
- tailwindcss-animate - Animation utilities
- @nivo/* - Bar (@nivo/bar), Heatmap (@nivo/heatmap), Line (@nivo/line), Pie (@nivo/pie), Area Bump (@nivo/bump), and Core (@nivo/core)
- echarts & echarts-for-react - Apache ECharts integration with React wrapper
- recharts - Composable charting library built on D3
- chart.js & react-chartjs-2 - Chart.js with React wrapper
- plotly.js-dist-min & react-plotly.js - Plotly.js with React integration
- d3 - Low-level data visualization library for custom SVG charts
- ESLint 9 - Code linting
- babel-plugin-react-compiler - React compiler plugin
- PostCSS - CSS transformations
dashboard/
├── src/
│ ├── app/
│ │ ├── api/
│ │ │ ├── charts/ # Internal API routes
│ │ │ │ ├── products/ # Product-related endpoints
│ │ │ │ │ ├── price-rating/route.ts
│ │ │ │ │ ├── categories/route.ts
│ │ │ │ │ ├── brand-counts/route.ts
│ │ │ │ │ ├── low-stock/route.ts
│ │ │ │ │ ├── discounts/route.ts
│ │ │ │ │ └── price-distribution/route.ts
│ │ │ │ ├── users/ # User demographics endpoints
│ │ │ │ │ ├── gender/route.ts
│ │ │ │ │ ├── age-distribution/route.ts
│ │ │ │ │ └── blood-type/route.ts
│ │ │ │ ├── recipes/ # Recipe data endpoints
│ │ │ │ │ ├── ratings/route.ts
│ │ │ │ │ ├── difficulty/route.ts
│ │ │ │ │ └── cooking-time/route.ts
│ │ │ │ ├── todos/ # Todo completion stats
│ │ │ │ │ └── status/route.ts
│ │ │ │ ├── posts/ # Social post metrics
│ │ │ │ │ └── reactions/route.ts
│ │ │ │ ├── carts/ # Shopping cart analytics
│ │ │ │ │ └── totals/route.ts
│ │ │ │ ├── quotes/ # Quote statistics
│ │ │ │ │ └── authors/route.ts
│ │ │ │ ├── heatmap/ # Heatmap sample data
│ │ │ │ │ └── sample/route.ts
│ │ │ │ ├── weather/ # Weather proxy
│ │ │ │ │ └── temperature/route.ts
│ │ │ │ ├── breweries/ # Brewery data proxy
│ │ │ │ │ └── states/route.ts
│ │ │ │ ├── library/ # Library data proxy
│ │ │ │ │ └── subject-works/route.ts
│ │ │ │ ├── pokemon/ # Pokemon stats proxy
│ │ │ │ │ ├── base-xp/route.ts
│ │ │ │ │ └── height-weight/route.ts
│ │ │ │ └── spacex/ # SpaceX data proxy
│ │ │ │ └── launches/route.ts
│ │ │ └── proxy/route.ts # General CORS proxy
│ │ ├── layout.tsx # Root layout with providers
│ │ └── page.tsx # Main dashboard page
│ ├── components/
│ │ ├── graphs/ # Chart components (20+ files)
│ │ │ ├── chartwrapper.tsx # Generic chart wrapper
│ │ │ ├── d3area.tsx # D3 area chart
│ │ │ ├── d3line.tsx # D3 line chart
│ │ │ ├── d3bar.tsx # D3 bar chart
│ │ │ ├── echartsdonut.tsx # ECharts donut chart
│ │ │ ├── echartsline.tsx # ECharts line chart
│ │ │ ├── echartspie.tsx # ECharts pie chart
│ │ │ ├── echartsbar.tsx # ECharts bar chart
│ │ │ ├── echartsscatter.tsx # ECharts scatter chart
│ │ │ ├── echartsradar.tsx # ECharts radar chart
│ │ │ ├── nivobar.tsx # Nivo bar chart
│ │ │ ├── nivoheatmap.tsx # Nivo heatmap
│ │ │ ├── nivoline.tsx # Nivo line chart
│ │ │ ├── nivopie.tsx # Nivo pie chart
│ │ │ ├── nivoarea.tsx # Nivo area (bump) chart
│ │ │ ├── plotlyscatter.tsx # Plotly scatter plot
│ │ │ ├── plotlyline.tsx # Plotly line chart
│ │ │ ├── plotlybar.tsx # Plotly bar chart
│ │ │ ├── plotlypie.tsx # Plotly pie chart
│ │ │ ├── rechartsbar.tsx # Recharts bar chart
│ │ │ ├── rechartsline.tsx # Recharts line chart
│ │ │ ├── recharts-line2.tsx # Recharts line (alt)
│ │ │ ├── rechartsarea.tsx # Recharts area chart
│ │ │ ├── rechartspie.tsx # Recharts pie chart
│ │ │ ├── rechartsradar.tsx # Recharts radar chart
│ │ │ ├── chartjsbar.tsx # Chart.js bar chart
│ │ │ ├── chartjsdoughnut.tsx # Chart.js doughnut chart
│ │ │ ├── chartjspie.tsx # Chart.js pie chart
│ │ │ └── chartjsradar.tsx # Chart.js radar chart
│ │ ├── ui/
│ │ │ ├── button.tsx # Button component
│ │ │ ├── card.tsx # Card component
│ │ │ ├── checkbox.tsx # Checkbox component
│ │ │ ├── skeleton.tsx # Loading skeleton
│ │ │ └── tabs.tsx # Tabs component
│ │ ├── providers.tsx # React Query provider setup
│ │ └── sidebar.tsx # Chart selection sidebar
│ └── lib/
│ ├── charts/
│ │ ├── registry.ts # Chart registry (106 configurations)
│ │ ├── transforms.ts # Data transformation functions
│ │ └── types.ts # Chart type definitions
│ ├── store.ts # Zustand store configuration
│ └── utils.ts # Utility functions
├── public/ # Static assets
├── components.json # shadcn/ui configuration
├── next.config.ts # Next.js configuration
├── tsconfig.json # TypeScript configuration
├── eslint.config.mjs # ESLint configuration
├── postcss.config.mjs # PostCSS configuration
└── package.json # Project dependencies
- Node.js 20+ recommended
- npm, yarn, pnpm, or bun
# Install dependencies
npm install# Run the development server
npm run devOpen http://localhost:3000 in your browser.
# Create production build
npm run build
# Start production server
npm start# Run ESLint
npm run lintThe dashboard uses a centralized registry pattern (src/lib/charts/registry.ts) that defines all available charts. Each chart configuration includes:
- Display metadata (name, display name)
- API configuration (endpoint, query key, data transformer)
- Component (the chart library component to use)
- Chart options (library-specific rendering options)
- User selects charts from the sidebar
- Selection is persisted to URL query parameters
- TanStack Query fetches data from configured APIs in parallel
- Data transformers convert API responses to chart-specific formats
- Charts render with transformed data
- Results are cached for 5 minutes to reduce API calls
The /api/proxy route handles API requests to avoid CORS issues and supports:
- Simple pass-through proxying
- Multi-fetch enrichment (used by PokéAPI to fetch detailed data)
- Zustand store manages selected charts and render keys
- URL params persist selections across page reloads
- TanStack Query handles server state and caching
This section provides a comprehensive walkthrough of how data flows through the system, using the SpaceX Launches chart as a concrete example.
The dashboard follows a declarative configuration pattern where each chart is defined by four key pieces:
- Registry Entry - Metadata and configuration (name, API endpoint, query key)
- Transform Function - Converts raw API data to chart-ready format
- Chart Component - Renders the visualization using a charting library
- Chart Options - Library-specific configuration (titles, colors, axes, etc.)
This separation of concerns allows:
- Decoupling - Change the chart library without touching data fetching
- Reusability - Same transform can feed multiple chart types
- Type Safety - TypeScript ensures data contracts are honored
- Testability - Each piece can be tested in isolation
Let's trace how the SpaceX: Launches per Year chart works from backend API to rendered visualization.
File: src/lib/charts/registry.ts:238-249
{
name: "SpaceX Launches",
displayName: "Launches per Year",
apiConfig: {
endpoint: "https://api.spacexdata.com/v5/launches",
queryKey: ["spacex", "launches"],
transform: transformSpaceXForLaunchesPerYearArea,
},
Component: D3AreaChart,
chartOptions: {
title: "SpaceX: Launches per Year",
},
}What this defines:
name- Internal identifier used for selectiondisplayName- User-facing name in the sidebarendpoint- SpaceX API endpoint to fetch fromqueryKey- TanStack Query cache key (enables caching and deduplication)transform- Function to convert API response to chart dataComponent- D3.js area chart componentchartOptions- Configuration passed to the chart component
File: src/app/page.tsx:13-31
When a user selects this chart, the main page component:
- Calls
useQueries()with the chart's configuration - TanStack Query checks its cache using
["spacex", "launches"] - If not cached (or stale), fetches via the proxy route:
GET /api/proxy?url=https%3A%2F%2Fapi.spacexdata.com%2Fv5%2Flaunches - The proxy route (
src/app/api/proxy/route.ts) forwards the request and returns the response
Example Raw API Response (truncated):
[
{
"id": "5eb87cd9ffd86e000604b32a",
"name": "FalconSat",
"date_utc": "2006-03-24T22:30:00.000Z",
"date_local": "2006-03-25T10:30:00+12:00",
"success": false,
...
},
{
"id": "5eb87cdaffd86e000604b32b",
"name": "DemoSat",
"date_utc": "2007-03-21T01:10:00.000Z",
"date_local": "2007-03-21T13:10:00+12:00",
"success": false,
...
},
{
"id": "5eb87cdbffd86e000604b32c",
"name": "Trailblazer",
"date_utc": "2008-08-03T03:34:00.000Z",
"success": false,
...
},
// ... hundreds more launch objects
]File: src/lib/charts/transforms.ts:213-226
The raw API response is passed to transformSpaceXForLaunchesPerYearArea():
export function transformSpaceXForLaunchesPerYearArea(raw: any) {
const launches = Array.isArray(raw) ? raw : [];
const counts: Record<string, number> = {};
// Count launches per year
for (const l of launches) {
const y = (l.date_utc || l.date_local || "").slice(0, 4);
if (y) counts[y] = (counts[y] || 0) + 1;
}
// Convert to array of {date, count} objects
return Object.entries(counts)
.map(([year, count]) => ({
date: new Date(`${year}-01-01T00:00:00Z`),
count,
}))
.sort((a, b) => a.date.getTime() - b.date.getTime());
}What this does:
- Safely handles non-array responses
- Extracts the year from each launch's UTC date (first 4 characters)
- Aggregates launches by year into a
Record<year, count>object - Converts to an array of
{date: Date, count: number}objects - Sorts chronologically by date
Example Transformed Data:
[
{ date: Date('2006-01-01T00:00:00Z'), count: 1 },
{ date: Date('2007-01-01T00:00:00Z'), count: 1 },
{ date: Date('2008-01-01T00:00:00Z'), count: 2 },
{ date: Date('2009-01-01T00:00:00Z'), count: 3 },
{ date: Date('2010-01-01T00:00:00Z'), count: 2 },
// ... continues through present year
{ date: Date('2023-01-01T00:00:00Z'), count: 98 },
{ date: Date('2024-01-01T00:00:00Z'), count: 134 },
]File: src/components/graphs/d3area.tsx:8-84
The D3 Area Chart component receives the transformed data and renders it:
Key rendering steps:
-
Setup (lines 11-12):
const dims = { width: 800, height: 360, margin: { top: 20, right: 20, bottom: 30, left: 40 } }; const series = useMemo(() => data ?? [], [data]);
-
Create Scales (lines 28-37):
// X-axis: Date scale from min to max date const x = d3.scaleUtc() .domain(d3.extent(series, (d: any) => d.date) as [Date, Date]) .range([0, innerW]); // Y-axis: Linear scale from 0 to max count const y = d3.scaleLinear() .domain([0, d3.max(series, (d: any) => d.count)!]) .nice() .range([innerH, 0]);
-
Define Area Generator (lines 39-43):
const area = d3.area<any>() .x((d) => x(d.date)) // X position based on date .y0(innerH) // Bottom of area (baseline) .y1((d) => y(d.count)); // Top of area (based on count)
-
Render Area + Line (lines 45-52):
// Draw filled area with 20% opacity g.append("path").datum(series) .attr("d", area as any) .attr("fill", "currentColor") .attr("opacity", 0.2); // Draw line on top g.append("path").datum(series) .attr("d", line as any) .attr("fill", "none") .attr("stroke", "currentColor") .attr("stroke-width", 2);
-
Add Axes (lines 54-55):
g.append("g").attr("transform", `translate(0,${innerH})`).call(d3.axisBottom(x)); g.append("g").call(d3.axisLeft(y));
Visual Output:
The final rendered chart shows:
- X-axis: Years from 2006 to present
- Y-axis: Launch count (0 to ~140)
- Area: Light shaded region showing growth in launch frequency
- Line: Solid line tracing the upper boundary of launches per year
- Trend: Exponential growth in SpaceX launches, especially 2020-2024
User Selection
↓
Registry Lookup → {endpoint, queryKey, transform, Component, options}
↓
TanStack Query → Check cache with queryKey
↓
Cache Miss → Fetch via /api/proxy?url={endpoint}
↓
SpaceX API → Returns array of launch objects
↓
Transform Function → Aggregates by year, formats as {date, count}[]
↓
React Component → D3.js renders area chart with scales, area, line, axes
↓
Browser DOM → User sees interactive SVG visualization
Benefits:
- Maintainability - Each chart is ~10 lines of config + transform function
- Flexibility - Swap Nivo for Recharts by changing one line
- Performance - TanStack Query handles caching, deduplication, retries
- DX - Add new charts without touching the core data fetching logic
- Type Safety - Centralized types ensure consistency across 20+ charts
Trade-offs:
- More abstraction layers vs. inline data fetching
- Learning curve for the registry pattern
- All charts must conform to the same data flow
This pattern scales elegantly from 1 chart to 100+ while keeping the codebase maintainable and the user experience performant.
The dashboard includes 106 chart visualizations across 6 charting libraries, showcasing:
- Product Analytics - Price/rating comparisons, category distributions, brand counts, stock levels, discounts, price distributions
- User Demographics - Gender splits, age distributions, blood type breakdowns
- Recipe Metrics - Ratings, difficulty levels, cooking times
- Social Engagement - Post reactions, todo completion status, quote authorship
- E-commerce - Shopping cart totals
- External APIs - Weather temperatures, brewery counts, library works, Pokemon stats, SpaceX launches
Each data source is visualized using multiple chart types across different libraries:
- Line Charts - Trends and time series (Nivo, Recharts, D3, Plotly, ECharts)
- Bar Charts - Categorical comparisons (Nivo, Recharts, D3, Plotly, ECharts, Chart.js)
- Pie/Doughnut Charts - Proportional data (Nivo, Recharts, Plotly, ECharts, Chart.js)
- Area Charts - Filled trends (Nivo, Recharts, D3)
- Scatter Charts - Two-variable correlations (Plotly, ECharts)
- Radar Charts - Multi-dimensional metrics (Recharts, ECharts, Chart.js)
- Heatmaps - Matrix visualizations (Nivo)
This comprehensive coverage enables side-by-side comparison of how different libraries render the same data.
- Dynamic imports for all chart components (code splitting)
- TanStack Query caching (5-min fresh, 15-min cache retention)
- Parallel data fetching with
useQueries - Smart refetch policies (no refetch on window focus/mount/reconnect)
- Memoized computations for chart data transformations
- URL-based state reduces unnecessary re-renders
- React Compiler enabled for automatic optimizations
- Create a chart component in
src/components/graphs/ - Add a data transformer in
src/lib/charts/transforms.ts - Register the chart in
src/lib/charts/registry.ts
Example:
{
name: "My Chart",
displayName: "My Custom Chart",
apiConfig: {
endpoint: "https://api.example.com/data",
queryKey: ["my-chart"],
transform: transformMyData,
},
Component: MyChartComponent,
chartOptions: {
title: "My Chart Title",
// ... library-specific options
},
}Each chart in the registry can use any compatible charting library. Simply swap the Component and adjust the chartOptions accordingly.
- Development port: 3000
- API proxy:
/api/proxy?url={encoded_url}
Modern browsers supporting ES2020+:
- Chrome/Edge (latest)
- Firefox (latest)
- Safari (latest)
- Some public APIs may have rate limits
- CORS issues are handled via the proxy route
- Chart re-rendering is optimized with render keys
Private project (not for distribution)
The easiest way to deploy is using Vercel:
See Next.js deployment documentation for other options.