Spaces:
Running
Running
File size: 33,704 Bytes
917845f 4db2af3 917845f 4db2af3 917845f 4db2af3 917845f dad6bef 917845f | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 | "use strict";
/* Visualize: ask a question about a dataset, get a chart, and open the rows behind any mark.
Built for the small datasets a hosted run writes, so every chart is exact and instant views need no model. */
(function () {
const INKS = ["#3a4bff", "#ff5c8f", "#00838a", "#ffb000", "#ff6c2f", "#765ba7"];
const OTHER = "#8a8fa0";
const SIZES = [25, 50, 100, 200];
const STAGES = [["plan", "Planning the chart"], ["label", "Reading the rows"], ["chart", "Counting"], ["write", "Writing it up"]];
const V = {
data: null, rows: [], byId: {}, samples: [], maxRows: 200, n: 50, hub: "", loadBusy: "", loadErr: "",
q: "", asking: null, askErr: "", history: [], cur: -1, scope: null, sel: null, selShow: 30, sorts: {}, drawn: {},
};
const $ = (s, el = document) => el.querySelector(s);
const esc = (t) => String(t ?? "").replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c]));
const on = () => window.DI_MODE === "visualize";
const plural = (n, w) => `${n.toLocaleString()} ${w}${n === 1 ? "" : "s"}`;
const fmt = (x) => { if (x == null || isNaN(x)) return ""; const a = Math.abs(x); return a >= 1000 ? Math.round(x).toLocaleString() : a >= 10 ? String(Math.round(x * 10) / 10) : String(Math.round(x * 100) / 100); };
const reduced = () => window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
async function api(path, body) {
const r = await fetch(path, { method: body ? "POST" : "GET", headers: body ? { "Content-Type": "application/json" } : {}, body: body ? JSON.stringify(body) : undefined });
let data = null; try { data = await r.json(); } catch (e) { /* not json */ }
if (!r.ok) throw new Error((data && (data.detail || data.message)) || `Request failed (${r.status})`);
return data;
}
const cols = () => (V.data ? V.data.columns : []);
const colType = (name) => (cols().find((c) => c.name === name) || {}).type;
const textCols = () => cols().filter((c) => c.type === "text").map((c) => c.name);
const catCols = () => cols().filter((c) => c.type === "cat").map((c) => c.name);
/* ------------------------------------------------------------ charts (hand-set SVG, two inks) */
function niceTicks(lo, hi, want = 5) {
if (!(hi > lo)) { hi = lo + 1; }
const raw = (hi - lo) / want, e = Math.pow(10, Math.floor(Math.log10(raw)));
const step = [1, 2, 2.5, 5, 10].map((m) => m * e).find((s) => s >= raw) || 10 * e;
const out = []; for (let v = Math.floor(lo / step) * step; v <= hi + step * 1e-9; v += step) out.push(+v.toFixed(10));
if (out[out.length - 1] < hi) out.push(out[out.length - 1] + step);
return out;
}
function labelW(keys, w) { const longest = Math.max(4, ...keys.map((k) => String(k).length)); return Math.min(Math.round(w * 0.38), Math.max(70, longest * 7.4 + 16)); }
/* narrow charts set each label above its bar instead of clipping it beside the bar */
function narrow(marks, w) { const lw = labelW(marks.map((m) => m.key), w); const longest = Math.max(0, ...marks.map((m) => String(m.key).length)); return w < 560 && longest * 7.4 + 16 > lw; }
function labText(key, lw, y, nar, w) {
return nar ? `<text class="lab" x="0" y="${y + 13}">${esc(clip(key, w - 10))}</text>` : `<text class="lab" x="${lw - 10}" y="${y + 19}" text-anchor="end">${esc(clip(key, lw - 12))}</text>`;
}
function clip(t, px) { const max = Math.max(4, Math.floor(px / 7.1)); t = String(t); return t.length > max ? t.slice(0, max - 1) + "…" : t; }
function colorMap(keys) { const m = {}; let i = 0; keys.forEach((k) => { m[k] = /^Other \(|^\(blank\)$|^unclear$/.test(k) ? OTHER : INKS[i++ % INKS.length]; }); return m; }
function mark(attrs, label, ids, inner) {
return `<g class="mk" tabindex="0" role="button" aria-label="${esc(label)}" data-vzsel="${esc(label.split(": ")[0])}" data-vzids="${ids.join(",")}" ${attrs}>${inner}</g>`;
}
function sorted(c, key) {
const how = V.sorts[key] || "value";
const m = (c.marks || []).slice();
if (how === "name") m.sort((a, b) => String(a.key).localeCompare(String(b.key), undefined, { numeric: true }));
return m;
}
function svgBar(c, w, key) {
const marks = sorted(c, key), nar = narrow(marks, w), row = nar ? 42 : 30, top = 6, lw = nar ? 0 : labelW(marks.map((m) => m.key), w), vw = 54, by = nar ? 14 : 0;
const bw = Math.max(60, w - lw - vw - 12), max = Math.max(...marks.map((m) => m.value), 1), h = top + marks.length * row + 4;
const yName = c.agg === "count" ? "rows" : `${c.agg} ${c.y}`;
const body = marks.map((m, i) => {
const y = top + i * row, len = Math.max(2, (m.value / max) * bw), lab = c.agg === "count" ? plural(m.value, "row") : `${fmt(m.value)} (${plural(m.n, "row")})`;
return mark(`style="--d:${i * 40}ms"`, `${c.x} = ${m.key}: ${lab}`, m.ids,
`${labText(m.key, lw, y, nar, w)}
<g class="grow" style="transform-origin:${lw}px 0"><rect class="ghost" x="${lw + 3}" y="${y + by + 7}" width="${len}" height="19"></rect>
<rect class="ink" x="${lw}" y="${y + by + 4}" width="${len}" height="19"></rect></g>
<text class="val" x="${lw + len + 8}" y="${y + by + 19}">${fmt(m.value)}</text><rect class="hit" x="0" y="${y}" width="${w}" height="${row}"></rect>`);
}).join("");
return { svg: `<svg viewBox="0 0 ${w} ${h}" width="${w}" height="${h}" role="img" aria-label="Bar chart of ${esc(yName)} by ${esc(c.x)}">${body}</svg>`, axis: `${c.x} · ${yName}` };
}
function svgStack(c, w, key) {
const marks = sorted(c, key), nar = narrow(marks, w), row = nar ? 42 : 30, top = 6, lw = nar ? 0 : labelW(marks.map((m) => m.key), w), vw = 46, by = nar ? 14 : 0;
const bw = Math.max(60, w - lw - vw - 10), share = c.agg !== "count", maxN = Math.max(...marks.map((m) => m.n), 1), cm = colorMap(c.colors || []);
const h = top + marks.length * row + 4;
const body = marks.map((m, i) => {
const y = top + i * row, scale = share ? bw / Math.max(1, m.n) : bw / maxN; let x = lw;
const segs = m.parts.map((p) => {
const sw = p.value * scale, pct = Math.round((100 * p.value) / m.n);
const g = mark("", `${c.x} = ${m.key}, ${c.color} = ${p.key}: ${plural(p.value, "row")} (${pct}%)`, p.ids,
`<rect class="seg" x="${x}" y="${y + by + 4}" width="${Math.max(1, sw - 1)}" height="19" fill="${cm[p.key]}"></rect>${sw > 30 ? `<text class="segv" x="${x + 5}" y="${y + by + 17.5}">${share ? pct + "%" : p.value}</text>` : ""}`);
x += sw; return g;
}).join("");
return `<g class="grow" style="--d:${i * 40}ms;transform-origin:${lw}px 0">${mark("", `${c.x} = ${m.key}: ${plural(m.n, "row")}`, m.ids, `${labText(m.key, lw, y, nar, w)}<rect class="hit" x="0" y="${y}" width="${nar ? w : lw}" height="${nar ? 15 : row}"></rect>`)}${segs}<text class="val" x="${lw + (share ? bw : m.n * scale) + 8}" y="${y + by + 19}">${m.n}</text></g>`;
}).join("");
const legend = (c.colors || []).map((k) => {
const ids = marks.flatMap((m) => (m.parts.find((p) => p.key === k) || { ids: [] }).ids);
return `<button class="lg" data-vzsel="${esc(`${c.color} = ${k}`)}" data-vzids="${ids.join(",")}"><i style="background:${cm[k]}"></i>${esc(k)}</button>`;
}).join("");
return { svg: `<svg viewBox="0 0 ${w} ${h}" width="${w}" height="${h}" role="img" aria-label="Stacked bars of ${esc(c.color)} within each ${esc(c.x)}">${body}</svg>`, legend, axis: `${c.x} · ${share ? "share of rows" : "rows"}, split by ${c.color}` };
}
function jitter(id) { const s = Math.sin(id * 12.9898) * 43758.5453; return s - Math.floor(s) - 0.5; }
function svgBox(c, w, key) {
const marks = sorted(c, key), nar = c.x && narrow(marks, w), row = nar ? 54 : 38, top = 6, lw = c.x && !nar ? labelW(marks.map((m) => m.key), w) : 8, bw = Math.max(80, w - lw - 16), by = nar ? 12 : 0, amp = nar ? 16 : row - 16;
const all = marks.flatMap((m) => m.points.map((p) => p[1])), ticks = niceTicks(Math.min(...all), Math.max(...all));
const lo = ticks[0], hi = ticks[ticks.length - 1], X = (v) => lw + ((v - lo) / (hi - lo || 1)) * bw, h = top + marks.length * row + 26;
const grid = ticks.map((t) => `<line class="grid" x1="${X(t)}" x2="${X(t)}" y1="${top}" y2="${h - 22}"></line><text class="tick" x="${X(t)}" y="${h - 6}" text-anchor="middle">${fmt(t)}</text>`).join("");
const body = marks.map((m, i) => {
const y = top + i * row, mid = y + row / 2 - 2 + by, thin = m.n < 3 ? " thin" : "";
const dots = m.points.map(([id, v]) => `<circle class="dot mk1" cx="${X(v)}" cy="${mid + jitter(id) * amp}" r="3.4" data-vzsel="row ${id}" data-vzids="${id}" tabindex="-1"><title>Row ${id}: ${fmt(v)}</title></circle>`).join("");
return `<g class="grow-o${thin}" style="--d:${i * 50}ms">${mark("", `${c.x ? `${c.x} = ${m.key}: ` : ""}median ${fmt(m.median)}, middle half ${fmt(m.q1)} to ${fmt(m.q3)}, ${plural(m.n, "row")}`, m.ids,
`${c.x ? (nar ? labText(m.key, 0, y, true, w) : `<text class="lab" x="${lw - 10}" y="${mid + 4}" text-anchor="end">${esc(clip(m.key, lw - 12))}</text>`) : ""}${m.n >= 4 ? `<line class="whisk" x1="${X(m.lo)}" x2="${X(m.hi)}" y1="${mid}" y2="${mid}"></line>
<rect class="bx" x="${X(m.q1)}" y="${mid - 9}" width="${Math.max(2, X(m.q3) - X(m.q1))}" height="18"></rect>` : ""}<rect class="hit" x="0" y="${y}" width="${nar ? w : lw}" height="${nar ? 16 : row}"></rect>`)}
${dots}<line class="med" x1="${X(m.median)}" x2="${X(m.median)}" y1="${mid - 11}" y2="${mid + 11}"></line></g>`;
}).join("");
return { svg: `<svg viewBox="0 0 ${w} ${h}" width="${w}" height="${h}" role="img" aria-label="Box plot of ${esc(c.y)}${c.x ? ` by ${esc(c.x)}` : ""}">${grid}${body}</svg>`, axis: `${c.y}${c.x ? ` by ${c.x}` : ""} · each dot is a row` };
}
function svgHist(c, w) {
const marks = c.marks || [], h = 210, left = 34, bottom = 26, top = 8, bw = (w - left - 8) / Math.max(1, marks.length);
const max = Math.max(...marks.map((m) => m.value), 1), yt = niceTicks(0, max, 4), Y = (v) => top + (1 - v / yt[yt.length - 1]) * (h - top - bottom);
const grid = yt.map((t) => `<line class="grid" x1="${left}" x2="${w - 4}" y1="${Y(t)}" y2="${Y(t)}"></line><text class="tick" x="${left - 6}" y="${Y(t) + 4}" text-anchor="end">${fmt(t)}</text>`).join("");
const body = marks.map((m, i) => {
const x = left + i * bw, y = Y(m.value);
return mark(`style="--d:${i * 35}ms"`, c.discrete ? `${c.x} = ${fmt(m.x0)}: ${plural(m.value, "row")}` : `${fmt(m.x0)} to ${fmt(m.x1)}: ${plural(m.value, "row")}`, m.ids,
`<g class="grow-y" style="transform-origin:0 ${h - bottom}px"><rect class="ghost" x="${x + 4}" y="${y + 3}" width="${Math.max(1, bw - 4)}" height="${Math.max(0, h - bottom - y)}"></rect>
<rect class="ink" x="${x + 1}" y="${y}" width="${Math.max(1, bw - 4)}" height="${Math.max(0, h - bottom - y)}"></rect></g>
<rect class="hit" x="${x}" y="${top}" width="${bw}" height="${h - top - bottom}"></rect>`);
}).join("");
const every = Math.ceil(marks.length / Math.max(2, Math.floor((w - left) / 52)));
const xl = c.discrete ? marks.map((m, i) => `<text class="tick" x="${left + i * bw + bw / 2}" y="${h - 8}" text-anchor="middle">${fmt(m.x0)}</text>`).join("") :
marks.map((m, i) => (i % every === 0 ? `<text class="tick" x="${left + i * bw}" y="${h - 8}" text-anchor="middle">${fmt(m.x0)}</text>` : "")).join("") +
(marks.length ? `<text class="tick" x="${left + marks.length * bw}" y="${h - 8}" text-anchor="end">${fmt(marks[marks.length - 1].x1)}</text>` : "");
return { svg: `<svg viewBox="0 0 ${w} ${h}" width="${w}" height="${h}" role="img" aria-label="Histogram of ${esc(c.x)}">${grid}${body}${xl}</svg>`, axis: `${c.x} · ${c.discrete ? "rows at each value" : "rows in each range"}` };
}
function svgScatter(c, w) {
const pts = c.marks || [], h = Math.min(360, Math.max(240, w * 0.55)), left = 40, bottom = 26, top = 8;
const xt = niceTicks(Math.min(...pts.map((p) => p.x)), Math.max(...pts.map((p) => p.x))), yt = niceTicks(Math.min(...pts.map((p) => p.y)), Math.max(...pts.map((p) => p.y)), 4);
const X = (v) => left + ((v - xt[0]) / (xt[xt.length - 1] - xt[0] || 1)) * (w - left - 10), Y = (v) => top + (1 - (v - yt[0]) / (yt[yt.length - 1] - yt[0] || 1)) * (h - top - bottom);
const cm = colorMap(c.colors || []);
const grid = xt.map((t) => `<line class="grid" x1="${X(t)}" x2="${X(t)}" y1="${top}" y2="${h - bottom}"></line><text class="tick" x="${X(t)}" y="${h - 8}" text-anchor="middle">${fmt(t)}</text>`).join("") +
yt.map((t) => `<line class="grid" x1="${left}" x2="${w - 10}" y1="${Y(t)}" y2="${Y(t)}"></line><text class="tick" x="${left - 6}" y="${Y(t) + 4}" text-anchor="end">${fmt(t)}</text>`).join("");
const dots = pts.map((p) => `<circle class="dot mk1" cx="${X(p.x)}" cy="${Y(p.y)}" r="4.2" style="${p.c ? `fill:${cm[p.c]}` : ""}" data-vzsel="row ${p.id}" data-vzids="${p.id}" tabindex="0" role="button" aria-label="Row ${p.id}: ${esc(c.x)} ${fmt(p.x)}, ${esc(c.y)} ${fmt(p.y)}"><title>Row ${p.id}</title></circle>`).join("");
const legend = (c.colors || []).map((k) => `<button class="lg" data-vzsel="${esc(`${c.color} = ${k}`)}" data-vzids="${pts.filter((p) => p.c === k).map((p) => p.id).join(",")}"><i style="background:${cm[k]}"></i>${esc(k)}</button>`).join("");
return { svg: `<svg viewBox="0 0 ${w} ${h}" width="${w}" height="${h}" role="img" aria-label="Scatter of ${esc(c.y)} against ${esc(c.x)}">${grid}${dots}</svg>`, legend, axis: `${c.x} → · ${c.y} ↑${c.r !== undefined ? ` · r = ${c.r}` : ""}` };
}
function chartHtml(c, w, key, fresh) {
if (!c || !(c.marks || []).length) return `<p class="hint">Nothing to draw: no rows have values for this.</p>`;
const f = { bar: svgBar, stack: svgStack, box: svgBox, hist: svgHist, scatter: svgScatter }[c.type];
const out = f(c, Math.max(260, Math.floor(w)), key);
const sortable = ["bar", "stack", "box"].includes(c.type) && (c.marks || []).length > 2;
return `${out.legend ? `<div class="legend">${out.legend}</div>` : ""}<div class="svgwrap${fresh ? " fresh" : ""}">${out.svg}</div>
<div class="axisline"><span>${esc(out.axis)}</span>${sortable ? `<button class="linkish sm" data-vzsort="${esc(key)}">${(V.sorts[key] || "value") === "value" ? "Sort by name" : "Sort by size"}</button>` : ""}</div>`;
}
/* ------------------------------------------------------------ pages */
function pickHtml() {
const job = window.DI_lastJob && window.DI_lastJob();
const card = (title, body) => `<div class="vsrc"><b>${title}</b>${body}</div>`;
const sizes = `<div class="seg" role="group" aria-label="Rows to load">${SIZES.filter((n) => n <= V.maxRows).map((n) => `<button data-vzn="${n}" aria-pressed="${V.n === n}">${n}</button>`).join("")}</div>`;
return `<h1>Ask your data a question</h1>
<p class="lede">Get a chart back, with the rows behind every bar one click away. It is built for the small datasets you write here: every number is exact, and the first charts need no model at all.</p>
${V.loadErr ? `<p class="note bad">${esc(V.loadErr)}</p>` : ""}
<div class="vsrcs">
${job ? card("The rows you just wrote", `<p class="hint">${plural(job.n, "row")} from “${esc(job.request.slice(0, 90))}”</p><div class="actions"><button class="btn go" data-vz="load-job" ${V.loadBusy ? "disabled" : ""}>${V.loadBusy === "job" ? "Loading" : "Look at them"}</button></div>`) : ""}
${card("A sample dataset", `<p class="hint">Written with DataInventor and published on Hugging Face. Pick how many rows to load.</p>${sizes}
<ul class="vsamples">${V.samples.map((s) => `<li><button class="vsample" data-vzsample="${esc(s.key)}" ${V.loadBusy ? "disabled" : ""}>${esc(s.title)}${V.loadBusy === s.key ? " <em>loading</em>" : ""}</button></li>`).join("")}</ul>`)}
${card("A Hugging Face dataset", `<p class="hint">Any public dataset the viewer can read. A spread of rows is loaded, not the first page.</p>
<div class="addrow"><input class="in" id="vz-hub" placeholder="owner/name" value="${esc(V.hub)}" spellcheck="false" autocomplete="off"><button class="btn" data-vz="load-hub" ${V.loadBusy || !V.hub.trim() ? "disabled" : ""}>${V.loadBusy === "hub" ? "Loading" : "Load"}</button></div>`)}
${card("A file", `<p class="hint">CSV with a header row, JSONL, or a JSON list of objects. Up to ${V.maxRows} rows are used.</p>
<label class="btn filebtn"><input type="file" id="vz-file" accept=".csv,.jsonl,.json,.txt" ${V.loadBusy ? "disabled" : ""}>${V.loadBusy === "file" ? "Reading" : "Choose a file"}</label>`)}
</div>`;
}
function stagesHtml() {
const a = V.asking; if (!a) return "";
const idx = STAGES.findIndex(([k]) => k === a.stage);
const items = STAGES.filter(([k]) => k !== "label" || a.total).map(([k, name]) => {
const i = STAGES.findIndex(([x]) => x === k), cls = i < idx ? "done" : i === idx ? "on" : "";
const extra = k === "label" && a.total ? ` ${a.done} of ${a.total}` : "";
return `<li class="${cls}">${name}${extra}</li>`;
}).join("");
return `<ol class="stages vz-stages" aria-live="polite">${items}</ol>`;
}
function answerHtml() {
const h = V.history[V.cur]; if (!h) return "";
const r = h.result, key = `a${V.cur}`;
return `<article class="answer" id="vz-answer">
${h.scopeLabel ? `<p class="hint">About ${esc(h.scopeLabel)}</p>` : ""}
<h2>${esc(r.title)}</h2>
${r.summary ? `<p class="prose">${esc(r.summary)}</p>` : V.asking && V.asking.stage === "write" ? `<p class="prose hint">Writing it up…</p>` : `<ul class="facts">${(r.facts || []).slice(0, 6).map((f) => `<li>${esc(f)}</li>`).join("")}</ul>`}
<div class="chart" id="vz-chart" data-vzkey="${key}"></div>
${(r.notes || []).map((n) => `<p class="hint vnote">${esc(n)}</p>`).join("")}
<div class="vtools"><button class="linkish sm" data-vz="svg">Download the chart</button>${r.chart.n ? `<a class="linkish sm" href="/api/vis/${V.data.id}/download?ids=${allIds(r.chart).join(",")}">Download its rows</a>` : ""}</div>
${r.follow_ups && r.follow_ups.length ? `<div class="chips">${r.follow_ups.map((q) => `<button class="chip" data-vzq="${esc(q)}">${esc(q)}</button>`).join("")}</div>` : ""}
</article>`;
}
function allIds(c) {
if (c.type === "scatter") return c.marks.map((m) => m.id);
const s = new Set(); (c.marks || []).forEach((m) => (m.ids || []).forEach((i) => s.add(i))); return [...s];
}
function glanceHtml() {
const g = V.data.glance || []; if (!g.length) return "";
return `<section class="glance"><h3>At a glance</h3><div class="gl-grid">${g.map((c, i) => `<div class="gl"><b>${esc(c.title)}</b><div class="chart small" id="vz-g${i}" data-vzkey="g${i}"></div></div>`).join("")}</div></section>`;
}
function rowHtml(r) {
const long = cols().filter((c) => c.type === "text" && (c.avg_words || 0) >= 12).map((c) => c.name);
const short = textCols().filter((k) => !long.includes(k));
const head = long[0] || short[0], rest = long.slice(1);
const chips = short.filter((k) => k !== head).concat(catCols()).map((k) => r[k] ? `<span class="tagc${derivedCol(k) ? " made" : ""}" title="${esc(k)}">${esc(String(r[k]).slice(0, 80))}</span>` : "").join("");
const nums = cols().filter((c) => c.type === "num" && derivedCol(c.name) && r[c.name] != null).map((c) => `<span class="tagc made" title="${esc(c.name)}">${esc(c.name)} ${fmt(r[c.name])}</span>`).join("");
return `<div class="trow"><span class="id">${r._id}</span><div>${head ? `<div class="p">${esc(String(r[head] || "").slice(0, 320))}${String(r[head] || "").length > 320 ? "…" : ""}</div>` : ""}
<div class="tags">${chips}${nums}</div>
${rest.map((k) => `<details><summary>Read the ${esc(k)}</summary><div class="full">${esc(r[k])}</div></details>`).join("")}
${head && String(r[head] || "").length > 320 ? `<details><summary>Read the whole ${esc(head)}</summary><div class="full">${esc(r[head])}</div></details>` : ""}</div></div>`;
}
const derivedCol = (k) => { const c = cols().find((x) => x.name === k); return c && ["label", "score", "pattern", "count"].includes(c.derived); };
function selHtml() {
const s = V.sel; if (!s) return "";
const rows = s.ids.map((i) => V.byId[i]).filter(Boolean);
return `<section class="drawer" id="vz-sel" aria-label="Selected rows">
<div class="drawhead"><div><b>${plural(rows.length, "row")}</b> <span class="hint">${esc(s.label)}</span></div>
<div class="sctl">${rows.length > 1 ? `<button class="btn sm" data-vz="scope">Ask about these rows</button>` : ""}<a class="btn sm" href="/api/vis/${V.data.id}/download?ids=${s.ids.join(",")}" style="text-decoration:none">Download them</a><button class="linkish sm" data-vz="clear">Close</button></div></div>
<div class="table">${rows.slice(0, V.selShow).map(rowHtml).join("")}</div>
${rows.length > V.selShow ? `<p style="margin-top:12px"><button class="linkish" data-vz="more">Show ${Math.min(30, rows.length - V.selShow)} more</button></p>` : ""}
</section>`;
}
function askHtml() {
const sug = V.history.length ? [] : V.data.suggestions || [];
const busy = !!V.asking;
return `<div class="askbar">
${V.scope ? `<div class="scopechip">Asking about ${esc(V.scope.label)} (${plural(V.scope.ids.length, "row")}) <button class="linkish sm" data-vz="unscope" aria-label="Ask about all rows again">Use all rows</button></div>` : ""}
<label class="f" style="margin-top:0"><span class="vh">Your question</span>
<textarea class="in vq" id="vz-q" rows="2" placeholder="How long are the replies in each area?" ${busy ? "disabled" : ""}>${esc(V.q)}</textarea></label>
<div class="actions" style="margin-top:12px"><button class="btn go" data-vz="ask" ${busy || !V.q.trim() ? "disabled" : ""}>${busy ? "Working on it" : "Ask"}</button>${stagesHtml()}</div>
${V.askErr ? `<p class="note bad">${esc(V.askErr)}</p>` : ""}
${sug.length ? `<div class="chips">${sug.map((q) => `<button class="chip" data-vzq="${esc(q)}" ${busy ? "disabled" : ""}>${esc(q)}</button>`).join("")}</div>` : ""}
</div>`;
}
function mainHtml() {
return `${askHtml()}<div id="vz-out">${answerHtml()}</div>${V.cur < 0 ? glanceHtml() : ""}<div id="vz-selwrap">${selHtml()}</div>`;
}
function ledgerHtml() {
if (!V.data) return `<li class="on"><button aria-current="step"><span class="n">1</span><span>Pick the data</span></button></li><li><button disabled><span class="n">2</span><span>Ask</span></button></li>`;
const items = V.history.map((h, i) => `<li class="${i === V.cur ? "on" : "done"}"><button data-vzh="${i}" ${i === V.cur ? 'aria-current="true"' : ""}><span class="n">${i + 1}</span><span>${esc(h.q.length > 64 ? h.q.slice(0, 62) + "…" : h.q)}</span></button></li>`).join("");
return `<li class="${V.cur < 0 ? "on" : "done"}"><button data-vzh="-1"><span class="n">·</span><span>At a glance</span></button></li>${items}`;
}
function sheetHtml() {
if (!V.data) return `<h2>Your data</h2><p class="hint">Nothing loaded yet. Charts are drawn from every row you load, so a small sample is fast and exact.</p>`;
const d = V.data, group = (t, f) => { const list = cols().filter(f); return list.length ? `<div class="line"><dt>${t}</dt><dd>${list.map((c) => `<span class="tagc${derivedCol(c.name) ? " made" : ""}">${esc(c.name)}</span>`).join(" ")}</dd></div>` : ""; };
return `<h2>Your data</h2><dl>
<div class="line"><dt>Name</dt><dd>${esc(d.name)}</dd></div>
<div class="line"><dt>From</dt><dd>${/\//.test(d.source) ? `<a href="https://huggingface.co/datasets/${esc(d.source)}" target="_blank" rel="noopener">${esc(d.source)}</a>` : esc(d.source)}</dd></div>
<div class="line"><dt>Rows</dt><dd>${d.n.toLocaleString()}</dd></div>
${group("Text", (c) => c.type === "text")}${group("Categories", (c) => c.type === "cat" && !derivedCol(c.name))}
${group("Numbers", (c) => c.type === "num" && !derivedCol(c.name))}${group("Made by the model", (c) => derivedCol(c.name))}
</dl>${d.note ? `<p class="hint" style="margin-top:12px">${esc(d.note)}</p>` : ""}
<p class="hint" style="margin-top:14px">Columns the model makes stay here, so follow-up questions reuse them instead of reading every row again.</p>
<div class="actions" style="margin-top:18px"><button class="btn" data-vz="change">Use different data</button></div>`;
}
function render() {
if (!on()) return;
$("#steps").innerHTML = ledgerHtml();
$("#sheet").innerHTML = sheetHtml();
const keep = document.activeElement && document.activeElement.id;
$("#panel").innerHTML = V.data ? mainHtml() : pickHtml();
if (V.data) { redrawCharts(); markSelection(); }
if (keep) { const el = document.getElementById(keep); if (el) { el.focus(); if (el.setSelectionRange && el.tagName === "TEXTAREA") { const l = el.value.length; el.setSelectionRange(l, l); } } }
}
/* charts need their container's width, which exists only after the first paint */
function redrawCharts() {
document.querySelectorAll("[data-vzkey]").forEach((el) => {
const key = el.dataset.vzkey, c = key.startsWith("g") ? V.data.glance[+key.slice(1)] : (V.history[+key.slice(1)] || {}).result?.chart;
if (c) { el.innerHTML = chartHtml(c, el.clientWidth || 600, key, !V.drawn[key] && !reduced()); V.drawn[key] = true; }
});
markSelection();
}
function markSelection() {
const ids = V.sel ? new Set(V.sel.ids) : null;
document.querySelectorAll("[data-vzids]").forEach((el) => {
const mine = el.dataset.vzids.split(",").filter(Boolean).map(Number);
const hit = ids && mine.length && mine.every((i) => ids.has(i)) && (V.sel.label === el.dataset.vzsel || el.tagName.toLowerCase() === "circle" && ids.has(mine[0]));
el.classList.toggle("sel", !!hit);
el.classList.toggle("dim", !!ids && !hit && !el.classList.contains("lg") && !mine.some((i) => ids.has(i)));
});
}
function refreshAsk() { const el = $(".askbar"); if (el) { el.outerHTML = askHtml(); } }
/* ------------------------------------------------------------ actions */
async function loadData(body, busyKey) {
V.loadBusy = busyKey; V.loadErr = ""; render();
try {
const d = await api("/api/vis/load", body);
V.data = d; V.rows = d.rows; V.byId = Object.fromEntries(d.rows.map((r) => [r._id, r]));
V.history = []; V.cur = -1; V.sel = null; V.scope = null; V.q = ""; V.askErr = ""; V.sorts = {}; V.drawn = {};
} catch (e) { V.loadErr = e.message; }
V.loadBusy = ""; render(); window.scrollTo({ top: 0 });
}
async function ask(q) {
q = (q || "").trim(); if (!q || V.asking || !V.data) return;
V.q = q; V.askErr = ""; V.asking = { stage: "plan", done: 0, total: 0 }; refreshAsk();
const scope = V.scope;
try {
const { ask: aid } = await api(`/api/vis/${V.data.id}/ask`, { question: q, scope: scope ? scope.ids : [], scope_label: scope ? scope.label : "" });
let shown = false;
for (;;) {
await new Promise((r) => setTimeout(r, 650));
const s = await api(`/api/vis/ask/${aid}`);
V.asking = s;
if (s.stage === "failed") throw new Error(s.error || "That question could not be answered.");
if (s.result && !shown) {
shown = true; mergeResult(s.result);
V.history.push({ q, result: s.result, scopeLabel: scope ? `${scope.label} (${plural(scope.ids.length, "row")})` : "" });
V.cur = V.history.length - 1; V.sel = null; render();
const out = $("#vz-out"); if (out && out.getBoundingClientRect().top > window.innerHeight * 0.6) out.scrollIntoView({ behavior: reduced() ? "auto" : "smooth", block: "start" });
} else if (shown) { V.history[V.history.length - 1].result = s.result; }
if (s.stage === "done") break;
if (!shown) refreshAsk(); else { const st = $(".vz-stages"); if (st) st.outerHTML = stagesHtml(); }
}
V.asking = null; V.q = ""; render();
} catch (e) { V.asking = null; V.askErr = e.message; render(); }
}
function mergeResult(r) {
Object.entries(r.derived || {}).forEach(([name, vals]) => Object.entries(vals).forEach(([id, v]) => { if (V.byId[id]) V.byId[id][name] = v; }));
if (r.columns) V.data.columns = r.columns;
}
function select(label, ids) {
if (V.sel && V.sel.label === label) { V.sel = null; } else { V.sel = { label, ids }; V.selShow = 30; }
const w = $("#vz-selwrap"); if (w) w.innerHTML = selHtml();
markSelection();
const d = $("#vz-sel"); if (d) d.scrollIntoView({ behavior: reduced() ? "auto" : "smooth", block: "nearest" });
}
function downloadSvg() {
const svg = $("#vz-chart svg"); if (!svg) return;
const css = `text{font:13px "Familjen Grotesk",system-ui,sans-serif;fill:#1d2036}.tick{font-size:11.5px;fill:#5d6376}.ink{fill:#3a4bff}.ghost{fill:#ff5c8f;opacity:.85}.grid{stroke:#c5c8d1}.bx{fill:#f1f2f4;stroke:#1d2036;stroke-width:1.5}.whisk{stroke:#1d2036;stroke-width:1.5}.med{stroke:#ff5c8f;stroke-width:3.5}.dot{fill:#3a4bff;opacity:.75}.hit{fill:transparent}.segv{fill:#fff;font-size:11.5px;font-weight:600}.val{font-weight:600}`;
const clone = svg.cloneNode(true); clone.setAttribute("xmlns", "http://www.w3.org/2000/svg");
clone.insertAdjacentHTML("afterbegin", `<style>${css}</style><rect width="100%" height="100%" fill="#e7e8ea"></rect>`);
const a = document.createElement("a"); a.href = URL.createObjectURL(new Blob([clone.outerHTML], { type: "image/svg+xml" }));
a.download = (V.history[V.cur]?.result.title || "chart").toLowerCase().replace(/[^a-z0-9]+/g, "-").slice(0, 50) + ".svg"; a.click();
setTimeout(() => URL.revokeObjectURL(a.href), 2000);
}
document.addEventListener("click", (ev) => {
if (!on()) return;
const t = ev.target.closest("[data-vz],[data-vzsel],[data-vzq],[data-vzn],[data-vzsample],[data-vzh],[data-vzsort]"); if (!t) return;
if (t.dataset.vzn) { V.n = +t.dataset.vzn; return render(); }
if (t.dataset.vzsample) return loadData({ source: "sample", key: t.dataset.vzsample, n: V.n }, t.dataset.vzsample);
if (t.dataset.vzq) { V.q = t.dataset.vzq; return ask(V.q); }
if (t.dataset.vzh !== undefined) { V.cur = +t.dataset.vzh; V.sel = null; render(); return window.scrollTo({ top: 0 }); }
if (t.dataset.vzsort) { const k = t.dataset.vzsort; V.sorts[k] = (V.sorts[k] || "value") === "value" ? "name" : "value"; return redrawCharts(); }
if (t.dataset.vzsel !== undefined && t.dataset.vzids !== undefined && !t.dataset.vz) {
ev.stopPropagation();
return select(t.dataset.vzsel, t.dataset.vzids.split(",").filter(Boolean).map(Number));
}
const a = t.dataset.vz;
if (a === "load-job") { const j = window.DI_lastJob && window.DI_lastJob(); if (j) loadData({ source: "job", job: j.id }, "job"); }
else if (a === "load-hub") loadData({ source: "hub", dataset: V.hub.trim(), n: V.n }, "hub");
else if (a === "ask") ask(V.q);
else if (a === "clear") select(V.sel ? V.sel.label : "", []);
else if (a === "more") { V.selShow += 30; const w = $("#vz-selwrap"); if (w) w.innerHTML = selHtml(); }
else if (a === "scope" && V.sel) { V.scope = { label: V.sel.label, ids: V.sel.ids.slice() }; V.sel = null; render(); const q = $("#vz-q"); if (q) { q.focus(); q.scrollIntoView({ block: "center" }); } }
else if (a === "unscope") { V.scope = null; refreshAsk(); }
else if (a === "change") { V.data = null; V.history = []; V.cur = -1; V.sel = null; V.scope = null; render(); }
else if (a === "svg") downloadSvg();
});
/* marks are SVG groups: Enter or Space opens them like a button */
document.addEventListener("keydown", (ev) => {
if (!on()) return;
if ((ev.key === "Enter" || ev.key === " ") && ev.target.matches && ev.target.matches("g.mk, circle.mk1")) { ev.preventDefault(); ev.target.dispatchEvent(new MouseEvent("click", { bubbles: true })); }
if (ev.key === "Enter" && !ev.shiftKey && ev.target.id === "vz-q") { ev.preventDefault(); ask(V.q); }
if (ev.key === "Enter" && ev.target.id === "vz-hub" && V.hub.trim()) loadData({ source: "hub", dataset: V.hub.trim(), n: V.n }, "hub");
});
document.addEventListener("input", (ev) => {
if (!on()) return;
if (ev.target.id === "vz-q") { V.q = ev.target.value; const b = $('[data-vz="ask"]'); if (b) b.disabled = !!V.asking || !V.q.trim(); }
if (ev.target.id === "vz-hub") { V.hub = ev.target.value; const b = $('[data-vz="load-hub"]'); if (b) b.disabled = !!V.loadBusy || !V.hub.trim(); }
});
document.addEventListener("change", (ev) => {
if (!on() || ev.target.id !== "vz-file" || !ev.target.files.length) return;
const f = ev.target.files[0];
if (f.size > 6e6) { V.loadErr = "That file is over 6 MB. Upload a smaller sample."; return render(); }
const rd = new FileReader();
rd.onload = () => loadData({ source: "upload", text: String(rd.result), filename: f.name }, "file");
rd.onerror = () => { V.loadErr = "Could not read that file."; render(); };
rd.readAsText(f);
});
let rz = 0;
window.addEventListener("resize", () => { if (!on() || !V.data) return; clearTimeout(rz); rz = setTimeout(redrawCharts, 120); });
window.DI_renderVis = () => {
if (!V.samples.length) api("/api/vis/samples").then((d) => { V.samples = d.samples; V.maxRows = d.max_rows; if (on() && !V.data) render(); }).catch(() => {});
render();
};
/* "Visualize these rows" on a finished run */
window.DI_visualizeJob = (id) => { window.DI_setMode && window.DI_setMode("visualize"); loadData({ source: "job", job: id }, "job"); };
})();
|