@forgecharts/sdk 1.1.23

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 (101) hide show
  1. package/package.json +50 -0
  2. package/src/__tests__/backwardCompatibility.test.ts +191 -0
  3. package/src/__tests__/candleInvariant.test.ts +500 -0
  4. package/src/__tests__/public-api-surface.ts +76 -0
  5. package/src/__tests__/timeframeBoundary.test.ts +583 -0
  6. package/src/api/DrawingManager.ts +188 -0
  7. package/src/api/EventBus.ts +53 -0
  8. package/src/api/IndicatorDAG.ts +389 -0
  9. package/src/api/IndicatorRegistry.ts +47 -0
  10. package/src/api/LayoutManager.ts +72 -0
  11. package/src/api/PaneManager.ts +129 -0
  12. package/src/api/ReferenceAPI.ts +195 -0
  13. package/src/api/TChart.ts +881 -0
  14. package/src/api/createChart.ts +43 -0
  15. package/src/api/drawing tools/fib gann menu/fibRetracement.ts +27 -0
  16. package/src/api/drawing tools/lines menu/crossLine.ts +21 -0
  17. package/src/api/drawing tools/lines menu/disjointChannel.ts +74 -0
  18. package/src/api/drawing tools/lines menu/extendedLine.ts +22 -0
  19. package/src/api/drawing tools/lines menu/flatTopBottom.ts +45 -0
  20. package/src/api/drawing tools/lines menu/horizontal.ts +24 -0
  21. package/src/api/drawing tools/lines menu/horizontalRay.ts +25 -0
  22. package/src/api/drawing tools/lines menu/infoLine.ts +127 -0
  23. package/src/api/drawing tools/lines menu/insidePitchfork.ts +21 -0
  24. package/src/api/drawing tools/lines menu/modifiedSchiffPitchfork.ts +18 -0
  25. package/src/api/drawing tools/lines menu/parallelChannel.ts +47 -0
  26. package/src/api/drawing tools/lines menu/pitchfork.ts +15 -0
  27. package/src/api/drawing tools/lines menu/ray.ts +28 -0
  28. package/src/api/drawing tools/lines menu/regressionTrend.ts +157 -0
  29. package/src/api/drawing tools/lines menu/schiffPitchfork.ts +18 -0
  30. package/src/api/drawing tools/lines menu/trendAngle.ts +64 -0
  31. package/src/api/drawing tools/lines menu/trendline.ts +16 -0
  32. package/src/api/drawing tools/lines menu/vertical.ts +16 -0
  33. package/src/api/drawing tools/pointers menu/crosshair.ts +17 -0
  34. package/src/api/drawing tools/pointers menu/cursor.ts +16 -0
  35. package/src/api/drawing tools/pointers menu/demonstration.ts +35 -0
  36. package/src/api/drawing tools/pointers menu/dot.ts +26 -0
  37. package/src/api/drawing tools/shapes menu/rectangle.ts +24 -0
  38. package/src/api/drawing tools/shapes menu/text.ts +30 -0
  39. package/src/api/drawingUtils.ts +82 -0
  40. package/src/core/CanvasLayer.ts +77 -0
  41. package/src/core/Chart.ts +917 -0
  42. package/src/core/CoordTransform.ts +282 -0
  43. package/src/core/Crosshair.ts +207 -0
  44. package/src/core/IndicatorEngine.ts +216 -0
  45. package/src/core/InteractionManager.ts +899 -0
  46. package/src/core/PriceScale.ts +133 -0
  47. package/src/core/Series.ts +132 -0
  48. package/src/core/TimeScale.ts +175 -0
  49. package/src/datafeed/DatafeedConnector.ts +300 -0
  50. package/src/engine/CandleEngine.ts +458 -0
  51. package/src/engine/__tests__/CandleEngine.test.ts +402 -0
  52. package/src/engine/candleInvariants.ts +172 -0
  53. package/src/engine/mergeUtils.ts +93 -0
  54. package/src/engine/timeframeUtils.ts +118 -0
  55. package/src/index.ts +190 -0
  56. package/src/internal.ts +41 -0
  57. package/src/licensing/ChartRuntimeResolver.ts +380 -0
  58. package/src/licensing/LicenseManager.ts +131 -0
  59. package/src/licensing/__tests__/ChartRuntimeResolver.test.ts +207 -0
  60. package/src/licensing/__tests__/LicenseManager.test.ts +180 -0
  61. package/src/licensing/licenseTypes.ts +19 -0
  62. package/src/pine/PineCompiler.ts +68 -0
  63. package/src/pine/diagnostics.ts +30 -0
  64. package/src/pine/index.ts +7 -0
  65. package/src/pine/pine-ast.ts +163 -0
  66. package/src/pine/pine-lexer.ts +265 -0
  67. package/src/pine/pine-parser.ts +439 -0
  68. package/src/pine/pine-transpiler.ts +301 -0
  69. package/src/pixi/LayerName.ts +35 -0
  70. package/src/pixi/PixiCandlestickRenderer.ts +125 -0
  71. package/src/pixi/PixiChart.ts +425 -0
  72. package/src/pixi/PixiCrosshairRenderer.ts +134 -0
  73. package/src/pixi/PixiDrawingRenderer.ts +121 -0
  74. package/src/pixi/PixiGridRenderer.ts +136 -0
  75. package/src/pixi/PixiLayerManager.ts +102 -0
  76. package/src/renderers/CandlestickRenderer.ts +130 -0
  77. package/src/renderers/HistogramRenderer.ts +63 -0
  78. package/src/renderers/LineRenderer.ts +77 -0
  79. package/src/theme/colors.ts +21 -0
  80. package/src/tools/barDivergenceCheck.ts +305 -0
  81. package/src/trading/TradingOverlayStore.ts +161 -0
  82. package/src/trading/UnmanagedIngestion.ts +156 -0
  83. package/src/trading/__tests__/ManagedTradingController.test.ts +338 -0
  84. package/src/trading/__tests__/TradingOverlayStore.test.ts +323 -0
  85. package/src/trading/__tests__/UnmanagedIngestion.test.ts +205 -0
  86. package/src/trading/managed/ManagedTradingController.ts +292 -0
  87. package/src/trading/managed/managedCapabilities.ts +98 -0
  88. package/src/trading/managed/managedTypes.ts +151 -0
  89. package/src/trading/tradingTypes.ts +135 -0
  90. package/src/tscript/TScriptIndicator.ts +54 -0
  91. package/src/tscript/ast.ts +105 -0
  92. package/src/tscript/lexer.ts +190 -0
  93. package/src/tscript/parser.ts +334 -0
  94. package/src/tscript/runtime.ts +525 -0
  95. package/src/tscript/series.ts +84 -0
  96. package/src/types/IChart.ts +56 -0
  97. package/src/types/IRenderer.ts +16 -0
  98. package/src/types/ISeries.ts +30 -0
  99. package/tsconfig.json +22 -0
  100. package/tsup.config.ts +15 -0
  101. package/vitest.config.ts +25 -0
@@ -0,0 +1,133 @@
1
+ import type { PriceRange, ChartColors, PriceScaleOptions } from '@forgecharts/types';
2
+ import { mapRange, computeTicks, roundToPrecision } from '@forgecharts/utils';
3
+ import type { CanvasLayer } from './CanvasLayer';
4
+
5
+ const AXIS_WIDTH = 70;
6
+ const TIME_SCALE_HEIGHT = 30;
7
+
8
+ /**
9
+ * PriceScale — manages the vertical price axis.
10
+ * Auto-scales to the visible data range and renders price labels.
11
+ */
12
+ export class PriceScale {
13
+ private _visibleRange: PriceRange;
14
+ private readonly _layer: CanvasLayer;
15
+ private readonly _colors: ChartColors;
16
+ private readonly _options: Required<PriceScaleOptions>;
17
+
18
+ constructor(layer: CanvasLayer, colors: ChartColors, options: PriceScaleOptions = {}) {
19
+ this._layer = layer;
20
+ this._colors = colors;
21
+ this._options = {
22
+ autoScale: options.autoScale ?? true,
23
+ invertScale: options.invertScale ?? false,
24
+ borderVisible: options.borderVisible ?? true,
25
+ ticksVisible: options.ticksVisible ?? true,
26
+ mode: options.mode ?? 'normal',
27
+ width: options.width ?? AXIS_WIDTH,
28
+ };
29
+ this._visibleRange = { min: 0, max: 100 };
30
+ }
31
+
32
+ get visibleRange(): PriceRange {
33
+ return this._visibleRange;
34
+ }
35
+
36
+ get width(): number {
37
+ return this._options.width;
38
+ }
39
+
40
+ setVisibleRange(range: PriceRange): void {
41
+ this._visibleRange = range;
42
+ }
43
+
44
+ /**
45
+ * Converts a price value to a canvas Y coordinate.
46
+ */
47
+ priceToY(price: number, canvasHeight: number): number {
48
+ const plotHeight = canvasHeight - TIME_SCALE_HEIGHT;
49
+ const { min, max } = this._visibleRange;
50
+
51
+ if (this._options.mode === 'logarithmic') {
52
+ const logMin = Math.log(Math.max(min, 1e-10));
53
+ const logMax = Math.log(Math.max(max, 1e-10));
54
+ const logPrice = Math.log(Math.max(price, 1e-10));
55
+ return mapRange(logPrice, logMax, logMin, 0, plotHeight);
56
+ }
57
+
58
+ return this._options.invertScale
59
+ ? mapRange(price, max, min, 0, plotHeight)
60
+ : mapRange(price, max, min, 0, plotHeight);
61
+ }
62
+
63
+ /**
64
+ * Converts a canvas Y coordinate back to a price value.
65
+ */
66
+ yToPrice(y: number, canvasHeight: number): number {
67
+ const plotHeight = canvasHeight - TIME_SCALE_HEIGHT;
68
+ const { min, max } = this._visibleRange;
69
+ return mapRange(y, 0, plotHeight, max, min);
70
+ }
71
+
72
+ render(ctx: CanvasRenderingContext2D, width: number, height: number): void {
73
+ if (!this._options.ticksVisible) return;
74
+
75
+ const axisX = width - this._options.width;
76
+ const plotHeight = height - TIME_SCALE_HEIGHT;
77
+
78
+ // Axis separator
79
+ if (this._options.borderVisible) {
80
+ ctx.beginPath();
81
+ ctx.strokeStyle = this._colors.border;
82
+ ctx.lineWidth = 1;
83
+ ctx.moveTo(axisX, 0);
84
+ ctx.lineTo(axisX, plotHeight);
85
+ ctx.stroke();
86
+ }
87
+
88
+ // Compute ticks
89
+ const approxTicks = Math.max(2, Math.floor(plotHeight / 50));
90
+ const ticks = computeTicks(this._visibleRange.min, this._visibleRange.max, approxTicks);
91
+
92
+ ctx.fillStyle = this._colors.text;
93
+ ctx.font = '11px system-ui, sans-serif';
94
+ ctx.textAlign = 'left';
95
+ ctx.textBaseline = 'middle';
96
+
97
+ for (const tick of ticks) {
98
+ if (tick < this._visibleRange.min || tick > this._visibleRange.max) continue;
99
+
100
+ const y = this.priceToY(tick, height);
101
+ if (y < 0 || y > plotHeight) continue;
102
+
103
+ // Grid line
104
+ ctx.beginPath();
105
+ ctx.strokeStyle = this._colors.grid;
106
+ ctx.setLineDash([1, 3]);
107
+ ctx.moveTo(0, y);
108
+ ctx.lineTo(axisX, y);
109
+ ctx.stroke();
110
+ ctx.setLineDash([]);
111
+
112
+ // Tick mark
113
+ ctx.beginPath();
114
+ ctx.strokeStyle = this._colors.border;
115
+ ctx.lineWidth = 1;
116
+ ctx.moveTo(axisX, y);
117
+ ctx.lineTo(axisX + 4, y);
118
+ ctx.stroke();
119
+
120
+ // Label
121
+ const label = this._formatPrice(tick);
122
+ ctx.fillText(label, axisX + 6, y);
123
+ }
124
+ }
125
+
126
+ // ─── Private ─────────────────────────────────────────────────────────────────
127
+
128
+ private _formatPrice(price: number): string {
129
+ const range = this._visibleRange.max - this._visibleRange.min;
130
+ const precision = range < 0.01 ? 6 : range < 1 ? 4 : range < 100 ? 2 : 0;
131
+ return roundToPrecision(price, precision).toFixed(precision);
132
+ }
133
+ }
@@ -0,0 +1,132 @@
1
+ import type { OHLCV, SeriesOptions, Viewport, Rect, ChartColors } from '@forgecharts/types';
2
+ import type { ISeries } from '../types/ISeries';
3
+ import type { IRenderer } from '../types/IRenderer';
4
+ import { CandlestickRenderer } from '../renderers/CandlestickRenderer';
5
+ import { LineRenderer } from '../renderers/LineRenderer';
6
+ import { HistogramRenderer } from '../renderers/HistogramRenderer';
7
+
8
+ /**
9
+ * Series holds a dataset (OHLCV[]) and delegates pixel-level rendering
10
+ * to the appropriate IRenderer implementation.
11
+ */
12
+ export class Series implements ISeries {
13
+ private _data: OHLCV[] = [];
14
+ private _options: SeriesOptions;
15
+ private readonly _renderer: IRenderer;
16
+
17
+ /**
18
+ * Optional callback invoked after `setData()` or `update()` mutates the dataset.
19
+ * Used by `PixiChart` to mark the PriceSeries layer dirty without polling.
20
+ */
21
+ onDataChanged?: () => void;
22
+
23
+ constructor(options: SeriesOptions, colors: ChartColors) {
24
+ this._options = options;
25
+ this._renderer = Series._createRenderer(options, colors);
26
+ }
27
+
28
+ // ─── ISeries implementation ──────────────────────────────────────────────────
29
+
30
+ setData(data: readonly OHLCV[]): void {
31
+ this._data = [...data].sort((a, b) => a.time - b.time);
32
+ this.onDataChanged?.();
33
+ }
34
+
35
+ /**
36
+ * Apply a real-time OHLCV tick to the series.
37
+ *
38
+ * Rules (mirrors TradingView / Binance behaviour):
39
+ * - time normalisation : if > 1e12 it is milliseconds → convert to seconds
40
+ * - same candle (time === last.time):
41
+ * open → unchanged (preserved from first tick of this candle)
42
+ * high → max(prev.high, incoming.high, incoming.close)
43
+ * low → min(prev.low, incoming.low, incoming.close)
44
+ * close → incoming.close
45
+ * volume → incoming.volume
46
+ * - new candle (time > last.time) → append
47
+ * - out-of-order (time < last.time) → silently ignored
48
+ */
49
+ update(bar: OHLCV): void {
50
+ // ── 1. Normalise timestamp: ms → s ────────────────────────────────────────
51
+ const t = bar.time > 1e12 ? Math.floor(bar.time / 1000) : bar.time;
52
+
53
+ // ── 2. First bar ever ────────────────────────────────────────────────────
54
+ if (this._data.length === 0) {
55
+ this._data.push({ ...bar, time: t });
56
+ this.onDataChanged?.();
57
+ return;
58
+ }
59
+
60
+ const last = this._data[this._data.length - 1]!;
61
+
62
+ if (t === last.time) {
63
+ // ── 3a. Same candle: replace last element (OHLCV properties are readonly)
64
+ // open stays untouched — never replace the candle's original open
65
+ this._data[this._data.length - 1] = {
66
+ time: last.time,
67
+ open: last.open,
68
+ high: Math.max(last.high, bar.high, bar.close),
69
+ low: Math.min(last.low, bar.low, bar.close),
70
+ close: bar.close,
71
+ volume: bar.volume,
72
+ };
73
+ } else if (t > last.time) {
74
+ // ── 3b. New candle: append ───────────────────────────────────────────────
75
+ this._data.push({ ...bar, time: t });
76
+ }
77
+ // ── 3c. Out-of-order (t < last.time): silently discard ──────────────────
78
+
79
+ this.onDataChanged?.();
80
+ }
81
+
82
+ data(): readonly OHLCV[] {
83
+ return this._data;
84
+ }
85
+
86
+ applyOptions(options: Partial<SeriesOptions>): void {
87
+ this._options = { ...this._options, ...options } as SeriesOptions;
88
+ }
89
+
90
+ options(): SeriesOptions {
91
+ return this._options;
92
+ }
93
+
94
+ // ─── Rendering (called by Chart) ────────────────────────────────────────────
95
+
96
+ render(ctx: CanvasRenderingContext2D, viewport: Viewport, rect: Rect): void {
97
+ if (this._data.length === 0) return;
98
+ const visible = this._options.visible ?? true;
99
+ if (!visible) return;
100
+
101
+ const visibleData = this._getVisibleData(viewport);
102
+ if (visibleData.length === 0) return;
103
+
104
+ this._renderer.draw(ctx, visibleData, viewport, rect, this._options);
105
+ }
106
+
107
+ // ─── Private helpers ─────────────────────────────────────────────────────────
108
+
109
+ private _getVisibleData(viewport: Viewport): OHLCV[] {
110
+ const { from, to } = viewport.timeRange;
111
+ // Small margin so partial bars at edges are included
112
+ return this._data.filter((b) => b.time >= from - 1 && b.time <= to + 1);
113
+ }
114
+
115
+ private static _createRenderer(options: SeriesOptions, colors: ChartColors): IRenderer {
116
+ switch (options.type) {
117
+ case 'candlestick':
118
+ case 'heikinashi':
119
+ case 'bar':
120
+ return new CandlestickRenderer(colors);
121
+ case 'line':
122
+ case 'area':
123
+ return new LineRenderer(colors);
124
+ case 'histogram':
125
+ return new HistogramRenderer(colors);
126
+ default: {
127
+ const _exhaustive: never = options;
128
+ throw new Error(`[ForgeCharts] Unknown series type: ${JSON.stringify(_exhaustive)}`);
129
+ }
130
+ }
131
+ }
132
+ }
@@ -0,0 +1,175 @@
1
+ import type { TimeRange, ChartColors, TimeScaleOptions, Timeframe } from '@forgecharts/types';
2
+ import { mapRange, computeTicks } from '@forgecharts/utils';
3
+ import { formatTimestampLabel } from '@forgecharts/utils';
4
+ import type { CanvasLayer } from './CanvasLayer';
5
+
6
+ const AXIS_HEIGHT = 30;
7
+
8
+ /**
9
+ * TimeScale — manages the horizontal time axis.
10
+ * Handles pan/zoom of the visible time range and renders tick labels.
11
+ */
12
+ export class TimeScale {
13
+ private _visibleRange: TimeRange;
14
+ private readonly _layer: CanvasLayer;
15
+ private readonly _colors: ChartColors;
16
+ private readonly _options: Required<TimeScaleOptions>;
17
+ private _activeTimeframe: Timeframe = '1h';
18
+ /** IANA timezone for axis labels. Falls back to UTC when unset. */
19
+ timezone: string | undefined = undefined;
20
+
21
+ constructor(layer: CanvasLayer, colors: ChartColors, options: TimeScaleOptions = {}) {
22
+ this._layer = layer;
23
+ this._colors = colors;
24
+ this._options = {
25
+ timeVisible: options.timeVisible ?? true,
26
+ secondsVisible: options.secondsVisible ?? false,
27
+ borderVisible: options.borderVisible ?? true,
28
+ ticksVisible: options.ticksVisible ?? true,
29
+ height: options.height ?? AXIS_HEIGHT,
30
+ };
31
+
32
+ const now = Math.floor(Date.now() / 1000);
33
+ this._visibleRange = { from: now - 86400, to: now };
34
+ }
35
+
36
+ get visibleRange(): TimeRange {
37
+ return this._visibleRange;
38
+ }
39
+
40
+ get height(): number {
41
+ return this._options.height;
42
+ }
43
+
44
+ get activeTimeframe(): Timeframe {
45
+ return this._activeTimeframe;
46
+ }
47
+
48
+ setVisibleRange(range: TimeRange): void {
49
+ this._visibleRange = range;
50
+ }
51
+
52
+ setTimeframe(tf: Timeframe): void {
53
+ this._activeTimeframe = tf;
54
+ }
55
+
56
+ /**
57
+ * Converts a Unix timestamp to a canvas X coordinate.
58
+ */
59
+ timeToX(time: number, canvasWidth: number): number {
60
+ const plotWidth = canvasWidth - this._priceScaleWidth();
61
+ return mapRange(time, this._visibleRange.from, this._visibleRange.to, 0, plotWidth);
62
+ }
63
+
64
+ /**
65
+ * Converts a canvas X coordinate back to a Unix timestamp.
66
+ */
67
+ xToTime(x: number, canvasWidth: number): number {
68
+ const plotWidth = canvasWidth - this._priceScaleWidth();
69
+ return mapRange(x, 0, plotWidth, this._visibleRange.from, this._visibleRange.to);
70
+ }
71
+
72
+ /**
73
+ * Zooms around a pivot X coordinate by a scale factor.
74
+ */
75
+ zoom(factor: number, pivotX: number, canvasWidth: number = this._layer.width): void {
76
+ const pivotTime = this.xToTime(pivotX, canvasWidth);
77
+ const span = this._visibleRange.to - this._visibleRange.from;
78
+ const newSpan = Math.max(60, span * factor);
79
+ const ratio = (pivotTime - this._visibleRange.from) / span;
80
+ const newFrom = pivotTime - ratio * newSpan;
81
+ this._visibleRange = { from: newFrom, to: newFrom + newSpan };
82
+ }
83
+
84
+ /**
85
+ * Scrolls by a delta in seconds.
86
+ */
87
+ scroll(deltaSeconds: number): void {
88
+ this._visibleRange = {
89
+ from: this._visibleRange.from + deltaSeconds,
90
+ to: this._visibleRange.to + deltaSeconds,
91
+ };
92
+ }
93
+
94
+ render(ctx: CanvasRenderingContext2D, width: number, height: number): void {
95
+ if (!this._options.ticksVisible) return;
96
+
97
+ const axisY = height - this._options.height;
98
+ const plotWidth = width - this._priceScaleWidth();
99
+
100
+ // Axis separator line
101
+ if (this._options.borderVisible) {
102
+ ctx.beginPath();
103
+ ctx.strokeStyle = this._colors.border;
104
+ ctx.lineWidth = 1;
105
+ ctx.moveTo(0, axisY);
106
+ ctx.lineTo(plotWidth, axisY);
107
+ ctx.stroke();
108
+ }
109
+
110
+ // Compute tick spacing — aim for a tick every ~100px
111
+ const approxTicks = Math.max(2, Math.floor(plotWidth / 100));
112
+ const span = this._visibleRange.to - this._visibleRange.from;
113
+ const rawStep = span / approxTicks;
114
+ const step = this._niceTimeStep(rawStep);
115
+
116
+ const startTime = Math.ceil(this._visibleRange.from / step) * step;
117
+
118
+ ctx.fillStyle = this._colors.text;
119
+ ctx.font = '11px system-ui, sans-serif';
120
+ ctx.textAlign = 'center';
121
+ ctx.textBaseline = 'middle';
122
+
123
+ for (let t = startTime; t <= this._visibleRange.to; t += step) {
124
+ const x = this.timeToX(t, width);
125
+ if (x < 0 || x > plotWidth) continue;
126
+
127
+ // Tick mark
128
+ ctx.beginPath();
129
+ ctx.strokeStyle = this._colors.border;
130
+ ctx.lineWidth = 1;
131
+ ctx.moveTo(x, axisY);
132
+ ctx.lineTo(x, axisY + 4);
133
+ ctx.stroke();
134
+
135
+ // Grid line
136
+ ctx.beginPath();
137
+ ctx.strokeStyle = this._colors.grid;
138
+ ctx.setLineDash([1, 3]);
139
+ ctx.moveTo(x, 0);
140
+ ctx.lineTo(x, axisY);
141
+ ctx.stroke();
142
+ ctx.setLineDash([]);
143
+
144
+ // Label
145
+ const label = formatTimestampLabel(t, step, this.timezone);
146
+ ctx.fillText(label, x, axisY + this._options.height / 2);
147
+ }
148
+ }
149
+
150
+ // ─── Private helpers ─────────────────────────────────────────────────────────
151
+
152
+ private _priceScaleWidth(): number {
153
+ return 70; // reserved for price scale — keep in sync with PriceScale
154
+ }
155
+
156
+ private _niceTimeStep(rawSeconds: number): number {
157
+ const nice = [
158
+ 1, 5, 10, 30, 60, 300, 600, 900, 1800, 3600, 7200, 14400, 21600, 43200,
159
+ 86400, // 1 day
160
+ 7 * 86400, // 1 week
161
+ 14 * 86400, // 2 weeks
162
+ 30 * 86400, // ~1 month
163
+ 91 * 86400, // ~quarter
164
+ 182 * 86400, // ~half year
165
+ 365 * 86400, // ~1 year
166
+ ];
167
+ for (const s of nice) {
168
+ if (s >= rawSeconds) return s;
169
+ }
170
+ return 365 * 86400 * Math.ceil(rawSeconds / (365 * 86400));
171
+ }
172
+ }
173
+
174
+ // Re-export computeTicks so renderers can use it without an extra import
175
+ export { computeTicks };