gloomberb 0.3.7 → 0.4.1
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/README.md +19 -8
- package/package.json +1 -1
- package/src/app.tsx +45 -69
- package/src/cli/base-converter.ts +31 -0
- package/src/cli/commands/search.ts +11 -3
- package/src/cli/commands/ticker.ts +22 -11
- package/src/cli/context.ts +27 -32
- package/src/cli/helpers.ts +1 -4
- package/src/cli/index.ts +140 -123
- package/src/cli/registry.test.ts +226 -0
- package/src/cli/registry.ts +201 -0
- package/src/cli.test.ts +219 -0
- package/src/components/chart/chart-controller.ts +309 -0
- package/src/components/chart/chart-data.ts +5 -6
- package/src/components/chart/chart-pane-settings.ts +27 -0
- package/src/components/chart/chart-resolution.test.ts +56 -0
- package/src/components/chart/chart-resolution.ts +362 -0
- package/src/components/chart/chart-types.ts +12 -4
- package/src/components/chart/chart-viewport.test.ts +25 -0
- package/src/components/chart/chart-viewport.ts +28 -0
- package/src/components/chart/comparison-chart-data.ts +20 -27
- package/src/components/chart/comparison-stock-chart.tsx +374 -99
- package/src/components/chart/stock-chart-renderer-switch.test.tsx +124 -0
- package/src/components/chart/stock-chart.tsx +455 -106
- package/src/components/command-bar/__snapshots__/command-bar.test.tsx.snap +4 -4
- package/src/components/command-bar/command-bar.test.tsx +650 -5
- package/src/components/command-bar/command-bar.tsx +363 -63
- package/src/components/command-bar/command-registry.ts +8 -0
- package/src/components/command-bar/helpers.test.ts +23 -1
- package/src/components/command-bar/helpers.ts +14 -5
- package/src/components/command-bar/pane-template-display.ts +12 -0
- package/src/components/command-bar/root-shortcuts.ts +2 -1
- package/src/components/command-bar/workflow-ops.ts +4 -2
- package/src/components/detail-feed-view.test.tsx +124 -49
- package/src/components/detail-feed-view.tsx +396 -160
- package/src/components/layout/floating-pane.tsx +4 -2
- package/src/components/layout/header.tsx +3 -3
- package/src/components/layout/pane-header.tsx +26 -2
- package/src/components/layout/pane-sizing.ts +6 -0
- package/src/components/layout/pane.tsx +5 -1
- package/src/components/layout/shell.test.tsx +77 -5
- package/src/components/layout/shell.tsx +67 -52
- package/src/components/layout/status-bar.test.tsx +66 -0
- package/src/components/layout/status-bar.tsx +26 -5
- package/src/components/onboarding/onboarding-wizard.test.tsx +65 -0
- package/src/components/onboarding/onboarding-wizard.tsx +5 -6
- package/src/components/ticker-list-table.test.tsx +83 -0
- package/src/components/ticker-list-table.tsx +181 -97
- package/src/components/ui/data-table.tsx +50 -5
- package/src/components/ui/index.ts +1 -0
- package/src/components/ui/page-stack-view.tsx +60 -0
- package/src/components/use-double-click-activation.ts +31 -0
- package/src/index.tsx +10 -10
- package/src/market-data/coordinator.test.ts +92 -4
- package/src/market-data/coordinator.ts +32 -5
- package/src/market-data/hooks.test.tsx +120 -0
- package/src/market-data/hooks.tsx +53 -16
- package/src/market-data/request-types.ts +4 -2
- package/src/market-data/selectors.ts +24 -58
- package/src/plugins/builtin/chat-controller.test.ts +400 -3
- package/src/plugins/builtin/chat-controller.ts +328 -35
- package/src/plugins/builtin/chat.test.tsx +118 -6
- package/src/plugins/builtin/chat.tsx +180 -116
- package/src/plugins/builtin/comparison-chart.test.tsx +66 -5
- package/src/plugins/builtin/comparison-chart.tsx +21 -1
- package/src/plugins/builtin/layout-manager.tsx +9 -0
- package/src/plugins/builtin/news.tsx +1 -11
- package/src/plugins/builtin/portfolio-list/cli/commands.ts +656 -0
- package/src/plugins/builtin/portfolio-list/command-bar.ts +122 -0
- package/src/plugins/builtin/portfolio-list/header.tsx +16 -34
- package/src/plugins/builtin/portfolio-list/index.tsx +5 -0
- package/src/plugins/builtin/portfolio-list/metrics.test.ts +61 -0
- package/src/plugins/builtin/portfolio-list/metrics.ts +2 -2
- package/src/plugins/builtin/portfolio-list/mutations.test.ts +187 -0
- package/src/plugins/builtin/portfolio-list/mutations.ts +226 -0
- package/src/plugins/builtin/portfolio-list/pane.tsx +39 -17
- package/src/plugins/builtin/portfolio-list/table.tsx +11 -2
- package/src/plugins/builtin/portfolio-list/use-throttled-cursor-symbol.test.tsx +163 -0
- package/src/plugins/builtin/portfolio-list/use-throttled-cursor-symbol.ts +81 -0
- package/src/plugins/builtin/sec.tsx +1 -11
- package/src/plugins/builtin/ticker-detail/chart-tab.tsx +13 -2
- package/src/plugins/builtin/ticker-detail/financials-tab.tsx +21 -0
- package/src/plugins/builtin/ticker-detail/overview-tab.tsx +45 -10
- package/src/plugins/builtin/ticker-detail/pane.tsx +35 -17
- package/src/plugins/builtin/ticker-detail/settings.ts +19 -1
- package/src/plugins/builtin/ticker-detail.test.tsx +47 -0
- package/src/plugins/catalog.ts +65 -0
- package/src/plugins/ibkr/broker-adapter.ts +28 -0
- package/src/plugins/ibkr/gateway-service.test.ts +67 -1
- package/src/plugins/ibkr/gateway-service.ts +147 -7
- package/src/plugins/ibkr/index.tsx +7 -1
- package/src/plugins/pane-manager.test.ts +1 -1
- package/src/plugins/plugin-runtime.tsx +27 -7
- package/src/plugins/prediction-markets/cli.ts +28 -0
- package/src/plugins/prediction-markets/controller-data.ts +132 -17
- package/src/plugins/prediction-markets/controller.ts +142 -122
- package/src/plugins/prediction-markets/index.tsx +7 -1
- package/src/plugins/prediction-markets/launch.ts +51 -16
- package/src/plugins/prediction-markets/metrics.ts +9 -5
- package/src/plugins/prediction-markets/pane.test.tsx +197 -57
- package/src/plugins/prediction-markets/pane.tsx +80 -155
- package/src/plugins/prediction-markets/services/fetch.ts +4 -0
- package/src/plugins/prediction-markets/table.tsx +6 -3
- package/src/plugins/prediction-markets/test-helpers.tsx +20 -5
- package/src/plugins/prediction-markets/types.ts +0 -1
- package/src/plugins/registry.ts +23 -0
- package/src/sources/gloomberb-cloud.test.ts +29 -0
- package/src/sources/gloomberb-cloud.ts +89 -1
- package/src/sources/provider-router.test.ts +265 -3
- package/src/sources/provider-router.ts +239 -11
- package/src/sources/yahoo-finance.test.ts +26 -0
- package/src/sources/yahoo-finance.ts +54 -3
- package/src/state/app-context-selector.test.tsx +99 -0
- package/src/state/app-context.test.ts +18 -0
- package/src/state/app-context.tsx +170 -65
- package/src/state/selectors.ts +25 -0
- package/src/theme/colors.ts +8 -8
- package/src/theme/themes.test.ts +2 -0
- package/src/theme/themes.ts +22 -10
- package/src/types/broker.ts +21 -0
- package/src/types/config.ts +17 -1
- package/src/types/data-provider.ts +18 -0
- package/src/types/financials.ts +31 -1
- package/src/types/plugin.ts +70 -0
- package/src/updater.test.ts +2 -0
- package/src/utils/api-client.test.ts +57 -0
- package/src/utils/api-client.ts +47 -12
- package/src/utils/back-navigation.ts +27 -0
- package/src/utils/format.test.ts +14 -0
- package/src/utils/format.ts +3 -3
- package/src/utils/market-status.ts +46 -1
- package/src/utils/quote-freshness.ts +127 -0
- package/src/utils/quote-resolution.test.ts +277 -0
- package/src/utils/quote-resolution.ts +641 -0
- package/src/utils/slugify.ts +3 -0
- package/src/utils/timestamp.ts +25 -0
- package/src/version.ts +1 -1
- package/src/cli/commands/portfolio.ts +0 -210
- package/src/cli/commands/watchlist.ts +0 -210
- package/src/plugins/builtin/data-providers.ts +0 -16
- package/src/sources/exchange-rates.ts +0 -18
- package/src/utils/ascii-chart.ts +0 -172
- package/src/utils/ticker-tokenizer.test.ts +0 -48
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
import type { QueryEntry } from "../../market-data/result-types";
|
|
2
|
+
import type { PricePoint } from "../../types/financials";
|
|
3
|
+
import { clampChartZoom, getVisiblePointCount } from "./chart-viewport";
|
|
4
|
+
import type { ChartResolution, ChartViewState, ComparisonChartViewState, TimeRange } from "./chart-types";
|
|
5
|
+
import {
|
|
6
|
+
clampTimeRangeToMaxRange,
|
|
7
|
+
getBestSupportedResolutionForPreset,
|
|
8
|
+
getCompatibleBufferRange,
|
|
9
|
+
getNextBufferRange,
|
|
10
|
+
getPresetResolution,
|
|
11
|
+
getSupportMaxRange,
|
|
12
|
+
getWidestPresetForResolution,
|
|
13
|
+
isDateWindowWithinTimeRange,
|
|
14
|
+
isTimeRangeAtOrBelow,
|
|
15
|
+
subtractTimeRange,
|
|
16
|
+
type ChartResolutionSupport,
|
|
17
|
+
type ManualChartResolution,
|
|
18
|
+
} from "./chart-resolution";
|
|
19
|
+
|
|
20
|
+
type ViewStateWithViewport = Pick<ChartViewState, "presetRange" | "bufferRange" | "activePreset" | "resolution" | "panOffset" | "zoomLevel" | "cursorX" | "cursorY">
|
|
21
|
+
| Pick<ComparisonChartViewState, "presetRange" | "bufferRange" | "activePreset" | "resolution" | "panOffset" | "zoomLevel" | "cursorX" | "cursorY">;
|
|
22
|
+
|
|
23
|
+
export interface VisibleDateWindow {
|
|
24
|
+
start: Date | null;
|
|
25
|
+
end: Date | null;
|
|
26
|
+
dates: Date[];
|
|
27
|
+
startIdx: number;
|
|
28
|
+
endIdx: number;
|
|
29
|
+
totalDates: number;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface ChartBodyState<T> {
|
|
33
|
+
data: T | null;
|
|
34
|
+
blocking: boolean;
|
|
35
|
+
updating: boolean;
|
|
36
|
+
emptyMessage: string | null;
|
|
37
|
+
errorMessage: string | null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function clamp(value: number, min: number, max: number): number {
|
|
41
|
+
return Math.max(min, Math.min(max, value));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function coerceDate(value: Date | string | number): Date {
|
|
45
|
+
return value instanceof Date ? value : new Date(value);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function getPointDates(points: readonly Pick<PricePoint, "date">[]): Date[] {
|
|
49
|
+
return points.map((point) => coerceDate(point.date as Date | string | number));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function getCanonicalVisiblePointCount(dates: readonly Date[], presetRange: TimeRange): number {
|
|
53
|
+
if (dates.length === 0) return 0;
|
|
54
|
+
if (presetRange === "ALL") return dates.length;
|
|
55
|
+
const endDate = dates[dates.length - 1]!;
|
|
56
|
+
const threshold = subtractTimeRange(endDate, presetRange).getTime();
|
|
57
|
+
const firstVisibleIndex = dates.findIndex((date) => date.getTime() >= threshold);
|
|
58
|
+
if (firstVisibleIndex < 0) return dates.length;
|
|
59
|
+
return Math.max(dates.length - firstVisibleIndex, 1);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function getCanonicalZoomLevel(dates: readonly Date[], presetRange: TimeRange): number {
|
|
63
|
+
if (dates.length === 0) return 1;
|
|
64
|
+
const visibleCount = getCanonicalVisiblePointCount(dates, presetRange);
|
|
65
|
+
return clampChartZoom(dates.length, dates.length / Math.max(visibleCount, 1));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function buildVisibleDateWindow(
|
|
69
|
+
dates: readonly Date[],
|
|
70
|
+
panOffset: number,
|
|
71
|
+
zoomLevel: number,
|
|
72
|
+
): VisibleDateWindow {
|
|
73
|
+
if (dates.length === 0) {
|
|
74
|
+
return { start: null, end: null, dates: [], startIdx: 0, endIdx: 0, totalDates: 0 };
|
|
75
|
+
}
|
|
76
|
+
const visibleCount = getVisiblePointCount(dates.length, zoomLevel);
|
|
77
|
+
const maxPan = Math.max(dates.length - visibleCount, 0);
|
|
78
|
+
const pan = clamp(panOffset, 0, maxPan);
|
|
79
|
+
const endIdx = dates.length - pan;
|
|
80
|
+
const startIdx = Math.max(endIdx - visibleCount, 0);
|
|
81
|
+
const visibleDates = dates.slice(startIdx, endIdx);
|
|
82
|
+
return {
|
|
83
|
+
start: visibleDates[0] ?? null,
|
|
84
|
+
end: visibleDates[visibleDates.length - 1] ?? null,
|
|
85
|
+
dates: visibleDates,
|
|
86
|
+
startIdx,
|
|
87
|
+
endIdx,
|
|
88
|
+
totalDates: dates.length,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function formatVisibleSpanLabel(window: Pick<VisibleDateWindow, "start" | "end">): string {
|
|
93
|
+
if (!window.start || !window.end) return "view: --";
|
|
94
|
+
const sameYear = window.start.getFullYear() === window.end.getFullYear();
|
|
95
|
+
const sameMonth = sameYear && window.start.getMonth() === window.end.getMonth();
|
|
96
|
+
const startLabel = window.start.toLocaleDateString("en-US", {
|
|
97
|
+
month: "short",
|
|
98
|
+
day: "numeric",
|
|
99
|
+
...(sameYear ? {} : { year: "numeric" }),
|
|
100
|
+
});
|
|
101
|
+
const endLabel = window.end.toLocaleDateString("en-US", {
|
|
102
|
+
month: sameMonth ? undefined : "short",
|
|
103
|
+
day: "numeric",
|
|
104
|
+
year: sameYear ? undefined : "numeric",
|
|
105
|
+
});
|
|
106
|
+
return `view:${startLabel}-${endLabel}`;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function isCanonicalPresetViewport(
|
|
110
|
+
dates: readonly Date[],
|
|
111
|
+
state: Pick<ViewStateWithViewport, "activePreset" | "panOffset" | "zoomLevel" | "resolution">,
|
|
112
|
+
): boolean {
|
|
113
|
+
if (!state.activePreset) return false;
|
|
114
|
+
if (state.panOffset !== 0) return false;
|
|
115
|
+
const canonicalZoom = getCanonicalZoomLevel(dates, state.activePreset);
|
|
116
|
+
return Math.abs(state.zoomLevel - canonicalZoom) < 0.001;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function resolvePresetSelectionWithResolution<S extends ViewStateWithViewport>(
|
|
120
|
+
state: S,
|
|
121
|
+
presetRange: TimeRange,
|
|
122
|
+
resolution: ManualChartResolution,
|
|
123
|
+
supportMaxRange: TimeRange | null = null,
|
|
124
|
+
): S {
|
|
125
|
+
return {
|
|
126
|
+
...state,
|
|
127
|
+
presetRange,
|
|
128
|
+
bufferRange: getCompatibleBufferRange(presetRange, supportMaxRange),
|
|
129
|
+
activePreset: presetRange,
|
|
130
|
+
resolution,
|
|
131
|
+
panOffset: 0,
|
|
132
|
+
zoomLevel: 1,
|
|
133
|
+
cursorX: null,
|
|
134
|
+
cursorY: null,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function resolvePresetSelection<S extends ViewStateWithViewport>(
|
|
139
|
+
state: S,
|
|
140
|
+
presetRange: TimeRange,
|
|
141
|
+
supportMaxRange: TimeRange | null = null,
|
|
142
|
+
): S {
|
|
143
|
+
return resolvePresetSelectionWithResolution(
|
|
144
|
+
state,
|
|
145
|
+
presetRange,
|
|
146
|
+
getPresetResolution(presetRange),
|
|
147
|
+
supportMaxRange,
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function resolveResolutionSelection<S extends ViewStateWithViewport>(
|
|
152
|
+
state: S,
|
|
153
|
+
resolution: ChartResolution,
|
|
154
|
+
support: ReadonlyMap<ManualChartResolution, TimeRange>,
|
|
155
|
+
visibleWindow: Pick<VisibleDateWindow, "start" | "end"> | null,
|
|
156
|
+
): S | null {
|
|
157
|
+
if (resolution === "auto") {
|
|
158
|
+
return {
|
|
159
|
+
...state,
|
|
160
|
+
resolution,
|
|
161
|
+
bufferRange: getNextBufferRange(state.presetRange),
|
|
162
|
+
activePreset: null,
|
|
163
|
+
panOffset: 0,
|
|
164
|
+
zoomLevel: 1,
|
|
165
|
+
cursorX: null,
|
|
166
|
+
cursorY: null,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const maxRange = getSupportMaxRange(support, resolution);
|
|
171
|
+
if (!maxRange) return null;
|
|
172
|
+
|
|
173
|
+
const visibleSpanTooWide = visibleWindow?.start && visibleWindow.end
|
|
174
|
+
? !isDateWindowWithinTimeRange(visibleWindow.start, visibleWindow.end, maxRange)
|
|
175
|
+
: false;
|
|
176
|
+
const shouldSnap = !isTimeRangeAtOrBelow(state.presetRange, maxRange) || visibleSpanTooWide;
|
|
177
|
+
const presetRange = shouldSnap
|
|
178
|
+
? getWidestPresetForResolution(resolution, maxRange)
|
|
179
|
+
: clampTimeRangeToMaxRange(state.presetRange, maxRange);
|
|
180
|
+
|
|
181
|
+
return {
|
|
182
|
+
...state,
|
|
183
|
+
presetRange,
|
|
184
|
+
bufferRange: getCompatibleBufferRange(presetRange, maxRange),
|
|
185
|
+
activePreset: shouldSnap ? presetRange : null,
|
|
186
|
+
resolution,
|
|
187
|
+
panOffset: 0,
|
|
188
|
+
zoomLevel: 1,
|
|
189
|
+
cursorX: null,
|
|
190
|
+
cursorY: null,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export function resolveStoredChartSelection<S extends ViewStateWithViewport>(
|
|
195
|
+
state: S,
|
|
196
|
+
presetRange: TimeRange,
|
|
197
|
+
resolution: ChartResolution,
|
|
198
|
+
support: ReadonlyMap<ManualChartResolution, TimeRange>,
|
|
199
|
+
): S {
|
|
200
|
+
const resetState = {
|
|
201
|
+
...state,
|
|
202
|
+
presetRange,
|
|
203
|
+
panOffset: 0,
|
|
204
|
+
zoomLevel: 1,
|
|
205
|
+
cursorX: null,
|
|
206
|
+
cursorY: null,
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
if (resolution === "auto") {
|
|
210
|
+
return {
|
|
211
|
+
...resetState,
|
|
212
|
+
resolution,
|
|
213
|
+
bufferRange: getNextBufferRange(presetRange),
|
|
214
|
+
activePreset: null,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const resolvedSelection = resolveResolutionSelection(
|
|
219
|
+
{
|
|
220
|
+
...resetState,
|
|
221
|
+
bufferRange: getNextBufferRange(presetRange),
|
|
222
|
+
activePreset: null,
|
|
223
|
+
resolution: "auto",
|
|
224
|
+
},
|
|
225
|
+
resolution,
|
|
226
|
+
support,
|
|
227
|
+
null,
|
|
228
|
+
);
|
|
229
|
+
|
|
230
|
+
if (!resolvedSelection) {
|
|
231
|
+
const fallbackResolution = getBestSupportedResolutionForPreset(presetRange, support);
|
|
232
|
+
if (!fallbackResolution) {
|
|
233
|
+
return {
|
|
234
|
+
...resetState,
|
|
235
|
+
resolution: "auto",
|
|
236
|
+
bufferRange: getNextBufferRange(presetRange),
|
|
237
|
+
activePreset: null,
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
return resolvePresetSelectionWithResolution(
|
|
241
|
+
resetState,
|
|
242
|
+
presetRange,
|
|
243
|
+
fallbackResolution,
|
|
244
|
+
getSupportMaxRange(support, fallbackResolution),
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return {
|
|
249
|
+
...resolvedSelection,
|
|
250
|
+
activePreset: resolvedSelection.resolution === getBestSupportedResolutionForPreset(resolvedSelection.presetRange, support)
|
|
251
|
+
? resolvedSelection.presetRange
|
|
252
|
+
: null,
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
export function clearActivePreset<S extends Pick<ViewStateWithViewport, "activePreset">>(state: S): S {
|
|
257
|
+
if (state.activePreset === null) return state;
|
|
258
|
+
return {
|
|
259
|
+
...state,
|
|
260
|
+
activePreset: null,
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
export function resolveChartBodyState<T>(
|
|
265
|
+
entry: QueryEntry<T> | null | undefined,
|
|
266
|
+
hasData: (value: T | null) => boolean,
|
|
267
|
+
emptyMessage: string,
|
|
268
|
+
): ChartBodyState<T> {
|
|
269
|
+
const data = entry?.data ?? null;
|
|
270
|
+
const hasCurrentData = hasData(data);
|
|
271
|
+
|
|
272
|
+
if (!entry || entry.phase === "idle" || ((entry.phase === "loading" || entry.phase === "refreshing") && !hasCurrentData)) {
|
|
273
|
+
return {
|
|
274
|
+
data: null,
|
|
275
|
+
blocking: true,
|
|
276
|
+
updating: false,
|
|
277
|
+
emptyMessage: null,
|
|
278
|
+
errorMessage: null,
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if (entry.phase === "error" && !hasCurrentData) {
|
|
283
|
+
return {
|
|
284
|
+
data: null,
|
|
285
|
+
blocking: false,
|
|
286
|
+
updating: false,
|
|
287
|
+
emptyMessage: null,
|
|
288
|
+
errorMessage: entry.error?.message ?? "Chart data failed to load.",
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
if (!hasCurrentData) {
|
|
293
|
+
return {
|
|
294
|
+
data,
|
|
295
|
+
blocking: false,
|
|
296
|
+
updating: false,
|
|
297
|
+
emptyMessage,
|
|
298
|
+
errorMessage: null,
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
return {
|
|
303
|
+
data,
|
|
304
|
+
blocking: false,
|
|
305
|
+
updating: entry.phase === "refreshing",
|
|
306
|
+
emptyMessage: null,
|
|
307
|
+
errorMessage: null,
|
|
308
|
+
};
|
|
309
|
+
}
|
|
@@ -78,22 +78,21 @@ export function getVisibleWindow(
|
|
|
78
78
|
viewState: ChartViewState,
|
|
79
79
|
chartWidth: number,
|
|
80
80
|
): VisibleWindow {
|
|
81
|
-
|
|
82
|
-
if (filtered.length === 0) {
|
|
81
|
+
if (history.length === 0) {
|
|
83
82
|
return { points: [], startIdx: 0, endIdx: 0 };
|
|
84
83
|
}
|
|
85
84
|
|
|
86
|
-
const visibleCount = getVisiblePointCount(
|
|
85
|
+
const visibleCount = getVisiblePointCount(history.length, viewState.zoomLevel);
|
|
87
86
|
|
|
88
87
|
// Clamp pan offset
|
|
89
|
-
const maxPan = Math.max(
|
|
88
|
+
const maxPan = Math.max(history.length - visibleCount, 0);
|
|
90
89
|
const pan = Math.min(Math.max(viewState.panOffset, 0), maxPan);
|
|
91
90
|
|
|
92
|
-
const endIdx =
|
|
91
|
+
const endIdx = history.length - pan;
|
|
93
92
|
const startIdx = Math.max(endIdx - visibleCount, 0);
|
|
94
93
|
|
|
95
94
|
return {
|
|
96
|
-
points:
|
|
95
|
+
points: history.slice(startIdx, endIdx),
|
|
97
96
|
startIdx,
|
|
98
97
|
endIdx,
|
|
99
98
|
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { saveConfig } from "../../data/config-store";
|
|
2
|
+
import { setPaneSettings } from "../../pane-settings";
|
|
3
|
+
import { useAppState, usePaneInstance, usePaneInstanceId } from "../../state/app-context";
|
|
4
|
+
import type { ChartResolution, TimeRange } from "./chart-types";
|
|
5
|
+
|
|
6
|
+
export function usePersistChartControlSelection(rangePresetKey: string): (
|
|
7
|
+
range: TimeRange,
|
|
8
|
+
resolution: ChartResolution,
|
|
9
|
+
) => void {
|
|
10
|
+
const { state, dispatch } = useAppState();
|
|
11
|
+
const paneId = usePaneInstanceId();
|
|
12
|
+
const pane = usePaneInstance();
|
|
13
|
+
|
|
14
|
+
return (range, resolution) => {
|
|
15
|
+
const layout = setPaneSettings(state.config.layout, paneId, {
|
|
16
|
+
...(pane?.settings ?? {}),
|
|
17
|
+
[rangePresetKey]: range,
|
|
18
|
+
chartResolution: resolution,
|
|
19
|
+
});
|
|
20
|
+
const layouts = state.config.layouts.map((savedLayout, index) => (
|
|
21
|
+
index === state.config.activeLayoutIndex ? { ...savedLayout, layout } : savedLayout
|
|
22
|
+
));
|
|
23
|
+
const nextConfig = { ...state.config, layout, layouts };
|
|
24
|
+
dispatch({ type: "SET_CONFIG", config: nextConfig });
|
|
25
|
+
saveConfig(nextConfig).catch(() => {});
|
|
26
|
+
};
|
|
27
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
clampTimeRangeForResolution,
|
|
4
|
+
getActiveRangePreset,
|
|
5
|
+
getPresetResolution,
|
|
6
|
+
intersectChartResolutions,
|
|
7
|
+
isRangePresetSupported,
|
|
8
|
+
normalizeChartResolution,
|
|
9
|
+
sortChartResolutions,
|
|
10
|
+
} from "./chart-resolution";
|
|
11
|
+
|
|
12
|
+
describe("chart-resolution", () => {
|
|
13
|
+
test("maps range presets to their default manual resolutions", () => {
|
|
14
|
+
expect(getPresetResolution("1W")).toBe("5m");
|
|
15
|
+
expect(getPresetResolution("1M")).toBe("15m");
|
|
16
|
+
expect(getPresetResolution("3M")).toBe("1h");
|
|
17
|
+
expect(getPresetResolution("6M")).toBe("1d");
|
|
18
|
+
expect(getPresetResolution("1Y")).toBe("1d");
|
|
19
|
+
expect(getPresetResolution("5Y")).toBe("1wk");
|
|
20
|
+
expect(getPresetResolution("ALL")).toBe("1mo");
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test("derives the active range preset only for the exact reset preset pair", () => {
|
|
24
|
+
expect(getActiveRangePreset("1Y", "1d", 1, 0)).toBe("1Y");
|
|
25
|
+
expect(getActiveRangePreset("1Y", "auto", 1, 0)).toBeNull();
|
|
26
|
+
expect(getActiveRangePreset("1Y", "1d", 1.2, 0)).toBeNull();
|
|
27
|
+
expect(getActiveRangePreset("1Y", "1d", 1, 2)).toBeNull();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("intersects and sorts provider capabilities", () => {
|
|
31
|
+
expect(intersectChartResolutions([
|
|
32
|
+
["1d", "5m", "1wk", "nope"],
|
|
33
|
+
["1wk", "1d", "1mo"],
|
|
34
|
+
["1d", "1wk"],
|
|
35
|
+
])).toEqual(["1d", "1wk"]);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test("checks whether a range preset is supported by the visible capability set", () => {
|
|
39
|
+
expect(isRangePresetSupported("1Y", ["1d", "1wk"])).toBe(true);
|
|
40
|
+
expect(isRangePresetSupported("ALL", ["1d", "1wk"])).toBe(false);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("clamps overly-wide ranges for manual intraday resolutions", () => {
|
|
44
|
+
expect(clampTimeRangeForResolution("5Y", "5m")).toBe("1W");
|
|
45
|
+
expect(clampTimeRangeForResolution("6M", "1h")).toBe("3M");
|
|
46
|
+
expect(clampTimeRangeForResolution("1M", "15m")).toBe("1M");
|
|
47
|
+
expect(clampTimeRangeForResolution("5Y", "1d")).toBe("5Y");
|
|
48
|
+
expect(clampTimeRangeForResolution("5Y", "auto")).toBe("5Y");
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test("normalizes and sorts chart resolution values", () => {
|
|
52
|
+
expect(normalizeChartResolution("1h")).toBe("1h");
|
|
53
|
+
expect(normalizeChartResolution("bogus", "1d")).toBe("1d");
|
|
54
|
+
expect(sortChartResolutions(["1wk", "auto", "15m", "1d"])).toEqual(["auto", "15m", "1d", "1wk"]);
|
|
55
|
+
});
|
|
56
|
+
});
|