gloomberb 0.9.3 → 0.9.5

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 (176) hide show
  1. package/README.md +1 -0
  2. package/package.json +3 -2
  3. package/patches/@opentui%2Fcore@0.3.2.patch +231 -29
  4. package/patches/react-reconciler@0.33.0.patch +10 -0
  5. package/src/app/runtime/desktop-menu.ts +5 -1
  6. package/src/app/runtime/ticker-refresh.ts +4 -1
  7. package/src/app.tsx +10 -1
  8. package/src/brokers/account-cache.ts +1 -1
  9. package/src/brokers/sync-broker-instance.test.ts +107 -0
  10. package/src/brokers/sync-broker-instance.ts +22 -10
  11. package/src/cli/desktop-pane-shot.ts +2 -1
  12. package/src/cli/entry.ts +8 -1
  13. package/src/cli/native-smoke.ts +5 -0
  14. package/src/cli/pane-functions/screenshot.ts +13 -1
  15. package/src/components/chart/bar-chart-renderer.test.ts +108 -3
  16. package/src/components/chart/bar-chart-renderer.ts +226 -27
  17. package/src/components/chart/core/pointer.ts +32 -6
  18. package/src/components/chart/native/bitmap-support.test.ts +27 -0
  19. package/src/components/chart/native/bitmap-support.ts +55 -0
  20. package/src/components/chart/native/kitty/index.test.ts +47 -0
  21. package/src/components/chart/native/surface/visibility.ts +8 -1
  22. package/src/components/chart/price-axis-labels.tsx +3 -1
  23. package/src/components/chart/static/bar-chart-surface.test.tsx +97 -0
  24. package/src/components/chart/static/bar-chart-surface.tsx +151 -28
  25. package/src/components/chart/static/chart/axis-overlays.tsx +243 -0
  26. package/src/components/chart/static/chart/bitmap.ts +16 -9
  27. package/src/components/chart/static/chart/surface.test.tsx +92 -0
  28. package/src/components/chart/static/chart/surface.tsx +167 -24
  29. package/src/components/chart/stock-chart/index.test.ts +25 -0
  30. package/src/components/command-bar/layout-items/current-layout.ts +5 -1
  31. package/src/components/command-bar/list/view.tsx +1 -1
  32. package/src/components/command-bar/theme-picker.tsx +1 -1
  33. package/src/components/data-table/view.tsx +0 -12
  34. package/src/components/layout/detached-pane-shell.tsx +6 -3
  35. package/src/components/layout/floating-pane.tsx +6 -5
  36. package/src/components/layout/pane/content.tsx +20 -8
  37. package/src/components/layout/pane/footer/registration.tsx +6 -2
  38. package/src/components/layout/pane/frame.tsx +4 -4
  39. package/src/components/layout/pane/header.tsx +45 -4
  40. package/src/components/layout/pane/index.tsx +6 -4
  41. package/src/components/layout/pane/sizing.test.ts +43 -0
  42. package/src/components/layout/pane/sizing.ts +13 -4
  43. package/src/components/layout/shell/action-menu-overlay.tsx +1 -1
  44. package/src/components/layout/shell/cursor-occlusion.ts +204 -0
  45. package/src/components/layout/shell/drag/runtime.ts +9 -2
  46. package/src/components/layout/shell/index.test.tsx +92 -2
  47. package/src/components/layout/shell/index.tsx +45 -3
  48. package/src/components/layout/shell/layout-state.test.ts +33 -0
  49. package/src/components/layout/shell/layout-state.ts +4 -9
  50. package/src/components/layout/shell/pane/layers.tsx +10 -4
  51. package/src/components/layout/shell/visible-layout.ts +17 -0
  52. package/src/components/layout/status-bar.test.tsx +6 -1
  53. package/src/components/layout/status-bar.tsx +10 -6
  54. package/src/components/onboarding/onboarding-steps.tsx +8 -28
  55. package/src/components/onboarding/onboarding-wizard.test.tsx +13 -12
  56. package/src/components/onboarding/onboarding-wizard.tsx +2 -4
  57. package/src/components/onboarding/portfolio-step/index.tsx +0 -12
  58. package/src/components/onboarding/portfolio-step/types.ts +1 -3
  59. package/src/components/onboarding/wizard-keyboard.ts +2 -5
  60. package/src/components/pane-settings-dialog/desktop.tsx +1 -1
  61. package/src/components/price-performance.tsx +1 -1
  62. package/src/components/price-sparkline/view.tsx +15 -14
  63. package/src/components/speedometer-gauge/terminal.tsx +18 -15
  64. package/src/components/table-view-shared.tsx +12 -14
  65. package/src/components/ticker/list-table-view.tsx +1 -7
  66. package/src/components/ticker/list-table.test.tsx +0 -4
  67. package/src/components/ui/data-table/opentui/index.tsx +29 -22
  68. package/src/components/ui/data-table/types.ts +10 -4
  69. package/src/components/ui/fields.tsx +10 -3
  70. package/src/components/ui/list-view.tsx +1 -1
  71. package/src/components/ui/page-stack-view.tsx +6 -2
  72. package/src/components/ui/table-layout.test.ts +25 -0
  73. package/src/components/ui/table-layout.ts +22 -6
  74. package/src/components/ui/tabs.tsx +27 -12
  75. package/src/components/ui/ui.test.tsx +87 -9
  76. package/src/core/state/app/layout-reducer.ts +2 -2
  77. package/src/plugins/builtin/account-management/form-components.tsx +2 -2
  78. package/src/plugins/builtin/alerts/pane.tsx +1 -1
  79. package/src/plugins/builtin/analytics/index.test.tsx +6 -2
  80. package/src/plugins/builtin/analytics/pane-model.ts +43 -8
  81. package/src/plugins/builtin/buildout/cells.tsx +1 -1
  82. package/src/plugins/builtin/buildout/pane/index.tsx +1 -1
  83. package/src/plugins/builtin/chat/message/desktop.tsx +1 -1
  84. package/src/plugins/builtin/chat/message/inline-tokens.tsx +1 -1
  85. package/src/plugins/builtin/chat/message/terminal.tsx +2 -2
  86. package/src/plugins/builtin/chat/sidebar.tsx +1 -1
  87. package/src/plugins/builtin/chat/status-widget.tsx +1 -1
  88. package/src/plugins/builtin/cloud/auth-actions.tsx +2 -2
  89. package/src/plugins/builtin/correlation/matrix/symbol-cell.tsx +1 -1
  90. package/src/plugins/builtin/debug/index.tsx +3 -4
  91. package/src/plugins/builtin/help/components.tsx +1 -1
  92. package/src/plugins/builtin/holders/treemap.tsx +1 -1
  93. package/src/plugins/builtin/kelly-sizer/constants.ts +3 -0
  94. package/src/plugins/builtin/kelly-sizer/fields.ts +214 -0
  95. package/src/plugins/builtin/kelly-sizer/index.test.tsx +197 -0
  96. package/src/plugins/builtin/kelly-sizer/index.tsx +47 -0
  97. package/src/plugins/builtin/kelly-sizer/model.test.ts +152 -0
  98. package/src/plugins/builtin/kelly-sizer/model.ts +580 -0
  99. package/src/plugins/builtin/kelly-sizer/pane.tsx +558 -0
  100. package/src/plugins/builtin/kelly-sizer/portfolio.ts +71 -0
  101. package/src/plugins/builtin/kelly-sizer/sections.tsx +142 -0
  102. package/src/plugins/builtin/kelly-sizer/state.ts +67 -0
  103. package/src/plugins/builtin/kelly-sizer/test-support.tsx +174 -0
  104. package/src/plugins/builtin/kelly-sizer/types.ts +156 -0
  105. package/src/plugins/builtin/kelly-sizer/view.test.tsx +77 -0
  106. package/src/plugins/builtin/kelly-sizer/view.tsx +375 -0
  107. package/src/plugins/builtin/layout-manager/index.tsx +5 -1
  108. package/src/plugins/builtin/options/table.ts +5 -5
  109. package/src/plugins/builtin/options/view.test.tsx +141 -4
  110. package/src/plugins/builtin/options/view.tsx +76 -15
  111. package/src/plugins/builtin/portfolio-list/account-metrics.test.ts +26 -1
  112. package/src/plugins/builtin/portfolio-list/account-metrics.ts +14 -0
  113. package/src/plugins/builtin/portfolio-list/index.test.ts +23 -0
  114. package/src/plugins/builtin/portfolio-list/pane/index.tsx +2 -0
  115. package/src/plugins/builtin/portfolio-list/summary/index.test.ts +59 -1
  116. package/src/plugins/builtin/portfolio-list/summary/index.tsx +36 -5
  117. package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/content.tsx +149 -48
  118. package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/model.ts +25 -2
  119. package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph.tsx +5 -4
  120. package/src/plugins/builtin/ticker-detail/data-panes.test.ts +16 -0
  121. package/src/plugins/builtin/ticker-detail/index.test.tsx +143 -0
  122. package/src/plugins/catalog-ui.ts +2 -0
  123. package/src/plugins/ibkr/broker-adapter.ts +19 -0
  124. package/src/plugins/ibkr/config.test.ts +31 -2
  125. package/src/plugins/ibkr/config.ts +9 -2
  126. package/src/plugins/ibkr/flex/index.test.ts +18 -9
  127. package/src/plugins/ibkr/flex/index.ts +30 -0
  128. package/src/plugins/ibkr/flex/statement-client.ts +68 -14
  129. package/src/plugins/ibkr/gateway/account-loaders.ts +1 -1
  130. package/src/plugins/ibkr/gateway/account-summary.ts +1 -0
  131. package/src/plugins/ibkr/gateway/service/index.test.ts +8 -0
  132. package/src/plugins/ibkr/trade/ticket-panel.tsx +1 -1
  133. package/src/plugins/pane-manager/gridlock.ts +13 -5
  134. package/src/plugins/pane-manager/layout-state.ts +25 -0
  135. package/src/plugins/pane-manager.test.ts +33 -0
  136. package/src/plugins/pane-manager.ts +6 -1
  137. package/src/plugins/prediction-markets/chart.tsx +10 -4
  138. package/src/plugins/prediction-markets/detail/book.tsx +141 -96
  139. package/src/plugins/prediction-markets/detail/overview.tsx +3 -0
  140. package/src/plugins/prediction-markets/detail/pane.tsx +71 -64
  141. package/src/plugins/prediction-markets/detail/shared.tsx +19 -0
  142. package/src/plugins/prediction-markets/detail/trades.tsx +64 -19
  143. package/src/plugins/prediction-markets/detail.test.tsx +50 -0
  144. package/src/plugins/prediction-markets/pane.test.tsx +43 -0
  145. package/src/plugins/prediction-markets/pane.tsx +50 -22
  146. package/src/plugins/prediction-markets/test-helpers.tsx +41 -23
  147. package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +13 -2
  148. package/src/renderers/electrobun/view/data-table/index.tsx +19 -12
  149. package/src/renderers/electrobun/view/data-table/row.tsx +32 -18
  150. package/src/renderers/electrobun/view/desktop/controls.tsx +11 -5
  151. package/src/renderers/electrobun/view/host/box.tsx +6 -1
  152. package/src/renderers/electrobun/view/host/input.tsx +115 -18
  153. package/src/renderers/electrobun/view/host/style.ts +1 -0
  154. package/src/renderers/electrobun/view/host/tabs.tsx +7 -2
  155. package/src/renderers/electrobun/view/styles.css +18 -0
  156. package/src/renderers/opentui/host.tsx +17 -1
  157. package/src/renderers/opentui/native-smoke.ts +1 -0
  158. package/src/renderers/opentui/ui-host.tsx +57 -25
  159. package/src/sources/provider-router/batches.ts +16 -2
  160. package/src/sources/provider-router/financial-routes.ts +5 -0
  161. package/src/sources/provider-router/financials.ts +11 -23
  162. package/src/sources/provider-router/index.test.ts +189 -0
  163. package/src/sources/provider-router/primary.ts +3 -2
  164. package/src/sources/sec-edgar.test.ts +83 -0
  165. package/src/sources/sec-edgar.ts +231 -0
  166. package/src/sources/yahoo-finance.test.ts +51 -0
  167. package/src/sources/yahoo-finance.ts +49 -1
  168. package/src/state/app/context/index.test.tsx +5 -8
  169. package/src/types/broker.ts +1 -0
  170. package/src/types/config.ts +10 -0
  171. package/src/types/trading.ts +2 -0
  172. package/src/ui/host.tsx +16 -0
  173. package/src/ui/index.tsx +2 -0
  174. package/src/utils/financial-statements.ts +23 -0
  175. package/src/version.ts +1 -1
  176. package/src/components/onboarding/portfolio-step/manual-name-panel.tsx +0 -62
@@ -0,0 +1,55 @@
1
+ import type { NativeRendererHost, PixelResolution } from "../../../ui";
2
+ import { computeBitmapSize } from "./chart-rasterizer";
3
+
4
+ interface NativeBitmapRendererCapabilities {
5
+ kitty_graphics?: boolean;
6
+ }
7
+
8
+ export interface NativeBitmapSize {
9
+ pixelWidth: number;
10
+ pixelHeight: number;
11
+ }
12
+
13
+ function hasKnownUnsupportedKittyGraphics(renderer: NativeRendererHost): boolean {
14
+ const capabilities = renderer.capabilities as NativeBitmapRendererCapabilities | null | undefined;
15
+ return capabilities?.kitty_graphics === false;
16
+ }
17
+
18
+ export function shouldRenderNativeBitmap(renderer: NativeRendererHost, nativeCharts: boolean): boolean {
19
+ return nativeCharts && !hasKnownUnsupportedKittyGraphics(renderer);
20
+ }
21
+
22
+ export function resolveNativeBitmapSize({
23
+ width,
24
+ height,
25
+ resolution,
26
+ terminalWidth,
27
+ terminalHeight,
28
+ cellWidthPx,
29
+ cellHeightPx,
30
+ pixelRatio,
31
+ }: {
32
+ width: number;
33
+ height: number;
34
+ resolution: PixelResolution | null;
35
+ terminalWidth: number;
36
+ terminalHeight: number;
37
+ cellWidthPx: number;
38
+ cellHeightPx: number;
39
+ pixelRatio: number;
40
+ }): NativeBitmapSize {
41
+ if (resolution && terminalWidth > 0 && terminalHeight > 0) {
42
+ return computeBitmapSize(
43
+ { x: 0, y: 0, width, height },
44
+ resolution,
45
+ terminalWidth,
46
+ terminalHeight,
47
+ );
48
+ }
49
+
50
+ const scale = Math.max(1, pixelRatio);
51
+ return {
52
+ pixelWidth: Math.max(1, Math.round(width * cellWidthPx * scale)),
53
+ pixelHeight: Math.max(1, Math.round(height * cellHeightPx * scale)),
54
+ };
55
+ }
@@ -143,6 +143,53 @@ describe("resolveNativeSurfaceVisibleRect", () => {
143
143
 
144
144
  expect(resolveNativeSurfaceVisibleRect(child, 100, 30)).toBeNull();
145
145
  });
146
+
147
+ test("stops at repeated ancestors in cyclic native renderable trees", () => {
148
+ const root: NativeSurfaceRenderableNode = {
149
+ x: 0,
150
+ y: 0,
151
+ width: 100,
152
+ height: 30,
153
+ parent: null,
154
+ };
155
+ const child: NativeSurfaceRenderableNode = {
156
+ x: 2,
157
+ y: 3,
158
+ width: 20,
159
+ height: 8,
160
+ parent: root,
161
+ };
162
+ root.parent = child;
163
+
164
+ expect(resolveNativeSurfaceVisibleRect(child, 100, 30)).toEqual({
165
+ x: 2,
166
+ y: 3,
167
+ width: 20,
168
+ height: 8,
169
+ });
170
+ });
171
+
172
+ test("fails closed when native renderable ancestors exceed a sane depth", () => {
173
+ let node: NativeSurfaceRenderableNode = {
174
+ x: 0,
175
+ y: 0,
176
+ width: 100,
177
+ height: 30,
178
+ parent: null,
179
+ };
180
+
181
+ for (let index = 0; index < 300; index += 1) {
182
+ node = {
183
+ x: 0,
184
+ y: 0,
185
+ width: 100,
186
+ height: 30,
187
+ parent: node,
188
+ };
189
+ }
190
+
191
+ expect(resolveNativeSurfaceVisibleRect(node, 100, 30)).toBeNull();
192
+ });
146
193
  });
147
194
 
148
195
  describe("renderNativeChartBase", () => {
@@ -9,6 +9,8 @@ export interface NativeSurfaceRenderableNode {
9
9
  parent: NativeSurfaceRenderableNode | null;
10
10
  }
11
11
 
12
+ const MAX_RENDERABLE_ANCESTOR_DEPTH = 256;
13
+
12
14
  export function getRenderableCellRect(
13
15
  renderable: Pick<NativeSurfaceRenderableNode, "x" | "y" | "width" | "height">,
14
16
  ): CellRect {
@@ -34,8 +36,13 @@ export function resolveNativeSurfaceVisibleRect(
34
36
  height: terminalHeight,
35
37
  };
36
38
  let current: NativeSurfaceRenderableNode | null = renderable;
39
+ const seen = new Set<NativeSurfaceRenderableNode>();
40
+ let depth = 0;
37
41
 
38
- while (current) {
42
+ while (current && !seen.has(current)) {
43
+ if (depth >= MAX_RENDERABLE_ANCESTOR_DEPTH) return null;
44
+ seen.add(current);
45
+ depth += 1;
39
46
  if (current.visible === false) return null;
40
47
 
41
48
  const nextVisible = intersectCellRects(visible, getRenderableCellRect(current));
@@ -12,6 +12,7 @@ interface PriceAxisLabelsProps {
12
12
  cursorPixelY?: number | null;
13
13
  cursorLabel: string | null;
14
14
  cursorColor: string;
15
+ axisColor?: string;
15
16
  }
16
17
 
17
18
  interface CursorPriceAxisOverlay {
@@ -61,6 +62,7 @@ export function PriceAxisLabels({
61
62
  cursorPixelY = null,
62
63
  cursorLabel,
63
64
  cursorColor,
65
+ axisColor = colors.textDim,
64
66
  }: PriceAxisLabelsProps) {
65
67
  const { cellHeightPx = 18, fractionalViewport = false } = useUiCapabilities();
66
68
  const overlay = useMemo(() => buildCursorPriceAxisOverlay({
@@ -100,7 +102,7 @@ export function PriceAxisLabels({
100
102
  const label = isCursorRow ? cursorLabel : (axisLabels.get(row) ?? null);
101
103
  return (
102
104
  <Box key={row} height={1}>
103
- {renderAxisLabel(label, isCursorRow ? cursorColor : colors.textDim)}
105
+ {renderAxisLabel(label, isCursorRow ? cursorColor : axisColor)}
104
106
  </Box>
105
107
  );
106
108
  })}
@@ -0,0 +1,97 @@
1
+ import { afterEach, describe, expect, test } from "bun:test";
2
+ import { act } from "react";
3
+ import { Text } from "../../../ui";
4
+ import { testRender } from "../../../renderers/opentui/test-utils";
5
+ import { StaticBarChartSurface } from "./bar-chart-surface";
6
+
7
+ let testSetup: Awaited<ReturnType<typeof testRender>> | undefined;
8
+
9
+ afterEach(async () => {
10
+ if (!testSetup) return;
11
+ await act(async () => {
12
+ testSetup!.renderer.destroy();
13
+ });
14
+ testSetup = undefined;
15
+ });
16
+
17
+ describe("StaticBarChartSurface", () => {
18
+ test("shows hover feedback without shifting chart rows", async () => {
19
+ testSetup = await testRender(
20
+ <StaticBarChartSurface
21
+ width={64}
22
+ height={12}
23
+ title="Operating Cash Flow (quarterly)"
24
+ formatValue={(value) => `${Math.round(value / 1000)}k`}
25
+ series={[{
26
+ id: "value",
27
+ label: "Value",
28
+ color: "#00ff66",
29
+ points: [
30
+ { category: "2025 Q1", value: -271000 },
31
+ { category: "2025 Q2", value: -138000 },
32
+ { category: "2025 Q3", value: 653000 },
33
+ ],
34
+ }]}
35
+ />,
36
+ { width: 66, height: 14 },
37
+ );
38
+
39
+ await act(async () => {
40
+ await testSetup!.renderOnce();
41
+ await testSetup!.renderOnce();
42
+ });
43
+
44
+ const initialFrame = testSetup.captureCharFrame();
45
+ const initialLines = initialFrame.split("\n");
46
+ const initialAxisRow = initialLines.findIndex((line) => line.includes("2025 Q1") && line.includes("2025 Q2"));
47
+
48
+ await act(async () => {
49
+ await testSetup!.mockMouse.moveTo(14, 4);
50
+ await testSetup!.renderOnce();
51
+ await testSetup!.renderOnce();
52
+ });
53
+
54
+ const hoverFrame = testSetup.captureCharFrame();
55
+ const hoverLines = hoverFrame.split("\n");
56
+ const hoverAxisRow = hoverLines.findIndex((line) => line.includes("2025 Q1") && line.includes("2025 Q2"));
57
+
58
+ expect(hoverFrame).toContain("2025 Q1: -271k");
59
+ expect(hoverFrame).toContain("▓");
60
+ expect(hoverLines).toHaveLength(initialLines.length);
61
+ expect(hoverAxisRow).toBe(initialAxisRow);
62
+ });
63
+
64
+ test("shows a hover readout when a custom header is rendered", async () => {
65
+ testSetup = await testRender(
66
+ <StaticBarChartSurface
67
+ width={64}
68
+ height={12}
69
+ header={<Text>Revenue Gross Profit</Text>}
70
+ formatValue={(value) => `${Math.round(value / 1000)}k`}
71
+ series={[{
72
+ id: "value",
73
+ label: "Value",
74
+ color: "#00ff66",
75
+ points: [
76
+ { category: "2025 Q1", value: -271000 },
77
+ { category: "2025 Q2", value: -138000 },
78
+ { category: "2025 Q3", value: 653000 },
79
+ ],
80
+ }]}
81
+ />,
82
+ { width: 66, height: 14 },
83
+ );
84
+
85
+ await act(async () => {
86
+ await testSetup!.renderOnce();
87
+ await testSetup!.renderOnce();
88
+ await testSetup!.mockMouse.moveTo(14, 4);
89
+ await testSetup!.renderOnce();
90
+ await testSetup!.renderOnce();
91
+ });
92
+
93
+ const frame = testSetup.captureCharFrame();
94
+ expect(frame).toContain("Revenue Gross Profit");
95
+ expect(frame).toContain("2025 Q1: -271k");
96
+ });
97
+ });
@@ -1,14 +1,19 @@
1
- import { useMemo } from "react";
2
- import { Box, ChartSurface, Span, Text } from "../../../ui";
1
+ import { useCallback, useMemo, useRef, useState, type ReactNode } from "react";
2
+ import { Box, ChartSurface, Span, Text, useNativeRenderer, type BoxRenderable } from "../../../ui";
3
3
  import { colors } from "../../../theme/colors";
4
+ import { consumeChartMouseEvent, getLocalPlotPointer, type ChartMouseEvent } from "../core/pointer";
4
5
  import type { NativeChartBitmap } from "../native/chart-rasterizer";
6
+ import { truncateWithEllipsis } from "../../../utils/text-wrap";
5
7
  import { useStaticChartBitmapSize } from "./chart/bitmap";
6
8
  import {
7
9
  buildBarChartScene,
8
10
  renderBarChart,
9
11
  renderBarChartAxis,
12
+ renderBarChartYAxis,
10
13
  renderNativeBarChart,
14
+ resolveBarChartHover,
11
15
  type BarChartColors,
16
+ type BarChartHover,
12
17
  type BarChartSeries,
13
18
  } from "../bar-chart-renderer";
14
19
 
@@ -17,42 +22,116 @@ export interface StaticBarChartSurfaceProps {
17
22
  width: number;
18
23
  height: number;
19
24
  colors?: BarChartColors;
25
+ title?: string;
26
+ header?: ReactNode;
27
+ formatValue?: (value: number) => string;
28
+ onHoverChange?: (hover: BarChartHover | null) => void;
29
+ onMouseDown?: (event: ChartMouseEvent) => void;
20
30
  }
21
31
 
22
32
  export function StaticBarChartSurface({
23
33
  series,
24
34
  width,
25
35
  height,
26
- colors: chartColors = {
27
- bgColor: colors.bg,
28
- gridColor: colors.border,
29
- axisColor: colors.textDim,
30
- },
36
+ title,
37
+ header,
38
+ formatValue,
39
+ onHoverChange,
40
+ onMouseDown,
41
+ colors: chartColors,
31
42
  }: StaticBarChartSurfaceProps) {
43
+ const renderer = useNativeRenderer();
44
+ const plotRef = useRef<BoxRenderable | null>(null);
45
+ const [hover, setHover] = useState<BarChartHover | null>(null);
32
46
  const totalWidth = Math.max(1, Math.floor(width));
33
47
  const totalHeight = Math.max(4, Math.floor(height));
48
+ const headerRows = title || header ? 1 : 0;
34
49
  const legendRows = series.length > 1 ? 1 : 0;
35
50
  const axisRows = 1;
36
- const plotHeight = Math.max(2, totalHeight - legendRows - axisRows);
51
+ const yAxisWidth = totalWidth >= 24 ? 8 : 0;
52
+ const plotRightPadding = totalWidth >= 32 ? 1 : 0;
53
+ const plotWidth = Math.max(1, totalWidth - yAxisWidth - plotRightPadding);
54
+ const plotHeight = Math.max(2, totalHeight - headerRows - legendRows - axisRows);
55
+ const resolvedChartColors = useMemo<BarChartColors>(() => ({
56
+ bgColor: chartColors?.bgColor ?? colors.bg,
57
+ gridColor: chartColors?.gridColor ?? colors.border,
58
+ axisColor: chartColors?.axisColor ?? colors.textDim,
59
+ negativeColor: chartColors?.negativeColor ?? colors.negative,
60
+ hoverColor: chartColors?.hoverColor ?? colors.textBright,
61
+ }), [chartColors]);
37
62
  const scene = useMemo(() => buildBarChartScene(series, {
38
- width: totalWidth,
63
+ width: plotWidth,
39
64
  height: plotHeight,
40
- colors: chartColors,
41
- }), [chartColors, plotHeight, series, totalWidth]);
65
+ colors: resolvedChartColors,
66
+ }), [plotHeight, plotWidth, resolvedChartColors, series]);
42
67
 
43
- const bitmapSize = useStaticChartBitmapSize(totalWidth, plotHeight);
68
+ const bitmapSize = useStaticChartBitmapSize(plotWidth, plotHeight);
44
69
 
45
70
  const bitmap = useMemo<NativeChartBitmap | null>(() => {
46
71
  if (!scene || !bitmapSize) return null;
47
- return renderNativeBarChart(scene, bitmapSize.pixelWidth, bitmapSize.pixelHeight);
48
- }, [bitmapSize, scene]);
72
+ return renderNativeBarChart(scene, bitmapSize.pixelWidth, bitmapSize.pixelHeight, hover);
73
+ }, [bitmapSize, hover, scene]);
49
74
 
50
- const textLines = useMemo(() => scene ? renderBarChart(scene) : [], [scene]);
51
- const axis = useMemo(() => scene ? renderBarChartAxis(scene) : "", [scene]);
75
+ const textLines = useMemo(() => scene ? renderBarChart(scene, hover) : [], [hover, scene]);
76
+ const axis = useMemo(() => scene ? renderBarChartAxis(scene, axisRows) : [], [axisRows, scene]);
77
+ const yAxis = useMemo(() => scene ? renderBarChartYAxis(scene, yAxisWidth, formatValue) : [], [formatValue, scene, yAxisWidth]);
78
+ const hoverLabel = useMemo(() => {
79
+ if (!hover) return null;
80
+ const value = formatValue ? formatValue(hover.value) : String(hover.value);
81
+ return series.length > 1 ? `${hover.category} ${hover.seriesLabel}: ${value}` : `${hover.category}: ${value}`;
82
+ }, [formatValue, hover, series.length]);
83
+ const readout = useMemo(() => {
84
+ if (!hoverLabel) return title ?? "";
85
+ return title ? `${title} ${hoverLabel}` : hoverLabel;
86
+ }, [hoverLabel, title]);
87
+ const hoverLabelLayout = useMemo(() => {
88
+ if (!hover || !hoverLabel) return null;
89
+ const text = truncateWithEllipsis(hoverLabel, plotWidth);
90
+ const labelWidth = Math.min(plotWidth, Math.max(1, text.length));
91
+ const labelLeft = Math.max(0, Math.min(
92
+ plotWidth - labelWidth,
93
+ hover.x + Math.floor(hover.width / 2) - Math.floor(labelWidth / 2),
94
+ ));
95
+ const labelTop = hover.value >= 0
96
+ ? Math.max(0, hover.row - 1)
97
+ : Math.min(plotHeight - 1, hover.row + 1);
98
+ return { labelLeft, labelTop, labelWidth, text };
99
+ }, [hover, hoverLabel, plotHeight, plotWidth]);
100
+
101
+ const handleMouseMove = useCallback((event: ChartMouseEvent) => {
102
+ if (!scene) return;
103
+ const pointer = getLocalPlotPointer(event, plotRef.current, renderer);
104
+ if (!pointer) {
105
+ setHover(null);
106
+ onHoverChange?.(null);
107
+ return;
108
+ }
109
+ const nextHover = resolveBarChartHover(scene, pointer.cellX);
110
+ setHover(nextHover);
111
+ onHoverChange?.(nextHover);
112
+ consumeChartMouseEvent(event);
113
+ }, [onHoverChange, renderer, scene]);
114
+
115
+ const handleMouseOut = useCallback(() => {
116
+ setHover(null);
117
+ onHoverChange?.(null);
118
+ }, [onHoverChange]);
119
+
120
+ const handleMouseDown = useCallback((event: ChartMouseEvent) => {
121
+ onMouseDown?.(event);
122
+ if (!scene) return;
123
+ const pointer = getLocalPlotPointer(event, plotRef.current, renderer);
124
+ if (!pointer) return;
125
+ const nextHover = resolveBarChartHover(scene, pointer.cellX);
126
+ setHover(nextHover);
127
+ onHoverChange?.(nextHover);
128
+ consumeChartMouseEvent(event);
129
+ }, [onHoverChange, onMouseDown, renderer, scene]);
52
130
 
53
131
  if (!scene) {
54
132
  return (
55
- <Box width={totalWidth} height={totalHeight}>
133
+ <Box flexDirection="column" width={totalWidth} height={totalHeight} onMouseDown={onMouseDown}>
134
+ {header ?? (title ? <Text fg={colors.textBright} bold>{truncateWithEllipsis(title, totalWidth)}</Text> : null)}
56
135
  <Text fg={colors.textDim}>No chart data</Text>
57
136
  </Box>
58
137
  );
@@ -60,6 +139,13 @@ export function StaticBarChartSurface({
60
139
 
61
140
  return (
62
141
  <Box flexDirection="column" width={totalWidth} height={totalHeight}>
142
+ {headerRows ? (
143
+ header ?? (
144
+ <Text fg={hover ? hover.value < 0 ? colors.negative : colors.textBright : colors.textDim} bold={!!title}>
145
+ {truncateWithEllipsis(readout, totalWidth)}
146
+ </Text>
147
+ )
148
+ ) : null}
63
149
  {legendRows ? (
64
150
  <Text>
65
151
  {series.map((item, index) => (
@@ -69,17 +155,54 @@ export function StaticBarChartSurface({
69
155
  ))}
70
156
  </Text>
71
157
  ) : null}
72
- <ChartSurface
73
- width={totalWidth}
74
- height={plotHeight}
75
- flexDirection="column"
76
- bitmaps={bitmap ? [bitmap] : null}
77
- >
78
- {textLines.map((line, index) => (
79
- <Text key={index} fg={colors.text}>{line}</Text>
80
- ))}
81
- </ChartSurface>
82
- <Text fg={colors.textDim}>{axis}</Text>
158
+ <Box flexDirection="row" width={totalWidth} height={plotHeight}>
159
+ {yAxisWidth ? (
160
+ <Box flexDirection="column" width={yAxisWidth} height={plotHeight}>
161
+ {yAxis.map((line, index) => (
162
+ <Text key={index} fg={colors.textDim}>{line}</Text>
163
+ ))}
164
+ </Box>
165
+ ) : null}
166
+ <Box position="relative" width={plotWidth} height={plotHeight}>
167
+ <ChartSurface
168
+ ref={plotRef}
169
+ width={plotWidth}
170
+ height={plotHeight}
171
+ flexDirection="column"
172
+ bitmaps={bitmap ? [bitmap] : null}
173
+ onMouseMove={handleMouseMove}
174
+ onMouseDown={handleMouseDown}
175
+ onMouseOut={handleMouseOut}
176
+ cursor="pointer"
177
+ data-gloom-interactive="true"
178
+ data-gloom-role="bar-chart-surface"
179
+ >
180
+ {textLines.map((line, index) => (
181
+ <Text key={index} fg={colors.text}>{line}</Text>
182
+ ))}
183
+ </ChartSurface>
184
+ {hoverLabelLayout ? (
185
+ <Box
186
+ position="absolute"
187
+ left={hoverLabelLayout.labelLeft}
188
+ top={hoverLabelLayout.labelTop}
189
+ width={hoverLabelLayout.labelWidth}
190
+ height={1}
191
+ zIndex={1}
192
+ backgroundColor={colors.panel}
193
+ >
194
+ <Text fg={hover && hover.value < 0 ? colors.negative : colors.textBright} bold>
195
+ {hoverLabelLayout.text}
196
+ </Text>
197
+ </Box>
198
+ ) : null}
199
+ </Box>
200
+ </Box>
201
+ {axis.map((line, index) => (
202
+ <Text key={index} fg={colors.textDim}>
203
+ {yAxisWidth ? `${" ".repeat(yAxisWidth)}${line}` : line}
204
+ </Text>
205
+ ))}
83
206
  </Box>
84
207
  );
85
208
  }