The demo below shows a tiny real dataset of 20 points drawn from two clusters. The generator measures the mean and standard deviation of each coordinate, adds Laplace noise scaled to 1/ε, then draws a fresh synthetic sample from the noisy parameters.
<!-- Main layout — controls at top, canvas plot, legend, stats summary -->
<div class="controls">
<label for="eps-slider">Privacy budget ε: <strong id="eps-val">1.0</strong></label>
<input id="eps-slider" type="range" min="0.1" max="10" step="0.1" value="1.0">
<button id="btn-regen" type="button">Regenerate</button>
</div>
<canvas id="plot" width="480" height="320"></canvas>
<div class="legend">
<span class="dot real"></span> Real data
<span class="dot synth"></span> Synthetic data
</div>
<div id="stats-box" class="stats-box"></div>
/* Demo styles — minimal layout, muted palette */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; margin: 0; color: #222; }
.controls { display: flex; align-items: center; gap: .6rem; flex-wrap: wrap; margin-bottom: .5rem; font-size: .92rem; }
label { font-weight: 600; }
input[type=range] { flex: 1 1 120px; accent-color: #1d3557; }
button { font: 600 13px system-ui; padding: .35rem .8rem; background: #1d3557; color: #fff; border: none; border-radius: 7px; cursor: pointer; }
canvas { display: block; border: 1px solid #cdd9e3; border-radius: 10px; background: #f8fafc; max-width: 100%; }
.legend { display: flex; gap: 1rem; font-size: .84rem; margin: .4rem 0; }
.dot { display: inline-block; width: 10px; height: 10px; border-radius: 50%; margin-right: 4px; }
.dot.real { background: #1d6fa5; }
.dot.synth { background: #e05c2b; }
.stats-box { font-size: .82rem; color: #445; margin-top: .3rem; line-height: 1.6; }
/* DP synthetic data demo — real points stay hidden, synthetic points mirror noisy stats */
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
/* 20 real points in two clusters: cluster A near (2.5, 3.2), cluster B near (8.1, 7.7) */
const REAL = [
[2.1,3.4],[2.8,2.9],[1.9,3.8],[3.2,2.6],[2.5,3.1],[2.0,4.0],[3.5,3.3],[2.3,2.8],[1.7,3.5],[3.0,2.4],
[8.1,7.6],[7.5,8.2],[8.8,7.1],[7.2,8.5],[8.4,7.8],[9.0,7.3],[7.8,8.0],[8.3,6.9],[7.0,8.4],[8.6,7.5]
];
/* Sensitivity for mean queries over a bounded range [MIN_V, MAX_V] with N points */
const MIN_V = 0, MAX_V = 12;
const RANGE = MAX_V - MIN_V;
const N = REAL.length;
const SENSITIVITY = RANGE / N;
function laplace(scale) {
/* Inverse CDF method for Laplace(0, scale) noise — privacy mechanism */
const u = Math.random() - 0.5;
return -scale * Math.sign(u) * Math.log(1 - 2 * Math.abs(u));
}
function clamp(v, lo, hi) { return Math.max(lo, Math.min(hi, v)); }
function computeStats(pts) {
/* Compute mean and std-dev for each coordinate across all points */
const mx = pts.reduce((s, p) => s + p[0], 0) / pts.length;
const my = pts.reduce((s, p) => s + p[1], 0) / pts.length;
const sx = Math.sqrt(pts.reduce((s, p) => s + (p[0]-mx)**2, 0) / pts.length);
const sy = Math.sqrt(pts.reduce((s, p) => s + (p[1]-my)**2, 0) / pts.length);
return { mx, my, sx, sy };
}
function generateSynth(eps) {
/* Add Laplace noise to real stats to produce noisy parameters for synthetic generation */
const s = computeStats(REAL);
const noiseScale = SENSITIVITY / eps;
const nmx = s.mx + laplace(noiseScale);
const nmy = s.my + laplace(noiseScale);
const nsx = clamp(s.sx + laplace(noiseScale), 0.1, RANGE);
const nsy = clamp(s.sy + laplace(noiseScale), 0.1, RANGE);
/* Draw N synthetic points from a Gaussian with the noisy mean and std-dev (Box-Muller) */
const synthPts = [];
for (let i = 0; i < N; i++) {
const u1 = Math.random(), u2 = Math.random();
const z1 = Math.sqrt(-2*Math.log(u1+1e-12)) * Math.cos(2*Math.PI*u2);
const z2 = Math.sqrt(-2*Math.log(u1+1e-12)) * Math.sin(2*Math.PI*u2);
const x = nmx + z1 * nsx;
const y = nmy + z2 * nsy;
synthPts.push([x, y]);
}
return { synthPts, noisyStats: { nmx, nmy, nsx, nsy } };
}
const canvas = document.getElementById('plot');
const ctx = canvas.getContext('2d');
const W = canvas.width, H = canvas.height;
const PAD = 28;
function toCanvas(v) {
return [
PAD + (v[0] - MIN_V) / RANGE * (W - 2*PAD),
H - PAD - (v[1] - MIN_V) / RANGE * (H - 2*PAD)
];
}
function drawAxes() {
/* Draw axis lines and tick labels */
ctx.strokeStyle = '#bcc8d4';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(PAD, PAD); ctx.lineTo(PAD, H-PAD);
ctx.lineTo(W-PAD, H-PAD);
ctx.stroke();
ctx.fillStyle = '#889';
ctx.font = '10px system-ui';
for (let v = 0; v <= MAX_V; v += 3) {
const [cx] = toCanvas([v, 0]);
const [, cy] = toCanvas([0, v]);
ctx.fillText(v, cx - 4, H-PAD+12);
ctx.fillText(v, 2, cy + 4);
}
}
function drawDots(pts, color, alpha) {
ctx.globalAlpha = alpha;
ctx.fillStyle = color;
for (const p of pts) {
const [x, y] = toCanvas(p);
ctx.beginPath(); ctx.arc(x, y, 5, 0, 2*Math.PI); ctx.fill();
}
ctx.globalAlpha = 1;
}
let currentSynth = null;
function render() {
const eps = parseFloat(document.getElementById('eps-slider').value);
document.getElementById('eps-val').textContent = eps.toFixed(1);
const { synthPts, noisyStats } = generateSynth(eps);
currentSynth = synthPts;
ctx.clearRect(0, 0, W, H);
drawAxes();
drawDots(REAL, '#1d6fa5', 0.7);
drawDots(synthPts, '#e05c2b', 0.7);
const rs = computeStats(REAL);
const statsBox = document.getElementById('stats-box');
statsBox.innerHTML =
`<b>Real stats:</b> ${i18nFmt('mean (%mx%, %my%)', { mx: rs.mx.toFixed(2), my: rs.my.toFixed(2) })}, ${i18nFmt('std (%sx%, %sy%)', { sx: rs.sx.toFixed(2), sy: rs.sy.toFixed(2) })}<br>` +
`<b>Noisy stats (used for synthesis):</b> ${i18nFmt('mean (%mx%, %my%)', { mx: noisyStats.nmx.toFixed(2), my: noisyStats.nmy.toFixed(2) })}, ${i18nFmt('std (%sx%, %sy%)', { sx: noisyStats.nsx.toFixed(2), sy: noisyStats.nsy.toFixed(2) })}<br>` +
`<span style="color:#888">${i18nFmt('ε = %eps% → Laplace noise scale = %scale%', { eps: eps.toFixed(1), scale: (SENSITIVITY/eps).toFixed(3) })}</span>`;
}
document.getElementById('eps-slider').addEventListener('input', render);
document.getElementById('btn-regen').addEventListener('click', render);
render();
<!-- Main layout — controls at top, canvas plot, legend, stats summary -->
<div class="controls">
<label for="eps-slider">Privacy budget ε: <strong id="eps-val">1.0</strong></label>
<input id="eps-slider" type="range" min="0.1" max="10" step="0.1" value="1.0">
<button id="btn-regen" type="button">Regenerate</button>
</div>
<canvas id="plot" width="480" height="320"></canvas>
<div class="legend">
<span class="dot real"></span> Real data
<span class="dot synth"></span> Synthetic data
</div>
<div id="stats-box" class="stats-box"></div>
/* Demo styles — minimal layout, muted palette */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; margin: 0; color: #222; }
.controls { display: flex; align-items: center; gap: .6rem; flex-wrap: wrap; margin-bottom: .5rem; font-size: .92rem; }
label { font-weight: 600; }
input[type=range] { flex: 1 1 120px; accent-color: #1d3557; }
button { font: 600 13px system-ui; padding: .35rem .8rem; background: #1d3557; color: #fff; border: none; border-radius: 7px; cursor: pointer; }
canvas { display: block; border: 1px solid #cdd9e3; border-radius: 10px; background: #f8fafc; max-width: 100%; }
.legend { display: flex; gap: 1rem; font-size: .84rem; margin: .4rem 0; }
.dot { display: inline-block; width: 10px; height: 10px; border-radius: 50%; margin-right: 4px; }
.dot.real { background: #1d6fa5; }
.dot.synth { background: #e05c2b; }
.stats-box { font-size: .82rem; color: #445; margin-top: .3rem; line-height: 1.6; }
/* DP synthetic data demo — real points stay hidden, synthetic points mirror noisy stats */
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
/* 20 real points in two clusters: cluster A near (2.5, 3.2), cluster B near (8.1, 7.7) */
const REAL = [
[2.1,3.4],[2.8,2.9],[1.9,3.8],[3.2,2.6],[2.5,3.1],[2.0,4.0],[3.5,3.3],[2.3,2.8],[1.7,3.5],[3.0,2.4],
[8.1,7.6],[7.5,8.2],[8.8,7.1],[7.2,8.5],[8.4,7.8],[9.0,7.3],[7.8,8.0],[8.3,6.9],[7.0,8.4],[8.6,7.5]
];
/* Sensitivity for mean queries over a bounded range [MIN_V, MAX_V] with N points */
const MIN_V = 0, MAX_V = 12;
const RANGE = MAX_V - MIN_V;
const N = REAL.length;
const SENSITIVITY = RANGE / N;
function laplace(scale) {
/* Inverse CDF method for Laplace(0, scale) noise — privacy mechanism */
const u = Math.random() - 0.5;
return -scale * Math.sign(u) * Math.log(1 - 2 * Math.abs(u));
}
function clamp(v, lo, hi) { return Math.max(lo, Math.min(hi, v)); }
function computeStats(pts) {
/* Compute mean and std-dev for each coordinate across all points */
const mx = pts.reduce((s, p) => s + p[0], 0) / pts.length;
const my = pts.reduce((s, p) => s + p[1], 0) / pts.length;
const sx = Math.sqrt(pts.reduce((s, p) => s + (p[0]-mx)**2, 0) / pts.length);
const sy = Math.sqrt(pts.reduce((s, p) => s + (p[1]-my)**2, 0) / pts.length);
return { mx, my, sx, sy };
}
function generateSynth(eps) {
/* Add Laplace noise to real stats to produce noisy parameters for synthetic generation */
const s = computeStats(REAL);
const noiseScale = SENSITIVITY / eps;
const nmx = s.mx + laplace(noiseScale);
const nmy = s.my + laplace(noiseScale);
const nsx = clamp(s.sx + laplace(noiseScale), 0.1, RANGE);
const nsy = clamp(s.sy + laplace(noiseScale), 0.1, RANGE);
/* Draw N synthetic points from a Gaussian with the noisy mean and std-dev (Box-Muller) */
const synthPts = [];
for (let i = 0; i < N; i++) {
const u1 = Math.random(), u2 = Math.random();
const z1 = Math.sqrt(-2*Math.log(u1+1e-12)) * Math.cos(2*Math.PI*u2);
const z2 = Math.sqrt(-2*Math.log(u1+1e-12)) * Math.sin(2*Math.PI*u2);
const x = nmx + z1 * nsx;
const y = nmy + z2 * nsy;
synthPts.push([x, y]);
}
return { synthPts, noisyStats: { nmx, nmy, nsx, nsy } };
}
const canvas = document.getElementById('plot');
const ctx = canvas.getContext('2d');
const W = canvas.width, H = canvas.height;
const PAD = 28;
function toCanvas(v) {
return [
PAD + (v[0] - MIN_V) / RANGE * (W - 2*PAD),
H - PAD - (v[1] - MIN_V) / RANGE * (H - 2*PAD)
];
}
function drawAxes() {
/* Draw axis lines and tick labels */
ctx.strokeStyle = '#bcc8d4';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(PAD, PAD); ctx.lineTo(PAD, H-PAD);
ctx.lineTo(W-PAD, H-PAD);
ctx.stroke();
ctx.fillStyle = '#889';
ctx.font = '10px system-ui';
for (let v = 0; v <= MAX_V; v += 3) {
const [cx] = toCanvas([v, 0]);
const [, cy] = toCanvas([0, v]);
ctx.fillText(v, cx - 4, H-PAD+12);
ctx.fillText(v, 2, cy + 4);
}
}
function drawDots(pts, color, alpha) {
ctx.globalAlpha = alpha;
ctx.fillStyle = color;
for (const p of pts) {
const [x, y] = toCanvas(p);
ctx.beginPath(); ctx.arc(x, y, 5, 0, 2*Math.PI); ctx.fill();
}
ctx.globalAlpha = 1;
}
let currentSynth = null;
function render() {
const eps = parseFloat(document.getElementById('eps-slider').value);
document.getElementById('eps-val').textContent = eps.toFixed(1);
const { synthPts, noisyStats } = generateSynth(eps);
currentSynth = synthPts;
ctx.clearRect(0, 0, W, H);
drawAxes();
drawDots(REAL, '#1d6fa5', 0.7);
drawDots(synthPts, '#e05c2b', 0.7);
const rs = computeStats(REAL);
const statsBox = document.getElementById('stats-box');
statsBox.innerHTML =
`<b>Real stats:</b> ${i18nFmt('mean (%mx%, %my%)', { mx: rs.mx.toFixed(2), my: rs.my.toFixed(2) })}, ${i18nFmt('std (%sx%, %sy%)', { sx: rs.sx.toFixed(2), sy: rs.sy.toFixed(2) })}<br>` +
`<b>Noisy stats (used for synthesis):</b> ${i18nFmt('mean (%mx%, %my%)', { mx: noisyStats.nmx.toFixed(2), my: noisyStats.nmy.toFixed(2) })}, ${i18nFmt('std (%sx%, %sy%)', { sx: noisyStats.nsx.toFixed(2), sy: noisyStats.nsy.toFixed(2) })}<br>` +
`<span style="color:#888">${i18nFmt('ε = %eps% → Laplace noise scale = %scale%', { eps: eps.toFixed(1), scale: (SENSITIVITY/eps).toFixed(3) })}</span>`;
}
document.getElementById('eps-slider').addEventListener('input', render);
document.getElementById('btn-regen').addEventListener('click', render);
render();
Comments
Loading comments...