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
|
@@ -8,6 +8,7 @@ import { buildChartKey } from "../../market-data/selectors";
|
|
|
8
8
|
import { useAppSelector, usePaneSettingValue } from "../../state/app-context";
|
|
9
9
|
import { usePaneFooter, type PaneHint } from "../layout/pane-footer";
|
|
10
10
|
import { blendHex, colors, getComparisonSeriesColor, priceColor } from "../../theme/colors";
|
|
11
|
+
import { useThemeColors } from "../../theme/theme-context";
|
|
11
12
|
import type { BrokerContractRef } from "../../types/instrument";
|
|
12
13
|
import type { PricePoint } from "../../types/financials";
|
|
13
14
|
import { formatPercentRaw } from "../../utils/format";
|
|
@@ -54,12 +55,10 @@ import {
|
|
|
54
55
|
normalizeChartResolutionSupport,
|
|
55
56
|
sortChartResolutions,
|
|
56
57
|
type ChartResolutionSupport,
|
|
57
|
-
type ManualChartResolution,
|
|
58
58
|
} from "./chart-resolution";
|
|
59
59
|
import { RIGHT_EDGE_ANCHOR_RATIO, resolveAnchoredChartZoom } from "./chart-viewport";
|
|
60
60
|
import {
|
|
61
61
|
buildComparisonChartScene,
|
|
62
|
-
formatComparisonAxisValue,
|
|
63
62
|
formatComparisonCursorAxisValue,
|
|
64
63
|
renderComparisonChart,
|
|
65
64
|
} from "./comparison-chart-renderer";
|
|
@@ -97,7 +96,9 @@ import {
|
|
|
97
96
|
resolveNativeSurfaceVisibleRect,
|
|
98
97
|
type NativeSurfaceRenderableNode,
|
|
99
98
|
} from "./native/surface-visibility";
|
|
100
|
-
import {
|
|
99
|
+
import { formatDateShort, resolveChartAxisWidth, type StyledContent } from "./chart-renderer";
|
|
100
|
+
import { TimeAxisLabel } from "./time-axis-label";
|
|
101
|
+
import { PriceAxisLabels } from "./price-axis-labels";
|
|
101
102
|
|
|
102
103
|
const MODE_CHIPS: Record<ComparisonChartRenderMode, string> = {
|
|
103
104
|
area: "A",
|
|
@@ -112,6 +113,7 @@ interface ComparisonStockChartProps {
|
|
|
112
113
|
symbols: string[];
|
|
113
114
|
axisMode: ChartAxisMode;
|
|
114
115
|
onOpenSymbol: (symbol: string) => void;
|
|
116
|
+
onEditTickers?: () => void;
|
|
115
117
|
}
|
|
116
118
|
|
|
117
119
|
interface ComparisonChartSymbolSource {
|
|
@@ -514,7 +516,9 @@ function ComparisonStockChartView({
|
|
|
514
516
|
preferredRenderer,
|
|
515
517
|
symbolSources,
|
|
516
518
|
onOpenSymbol,
|
|
519
|
+
onEditTickers,
|
|
517
520
|
}: ComparisonStockChartViewProps) {
|
|
521
|
+
useThemeColors();
|
|
518
522
|
const renderer = useNativeRenderer();
|
|
519
523
|
const { canvasCharts, cellWidthPx = 8, cellHeightPx = 18, pixelRatio = 1 } = useUiCapabilities();
|
|
520
524
|
const nativeSurfaceManager = useMemo(() => getNativeSurfaceManager(renderer), [renderer]);
|
|
@@ -548,6 +552,7 @@ function ComparisonStockChartView({
|
|
|
548
552
|
const lastCanvasBaseBitmapRef = useRef<{ key: string; bitmap: NativeChartBitmap } | null>(null);
|
|
549
553
|
const displayCursorRef = useRef<DisplayCursorState>(EMPTY_DISPLAY_CURSOR);
|
|
550
554
|
const targetCursorRef = useRef<DisplayCursorState>(EMPTY_DISPLAY_CURSOR);
|
|
555
|
+
const mouseCrosshairDisabledRef = useRef(false);
|
|
551
556
|
const cursorMotionKindRef = useRef<ChartCursorMotionKind>("discrete");
|
|
552
557
|
const animationFrameRef = useRef<number | null>(null);
|
|
553
558
|
const pendingCanonicalResetRef = useRef(1);
|
|
@@ -908,11 +913,10 @@ function ComparisonStockChartView({
|
|
|
908
913
|
}
|
|
909
914
|
return {
|
|
910
915
|
...current,
|
|
911
|
-
panOffset: clamp(pendingExpansion.targetPanOffset, 0, getMaxComparisonPanOffset(series, current.
|
|
916
|
+
panOffset: clamp(pendingExpansion.targetPanOffset, 0, getMaxComparisonPanOffset(series, current.zoomLevel)),
|
|
912
917
|
};
|
|
913
918
|
});
|
|
914
919
|
}, [
|
|
915
|
-
chartWidth,
|
|
916
920
|
series,
|
|
917
921
|
seriesDates,
|
|
918
922
|
viewState.activePreset,
|
|
@@ -982,8 +986,8 @@ function ComparisonStockChartView({
|
|
|
982
986
|
}), [viewState.panOffset, viewState.renderMode, viewState.zoomLevel]);
|
|
983
987
|
|
|
984
988
|
const visibleWindow = useMemo(() => (
|
|
985
|
-
getVisibleComparisonWindow(series, projectionViewState
|
|
986
|
-
), [
|
|
989
|
+
getVisibleComparisonWindow(series, projectionViewState)
|
|
990
|
+
), [projectionViewState, series]);
|
|
987
991
|
|
|
988
992
|
const projection = useMemo(() => (
|
|
989
993
|
projectComparisonChartData(series, chartWidth, projectionViewState, axisMode)
|
|
@@ -1037,6 +1041,14 @@ function ComparisonStockChartView({
|
|
|
1037
1041
|
selectedSymbol: viewState.selectedSymbol,
|
|
1038
1042
|
colors: chartColors,
|
|
1039
1043
|
}), [chartColors, chartHeight, chartWidth, projection, viewState.selectedSymbol]);
|
|
1044
|
+
const displayScene = useMemo(() => buildComparisonChartScene(projection, {
|
|
1045
|
+
width: chartWidth,
|
|
1046
|
+
height: chartHeight,
|
|
1047
|
+
cursorX: displayCursorX,
|
|
1048
|
+
cursorY: displayCursorY,
|
|
1049
|
+
selectedSymbol: viewState.selectedSymbol,
|
|
1050
|
+
colors: chartColors,
|
|
1051
|
+
}), [chartColors, chartHeight, chartWidth, displayCursorX, displayCursorY, projection, viewState.selectedSymbol]);
|
|
1040
1052
|
|
|
1041
1053
|
const staticResult = useMemo(() => renderComparisonChart(projection, {
|
|
1042
1054
|
width: chartWidth,
|
|
@@ -1088,6 +1100,12 @@ function ComparisonStockChartView({
|
|
|
1088
1100
|
};
|
|
1089
1101
|
|
|
1090
1102
|
return [
|
|
1103
|
+
...(onEditTickers ? [{
|
|
1104
|
+
id: "tickers",
|
|
1105
|
+
key: "t",
|
|
1106
|
+
label: "ickers",
|
|
1107
|
+
onPress: onEditTickers,
|
|
1108
|
+
}] : []),
|
|
1091
1109
|
{
|
|
1092
1110
|
id: "mode",
|
|
1093
1111
|
key: "m",
|
|
@@ -1111,26 +1129,13 @@ function ComparisonStockChartView({
|
|
|
1111
1129
|
{ id: "reset", key: "0", label: "reset", onPress: resetView },
|
|
1112
1130
|
...(width >= 72 ? [{ id: "range", key: "1-7", label: "range", onPress: cycleRange }] : []),
|
|
1113
1131
|
];
|
|
1114
|
-
}, [effectiveResolution, resolutionChips, series, supportMap, viewState.presetRange, visibleDateWindow, width]);
|
|
1132
|
+
}, [effectiveResolution, onEditTickers, resolutionChips, series, supportMap, viewState.presetRange, visibleDateWindow, width]);
|
|
1115
1133
|
|
|
1116
1134
|
usePaneFooter("comparison-chart", () => ({
|
|
1117
1135
|
order: 10,
|
|
1118
1136
|
hints: footerHints,
|
|
1119
1137
|
}), [footerHints]);
|
|
1120
1138
|
|
|
1121
|
-
const liveScene = useMemo(() => (
|
|
1122
|
-
effectiveRenderer === "kitty"
|
|
1123
|
-
? staticScene
|
|
1124
|
-
: buildComparisonChartScene(projection, {
|
|
1125
|
-
width: chartWidth,
|
|
1126
|
-
height: chartHeight,
|
|
1127
|
-
cursorX: displayCursorX,
|
|
1128
|
-
cursorY: displayCursorY,
|
|
1129
|
-
selectedSymbol: viewState.selectedSymbol,
|
|
1130
|
-
colors: chartColors,
|
|
1131
|
-
})
|
|
1132
|
-
), [chartColors, chartHeight, chartWidth, displayCursorX, displayCursorY, effectiveRenderer, projection, staticScene, viewState.selectedSymbol]);
|
|
1133
|
-
|
|
1134
1139
|
const nativeCrosshair = useMemo<NativeCrosshairOverlay | null>(() => {
|
|
1135
1140
|
if (displayCursor.cellX === null || displayCursor.cellY === null) return null;
|
|
1136
1141
|
return {
|
|
@@ -1385,7 +1390,7 @@ function ComparisonStockChartView({
|
|
|
1385
1390
|
return;
|
|
1386
1391
|
case "a": {
|
|
1387
1392
|
const panStep = getKeyboardPanCellCount(chartWidth);
|
|
1388
|
-
if (viewState.panOffset >= getMaxComparisonPanOffset(series, viewState.
|
|
1393
|
+
if (viewState.panOffset >= getMaxComparisonPanOffset(series, viewState.zoomLevel) && expandBufferRange({
|
|
1389
1394
|
kind: "pan-left",
|
|
1390
1395
|
targetPanOffset: viewState.panOffset + panStep,
|
|
1391
1396
|
})) {
|
|
@@ -1393,7 +1398,7 @@ function ComparisonStockChartView({
|
|
|
1393
1398
|
}
|
|
1394
1399
|
setViewState((current) => ({
|
|
1395
1400
|
...clearActivePreset(current),
|
|
1396
|
-
panOffset: clamp(current.panOffset + panStep, 0, getMaxComparisonPanOffset(series, current.
|
|
1401
|
+
panOffset: clamp(current.panOffset + panStep, 0, getMaxComparisonPanOffset(series, current.zoomLevel)),
|
|
1397
1402
|
}));
|
|
1398
1403
|
return;
|
|
1399
1404
|
}
|
|
@@ -1401,7 +1406,7 @@ function ComparisonStockChartView({
|
|
|
1401
1406
|
const panStep = getKeyboardPanCellCount(chartWidth);
|
|
1402
1407
|
setViewState((current) => ({
|
|
1403
1408
|
...clearActivePreset(current),
|
|
1404
|
-
panOffset: clamp(current.panOffset - panStep, 0, getMaxComparisonPanOffset(series, current.
|
|
1409
|
+
panOffset: clamp(current.panOffset - panStep, 0, getMaxComparisonPanOffset(series, current.zoomLevel)),
|
|
1405
1410
|
}));
|
|
1406
1411
|
return;
|
|
1407
1412
|
}
|
|
@@ -1417,7 +1422,11 @@ function ComparisonStockChartView({
|
|
|
1417
1422
|
setResolution(nextResolution);
|
|
1418
1423
|
return;
|
|
1419
1424
|
}
|
|
1425
|
+
case "t":
|
|
1426
|
+
onEditTickers?.();
|
|
1427
|
+
return;
|
|
1420
1428
|
case "h":
|
|
1429
|
+
mouseCrosshairDisabledRef.current = false;
|
|
1421
1430
|
cursorMotionKindRef.current = "discrete";
|
|
1422
1431
|
setViewState((current) => ({
|
|
1423
1432
|
...current,
|
|
@@ -1425,6 +1434,7 @@ function ComparisonStockChartView({
|
|
|
1425
1434
|
}));
|
|
1426
1435
|
return;
|
|
1427
1436
|
case "l":
|
|
1437
|
+
mouseCrosshairDisabledRef.current = false;
|
|
1428
1438
|
cursorMotionKindRef.current = "discrete";
|
|
1429
1439
|
setViewState((current) => ({
|
|
1430
1440
|
...current,
|
|
@@ -1432,6 +1442,7 @@ function ComparisonStockChartView({
|
|
|
1432
1442
|
}));
|
|
1433
1443
|
return;
|
|
1434
1444
|
case "escape":
|
|
1445
|
+
mouseCrosshairDisabledRef.current = true;
|
|
1435
1446
|
updateDisplayCursorTarget(EMPTY_DISPLAY_CURSOR, "discrete");
|
|
1436
1447
|
setViewState((current) => ({ ...current, cursorX: null, cursorY: null }));
|
|
1437
1448
|
return;
|
|
@@ -1488,6 +1499,7 @@ function ComparisonStockChartView({
|
|
|
1488
1499
|
});
|
|
1489
1500
|
|
|
1490
1501
|
const handlePlotMove = (event: ChartMouseEvent) => {
|
|
1502
|
+
if (mouseCrosshairDisabledRef.current) return;
|
|
1491
1503
|
const localPointer = getLocalPlotPointer(event, plotRef.current, renderer);
|
|
1492
1504
|
if (!localPointer) return;
|
|
1493
1505
|
const selectionCursor = resolveSelectionCursor(localPointer, projection.dates.length, chartWidth);
|
|
@@ -1506,6 +1518,7 @@ function ComparisonStockChartView({
|
|
|
1506
1518
|
};
|
|
1507
1519
|
|
|
1508
1520
|
const handlePlotDown = (event: ChartMouseEvent) => {
|
|
1521
|
+
mouseCrosshairDisabledRef.current = false;
|
|
1509
1522
|
const localPointer = getLocalPlotPointer(event, plotRef.current, renderer);
|
|
1510
1523
|
if (!localPointer) return;
|
|
1511
1524
|
const selectionCursor = resolveSelectionCursor(localPointer, projection.dates.length, chartWidth);
|
|
@@ -1528,6 +1541,7 @@ function ComparisonStockChartView({
|
|
|
1528
1541
|
};
|
|
1529
1542
|
|
|
1530
1543
|
const handlePlotDrag = (event: ChartMouseEvent) => {
|
|
1544
|
+
if (mouseCrosshairDisabledRef.current && !dragRef.current) return;
|
|
1531
1545
|
const localPointer = getLocalPlotPointer(event, plotRef.current, renderer);
|
|
1532
1546
|
if (localPointer) {
|
|
1533
1547
|
const selectionCursor = resolveSelectionCursor(localPointer, projection.dates.length, chartWidth);
|
|
@@ -1592,7 +1606,7 @@ function ComparisonStockChartView({
|
|
|
1592
1606
|
if (scrollPanCells === 0) return;
|
|
1593
1607
|
|
|
1594
1608
|
const targetPanOffset = viewState.panOffset + scrollPanCells;
|
|
1595
|
-
if (scrollPanCells > 0 && viewState.panOffset >= getMaxComparisonPanOffset(series, viewState.
|
|
1609
|
+
if (scrollPanCells > 0 && viewState.panOffset >= getMaxComparisonPanOffset(series, viewState.zoomLevel) && expandBufferRange({
|
|
1596
1610
|
kind: "pan-left",
|
|
1597
1611
|
targetPanOffset,
|
|
1598
1612
|
})) {
|
|
@@ -1603,7 +1617,7 @@ function ComparisonStockChartView({
|
|
|
1603
1617
|
panOffset: clamp(
|
|
1604
1618
|
current.panOffset + scrollPanCells,
|
|
1605
1619
|
0,
|
|
1606
|
-
getMaxComparisonPanOffset(series, current.
|
|
1620
|
+
getMaxComparisonPanOffset(series, current.zoomLevel),
|
|
1607
1621
|
),
|
|
1608
1622
|
}));
|
|
1609
1623
|
};
|
|
@@ -1613,7 +1627,8 @@ function ComparisonStockChartView({
|
|
|
1613
1627
|
}
|
|
1614
1628
|
|
|
1615
1629
|
const hasChartData = series.some((entry) => entry.points.length > 0);
|
|
1616
|
-
const
|
|
1630
|
+
const hasDisplayCursor = displayCursorX !== null && displayCursorY !== null;
|
|
1631
|
+
const cursorScene = hasDisplayCursor ? displayScene : staticScene;
|
|
1617
1632
|
const selectedSeries = hasChartData ? (cursorScene?.selectedSeries ?? staticResult.selectedSeries) : null;
|
|
1618
1633
|
const selectedPoint = hasChartData ? (cursorScene?.selectedPoint ?? staticResult.selectedPoint) : null;
|
|
1619
1634
|
const selectedRawValue = selectedPoint?.rawValue ?? selectedSeries?.latestRawValue ?? null;
|
|
@@ -1641,6 +1656,8 @@ function ComparisonStockChartView({
|
|
|
1641
1656
|
start: visibleWindow.dates[0] ?? null,
|
|
1642
1657
|
end: visibleWindow.dates[visibleWindow.dates.length - 1] ?? null,
|
|
1643
1658
|
});
|
|
1659
|
+
const cursorTimeAxisColumn = hasDisplayCursor ? displayScene?.cursorColumn ?? null : null;
|
|
1660
|
+
const cursorTimeAxisDate = hasDisplayCursor ? displayScene?.activeDate ?? null : null;
|
|
1644
1661
|
|
|
1645
1662
|
const canvasBitmapSize = useMemo(() => {
|
|
1646
1663
|
if (!canvasCharts) return null;
|
|
@@ -1813,17 +1830,16 @@ function ComparisonStockChartView({
|
|
|
1813
1830
|
);
|
|
1814
1831
|
|
|
1815
1832
|
const axisBox = (
|
|
1816
|
-
<
|
|
1817
|
-
{
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
</Box>
|
|
1833
|
+
<PriceAxisLabels
|
|
1834
|
+
axisLabels={axisLabels}
|
|
1835
|
+
axisWidth={axisWidth}
|
|
1836
|
+
axisSectionWidth={axisSectionWidth}
|
|
1837
|
+
height={chartHeight}
|
|
1838
|
+
cursorRow={cursorRow}
|
|
1839
|
+
cursorPixelY={hasDisplayCursor ? displayCursor.pixelY : null}
|
|
1840
|
+
cursorLabel={cursorAxisLabel}
|
|
1841
|
+
cursorColor={chartColors.crosshairColor}
|
|
1842
|
+
/>
|
|
1827
1843
|
);
|
|
1828
1844
|
|
|
1829
1845
|
const legendRowsData = Array.from({ length: Math.ceil(symbols.length / legendColumns) }, (_, rowIndex) => (
|
|
@@ -1929,7 +1945,15 @@ function ComparisonStockChartView({
|
|
|
1929
1945
|
</Box>
|
|
1930
1946
|
|
|
1931
1947
|
<Box height={timeAxisRows}>
|
|
1932
|
-
<
|
|
1948
|
+
<TimeAxisLabel
|
|
1949
|
+
timeLabels={timeAxisLabel}
|
|
1950
|
+
width={chartWidth}
|
|
1951
|
+
cursorColumn={cursorTimeAxisColumn}
|
|
1952
|
+
cursorPixelX={hasDisplayCursor ? displayCursor.pixelX : null}
|
|
1953
|
+
cursorDate={cursorTimeAxisDate}
|
|
1954
|
+
dates={projection.dates}
|
|
1955
|
+
cursorColor={chartColors.crosshairColor}
|
|
1956
|
+
/>
|
|
1933
1957
|
</Box>
|
|
1934
1958
|
|
|
1935
1959
|
{legendRows > 0 && (
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { useMemo } from "react";
|
|
2
|
+
import { Box, Text, useUiCapabilities } from "../../ui";
|
|
3
|
+
import { colors } from "../../theme/colors";
|
|
4
|
+
import { formatAxisCell } from "./chart-renderer";
|
|
5
|
+
|
|
6
|
+
interface PriceAxisLabelsProps {
|
|
7
|
+
axisLabels: ReadonlyMap<number, string>;
|
|
8
|
+
axisWidth: number;
|
|
9
|
+
axisSectionWidth: number;
|
|
10
|
+
height: number;
|
|
11
|
+
cursorRow: number | null;
|
|
12
|
+
cursorPixelY?: number | null;
|
|
13
|
+
cursorLabel: string | null;
|
|
14
|
+
cursorColor: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface CursorPriceAxisOverlay {
|
|
18
|
+
labelText: string | null;
|
|
19
|
+
topPercent: number | null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function clamp(value: number, min: number, max: number): number {
|
|
23
|
+
return Math.max(min, Math.min(max, value));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function buildCursorPriceAxisOverlay({
|
|
27
|
+
axisWidth,
|
|
28
|
+
axisSectionWidth,
|
|
29
|
+
height,
|
|
30
|
+
cursorPixelY,
|
|
31
|
+
cursorLabel,
|
|
32
|
+
cellHeightPx,
|
|
33
|
+
}: {
|
|
34
|
+
axisWidth: number;
|
|
35
|
+
axisSectionWidth: number;
|
|
36
|
+
height: number;
|
|
37
|
+
cursorPixelY: number | null | undefined;
|
|
38
|
+
cursorLabel: string | null;
|
|
39
|
+
cellHeightPx: number;
|
|
40
|
+
}): CursorPriceAxisOverlay {
|
|
41
|
+
const labelText = cursorLabel === null
|
|
42
|
+
? null
|
|
43
|
+
: formatAxisCell(cursorLabel, axisWidth).padEnd(axisSectionWidth);
|
|
44
|
+
const pixelHeight = Math.max(height * cellHeightPx, 1);
|
|
45
|
+
let topPercent: number | null = null;
|
|
46
|
+
|
|
47
|
+
if (labelText && cursorPixelY !== null && cursorPixelY !== undefined && Number.isFinite(cursorPixelY) && pixelHeight > 1) {
|
|
48
|
+
const halfLabelHeight = Math.min(cellHeightPx / 2, (pixelHeight - 1) / 2);
|
|
49
|
+
const topPx = clamp(cursorPixelY, halfLabelHeight, Math.max(pixelHeight - halfLabelHeight, halfLabelHeight));
|
|
50
|
+
topPercent = (topPx / Math.max(pixelHeight - 1, 1)) * 100;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return { labelText, topPercent };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function PriceAxisLabels({
|
|
57
|
+
axisLabels,
|
|
58
|
+
axisWidth,
|
|
59
|
+
axisSectionWidth,
|
|
60
|
+
height,
|
|
61
|
+
cursorRow,
|
|
62
|
+
cursorPixelY = null,
|
|
63
|
+
cursorLabel,
|
|
64
|
+
cursorColor,
|
|
65
|
+
}: PriceAxisLabelsProps) {
|
|
66
|
+
const { cellHeightPx = 18, fractionalViewport = false } = useUiCapabilities();
|
|
67
|
+
const overlay = useMemo(() => buildCursorPriceAxisOverlay({
|
|
68
|
+
axisWidth,
|
|
69
|
+
axisSectionWidth,
|
|
70
|
+
height,
|
|
71
|
+
cursorPixelY,
|
|
72
|
+
cursorLabel,
|
|
73
|
+
cellHeightPx,
|
|
74
|
+
}), [axisSectionWidth, axisWidth, cellHeightPx, cursorLabel, cursorPixelY, height]);
|
|
75
|
+
const usePixelOverlay = fractionalViewport && overlay.labelText !== null && overlay.topPercent !== null;
|
|
76
|
+
|
|
77
|
+
return (
|
|
78
|
+
<Box
|
|
79
|
+
width={axisSectionWidth}
|
|
80
|
+
height={height}
|
|
81
|
+
flexDirection="column"
|
|
82
|
+
overflow="hidden"
|
|
83
|
+
style={usePixelOverlay ? { position: "relative" } : undefined}
|
|
84
|
+
>
|
|
85
|
+
{Array.from({ length: height }, (_, row) => {
|
|
86
|
+
const isCursorRow = !usePixelOverlay && cursorLabel !== null && cursorRow === row;
|
|
87
|
+
const label = isCursorRow ? cursorLabel : (axisLabels.get(row) ?? null);
|
|
88
|
+
return (
|
|
89
|
+
<Text key={row} fg={isCursorRow ? cursorColor : colors.textDim}>
|
|
90
|
+
{formatAxisCell(label, axisWidth).padEnd(axisSectionWidth)}
|
|
91
|
+
</Text>
|
|
92
|
+
);
|
|
93
|
+
})}
|
|
94
|
+
{usePixelOverlay ? (
|
|
95
|
+
<Text
|
|
96
|
+
width={axisSectionWidth}
|
|
97
|
+
fg={cursorColor}
|
|
98
|
+
bg={colors.bg}
|
|
99
|
+
style={{
|
|
100
|
+
position: "absolute",
|
|
101
|
+
left: 0,
|
|
102
|
+
top: `${overlay.topPercent}%`,
|
|
103
|
+
transform: "translateY(-50%)",
|
|
104
|
+
whiteSpace: "pre",
|
|
105
|
+
pointerEvents: "none",
|
|
106
|
+
zIndex: 1,
|
|
107
|
+
}}
|
|
108
|
+
>
|
|
109
|
+
{overlay.labelText}
|
|
110
|
+
</Text>
|
|
111
|
+
) : null}
|
|
112
|
+
</Box>
|
|
113
|
+
);
|
|
114
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useMemo } from "react";
|
|
2
|
-
import { Box, ChartSurface, Text, useUiCapabilities } from "../../ui";
|
|
2
|
+
import { Box, ChartSurface, Text, useNativeRenderer, useUiCapabilities } from "../../ui";
|
|
3
|
+
import { useThemeColors } from "../../theme/theme-context";
|
|
3
4
|
import type { ProjectedChartPoint } from "./chart-data";
|
|
4
5
|
import {
|
|
5
6
|
buildChartScene,
|
|
@@ -8,7 +9,7 @@ import {
|
|
|
8
9
|
renderChart,
|
|
9
10
|
type RenderChartOptions,
|
|
10
11
|
} from "./chart-renderer";
|
|
11
|
-
import { renderNativeChartBase, type NativeChartBitmap } from "./native/chart-rasterizer";
|
|
12
|
+
import { computeBitmapSize, renderNativeChartBase, type NativeChartBitmap } from "./native/chart-rasterizer";
|
|
12
13
|
|
|
13
14
|
export interface StaticChartSurfaceProps extends Omit<
|
|
14
15
|
RenderChartOptions,
|
|
@@ -45,12 +46,15 @@ export function StaticChartSurface({
|
|
|
45
46
|
yAxisColor,
|
|
46
47
|
formatYAxisValue,
|
|
47
48
|
}: StaticChartSurfaceProps) {
|
|
49
|
+
useThemeColors();
|
|
48
50
|
const {
|
|
49
51
|
canvasCharts,
|
|
52
|
+
nativeCharts,
|
|
50
53
|
cellWidthPx = 8,
|
|
51
54
|
cellHeightPx = 18,
|
|
52
55
|
pixelRatio = 1,
|
|
53
56
|
} = useUiCapabilities();
|
|
57
|
+
const nativeRenderer = useNativeRenderer();
|
|
54
58
|
const timeAxisRows = showTimeAxis ? 1 : 0;
|
|
55
59
|
const labelRows = yAxisLabel ? 1 : 0;
|
|
56
60
|
const totalWidth = Math.max(1, Math.floor(width));
|
|
@@ -101,14 +105,36 @@ export function StaticChartSurface({
|
|
|
101
105
|
: 0;
|
|
102
106
|
const axisGap = axisWidth > 0 ? 1 : 0;
|
|
103
107
|
const plotWidth = Math.max(1, totalWidth - axisWidth - axisGap);
|
|
108
|
+
const rendererResolution = nativeRenderer.resolution;
|
|
109
|
+
const rendererTerminalWidth = nativeRenderer.terminalWidth;
|
|
110
|
+
const rendererTerminalHeight = nativeRenderer.terminalHeight;
|
|
104
111
|
const bitmapSize = useMemo(() => {
|
|
105
|
-
if (
|
|
112
|
+
if (nativeCharts && rendererResolution && rendererTerminalWidth > 0 && rendererTerminalHeight > 0) {
|
|
113
|
+
return computeBitmapSize(
|
|
114
|
+
{ x: 0, y: 0, width: plotWidth, height: plotHeight },
|
|
115
|
+
rendererResolution,
|
|
116
|
+
rendererTerminalWidth,
|
|
117
|
+
rendererTerminalHeight,
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
if (!canvasCharts && !nativeCharts) return null;
|
|
106
121
|
const scale = Math.max(1, pixelRatio);
|
|
107
122
|
return {
|
|
108
123
|
pixelWidth: Math.max(1, Math.round(plotWidth * cellWidthPx * scale)),
|
|
109
124
|
pixelHeight: Math.max(1, Math.round(plotHeight * cellHeightPx * scale)),
|
|
110
125
|
};
|
|
111
|
-
}, [
|
|
126
|
+
}, [
|
|
127
|
+
canvasCharts,
|
|
128
|
+
cellHeightPx,
|
|
129
|
+
cellWidthPx,
|
|
130
|
+
nativeCharts,
|
|
131
|
+
pixelRatio,
|
|
132
|
+
plotHeight,
|
|
133
|
+
plotWidth,
|
|
134
|
+
rendererResolution,
|
|
135
|
+
rendererTerminalHeight,
|
|
136
|
+
rendererTerminalWidth,
|
|
137
|
+
]);
|
|
112
138
|
const renderOptions = useMemo<RenderChartOptions>(() => ({
|
|
113
139
|
width: plotWidth,
|
|
114
140
|
height: plotHeight,
|
|
@@ -9,7 +9,6 @@ import {
|
|
|
9
9
|
resolveAdjacentSelectionCursorX,
|
|
10
10
|
resolveChartKeyboardKey,
|
|
11
11
|
resolveIndicatorBufferRange,
|
|
12
|
-
resolveVisibleChartDateWindow,
|
|
13
12
|
resolveSelectionDisplayCursorState,
|
|
14
13
|
} from "./stock-chart";
|
|
15
14
|
import { getPointTerminalColumn } from "./chart-renderer";
|
|
@@ -215,21 +214,6 @@ describe("stock chart auto helpers", () => {
|
|
|
215
214
|
expect(nextState.window).toEqual(renderedAutoView.window);
|
|
216
215
|
});
|
|
217
216
|
|
|
218
|
-
test("derives the visible label window from the actual visible chart points", () => {
|
|
219
|
-
const visibleWindow = resolveVisibleChartDateWindow([
|
|
220
|
-
{ date: new Date("2026-01-03T00:00:00Z") },
|
|
221
|
-
{ date: new Date("2026-01-06T00:00:00Z") },
|
|
222
|
-
], {
|
|
223
|
-
start: new Date("2026-01-01T00:00:00Z"),
|
|
224
|
-
end: new Date("2026-01-08T00:00:00Z"),
|
|
225
|
-
});
|
|
226
|
-
|
|
227
|
-
expect(visibleWindow).toEqual({
|
|
228
|
-
start: new Date("2026-01-03T00:00:00Z"),
|
|
229
|
-
end: new Date("2026-01-06T00:00:00Z"),
|
|
230
|
-
});
|
|
231
|
-
});
|
|
232
|
-
|
|
233
217
|
test("zooms in by at least one visible point on every auto step when more detail is available", () => {
|
|
234
218
|
const history = makeDailyHistory(1, 10);
|
|
235
219
|
|