diff --git a/max_outlier_visualization/js/ui.js b/max_outlier_visualization/js/ui.js new file mode 100644 index 0000000..84783b6 --- /dev/null +++ b/max_outlier_visualization/js/ui.js @@ -0,0 +1,168 @@ +"use strict"; + +/** + * UI Logic for Max-Outlier Visualization + * Handles rendering of table, chart, and filter interactions. + * Relies on api.js for data fetching. + */ + +import { fetchMaxOutlierResults } from './api.js'; + +const ui = (() => { + const tableContainer = document.getElementById('outlier-table'); + const chartContainer = document.getElementById('outlier-chart'); + const filters = document.querySelectorAll('.filter-input'); + + /** + * Render results table from API data. + * @param {Array} data + */ + const renderTable = (data) => { + if (!tableContainer) return; + const table = document.createElement('table'); + table.className = 'outlier__table'; + + const header = document.createElement('thead'); + header.innerHTML = ` + + Run ID + Parallelism + Stratum + Δt + p95 + p99 + Retry Status + + `; + table.appendChild(header); + + const body = document.createElement('tbody'); + data.forEach((row) => { + const tr = document.createElement('tr'); + tr.innerHTML = ` + ${row.run_id ?? '-'} + ${row.parallelism ?? '-'} + ${row.stratum ?? '-'} + ${row.delta_t ?? '-'} + ${row.p95 ?? '-'} + ${row.p99 ?? '-'} + ${row.retry_status ?? '-'} + `; + body.appendChild(tr); + }); + table.appendChild(body); + + tableContainer.innerHTML = ''; + tableContainer.appendChild(table); + }; + + /** + * Render chart for p95/p99 visualization. + * @param {Array} data + */ + const renderChart = (data) => { + if (!chartContainer) return; + const ctx = chartContainer.getContext('2d'); + if (!ctx) return; + + const labels = data.map((d) => `${d.run_id}`); + const p95Values = data.map((d) => d.p95); + const p99Values = data.map((d) => d.p99); + + if (chartContainer.chartInstance) { + chartContainer.chartInstance.destroy(); + } + + // Basic chart setup using Chart.js style interface if loaded + chartContainer.chartInstance = new Chart(ctx, { + type: 'line', + data: { + labels, + datasets: [ + { + label: 'p95', + data: p95Values, + borderColor: 'var(--primary)', + backgroundColor: 'rgba(0, 128, 255, 0.2)', + fill: true, + tension: 0.2, + }, + { + label: 'p99', + data: p99Values, + borderColor: 'var(--accent)', + backgroundColor: 'rgba(255, 64, 64, 0.2)', + fill: true, + tension: 0.2, + }, + ], + }, + options: { + responsive: true, + maintainAspectRatio: false, + scales: { + x: { + title: { + display: true, + text: 'Run ID' + } + }, + y: { + title: { + display: true, + text: 'Metric Value' + } + } + }, + plugins: { + legend: { + position: 'top' + }, + tooltip: { + mode: 'index', + intersect: false + } + } + } + }); + }; + + /** + * Handle filter input change, fetch new data and re-render views. + * @param {Event} e + */ + const handleFilterChange = async (e) => { + e?.preventDefault(); + const params = {}; + filters.forEach((input) => { + if (input.value.trim()) { + params[input.name] = input.value.trim(); + } + }); + + try { + const results = await fetchMaxOutlierResults(params); + renderTable(results); + renderChart(results); + } catch (err) { + console.error('Failed to update data:', err); + } + }; + + // Binding filter events on page load + const bindFilterEvents = () => { + filters.forEach((input) => { + input.addEventListener('change', handleFilterChange); + }); + }; + + // Public API + return { + renderTable, + renderChart, + handleFilterChange, + bindFilterEvents + }; +})(); + +export default ui; \ No newline at end of file