@blotch/dolan 22.2.2 → 22.2.3

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.
Files changed (154) hide show
  1. package/dist/commands/add.d.ts +6 -2
  2. package/dist/commands/add.js +32 -10
  3. package/dist/commands/add.js.map +1 -1
  4. package/dist/commands/add.test.js +129 -9
  5. package/dist/commands/add.test.js.map +1 -1
  6. package/dist/commands/diff.d.ts +2 -1
  7. package/dist/commands/diff.js +38 -25
  8. package/dist/commands/diff.js.map +1 -1
  9. package/dist/commands/diff.test.js +72 -11
  10. package/dist/commands/diff.test.js.map +1 -1
  11. package/dist/commands/doctor.js +41 -27
  12. package/dist/commands/doctor.js.map +1 -1
  13. package/dist/commands/doctor.test.js +93 -9
  14. package/dist/commands/doctor.test.js.map +1 -1
  15. package/dist/commands/eject.js +7 -0
  16. package/dist/commands/eject.js.map +1 -1
  17. package/dist/commands/eject.test.js +39 -0
  18. package/dist/commands/eject.test.js.map +1 -1
  19. package/dist/commands/info.js +3 -2
  20. package/dist/commands/info.js.map +1 -1
  21. package/dist/commands/init.d.ts +37 -1
  22. package/dist/commands/init.js +149 -40
  23. package/dist/commands/init.js.map +1 -1
  24. package/dist/commands/init.test.js +285 -26
  25. package/dist/commands/init.test.js.map +1 -1
  26. package/dist/commands/install-packages.d.ts +6 -0
  27. package/dist/commands/install-packages.js +20 -0
  28. package/dist/commands/install-packages.js.map +1 -0
  29. package/dist/commands/install-packages.test.d.ts +1 -0
  30. package/dist/commands/install-packages.test.js +55 -0
  31. package/dist/commands/install-packages.test.js.map +1 -0
  32. package/dist/commands/list.js +21 -14
  33. package/dist/commands/list.js.map +1 -1
  34. package/dist/commands/list.test.js +62 -5
  35. package/dist/commands/list.test.js.map +1 -1
  36. package/dist/commands/outdated.js +11 -10
  37. package/dist/commands/outdated.js.map +1 -1
  38. package/dist/commands/outdated.test.js +20 -5
  39. package/dist/commands/outdated.test.js.map +1 -1
  40. package/dist/commands/remove.d.ts +4 -0
  41. package/dist/commands/remove.js +17 -4
  42. package/dist/commands/remove.js.map +1 -1
  43. package/dist/commands/remove.test.js +53 -2
  44. package/dist/commands/remove.test.js.map +1 -1
  45. package/dist/commands/update.d.ts +6 -0
  46. package/dist/commands/update.js +59 -24
  47. package/dist/commands/update.js.map +1 -1
  48. package/dist/commands/update.test.js +273 -31
  49. package/dist/commands/update.test.js.map +1 -1
  50. package/dist/index.js +7 -0
  51. package/dist/index.js.map +1 -1
  52. package/dist/registry.d.ts +8 -0
  53. package/dist/registry.js +32 -24
  54. package/dist/registry.js.map +1 -1
  55. package/dist/registry.test.js +76 -37
  56. package/dist/registry.test.js.map +1 -1
  57. package/dist/testing/set-input-signal.d.ts +13 -0
  58. package/dist/testing/set-input-signal.js +20 -0
  59. package/dist/testing/set-input-signal.js.map +1 -0
  60. package/dist/testing/set-input-signal.test.d.ts +1 -0
  61. package/dist/testing/set-input-signal.test.js +20 -0
  62. package/dist/testing/set-input-signal.test.js.map +1 -0
  63. package/dist/ui/components/action-sheet/action-sheet.test.ts +48 -1
  64. package/dist/ui/components/action-sheet/action-sheet.ts +37 -9
  65. package/dist/ui/components/alert-dialog/alert-dialog.test.ts +49 -2
  66. package/dist/ui/components/alert-dialog/alert-dialog.ts +43 -12
  67. package/dist/ui/components/area-chart/area-chart-render.test.ts +52 -110
  68. package/dist/ui/components/area-chart/area-chart.test.ts +58 -123
  69. package/dist/ui/components/area-chart/area-chart.ts +100 -315
  70. package/dist/ui/components/area-chart/index.ts +2 -3
  71. package/dist/ui/components/bar-chart/bar-chart-render.test.ts +47 -163
  72. package/dist/ui/components/bar-chart/bar-chart.test.ts +83 -37
  73. package/dist/ui/components/bar-chart/bar-chart.ts +140 -294
  74. package/dist/ui/components/bar-chart/index.ts +3 -3
  75. package/dist/ui/components/bottom-sheet/bottom-sheet.test.ts +191 -9
  76. package/dist/ui/components/bottom-sheet/bottom-sheet.ts +136 -20
  77. package/dist/ui/components/button/button-render.test.ts +19 -0
  78. package/dist/ui/components/button/button.ts +18 -2
  79. package/dist/ui/components/candlestick-chart/candlestick-chart-render.test.ts +48 -163
  80. package/dist/ui/components/candlestick-chart/candlestick-chart.test.ts +106 -62
  81. package/dist/ui/components/candlestick-chart/candlestick-chart.ts +150 -323
  82. package/dist/ui/components/candlestick-chart/index.ts +3 -4
  83. package/dist/ui/components/chart/chart-focus.test.ts +132 -0
  84. package/dist/ui/components/chart/chart-focus.ts +115 -0
  85. package/dist/ui/components/chart/chart-presets.test.ts +139 -0
  86. package/dist/ui/components/chart/chart-presets.ts +271 -0
  87. package/dist/ui/components/chart/chart-render.test.ts +730 -0
  88. package/dist/ui/components/chart/chart-scene.test.ts +589 -0
  89. package/dist/ui/components/chart/chart-scene.ts +483 -0
  90. package/dist/ui/components/chart/chart-theme.test.ts +85 -0
  91. package/dist/ui/components/chart/chart-theme.ts +198 -0
  92. package/dist/ui/components/chart/chart-zoom.test.ts +159 -0
  93. package/dist/ui/components/chart/chart-zoom.ts +162 -0
  94. package/dist/ui/components/chart/chart.ts +921 -0
  95. package/dist/ui/components/chart/index.ts +24 -0
  96. package/dist/ui/components/dialog/dialog.test.ts +49 -2
  97. package/dist/ui/components/dialog/dialog.ts +43 -12
  98. package/dist/ui/components/icon/icon-theme.test.ts +133 -0
  99. package/dist/ui/components/icon/icon-theme.ts +204 -0
  100. package/dist/ui/components/icon/icon.test.ts +51 -2
  101. package/dist/ui/components/icon/icon.ts +38 -21
  102. package/dist/ui/components/line-chart/index.ts +2 -3
  103. package/dist/ui/components/line-chart/line-chart-render.test.ts +69 -107
  104. package/dist/ui/components/line-chart/line-chart.test.ts +63 -45
  105. package/dist/ui/components/line-chart/line-chart.ts +83 -241
  106. package/dist/ui/components/list/list.test.ts +69 -2
  107. package/dist/ui/components/list/list.ts +21 -15
  108. package/dist/ui/components/nav-drawer/nav-drawer.test.ts +80 -1
  109. package/dist/ui/components/nav-drawer/nav-drawer.ts +66 -11
  110. package/dist/ui/components/pagination/pagination.test.ts +62 -0
  111. package/dist/ui/components/pagination/pagination.ts +55 -8
  112. package/dist/ui/components/scatter-chart/index.ts +2 -3
  113. package/dist/ui/components/scatter-chart/scatter-chart-render.test.ts +51 -144
  114. package/dist/ui/components/scatter-chart/scatter-chart.test.ts +57 -25
  115. package/dist/ui/components/scatter-chart/scatter-chart.ts +104 -250
  116. package/dist/ui/components/scroll-area/scroll-area.test.ts +37 -2
  117. package/dist/ui/components/scroll-area/scroll-area.ts +8 -8
  118. package/dist/ui/components/spinner/spinner.test.ts +27 -0
  119. package/dist/ui/components/spinner/spinner.ts +33 -23
  120. package/dist/ui/components/toast/toast.test.ts +37 -0
  121. package/dist/ui/components/toast/toast.ts +20 -4
  122. package/dist/ui/theme/chart-colors.test.ts +30 -0
  123. package/dist/ui/theme/dark.css +8 -0
  124. package/dist/ui/theme/default.css +8 -0
  125. package/dist/ui/theme/icon-colors.test.ts +30 -0
  126. package/dist/ui/theme/tailwind-plugin.ts +10 -1
  127. package/dist/utils/installed.d.ts +17 -0
  128. package/dist/utils/installed.js +27 -0
  129. package/dist/utils/installed.js.map +1 -0
  130. package/dist/utils/installed.test.d.ts +1 -0
  131. package/dist/utils/installed.test.js +41 -0
  132. package/dist/utils/installed.test.js.map +1 -0
  133. package/dist/utils/interactive.d.ts +9 -0
  134. package/dist/utils/interactive.js +10 -0
  135. package/dist/utils/interactive.js.map +1 -0
  136. package/dist/utils/interactive.test.d.ts +1 -0
  137. package/dist/utils/interactive.test.js +24 -0
  138. package/dist/utils/interactive.test.js.map +1 -0
  139. package/dist/utils/packages.d.ts +36 -0
  140. package/dist/utils/packages.js +70 -0
  141. package/dist/utils/packages.js.map +1 -0
  142. package/dist/utils/packages.test.d.ts +1 -0
  143. package/dist/utils/packages.test.js +89 -0
  144. package/dist/utils/packages.test.js.map +1 -0
  145. package/dist/utils/resolve-paths.d.ts +17 -1
  146. package/dist/utils/resolve-paths.js +23 -3
  147. package/dist/utils/resolve-paths.js.map +1 -1
  148. package/dist/utils/resolve-paths.test.js +34 -2
  149. package/dist/utils/resolve-paths.test.js.map +1 -1
  150. package/package.json +5 -1
  151. package/dist/ui/components/cartesian-chart/cartesian-chart-render.test.ts +0 -188
  152. package/dist/ui/components/cartesian-chart/cartesian-chart.test.ts +0 -525
  153. package/dist/ui/components/cartesian-chart/cartesian-chart.ts +0 -1140
  154. package/dist/ui/components/cartesian-chart/index.ts +0 -18
@@ -0,0 +1,115 @@
1
+ import {
2
+ resolveChartFocusStrategy,
3
+ resolveChartPointerFocus,
4
+ restoreChartFocusPoint,
5
+ sameChartPointIdentity,
6
+ } from '@tanstack/charts/cursor/host';
7
+ import { focusDisabled } from '@tanstack/charts/focus/disabled';
8
+ import {
9
+ findNearestPoint,
10
+ viewportInteractionPoints,
11
+ } from '@tanstack/charts/scene';
12
+ import type {
13
+ ChartDefinition,
14
+ ChartFocusStrategy,
15
+ ChartPoint,
16
+ ChartScene,
17
+ } from '@tanstack/charts/types';
18
+
19
+ /**
20
+ * Which data points a touch lands on, and the order a screen reader steps
21
+ * through them.
22
+ *
23
+ * A port of TanStack's React Native focus model: it runs the definition's focus
24
+ * strategy (nearest point, a whole x column for `focus: 'x'`, …) over the
25
+ * scene's points rather than hit-testing painted shapes, which a single
26
+ * rasterized `<svg>` couldn't do anyway. One chart-level touch handler plus this
27
+ * model is also what TanStack recommends for native hosts, instead of a
28
+ * responder per mark.
29
+ */
30
+ export type ChartFocusModel = {
31
+ /** The focused group for a touch at scene coordinates, or `[]`. */
32
+ resolve(x: number, y: number): readonly ChartPoint[];
33
+ /** The group that focusing `point` shows (its whole x column, say). */
34
+ group(point: ChartPoint): readonly ChartPoint[];
35
+ /** Points in reading order, for stepping with accessibility actions. */
36
+ readonly navigation: readonly ChartPoint[];
37
+ /** The same datum's point in a newer scene, or `null` if it's gone. */
38
+ restore(point: ChartPoint): ChartPoint | null;
39
+ };
40
+
41
+ /** TanStack's default reach for pointer focus, in scene pixels. */
42
+ const DEFAULT_MAX_FOCUS_DISTANCE = 48;
43
+
44
+ export const createChartFocusModel = (
45
+ scene: ChartScene<any, any, any>,
46
+ definition: ChartDefinition<any, any, any>,
47
+ ): ChartFocusModel => {
48
+ const points = viewportInteractionPoints(scene);
49
+ const strategy: ChartFocusStrategy<any, any, any> | undefined =
50
+ definition.focus === false
51
+ ? focusDisabled
52
+ : resolveChartFocusStrategy(definition.focus);
53
+ const spatialIndex =
54
+ definition.focus === false
55
+ ? undefined
56
+ : definition.spatialIndex?.(points, { scene });
57
+ const maxDistance = definition.maxFocusDistance ?? DEFAULT_MAX_FOCUS_DISTANCE;
58
+
59
+ return {
60
+ resolve(x, y) {
61
+ const focused = resolveChartPointerFocus(
62
+ scene,
63
+ strategy,
64
+ x,
65
+ y,
66
+ maxDistance,
67
+ points,
68
+ );
69
+ if (focused) return focused;
70
+ const point = spatialIndex
71
+ ? spatialIndex.findNearest(x, y, maxDistance)
72
+ : findNearestPoint(scene, x, y, maxDistance, points);
73
+ const visible = point ? restoreChartFocusPoint(points, point) : null;
74
+ return visible ? [visible] : [];
75
+ },
76
+ group(point) {
77
+ return strategy?.group(points, { point }) ?? [point];
78
+ },
79
+ navigation: strategy?.navigation(points) ?? readingOrder(points),
80
+ restore(point) {
81
+ return restoreChartFocusPoint(points, point);
82
+ },
83
+ };
84
+ };
85
+
86
+ /** The point `direction` steps away from `current` in navigation order. */
87
+ export const adjacentFocusPoint = (
88
+ model: ChartFocusModel,
89
+ current: ChartPoint | null,
90
+ direction: -1 | 1,
91
+ ): ChartPoint | null => {
92
+ const { navigation } = model;
93
+ const index = current
94
+ ? navigation.findIndex((point) => sameChartPointIdentity(point, current))
95
+ : -1;
96
+ const next =
97
+ index < 0
98
+ ? direction > 0
99
+ ? 0
100
+ : navigation.length - 1
101
+ : Math.max(0, Math.min(navigation.length - 1, index + direction));
102
+ return navigation[next] ?? null;
103
+ };
104
+
105
+ /**
106
+ * Left to right, then top to bottom; ties keep scene order.
107
+ */
108
+ const readingOrder = (points: readonly ChartPoint[]): ChartPoint[] =>
109
+ points
110
+ .map((point, index) => ({ point, index }))
111
+ .sort(
112
+ (a, b) =>
113
+ a.point.x - b.point.x || a.point.y - b.point.y || a.index - b.index,
114
+ )
115
+ .map(({ point }) => point);
@@ -0,0 +1,139 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { linearAxis, minSpacing, presetAxes } from './chart-presets';
3
+
4
+ /**
5
+ * The configured domain of an axis config's scale instance.
6
+ */
7
+ const domainOf = (axis: { scale: unknown }): number[] =>
8
+ (axis.scale as { domain: () => number[] }).domain();
9
+
10
+ describe('linearAxis', () => {
11
+ const base = { tickCount: 5, grid: true };
12
+
13
+ it('fits the data and rounds out to nice values', () => {
14
+ expect(domainOf(linearAxis({ ...base, values: [3, 47] }))).toEqual([0, 50]);
15
+ });
16
+
17
+ it('keeps fixed ends exact and rounds only the free one', () => {
18
+ expect(
19
+ domainOf(linearAxis({ ...base, values: [3, 47], min: 1, max: 2 })),
20
+ ).toEqual([1, 2]);
21
+ expect(domainOf(linearAxis({ ...base, values: [3, 47], min: 1 }))).toEqual([
22
+ 1, 50,
23
+ ]);
24
+ });
25
+
26
+ it('includes extra values, such as a baseline', () => {
27
+ expect(
28
+ domainOf(linearAxis({ ...base, values: [5, 10], include: [0] })),
29
+ ).toEqual([0, 10]);
30
+ });
31
+
32
+ it('gives flat and empty data a span around the value', () => {
33
+ expect(domainOf(linearAxis({ ...base, values: [10, 10] }))).toEqual([
34
+ 4, 16,
35
+ ]);
36
+ expect(domainOf(linearAxis({ ...base, values: [0] }))).toEqual([-1, 1]);
37
+ expect(domainOf(linearAxis({ ...base, values: [] }))).toEqual([0, 1]);
38
+ expect(domainOf(linearAxis({ ...base, values: [Number.NaN] }))).toEqual([
39
+ 0, 1,
40
+ ]);
41
+ });
42
+
43
+ it('orders reversed fixed ends', () => {
44
+ expect(
45
+ domainOf(linearAxis({ ...base, values: [], min: 10, max: 0 })),
46
+ ).toEqual([0, 10]);
47
+ });
48
+
49
+ it('widens by a margin, then by padding fractions', () => {
50
+ expect(
51
+ domainOf(linearAxis({ ...base, values: [], min: 0, max: 10, margin: 1 })),
52
+ ).toEqual([-1, 11]);
53
+ // 10% each side of a 0–8 domain: the plot spans 10 units, inset 1 each side.
54
+ const [lo, hi] = domainOf(
55
+ linearAxis({ ...base, values: [], min: 0, max: 8, padding: [0.1, 0.1] }),
56
+ );
57
+ expect(lo).toBeCloseTo(-1);
58
+ expect(hi).toBeCloseTo(9);
59
+ // Padding that leaves no plot is ignored.
60
+ expect(
61
+ domainOf(
62
+ linearAxis({
63
+ ...base,
64
+ values: [],
65
+ min: 0,
66
+ max: 8,
67
+ padding: [0.6, 0.6],
68
+ }),
69
+ ),
70
+ ).toEqual([0, 8]);
71
+ });
72
+
73
+ it('configures grid, label and ticks', () => {
74
+ const format = (value: number) => `${value}%`;
75
+ const axis = linearAxis({
76
+ ...base,
77
+ values: [0, 1],
78
+ label: 'Share',
79
+ format,
80
+ grid: false,
81
+ });
82
+ expect(axis.grid).toBe(false);
83
+ expect(axis.axis).toEqual({ label: 'Share', ticks: { count: 5, format } });
84
+ expect(
85
+ (
86
+ linearAxis({ ...base, values: [0, 1], label: '' }).axis as {
87
+ label?: string;
88
+ }
89
+ ).label,
90
+ ).toBeUndefined();
91
+ });
92
+ });
93
+
94
+ describe('minSpacing', () => {
95
+ it('finds the smallest gap between distinct values', () => {
96
+ expect(minSpacing([5, 1, 3, 3, 4])).toBe(1);
97
+ expect(minSpacing([0, 10])).toBe(10);
98
+ });
99
+
100
+ it('defaults to 1 without a gap', () => {
101
+ expect(minSpacing([])).toBe(1);
102
+ expect(minSpacing([2])).toBe(1);
103
+ expect(minSpacing([2, 2])).toBe(1);
104
+ });
105
+ });
106
+
107
+ describe('presetAxes', () => {
108
+ it('maps the shared options onto both axes', () => {
109
+ const axes = presetAxes(
110
+ {
111
+ tickCount: 3,
112
+ showGrid: false,
113
+ showXGrid: true,
114
+ xAxisLabel: 'X',
115
+ yAxisLabel: 'Y',
116
+ padding: { left: 0.1, right: 0.1, top: 0.1, bottom: 0.1 },
117
+ xMin: 0,
118
+ xMax: 8,
119
+ yMin: 0,
120
+ yMax: 8,
121
+ },
122
+ { x: [], y: [], xMargin: 0 },
123
+ );
124
+ expect(axes.x.grid).toBe(true);
125
+ expect(axes.y.grid).toBe(false);
126
+ expect(axes.x.axis).toMatchObject({ label: 'X', ticks: { count: 3 } });
127
+ expect(axes.y.axis).toMatchObject({ label: 'Y' });
128
+ expect(domainOf(axes.x)[0]).toBeCloseTo(-1);
129
+ expect(domainOf(axes.y)[1]).toBeCloseTo(9);
130
+ });
131
+
132
+ it('defaults to five ticks, a y grid and no padding', () => {
133
+ const axes = presetAxes({}, { x: [0, 10], y: [0, 10], yInclude: [-5] });
134
+ expect(axes.x.grid).toBe(false);
135
+ expect(axes.y.grid).toBe(true);
136
+ expect(domainOf(axes.x)).toEqual([0, 10]);
137
+ expect(domainOf(axes.y)).toEqual([-10, 10]);
138
+ });
139
+ });
@@ -0,0 +1,271 @@
1
+ import { Directive, input, viewChild } from '@angular/core';
2
+ import { scaleLinear } from '@tanstack/charts/scales/linear';
3
+ import type {
4
+ ChartPoint,
5
+ ChartPositionScaleOptions,
6
+ } from '@tanstack/charts/types';
7
+
8
+ import { UiChart } from './chart';
9
+ import type { ChartColorSchemeOption } from './chart-theme';
10
+ import type { ChartZoomAxes } from './chart-zoom';
11
+
12
+ /**
13
+ * Shared pieces of the chart presets (`<ui-line-chart>`, `<ui-bar-chart>`, …).
14
+ * Each preset is a thin component that turns its inputs into a TanStack
15
+ * definition for `<ui-chart>`; this file holds the axis handling and the inputs
16
+ * they all share, so every preset behaves the same way.
17
+ */
18
+
19
+ /** A point in x/y data space, the datum the line, area and scatter presets plot. */
20
+ export type XYDatum = {
21
+ readonly x: number;
22
+ readonly y: number;
23
+ };
24
+
25
+ /**
26
+ * Inner inset per side, as a **fraction** (`0`–`1`) of the plot's width or
27
+ * height, so the first and last marks don't sit on the frame. Fractions keep
28
+ * the inset proportional as the chart resizes. Omitted sides default to `0`.
29
+ */
30
+ export type ChartPadding = {
31
+ readonly top?: number;
32
+ readonly right?: number;
33
+ readonly bottom?: number;
34
+ readonly left?: number;
35
+ };
36
+
37
+ /** The axis, grid and tooltip options every cartesian preset takes. */
38
+ export type CartesianPresetOptions = {
39
+ readonly tickCount?: number;
40
+ readonly showGrid?: boolean;
41
+ readonly showXGrid?: boolean;
42
+ readonly xAxisLabel?: string;
43
+ readonly yAxisLabel?: string;
44
+ readonly padding?: ChartPadding;
45
+ readonly xMin?: number;
46
+ readonly xMax?: number;
47
+ readonly yMin?: number;
48
+ readonly yMax?: number;
49
+ readonly xTickFormat?: (value: number) => string;
50
+ readonly yTickFormat?: (value: number) => string;
51
+ /** `false` turns the tooltip off. */
52
+ readonly tooltip?: boolean;
53
+ };
54
+
55
+ /**
56
+ * The x and y axes for a preset's data, from the shared options: `x` and `y`
57
+ * are the values each axis fits, `xMargin` widens x by data units (half a bar),
58
+ * `yInclude` keeps values like a baseline in view.
59
+ */
60
+ export const presetAxes = (
61
+ options: CartesianPresetOptions,
62
+ data: {
63
+ readonly x: readonly number[];
64
+ readonly y: readonly number[];
65
+ readonly xMargin?: number;
66
+ readonly yInclude?: readonly number[];
67
+ },
68
+ ) => {
69
+ const { tickCount = 5, padding = {} } = options;
70
+ return {
71
+ x: linearAxis({
72
+ values: data.x,
73
+ min: options.xMin,
74
+ max: options.xMax,
75
+ margin: data.xMargin,
76
+ tickCount,
77
+ padding: [padding.left ?? 0, padding.right ?? 0],
78
+ format: options.xTickFormat,
79
+ label: options.xAxisLabel,
80
+ grid: options.showXGrid ?? false,
81
+ }),
82
+ y: linearAxis({
83
+ values: data.y,
84
+ include: data.yInclude,
85
+ min: options.yMin,
86
+ max: options.yMax,
87
+ tickCount,
88
+ padding: [padding.bottom ?? 0, padding.top ?? 0],
89
+ format: options.yTickFormat,
90
+ label: options.yAxisLabel,
91
+ grid: options.showGrid ?? true,
92
+ }),
93
+ };
94
+ };
95
+
96
+ export type LinearAxisOptions = {
97
+ /** Data values on this axis, to fit the domain to. */
98
+ readonly values: readonly number[];
99
+ /** Fixed ends; a missing end fits the data. */
100
+ readonly min?: number;
101
+ readonly max?: number;
102
+ /** Values that must stay in view, such as an area's baseline. */
103
+ readonly include?: readonly number[];
104
+ readonly tickCount: number;
105
+ /** Inset at the low and high end, as fractions of the plot (see {@link ChartPadding}). */
106
+ readonly padding?: readonly [number, number];
107
+ /** Extra data units added beyond each end, e.g. half a bar so the edge bars fit. */
108
+ readonly margin?: number;
109
+ readonly format?: (value: number) => string;
110
+ readonly label?: string;
111
+ readonly grid: boolean;
112
+ };
113
+
114
+ /**
115
+ * A linear axis config: the domain fits the data (and `include`), is rounded
116
+ * out to nice tick values unless both ends are fixed, then widened by `margin`
117
+ * and `padding`. Flat or empty data gets a span around the value so the marks
118
+ * sit mid-plot instead of collapsing onto one edge.
119
+ *
120
+ * The domain is computed here, not left to TanStack's inference, because
121
+ * padding has to apply after rounding, and a zoomable axis needs a configured
122
+ * scale to carry its viewport.
123
+ */
124
+ export const linearAxis = (
125
+ options: LinearAxisOptions,
126
+ ): ChartPositionScaleOptions<number> => {
127
+ const values = [...options.values, ...(options.include ?? [])].filter(
128
+ Number.isFinite,
129
+ );
130
+ let lo = options.min ?? (values.length ? Math.min(...values) : 0);
131
+ let hi = options.max ?? (values.length ? Math.max(...values) : 1);
132
+ if (lo === hi) {
133
+ const pad = lo === 0 ? 1 : Math.abs(lo) * 0.5;
134
+ lo -= pad;
135
+ hi += pad;
136
+ }
137
+ if (lo > hi) [lo, hi] = [hi, lo];
138
+ if (options.min === undefined || options.max === undefined) {
139
+ const nice = scaleLinear()
140
+ .domain([lo, hi])
141
+ .nice(options.tickCount)
142
+ .domain();
143
+ lo = options.min ?? nice[0];
144
+ hi = options.max ?? nice[1];
145
+ }
146
+ const margin = options.margin ?? 0;
147
+ lo -= margin;
148
+ hi += margin;
149
+ const [padLo, padHi] = options.padding ?? [0, 0];
150
+ const keep = 1 - padLo - padHi;
151
+ if ((padLo || padHi) && keep > 0) {
152
+ const span = (hi - lo) / keep;
153
+ lo -= padLo * span;
154
+ hi = lo + span;
155
+ }
156
+ return {
157
+ scale: scaleLinear().domain([lo, hi]),
158
+ grid: options.grid,
159
+ axis: {
160
+ label: options.label || undefined,
161
+ ticks: { count: options.tickCount, format: options.format },
162
+ },
163
+ };
164
+ };
165
+
166
+ /** Smallest gap between neighboring values, `1` for fewer than two. */
167
+ export const minSpacing = (values: readonly number[]): number => {
168
+ const sorted = [...new Set(values)].sort((a, b) => a - b);
169
+ let spacing = Infinity;
170
+ for (let i = 1; i < sorted.length; i++) {
171
+ spacing = Math.min(spacing, sorted[i]! - sorted[i - 1]!);
172
+ }
173
+ return Number.isFinite(spacing) && spacing > 0 ? spacing : 1;
174
+ };
175
+
176
+ /**
177
+ * The inputs and zoom API every cartesian preset shares. A preset extends this
178
+ * and adds its own data and mark inputs, and renders `<ui-chart #chart>`
179
+ * with the shared bindings.
180
+ */
181
+ @Directive()
182
+ export abstract class UiCartesianPreset<TDatum> {
183
+ /** Fixed width in px. Omit to fill the container. */
184
+ readonly width = input<number>();
185
+ readonly height = input(200);
186
+ /** Target number of ticks per axis; the axis picks nice values near it. */
187
+ readonly tickCount = input(5);
188
+ /** Horizontal gridlines at the y ticks. */
189
+ readonly showGrid = input(true);
190
+ /** Vertical gridlines at the x ticks. */
191
+ readonly showXGrid = input(false);
192
+ readonly xAxisLabel = input('');
193
+ readonly yAxisLabel = input('');
194
+ readonly padding = input<ChartPadding>({});
195
+ readonly xMin = input<number>();
196
+ readonly xMax = input<number>();
197
+ readonly yMin = input<number>();
198
+ readonly yMax = input<number>();
199
+ readonly xTickFormat = input<(value: number) => string>();
200
+ readonly yTickFormat = input<(value: number) => string>();
201
+ /** Shows a tooltip for the touched point; a tap pins it. */
202
+ readonly tooltip = input(true);
203
+ /** What a screen reader announces for the chart. */
204
+ readonly label = input('Chart');
205
+ /** Light, dark or `'system'` colors; the chart theme's setting when unset. */
206
+ readonly colorScheme = input<Exclude<ChartColorSchemeOption, Function>>();
207
+ readonly zoomable = input(false);
208
+ readonly zoomAxes = input<ChartZoomAxes>('xy');
209
+ readonly showZoomControls = input(true);
210
+ readonly minZoom = input(1);
211
+ readonly maxZoom = input(8);
212
+ readonly zoomStep = input(1.4);
213
+ readonly userClass = input<string>('', { alias: 'class' });
214
+
215
+ protected readonly chart = viewChild.required(UiChart);
216
+
217
+ /**
218
+ * The shared inputs, as the options every preset's definition takes.
219
+ */
220
+ protected commonOptions(): CartesianPresetOptions {
221
+ return {
222
+ tickCount: this.tickCount(),
223
+ showGrid: this.showGrid(),
224
+ showXGrid: this.showXGrid(),
225
+ xAxisLabel: this.xAxisLabel(),
226
+ yAxisLabel: this.yAxisLabel(),
227
+ padding: this.padding(),
228
+ xMin: this.xMin(),
229
+ xMax: this.xMax(),
230
+ yMin: this.yMin(),
231
+ yMax: this.yMax(),
232
+ xTickFormat: this.xTickFormat(),
233
+ yTickFormat: this.yTickFormat(),
234
+ tooltip: this.tooltip(),
235
+ };
236
+ }
237
+
238
+ /**
239
+ * The tapped datum, unwrapped from TanStack's point.
240
+ */
241
+ protected datumOf(point: ChartPoint | null): TDatum | null {
242
+ return point ? (point.datum as TDatum) : null;
243
+ }
244
+
245
+ zoomIn(): void {
246
+ this.chart().zoomIn();
247
+ }
248
+
249
+ zoomOut(): void {
250
+ this.chart().zoomOut();
251
+ }
252
+
253
+ resetZoom(): void {
254
+ this.chart().resetZoom();
255
+ }
256
+ }
257
+
258
+ /** The `<ui-chart>` bindings a preset passes straight through. */
259
+ export const PRESET_CHART_BINDINGS = `
260
+ [width]="width()"
261
+ [height]="height()"
262
+ [label]="label()"
263
+ [colorScheme]="colorScheme()"
264
+ [class]="userClass()"
265
+ [zoomable]="zoomable()"
266
+ [zoomAxes]="zoomAxes()"
267
+ [showZoomControls]="showZoomControls()"
268
+ [minZoom]="minZoom()"
269
+ [maxZoom]="maxZoom()"
270
+ [zoomStep]="zoomStep()"
271
+ `;