markgraphy 0.1.1
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/LICENSE.txt +21 -0
- package/README.md +154 -0
- package/dist/animated/GraphAgni.svelte +430 -0
- package/dist/animated/GraphAgni.svelte.d.ts +22 -0
- package/dist/animated/GraphAum.svelte +294 -0
- package/dist/animated/GraphAum.svelte.d.ts +20 -0
- package/dist/animated/GraphBoot.svelte +325 -0
- package/dist/animated/GraphBoot.svelte.d.ts +28 -0
- package/dist/animated/GraphCron.svelte +476 -0
- package/dist/animated/GraphCron.svelte.d.ts +42 -0
- package/dist/animated/GraphDamru.svelte +341 -0
- package/dist/animated/GraphDamru.svelte.d.ts +18 -0
- package/dist/animated/GraphDeps.svelte +245 -0
- package/dist/animated/GraphDeps.svelte.d.ts +29 -0
- package/dist/animated/GraphDiya.svelte +330 -0
- package/dist/animated/GraphDiya.svelte.d.ts +22 -0
- package/dist/animated/GraphFire.svelte +246 -0
- package/dist/animated/GraphFire.svelte.d.ts +22 -0
- package/dist/animated/GraphFlame.svelte +258 -0
- package/dist/animated/GraphFlame.svelte.d.ts +14 -0
- package/dist/animated/GraphFlowPlayer.svelte +346 -0
- package/dist/animated/GraphFlowPlayer.svelte.d.ts +27 -0
- package/dist/animated/GraphGanga.svelte +305 -0
- package/dist/animated/GraphGanga.svelte.d.ts +20 -0
- package/dist/animated/GraphHash.svelte +251 -0
- package/dist/animated/GraphHash.svelte.d.ts +20 -0
- package/dist/animated/GraphJapa.svelte +253 -0
- package/dist/animated/GraphJapa.svelte.d.ts +20 -0
- package/dist/animated/GraphLife.svelte +312 -0
- package/dist/animated/GraphLife.svelte.d.ts +27 -0
- package/dist/animated/GraphMandala.svelte +262 -0
- package/dist/animated/GraphMandala.svelte.d.ts +24 -0
- package/dist/animated/GraphMandel.svelte +290 -0
- package/dist/animated/GraphMandel.svelte.d.ts +27 -0
- package/dist/animated/GraphPulse.svelte +248 -0
- package/dist/animated/GraphPulse.svelte.d.ts +24 -0
- package/dist/animated/GraphRain.svelte +280 -0
- package/dist/animated/GraphRain.svelte.d.ts +22 -0
- package/dist/animated/GraphScatter.svelte +395 -0
- package/dist/animated/GraphScatter.svelte.d.ts +31 -0
- package/dist/animated/GraphScope.svelte +197 -0
- package/dist/animated/GraphScope.svelte.d.ts +22 -0
- package/dist/animated/GraphSequence.svelte +247 -0
- package/dist/animated/GraphSequence.svelte.d.ts +18 -0
- package/dist/animated/GraphSpinners.svelte +109 -0
- package/dist/animated/GraphSpinners.svelte.d.ts +22 -0
- package/dist/animated/GraphState.svelte +270 -0
- package/dist/animated/GraphState.svelte.d.ts +14 -0
- package/dist/animated/GraphStream.svelte +223 -0
- package/dist/animated/GraphStream.svelte.d.ts +24 -0
- package/dist/animated/GraphSurya.svelte +268 -0
- package/dist/animated/GraphSurya.svelte.d.ts +22 -0
- package/dist/animated/GraphTerminal.svelte +238 -0
- package/dist/animated/GraphTerminal.svelte.d.ts +28 -0
- package/dist/animated/GraphTicker.svelte +163 -0
- package/dist/animated/GraphTicker.svelte.d.ts +21 -0
- package/dist/animated/GraphTypewriter.svelte +130 -0
- package/dist/animated/GraphTypewriter.svelte.d.ts +22 -0
- package/dist/animated/GraphWorkflow.svelte +570 -0
- package/dist/animated/GraphWorkflow.svelte.d.ts +29 -0
- package/dist/assets/favicon.svg +21 -0
- package/dist/diagram/AmplifierDiagram.svelte +206 -0
- package/dist/diagram/AmplifierDiagram.svelte.d.ts +12 -0
- package/dist/diagram/AsciiDiagram.svelte +212 -0
- package/dist/diagram/AsciiDiagram.svelte.d.ts +24 -0
- package/dist/diagram/DiagramEditor.svelte +414 -0
- package/dist/diagram/DiagramEditor.svelte.d.ts +3 -0
- package/dist/diagram/MetricsTableDiagram.svelte +125 -0
- package/dist/diagram/MetricsTableDiagram.svelte.d.ts +18 -0
- package/dist/diagram/NestedRadiiDiagram.svelte +218 -0
- package/dist/diagram/NestedRadiiDiagram.svelte.d.ts +12 -0
- package/dist/diagram/PromptLoopDiagram.svelte +331 -0
- package/dist/diagram/PromptLoopDiagram.svelte.d.ts +12 -0
- package/dist/diagram/ascii.d.ts +35 -0
- package/dist/diagram/ascii.js +83 -0
- package/dist/diagram/parser.d.ts +16 -0
- package/dist/diagram/parser.js +71 -0
- package/dist/frame/Graph.svelte +141 -0
- package/dist/frame/Graph.svelte.d.ts +14 -0
- package/dist/frame/GraphArrow.svelte +49 -0
- package/dist/frame/GraphArrow.svelte.d.ts +9 -0
- package/dist/frame/GraphBody.svelte +39 -0
- package/dist/frame/GraphBody.svelte.d.ts +10 -0
- package/dist/frame/GraphRule.svelte +21 -0
- package/dist/frame/GraphRule.svelte.d.ts +6 -0
- package/dist/frame/GraphTick.svelte +23 -0
- package/dist/frame/GraphTick.svelte.d.ts +9 -0
- package/dist/frame/GraphTrack.svelte +29 -0
- package/dist/frame/GraphTrack.svelte.d.ts +14 -0
- package/dist/frame/clock.d.ts +10 -0
- package/dist/frame/clock.js +62 -0
- package/dist/frame/glyphs.d.ts +22 -0
- package/dist/frame/glyphs.js +53 -0
- package/dist/frame/motion.css +27 -0
- package/dist/frame/motion.d.ts +24 -0
- package/dist/frame/motion.js +47 -0
- package/dist/frame/tone.d.ts +11 -0
- package/dist/frame/tone.js +64 -0
- package/dist/graphs/GraphActivity.svelte +387 -0
- package/dist/graphs/GraphActivity.svelte.d.ts +22 -0
- package/dist/graphs/GraphBars.svelte +194 -0
- package/dist/graphs/GraphBars.svelte.d.ts +20 -0
- package/dist/graphs/GraphBullet.svelte +208 -0
- package/dist/graphs/GraphBullet.svelte.d.ts +22 -0
- package/dist/graphs/GraphCalendar.svelte +203 -0
- package/dist/graphs/GraphCalendar.svelte.d.ts +20 -0
- package/dist/graphs/GraphCells.svelte +181 -0
- package/dist/graphs/GraphCells.svelte.d.ts +18 -0
- package/dist/graphs/GraphCompare.svelte +192 -0
- package/dist/graphs/GraphCompare.svelte.d.ts +18 -0
- package/dist/graphs/GraphCountdown.svelte +96 -0
- package/dist/graphs/GraphCountdown.svelte.d.ts +14 -0
- package/dist/graphs/GraphDiff.svelte +181 -0
- package/dist/graphs/GraphDiff.svelte.d.ts +18 -0
- package/dist/graphs/GraphFlow.svelte +137 -0
- package/dist/graphs/GraphFlow.svelte.d.ts +21 -0
- package/dist/graphs/GraphFunnel.svelte +190 -0
- package/dist/graphs/GraphFunnel.svelte.d.ts +21 -0
- package/dist/graphs/GraphGantt.svelte +258 -0
- package/dist/graphs/GraphGantt.svelte.d.ts +25 -0
- package/dist/graphs/GraphHeatmap.svelte +287 -0
- package/dist/graphs/GraphHeatmap.svelte.d.ts +22 -0
- package/dist/graphs/GraphInvoice.svelte +294 -0
- package/dist/graphs/GraphInvoice.svelte.d.ts +34 -0
- package/dist/graphs/GraphKpi.svelte +166 -0
- package/dist/graphs/GraphKpi.svelte.d.ts +17 -0
- package/dist/graphs/GraphLatency.svelte +340 -0
- package/dist/graphs/GraphLatency.svelte.d.ts +28 -0
- package/dist/graphs/GraphMeter.svelte +121 -0
- package/dist/graphs/GraphMeter.svelte.d.ts +16 -0
- package/dist/graphs/GraphPlot.svelte +226 -0
- package/dist/graphs/GraphPlot.svelte.d.ts +18 -0
- package/dist/graphs/GraphQuota.svelte +303 -0
- package/dist/graphs/GraphQuota.svelte.d.ts +27 -0
- package/dist/graphs/GraphRank.svelte +141 -0
- package/dist/graphs/GraphRank.svelte.d.ts +21 -0
- package/dist/graphs/GraphSlo.svelte +325 -0
- package/dist/graphs/GraphSlo.svelte.d.ts +31 -0
- package/dist/graphs/GraphSlope.svelte +197 -0
- package/dist/graphs/GraphSlope.svelte.d.ts +19 -0
- package/dist/graphs/GraphSpark.svelte +125 -0
- package/dist/graphs/GraphSpark.svelte.d.ts +15 -0
- package/dist/graphs/GraphSpec.svelte +69 -0
- package/dist/graphs/GraphSpec.svelte.d.ts +14 -0
- package/dist/graphs/GraphStack.svelte +266 -0
- package/dist/graphs/GraphStack.svelte.d.ts +25 -0
- package/dist/graphs/GraphStat.svelte +103 -0
- package/dist/graphs/GraphStat.svelte.d.ts +16 -0
- package/dist/graphs/GraphTable.svelte +133 -0
- package/dist/graphs/GraphTable.svelte.d.ts +15 -0
- package/dist/graphs/GraphTimeline.svelte +164 -0
- package/dist/graphs/GraphTimeline.svelte.d.ts +18 -0
- package/dist/graphs/GraphTimer.svelte +117 -0
- package/dist/graphs/GraphTimer.svelte.d.ts +15 -0
- package/dist/graphs/GraphTree.svelte +147 -0
- package/dist/graphs/GraphTree.svelte.d.ts +16 -0
- package/dist/graphs/GraphUptime.svelte +210 -0
- package/dist/graphs/GraphUptime.svelte.d.ts +18 -0
- package/dist/graphs/GraphWaffle.svelte +131 -0
- package/dist/graphs/GraphWaffle.svelte.d.ts +17 -0
- package/dist/graphs/GraphWaterfall.svelte +258 -0
- package/dist/graphs/GraphWaterfall.svelte.d.ts +22 -0
- package/dist/index.d.ts +147 -0
- package/dist/index.js +87 -0
- package/dist/themes.css +54 -0
- package/package.json +67 -0
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
<script module lang="ts">
|
|
2
|
+
/** A single point in the scatter plot. */
|
|
3
|
+
export interface ScatterPoint {
|
|
4
|
+
x: number;
|
|
5
|
+
y: number;
|
|
6
|
+
/** Optional label rendered muted next to the dot. */
|
|
7
|
+
label?: string;
|
|
8
|
+
/** Accent (hot) — useful to highlight a chosen point or trend seed. */
|
|
9
|
+
accent?: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface GraphScatterProps {
|
|
13
|
+
/** Caption drawn on the top edge as `[ TITLE ]`. */
|
|
14
|
+
title: string;
|
|
15
|
+
/** Points to plot. x and y are real numbers; the plot scales them. */
|
|
16
|
+
data: ScatterPoint[];
|
|
17
|
+
/** Scene width in columns (excluding y-axis gutter). Default 60. */
|
|
18
|
+
cols?: number;
|
|
19
|
+
/** Scene height in rows (excluding x-axis row and optional trend row). Default 14. */
|
|
20
|
+
rows?: number;
|
|
21
|
+
/** Render a single-row linear trend strip below the plot. Default false. */
|
|
22
|
+
trend?: boolean;
|
|
23
|
+
/** Animate the reveal. Default true. Reduced motion shows the full plot. */
|
|
24
|
+
animated?: boolean;
|
|
25
|
+
/** Milliseconds between reveals of new points. Default 60. */
|
|
26
|
+
speedMs?: number;
|
|
27
|
+
/** Caption rendered muted under the plot. */
|
|
28
|
+
label?: string;
|
|
29
|
+
class?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface Seg {
|
|
33
|
+
text: string;
|
|
34
|
+
cls?: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Deterministic mulberry32 PRNG so a given seed always shows the same dots. */
|
|
38
|
+
function mulberry32(seed: number): () => number {
|
|
39
|
+
let a = seed >>> 0;
|
|
40
|
+
return () => {
|
|
41
|
+
a = (a + 0x6d2b79f5) | 0;
|
|
42
|
+
let t = Math.imul(a ^ (a >>> 15), 1 | a);
|
|
43
|
+
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
|
|
44
|
+
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Find the linear regression line (slope, intercept) for a set of points. */
|
|
49
|
+
function regression(points: { x: number; y: number }[]): { slope: number; intercept: number } {
|
|
50
|
+
const n = points.length;
|
|
51
|
+
if (n < 2) return { slope: 0, intercept: points[0]?.y ?? 0 };
|
|
52
|
+
|
|
53
|
+
let sx = 0;
|
|
54
|
+
let sy = 0;
|
|
55
|
+
let sxx = 0;
|
|
56
|
+
let sxy = 0;
|
|
57
|
+
for (const p of points) {
|
|
58
|
+
sx += p.x;
|
|
59
|
+
sy += p.y;
|
|
60
|
+
sxx += p.x * p.x;
|
|
61
|
+
sxy += p.x * p.y;
|
|
62
|
+
}
|
|
63
|
+
const denom = n * sxx - sx * sx;
|
|
64
|
+
if (denom === 0) return { slope: 0, intercept: sy / n };
|
|
65
|
+
const slope = (n * sxy - sx * sy) / denom;
|
|
66
|
+
const intercept = (sy - slope * sx) / n;
|
|
67
|
+
return { slope, intercept };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Format a number to up to 3 significant characters, trimming trailing zeros. */
|
|
71
|
+
function compactNumber(n: number): string {
|
|
72
|
+
if (!Number.isFinite(n)) return '0';
|
|
73
|
+
const abs = Math.abs(n);
|
|
74
|
+
if (abs >= 1000) return Math.round(n).toString();
|
|
75
|
+
if (abs >= 10) return n.toFixed(1).replace(/\.0$/, '');
|
|
76
|
+
if (abs >= 1) return n.toFixed(2).replace(/\.?0+$/, '');
|
|
77
|
+
return n.toFixed(3).replace(/\.?0+$/, '');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Build the full row-by-row, run-length-encoded view of the plot. */
|
|
81
|
+
function buildView(
|
|
82
|
+
data: ScatterPoint[],
|
|
83
|
+
plotCols: number,
|
|
84
|
+
plotRows: number,
|
|
85
|
+
trend: boolean,
|
|
86
|
+
rand: () => number
|
|
87
|
+
): Seg[][] {
|
|
88
|
+
const totalCols = plotCols;
|
|
89
|
+
const totalRows = plotRows + (trend ? 1 : 0);
|
|
90
|
+
|
|
91
|
+
// Empty grid.
|
|
92
|
+
const grid: string[][] = [];
|
|
93
|
+
for (let y = 0; y < totalRows; y++) {
|
|
94
|
+
grid.push(new Array<string>(totalCols).fill(' '));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Compute extent.
|
|
98
|
+
const xs = data.map((p) => p.x);
|
|
99
|
+
const ys = data.map((p) => p.y);
|
|
100
|
+
const xMin = Math.min(...xs);
|
|
101
|
+
const xMax = Math.max(...xs);
|
|
102
|
+
const yMin = Math.min(...ys);
|
|
103
|
+
const yMax = Math.max(...ys);
|
|
104
|
+
const xRange = xMax - xMin || 1;
|
|
105
|
+
const yRange = yMax - yMin || 1;
|
|
106
|
+
|
|
107
|
+
// Map each point to a plot cell. Two points landing on the same cell merge
|
|
108
|
+
// and the higher-tier glyph wins (accent > mid > faint).
|
|
109
|
+
const tier: number[][] = [];
|
|
110
|
+
for (let y = 0; y < plotRows; y++) tier.push(new Array<number>(plotCols).fill(-1));
|
|
111
|
+
|
|
112
|
+
for (const p of data) {
|
|
113
|
+
const col = Math.max(0, Math.min(plotCols - 1, Math.round(((p.x - xMin) / xRange) * (plotCols - 1))));
|
|
114
|
+
const row = Math.max(0, Math.min(plotRows - 1, plotRows - 1 - Math.round(((p.y - yMin) / yRange) * (plotRows - 1))));
|
|
115
|
+
const t = p.accent ? 2 : 1;
|
|
116
|
+
if (tier[row][col] < t) tier[row][col] = t;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
for (let row = 0; row < plotRows; row++) {
|
|
120
|
+
for (let col = 0; col < plotCols; col++) {
|
|
121
|
+
const t = tier[row][col];
|
|
122
|
+
if (t < 0) continue;
|
|
123
|
+
grid[row][col] = t === 2 ? 'X' : t === 1 ? 'o' : '.';
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Faint grid baseline (a thin `·` ribbon every 4 columns, mid on the bottom row).
|
|
128
|
+
for (let c = 0; c < plotCols; c += 4) {
|
|
129
|
+
if (grid[plotRows - 1][c] === ' ') grid[plotRows - 1][c] = '.';
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Trend row: linear regression rendered as a single row of bar glyphs.
|
|
133
|
+
if (trend) {
|
|
134
|
+
const { slope, intercept } = regression(data);
|
|
135
|
+
const trendRow = totalRows - 1;
|
|
136
|
+
const barAt = (xVal: number) => {
|
|
137
|
+
const y = slope * xVal + intercept;
|
|
138
|
+
const norm = (y - yMin) / yRange;
|
|
139
|
+
return norm;
|
|
140
|
+
};
|
|
141
|
+
for (let c = 0; c < plotCols; c++) {
|
|
142
|
+
const xVal = xMin + (c / (plotCols - 1)) * xRange;
|
|
143
|
+
const norm = barAt(xVal);
|
|
144
|
+
const h = Math.max(0, Math.min(1, norm));
|
|
145
|
+
grid[trendRow][c] = h >= 0.85 ? '#' : h >= 0.6 ? '*' : h >= 0.35 ? ':' : h > 0.1 ? '.' : ' ';
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Y-axis tick lines: a `|` at col -1 (we use a separator column in the
|
|
150
|
+
// caller) — but here we just leave the plot area clean.
|
|
151
|
+
void rand;
|
|
152
|
+
return grid.map((row) => runLength(row));
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** Run-length encode a row into classed segments. */
|
|
156
|
+
function runLength(row: string[]): Seg[] {
|
|
157
|
+
const segs: Seg[] = [];
|
|
158
|
+
let buf = '';
|
|
159
|
+
let cls: string | undefined;
|
|
160
|
+
|
|
161
|
+
const setSeg = (ch: string, next: string | undefined) => {
|
|
162
|
+
if (next === cls) {
|
|
163
|
+
buf += ch;
|
|
164
|
+
} else {
|
|
165
|
+
if (buf.length > 0) segs.push({ text: buf, cls });
|
|
166
|
+
cls = next;
|
|
167
|
+
buf = ch;
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
for (const ch of row) {
|
|
172
|
+
if (ch === 'X') setSeg(ch, 'hot');
|
|
173
|
+
else if (ch === 'o') setSeg(ch, 'mid');
|
|
174
|
+
else if (ch === '*' || ch === ':' || ch === '.') setSeg(ch, 'faint');
|
|
175
|
+
else if (ch === '#') setSeg(ch, 'trend');
|
|
176
|
+
else setSeg(ch, undefined);
|
|
177
|
+
}
|
|
178
|
+
if (buf.length > 0) segs.push({ text: buf, cls });
|
|
179
|
+
return segs;
|
|
180
|
+
}
|
|
181
|
+
</script>
|
|
182
|
+
|
|
183
|
+
<script lang="ts">
|
|
184
|
+
import Graph from '../frame/Graph.svelte';
|
|
185
|
+
import GraphBody from '../frame/GraphBody.svelte';
|
|
186
|
+
|
|
187
|
+
let {
|
|
188
|
+
title,
|
|
189
|
+
data,
|
|
190
|
+
cols = 60,
|
|
191
|
+
rows = 14,
|
|
192
|
+
trend = false,
|
|
193
|
+
animated = true,
|
|
194
|
+
speedMs = 60,
|
|
195
|
+
label = '',
|
|
196
|
+
class: className = ''
|
|
197
|
+
}: GraphScatterProps = $props();
|
|
198
|
+
|
|
199
|
+
// Layout constants. The y-axis gutter is 5 chars wide; the x-axis is one
|
|
200
|
+
// row at the bottom; the trend strip is one row below that.
|
|
201
|
+
const Y_GUTTER = 5;
|
|
202
|
+
const X_AXIS_ROWS = 1;
|
|
203
|
+
// svelte-ignore state_referenced_locally
|
|
204
|
+
const TREND_ROWS = trend ? 1 : 0;
|
|
205
|
+
const plotCols = $derived(Math.max(8, cols - Y_GUTTER));
|
|
206
|
+
const plotRows = $derived(Math.max(3, rows - X_AXIS_ROWS - TREND_ROWS));
|
|
207
|
+
|
|
208
|
+
// svelte-ignore state_referenced_locally
|
|
209
|
+
const rand = mulberry32(7);
|
|
210
|
+
|
|
211
|
+
// Compute extents for axis labels.
|
|
212
|
+
const extent = $derived.by(() => {
|
|
213
|
+
if (data.length === 0) {
|
|
214
|
+
return { xMin: 0, xMax: 1, yMin: 0, yMax: 1 };
|
|
215
|
+
}
|
|
216
|
+
const xs = data.map((p) => p.x);
|
|
217
|
+
const ys = data.map((p) => p.y);
|
|
218
|
+
return {
|
|
219
|
+
xMin: Math.min(...xs),
|
|
220
|
+
xMax: Math.max(...xs),
|
|
221
|
+
yMin: Math.min(...ys),
|
|
222
|
+
yMax: Math.max(...ys)
|
|
223
|
+
};
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
const moving = $derived(
|
|
227
|
+
animated &&
|
|
228
|
+
(typeof window === 'undefined' ||
|
|
229
|
+
!window.matchMedia('(prefers-reduced-motion: reduce)').matches)
|
|
230
|
+
);
|
|
231
|
+
|
|
232
|
+
let revealed = $state(0);
|
|
233
|
+
|
|
234
|
+
// svelte-ignore state_referenced_locally
|
|
235
|
+
const preRevealed = moving ? 0 : data.length;
|
|
236
|
+
|
|
237
|
+
$effect(() => {
|
|
238
|
+
revealed = preRevealed;
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
$effect(() => {
|
|
242
|
+
if (!moving || data.length < 2) return;
|
|
243
|
+
const timer = window.setInterval(() => {
|
|
244
|
+
revealed = revealed >= data.length ? 0 : revealed + 1;
|
|
245
|
+
}, Math.max(40, speedMs));
|
|
246
|
+
return () => window.clearInterval(timer);
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
// The full grid is built once; the reveal mask determines which points show.
|
|
250
|
+
const fullView = $derived.by((): Seg[][] => buildView(data, plotCols, plotRows, trend, rand));
|
|
251
|
+
|
|
252
|
+
// Apply reveal: only the first `revealed` data points are drawn.
|
|
253
|
+
const view = $derived.by((): Seg[][] => {
|
|
254
|
+
if (revealed >= data.length) return fullView;
|
|
255
|
+
// Build a mask of which plot cells are revealed.
|
|
256
|
+
const mask = Array.from({ length: plotRows }, () => new Array<boolean>(plotCols).fill(false));
|
|
257
|
+
for (let i = 0; i < revealed; i++) {
|
|
258
|
+
const p = data[i];
|
|
259
|
+
if (!p) continue;
|
|
260
|
+
const xRange = extent.xMax - extent.xMin || 1;
|
|
261
|
+
const yRange = extent.yMax - extent.yMin || 1;
|
|
262
|
+
const col = Math.max(0, Math.min(plotCols - 1, Math.round(((p.x - extent.xMin) / xRange) * (plotCols - 1))));
|
|
263
|
+
const row = Math.max(0, Math.min(plotRows - 1, plotRows - 1 - Math.round(((p.y - extent.yMin) / yRange) * (plotRows - 1))));
|
|
264
|
+
mask[row][col] = true;
|
|
265
|
+
}
|
|
266
|
+
return fullView.map((rowSegs, r) => {
|
|
267
|
+
// When trend is on, the last row is the trend strip — never reveal-masked.
|
|
268
|
+
if (trend && r === plotRows) return rowSegs;
|
|
269
|
+
// For plot rows, hide any point cell that isn't yet revealed.
|
|
270
|
+
return rowSegs.map((seg) => {
|
|
271
|
+
if (!seg.cls || seg.cls === 'faint' || seg.cls === 'trend') return seg;
|
|
272
|
+
return { text: seg.text.replace(/[Xo]/g, ' '), cls: undefined };
|
|
273
|
+
});
|
|
274
|
+
});
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
// Build the y-axis label column (top: yMax, bottom: yMin).
|
|
278
|
+
const yAxisLabel = $derived.by((): Seg[][] => {
|
|
279
|
+
const totalRows = plotRows + TREND_ROWS;
|
|
280
|
+
const grid: string[][] = [];
|
|
281
|
+
for (let r = 0; r < totalRows; r++) grid.push(new Array<string>(Y_GUTTER).fill(' '));
|
|
282
|
+
// Right-align labels.
|
|
283
|
+
const writeLabel = (row: number, text: string) => {
|
|
284
|
+
const offset = Math.max(0, Y_GUTTER - 1 - text.length);
|
|
285
|
+
for (let i = 0; i < text.length && offset + i < Y_GUTTER; i++) {
|
|
286
|
+
grid[row][offset + i] = text[i];
|
|
287
|
+
}
|
|
288
|
+
};
|
|
289
|
+
writeLabel(0, compactNumber(extent.yMax));
|
|
290
|
+
writeLabel(plotRows - 1, compactNumber(extent.yMin));
|
|
291
|
+
if (trend) writeLabel(plotRows, 'trend');
|
|
292
|
+
return grid.map((row) => runLengthY(row));
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
function runLengthY(row: string[]): Seg[] {
|
|
296
|
+
const segs: Seg[] = [];
|
|
297
|
+
let buf = '';
|
|
298
|
+
let cls: string | undefined;
|
|
299
|
+
for (const ch of row) {
|
|
300
|
+
const next: string | undefined = ch === ' ' ? undefined : 'ax';
|
|
301
|
+
if (next === cls) {
|
|
302
|
+
buf += ch;
|
|
303
|
+
} else {
|
|
304
|
+
if (buf.length > 0) segs.push({ text: buf, cls });
|
|
305
|
+
cls = next;
|
|
306
|
+
buf = ch;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
if (buf.length > 0) segs.push({ text: buf, cls });
|
|
310
|
+
return segs;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// X-axis label row: left = xMin, center, right = xMax.
|
|
314
|
+
const xAxisLabel = $derived.by((): Seg[] => {
|
|
315
|
+
const total = plotCols;
|
|
316
|
+
const text = `${compactNumber(extent.xMin)} ─────── ${compactNumber(extent.xMax)}`;
|
|
317
|
+
if (text.length <= total) {
|
|
318
|
+
const pad = total - text.length;
|
|
319
|
+
const left = Math.floor(pad / 2);
|
|
320
|
+
return [{ text: ' '.repeat(left) + text + ' '.repeat(pad - left), cls: 'ax' }];
|
|
321
|
+
}
|
|
322
|
+
return [{ text: text.slice(0, total), cls: 'ax' }];
|
|
323
|
+
});
|
|
324
|
+
</script>
|
|
325
|
+
|
|
326
|
+
<Graph {title} class={className}>
|
|
327
|
+
<GraphBody>
|
|
328
|
+
<div class="viewport">
|
|
329
|
+
<pre class="art" aria-hidden="true"><code>{#each view as row, r (r)}{#each yAxisLabel[r] as seg, j (j)}{#if seg.cls}<span class={seg.cls}>{seg.text}</span>{:else}{seg.text}{/if}{/each} │ {#each row as seg, j (j)}{#if seg.cls}<span class={seg.cls}>{seg.text}</span>{:else}{seg.text}{/if}{/each}{#if r < view.length - 1}{'\n'}{/if}{/each}
|
|
330
|
+
{#each xAxisLabel as seg, j (j)}{#if seg.cls}<span class={seg.cls}>{seg.text}</span>{:else}{seg.text}{/if}{/each}{#if trend}
|
|
331
|
+
{#each view[view.length - 1] ?? [] as seg, j (j)}{#each [''] as _}{''}{/each}{/each}{/if}</code></pre>
|
|
332
|
+
{#if label}
|
|
333
|
+
<p class="caption">{label}</p>
|
|
334
|
+
{/if}
|
|
335
|
+
<span class="sr-only">
|
|
336
|
+
Scatter plot with {data.length} points, x {compactNumber(extent.xMin)} to {compactNumber(extent.xMax)},
|
|
337
|
+
y {compactNumber(extent.yMin)} to {compactNumber(extent.yMax)}{trend ? ', trend strip below' : ''}.
|
|
338
|
+
</span>
|
|
339
|
+
</div>
|
|
340
|
+
</GraphBody>
|
|
341
|
+
</Graph>
|
|
342
|
+
|
|
343
|
+
<style>
|
|
344
|
+
.viewport {
|
|
345
|
+
overflow-x: auto;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
.art {
|
|
349
|
+
margin: 0;
|
|
350
|
+
font-size: 0.78rem;
|
|
351
|
+
line-height: 1.2;
|
|
352
|
+
color: var(--graph-foreground, oklch(0.93 0 0));
|
|
353
|
+
white-space: pre;
|
|
354
|
+
font-variant-numeric: tabular-nums;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
.hot {
|
|
358
|
+
color: var(--graph-accent, oklch(0.78 0.17 155));
|
|
359
|
+
font-weight: 600;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
.mid {
|
|
363
|
+
color: var(--graph-foreground, oklch(0.93 0 0));
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
.faint {
|
|
367
|
+
color: var(--graph-faint, oklch(0.3 0 0));
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
.trend {
|
|
371
|
+
color: var(--graph-accent-2, oklch(0.78 0.12 70));
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
.ax {
|
|
375
|
+
color: var(--graph-muted, oklch(0.62 0 0));
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
.caption {
|
|
379
|
+
margin: 0.4rem 0 0;
|
|
380
|
+
font-size: 0.78rem;
|
|
381
|
+
color: var(--graph-muted, oklch(0.62 0 0));
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
.sr-only {
|
|
385
|
+
position: absolute;
|
|
386
|
+
width: 1px;
|
|
387
|
+
height: 1px;
|
|
388
|
+
padding: 0;
|
|
389
|
+
margin: -1px;
|
|
390
|
+
overflow: hidden;
|
|
391
|
+
clip-path: inset(50%);
|
|
392
|
+
white-space: nowrap;
|
|
393
|
+
border-width: 0;
|
|
394
|
+
}
|
|
395
|
+
</style>
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/** A single point in the scatter plot. */
|
|
2
|
+
export interface ScatterPoint {
|
|
3
|
+
x: number;
|
|
4
|
+
y: number;
|
|
5
|
+
/** Optional label rendered muted next to the dot. */
|
|
6
|
+
label?: string;
|
|
7
|
+
/** Accent (hot) — useful to highlight a chosen point or trend seed. */
|
|
8
|
+
accent?: boolean;
|
|
9
|
+
}
|
|
10
|
+
export interface GraphScatterProps {
|
|
11
|
+
/** Caption drawn on the top edge as `[ TITLE ]`. */
|
|
12
|
+
title: string;
|
|
13
|
+
/** Points to plot. x and y are real numbers; the plot scales them. */
|
|
14
|
+
data: ScatterPoint[];
|
|
15
|
+
/** Scene width in columns (excluding y-axis gutter). Default 60. */
|
|
16
|
+
cols?: number;
|
|
17
|
+
/** Scene height in rows (excluding x-axis row and optional trend row). Default 14. */
|
|
18
|
+
rows?: number;
|
|
19
|
+
/** Render a single-row linear trend strip below the plot. Default false. */
|
|
20
|
+
trend?: boolean;
|
|
21
|
+
/** Animate the reveal. Default true. Reduced motion shows the full plot. */
|
|
22
|
+
animated?: boolean;
|
|
23
|
+
/** Milliseconds between reveals of new points. Default 60. */
|
|
24
|
+
speedMs?: number;
|
|
25
|
+
/** Caption rendered muted under the plot. */
|
|
26
|
+
label?: string;
|
|
27
|
+
class?: string;
|
|
28
|
+
}
|
|
29
|
+
declare const GraphScatter: import("svelte").Component<GraphScatterProps, {}, "">;
|
|
30
|
+
type GraphScatter = ReturnType<typeof GraphScatter>;
|
|
31
|
+
export default GraphScatter;
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
<script module lang="ts">
|
|
2
|
+
import type { ArtSeg } from '../diagram/ascii';
|
|
3
|
+
|
|
4
|
+
/** How a column is painted: a single block, or a fill down to the baseline. */
|
|
5
|
+
export type ScopeMode = 'line' | 'area';
|
|
6
|
+
|
|
7
|
+
export interface GraphScopeProps {
|
|
8
|
+
/** Caption on the frame's top edge. */
|
|
9
|
+
title: string;
|
|
10
|
+
/** Series to scroll through. The window loops over it. */
|
|
11
|
+
data: number[];
|
|
12
|
+
/** Visible columns. Default 44. */
|
|
13
|
+
window?: number;
|
|
14
|
+
/** Grid height in rows. Default 6. */
|
|
15
|
+
rows?: number;
|
|
16
|
+
/** Milliseconds between ticks. Default 160. */
|
|
17
|
+
speedMs?: number;
|
|
18
|
+
/** 'line' draws one block per column; 'area' fills down to the baseline. */
|
|
19
|
+
mode?: ScopeMode;
|
|
20
|
+
/** Animate the scroll. Default true. Reduced motion freezes the window. */
|
|
21
|
+
animated?: boolean;
|
|
22
|
+
class?: string;
|
|
23
|
+
}
|
|
24
|
+
</script>
|
|
25
|
+
|
|
26
|
+
<script lang="ts">
|
|
27
|
+
import Graph from '../frame/Graph.svelte';
|
|
28
|
+
import GraphBody from '../frame/GraphBody.svelte';
|
|
29
|
+
import { reveal } from '../frame/motion';
|
|
30
|
+
|
|
31
|
+
let {
|
|
32
|
+
title,
|
|
33
|
+
data,
|
|
34
|
+
window: visible = 44,
|
|
35
|
+
rows = 6,
|
|
36
|
+
speedMs = 160,
|
|
37
|
+
mode = 'line',
|
|
38
|
+
animated = true,
|
|
39
|
+
class: className = ''
|
|
40
|
+
}: GraphScopeProps = $props();
|
|
41
|
+
|
|
42
|
+
let offset = $state(0);
|
|
43
|
+
|
|
44
|
+
const moving = $derived(
|
|
45
|
+
animated &&
|
|
46
|
+
(typeof window === 'undefined' ||
|
|
47
|
+
!window.matchMedia('(prefers-reduced-motion: reduce)').matches)
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
const cols = $derived(Math.max(1, Math.floor(visible)));
|
|
51
|
+
const gridRows = $derived(Math.max(1, Math.floor(rows)));
|
|
52
|
+
const loop = $derived(Math.max(1, data.length));
|
|
53
|
+
|
|
54
|
+
$effect(() => {
|
|
55
|
+
if (!moving) {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const timer = window.setInterval(() => {
|
|
60
|
+
offset = (offset + 1) % loop;
|
|
61
|
+
}, speedMs);
|
|
62
|
+
|
|
63
|
+
return () => window.clearInterval(timer);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
/** The values currently inside the window, left to right. */
|
|
67
|
+
const values = $derived.by(() => {
|
|
68
|
+
if (data.length === 0) {
|
|
69
|
+
return [];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return Array.from({ length: cols }, (_, col) => data[(offset + col) % data.length]);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
/** Value to grid row, normalized within the current window. Top row = 0. */
|
|
76
|
+
const points = $derived.by(() => {
|
|
77
|
+
if (values.length === 0) {
|
|
78
|
+
return [];
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const min = Math.min(...values);
|
|
82
|
+
const max = Math.max(...values);
|
|
83
|
+
const span = max - min;
|
|
84
|
+
const bottom = gridRows - 1;
|
|
85
|
+
|
|
86
|
+
return values.map((value) => {
|
|
87
|
+
if (span <= 0) {
|
|
88
|
+
return Math.round(bottom / 2);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return Math.round((1 - (value - min) / span) * bottom);
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* rows × cols grid as segment rows. Only the newest (rightmost) column's
|
|
97
|
+
* top cell takes a class, so each row splits into at most two segments.
|
|
98
|
+
*/
|
|
99
|
+
const art = $derived.by((): ArtSeg[][] => {
|
|
100
|
+
const grid: ArtSeg[][] = [];
|
|
101
|
+
const last = cols - 1;
|
|
102
|
+
|
|
103
|
+
for (let row = 0; row < gridRows; row++) {
|
|
104
|
+
const chars: string[] = [];
|
|
105
|
+
let nowAt = -1;
|
|
106
|
+
|
|
107
|
+
for (let col = 0; col < cols; col++) {
|
|
108
|
+
const point = points[col];
|
|
109
|
+
|
|
110
|
+
if (point === undefined) {
|
|
111
|
+
chars.push(' ');
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (row === point) {
|
|
116
|
+
chars.push('█');
|
|
117
|
+
|
|
118
|
+
if (col === last) {
|
|
119
|
+
nowAt = col;
|
|
120
|
+
}
|
|
121
|
+
} else if (mode === 'area' && row > point) {
|
|
122
|
+
chars.push('▒');
|
|
123
|
+
} else {
|
|
124
|
+
chars.push(' ');
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const segs: ArtSeg[] = [];
|
|
129
|
+
|
|
130
|
+
if (nowAt > 0) {
|
|
131
|
+
segs.push({ text: chars.slice(0, nowAt).join('') });
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (nowAt >= 0) {
|
|
135
|
+
segs.push({ text: chars.slice(nowAt).join(''), cls: 'now' });
|
|
136
|
+
} else {
|
|
137
|
+
segs.push({ text: chars.join('') });
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
grid.push(segs);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return grid;
|
|
144
|
+
});
|
|
145
|
+
</script>
|
|
146
|
+
|
|
147
|
+
<Graph {title} class={className}>
|
|
148
|
+
<GraphBody>
|
|
149
|
+
<div class="scope" use:reveal={{ amount: 0.5 }}>
|
|
150
|
+
<div class="viewport" aria-hidden="true">
|
|
151
|
+
<pre class="art"><code>{#each art as line, i (i)}{#each line as seg, j (j)}{#if seg.cls}<span class={seg.cls}>{seg.text}</span>{:else}{seg.text}{/if}{/each}{#if i < art.length - 1}{'\n'}{/if}{/each}</code></pre>
|
|
152
|
+
</div>
|
|
153
|
+
<span class="sr-only">
|
|
154
|
+
Oscilloscope window of {cols} columns over {data.length} samples, {mode} mode
|
|
155
|
+
</span>
|
|
156
|
+
</div>
|
|
157
|
+
</GraphBody>
|
|
158
|
+
</Graph>
|
|
159
|
+
|
|
160
|
+
<style>
|
|
161
|
+
.scope {
|
|
162
|
+
display: flex;
|
|
163
|
+
flex-direction: column;
|
|
164
|
+
gap: 0.75rem;
|
|
165
|
+
min-width: 0;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
.viewport {
|
|
169
|
+
overflow-x: auto;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
.art {
|
|
173
|
+
margin: 0;
|
|
174
|
+
font-size: 0.85rem;
|
|
175
|
+
line-height: 1.4;
|
|
176
|
+
color: var(--graph-foreground, oklch(0.93 0 0));
|
|
177
|
+
white-space: pre;
|
|
178
|
+
user-select: none;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
.now {
|
|
182
|
+
color: var(--graph-accent, oklch(0.78 0.17 155));
|
|
183
|
+
font-weight: 600;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
.sr-only {
|
|
187
|
+
position: absolute;
|
|
188
|
+
width: 1px;
|
|
189
|
+
height: 1px;
|
|
190
|
+
padding: 0;
|
|
191
|
+
margin: -1px;
|
|
192
|
+
overflow: hidden;
|
|
193
|
+
clip-path: inset(50%);
|
|
194
|
+
white-space: nowrap;
|
|
195
|
+
border-width: 0;
|
|
196
|
+
}
|
|
197
|
+
</style>
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/** How a column is painted: a single block, or a fill down to the baseline. */
|
|
2
|
+
export type ScopeMode = 'line' | 'area';
|
|
3
|
+
export interface GraphScopeProps {
|
|
4
|
+
/** Caption on the frame's top edge. */
|
|
5
|
+
title: string;
|
|
6
|
+
/** Series to scroll through. The window loops over it. */
|
|
7
|
+
data: number[];
|
|
8
|
+
/** Visible columns. Default 44. */
|
|
9
|
+
window?: number;
|
|
10
|
+
/** Grid height in rows. Default 6. */
|
|
11
|
+
rows?: number;
|
|
12
|
+
/** Milliseconds between ticks. Default 160. */
|
|
13
|
+
speedMs?: number;
|
|
14
|
+
/** 'line' draws one block per column; 'area' fills down to the baseline. */
|
|
15
|
+
mode?: ScopeMode;
|
|
16
|
+
/** Animate the scroll. Default true. Reduced motion freezes the window. */
|
|
17
|
+
animated?: boolean;
|
|
18
|
+
class?: string;
|
|
19
|
+
}
|
|
20
|
+
declare const GraphScope: import("svelte").Component<GraphScopeProps, {}, "">;
|
|
21
|
+
type GraphScope = ReturnType<typeof GraphScope>;
|
|
22
|
+
export default GraphScope;
|