gloomberb 0.6.1 → 0.7.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.
Files changed (139) hide show
  1. package/package.json +1 -1
  2. package/patches/@opentui%2Fcore@0.1.90.patch +48 -24
  3. package/src/app.tsx +9 -6
  4. package/src/cli/registry.test.ts +0 -1
  5. package/src/components/chart/chart-controller.ts +0 -1
  6. package/src/components/chart/chart-data.ts +1 -31
  7. package/src/components/chart/chart-renderer.ts +70 -0
  8. package/src/components/chart/chart-resolution.test.ts +0 -36
  9. package/src/components/chart/chart-resolution.ts +0 -27
  10. package/src/components/chart/chart.test.ts +17 -45
  11. package/src/components/chart/comparison-chart-data.test.ts +1 -5
  12. package/src/components/chart/comparison-chart-data.ts +1 -4
  13. package/src/components/chart/comparison-stock-chart.tsx +48 -27
  14. package/src/components/chart/static-chart-surface.tsx +28 -4
  15. package/src/components/chart/stock-chart.test.ts +0 -16
  16. package/src/components/chart/stock-chart.tsx +38 -21
  17. package/src/components/chart/time-axis-label.tsx +43 -0
  18. package/src/components/command-bar/command-bar.test.tsx +71 -8
  19. package/src/components/command-bar/command-bar.tsx +388 -713
  20. package/src/components/command-bar/confirm-body.tsx +51 -0
  21. package/src/components/command-bar/list-model.ts +77 -0
  22. package/src/components/command-bar/view-model.ts +0 -19
  23. package/src/components/command-bar/workflow-ops.ts +4 -0
  24. package/src/components/data-table-stack-view.test.tsx +39 -4
  25. package/src/components/data-table-stack-view.tsx +11 -104
  26. package/src/components/data-table-view.tsx +43 -79
  27. package/src/components/feed-data-table-stack-view.tsx +10 -17
  28. package/src/components/feed-stack-controller.ts +39 -0
  29. package/src/components/index.ts +5 -1
  30. package/src/components/layout/floating-pane.tsx +30 -2
  31. package/src/components/layout/header.tsx +10 -7
  32. package/src/components/layout/pane-cycle-order.test.ts +30 -0
  33. package/src/components/layout/pane-cycle-order.ts +32 -0
  34. package/src/components/layout/pane-footer.tsx +11 -4
  35. package/src/components/layout/pane-header.tsx +5 -5
  36. package/src/components/layout/shell.test.tsx +66 -22
  37. package/src/components/layout/shell.tsx +13 -6
  38. package/src/components/onboarding/onboarding-wizard.tsx +1 -2
  39. package/src/components/pane-settings-dialog.tsx +12 -4
  40. package/src/components/pane-template-wizard.test.tsx +3 -4
  41. package/src/components/speedometer-gauge.test.tsx +45 -0
  42. package/src/components/speedometer-gauge.tsx +558 -0
  43. package/src/components/table-view-shared.tsx +158 -0
  44. package/src/components/ticker-list-table-view.tsx +42 -72
  45. package/src/components/ui/confirm-dialog.tsx +66 -0
  46. package/src/components/ui/data-table.tsx +18 -10
  47. package/src/components/ui/index.ts +0 -1
  48. package/src/components/ui/page-stack-view.tsx +39 -10
  49. package/src/components/ui/table-layout.test.ts +16 -0
  50. package/src/components/ui/table-layout.ts +10 -3
  51. package/src/components/ui/ui.test.tsx +100 -20
  52. package/src/core/state/app-state.ts +6 -3
  53. package/src/market-data/coordinator.test.ts +1 -1
  54. package/src/market-data/coordinator.ts +0 -1
  55. package/src/news/aggregator.ts +0 -3
  56. package/src/plugins/builtin/ai/ask-ai-detail-tab.tsx +46 -16
  57. package/src/plugins/builtin/ai/providers.ts +0 -17
  58. package/src/plugins/builtin/ai/screener-pane.test.tsx +3 -4
  59. package/src/plugins/builtin/ai/screener-pane.tsx +0 -3
  60. package/src/plugins/builtin/ask-ai.test.tsx +89 -2
  61. package/src/plugins/builtin/broker-manager/index.test.tsx +2 -2
  62. package/src/plugins/builtin/broker-manager/index.tsx +19 -36
  63. package/src/plugins/builtin/chat-controller.test.ts +105 -0
  64. package/src/plugins/builtin/chat-controller.ts +75 -6
  65. package/src/plugins/builtin/chat.test.tsx +6 -6
  66. package/src/plugins/builtin/chat.tsx +29 -20
  67. package/src/plugins/builtin/cloud-tweets.tsx +0 -1
  68. package/src/plugins/builtin/comparison-chart.test.tsx +53 -2
  69. package/src/plugins/builtin/comparison-chart.tsx +11 -5
  70. package/src/plugins/builtin/correlation/index.tsx +1 -6
  71. package/src/plugins/builtin/debug.tsx +1 -3
  72. package/src/plugins/builtin/econ/index.tsx +1 -2
  73. package/src/plugins/builtin/fear-greed/fear-greed-data.test.ts +72 -0
  74. package/src/plugins/builtin/fear-greed/fear-greed-data.ts +366 -0
  75. package/src/plugins/builtin/fear-greed/index.tsx +478 -0
  76. package/src/plugins/builtin/fx-matrix/index.tsx +2 -2
  77. package/src/plugins/builtin/help.test.tsx +2 -2
  78. package/src/plugins/builtin/help.tsx +2 -2
  79. package/src/plugins/builtin/holders/index.tsx +10 -15
  80. package/src/plugins/builtin/insider/index.tsx +22 -89
  81. package/src/plugins/builtin/market-movers/index.tsx +1 -1
  82. package/src/plugins/builtin/news-wire/index.tsx +41 -2
  83. package/src/plugins/builtin/news-wire/news-table.tsx +11 -11
  84. package/src/plugins/builtin/news.tsx +4 -16
  85. package/src/plugins/builtin/notes.tsx +18 -55
  86. package/src/plugins/builtin/options.test.tsx +58 -2
  87. package/src/plugins/builtin/options.tsx +108 -49
  88. package/src/plugins/builtin/plugin-groups.ts +2 -0
  89. package/src/plugins/builtin/portfolio-list/cli/commands.ts +1 -1
  90. package/src/plugins/builtin/research/index.tsx +5 -21
  91. package/src/plugins/builtin/sec-filing-display.tsx +84 -0
  92. package/src/plugins/builtin/sec.tsx +13 -66
  93. package/src/plugins/builtin/sectors/index.tsx +1 -1
  94. package/src/plugins/builtin/ticker-detail/chart-tab.tsx +0 -1
  95. package/src/plugins/builtin/ticker-detail/index.tsx +0 -1
  96. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +0 -1
  97. package/src/plugins/builtin/ticker-detail-chart-tab-switch.test.tsx +3 -4
  98. package/src/plugins/builtin/world-indices/index.tsx +1 -1
  99. package/src/plugins/builtin/yield-curve/index.tsx +1 -1
  100. package/src/plugins/ibkr/broker-adapter.ts +1 -3
  101. package/src/plugins/ibkr/gateway-service-native.ts +1 -5
  102. package/src/plugins/ibkr/gateway-service.ts +1 -1
  103. package/src/plugins/ibkr/index.test.tsx +0 -2
  104. package/src/plugins/ibkr/trade-tab.test.tsx +3 -4
  105. package/src/plugins/pane-manager.ts +0 -13
  106. package/src/plugins/plugin-runtime.test.tsx +6 -6
  107. package/src/plugins/plugin-runtime.tsx +13 -8
  108. package/src/plugins/prediction-markets/cli.ts +0 -1
  109. package/src/plugins/prediction-markets/controller-data.ts +0 -2
  110. package/src/plugins/prediction-markets/pane.test.tsx +3 -11
  111. package/src/plugins/prediction-markets/test-helpers.tsx +1 -1
  112. package/src/plugins/registry.ts +10 -39
  113. package/src/react/input.ts +1 -0
  114. package/src/renderers/electrobun/view/data-table.tsx +28 -6
  115. package/src/renderers/electrobun/view/desktop-controls.tsx +34 -20
  116. package/src/renderers/electrobun/view/dialog-host.tsx +11 -1
  117. package/src/renderers/electrobun/view/input-host.tsx +74 -10
  118. package/src/renderers/electrobun/view/key-event.ts +1 -1
  119. package/src/renderers/electrobun/view/styles.css +331 -0
  120. package/src/renderers/opentui/chart-surface.tsx +179 -0
  121. package/src/renderers/opentui/start.tsx +48 -30
  122. package/src/renderers/opentui/test-utils.tsx +17 -0
  123. package/src/renderers/opentui/ui-host.tsx +5 -2
  124. package/src/sources/provider-router.ts +0 -4
  125. package/src/state/app-context.test.ts +45 -0
  126. package/src/state/use-inline-tickers.test.tsx +73 -0
  127. package/src/state/use-inline-tickers.ts +7 -1
  128. package/src/test-support/plugin-runtime.ts +3 -2
  129. package/src/theme/colors.ts +12 -3
  130. package/src/theme/themes.test.ts +23 -1
  131. package/src/theme/themes.ts +115 -0
  132. package/src/types/broker.ts +1 -1
  133. package/src/types/data-provider.ts +0 -1
  134. package/src/types/plugin.ts +0 -2
  135. package/src/ui/host.tsx +1 -0
  136. package/src/utils/back-navigation.ts +24 -1
  137. package/src/version.ts +1 -1
  138. package/src/plugins/builtin/news-wire/feed-pane.tsx +0 -21
  139. package/src/plugins/builtin/news-wire/top-pane.tsx +0 -21
@@ -54,12 +54,10 @@ import {
54
54
  normalizeChartResolutionSupport,
55
55
  sortChartResolutions,
56
56
  type ChartResolutionSupport,
57
- type ManualChartResolution,
58
57
  } from "./chart-resolution";
59
58
  import { RIGHT_EDGE_ANCHOR_RATIO, resolveAnchoredChartZoom } from "./chart-viewport";
60
59
  import {
61
60
  buildComparisonChartScene,
62
- formatComparisonAxisValue,
63
61
  formatComparisonCursorAxisValue,
64
62
  renderComparisonChart,
65
63
  } from "./comparison-chart-renderer";
@@ -98,6 +96,7 @@ import {
98
96
  type NativeSurfaceRenderableNode,
99
97
  } from "./native/surface-visibility";
100
98
  import { formatAxisCell, formatDateShort, resolveChartAxisWidth, type StyledContent } from "./chart-renderer";
99
+ import { TimeAxisLabel } from "./time-axis-label";
101
100
 
102
101
  const MODE_CHIPS: Record<ComparisonChartRenderMode, string> = {
103
102
  area: "A",
@@ -112,6 +111,7 @@ interface ComparisonStockChartProps {
112
111
  symbols: string[];
113
112
  axisMode: ChartAxisMode;
114
113
  onOpenSymbol: (symbol: string) => void;
114
+ onEditTickers?: () => void;
115
115
  }
116
116
 
117
117
  interface ComparisonChartSymbolSource {
@@ -514,6 +514,7 @@ function ComparisonStockChartView({
514
514
  preferredRenderer,
515
515
  symbolSources,
516
516
  onOpenSymbol,
517
+ onEditTickers,
517
518
  }: ComparisonStockChartViewProps) {
518
519
  const renderer = useNativeRenderer();
519
520
  const { canvasCharts, cellWidthPx = 8, cellHeightPx = 18, pixelRatio = 1 } = useUiCapabilities();
@@ -548,6 +549,7 @@ function ComparisonStockChartView({
548
549
  const lastCanvasBaseBitmapRef = useRef<{ key: string; bitmap: NativeChartBitmap } | null>(null);
549
550
  const displayCursorRef = useRef<DisplayCursorState>(EMPTY_DISPLAY_CURSOR);
550
551
  const targetCursorRef = useRef<DisplayCursorState>(EMPTY_DISPLAY_CURSOR);
552
+ const mouseCrosshairDisabledRef = useRef(false);
551
553
  const cursorMotionKindRef = useRef<ChartCursorMotionKind>("discrete");
552
554
  const animationFrameRef = useRef<number | null>(null);
553
555
  const pendingCanonicalResetRef = useRef(1);
@@ -908,11 +910,10 @@ function ComparisonStockChartView({
908
910
  }
909
911
  return {
910
912
  ...current,
911
- panOffset: clamp(pendingExpansion.targetPanOffset, 0, getMaxComparisonPanOffset(series, current.presetRange, current.zoomLevel, chartWidth)),
913
+ panOffset: clamp(pendingExpansion.targetPanOffset, 0, getMaxComparisonPanOffset(series, current.zoomLevel)),
912
914
  };
913
915
  });
914
916
  }, [
915
- chartWidth,
916
917
  series,
917
918
  seriesDates,
918
919
  viewState.activePreset,
@@ -982,8 +983,8 @@ function ComparisonStockChartView({
982
983
  }), [viewState.panOffset, viewState.renderMode, viewState.zoomLevel]);
983
984
 
984
985
  const visibleWindow = useMemo(() => (
985
- getVisibleComparisonWindow(series, projectionViewState, chartWidth)
986
- ), [chartWidth, projectionViewState, series]);
986
+ getVisibleComparisonWindow(series, projectionViewState)
987
+ ), [projectionViewState, series]);
987
988
 
988
989
  const projection = useMemo(() => (
989
990
  projectComparisonChartData(series, chartWidth, projectionViewState, axisMode)
@@ -1037,6 +1038,14 @@ function ComparisonStockChartView({
1037
1038
  selectedSymbol: viewState.selectedSymbol,
1038
1039
  colors: chartColors,
1039
1040
  }), [chartColors, chartHeight, chartWidth, projection, viewState.selectedSymbol]);
1041
+ const displayScene = useMemo(() => buildComparisonChartScene(projection, {
1042
+ width: chartWidth,
1043
+ height: chartHeight,
1044
+ cursorX: displayCursorX,
1045
+ cursorY: displayCursorY,
1046
+ selectedSymbol: viewState.selectedSymbol,
1047
+ colors: chartColors,
1048
+ }), [chartColors, chartHeight, chartWidth, displayCursorX, displayCursorY, projection, viewState.selectedSymbol]);
1040
1049
 
1041
1050
  const staticResult = useMemo(() => renderComparisonChart(projection, {
1042
1051
  width: chartWidth,
@@ -1088,6 +1097,12 @@ function ComparisonStockChartView({
1088
1097
  };
1089
1098
 
1090
1099
  return [
1100
+ ...(onEditTickers ? [{
1101
+ id: "tickers",
1102
+ key: "t",
1103
+ label: "ickers",
1104
+ onPress: onEditTickers,
1105
+ }] : []),
1091
1106
  {
1092
1107
  id: "mode",
1093
1108
  key: "m",
@@ -1111,26 +1126,13 @@ function ComparisonStockChartView({
1111
1126
  { id: "reset", key: "0", label: "reset", onPress: resetView },
1112
1127
  ...(width >= 72 ? [{ id: "range", key: "1-7", label: "range", onPress: cycleRange }] : []),
1113
1128
  ];
1114
- }, [effectiveResolution, resolutionChips, series, supportMap, viewState.presetRange, visibleDateWindow, width]);
1129
+ }, [effectiveResolution, onEditTickers, resolutionChips, series, supportMap, viewState.presetRange, visibleDateWindow, width]);
1115
1130
 
1116
1131
  usePaneFooter("comparison-chart", () => ({
1117
1132
  order: 10,
1118
1133
  hints: footerHints,
1119
1134
  }), [footerHints]);
1120
1135
 
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
1136
  const nativeCrosshair = useMemo<NativeCrosshairOverlay | null>(() => {
1135
1137
  if (displayCursor.cellX === null || displayCursor.cellY === null) return null;
1136
1138
  return {
@@ -1385,7 +1387,7 @@ function ComparisonStockChartView({
1385
1387
  return;
1386
1388
  case "a": {
1387
1389
  const panStep = getKeyboardPanCellCount(chartWidth);
1388
- if (viewState.panOffset >= getMaxComparisonPanOffset(series, viewState.presetRange, viewState.zoomLevel, chartWidth) && expandBufferRange({
1390
+ if (viewState.panOffset >= getMaxComparisonPanOffset(series, viewState.zoomLevel) && expandBufferRange({
1389
1391
  kind: "pan-left",
1390
1392
  targetPanOffset: viewState.panOffset + panStep,
1391
1393
  })) {
@@ -1393,7 +1395,7 @@ function ComparisonStockChartView({
1393
1395
  }
1394
1396
  setViewState((current) => ({
1395
1397
  ...clearActivePreset(current),
1396
- panOffset: clamp(current.panOffset + panStep, 0, getMaxComparisonPanOffset(series, current.presetRange, current.zoomLevel, chartWidth)),
1398
+ panOffset: clamp(current.panOffset + panStep, 0, getMaxComparisonPanOffset(series, current.zoomLevel)),
1397
1399
  }));
1398
1400
  return;
1399
1401
  }
@@ -1401,7 +1403,7 @@ function ComparisonStockChartView({
1401
1403
  const panStep = getKeyboardPanCellCount(chartWidth);
1402
1404
  setViewState((current) => ({
1403
1405
  ...clearActivePreset(current),
1404
- panOffset: clamp(current.panOffset - panStep, 0, getMaxComparisonPanOffset(series, current.presetRange, current.zoomLevel, chartWidth)),
1406
+ panOffset: clamp(current.panOffset - panStep, 0, getMaxComparisonPanOffset(series, current.zoomLevel)),
1405
1407
  }));
1406
1408
  return;
1407
1409
  }
@@ -1417,7 +1419,11 @@ function ComparisonStockChartView({
1417
1419
  setResolution(nextResolution);
1418
1420
  return;
1419
1421
  }
1422
+ case "t":
1423
+ onEditTickers?.();
1424
+ return;
1420
1425
  case "h":
1426
+ mouseCrosshairDisabledRef.current = false;
1421
1427
  cursorMotionKindRef.current = "discrete";
1422
1428
  setViewState((current) => ({
1423
1429
  ...current,
@@ -1425,6 +1431,7 @@ function ComparisonStockChartView({
1425
1431
  }));
1426
1432
  return;
1427
1433
  case "l":
1434
+ mouseCrosshairDisabledRef.current = false;
1428
1435
  cursorMotionKindRef.current = "discrete";
1429
1436
  setViewState((current) => ({
1430
1437
  ...current,
@@ -1432,6 +1439,7 @@ function ComparisonStockChartView({
1432
1439
  }));
1433
1440
  return;
1434
1441
  case "escape":
1442
+ mouseCrosshairDisabledRef.current = true;
1435
1443
  updateDisplayCursorTarget(EMPTY_DISPLAY_CURSOR, "discrete");
1436
1444
  setViewState((current) => ({ ...current, cursorX: null, cursorY: null }));
1437
1445
  return;
@@ -1488,6 +1496,7 @@ function ComparisonStockChartView({
1488
1496
  });
1489
1497
 
1490
1498
  const handlePlotMove = (event: ChartMouseEvent) => {
1499
+ if (mouseCrosshairDisabledRef.current) return;
1491
1500
  const localPointer = getLocalPlotPointer(event, plotRef.current, renderer);
1492
1501
  if (!localPointer) return;
1493
1502
  const selectionCursor = resolveSelectionCursor(localPointer, projection.dates.length, chartWidth);
@@ -1506,6 +1515,7 @@ function ComparisonStockChartView({
1506
1515
  };
1507
1516
 
1508
1517
  const handlePlotDown = (event: ChartMouseEvent) => {
1518
+ mouseCrosshairDisabledRef.current = false;
1509
1519
  const localPointer = getLocalPlotPointer(event, plotRef.current, renderer);
1510
1520
  if (!localPointer) return;
1511
1521
  const selectionCursor = resolveSelectionCursor(localPointer, projection.dates.length, chartWidth);
@@ -1528,6 +1538,7 @@ function ComparisonStockChartView({
1528
1538
  };
1529
1539
 
1530
1540
  const handlePlotDrag = (event: ChartMouseEvent) => {
1541
+ if (mouseCrosshairDisabledRef.current && !dragRef.current) return;
1531
1542
  const localPointer = getLocalPlotPointer(event, plotRef.current, renderer);
1532
1543
  if (localPointer) {
1533
1544
  const selectionCursor = resolveSelectionCursor(localPointer, projection.dates.length, chartWidth);
@@ -1592,7 +1603,7 @@ function ComparisonStockChartView({
1592
1603
  if (scrollPanCells === 0) return;
1593
1604
 
1594
1605
  const targetPanOffset = viewState.panOffset + scrollPanCells;
1595
- if (scrollPanCells > 0 && viewState.panOffset >= getMaxComparisonPanOffset(series, viewState.presetRange, viewState.zoomLevel, chartWidth) && expandBufferRange({
1606
+ if (scrollPanCells > 0 && viewState.panOffset >= getMaxComparisonPanOffset(series, viewState.zoomLevel) && expandBufferRange({
1596
1607
  kind: "pan-left",
1597
1608
  targetPanOffset,
1598
1609
  })) {
@@ -1603,7 +1614,7 @@ function ComparisonStockChartView({
1603
1614
  panOffset: clamp(
1604
1615
  current.panOffset + scrollPanCells,
1605
1616
  0,
1606
- getMaxComparisonPanOffset(series, current.presetRange, current.zoomLevel, chartWidth),
1617
+ getMaxComparisonPanOffset(series, current.zoomLevel),
1607
1618
  ),
1608
1619
  }));
1609
1620
  };
@@ -1613,7 +1624,8 @@ function ComparisonStockChartView({
1613
1624
  }
1614
1625
 
1615
1626
  const hasChartData = series.some((entry) => entry.points.length > 0);
1616
- const cursorScene = effectiveRenderer === "kitty" ? staticScene : liveScene;
1627
+ const hasDisplayCursor = displayCursorX !== null && displayCursorY !== null;
1628
+ const cursorScene = hasDisplayCursor ? displayScene : staticScene;
1617
1629
  const selectedSeries = hasChartData ? (cursorScene?.selectedSeries ?? staticResult.selectedSeries) : null;
1618
1630
  const selectedPoint = hasChartData ? (cursorScene?.selectedPoint ?? staticResult.selectedPoint) : null;
1619
1631
  const selectedRawValue = selectedPoint?.rawValue ?? selectedSeries?.latestRawValue ?? null;
@@ -1641,6 +1653,8 @@ function ComparisonStockChartView({
1641
1653
  start: visibleWindow.dates[0] ?? null,
1642
1654
  end: visibleWindow.dates[visibleWindow.dates.length - 1] ?? null,
1643
1655
  });
1656
+ const cursorTimeAxisColumn = hasDisplayCursor ? displayScene?.cursorColumn ?? null : null;
1657
+ const cursorTimeAxisDate = hasDisplayCursor ? displayScene?.activeDate ?? null : null;
1644
1658
 
1645
1659
  const canvasBitmapSize = useMemo(() => {
1646
1660
  if (!canvasCharts) return null;
@@ -1929,7 +1943,14 @@ function ComparisonStockChartView({
1929
1943
  </Box>
1930
1944
 
1931
1945
  <Box height={timeAxisRows}>
1932
- <Text fg={colors.textDim}>{timeAxisLabel}</Text>
1946
+ <TimeAxisLabel
1947
+ timeLabels={timeAxisLabel}
1948
+ width={chartWidth}
1949
+ cursorColumn={cursorTimeAxisColumn}
1950
+ cursorDate={cursorTimeAxisDate}
1951
+ dates={projection.dates}
1952
+ cursorColor={chartColors.crosshairColor}
1953
+ />
1933
1954
  </Box>
1934
1955
 
1935
1956
  {legendRows > 0 && (
@@ -1,5 +1,5 @@
1
1
  import { useMemo } from "react";
2
- import { Box, ChartSurface, Text, useUiCapabilities } from "../../ui";
2
+ import { Box, ChartSurface, Text, useNativeRenderer, useUiCapabilities } from "../../ui";
3
3
  import type { ProjectedChartPoint } from "./chart-data";
4
4
  import {
5
5
  buildChartScene,
@@ -8,7 +8,7 @@ import {
8
8
  renderChart,
9
9
  type RenderChartOptions,
10
10
  } from "./chart-renderer";
11
- import { renderNativeChartBase, type NativeChartBitmap } from "./native/chart-rasterizer";
11
+ import { computeBitmapSize, renderNativeChartBase, type NativeChartBitmap } from "./native/chart-rasterizer";
12
12
 
13
13
  export interface StaticChartSurfaceProps extends Omit<
14
14
  RenderChartOptions,
@@ -47,10 +47,12 @@ export function StaticChartSurface({
47
47
  }: StaticChartSurfaceProps) {
48
48
  const {
49
49
  canvasCharts,
50
+ nativeCharts,
50
51
  cellWidthPx = 8,
51
52
  cellHeightPx = 18,
52
53
  pixelRatio = 1,
53
54
  } = useUiCapabilities();
55
+ const nativeRenderer = useNativeRenderer();
54
56
  const timeAxisRows = showTimeAxis ? 1 : 0;
55
57
  const labelRows = yAxisLabel ? 1 : 0;
56
58
  const totalWidth = Math.max(1, Math.floor(width));
@@ -101,14 +103,36 @@ export function StaticChartSurface({
101
103
  : 0;
102
104
  const axisGap = axisWidth > 0 ? 1 : 0;
103
105
  const plotWidth = Math.max(1, totalWidth - axisWidth - axisGap);
106
+ const rendererResolution = nativeRenderer.resolution;
107
+ const rendererTerminalWidth = nativeRenderer.terminalWidth;
108
+ const rendererTerminalHeight = nativeRenderer.terminalHeight;
104
109
  const bitmapSize = useMemo(() => {
105
- if (!canvasCharts) return null;
110
+ if (nativeCharts && rendererResolution && rendererTerminalWidth > 0 && rendererTerminalHeight > 0) {
111
+ return computeBitmapSize(
112
+ { x: 0, y: 0, width: plotWidth, height: plotHeight },
113
+ rendererResolution,
114
+ rendererTerminalWidth,
115
+ rendererTerminalHeight,
116
+ );
117
+ }
118
+ if (!canvasCharts && !nativeCharts) return null;
106
119
  const scale = Math.max(1, pixelRatio);
107
120
  return {
108
121
  pixelWidth: Math.max(1, Math.round(plotWidth * cellWidthPx * scale)),
109
122
  pixelHeight: Math.max(1, Math.round(plotHeight * cellHeightPx * scale)),
110
123
  };
111
- }, [canvasCharts, cellHeightPx, cellWidthPx, pixelRatio, plotHeight, plotWidth]);
124
+ }, [
125
+ canvasCharts,
126
+ cellHeightPx,
127
+ cellWidthPx,
128
+ nativeCharts,
129
+ pixelRatio,
130
+ plotHeight,
131
+ plotWidth,
132
+ rendererResolution,
133
+ rendererTerminalHeight,
134
+ rendererTerminalWidth,
135
+ ]);
112
136
  const renderOptions = useMemo<RenderChartOptions>(() => ({
113
137
  width: plotWidth,
114
138
  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
 
@@ -27,7 +27,7 @@ import {
27
27
  getKeyboardPanCellCount,
28
28
  resolveDragPanOffset,
29
29
  } from "./chart-scroll";
30
- import { getVisibleWindow, projectChartData, resolveBarSize, resolveStableOhlcProjectionOptions, type ProjectedChartPoint } from "./chart-data";
30
+ import { projectChartData, resolveBarSize, resolveStableOhlcProjectionOptions, type ProjectedChartPoint } from "./chart-data";
31
31
  import {
32
32
  buildPresetDateWindow,
33
33
  buildVisibleDateWindow,
@@ -127,6 +127,7 @@ import {
127
127
  } from "./native/surface-visibility";
128
128
  import type { PricePoint, TickerFinancials } from "../../types/financials";
129
129
  import type { TickerRecord } from "../../types/ticker";
130
+ import { TimeAxisLabel } from "./time-axis-label";
130
131
 
131
132
  const MODE_CHIPS: Record<ChartRenderMode, string> = {
132
133
  area: "A",
@@ -173,7 +174,6 @@ interface StockChartProps {
173
174
  }
174
175
 
175
176
  interface ResolvedStockChartProps extends StockChartProps {
176
- symbol: string | null;
177
177
  ticker: TickerRecord | null;
178
178
  financials: TickerFinancials | null;
179
179
  }
@@ -726,14 +726,6 @@ function isSeriesAcceptedForRequest(
726
726
  return windowPoints.length >= minimumPointCount;
727
727
  }
728
728
 
729
- function canRenderAutoViewForWindow(
730
- render: AutoRenderedView | null,
731
- requestedWindow: DateWindowRange | null | undefined,
732
- ): render is AutoRenderedView {
733
- if (!render || !requestedWindow?.start || !requestedWindow.end) return false;
734
- return getWindowPoints(render.data, requestedWindow).length > 0;
735
- }
736
-
737
729
  export function resolveAutoDisplayState(options: {
738
730
  shouldUseRenderedAutoView: boolean;
739
731
  renderedAutoView: AutoRenderedView | null;
@@ -772,13 +764,6 @@ export function resolveAutoDisplayState(options: {
772
764
  };
773
765
  }
774
766
 
775
- export function resolveVisibleChartDateWindow(
776
- points: readonly Pick<PricePoint, "date">[],
777
- preferredWindow: DateWindowRange | null | undefined,
778
- ): DateWindowRange | null {
779
- return getDateWindowBounds(getPointDates(points)) ?? preferredWindow ?? null;
780
- }
781
-
782
767
  function buildDateWindowFromIndices(dates: readonly Date[], startIdx: number, endIdx: number): DateWindowRange | null {
783
768
  if (dates.length === 0 || endIdx <= startIdx) return null;
784
769
  return {
@@ -1430,8 +1415,8 @@ function buildIndicatorProjectionKey(options: {
1430
1415
  }
1431
1416
 
1432
1417
  export function StockChart(props: StockChartProps) {
1433
- const { symbol, ticker, financials } = usePaneTicker();
1434
- return <ResolvedStockChart {...props} symbol={symbol} ticker={ticker} financials={financials} />;
1418
+ const { ticker, financials } = usePaneTicker();
1419
+ return <ResolvedStockChart {...props} ticker={ticker} financials={financials} />;
1435
1420
  }
1436
1421
 
1437
1422
  export const ResolvedStockChart = memo(function ResolvedStockChart({
@@ -1448,7 +1433,6 @@ export const ResolvedStockChart = memo(function ResolvedStockChart({
1448
1433
  showVolume: showVolumeOverride,
1449
1434
  footerControls,
1450
1435
  footerHints,
1451
- symbol,
1452
1436
  ticker,
1453
1437
  financials,
1454
1438
  }: ResolvedStockChartProps) {
@@ -1514,6 +1498,7 @@ export const ResolvedStockChart = memo(function ResolvedStockChart({
1514
1498
  const lastCanvasBaseBitmapRef = useRef<{ key: string; bitmap: NativeChartBitmap } | null>(null);
1515
1499
  const displayCursorRef = useRef<DisplayCursorState>(EMPTY_DISPLAY_CURSOR);
1516
1500
  const targetCursorRef = useRef<DisplayCursorState>(EMPTY_DISPLAY_CURSOR);
1501
+ const mouseCrosshairDisabledRef = useRef(false);
1517
1502
  const cursorMotionKindRef = useRef<ChartCursorMotionKind>("discrete");
1518
1503
  const animationFrameRef = useRef<number | null>(null);
1519
1504
  const pendingCanonicalResetRef = useRef(1);
@@ -2906,6 +2891,11 @@ export const ResolvedStockChart = memo(function ResolvedStockChart({
2906
2891
  if (!interactive) return;
2907
2892
 
2908
2893
  switch (key) {
2894
+ case "escape":
2895
+ mouseCrosshairDisabledRef.current = true;
2896
+ updateDisplayCursorTarget(EMPTY_DISPLAY_CURSOR, "discrete");
2897
+ commitSelectionCursor({ cursorX: null, cursorY: null });
2898
+ return;
2909
2899
  case "left":
2910
2900
  if (event.shift) {
2911
2901
  if (effectiveResolution === "auto") {
@@ -2923,6 +2913,7 @@ export const ResolvedStockChart = memo(function ResolvedStockChart({
2923
2913
  return { ...cleared, panOffset: current.panOffset + panStep };
2924
2914
  });
2925
2915
  } else {
2916
+ mouseCrosshairDisabledRef.current = false;
2926
2917
  cursorMotionKindRef.current = "discrete";
2927
2918
  const pointCount = projection.points.length;
2928
2919
  const currentIndex = pointCount <= 0
@@ -2974,6 +2965,7 @@ export const ResolvedStockChart = memo(function ResolvedStockChart({
2974
2965
  return cleared.panOffset === nextPanOffset ? cleared : { ...cleared, panOffset: nextPanOffset };
2975
2966
  });
2976
2967
  } else {
2968
+ mouseCrosshairDisabledRef.current = false;
2977
2969
  cursorMotionKindRef.current = "discrete";
2978
2970
  const pointCount = projection.points.length;
2979
2971
  const currentIndex = pointCount <= 0
@@ -3058,6 +3050,19 @@ export const ResolvedStockChart = memo(function ResolvedStockChart({
3058
3050
  colors: chartColors,
3059
3051
  timeAxisDates,
3060
3052
  }), [axisMode, chartColors, chartHeight, chartWidth, compact, projection.effectiveMode, projection.points, selectionCursorX, selectionCursorY, showVolume, timeAxisDates, volumeHeight]);
3053
+ const displayScene = useMemo(() => buildChartScene(projection.points, {
3054
+ width: chartWidth,
3055
+ height: chartHeight,
3056
+ showVolume: showVolume && !compact,
3057
+ volumeHeight,
3058
+ cursorX: displayCursorX,
3059
+ cursorY: displayCursorY,
3060
+ mode: projection.effectiveMode,
3061
+ axisMode,
3062
+ colors: chartColors,
3063
+ timeAxisDates,
3064
+ indicators,
3065
+ }), [axisMode, chartColors, chartHeight, chartWidth, compact, displayCursorX, displayCursorY, indicators, projection.effectiveMode, projection.points, showVolume, timeAxisDates, volumeHeight]);
3061
3066
  const snappedSelectionCursorX = selectionScene
3062
3067
  ? getPointTerminalColumn(selectionScene.activeIdx, projection.points.length, chartWidth, projection.effectiveMode)
3063
3068
  : null;
@@ -3419,6 +3424,8 @@ export const ResolvedStockChart = memo(function ResolvedStockChart({
3419
3424
  : null;
3420
3425
  const isUpdating = !compact && (renderBodyState.updating || boundsBodyState.updating);
3421
3426
  const timeAxisLabel = selectionScene?.timeLabels ?? staticResult.timeLabels;
3427
+ const cursorTimeAxisColumn = hasDisplayCursor ? displayScene?.cursorColumn ?? null : null;
3428
+ const cursorTimeAxisDate = hasDisplayCursor ? displayScene?.dateAtCursor ?? null : null;
3422
3429
  const timeAxisBox = (
3423
3430
  <Box
3424
3431
  height={1}
@@ -3426,7 +3433,14 @@ export const ResolvedStockChart = memo(function ResolvedStockChart({
3426
3433
  overflow="hidden"
3427
3434
  data-gloom-role="chart-time-axis"
3428
3435
  >
3429
- <Text fg={colors.textDim} style={{ whiteSpace: "pre" }}>{timeAxisLabel}</Text>
3436
+ <TimeAxisLabel
3437
+ timeLabels={timeAxisLabel}
3438
+ width={chartWidth}
3439
+ cursorColumn={cursorTimeAxisColumn}
3440
+ cursorDate={cursorTimeAxisDate}
3441
+ dates={timeAxisDates}
3442
+ cursorColor={chartColors.crosshairColor}
3443
+ />
3430
3444
  </Box>
3431
3445
  );
3432
3446
  const chartFooterHints = useMemo<PaneHint[]>(() => {
@@ -3573,6 +3587,7 @@ export const ResolvedStockChart = memo(function ResolvedStockChart({
3573
3587
 
3574
3588
  const handlePlotMove = (event: ChartMouseEvent) => {
3575
3589
  if (!interactive || compact) return;
3590
+ if (mouseCrosshairDisabledRef.current) return;
3576
3591
  const localPointer = getLocalPlotPointer(event, plotRef.current, renderer);
3577
3592
  if (!localPointer) return;
3578
3593
  const selectionCursor = resolveSelectionCursor(localPointer, projection.points.length, chartWidth, projection.effectiveMode);
@@ -3594,6 +3609,7 @@ export const ResolvedStockChart = memo(function ResolvedStockChart({
3594
3609
  if (compact) return;
3595
3610
  if (!interactive) onActivate?.();
3596
3611
  focusPaneForMouseInteraction(event);
3612
+ mouseCrosshairDisabledRef.current = false;
3597
3613
  const localPointer = getLocalPlotPointer(event, plotRef.current, renderer);
3598
3614
  if (!localPointer) return;
3599
3615
  const selectionCursor = resolveSelectionCursor(localPointer, projection.points.length, chartWidth, projection.effectiveMode);
@@ -3618,6 +3634,7 @@ export const ResolvedStockChart = memo(function ResolvedStockChart({
3618
3634
 
3619
3635
  const handlePlotDrag = (event: ChartMouseEvent) => {
3620
3636
  if (compact || (!interactive && !dragRef.current)) return;
3637
+ if (mouseCrosshairDisabledRef.current && !dragRef.current) return;
3621
3638
  const localPointer = getLocalPlotPointer(event, plotRef.current, renderer);
3622
3639
  if (localPointer) {
3623
3640
  const selectionCursor = resolveSelectionCursor(localPointer, projection.points.length, chartWidth, projection.effectiveMode);
@@ -0,0 +1,43 @@
1
+ import { useMemo } from "react";
2
+ import { Span, Text } from "../../ui";
3
+ import { colors } from "../../theme/colors";
4
+ import { buildCursorTimeAxisSegments } from "./chart-renderer";
5
+
6
+ interface TimeAxisLabelProps {
7
+ timeLabels: string;
8
+ width: number;
9
+ cursorColumn: number | null;
10
+ cursorDate: Date | string | number | null;
11
+ dates: Array<Date | string | number>;
12
+ cursorColor: string;
13
+ }
14
+
15
+ export function TimeAxisLabel({
16
+ timeLabels,
17
+ width,
18
+ cursorColumn,
19
+ cursorDate,
20
+ dates,
21
+ cursorColor,
22
+ }: TimeAxisLabelProps) {
23
+ const segments = useMemo(() => buildCursorTimeAxisSegments({
24
+ timeLabels,
25
+ width,
26
+ cursorColumn,
27
+ cursorDate,
28
+ dates,
29
+ }), [cursorColumn, cursorDate, dates, timeLabels, width]);
30
+
31
+ return (
32
+ <Text style={{ whiteSpace: "pre" }}>
33
+ {segments.map((segment, index) => (
34
+ <Span
35
+ key={index}
36
+ fg={segment.highlighted ? cursorColor : colors.textDim}
37
+ >
38
+ {segment.text}
39
+ </Span>
40
+ ))}
41
+ </Text>
42
+ );
43
+ }