1import { useState } from "react";
2import type { TreeNode } from "./tree";
4function fmt(x: number): string {
5 if (Number.isNaN(x)) return "NaN";
6 if (!Number.isFinite(x)) return x > 0 ? "Inf" : "-Inf";
7 return Number.isInteger(x) ? String(x) : Number(x.toPrecision(6)).toString();
8}
10// Hard cap on how many values/rows are ever rendered as text, to keep large
11// arrays (e.g. big surf grids) from freezing the UI.
12const MAX_RENDER = 5000;
13const STEP = 1000;
15function DataValues({ data }: { data: unknown }) {
16 const [limit, setLimit] = useState(200);
18 if (!Array.isArray(data) || data.length === 0)
19 return <p className="muted small">No data.</p>;
21 const isNumber = (e: unknown) => typeof e === "number";
22 const allNumbers = (data as unknown[]).every(isNumber);
24 const total = data.length;
25 const renderCap = Math.min(total, MAX_RENDER);
26 const shown = Math.min(limit, renderCap);
27 const hasMore = shown < renderCap;
28 const capped = total > MAX_RENDER && shown >= MAX_RENDER;
29 const unit = allNumbers ? "values" : "rows";
31 let body: string;
32 if (allNumbers) {
33 body = (data as number[]).slice(0, shown).map(fmt).join(", ");
34 } else {
35 body = (data as unknown[])
36 .slice(0, shown)
37 .map((r, i) => {
38 const text = Array.isArray(r)
39 ? `[${(r as number[]).map(fmt).join(", ")}]`
40 : fmt(r as number);
41 return `${i}: ${text}`;
42 })
43 .join("\n");
44 }
45 if (shown < total) body += allNumbers ? ", …" : "\n…";
47 return (
48 <>
49 <pre className="preview">{body}</pre>
50 <div className="vfooter">
51 <span className="muted small">
52 {shown < total ? `Showing ${shown} of ${total} ${unit}` : `${total} ${unit}`}
53 {capped ? " — display capped" : ""}
54 </span>
55 {hasMore && (
56 <button
57 className="text-btn sm"
58 onClick={() => setLimit(l => Math.min(l + STEP, renderCap))}
59 >
60 Show more
61 </button>
62 )}
63 {!capped && shown < total && total <= MAX_RENDER && (
64 <button className="text-btn sm" onClick={() => setLimit(total)}>
65 Show all
66 </button>
67 )}
68 </div>
69 </>
70 );
71}
73export function InfoPanel({ node }: { node: TreeNode | null }) {
74 if (!node) {
75 return (
76 <div className="info empty">
77 <p>Select an object in the tree to inspect it.</p>
78 </div>
79 );
80 }
82 const { detail } = node;
84 return (
85 <div className="info">
86 <div className="info-header">
87 <span className="info-title">{node.label}</span>
88 <span className="info-kind">
89 {detail.type === "dataset" ? "dataset" : detail.kind}
90 </span>
91 </div>
93 {detail.type === "object" ? (
94 detail.properties.length === 0 ? (
95 <p className="muted">No properties.</p>
96 ) : (
97 <table className="props">
98 <tbody>
99 {detail.properties.map(p => (
100 <tr key={p.key}>
101 <td className="pk">{p.key}</td>
102 <td className="pv">
103 {p.swatch && (
104 <span className="swatch" style={{ background: p.swatch }} />
105 )}
106 {p.value}
107 </td>
108 </tr>
109 ))}
110 </tbody>
111 </table>
112 )
113 ) : (
114 <div className="dataset-detail">
115 <table className="props">
116 <tbody>
117 <tr>
118 <td className="pk">shape</td>
119 <td className="pv">{detail.shapeLabel}</td>
120 </tr>
121 <tr>
122 <td className="pk">count</td>
123 <td className="pv">{detail.count}</td>
124 </tr>
125 {detail.stats && (
126 <>
127 <tr>
128 <td className="pk">min</td>
129 <td className="pv">{fmt(detail.stats.min)}</td>
130 </tr>
131 <tr>
132 <td className="pk">max</td>
133 <td className="pv">{fmt(detail.stats.max)}</td>
134 </tr>
135 <tr>
136 <td className="pk">mean</td>
137 <td className="pv">{fmt(detail.stats.mean)}</td>
138 </tr>
139 {detail.stats.nan > 0 && (
140 <tr>
141 <td className="pk">NaN</td>
142 <td className="pv">{detail.stats.nan}</td>
143 </tr>
144 )}
145 </>
146 )}
147 </tbody>
148 </table>
149 <div className="preview-label">values</div>
150 <DataValues key={node.id} data={detail.data} />
151 </div>
152 )}
153 </div>
154 );
155}