Synthetic Data Generator
blockaiGretel and Tonic-style LLM and mathematical synthetic dataset generator with differential privacy epsilon controls, custom schema field builders, K-anonymity policy toggles, live tabular CSV preview, and streaming JSON Lines export.
Installation
$pnpm dlx uipkge-ng@latest add synthetic-data-generator$npx uipkge-ng@latest add synthetic-data-generator$yarn dlx uipkge-ng@latest add synthetic-data-generator$bunx uipkge-ng@latest add synthetic-data-generatorManifest:
https://uipkge.dev/r/angular/synthetic-data-generator.jsonInstalls to:src/app/components/blocks/synthetic-data-generator/Variants
Loading interactive previews…
Loading interactive previews…
Schema
Type aliases exported from this item's source. Use these to shape the data you pass in.
SchemaFieldinterface SchemaField {
id: string
name: string
label: string
type: FieldDataType
description?: string
params: {
mean?: number
stdDev?: number
currency?: string
unit?: string
categories?: string[]
weights?: number[]
domain?: string
dateStart?: string
dateEnd?: string
}
}TemplatePresetinterface TemplatePreset {
id: string
name: string
description: string
category: string
fields: SchemaField[]
}GeneratedRecordinterface GeneratedRecord {
id: string
[key: string]: string | number
}FidelityMetricsinterface FidelityMetrics {
similarity: number
leakageRisk: string
correlationRetention: number
wassersteinDistance: number
laplaceNoiseStd: string
privacyTier: string
privacyBadgeColor: string
}Files installed (5)
src/app/components/blocks/synthetic-data-generator/synthetic-data-generator.component.ts35.5 kB
import { Component, DestroyRef, Input, type OnInit, computed, inject, signal, ChangeDetectionStrategy, } from '@angular/core' import { Check, Code2, Cpu, Database, Download, FileSpreadsheet, Info, Layers, Lock, LucideAngularModule, Plus, RefreshCw, ShieldCheck, Sliders, Sparkles, Trash2, } from 'lucide-angular' import { cn } from '@/lib/utils' import { UiBadgeComponent } from '@/ui/badge/badge.component' import { UiButtonComponent } from '@/ui/button/button.component' import { UiCardComponent, UiCardContentComponent, UiCardDescriptionComponent, UiCardHeaderComponent, UiCardTitleComponent, } from '@/ui/card/card.component' import { UiInputComponent } from '@/ui/input/input.component' import { UiSelectComponent, UiSelectContentComponent, UiSelectItemComponent, UiSelectTriggerComponent, UiSelectValueComponent, } from '@/ui/select/select.component' import { UiSeparatorComponent } from '@/ui/separator/separator.component' import { UiSliderComponent } from '@/ui/slider/slider.component' import { UiSwitchComponent } from '@/ui/switch/switch.component' import type { FieldDataType, FidelityMetrics, GeneratedRecord, SchemaField, SyntheticDataGeneratorProps, TemplatePreset, } from './synthetic-data-types' import { boxMullerRandom as boxMullerRandomHelper, CARD_BRANDS, FIRST_NAMES, LAST_NAMES, TEMPLATES, } from './synthetic-data-templates' import { UiSyntheticDataPreviewComponent, type SyntheticDataPreviewTab } from './synthetic-data-preview.component' export type { FieldDataType, FidelityMetrics, GeneratedRecord, SchemaField, SyntheticDataGeneratorProps, TemplatePreset, } const ROW_PRESETS = [100, 1000, 5000, 10000, 50000] const EPSILON_PRESETS = [ { eps: 0.5, label: 'Healthcare' }, { eps: 1.2, label: 'Standard' }, { eps: 3.0, label: 'Analytics' }, ] const FIELD_TYPE_BADGE: Record<FieldDataType, string> = { gaussian_float: 'border-info/20 bg-info/10 text-info', name: 'border-chart-1/20 bg-chart-1/10 text-chart-1', email: 'border-chart-2/20 bg-chart-2/10 text-chart-2', credit_card: 'border-warning/20 bg-warning/10 text-warning', category_weights: 'border-success/20 bg-success/10 text-success', date_range: 'border-destructive/20 bg-destructive/10 text-destructive', } const FORMAT_BUTTON = 'flex cursor-pointer items-center gap-1.5 rounded-md px-2.5 py-1.5 font-medium transition-colors' function cloneFields(key: string): SchemaField[] { const tpl = TEMPLATES[key] return tpl?.fields ? (JSON.parse(JSON.stringify(tpl.fields)) as SchemaField[]) : [] } /** * Synthetic dataset generator & privacy engine (Gretel / Tonic style). Port of the React * `SyntheticDataGenerator` 1:1: schema template picker, CSV / JSON Lines format toggle, row * volume slider + presets, differential-privacy epsilon budget, a custom schema field builder, * PII anonymization switches, and the live preview column (`ui-synthetic-data-preview`). * `initialTemplate`, `initialRowCount` and `initialEpsilon` seed the state once. */ @Component({ changeDetection: ChangeDetectionStrategy.Eager, selector: 'ui-synthetic-data-generator, [ui-synthetic-data-generator]', standalone: true, // React renders the root <div> itself: the host stays out of layout and `class` goes to the root. host: { '[attr.class]': '"contents"' }, imports: [ LucideAngularModule, UiBadgeComponent, UiButtonComponent, UiCardComponent, UiCardContentComponent, UiCardDescriptionComponent, UiCardHeaderComponent, UiCardTitleComponent, UiInputComponent, UiSelectComponent, UiSelectContentComponent, UiSelectItemComponent, UiSelectTriggerComponent, UiSelectValueComponent, UiSeparatorComponent, UiSliderComponent, UiSwitchComponent, UiSyntheticDataPreviewComponent, ], template: ` <div data-slot="synthetic-data-generator" [class]="rootClass"> <!-- TOP HEADER HERO --> <div ui-card class="border-border overflow-hidden shadow-xs"> <div ui-card-header class="border-border/60 bg-muted/20 border-b pb-4"> <div class="flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between"> <div class="space-y-1"> <div class="flex flex-wrap items-center gap-2"> <div class="bg-primary/10 text-primary flex size-8 items-center justify-center rounded-lg"> <lucide-icon [img]="Cpu" class="size-4.5" /> </div> <h3 ui-card-title class="text-lg font-bold tracking-tight md:text-xl"> Synthetic Dataset Generator & Privacy Engine </h3> <span ui-badge variant="outline" class="gap-1 text-xs font-normal"> <span class="bg-success size-1.5 animate-pulse rounded-full"></span> Differential Privacy Enabled </span> </div> <p ui-card-description class="text-xs md:text-sm"> Mathematical & LLM-guided synthetic tabular dataset synthesizer with configurable (ε, δ)-DP noise injection. </p> </div> <!-- Header Right Quick Actions --> <div class="flex flex-wrap items-center gap-2.5"> <!-- Format Selector Segmented Control --> <div class="bg-muted/60 border-border inline-flex items-center rounded-lg border p-0.5 text-xs"> <button type="button" [class]="formatButtonClass('csv')" (click)="outputFormat.set('csv')"> <lucide-icon [img]="FileSpreadsheet" class="size-3.5" /> <span>Tabular CSV</span> </button> <button type="button" [class]="formatButtonClass('jsonl')" (click)="outputFormat.set('jsonl')"> <lucide-icon [img]="Code2" class="size-3.5" /> <span>JSON Lines</span> </button> </div> <!-- Download Button --> <button ui-button aria-label="Download attachment" variant="outline" size="sm" class="h-9 gap-1.5 text-xs font-medium" (click)="handleDownloadDataset()" > <lucide-icon [img]="Download" class="size-3.5" /> <span>Download Dataset</span> </button> <!-- Primary Generate Batch Button --> <button ui-button variant="default" size="sm" class="h-9 gap-1.5 px-4 text-xs font-semibold shadow-xs transition-[color,background-color,border-color,box-shadow,opacity,transform,scale,translate,rotate] active:scale-95" [disabled]="isGenerating()" (click)="triggerRegeneration()" > @if (isGenerating()) { <!-- [class] (not class=) feeds only the svg, so the host does not spin as well. --> <lucide-icon [img]="RefreshCw" [class]="'size-3.5 animate-spin'" /> } @else { <lucide-icon [img]="Sparkles" class="size-3.5" /> } <span>{{ isGenerating() ? 'Synthesizing...' : 'Generate Synthetic Batch' }}</span> </button> </div> </div> <!-- Template Selector Bar --> <div class="border-border/50 bg-background/50 mt-3 flex flex-wrap items-center justify-between gap-3 rounded-lg border px-3.5 py-2" > <div class="flex items-center gap-2"> <lucide-icon [img]="Layers" class="text-muted-foreground size-4" /> <span class="text-foreground text-xs font-semibold">Schema Template:</span> </div> <div class="w-full min-w-0 sm:max-w-md sm:flex-1"> <ui-select [value]="selectedTemplateKey()" (valueChange)="handleTemplateChange($event)"> <button ui-select-trigger class="bg-background h-8.5 w-full text-xs font-medium [&>span]:truncate [&>svg]:shrink-0" > <span ui-select-value placeholder="Select Schema Template"></span> </button> <ui-select-content> @for (tpl of templates; track tpl[0]) { <ui-select-item [value]="tpl[0]" class="text-xs">{{ tpl[1].name }}</ui-select-item> } </ui-select-content> </ui-select> </div> <div class="text-muted-foreground hidden items-center gap-2 text-xs sm:flex"> <span ui-badge variant="secondary" class="px-2 py-0.5 text-xs font-normal"> {{ templateCategory() }} </span> <span>•</span> <span class="font-mono tabular-nums">{{ activeFields().length }} fields configured</span> </div> </div> </div> </div> <!-- DOWNLOAD SUCCESS NOTIFICATION --> @if (downloadToast(); as toast) { <div class="border-success/30 bg-success/10 text-success text-success flex items-center justify-between rounded-lg border px-4 py-2.5 text-xs font-medium shadow-xs" > <div class="flex items-center gap-2"> <lucide-icon [img]="Check" class="size-4 stroke-[2.5]" /> <span>{{ toast }}</span> </div> <span ui-badge variant="outline" class="border-success/40 font-mono text-xs"> {{ estimatedFileSize() }} </span> </div> } <!-- 2-COLUMN STUDIO GENERATOR --> <div class="grid grid-cols-1 gap-6 lg:grid-cols-12"> <!-- LEFT COLUMN: SCHEMA & PRIVACY CONFIGURATION (40% / 5 Cols) --> <div class="space-y-6 lg:col-span-5"> <!-- CARD 1: ROW COUNT & BATCH SCALE --> <div ui-card class="border-border shadow-xs"> <div ui-card-header class="border-border/50 border-b pb-3"> <div class="flex items-center justify-between"> <div class="flex items-center gap-2"> <div class="bg-muted text-muted-foreground flex size-7 items-center justify-center rounded-md"> <lucide-icon [img]="Database" class="size-3.5" /> </div> <h3 ui-card-title class="text-sm font-semibold">Dataset Generation Volume</h3> </div> <span ui-badge variant="secondary" class="font-mono text-xs font-semibold tabular-nums"> {{ rowCount().toLocaleString() }} rows </span> </div> </div> <div ui-card-content class="space-y-4 pt-4"> <!-- Row Slider --> <div class="space-y-2"> <div class="text-muted-foreground flex items-center justify-between text-xs"> <span>100 rows</span> <span class="text-foreground font-mono font-medium">Estimated: {{ estimatedFileSize() }}</span> <span>50,000 rows</span> </div> <ui-slider [value]="[rowCount()]" [min]="100" [max]="50000" [step]="100" class="w-full py-1" (valueChange)="onRowCountChange($event)" /> </div> <!-- Quick Preset Chips --> <div class="flex flex-wrap items-center gap-1.5 pt-1"> <span class="text-muted-foreground mr-1 text-xs">Presets:</span> @for (preset of rowPresets; track preset) { <button type="button" [class]="rowPresetClass(preset)" (click)="rowCount.set(preset)"> {{ presetLabel(preset) }} </button> } </div> </div> </div> <!-- CARD 2: DIFFERENTIAL PRIVACY EPSILON (ε) CONTROLS --> <div ui-card class="border-border shadow-xs"> <div ui-card-header class="border-border/50 border-b pb-3"> <div class="flex items-center justify-between"> <div class="flex items-center gap-2"> <div class="bg-primary/10 text-primary flex size-7 items-center justify-center rounded-md"> <lucide-icon [img]="ShieldCheck" class="size-3.5" /> </div> <div> <h3 ui-card-title class="text-sm font-semibold">Differential Privacy Budget</h3> <p ui-card-description class="text-xs">Laplace (ε, δ)-DP noise calibration</p> </div> </div> <span ui-badge variant="outline" class="font-mono text-xs font-bold tabular-nums"> ε = {{ epsilon().toFixed(1) }} </span> </div> </div> <div ui-card-content class="space-y-4 pt-4"> <!-- Epsilon Slider --> <div class="space-y-2"> <div class="flex items-center justify-between text-xs"> <span class="text-success font-medium">ε=0.1 (High Privacy)</span> <span class="text-warning font-medium">ε=5.0 (High Fidelity)</span> </div> <ui-slider [value]="[epsilon()]" [min]="0.1" [max]="5.0" [step]="0.1" class="w-full py-1" (valueChange)="onEpsilonChange($event)" /> </div> <!-- Epsilon Interpretation Banner --> <div [class]="privacyBannerClass()"> <!-- [class] (not class=) feeds only the svg, so mt-0.5 is not applied twice (host + svg). --> <lucide-icon [img]="Info" [class]="'mt-0.5 size-4 shrink-0'" /> <div class="space-y-1"> <div class="font-semibold">{{ fidelityMetrics().privacyTier }}</div> <p class="text-muted-foreground text-xs leading-relaxed"> Laplace noise scale: <span class="text-foreground font-mono font-medium" >σ=±{{ fidelityMetrics().laplaceNoiseStd }}</span > • Delta: <span class="text-foreground font-mono font-medium">δ=10⁻⁵</span> • Zero memorization guarantee. </p> </div> </div> <!-- Quick Epsilon Presets --> <div class="grid grid-cols-3 gap-2 pt-1"> @for (p of epsilonPresets; track p.eps) { <button type="button" [class]="epsilonPresetClass(p.eps)" (click)="epsilon.set(p.eps)"> <div class="font-bold">ε = {{ p.eps.toFixed(1) }}</div> <div class="text-muted-foreground text-xs">{{ p.label }}</div> </button> } </div> </div> </div> <!-- CARD 3: FIELD SCHEMAS GENERATOR --> <div ui-card class="border-border shadow-xs"> <div ui-card-header class="border-border/50 border-b pb-3"> <div class="flex items-center justify-between"> <div class="flex items-center gap-2"> <div class="bg-muted text-muted-foreground flex size-7 items-center justify-center rounded-md"> <lucide-icon [img]="Sliders" class="size-3.5" /> </div> <div> <h3 ui-card-title class="text-sm font-semibold">Schema Fields Generator</h3> <p ui-card-description class="text-xs"> {{ activeFields().length }} active synthesizer distributions </p> </div> </div> <button ui-button variant="outline" size="sm" class="h-7.5 gap-1 px-2.5 text-xs" (click)="showAddField.set(!showAddField())" > <lucide-icon [img]="Plus" class="size-3.5" /> <span>{{ showAddField() ? 'Cancel' : 'Add Field' }}</span> </button> </div> </div> <div ui-card-content class="space-y-3 pt-4"> <!-- Add Field Form --> @if (showAddField()) { <div class="border-border/80 bg-muted/30 space-y-3 rounded-lg border p-3.5 text-xs"> <div class="text-foreground font-semibold">Configure New Synthetic Field</div> <div class="grid grid-cols-1 gap-2 sm:grid-cols-2"> <div class="space-y-1"> <label class="text-muted-foreground text-xs">Field Identifier</label> <ui-input [value]="newFieldName()" (valueChange)="newFieldName.set($event)" placeholder="e.g. account_balance" class="h-8 text-xs" /> </div> <div class="space-y-1"> <label class="text-muted-foreground text-xs">Data Distribution Type</label> <ui-select [value]="newFieldType()" (valueChange)="setNewFieldType($event)"> <button ui-select-trigger class="bg-background h-8 w-full min-w-0 text-xs"> <span ui-select-value placeholder="Type"></span> </button> <ui-select-content> <ui-select-item value="gaussian_float" class="text-xs" >Float Gaussian Distribution</ui-select-item > <ui-select-item value="name" class="text-xs">Full Name Faker</ui-select-item> <ui-select-item value="email" class="text-xs">Email Faker</ui-select-item> <ui-select-item value="credit_card" class="text-xs">Credit Card Faker</ui-select-item> <ui-select-item value="category_weights" class="text-xs">Category Weights</ui-select-item> <ui-select-item value="date_range" class="text-xs">Date Range</ui-select-item> </ui-select-content> </ui-select> </div> </div> <div class="flex justify-end gap-2 pt-1"> <button ui-button variant="ghost" size="sm" class="h-7.5 text-xs" (click)="showAddField.set(false)"> Cancel </button> <button ui-button variant="default" size="sm" class="h-7.5 text-xs" [disabled]="!newFieldName().trim()" (click)="handleAddCustomField()" > Add to Schema </button> </div> </div> } <!-- Fields List --> <div class="divide-border/60 max-h-[320px] space-y-2 overflow-y-auto pr-1"> @for (field of activeFields(); track field.id) { <div class="group border-border/70 bg-card hover:bg-muted/20 flex items-center justify-between rounded-lg border p-2.5 transition-colors" > <div class="min-w-0 space-y-0.5"> <div class="flex items-center gap-2"> <span class="text-foreground font-mono text-xs font-semibold">{{ field.name }}</span> <span ui-badge variant="outline" [class]="fieldTypeBadgeClass(field.type)"> {{ field.type.replace('_', ' ') }} </span> </div> <p class="text-muted-foreground truncate text-xs"> {{ field.description || 'Configured synthesizer parameter' }} </p> </div> <button ui-button variant="ghost" size="sm" class="text-muted-foreground hover:text-destructive h-7 w-7 p-0 opacity-80 group-hover:opacity-100" aria-label="Delete field" [disabled]="activeFields().length <= 2" (click)="removeField(field.id)" > <lucide-icon [img]="Trash2" class="size-3.5" /> </button> </div> } </div> </div> </div> <!-- CARD 4: PII ANONYMIZATION POLICY SWITCHES --> <div ui-card class="border-border shadow-xs"> <div ui-card-header class="border-border/50 border-b pb-3"> <div class="flex items-center gap-2"> <div class="bg-muted text-muted-foreground flex size-7 items-center justify-center rounded-md"> <lucide-icon [img]="Lock" class="size-3.5" /> </div> <div> <h3 ui-card-title class="text-sm font-semibold">PII Anonymization Policy</h3> <p ui-card-description class="text-xs">Zero linkage & re-identification guarantees</p> </div> </div> </div> <div ui-card-content class="space-y-4 pt-4"> <!-- K-Anonymity Switch --> <div class="flex items-start justify-between gap-3"> <div class="space-y-0.5"> <div class="text-foreground text-xs font-semibold">K-Anonymity (k = 5 Cohort Enforcement)</div> <p class="text-muted-foreground text-xs leading-relaxed"> Generalizes quasi-identifiers so each demographic bucket contains ≥ 5 records. </p> </div> <button ui-switch [checked]="kAnonymity()" (checkedChange)="kAnonymity.set($event)"></button> </div> <div ui-separator></div> <!-- Salted Hash Switch --> <div class="flex items-start justify-between gap-3"> <div class="space-y-0.5"> <div class="text-foreground text-xs font-semibold">Salted HMAC-SHA256 for Primary Keys</div> <p class="text-muted-foreground text-xs leading-relaxed"> Deterministically pseudonymizes primary keys with ephemeral cryptographic salt. </p> </div> <button ui-switch [checked]="saltedHash()" (checkedChange)="saltedHash.set($event)"></button> </div> <div ui-separator></div> <!-- Noise Injection Switch --> <div class="flex items-start justify-between gap-3"> <div class="space-y-0.5"> <div class="text-foreground text-xs font-semibold">Differential Privacy Gaussian Noise</div> <p class="text-muted-foreground text-xs leading-relaxed"> Injects calibrated noise calibrated to global sensitivity (Δf / ε). </p> </div> <button ui-switch [checked]="noiseMechanism()" (checkedChange)="noiseMechanism.set($event)"></button> </div> </div> </div> </div> <!-- RIGHT COLUMN: LIVE GENERATED SYNTHETIC DATA PREVIEW (60% / 7 Cols) --> <div class="space-y-6 lg:col-span-7"> <ui-synthetic-data-preview [previewTab]="previewTab()" (previewTabChange)="previewTab.set($event)" [previewRecords]="previewRecords()" [activeFields]="activeFields()" [fidelityMetrics]="fidelityMetrics()" [epsilon]="epsilon()" [rowCount]="rowCount()" [outputFormat]="outputFormat()" [generationSeed]="generationSeed()" [copied]="copied()" (copyJsonLines)="copyJsonLines()" /> </div> </div> </div> `, }) export class UiSyntheticDataGeneratorComponent implements OnInit { @Input('class') className?: string @Input() initialTemplate = 'customer-transactions' @Input() initialRowCount = 1000 @Input() initialEpsilon = 1.2 // --- STATE --- readonly selectedTemplateKey = signal<string>('customer-transactions') readonly outputFormat = signal<'csv' | 'jsonl'>('csv') readonly rowCount = signal<number>(1000) readonly epsilon = signal<number>(1.2) readonly kAnonymity = signal<boolean>(true) readonly saltedHash = signal<boolean>(true) readonly noiseMechanism = signal<boolean>(true) readonly activeFields = signal<SchemaField[]>(cloneFields('customer-transactions')) readonly isGenerating = signal<boolean>(false) readonly copied = signal<boolean>(false) readonly downloadToast = signal<string | null>(null) readonly previewTab = signal<SyntheticDataPreviewTab>('table') readonly generationSeed = signal<number>(48291) // Field addition state readonly showAddField = signal<boolean>(false) readonly newFieldName = signal<string>('') readonly newFieldType = signal<FieldDataType>('gaussian_float') protected readonly templates = Object.entries(TEMPLATES) protected readonly rowPresets = ROW_PRESETS protected readonly epsilonPresets = EPSILON_PRESETS protected readonly Check = Check protected readonly Code2 = Code2 protected readonly Cpu = Cpu protected readonly Database = Database protected readonly Download = Download protected readonly FileSpreadsheet = FileSpreadsheet protected readonly Info = Info protected readonly Layers = Layers protected readonly Lock = Lock protected readonly Plus = Plus protected readonly RefreshCw = RefreshCw protected readonly ShieldCheck = ShieldCheck protected readonly Sliders = Sliders protected readonly Sparkles = Sparkles protected readonly Trash2 = Trash2 private readonly timers = new Set<ReturnType<typeof setTimeout>>() constructor() { inject(DestroyRef).onDestroy(() => this.timers.forEach((t) => clearTimeout(t))) } ngOnInit(): void { this.selectedTemplateKey.set(this.initialTemplate) this.rowCount.set(this.initialRowCount) this.epsilon.set(this.initialEpsilon) this.activeFields.set(cloneFields(this.initialTemplate)) } get rootClass(): string { return cn('mx-auto w-full max-w-7xl space-y-6', this.className) } protected readonly templateCategory = computed(() => TEMPLATES[this.selectedTemplateKey()]?.category) // Dynamic preview records computation readonly previewRecords = computed<GeneratedRecord[]>(() => { const generationSeed = this.generationSeed() const saltedHash = this.saltedHash() const noiseMechanism = this.noiseMechanism() const epsilon = this.epsilon() const rows: GeneratedRecord[] = [] const count = 6 // Box-Muller Gaussian random generator with differential privacy noise const boxMullerRandom = (mean: number, stdDev: number, seedOffset: number): number => boxMullerRandomHelper(mean, stdDev, seedOffset, generationSeed, noiseMechanism, epsilon) for (let i = 0; i < count; i++) { const fn = FIRST_NAMES[(i * 3 + generationSeed) % FIRST_NAMES.length]! const ln = LAST_NAMES[(i * 5 + generationSeed + 2) % LAST_NAMES.length]! const fullName = `${fn} ${ln}` const baseSlug = `${fn.charAt(0).toLowerCase()}.${ln.toLowerCase().replace(/[^a-z]/g, '')}` const record: GeneratedRecord = { id: saltedHash ? `syn_${((generationSeed * 9301 + i * 49297) % 233280).toString(16).padStart(6, '0')}` : `usr_${1000 + i}`, } for (const field of this.activeFields()) { if (field.name.includes('id') || field.name.includes('uuid') || field.name.includes('token')) { record[field.name] = record.id continue } switch (field.type) { case 'name': record[field.name] = fullName break case 'email': { const dom = field.params.domain || 'synthetic-vault.io' record[field.name] = `${baseSlug}@${dom}` break } case 'credit_card': { const last4 = (((generationSeed * 7 + i * 1337 + 1000) % 9000) + 1000).toString() const brand = CARD_BRANDS[i % CARD_BRANDS.length] record[field.name] = `${brand} •••• ${last4}` break } case 'gaussian_float': { const m = field.params.mean ?? 100 const s = field.params.stdDev ?? 20 const rawVal = boxMullerRandom(m, s, i) const val = Math.max(m * 0.1, rawVal) if (field.params.currency) { record[field.name] = `${field.params.currency}${val.toFixed(2)}` } else if (field.params.unit) { record[field.name] = `${val.toFixed(1)} ${field.params.unit}` } else { record[field.name] = parseFloat(val.toFixed(2)) } break } case 'category_weights': { const cats = field.params.categories || ['Standard', 'Premium'] const catIndex = (i + Math.floor(generationSeed / 100)) % cats.length record[field.name] = cats[catIndex]! break } case 'date_range': { const day = ((10 + i * 4) % 28) + 1 const month = (i % 8) + 1 const dayStr = day.toString().padStart(2, '0') const monthStr = month.toString().padStart(2, '0') const hour = (8 + i * 3) % 24 const hourStr = hour.toString().padStart(2, '0') record[field.name] = `2025-${monthStr}-${dayStr} ${hourStr}:24:18 UTC` break } default: record[field.name] = `value_${i}` } } rows.push(record) } return rows }) // Statistical fidelity metrics readonly fidelityMetrics = computed<FidelityMetrics>(() => { const epsilon = this.epsilon() const rawSimilarity = Math.min(99.4, Math.max(82.0, 99.2 - ((5.0 - epsilon) / 4.9) * 9.6)) const similarity = parseFloat(rawSimilarity.toFixed(1)) const leakageRisk = this.kAnonymity() && this.saltedHash() && epsilon <= 3.5 ? 0.0 : Math.max(0, (epsilon - 3.5) * 0.04) const correlationRetention = parseFloat(Math.min(99.1, 98.6 - ((5.0 - epsilon) / 4.9) * 6.2).toFixed(1)) const wassersteinDistance = parseFloat((0.032 + ((5.0 - epsilon) / 4.9) * 0.024).toFixed(3)) const laplaceNoiseStd = (0.12 / Math.max(0.1, epsilon)).toFixed(3) let privacyTier = 'Balanced Privacy & Utility' let privacyBadgeColor = 'bg-info/10 text-info border-info/30' if (epsilon <= 0.5) { privacyTier = 'Maximum Privacy / High DP Noise' privacyBadgeColor = 'bg-success/10 text-success border-success/30' } else if (epsilon > 2.5) { privacyTier = 'High Statistical Utility / Minimal Noise' privacyBadgeColor = 'bg-warning/10 text-warning border-warning/30' } return { similarity, leakageRisk: leakageRisk.toFixed(2), correlationRetention, wassersteinDistance, laplaceNoiseStd, privacyTier, privacyBadgeColor, } }) // Estimated file size calculation readonly estimatedFileSize = computed(() => { const bytesPerRow = this.outputFormat() === 'csv' ? 180 : 380 const totalKb = (this.rowCount() * bytesPerRow) / 1024 if (totalKb < 1024) { return `~${totalKb.toFixed(1)} KB` } return `~${(totalKb / 1024).toFixed(2)} MB` }) protected formatButtonClass(format: 'csv' | 'jsonl'): string { return cn( FORMAT_BUTTON, this.outputFormat() === format ? 'bg-background text-foreground shadow-xs' : 'text-muted-foreground hover:text-foreground', ) } protected rowPresetClass(preset: number): string { return cn( 'cursor-pointer rounded-md border px-2.5 py-1 text-xs font-medium transition-colors', this.rowCount() === preset ? 'bg-primary text-primary-foreground border-primary shadow-xs' : 'bg-muted/40 text-muted-foreground border-border hover:bg-muted hover:text-foreground', ) } protected presetLabel(preset: number): string { return preset >= 1000 ? `${preset / 1000}k` : String(preset) } protected epsilonPresetClass(eps: number): string { return cn( 'cursor-pointer rounded-lg border p-2 text-center text-xs transition-colors', this.epsilon() === eps ? 'border-primary bg-primary/10 text-primary font-semibold' : 'border-border bg-background text-muted-foreground hover:bg-muted/40', ) } protected privacyBannerClass(): string { return cn('flex items-start gap-2.5 rounded-lg border p-3 text-xs', this.fidelityMetrics().privacyBadgeColor) } protected fieldTypeBadgeClass(type: FieldDataType): string { return cn('px-1.5 py-0 text-xs font-semibold tracking-wider uppercase', FIELD_TYPE_BADGE[type]) } protected onRowCountChange(value: number[]): void { if (value[0] !== undefined) this.rowCount.set(value[0]) } protected onEpsilonChange(value: number[]): void { if (value[0] !== undefined) this.epsilon.set(value[0]) } protected setNewFieldType(value: string): void { this.newFieldType.set(value as FieldDataType) } // Handle template selection switch handleTemplateChange(key: string): void { this.selectedTemplateKey.set(key) if (TEMPLATES[key]) { this.activeFields.set(cloneFields(key)) this.triggerRegeneration() } } // Generation trigger triggerRegeneration(): void { this.isGenerating.set(true) this.generationSeed.set(Math.floor(Math.random() * 90000) + 10000) this.later(() => this.isGenerating.set(false), 450) } // Add field handler handleAddCustomField(): void { const name = this.newFieldName() if (!name.trim()) return const type = this.newFieldType() const cleanName = name.trim().toLowerCase().replace(/\s+/g, '_') const newField: SchemaField = { id: `f-custom-${Date.now()}`, name: cleanName, label: name.trim(), type, description: `Custom ${type} schema column`, params: type === 'gaussian_float' ? { mean: 500, stdDev: 120, currency: '$' } : type === 'category_weights' ? { categories: ['Alpha', 'Beta', 'Gamma'], weights: [0.5, 0.3, 0.2] } : {}, } this.activeFields.update((prev) => [...prev, newField]) this.newFieldName.set('') this.showAddField.set(false) this.triggerRegeneration() } // Remove field handler removeField(fieldId: string): void { if (this.activeFields().length <= 2) return this.activeFields.update((prev) => prev.filter((f) => f.id !== fieldId)) this.triggerRegeneration() } // Copy JSON lines async copyJsonLines(): Promise<void> { try { const jsonLinesText = this.previewRecords() .map((r) => JSON.stringify(r)) .join('\n') if (navigator?.clipboard?.writeText) { await navigator.clipboard.writeText(jsonLinesText) } this.copied.set(true) this.later(() => this.copied.set(false), 2000) } catch (err) { console.error('Failed to copy to clipboard', err) } } // Download simulation handleDownloadDataset(): void { const rowCount = this.rowCount() const filename = `synthetic_${this.selectedTemplateKey()}_${rowCount}_rows.${this.outputFormat() === 'csv' ? 'csv' : 'jsonl'}` this.downloadToast.set(`Downloaded ${rowCount.toLocaleString()} synthetic records (${filename})`) this.later(() => this.downloadToast.set(null), 3500) } private later(fn: () => void, ms: number): void { const t = setTimeout(() => { this.timers.delete(t) fn() }, ms) this.timers.add(t) } }src/app/components/blocks/synthetic-data-generator/synthetic-data-preview.component.ts17.9 kB
import { Component, EventEmitter, Input, Output, ChangeDetectionStrategy } from '@angular/core' import { BarChart3, Binary, Check, Code2, Copy, LucideAngularModule, Shield, ShieldCheck, Table, Zap, } from 'lucide-angular' import { UiBadgeComponent } from '@/ui/badge/badge.component' import { UiButtonComponent } from '@/ui/button/button.component' import { UiCardComponent, UiCardContentComponent, UiCardDescriptionComponent, UiCardFooterComponent, UiCardHeaderComponent, UiCardTitleComponent, } from '@/ui/card/card.component' import { UiProgressComponent } from '@/ui/progress/progress.component' import { UiTableBodyComponent, UiTableCellComponent, UiTableComponent, UiTableHeadComponent, UiTableHeaderComponent, UiTableRowComponent, } from '@/ui/table/table.component' import { UiTabsComponent, UiTabsListComponent, UiTabsTriggerComponent } from '@/ui/tabs/tabs.component' import type { FidelityMetrics, GeneratedRecord, SchemaField } from './synthetic-data-types' export type SyntheticDataPreviewTab = 'table' | 'json' | 'metrics' /** How a preview cell renders its value (React's nested ternary in SyntheticDataPreview). */ type CellKind = 'key' | 'category' | 'float' | 'card' | 'email' | 'text' /** * Right-hand column of the synthetic data generator: the fidelity / privacy gauge banner and * the live preview card with Table / JSONL / Marginals views. Port of the React * `SyntheticDataPreview` 1:1; the parent owns all state and passes it in. */ @Component({ changeDetection: ChangeDetectionStrategy.Eager, selector: 'ui-synthetic-data-preview, [ui-synthetic-data-preview]', standalone: true, host: { class: 'contents' }, imports: [ LucideAngularModule, UiBadgeComponent, UiButtonComponent, UiCardComponent, UiCardContentComponent, UiCardDescriptionComponent, UiCardFooterComponent, UiCardHeaderComponent, UiCardTitleComponent, UiProgressComponent, UiTableComponent, UiTableHeaderComponent, UiTableBodyComponent, UiTableRowComponent, UiTableHeadComponent, UiTableCellComponent, UiTabsComponent, UiTabsListComponent, UiTabsTriggerComponent, ], template: ` <div class="space-y-6"> <!-- TOP GAUGE BANNER: STATISTICAL FIDELITY VS PRIVACY --> <div ui-card class="border-border overflow-hidden shadow-xs"> <div class="border-border/60 bg-success/10 dark:bg-success/10 border-b px-4 py-3"> <div class="flex flex-wrap items-center justify-between gap-2"> <div class="flex items-center gap-2"> <lucide-icon [img]="ShieldCheck" class="text-success size-4.5" /> <span class="text-foreground font-mono text-sm font-bold tracking-tight"> {{ fidelityMetrics.similarity }}% Distribution Similarity • {{ fidelityMetrics.leakageRisk }}% PII Leakage Risk </span> </div> <span ui-badge variant="outline" class="bg-background border-success/40 text-success text-xs font-semibold"> ε-DP Bound: ≤ {{ epsilon.toFixed(1) }} </span> </div> </div> <div ui-card-content class="grid grid-cols-1 gap-4 pt-4 sm:grid-cols-3"> <!-- Metric 1: Fidelity Score --> <div class="border-border/60 bg-muted/10 space-y-1.5 rounded-lg border p-3"> <div class="text-muted-foreground flex items-center justify-between text-xs"> <span>Fidelity Score</span> <lucide-icon [img]="Zap" class="text-warning size-3.5" /> </div> <div class="text-foreground font-mono text-lg font-bold tabular-nums"> {{ fidelityMetrics.similarity }}% </div> <div ui-progress [value]="fidelityMetrics.similarity" class="h-1.5"></div> </div> <!-- Metric 2: Correlation Preservation --> <div class="border-border/60 bg-muted/10 space-y-1.5 rounded-lg border p-3"> <div class="text-muted-foreground flex items-center justify-between text-xs"> <span>Covariance Retention</span> <lucide-icon [img]="BarChart3" class="text-info size-3.5" /> </div> <div class="text-foreground font-mono text-lg font-bold tabular-nums"> {{ fidelityMetrics.correlationRetention }}% </div> <div ui-progress [value]="fidelityMetrics.correlationRetention" class="h-1.5"></div> </div> <!-- Metric 3: Wasserstein Distance --> <div class="border-border/60 bg-muted/10 space-y-1.5 rounded-lg border p-3"> <div class="text-muted-foreground flex items-center justify-between text-xs"> <span>Wasserstein Distance</span> <lucide-icon [img]="Binary" class="text-chart-1 size-3.5" /> </div> <div class="text-foreground font-mono text-lg font-bold tabular-nums"> {{ fidelityMetrics.wassersteinDistance }} </div> <div class="text-muted-foreground text-xs">Low error bound (< 0.05)</div> </div> </div> </div> <!-- PREVIEW TABS CARD --> <div ui-card class="border-border shadow-xs"> <div ui-card-header class="border-border/50 border-b pb-3"> <div class="flex flex-wrap items-center justify-between gap-2"> <div class="flex items-center gap-2"> <div class="bg-muted text-muted-foreground flex size-7 items-center justify-center rounded-md"> <lucide-icon [img]="TableIcon" class="size-3.5" /> </div> <div> <h3 ui-card-title class="text-sm font-semibold">Live Generated Synthetic Data Preview</h3> <p ui-card-description class="text-xs"> Real-time sampler showing first {{ previewRecords.length }} synthesized records </p> </div> </div> <!-- View Switcher --> <div ui-tabs [value]="previewTab" (valueChange)="onTabChange($event)" class="w-auto"> <div ui-tabs-list class="h-8"> <!-- [class] (not class=) feeds only the svg, so mr-1 is not applied twice (host + svg). --> <button ui-tabs-trigger value="table" class="h-7 px-2.5 text-xs"> <lucide-icon [img]="TableIcon" [class]="'mr-1 size-3'" /> <span>Table</span> </button> <button ui-tabs-trigger value="json" class="h-7 px-2.5 text-xs"> <lucide-icon [img]="Code2" [class]="'mr-1 size-3'" /> <span>JSONL</span> </button> <button ui-tabs-trigger value="metrics" class="h-7 px-2.5 text-xs"> <lucide-icon [img]="BarChart3" [class]="'mr-1 size-3'" /> <span>Marginals</span> </button> </div> </div> </div> </div> <div ui-card-content class="pt-4"> <!-- TAB 1: TABULAR PREVIEW --> @if (previewTab === 'table') { <div class="space-y-3"> <div class="border-border overflow-hidden rounded-lg border"> <div class="overflow-x-auto"> <ui-table class="min-w-full text-xs"> <thead ui-table-header class="bg-muted/40"> <tr ui-table-row> @for (f of activeFields; track f.id) { <th ui-table-head class="text-foreground font-mono text-xs font-semibold whitespace-nowrap"> {{ f.name }} </th> } </tr> </thead> <tbody ui-table-body> @for (row of previewRecords; track $index) { <tr ui-table-row class="hover:bg-muted/30 transition-colors"> @for (f of activeFields; track f.id) { <td ui-table-cell class="py-2.5 whitespace-nowrap"> @switch (cellKind(f)) { @case ('key') { <span class="text-muted-foreground font-mono font-medium">{{ row[f.name] }}</span> } @case ('category') { <span ui-badge variant="secondary" class="font-mono text-xs font-medium"> {{ row[f.name] }} </span> } @case ('float') { <span class="text-foreground font-mono font-semibold tabular-nums"> {{ row[f.name] }} </span> } @case ('card') { <span class="text-foreground font-mono text-xs font-medium">{{ row[f.name] }}</span> } @case ('email') { <span class="text-primary font-mono text-xs font-normal">{{ row[f.name] }}</span> } @default { <span class="text-foreground font-medium">{{ row[f.name] }}</span> } } </td> } </tr> } </tbody> </ui-table> </div> </div> <div class="text-muted-foreground flex flex-wrap items-center justify-between gap-2 px-1 text-xs"> <span class="flex items-center gap-1.5 font-mono tabular-nums"> <span class="bg-success size-1.5 rounded-full"></span> Showing {{ previewRecords.length }} sample records of {{ rowCount.toLocaleString() }} generated rows </span> <span class="text-muted-foreground font-mono text-xs">Seed: #{{ generationSeed }}</span> </div> </div> } <!-- TAB 2: JSON LINES PREVIEW --> @if (previewTab === 'json') { <div class="space-y-3"> <div class="flex items-center justify-between"> <span class="text-muted-foreground text-xs">Streaming JSON Lines formatted output:</span> <button ui-button variant="outline" size="sm" class="h-7.5 gap-1.5 px-2.5 text-xs font-medium" (click)="copyJsonLines.emit()" > @if (copied) { <lucide-icon [img]="Check" class="text-success size-3 stroke-[2.5]" /> } @else { <lucide-icon [img]="Copy" class="size-3" /> } <span>{{ copied ? 'Copied JSONL' : 'Copy JSON Lines' }}</span> </button> </div> <div class="border-border/80 bg-muted/40 max-h-[380px] overflow-x-auto overflow-y-auto rounded-lg border p-4 font-mono text-xs leading-relaxed" > @for (rec of previewRecords; track $index; let idx = $index) { <div class="hover:bg-muted/60 rounded px-1.5 py-1 whitespace-pre transition-colors"> <span class="text-muted-foreground select-none">{{ idx + 1 }} </span> <span class="text-foreground">{{ '{ ' }}</span> @for (entry of entries(rec); track entry[0]; let last = $last) { <span class="text-info font-semibold">"{{ entry[0] }}"</span> <span class="text-muted-foreground">: </span> <span [class]="isNumber(entry[1]) ? 'text-warning font-medium' : 'text-success'">{{ jsonValue(entry[1]) }}</span> @if (!last) { <span class="text-muted-foreground">, </span> } } <span class="text-foreground">{{ ' }' }}</span> </div> } </div> </div> } <!-- TAB 3: MARGINALS & FIDELITY DENSITY --> @if (previewTab === 'metrics') { <div class="space-y-4"> <div class="grid grid-cols-1 gap-3 sm:grid-cols-2"> <!-- Density Gauge 1 --> <div class="border-border/60 bg-muted/20 space-y-2 rounded-lg border p-3.5"> <div class="flex items-center justify-between text-xs"> <span class="text-foreground font-semibold">Gaussian Distribution Overlap</span> <span ui-badge variant="outline" class="font-mono text-xs">96.2%</span> </div> <p class="text-muted-foreground text-xs"> Kolmogorov-Smirnov distance between synthetic batch and continuous Gaussian ground truth. </p> <div ui-progress [value]="96.2" class="h-2"></div> </div> <!-- Density Gauge 2 --> <div class="border-border/60 bg-muted/20 space-y-2 rounded-lg border p-3.5"> <div class="flex items-center justify-between text-xs"> <span class="text-foreground font-semibold">Categorical Frequency Prior</span> <span ui-badge variant="outline" class="font-mono text-xs">98.4%</span> </div> <p class="text-muted-foreground text-xs"> Total variation distance across categorical weight assignments. </p> <div ui-progress [value]="98.4" class="h-2"></div> </div> </div> <!-- Mathematical Guarantees Breakdown --> <div class="border-border/60 bg-muted/10 space-y-2.5 rounded-lg border p-4 text-xs"> <div class="text-foreground flex items-center gap-1.5 font-semibold"> <lucide-icon [img]="Shield" class="text-primary size-4" /> <span>Mathematical Privacy Guarantees • Formal Specs</span> </div> <div class="text-muted-foreground grid grid-cols-1 gap-2 sm:grid-cols-2"> <div class="flex items-center gap-1.5"> <lucide-icon [img]="Check" class="text-success size-3.5" /> <span>Differential Privacy (ε = {{ epsilon.toFixed(1) }}, δ = 10⁻⁵)</span> </div> <div class="flex items-center gap-1.5"> <lucide-icon [img]="Check" class="text-success size-3.5" /> <span>K-Anonymity (k = 5) Cohort Clamping</span> </div> <div class="flex items-center gap-1.5"> <lucide-icon [img]="Check" class="text-success size-3.5" /> <span>Zero Direct Row Memorization</span> </div> <div class="flex items-center gap-1.5"> <lucide-icon [img]="Check" class="text-success size-3.5" /> <span>Ephemeral In-Memory Compute</span> </div> </div> </div> </div> } </div> <div ui-card-footer class="border-border/40 text-muted-foreground flex flex-wrap items-center justify-between border-t py-3 text-xs" > <span class="flex items-center gap-1.5"> <lucide-icon [img]="Zap" class="text-warning size-3.5" /> Powered by LLM Semantic Synthesis & Laplace Noise Engine </span> <span class="font-mono tabular-nums"> Output: {{ outputFormat.toUpperCase() }} • {{ activeFields.length }} fields • {{ rowCount.toLocaleString() }} records </span> </div> </div> </div> `, }) export class UiSyntheticDataPreviewComponent { @Input() previewTab: SyntheticDataPreviewTab = 'table' @Output() previewTabChange = new EventEmitter<SyntheticDataPreviewTab>() @Input() previewRecords: GeneratedRecord[] = [] @Input() activeFields: SchemaField[] = [] @Input() fidelityMetrics: FidelityMetrics = { similarity: 0, leakageRisk: '0.00', correlationRetention: 0, wassersteinDistance: 0, laplaceNoiseStd: '0.000', privacyTier: '', privacyBadgeColor: '', } @Input() epsilon = 0 @Input() rowCount = 0 @Input() outputFormat: 'csv' | 'jsonl' = 'csv' @Input() generationSeed = 0 @Input() copied = false @Output() copyJsonLines = new EventEmitter<void>() protected readonly BarChart3 = BarChart3 protected readonly Binary = Binary protected readonly Check = Check protected readonly Code2 = Code2 protected readonly Copy = Copy protected readonly Shield = Shield protected readonly ShieldCheck = ShieldCheck protected readonly TableIcon = Table protected readonly Zap = Zap protected onTabChange(value: string): void { this.previewTabChange.emit(value as SyntheticDataPreviewTab) } /** Keys (id / uuid / token columns) first, then by field type, exactly like React's ternary chain. */ protected cellKind(f: SchemaField): CellKind { if (f.name === 'id' || f.name.includes('id') || f.name.includes('uuid') || f.name.includes('token')) return 'key' if (f.type === 'category_weights') return 'category' if (f.type === 'gaussian_float') return 'float' if (f.type === 'credit_card') return 'card' if (f.type === 'email') return 'email' return 'text' } protected entries(rec: GeneratedRecord): [string, string | number][] { return Object.entries(rec) } protected isNumber(value: string | number): boolean { return typeof value === 'number' } protected jsonValue(value: string | number): string { return typeof value === 'number' ? String(value) : `"${value}"` } }src/app/components/blocks/synthetic-data-generator/synthetic-data-types.ts1.1 kB
export type FieldDataType = 'name' | 'email' | 'credit_card' | 'gaussian_float' | 'date_range' | 'category_weights' export interface SchemaField { id: string name: string label: string type: FieldDataType description?: string params: { mean?: number stdDev?: number currency?: string unit?: string categories?: string[] weights?: number[] domain?: string dateStart?: string dateEnd?: string } } export interface TemplatePreset { id: string name: string description: string category: string fields: SchemaField[] } export interface GeneratedRecord { id: string [key: string]: string | number } export interface FidelityMetrics { similarity: number leakageRisk: string correlationRetention: number wassersteinDistance: number laplaceNoiseStd: string privacyTier: string privacyBadgeColor: string } /** Inputs of `<ui-synthetic-data-generator>` (`class` is merged onto the root). */ export interface SyntheticDataGeneratorProps { class?: string initialTemplate?: string initialRowCount?: number initialEpsilon?: number }src/app/components/blocks/synthetic-data-generator/synthetic-data-templates.ts8.9 kB
import type { TemplatePreset } from './synthetic-data-types' export const TEMPLATES: Record<string, TemplatePreset> = { 'customer-transactions': { id: 'customer-transactions', name: 'Customer Transactions & Demographics Dataset', description: 'High-dimensional e-commerce customer cohort, synthetic PII, spending distribution, and country codes.', category: 'E-Commerce & Retail', fields: [ { id: 'f-id', name: 'cust_id', label: 'Customer ID', type: 'name', description: 'Salted pseudonymized primary key', params: {}, }, { id: 'f-name', name: 'full_name', label: 'Full Name', type: 'name', description: 'Synthetic person name generator', params: {}, }, { id: 'f-email', name: 'email_address', label: 'Synthetic Email', type: 'email', description: 'Faker email with domain isolation', params: { domain: 'syn-corp.io' }, }, { id: 'f-card', name: 'masked_card', label: 'Card PAN', type: 'credit_card', description: 'Luhn-compliant masked payment token', params: {}, }, { id: 'f-spend', name: 'spending_amount', label: 'Spending Amount', type: 'gaussian_float', description: 'Gaussian spending curve (μ=$342.50, σ=$85.20)', params: { mean: 342.5, stdDev: 85.2, currency: '$' }, }, { id: 'f-country', name: 'country_code', label: 'Country Region', type: 'category_weights', description: 'Categorical distribution [US, DE, GB, FR, JP]', params: { categories: ['US', 'DE', 'GB', 'FR', 'JP'], weights: [0.45, 0.2, 0.15, 0.1, 0.1], }, }, { id: 'f-created', name: 'created_at', label: 'Registration Date', type: 'date_range', description: 'ISO-8601 timestamp range (2024-2025)', params: { dateStart: '2024-01-01', dateEnd: '2025-08-20' }, }, ], }, 'healthcare-ehr': { id: 'healthcare-ehr', name: 'Healthcare Patient Clinical Records (EHR)', description: 'HIPAA-compliant patient cohort with biomarker telemetry, blood pressure, and triage severity tiers.', category: 'Healthcare & Life Sciences', fields: [ { id: 'f-h-id', name: 'patient_id', label: 'Patient ID', type: 'name', description: 'Deterministic HIPAA salted identifier', params: {}, }, { id: 'f-h-name', name: 'patient_name', label: 'Patient Name', type: 'name', description: 'Synthetic medical record moniker', params: {}, }, { id: 'f-h-email', name: 'contact_email', label: 'Contact Email', type: 'email', description: 'Synthetic provider contact', params: { domain: 'med-synthetic.org' }, }, { id: 'f-h-glucose', name: 'blood_glucose', label: 'Glucose (mg/dL)', type: 'gaussian_float', description: 'Fast fasting blood glucose (μ=108.4, σ=22.1)', params: { mean: 108.4, stdDev: 22.1, unit: 'mg/dL' }, }, { id: 'f-h-bp', name: 'systolic_bp', label: 'Systolic BP', type: 'gaussian_float', description: 'Arterial blood pressure (μ=124.0, σ=15.5)', params: { mean: 124.0, stdDev: 15.5, unit: 'mmHg' }, }, { id: 'f-h-tier', name: 'triage_tier', label: 'Triage Severity', type: 'category_weights', description: 'Clinical priority classification', params: { categories: ['Routine', 'Elevated', 'Urgent', 'Critical'], weights: [0.52, 0.28, 0.14, 0.06], }, }, { id: 'f-h-date', name: 'admission_date', label: 'Admission Date', type: 'date_range', description: 'Encounter timestamps', params: { dateStart: '2024-06-01', dateEnd: '2025-08-20' }, }, ], }, 'fintech-fraud': { id: 'fintech-fraud', name: 'Fintech Fraud Detection & Risk Scoring', description: 'Transaction monitoring telemetry for anti-fraud classifiers with skewed anomalous patterns.', category: 'Fintech & Risk', fields: [ { id: 'f-ff-id', name: 'tx_token', label: 'Transaction ID', type: 'name', description: 'Cryptographic ledger token', params: {}, }, { id: 'f-ff-name', name: 'merchant_name', label: 'Merchant Name', type: 'name', description: 'Synthetic counterparty vendor', params: {}, }, { id: 'f-ff-email', name: 'auth_email', label: 'Auth Email', type: 'email', description: 'Tokenized transaction origin email', params: { domain: 'syn-pay.net' }, }, { id: 'f-ff-card', name: 'card_pan', label: 'Card PAN', type: 'credit_card', description: 'Tokenized 16-digit card surrogate', params: {}, }, { id: 'f-ff-amt', name: 'settlement_usd', label: 'Settlement ($)', type: 'gaussian_float', description: 'Transaction gross volume (μ=$1420.00, σ=$480.00)', params: { mean: 1420.0, stdDev: 480.0, currency: '$' }, }, { id: 'f-ff-verdict', name: 'risk_verdict', label: 'Risk Verdict', type: 'category_weights', description: 'Ground truth classifier target', params: { categories: ['Legitimate', 'Suspicious', 'Flagged_Fraud'], weights: [0.89, 0.08, 0.03], }, }, { id: 'f-ff-ts', name: 'settled_at', label: 'Settlement Time', type: 'date_range', description: 'High-precision event timestamp', params: { dateStart: '2025-01-01', dateEnd: '2025-08-20' }, }, ], }, 'saas-telemetry': { id: 'saas-telemetry', name: 'SaaS Product Telemetry & Usage Metrics', description: 'B2B subscription telemetry with active seat distribution, recurring revenue, and subscription tiers.', category: 'Cloud Software & B2B', fields: [ { id: 'f-st-id', name: 'org_uuid', label: 'Organization UUID', type: 'name', description: 'Tenant identifier', params: {}, }, { id: 'f-st-name', name: 'tenant_name', label: 'Tenant Name', type: 'name', description: 'Synthetic enterprise tenant moniker', params: {}, }, { id: 'f-st-email', name: 'admin_email', label: 'Admin Email', type: 'email', description: 'Primary owner email', params: { domain: 'syn-saas.io' }, }, { id: 'f-st-seats', name: 'active_seats', label: 'Active Seats', type: 'gaussian_float', description: 'Provisioned enterprise seats (μ=48.0, σ=18.0)', params: { mean: 48.0, stdDev: 18.0, unit: 'seats' }, }, { id: 'f-st-mrr', name: 'mrr_usd', label: 'Monthly Revenue', type: 'gaussian_float', description: 'MRR ledger (μ=$2850.00, σ=$950.00)', params: { mean: 2850.0, stdDev: 950.0, currency: '$' }, }, { id: 'f-st-plan', name: 'plan_tier', label: 'Plan Tier', type: 'category_weights', description: 'Subscription entitlement level', params: { categories: ['Starter', 'Growth', 'Enterprise'], weights: [0.48, 0.36, 0.16], }, }, { id: 'f-st-date', name: 'provisioned_at', label: 'Provisioned Date', type: 'date_range', description: 'Cohort subscription activation', params: { dateStart: '2024-01-01', dateEnd: '2025-08-20' }, }, ], }, } export const FIRST_NAMES = [ 'Elena', 'Malik', 'Siddharth', 'Claire', 'Liam', 'Amara', 'Kenji', 'Sofia', 'Devon', 'Maya', 'Tobias', 'Fatima', 'Mateo', 'Astrid', 'Zara', ] export const LAST_NAMES = [ 'Rostova', 'Al-Mansoor', 'Patel', 'Chen', "O'Connor", 'Okafor', 'Sato', 'Alvarez', 'Vance', 'Lin', 'Richter', 'Zahra', 'Silva', 'Lindqvist', 'Novak', ] export const CARD_BRANDS = ['Visa', 'Mastercard', 'Amex'] export function boxMullerRandom( mean: number, stdDev: number, seedOffset: number, seed: number, noiseMechanism: boolean, epsilon: number, ): number { const u1 = Math.max(1e-6, Math.abs(Math.sin(seed + seedOffset * 17.3))) const u2 = Math.max(1e-6, Math.abs(Math.cos(seed + seedOffset * 31.7))) const z0 = Math.sqrt(-2.0 * Math.log(u1)) * Math.cos(2.0 * Math.PI * u2) const dpNoiseScale = noiseMechanism ? stdDev / (Math.max(0.1, epsilon) * 12) : 0 const dpNoise = Math.sin(seed * 3.7 + seedOffset * 5.1) * dpNoiseScale return mean + z0 * stdDev + dpNoise }src/app/components/blocks/synthetic-data-generator/index.ts0.5 kB
export { UiSyntheticDataGeneratorComponent, type FieldDataType, type FidelityMetrics, type GeneratedRecord, type SchemaField, type SyntheticDataGeneratorProps, type TemplatePreset, } from './synthetic-data-generator.component' export { UiSyntheticDataPreviewComponent, type SyntheticDataPreviewTab } from './synthetic-data-preview.component' export { TEMPLATES, FIRST_NAMES, LAST_NAMES, CARD_BRANDS, boxMullerRandom } from './synthetic-data-templates'
Raw manifest:https://uipkge.dev/r/angular/synthetic-data-generator.json