WORK 05UI ENGINEERING · ● LIVE DEMOS
← ALL WORKS

Data Visualization: Growth Metrics

Interactive visualizations built with Next.js, React, Tailwind CSS, and Recharts, using anonymized metrics from a past B2B SaaS project. Every chart is the real component, running live, with its source alongside.

  • NEXT.JS
  • REACT
  • TAILWIND
  • RECHARTS

2023 Revenue and New Customers

This interactive visualization provides a clear picture of a B2B SaaS company's performance in 2023, showcasing monthly revenue alongside new customer acquisition. By presenting anonymized metrics, it allows for effective communication of key business trends.

Line graph based on anonymized monthly revenue and new customer data

Table data

MonthRevenueNew Customers
Jan$120,00015
Feb$125,00018
Mar$135,00022
Apr$132,00020
May$140,00025
Jun$138,00023
Jul$142,00021
Aug$139,00019
Sep$150,00028
Oct$160,00032
Nov$165,00030
Dec$158,00026

Findings

Revenue grew 31.67% from $120,000 to $158,000 from January to December.

New customers increased 73.33% from 15 to 26 over the year.

Q4 outperformed, with October seeing 32 new customers (14% above the next highest month).

Related front-end code

This code block demonstrates the use of Next.js, React, Tailwind CSS, Recharts, and shadcn/ui to create the interactive data visualization above.

revenueAndNewCustomers.tsx
1
import React from "react";
2
import {
3
LineChart,
4
Line,
5
XAxis,
6
YAxis,
7
CartesianGrid,
8
Tooltip,
9
Legend,
10
ResponsiveContainer,
11
} from "recharts";
12
import { ChartSpline, TrendingUp, ChevronsUp } from "lucide-react";
13
import {
14
Table,
15
TableBody,
16
TableCell,
17
TableHead,
18
TableHeader,
19
TableRow,
20
} from "@/components/ui/table";
21
22
// Sample data for the chart and table
23
const data = [
24
{ month: "Jan", revenue: 120000, newCustomers: 15 },
25
{ month: "Feb", revenue: 125000, newCustomers: 18 },
26
{ month: "Mar", revenue: 135000, newCustomers: 22 },
27
{ month: "Apr", revenue: 132000, newCustomers: 20 },
28
{ month: "May", revenue: 140000, newCustomers: 25 },
29
{ month: "Jun", revenue: 138000, newCustomers: 23 },
30
{ month: "Jul", revenue: 142000, newCustomers: 21 },
31
{ month: "Aug", revenue: 139000, newCustomers: 19 },
32
{ month: "Sep", revenue: 150000, newCustomers: 28 },
33
{ month: "Oct", revenue: 160000, newCustomers: 32 },
34
{ month: "Nov", revenue: 165000, newCustomers: 30 },
35
{ month: "Dec", revenue: 158000, newCustomers: 26 },
36
];
37
38
// Function to format revenue values
39
// Converts raw numbers to a more readable format (e.g., 120000 to $120K)
40
const formatRevenue = (value: number) => `$${(value / 1000).toFixed(0)}K`;
41
42
// Context providing description component
43
const Description = () => (
44
<div className="py-2 px-3 flex flex-col gap-2 mb-6 bg-secondary">
45
<p>
46
Anonymized monthly revenue and new customer data for a B2B SaaS company in
47
2023
48
</p>
49
</div>
50
);
51
52
// Chart component using Recharts to visualize the data
53
const Chart = () => (
54
<div className="h-96">
55
<ResponsiveContainer width="100%" height="100%">
56
<LineChart
57
data={data}
58
margin={{
59
top: 5,
60
right: 0,
61
left: 0,
62
bottom: 5,
63
}}
64
>
65
<CartesianGrid className="text-gray-300" />
66
<XAxis dataKey="month" axisLine={false} tickLine={false} />
67
{/* Left Y-axis for revenue */}
68
<YAxis
69
yAxisId="left"
70
tickFormatter={formatRevenue}
71
axisLine={false}
72
tickLine={false}
73
/>
74
{/* Right Y-axis for new customers */}
75
<YAxis
76
yAxisId="right"
77
orientation="right"
78
axisLine={false}
79
tickLine={false}
80
/>
81
<Tooltip
82
labelStyle={{ color: "#030712", paddingBottom: "4px" }}
83
contentStyle={{ fontSize: "12px", lineHeight: 1 }}
84
formatter={(value: number, name: string) => [
85
name === "revenue" ? formatRevenue(value) : value,
86
name,
87
]}
88
/>
89
<Legend wrapperStyle={{ paddingTop: "16px" }} />
90
{/* Line for revenue data */}
91
<Line
92
yAxisId="left"
93
type="monotone"
94
dataKey="revenue"
95
name="Monthly Revenue"
96
stroke="#805AD5"
97
strokeWidth={2}
98
dot={{ fill: "#805AD5", strokeWidth: 2 }}
99
activeDot={{ r: 8 }}
100
/>
101
{/* Line for new customers data */}
102
<Line
103
yAxisId="right"
104
type="monotone"
105
dataKey="newCustomers"
106
name="New Customers"
107
stroke="#38A169"
108
strokeWidth={2}
109
dot={{ fill: "#38A169", strokeWidth: 2 }}
110
/>
111
</LineChart>
112
</ResponsiveContainer>
113
</div>
114
);
115
116
// DataTable component to display the data in a tabular format
117
const DataTable = () => (
118
<div className="x-4 flex flex-col mt-4 border-t">
119
<div className="py-2 px-3 flex flex-col gap-2 bg-secondary">
120
<p>Table data</p>
121
</div>
122
<div>
123
<Table className="text-xs">
124
<TableHeader>
125
<TableRow>
126
<TableHead className="pl-3">Month</TableHead>
127
<TableHead className="text-right">Revenue</TableHead>
128
<TableHead className="text-right pr-3">New Customers</TableHead>
129
</TableRow>
130
</TableHeader>
131
<TableBody>
132
{data.map((item) => (
133
<TableRow key={item.month}>
134
<TableCell className="font-medium pl-3.5">{item.month}</TableCell>
135
<TableCell className="text-right">
136
${item.revenue.toLocaleString()}
137
</TableCell>
138
<TableCell className="text-right pr-3.5">
139
{item.newCustomers}
140
</TableCell>
141
</TableRow>
142
))}
143
</TableBody>
144
</Table>
145
</div>
146
</div>
147
);
148
149
// Findings component to highlight key insights from the data
150
const Findings = () => (
151
<div className="flex flex-col mt-4 border-t">
152
<div className="py-2 px-3 flex flex-col gap-2 bg-secondary">
153
<p>Findings</p>
154
</div>
155
<div className="py-3 px-3 flex flex-col gap-4">
156
{/* Revenue growth finding */}
157
<div className="flex flex-row gap-2 items-center">
158
<ChartSpline className="h-4 w-4" />
159
<p>
160
Revenue grew 31.67% from $120,000 to $158,000 from January to
161
December.
162
</p>
163
</div>
164
{/* New customer growth finding */}
165
<div className="flex flex-row gap-2 items-center">
166
<ChevronsUp className="h-4 w-4" />
167
<p>New customers increased 73.33% from 15 to 26 over the year.</p>
168
</div>
169
{/* Q4 performance finding */}
170
<div className="flex flex-row gap-2 items-center">
171
<TrendingUp className="h-4 w-4" />
172
<p>
173
Q4 outperformed, with October seeing 32 new customers (14% above the
174
next highest month).
175
</p>
176
</div>
177
</div>
178
</div>
179
);
180
181
// Composes graph and related sections
182
export function RevenueAndNewCustomers() {
183
return (
184
<div className="border rounded-sm">
185
<Header />
186
<Description />
187
<Chart />
188
<DataTable />
189
<Findings />
190
</div>
191
);
192
}

Customer Experience vs Platform Usage

This scatter plot visualization illustrates the relationship between years of professional experience and weekly platform usage hours for B2B and B2C customers. By presenting anonymized data points, it offers insights into usage patterns across different customer segments and experience levels.

Simulated data showing weekly platform usage hours vs. years of professional experience for B2B and B2C customers

Graph code

This code block demonstrates the use of Next.js, React, Tailwind CSS, and Recharts to create the interactive data visualization above.

platformUsage.tsx
1
import React from "react";
2
import {
3
ScatterChart,
4
Scatter,
5
XAxis,
6
YAxis,
7
ZAxis,
8
Tooltip,
9
Legend,
10
ResponsiveContainer,
11
CartesianGrid,
12
} from "recharts";
13
import { PlatformUsageCode } from "@/components/works/code-examples/platformUsageCode";
14
15
interface DataPoint {
16
experience: number;
17
hoursSpent: number;
18
users: number;
19
type: "B2B" | "B2C";
20
}
21
22
const generateData = (count: number, type: "B2B" | "B2C"): DataPoint[] => {
23
return Array.from({ length: count }, () => ({
24
experience: Math.floor(Math.random() * 31),
25
hoursSpent: Math.floor(Math.random() * 16) + 1,
26
users: Math.floor(Math.random() * 900) + 100,
27
type,
28
}));
29
};
30
31
const data: DataPoint[] = [
32
...generateData(25, "B2B"),
33
...generateData(25, "B2C"),
34
];
35
36
interface CustomTooltipProps {
37
active?: boolean;
38
payload?: Array<{ payload: DataPoint }>;
39
}
40
41
const CustomTooltip: React.FC<CustomTooltipProps> = ({ active, payload }) => {
42
if (active && payload && payload.length) {
43
const data = payload[0].payload;
44
return (
45
<div className="bg-secondary rounded-sm shadow-md">
46
<div className="border-b border-gray-300 px-2 py-1">
47
<p className="font-semibold">{data.type} Customer</p>
48
</div>
49
<div className="flex flex-col gap-1 px-2 py-1">
50
<p className="">Experience: {data.experience} years</p>
51
<p className="">Usage Hours: {data.hoursSpent}h</p>
52
<p className="">Users: {data.users}</p>
53
</div>
54
</div>
55
);
56
}
57
return null;
58
};
59
60
export function PlatformUsage() {
61
return (
62
<ResponsiveContainer width="100%" height={400}>
63
<ScatterChart margin={{ top: 0, right: 50, bottom: 40, left: 20 }}>
64
<CartesianGrid vertical={false} />
65
<XAxis
66
dataKey="experience"
67
name="Years of Professional Experience"
68
unit=" years"
69
type="number"
70
domain={[0, 30]}
71
tickCount={7}
72
axisLine={true}
73
tickLine={true}
74
tick={{ dy: 10 }}
75
label={{
76
value: "Years of Experience",
77
position: "bottom",
78
offset: 25,
79
}}
80
/>
81
<YAxis
82
dataKey="hoursSpent"
83
name="Hours Spent on Platform"
84
unit="h"
85
domain={[0, 16]}
86
// tickCount={9}
87
axisLine={false}
88
tickLine={false}
89
// tick={{ dx: -10 }}
90
label={{
91
value: "Usage Hours",
92
angle: -90,
93
position: "left",
94
offset: -10,
95
}}
96
/>
97
<ZAxis dataKey="users" range={[200, 1200]} name="Number of Users" />
98
<Tooltip content={<CustomTooltip />} />
99
<Legend verticalAlign="top" wrapperStyle={{ paddingBottom: "32px" }} />
100
<Scatter
101
name="B2B Customer"
102
data={data.filter((item) => item.type === "B2B")}
103
fill="#f59e0b"
104
fillOpacity={0.7}
105
/>
106
<Scatter
107
name="B2C Customer"
108
data={data.filter((item) => item.type === "B2C")}
109
fill="#3b82f6"
110
fillOpacity={0.7}
111
/>
112
</ScatterChart>
113
</ResponsiveContainer>
114
);
115
};

Platform Outage Detected: 24-Hour Incident Timeline

This visualization provides an hourly breakdown of reported disruptions over a 24-hour period. By presenting this data, it allows for quick identification of critical timeframes and the scale of the incident's impact.

Hourly breakdown of reported disruptions, with significant spike occurring 4-8 PM

Graph code

This code block demonstrates the use of Next.js, React, Tailwind CSS, and Recharts to create the interactive data visualization above.

outageGraph.tsx
1
import React from "react";
2
import {
3
AreaChart,
4
Area,
5
XAxis,
6
YAxis,
7
CartesianGrid,
8
Tooltip,
9
ResponsiveContainer,
10
TooltipProps,
11
} from "recharts";
12
import { OutageGraphCode } from "@/components/works/code-examples/outageGraphCode";
13
14
// Define the structure of each data point
15
interface DataPoint {
16
time: number;
17
reports: number;
18
}
19
20
// Generate mock data for the graph
21
const generateMockData = (): DataPoint[] => {
22
const data: DataPoint[] = [];
23
for (let i = 0; i < 25; i++) {
24
data.push({
25
time: i,
26
reports: Math.floor(Math.random() * 10),
27
});
28
}
29
// Simulate spikes in the data
30
data[17].reports = 180;
31
data[18].reports = 283;
32
data[19].reports = 110;
33
return data;
34
};
35
36
// Format the X-axis labels to show time in AM/PM format
37
const formatXAxis = (tickItem: number): string => {
38
const hour = tickItem % 12 || 12;
39
const ampm = tickItem < 12 || tickItem === 24 ? "AM" : "PM";
40
return `${hour} ${ampm}`;
41
};
42
43
// Custom tooltip component for the graph
44
const CustomTooltip: React.FC<TooltipProps<number, string>> = ({
45
active,
46
payload,
47
label,
48
}) => {
49
if (active && payload && payload.length) {
50
const time = formatXAxis(label as number);
51
return (
52
<div className="bg-secondary rounded-sm shadow-md">
53
<div className="border-b border-gray-300 px-2 py-1">
54
<p className="font-semibold">{`February 28th, 2023 at ${time}`}</p>
55
</div>
56
<div className="flex flex-col gap-1 px-2 py-1">
57
<p className="">Baseline: 2</p>
58
<p className="">Reports: {payload[0].value}</p>
59
</div>
60
</div>
61
);
62
}
63
return null;
64
};
65
66
// Graph component
67
export function OutageGraph() {
68
const data = generateMockData();
69
70
return (
71
<div className="h-80">
72
<ResponsiveContainer width="100%" height="100%">
73
<AreaChart
74
data={data}
75
margin={{ top: 10, right: 40, left: 0, bottom: 0 }}
76
>
77
<CartesianGrid strokeDasharray="3 3" vertical={false} />
78
<XAxis
79
dataKey="time"
80
stroke="#9ca3af"
81
tick={{ fontSize: 12 }}
82
tickFormatter={formatXAxis}
83
interval="preserveStartEnd"
84
domain={[0, 24]} // Ensure the domain covers the full 24-hour range
85
/>
86
<YAxis stroke="#9ca3af" tick={{ fontSize: 12 }} />
87
<Tooltip content={<CustomTooltip />} />
88
<Area
89
type="monotone"
90
dataKey="reports"
91
stroke="#f59e0b"
92
fill="#fcd34d"
93
/>
94
</AreaChart>
95
</ResponsiveContainer>
96
</div>
97
);
98
};