diff --git a/src/viz/table-renderer.ts b/src/viz/table-renderer.ts new file mode 100644 index 0000000..fbd9676 --- /dev/null +++ b/src/viz/table-renderer.ts @@ -0,0 +1,125 @@ +// --------------------------------------------------------------------------- +// Table +// --------------------------------------------------------------------------- + +export function renderTable(el: HTMLElement, rows: Record[], columns?: string[]): void { + const table = el.createEl('table', { cls: 'logfire-table' }); + const allKeys = columns ?? Object.keys(rows[0]); + + const thead = table.createEl('thead'); + const headerRow = thead.createEl('tr'); + for (const key of allKeys) { + headerRow.createEl('th', { text: key }); + } + + const tbody = table.createEl('tbody'); + for (const row of rows) { + const tr = tbody.createEl('tr'); + for (const key of allKeys) { + tr.createEl('td', { text: formatValue(row[key]) }); + } + } +} + +// --------------------------------------------------------------------------- +// Timeline +// --------------------------------------------------------------------------- + +export function renderTimeline(el: HTMLElement, rows: Record[]): void { + const list = el.createEl('ul', { cls: 'logfire-timeline' }); + for (const row of rows) { + const ts = typeof row.timestamp === 'number' + ? new Date(row.timestamp).toLocaleTimeString() + : ''; + const type = String(row.type ?? ''); + const source = String(row.source ?? ''); + list.createEl('li', { text: `${ts} ${type} ${source}` }); + } +} + +// --------------------------------------------------------------------------- +// Summary +// --------------------------------------------------------------------------- + +export function renderSummary(el: HTMLElement, rows: Record[]): void { + const container = el.createDiv({ cls: 'logfire-summary' }); + for (const row of rows) { + for (const [key, value] of Object.entries(row)) { + const line = container.createDiv(); + line.createEl('strong', { text: `${key}: ` }); + line.appendText(formatValue(value)); + } + } +} + +// --------------------------------------------------------------------------- +// Metric (single big number) +// --------------------------------------------------------------------------- + +export function renderMetric(el: HTMLElement, rows: Record[]): void { + const record = rows[0]; + const values = Object.values(record); + const value = values.length > 0 ? values[values.length - 1] : 0; + const div = el.createEl('div', { + text: formatValue(value), + cls: 'logfire-metric', + }); + div.style.fontSize = '2em'; +} + +// --------------------------------------------------------------------------- +// List +// --------------------------------------------------------------------------- + +export function renderList(el: HTMLElement, rows: Record[]): void { + const list = el.createEl('ul', { cls: 'logfire-list' }); + for (const row of rows) { + const text = Object.values(row).map(formatValue).join(' | '); + list.createEl('li', { text }); + } +} + +// --------------------------------------------------------------------------- +// Heatmap (text-based bar chart) +// --------------------------------------------------------------------------- + +export function renderHeatmap(el: HTMLElement, rows: Record[]): void { + const container = el.createDiv({ cls: 'logfire-heatmap' }); + + for (const row of rows) { + const group = String(row.group ?? ''); + const count = Number(row.count ?? 0); + const bar = '\u2588'.repeat(Math.min(count, 50)); + container.createDiv({ text: `${group} ${bar} ${count}` }); + } +} + +// --------------------------------------------------------------------------- +// Markdown table generation (for copy/export) +// --------------------------------------------------------------------------- + +export function toMarkdownTable(keys: string[], rows: Record[]): string { + const header = `| ${keys.join(' | ')} |`; + const separator = `| ${keys.map(() => '---').join(' | ')} |`; + const body = rows.map(row => + `| ${keys.map(k => { + const v = row[k]; + return v === null || v === undefined ? '' : String(v); + }).join(' | ')} |` + ).join('\n'); + + return `${header}\n${separator}\n${body}`; +} + +// --------------------------------------------------------------------------- +// Value formatting +// --------------------------------------------------------------------------- + +export function formatValue(value: unknown): string { + if (value === null || value === undefined) return ''; + if (typeof value === 'number') { + if (value > 1e12) return new Date(value).toLocaleString(); + return value.toLocaleString(); + } + return String(value); +}