fliarbi's picture
Update app
8e6d64a verified
Raw History Blame Contribute Delete
6.66 kB
// Shared helpers for the Fliar BI static explorers (no build step).
import { parquetReadObjects } from "https://cdn.jsdelivr.net/npm/hyparquet@1.31.2/+esm";
export const ALL = "__all__";
export async function loadParquet(repo, columns) {
// ?data=local reads /_data/<owner>__<name>.parquet (local testing only)
const local = new URLSearchParams(location.search).get("data") === "local";
const url = local ? `/_data/${repo.replace("/", "__")}.parquet`
: `https://huggingface.co/datasets/${repo}/resolve/main/data/train.parquet`;
let buf;
try {
const res = await fetch(url);
if (!res.ok) throw new Error(`${repo}: HTTP ${res.status}`);
buf = await res.arrayBuffer();
} catch (e) {
// fallback: slim column subset of the same file, shipped with this Space (e.g. when the Hub asks for a bot check)
console.warn("Hub fetch failed, using the bundled copy:", e.message);
const res = await fetch(`data/${repo.replace("/", "__")}.parquet`);
if (!res.ok) throw new Error(`${repo}: HTTP ${res.status}`);
buf = await res.arrayBuffer();
}
const rows = await parquetReadObjects({ file: buf, columns });
for (const r of rows) {
for (const k in r) {
const v = r[k];
if (typeof v === "bigint") r[k] = Number(v);
else if (v === undefined) r[k] = null;
}
}
return rows;
}
export const isNum = (v) => typeof v === "number" && Number.isFinite(v);
// pandas-style linear quantile on a sorted numeric array
export function quantileSorted(s, q) {
if (!s.length) return NaN;
const pos = (s.length - 1) * q, lo = Math.floor(pos), hi = Math.ceil(pos);
return s[lo] + (s[hi] - s[lo]) * (pos - lo);
}
export function quantiles(values, qs) {
const s = values.filter(isNum).sort((a, b) => a - b);
return qs.map((q) => quantileSorted(s, q));
}
export const sum = (a) => a.reduce((t, v) => (isNum(v) ? t + v : t), 0);
export function groupBy(rows, keyFn) {
const m = new Map();
for (const r of rows) {
const k = keyFn(r);
let g = m.get(k);
if (!g) m.set(k, (g = []));
g.push(r);
}
return m;
}
export function countBy(rows, keyFn) {
const m = new Map();
for (const r of rows) { const k = keyFn(r); m.set(k, (m.get(k) || 0) + 1); }
return [...m.entries()].sort((a, b) => b[1] - a[1]);
}
export function mode(arr) {
const c = countBy(arr.filter((x) => x !== "" && x != null), (x) => x);
return c.length ? c[0][0] : "";
}
export function money(v, cur = "$") {
if (!isNum(v) || v === 0) return "–";
const a = Math.abs(v);
for (const [d, s] of [[1e12, "T"], [1e9, "B"], [1e6, "M"], [1e3, "K"]]) if (a >= d) return `${cur}${(v / d).toLocaleString("en-US", { maximumFractionDigits: 1, minimumFractionDigits: 1 })}${s}`;
return `${cur}${Math.round(v).toLocaleString("en-US")}`;
}
export const int = (v) => (isNum(v) ? Math.round(v).toLocaleString("en-US") : "–");
export const pct = (v) => (isNum(v) ? `${Math.round(v * 100)}%` : "–");
export const esc = (s) => String(s ?? "").replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" }[c]));
export const link = (text, url) => (typeof url === "string" && url.startsWith("http") ? `<a href="${esc(url)}" target="_blank" rel="noopener">${esc(text)}</a>` : esc(text));
// fill a <select> with "All (n)" + value counts
export function fillSelect(sel, rows, keyFn, { top = 400, minN = 1, allLabel = "All", order = null } = {}) {
let counts = countBy(rows, keyFn).filter(([k, n]) => k !== "" && k != null && n >= minN);
if (order) counts.sort((a, b) => order.indexOf(a[0]) - order.indexOf(b[0]));
counts = counts.slice(0, top);
const prev = sel.value;
sel.innerHTML = `<option value="${ALL}">${allLabel} (${rows.length.toLocaleString("en-US")})</option>` +
counts.map(([k, n]) => `<option value="${esc(k)}">${esc(k)} (${n.toLocaleString("en-US")})</option>`).join("");
sel.value = counts.some(([k]) => String(k) === prev) ? prev : ALL;
}
export function segmented(el, options, initial, onChange) {
el.innerHTML = options.map((o) => `<button type="button" data-v="${esc(o)}">${esc(o)}</button>`).join("");
const set = (v) => { [...el.children].forEach((b) => b.classList.toggle("on", b.dataset.v === v)); };
el.addEventListener("click", (e) => { const b = e.target.closest("button"); if (!b) return; set(b.dataset.v); onChange(b.dataset.v); });
set(initial);
}
export function tabs(el) {
const btns = [...el.querySelectorAll(".tabs button")];
const panes = [...el.querySelectorAll("[data-pane]")];
const show = (i) => { btns.forEach((b, j) => b.classList.toggle("on", i === j)); panes.forEach((p, j) => (p.hidden = i !== j)); };
btns.forEach((b, i) => b.addEventListener("click", () => show(i)));
show(0);
}
// columns: [{key, label, num?, html?}] ; rows: objects ; hl: row => bool
export function renderTable(el, columns, rows, hl = null) {
if (!rows.length) { el.innerHTML = `<p class="loading">No rows match these filters.</p>`; return; }
const th = columns.map((c) => `<th class="${c.num ? "num" : ""}">${esc(c.label)}</th>`).join("");
const tr = rows.map((r) => `<tr class="${hl && hl(r) ? "hl" : ""}">` + columns.map((c) => {
const v = r[c.key];
return `<td class="${c.num ? "num" : ""}">${c.html ? (v ?? "") : esc(v ?? "")}</td>`;
}).join("") + "</tr>").join("");
el.innerHTML = `<div class="tablewrap"><table class="data"><thead><tr>${th}</tr></thead><tbody>${tr}</tbody></table></div>`;
}
export function css(name) { return getComputedStyle(document.documentElement).getPropertyValue(name).trim(); }
export const PALETTE = ["#4f46e5", "#0ea5e9", "#10b981", "#f59e0b", "#ef4444", "#8b5cf6", "#14b8a6", "#f97316", "#64748b", "#ec4899", "#84cc16", "#06b6d4", "#a855f7", "#eab308"];
export function baseLayout(title, extra = {}) {
const ink = css("--ink"), muted = css("--muted"), line = css("--line");
return Object.assign({
title: { text: title, font: { size: 15, color: ink }, x: 0.01 },
paper_bgcolor: "rgba(0,0,0,0)", plot_bgcolor: "rgba(0,0,0,0)",
font: { color: muted, size: 12 },
margin: { l: 56, r: 56, t: 46, b: 40 },
xaxis: { gridcolor: line, linecolor: line, zerolinecolor: line },
yaxis: { gridcolor: line, linecolor: line, zerolinecolor: line },
legend: { orientation: "h", y: 1.12, x: 1, xanchor: "right", font: { size: 11 } },
hoverlabel: { font: { size: 12 } },
}, extra);
}
export function plot(el, traces, layout) {
window.Plotly.react(el, traces, layout, { displayModeBar: false, responsive: true });
}
export const debounce = (fn, ms = 200) => { let t; return (...a) => { clearTimeout(t); t = setTimeout(() => fn(...a), ms); }; };