| import type { Customer, SegmentSummary, SegmentName } from "./types.js"; |
| import { SEGMENT_COLORS, SEGMENTS } from "./types.js"; |
|
|
| |
| const PREFIXES = [ |
| "Apex", |
| "Nova", |
| "Prime", |
| "Vertex", |
| "Atlas", |
| "Quantum", |
| "Summit", |
| "Nexus", |
| "Titan", |
| "Pinnacle", |
| "Zenith", |
| "Vanguard", |
| "Horizon", |
| "Stellar", |
| "Onyx", |
| "Cobalt", |
| "Vector", |
| "Pulse", |
| "Forge", |
| "Spark", |
| ]; |
|
|
| const CORES = [ |
| "Tech", |
| "Data", |
| "Cloud", |
| "Logic", |
| "Sync", |
| "Flow", |
| "Core", |
| "Net", |
| "Soft", |
| "Wave", |
| "Link", |
| "Mind", |
| "Byte", |
| "Grid", |
| "Hub", |
| ]; |
|
|
| const SUFFIXES = [ |
| "Corp", |
| "Inc", |
| "Solutions", |
| "Systems", |
| "Labs", |
| "Group", |
| "Industries", |
| "Dynamics", |
| "Partners", |
| "Ventures", |
| "Global", |
| "Digital", |
| ]; |
|
|
| |
| interface ClusterCenter { |
| annualRevenue: { min: number; max: number }; |
| employeeCount: { min: number; max: number }; |
| accountAge: { min: number; max: number }; |
| engagementScore: { min: number; max: number }; |
| supportTickets: { min: number; max: number }; |
| nps: { min: number; max: number }; |
| } |
|
|
| const CLUSTER_CENTERS: Record<SegmentName, ClusterCenter> = { |
| Enterprise: { |
| annualRevenue: { min: 2_000_000, max: 10_000_000 }, |
| employeeCount: { min: 500, max: 5000 }, |
| accountAge: { min: 60, max: 120 }, |
| engagementScore: { min: 70, max: 95 }, |
| supportTickets: { min: 5, max: 20 }, |
| nps: { min: 40, max: 80 }, |
| }, |
| "Mid-Market": { |
| annualRevenue: { min: 500_000, max: 2_000_000 }, |
| employeeCount: { min: 100, max: 500 }, |
| accountAge: { min: 36, max: 84 }, |
| engagementScore: { min: 60, max: 85 }, |
| supportTickets: { min: 10, max: 30 }, |
| nps: { min: 20, max: 60 }, |
| }, |
| SMB: { |
| annualRevenue: { min: 50_000, max: 500_000 }, |
| employeeCount: { min: 10, max: 100 }, |
| accountAge: { min: 12, max: 48 }, |
| engagementScore: { min: 40, max: 70 }, |
| supportTickets: { min: 15, max: 40 }, |
| nps: { min: 0, max: 40 }, |
| }, |
| Startup: { |
| annualRevenue: { min: 10_000, max: 200_000 }, |
| employeeCount: { min: 1, max: 50 }, |
| accountAge: { min: 1, max: 24 }, |
| engagementScore: { min: 50, max: 90 }, |
| supportTickets: { min: 5, max: 25 }, |
| nps: { min: 10, max: 70 }, |
| }, |
| }; |
|
|
| |
| const SEGMENT_WEIGHTS: Record<SegmentName, number> = { |
| Enterprise: 0.15, |
| "Mid-Market": 0.25, |
| SMB: 0.35, |
| Startup: 0.25, |
| }; |
|
|
| |
| function gaussianRandom(mean: number, stdDev: number): number { |
| const u1 = Math.random(); |
| const u2 = Math.random(); |
| const z0 = Math.sqrt(-2.0 * Math.log(u1)) * Math.cos(2.0 * Math.PI * u2); |
| return z0 * stdDev + mean; |
| } |
|
|
| |
| function generateClusteredValue(min: number, max: number): number { |
| const mean = (min + max) / 2; |
| const stdDev = (max - min) / 4; |
| const value = gaussianRandom(mean, stdDev); |
| return Math.max(min * 0.8, Math.min(max * 1.2, value)); |
| } |
|
|
| |
| function generateCompanyName(usedNames: Set<string>): string { |
| let attempts = 0; |
| while (attempts < 100) { |
| const prefix = PREFIXES[Math.floor(Math.random() * PREFIXES.length)]; |
| const core = CORES[Math.floor(Math.random() * CORES.length)]; |
| const suffix = SUFFIXES[Math.floor(Math.random() * SUFFIXES.length)]; |
| const name = `${prefix} ${core} ${suffix}`; |
| if (!usedNames.has(name)) { |
| usedNames.add(name); |
| return name; |
| } |
| attempts++; |
| } |
| |
| const prefix = PREFIXES[Math.floor(Math.random() * PREFIXES.length)]; |
| const core = CORES[Math.floor(Math.random() * CORES.length)]; |
| const num = Math.floor(Math.random() * 1000); |
| return `${prefix} ${core} ${num}`; |
| } |
|
|
| |
| function selectSegment(): SegmentName { |
| const rand = Math.random(); |
| let cumulative = 0; |
| for (const segment of SEGMENTS) { |
| cumulative += SEGMENT_WEIGHTS[segment]; |
| if (rand < cumulative) { |
| return segment; |
| } |
| } |
| return "SMB"; |
| } |
|
|
| |
| function generateCustomer(id: number, usedNames: Set<string>): Customer { |
| const segment = selectSegment(); |
| const center = CLUSTER_CENTERS[segment]; |
|
|
| return { |
| id: `cust-${id.toString().padStart(4, "0")}`, |
| name: generateCompanyName(usedNames), |
| segment, |
| annualRevenue: Math.round( |
| generateClusteredValue( |
| center.annualRevenue.min, |
| center.annualRevenue.max, |
| ), |
| ), |
| employeeCount: Math.round( |
| generateClusteredValue( |
| center.employeeCount.min, |
| center.employeeCount.max, |
| ), |
| ), |
| accountAge: Math.round( |
| generateClusteredValue(center.accountAge.min, center.accountAge.max), |
| ), |
| engagementScore: Math.round( |
| generateClusteredValue( |
| center.engagementScore.min, |
| center.engagementScore.max, |
| ), |
| ), |
| supportTickets: Math.round( |
| generateClusteredValue( |
| center.supportTickets.min, |
| center.supportTickets.max, |
| ), |
| ), |
| nps: Math.round(generateClusteredValue(center.nps.min, center.nps.max)), |
| }; |
| } |
|
|
| |
| export function generateCustomers(count: number = 250): Customer[] { |
| const usedNames = new Set<string>(); |
| const customers: Customer[] = []; |
|
|
| for (let i = 0; i < count; i++) { |
| customers.push(generateCustomer(i + 1, usedNames)); |
| } |
|
|
| return customers; |
| } |
|
|
| |
| export function generateSegmentSummaries( |
| customers: Customer[], |
| ): SegmentSummary[] { |
| const counts = new Map<string, number>(); |
|
|
| for (const customer of customers) { |
| counts.set(customer.segment, (counts.get(customer.segment) || 0) + 1); |
| } |
|
|
| return SEGMENTS.map((segment) => ({ |
| name: segment, |
| count: counts.get(segment) || 0, |
| color: SEGMENT_COLORS[segment], |
| })); |
| } |
|
|