| | import type { Customer, SegmentSummary, SegmentName } from "./types.ts"; |
| | import { SEGMENT_COLORS, SEGMENTS } from "./types.ts"; |
| |
|
| | |
| | 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], |
| | })); |
| | } |
| |
|