gloomberb 0.9.2 → 0.9.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -2
- package/bin/gloomberb +1 -1
- package/package.json +1 -1
- package/src/app/runtime/desktop-menu.ts +5 -1
- package/src/app/runtime/ticker-refresh.ts +4 -1
- package/src/app.tsx +20 -8
- package/src/brokers/account-cache.ts +1 -1
- package/src/brokers/sync-broker-instance.test.ts +107 -0
- package/src/brokers/sync-broker-instance.ts +22 -10
- package/src/capabilities/registry.ts +1 -0
- package/src/capabilities/types.ts +23 -0
- package/src/cli/commands/api.ts +119 -0
- package/src/cli/commands/automation.ts +173 -0
- package/src/cli/commands/command-utils.ts +53 -0
- package/src/cli/commands/market.ts +527 -0
- package/src/cli/commands/overview.ts +222 -0
- package/src/cli/commands/search.ts +39 -0
- package/src/cli/commands/system.ts +553 -0
- package/src/cli/commands/ticker.ts +104 -12
- package/src/cli/context.ts +35 -2
- package/src/cli/desktop-pane-shot.ts +2 -1
- package/src/cli/entry.ts +51 -0
- package/src/cli/errors.ts +56 -5
- package/src/cli/index.ts +73 -11
- package/src/cli/options.test.ts +38 -0
- package/src/cli/options.ts +95 -0
- package/src/cli/pane-functions/index.ts +10 -1
- package/src/cli/pane-functions/screenshot.ts +13 -1
- package/src/cli/registry.test.ts +40 -0
- package/src/cli/registry.ts +46 -5
- package/src/cli/result.test.ts +82 -0
- package/src/cli/result.ts +149 -0
- package/src/cli/types.ts +11 -0
- package/src/cli.test.ts +11 -11
- package/src/components/chart/bar-chart-renderer.test.ts +108 -3
- package/src/components/chart/bar-chart-renderer.ts +226 -27
- package/src/components/chart/comparison-stock-chart/interactions.ts +7 -1
- package/src/components/chart/comparison-stock-chart/legend.tsx +4 -0
- package/src/components/chart/comparison-stock-chart/render-data.ts +37 -8
- package/src/components/chart/comparison-stock-chart/selection-runtime.ts +19 -2
- package/src/components/chart/comparison-stock-chart/sources.ts +8 -4
- package/src/components/chart/comparison-stock-chart/toolbar.tsx +13 -3
- package/src/components/chart/comparison-stock-chart/view.tsx +12 -2
- package/src/components/chart/core/pane-settings.test.ts +27 -0
- package/src/components/chart/core/pane-settings.ts +60 -1
- package/src/components/chart/core/pointer.ts +34 -6
- package/src/components/chart/price-axis-labels.tsx +3 -1
- package/src/components/chart/static/bar-chart-surface.test.tsx +97 -0
- package/src/components/chart/static/bar-chart-surface.tsx +151 -28
- package/src/components/chart/static/chart/axis-overlays.tsx +243 -0
- package/src/components/chart/static/chart/surface.test.tsx +92 -0
- package/src/components/chart/static/chart/surface.tsx +167 -24
- package/src/components/chart/stock-chart/index.test.ts +25 -0
- package/src/components/chart/stock-chart/resolved/runtime.ts +1 -1
- package/src/components/chart/stock-chart/settings.ts +7 -3
- package/src/components/chart/stock-chart/viewport/runtime.ts +5 -5
- package/src/components/command-bar/layout-items/current-layout.ts +5 -1
- package/src/components/command-bar/pane-templates/workflow-route.ts +10 -0
- package/src/components/command-bar/surface/pane.test.tsx +76 -0
- package/src/components/data-table/view.tsx +13 -2
- package/src/components/feed-data-table/stack-view.tsx +9 -1
- package/src/components/index.ts +1 -1
- package/src/components/input-search-bar.test.tsx +103 -0
- package/src/components/input-search-bar.tsx +16 -0
- package/src/components/layout/detached-pane-shell.tsx +6 -3
- package/src/components/layout/floating-pane.tsx +3 -2
- package/src/components/layout/pane/content.tsx +20 -8
- package/src/components/layout/pane/footer/registration.tsx +6 -2
- package/src/components/layout/pane/frame.tsx +4 -4
- package/src/components/layout/pane/header.tsx +40 -4
- package/src/components/layout/pane/index.tsx +3 -1
- package/src/components/layout/pane/sizing.test.ts +43 -0
- package/src/components/layout/pane/sizing.ts +13 -4
- package/src/components/layout/shell/active-drag.ts +5 -1
- package/src/components/layout/shell/cursor-occlusion.ts +196 -0
- package/src/components/layout/shell/drag/runtime.ts +28 -3
- package/src/components/layout/shell/fullscreen.ts +10 -0
- package/src/components/layout/shell/index.test.tsx +487 -4
- package/src/components/layout/shell/index.tsx +165 -4
- package/src/components/layout/shell/layout-state.test.ts +33 -0
- package/src/components/layout/shell/layout-state.ts +4 -9
- package/src/components/layout/shell/native/pointer-runtime.ts +10 -4
- package/src/components/layout/shell/pane/layers.tsx +34 -16
- package/src/components/layout/shell/pane/management-shortcuts.ts +19 -0
- package/src/components/layout/shell/shortcuts.ts +4 -0
- package/src/components/layout/shell/terminal-pointer-runtime.ts +29 -19
- package/src/components/layout/shell/visible-layout.ts +17 -0
- package/src/components/layout/shell/window-mode/index.ts +11 -3
- package/src/components/layout/shell/window-mode/overlays.tsx +8 -1
- package/src/components/layout/status-bar.test.tsx +111 -1
- package/src/components/layout/status-bar.tsx +37 -7
- package/src/components/layout/transient-layout.tsx +57 -0
- package/src/components/layout/window-edit/mode.test.ts +76 -5
- package/src/components/layout/window-edit/mode.ts +47 -2
- package/src/components/layout/window-edit/presentation.ts +2 -1
- package/src/components/onboarding/onboarding-steps.tsx +8 -28
- package/src/components/onboarding/onboarding-wizard.test.tsx +13 -12
- package/src/components/onboarding/onboarding-wizard.tsx +2 -4
- package/src/components/onboarding/portfolio-step/index.tsx +0 -12
- package/src/components/onboarding/portfolio-step/types.ts +1 -3
- package/src/components/onboarding/wizard-keyboard.ts +2 -5
- package/src/components/price-performance.tsx +9 -1
- package/src/components/table-view-shared.tsx +12 -4
- package/src/components/ui/data-table/opentui/index.tsx +25 -12
- package/src/components/ui/data-table/types.ts +4 -0
- package/src/components/ui/fields.tsx +10 -3
- package/src/components/ui/page-stack-view.tsx +6 -2
- package/src/components/ui/table-layout.test.ts +25 -0
- package/src/components/ui/table-layout.ts +22 -6
- package/src/components/ui/tabs.tsx +26 -10
- package/src/components/ui/ui.test.tsx +87 -0
- package/src/core/app-services.ts +7 -4
- package/src/core/state/app/layout-reducer.ts +2 -2
- package/src/data/config/store/index.ts +1 -2
- package/src/plugins/builtin/analytics/index.test.tsx +6 -2
- package/src/plugins/builtin/analytics/pane-model.ts +43 -8
- package/src/plugins/builtin/cloud-tweets/pane.tsx +2 -0
- package/src/plugins/builtin/cloud-tweets/search-bar.tsx +3 -0
- package/src/plugins/builtin/cloud-tweets/table.tsx +14 -2
- package/src/plugins/builtin/comparison-chart/index.test.tsx +130 -6
- package/src/plugins/builtin/kelly-sizer/constants.ts +3 -0
- package/src/plugins/builtin/kelly-sizer/fields.ts +214 -0
- package/src/plugins/builtin/kelly-sizer/index.test.tsx +197 -0
- package/src/plugins/builtin/kelly-sizer/index.tsx +47 -0
- package/src/plugins/builtin/kelly-sizer/model.test.ts +152 -0
- package/src/plugins/builtin/kelly-sizer/model.ts +580 -0
- package/src/plugins/builtin/kelly-sizer/pane.tsx +558 -0
- package/src/plugins/builtin/kelly-sizer/portfolio.ts +71 -0
- package/src/plugins/builtin/kelly-sizer/sections.tsx +142 -0
- package/src/plugins/builtin/kelly-sizer/state.ts +67 -0
- package/src/plugins/builtin/kelly-sizer/test-support.tsx +174 -0
- package/src/plugins/builtin/kelly-sizer/types.ts +156 -0
- package/src/plugins/builtin/kelly-sizer/view.test.tsx +77 -0
- package/src/plugins/builtin/kelly-sizer/view.tsx +375 -0
- package/src/plugins/builtin/layout-manager/index.tsx +5 -1
- package/src/plugins/builtin/news/wire/read-state.ts +9 -32
- package/src/plugins/builtin/options/view.test.tsx +141 -4
- package/src/plugins/builtin/options/view.tsx +76 -15
- package/src/plugins/builtin/portfolio-list/account-metrics.test.ts +26 -1
- package/src/plugins/builtin/portfolio-list/account-metrics.ts +14 -0
- package/src/plugins/builtin/portfolio-list/cli/portfolio-command.ts +23 -0
- package/src/plugins/builtin/portfolio-list/cli/watchlist-command.ts +19 -2
- package/src/plugins/builtin/portfolio-list/index.test.ts +23 -0
- package/src/plugins/builtin/portfolio-list/pane/index.tsx +3 -1
- package/src/plugins/builtin/portfolio-list/quick-add/index.tsx +2 -7
- package/src/plugins/builtin/portfolio-list/summary/index.test.ts +59 -1
- package/src/plugins/builtin/portfolio-list/summary/index.tsx +36 -5
- package/src/plugins/builtin/shared/read-state.ts +43 -0
- package/src/plugins/builtin/substack/article-detail.test.tsx +10 -0
- package/src/plugins/builtin/substack/article-detail.tsx +39 -15
- package/src/plugins/builtin/substack/article-stack.tsx +6 -2
- package/src/plugins/builtin/substack/feed-tabs.tsx +12 -9
- package/src/plugins/builtin/substack/pane.tsx +8 -3
- package/src/plugins/builtin/substack/read-state.ts +55 -0
- package/src/plugins/builtin/thirteenf/pane.test.tsx +26 -0
- package/src/plugins/builtin/thirteenf/pane.tsx +12 -1
- package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/content.tsx +149 -48
- package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/model.ts +25 -2
- package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph.tsx +5 -4
- package/src/plugins/builtin/ticker-detail/data-panes.test.ts +16 -0
- package/src/plugins/builtin/ticker-detail/index.test.tsx +143 -0
- package/src/plugins/catalog-ui.ts +2 -0
- package/src/plugins/ibkr/broker-adapter.ts +19 -0
- package/src/plugins/ibkr/config.test.ts +31 -2
- package/src/plugins/ibkr/config.ts +9 -2
- package/src/plugins/ibkr/flex/index.test.ts +18 -9
- package/src/plugins/ibkr/flex/index.ts +30 -0
- package/src/plugins/ibkr/flex/statement-client.ts +68 -14
- package/src/plugins/ibkr/gateway/account-loaders.ts +1 -1
- package/src/plugins/ibkr/gateway/account-summary.ts +1 -0
- package/src/plugins/ibkr/gateway/service/index.test.ts +8 -0
- package/src/plugins/pane-manager/docking.ts +9 -2
- package/src/plugins/pane-manager/gridlock.ts +13 -5
- package/src/plugins/pane-manager/layout-state.ts +25 -0
- package/src/plugins/pane-manager/queries.ts +8 -2
- package/src/plugins/pane-manager.test.ts +33 -0
- package/src/plugins/pane-manager.ts +6 -1
- package/src/plugins/prediction-markets/chart.tsx +10 -4
- package/src/plugins/prediction-markets/controller/index.ts +2 -0
- package/src/plugins/prediction-markets/controller/keyboard.ts +34 -5
- package/src/plugins/prediction-markets/detail/book.tsx +141 -96
- package/src/plugins/prediction-markets/detail/overview.tsx +3 -0
- package/src/plugins/prediction-markets/detail/pane.tsx +71 -64
- package/src/plugins/prediction-markets/detail/shared.tsx +19 -0
- package/src/plugins/prediction-markets/detail/trades.tsx +64 -19
- package/src/plugins/prediction-markets/detail.test.tsx +50 -0
- package/src/plugins/prediction-markets/pane.test.tsx +75 -0
- package/src/plugins/prediction-markets/pane.tsx +71 -23
- package/src/plugins/prediction-markets/test-helpers.tsx +42 -23
- package/src/renderers/electrobun/bun/application-menu/click.test.ts +9 -0
- package/src/renderers/electrobun/bun/application-menu/click.ts +1 -0
- package/src/renderers/electrobun/bun/application-menu/index.test.ts +9 -0
- package/src/renderers/electrobun/bun/application-menu/index.ts +3 -2
- package/src/renderers/electrobun/bun/index.ts +13 -0
- package/src/renderers/electrobun/bun/window/focus.test.ts +35 -0
- package/src/renderers/electrobun/bun/window/focus.ts +5 -0
- package/src/renderers/electrobun/view/app-services.ts +6 -4
- package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +13 -2
- package/src/renderers/electrobun/view/data-table/index.tsx +19 -4
- package/src/renderers/electrobun/view/data-table/row.tsx +30 -6
- package/src/renderers/electrobun/view/desktop/controls.tsx +11 -5
- package/src/renderers/electrobun/view/host/ascii-text.ts +50 -0
- package/src/renderers/electrobun/view/host/input.tsx +115 -18
- package/src/renderers/electrobun/view/host/tabs.tsx +7 -2
- package/src/renderers/electrobun/view/host/text.test.ts +22 -0
- package/src/renderers/electrobun/view/host/text.tsx +24 -20
- package/src/renderers/electrobun/view/styles.css +11 -0
- package/src/renderers/electrobun/view/ui-host.tsx +1 -1
- package/src/renderers/opentui/host.tsx +17 -1
- package/src/renderers/opentui/start.tsx +13 -5
- package/src/renderers/opentui/ui-host.tsx +57 -25
- package/src/sources/provider-router/batches.ts +16 -2
- package/src/sources/provider-router/financial-routes.ts +5 -0
- package/src/sources/provider-router/financials.ts +11 -23
- package/src/sources/provider-router/index.test.ts +189 -0
- package/src/sources/provider-router/primary.ts +3 -2
- package/src/sources/sec-edgar.test.ts +83 -0
- package/src/sources/sec-edgar.ts +231 -0
- package/src/sources/yahoo-finance.test.ts +51 -0
- package/src/sources/yahoo-finance.ts +49 -1
- package/src/state/app/context/index.test.tsx +5 -8
- package/src/state/app/input-capture.ts +12 -0
- package/src/types/broker.ts +1 -0
- package/src/types/config.ts +10 -0
- package/src/types/plugin.ts +28 -0
- package/src/types/trading.ts +2 -0
- package/src/ui/host.tsx +16 -0
- package/src/ui/index.tsx +2 -0
- package/src/utils/cli-output.ts +6 -0
- package/src/utils/financial-statements.ts +23 -0
- package/src/utils/search-focus-navigation.ts +24 -0
- package/src/version.ts +1 -1
- package/src/components/onboarding/portfolio-step/manual-name-panel.tsx +0 -62
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
type ComparisonChartRenderMode,
|
|
13
13
|
type TimeRange,
|
|
14
14
|
} from "../core/types";
|
|
15
|
+
import type { MouseInteractionEvent } from "../core/pointer";
|
|
15
16
|
|
|
16
17
|
const MODE_CHIPS: Record<ComparisonChartRenderMode, string> = {
|
|
17
18
|
area: "A",
|
|
@@ -22,6 +23,7 @@ interface ComparisonChartToolbarProps {
|
|
|
22
23
|
activePreset: TimeRange | null;
|
|
23
24
|
availableManualResolutions: ManualChartResolution[];
|
|
24
25
|
effectiveResolution: ChartResolution;
|
|
26
|
+
focusPaneForMouseInteraction: (event: MouseInteractionEvent | null | undefined) => void;
|
|
25
27
|
isUpdating: boolean;
|
|
26
28
|
onRangeSelect: (range: TimeRange) => void;
|
|
27
29
|
onRenderModeSelect: (mode: ComparisonChartRenderMode) => void;
|
|
@@ -36,6 +38,7 @@ export function ComparisonChartToolbar({
|
|
|
36
38
|
activePreset,
|
|
37
39
|
availableManualResolutions,
|
|
38
40
|
effectiveResolution,
|
|
41
|
+
focusPaneForMouseInteraction,
|
|
39
42
|
isUpdating,
|
|
40
43
|
onRangeSelect,
|
|
41
44
|
onRenderModeSelect,
|
|
@@ -54,7 +57,8 @@ export function ComparisonChartToolbar({
|
|
|
54
57
|
key={range}
|
|
55
58
|
fg={activePreset === range ? colors.textBright : (isRangePresetSupported(range, availableManualResolutions) ? colors.textDim : colors.textMuted)}
|
|
56
59
|
attributes={activePreset === range ? TextAttributes.BOLD : 0}
|
|
57
|
-
onMouseDown={() => {
|
|
60
|
+
onMouseDown={(event: any) => {
|
|
61
|
+
focusPaneForMouseInteraction(event);
|
|
58
62
|
if (isRangePresetSupported(range, availableManualResolutions)) {
|
|
59
63
|
onRangeSelect(range);
|
|
60
64
|
}
|
|
@@ -73,7 +77,10 @@ export function ComparisonChartToolbar({
|
|
|
73
77
|
key={resolution}
|
|
74
78
|
fg={effectiveResolution === resolution ? colors.textBright : colors.textDim}
|
|
75
79
|
attributes={effectiveResolution === resolution ? TextAttributes.BOLD : 0}
|
|
76
|
-
onMouseDown={() =>
|
|
80
|
+
onMouseDown={(event: any) => {
|
|
81
|
+
focusPaneForMouseInteraction(event);
|
|
82
|
+
onResolutionSelect(resolution);
|
|
83
|
+
}}
|
|
77
84
|
>
|
|
78
85
|
{getChartResolutionLabel(resolution)}
|
|
79
86
|
</Text>
|
|
@@ -89,7 +96,10 @@ export function ComparisonChartToolbar({
|
|
|
89
96
|
key={mode}
|
|
90
97
|
fg={renderMode === mode ? colors.textBright : colors.textDim}
|
|
91
98
|
attributes={renderMode === mode ? TextAttributes.BOLD : 0}
|
|
92
|
-
onMouseDown={() =>
|
|
99
|
+
onMouseDown={(event: any) => {
|
|
100
|
+
focusPaneForMouseInteraction(event);
|
|
101
|
+
onRenderModeSelect(mode);
|
|
102
|
+
}}
|
|
93
103
|
>
|
|
94
104
|
{MODE_CHIPS[mode]}
|
|
95
105
|
</Text>
|
|
@@ -270,6 +270,7 @@ function ComparisonStockChartView({
|
|
|
270
270
|
isUpdating,
|
|
271
271
|
resolutionChips,
|
|
272
272
|
selectionSupportMap,
|
|
273
|
+
performanceHistoryBySymbol,
|
|
273
274
|
series,
|
|
274
275
|
supportMap,
|
|
275
276
|
} = useComparisonChartRenderData({
|
|
@@ -279,7 +280,11 @@ function ComparisonStockChartView({
|
|
|
279
280
|
const performanceBySymbol = useMemo(() => {
|
|
280
281
|
const bySymbol = new Map<string, PriceReturnField[]>();
|
|
281
282
|
for (const source of symbolSources) {
|
|
282
|
-
const
|
|
283
|
+
const baselineHistory = performanceHistoryBySymbol.get(source.symbol) ?? [];
|
|
284
|
+
const sourceHistory = appendQuoteToPriceReturnHistory(
|
|
285
|
+
baselineHistory.length >= 2 ? baselineHistory : source.priceHistory,
|
|
286
|
+
source.quote,
|
|
287
|
+
);
|
|
283
288
|
const fallbackHistory = series.find((entry) => entry.symbol === source.symbol)?.points ?? [];
|
|
284
289
|
bySymbol.set(
|
|
285
290
|
source.symbol,
|
|
@@ -287,7 +292,7 @@ function ComparisonStockChartView({
|
|
|
287
292
|
);
|
|
288
293
|
}
|
|
289
294
|
return bySymbol;
|
|
290
|
-
}, [series, symbolSources]);
|
|
295
|
+
}, [performanceHistoryBySymbol, series, symbolSources]);
|
|
291
296
|
const summarySymbols = useMemo(
|
|
292
297
|
() => sortSymbolsByOneYearReturn(symbols, performanceBySymbol),
|
|
293
298
|
[performanceBySymbol, symbols],
|
|
@@ -340,6 +345,7 @@ function ComparisonStockChartView({
|
|
|
340
345
|
displayCursor,
|
|
341
346
|
displayCursorX,
|
|
342
347
|
displayCursorY,
|
|
348
|
+
focusPaneForMouseInteraction,
|
|
343
349
|
mouseCrosshairDisabledRef,
|
|
344
350
|
plotRef,
|
|
345
351
|
scrollPanCellRemainderRef,
|
|
@@ -351,6 +357,7 @@ function ComparisonStockChartView({
|
|
|
351
357
|
chartWidth,
|
|
352
358
|
cursorX: viewState.cursorX,
|
|
353
359
|
cursorY: viewState.cursorY,
|
|
360
|
+
focused,
|
|
354
361
|
renderer,
|
|
355
362
|
setViewState,
|
|
356
363
|
symbols: summarySymbols,
|
|
@@ -444,6 +451,7 @@ function ComparisonStockChartView({
|
|
|
444
451
|
chartWidth,
|
|
445
452
|
commitSelectionCursor,
|
|
446
453
|
expandBufferRange,
|
|
454
|
+
focusPaneForMouseInteraction,
|
|
447
455
|
mouseCrosshairDisabledRef,
|
|
448
456
|
plotRef,
|
|
449
457
|
pointCount: projection.dates.length,
|
|
@@ -522,6 +530,7 @@ function ComparisonStockChartView({
|
|
|
522
530
|
legendActiveIndex={legendActiveIndex}
|
|
523
531
|
legendItemWidth={legendItemWidth}
|
|
524
532
|
legendRows={legendRows}
|
|
533
|
+
onFocusInteraction={focusPaneForMouseInteraction}
|
|
525
534
|
onOpenSymbol={onOpenSymbol}
|
|
526
535
|
onSelectSymbol={setSelectedSymbol}
|
|
527
536
|
performanceBySymbol={performanceBySymbol}
|
|
@@ -542,6 +551,7 @@ function ComparisonStockChartView({
|
|
|
542
551
|
activePreset={activePreset}
|
|
543
552
|
availableManualResolutions={availableManualResolutions}
|
|
544
553
|
effectiveResolution={effectiveResolution}
|
|
554
|
+
focusPaneForMouseInteraction={focusPaneForMouseInteraction}
|
|
545
555
|
isUpdating={isUpdating}
|
|
546
556
|
onRangeSelect={setRangePreset}
|
|
547
557
|
onRenderModeSelect={(mode) => setViewState((current) => ({ ...current, renderMode: mode }))}
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
createDefaultConfig,
|
|
4
|
+
findPaneInstance,
|
|
5
|
+
} from "../../../types/config";
|
|
2
6
|
import {
|
|
3
7
|
consumeStoredChartSelectionChange,
|
|
8
|
+
createChartRenderModeConfig,
|
|
4
9
|
createStoredChartSelectionSyncState,
|
|
5
10
|
markStoredChartSelectionLocallyApplied,
|
|
6
11
|
} from "./pane-settings";
|
|
@@ -22,4 +27,26 @@ describe("chart pane setting sync", () => {
|
|
|
22
27
|
expect(consumeStoredChartSelectionChange(state, "5Y", "1wk")).toBe(true);
|
|
23
28
|
expect(state.lastAppliedKey).toBe("5Y:1wk");
|
|
24
29
|
});
|
|
30
|
+
|
|
31
|
+
test("persists chart render mode as both pane state and global default", () => {
|
|
32
|
+
const config = createDefaultConfig("/tmp/gloomberb-chart-mode-test");
|
|
33
|
+
|
|
34
|
+
const nextConfig = createChartRenderModeConfig({
|
|
35
|
+
activePanel: "right",
|
|
36
|
+
config,
|
|
37
|
+
focusedPaneId: "ticker-detail:main",
|
|
38
|
+
paneState: {},
|
|
39
|
+
}, "ticker-detail:main", "candles");
|
|
40
|
+
|
|
41
|
+
expect(nextConfig.chartPreferences.defaultRenderMode).toBe("candles");
|
|
42
|
+
expect(findPaneInstance(nextConfig.layout, "ticker-detail:main")?.settings).toMatchObject({
|
|
43
|
+
chartRangePreset: "5Y",
|
|
44
|
+
chartRenderMode: "candles",
|
|
45
|
+
chartResolution: "auto",
|
|
46
|
+
});
|
|
47
|
+
expect(findPaneInstance(
|
|
48
|
+
nextConfig.layouts[nextConfig.activeLayoutIndex]!.layout,
|
|
49
|
+
"ticker-detail:main",
|
|
50
|
+
)?.settings?.chartRenderMode).toBe("candles");
|
|
51
|
+
});
|
|
25
52
|
});
|
|
@@ -7,7 +7,18 @@ import {
|
|
|
7
7
|
syncConfigActiveLayoutState,
|
|
8
8
|
} from "../../../state/app/context";
|
|
9
9
|
import { scheduleConfigSave } from "../../../state/config-save-scheduler";
|
|
10
|
-
import
|
|
10
|
+
import {
|
|
11
|
+
findPaneInstance,
|
|
12
|
+
type AppConfig,
|
|
13
|
+
} from "../../../types/config";
|
|
14
|
+
import type { ChartRenderMode, ChartResolution, TimeRange } from "./types";
|
|
15
|
+
|
|
16
|
+
interface ChartRenderModePersistenceState {
|
|
17
|
+
activePanel: "left" | "right";
|
|
18
|
+
config: AppConfig;
|
|
19
|
+
focusedPaneId: string | null;
|
|
20
|
+
paneState: Record<string, Record<string, unknown>>;
|
|
21
|
+
}
|
|
11
22
|
|
|
12
23
|
export interface StoredChartSelectionSyncState {
|
|
13
24
|
lastAppliedKey: string;
|
|
@@ -80,3 +91,51 @@ export function usePersistChartControlSelection(rangePresetKey: string): (
|
|
|
80
91
|
scheduleConfigSave(nextConfig);
|
|
81
92
|
};
|
|
82
93
|
}
|
|
94
|
+
|
|
95
|
+
export function createChartRenderModeConfig(
|
|
96
|
+
currentState: ChartRenderModePersistenceState,
|
|
97
|
+
paneId: string,
|
|
98
|
+
renderMode: ChartRenderMode,
|
|
99
|
+
): AppConfig {
|
|
100
|
+
const currentPane = findPaneInstance(currentState.config.layout, paneId);
|
|
101
|
+
const layout = currentPane
|
|
102
|
+
? setPaneSettings(currentState.config.layout, paneId, {
|
|
103
|
+
...(currentPane.settings ?? {}),
|
|
104
|
+
chartRenderMode: renderMode,
|
|
105
|
+
})
|
|
106
|
+
: currentState.config.layout;
|
|
107
|
+
return syncConfigActiveLayoutState(
|
|
108
|
+
{
|
|
109
|
+
...currentState.config,
|
|
110
|
+
chartPreferences: {
|
|
111
|
+
...currentState.config.chartPreferences,
|
|
112
|
+
defaultRenderMode: renderMode,
|
|
113
|
+
},
|
|
114
|
+
layout,
|
|
115
|
+
},
|
|
116
|
+
currentState.paneState,
|
|
117
|
+
currentState.focusedPaneId,
|
|
118
|
+
currentState.activePanel,
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function usePersistChartRenderMode(): (renderMode: ChartRenderMode) => void {
|
|
123
|
+
const dispatch = useAppDispatch();
|
|
124
|
+
const stateRef = useAppStateRef();
|
|
125
|
+
const paneId = usePaneInstanceId();
|
|
126
|
+
|
|
127
|
+
return (renderMode) => {
|
|
128
|
+
const currentState = stateRef.current;
|
|
129
|
+
const currentPane = findPaneInstance(currentState.config.layout, paneId);
|
|
130
|
+
if (
|
|
131
|
+
currentState.config.chartPreferences.defaultRenderMode === renderMode
|
|
132
|
+
&& currentPane?.settings?.chartRenderMode === renderMode
|
|
133
|
+
) {
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const nextConfig = createChartRenderModeConfig(currentState, paneId, renderMode);
|
|
138
|
+
dispatch({ type: "SET_CONFIG", config: nextConfig });
|
|
139
|
+
scheduleConfigSave(nextConfig);
|
|
140
|
+
};
|
|
141
|
+
}
|
|
@@ -11,11 +11,16 @@ interface RenderableSizeLike {
|
|
|
11
11
|
interface PlotRenderableLike extends RenderableSizeLike {
|
|
12
12
|
x?: number;
|
|
13
13
|
y?: number;
|
|
14
|
+
absoluteX?: number;
|
|
15
|
+
absoluteY?: number;
|
|
16
|
+
absoluteBounds?: { x: number; y: number; width: number; height: number };
|
|
14
17
|
}
|
|
15
18
|
|
|
16
19
|
export interface ChartMouseEvent {
|
|
17
20
|
x: number;
|
|
18
21
|
y: number;
|
|
22
|
+
preciseX?: number;
|
|
23
|
+
preciseY?: number;
|
|
19
24
|
pixelX?: number;
|
|
20
25
|
pixelY?: number;
|
|
21
26
|
stopPropagation?: () => void;
|
|
@@ -31,6 +36,8 @@ export interface ChartMouseEvent {
|
|
|
31
36
|
};
|
|
32
37
|
}
|
|
33
38
|
|
|
39
|
+
export type MouseInteractionEvent = Pick<ChartMouseEvent, "stopPropagation" | "preventDefault">;
|
|
40
|
+
|
|
34
41
|
export function consumeChartMouseEvent(event: Pick<ChartMouseEvent, "stopPropagation" | "preventDefault">): void {
|
|
35
42
|
event.stopPropagation?.();
|
|
36
43
|
event.preventDefault?.();
|
|
@@ -93,14 +100,33 @@ function getRendererCellMetrics(renderer: RendererMetricsHost) {
|
|
|
93
100
|
}
|
|
94
101
|
|
|
95
102
|
function getRenderableSize(renderable: RenderableSizeLike | null): { width: number; height: number } | null {
|
|
96
|
-
if (!renderable
|
|
97
|
-
|
|
103
|
+
if (!renderable) return null;
|
|
104
|
+
if (typeof renderable.width === "number" && typeof renderable.height === "number") {
|
|
105
|
+
return { width: renderable.width, height: renderable.height };
|
|
106
|
+
}
|
|
107
|
+
const absoluteBounds = (renderable as PlotRenderableLike).absoluteBounds;
|
|
108
|
+
if (absoluteBounds && typeof absoluteBounds.width === "number" && typeof absoluteBounds.height === "number") {
|
|
109
|
+
return { width: absoluteBounds.width, height: absoluteBounds.height };
|
|
110
|
+
}
|
|
111
|
+
return null;
|
|
98
112
|
}
|
|
99
113
|
|
|
100
114
|
function getPlotBounds(renderable: PlotRenderableLike | null): { x: number; y: number; width: number; height: number } | null {
|
|
101
|
-
if (!renderable
|
|
115
|
+
if (!renderable) return null;
|
|
102
116
|
const size = getRenderableSize(renderable);
|
|
103
|
-
|
|
117
|
+
if (!size) return null;
|
|
118
|
+
const x = typeof renderable.absoluteX === "number"
|
|
119
|
+
? renderable.absoluteX
|
|
120
|
+
: typeof renderable.absoluteBounds?.x === "number"
|
|
121
|
+
? renderable.absoluteBounds.x
|
|
122
|
+
: renderable.x;
|
|
123
|
+
const y = typeof renderable.absoluteY === "number"
|
|
124
|
+
? renderable.absoluteY
|
|
125
|
+
: typeof renderable.absoluteBounds?.y === "number"
|
|
126
|
+
? renderable.absoluteBounds.y
|
|
127
|
+
: renderable.y;
|
|
128
|
+
if (typeof x !== "number" || typeof y !== "number") return null;
|
|
129
|
+
return { x, y, width: size.width, height: size.height };
|
|
104
130
|
}
|
|
105
131
|
|
|
106
132
|
export function getRenderablePixelSize(
|
|
@@ -214,8 +240,10 @@ export function getLocalPlotPointer(
|
|
|
214
240
|
const bounds = getPlotBounds(renderable);
|
|
215
241
|
if (!bounds) return null;
|
|
216
242
|
|
|
217
|
-
const
|
|
218
|
-
const
|
|
243
|
+
const rawCellX = typeof event.preciseX === "number" ? event.preciseX : event.x;
|
|
244
|
+
const rawCellY = typeof event.preciseY === "number" ? event.preciseY : event.y;
|
|
245
|
+
const localCellX = rawCellX - bounds.x;
|
|
246
|
+
const localCellY = rawCellY - bounds.y;
|
|
219
247
|
if (localCellX < 0 || localCellX >= bounds.width || localCellY < 0 || localCellY >= bounds.height) {
|
|
220
248
|
return null;
|
|
221
249
|
}
|
|
@@ -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 :
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
|
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:
|
|
63
|
+
width: plotWidth,
|
|
39
64
|
height: plotHeight,
|
|
40
|
-
colors:
|
|
41
|
-
}), [
|
|
65
|
+
colors: resolvedChartColors,
|
|
66
|
+
}), [plotHeight, plotWidth, resolvedChartColors, series]);
|
|
42
67
|
|
|
43
|
-
const bitmapSize = useStaticChartBitmapSize(
|
|
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) :
|
|
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
|
-
<
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
}
|