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
| Month | Revenue | New Customers |
|---|---|---|
| Jan | $120,000 | 15 |
| Feb | $125,000 | 18 |
| Mar | $135,000 | 22 |
| Apr | $132,000 | 20 |
| May | $140,000 | 25 |
| Jun | $138,000 | 23 |
| Jul | $142,000 | 21 |
| Aug | $139,000 | 19 |
| Sep | $150,000 | 28 |
| Oct | $160,000 | 32 |
| Nov | $165,000 | 30 |
| Dec | $158,000 | 26 |
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.
1import React from "react";2import {3LineChart,4Line,5XAxis,6YAxis,7CartesianGrid,8Tooltip,9Legend,10ResponsiveContainer,11} from "recharts";12import { ChartSpline, TrendingUp, ChevronsUp } from "lucide-react";13import {14Table,15TableBody,16TableCell,17TableHead,18TableHeader,19TableRow,20} from "@/components/ui/table";2122// Sample data for the chart and table23const 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];3738// Function to format revenue values39// Converts raw numbers to a more readable format (e.g., 120000 to $120K)40const formatRevenue = (value: number) => `$${(value / 1000).toFixed(0)}K`;4142// Context providing description component43const Description = () => (44<div className="py-2 px-3 flex flex-col gap-2 mb-6 bg-secondary">45<p>46Anonymized monthly revenue and new customer data for a B2B SaaS company in47202348</p>49</div>50);5152// Chart component using Recharts to visualize the data53const Chart = () => (54<div className="h-96">55<ResponsiveContainer width="100%" height="100%">56<LineChart57data={data}58margin={{59top: 5,60right: 0,61left: 0,62bottom: 5,63}}64>65<CartesianGrid className="text-gray-300" />66<XAxis dataKey="month" axisLine={false} tickLine={false} />67{/* Left Y-axis for revenue */}68<YAxis69yAxisId="left"70tickFormatter={formatRevenue}71axisLine={false}72tickLine={false}73/>74{/* Right Y-axis for new customers */}75<YAxis76yAxisId="right"77orientation="right"78axisLine={false}79tickLine={false}80/>81<Tooltip82labelStyle={{ color: "#030712", paddingBottom: "4px" }}83contentStyle={{ fontSize: "12px", lineHeight: 1 }}84formatter={(value: number, name: string) => [85name === "revenue" ? formatRevenue(value) : value,86name,87]}88/>89<Legend wrapperStyle={{ paddingTop: "16px" }} />90{/* Line for revenue data */}91<Line92yAxisId="left"93type="monotone"94dataKey="revenue"95name="Monthly Revenue"96stroke="#805AD5"97strokeWidth={2}98dot={{ fill: "#805AD5", strokeWidth: 2 }}99activeDot={{ r: 8 }}100/>101{/* Line for new customers data */}102<Line103yAxisId="right"104type="monotone"105dataKey="newCustomers"106name="New Customers"107stroke="#38A169"108strokeWidth={2}109dot={{ fill: "#38A169", strokeWidth: 2 }}110/>111</LineChart>112</ResponsiveContainer>113</div>114);115116// DataTable component to display the data in a tabular format117const 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);148149// Findings component to highlight key insights from the data150const 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>160Revenue grew 31.67% from $120,000 to $158,000 from January to161December.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>173Q4 outperformed, with October seeing 32 new customers (14% above the174next highest month).175</p>176</div>177</div>178</div>179);180181// Composes graph and related sections182export function RevenueAndNewCustomers() {183return (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.
1import React from "react";2import {3ScatterChart,4Scatter,5XAxis,6YAxis,7ZAxis,8Tooltip,9Legend,10ResponsiveContainer,11CartesianGrid,12} from "recharts";13import { PlatformUsageCode } from "@/components/works/code-examples/platformUsageCode";1415interface DataPoint {16experience: number;17hoursSpent: number;18users: number;19type: "B2B" | "B2C";20}2122const generateData = (count: number, type: "B2B" | "B2C"): DataPoint[] => {23return Array.from({ length: count }, () => ({24experience: Math.floor(Math.random() * 31),25hoursSpent: Math.floor(Math.random() * 16) + 1,26users: Math.floor(Math.random() * 900) + 100,27type,28}));29};3031const data: DataPoint[] = [32...generateData(25, "B2B"),33...generateData(25, "B2C"),34];3536interface CustomTooltipProps {37active?: boolean;38payload?: Array<{ payload: DataPoint }>;39}4041const CustomTooltip: React.FC<CustomTooltipProps> = ({ active, payload }) => {42if (active && payload && payload.length) {43const data = payload[0].payload;44return (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}57return null;58};5960export function PlatformUsage() {61return (62<ResponsiveContainer width="100%" height={400}>63<ScatterChart margin={{ top: 0, right: 50, bottom: 40, left: 20 }}>64<CartesianGrid vertical={false} />65<XAxis66dataKey="experience"67name="Years of Professional Experience"68unit=" years"69type="number"70domain={[0, 30]}71tickCount={7}72axisLine={true}73tickLine={true}74tick={{ dy: 10 }}75label={{76value: "Years of Experience",77position: "bottom",78offset: 25,79}}80/>81<YAxis82dataKey="hoursSpent"83name="Hours Spent on Platform"84unit="h"85domain={[0, 16]}86// tickCount={9}87axisLine={false}88tickLine={false}89// tick={{ dx: -10 }}90label={{91value: "Usage Hours",92angle: -90,93position: "left",94offset: -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<Scatter101name="B2B Customer"102data={data.filter((item) => item.type === "B2B")}103fill="#f59e0b"104fillOpacity={0.7}105/>106<Scatter107name="B2C Customer"108data={data.filter((item) => item.type === "B2C")}109fill="#3b82f6"110fillOpacity={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.
1import React from "react";2import {3AreaChart,4Area,5XAxis,6YAxis,7CartesianGrid,8Tooltip,9ResponsiveContainer,10TooltipProps,11} from "recharts";12import { OutageGraphCode } from "@/components/works/code-examples/outageGraphCode";1314// Define the structure of each data point15interface DataPoint {16time: number;17reports: number;18}1920// Generate mock data for the graph21const generateMockData = (): DataPoint[] => {22const data: DataPoint[] = [];23for (let i = 0; i < 25; i++) {24data.push({25time: i,26reports: Math.floor(Math.random() * 10),27});28}29// Simulate spikes in the data30data[17].reports = 180;31data[18].reports = 283;32data[19].reports = 110;33return data;34};3536// Format the X-axis labels to show time in AM/PM format37const formatXAxis = (tickItem: number): string => {38const hour = tickItem % 12 || 12;39const ampm = tickItem < 12 || tickItem === 24 ? "AM" : "PM";40return `${hour} ${ampm}`;41};4243// Custom tooltip component for the graph44const CustomTooltip: React.FC<TooltipProps<number, string>> = ({45active,46payload,47label,48}) => {49if (active && payload && payload.length) {50const time = formatXAxis(label as number);51return (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}63return null;64};6566// Graph component67export function OutageGraph() {68const data = generateMockData();6970return (71<div className="h-80">72<ResponsiveContainer width="100%" height="100%">73<AreaChart74data={data}75margin={{ top: 10, right: 40, left: 0, bottom: 0 }}76>77<CartesianGrid strokeDasharray="3 3" vertical={false} />78<XAxis79dataKey="time"80stroke="#9ca3af"81tick={{ fontSize: 12 }}82tickFormatter={formatXAxis}83interval="preserveStartEnd"84domain={[0, 24]} // Ensure the domain covers the full 24-hour range85/>86<YAxis stroke="#9ca3af" tick={{ fontSize: 12 }} />87<Tooltip content={<CustomTooltip />} />88<Area89type="monotone"90dataKey="reports"91stroke="#f59e0b"92fill="#fcd34d"93/>94</AreaChart>95</ResponsiveContainer>96</div>97);98};