qsharp-lang 1.0.23-dev → 1.0.24-dev
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/compiler/compiler.d.ts +2 -0
- package/dist/compiler/compiler.js +3 -0
- package/dist/compiler/events.d.ts +0 -1
- package/dist/compiler/events.js +3 -6
- package/dist/compiler/worker-proxy.js +1 -0
- package/dist/samples.generated.js +5 -5
- package/lib/node/qsc_wasm.cjs +34 -0
- package/lib/node/qsc_wasm.d.cts +6 -0
- package/lib/node/qsc_wasm_bg.wasm +0 -0
- package/lib/web/qsc_wasm.d.ts +7 -0
- package/lib/web/qsc_wasm.js +34 -0
- package/lib/web/qsc_wasm_bg.wasm +0 -0
- package/package.json +5 -3
- package/ux/README.md +4 -0
- package/ux/histogram.tsx +438 -0
- package/ux/index.ts +11 -0
- package/ux/qsharp-ux.css +616 -0
- package/ux/reTable.tsx +113 -0
- package/ux/resultsTable.tsx +385 -0
- package/ux/spaceChart.tsx +148 -0
- package/ux/tsconfig.json +15 -0
package/ux/histogram.tsx
ADDED
|
@@ -0,0 +1,438 @@
|
|
|
1
|
+
// Copyright (c) Microsoft Corporation.
|
|
2
|
+
// Licensed under the MIT License.
|
|
3
|
+
|
|
4
|
+
import { useRef, useState } from "preact/hooks";
|
|
5
|
+
|
|
6
|
+
const enablePanning = false;
|
|
7
|
+
const altKeyPans = true;
|
|
8
|
+
|
|
9
|
+
const menuItems = [
|
|
10
|
+
{
|
|
11
|
+
category: "itemCount",
|
|
12
|
+
options: ["Show all", "Top 10", "Top 25"],
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
category: "sortOrder",
|
|
16
|
+
options: ["Sort a-z", "High to low", "Low to high"],
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
category: "labels",
|
|
20
|
+
options: ["Raw labels", "Ket labels", "No labels"],
|
|
21
|
+
},
|
|
22
|
+
];
|
|
23
|
+
const maxMenuOptions = 3;
|
|
24
|
+
const defaultMenuSelection: { [idx: string]: number } = {
|
|
25
|
+
itemCount: 0,
|
|
26
|
+
sortOrder: 0,
|
|
27
|
+
labels: 0,
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const reKetResult = /^\[(?:(Zero|One), *)*(Zero|One)\]$/;
|
|
31
|
+
function resultToKet(result: string): string {
|
|
32
|
+
if (typeof result !== "string") return "ERROR";
|
|
33
|
+
|
|
34
|
+
if (reKetResult.test(result)) {
|
|
35
|
+
// The result is a simple array of Zero and One
|
|
36
|
+
// The below will return an array of "Zero" or "One" in the order found
|
|
37
|
+
const matches = result.match(/(One|Zero)/g);
|
|
38
|
+
matches?.reverse();
|
|
39
|
+
let ket = "|";
|
|
40
|
+
matches?.forEach((digit) => (ket += digit == "One" ? "1" : "0"));
|
|
41
|
+
ket += "⟩";
|
|
42
|
+
return ket;
|
|
43
|
+
} else {
|
|
44
|
+
return result;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function Histogram(props: {
|
|
49
|
+
shotCount: number;
|
|
50
|
+
data: Map<string, number>;
|
|
51
|
+
filter: string;
|
|
52
|
+
onFilter: (filter: string) => void;
|
|
53
|
+
shotsHeader: boolean;
|
|
54
|
+
}) {
|
|
55
|
+
const [hoverLabel, setHoverLabel] = useState("");
|
|
56
|
+
const [scale, setScale] = useState({ zoom: 1.0, offset: 1.0 });
|
|
57
|
+
const [menuSelection, setMenuSelection] = useState(defaultMenuSelection);
|
|
58
|
+
|
|
59
|
+
const gMenu = useRef<SVGGElement>(null);
|
|
60
|
+
const gInfo = useRef<SVGGElement>(null);
|
|
61
|
+
|
|
62
|
+
let maxItemsToShow = 0; // All
|
|
63
|
+
switch (menuSelection["itemCount"]) {
|
|
64
|
+
case 1:
|
|
65
|
+
maxItemsToShow = 10;
|
|
66
|
+
break;
|
|
67
|
+
case 2:
|
|
68
|
+
maxItemsToShow = 25;
|
|
69
|
+
break;
|
|
70
|
+
}
|
|
71
|
+
const showKetLabels = menuSelection["labels"] === 1;
|
|
72
|
+
|
|
73
|
+
const bucketArray = [...props.data];
|
|
74
|
+
|
|
75
|
+
// Calculate bucket percentages before truncating for display
|
|
76
|
+
let totalAllBuckets = 0;
|
|
77
|
+
let sizeBiggestBucket = 0;
|
|
78
|
+
bucketArray.forEach((x) => {
|
|
79
|
+
totalAllBuckets += x[1];
|
|
80
|
+
sizeBiggestBucket = Math.max(x[1], sizeBiggestBucket);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
let histogramLabel = `${bucketArray.length} unique results`;
|
|
84
|
+
if (maxItemsToShow > 0) {
|
|
85
|
+
// Sort from high to low then take the first n
|
|
86
|
+
bucketArray.sort((a, b) => (a[1] < b[1] ? 1 : -1));
|
|
87
|
+
if (bucketArray.length > maxItemsToShow) {
|
|
88
|
+
histogramLabel = `Top ${maxItemsToShow} of ${histogramLabel}`;
|
|
89
|
+
bucketArray.length = maxItemsToShow;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
if (props.filter) {
|
|
93
|
+
histogramLabel += `. Shot filter: ${
|
|
94
|
+
showKetLabels ? resultToKet(props.filter) : props.filter
|
|
95
|
+
}`;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
bucketArray.sort((a, b) => {
|
|
99
|
+
// If they can be converted to numbers, then sort as numbers, else lexically
|
|
100
|
+
const ax = Number(a[0]);
|
|
101
|
+
const bx = Number(b[0]);
|
|
102
|
+
switch (menuSelection["sortOrder"]) {
|
|
103
|
+
case 1: // high-to-low
|
|
104
|
+
return a[1] < b[1] ? 1 : -1;
|
|
105
|
+
break;
|
|
106
|
+
case 2: // low-to-high
|
|
107
|
+
return a[1] > b[1] ? 1 : -1;
|
|
108
|
+
break;
|
|
109
|
+
default: // a-z
|
|
110
|
+
if (!isNaN(ax) && !isNaN(bx)) return ax < bx ? -1 : 1;
|
|
111
|
+
return a[0] < b[0] ? -1 : 1;
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
function onMouseOverRect(evt: MouseEvent) {
|
|
117
|
+
const target = evt.target as SVGRectElement;
|
|
118
|
+
const title = target.querySelector("title")?.textContent;
|
|
119
|
+
setHoverLabel(title || "");
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function onMouseOutRect() {
|
|
123
|
+
setHoverLabel("");
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function onClickRect(evt: MouseEvent) {
|
|
127
|
+
const targetElem = evt.target as SVGRectElement;
|
|
128
|
+
const rawLabel = targetElem.getAttribute("data-raw-label");
|
|
129
|
+
|
|
130
|
+
if (rawLabel === props.filter) {
|
|
131
|
+
// Clicked the already selected bar. Clear the filter
|
|
132
|
+
props.onFilter("");
|
|
133
|
+
} else {
|
|
134
|
+
props.onFilter(rawLabel || "");
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function toggleMenu() {
|
|
139
|
+
if (!gMenu.current) return;
|
|
140
|
+
if (gMenu.current.style.display === "inline") {
|
|
141
|
+
gMenu.current.style.display = "none";
|
|
142
|
+
} else {
|
|
143
|
+
gMenu.current.style.display = "inline";
|
|
144
|
+
if (gInfo.current) gInfo.current.style.display = "none";
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function menuClicked(category: string, idx: number) {
|
|
149
|
+
if (!gMenu.current) return;
|
|
150
|
+
const newMenuSelection = { ...menuSelection };
|
|
151
|
+
newMenuSelection[category] = idx;
|
|
152
|
+
setMenuSelection(newMenuSelection);
|
|
153
|
+
if (category === "itemCount") {
|
|
154
|
+
setScale({ zoom: 1, offset: 1 });
|
|
155
|
+
}
|
|
156
|
+
gMenu.current.style.display = "none";
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function toggleInfo() {
|
|
160
|
+
if (!gInfo.current) return;
|
|
161
|
+
|
|
162
|
+
gInfo.current.style.display === "inline"
|
|
163
|
+
? (gInfo.current.style.display = "none")
|
|
164
|
+
: (gInfo.current.style.display = "inline");
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Each menu item has a width of 32px and a height of 10px
|
|
168
|
+
// Menu items are 38px apart on the x-axis, and 11px on the y-axis.
|
|
169
|
+
const menuItemWidth = 38;
|
|
170
|
+
const menuItemHeight = 11;
|
|
171
|
+
const menuBoxWidth = menuItems.length * menuItemWidth - 2;
|
|
172
|
+
const menuBoxHeight = maxMenuOptions * menuItemHeight + 3;
|
|
173
|
+
|
|
174
|
+
const barAreaWidth = 163;
|
|
175
|
+
const barAreaHeight = 72;
|
|
176
|
+
const fontOffset = 1.2;
|
|
177
|
+
|
|
178
|
+
// Scale the below for when zoomed
|
|
179
|
+
const barBoxWidth = (barAreaWidth * scale.zoom) / bucketArray.length;
|
|
180
|
+
const barPaddingPercent = 0.1; // 10%
|
|
181
|
+
const barPaddingSize = barBoxWidth * barPaddingPercent;
|
|
182
|
+
const barFillWidth = barBoxWidth - 2 * barPaddingSize;
|
|
183
|
+
const showLabels = barBoxWidth > 5 && menuSelection["labels"] !== 2;
|
|
184
|
+
|
|
185
|
+
function onWheel(e: WheelEvent): void {
|
|
186
|
+
e.preventDefault();
|
|
187
|
+
|
|
188
|
+
// currentTarget is the element the listener is attached to, the main svg
|
|
189
|
+
// element in this case.
|
|
190
|
+
const svgElem = e.currentTarget as SVGSVGElement;
|
|
191
|
+
|
|
192
|
+
// Below gets the mouse location in the svg element coordinates. This stays
|
|
193
|
+
// consistent while the scroll is occuring (i.e. it is the point the mouse
|
|
194
|
+
// was at when scrolling started).
|
|
195
|
+
const mousePoint = new DOMPoint(e.clientX, e.clientY).matrixTransform(
|
|
196
|
+
svgElem.getScreenCTM()?.inverse(),
|
|
197
|
+
);
|
|
198
|
+
|
|
199
|
+
/*
|
|
200
|
+
While zooming, we want is to track the point the mouse is at when scrolling, and pin
|
|
201
|
+
that location on the screen. That means adjusting the scroll offset.
|
|
202
|
+
|
|
203
|
+
SVG translation is used to pan left and right, but zooming is done manually (making the
|
|
204
|
+
bars wider or thinner) to keep the fonts from getting streched, which occurs with scaling.
|
|
205
|
+
|
|
206
|
+
deltaX and deltaY do not accumulate across events, they are a new delta each time.
|
|
207
|
+
*/
|
|
208
|
+
|
|
209
|
+
let newScrollOffset = scale.offset;
|
|
210
|
+
let newZoom = scale.zoom;
|
|
211
|
+
|
|
212
|
+
// *** First handle any zooming ***
|
|
213
|
+
if (!altKeyPans || !e.altKey) {
|
|
214
|
+
newZoom = scale.zoom + e.deltaY * 0.05;
|
|
215
|
+
newZoom = Math.min(Math.max(1, newZoom), 50);
|
|
216
|
+
|
|
217
|
+
// On zooming in, need to shift left to maintain mouse point, and vice verca.
|
|
218
|
+
const oldChartWidth = barAreaWidth * scale.zoom;
|
|
219
|
+
const mousePointOnChart = 0 - scale.offset + mousePoint.x;
|
|
220
|
+
const percentRightOnChart = mousePointOnChart / oldChartWidth;
|
|
221
|
+
const chartWidthGrowth =
|
|
222
|
+
newZoom * barAreaWidth - scale.zoom * barAreaWidth;
|
|
223
|
+
const shiftLeftAdjust = percentRightOnChart * chartWidthGrowth;
|
|
224
|
+
newScrollOffset = scale.offset - shiftLeftAdjust;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// *** Then handle any panning ***
|
|
228
|
+
if (enablePanning) {
|
|
229
|
+
newScrollOffset -= e.deltaX;
|
|
230
|
+
}
|
|
231
|
+
if (!enablePanning && altKeyPans && e.altKey) {
|
|
232
|
+
newScrollOffset -= e.deltaY;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Don't allow offset > 1 (scrolls the first bar right of the left edge of the area)
|
|
236
|
+
// Don't allow for less than 0 - barwidths + screen width (scrolls last bar left of the right edge)
|
|
237
|
+
const maxScrollRight = 1 - (barAreaWidth * newZoom - barAreaWidth);
|
|
238
|
+
const boundScrollOffset = Math.min(
|
|
239
|
+
Math.max(newScrollOffset, maxScrollRight),
|
|
240
|
+
1,
|
|
241
|
+
);
|
|
242
|
+
|
|
243
|
+
setScale({ zoom: newZoom, offset: boundScrollOffset });
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return (
|
|
247
|
+
<>
|
|
248
|
+
{props.shotsHeader ? (
|
|
249
|
+
<h4 style="margin: 8px 0px">Total shots: {props.shotCount}</h4>
|
|
250
|
+
) : null}
|
|
251
|
+
<svg class="histogram" viewBox="0 0 165 100" onWheel={onWheel}>
|
|
252
|
+
<g transform={`translate(${scale.offset},4)`}>
|
|
253
|
+
{bucketArray.map((entry, idx) => {
|
|
254
|
+
const label = showKetLabels ? resultToKet(entry[0]) : entry[0];
|
|
255
|
+
|
|
256
|
+
const height = barAreaHeight * (entry[1] / sizeBiggestBucket);
|
|
257
|
+
const x = barBoxWidth * idx + barPaddingSize;
|
|
258
|
+
const labelX = barBoxWidth * idx + barBoxWidth / 2 - fontOffset;
|
|
259
|
+
const y = barAreaHeight + 15 - height;
|
|
260
|
+
const barLabel = `${label} at ${(
|
|
261
|
+
(entry[1] / totalAllBuckets) *
|
|
262
|
+
100
|
|
263
|
+
).toFixed(2)}%`;
|
|
264
|
+
let barClass = "bar";
|
|
265
|
+
|
|
266
|
+
if (entry[0] === props.filter) {
|
|
267
|
+
barClass += " bar-selected";
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return (
|
|
271
|
+
<>
|
|
272
|
+
<rect
|
|
273
|
+
class={barClass}
|
|
274
|
+
x={x}
|
|
275
|
+
y={y}
|
|
276
|
+
width={barFillWidth}
|
|
277
|
+
height={height}
|
|
278
|
+
onMouseOver={onMouseOverRect}
|
|
279
|
+
onMouseOut={onMouseOutRect}
|
|
280
|
+
onClick={onClickRect}
|
|
281
|
+
data-raw-label={entry[0]}
|
|
282
|
+
>
|
|
283
|
+
<title>{barLabel}</title>
|
|
284
|
+
</rect>
|
|
285
|
+
{
|
|
286
|
+
<text
|
|
287
|
+
class="bar-label"
|
|
288
|
+
x={labelX}
|
|
289
|
+
y="85"
|
|
290
|
+
visibility={showLabels ? "visible" : "hidden"}
|
|
291
|
+
transform={`rotate(90, ${labelX}, 85)`}
|
|
292
|
+
>
|
|
293
|
+
{label}
|
|
294
|
+
</text>
|
|
295
|
+
}
|
|
296
|
+
</>
|
|
297
|
+
);
|
|
298
|
+
})}
|
|
299
|
+
</g>
|
|
300
|
+
|
|
301
|
+
<text class="histo-label" x="2" y="97">
|
|
302
|
+
{histogramLabel}
|
|
303
|
+
</text>
|
|
304
|
+
<text class="hover-text" x="85" y="6">
|
|
305
|
+
{hoverLabel}
|
|
306
|
+
</text>
|
|
307
|
+
|
|
308
|
+
{/* The settings icon */}
|
|
309
|
+
<g
|
|
310
|
+
class="menu-icon"
|
|
311
|
+
transform="translate(2, 2) scale(0.3 0.3)"
|
|
312
|
+
onClick={toggleMenu}
|
|
313
|
+
>
|
|
314
|
+
<rect width="24" height="24" fill="white" stroke-widths="0.5"></rect>
|
|
315
|
+
<path
|
|
316
|
+
d="M3 5 H21 M3 12 H21 M3 19 H21"
|
|
317
|
+
stroke-width="1.75"
|
|
318
|
+
stroke-linecap="round"
|
|
319
|
+
/>
|
|
320
|
+
<rect x="6" y="3" width="4" height="4" rx="1" stroke-width="1.5" />
|
|
321
|
+
<rect x="15" y="10" width="4" height="4" rx="1" stroke-width="1.5" />
|
|
322
|
+
<rect x="9" y="17" width="4" height="4" rx="1" stroke-width="1.5" />
|
|
323
|
+
</g>
|
|
324
|
+
|
|
325
|
+
{/* The info icon */}
|
|
326
|
+
<g
|
|
327
|
+
class="menu-icon"
|
|
328
|
+
transform="translate(156, 2) scale(0.3 0.3)"
|
|
329
|
+
onClick={toggleInfo}
|
|
330
|
+
>
|
|
331
|
+
<rect width="24" height="24" stroke-width="0"></rect>
|
|
332
|
+
<circle cx="12" cy="13" r="10" stroke-width="1.5" />
|
|
333
|
+
<path
|
|
334
|
+
stroke-width="2.5"
|
|
335
|
+
stroke-linecap="round"
|
|
336
|
+
d="M12 8 V8 M12 12.5 V18"
|
|
337
|
+
/>
|
|
338
|
+
</g>
|
|
339
|
+
|
|
340
|
+
{/* The menu box */}
|
|
341
|
+
<g
|
|
342
|
+
id="menu"
|
|
343
|
+
ref={gMenu}
|
|
344
|
+
transform="translate(8, 2)"
|
|
345
|
+
style="display: none;"
|
|
346
|
+
>
|
|
347
|
+
<rect
|
|
348
|
+
x="0"
|
|
349
|
+
y="0"
|
|
350
|
+
rx="2"
|
|
351
|
+
width={menuBoxWidth}
|
|
352
|
+
height={menuBoxHeight}
|
|
353
|
+
class="menu-box"
|
|
354
|
+
></rect>
|
|
355
|
+
|
|
356
|
+
{
|
|
357
|
+
// Menu items
|
|
358
|
+
menuItems.map((item, col) => {
|
|
359
|
+
return item.options.map((option, row) => {
|
|
360
|
+
let classList = "menu-item";
|
|
361
|
+
if (menuSelection[item.category] === row)
|
|
362
|
+
classList += " menu-selected";
|
|
363
|
+
return (
|
|
364
|
+
<>
|
|
365
|
+
<rect
|
|
366
|
+
x={2 + col * menuItemWidth}
|
|
367
|
+
y={2 + row * menuItemHeight}
|
|
368
|
+
rx="1"
|
|
369
|
+
class={classList}
|
|
370
|
+
onClick={() => menuClicked(item.category, row)}
|
|
371
|
+
></rect>
|
|
372
|
+
<text
|
|
373
|
+
x={5 + col * menuItemWidth}
|
|
374
|
+
y={9 + row * menuItemHeight}
|
|
375
|
+
class="menu-text"
|
|
376
|
+
>
|
|
377
|
+
{option}
|
|
378
|
+
</text>
|
|
379
|
+
</>
|
|
380
|
+
);
|
|
381
|
+
});
|
|
382
|
+
})
|
|
383
|
+
}
|
|
384
|
+
{
|
|
385
|
+
// Column separators
|
|
386
|
+
menuItems.map((item, idx) => {
|
|
387
|
+
return idx >= menuItems.length - 1 ? null : (
|
|
388
|
+
<line
|
|
389
|
+
class="menu-separator"
|
|
390
|
+
x1={37 + idx * menuItemWidth}
|
|
391
|
+
y1="2"
|
|
392
|
+
x2={37 + idx * menuItemWidth}
|
|
393
|
+
y2={maxMenuOptions * menuItemHeight + 1}
|
|
394
|
+
></line>
|
|
395
|
+
);
|
|
396
|
+
})
|
|
397
|
+
}
|
|
398
|
+
</g>
|
|
399
|
+
|
|
400
|
+
{/* The info box */}
|
|
401
|
+
<g ref={gInfo} style="display: none;">
|
|
402
|
+
<rect
|
|
403
|
+
width="155"
|
|
404
|
+
height="76"
|
|
405
|
+
rx="5"
|
|
406
|
+
x="5"
|
|
407
|
+
y="6"
|
|
408
|
+
class="help-info"
|
|
409
|
+
onClick={toggleInfo}
|
|
410
|
+
/>
|
|
411
|
+
<text y="6" class="help-info-text">
|
|
412
|
+
<tspan x="10" dy="10">
|
|
413
|
+
This histogram shows the frequency of unique 'shot' results.
|
|
414
|
+
</tspan>
|
|
415
|
+
<tspan x="10" dy="10">
|
|
416
|
+
Click the top-left 'settings' icon for display options.
|
|
417
|
+
</tspan>
|
|
418
|
+
<tspan x="10" dy="10">
|
|
419
|
+
You can zoom the chart using the mouse scroll wheel.
|
|
420
|
+
</tspan>
|
|
421
|
+
<tspan x="10" dy="7">
|
|
422
|
+
(Or using a trackpad gesture).
|
|
423
|
+
</tspan>
|
|
424
|
+
<tspan x="10" dy="10">
|
|
425
|
+
When zoomed, to pan left & right, press 'Alt' while scrolling.
|
|
426
|
+
</tspan>
|
|
427
|
+
<tspan x="10" dy="10">
|
|
428
|
+
Click on a bar to filter the shot details to that result.
|
|
429
|
+
</tspan>
|
|
430
|
+
<tspan x="10" dy="12">
|
|
431
|
+
Click anywhere in this box to dismiss it.
|
|
432
|
+
</tspan>
|
|
433
|
+
</text>
|
|
434
|
+
</g>
|
|
435
|
+
</svg>
|
|
436
|
+
</>
|
|
437
|
+
);
|
|
438
|
+
}
|
package/ux/index.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// Copyright (c) Microsoft Corporation.
|
|
2
|
+
// Licensed under the MIT License.
|
|
3
|
+
|
|
4
|
+
// By importing the CSS here, esbuild will by default bundle it up and copy it
|
|
5
|
+
// to a CSS file adjacent to the JS bundle and with the same name.
|
|
6
|
+
import "./qsharp-ux.css";
|
|
7
|
+
|
|
8
|
+
export { Histogram } from "./histogram.js";
|
|
9
|
+
export { ReTable, type ReData } from "./reTable.js";
|
|
10
|
+
export { SpaceChart } from "./spaceChart.js";
|
|
11
|
+
export { ResultsTable, type CellValue } from "./resultsTable.js";
|