@pond-ts/charts 0.46.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 +130 -3
- package/dist/ChartContainer.d.ts +56 -7
- package/dist/ChartContainer.js +32 -9
- package/dist/Layers.js +120 -18
- package/dist/XAxis.d.ts +18 -1
- package/dist/XAxis.js +140 -76
- package/dist/context.d.ts +8 -0
- package/dist/format.d.ts +20 -0
- package/dist/grid.d.ts +32 -5
- package/dist/grid.js +81 -13
- package/dist/index.d.ts +2 -2
- package/dist/theme.d.ts +13 -0
- package/dist/theme.js +5 -0
- package/dist/tickLadder.d.ts +156 -16
- package/dist/tickLadder.js +634 -25
- package/dist/tradingTimeScale.d.ts +104 -10
- package/dist/tradingTimeScale.js +116 -4
- package/package.json +4 -4
package/dist/XAxis.js
CHANGED
|
@@ -9,8 +9,8 @@ import { resolveAxisFormat, resolveTimeFormat, } from './format.js';
|
|
|
9
9
|
const TICK_STRIP = 22;
|
|
10
10
|
/** Extra height reserved for an axis `label` line. */
|
|
11
11
|
const LABEL_STRIP = 16;
|
|
12
|
-
/** Extra height reserved for the
|
|
13
|
-
const
|
|
12
|
+
/** Extra height reserved for the stacked **band** (second) row. */
|
|
13
|
+
const BAND_STRIP = 20;
|
|
14
14
|
/** Minimum pixel gap between derived-unit (`transform`) ticks — the room a
|
|
15
15
|
* short numeric label needs plus breathing space, in the spirit of the
|
|
16
16
|
* ladder's per-tick budget (a hair tighter: derived labels are short). */
|
|
@@ -55,7 +55,7 @@ function thinCategoryLabels(ticks, plotWidth, fontSize) {
|
|
|
55
55
|
*
|
|
56
56
|
* `<TimeAxis>` is the time-flavoured preset (`<XAxis />`).
|
|
57
57
|
*/
|
|
58
|
-
export function XAxis({ format, label, side = 'bottom', height, ticks: customTicks, transform, color, align = 'center', } = {}) {
|
|
58
|
+
export function XAxis({ format, label, side = 'bottom', height, ticks: customTicks, transform, color, align = 'center', dateStyle = 'flat', } = {}) {
|
|
59
59
|
const container = useContext(ContainerContext);
|
|
60
60
|
if (container === null) {
|
|
61
61
|
throw new Error('<XAxis> must be rendered inside a <ChartContainer>');
|
|
@@ -90,12 +90,59 @@ export function XAxis({ format, label, side = 'bottom', height, ticks: customTic
|
|
|
90
90
|
return resolveAxisFormat(scaleLinear().domain(u), xTickCount, format);
|
|
91
91
|
})()
|
|
92
92
|
: null;
|
|
93
|
-
//
|
|
94
|
-
//
|
|
95
|
-
//
|
|
93
|
+
// Whether this axis draws **ladder-derived** date context (flat promotions or
|
|
94
|
+
// the stacked boundary row). Only a ladder-driven time scale supplies it;
|
|
95
|
+
// explicit `ticks`, an explicit axis `format`, and a container-level
|
|
96
|
+
// `timeFormat` all opt out (a custom format owns the whole label, and custom
|
|
97
|
+
// ticks have no grain).
|
|
98
|
+
const laddered = xKind === 'time' &&
|
|
99
|
+
customTicks === undefined &&
|
|
100
|
+
transform === undefined &&
|
|
101
|
+
format === undefined &&
|
|
102
|
+
!container.xFormatCustom &&
|
|
103
|
+
'tickBoundaries' in xScale;
|
|
104
|
+
// The flat-style single-row label formatter — the coarsest calendar period
|
|
105
|
+
// each tick opens promoted inline, terse base labels otherwise (the
|
|
106
|
+
// TradingView default `dateStyle`). Replaces the shared `formatTime` for the
|
|
107
|
+
// tick labels; a non-tick instant (the cursor pill) still reads a full
|
|
108
|
+
// timestamp through it. `undefined` for the stacked style / a custom format.
|
|
109
|
+
const flatFmt = laddered && dateStyle === 'flat' && 'flatFormat' in xScale
|
|
110
|
+
? xScale.flatFormat(xTickCount)
|
|
111
|
+
: undefined;
|
|
112
|
+
// The **stacked** date style renders a segmented **band** row (the coarser
|
|
113
|
+
// calendar period as zebra-shaded cells with left-aligned labels + dividers)
|
|
114
|
+
// beneath a terse top row, with the band-turn tick emphasized. The top row
|
|
115
|
+
// reads `baseFormat` (the grain's bare unit, no inline promotion — the
|
|
116
|
+
// context lives in the band), and `bands` are the segments.
|
|
117
|
+
const stacked = laddered && dateStyle === 'stacked' && 'bands' in xScale;
|
|
118
|
+
// The terse base label (the grain's bare unit, no promotion): the stacked
|
|
119
|
+
// top row, and — in flat — the yardstick for detecting which ticks were
|
|
120
|
+
// *promoted* to a coarser period, so those can be emphasized to match the
|
|
121
|
+
// stacked band turns (a period boundary reads the same in both styles).
|
|
122
|
+
const baseFmt = laddered && 'baseFormat' in xScale
|
|
123
|
+
? xScale.baseFormat(xTickCount)
|
|
124
|
+
: undefined;
|
|
125
|
+
const bands = stacked ? xScale.bands(xTickCount) : [];
|
|
126
|
+
// Pixel positions where a band **divider** is drawn — the interior band
|
|
127
|
+
// starts (a start at/left of the plot edge maps to px ≤ 0 and draws no
|
|
128
|
+
// divider). A top-row tick landing on one is the band turn: it renders bold
|
|
129
|
+
// and in the divider colour, so tick + divider read as one continuous
|
|
130
|
+
// boundary line. Matched by **pixel**, not by instant, because on a trading
|
|
131
|
+
// axis the turn tick is the session OPEN sitting at the collapsed-midnight
|
|
132
|
+
// seam — the same pixel as the midnight band-start, but a different instant.
|
|
133
|
+
const dividerXs = new Set(bands
|
|
134
|
+
.map((b) => xScale(b.start))
|
|
135
|
+
.filter((px) => px > 0) // same threshold the band border uses (startPx > 0)
|
|
136
|
+
.map((px) => Math.round(px)));
|
|
137
|
+
// Tick / readout formatter: an explicit `format` is resolved against the axis
|
|
138
|
+
// kind (a time specifier through the time scale, a number specifier through
|
|
139
|
+
// the value scale); otherwise the container's shared formatter — the one the
|
|
96
140
|
// cursor readout uses, so a tick and the cursor read identically. On a
|
|
97
141
|
// transformed axis every readout (cursor pill, marker indicator) speaks the
|
|
98
|
-
// **derived unit** — the axis's own language.
|
|
142
|
+
// **derived unit** — the axis's own language. NOTE: the flat date style
|
|
143
|
+
// applies *only* to the rendered tick labels (`tickFmt` below), never to this
|
|
144
|
+
// `fmt` — the cursor pill and marker indicators keep reading a full timestamp
|
|
145
|
+
// rather than a terse/promoted axis label.
|
|
99
146
|
const fmt = transform !== undefined && uFmt !== null && xKind !== 'category'
|
|
100
147
|
? (v) => uFmt(transform.to(v))
|
|
101
148
|
: // A category axis labels by name (the container's `formatTime` = the band
|
|
@@ -106,6 +153,11 @@ export function XAxis({ format, label, side = 'bottom', height, ticks: customTic
|
|
|
106
153
|
: xKind === 'time'
|
|
107
154
|
? resolveTimeFormat(xScale, xTickCount, format)
|
|
108
155
|
: resolveAxisFormat(xScale, xTickCount, format);
|
|
156
|
+
// The formatter for the rendered **tick labels** specifically: flat-style
|
|
157
|
+
// promoted labels, stacked-style terse base labels, else the shared `fmt`.
|
|
158
|
+
// Split from `fmt` so an axis's terse tick labels never leak into the cursor /
|
|
159
|
+
// marker readouts, which stay full timestamps.
|
|
160
|
+
const tickFmt = flatFmt ?? (stacked ? baseFmt : undefined) ?? fmt;
|
|
109
161
|
// Marker annotations that opted into an axis indicator (`<Marker indicator>`)
|
|
110
162
|
// pin their **time** to this shared x-axis — a pill at `at`, in the annotation
|
|
111
163
|
// colour, reading like a tick. An indicator always shows the axis coordinate
|
|
@@ -152,28 +204,10 @@ export function XAxis({ format, label, side = 'bottom', height, ticks: customTic
|
|
|
152
204
|
markerLanes.set(t.id, lane);
|
|
153
205
|
}
|
|
154
206
|
const maxPillLane = Math.max(0, pillLaneEnds.length - 1);
|
|
155
|
-
// The
|
|
156
|
-
//
|
|
157
|
-
//
|
|
158
|
-
//
|
|
159
|
-
// explicit axis `format`, and a container-level `timeFormat` all opt out (a
|
|
160
|
-
// custom format owns the whole label, and custom ticks have no grain).
|
|
161
|
-
const boundaryOf = xKind === 'time' &&
|
|
162
|
-
customTicks === undefined &&
|
|
163
|
-
transform === undefined &&
|
|
164
|
-
format === undefined &&
|
|
165
|
-
!container.xFormatCustom &&
|
|
166
|
-
'tickBoundaries' in xScale
|
|
167
|
-
? xScale.tickBoundaries(xTickCount)
|
|
168
|
-
: undefined;
|
|
169
|
-
// The pinned left-edge **context** label — what period the domain starts in
|
|
170
|
-
// (`Jan 01` over an intraday axis, the year over a month axis). A property
|
|
171
|
-
// of the domain, not of any tick: anchoring it to the first tick made it
|
|
172
|
-
// hop tick-to-tick on a live sliding window. Crossing labels ride their
|
|
173
|
-
// ticks and **push it off** the left edge as they approach (below).
|
|
174
|
-
const boundaryContext = boundaryOf !== undefined && 'boundaryContext' in xScale
|
|
175
|
-
? xScale.boundaryContext(xTickCount)
|
|
176
|
-
: undefined;
|
|
207
|
+
// (The `'stacked'` date style's coarser context is the segmented **band**
|
|
208
|
+
// row — `bands`, computed above — not a per-tick boundary label. The old
|
|
209
|
+
// ride-a-tick boundary row + pinned context are retired; the scale still
|
|
210
|
+
// exposes `tickBoundaries` / `boundaryContext` for any external consumer.)
|
|
177
211
|
// Derived ticks pass a **label-honesty filter**: the fill can descend below
|
|
178
212
|
// the format's resolution (a delta tick at u = 0.498 renders as "+0.50" under
|
|
179
213
|
// `+.2f` — a lie about its position), so a tick survives only when its
|
|
@@ -205,8 +239,17 @@ export function XAxis({ format, label, side = 'bottom', height, ticks: customTic
|
|
|
205
239
|
? honestDerived()
|
|
206
240
|
: xScale.ticks(xTickCount).map((d) => ({
|
|
207
241
|
x: xScale(d),
|
|
208
|
-
label:
|
|
209
|
-
|
|
242
|
+
label: tickFmt(+d),
|
|
243
|
+
// A **period turn** renders emphasized (bold), consistently across
|
|
244
|
+
// styles: in stacked, a tick on a band divider (matched by pixel);
|
|
245
|
+
// in flat, a tick whose label was *promoted* to a coarser period
|
|
246
|
+
// (its flat label differs from the terse base) — the same boundaries,
|
|
247
|
+
// so `Feb` / `2026` read as strong in flat just as the band turns do.
|
|
248
|
+
bold: stacked
|
|
249
|
+
? dividerXs.has(Math.round(xScale(d)))
|
|
250
|
+
: flatFmt !== undefined &&
|
|
251
|
+
baseFmt !== undefined &&
|
|
252
|
+
flatFmt(+d) !== baseFmt(+d),
|
|
210
253
|
}));
|
|
211
254
|
// A category axis ticks once per category; thin + truncate its labels when they
|
|
212
255
|
// crowd (an explicit `customTicks` axis keeps its labels verbatim).
|
|
@@ -219,34 +262,19 @@ export function XAxis({ format, label, side = 'bottom', height, ticks: customTic
|
|
|
219
262
|
const pillOffset = align === 'right' ? 2 : 6;
|
|
220
263
|
// Per-lane vertical step for stacked pills; grow the strip to fit the stack.
|
|
221
264
|
const PILL_LANE_H = theme.font.size + 6;
|
|
222
|
-
//
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
//
|
|
227
|
-
//
|
|
228
|
-
//
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
// use; the crossing's label is centred on its tick.
|
|
234
|
-
const charW = theme.font.size * 0.62;
|
|
235
|
-
const contextWidth = (boundaryContext?.length ?? 0) * charW;
|
|
236
|
-
const contextRight = align === 'center'
|
|
237
|
-
? contextWidth / 2
|
|
238
|
-
: align === 'right'
|
|
239
|
-
? 4 + contextWidth
|
|
240
|
-
: contextWidth;
|
|
241
|
-
const firstCrossing = placed.find((t) => t.boundary !== undefined);
|
|
242
|
-
const crossingLeft = firstCrossing
|
|
243
|
-
? align === 'right'
|
|
244
|
-
? firstCrossing.x + 4
|
|
245
|
-
: firstCrossing.x - (firstCrossing.boundary.length * charW) / 2
|
|
246
|
-
: Infinity;
|
|
247
|
-
const showContext = boundaryContext !== undefined && crossingLeft > contextRight + 6;
|
|
265
|
+
// The stacked **band** row grows the strip by one band-height row (like pill
|
|
266
|
+
// lanes do). Only when the stacked style actually has bands (not at year
|
|
267
|
+
// grain / on a non-ladder axis).
|
|
268
|
+
const hasBands = stacked && bands.length > 0;
|
|
269
|
+
// Band-row colours (themeable via `theme.axis.band`): the zebra shade fill,
|
|
270
|
+
// the turn divider, and the label ink. A per-axis `color` override wins for
|
|
271
|
+
// divider + label (the fill stays the band's own shade).
|
|
272
|
+
const bandTheme = theme.axis.band;
|
|
273
|
+
const bandFill = bandTheme?.fill ?? theme.chip?.background ?? 'rgba(0,0,0,0.04)';
|
|
274
|
+
const bandDivider = color ?? bandTheme?.divider ?? theme.axis.grid;
|
|
275
|
+
const bandLabelColor = color ?? bandTheme?.label ?? theme.axis.title?.color ?? theme.axis.label;
|
|
248
276
|
const stripHeight = (height ?? TICK_STRIP + (label ? LABEL_STRIP : 0)) +
|
|
249
|
-
(
|
|
277
|
+
(hasBands ? BAND_STRIP : 0) +
|
|
250
278
|
maxPillLane * PILL_LANE_H;
|
|
251
279
|
return (_jsxs("div", { style: {
|
|
252
280
|
position: 'relative',
|
|
@@ -271,8 +299,18 @@ export function XAxis({ format, label, side = 'bottom', height, ticks: customTic
|
|
|
271
299
|
: align === 'auto' && isLast
|
|
272
300
|
? 'translateX(-100%)'
|
|
273
301
|
: 'translateX(-50%)';
|
|
274
|
-
//
|
|
275
|
-
|
|
302
|
+
// Stacked band mode: every tick runs the full tick-row height to *meet*
|
|
303
|
+
// the inter-row rule, so the boundary reads as one line continuing into
|
|
304
|
+
// the band divider below. `right` drops a longer tick beside the label;
|
|
305
|
+
// otherwise a 4px stub.
|
|
306
|
+
const tickHeight = hasBands
|
|
307
|
+
? stripHeight - BAND_STRIP
|
|
308
|
+
: align === 'right'
|
|
309
|
+
? theme.font.size + 4
|
|
310
|
+
: 4;
|
|
311
|
+
// The band-turn tick takes the divider colour so tick + divider read as
|
|
312
|
+
// one continuous boundary; minor ticks stay the faint grid colour.
|
|
313
|
+
const tickColor = hasBands && t.bold ? bandDivider : (color ?? theme.axis.grid);
|
|
276
314
|
const labelLeft = align === 'right' ? t.x + 4 : t.x;
|
|
277
315
|
const labelOffset = align === 'right' ? 2 : 6;
|
|
278
316
|
return (_jsxs(Fragment, { children: [_jsx("div", { style: {
|
|
@@ -281,29 +319,55 @@ export function XAxis({ format, label, side = 'bottom', height, ticks: customTic
|
|
|
281
319
|
[onTop ? 'bottom' : 'top']: 0,
|
|
282
320
|
width: '1px',
|
|
283
321
|
height: `${tickHeight}px`,
|
|
284
|
-
background:
|
|
322
|
+
background: tickColor,
|
|
285
323
|
} }), _jsx("div", { style: {
|
|
286
324
|
position: 'absolute',
|
|
287
325
|
left: `${labelLeft}px`,
|
|
288
326
|
[onTop ? 'bottom' : 'top']: `${labelOffset}px`,
|
|
289
327
|
transform: labelTransform,
|
|
290
328
|
whiteSpace: 'nowrap',
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
whiteSpace: 'nowrap',
|
|
297
|
-
opacity: 0.75,
|
|
298
|
-
}, children: t.boundary }))] }, `${t.x}-${i}`));
|
|
299
|
-
}), showContext && (_jsx("div", { "data-boundary-label": true, "data-boundary-context": true, style: {
|
|
329
|
+
// Stacked band style bolds the tick sitting on a band turn
|
|
330
|
+
// (the day/month/year start) — the emphasized boundary tick.
|
|
331
|
+
fontWeight: t.bold ? 700 : undefined,
|
|
332
|
+
}, children: t.label })] }, `${t.x}-${i}`));
|
|
333
|
+
}), hasBands && (_jsx("div", { style: {
|
|
300
334
|
position: 'absolute',
|
|
301
|
-
left:
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
335
|
+
left: 0,
|
|
336
|
+
width: `${plotWidth}px`,
|
|
337
|
+
[onTop ? 'top' : 'bottom']: 0,
|
|
338
|
+
height: `${BAND_STRIP}px`,
|
|
339
|
+
// The rule between the terse tick row and the band row.
|
|
340
|
+
[onTop ? 'borderBottom' : 'borderTop']: `1px solid ${color ?? theme.axis.grid}`,
|
|
341
|
+
overflow: 'hidden',
|
|
342
|
+
}, children: bands.map((b, i) => {
|
|
343
|
+
// Each band spans [its start, the next band's start) in pixels,
|
|
344
|
+
// clamped to the plot. The first (partial) band starts off-screen
|
|
345
|
+
// left, so its label pins at x=0; interior bands carry a divider
|
|
346
|
+
// at their start. Shaded bands paint the zebra fill.
|
|
347
|
+
const startPx = xScale(b.start);
|
|
348
|
+
const left = Math.max(0, startPx);
|
|
349
|
+
const nextPx = i + 1 < bands.length ? xScale(bands[i + 1].start) : plotWidth;
|
|
350
|
+
const width = Math.max(0, Math.min(plotWidth, nextPx) - left);
|
|
351
|
+
if (width <= 0)
|
|
352
|
+
return null;
|
|
353
|
+
return (_jsx("div", { "data-band-label": true, style: {
|
|
354
|
+
position: 'absolute',
|
|
355
|
+
left: `${left}px`,
|
|
356
|
+
top: 0,
|
|
357
|
+
bottom: 0,
|
|
358
|
+
width: `${width}px`,
|
|
359
|
+
boxSizing: 'border-box',
|
|
360
|
+
background: b.shaded ? bandFill : 'transparent',
|
|
361
|
+
borderLeft: startPx > 0 ? `1px solid ${bandDivider}` : 'none',
|
|
362
|
+
display: 'flex',
|
|
363
|
+
alignItems: 'center',
|
|
364
|
+
paddingLeft: '8px',
|
|
365
|
+
color: bandLabelColor,
|
|
366
|
+
fontWeight: 600,
|
|
367
|
+
whiteSpace: 'nowrap',
|
|
368
|
+
overflow: 'hidden',
|
|
369
|
+
}, children: b.label }, b.start));
|
|
370
|
+
}) })), label !== undefined && (_jsx("div", { style: {
|
|
307
371
|
position: 'absolute',
|
|
308
372
|
left: 0,
|
|
309
373
|
width: '100%',
|
package/dist/context.d.ts
CHANGED
|
@@ -203,6 +203,14 @@ export interface ContainerFrame {
|
|
|
203
203
|
* rather than raw wall-clock ms.
|
|
204
204
|
*/
|
|
205
205
|
readonly discontinuities?: DiscontinuityProvider | undefined;
|
|
206
|
+
/** Draw the reference gridlines behind the data (default `true`; the
|
|
207
|
+
* container's `grid` prop). Session dividers are independent of this. */
|
|
208
|
+
readonly grid: boolean;
|
|
209
|
+
/** Where session dividers draw on a trading axis: `'none'` (the default —
|
|
210
|
+
* the hierarchical grid already marks calendar structure), `'all'` (every
|
|
211
|
+
* session boundary in view — the TradingView separator look), or
|
|
212
|
+
* `'labeled'` (only under labelled collapse points). */
|
|
213
|
+
readonly sessionDividers: 'labeled' | 'all' | 'none';
|
|
206
214
|
/**
|
|
207
215
|
* The resolved kind of the shared x scale — `'time'` (a `scaleTime`),
|
|
208
216
|
* `'value'` (a `scaleLinear`), or `'category'` (a {@link ScaleBand}: an ordinal
|
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/grid.d.ts
CHANGED
|
@@ -1,13 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Stroke the plot's gridlines: a vertical line at each `xTicks` pixel and a
|
|
3
|
-
* horizontal line at each `yTicks` pixel, faint and dashed.
|
|
4
|
-
*
|
|
5
|
-
*
|
|
3
|
+
* horizontal line at each `yTicks` pixel, faint and dashed. On a calendar
|
|
4
|
+
* axis the verticals are the **full grain populations** (every day / month /
|
|
5
|
+
* aligned hour in view — see `TradingTimeScale.gridLevels`), not just the
|
|
6
|
+
* labelled ticks. `+0.5` aligns each 1px stroke to the device grid for a
|
|
7
|
+
* crisp line.
|
|
8
|
+
*
|
|
9
|
+
* `xAlphas` (parallel to `xTicks`) fades individual verticals — the
|
|
10
|
+
* hierarchical density falloff: a crowding grain's lines dim toward invisible
|
|
11
|
+
* while coarser grains hold full strength. Full-alpha lines (and all the
|
|
12
|
+
* horizontals) batch into one path; only the fading remainder pays a
|
|
13
|
+
* per-line stroke. A near-zero alpha is skipped.
|
|
6
14
|
*
|
|
7
15
|
* `save`/`restore` brackets the dash + stroke state so it doesn't leak into the
|
|
8
16
|
* data layers that draw next.
|
|
9
17
|
*/
|
|
10
|
-
export declare function drawGrid(ctx: CanvasRenderingContext2D, xTicks: readonly number[], yTicks: readonly number[], width: number, height: number, color: string, dash: readonly number[]): void;
|
|
18
|
+
export declare function drawGrid(ctx: CanvasRenderingContext2D, xTicks: readonly number[], yTicks: readonly number[], width: number, height: number, color: string, dash: readonly number[], xAlphas?: readonly number[]): void;
|
|
11
19
|
/**
|
|
12
20
|
* Greedily thin an **ascending** list of pixel positions so no two kept lines
|
|
13
21
|
* are closer than `minGap` px — keeps the axis from crowding when collapse
|
|
@@ -20,6 +28,25 @@ export declare function thinPixels(xs: readonly number[], minGap: number): numbe
|
|
|
20
28
|
* the plot height. Drawn a touch stronger than the dashed gridlines (solid, so a
|
|
21
29
|
* session/day boundary reads as structural, not just another tick) at the
|
|
22
30
|
* discontinuity provider's collapse points (see `DiscontinuityProvider.boundaries`).
|
|
31
|
+
*
|
|
32
|
+
* `alphas` (parallel to `xs`) fades individual lines — used by the `'all'`
|
|
33
|
+
* session-divider mode so crowding lines dim smoothly toward invisible instead
|
|
34
|
+
* of popping in/out with a hard density cutoff. Omitted ⇒ every line at full
|
|
35
|
+
* opacity in a single path (the fast default). A near-zero alpha is skipped.
|
|
36
|
+
*/
|
|
37
|
+
export declare function drawDividers(ctx: CanvasRenderingContext2D, xs: readonly number[], height: number, color: string, alphas?: readonly number[]): void;
|
|
38
|
+
/**
|
|
39
|
+
* Per-line opacity for {@link drawDividers} in `'all'` mode: each line keys off
|
|
40
|
+
* the gap to its nearest neighbour — full at `fullPx`+, **zero** at `gonePx`
|
|
41
|
+
* and below, a quadratic ramp between. So as a zoom-out crowds the session
|
|
42
|
+
* lines they dim toward a clean plot — no hard drop that pops on pan.
|
|
43
|
+
*
|
|
44
|
+
* The curve must fall **superlinearly** in the gap: the veil a reader sees is
|
|
45
|
+
* `alpha × density = alpha / gap`, so the earlier linear ramp (`alpha = gap/f`)
|
|
46
|
+
* cancelled the density growth exactly and pinned a constant gray wash over the
|
|
47
|
+
* whole plot no matter how far out you zoomed. Quadratic-to-a-floor makes the
|
|
48
|
+
* wash itself → 0 as lines converge: alpha `t²` with
|
|
49
|
+
* `t = (gap − gonePx) / (fullPx − gonePx)`. `xs` ascending.
|
|
23
50
|
*/
|
|
24
|
-
export declare function
|
|
51
|
+
export declare function dividerAlphas(xs: readonly number[], gonePx: number, fullPx: number): number[];
|
|
25
52
|
//# sourceMappingURL=grid.d.ts.map
|
package/dist/grid.js
CHANGED
|
@@ -1,20 +1,30 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Stroke the plot's gridlines: a vertical line at each `xTicks` pixel and a
|
|
3
|
-
* horizontal line at each `yTicks` pixel, faint and dashed.
|
|
4
|
-
*
|
|
5
|
-
*
|
|
3
|
+
* horizontal line at each `yTicks` pixel, faint and dashed. On a calendar
|
|
4
|
+
* axis the verticals are the **full grain populations** (every day / month /
|
|
5
|
+
* aligned hour in view — see `TradingTimeScale.gridLevels`), not just the
|
|
6
|
+
* labelled ticks. `+0.5` aligns each 1px stroke to the device grid for a
|
|
7
|
+
* crisp line.
|
|
8
|
+
*
|
|
9
|
+
* `xAlphas` (parallel to `xTicks`) fades individual verticals — the
|
|
10
|
+
* hierarchical density falloff: a crowding grain's lines dim toward invisible
|
|
11
|
+
* while coarser grains hold full strength. Full-alpha lines (and all the
|
|
12
|
+
* horizontals) batch into one path; only the fading remainder pays a
|
|
13
|
+
* per-line stroke. A near-zero alpha is skipped.
|
|
6
14
|
*
|
|
7
15
|
* `save`/`restore` brackets the dash + stroke state so it doesn't leak into the
|
|
8
16
|
* data layers that draw next.
|
|
9
17
|
*/
|
|
10
|
-
export function drawGrid(ctx, xTicks, yTicks, width, height, color, dash) {
|
|
18
|
+
export function drawGrid(ctx, xTicks, yTicks, width, height, color, dash, xAlphas) {
|
|
11
19
|
ctx.save();
|
|
12
20
|
ctx.strokeStyle = color;
|
|
13
21
|
ctx.lineWidth = 1;
|
|
14
22
|
ctx.setLineDash([...dash]);
|
|
15
23
|
ctx.beginPath();
|
|
16
|
-
for (
|
|
17
|
-
|
|
24
|
+
for (let i = 0; i < xTicks.length; i++) {
|
|
25
|
+
if (xAlphas !== undefined && (xAlphas[i] ?? 1) < 1)
|
|
26
|
+
continue;
|
|
27
|
+
const px = Math.round(xTicks[i]) + 0.5;
|
|
18
28
|
ctx.moveTo(px, 0);
|
|
19
29
|
ctx.lineTo(px, height);
|
|
20
30
|
}
|
|
@@ -24,6 +34,19 @@ export function drawGrid(ctx, xTicks, yTicks, width, height, color, dash) {
|
|
|
24
34
|
ctx.lineTo(width, py);
|
|
25
35
|
}
|
|
26
36
|
ctx.stroke();
|
|
37
|
+
if (xAlphas !== undefined) {
|
|
38
|
+
for (let i = 0; i < xTicks.length; i++) {
|
|
39
|
+
const a = xAlphas[i] ?? 1;
|
|
40
|
+
if (a >= 1 || a <= 0.02)
|
|
41
|
+
continue;
|
|
42
|
+
ctx.globalAlpha = a;
|
|
43
|
+
const px = Math.round(xTicks[i]) + 0.5;
|
|
44
|
+
ctx.beginPath();
|
|
45
|
+
ctx.moveTo(px, 0);
|
|
46
|
+
ctx.lineTo(px, height);
|
|
47
|
+
ctx.stroke();
|
|
48
|
+
}
|
|
49
|
+
}
|
|
27
50
|
ctx.restore();
|
|
28
51
|
}
|
|
29
52
|
/**
|
|
@@ -45,21 +68,66 @@ export function thinPixels(xs, minGap) {
|
|
|
45
68
|
* the plot height. Drawn a touch stronger than the dashed gridlines (solid, so a
|
|
46
69
|
* session/day boundary reads as structural, not just another tick) at the
|
|
47
70
|
* discontinuity provider's collapse points (see `DiscontinuityProvider.boundaries`).
|
|
71
|
+
*
|
|
72
|
+
* `alphas` (parallel to `xs`) fades individual lines — used by the `'all'`
|
|
73
|
+
* session-divider mode so crowding lines dim smoothly toward invisible instead
|
|
74
|
+
* of popping in/out with a hard density cutoff. Omitted ⇒ every line at full
|
|
75
|
+
* opacity in a single path (the fast default). A near-zero alpha is skipped.
|
|
48
76
|
*/
|
|
49
|
-
export function drawDividers(ctx, xs, height, color) {
|
|
77
|
+
export function drawDividers(ctx, xs, height, color, alphas) {
|
|
50
78
|
if (xs.length === 0)
|
|
51
79
|
return;
|
|
52
80
|
ctx.save();
|
|
53
81
|
ctx.strokeStyle = color;
|
|
54
82
|
ctx.lineWidth = 1;
|
|
55
83
|
ctx.setLineDash([]);
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
84
|
+
if (alphas === undefined) {
|
|
85
|
+
ctx.beginPath();
|
|
86
|
+
for (const x of xs) {
|
|
87
|
+
const px = Math.round(x) + 0.5;
|
|
88
|
+
ctx.moveTo(px, 0);
|
|
89
|
+
ctx.lineTo(px, height);
|
|
90
|
+
}
|
|
91
|
+
ctx.stroke();
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
// Per-line opacity → each line is its own path (globalAlpha can't vary
|
|
95
|
+
// within one stroke). Dividers draw at base opacity 1 (nothing sets it
|
|
96
|
+
// before this pass), so set the line alpha directly; `restore` resets it.
|
|
97
|
+
for (let i = 0; i < xs.length; i++) {
|
|
98
|
+
const a = alphas[i] ?? 1;
|
|
99
|
+
if (a <= 0.02)
|
|
100
|
+
continue;
|
|
101
|
+
ctx.globalAlpha = a;
|
|
102
|
+
const px = Math.round(xs[i]) + 0.5;
|
|
103
|
+
ctx.beginPath();
|
|
104
|
+
ctx.moveTo(px, 0);
|
|
105
|
+
ctx.lineTo(px, height);
|
|
106
|
+
ctx.stroke();
|
|
107
|
+
}
|
|
61
108
|
}
|
|
62
|
-
ctx.stroke();
|
|
63
109
|
ctx.restore();
|
|
64
110
|
}
|
|
111
|
+
/**
|
|
112
|
+
* Per-line opacity for {@link drawDividers} in `'all'` mode: each line keys off
|
|
113
|
+
* the gap to its nearest neighbour — full at `fullPx`+, **zero** at `gonePx`
|
|
114
|
+
* and below, a quadratic ramp between. So as a zoom-out crowds the session
|
|
115
|
+
* lines they dim toward a clean plot — no hard drop that pops on pan.
|
|
116
|
+
*
|
|
117
|
+
* The curve must fall **superlinearly** in the gap: the veil a reader sees is
|
|
118
|
+
* `alpha × density = alpha / gap`, so the earlier linear ramp (`alpha = gap/f`)
|
|
119
|
+
* cancelled the density growth exactly and pinned a constant gray wash over the
|
|
120
|
+
* whole plot no matter how far out you zoomed. Quadratic-to-a-floor makes the
|
|
121
|
+
* wash itself → 0 as lines converge: alpha `t²` with
|
|
122
|
+
* `t = (gap − gonePx) / (fullPx − gonePx)`. `xs` ascending.
|
|
123
|
+
*/
|
|
124
|
+
export function dividerAlphas(xs, gonePx, fullPx) {
|
|
125
|
+
return xs.map((x, i) => {
|
|
126
|
+
const left = i > 0 ? x - xs[i - 1] : Infinity;
|
|
127
|
+
const right = i < xs.length - 1 ? xs[i + 1] - x : Infinity;
|
|
128
|
+
const gap = Math.min(left, right);
|
|
129
|
+
const t = Math.max(0, Math.min(1, (gap - gonePx) / (fullPx - gonePx)));
|
|
130
|
+
return t * t;
|
|
131
|
+
});
|
|
132
|
+
}
|
|
65
133
|
//# sourceMappingURL=grid.js.map
|
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';
|
package/dist/theme.d.ts
CHANGED
|
@@ -111,6 +111,19 @@ export interface ChartTheme {
|
|
|
111
111
|
* boundary reads as structural.
|
|
112
112
|
*/
|
|
113
113
|
readonly sessionDivider?: string;
|
|
114
|
+
/**
|
|
115
|
+
* The stacked date style's **band** row (the segmented second row: zebra
|
|
116
|
+
* date/month/year cells with left-aligned labels + dividers). `fill` is the
|
|
117
|
+
* shaded (odd-parity) cell background — "could be a background color" per
|
|
118
|
+
* the design; `divider` the turn line (falls back to {@link grid}); `label`
|
|
119
|
+
* the band-label ink (falls back to {@link title}.color → {@link label}).
|
|
120
|
+
* Optional; the whole band row is stacked-only.
|
|
121
|
+
*/
|
|
122
|
+
readonly band?: {
|
|
123
|
+
readonly fill: string;
|
|
124
|
+
readonly divider?: string;
|
|
125
|
+
readonly label?: string;
|
|
126
|
+
};
|
|
114
127
|
/**
|
|
115
128
|
* Typography for the axis **title** — the rotated y-axis unit strip and the
|
|
116
129
|
* x-axis label (distinct from the per-tick `label` colour above). Omit a
|
package/dist/theme.js
CHANGED
|
@@ -112,6 +112,11 @@ export const defaultTheme = {
|
|
|
112
112
|
grid: '#e2e8f0',
|
|
113
113
|
gridDash: [2, 2],
|
|
114
114
|
sessionDivider: '#cbd5e1', // slate-300 — a step stronger than the gridlines
|
|
115
|
+
band: {
|
|
116
|
+
fill: '#f8fafc', // slate-50 — the zebra shade on the stacked band row
|
|
117
|
+
divider: '#cbd5e1', // slate-300 turn line
|
|
118
|
+
label: '#334155', // slate-700 ink for band labels
|
|
119
|
+
},
|
|
115
120
|
},
|
|
116
121
|
font: {
|
|
117
122
|
family: 'system-ui, -apple-system, sans-serif',
|