Percentile Visualization Tool
Summary Statistics
Enter data in CSV format with a header (e.g., 'Salary'). The tool will use the first column of numeric data.
No data to display.
'; return; } const stats = [ { label: 'Count', value: data.length.toLocaleString() }, { label: 'Min', value: formatCurrency(d3.min(data)) }, { label: '10th Percentile', value: formatCurrency(d3.quantile(data, 0.10)) }, { label: '25th Percentile (Q1)', value: formatCurrency(d3.quantile(data, 0.25)) }, { label: 'Median (50th)', value: formatCurrency(d3.median(data)) }, { label: '75th Percentile (Q3)', value: formatCurrency(d3.quantile(data, 0.75)) }, { label: '90th Percentile', value: formatCurrency(d3.quantile(data, 0.90)) }, { label: 'Max', value: formatCurrency(d3.max(data)) }, { label: 'Mean', value: formatCurrency(d3.mean(data)) }, ]; summaryStatsEl.innerHTML = stats.map(stat => `
${stat.label}
${stat.value}
`).join('');
}
function renderVisualization(data) {
const svg = d3.select("#visualization-svg");
svg.selectAll("*").remove();
if (data.length === 0) return;
const container = document.getElementById('visualization-container');
const width = container.clientWidth;
const height = container.clientHeight;
const margin = { top: 20, right: 50, bottom: 50, left: 80 };
const y = d3.scaleLinear()
.domain(d3.extent(data)).nice()
.range([height - margin.bottom, margin.top]);
const yAxis = d3.axisLeft(y).tickFormat(d => `$${d/1000}k`);
svg.append("g")
.attr("transform", `translate(${margin.left},0)`)
.call(yAxis)
.call(g => g.select(".domain").remove());
const chartWidth = 150;
const chartCenter = margin.left + (width - margin.left - margin.right) / 2;
// Violin Plot
const kde = kernelDensityEstimator(kernelEpanechnikov(7), y.ticks(40));
const density = kde(data);
const x = d3.scaleLinear()
.range([0, chartWidth / 2])
.domain([0, d3.max(density, d => d.y)]);
svg.append("path")
.datum(density)
.attr("transform", `translate(${chartCenter},0)`)
.attr("fill", "#bfdbfe")
.attr("stroke", "#93c5fd")
.attr("stroke-width", 1.5)
.attr("d", d3.area()
.x0(d => -x(d.y))
.x1(d => x(d.y))
.y(d => y(d.x0))
.curve(d3.curveCatmullRom)
);
// Box Plot
const q1 = d3.quantile(data, 0.25);
const median = d3.quantile(data, 0.5);
const q3 = d3.quantile(data, 0.75);
const boxWidth = 40;
svg.append("rect")
.attr("x", chartCenter - boxWidth / 2)
.attr("y", y(q3))
.attr("height", y(q1) - y(q3))
.attr("width", boxWidth)
.attr("stroke", "#6b7280")
.attr("fill", "#d1d5db");
svg.append("line")
.attr("x1", chartCenter - boxWidth / 2)
.attr("x2", chartCenter + boxWidth / 2)
.attr("y1", y(median))
.attr("y2", y(median))
.attr("stroke", "#1f2937")
.style("stroke-width", "2px");
// Jittered dots
svg.selectAll("indPoints")
.data(data)
.enter()
.append("circle")
.attr("cy", d => y(d))
.attr("cx", d => chartCenter - chartWidth/4 + Math.random()*(chartWidth/2))
.attr("r", 3)
.style("fill", "#3b82f6")
.attr("fill-opacity", 0.3);
}
// Helper for Violin Plot
function kernelDensityEstimator(kernel, X) {
return function(V) {
return X.map(function(x) {
return [x, d3.mean(V, function(v) { return kernel(x - v); })];
});
};
}
function kernelEpanechnikov(k) {
return function(v) {
return Math.abs(v /= k) <= 1 ? 0.75 * (1 - v * v) / k : 0;
};
}
window.updateDataAndVisualize = function() {
currentData = parseCSV(dataInputEl.value);
if (currentData.length === 0) {
alert('No valid numeric data found. Please check your CSV.');
return;
}
currentData.sort(d3.ascending);
updateSummary(currentData);
renderVisualization(currentData);
changeTab('dashboard');
}
window.changeTab = function(tabName) {
activeTab = tabName;
Object.values(tabButtons).forEach(btn => btn.classList.remove('active'));
Object.values(tabContents).forEach(content => content.style.display = 'none');
tabButtons[tabName].classList.add('active');
tabContents[tabName].style.display = 'block';
updateNavButtons();
if (tabName === 'dashboard') {
setTimeout(() => renderVisualization(currentData), 10);
}
}
function updateNavButtons() {
navButtons.prev.style.visibility = (activeTab === 'dashboard') ? 'hidden' : 'visible';
navButtons.next.style.visibility = (activeTab === 'dataConfig') ? 'hidden' : 'visible';
}
window.navigateTabs = function(direction) {
if (direction === 'next' && activeTab === 'dashboard') changeTab('dataConfig');
else if (direction === 'prev' && activeTab === 'dataConfig') changeTab('dashboard');
}
window.downloadPDF = function() {
const { jsPDF } = window.jspdf;
const dashboardContent = document.getElementById('dashboard-content');
const downloadBtn = document.getElementById('download-pdf-btn');
if (!dashboardContent) return;
downloadBtn.style.visibility = 'hidden';
html2canvas(dashboardContent, { scale: 2, backgroundColor: '#ffffff' }).then(canvas => {
const imgData = canvas.toDataURL('image/png');
const pdf = new jsPDF({ orientation: 'landscape', unit: 'px', format: [canvas.width, canvas.height] });
pdf.addImage(imgData, 'PNG', 0, 0, canvas.width, canvas.height);
pdf.save('percentile_visualization.pdf');
downloadBtn.style.visibility = 'visible';
}).catch(err => {
console.error("PDF generation failed:", err);
downloadBtn.style.visibility = 'visible';
});
}
function initializeTool() {
dataInputEl.value = sampleData;
updateDataAndVisualize();
changeTab('dashboard');
window.addEventListener('resize', () => {
if (activeTab === 'dashboard') {
renderVisualization(currentData);
}
});
}
initializeTool();
});
