@promptctl/cc-candybar 1.6.0 → 1.7.0
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/index.mjs +34 -34
- package/package.json +5 -5
- package/src/config/default-dsl-config.ts +26 -0
- package/src/daemon/cache/session-usage-store.ts +50 -14
- package/src/daemon/render-payload.ts +59 -1
- package/src/template-engine/engine.ts +2 -1
- package/src/template-engine/funcs.ts +17 -0
- package/src/template-engine/sparkline.ts +79 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@promptctl/cc-candybar",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.7.0",
|
|
4
4
|
"description": "Statusline renderer for Claude Code — a JSON5-configurable DSL with daemon-cached data sources, byte-clean palette-aware composition, and OSC8 click verbs.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.mjs",
|
|
@@ -91,10 +91,10 @@
|
|
|
91
91
|
"mobx": "^6.15.0"
|
|
92
92
|
},
|
|
93
93
|
"optionalDependencies": {
|
|
94
|
-
"@promptctl/cc-candybar-darwin-arm64": "1.
|
|
95
|
-
"@promptctl/cc-candybar-darwin-x64": "1.
|
|
96
|
-
"@promptctl/cc-candybar-linux-x64": "1.
|
|
97
|
-
"@promptctl/cc-candybar-linux-arm64": "1.
|
|
94
|
+
"@promptctl/cc-candybar-darwin-arm64": "1.7.0",
|
|
95
|
+
"@promptctl/cc-candybar-darwin-x64": "1.7.0",
|
|
96
|
+
"@promptctl/cc-candybar-linux-x64": "1.7.0",
|
|
97
|
+
"@promptctl/cc-candybar-linux-arm64": "1.7.0"
|
|
98
98
|
},
|
|
99
99
|
"pnpm": {
|
|
100
100
|
"supportedArchitectures": {
|
|
@@ -425,6 +425,17 @@ export const DEFAULT_DSL_CONFIG = {
|
|
|
425
425
|
type: "number",
|
|
426
426
|
default: -1,
|
|
427
427
|
},
|
|
428
|
+
// Recent burn-rate trend: a comma-delimited series of total-lane tok/s the
|
|
429
|
+
// daemon folds from its sample ring (render-payload.ts). A series cannot
|
|
430
|
+
// cross the scalar var-system seam, so it travels as a string the
|
|
431
|
+
// `sparkline` helper decodes. Default "" is the genuine "no history yet"
|
|
432
|
+
// form (the helper renders nothing); the segment gates on it being present.
|
|
433
|
+
"speed.history": {
|
|
434
|
+
kind: "input",
|
|
435
|
+
path: "speed.history",
|
|
436
|
+
type: "string",
|
|
437
|
+
default: "",
|
|
438
|
+
},
|
|
428
439
|
|
|
429
440
|
// Context — daemon fetches via ContextProvider; contextLeftPercentage.
|
|
430
441
|
"context.totalTokens": {
|
|
@@ -656,6 +667,21 @@ export const DEFAULT_DSL_CONFIG = {
|
|
|
656
667
|
fg: "foreground",
|
|
657
668
|
when: "{{ gt .session.tokens 0 }}",
|
|
658
669
|
},
|
|
670
|
+
// Burn-rate sparkline: the recent total-lane tok/s trend as a unicode
|
|
671
|
+
// mini-graph. Declared-but-opt-in (NOT in the default root, like speed /
|
|
672
|
+
// block / weekly): a user adds `tokenSparkline` to their layout. The
|
|
673
|
+
// `sparkline` helper decodes the daemon-owned series and draws it; `24`
|
|
674
|
+
// caps the glyph count to the cell, showing the live tail of the ring. The
|
|
675
|
+
// segment's fg colors the whole graph (no per-glyph color). Gated on the
|
|
676
|
+
// history being present so the cell never renders empty (the series needs
|
|
677
|
+
// two samples before its first bar). [LAW:effects-at-boundaries] — all the
|
|
678
|
+
// history lives in the daemon ring, the template only draws.
|
|
679
|
+
tokenSparkline: {
|
|
680
|
+
template: " ⚡ {{ sparkline .speed.history 24 }} ",
|
|
681
|
+
bg: "panel",
|
|
682
|
+
fg: "foreground",
|
|
683
|
+
when: '{{ ne .speed.history "" }}',
|
|
684
|
+
},
|
|
659
685
|
// Prompt-cache warmth countdown. minutesUntilReset clamps a past expiry
|
|
660
686
|
// to 0, so an expired cache renders "cold" (and reads red via the ≤8
|
|
661
687
|
// arm) rather than a negative number. [LAW:dataflow-not-control-flow]
|
|
@@ -61,14 +61,24 @@ export interface SpeedSample {
|
|
|
61
61
|
readonly atMs: number;
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
-
// The prior observation (absent on the very first render of a session)
|
|
65
|
-
//
|
|
66
|
-
// this store only remembers and
|
|
64
|
+
// The prior observation (absent on the very first render of a session), the one
|
|
65
|
+
// just taken, and the recent ring (oldest→newest, INCLUDING `cur`). The pure
|
|
66
|
+
// projections live at the render-payload boundary; this store only remembers and
|
|
67
|
+
// reports. [LAW:one-source-of-truth] `prev === samples[samples.length - 2]` — the
|
|
68
|
+
// tok/s baseline and the burn-rate history fold from the SAME owned ring, not two
|
|
69
|
+
// parallel stores. tok/s reads the last pair; the sparkline reads every pair.
|
|
67
70
|
export interface SpeedObservation {
|
|
68
71
|
readonly prev?: SpeedSample;
|
|
69
72
|
readonly cur: SpeedSample;
|
|
73
|
+
readonly samples: readonly SpeedSample[];
|
|
70
74
|
}
|
|
71
75
|
|
|
76
|
+
// How many recent samples the burn-rate ring retains per session. A render-cadence
|
|
77
|
+
// trend, not an archive: enough to fill a wide sparkline cell, capped so an
|
|
78
|
+
// idle-but-alive session can't grow it without bound. The window the sparkline
|
|
79
|
+
// draws is a tail slice of this (the `width` arg), so this only sets the ceiling.
|
|
80
|
+
const SPEED_RING_CAPACITY = 64;
|
|
81
|
+
|
|
72
82
|
function speedSampleOf(
|
|
73
83
|
breakdown: TokenBreakdown | null,
|
|
74
84
|
atMs: number,
|
|
@@ -202,11 +212,13 @@ export class SessionUsageStore {
|
|
|
202
212
|
// first seed completes every later read awaits an already-settled promise —
|
|
203
213
|
// zero rescan. A rejected seed is dropped so the next read retries.
|
|
204
214
|
private readonly seeded = new Map<string, Promise<void>>();
|
|
205
|
-
// [LAW:one-source-of-truth] The
|
|
206
|
-
// a derivative of the SAME token totals
|
|
207
|
-
// baseline (
|
|
208
|
-
//
|
|
209
|
-
|
|
215
|
+
// [LAW:one-source-of-truth] The recent tok/s observations per session, a
|
|
216
|
+
// bounded ring (oldest→newest). tok/s is a derivative of the SAME token totals
|
|
217
|
+
// the records map already owns; the baseline (prior counts + time) is the ring's
|
|
218
|
+
// last element, not a parallel counter. The burn-rate sparkline folds over the
|
|
219
|
+
// whole ring; tok/s folds over its final pair. One call to observeSpeed appends;
|
|
220
|
+
// the pure delta math is render-payload's.
|
|
221
|
+
private readonly speedRings = new Map<string, SpeedSample[]>();
|
|
210
222
|
// [LAW:no-ambient-temporal-coupling] Explicit owner of observe/commit ordering
|
|
211
223
|
// for the speed sample. Concurrent renders observing the SAME transcript state
|
|
212
224
|
// (key = `${sessionId}:${mtime}`) share ONE observation and commit the baseline
|
|
@@ -356,9 +368,33 @@ export class SessionUsageStore {
|
|
|
356
368
|
const breakdown =
|
|
357
369
|
record.kind === "ok" ? record.value.sessionInfo.tokenBreakdown : null;
|
|
358
370
|
const cur = speedSampleOf(breakdown, nowMs);
|
|
359
|
-
const
|
|
360
|
-
|
|
361
|
-
|
|
371
|
+
const ring = this.speedRings.get(sessionId) ?? [];
|
|
372
|
+
// [LAW:no-ambient-temporal-coupling] Observation time (atMs, the render
|
|
373
|
+
// clock) owns ring order — NOT ingest-completion order. Two concurrent
|
|
374
|
+
// observes with different mtimes don't coalesce in speedFlight and each
|
|
375
|
+
// awaits ingest before this mutation, so a plain append would record
|
|
376
|
+
// samples in whichever-ingest-settled-first order and invert oldest→newest.
|
|
377
|
+
// prev is the latest sample strictly before this observation; the post-
|
|
378
|
+
// insert sort by atMs makes the ring's order independent of completion
|
|
379
|
+
// order. (get→insert→set is synchronous after the await, so each resumed
|
|
380
|
+
// continuation mutates atomically — no lost update.)
|
|
381
|
+
let prev: SpeedSample | undefined;
|
|
382
|
+
for (const s of ring) {
|
|
383
|
+
if (s.atMs < cur.atMs && (prev === undefined || s.atMs > prev.atMs)) {
|
|
384
|
+
prev = s;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
ring.push(cur);
|
|
388
|
+
ring.sort((a, b) => a.atMs - b.atMs);
|
|
389
|
+
// Drop oldest (smallest atMs ⇒ ring[0]) beyond the cap — a tail window,
|
|
390
|
+
// not an archive.
|
|
391
|
+
if (ring.length > SPEED_RING_CAPACITY) ring.shift();
|
|
392
|
+
this.speedRings.set(sessionId, ring);
|
|
393
|
+
return ok({
|
|
394
|
+
...(prev !== undefined && { prev }),
|
|
395
|
+
cur,
|
|
396
|
+
samples: [...ring],
|
|
397
|
+
});
|
|
362
398
|
});
|
|
363
399
|
}
|
|
364
400
|
|
|
@@ -494,7 +530,7 @@ export class SessionUsageStore {
|
|
|
494
530
|
for (const [sid, record] of this.entries) {
|
|
495
531
|
if (now - record.lastSeenAt > this.staleAgeMs) {
|
|
496
532
|
this.entries.delete(sid);
|
|
497
|
-
this.
|
|
533
|
+
this.speedRings.delete(sid);
|
|
498
534
|
dropped++;
|
|
499
535
|
}
|
|
500
536
|
}
|
|
@@ -510,7 +546,7 @@ export class SessionUsageStore {
|
|
|
510
546
|
const oldest = this.entries.keys().next().value;
|
|
511
547
|
if (oldest === undefined) break;
|
|
512
548
|
this.entries.delete(oldest);
|
|
513
|
-
this.
|
|
549
|
+
this.speedRings.delete(oldest);
|
|
514
550
|
dlog("info", `usageStore evict ${oldest}`);
|
|
515
551
|
}
|
|
516
552
|
}
|
|
@@ -522,6 +558,6 @@ export class SessionUsageStore {
|
|
|
522
558
|
}
|
|
523
559
|
this.entries.clear();
|
|
524
560
|
this.seeded.clear();
|
|
525
|
-
this.
|
|
561
|
+
this.speedRings.clear();
|
|
526
562
|
}
|
|
527
563
|
}
|
|
@@ -140,6 +140,14 @@ export interface SpeedPayload {
|
|
|
140
140
|
readonly input?: number;
|
|
141
141
|
readonly output?: number;
|
|
142
142
|
readonly total?: number;
|
|
143
|
+
// [LAW:one-type-per-behavior] The recent burn-rate trend: a delimited series
|
|
144
|
+
// of total-lane tok/s over the store's sample ring, for the `sparkline` helper.
|
|
145
|
+
// It rides the speed lane because it folds from the SAME observation the three
|
|
146
|
+
// instantaneous rates do, but it is INDEPENDENTLY optional — a session that
|
|
147
|
+
// burst then went idle has no current rate yet still has a history to draw. It
|
|
148
|
+
// travels as a string because a series cannot cross the scalar var-system seam;
|
|
149
|
+
// the helper decodes it. Absent (no measurable in-window pair) → missing → "".
|
|
150
|
+
readonly history?: string;
|
|
143
151
|
}
|
|
144
152
|
|
|
145
153
|
export interface BlockPayload {
|
|
@@ -330,6 +338,42 @@ function projectSpeed(obs: SpeedObservation): SpeedPayload | undefined {
|
|
|
330
338
|
};
|
|
331
339
|
}
|
|
332
340
|
|
|
341
|
+
// [LAW:effects-at-boundaries] Pure fold of the observation's sample ring into the
|
|
342
|
+
// burn-rate history string. Each adjacent pair becomes one total-lane tok/s; an
|
|
343
|
+
// un-projectable pair (no new tokens, or a gap outside the sample window) is a
|
|
344
|
+
// real ZERO-throughput interval, not absence — the series is a string of numbers,
|
|
345
|
+
// and 0 is the honest value for "burned nothing here" ([LAW:no-silent-failure] —
|
|
346
|
+
// the gap is reported, not dropped to misalign the graph). Fewer than two samples
|
|
347
|
+
// ⇒ no pair ⇒ undefined (the whole field drops to the "" default).
|
|
348
|
+
function projectSpeedHistory(obs: SpeedObservation): string | undefined {
|
|
349
|
+
const { samples } = obs;
|
|
350
|
+
const rates: number[] = [];
|
|
351
|
+
for (let i = 1; i < samples.length; i++) {
|
|
352
|
+
const prev = samples[i - 1]!;
|
|
353
|
+
const cur = samples[i]!;
|
|
354
|
+
// [LAW:no-silent-failure] Only an in-window interval is a measurable reading.
|
|
355
|
+
// An out-of-window pair — samples too close to time a rate reliably, or a
|
|
356
|
+
// stale gap spanning idle time — is UNMEASURABLE, not zero burn, so it is
|
|
357
|
+
// SKIPPED rather than fabricated as a 0 bar that would read as real activity.
|
|
358
|
+
// projectTokensPerSecond stays the single formula+window authority (same
|
|
359
|
+
// module constants); this classifier disambiguates its undefined: out-of-
|
|
360
|
+
// window ⇒ skip, in-window ⇒ undefined means a non-positive token delta, a
|
|
361
|
+
// genuine zero-burn reading that stays 0.
|
|
362
|
+
const deltaMs = cur.atMs - prev.atMs;
|
|
363
|
+
if (deltaMs < MIN_SPEED_SAMPLE_MS || deltaMs > MAX_SPEED_SAMPLE_MS)
|
|
364
|
+
continue;
|
|
365
|
+
const rate = projectTokensPerSecond(
|
|
366
|
+
prev.total,
|
|
367
|
+
prev.atMs,
|
|
368
|
+
cur.total,
|
|
369
|
+
cur.atMs,
|
|
370
|
+
);
|
|
371
|
+
rates.push(rate ?? 0);
|
|
372
|
+
}
|
|
373
|
+
if (rates.length === 0) return undefined;
|
|
374
|
+
return rates.join(",");
|
|
375
|
+
}
|
|
376
|
+
|
|
333
377
|
// ─── Builder ─────────────────────────────────────────────────────────────────
|
|
334
378
|
|
|
335
379
|
// ─── Config-driven provider gating ───────────────────────────────────────────
|
|
@@ -657,8 +701,22 @@ export async function buildRenderPayload(
|
|
|
657
701
|
// that data here at the edge. Absent observation (lane skipped/failed) or no
|
|
658
702
|
// projectable lane → no `speed` key → every lane reads its -1 default.
|
|
659
703
|
const speedObs = take(speed);
|
|
660
|
-
|
|
704
|
+
// [LAW:dataflow-not-control-flow] The instantaneous rates and the burn-rate
|
|
705
|
+
// history fold independently from one observation — either can be present
|
|
706
|
+
// without the other (a fresh burst has rates but a one-sample history; an
|
|
707
|
+
// idle-after-burst session has a history but no current rate). Merge whatever
|
|
708
|
+
// each yields; the whole `speed` key drops only when both are absent.
|
|
709
|
+
const speedRates =
|
|
661
710
|
speedObs !== undefined ? projectSpeed(speedObs) : undefined;
|
|
711
|
+
const speedHistory =
|
|
712
|
+
speedObs !== undefined ? projectSpeedHistory(speedObs) : undefined;
|
|
713
|
+
const speedPayload =
|
|
714
|
+
speedRates !== undefined || speedHistory !== undefined
|
|
715
|
+
? {
|
|
716
|
+
...speedRates,
|
|
717
|
+
...(speedHistory !== undefined && { history: speedHistory }),
|
|
718
|
+
}
|
|
719
|
+
: undefined;
|
|
662
720
|
|
|
663
721
|
// [LAW:one-source-of-truth] The theme variable surfaces the session's
|
|
664
722
|
// resolved theme so the toolbar/tray DSL templates can encode it into
|
|
@@ -20,7 +20,8 @@
|
|
|
20
20
|
// `dict` lets a helper take multiple named inputs through its one dot arg.
|
|
21
21
|
// • richTextFuncs: bold, italic, red, green, … (styling from rich-js).
|
|
22
22
|
// • paletteFuncs (when resolver provided): primary, accent, palette, paletteOver, auto.
|
|
23
|
-
// • ccCandybarFuncs: basename, dirname, int, string, bool, urlEncode
|
|
23
|
+
// • ccCandybarFuncs: basename, dirname, int, string, bool, urlEncode,
|
|
24
|
+
// themes, styles, sparkline.
|
|
24
25
|
// • formatterFuncs: minutesUntilReset (clock-reading numeric primitive),
|
|
25
26
|
// formatInteger, round, formatModelName, shortenModelName. (The cost/token/
|
|
26
27
|
// budget AND duration/time-remaining formatters moved to DSL helper templates
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
shortenModelName,
|
|
18
18
|
} from "../utils/formatters.js";
|
|
19
19
|
import { listResolvablePaletteNames, STRIP_STYLES } from "../themes/policy.js";
|
|
20
|
+
import { renderSparkline, parseSeries } from "./sparkline.js";
|
|
20
21
|
|
|
21
22
|
// [LAW:one-source-of-truth] The DSL `themes()` and `styles()` bindings
|
|
22
23
|
// project the SAME canonical sources the set-state validator consults
|
|
@@ -128,6 +129,22 @@ export function ccCandybarFuncs(): FuncMap {
|
|
|
128
129
|
fn: () => STYLES_LIST,
|
|
129
130
|
argTypes: [],
|
|
130
131
|
},
|
|
132
|
+
|
|
133
|
+
// [LAW:effects-at-boundaries] Pure trend renderer: a numeric series (the
|
|
134
|
+
// daemon-owned ring, projected through the payload as a delimited string)
|
|
135
|
+
// becomes a unicode mini-graph. The series crosses the scalar var-system
|
|
136
|
+
// seam as a string, so the FuncMap slot is "string"; `parseSeries` decodes
|
|
137
|
+
// it and `renderSparkline` draws it — neither accumulates state. The
|
|
138
|
+
// optional trailing "int" slot caps the glyph count to fit a cell (the
|
|
139
|
+
// evaluator validates only supplied args, so `{{ sparkline .series }}` and
|
|
140
|
+
// `{{ sparkline .series 24 }}` are both well-typed). Returns a bare string;
|
|
141
|
+
// the engine lifts it to RichText so the segment's fg/bg palette colors the
|
|
142
|
+
// whole graph — no per-glyph color math here.
|
|
143
|
+
sparkline: {
|
|
144
|
+
fn: (series: string, width?: number) =>
|
|
145
|
+
renderSparkline(parseSeries(series), width),
|
|
146
|
+
argTypes: ["string", "int"],
|
|
147
|
+
},
|
|
131
148
|
};
|
|
132
149
|
}
|
|
133
150
|
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// [LAW:effects-at-boundaries] A sparkline is a PURE projection: a numeric
|
|
2
|
+
// series in, a string of block glyphs out. It reads no clock, touches no
|
|
3
|
+
// store, accumulates nothing — any history it draws is owned by the daemon
|
|
4
|
+
// cache and handed in as data (cf. session-usage-store's burn-rate ring).
|
|
5
|
+
// [LAW:one-source-of-truth] The renderer operates on number[] (the real
|
|
6
|
+
// domain); the var-system can only carry a scalar across the payload→template
|
|
7
|
+
// seam, so `parseSeries` decodes the delimited string form at that one edge —
|
|
8
|
+
// the wire shape and the domain shape have a single, tested conversion.
|
|
9
|
+
|
|
10
|
+
// The eight-level block ramp, lowest→highest. Index into this is the only
|
|
11
|
+
// place a value's normalized height becomes a glyph.
|
|
12
|
+
export const SPARK_LEVELS = [
|
|
13
|
+
"▁", // ▁
|
|
14
|
+
"▂", // ▂
|
|
15
|
+
"▃", // ▃
|
|
16
|
+
"▄", // ▄
|
|
17
|
+
"▅", // ▅
|
|
18
|
+
"▆", // ▆
|
|
19
|
+
"▇", // ▇
|
|
20
|
+
"█", // █
|
|
21
|
+
] as const;
|
|
22
|
+
|
|
23
|
+
const LEVELS = SPARK_LEVELS.length;
|
|
24
|
+
|
|
25
|
+
// Render a numeric series as a unicode mini-graph.
|
|
26
|
+
//
|
|
27
|
+
// `width` caps the glyph count to fit a cell: the MOST RECENT `width` samples
|
|
28
|
+
// are shown (tail slice), so a fixed-width cell tracks the live tail of a
|
|
29
|
+
// growing series. Omitted → every sample renders; `width <= 0` → empty.
|
|
30
|
+
//
|
|
31
|
+
// Heights are RELATIVE to the rendered window's own min/max — a sparkline shows
|
|
32
|
+
// shape, never absolute magnitude, so the full ramp is always used when the
|
|
33
|
+
// window varies. [LAW:dataflow-not-control-flow] The mapping is one unconditional
|
|
34
|
+
// fold over the values; the only data-driven value is `range`, and a flat
|
|
35
|
+
// window (range === 0) falls to the lowest tier by the same formula's limit
|
|
36
|
+
// (height-above-min is 0 for every sample), not a special-cased branch.
|
|
37
|
+
export function renderSparkline(values: number[], width?: number): string {
|
|
38
|
+
const window =
|
|
39
|
+
width === undefined ? values : width <= 0 ? [] : values.slice(-width);
|
|
40
|
+
if (window.length === 0) return "";
|
|
41
|
+
|
|
42
|
+
let min = window[0]!;
|
|
43
|
+
let max = window[0]!;
|
|
44
|
+
for (const v of window) {
|
|
45
|
+
if (v < min) min = v;
|
|
46
|
+
if (v > max) max = v;
|
|
47
|
+
}
|
|
48
|
+
const range = max - min;
|
|
49
|
+
|
|
50
|
+
let out = "";
|
|
51
|
+
for (const v of window) {
|
|
52
|
+
// range === 0 ⇒ every sample sits at its own min ⇒ height 0 ⇒ lowest tier.
|
|
53
|
+
const idx =
|
|
54
|
+
range === 0 ? 0 : Math.round(((v - min) / range) * (LEVELS - 1));
|
|
55
|
+
out += SPARK_LEVELS[idx]!;
|
|
56
|
+
}
|
|
57
|
+
return out;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Decode the delimited string a series travels as across the scalar var-system
|
|
61
|
+
// seam into the number[] the renderer consumes. Empty / blank tokens are the
|
|
62
|
+
// genuine "no sample" form (an empty payload field is ""), so they drop; a
|
|
63
|
+
// non-empty, non-numeric token is malformed input and fails LOUDLY rather than
|
|
64
|
+
// being silently skipped into a wrong-shaped graph. [LAW:no-silent-failure]
|
|
65
|
+
export function parseSeries(s: string): number[] {
|
|
66
|
+
const out: number[] = [];
|
|
67
|
+
for (const tok of s.split(",")) {
|
|
68
|
+
const t = tok.trim();
|
|
69
|
+
if (t === "") continue;
|
|
70
|
+
const n = Number(t);
|
|
71
|
+
if (!Number.isFinite(n)) {
|
|
72
|
+
throw new TypeError(
|
|
73
|
+
`sparkline: non-numeric series element ${JSON.stringify(tok)}`,
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
out.push(n);
|
|
77
|
+
}
|
|
78
|
+
return out;
|
|
79
|
+
}
|