// 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/__.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) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c])); export const link = (text, url) => (typeof url === "string" && url.startsWith("http") ? `${esc(text)}` : esc(text)); // fill a