basalt-ui 2.3.0 → 2.5.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/CHANGELOG.md +34 -0
- package/MIGRATING.md +79 -12
- package/agent/rules/basalt-state.md +1 -1
- package/configs/lefthook.yml +4 -1
- package/dist/agent/ai-sdk-transport.d.ts.map +1 -1
- package/dist/agent/ai-sdk-transport.js +4 -1
- package/dist/agent/index.d.ts +1 -1
- package/dist/agent/index.d.ts.map +1 -1
- package/dist/agent/run-records.d.ts +12 -3
- package/dist/agent/run-records.d.ts.map +1 -1
- package/dist/agent/run-records.js +16 -3
- package/dist/agent/transport.d.ts +10 -3
- package/dist/agent/transport.d.ts.map +1 -1
- package/dist/agent/use-agent-thread-runs.d.ts.map +1 -1
- package/dist/agent/use-agent-thread-runs.js +23 -7
- package/dist/charts/CartesianChart.d.ts.map +1 -1
- package/dist/charts/CartesianChart.js +6 -2
- package/dist/charts/DualPanel.d.ts +5 -1
- package/dist/charts/DualPanel.d.ts.map +1 -1
- package/dist/charts/DualPanel.js +46 -19
- package/dist/charts/Heatmap.js +2 -2
- package/dist/charts/Lines.js +2 -2
- package/dist/charts/MirroredBars.d.ts.map +1 -1
- package/dist/charts/MirroredBars.js +14 -11
- package/dist/charts/compile.d.ts +19 -2
- package/dist/charts/compile.d.ts.map +1 -1
- package/dist/charts/compile.js +192 -42
- package/dist/charts/marks.d.ts +8 -0
- package/dist/charts/marks.d.ts.map +1 -1
- package/dist/charts/marks.js +22 -1
- package/dist/charts/panel.d.ts +6 -0
- package/dist/charts/panel.d.ts.map +1 -1
- package/dist/charts/panel.js +12 -3
- package/dist/charts/types.d.ts +39 -1
- package/dist/charts/types.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/provider/index.d.ts.map +1 -1
- package/dist/provider/index.js +8 -2
- package/dist/state/fields.d.ts +14 -3
- package/dist/state/fields.d.ts.map +1 -1
- package/dist/state/fields.js +41 -9
- package/dist/telemetry.d.ts +58 -4
- package/dist/telemetry.d.ts.map +1 -1
- package/dist/telemetry.js +13 -5
- package/llms.txt +2 -2
- package/package.json +1 -1
- package/src/agent/ai-sdk-transport.ts +5 -2
- package/src/agent/index.ts +1 -1
- package/src/agent/run-records.ts +27 -3
- package/src/agent/transport.ts +11 -5
- package/src/agent/use-agent-thread-runs.ts +35 -8
- package/src/charts/CartesianChart.tsx +7 -2
- package/src/charts/DualPanel.tsx +50 -19
- package/src/charts/Heatmap.tsx +2 -2
- package/src/charts/Lines.tsx +2 -2
- package/src/charts/MirroredBars.tsx +13 -10
- package/src/charts/compile.ts +261 -46
- package/src/charts/marks.ts +35 -1
- package/src/charts/panel.tsx +18 -3
- package/src/charts/types.ts +37 -1
- package/src/index.ts +1 -1
- package/src/provider/index.tsx +8 -2
- package/src/state/fields.ts +79 -21
- package/src/telemetry.ts +70 -7
package/dist/telemetry.js
CHANGED
|
@@ -1,14 +1,22 @@
|
|
|
1
1
|
import { createContext } from "react";
|
|
2
2
|
const now = () => performance.timeOrigin + performance.now();
|
|
3
3
|
const errorName = (error) => error instanceof Error ? error.name : "Error";
|
|
4
|
-
function
|
|
5
|
-
|
|
6
|
-
return (event) => {
|
|
4
|
+
function createLiveEmitter(read) {
|
|
5
|
+
const emit = (event) => {
|
|
7
6
|
try {
|
|
8
|
-
|
|
7
|
+
read()?.(event);
|
|
9
8
|
} catch {
|
|
10
9
|
}
|
|
11
10
|
};
|
|
11
|
+
return Object.assign(emit, {
|
|
12
|
+
startChatTurn(turn) {
|
|
13
|
+
try {
|
|
14
|
+
return read()?.startChatTurn?.(turn);
|
|
15
|
+
} catch {
|
|
16
|
+
return void 0;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
});
|
|
12
20
|
}
|
|
13
21
|
function noop() {
|
|
14
22
|
}
|
|
@@ -26,7 +34,7 @@ function routerTelemetry(router) {
|
|
|
26
34
|
export {
|
|
27
35
|
TelemetryContext,
|
|
28
36
|
bindRouterTelemetry,
|
|
29
|
-
|
|
37
|
+
createLiveEmitter,
|
|
30
38
|
errorName,
|
|
31
39
|
now,
|
|
32
40
|
routerTelemetry
|
package/llms.txt
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# basalt-ui v2.
|
|
1
|
+
# basalt-ui v2.5.0
|
|
2
2
|
# Opinionated framework for Mantine-based React apps — theme, app shell, TanStack Charts, toolchain presets
|
|
3
3
|
#
|
|
4
4
|
# Entry points: use the exact import specifier below; do not guess.
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
# A subpath's `Install:` line is its required peers; `OptionalPeers:` are import()-only, install on use.
|
|
14
14
|
|
|
15
15
|
Package: basalt-ui
|
|
16
|
-
Version: 2.
|
|
16
|
+
Version: 2.5.0
|
|
17
17
|
|
|
18
18
|
## basalt-ui
|
|
19
19
|
Import: import { ... } from 'basalt-ui'
|
package/package.json
CHANGED
|
@@ -49,7 +49,7 @@ import { assertNever } from '../register'
|
|
|
49
49
|
import { mintMessageId, mintThreadId } from './id'
|
|
50
50
|
import { TERMINAL_TOOL_STATES } from './parts'
|
|
51
51
|
import type { AgentPart, AgentPartDraft } from './parts'
|
|
52
|
-
import type { ResumableAgentTransport } from './transport'
|
|
52
|
+
import type { AgentStreamContext, ResumableAgentTransport } from './transport'
|
|
53
53
|
|
|
54
54
|
// ── ai (optional peer) — TYPE-ONLY imports, erased at runtime ────────────────
|
|
55
55
|
// verbatimModuleSyntax-safe: these never trigger a runtime resolution of 'ai'. The actual runtime
|
|
@@ -415,7 +415,7 @@ export function aiSdkTransport<TPart = AgentPart>(
|
|
|
415
415
|
async *stream(
|
|
416
416
|
input: string,
|
|
417
417
|
signal?: AbortSignal,
|
|
418
|
-
ctx?:
|
|
418
|
+
ctx?: AgentStreamContext,
|
|
419
419
|
): AsyncGenerator<TPart> {
|
|
420
420
|
// Synthesized locally, first — we already know the chat id client-side, so there is no
|
|
421
421
|
// need to wait on the server to hand us a run id (unlike a river-style protocol).
|
|
@@ -439,6 +439,9 @@ export function aiSdkTransport<TPart = AgentPart>(
|
|
|
439
439
|
trigger: 'submit-message',
|
|
440
440
|
messageId: undefined,
|
|
441
441
|
abortSignal: signal,
|
|
442
|
+
// Per-turn trace context goes to the chat endpoint only (merged over the static headers by
|
|
443
|
+
// AI SDK's HTTP transport); absent, the request is exactly what it was.
|
|
444
|
+
...(ctx?.headers !== undefined ? { headers: ctx.headers } : {}),
|
|
442
445
|
})
|
|
443
446
|
yield* diffChunkStream<TPart>(chatId, chunkStream, readStream, signal)
|
|
444
447
|
},
|
package/src/agent/index.ts
CHANGED
|
@@ -24,7 +24,7 @@ export { toAgentError } from './error'
|
|
|
24
24
|
export type { ChatMessage } from './history'
|
|
25
25
|
export type { AgentThread, ThreadStatus, ThreadsStore } from './thread'
|
|
26
26
|
export type { AgentOutcome, OutcomeResolver } from './outcome'
|
|
27
|
-
export type { AgentTransport, ResumableAgentTransport } from './transport'
|
|
27
|
+
export type { AgentStreamContext, AgentTransport, ResumableAgentTransport } from './transport'
|
|
28
28
|
|
|
29
29
|
// ── Chat UI ───────────────────────────────────────────────────────────────────
|
|
30
30
|
export { ThreadFeed } from './chat/thread-feed'
|
package/src/agent/run-records.ts
CHANGED
|
@@ -12,10 +12,12 @@
|
|
|
12
12
|
* covers: it outlives the run only until a render shows the outcome (`sweepFlushed`).
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
+
import type { ChatTurnEnd, ChatTurnTrace } from '../telemetry'
|
|
16
|
+
|
|
15
17
|
/** The part of a rendered `runs` entry this module reads. */
|
|
16
18
|
type RenderedRun<TPart> = { readonly status: string; readonly parts: TPart[] }
|
|
17
19
|
|
|
18
|
-
export type RunEnd =
|
|
20
|
+
export type RunEnd = ChatTurnEnd
|
|
19
21
|
|
|
20
22
|
export type RunRecord<TPart> = {
|
|
21
23
|
readonly controller: AbortController
|
|
@@ -28,15 +30,22 @@ export type RunRecord<TPart> = {
|
|
|
28
30
|
ended: RunEnd | undefined
|
|
29
31
|
/** A newer run began on the same thread (set by `beginRun` on the record it replaces). */
|
|
30
32
|
superseded: boolean
|
|
33
|
+
/** The turn's telemetry `settle`, run once by `settleTrace`. */
|
|
34
|
+
settle: ChatTurnTrace['settle']
|
|
31
35
|
}
|
|
32
36
|
|
|
33
37
|
export type RunRecords<TPart> = Map<string, RunRecord<TPart>>
|
|
34
38
|
|
|
35
|
-
/**
|
|
39
|
+
/**
|
|
40
|
+
* Registers a live run for `threadId`, replacing whatever record the previous run left. A previous
|
|
41
|
+
* run still being driven is cut short: its trace settles as `stopped`. `settle` is the new turn's
|
|
42
|
+
* telemetry hook (absent for a resume).
|
|
43
|
+
*/
|
|
36
44
|
export function beginRun<TPart>(
|
|
37
45
|
records: RunRecords<TPart>,
|
|
38
46
|
threadId: string,
|
|
39
47
|
controller: AbortController,
|
|
48
|
+
settle?: ChatTurnTrace['settle'],
|
|
40
49
|
): RunRecord<TPart> {
|
|
41
50
|
const record: RunRecord<TPart> = {
|
|
42
51
|
controller,
|
|
@@ -45,13 +54,28 @@ export function beginRun<TPart>(
|
|
|
45
54
|
resolving: false,
|
|
46
55
|
ended: undefined,
|
|
47
56
|
superseded: false,
|
|
57
|
+
settle,
|
|
48
58
|
}
|
|
49
59
|
const previous = records.get(threadId)
|
|
50
|
-
if (previous !== undefined)
|
|
60
|
+
if (previous !== undefined) {
|
|
61
|
+
previous.superseded = true
|
|
62
|
+
settleTrace(previous, 'stopped')
|
|
63
|
+
}
|
|
51
64
|
records.set(threadId, record)
|
|
52
65
|
return record
|
|
53
66
|
}
|
|
54
67
|
|
|
68
|
+
/** Runs the turn's telemetry `settle` once; a throwing consumer hook never breaks the run. */
|
|
69
|
+
export function settleTrace<TPart>(record: RunRecord<TPart>, end: RunEnd): void {
|
|
70
|
+
const { settle } = record
|
|
71
|
+
record.settle = undefined
|
|
72
|
+
try {
|
|
73
|
+
settle?.(end)
|
|
74
|
+
} catch {
|
|
75
|
+
// a throwing sink must never break the app (§ 7 delivery rules)
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
55
79
|
/** Marks a run as no longer driven and drops the content it held. */
|
|
56
80
|
export function endRun<TPart>(record: RunRecord<TPart>, reason: RunEnd): void {
|
|
57
81
|
record.ended = reason
|
package/src/agent/transport.ts
CHANGED
|
@@ -16,6 +16,16 @@ import type { AgentPartDraft } from './parts'
|
|
|
16
16
|
|
|
17
17
|
// ── AgentTransport ────────────────────────────────────────────────────────────
|
|
18
18
|
|
|
19
|
+
/**
|
|
20
|
+
* The third `stream` argument. `headers` are extra request headers for THIS turn's request to the
|
|
21
|
+
* transport's own endpoint — today the W3C trace context a telemetry sink's `startChatTurn`
|
|
22
|
+
* returned. Absent unless a sink contributed; a transport that ignores them keeps working.
|
|
23
|
+
*/
|
|
24
|
+
export type AgentStreamContext = {
|
|
25
|
+
readonly messageId: string
|
|
26
|
+
readonly headers?: Readonly<Record<string, string>> | undefined
|
|
27
|
+
}
|
|
28
|
+
|
|
19
29
|
/**
|
|
20
30
|
* The injected transport seam. `stream(input, signal, ctx)` returns an async generator of parts.
|
|
21
31
|
* Implement this interface for any backend — fetch-based SSE, AI SDK, or a mock.
|
|
@@ -43,11 +53,7 @@ import type { AgentPartDraft } from './parts'
|
|
|
43
53
|
* }
|
|
44
54
|
*/
|
|
45
55
|
export type AgentTransport<TPart = AgentPartDraft, TInput = string> = {
|
|
46
|
-
stream: (
|
|
47
|
-
input: TInput,
|
|
48
|
-
signal?: AbortSignal,
|
|
49
|
-
ctx?: { readonly messageId: string },
|
|
50
|
-
) => AsyncGenerator<TPart>
|
|
56
|
+
stream: (input: TInput, signal?: AbortSignal, ctx?: AgentStreamContext) => AsyncGenerator<TPart>
|
|
51
57
|
/** Optional: resume a previously-started run from its resumeToken (see StartPart). Transports that don't support resumption simply omit this. */
|
|
52
58
|
resume?: ((resumeToken: string, signal?: AbortSignal) => AsyncGenerator<TPart>) | undefined
|
|
53
59
|
}
|
|
@@ -50,6 +50,7 @@ import { createRunCommitter, isContentPart } from './run-committer'
|
|
|
50
50
|
import {
|
|
51
51
|
beginRun,
|
|
52
52
|
endRun,
|
|
53
|
+
settleTrace,
|
|
53
54
|
failedThreadIds,
|
|
54
55
|
dismissRun,
|
|
55
56
|
hasLiveRun,
|
|
@@ -62,8 +63,8 @@ import type { RunRecord, RunRecords } from './run-records'
|
|
|
62
63
|
import type { AgentThread, LiveRunStatus, ThreadStatus, ThreadsStore } from './thread'
|
|
63
64
|
import { isResumable } from './transport'
|
|
64
65
|
import type { AgentTransport } from './transport'
|
|
65
|
-
import { TelemetryContext,
|
|
66
|
-
import type { BasaltEvent, Telemetry } from '../telemetry'
|
|
66
|
+
import { TelemetryContext, createLiveEmitter, errorName, now } from '../telemetry'
|
|
67
|
+
import type { BasaltEvent, Telemetry, TraceHeaders } from '../telemetry'
|
|
67
68
|
|
|
68
69
|
// ── ThreadRunState ────────────────────────────────────────────────────────────
|
|
69
70
|
|
|
@@ -388,6 +389,7 @@ async function consumeAndFinalize<TPart extends PartLike>(args: {
|
|
|
388
389
|
status: outcome.status,
|
|
389
390
|
}),
|
|
390
391
|
)
|
|
392
|
+
settleTrace(record, 'settled')
|
|
391
393
|
} catch (err) {
|
|
392
394
|
// Guard: the run ended (stop(), unmount — which abort the controller, so their AbortError lands
|
|
393
395
|
// here) or a newer stream took the thread over. Only an abort from somewhere else gets past.
|
|
@@ -421,6 +423,7 @@ async function consumeAndFinalize<TPart extends PartLike>(args: {
|
|
|
421
423
|
failure: { error: err, agentError },
|
|
422
424
|
}),
|
|
423
425
|
)
|
|
426
|
+
settleTrace(record, 'failed')
|
|
424
427
|
onError?.({ threadId, error: err })
|
|
425
428
|
} finally {
|
|
426
429
|
committer.cancel()
|
|
@@ -555,6 +558,24 @@ async function finalizeStop<TPart extends PartLike>(args: {
|
|
|
555
558
|
}
|
|
556
559
|
}
|
|
557
560
|
|
|
561
|
+
const TRACEPARENT = /^[\da-f]{2}-[\da-f]{32}-[\da-f]{16}-[\da-f]{2}$/
|
|
562
|
+
const TRACESTATE_MEMBER = /^[\da-z][\w*/@-]{0,255}=[^\s,=]([^,=]{0,254}[^\s,=])?$/
|
|
563
|
+
|
|
564
|
+
/**
|
|
565
|
+
* The wire form of a turn's trace context: a well-formed `traceparent` (else none — the request
|
|
566
|
+
* goes out untraced, the sink's `settle` still runs) and a `tracestate` only if valid.
|
|
567
|
+
*/
|
|
568
|
+
function traceRequestHeaders(headers: TraceHeaders): Record<string, string> | undefined {
|
|
569
|
+
const { traceparent, tracestate } = headers
|
|
570
|
+
if (typeof traceparent !== 'string' || !TRACEPARENT.test(traceparent)) return undefined
|
|
571
|
+
const members = typeof tracestate === 'string' ? tracestate.split(',') : []
|
|
572
|
+
const valid =
|
|
573
|
+
members.length > 0 &&
|
|
574
|
+
members.length <= 32 &&
|
|
575
|
+
members.every((member) => TRACESTATE_MEMBER.test(member.trim()))
|
|
576
|
+
return valid ? { traceparent, tracestate: tracestate as string } : { traceparent }
|
|
577
|
+
}
|
|
578
|
+
|
|
558
579
|
/** The `chat.turn` event for a turn that completed (`failure` absent) or failed. */
|
|
559
580
|
function buildTurnEvent(args: {
|
|
560
581
|
threadId: string
|
|
@@ -654,10 +675,7 @@ export function useAgentThreadRuns<TPart extends PartLike = AgentPart>({
|
|
|
654
675
|
toUserPartsRef.current = resolvedToUserParts
|
|
655
676
|
|
|
656
677
|
// Stable over the sink ref, so an inline `telemetry` function never changes what `start` closes over.
|
|
657
|
-
const emitTelemetry = useMemo<Telemetry>(
|
|
658
|
-
() => createEmitter((event) => telemetryRef.current?.(event)),
|
|
659
|
-
[],
|
|
660
|
-
)
|
|
678
|
+
const emitTelemetry = useMemo<Telemetry>(() => createLiveEmitter(() => telemetryRef.current), [])
|
|
661
679
|
|
|
662
680
|
// Resolves the transport for `threadId`: the shared object as-is for the plain-object form
|
|
663
681
|
// (unchanged behavior), or the factory's result — resolved once per thread id and cached in
|
|
@@ -707,6 +725,7 @@ export function useAgentThreadRuns<TPart extends PartLike = AgentPart>({
|
|
|
707
725
|
if (record.ended !== undefined) continue
|
|
708
726
|
abortedThreadIds.push(threadId)
|
|
709
727
|
record.controller.abort()
|
|
728
|
+
settleTrace(record, 'stopped')
|
|
710
729
|
}
|
|
711
730
|
recordsRef.current.clear()
|
|
712
731
|
if (abortedThreadIds.length === 0) return
|
|
@@ -841,8 +860,12 @@ export function useAgentThreadRuns<TPart extends PartLike = AgentPart>({
|
|
|
841
860
|
})
|
|
842
861
|
|
|
843
862
|
const controller = new AbortController()
|
|
844
|
-
|
|
863
|
+
// Opened before the request so its context rides on it; closed by `settleTrace` (a resume is
|
|
864
|
+
// the same turn's reconnect, not a new one, and carries none).
|
|
865
|
+
const trace = emitTelemetry.startChatTurn?.({ threadId, messageId: userMessage.id })
|
|
866
|
+
const record = beginRun(recordsRef.current, threadId, controller, trace?.settle)
|
|
845
867
|
const resolvedTransport = resolveTransport(threadId)
|
|
868
|
+
const headers = trace?.headers === undefined ? undefined : traceRequestHeaders(trace.headers)
|
|
846
869
|
|
|
847
870
|
// Non-awaited: this consumer runs in the background so start() returns immediately and
|
|
848
871
|
// multiple threads can stream at once.
|
|
@@ -855,7 +878,10 @@ export function useAgentThreadRuns<TPart extends PartLike = AgentPart>({
|
|
|
855
878
|
// id instead of a second, unrelated one (R1 — see AgentTransport.stream's doc).
|
|
856
879
|
generator: withPartIds(
|
|
857
880
|
threadId,
|
|
858
|
-
resolvedTransport.stream(input, controller.signal, {
|
|
881
|
+
resolvedTransport.stream(input, controller.signal, {
|
|
882
|
+
messageId: userMessage.id,
|
|
883
|
+
...(headers !== undefined ? { headers } : {}),
|
|
884
|
+
}),
|
|
859
885
|
),
|
|
860
886
|
userMessage,
|
|
861
887
|
recordsRef,
|
|
@@ -913,6 +939,7 @@ export function useAgentThreadRuns<TPart extends PartLike = AgentPart>({
|
|
|
913
939
|
const { appended, resolving } = record
|
|
914
940
|
endRun(record, 'stopped')
|
|
915
941
|
record.controller.abort()
|
|
942
|
+
settleTrace(record, 'stopped')
|
|
916
943
|
void finalizeStop({
|
|
917
944
|
threadId,
|
|
918
945
|
parts,
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
import { useCallback, useId, useMemo, useState } from 'react'
|
|
10
10
|
import type { CSSProperties, ReactElement, ReactNode } from 'react'
|
|
11
11
|
import { useInChartCard } from './card-context'
|
|
12
|
-
import { compileCartesian } from './compile'
|
|
12
|
+
import { compileCartesian, refLegendSeries } from './compile'
|
|
13
13
|
import type { CartesianDefinition } from './compile'
|
|
14
14
|
import { ChartEngine, idPrefixOf } from './host'
|
|
15
15
|
import { ChartLegend, legendEntries } from './legend'
|
|
@@ -170,9 +170,14 @@ export function CartesianRung<T, X extends ChartX>(
|
|
|
170
170
|
(ctx: ShellContext) => compileCartesian({ ...stable, ...ctx }),
|
|
171
171
|
[stable],
|
|
172
172
|
)
|
|
173
|
+
// Reference lines that name themselves join the legend after the series (role `reference`).
|
|
174
|
+
const legendSeries = useMemo(
|
|
175
|
+
() => [...stable.series, ...refLegendSeries<T>(stable.refLines)],
|
|
176
|
+
[stable.series, stable.refLines],
|
|
177
|
+
)
|
|
173
178
|
return (
|
|
174
179
|
<ChartShell
|
|
175
|
-
series={
|
|
180
|
+
series={legendSeries}
|
|
176
181
|
compile={compile}
|
|
177
182
|
ariaLabel={ariaLabel}
|
|
178
183
|
{...(legend !== undefined && { legend })}
|
package/src/charts/DualPanel.tsx
CHANGED
|
@@ -14,9 +14,9 @@ import { useCallback, useMemo } from 'react'
|
|
|
14
14
|
import type { ReactElement } from 'react'
|
|
15
15
|
import { VX, alpha } from '../tokens'
|
|
16
16
|
import { resolveContainerClass } from '../tokens/size-classes'
|
|
17
|
-
import { defaultFormatX, defaultNumberFormat, visibleSeries } from './compile'
|
|
17
|
+
import { defaultFormatX, defaultNumberFormat, engineRow, seriesRow, visibleSeries } from './compile'
|
|
18
18
|
import { barMark, emphasis, lineMark, markersOf, thinFill } from './marks'
|
|
19
|
-
import { PanelRung, paneX, paneY, rowsOf } from './panel'
|
|
19
|
+
import { PanelRung, chartHeightOf, paneX, paneY, rowsOf } from './panel'
|
|
20
20
|
import type { PaneBuilder, PanelInput, PanelPlan } from './panel'
|
|
21
21
|
import { xScaleFor } from './scales'
|
|
22
22
|
import { rowX } from './types'
|
|
@@ -61,8 +61,10 @@ export type DualPanelBar<T> = {
|
|
|
61
61
|
positive: string
|
|
62
62
|
/** The fill below zero. */
|
|
63
63
|
negative: string
|
|
64
|
-
/** Tick and tooltip
|
|
64
|
+
/** Tick formatter, and the tooltip's too unless `tooltipFormat` is given. */
|
|
65
65
|
format: (v: number) => string
|
|
66
|
+
/** The tooltip row's formatter. Default: `format`. */
|
|
67
|
+
tooltipFormat?: ((v: number) => string) | undefined
|
|
66
68
|
/** The tooltip row's value, for a unit or a decimal `format` cannot carry. */
|
|
67
69
|
formatValue?: ((v: number, d: T) => string) | undefined
|
|
68
70
|
/** Default `'auto'`: a symmetric `[-maxAbs, maxAbs]` from the data, rounded to nice ticks. A tuple
|
|
@@ -95,6 +97,8 @@ export type DualPanelProps<T, X extends Date | number = Date | number> = Pick<
|
|
|
95
97
|
| {
|
|
96
98
|
domain?: readonly [number, number] | 'auto' | undefined
|
|
97
99
|
format?: ((v: number) => string) | undefined
|
|
100
|
+
/** The lines' tooltip formatter. Default: `format`. */
|
|
101
|
+
tooltipFormat?: ((v: number) => string) | undefined
|
|
98
102
|
}
|
|
99
103
|
| undefined
|
|
100
104
|
/**
|
|
@@ -157,6 +161,8 @@ export function DualPanel<T, X extends Date | number = Date | number>(
|
|
|
157
161
|
)
|
|
158
162
|
const all = useMemo(() => [...lines, barSeries], [lines, barSeries])
|
|
159
163
|
const topFormat = top?.format ?? defaultNumberFormat
|
|
164
|
+
const topTooltipFormat = top?.tooltipFormat ?? topFormat
|
|
165
|
+
const barTooltipFormat = bar.tooltipFormat ?? bar.format
|
|
160
166
|
|
|
161
167
|
const plan = useCallback(
|
|
162
168
|
({ hidden, highlighted }: PanelInput): PanelPlan<T> => {
|
|
@@ -263,13 +269,21 @@ export function DualPanel<T, X extends Date | number = Date | number>(
|
|
|
263
269
|
],
|
|
264
270
|
scales: {
|
|
265
271
|
x: { scale: xScale(), axis: false },
|
|
266
|
-
y: paneY({
|
|
272
|
+
y: paneY({
|
|
273
|
+
scale: topY,
|
|
274
|
+
domain: topDomain,
|
|
275
|
+
format: topFormat,
|
|
276
|
+
ticks: TOP_TICKS,
|
|
277
|
+
cls,
|
|
278
|
+
height: chartHeightOf(h, topFraction, PANE_GAP),
|
|
279
|
+
}),
|
|
267
280
|
},
|
|
268
281
|
}
|
|
269
282
|
}
|
|
270
283
|
|
|
271
|
-
const barPane: PaneBuilder = ({ width }) => {
|
|
284
|
+
const barPane: PaneBuilder = ({ width, height: h }) => {
|
|
272
285
|
const cls = resolveContainerClass(width)
|
|
286
|
+
const whole = chartHeightOf(h, 1 - topFraction, PANE_GAP)
|
|
273
287
|
const barRows = barOn ? rowsOf(data, x, barSeries) : []
|
|
274
288
|
const dim = emphasis(highlighted, { key: BAR_KEY })
|
|
275
289
|
return {
|
|
@@ -284,13 +298,14 @@ export function DualPanel<T, X extends Date | number = Date | number>(
|
|
|
284
298
|
}),
|
|
285
299
|
],
|
|
286
300
|
scales: {
|
|
287
|
-
x: paneX({ scale: xScale(), model, format, cls }),
|
|
301
|
+
x: paneX({ scale: xScale(), model, format, cls, height: whole }),
|
|
288
302
|
y: paneY({
|
|
289
303
|
scale: barScale,
|
|
290
304
|
domain: barDomain,
|
|
291
305
|
format: bar.format,
|
|
292
306
|
ticks: BAR_TICKS,
|
|
293
307
|
cls,
|
|
308
|
+
height: whole,
|
|
294
309
|
}),
|
|
295
310
|
},
|
|
296
311
|
}
|
|
@@ -307,26 +322,42 @@ export function DualPanel<T, X extends Date | number = Date | number>(
|
|
|
307
322
|
for (const s of visible) {
|
|
308
323
|
const v = s.getValue(d)
|
|
309
324
|
if (s.tooltip === false || v === null || !Number.isFinite(v)) continue
|
|
310
|
-
out.push(
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
325
|
+
out.push(
|
|
326
|
+
seriesRow(
|
|
327
|
+
s,
|
|
328
|
+
s.formatValue ? s.formatValue(v, d) : topTooltipFormat(v),
|
|
329
|
+
highlighted === s.key,
|
|
330
|
+
),
|
|
331
|
+
)
|
|
316
332
|
}
|
|
317
333
|
const v = barOn ? bar.getValue(d) : null
|
|
318
334
|
if (v !== null && Number.isFinite(v))
|
|
319
|
-
out.push(
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
335
|
+
out.push(
|
|
336
|
+
engineRow({
|
|
337
|
+
label: bar.label,
|
|
338
|
+
value: bar.formatValue ? bar.formatValue(v, d) : barTooltipFormat(v),
|
|
339
|
+
color: v >= 0 ? bar.positive : bar.negative,
|
|
340
|
+
active: highlighted === BAR_KEY,
|
|
341
|
+
}),
|
|
342
|
+
)
|
|
325
343
|
return out
|
|
326
344
|
},
|
|
327
345
|
}
|
|
328
346
|
},
|
|
329
|
-
[
|
|
347
|
+
[
|
|
348
|
+
data,
|
|
349
|
+
x,
|
|
350
|
+
lines,
|
|
351
|
+
barSeries,
|
|
352
|
+
top,
|
|
353
|
+
fillBetween,
|
|
354
|
+
bar,
|
|
355
|
+
topFraction,
|
|
356
|
+
topFormat,
|
|
357
|
+
topTooltipFormat,
|
|
358
|
+
barTooltipFormat,
|
|
359
|
+
formatX,
|
|
360
|
+
],
|
|
330
361
|
)
|
|
331
362
|
|
|
332
363
|
return <PanelRung {...rung} series={all} plan={plan} />
|
package/src/charts/Heatmap.tsx
CHANGED
|
@@ -18,7 +18,7 @@ import type { CSSProperties, ReactElement } from 'react'
|
|
|
18
18
|
import type { BasaltProps } from '../common/props'
|
|
19
19
|
import { alpha, VX } from '../tokens'
|
|
20
20
|
import { resolveContainerClass } from '../tokens/size-classes'
|
|
21
|
-
import { strideTicks } from './compile'
|
|
21
|
+
import { engineRow, strideTicks } from './compile'
|
|
22
22
|
import type { CartesianDefinition } from './compile'
|
|
23
23
|
import { ChartEngine, idPrefixOf } from './host'
|
|
24
24
|
import { useCoarsePointer } from '../common/use-media-query'
|
|
@@ -244,7 +244,7 @@ export function Heatmap<T>(props: HeatmapProps<T>): ReactElement {
|
|
|
244
244
|
...(point.color && { color: point.color }),
|
|
245
245
|
},
|
|
246
246
|
]
|
|
247
|
-
return { title: header(c), rows: [...value, ...extra] }
|
|
247
|
+
return { title: header(c), rows: [...value, ...extra].map(engineRow) }
|
|
248
248
|
},
|
|
249
249
|
}
|
|
250
250
|
: false,
|
package/src/charts/Lines.tsx
CHANGED
|
@@ -17,7 +17,7 @@ import { VX } from '../tokens'
|
|
|
17
17
|
import { CartesianRung } from './CartesianChart'
|
|
18
18
|
import { areaGradientId } from './compile'
|
|
19
19
|
import { chartCurve } from './curves'
|
|
20
|
-
import { lineMark, markersOf, yScaleOf } from './marks'
|
|
20
|
+
import { drawnRows, lineMark, markersOf, yScaleOf } from './marks'
|
|
21
21
|
import { rowX } from './types'
|
|
22
22
|
import type {
|
|
23
23
|
CartesianChartProps,
|
|
@@ -120,7 +120,7 @@ export function Lines<T, X extends ChartX = ChartX>(props: LinesProps<T, X>): Re
|
|
|
120
120
|
if (!s.area) continue
|
|
121
121
|
out.push(
|
|
122
122
|
decorative(
|
|
123
|
-
areaY(plot
|
|
123
|
+
areaY(drawnRows(plot, s), {
|
|
124
124
|
id: `${s.key}-area`,
|
|
125
125
|
x: rowX,
|
|
126
126
|
y2: 'y',
|
|
@@ -21,9 +21,9 @@ import { useCallback, useMemo } from 'react'
|
|
|
21
21
|
import type { ReactElement } from 'react'
|
|
22
22
|
import { VX, alpha } from '../tokens'
|
|
23
23
|
import { resolveContainerClass } from '../tokens/size-classes'
|
|
24
|
-
import { defaultFormatX, defaultNumberFormat, visibleSeries } from './compile'
|
|
24
|
+
import { defaultFormatX, defaultNumberFormat, seriesRow, visibleSeries } from './compile'
|
|
25
25
|
import { barMark, emphasis } from './marks'
|
|
26
|
-
import { PanelRung, paneX, paneY, rowsOf } from './panel'
|
|
26
|
+
import { PanelRung, chartHeightOf, paneX, paneY, rowsOf } from './panel'
|
|
27
27
|
import type { PaneBuilder, PanelInput, PanelPlan } from './panel'
|
|
28
28
|
import { xScaleFor } from './scales'
|
|
29
29
|
import { rowX } from './types'
|
|
@@ -174,8 +174,9 @@ export function MirroredBars<T, X extends ChartX = ChartX>(
|
|
|
174
174
|
on: boolean,
|
|
175
175
|
max: number,
|
|
176
176
|
): PaneBuilder =>
|
|
177
|
-
({ width: w }) => {
|
|
177
|
+
({ width: w, height: h }) => {
|
|
178
178
|
const cls = resolveContainerClass(w)
|
|
179
|
+
const whole = chartHeightOf(h, side === 'up' ? upFraction : 1 - upFraction, 0)
|
|
179
180
|
// The down pane's zero is at its top.
|
|
180
181
|
const domain: [number, number] = side === 'up' ? [0, max] : [max, 0]
|
|
181
182
|
const rows: PlotRow<T, X>[] = on ? rowsOf(drawn, x, s) : []
|
|
@@ -249,7 +250,7 @@ export function MirroredBars<T, X extends ChartX = ChartX>(
|
|
|
249
250
|
scales: {
|
|
250
251
|
x:
|
|
251
252
|
side === 'down'
|
|
252
|
-
? paneX({ scale: bandScale(), model, format, cls })
|
|
253
|
+
? paneX({ scale: bandScale(), model, format, cls, height: whole })
|
|
253
254
|
: { scale: bandScale(), axis: false },
|
|
254
255
|
y: paneY({
|
|
255
256
|
scale: scaleLinear().domain(domain),
|
|
@@ -257,6 +258,7 @@ export function MirroredBars<T, X extends ChartX = ChartX>(
|
|
|
257
258
|
format: cfg.format,
|
|
258
259
|
ticks: cfg.ticks ?? DEFAULT_PANE_TICKS,
|
|
259
260
|
cls,
|
|
261
|
+
height: whole,
|
|
260
262
|
}),
|
|
261
263
|
zx: { channel: 'x', scale: scaleLinear().domain([0, 1]), axis: false },
|
|
262
264
|
},
|
|
@@ -277,12 +279,13 @@ export function MirroredBars<T, X extends ChartX = ChartX>(
|
|
|
277
279
|
// Each pane's row falls back to ITS OWN formatter: the panes are in different units.
|
|
278
280
|
const fallback =
|
|
279
281
|
s.key === up.key ? up.format : s.key === down.key ? down.format : defaultNumberFormat
|
|
280
|
-
out.push(
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
282
|
+
out.push(
|
|
283
|
+
seriesRow(
|
|
284
|
+
s,
|
|
285
|
+
s.formatValue ? s.formatValue(v, d) : fallback(v),
|
|
286
|
+
highlighted === s.key,
|
|
287
|
+
),
|
|
288
|
+
)
|
|
286
289
|
}
|
|
287
290
|
return out
|
|
288
291
|
},
|