gloomberb 0.6.1 → 0.7.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/package.json +1 -1
- package/patches/@opentui%2Fcore@0.1.90.patch +48 -24
- package/src/app.tsx +158 -57
- package/src/capabilities/factories.ts +3 -0
- package/src/cli/registry.test.ts +0 -1
- package/src/components/chart/chart-controller.ts +0 -1
- package/src/components/chart/chart-data.ts +1 -31
- package/src/components/chart/chart-renderer.ts +94 -12
- package/src/components/chart/chart-resolution.test.ts +0 -36
- package/src/components/chart/chart-resolution.ts +0 -27
- package/src/components/chart/chart.test.ts +60 -45
- package/src/components/chart/comparison-chart-data.test.ts +1 -5
- package/src/components/chart/comparison-chart-data.ts +1 -4
- package/src/components/chart/comparison-chart-renderer.test.ts +26 -0
- package/src/components/chart/comparison-chart-renderer.ts +13 -6
- package/src/components/chart/comparison-stock-chart.tsx +63 -39
- package/src/components/chart/price-axis-labels.tsx +114 -0
- package/src/components/chart/static-chart-surface.tsx +30 -4
- package/src/components/chart/stock-chart.test.ts +0 -16
- package/src/components/chart/stock-chart.tsx +69 -41
- package/src/components/chart/time-axis-label.tsx +120 -0
- package/src/components/command-bar/__snapshots__/command-bar.test.tsx.snap +0 -28
- package/src/components/command-bar/command-bar.test.tsx +182 -36
- package/src/components/command-bar/command-bar.tsx +943 -1016
- package/src/components/command-bar/command-registry.ts +0 -10
- package/src/components/command-bar/confirm-body.tsx +51 -0
- package/src/components/command-bar/helpers.ts +2 -5
- package/src/components/command-bar/list-model.ts +76 -0
- package/src/components/command-bar/theme-picker.tsx +254 -0
- package/src/components/command-bar/view-model.test.ts +4 -4
- package/src/components/command-bar/view-model.ts +1 -20
- package/src/components/command-bar/workflow-ops.ts +4 -0
- package/src/components/command-bar/workflow-types.ts +1 -2
- package/src/components/data-table-stack-view.test.tsx +39 -4
- package/src/components/data-table-stack-view.tsx +11 -104
- package/src/components/data-table-view.tsx +43 -79
- package/src/components/feed-data-table-stack-view.tsx +10 -17
- package/src/components/feed-stack-controller.ts +39 -0
- package/src/components/index.ts +5 -1
- package/src/components/layout/detached-pane-shell.tsx +29 -10
- package/src/components/layout/floating-pane.tsx +35 -4
- package/src/components/layout/header.tsx +12 -7
- package/src/components/layout/pane-content.test.tsx +64 -0
- package/src/components/layout/pane-content.tsx +2 -0
- package/src/components/layout/pane-cycle-order.test.ts +30 -0
- package/src/components/layout/pane-cycle-order.ts +32 -0
- package/src/components/layout/pane-footer.tsx +11 -4
- package/src/components/layout/pane-header.tsx +5 -5
- package/src/components/layout/pane-sizing.ts +9 -2
- package/src/components/layout/pane-title.ts +4 -1
- package/src/components/layout/pane.tsx +11 -5
- package/src/components/layout/shell.test.tsx +264 -22
- package/src/components/layout/shell.tsx +46 -8
- package/src/components/layout/status-bar.tsx +2 -0
- package/src/components/onboarding/onboarding-wizard.tsx +1 -2
- package/src/components/pane-settings-dialog.tsx +12 -4
- package/src/components/pane-template-wizard.test.tsx +3 -4
- package/src/components/speedometer-gauge.test.tsx +45 -0
- package/src/components/speedometer-gauge.tsx +562 -0
- package/src/components/table-view-shared.tsx +218 -0
- package/src/components/ticker-list-table-view.tsx +57 -73
- package/src/components/ticker-list-table.test.tsx +82 -1
- package/src/components/ticker-list-table.tsx +22 -9
- package/src/components/ui/confirm-dialog.tsx +66 -0
- package/src/components/ui/data-table.tsx +30 -19
- package/src/components/ui/frame.tsx +2 -0
- package/src/components/ui/index.ts +0 -1
- package/src/components/ui/page-stack-view.tsx +39 -10
- package/src/components/ui/table-layout.test.ts +42 -0
- package/src/components/ui/table-layout.ts +49 -4
- package/src/components/ui/tabs.tsx +34 -1
- package/src/components/ui/ui.test.tsx +168 -21
- package/src/core/state/app-state.ts +10 -17
- package/src/market-data/coordinator.test.ts +76 -1
- package/src/market-data/coordinator.ts +218 -49
- package/src/news/aggregator.test.ts +49 -1
- package/src/news/aggregator.ts +48 -3
- package/src/news/hooks.ts +6 -2
- package/src/plugins/builtin/ai/ask-ai-detail-tab.tsx +46 -16
- package/src/plugins/builtin/ai/providers.ts +0 -17
- package/src/plugins/builtin/ai/screener-pane.test.tsx +3 -4
- package/src/plugins/builtin/ai/screener-pane.tsx +3 -5
- package/src/plugins/builtin/analytics/index.tsx +1 -18
- package/src/plugins/builtin/ask-ai.test.tsx +89 -2
- package/src/plugins/builtin/broker-manager/index.test.tsx +2 -2
- package/src/plugins/builtin/broker-manager/index.tsx +19 -36
- package/src/plugins/builtin/chat-controller.test.ts +188 -1
- package/src/plugins/builtin/chat-controller.ts +537 -220
- package/src/plugins/builtin/chat.test.tsx +431 -13
- package/src/plugins/builtin/chat.tsx +1134 -227
- package/src/plugins/builtin/cloud-tweets.tsx +1 -1
- package/src/plugins/builtin/comparison-chart.test.tsx +53 -2
- package/src/plugins/builtin/comparison-chart.tsx +11 -5
- package/src/plugins/builtin/correlation/index.tsx +1 -6
- package/src/plugins/builtin/debug.tsx +1 -3
- package/src/plugins/builtin/econ/index.tsx +1 -2
- package/src/plugins/builtin/fear-greed/fear-greed-data.test.ts +72 -0
- package/src/plugins/builtin/fear-greed/fear-greed-data.ts +366 -0
- package/src/plugins/builtin/fear-greed/index.tsx +587 -0
- package/src/plugins/builtin/fx-matrix/index.tsx +2 -2
- package/src/plugins/builtin/help.test.tsx +2 -2
- package/src/plugins/builtin/help.tsx +2 -2
- package/src/plugins/builtin/holders/index.test.ts +13 -1
- package/src/plugins/builtin/holders/index.tsx +64 -29
- package/src/plugins/builtin/insider/index.tsx +22 -89
- package/src/plugins/builtin/market-movers/index.tsx +13 -19
- package/src/plugins/builtin/market-movers/screener.test.ts +39 -1
- package/src/plugins/builtin/market-movers/screener.ts +70 -17
- package/src/plugins/builtin/news-wire/breaking-pane.tsx +3 -2
- package/src/plugins/builtin/news-wire/index.tsx +41 -2
- package/src/plugins/builtin/news-wire/industry-pane.tsx +4 -18
- package/src/plugins/builtin/news-wire/news-detail-view.tsx +46 -4
- package/src/plugins/builtin/news-wire/news-preset-pane.tsx +3 -2
- package/src/plugins/builtin/news-wire/news-table.tsx +11 -11
- package/src/plugins/builtin/news.tsx +4 -16
- package/src/plugins/builtin/notes.tsx +19 -55
- package/src/plugins/builtin/options.test.tsx +58 -2
- package/src/plugins/builtin/options.tsx +110 -65
- package/src/plugins/builtin/plugin-groups.ts +2 -0
- package/src/plugins/builtin/portfolio-list/cli/commands.ts +1 -1
- package/src/plugins/builtin/portfolio-list/pane.tsx +16 -39
- package/src/plugins/builtin/research/index.test.ts +94 -0
- package/src/plugins/builtin/research/index.tsx +164 -33
- package/src/plugins/builtin/sec-filing-display.tsx +84 -0
- package/src/plugins/builtin/sec.tsx +13 -66
- package/src/plugins/builtin/sectors/index.tsx +1 -1
- package/src/plugins/builtin/ticker-detail/chart-tab.tsx +0 -1
- package/src/plugins/builtin/ticker-detail/index.tsx +0 -1
- package/src/plugins/builtin/ticker-detail/overview-tab.tsx +0 -1
- package/src/plugins/builtin/ticker-detail/pane.tsx +28 -22
- package/src/plugins/builtin/ticker-detail-chart-tab-switch.test.tsx +3 -4
- package/src/plugins/builtin/ticker-detail.test.tsx +38 -0
- package/src/plugins/builtin/world-indices/index.tsx +1 -1
- package/src/plugins/builtin/yield-curve/index.tsx +1 -1
- package/src/plugins/ibkr/broker-adapter.ts +1 -3
- package/src/plugins/ibkr/gateway-service-native.ts +1 -5
- package/src/plugins/ibkr/gateway-service.ts +1 -1
- package/src/plugins/ibkr/index.test.tsx +0 -2
- package/src/plugins/ibkr/trade-tab.test.tsx +3 -4
- package/src/plugins/pane-manager.ts +0 -13
- package/src/plugins/plugin-runtime.test.tsx +40 -6
- package/src/plugins/plugin-runtime.tsx +20 -10
- package/src/plugins/prediction-markets/cli.ts +0 -1
- package/src/plugins/prediction-markets/controller-data.ts +0 -2
- package/src/plugins/prediction-markets/controller.ts +0 -15
- package/src/plugins/prediction-markets/detail/pane.tsx +1 -0
- package/src/plugins/prediction-markets/pane.test.tsx +3 -11
- package/src/plugins/prediction-markets/test-helpers.tsx +1 -1
- package/src/plugins/registry.ts +13 -42
- package/src/plugins/ticker-navigation.test.ts +66 -0
- package/src/plugins/ticker-navigation.ts +44 -0
- package/src/react/input.ts +2 -1
- package/src/renderers/electrobun/bun/index.ts +32 -11
- package/src/renderers/electrobun/bun/window-focus.ts +37 -0
- package/src/renderers/electrobun/shared/protocol.ts +7 -1
- package/src/renderers/electrobun/view/app-services.ts +88 -31
- package/src/renderers/electrobun/view/backend-rpc.ts +15 -0
- package/src/renderers/electrobun/view/data-table.tsx +138 -101
- package/src/renderers/electrobun/view/desktop-controls.tsx +42 -20
- package/src/renderers/electrobun/view/desktop-window-bridge.ts +12 -1
- package/src/renderers/electrobun/view/dialog-host.tsx +13 -1
- package/src/renderers/electrobun/view/input-host.tsx +74 -10
- package/src/renderers/electrobun/view/key-event.ts +1 -1
- package/src/renderers/electrobun/view/main.tsx +18 -1
- package/src/renderers/electrobun/view/styles.css +329 -0
- package/src/renderers/electrobun/view/toast-host.tsx +29 -3
- package/src/renderers/electrobun/view/ui-host.tsx +42 -0
- package/src/renderers/opentui/chart-surface.tsx +179 -0
- package/src/renderers/opentui/input-host.tsx +1 -1
- package/src/renderers/opentui/start.tsx +48 -30
- package/src/renderers/opentui/test-utils.tsx +17 -0
- package/src/renderers/opentui/ui-host.tsx +5 -2
- package/src/sources/gloomberb-cloud.test.ts +86 -1
- package/src/sources/gloomberb-cloud.ts +117 -21
- package/src/sources/provider-router.test.ts +122 -0
- package/src/sources/provider-router.ts +198 -15
- package/src/sources/yahoo-finance.test.ts +44 -0
- package/src/sources/yahoo-finance.ts +6 -0
- package/src/state/app-bootstrap.ts +20 -7
- package/src/state/app-context-selector.test.tsx +189 -4
- package/src/state/app-context.test.ts +65 -0
- package/src/state/app-context.tsx +48 -16
- package/src/state/use-inline-tickers.test.tsx +73 -0
- package/src/state/use-inline-tickers.ts +19 -10
- package/src/test-support/plugin-runtime.ts +3 -2
- package/src/theme/colors.test.ts +75 -0
- package/src/theme/colors.ts +76 -6
- package/src/theme/theme-context.tsx +26 -0
- package/src/theme/themes.test.ts +23 -1
- package/src/theme/themes.ts +115 -0
- package/src/types/broker.ts +1 -1
- package/src/types/capability-route-source.ts +1 -0
- package/src/types/data-provider.ts +15 -2
- package/src/types/desktop-window.ts +6 -0
- package/src/types/financials.ts +2 -0
- package/src/types/plugin.ts +1 -3
- package/src/ui/host.tsx +7 -0
- package/src/utils/api-client.test.ts +32 -0
- package/src/utils/api-client.ts +119 -18
- package/src/utils/back-navigation.ts +24 -1
- package/src/utils/double-escape-close.test.ts +33 -0
- package/src/utils/double-escape-close.ts +40 -0
- package/src/utils/throttled-fetch.test.ts +22 -0
- package/src/utils/throttled-fetch.ts +3 -1
- package/src/utils/ticker-search.ts +53 -9
- package/src/version.ts +1 -1
- package/src/plugins/builtin/news-wire/feed-pane.tsx +0 -21
- package/src/plugins/builtin/news-wire/top-pane.tsx +0 -21
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import type { PricePoint } from "../../types/financials";
|
|
2
|
-
import type { ChartRenderMode
|
|
3
|
-
import { getVisiblePointCount } from "./chart-viewport";
|
|
2
|
+
import type { ChartRenderMode } from "./chart-types";
|
|
4
3
|
|
|
5
4
|
export interface ProjectedChartPoint {
|
|
6
5
|
date: Date;
|
|
@@ -76,34 +75,6 @@ export function resolveRenderMode(
|
|
|
76
75
|
};
|
|
77
76
|
}
|
|
78
77
|
|
|
79
|
-
/**
|
|
80
|
-
* Apply zoom and pan to get the visible data window.
|
|
81
|
-
*/
|
|
82
|
-
export function getVisibleWindow(
|
|
83
|
-
history: PricePoint[],
|
|
84
|
-
viewState: ChartViewState,
|
|
85
|
-
chartWidth: number,
|
|
86
|
-
): VisibleWindow {
|
|
87
|
-
if (history.length === 0) {
|
|
88
|
-
return { points: [], startIdx: 0, endIdx: 0 };
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
const visibleCount = getVisiblePointCount(history.length, viewState.zoomLevel);
|
|
92
|
-
|
|
93
|
-
// Clamp pan offset
|
|
94
|
-
const maxPan = Math.max(history.length - visibleCount, 0);
|
|
95
|
-
const pan = Math.min(Math.max(viewState.panOffset, 0), maxPan);
|
|
96
|
-
|
|
97
|
-
const endIdx = history.length - pan;
|
|
98
|
-
const startIdx = Math.max(endIdx - visibleCount, 0);
|
|
99
|
-
|
|
100
|
-
return {
|
|
101
|
-
points: history.slice(startIdx, endIdx),
|
|
102
|
-
startIdx,
|
|
103
|
-
endIdx,
|
|
104
|
-
};
|
|
105
|
-
}
|
|
106
|
-
|
|
107
78
|
function normalizePoint(point: PricePoint): ProjectedChartPoint {
|
|
108
79
|
const open = point.open ?? point.close;
|
|
109
80
|
const high = point.high ?? point.close;
|
|
@@ -334,7 +305,6 @@ export function bucketOhlcSeries(
|
|
|
334
305
|
return result;
|
|
335
306
|
}
|
|
336
307
|
|
|
337
|
-
const MS_HOUR = 3600_000;
|
|
338
308
|
const MS_DAY = 86400_000;
|
|
339
309
|
|
|
340
310
|
/**
|
|
@@ -157,10 +157,14 @@ export function createPixelBuffer(width: number, heightPixels: number): PixelBuf
|
|
|
157
157
|
}
|
|
158
158
|
|
|
159
159
|
function setPixel(buf: PixelBuffer, x: number, y: number, color: string, layer: number) {
|
|
160
|
-
|
|
161
|
-
|
|
160
|
+
const px = Math.round(x);
|
|
161
|
+
const py = Math.round(y);
|
|
162
|
+
if (px >= 0 && px < buf.width && py >= 0 && py < buf.height) {
|
|
163
|
+
const row = buf.pixels[py];
|
|
164
|
+
if (!row) return;
|
|
165
|
+
const existing = row[px];
|
|
162
166
|
if (!existing || layer >= existing.layer) {
|
|
163
|
-
|
|
167
|
+
row[px] = { color, layer };
|
|
164
168
|
}
|
|
165
169
|
}
|
|
166
170
|
}
|
|
@@ -178,17 +182,22 @@ export function drawLine(
|
|
|
178
182
|
color: string,
|
|
179
183
|
layer: number = LAYER_DATA,
|
|
180
184
|
) {
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
const
|
|
184
|
-
const
|
|
185
|
+
const startX = Math.round(x0);
|
|
186
|
+
const startY = Math.round(y0);
|
|
187
|
+
const endX = Math.round(x1);
|
|
188
|
+
const endY = Math.round(y1);
|
|
189
|
+
if (![startX, startY, endX, endY].every(Number.isFinite)) return;
|
|
190
|
+
let dx = Math.abs(endX - startX);
|
|
191
|
+
let dy = Math.abs(endY - startY);
|
|
192
|
+
const sx = startX < endX ? 1 : -1;
|
|
193
|
+
const sy = startY < endY ? 1 : -1;
|
|
185
194
|
let err = dx - dy;
|
|
186
|
-
let x =
|
|
187
|
-
let y =
|
|
195
|
+
let x = startX;
|
|
196
|
+
let y = startY;
|
|
188
197
|
|
|
189
198
|
while (true) {
|
|
190
199
|
setPixel(buf, x, y, color, layer);
|
|
191
|
-
if (x ===
|
|
200
|
+
if (x === endX && y === endY) break;
|
|
192
201
|
const e2 = 2 * err;
|
|
193
202
|
if (e2 > -dy) {
|
|
194
203
|
err -= dy;
|
|
@@ -492,11 +501,14 @@ export function drawGridLines(
|
|
|
492
501
|
yPositions: number[],
|
|
493
502
|
color: string,
|
|
494
503
|
) {
|
|
495
|
-
for (const
|
|
504
|
+
for (const rawY of yPositions) {
|
|
505
|
+
const y = Math.round(rawY);
|
|
496
506
|
if (y < 0 || y >= buf.height) continue;
|
|
497
507
|
// Dotted horizontal line — dot every 6th column (≈ every 3 terminal columns)
|
|
508
|
+
const row = buf.pixels[y];
|
|
509
|
+
if (!row) continue;
|
|
498
510
|
for (let x = 0; x < buf.width; x++) {
|
|
499
|
-
if (x % 6 === 0 && !
|
|
511
|
+
if (x % 6 === 0 && !row[x]) {
|
|
500
512
|
setPixel(buf, x, y, color, LAYER_GRID);
|
|
501
513
|
}
|
|
502
514
|
}
|
|
@@ -918,6 +930,76 @@ function writeAxisLabel(axis: string[], start: number, label: string) {
|
|
|
918
930
|
}
|
|
919
931
|
}
|
|
920
932
|
|
|
933
|
+
export interface AxisLabelSegment {
|
|
934
|
+
text: string;
|
|
935
|
+
highlighted: boolean;
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
function formatCursorTimeAxisValue(
|
|
939
|
+
value: Date | string | number,
|
|
940
|
+
dates: Array<Date | string | number>,
|
|
941
|
+
width: number,
|
|
942
|
+
): string {
|
|
943
|
+
const date = value instanceof Date ? value : new Date(value);
|
|
944
|
+
if (isNaN(date.getTime())) return "—";
|
|
945
|
+
|
|
946
|
+
const normalizedDates = dates.map((entry) => (entry instanceof Date ? entry : new Date(entry)));
|
|
947
|
+
const validDates = normalizedDates.filter((entry) => !isNaN(entry.getTime()));
|
|
948
|
+
if (validDates.length === 0) return formatDateShort(date);
|
|
949
|
+
|
|
950
|
+
const first = validDates[0]!;
|
|
951
|
+
const last = validDates[validDates.length - 1]!;
|
|
952
|
+
const spanMs = Math.max(last.getTime() - first.getTime(), 1);
|
|
953
|
+
const roughLabelCount = Math.max(Math.floor(normalizeCount(width, 0) / 10), 2);
|
|
954
|
+
const minGapMs = getMinPositiveGapMs(validDates);
|
|
955
|
+
const effectiveStepMs = Math.max(spanMs / Math.max(roughLabelCount - 1, 1), minGapMs || 0);
|
|
956
|
+
const unit = resolveAxisLabelUnit(effectiveStepMs);
|
|
957
|
+
const counterpart = isSameCalendarDay(first, last)
|
|
958
|
+
? first
|
|
959
|
+
: isSameCalendarDay(date, first)
|
|
960
|
+
? last
|
|
961
|
+
: first;
|
|
962
|
+
|
|
963
|
+
return formatTimeAxisBoundaryLabel(date, unit, counterpart);
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
export function buildCursorTimeAxisSegments({
|
|
967
|
+
timeLabels,
|
|
968
|
+
width,
|
|
969
|
+
cursorColumn,
|
|
970
|
+
cursorDate,
|
|
971
|
+
dates,
|
|
972
|
+
}: {
|
|
973
|
+
timeLabels: string;
|
|
974
|
+
width: number;
|
|
975
|
+
cursorColumn: number | null;
|
|
976
|
+
cursorDate: Date | string | number | null;
|
|
977
|
+
dates: Array<Date | string | number>;
|
|
978
|
+
}): AxisLabelSegment[] {
|
|
979
|
+
const axisWidth = normalizeCount(width, 0);
|
|
980
|
+
if (axisWidth <= 0) return [];
|
|
981
|
+
|
|
982
|
+
const base = timeLabels.padEnd(axisWidth).slice(0, axisWidth);
|
|
983
|
+
if (cursorColumn === null || cursorDate === null) {
|
|
984
|
+
return [{ text: base, highlighted: false }];
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
const label = formatCursorTimeAxisValue(cursorDate, dates, axisWidth);
|
|
988
|
+
if (!label) return [{ text: base, highlighted: false }];
|
|
989
|
+
|
|
990
|
+
const axis = base.split("");
|
|
991
|
+
const column = Math.max(Math.min(Math.round(cursorColumn), axisWidth - 1), 0);
|
|
992
|
+
const start = resolveAxisLabelStart(column, label, axisWidth);
|
|
993
|
+
const end = Math.min(start + label.length, axisWidth);
|
|
994
|
+
writeAxisLabel(axis, start, label);
|
|
995
|
+
|
|
996
|
+
return [
|
|
997
|
+
{ text: axis.slice(0, start).join(""), highlighted: false },
|
|
998
|
+
{ text: axis.slice(start, end).join(""), highlighted: true },
|
|
999
|
+
{ text: axis.slice(end).join(""), highlighted: false },
|
|
1000
|
+
].filter((segment) => segment.text.length > 0);
|
|
1001
|
+
}
|
|
1002
|
+
|
|
921
1003
|
export function buildTimeAxis(dates: Array<Date | string | number>, width: number): string {
|
|
922
1004
|
const axisWidth = normalizeCount(width, 0);
|
|
923
1005
|
if (dates.length === 0 || axisWidth <= 0) return "";
|
|
@@ -3,10 +3,8 @@ import {
|
|
|
3
3
|
CHART_RESOLUTION_STEP_MS,
|
|
4
4
|
clampTimeRangeForResolution,
|
|
5
5
|
getActiveRangePreset,
|
|
6
|
-
getBestSupportedResolutionForDateWindow,
|
|
7
6
|
getBestSupportedResolutionForVisibleWindow,
|
|
8
7
|
getPresetResolution,
|
|
9
|
-
intersectChartResolutions,
|
|
10
8
|
isRangePresetSupported,
|
|
11
9
|
normalizeChartResolution,
|
|
12
10
|
sortChartResolutions,
|
|
@@ -30,14 +28,6 @@ describe("chart-resolution", () => {
|
|
|
30
28
|
expect(getActiveRangePreset("1Y", "1d", 1, 2)).toBeNull();
|
|
31
29
|
});
|
|
32
30
|
|
|
33
|
-
test("intersects and sorts provider capabilities", () => {
|
|
34
|
-
expect(intersectChartResolutions([
|
|
35
|
-
["1d", "5m", "1wk", "nope"],
|
|
36
|
-
["1wk", "1d", "1mo"],
|
|
37
|
-
["1d", "1wk"],
|
|
38
|
-
])).toEqual(["1d", "1wk"]);
|
|
39
|
-
});
|
|
40
|
-
|
|
41
31
|
test("checks whether a range preset is supported by the visible capability set", () => {
|
|
42
32
|
expect(isRangePresetSupported("1Y", ["1d", "1wk"])).toBe(true);
|
|
43
33
|
expect(isRangePresetSupported("ALL", ["1d", "1wk"])).toBe(false);
|
|
@@ -57,32 +47,6 @@ describe("chart-resolution", () => {
|
|
|
57
47
|
expect(sortChartResolutions(["1wk", "auto", "15m", "1d"])).toEqual(["auto", "15m", "1d", "1wk"]);
|
|
58
48
|
});
|
|
59
49
|
|
|
60
|
-
test("picks the best supported resolution for the current visible date window", () => {
|
|
61
|
-
const support = [
|
|
62
|
-
{ resolution: "1m", maxRange: "1D" },
|
|
63
|
-
{ resolution: "5m", maxRange: "1W" },
|
|
64
|
-
{ resolution: "15m", maxRange: "1M" },
|
|
65
|
-
{ resolution: "1h", maxRange: "3M" },
|
|
66
|
-
{ resolution: "1d", maxRange: "5Y" },
|
|
67
|
-
{ resolution: "1wk", maxRange: "ALL" },
|
|
68
|
-
] as const;
|
|
69
|
-
|
|
70
|
-
expect(getBestSupportedResolutionForDateWindow({
|
|
71
|
-
start: new Date("2026-01-08T10:00:00Z"),
|
|
72
|
-
end: new Date("2026-01-08T16:00:00Z"),
|
|
73
|
-
}, support)).toBe("1m");
|
|
74
|
-
|
|
75
|
-
expect(getBestSupportedResolutionForDateWindow({
|
|
76
|
-
start: new Date("2026-01-01T00:00:00Z"),
|
|
77
|
-
end: new Date("2026-01-08T00:00:00Z"),
|
|
78
|
-
}, support)).toBe("5m");
|
|
79
|
-
|
|
80
|
-
expect(getBestSupportedResolutionForDateWindow({
|
|
81
|
-
start: new Date("2021-01-01T00:00:00Z"),
|
|
82
|
-
end: new Date("2026-01-01T00:00:00Z"),
|
|
83
|
-
}, support)).toBe("1wk");
|
|
84
|
-
});
|
|
85
|
-
|
|
86
50
|
test("picks a denser supported resolution as the visible window narrows", () => {
|
|
87
51
|
const support = [
|
|
88
52
|
{ resolution: "1m", maxRange: "1D" },
|
|
@@ -151,24 +151,6 @@ export function sortChartResolutions<T extends ChartResolution>(resolutions: rea
|
|
|
151
151
|
));
|
|
152
152
|
}
|
|
153
153
|
|
|
154
|
-
function normalizeManualChartResolutions(resolutions: readonly string[]): ManualChartResolution[] {
|
|
155
|
-
const seen = new Set<ManualChartResolution>();
|
|
156
|
-
for (const resolution of resolutions) {
|
|
157
|
-
if (isManualChartResolution(resolution)) {
|
|
158
|
-
seen.add(resolution);
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
return sortChartResolutions([...seen]);
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
export function intersectChartResolutions(resolutionSets: Array<readonly string[]>): ManualChartResolution[] {
|
|
165
|
-
if (resolutionSets.length === 0) return [];
|
|
166
|
-
const [first = new Set<ManualChartResolution>(), ...rest] = resolutionSets.map((set) => new Set(normalizeManualChartResolutions(set)));
|
|
167
|
-
return sortChartResolutions(
|
|
168
|
-
[...first].filter((resolution) => rest.every((set) => set.has(resolution))),
|
|
169
|
-
);
|
|
170
|
-
}
|
|
171
|
-
|
|
172
154
|
function sortChartResolutionSupport(support: readonly ChartResolutionSupport[]): ChartResolutionSupport[] {
|
|
173
155
|
return [...support].sort((left, right) => (
|
|
174
156
|
CHART_RESOLUTION_ORDER.indexOf(left.resolution) - CHART_RESOLUTION_ORDER.indexOf(right.resolution)
|
|
@@ -324,15 +306,6 @@ export function getBestSupportedResolutionForPreset(
|
|
|
324
306
|
return supportedResolutions[supportedResolutions.length - 1] ?? null;
|
|
325
307
|
}
|
|
326
308
|
|
|
327
|
-
export function getBestSupportedResolutionForDateWindow(
|
|
328
|
-
window: { start: Date | null; end: Date | null } | null,
|
|
329
|
-
support: readonly ChartResolutionSupport[] | ReadonlyMap<ManualChartResolution, TimeRange>,
|
|
330
|
-
): ManualChartResolution | null {
|
|
331
|
-
if (!window?.start || !window.end) return null;
|
|
332
|
-
const range = getTimeRangeForDateWindow(window);
|
|
333
|
-
return getBestSupportedResolutionForPreset(range, support);
|
|
334
|
-
}
|
|
335
|
-
|
|
336
309
|
export function getTimeRangeForDateWindow(
|
|
337
310
|
window: { start: Date | null; end: Date | null } | null,
|
|
338
311
|
): TimeRange {
|
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
2
|
import {
|
|
3
3
|
bucketOhlcSeries,
|
|
4
|
-
getVisibleWindow,
|
|
5
4
|
projectChartData,
|
|
6
5
|
resolveRenderMode,
|
|
7
6
|
resolveStableOhlcProjectionOptions,
|
|
8
7
|
} from "./chart-data";
|
|
9
8
|
import { stepCursorTowards } from "./cursor-motion";
|
|
10
|
-
import { buildChartScene, buildTimeAxis, formatAxisValue, formatCursorAxisValue, renderChart, resolveChartAxisWidth, resolveChartPalette } from "./chart-renderer";
|
|
9
|
+
import { buildChartScene, buildCursorTimeAxisSegments, buildTimeAxis, formatAxisValue, formatCursorAxisValue, renderChart, resolveChartAxisWidth, resolveChartPalette } from "./chart-renderer";
|
|
10
|
+
import { buildCursorTimeAxisOverlay } from "./time-axis-label";
|
|
11
|
+
import { buildCursorPriceAxisOverlay } from "./price-axis-labels";
|
|
11
12
|
import type { PricePoint } from "../../types/financials";
|
|
12
|
-
import type { ChartRenderMode
|
|
13
|
+
import type { ChartRenderMode } from "./chart-types";
|
|
13
14
|
|
|
14
15
|
const aggregationFixture: PricePoint[] = [
|
|
15
16
|
{ date: new Date("2024-01-02"), close: 10, volume: 100 },
|
|
@@ -225,48 +226,6 @@ describe("resolveRenderMode", () => {
|
|
|
225
226
|
});
|
|
226
227
|
});
|
|
227
228
|
|
|
228
|
-
describe("getVisibleWindow", () => {
|
|
229
|
-
test("shows the full selected range at default zoom and lets the renderer downsample it", () => {
|
|
230
|
-
const history = buildDenseHistory(252);
|
|
231
|
-
const viewState: ChartViewState = {
|
|
232
|
-
presetRange: "1Y",
|
|
233
|
-
bufferRange: "1Y",
|
|
234
|
-
activePreset: "1Y",
|
|
235
|
-
resolution: "auto",
|
|
236
|
-
panOffset: 0,
|
|
237
|
-
zoomLevel: 1,
|
|
238
|
-
cursorX: null,
|
|
239
|
-
cursorY: null,
|
|
240
|
-
};
|
|
241
|
-
|
|
242
|
-
const window = getVisibleWindow(history, viewState, 80);
|
|
243
|
-
|
|
244
|
-
expect(window.points).toHaveLength(history.length);
|
|
245
|
-
expect(window.points[0]?.date.toISOString()).toBe(history[0]!.date.toISOString());
|
|
246
|
-
expect(window.points.at(-1)?.date.toISOString()).toBe(history.at(-1)!.date.toISOString());
|
|
247
|
-
});
|
|
248
|
-
|
|
249
|
-
test("zooms into the selected range instead of pinning the default view to chart width", () => {
|
|
250
|
-
const history = buildDenseHistory(252);
|
|
251
|
-
const viewState: ChartViewState = {
|
|
252
|
-
presetRange: "1Y",
|
|
253
|
-
bufferRange: "1Y",
|
|
254
|
-
activePreset: "1Y",
|
|
255
|
-
resolution: "auto",
|
|
256
|
-
panOffset: 0,
|
|
257
|
-
zoomLevel: 2,
|
|
258
|
-
cursorX: null,
|
|
259
|
-
cursorY: null,
|
|
260
|
-
};
|
|
261
|
-
|
|
262
|
-
const window = getVisibleWindow(history, viewState, 80);
|
|
263
|
-
|
|
264
|
-
expect(window.points).toHaveLength(126);
|
|
265
|
-
expect(window.points[0]?.date.toISOString()).toBe(history[126]!.date.toISOString());
|
|
266
|
-
expect(window.points.at(-1)?.date.toISOString()).toBe(history.at(-1)!.date.toISOString());
|
|
267
|
-
});
|
|
268
|
-
});
|
|
269
|
-
|
|
270
229
|
describe("renderChart", () => {
|
|
271
230
|
test("sizes the y-axis to the visible label width instead of a fixed gutter", () => {
|
|
272
231
|
expect(resolveChartAxisWidth(["$18", "$17", "$16", "$14"], 4, 10)).toBe(4);
|
|
@@ -457,6 +416,62 @@ describe("renderChart", () => {
|
|
|
457
416
|
);
|
|
458
417
|
});
|
|
459
418
|
|
|
419
|
+
test("overlays the cursor date on the x-axis without changing its width", () => {
|
|
420
|
+
const dates = Array.from({ length: 13 }, (_, index) => new Date(2026, 0, 5, 9, 30 + index * 30));
|
|
421
|
+
const width = 72;
|
|
422
|
+
const segments = buildCursorTimeAxisSegments({
|
|
423
|
+
timeLabels: buildTimeAxis(dates, width),
|
|
424
|
+
width,
|
|
425
|
+
cursorColumn: 36,
|
|
426
|
+
cursorDate: dates[6]!,
|
|
427
|
+
dates,
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
expect(segments.map((segment) => segment.text).join("")).toHaveLength(width);
|
|
431
|
+
expect(segments.find((segment) => segment.highlighted)?.text).toBe("12:30");
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
test("positions desktop cursor x-axis overlay from exact pixels", () => {
|
|
435
|
+
const dates = Array.from({ length: 13 }, (_, index) => new Date(2026, 0, 5, 9, 30 + index * 30));
|
|
436
|
+
const width = 72;
|
|
437
|
+
const cursorPixelX = 291.5;
|
|
438
|
+
const cellWidthPx = 8;
|
|
439
|
+
const segments = buildCursorTimeAxisSegments({
|
|
440
|
+
timeLabels: buildTimeAxis(dates, width),
|
|
441
|
+
width,
|
|
442
|
+
cursorColumn: 36,
|
|
443
|
+
cursorDate: dates[6]!,
|
|
444
|
+
dates,
|
|
445
|
+
});
|
|
446
|
+
const overlay = buildCursorTimeAxisOverlay({
|
|
447
|
+
segments,
|
|
448
|
+
width,
|
|
449
|
+
cursorPixelX,
|
|
450
|
+
cellWidthPx,
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
expect(overlay.label).toBe("12:30");
|
|
454
|
+
expect(overlay.baseText).toHaveLength(width);
|
|
455
|
+
expect(overlay.leftPercent).toBeCloseTo((cursorPixelX / (width * cellWidthPx - 1)) * 100, 5);
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
test("positions desktop cursor y-axis overlay from exact pixels", () => {
|
|
459
|
+
const height = 8;
|
|
460
|
+
const cellHeightPx = 18;
|
|
461
|
+
const cursorPixelY = 45.25;
|
|
462
|
+
const overlay = buildCursorPriceAxisOverlay({
|
|
463
|
+
axisWidth: 7,
|
|
464
|
+
axisSectionWidth: 8,
|
|
465
|
+
height,
|
|
466
|
+
cursorPixelY,
|
|
467
|
+
cursorLabel: "$214.03",
|
|
468
|
+
cellHeightPx,
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
expect(overlay.labelText).toBe("$214.03 ");
|
|
472
|
+
expect(overlay.topPercent).toBeCloseTo((cursorPixelY / (height * cellHeightPx - 1)) * 100, 5);
|
|
473
|
+
});
|
|
474
|
+
|
|
460
475
|
test("shows second precision when the visible window reaches second-level data", () => {
|
|
461
476
|
const dates = Array.from({ length: 6 }, (_, index) => new Date(2026, 0, 5, 9, 30, index * 10));
|
|
462
477
|
|
|
@@ -25,10 +25,9 @@ describe("projectComparisonChartData", () => {
|
|
|
25
25
|
const window = getVisibleComparisonWindow([
|
|
26
26
|
makeSeries("AAPL", "USD", closes),
|
|
27
27
|
], {
|
|
28
|
-
timeRange: "ALL",
|
|
29
28
|
panOffset: 0,
|
|
30
29
|
zoomLevel: 1,
|
|
31
|
-
}
|
|
30
|
+
});
|
|
32
31
|
|
|
33
32
|
expect(window.dates).toHaveLength(120);
|
|
34
33
|
expect(window.dates[0]?.toISOString()).toBe("2024-01-01T00:00:00.000Z");
|
|
@@ -48,7 +47,6 @@ describe("projectComparisonChartData", () => {
|
|
|
48
47
|
["2024-01-05", 22],
|
|
49
48
|
]),
|
|
50
49
|
], 8, {
|
|
51
|
-
timeRange: "ALL",
|
|
52
50
|
panOffset: 0,
|
|
53
51
|
zoomLevel: 1,
|
|
54
52
|
renderMode: "line",
|
|
@@ -72,7 +70,6 @@ describe("projectComparisonChartData", () => {
|
|
|
72
70
|
["2024-01-04", 60],
|
|
73
71
|
]),
|
|
74
72
|
], 8, {
|
|
75
|
-
timeRange: "ALL",
|
|
76
73
|
panOffset: 0,
|
|
77
74
|
zoomLevel: 1,
|
|
78
75
|
renderMode: "line",
|
|
@@ -94,7 +91,6 @@ describe("projectComparisonChartData", () => {
|
|
|
94
91
|
["2024-01-03", 2100],
|
|
95
92
|
]),
|
|
96
93
|
], 8, {
|
|
97
|
-
timeRange: "ALL",
|
|
98
94
|
panOffset: 0,
|
|
99
95
|
zoomLevel: 1,
|
|
100
96
|
renderMode: "line",
|
|
@@ -83,9 +83,7 @@ function getUniqueSortedDates(series: ComparisonChartSeries[]): Date[] {
|
|
|
83
83
|
|
|
84
84
|
export function getMaxComparisonPanOffset(
|
|
85
85
|
series: ComparisonChartSeries[],
|
|
86
|
-
_timeRange: TimeRange,
|
|
87
86
|
zoomLevel: number,
|
|
88
|
-
chartWidth: number,
|
|
89
87
|
): number {
|
|
90
88
|
const dates = getUniqueSortedDates(series);
|
|
91
89
|
const visibleCount = getVisiblePointCount(dates.length, zoomLevel);
|
|
@@ -95,7 +93,6 @@ export function getMaxComparisonPanOffset(
|
|
|
95
93
|
export function getVisibleComparisonWindow(
|
|
96
94
|
series: ComparisonChartSeries[],
|
|
97
95
|
viewState: Pick<ComparisonChartViewState, "panOffset" | "zoomLevel">,
|
|
98
|
-
chartWidth: number,
|
|
99
96
|
): ComparisonVisibleWindow {
|
|
100
97
|
const dates = getUniqueSortedDates(series);
|
|
101
98
|
|
|
@@ -214,7 +211,7 @@ export function projectComparisonChartData(
|
|
|
214
211
|
requestedAxisMode: ChartAxisMode,
|
|
215
212
|
): ComparisonChartProjection {
|
|
216
213
|
const normalizedSeries = series.map((entry) => ({ ...entry, points: normalizeSeriesPoints(entry.points) }));
|
|
217
|
-
const window = getVisibleComparisonWindow(normalizedSeries, viewState
|
|
214
|
+
const window = getVisibleComparisonWindow(normalizedSeries, viewState);
|
|
218
215
|
const axisResolution = resolveComparisonAxisMode(requestedAxisMode, normalizedSeries, window.dates);
|
|
219
216
|
const requestedMode = normalizeComparisonMode(viewState.renderMode);
|
|
220
217
|
if (window.dates.length === 0) {
|
|
@@ -119,6 +119,32 @@ describe("renderComparisonChart", () => {
|
|
|
119
119
|
expect(result.activeDate?.toISOString()).toBe("2024-01-03T00:00:00.000Z");
|
|
120
120
|
});
|
|
121
121
|
|
|
122
|
+
test("renders when precise desktop layout passes a fractional chart height", () => {
|
|
123
|
+
const projection = projectComparisonChartData([
|
|
124
|
+
makeSeries("AAPL", "#00ff00", [10, 12, 11, 13]),
|
|
125
|
+
], 12, {
|
|
126
|
+
panOffset: 0,
|
|
127
|
+
zoomLevel: 1,
|
|
128
|
+
renderMode: "area",
|
|
129
|
+
}, "percent");
|
|
130
|
+
|
|
131
|
+
const result = renderComparisonChart(projection, {
|
|
132
|
+
width: 12,
|
|
133
|
+
height: 6.5,
|
|
134
|
+
cursorX: null,
|
|
135
|
+
cursorY: null,
|
|
136
|
+
selectedSymbol: "AAPL",
|
|
137
|
+
colors: {
|
|
138
|
+
bgColor: "#000000",
|
|
139
|
+
gridColor: "#333333",
|
|
140
|
+
crosshairColor: "#ffffff",
|
|
141
|
+
},
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
expect(result.lines.length).toBeGreaterThan(0);
|
|
145
|
+
expect(textLines(result).some((line) => /[^\s]/.test(line))).toBe(true);
|
|
146
|
+
});
|
|
147
|
+
|
|
122
148
|
test("keeps terminal comparison chart cells transparent", () => {
|
|
123
149
|
const projection = projectComparisonChartData([
|
|
124
150
|
makeSeries("AAPL", "#00ff00", [10, 12, 11, 13]),
|
|
@@ -80,6 +80,11 @@ function clamp(value: number, min: number, max: number): number {
|
|
|
80
80
|
return Math.max(min, Math.min(max, value));
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
+
function normalizeCount(value: number, min: number): number {
|
|
84
|
+
if (!Number.isFinite(value)) return min;
|
|
85
|
+
return Math.max(Math.floor(value), min);
|
|
86
|
+
}
|
|
87
|
+
|
|
83
88
|
function getComparisonDotX(index: number, count: number, width: number): number {
|
|
84
89
|
if (count <= 1) return Math.round(Math.max(width - 1, 0) / 2);
|
|
85
90
|
return Math.round((index / (count - 1)) * Math.max(width - 1, 0));
|
|
@@ -260,15 +265,17 @@ export function buildComparisonChartScene(
|
|
|
260
265
|
|
|
261
266
|
const min = Math.min(...values);
|
|
262
267
|
const max = Math.max(...values);
|
|
268
|
+
const width = normalizeCount(opts.width, 1);
|
|
269
|
+
const height = normalizeCount(opts.height, 1);
|
|
263
270
|
const selectedSeries = getSelectedSeries(projection.series, opts.selectedSymbol);
|
|
264
|
-
const activeIdx = getActiveIndex(projection.dates.length,
|
|
271
|
+
const activeIdx = getActiveIndex(projection.dates.length, width, opts.cursorX);
|
|
265
272
|
const activeDate = projection.dates[activeIdx] ?? projection.dates[projection.dates.length - 1]!;
|
|
266
273
|
const selectedPoint = selectedSeries?.points[activeIdx] ?? null;
|
|
267
|
-
const chartRows =
|
|
274
|
+
const chartRows = height;
|
|
268
275
|
const range = max - min || 1;
|
|
269
276
|
const cursorX = opts.cursorX === null
|
|
270
277
|
? null
|
|
271
|
-
: clamp(opts.cursorX, 0, Math.max(
|
|
278
|
+
: clamp(opts.cursorX, 0, Math.max(width - 1, 0));
|
|
272
279
|
const fallbackValue = selectedPoint?.value ?? selectedSeries?.latestValue ?? min;
|
|
273
280
|
const cursorY = cursorX === null
|
|
274
281
|
? null
|
|
@@ -282,7 +289,7 @@ export function buildComparisonChartScene(
|
|
|
282
289
|
const cursorColumn = cursorX === null ? null : Math.round(cursorX);
|
|
283
290
|
const cursorDotX = cursorX === null
|
|
284
291
|
? null
|
|
285
|
-
: Math.round((cursorX / Math.max(
|
|
292
|
+
: Math.round((cursorX / Math.max(width - 1, 1)) * Math.max(width * 2 - 1, 0));
|
|
286
293
|
const cursorRow = cursorY === null ? null : Math.round(cursorY);
|
|
287
294
|
const crosshairValue = cursorY === null
|
|
288
295
|
? null
|
|
@@ -291,8 +298,8 @@ export function buildComparisonChartScene(
|
|
|
291
298
|
return {
|
|
292
299
|
dates: projection.dates,
|
|
293
300
|
series: projection.series,
|
|
294
|
-
width
|
|
295
|
-
height
|
|
301
|
+
width,
|
|
302
|
+
height,
|
|
296
303
|
chartRows,
|
|
297
304
|
mode: projection.effectiveMode,
|
|
298
305
|
axisMode: projection.effectiveAxisMode,
|