@pond-ts/charts 0.47.0 → 0.48.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 CHANGED
@@ -8,7 +8,8 @@ The `@pond-ts` packages — `pond-ts`, `@pond-ts/react`, `@pond-ts/charts`,
8
8
  tag, so this file covers them all. Pre-1.0: minor bumps may include new features
9
9
  and type-level changes; patch bumps are strictly additive.
10
10
 
11
- [Unreleased]: https://github.com/pond-ts/pond/compare/v0.47.0...HEAD
11
+ [Unreleased]: https://github.com/pond-ts/pond/compare/v0.48.0...HEAD
12
+ [0.48.0]: https://github.com/pond-ts/pond/compare/v0.47.0...v0.48.0
12
13
  [0.47.0]: https://github.com/pond-ts/pond/compare/v0.46.0...v0.47.0
13
14
  [0.46.0]: https://github.com/pjm17971/pond-ts/compare/v0.45.0...v0.46.0
14
15
  [0.45.0]: https://github.com/pjm17971/pond-ts/compare/v0.44.1...v0.45.0
@@ -46,6 +47,34 @@ and type-level changes; patch bumps are strictly additive.
46
47
 
47
48
  ## [Unreleased]
48
49
 
50
+ ## [0.48.0] — 2026-07-17
51
+
52
+ ### Added
53
+
54
+ - **charts:** `<ChartContainer>` gains **`cursorFormat`** — an independent
55
+ format for the **cursor / marker readout** (the crosshair time pill, marker
56
+ axis indicators, annotation auto-labels), separate from the tick-label
57
+ `timeFormat` / `format`. Unlike `timeFormat` (which _owns the labels_ and so
58
+ opts the axis out of the `dateStyle` ladder by design), `cursorFormat` shapes
59
+ only the readout and **keeps the flat / stacked date style** — resolving the
60
+ "one knob, two concerns" bind where the only way to fix the pill was to give
61
+ up the styled axis. A d3 specifier **string** formats uniformly; a
62
+ **function** `(epochMs, { grain, defaultText }) => string` is handed the
63
+ axis's resolved coarse **`TimeGrain`** (`year` … `second`) and the
64
+ grain-aware default text, so it can branch on zoom and pass the default
65
+ through — no re-deriving the grain from the range. New public types
66
+ **`CursorFormat`** / **`TimeGrain`**; `TradingTimeScale` gains
67
+ `readoutFormat(count)` and `grain(count)`.
68
+
69
+ ### Fixed
70
+
71
+ - **charts:** the cursor / marker time readout now formats at the axis's
72
+ **own grain** by default instead of d3's multi-scale default — a
73
+ day-or-coarser axis reads a **date** (`Sep 14, 2026`), a sub-day axis reads
74
+ date + clock. Fixes the 0.47.0 flat-axis regression where a daily bar at a
75
+ foreign-timezone midnight rendered as a bare time-of-day (`02 AM`) in the
76
+ crosshair pill (Tidal F-charts-7). Tick labels are unchanged.
77
+
49
78
  ## [0.47.0] — 2026-07-17
50
79
 
51
80
  ### Added
@@ -3,7 +3,7 @@ import { type DiscontinuityProvider, type TradingCalendarLike } from './tradingT
3
3
  import { Sequence, BoundedSequence } from 'pond-ts';
4
4
  import type { TimeRange } from 'pond-ts';
5
5
  import { type AnnotationKind, type CreateSpec, type CursorMode, type SelectInfo, type TrackerInfo } from './context.js';
6
- import { type AxisFormat } from './format.js';
6
+ import { type AxisFormat, type CursorFormat } from './format.js';
7
7
  import { type ChartTheme } from './theme.js';
8
8
  export interface ChartContainerProps {
9
9
  /**
@@ -290,12 +290,33 @@ export interface ChartContainerProps {
290
290
  */
291
291
  snap?: boolean;
292
292
  /**
293
- * Time-axis value formatting — a d3 time specifier string (e.g. `'%H:%M'`) or a
294
- * `(epochMs) => string` function ({@link AxisFormat}); applies to both the time
295
- * axis labels and the cursor-time readout. **Omitted d3's multi-scale time
296
- * format** (`12 PM`, `12:10`, …).
293
+ * Time-axis **label** formatting — a d3 time specifier string (e.g. `'%H:%M'`)
294
+ * or a `(epochMs) => string` function ({@link AxisFormat}). A custom format
295
+ * **owns the labels**, so it opts the axis out of the `dateStyle` ladder
296
+ * (flat / stacked) by design. **Omitted the flat/stacked date style.** To
297
+ * shape only the cursor readout while keeping a date style, use
298
+ * {@link cursorFormat} instead. (For back-compat this also shapes the readout
299
+ * when `cursorFormat` is absent.)
297
300
  */
298
301
  timeFormat?: AxisFormat;
302
+ /**
303
+ * The **cursor / marker readout** format — the crosshair time pill, marker
304
+ * axis indicators, and annotation auto-labels — **independent of the tick
305
+ * labels**, so it does **not** disqualify the `dateStyle` ladder.
306
+ * **Omitted ⇒ a grain-aware default**: the readout formats at the axis's own
307
+ * granularity, so a day-or-coarser axis reads a **date** (never a
308
+ * time-of-day) and a sub-day axis reads date + clock — a daily bar at a
309
+ * foreign-tz midnight no longer renders as `02 AM`.
310
+ *
311
+ * A d3 time-specifier **string** formats uniformly at every zoom; a
312
+ * **function** `(epochMs, { grain, defaultText }) => string` receives the
313
+ * axis's resolved coarse {@link TimeGrain} and the grain-aware default text,
314
+ * so it can branch on the zoom level and pass `defaultText` through for
315
+ * grains it doesn't override (no re-deriving the grain from the range). See
316
+ * {@link CursorFormat}. This is the independent readout channel;
317
+ * {@link timeFormat} owns the labels.
318
+ */
319
+ cursorFormat?: CursorFormat;
299
320
  /** Visual theme for all rows; defaults to {@link defaultTheme}. */
300
321
  theme?: ChartTheme;
301
322
  children?: ReactNode;
@@ -309,5 +330,5 @@ export interface ChartContainerProps {
309
330
  * {@link TimeAxis} at the bottom, aligned under the plots. Y axes are per-row
310
331
  * (`<YAxis>`).
311
332
  */
312
- export declare function ChartContainer({ range, width, rowGap, showAxis, trackerPosition, onTrackerChanged, selected, onSelect, hovered, onHover, panZoom, onTimeRangeChange, minDuration, cursor, cursorSequence, onRegionSelect, regionSelectModifier, cursorTime, crosshairSnap, editAnnotations, creating, onCreate, onSelectAnnotation, onHoverAnnotation, onEditAnnotation, snap, timeFormat, theme, discontinuities, calendar, spacing, grid, sessionDividers, children, }: ChartContainerProps): import("react/jsx-runtime").JSX.Element;
333
+ export declare function ChartContainer({ range, width, rowGap, showAxis, trackerPosition, onTrackerChanged, selected, onSelect, hovered, onHover, panZoom, onTimeRangeChange, minDuration, cursor, cursorSequence, onRegionSelect, regionSelectModifier, cursorTime, crosshairSnap, editAnnotations, creating, onCreate, onSelectAnnotation, onHoverAnnotation, onEditAnnotation, snap, timeFormat, cursorFormat, theme, discontinuities, calendar, spacing, grid, sessionDividers, children, }: ChartContainerProps): import("react/jsx-runtime").JSX.Element;
313
334
  //# sourceMappingURL=ChartContainer.d.ts.map
@@ -43,7 +43,7 @@ function normalizeRange(range) {
43
43
  * {@link TimeAxis} at the bottom, aligned under the plots. Y axes are per-row
44
44
  * (`<YAxis>`).
45
45
  */
46
- export function ChartContainer({ range, width, rowGap = 0, showAxis = true, trackerPosition, onTrackerChanged, selected, onSelect, hovered, onHover, panZoom = false, onTimeRangeChange, minDuration = 1, cursor = DEFAULT_CURSOR_MODE, cursorSequence, onRegionSelect, regionSelectModifier, cursorTime = false, crosshairSnap = true, editAnnotations = false, creating = null, onCreate, onSelectAnnotation, onHoverAnnotation, onEditAnnotation, snap = true, timeFormat, theme, discontinuities, calendar, spacing, grid = true, sessionDividers = 'none', children, }) {
46
+ export function ChartContainer({ range, width, rowGap = 0, showAxis = true, trackerPosition, onTrackerChanged, selected, onSelect, hovered, onHover, panZoom = false, onTimeRangeChange, minDuration = 1, cursor = DEFAULT_CURSOR_MODE, cursorSequence, onRegionSelect, regionSelectModifier, cursorTime = false, crosshairSnap = true, editAnnotations = false, creating = null, onCreate, onSelectAnnotation, onHoverAnnotation, onEditAnnotation, snap = true, timeFormat, cursorFormat, theme, discontinuities, calendar, spacing, grid = true, sessionDividers = 'none', children, }) {
47
47
  // The explicit base domain from `range` (a tuple or a TimeRange). `undefined`
48
48
  // ⇒ auto-fit (resolved from the layers below). Pan/zoom seeds from it; `seed`
49
49
  // is the placeholder while auto-fitting.
@@ -361,6 +361,30 @@ export function ChartContainer({ range, width, rowGap = 0, showAxis = true, trac
361
361
  formatTime: resolveAxisFormat(s, xTickCount, timeFormat),
362
362
  };
363
363
  }
364
+ // The cursor / marker / annotation **readout** formatter. `cursorFormat`
365
+ // wins (the independent readout channel — it never disqualifies the date
366
+ // style); else a container `timeFormat` (back-compat: shapes the readout
367
+ // too, and already opts labels out of the ladder); else the scale's
368
+ // **grain-aware** default (a day-or-coarser axis reads a date, not a
369
+ // time-of-day — the F-charts-7 `02 AM` fix), never d3's multi-scale default.
370
+ const timeReadout = (s) => {
371
+ // A `cursorFormat` **function** gets the axis's resolved coarse grain and
372
+ // the grain-aware default text per instant, so it can branch on zoom and
373
+ // pass the default through. A **string** formats uniformly (d3 specifier).
374
+ if (typeof cursorFormat === 'function') {
375
+ const grain = s.grain(xTickCount);
376
+ const def = s.readoutFormat(xTickCount);
377
+ return (v) => cursorFormat(v, { grain, defaultText: def(v) });
378
+ }
379
+ if (cursorFormat !== undefined) {
380
+ return resolveTimeFormat(s, xTickCount, cursorFormat);
381
+ }
382
+ // No cursorFormat: a container timeFormat still shapes the readout
383
+ // (back-compat); else the scale's grain-aware default.
384
+ return timeFormat !== undefined
385
+ ? resolveTimeFormat(s, xTickCount, timeFormat)
386
+ : s.readoutFormat(xTickCount);
387
+ };
364
388
  if (xDiscontinuities !== undefined) {
365
389
  // Trading-time axis: closed-market gaps collapse, time proportional within
366
390
  // sessions. Same tickFormat surface as scaleTime, so the readout is shared.
@@ -369,10 +393,7 @@ export function ChartContainer({ range, width, rowGap = 0, showAxis = true, trac
369
393
  const s = scaleTradingTime(xDiscontinuities)
370
394
  .domain([d0, d1])
371
395
  .range([0, plotWidth]);
372
- return {
373
- xScale: s,
374
- formatTime: resolveTimeFormat(s, xTickCount, timeFormat),
375
- };
396
+ return { xScale: s, formatTime: timeReadout(s) };
376
397
  }
377
398
  // Plain continuous time axis: the same trading-time scale over the
378
399
  // gap-free identity provider, so it runs the same logical tick ladder
@@ -383,10 +404,7 @@ export function ChartContainer({ range, width, rowGap = 0, showAxis = true, trac
383
404
  const s = scaleTradingTime(identityProvider())
384
405
  .domain([d0, d1])
385
406
  .range([0, plotWidth]);
386
- return {
387
- xScale: s,
388
- formatTime: resolveTimeFormat(s, xTickCount, timeFormat),
389
- };
407
+ return { xScale: s, formatTime: timeReadout(s) };
390
408
  }, [
391
409
  resolvedKind,
392
410
  categories,
@@ -394,6 +412,7 @@ export function ChartContainer({ range, width, rowGap = 0, showAxis = true, trac
394
412
  d1,
395
413
  plotWidth,
396
414
  timeFormat,
415
+ cursorFormat,
397
416
  xDiscontinuities,
398
417
  xTickCount,
399
418
  ]);
package/dist/format.d.ts CHANGED
@@ -6,12 +6,32 @@
6
6
  * uses for the ticks), a **function** is used verbatim, and `undefined` falls
7
7
  * back to the scale's default `tickFormat`.
8
8
  */
9
+ import type { TimeGrain } from './tickLadder.js';
9
10
  /**
10
11
  * How to format an axis's values — a d3 [format specifier]
11
12
  * (https://github.com/d3/d3-format#locale_format) string, or a custom
12
13
  * `(value) => string` function. Omit for the scale's d3 default.
13
14
  */
14
15
  export type AxisFormat = string | ((value: number) => string);
16
+ /**
17
+ * How to format the **cursor / marker readout** on a time axis
18
+ * ({@link ChartContainerProps.cursorFormat}). Either:
19
+ *
20
+ * - a d3 time specifier **string** (e.g. `'%b %-d'`) applied uniformly at every
21
+ * zoom; or
22
+ * - a **function** `(epochMs, ctx) => string`, where `ctx.grain` is the axis's
23
+ * resolved coarse {@link TimeGrain} (`year` … `second`) and `ctx.defaultText`
24
+ * is the library's grain-aware default readout for that instant — so a
25
+ * consumer can branch on the zoom level (`grain === 'year' ? … : …`) and
26
+ * pass `defaultText` through for the grains they don't want to override.
27
+ *
28
+ * The library hands you the grain because it already resolved it — you never
29
+ * re-derive it from the range.
30
+ */
31
+ export type CursorFormat = string | ((epochMs: number, ctx: {
32
+ readonly grain: TimeGrain;
33
+ readonly defaultText: string;
34
+ }) => string);
15
35
  /** The slice of a d3 scale {@link resolveAxisFormat} needs — `tickFormat` with an
16
36
  * optional specifier. A d3 `ScaleLinear` / `ScaleTime` satisfies it. */
17
37
  interface Tickable {
package/dist/index.d.ts CHANGED
@@ -31,7 +31,7 @@ export type { XAxisProps } from './XAxis.js';
31
31
  export type { AxisTransform } from './derivedTicks.js';
32
32
  export { TimeAxis } from './TimeAxis.js';
33
33
  export { CategoryAxis } from './CategoryAxis.js';
34
- export type { AxisFormat } from './format.js';
34
+ export type { AxisFormat, CursorFormat } from './format.js';
35
35
  export { LineChart } from './LineChart.js';
36
36
  export type { LineChartProps } from './LineChart.js';
37
37
  export { BandChart } from './BandChart.js';
@@ -48,7 +48,7 @@ export { Candlestick } from './Candlestick.js';
48
48
  export type { CandlestickProps } from './Candlestick.js';
49
49
  export type { CandleVariant, ColorBy } from './ohlc.js';
50
50
  export { scaleTradingTime } from './tradingTimeScale.js';
51
- export type { TradingTimeScale, DiscontinuityProvider, } from './tradingTimeScale.js';
51
+ export type { TradingTimeScale, DiscontinuityProvider, TimeGrain, } from './tradingTimeScale.js';
52
52
  export { scaleBand } from './bandScale.js';
53
53
  export type { ScaleBand } from './bandScale.js';
54
54
  export { Region, Baseline, Marker } from './annotations.js';
@@ -24,6 +24,16 @@ import type { DiscontinuityProvider } from './tradingTimeScale.js';
24
24
  */
25
25
  /** The calendar grain a run of tick anchors is bucketed to. */
26
26
  export type TickGranularity = 'second1' | 'second5' | 'second15' | 'second30' | 'minute1' | 'minute5' | 'minute15' | 'minute30' | 'hour1' | 'hour3' | 'hour6' | 'hour12' | 'day' | 'week' | 'month' | 'quarter' | 'year';
27
+ /**
28
+ * The **coarse calendar unit** of a resolved grain — the public grain a
29
+ * `cursorFormat` callback branches on, with the internal 1/5/15/30 strides
30
+ * collapsed to their unit (`hour1|3|6|12` → `hour`). Stable across zoom steps
31
+ * within a unit, so a consumer's `grain === 'hour'` check doesn't churn as the
32
+ * stride changes.
33
+ */
34
+ export type TimeGrain = 'year' | 'quarter' | 'month' | 'week' | 'day' | 'hour' | 'minute' | 'second';
35
+ /** Collapse a {@link TickGranularity} to its coarse {@link TimeGrain} unit. */
36
+ export declare function coarseUnitOf(g: TickGranularity): TimeGrain;
27
37
  /**
28
38
  * The local-time bucket key for `t` at grain `g` — two instants in the same
29
39
  * day / week / month / quarter / year share a key. Local time (not UTC) so it
@@ -143,6 +153,16 @@ export declare function boundaryFormatFor(g: TickGranularity): string;
143
153
  * the next coarser unit — sub-day → day, day/week → month, month/quarter →
144
154
  * year, year → none. */
145
155
  export declare function bandGrainFor(g: TickGranularity): TickGranularity | undefined;
156
+ /**
157
+ * d3 specifier for the **cursor / marker readout** at grain `g` — a hovered
158
+ * instant formatted at the axis's own granularity, never finer. A day-or-coarser
159
+ * axis reads a **date** (no time-of-day), so a daily bar at a foreign-tz
160
+ * midnight can't render as `02 AM`; a sub-day axis reads date **+** clock. This
161
+ * is the grain-aware default that replaces d3's multi-scale default for the
162
+ * readout (a `cursorFormat` override, when given, wins over it). Unambiguous by
163
+ * design — the readout carries the year / date the terse tick labels omit.
164
+ */
165
+ export declare function readoutFormatFor(g: TickGranularity): string;
146
166
  /** d3 specifier for a **band** label at band grain `g`: the date for a day band
147
167
  * (`Jan 12`), the full month for a month band (`January`), the year for a year
148
168
  * band (`2031`). Left-aligned in the band by the renderer. */
@@ -1,3 +1,13 @@
1
+ /** Collapse a {@link TickGranularity} to its coarse {@link TimeGrain} unit. */
2
+ export function coarseUnitOf(g) {
3
+ if (g.startsWith('second'))
4
+ return 'second';
5
+ if (g.startsWith('minute'))
6
+ return 'minute';
7
+ if (g.startsWith('hour'))
8
+ return 'hour';
9
+ return g; // 'day' | 'week' | 'month' | 'quarter' | 'year'
10
+ }
1
11
  const SEC_MS = 1_000;
2
12
  const MIN_MS = 60_000;
3
13
  const HOUR_MS = 3_600_000;
@@ -651,6 +661,34 @@ export function bandGrainFor(g) {
651
661
  return undefined; // year — no coarser band
652
662
  }
653
663
  }
664
+ /**
665
+ * d3 specifier for the **cursor / marker readout** at grain `g` — a hovered
666
+ * instant formatted at the axis's own granularity, never finer. A day-or-coarser
667
+ * axis reads a **date** (no time-of-day), so a daily bar at a foreign-tz
668
+ * midnight can't render as `02 AM`; a sub-day axis reads date **+** clock. This
669
+ * is the grain-aware default that replaces d3's multi-scale default for the
670
+ * readout (a `cursorFormat` override, when given, wins over it). Unambiguous by
671
+ * design — the readout carries the year / date the terse tick labels omit.
672
+ */
673
+ export function readoutFormatFor(g) {
674
+ if (g === 'second1' ||
675
+ g === 'second5' ||
676
+ g === 'second15' ||
677
+ g === 'second30')
678
+ return '%b %-d, %H:%M:%S';
679
+ if (isSubDay(g))
680
+ return '%b %-d, %H:%M';
681
+ switch (g) {
682
+ case 'day':
683
+ case 'week':
684
+ return '%b %-d, %Y';
685
+ case 'month':
686
+ case 'quarter':
687
+ return '%b %Y';
688
+ default:
689
+ return '%Y'; // year
690
+ }
691
+ }
654
692
  /** d3 specifier for a **band** label at band grain `g`: the date for a day band
655
693
  * (`Jan 12`), the full month for a month band (`January`), the year for a year
656
694
  * band (`2031`). Left-aligned in the band by the renderer. */
@@ -1,4 +1,4 @@
1
- import { type TickGranularity } from './tickLadder.js';
1
+ import { type TickGranularity, type TimeGrain } from './tickLadder.js';
2
2
  /**
3
3
  * The structural discontinuity-provider surface `scaleTradingTime` consumes to
4
4
  * collapse closed-market time. Charts declares this **shape** itself and never
@@ -59,7 +59,8 @@ export interface TradingCalendarLike {
59
59
  * month grain) and returns the finest rung that fits `count`, and
60
60
  * `.tickFormat` labels each anchor at that grain (`%H:%M` for hours, `%b %d`
61
61
  * for days/weeks, `%b` for months/quarters, `%Y` for years) while formatting
62
- * any other instant (the cursor readout) with the d3 multi-scale default. The
62
+ * any other instant with the d3 multi-scale default (the cursor readout uses
63
+ * `.readoutFormat`, a grain-aware date/clock format, not this). The
63
64
  * coarser context a label drops lives on `.tickBoundaries` — the second-row
64
65
  * boundary labels of the **stacked** date style (the date over a clock axis,
65
66
  * the year over a day / week / month axis), one per boundary crossing plus the
@@ -97,10 +98,10 @@ export interface TradingTimeScale {
97
98
  * base label (bare day-of-month, month abbrev, clock time) otherwise — the
98
99
  * TradingView default axis, the alternative to the two-row {@link tickFormat}
99
100
  * + {@link tickBoundaries} stack. Same grain selection as {@link ticks} at the
100
- * same `count`, so the labels sit on the tick instants. A non-tick value (the
101
- * cursor readout) formats with the d3 multi-scale default, like
102
- * {@link tickFormat} so ticks read terse while the crosshair reads a full
103
- * timestamp.
101
+ * same `count`, so the labels sit on the tick instants. A non-tick value
102
+ * formats with the d3 multi-scale default, like {@link tickFormat}; the
103
+ * cursor readout itself uses {@link readoutFormat} (grain-aware), so ticks
104
+ * read terse while the crosshair reads an unambiguous date/clock.
104
105
  */
105
106
  flatFormat(count?: number): (value: number) => string;
106
107
  /**
@@ -147,6 +148,24 @@ export interface TradingTimeScale {
147
148
  * band row ({@link bands}) instead of inline.
148
149
  */
149
150
  baseFormat(count?: number): (value: number) => string;
151
+ /**
152
+ * The **cursor / marker readout** formatter — a hovered instant formatted at
153
+ * the axis's own grain, never finer (a day-or-coarser axis reads a **date**,
154
+ * so a daily bar at a foreign-tz midnight never renders as a time-of-day like
155
+ * `02 AM`; a sub-day axis reads date **+** clock). This is the grain-aware
156
+ * default the crosshair pill, marker indicators, and annotation auto-labels
157
+ * use — replacing d3's multi-scale default, which showed local time-of-day
158
+ * for any off-local-midnight instant. A container `cursorFormat` overrides it.
159
+ */
160
+ readoutFormat(count?: number): (value: number) => string;
161
+ /**
162
+ * The axis's resolved **coarse grain** ({@link TimeGrain}) at `count` — the
163
+ * unit the ticks currently sit on (`year` … `second`), strides collapsed. A
164
+ * `cursorFormat` callback receives this so it can branch on the zoom level
165
+ * (return a year when zoomed out, a clock when zoomed in) without
166
+ * re-deriving the ladder the axis already ran.
167
+ */
168
+ grain(count?: number): TimeGrain;
150
169
  /**
151
170
  * The **date bands** — the segmented second row of the stacked style. One
152
171
  * entry per next-coarser calendar period touching the domain (day bands
@@ -171,7 +190,7 @@ export interface TradingTimeScale {
171
190
  copy(): TradingTimeScale;
172
191
  }
173
192
  export { coarsenCalendar } from './tickLadder.js';
174
- export type { TickGranularity } from './tickLadder.js';
193
+ export type { TickGranularity, TimeGrain } from './tickLadder.js';
175
194
  /**
176
195
  * The trivial gap-free {@link DiscontinuityProvider}: live time **is** wall
177
196
  * time, and every local midnight is a "session open". Backing a plain
@@ -1,5 +1,5 @@
1
1
  import { scaleTime } from 'd3-scale';
2
- import { bandFormatFor, bandGrainFor, bandNext, bandShaded, bandStartOf, boundaryFormatFor, boundaryGrainFor, boundaryTicks, buildGridLevels, buildTicks, flatBaseFormatFor, flatFormats, majorFormatFor, nominalStepMs, } from './tickLadder.js';
2
+ import { bandFormatFor, bandGrainFor, bandNext, bandShaded, bandStartOf, boundaryFormatFor, boundaryGrainFor, boundaryTicks, buildGridLevels, buildTicks, coarseUnitOf, flatBaseFormatFor, flatFormats, majorFormatFor, nominalStepMs, readoutFormatFor, } from './tickLadder.js';
3
3
  // Grain selection lives in `tickLadder.ts` (the full hour1…year ladder plus
4
4
  // the boundary-row helpers); re-exported here so existing imports keep working.
5
5
  export { coarsenCalendar } from './tickLadder.js';
@@ -140,9 +140,9 @@ export function scaleTradingTime(provider) {
140
140
  return (value) => labelled.get(value);
141
141
  };
142
142
  scale.flatFormat = (count = 10) => {
143
- // A non-tick instant (the cursor readout) always uses the d3 multi-scale
144
- // default same as tickFormat, so the crosshair reads a full timestamp
145
- // while the ticks read terse. Without a calendar there are no ladder
143
+ // A non-tick instant always uses the d3 multi-scale default — same as
144
+ // tickFormat. (The cursor readout doesn't route through here; it uses
145
+ // readoutFormat, grain-aware.) Without a calendar there are no ladder
146
146
  // anchors, so every value falls through to the default.
147
147
  const defFmt = base.tickFormat(count);
148
148
  if (!hasCalendar())
@@ -181,6 +181,14 @@ export function scaleTradingTime(provider) {
181
181
  const terse = base.tickFormat(count, flatBaseFormatFor(granularity));
182
182
  return (value) => anchors.has(value) ? terse(new Date(value)) : defFmt(new Date(value));
183
183
  };
184
+ scale.readoutFormat = (count = 10) => {
185
+ const defFmt = base.tickFormat(count);
186
+ if (!hasCalendar())
187
+ return (value) => defFmt(new Date(value));
188
+ const fmt = base.tickFormat(count, readoutFormatFor(resolved(count).granularity));
189
+ return (value) => fmt(new Date(value));
190
+ };
191
+ scale.grain = (count = 10) => hasCalendar() ? coarseUnitOf(resolved(count).granularity) : 'day';
184
192
  scale.bands = (count = 10) => {
185
193
  if (!hasCalendar())
186
194
  return [];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pond-ts/charts",
3
- "version": "0.47.0",
3
+ "version": "0.48.0",
4
4
  "private": false,
5
5
  "description": "Canvas-rendered, streaming-first time-series charts for pond-ts",
6
6
  "license": "MIT",
@@ -38,8 +38,8 @@
38
38
  "perf": "PERF_BENCH=1 playwright test perf.spec.ts --workers=1"
39
39
  },
40
40
  "peerDependencies": {
41
- "@pond-ts/react": "^0.47.0",
42
- "pond-ts": "^0.47.0",
41
+ "@pond-ts/react": "^0.48.0",
42
+ "pond-ts": "^0.48.0",
43
43
  "react": "^18.0.0 || ^19.0.0"
44
44
  },
45
45
  "devDependencies": {