Data Visualization Guide

Create charts and dashboards with Recharts

This guide shows you how to create data visualizations using the built-in Recharts library.

Getting Started#

Access the charts library from the API:

javascript
const { React, charts, components } = window.SubwayBuilderAPI.utils;
const {
    ResponsiveContainer, PieChart, Pie, Cell,
    BarChart, Bar, LineChart, Line,
    XAxis, YAxis, CartesianGrid, Tooltip, Legend
} = charts;
const { Card, CardContent, CardHeader, CardTitle } = components;
const h = React.createElement;

Pie Chart: Mode Share#

Display how commuters travel:

javascript
function ModeShareChart() {
    const modes = api.gameState.getModeChoiceStats();

    const data = [
        { name: 'Transit', value: modes.transit, color: '#22c55e' },
        { name: 'Driving', value: modes.driving, color: '#ef4444' },
        { name: 'Walking', value: modes.walking, color: '#3b82f6' }
    ];

    return h(Card, null, [
        h(CardHeader, { key: 'header' },
            h(CardTitle, null, 'Mode Share')
        ),
        h(CardContent, { key: 'content' },
            h(ResponsiveContainer, { width: '100%', height: 200 },
                h(PieChart, null,
                    h(Pie, {
                        data: data,
                        dataKey: 'value',
                        nameKey: 'name',
                        cx: '50%',
                        cy: '50%',
                        outerRadius: 60,
                        label: ({ name, percent }) =>
                            `${name} ${(percent * 100).toFixed(0)}%`
                    },
                        data.map((entry, i) =>
                            h(Cell, { key: i, fill: entry.color })
                        )
                    )
                )
            )
        )
    ]);
}

Bar Chart: Station Ridership#

Compare ridership across stations:

javascript
function StationRidershipChart() {
    const stations = api.gameState.getStations().slice(0, 10);

    const data = stations.map(station => {
        const stats = api.gameState.getStationRidership(station.id);
        return {
            name: station.name.slice(0, 15),
            riders: stats?.total || 0
        };
    });

    return h(Card, null, [
        h(CardHeader, { key: 'header' },
            h(CardTitle, null, 'Top Stations by Ridership')
        ),
        h(CardContent, { key: 'content' },
            h(ResponsiveContainer, { width: '100%', height: 300 },
                h(BarChart, { data: data }, [
                    h(CartesianGrid, { key: 'grid', strokeDasharray: '3 3' }),
                    h(XAxis, {
                        key: 'x',
                        dataKey: 'name',
                        tick: { fontSize: 10 },
                        angle: -45,
                        textAnchor: 'end'
                    }),
                    h(YAxis, { key: 'y' }),
                    h(Tooltip, { key: 'tooltip' }),
                    h(Bar, {
                        key: 'bar',
                        dataKey: 'riders',
                        fill: '#8b5cf6'
                    })
                ])
            )
        )
    ]);
}

Line Chart: Ridership Over Time#

Track ridership trends (requires storing historical data):

javascript
// Store historical data
let ridershipHistory = [];

api.hooks.onDayChange((day) => {
    const stats = api.gameState.getRidershipStats();
    ridershipHistory.push({
        day: day,
        riders: stats.totalRidersPerHour
    });

    // Keep last 30 days
    if (ridershipHistory.length > 30) {
        ridershipHistory = ridershipHistory.slice(-30);
    }
});

function RidershipTrendChart() {
    return h(Card, null, [
        h(CardHeader, { key: 'header' },
            h(CardTitle, null, 'Ridership Trend')
        ),
        h(CardContent, { key: 'content' },
            h(ResponsiveContainer, { width: '100%', height: 250 },
                h(LineChart, { data: ridershipHistory }, [
                    h(CartesianGrid, { key: 'grid', strokeDasharray: '3 3' }),
                    h(XAxis, { key: 'x', dataKey: 'day' }),
                    h(YAxis, { key: 'y' }),
                    h(Tooltip, { key: 'tooltip' }),
                    h(Line, {
                        key: 'line',
                        type: 'monotone',
                        dataKey: 'riders',
                        stroke: '#22c55e',
                        strokeWidth: 2
                    })
                ])
            )
        )
    ]);
}

Complete Dashboard#

Combine charts into a dashboard:

javascript
function ModDashboard() {
    return h('div', { className: 'space-y-4 p-4' }, [
        // Header
        h('h2', {
            key: 'title',
            className: 'text-xl font-bold'
        }, 'Network Analytics'),

        // Stats row
        h('div', {
            key: 'stats',
            className: 'grid grid-cols-3 gap-4'
        }, [
            StatCard('Stations', api.gameState.getStations().length, 'MapPin'),
            StatCard('Trains', api.gameState.getTrains().length, 'Train'),
            StatCard('Budget', `$${api.gameState.getBudget().toLocaleString()}`, 'DollarSign')
        ]),

        // Charts row
        h('div', {
            key: 'charts',
            className: 'grid grid-cols-2 gap-4'
        }, [
            h(ModeShareChart, { key: 'mode' }),
            h(StationRidershipChart, { key: 'stations' })
        ]),

        // Full width chart
        h(RidershipTrendChart, { key: 'trend' })
    ]);
}

function StatCard(label, value, iconName) {
    const Icon = api.utils.icons[iconName];
    return h(Card, { key: label, className: 'p-4' }, [
        h('div', { className: 'flex items-center gap-2 text-muted-foreground text-sm' }, [
            h(Icon, { className: 'h-4 w-4' }),
            label
        ]),
        h('div', { className: 'text-2xl font-bold mt-1' }, value)
    ]);
}

// Register as toolbar panel
api.ui.addToolbarPanel({
    id: 'analytics-dashboard',
    icon: 'BarChart3',
    tooltip: 'Analytics Dashboard',
    title: 'Network Analytics',
    width: 600,
    render: ModDashboard
});

Real-Time Updates#

For live-updating charts, use React state:

javascript
function LiveRidershipDisplay() {
    const [ridership, setRidership] = React.useState(0);

    React.useEffect(() => {
        const interval = setInterval(() => {
            const stats = api.gameState.getRidershipStats();
            setRidership(stats.totalRidersPerHour);
        }, 1000);

        return () => clearInterval(interval);
    }, []);

    return h('div', { className: 'text-center' }, [
        h('div', { className: 'text-4xl font-bold' }, ridership.toLocaleString()),
        h('div', { className: 'text-sm text-muted-foreground' }, 'riders/hour')
    ]);
}

Available Chart Types#

From Recharts:

  • LineChart, Line - Trend data
  • BarChart, Bar - Comparisons
  • PieChart, Pie, Cell - Proportions
  • AreaChart, Area - Volume over time
  • RadarChart, Radar - Multi-dimensional
  • ComposedChart - Mixed chart types

Common components:

  • ResponsiveContainer - Auto-sizing wrapper
  • XAxis, YAxis - Axes
  • CartesianGrid - Background grid
  • Tooltip - Hover info
  • Legend - Chart legend

Tips#

  1. Always use ResponsiveContainer - Charts need a defined size
  2. Limit data points - Too many points slow down rendering
  3. Update efficiently - Don't re-render on every frame
  4. Match colors - Use the game's color palette
  5. Add tooltips - Help players understand the data