gloomberb 0.3.3 → 0.4.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/README.md +27 -16
- package/package.json +1 -1
- package/src/app.tsx +29 -65
- 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-renderer.ts +7 -2
- 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/chart.test.ts +26 -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 +484 -112
- 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 +359 -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 +1 -1
- package/src/components/layout/pane-header.tsx +26 -2
- package/src/components/layout/shell.tsx +62 -50
- 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 +174 -97
- package/src/components/ui/data-table.tsx +27 -2
- 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 +370 -3
- package/src/plugins/builtin/chat-controller.ts +310 -35
- package/src/plugins/builtin/chat.test.tsx +118 -6
- package/src/plugins/builtin/chat.tsx +175 -116
- package/src/plugins/builtin/comparison-chart.test.tsx +66 -5
- package/src/plugins/builtin/comparison-chart.tsx +21 -1
- 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 +75 -1
- package/src/plugins/ibkr/gateway-service.ts +179 -9
- package/src/plugins/pane-manager.test.ts +1 -1
- package/src/plugins/plugin-runtime.tsx +27 -7
- package/src/plugins/prediction-markets/chart.test.tsx +139 -0
- package/src/plugins/prediction-markets/chart.tsx +65 -436
- package/src/plugins/prediction-markets/cli.ts +28 -0
- package/src/plugins/prediction-markets/controller-data.ts +17 -5
- package/src/plugins/prediction-markets/controller.ts +141 -121
- package/src/plugins/prediction-markets/detail/outcomes.tsx +2 -6
- package/src/plugins/prediction-markets/detail/pane.tsx +4 -25
- package/src/plugins/prediction-markets/detail/shared.tsx +0 -1
- package/src/plugins/prediction-markets/detail.test.tsx +4 -0
- package/src/plugins/prediction-markets/index.tsx +2 -0
- 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 +160 -58
- package/src/plugins/prediction-markets/pane.tsx +74 -183
- package/src/plugins/prediction-markets/table.tsx +6 -3
- package/src/plugins/prediction-markets/types.ts +0 -1
- package/src/sources/gloomberb-cloud.test.ts +29 -0
- package/src/sources/gloomberb-cloud.ts +89 -1
- package/src/sources/provider-router.test.ts +308 -3
- package/src/sources/provider-router.ts +247 -17
- 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/utils/api-client.test.ts +57 -0
- package/src/utils/api-client.ts +35 -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/price-history.test.ts +25 -0
- package/src/utils/price-history.ts +29 -5
- 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
|
+
}
|
|
@@ -917,6 +917,7 @@ export interface RenderChartOptions {
|
|
|
917
917
|
axisMode?: ChartAxisMode;
|
|
918
918
|
currency?: string;
|
|
919
919
|
colors: ResolvedChartPalette;
|
|
920
|
+
timeAxisDates?: Array<Date | string | number>;
|
|
920
921
|
}
|
|
921
922
|
|
|
922
923
|
export interface ChartScene {
|
|
@@ -1027,6 +1028,8 @@ export function buildChartScene(
|
|
|
1027
1028
|
? null
|
|
1028
1029
|
: max - (cursorY / Math.max(chartRows - 1, 1)) * range;
|
|
1029
1030
|
|
|
1031
|
+
const timeAxisDates = opts.timeAxisDates ?? points.map((point) => point.date);
|
|
1032
|
+
|
|
1030
1033
|
return {
|
|
1031
1034
|
points,
|
|
1032
1035
|
width: opts.width,
|
|
@@ -1045,7 +1048,7 @@ export function buildChartScene(
|
|
|
1045
1048
|
dateAtCursor: activePoint.date,
|
|
1046
1049
|
changeAtCursor: activePoint.close - points[0]!.close,
|
|
1047
1050
|
changePctAtCursor: points[0]!.close ? ((activePoint.close - points[0]!.close) / points[0]!.close) * 100 : 0,
|
|
1048
|
-
timeLabels: buildTimeAxis(
|
|
1051
|
+
timeLabels: buildTimeAxis(timeAxisDates, opts.width),
|
|
1049
1052
|
cursorX,
|
|
1050
1053
|
cursorY,
|
|
1051
1054
|
cursorColumn,
|
|
@@ -1138,10 +1141,12 @@ export function renderChart(
|
|
|
1138
1141
|
);
|
|
1139
1142
|
}
|
|
1140
1143
|
|
|
1144
|
+
const timeAxisDates = opts.timeAxisDates ?? points.map((point) => point.date);
|
|
1145
|
+
|
|
1141
1146
|
return {
|
|
1142
1147
|
lines: bufferToBrailleLines(buf, palette.bgColor),
|
|
1143
1148
|
axisLabels: [...axisLabelsByRow.entries()].map(([row, label]) => ({ row, label })),
|
|
1144
|
-
timeLabels: buildTimeAxis(
|
|
1149
|
+
timeLabels: buildTimeAxis(timeAxisDates, width),
|
|
1145
1150
|
activePoint,
|
|
1146
1151
|
priceAtCursor,
|
|
1147
1152
|
crosshairPrice,
|
|
@@ -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
|
+
});
|