@pond-ts/charts 0.71.0 → 0.73.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/API.md +5 -4
- package/CHANGELOG.md +334 -5
- package/README.md +11 -0
- package/dist/AreaChart.d.ts +35 -12
- package/dist/AreaChart.js +28 -13
- package/dist/BandChart.d.ts +9 -1
- package/dist/BandChart.js +5 -1
- package/dist/ChartRow.js +2 -2
- package/dist/LineChart.d.ts +25 -1
- package/dist/LineChart.js +5 -1
- package/dist/XAxis.d.ts +70 -3
- package/dist/XAxis.js +187 -35
- package/dist/area.d.ts +1 -13
- package/dist/area.js +65 -69
- package/dist/bars.d.ts +7 -6
- package/dist/bars.js +7 -6
- package/dist/context.d.ts +2 -7
- package/dist/line.d.ts +14 -0
- package/dist/line.js +47 -7
- package/dist/tradingTimeScale.d.ts +7 -1
- package/dist/tradingTimeScale.js +3 -2
- package/dist/x-offset.d.ts +20 -0
- package/dist/x-offset.js +23 -0
- package/package.json +3 -3
package/dist/BandChart.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ import type { SeriesSchema, TimeSeries, ValueSeriesSchema } from 'pond-ts';
|
|
|
3
3
|
import type { NumericColumn, ValueNumericColumn } from './column-names.js';
|
|
4
4
|
import { type Curve } from './curve.js';
|
|
5
5
|
import type { DecimateOption } from './decimate.js';
|
|
6
|
+
import { type XOffset } from './x-offset.js';
|
|
6
7
|
export interface BandChartCommon<S extends SeriesSchema = SeriesSchema, VS extends ValueSeriesSchema = ValueSeriesSchema> {
|
|
7
8
|
/**
|
|
8
9
|
* The band's semantic identifier — what the spread _is_ (e.g. `outer` for a
|
|
@@ -44,6 +45,13 @@ export interface BandChartCommon<S extends SeriesSchema = SeriesSchema, VS exten
|
|
|
44
45
|
* `boundaries`.
|
|
45
46
|
*/
|
|
46
47
|
sessionBreaks?: boolean;
|
|
48
|
+
/**
|
|
49
|
+
* Draw each row this long **after its key** — milliseconds or a duration
|
|
50
|
+
* string. **Omitted ⇒ `0`**. Time series only. Same knob as
|
|
51
|
+
* {@link LineChartCommon.xOffset}, so a band and the line it pairs with
|
|
52
|
+
* move together.
|
|
53
|
+
*/
|
|
54
|
+
xOffset?: XOffset;
|
|
47
55
|
/**
|
|
48
56
|
* **M4 viewport decimation** (charts decimator wave). **Omitted ⇒ `true`**:
|
|
49
57
|
* once the visible envelope is denser than ~2 samples per device pixel, it is
|
|
@@ -111,6 +119,6 @@ export type BandChartProps<S extends SeriesSchema = SeriesSchema, VS extends Val
|
|
|
111
119
|
* </Layers>
|
|
112
120
|
* ```
|
|
113
121
|
*/
|
|
114
|
-
export declare function BandChart<S extends SeriesSchema = SeriesSchema, VS extends ValueSeriesSchema = ValueSeriesSchema>({ series, lower, upper, as: semantic, axis, curve, sessionBreaks, decimate, legend, index, }: BandChartProps<S, VS>): null;
|
|
122
|
+
export declare function BandChart<S extends SeriesSchema = SeriesSchema, VS extends ValueSeriesSchema = ValueSeriesSchema>({ series: source, lower, upper, as: semantic, axis, curve, sessionBreaks, decimate, legend, xOffset, index, }: BandChartProps<S, VS>): null;
|
|
115
123
|
export {};
|
|
116
124
|
//# sourceMappingURL=BandChart.d.ts.map
|
package/dist/BandChart.js
CHANGED
|
@@ -6,6 +6,7 @@ import { resolveCurve } from './curve.js';
|
|
|
6
6
|
import { ContainerContext, LayersContext } from './context.js';
|
|
7
7
|
import { legendLabelFor, useLegendItems, } from './swatch.js';
|
|
8
8
|
import { useSlotKey } from './use-slot-key.js';
|
|
9
|
+
import { useXOffset } from './x-offset.js';
|
|
9
10
|
/** Stable empty boundary list — so `sessionBreaks={false}` keeps a referentially
|
|
10
11
|
* constant array and the layer entry isn't rebuilt every render. */
|
|
11
12
|
const NO_BREAKS = [];
|
|
@@ -26,7 +27,7 @@ const NO_BREAKS = [];
|
|
|
26
27
|
* </Layers>
|
|
27
28
|
* ```
|
|
28
29
|
*/
|
|
29
|
-
export function BandChart({ series, lower, upper, as: semantic, axis, curve, sessionBreaks = false, decimate = true, legend, index = 0, }) {
|
|
30
|
+
export function BandChart({ series: source, lower, upper, as: semantic, axis, curve, sessionBreaks = false, decimate = true, legend, xOffset, index = 0, }) {
|
|
30
31
|
const container = useContext(ContainerContext);
|
|
31
32
|
if (container === null) {
|
|
32
33
|
throw new Error('<BandChart> must be rendered inside a <ChartContainer>');
|
|
@@ -35,6 +36,9 @@ export function BandChart({ series, lower, upper, as: semantic, axis, curve, ses
|
|
|
35
36
|
if (layers === null) {
|
|
36
37
|
throw new Error('<BandChart> must be rendered inside a <Layers>');
|
|
37
38
|
}
|
|
39
|
+
// The series as drawn: moved by `xOffset` (the source itself when there is
|
|
40
|
+
// none), so every read below shares one axis.
|
|
41
|
+
const series = useXOffset(source, xOffset, 'BandChart');
|
|
38
42
|
const bs = useMemo(() => series instanceof ValueSeries
|
|
39
43
|
? bandFromValueSeries(series, lower, upper)
|
|
40
44
|
: bandFromTimeSeries(series, lower, upper), [series, lower, upper]);
|
package/dist/ChartRow.js
CHANGED
|
@@ -365,7 +365,7 @@ export function ChartRow({ height: heightProp, flex, children, }) {
|
|
|
365
365
|
const extents = needsExtents(ax)
|
|
366
366
|
? layerList
|
|
367
367
|
.filter((entry) => (entry.axisId ?? defaultAxisId) === ax.id)
|
|
368
|
-
.map((entry) => entry.layer.yExtent(
|
|
368
|
+
.map((entry) => entry.layer.yExtent())
|
|
369
369
|
: [];
|
|
370
370
|
const [lo, hi] = resolveYDomain(ax.min, ax.max, extents, ax.pad, ax.scale);
|
|
371
371
|
// Reserve a header band at the top when any axis draws a `'top'` title,
|
|
@@ -470,7 +470,7 @@ export function ChartRow({ height: heightProp, flex, children, }) {
|
|
|
470
470
|
// about.
|
|
471
471
|
const message = logAxisWarning(ax, layerList
|
|
472
472
|
.filter((entry) => (entry.axisId ?? defaultAxisId) === ax.id)
|
|
473
|
-
.map((entry) => entry.layer.yExtent(
|
|
473
|
+
.map((entry) => entry.layer.yExtent()));
|
|
474
474
|
if (message === null)
|
|
475
475
|
warned.delete(ax.id);
|
|
476
476
|
else if (warned.get(ax.id) !== message) {
|
package/dist/LineChart.d.ts
CHANGED
|
@@ -4,6 +4,7 @@ import type { NumericColumn, ValueNumericColumn } from './column-names.js';
|
|
|
4
4
|
import type { DecimateOption } from './decimate.js';
|
|
5
5
|
import { type Curve } from './curve.js';
|
|
6
6
|
import { type GapMode } from './gaps.js';
|
|
7
|
+
import { type XOffset } from './x-offset.js';
|
|
7
8
|
export interface LineChartCommon<S extends SeriesSchema = SeriesSchema, VS extends ValueSeriesSchema = ValueSeriesSchema> {
|
|
8
9
|
/**
|
|
9
10
|
* The series' semantic identifier — what the data _is_ / how it should read
|
|
@@ -68,6 +69,29 @@ export interface LineChartCommon<S extends SeriesSchema = SeriesSchema, VS exten
|
|
|
68
69
|
* no-op on a continuous axis (no provider) or a provider without `boundaries`.
|
|
69
70
|
*/
|
|
70
71
|
sessionBreaks?: boolean;
|
|
72
|
+
/**
|
|
73
|
+
* Draw each row this long **after its key** — milliseconds or a duration
|
|
74
|
+
* string (`'1m'`, `'-30s'`). **Omitted ⇒ `0`**, drawn at the key. Time
|
|
75
|
+
* series only (throws on a `ValueSeries`, which has no time to move).
|
|
76
|
+
*
|
|
77
|
+
* For a value stamped at one instant that belongs at another: a 1-minute
|
|
78
|
+
* bar is keyed at its open, but its `close` is the price at the bar's
|
|
79
|
+
* **end**, so `xOffset="1m"` puts the close line where it happened — and the
|
|
80
|
+
* session's last close lands on the close, not a bar early. The series you
|
|
81
|
+
* pass is left as it is, so a `<Candlestick>` drawn from it still draws at
|
|
82
|
+
* the key; the layer moves only its own copy of the key (one pass, value
|
|
83
|
+
* columns shared).
|
|
84
|
+
*
|
|
85
|
+
* On a trading axis, an offset larger than a bar can carry a row out of its
|
|
86
|
+
* session into closed time — e.g. a negative offset on bars keyed at their
|
|
87
|
+
* open moves each session's first bar into the previous night. Keep it within
|
|
88
|
+
* the bar the value belongs to.
|
|
89
|
+
*
|
|
90
|
+
* Everything this layer reports is in **drawn** time: its auto-fit extent,
|
|
91
|
+
* the tracker dot, `sessionBreaks`, a sweep's span. To move the data itself,
|
|
92
|
+
* use pond's `series.offsetTime(by)`.
|
|
93
|
+
*/
|
|
94
|
+
xOffset?: XOffset;
|
|
71
95
|
/**
|
|
72
96
|
* **M4 viewport decimation** (charts decimator wave). **Omitted ⇒ `true`**:
|
|
73
97
|
* once the visible data is denser than ~2 samples per device pixel, the line
|
|
@@ -124,6 +148,6 @@ export type LineChartProps<S extends SeriesSchema = SeriesSchema, VS extends Val
|
|
|
124
148
|
* (scaling against its `axis`), and renders nothing to the DOM — the row draws
|
|
125
149
|
* it. The line breaks at gaps rather than spanning them.
|
|
126
150
|
*/
|
|
127
|
-
export declare function LineChart<S extends SeriesSchema = SeriesSchema, VS extends ValueSeriesSchema = ValueSeriesSchema>({ series, column, readout, as: semantic, axis, id, curve, gaps, sessionBreaks, decimate, legend, index, }: LineChartProps<S, VS>): null;
|
|
151
|
+
export declare function LineChart<S extends SeriesSchema = SeriesSchema, VS extends ValueSeriesSchema = ValueSeriesSchema>({ series: source, column, readout, as: semantic, axis, id, curve, gaps, sessionBreaks, decimate, legend, xOffset, index, }: LineChartProps<S, VS>): null;
|
|
128
152
|
export {};
|
|
129
153
|
//# sourceMappingURL=LineChart.d.ts.map
|
package/dist/LineChart.js
CHANGED
|
@@ -8,6 +8,7 @@ import { DEFAULT_GAP_MODE, DEFAULT_GAP_CONNECTOR_OPACITY, } from './gaps.js';
|
|
|
8
8
|
import { ContainerContext, LayersContext, } from './context.js';
|
|
9
9
|
import { legendLabelFor, useLegendItems, } from './swatch.js';
|
|
10
10
|
import { useSlotKey } from './use-slot-key.js';
|
|
11
|
+
import { useXOffset } from './x-offset.js';
|
|
11
12
|
/** Stable empty boundary list — so `sessionBreaks={false}` keeps a referentially
|
|
12
13
|
* constant array and the layer entry isn't rebuilt every render. */
|
|
13
14
|
const NO_BREAKS = [];
|
|
@@ -17,7 +18,7 @@ const NO_BREAKS = [];
|
|
|
17
18
|
* (scaling against its `axis`), and renders nothing to the DOM — the row draws
|
|
18
19
|
* it. The line breaks at gaps rather than spanning them.
|
|
19
20
|
*/
|
|
20
|
-
export function LineChart({ series, column, readout, as: semantic, axis, id, curve, gaps = DEFAULT_GAP_MODE, sessionBreaks = false, decimate = true, legend, index = 0, }) {
|
|
21
|
+
export function LineChart({ series: source, column, readout, as: semantic, axis, id, curve, gaps = DEFAULT_GAP_MODE, sessionBreaks = false, decimate = true, legend, xOffset, index = 0, }) {
|
|
21
22
|
const container = useContext(ContainerContext);
|
|
22
23
|
if (container === null) {
|
|
23
24
|
throw new Error('<LineChart> must be rendered inside a <ChartContainer>');
|
|
@@ -26,6 +27,9 @@ export function LineChart({ series, column, readout, as: semantic, axis, id, cur
|
|
|
26
27
|
if (layers === null) {
|
|
27
28
|
throw new Error('<LineChart> must be rendered inside a <Layers>');
|
|
28
29
|
}
|
|
30
|
+
// The series as drawn: moved by `xOffset` (the source itself when there is
|
|
31
|
+
// none), so every read below shares one axis.
|
|
32
|
+
const series = useXOffset(source, xOffset, 'LineChart');
|
|
29
33
|
const cs = useMemo(() => series instanceof ValueSeries
|
|
30
34
|
? fromValueSeries(series, column)
|
|
31
35
|
: fromTimeSeries(series, column), [series, column]);
|
package/dist/XAxis.d.ts
CHANGED
|
@@ -10,6 +10,18 @@ interface PlacedTick {
|
|
|
10
10
|
* renders emphasized (bold): a band turn in stacked, an inline promotion in
|
|
11
11
|
* flat — the same boundaries in both styles. */
|
|
12
12
|
readonly bold?: boolean;
|
|
13
|
+
/** A session open on a trading axis — live time starts here after a
|
|
14
|
+
* collapsed gap. Outranks a plain clock tick when labels collide. */
|
|
15
|
+
readonly seam?: boolean;
|
|
16
|
+
/** An instant inside a collapsed gap (closed-market time an elapsed axis's
|
|
17
|
+
* wall-clock steps can land on): it sits on the seam pixel but names a
|
|
18
|
+
* moment the axis doesn't draw, so any live tick outranks it. */
|
|
19
|
+
readonly dead?: boolean;
|
|
20
|
+
/** Under `align="auto"`, an automatic time tick that is the first
|
|
21
|
+
* (`'start'`) or last (`'end'`) of the axis's **full** tick list: its label
|
|
22
|
+
* anchors inward. Fixed per tick rather than per drawn set, so dropping a
|
|
23
|
+
* colliding neighbour never re-anchors another label into a new collision. */
|
|
24
|
+
readonly anchor?: 'start' | 'end' | undefined;
|
|
13
25
|
}
|
|
14
26
|
/**
|
|
15
27
|
* Thin + truncate a **category** axis's labels so a dense axis stays legible
|
|
@@ -25,6 +37,53 @@ interface PlacedTick {
|
|
|
25
37
|
* Exported for tests only — not re-exported from the package index.
|
|
26
38
|
*/
|
|
27
39
|
export declare function thinCategoryLabels(ticks: readonly PlacedTick[], slot: number, plotWidth: number, fontSize: number, fontFamily: string): PlacedTick[];
|
|
40
|
+
/**
|
|
41
|
+
* Which **time** ticks keep their labels: the indices of `ticks` whose drawn
|
|
42
|
+
* extents (per `align`) clear one another by {@link LABEL_GAP}. The tick ladder
|
|
43
|
+
* spaces its clock steps by the grain, but it places every session open
|
|
44
|
+
* unconditionally, so two anchors can land closer than a label is wide — a
|
|
45
|
+
* micro-session (one stray pre-market bar in a calendar derived from bars)
|
|
46
|
+
* puts its open a pixel left of the real open, and a seam can sit just past
|
|
47
|
+
* the last clock tick before it.
|
|
48
|
+
*
|
|
49
|
+
* Greedy in **priority** order — each tick is kept unless it would overlap one
|
|
50
|
+
* already kept, so within a pass a tick is only ever dropped by a survivor:
|
|
51
|
+
*
|
|
52
|
+
* 1. a **period turn** (`bold` — the label carrying the date / month / year,
|
|
53
|
+
* or a stacked band turn) before a **session open** (`seam`), before a plain
|
|
54
|
+
* clock tick, before a tick in **closed time** (`dead`) — a date stays on
|
|
55
|
+
* the turn it marks, and the seam is labelled with live time;
|
|
56
|
+
* 2. between two turns, or two session opens, the **later** (it owns the
|
|
57
|
+
* ticks that follow); between two plain ticks the earlier.
|
|
58
|
+
*
|
|
59
|
+
* A blank label (a custom `format` returning `''` to thin its own labels)
|
|
60
|
+
* draws nothing, so it is always kept and never blocks another.
|
|
61
|
+
*
|
|
62
|
+
* Under `'auto'` an edge label's extent follows its fixed {@link
|
|
63
|
+
* PlacedTick.anchor}, never its position in the drawn set — so no drop moves
|
|
64
|
+
* another label. The caller re-labels the survivors (see {@link fitTimeTicks}):
|
|
65
|
+
* a turn dropped beside a tick of a later, different period hands its period
|
|
66
|
+
* on (a stray New Year's open's `2027` moves onto the next real open). Two
|
|
67
|
+
* turns of the same level too close to both draw can't both be drawn; the
|
|
68
|
+
* later one is.
|
|
69
|
+
*
|
|
70
|
+
* Exported for tests only — not re-exported from the package index.
|
|
71
|
+
*/
|
|
72
|
+
export declare function fitTimeLabels(ticks: readonly PlacedTick[], align: 'auto' | 'center' | 'right', fontSize: number, fontFamily: string): number[];
|
|
73
|
+
/**
|
|
74
|
+
* Place a time axis's automatic ticks with no label overprinting another:
|
|
75
|
+
* `place` labels a set of tick instants (flat promotions walk only the
|
|
76
|
+
* instants it is given), {@link fitTimeLabels} keeps the ones that clear,
|
|
77
|
+
* and the survivors are placed again — re-labelling can promote a survivor
|
|
78
|
+
* and change its width — until a pass drops nothing (each pass only removes,
|
|
79
|
+
* so it terminates). A re-label can also *narrow* a survivor (`Jan 2` becomes
|
|
80
|
+
* `2026` once the stub that turned the year is dropped), so each dropped tick
|
|
81
|
+
* is then offered back in time order and re-admitted if the whole set, placed
|
|
82
|
+
* again, still fits.
|
|
83
|
+
*
|
|
84
|
+
* Exported for tests only — not re-exported from the package index.
|
|
85
|
+
*/
|
|
86
|
+
export declare function fitTimeTicks(values: readonly number[], place: (values: readonly number[]) => PlacedTick[], align: 'auto' | 'center' | 'right', fontSize: number, fontFamily: string): PlacedTick[];
|
|
28
87
|
export interface XAxisProps {
|
|
29
88
|
/**
|
|
30
89
|
* Tick / cursor value formatting — a d3 format/time specifier string or a
|
|
@@ -85,9 +144,17 @@ export interface XAxisProps {
|
|
|
85
144
|
* right-anchors so the edge labels stay inside the plot (the old default).
|
|
86
145
|
* - `'right'` — the label sits to the **right** of an extended tick that
|
|
87
146
|
* drops from the axis line (label beside the tick, not under it) — a
|
|
88
|
-
* *style* choice (the TradingView look)
|
|
89
|
-
*
|
|
90
|
-
*
|
|
147
|
+
* *style* choice (the TradingView look), **not** a remedy for colliding
|
|
148
|
+
* labels. Collisions are handled by measurement, in every mode, on a
|
|
149
|
+
* category axis (thin + middle-ellipsize) and on a time axis's automatic
|
|
150
|
+
* ticks (the label that would overprint is dropped — a date / month /
|
|
151
|
+
* year turn outlasts a clock label — and the survivors are re-labelled so
|
|
152
|
+
* the dates they show stay correct; a blank label never blocks one).
|
|
153
|
+
* Under `'auto'` the edge labels anchor inward, so a crowded edge costs a
|
|
154
|
+
* label — which one follows the same priority (between two plain clock
|
|
155
|
+
* labels, the inner neighbour at the left edge and the edge label itself
|
|
156
|
+
* at the right). Explicit `ticks`, `transform` ticks and numeric axes are
|
|
157
|
+
* not fitted.
|
|
91
158
|
*/
|
|
92
159
|
align?: 'auto' | 'center' | 'right';
|
|
93
160
|
/**
|
package/dist/XAxis.js
CHANGED
|
@@ -151,6 +151,113 @@ export function thinCategoryLabels(ticks, slot, plotWidth, fontSize, fontFamily)
|
|
|
151
151
|
}
|
|
152
152
|
return out;
|
|
153
153
|
}
|
|
154
|
+
/**
|
|
155
|
+
* Which **time** ticks keep their labels: the indices of `ticks` whose drawn
|
|
156
|
+
* extents (per `align`) clear one another by {@link LABEL_GAP}. The tick ladder
|
|
157
|
+
* spaces its clock steps by the grain, but it places every session open
|
|
158
|
+
* unconditionally, so two anchors can land closer than a label is wide — a
|
|
159
|
+
* micro-session (one stray pre-market bar in a calendar derived from bars)
|
|
160
|
+
* puts its open a pixel left of the real open, and a seam can sit just past
|
|
161
|
+
* the last clock tick before it.
|
|
162
|
+
*
|
|
163
|
+
* Greedy in **priority** order — each tick is kept unless it would overlap one
|
|
164
|
+
* already kept, so within a pass a tick is only ever dropped by a survivor:
|
|
165
|
+
*
|
|
166
|
+
* 1. a **period turn** (`bold` — the label carrying the date / month / year,
|
|
167
|
+
* or a stacked band turn) before a **session open** (`seam`), before a plain
|
|
168
|
+
* clock tick, before a tick in **closed time** (`dead`) — a date stays on
|
|
169
|
+
* the turn it marks, and the seam is labelled with live time;
|
|
170
|
+
* 2. between two turns, or two session opens, the **later** (it owns the
|
|
171
|
+
* ticks that follow); between two plain ticks the earlier.
|
|
172
|
+
*
|
|
173
|
+
* A blank label (a custom `format` returning `''` to thin its own labels)
|
|
174
|
+
* draws nothing, so it is always kept and never blocks another.
|
|
175
|
+
*
|
|
176
|
+
* Under `'auto'` an edge label's extent follows its fixed {@link
|
|
177
|
+
* PlacedTick.anchor}, never its position in the drawn set — so no drop moves
|
|
178
|
+
* another label. The caller re-labels the survivors (see {@link fitTimeTicks}):
|
|
179
|
+
* a turn dropped beside a tick of a later, different period hands its period
|
|
180
|
+
* on (a stray New Year's open's `2027` moves onto the next real open). Two
|
|
181
|
+
* turns of the same level too close to both draw can't both be drawn; the
|
|
182
|
+
* later one is.
|
|
183
|
+
*
|
|
184
|
+
* Exported for tests only — not re-exported from the package index.
|
|
185
|
+
*/
|
|
186
|
+
export function fitTimeLabels(ticks, align, fontSize, fontFamily) {
|
|
187
|
+
const extents = ticks.map((t) => {
|
|
188
|
+
const font = `${t.bold ? '700 ' : ''}${fontSize}px ${fontFamily}`;
|
|
189
|
+
const w = labelWidth(t.label, font, fontSize);
|
|
190
|
+
// Mirrors the render's placement (`labelLeft` + `labelTransform`).
|
|
191
|
+
if (align === 'right')
|
|
192
|
+
return [t.x + 4, t.x + 4 + w];
|
|
193
|
+
if (align === 'auto' && t.anchor === 'start')
|
|
194
|
+
return [t.x, t.x + w];
|
|
195
|
+
if (align === 'auto' && t.anchor === 'end')
|
|
196
|
+
return [t.x - w, t.x];
|
|
197
|
+
return [t.x - w / 2, t.x + w / 2];
|
|
198
|
+
});
|
|
199
|
+
const blank = (i) => ticks[i].label.trim() === '';
|
|
200
|
+
const rank = (t) => (t.dead ? -1 : t.bold ? 2 : t.seam ? 1 : 0);
|
|
201
|
+
const order = ticks
|
|
202
|
+
.map((_, i) => i)
|
|
203
|
+
.sort((i, j) => {
|
|
204
|
+
const ri = rank(ticks[i]);
|
|
205
|
+
const rj = rank(ticks[j]);
|
|
206
|
+
if (ri !== rj)
|
|
207
|
+
return rj - ri;
|
|
208
|
+
return ri > 0 ? j - i : i - j;
|
|
209
|
+
});
|
|
210
|
+
const kept = [];
|
|
211
|
+
for (const i of order) {
|
|
212
|
+
const [l, r] = extents[i];
|
|
213
|
+
const clear = blank(i) ||
|
|
214
|
+
kept.every((j) => {
|
|
215
|
+
const [kl, kr] = extents[j];
|
|
216
|
+
return blank(j) || r + LABEL_GAP <= kl || kr + LABEL_GAP <= l;
|
|
217
|
+
});
|
|
218
|
+
if (clear)
|
|
219
|
+
kept.push(i);
|
|
220
|
+
}
|
|
221
|
+
return kept.sort((i, j) => i - j);
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Place a time axis's automatic ticks with no label overprinting another:
|
|
225
|
+
* `place` labels a set of tick instants (flat promotions walk only the
|
|
226
|
+
* instants it is given), {@link fitTimeLabels} keeps the ones that clear,
|
|
227
|
+
* and the survivors are placed again — re-labelling can promote a survivor
|
|
228
|
+
* and change its width — until a pass drops nothing (each pass only removes,
|
|
229
|
+
* so it terminates). A re-label can also *narrow* a survivor (`Jan 2` becomes
|
|
230
|
+
* `2026` once the stub that turned the year is dropped), so each dropped tick
|
|
231
|
+
* is then offered back in time order and re-admitted if the whole set, placed
|
|
232
|
+
* again, still fits.
|
|
233
|
+
*
|
|
234
|
+
* Exported for tests only — not re-exported from the package index.
|
|
235
|
+
*/
|
|
236
|
+
export function fitTimeTicks(values, place, align, fontSize, fontFamily) {
|
|
237
|
+
const fits = (placed) => fitTimeLabels(placed, align, fontSize, fontFamily);
|
|
238
|
+
let cur = values;
|
|
239
|
+
let placed = place(cur);
|
|
240
|
+
for (let kept = fits(placed); kept.length < placed.length;) {
|
|
241
|
+
cur = kept.map((i) => cur[i]);
|
|
242
|
+
placed = place(cur);
|
|
243
|
+
kept = fits(placed);
|
|
244
|
+
}
|
|
245
|
+
if (cur.length === values.length)
|
|
246
|
+
return placed;
|
|
247
|
+
const drawn = new Set(cur);
|
|
248
|
+
for (const v of values) {
|
|
249
|
+
if (drawn.has(v))
|
|
250
|
+
continue;
|
|
251
|
+
const trial = [...cur, v].sort((a, b) => a - b);
|
|
252
|
+
const again = place(trial);
|
|
253
|
+
if (fits(again).length === again.length) {
|
|
254
|
+
cur = trial;
|
|
255
|
+
placed = again;
|
|
256
|
+
drawn.add(v);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
return placed;
|
|
260
|
+
}
|
|
154
261
|
/**
|
|
155
262
|
* The shared **x axis**, a sibling of {@link YAxis} for the horizontal axis. A
|
|
156
263
|
* child of {@link ChartContainer}, rendered as DOM chrome (crisp text,
|
|
@@ -399,38 +506,79 @@ export function XAxis({ format, label, side = 'bottom', height, ticks: customTic
|
|
|
399
506
|
}
|
|
400
507
|
return out;
|
|
401
508
|
};
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
509
|
+
// The automatic tick instants (none when `ticks` or a `transform` lays the
|
|
510
|
+
// axis out). `tickValues`, not `xScale.ticks` — d3's raw `scaleLog.ticks()`
|
|
511
|
+
// is nearly a step function (see `yticks.ts`), so a log x needs the same
|
|
512
|
+
// decade ladder the y axis already builds. For a time or linear scale it
|
|
513
|
+
// defers to `scale.ticks(count)`, so this is a no-op there.
|
|
514
|
+
const autoValues = customTicks !== undefined || derived !== null
|
|
515
|
+
? []
|
|
516
|
+
: tickValues(xScale, xTickCount).map((d) => +d);
|
|
517
|
+
// Place a set of automatic ticks: x, label, emphasis. The flat style labels
|
|
518
|
+
// only the ticks it is handed, so when the time-axis fit drops one, its
|
|
519
|
+
// period turn is promoted onto the next drawn tick instead of vanishing.
|
|
520
|
+
const disc = xKind === 'time' ? container.discontinuities : undefined;
|
|
521
|
+
const placeAuto = (values) => {
|
|
522
|
+
const flatDrawn = flatFmt !== undefined
|
|
523
|
+
? xScale.flatFormat(xTickCount, values)
|
|
524
|
+
: undefined;
|
|
525
|
+
const label = flatDrawn ?? tickFmt;
|
|
526
|
+
return values.map((v) => ({
|
|
527
|
+
x: xScale(v),
|
|
528
|
+
// A JS `format` may return null / a number (`AxisFormat` says string);
|
|
529
|
+
// the fit measures the text, so normalise it as the JSX always did.
|
|
530
|
+
label: String(label(v) ?? ''),
|
|
531
|
+
// A **period turn** renders emphasized (bold), consistently across
|
|
532
|
+
// styles: in stacked, a tick on a band divider (matched by pixel);
|
|
533
|
+
// in flat, a tick whose label was *promoted* to a coarser period
|
|
534
|
+
// (its flat label differs from the terse base) — the same boundaries,
|
|
535
|
+
// so `Feb` / `2026` read as strong in flat just as the band turns do.
|
|
536
|
+
bold: stacked
|
|
537
|
+
? dividerXs.has(Math.round(xScale(v)))
|
|
538
|
+
: flatDrawn !== undefined &&
|
|
539
|
+
baseFmt !== undefined &&
|
|
540
|
+
flatDrawn(v) !== baseFmt(v),
|
|
541
|
+
// Live from here, dead just before: a session open on a collapsed seam
|
|
542
|
+
// (the probe `Layers` uses for session dividers, plus the live side, so
|
|
543
|
+
// an instant inside the gap doesn't qualify).
|
|
544
|
+
seam: disc !== undefined &&
|
|
545
|
+
disc.distance(v - 1, v) <= 0 &&
|
|
546
|
+
disc.distance(v, v + 1) > 0,
|
|
547
|
+
dead: disc !== undefined && disc.distance(v, v + 1) <= 0,
|
|
548
|
+
// `auto` anchors the edge ticks of the FULL list, whichever survive.
|
|
549
|
+
anchor: align !== 'auto'
|
|
550
|
+
? undefined
|
|
551
|
+
: v === autoValues[0]
|
|
552
|
+
? 'start'
|
|
553
|
+
: v === autoValues[autoValues.length - 1]
|
|
554
|
+
? 'end'
|
|
555
|
+
: undefined,
|
|
556
|
+
}));
|
|
557
|
+
};
|
|
558
|
+
// A time axis's automatic ticks drop any that would overprint a neighbour
|
|
559
|
+
// (session opens can crowd — see `fitTimeTicks`), re-labelling the
|
|
560
|
+
// survivors. A category axis ticks once per category; thin + truncate its
|
|
561
|
+
// labels when they crowd. The slot is the scale's own band pitch — under
|
|
562
|
+
// `maxBandWidth` packing it is narrower than `plotWidth / n`, and the fit
|
|
563
|
+
// must measure against the pitch labels actually sit on. Explicit `ticks`
|
|
564
|
+
// stay verbatim; derived ticks have their own honesty pass.
|
|
565
|
+
const timeFitted = xKind === 'time' && customTicks === undefined && derived === null;
|
|
566
|
+
const placed = (() => {
|
|
567
|
+
if (timeFitted) {
|
|
568
|
+
return fitTimeTicks(autoValues, placeAuto, align, theme.font.size, theme.font.family);
|
|
569
|
+
}
|
|
570
|
+
const raw = customTicks
|
|
571
|
+
? customTicks.map((t) => ({ x: xScale(t.at), label: t.label }))
|
|
572
|
+
: derived !== null
|
|
573
|
+
? honestDerived()
|
|
574
|
+
: placeAuto(autoValues);
|
|
575
|
+
if (xKind !== 'category' || customTicks !== undefined || raw.length === 0) {
|
|
576
|
+
return raw;
|
|
577
|
+
}
|
|
578
|
+
return thinCategoryLabels(raw, 'bandwidth' in xScale
|
|
431
579
|
? xScale.bandwidth()
|
|
432
|
-
: plotWidth /
|
|
433
|
-
|
|
580
|
+
: plotWidth / raw.length, plotWidth, theme.font.size, theme.font.family);
|
|
581
|
+
})();
|
|
434
582
|
const onTop = side === 'top';
|
|
435
583
|
// Axis pills (marker / crosshair) sit at the same offset as the tick labels so
|
|
436
584
|
// they line up with their tick-label neighbours (matches `labelOffset` below).
|
|
@@ -545,11 +693,15 @@ export function XAxis({ format, label, side = 'bottom', height, ticks: customTic
|
|
|
545
693
|
fontSize: `${theme.font.size}px`,
|
|
546
694
|
color: color ?? theme.axis.label,
|
|
547
695
|
}, children: [placed.map((t, i) => {
|
|
548
|
-
const isFirst = i === 0;
|
|
549
|
-
const isLast = i === placed.length - 1;
|
|
550
696
|
// `center`: every label centred on its tick. `auto`: centred, but the
|
|
551
|
-
// edge labels end-align so they stay within [0, plotWidth]
|
|
552
|
-
//
|
|
697
|
+
// edge labels end-align so they stay within [0, plotWidth] — on a time
|
|
698
|
+
// axis the edges of the full tick list (`anchor`, which the label fit
|
|
699
|
+
// measured), elsewhere the first / last drawn. `right`: label
|
|
700
|
+
// left-anchored just past an extended tick (beside, not under).
|
|
701
|
+
const isFirst = timeFitted ? t.anchor === 'start' : i === 0;
|
|
702
|
+
const isLast = timeFitted
|
|
703
|
+
? t.anchor === 'end'
|
|
704
|
+
: i === placed.length - 1;
|
|
553
705
|
const labelTransform = align === 'right'
|
|
554
706
|
? 'none'
|
|
555
707
|
: align === 'auto' && isFirst
|
package/dist/area.d.ts
CHANGED
|
@@ -23,18 +23,6 @@ export declare function columnFiniteExtent(y: Float64Array, length: number): rea
|
|
|
23
23
|
* `xs`/`ys` are aligned index-for-index.
|
|
24
24
|
*/
|
|
25
25
|
export declare function fillAffineArea(ctx: CanvasRenderingContext2D, xs: Float64Array, ys: Float64Array, baselinePx: number, ax: Affine, ay: Affine): void;
|
|
26
|
-
/**
|
|
27
|
-
* The `[min, max]` vertical extent an area occupies — the finite values of
|
|
28
|
-
* `cs.y` widened to include `baseline`, since the fill spans from each value to
|
|
29
|
-
* the baseline (so the baseline must be in-domain or the fill clips). `null` if
|
|
30
|
-
* no value is finite. When `baseline` is `undefined` (`<AreaChart
|
|
31
|
-
* baseline="floor">`) the area rests on the axis's own lower bound (resolved
|
|
32
|
-
* later), so only the values constrain the
|
|
33
|
-
* domain — matching {@link yExtent}.
|
|
34
|
-
*
|
|
35
|
-
* NaN values (the gap signal) are ignored, so a coast doesn't drag the domain.
|
|
36
|
-
*/
|
|
37
|
-
export declare function areaExtent(cs: ChartSeries, baseline: number | undefined): [number, number] | null;
|
|
38
26
|
/**
|
|
39
27
|
* Fill the area between `cs`'s value line and a horizontal `baseline`, with a
|
|
40
28
|
* vertical gradient (most opaque at the line, fading to transparent at the
|
|
@@ -71,7 +59,7 @@ export declare function areaExtent(cs: ChartSeries, baseline: number | undefined
|
|
|
71
59
|
* bracketed by `save`/`restore` so they don't leak into later layers. Gap edges
|
|
72
60
|
* are collected by one O(N) walk ({@link collectGapEdges}).
|
|
73
61
|
*/
|
|
74
|
-
export declare function drawArea(ctx: CanvasRenderingContext2D, cs: ChartSeries, xScale: Scale, yScale: Scale, style: AreaStyle, baselineValue: number, curve?: CurveFactory, gaps?: GapMode, gapConnectorOpacity?: number, decimate?: DecimateOption, banding?: BandLadder): LayerDrawStats;
|
|
62
|
+
export declare function drawArea(ctx: CanvasRenderingContext2D, cs: ChartSeries, xScale: Scale, yScale: Scale, style: AreaStyle, baselineValue: number, curve?: CurveFactory, gaps?: GapMode, gapConnectorOpacity?: number, decimate?: DecimateOption, banding?: BandLadder, boundaries?: readonly number[]): LayerDrawStats;
|
|
75
63
|
/**
|
|
76
64
|
* The banded fill + stroke for `<AreaChart thresholds>` ([PND-BANDAREA]): one
|
|
77
65
|
* vertical **hard-stop gradient in pixel space**, a colour switch at every
|