@pond-ts/charts 0.58.0 → 0.59.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 +576 -0
- package/CHANGELOG.md +144 -1
- package/dist/BarChart.d.ts +56 -7
- package/dist/BarChart.js +82 -12
- package/dist/ChartRow.js +90 -2
- package/dist/YAxis.d.ts +58 -2
- package/dist/YAxis.js +3 -1
- package/dist/bars.d.ts +10 -3
- package/dist/bars.js +13 -9
- package/dist/context.d.ts +11 -6
- package/dist/data.d.ts +38 -0
- package/dist/data.js +43 -0
- package/dist/format.d.ts +15 -0
- package/dist/format.js +16 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +5 -0
- package/dist/range.d.ts +14 -1
- package/dist/range.js +24 -3
- package/dist/theme.d.ts +58 -4
- package/dist/yticks.d.ts +3 -0
- package/dist/yticks.js +104 -0
- package/package.json +6 -5
package/dist/YAxis.js
CHANGED
|
@@ -17,7 +17,7 @@ const DEFAULT_TICK_COUNT = 5;
|
|
|
17
17
|
* tick marks + labels from that scale. Charts attach via `<LineChart axis="id">`
|
|
18
18
|
* (default: the first axis).
|
|
19
19
|
*/
|
|
20
|
-
export function YAxis({ id, side = 'left', label, scale = 'linear', min, max, format, ticks, tickCount, pad = 0, boundaryLabels = true, width = DEFAULT_WIDTH, hide = false, labelPlacement = 'rotated', color, index = 0, }) {
|
|
20
|
+
export function YAxis({ id, side = 'left', label, scale = 'linear', linearWindow, min, max, format, ticks, tickCount, pad = 0, boundaryLabels = true, width = DEFAULT_WIDTH, hide = false, labelPlacement = 'rotated', color, index = 0, }) {
|
|
21
21
|
const container = useContext(ContainerContext);
|
|
22
22
|
if (container === null) {
|
|
23
23
|
throw new Error('<YAxis> must be rendered inside a <ChartContainer>');
|
|
@@ -35,6 +35,7 @@ export function YAxis({ id, side = 'left', label, scale = 'linear', min, max, fo
|
|
|
35
35
|
// and layers still bind to it, which is the whole point of the prop.
|
|
36
36
|
width: hide ? 0 : width,
|
|
37
37
|
scale,
|
|
38
|
+
linearWindow,
|
|
38
39
|
min,
|
|
39
40
|
max,
|
|
40
41
|
pad,
|
|
@@ -49,6 +50,7 @@ export function YAxis({ id, side = 'left', label, scale = 'linear', min, max, fo
|
|
|
49
50
|
width,
|
|
50
51
|
hide,
|
|
51
52
|
scale,
|
|
53
|
+
linearWindow,
|
|
52
54
|
min,
|
|
53
55
|
max,
|
|
54
56
|
pad,
|
package/dist/bars.d.ts
CHANGED
|
@@ -55,7 +55,7 @@ export declare function resolveBarBaseline(yScale: Scale): number;
|
|
|
55
55
|
* hit region are deliberately *not* the same geometry: the `gapPx` inset
|
|
56
56
|
* separates columns visually without carving a dead channel out of the target.
|
|
57
57
|
*/
|
|
58
|
-
export declare function barRect(cs: BarSeries, i: number, xScale: Scale, yScale: Scale, baseline: number, gapPx: number, minWidthPx: number): [x0: number, x1: number, yTop: number, yBottom: number] | null;
|
|
58
|
+
export declare function barRect(cs: BarSeries, i: number, xScale: Scale, yScale: Scale, baseline: number, gapPx: number, minWidthPx: number, maxWidthPx?: number): [x0: number, x1: number, yTop: number, yBottom: number] | null;
|
|
59
59
|
/**
|
|
60
60
|
* The value-space span `[lo, hi]` of **threshold band `k`** along a bar running
|
|
61
61
|
* from `base` to `v`, or `null` when the bar doesn't reach that band.
|
|
@@ -306,6 +306,9 @@ export interface StackStyle {
|
|
|
306
306
|
readonly fills: readonly string[];
|
|
307
307
|
readonly opacity: number;
|
|
308
308
|
readonly outlineWidth: number;
|
|
309
|
+
/** Cap on a segment's ink span in px, centred in the slot — the stacked
|
|
310
|
+
* counterpart of `BarStyle.maxWidth` ([PND-BARWIDTH]). Omitted ⇒ uncapped. */
|
|
311
|
+
readonly maxWidth?: number;
|
|
309
312
|
/**
|
|
310
313
|
* Optional **per-bin** fill override, aligned index-for-index to the bins
|
|
311
314
|
* (bin `b` uses `binFills[b]`), taking precedence over the per-group
|
|
@@ -457,7 +460,7 @@ export declare function stackBase(orientation: Orientation, xScale: Scale, yScal
|
|
|
457
460
|
* is unfloored. Shared by {@link drawStacks} and {@link stackAt} so the drawn rect
|
|
458
461
|
* and the hit rect are identical.
|
|
459
462
|
*/
|
|
460
|
-
export declare function segmentRect(ss: StackedBarSeries, b: number, g: number, orientation: Orientation, xScale: Scale, yScale: Scale, cumBefore: number, gapPx: number, minSpanPx: number): [x0: number, x1: number, yTop: number, yBottom: number] | null;
|
|
463
|
+
export declare function segmentRect(ss: StackedBarSeries, b: number, g: number, orientation: Orientation, xScale: Scale, yScale: Scale, cumBefore: number, gapPx: number, minSpanPx: number, maxSpanPx?: number): [x0: number, x1: number, yTop: number, yBottom: number] | null;
|
|
461
464
|
/**
|
|
462
465
|
* Fill every segment of every bin in `ss`, stacking each bin's groups from the
|
|
463
466
|
* value baseline outward (bottom → top vertical, left → right horizontal). A gap
|
|
@@ -484,5 +487,9 @@ export declare function drawStacks(ctx: CanvasRenderingContext2D, ss: StackedBar
|
|
|
484
487
|
* O(N·G) over bins × groups (no spatial index — histogram bin/group counts are
|
|
485
488
|
* small; click / hover are cheap events).
|
|
486
489
|
*/
|
|
487
|
-
export declare function stackAt(ss: StackedBarSeries, px: number, py: number, orientation: Orientation, xScale: Scale, yScale: Scale, gapPx: number, minSpanPx: number
|
|
490
|
+
export declare function stackAt(ss: StackedBarSeries, px: number, py: number, orientation: Orientation, xScale: Scale, yScale: Scale, gapPx: number, minSpanPx: number,
|
|
491
|
+
/** Must match the draw's cap ([PND-BARWIDTH]) — this function's whole
|
|
492
|
+
* contract is that its rect is the drawn rect, so a cap applied to one and
|
|
493
|
+
* not the other silently drifts the hit target off the ink. */
|
|
494
|
+
maxSpanPx?: number): [bin: number, group: number, begin: number, name: string, value: number] | null;
|
|
488
495
|
//# sourceMappingURL=bars.d.ts.map
|
package/dist/bars.js
CHANGED
|
@@ -73,11 +73,11 @@ export function resolveBarBaseline(yScale) {
|
|
|
73
73
|
* hit region are deliberately *not* the same geometry: the `gapPx` inset
|
|
74
74
|
* separates columns visually without carving a dead channel out of the target.
|
|
75
75
|
*/
|
|
76
|
-
export function barRect(cs, i, xScale, yScale, baseline, gapPx, minWidthPx) {
|
|
76
|
+
export function barRect(cs, i, xScale, yScale, baseline, gapPx, minWidthPx, maxWidthPx) {
|
|
77
77
|
const v = cs.y[i];
|
|
78
78
|
if (!Number.isFinite(v))
|
|
79
79
|
return null;
|
|
80
|
-
const [x0, x1] = barSpanPx(cs.begin[i], cs.end[i], xScale, gapPx, minWidthPx);
|
|
80
|
+
const [x0, x1] = barSpanPx(cs.begin[i], cs.end[i], xScale, gapPx, minWidthPx, maxWidthPx);
|
|
81
81
|
const yValue = yScale(v);
|
|
82
82
|
const yBase = yScale(baseline);
|
|
83
83
|
return [x0, x1, Math.min(yValue, yBase), Math.max(yValue, yBase)];
|
|
@@ -430,7 +430,7 @@ spans = NO_SPANS) {
|
|
|
430
430
|
: null;
|
|
431
431
|
let drawn = 0;
|
|
432
432
|
for (let i = vStart; i < vEnd; i += 1) {
|
|
433
|
-
const rect = barRect(cs, i, xScale, yScale, baseline, gapPx, style.minWidth);
|
|
433
|
+
const rect = barRect(cs, i, xScale, yScale, baseline, gapPx, style.minWidth, style.maxWidth);
|
|
434
434
|
if (rect === null)
|
|
435
435
|
continue;
|
|
436
436
|
const [x0, x1, yTop, yBottom] = rect;
|
|
@@ -779,7 +779,7 @@ export function stackBase(orientation, xScale, yScale) {
|
|
|
779
779
|
* is unfloored. Shared by {@link drawStacks} and {@link stackAt} so the drawn rect
|
|
780
780
|
* and the hit rect are identical.
|
|
781
781
|
*/
|
|
782
|
-
export function segmentRect(ss, b, g, orientation, xScale, yScale, cumBefore, gapPx, minSpanPx) {
|
|
782
|
+
export function segmentRect(ss, b, g, orientation, xScale, yScale, cumBefore, gapPx, minSpanPx, maxSpanPx) {
|
|
783
783
|
const G = ss.groups.length;
|
|
784
784
|
const v = ss.values[b * G + g];
|
|
785
785
|
// Skip non-finite (a gap) or zero (a zero-extent rect that can't draw or be
|
|
@@ -801,12 +801,12 @@ export function segmentRect(ss, b, g, orientation, xScale, yScale, cumBefore, ga
|
|
|
801
801
|
if (!Number.isFinite(v) || v === 0)
|
|
802
802
|
return null;
|
|
803
803
|
if (orientation === 'vertical') {
|
|
804
|
-
const [x0, x1] = barSpanPx(ss.begin[b], ss.end[b], xScale, gapPx, minSpanPx);
|
|
804
|
+
const [x0, x1] = barSpanPx(ss.begin[b], ss.end[b], xScale, gapPx, minSpanPx, maxSpanPx);
|
|
805
805
|
const yA = yScale(cumBefore);
|
|
806
806
|
const yB = yScale(cumBefore + v);
|
|
807
807
|
return [x0, x1, Math.min(yA, yB), Math.max(yA, yB)];
|
|
808
808
|
}
|
|
809
|
-
const [y0, y1] = barSpanPx(ss.begin[b], ss.end[b], yScale, gapPx, minSpanPx);
|
|
809
|
+
const [y0, y1] = barSpanPx(ss.begin[b], ss.end[b], yScale, gapPx, minSpanPx, maxSpanPx);
|
|
810
810
|
const xA = xScale(cumBefore);
|
|
811
811
|
const xB = xScale(cumBefore + v);
|
|
812
812
|
return [Math.min(xA, xB), Math.max(xA, xB), y0, y1];
|
|
@@ -865,7 +865,7 @@ spans = NO_SPANS) {
|
|
|
865
865
|
let cumNeg = base;
|
|
866
866
|
for (let g = 0; g < G; g += 1) {
|
|
867
867
|
const v = ss.values[b * G + g];
|
|
868
|
-
const rect = segmentRect(ss, b, g, orientation, xScale, yScale, v < 0 ? cumNeg : cumPos, gapPx, minSpanPx);
|
|
868
|
+
const rect = segmentRect(ss, b, g, orientation, xScale, yScale, v < 0 ? cumNeg : cumPos, gapPx, minSpanPx, style.maxWidth);
|
|
869
869
|
if (Number.isFinite(v)) {
|
|
870
870
|
if (v > 0)
|
|
871
871
|
cumPos += v;
|
|
@@ -1011,7 +1011,11 @@ spans = NO_SPANS) {
|
|
|
1011
1011
|
* O(N·G) over bins × groups (no spatial index — histogram bin/group counts are
|
|
1012
1012
|
* small; click / hover are cheap events).
|
|
1013
1013
|
*/
|
|
1014
|
-
export function stackAt(ss, px, py, orientation, xScale, yScale, gapPx, minSpanPx
|
|
1014
|
+
export function stackAt(ss, px, py, orientation, xScale, yScale, gapPx, minSpanPx,
|
|
1015
|
+
/** Must match the draw's cap ([PND-BARWIDTH]) — this function's whole
|
|
1016
|
+
* contract is that its rect is the drawn rect, so a cap applied to one and
|
|
1017
|
+
* not the other silently drifts the hit target off the ink. */
|
|
1018
|
+
maxSpanPx) {
|
|
1015
1019
|
const G = ss.groups.length;
|
|
1016
1020
|
const base = stackBase(orientation, xScale, yScale);
|
|
1017
1021
|
for (let b = 0; b < ss.length; b += 1) {
|
|
@@ -1021,7 +1025,7 @@ export function stackAt(ss, px, py, orientation, xScale, yScale, gapPx, minSpanP
|
|
|
1021
1025
|
let cumNeg = base;
|
|
1022
1026
|
for (let g = 0; g < G; g += 1) {
|
|
1023
1027
|
const v = ss.values[b * G + g];
|
|
1024
|
-
const rect = segmentRect(ss, b, g, orientation, xScale, yScale, v < 0 ? cumNeg : cumPos, gapPx, minSpanPx);
|
|
1028
|
+
const rect = segmentRect(ss, b, g, orientation, xScale, yScale, v < 0 ? cumNeg : cumPos, gapPx, minSpanPx, maxSpanPx);
|
|
1025
1029
|
if (Number.isFinite(v)) {
|
|
1026
1030
|
if (v > 0)
|
|
1027
1031
|
cumPos += v;
|
package/dist/context.d.ts
CHANGED
|
@@ -1597,17 +1597,17 @@ export interface LayerEntry {
|
|
|
1597
1597
|
}
|
|
1598
1598
|
/** A y-axis declared in a {@link ChartRow} via `<YAxis>`. */
|
|
1599
1599
|
/** Which scale a y axis maps its domain through. */
|
|
1600
|
-
export type YScaleKind = 'linear' | 'log';
|
|
1600
|
+
export type YScaleKind = 'linear' | 'log' | 'symlog';
|
|
1601
1601
|
/**
|
|
1602
|
-
* A row's resolved y scale — d3's `scaleLinear()`,
|
|
1603
|
-
*
|
|
1602
|
+
* A row's resolved y scale — d3's `scaleLinear()`, `scaleLog()` when the axis
|
|
1603
|
+
* asks for `scale="log"`, or `scaleSymlog()` for `scale="symlog"`.
|
|
1604
1604
|
*
|
|
1605
1605
|
* Deliberately the **continuous-numeric** supertype rather than `ScaleLinear`:
|
|
1606
1606
|
* every consumer (the axis labels, the row's gridlines, the cursor readout, and
|
|
1607
1607
|
* every draw layer) only ever calls it, or reads `domain` / `range` / `ticks` /
|
|
1608
|
-
* `tickFormat` / `invert` — the surface
|
|
1609
|
-
* type here is what lets a log axis be transparent to the draw
|
|
1610
|
-
* of every layer growing a branch.
|
|
1608
|
+
* `tickFormat` / `invert` — the surface all three scales share. Keeping the
|
|
1609
|
+
* shared type here is what lets a log or symlog axis be transparent to the draw
|
|
1610
|
+
* layers instead of every layer growing a branch.
|
|
1611
1611
|
*/
|
|
1612
1612
|
export type YScale = ScaleContinuousNumeric<number, number>;
|
|
1613
1613
|
export interface AxisSpec {
|
|
@@ -1617,6 +1617,11 @@ export interface AxisSpec {
|
|
|
1617
1617
|
readonly width: number;
|
|
1618
1618
|
/** Which scale the axis maps its domain through ({@link YAxisProps.scale}). */
|
|
1619
1619
|
readonly scale: YScaleKind;
|
|
1620
|
+
/** `scale="symlog"`'s linear window as a **fraction of the domain's largest
|
|
1621
|
+
* magnitude** ({@link YAxisProps.linearWindow}) — `undefined` on any other
|
|
1622
|
+
* scale. Domain-relative rather than absolute so it survives a domain change
|
|
1623
|
+
* without a recompute ([PND-SYMLOG]). */
|
|
1624
|
+
readonly linearWindow?: number | undefined;
|
|
1620
1625
|
/** Explicit domain bounds, or `undefined` to auto-fit linked layers. */
|
|
1621
1626
|
readonly min: number | undefined;
|
|
1622
1627
|
readonly max: number | undefined;
|
package/dist/data.d.ts
CHANGED
|
@@ -499,6 +499,44 @@ export interface CategoryDatum {
|
|
|
499
499
|
readonly label: string;
|
|
500
500
|
readonly value: number;
|
|
501
501
|
}
|
|
502
|
+
/**
|
|
503
|
+
* One category of a **stacked** category chart ([PND-CATSTACK]) — a label plus a
|
|
504
|
+
* value **per group**, read by name through `<BarChart columns>`.
|
|
505
|
+
*
|
|
506
|
+
* The `values` record is the shape the list family already uses
|
|
507
|
+
* (`ListRow.values`), deliberately: "a row with named values" is one concept in
|
|
508
|
+
* this library and a category with several groups is exactly that. A missing or
|
|
509
|
+
* non-finite entry reads as a gap, so a group absent from one category is a hole
|
|
510
|
+
* rather than a zero — the same rule every other reader applies.
|
|
511
|
+
*/
|
|
512
|
+
export interface CategoryStackDatum {
|
|
513
|
+
readonly label: string;
|
|
514
|
+
readonly values: Readonly<Record<string, number | undefined>>;
|
|
515
|
+
}
|
|
516
|
+
/**
|
|
517
|
+
* Build a {@link StackedBarSeries} from ordered categories carrying a value
|
|
518
|
+
* **per group** — the stacked counterpart of {@link categoryStack}
|
|
519
|
+
* ([PND-CATSTACK]).
|
|
520
|
+
*
|
|
521
|
+
* Geometry is identical to the single-value case (one unit slot `[i, i+1]` per
|
|
522
|
+
* category, `marks` carrying the names), so the categorical axis derives its
|
|
523
|
+
* ordered labels exactly as before and a pinned selection still keys on the
|
|
524
|
+
* stable name. Only `groups` and the `values` layout differ, and both match
|
|
525
|
+
* {@link stacksFromColumns} — `values[i * G + g]`, bin-major — so this reaches
|
|
526
|
+
* the shipped `drawStacks` path with no new draw code.
|
|
527
|
+
*
|
|
528
|
+
* **Why this replaces a real workaround.** Composing the same picture from one
|
|
529
|
+
* `<BarChart categories>` layer per cumulative total (drawn outermost-first so
|
|
530
|
+
* each overpaints the one beneath) costs three things a first-class stack does
|
|
531
|
+
* not: hand-assembled legends, label thinning that cannot see the other layers,
|
|
532
|
+
* and — since selection entries key on `(layer id, mark)` — a controlled set
|
|
533
|
+
* that must be replicated across every segment layer, where missing one makes a
|
|
534
|
+
* selected bar recede *from the waist up*. With one layer and one `mark` per
|
|
535
|
+
* bar, that last failure is not expressible.
|
|
536
|
+
*
|
|
537
|
+
* O(n·G) with one `Float64Array` allocation, matching `stacksFromColumns`.
|
|
538
|
+
*/
|
|
539
|
+
export declare function categoryStacks(records: readonly CategoryStackDatum[], columns: readonly string[]): StackedBarSeries;
|
|
502
540
|
/**
|
|
503
541
|
* Build a {@link StackedBarSeries} (single group, `G === 1`) from an ordered list
|
|
504
542
|
* of `{ label, value }` categories — one **unit slot** `[i, i+1]` per category, in
|
package/dist/data.js
CHANGED
|
@@ -619,6 +619,49 @@ export function stacksFromBins(bins, columns, options = {}) {
|
|
|
619
619
|
}
|
|
620
620
|
return { begin, end, groups: columns, values, length: n };
|
|
621
621
|
}
|
|
622
|
+
/**
|
|
623
|
+
* Build a {@link StackedBarSeries} from ordered categories carrying a value
|
|
624
|
+
* **per group** — the stacked counterpart of {@link categoryStack}
|
|
625
|
+
* ([PND-CATSTACK]).
|
|
626
|
+
*
|
|
627
|
+
* Geometry is identical to the single-value case (one unit slot `[i, i+1]` per
|
|
628
|
+
* category, `marks` carrying the names), so the categorical axis derives its
|
|
629
|
+
* ordered labels exactly as before and a pinned selection still keys on the
|
|
630
|
+
* stable name. Only `groups` and the `values` layout differ, and both match
|
|
631
|
+
* {@link stacksFromColumns} — `values[i * G + g]`, bin-major — so this reaches
|
|
632
|
+
* the shipped `drawStacks` path with no new draw code.
|
|
633
|
+
*
|
|
634
|
+
* **Why this replaces a real workaround.** Composing the same picture from one
|
|
635
|
+
* `<BarChart categories>` layer per cumulative total (drawn outermost-first so
|
|
636
|
+
* each overpaints the one beneath) costs three things a first-class stack does
|
|
637
|
+
* not: hand-assembled legends, label thinning that cannot see the other layers,
|
|
638
|
+
* and — since selection entries key on `(layer id, mark)` — a controlled set
|
|
639
|
+
* that must be replicated across every segment layer, where missing one makes a
|
|
640
|
+
* selected bar recede *from the waist up*. With one layer and one `mark` per
|
|
641
|
+
* bar, that last failure is not expressible.
|
|
642
|
+
*
|
|
643
|
+
* O(n·G) with one `Float64Array` allocation, matching `stacksFromColumns`.
|
|
644
|
+
*/
|
|
645
|
+
export function categoryStacks(records, columns) {
|
|
646
|
+
const n = records.length;
|
|
647
|
+
const G = columns.length;
|
|
648
|
+
const begin = new Float64Array(n);
|
|
649
|
+
const end = new Float64Array(n);
|
|
650
|
+
const values = new Float64Array(n * G);
|
|
651
|
+
const marks = new Array(n);
|
|
652
|
+
for (let i = 0; i < n; i += 1) {
|
|
653
|
+
begin[i] = i;
|
|
654
|
+
end[i] = i + 1;
|
|
655
|
+
marks[i] = records[i].label;
|
|
656
|
+
const row = records[i].values;
|
|
657
|
+
for (let g = 0; g < G; g += 1) {
|
|
658
|
+
const v = row[columns[g]];
|
|
659
|
+
// A missing key and a non-finite value are the same thing here: no bar.
|
|
660
|
+
values[i * G + g] = typeof v === 'number' && Number.isFinite(v) ? v : NaN;
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
return { begin, end, groups: columns, values, length: n, marks };
|
|
664
|
+
}
|
|
622
665
|
/**
|
|
623
666
|
* Build a {@link StackedBarSeries} (single group, `G === 1`) from an ordered list
|
|
624
667
|
* of `{ label, value }` categories — one **unit slot** `[i, i+1]` per category, in
|
package/dist/format.d.ts
CHANGED
|
@@ -47,6 +47,9 @@ interface Tickable {
|
|
|
47
47
|
tickFormat(count: number, specifier?: string): (value: number) => string;
|
|
48
48
|
/** Present on d3's `scaleLog` and on no other continuous scale. */
|
|
49
49
|
base?: () => number;
|
|
50
|
+
/** Present on d3's `scaleSymlog` and on no other continuous scale — the linear
|
|
51
|
+
* window's half-width in data units. */
|
|
52
|
+
constant?: () => number;
|
|
50
53
|
domain?: () => number[];
|
|
51
54
|
}
|
|
52
55
|
/**
|
|
@@ -75,6 +78,18 @@ interface Tickable {
|
|
|
75
78
|
* applies the specifier to whatever it is handed, which is what every consumer
|
|
76
79
|
* of this function actually wants; the axis's own tick *thinning* is handled by
|
|
77
80
|
* `yTickValues`, not here.
|
|
81
|
+
*
|
|
82
|
+
* **A symlog scale's precision comes from its knee, not its span** ([PND-SYMLOG]).
|
|
83
|
+
* `scaleSymlog.tickFormat` is `linearish`, so it derives precision from the
|
|
84
|
+
* domain — and a symlog axis is chosen precisely when the interesting values are
|
|
85
|
+
* *orders of magnitude smaller* than the domain. On `[-1, 1]` with a `0.02` knee
|
|
86
|
+
* the ladder emits `-0.02, 0, 0.02` and a span-derived formatter labels all three
|
|
87
|
+
* **`"0.0"`**: three ticks at three positions asserting the same value, on the
|
|
88
|
+
* axis whose whole purpose was to separate them. Formatting through a linear
|
|
89
|
+
* scale over `[-knee, knee]` calibrates to the smallest magnitude the ladder can
|
|
90
|
+
* emit. It changes nothing when the knee is already coarse (a 20k knee on a ±1M
|
|
91
|
+
* domain formats identically), and the cursor readout inherits the same extra
|
|
92
|
+
* precision — which is wanted, since near-zero is where a symlog readout is read.
|
|
78
93
|
*/
|
|
79
94
|
export declare function resolveAxisFormat(scale: Tickable, count: number, format: AxisFormat | undefined): (value: number) => string;
|
|
80
95
|
/** The slice of a d3 **time** scale {@link resolveTimeFormat} needs. A d3
|
package/dist/format.js
CHANGED
|
@@ -25,13 +25,28 @@ import { scaleLinear } from 'd3-scale';
|
|
|
25
25
|
* applies the specifier to whatever it is handed, which is what every consumer
|
|
26
26
|
* of this function actually wants; the axis's own tick *thinning* is handled by
|
|
27
27
|
* `yTickValues`, not here.
|
|
28
|
+
*
|
|
29
|
+
* **A symlog scale's precision comes from its knee, not its span** ([PND-SYMLOG]).
|
|
30
|
+
* `scaleSymlog.tickFormat` is `linearish`, so it derives precision from the
|
|
31
|
+
* domain — and a symlog axis is chosen precisely when the interesting values are
|
|
32
|
+
* *orders of magnitude smaller* than the domain. On `[-1, 1]` with a `0.02` knee
|
|
33
|
+
* the ladder emits `-0.02, 0, 0.02` and a span-derived formatter labels all three
|
|
34
|
+
* **`"0.0"`**: three ticks at three positions asserting the same value, on the
|
|
35
|
+
* axis whose whole purpose was to separate them. Formatting through a linear
|
|
36
|
+
* scale over `[-knee, knee]` calibrates to the smallest magnitude the ladder can
|
|
37
|
+
* emit. It changes nothing when the knee is already coarse (a 20k knee on a ±1M
|
|
38
|
+
* domain formats identically), and the cursor readout inherits the same extra
|
|
39
|
+
* precision — which is wanted, since near-zero is where a symlog readout is read.
|
|
28
40
|
*/
|
|
29
41
|
export function resolveAxisFormat(scale, count, format) {
|
|
30
42
|
if (typeof format === 'function')
|
|
31
43
|
return format;
|
|
44
|
+
const knee = typeof scale.constant === 'function' ? Math.abs(scale.constant()) : 0;
|
|
32
45
|
const source = typeof scale.base === 'function' && typeof scale.domain === 'function'
|
|
33
46
|
? scaleLinear().domain(scale.domain())
|
|
34
|
-
:
|
|
47
|
+
: Number.isFinite(knee) && knee > 0
|
|
48
|
+
? scaleLinear().domain([-knee, knee])
|
|
49
|
+
: scale;
|
|
35
50
|
return format !== undefined
|
|
36
51
|
? source.tickFormat(count, format)
|
|
37
52
|
: source.tickFormat(count);
|
package/dist/index.d.ts
CHANGED
|
@@ -73,8 +73,8 @@ export type { RegionProps, BaselineProps, MarkerProps, ZoneProps, } from './anno
|
|
|
73
73
|
export type { AnnotationKind, CreateSpec } from './context.js';
|
|
74
74
|
export { YAxisIndicator, createLiveValue } from './indicators.js';
|
|
75
75
|
export type { YAxisIndicatorProps, LiveValue } from './indicators.js';
|
|
76
|
-
export { fromTimeSeries, bandFromTimeSeries, boxFromTimeSeries, barsFromTimeSeries, barsFromBins, ohlcFromTimeSeries, stacksFromGroups, stacksFromColumns, stacksFromBins, categoryStack, transposeRow, } from './data.js';
|
|
77
|
-
export type { ChartSeries, BandSeries, BoxSeries, BoxColumns, BarSeries, OhlcSeries, OhlcColumns, StackedBarSeries, BinRecord, StacksFromBinsOptions, CategoryDatum, RowAt, TransposeRowOptions, } from './data.js';
|
|
76
|
+
export { fromTimeSeries, bandFromTimeSeries, boxFromTimeSeries, barsFromTimeSeries, barsFromBins, ohlcFromTimeSeries, stacksFromGroups, stacksFromColumns, stacksFromBins, categoryStack, categoryStacks, transposeRow, } from './data.js';
|
|
77
|
+
export type { ChartSeries, BandSeries, BoxSeries, BoxColumns, BarSeries, OhlcSeries, OhlcColumns, StackedBarSeries, BinRecord, StacksFromBinsOptions, CategoryDatum, CategoryStackDatum, RowAt, TransposeRowOptions, } from './data.js';
|
|
78
78
|
export type { Orientation } from './bars.js';
|
|
79
79
|
export type { RadiusEncoding, ColorEncoding } from './encoding.js';
|
|
80
80
|
export type { Curve } from './curve.js';
|
package/dist/index.js
CHANGED
|
@@ -68,6 +68,11 @@ export { fromTimeSeries, bandFromTimeSeries, boxFromTimeSeries, barsFromTimeSeri
|
|
|
68
68
|
stacksFromGroups, stacksFromColumns, stacksFromBins,
|
|
69
69
|
// Categorical row-read: one bar per `{ label, value }` on the category axis.
|
|
70
70
|
categoryStack,
|
|
71
|
+
// …and its stacked sibling: one bar per `{ label, values }`, segments named by
|
|
72
|
+
// `columns` ([PND-CATSTACK]). Public for the same reason every reader above is
|
|
73
|
+
// — a caller assembling a `StackedBarSeries` by hand needs it — and because
|
|
74
|
+
// API.md already documented it as public while `index.ts` did not export it.
|
|
75
|
+
categoryStacks,
|
|
71
76
|
// The transpose reader — one row of a wide series read across into categories.
|
|
72
77
|
transposeRow, } from './data.js';
|
|
73
78
|
export { defaultTheme, estelaTheme } from './theme.js';
|
package/dist/range.d.ts
CHANGED
|
@@ -10,6 +10,19 @@
|
|
|
10
10
|
* A span that the gap would invert (narrower than `minWidthPx` after the inset)
|
|
11
11
|
* collapses to a `minWidthPx` mark centred in the slot, so a too-thin bucket
|
|
12
12
|
* stays visible and the bar never flips inside-out.
|
|
13
|
+
*
|
|
14
|
+
* `maxWidthPx` caps the **ink** and is applied *after* the inset, centred in the
|
|
15
|
+
* slot ([PND-BARWIDTH]). It is the missing half of the width vocabulary: `gapPx`
|
|
16
|
+
* is a *relative* inset, so on its own bar width is always `slot - gap` and
|
|
17
|
+
* fattens with the slot. Two independent sizes — slots spreading to fill the
|
|
18
|
+
* plot, ink pinned to N px — is what makes a measure comparable **between**
|
|
19
|
+
* panes, since a bar that widens with its pane reads as a different weight of
|
|
20
|
+
* the same thing. Expressing that with the relative knob alone requires
|
|
21
|
+
* predicting the slot width and back-solving the gap, which re-derives this
|
|
22
|
+
* function's arithmetic in consumer code.
|
|
23
|
+
*
|
|
24
|
+
* `minWidthPx` still wins: a cap below the floor yields the floor, so the two
|
|
25
|
+
* bounds can never invert the rect.
|
|
13
26
|
*/
|
|
14
|
-
export declare function barSpanPx(beginMs: number, endMs: number, xScale: (value: number) => number, gapPx?: number, minWidthPx?: number): [number, number];
|
|
27
|
+
export declare function barSpanPx(beginMs: number, endMs: number, xScale: (value: number) => number, gapPx?: number, minWidthPx?: number, maxWidthPx?: number): [number, number];
|
|
15
28
|
//# sourceMappingURL=range.d.ts.map
|
package/dist/range.js
CHANGED
|
@@ -10,15 +10,36 @@
|
|
|
10
10
|
* A span that the gap would invert (narrower than `minWidthPx` after the inset)
|
|
11
11
|
* collapses to a `minWidthPx` mark centred in the slot, so a too-thin bucket
|
|
12
12
|
* stays visible and the bar never flips inside-out.
|
|
13
|
+
*
|
|
14
|
+
* `maxWidthPx` caps the **ink** and is applied *after* the inset, centred in the
|
|
15
|
+
* slot ([PND-BARWIDTH]). It is the missing half of the width vocabulary: `gapPx`
|
|
16
|
+
* is a *relative* inset, so on its own bar width is always `slot - gap` and
|
|
17
|
+
* fattens with the slot. Two independent sizes — slots spreading to fill the
|
|
18
|
+
* plot, ink pinned to N px — is what makes a measure comparable **between**
|
|
19
|
+
* panes, since a bar that widens with its pane reads as a different weight of
|
|
20
|
+
* the same thing. Expressing that with the relative knob alone requires
|
|
21
|
+
* predicting the slot width and back-solving the gap, which re-derives this
|
|
22
|
+
* function's arithmetic in consumer code.
|
|
23
|
+
*
|
|
24
|
+
* `minWidthPx` still wins: a cap below the floor yields the floor, so the two
|
|
25
|
+
* bounds can never invert the rect.
|
|
13
26
|
*/
|
|
14
|
-
export function barSpanPx(beginMs, endMs, xScale, gapPx = 0, minWidthPx = 1) {
|
|
27
|
+
export function barSpanPx(beginMs, endMs, xScale, gapPx = 0, minWidthPx = 1, maxWidthPx) {
|
|
15
28
|
const a = xScale(beginMs);
|
|
16
29
|
const b = xScale(endMs);
|
|
17
30
|
const lo = Math.min(a, b);
|
|
18
31
|
const hi = Math.max(a, b);
|
|
19
32
|
const inset = gapPx / 2;
|
|
20
|
-
|
|
21
|
-
|
|
33
|
+
let x0 = lo + inset;
|
|
34
|
+
let x1 = hi - inset;
|
|
35
|
+
// The cap reads on the INSET span, not the raw slot, so `gap` keeps its
|
|
36
|
+
// meaning as the minimum breathing room: a bar is never wider than the gap
|
|
37
|
+
// allows, and never wider than the cap, whichever binds first.
|
|
38
|
+
if (maxWidthPx !== undefined && maxWidthPx > 0 && x1 - x0 > maxWidthPx) {
|
|
39
|
+
const mid = (lo + hi) / 2;
|
|
40
|
+
x0 = mid - maxWidthPx / 2;
|
|
41
|
+
x1 = mid + maxWidthPx / 2;
|
|
42
|
+
}
|
|
22
43
|
if (x1 - x0 >= minWidthPx)
|
|
23
44
|
return [x0, x1];
|
|
24
45
|
const mid = (lo + hi) / 2;
|
package/dist/theme.d.ts
CHANGED
|
@@ -296,10 +296,14 @@ export interface ChartTheme {
|
|
|
296
296
|
*
|
|
297
297
|
* So the two values here are the ones with no canvas counterpart: the row
|
|
298
298
|
* **band** tints. Everything else resolves from tokens that already exist
|
|
299
|
-
* and are per-metric where they should be — a selected fill takes
|
|
300
|
-
*
|
|
301
|
-
*
|
|
302
|
-
*
|
|
299
|
+
* and are per-metric where they should be — a selected glyph fill takes
|
|
300
|
+
* `bar[as].highlight` and a dimmed one `bar[as].dimmed` — so a consumer who
|
|
301
|
+
* themes their bars gets a coherent list without theming it twice.
|
|
302
|
+
*
|
|
303
|
+
* **To be unambiguous, because this reads as a list of fields on this
|
|
304
|
+
* block:** `highlight` / `dimmed` are **`BarStyle`** tokens, resolved through
|
|
305
|
+
* `theme.bar[as]`. They are not fields of `theme.list` and there is no
|
|
306
|
+
* `list.dimmed` — this register carries exactly the five values below.
|
|
303
307
|
*
|
|
304
308
|
* **The rail is deliberately NOT per-metric.** There is one rail per row
|
|
305
309
|
* and a row may carry several metrics, so it cannot resolve through
|
|
@@ -730,6 +734,28 @@ export interface BarStyle {
|
|
|
730
734
|
readonly highlight: string;
|
|
731
735
|
readonly gap: number;
|
|
732
736
|
readonly minWidth: number;
|
|
737
|
+
/**
|
|
738
|
+
* Cap on a bar's **ink** width in px, applied after the `gap` inset and
|
|
739
|
+
* centred in the slot ([PND-BARWIDTH]). **Omitted ⇒ uncapped** — a bar is
|
|
740
|
+
* `slot - gap` wide, as it always was.
|
|
741
|
+
*
|
|
742
|
+
* It is the missing half of the width vocabulary, and the reason it cannot be
|
|
743
|
+
* spelled with `gap` alone: `gap` is a *relative* inset, so bar width tracks
|
|
744
|
+
* the slot and fattens as the plot widens. A **fixed** ink width is what makes
|
|
745
|
+
* a measure comparable *between* panes — bars that widen with their pane read
|
|
746
|
+
* as different weights of the same thing. Wanting both (slots spreading to
|
|
747
|
+
* fill, ink pinned) needs two independent knobs; with one, a consumer has to
|
|
748
|
+
* predict the slot width and back-solve the gap, re-deriving pond's own layout
|
|
749
|
+
* arithmetic in their code.
|
|
750
|
+
*
|
|
751
|
+
* Pairs with `<ChartContainer maxBandWidth>`, which caps the **slot**: that
|
|
752
|
+
* one decides how far the bars spread, this one how wide the ink is inside
|
|
753
|
+
* whatever slot results. `minWidth` still wins if the two would invert.
|
|
754
|
+
*
|
|
755
|
+
* `<BarChart maxBarWidth>` overrides this per layer, the same relationship
|
|
756
|
+
* `gap` has.
|
|
757
|
+
*/
|
|
758
|
+
readonly maxWidth?: number;
|
|
733
759
|
readonly outlineWidth: number;
|
|
734
760
|
/**
|
|
735
761
|
* Optional distinct **hover** fill, so a bar can read a three-step emphasis —
|
|
@@ -757,6 +783,11 @@ export interface BarStyle {
|
|
|
757
783
|
* both states so a red/green volume bar keeps its meaning while live —
|
|
758
784
|
* the one *design* exclusion rather than a path consequence.
|
|
759
785
|
*
|
|
786
|
+
* **Scope note: that `binColors` exclusion is about the LIVE states only.**
|
|
787
|
+
* It does not carry over to {@link dimmed}, which *replaces* a per-bar fill
|
|
788
|
+
* on an unselected bar — see that token, which spells out the asymmetry and
|
|
789
|
+
* why emphasis preserves a per-bar colour while recession suppresses it.
|
|
790
|
+
*
|
|
760
791
|
* The **decimated** dense-bar pass also draws the flat fill only, as it
|
|
761
792
|
* already did for `highlight`.
|
|
762
793
|
*/
|
|
@@ -859,6 +890,29 @@ export interface BarStyle {
|
|
|
859
890
|
* component, and drifted immediately — one consumer had three charts using
|
|
860
891
|
* `color-mix` at 22%, 28% and 30% for the same concept, in the same week, for
|
|
861
892
|
* no reason. One theme value fixes that permanently.
|
|
893
|
+
*
|
|
894
|
+
* **It OVERRIDES a per-bar fill, unlike the live states.** This is the one
|
|
895
|
+
* place `dimmed` and {@link hover} deliberately disagree, and the asymmetry
|
|
896
|
+
* is easy to read the wrong way round:
|
|
897
|
+
*
|
|
898
|
+
* - **{@link binColors} / {@link binFills}:** an unselected bar paints
|
|
899
|
+
* `dimmed`, discarding its own colour. (Hover and selection do the
|
|
900
|
+
* opposite — they keep the per-bar colour and pop the alpha, so a
|
|
901
|
+
* red/green volume bar stays red/green while live.)
|
|
902
|
+
* - **{@link bands} / thresholds:** an unselected banded bar draws **flat**
|
|
903
|
+
* in `dimmed`, discarding the ladder entirely rather than dimming each
|
|
904
|
+
* band.
|
|
905
|
+
* - **A multi-group stack** resolves per group through
|
|
906
|
+
* {@link StackStyle.dimmedFills} first, falling back to this flat value —
|
|
907
|
+
* a stack dimmed to one colour stops reading as a stack.
|
|
908
|
+
*
|
|
909
|
+
* The rule behind all three: a per-bar or per-band colour encodes *what the
|
|
910
|
+
* value is*, and a receded bar's whole job is to stop competing over that.
|
|
911
|
+
* Emphasis preserves meaning; recession suppresses it. So a chart that keeps
|
|
912
|
+
* `binColors` or `thresholds` for reasons unrelated to selection still gets
|
|
913
|
+
* a visible de-emphasis for free, and does **not** need to dim inside its own
|
|
914
|
+
* colour arrays. (Asked by a consumer who reasonably generalized `hover`'s
|
|
915
|
+
* `binColors` exclusion to this token; the exclusion is live-states-only.)
|
|
862
916
|
*/
|
|
863
917
|
readonly dimmed?: string;
|
|
864
918
|
}
|
package/dist/yticks.d.ts
CHANGED
|
@@ -23,6 +23,9 @@ interface TickableScale {
|
|
|
23
23
|
domain(): number[];
|
|
24
24
|
/** Present on d3's `scaleLog` and on no other continuous scale. */
|
|
25
25
|
base?: () => number;
|
|
26
|
+
/** Present on d3's `scaleSymlog` and on no other continuous scale — the
|
|
27
|
+
* linear window's half-width, i.e. the knee ([PND-SYMLOG]). */
|
|
28
|
+
constant?: () => number;
|
|
26
29
|
}
|
|
27
30
|
/**
|
|
28
31
|
* The y tick **values** a `<YAxis>`'s labels and the row's gridlines draw —
|
package/dist/yticks.js
CHANGED
|
@@ -61,6 +61,8 @@ export function resolveYTickCount(height, explicit) {
|
|
|
61
61
|
* approach `resolveBarBaseline` takes to read `.domain()`.
|
|
62
62
|
*/
|
|
63
63
|
export function yTickValues(scale, count) {
|
|
64
|
+
if (typeof scale.constant === 'function')
|
|
65
|
+
return symlogTickValues(scale, count);
|
|
64
66
|
if (typeof scale.base !== 'function')
|
|
65
67
|
return scale.ticks(count);
|
|
66
68
|
const domain = scale.domain();
|
|
@@ -80,4 +82,106 @@ export function yTickValues(scale, count) {
|
|
|
80
82
|
out.push(10 ** e);
|
|
81
83
|
return out;
|
|
82
84
|
}
|
|
85
|
+
/**
|
|
86
|
+
* Tick values for a **symlog** axis — linear through zero, logarithmic beyond
|
|
87
|
+
* ([PND-SYMLOG]).
|
|
88
|
+
*
|
|
89
|
+
* **This is the feature, not a refinement of it.** d3's `scaleSymlog` supplies
|
|
90
|
+
* the transform but its `ticks()` is `linearish` — evenly spaced in *value*. On
|
|
91
|
+
* a ±1M domain with a 20k knee that yields `-1M, -500k, 0, 500k, 1M`: **nothing
|
|
92
|
+
* at all below the knee**, which is the region a symlog axis exists to reveal.
|
|
93
|
+
* The mapping does spread that region generously (0→250px, 20k→294px,
|
|
94
|
+
* 100k→364px on a 500px range), so such a chart is readable-but-unlabelled —
|
|
95
|
+
* confidently gridded on the one part of the scale that isn't the point. Owning
|
|
96
|
+
* the ladder is therefore inseparable from owning the transform.
|
|
97
|
+
*
|
|
98
|
+
* The ladder, and why each piece is there:
|
|
99
|
+
*
|
|
100
|
+
* - **Zero, always.** It is the axis's centre of symmetry and the one value a
|
|
101
|
+
* symlog scale is chosen to keep visible.
|
|
102
|
+
* - **The knee, ±`constant`.** Where the reading changes from linear to
|
|
103
|
+
* logarithmic. Unlabelled, a reader has no way to know which régime a given
|
|
104
|
+
* gap belongs to, and the same pixel distance means different things either
|
|
105
|
+
* side of it.
|
|
106
|
+
* - **Decades beyond the knee, mirrored.** What a log plot is conventionally
|
|
107
|
+
* gridded on, thinned by the same "every `k`th power of ten" rule the log path
|
|
108
|
+
* above uses, so it degrades predictably as the row shrinks instead of
|
|
109
|
+
* exploding.
|
|
110
|
+
* - **Bounds are NOT labelled.** A data-derived bound is rarely round, so
|
|
111
|
+
* printing it puts an arbitrary number next to a decade — the noise a log grid
|
|
112
|
+
* exists to avoid.
|
|
113
|
+
*
|
|
114
|
+
* Below one decade of span past the knee there is nothing to grid
|
|
115
|
+
* logarithmically, so it defers to `scale.ticks(count)` — which is linear, and
|
|
116
|
+
* correct, because inside the knee symlog *is* linear.
|
|
117
|
+
*
|
|
118
|
+
* **Clip first, then thin — never the other way round.** A pan/zoom (or explicit
|
|
119
|
+
* bounds) can leave a window that contains *none* of the ideal ladder:
|
|
120
|
+
* `[510_000, 990_000]` with a 19_800 knee excludes zero, both knees, and its one
|
|
121
|
+
* candidate decade, so an order that thinned a symmetric ladder and clipped
|
|
122
|
+
* afterwards handed **`[]`** to the labels and the gridlines — an axis with no
|
|
123
|
+
* ticks at all, which reads as a rendering failure rather than as a scale. The
|
|
124
|
+
* budget is likewise spent on what *survives* the domain, not on an ideal
|
|
125
|
+
* two-sided ladder, so an asymmetric window is not thinned as if it were twice
|
|
126
|
+
* its size. If nothing survives, the linear ticks are the honest answer.
|
|
127
|
+
*
|
|
128
|
+
* Detection is structural, matching the log path's use of `base()`: `constant()`
|
|
129
|
+
* exists on `scaleSymlog` and on no other continuous scale.
|
|
130
|
+
*/
|
|
131
|
+
function symlogTickValues(scale, count) {
|
|
132
|
+
const domain = scale.domain();
|
|
133
|
+
const lo = Math.min(domain[0], domain[domain.length - 1]);
|
|
134
|
+
const hi = Math.max(domain[0], domain[domain.length - 1]);
|
|
135
|
+
const knee = Math.abs(scale.constant?.() ?? 1);
|
|
136
|
+
const maxAbs = Math.max(Math.abs(lo), Math.abs(hi));
|
|
137
|
+
// Finiteness is checked, not assumed: an explicit `max={Infinity}` reaches here
|
|
138
|
+
// intact, and `floor(log10(Infinity))` is `Infinity` — which made the decade
|
|
139
|
+
// loop's `e += step` a no-op and hung the render in a `for` that could never
|
|
140
|
+
// end. d3's own linear ticks return `[]` on such a domain, so deferring is both
|
|
141
|
+
// safe and the truthful answer for a domain with no finite extent.
|
|
142
|
+
if (!Number.isFinite(lo) || !Number.isFinite(hi) || !Number.isFinite(knee))
|
|
143
|
+
return scale.ticks(count);
|
|
144
|
+
if (!(knee > 0) || !(maxAbs > knee) || !(hi > lo))
|
|
145
|
+
return scale.ticks(count);
|
|
146
|
+
// The ladder starts at the first decade **at least half a decade above the
|
|
147
|
+
// knee** (`× √10`), not merely above it. `ceil(log10(knee))` alone puts a
|
|
148
|
+
// decade arbitrarily close to the knee tick whenever the knee lands just under
|
|
149
|
+
// a power of ten — a data-derived `maxAbs` of 4.95e6 gives a 99k knee and a
|
|
150
|
+
// 100k decade, two ticks a few pixels apart whose labels round to the *same
|
|
151
|
+
// string*. Since the knee itself is always drawn, dropping that decade loses
|
|
152
|
+
// no information; a chart that prints one number twice at two positions is
|
|
153
|
+
// reporting something false about the scale.
|
|
154
|
+
const firstExp = Math.ceil(Math.log10(knee) + 0.5);
|
|
155
|
+
const lastExp = Math.floor(Math.log10(maxAbs));
|
|
156
|
+
const inDomain = (v) => v >= lo && v <= hi;
|
|
157
|
+
// Zero and ±knee are the fixed part of the ladder — kept whole, never thinned,
|
|
158
|
+
// because they are what distinguishes a symlog axis from a log one.
|
|
159
|
+
const fixed = [0, knee, -knee].filter(inDomain);
|
|
160
|
+
// Every decade past the knee that the domain actually contains, in ascending
|
|
161
|
+
// magnitude, each carrying whichever of ±10^e survived.
|
|
162
|
+
const rungs = [];
|
|
163
|
+
for (let e = firstExp; e <= lastExp; e += 1) {
|
|
164
|
+
const pair = [10 ** e, -(10 ** e)].filter(inDomain);
|
|
165
|
+
if (pair.length > 0)
|
|
166
|
+
rungs.push(pair);
|
|
167
|
+
}
|
|
168
|
+
// No rung survives ⇒ there is no logarithmic region to grid, so the linear
|
|
169
|
+
// ticks are the answer — the same reasoning as the knee-swallows-the-domain
|
|
170
|
+
// guard above, and the case that used to produce an empty axis. Note this also
|
|
171
|
+
// covers a window sitting *between* the knee and the first decade.
|
|
172
|
+
if (rungs.length === 0)
|
|
173
|
+
return scale.ticks(count);
|
|
174
|
+
// Thin the SURVIVING rungs to what is left of the budget after the fixed
|
|
175
|
+
// ticks. Stepping by magnitude keeps a rung's ± pair together, so the grid
|
|
176
|
+
// stays symmetric wherever the domain is.
|
|
177
|
+
const budget = Math.max(2, count);
|
|
178
|
+
const total = rungs.reduce((n, pair) => n + pair.length, 0);
|
|
179
|
+
const room = Math.max(1, budget - fixed.length);
|
|
180
|
+
const step = Math.max(1, Math.ceil(total / room));
|
|
181
|
+
const out = new Set(fixed);
|
|
182
|
+
for (let i = 0; i < rungs.length; i += step)
|
|
183
|
+
for (const v of rungs[i])
|
|
184
|
+
out.add(v);
|
|
185
|
+
return [...out].sort((a, b) => a - b);
|
|
186
|
+
}
|
|
83
187
|
//# sourceMappingURL=yticks.js.map
|