gloomberb 0.9.7 → 0.9.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/api-client/auth.ts +20 -0
- package/src/api-client/index.test.ts +3 -3
- package/src/api-client/index.ts +48 -6
- package/src/api-client/request.test.ts +63 -0
- package/src/api-client/request.ts +53 -5
- package/src/api-client/types.ts +45 -2
- package/src/app/pane-runtime/layout-placement.ts +0 -1
- package/src/app/runtime/desktop-deeplink.test.ts +87 -0
- package/src/app/runtime/desktop-deeplink.ts +496 -0
- package/src/app.tsx +34 -0
- package/src/brokers/account-cache.ts +2 -10
- package/src/brokers/broker-portfolio-sync.ts +0 -1
- package/src/brokers/broker-ticker-sync.ts +20 -16
- package/src/brokers/remote-broker-adapter.ts +3 -0
- package/src/capabilities/registry.test.ts +25 -0
- package/src/capabilities/registry.ts +22 -5
- package/src/cli/commands/automation.ts +1 -3
- package/src/cli/commands/command-utils.ts +0 -7
- package/src/cli/commands/overview.ts +2 -2
- package/src/cli/commands/plugins.ts +34 -17
- package/src/cli/index.ts +2 -2
- package/src/cli.test.ts +0 -157
- package/src/components/chart/bar-chart-renderer.test.ts +87 -12
- package/src/components/chart/bar-chart-renderer.ts +111 -55
- package/src/components/chart/comparison/renderer.ts +0 -1
- package/src/components/chart/comparison-stock-chart/render-output.ts +3 -8
- package/src/components/chart/comparison-stock-chart/view.tsx +0 -2
- package/src/components/chart/core/index.test.ts +0 -24
- package/src/components/chart/core/renderer.ts +0 -4
- package/src/components/chart/core/resolution.test.ts +0 -17
- package/src/components/chart/core/resolution.ts +0 -26
- package/src/components/chart/core/scroll.test.ts +0 -6
- package/src/components/chart/core/scroll.ts +0 -22
- package/src/components/chart/core/viewport.test.ts +2 -23
- package/src/components/chart/core/viewport.ts +0 -27
- package/src/components/chart/market-session.ts +1 -1
- package/src/components/chart/native/surface/hooks.ts +10 -3
- package/src/components/chart/static/bar-chart-surface.tsx +104 -20
- package/src/components/chart/stock-chart/data-runtime.ts +0 -1
- package/src/components/chart/stock-chart/footer.test.ts +65 -0
- package/src/components/chart/stock-chart/footer.ts +68 -13
- package/src/components/chart/stock-chart/resolved.tsx +18 -4
- package/src/components/chart/stock-chart/viewport/effects.ts +0 -3
- package/src/components/command-bar/helpers.test.ts +1 -80
- package/src/components/command-bar/helpers.ts +1 -36
- package/src/components/command-bar/pane-settings.ts +1 -1
- package/src/components/command-bar/pane-templates/items.ts +11 -3
- package/src/components/command-bar/panel/runtime.ts +0 -2
- package/src/components/command-bar/routes/root/results.ts +1 -1
- package/src/components/command-bar/routes/ticker-search/results.ts +0 -11
- package/src/components/command-bar/surface/index.test.tsx +52 -16
- package/src/components/command-bar/surface/index.tsx +0 -1
- package/src/components/command-bar/workflow/ops.ts +81 -20
- package/src/components/feed-data-table/stack-view.test.tsx +36 -130
- package/src/components/feed-data-table/stack-view.tsx +0 -1
- package/src/components/index.ts +1 -0
- package/src/components/layout/shell/index.test.tsx +2 -113
- package/src/components/layout/shell/index.tsx +0 -1
- package/src/components/layout/shell/menu.ts +0 -1
- package/src/components/layout/shell/terminal-pointer-runtime.ts +0 -1
- package/src/components/onboarding/onboarding-wizard.tsx +3 -3
- package/src/components/onboarding/wizard-keyboard.ts +2 -23
- package/src/components/price-selector-dialog.tsx +3 -3
- package/src/components/ticker/badge/text.tsx +0 -1
- package/src/components/toggle-list.tsx +29 -73
- package/src/components/ui/button.tsx +4 -1
- package/src/components/ui/checkbox.tsx +76 -0
- package/src/components/ui/choice-dialog.tsx +15 -3
- package/src/components/ui/frame.tsx +1 -1
- package/src/components/ui/index.ts +2 -0
- package/src/components/ui/message-composer.tsx +4 -0
- package/src/components/ui/native-select.tsx +4 -2
- package/src/components/ui/page-stack-view.tsx +1 -1
- package/src/components/ui/ui.test.tsx +0 -78
- package/src/core/state/app/layout-reducer.ts +0 -1
- package/src/data/config/layout.ts +3 -3
- package/src/data/resource-store.ts +6 -0
- package/src/data/sqlite/cache.test.ts +5 -0
- package/src/data/ticker-store.ts +0 -7
- package/src/market-data/coordinator/auxiliary.ts +1 -44
- package/src/market-data/coordinator/entries.ts +0 -1
- package/src/market-data/coordinator/events.test.ts +49 -0
- package/src/market-data/coordinator/events.ts +25 -4
- package/src/market-data/coordinator/index.test.ts +223 -10
- package/src/market-data/coordinator/index.ts +41 -21
- package/src/market-data/coordinator/quotes.ts +13 -11
- package/src/market-data/coordinator/subscriptions.test.ts +1 -0
- package/src/market-data/hooks.tsx +17 -13
- package/src/market-data/market/freshness.ts +58 -1
- package/src/market-data/market/status.ts +19 -1
- package/src/market-data/query-store.ts +0 -4
- package/src/market-data/quotes/freshness.test.ts +49 -1
- package/src/market-data/quotes/freshness.ts +8 -0
- package/src/market-data/quotes/resolution.test.ts +185 -0
- package/src/market-data/quotes/resolution.ts +96 -10
- package/src/market-data/request-types.test.ts +61 -1
- package/src/market-data/request-types.ts +42 -8
- package/src/market-data/result-types.ts +0 -11
- package/src/market-data/selectors.ts +1 -5
- package/src/news/aggregator.ts +5 -1
- package/src/pane-settings.test.ts +0 -18
- package/src/plugins/builtin/account-management/footer.ts +2 -5
- package/src/plugins/builtin/account-management/form-components.tsx +175 -59
- package/src/plugins/builtin/account-management/keyboard.ts +32 -2
- package/src/plugins/builtin/account-management/model.test.ts +99 -4
- package/src/plugins/builtin/account-management/model.ts +259 -23
- package/src/plugins/builtin/account-management/pane.tsx +784 -155
- package/src/plugins/builtin/broker-manager/index.test.tsx +0 -32
- package/src/plugins/builtin/buildout/cells.tsx +1 -1
- package/src/plugins/builtin/buildout/detail/satellite-images.tsx +1 -1
- package/src/plugins/builtin/buildout/detail/ui.tsx +0 -1
- package/src/plugins/builtin/buildout/format.ts +2 -11
- package/src/plugins/builtin/chat/chat.test.tsx +127 -1
- package/src/plugins/builtin/chat/content/channel-navigation.ts +1 -21
- package/src/plugins/builtin/chat/content/composer-cursor.ts +29 -0
- package/src/plugins/builtin/chat/content/composer-runtime.ts +22 -24
- package/src/plugins/builtin/chat/content/composer.tsx +84 -1
- package/src/plugins/builtin/chat/content/index.tsx +153 -16
- package/src/plugins/builtin/chat/content/layout-metrics.ts +10 -1
- package/src/plugins/builtin/chat/content/mentions.test.ts +74 -0
- package/src/plugins/builtin/chat/content/mentions.ts +88 -0
- package/src/plugins/builtin/chat/content/new-dm-dialog.tsx +264 -0
- package/src/plugins/builtin/chat/content/shortcuts.ts +47 -1
- package/src/plugins/builtin/chat/content/snapshot.ts +7 -0
- package/src/plugins/builtin/chat/content/transcript.tsx +0 -5
- package/src/plugins/builtin/chat/controller/index.ts +0 -10
- package/src/plugins/builtin/chat/controller.test.ts +5 -5
- package/src/plugins/builtin/chat/message/profile-popover.test.tsx +36 -0
- package/src/plugins/builtin/chat/message/profile-popover.tsx +124 -20
- package/src/plugins/builtin/chat/sidebar.test.tsx +83 -6
- package/src/plugins/builtin/chat/sidebar.tsx +26 -2
- package/src/plugins/builtin/chat/test-harness.tsx +22 -12
- package/src/plugins/builtin/cloud/plugin.tsx +8 -1
- package/src/plugins/builtin/cloud-tweets/pane.tsx +0 -1
- package/src/plugins/builtin/congress-trades/model.ts +2 -7
- package/src/plugins/builtin/congress-trades/pane.tsx +0 -1
- package/src/plugins/builtin/correlation/index.tsx +4 -1
- package/src/plugins/builtin/correlation/relationship/controls.tsx +8 -2
- package/src/plugins/builtin/correlation/relationship/model.ts +22 -2
- package/src/plugins/builtin/correlation/relationship/pane.tsx +9 -3
- package/src/plugins/builtin/earnings/index.tsx +0 -9
- package/src/plugins/builtin/econ/index.tsx +1 -5
- package/src/plugins/builtin/help/index.tsx +2 -2
- package/src/plugins/builtin/holders/pane.tsx +0 -5
- package/src/plugins/builtin/insider/index.tsx +1 -8
- package/src/plugins/builtin/kelly-sizer/index.test.tsx +0 -26
- package/src/plugins/builtin/kelly-sizer/index.tsx +0 -2
- package/src/plugins/builtin/market-movers/index.tsx +0 -2
- package/src/plugins/builtin/news/index.tsx +0 -1
- package/src/plugins/builtin/news/wire/news/table.tsx +0 -11
- package/src/plugins/builtin/options/view.test.tsx +1 -38
- package/src/plugins/builtin/plugin-groups.ts +17 -3
- package/src/plugins/builtin/portfolio-list/column-values.ts +15 -8
- package/src/plugins/builtin/portfolio-list/index.test.ts +52 -25
- package/src/plugins/builtin/portfolio-list/metrics.test.ts +44 -7
- package/src/plugins/builtin/portfolio-list/pane/data.test.ts +54 -0
- package/src/plugins/builtin/portfolio-list/pane/data.ts +50 -3
- package/src/plugins/builtin/portfolio-list/pane/index.test.tsx +314 -16
- package/src/plugins/builtin/portfolio-list/pane/index.tsx +11 -4
- package/src/plugins/builtin/portfolio-list/pane/streaming.ts +83 -24
- package/src/plugins/builtin/sec/filing-documents.ts +0 -11
- package/src/plugins/builtin/sec/index.tsx +0 -2
- package/src/plugins/builtin/substack/article-detail.tsx +1 -1
- package/src/plugins/builtin/thirteenf/api.ts +0 -15
- package/src/plugins/builtin/thirteenf/pane.tsx +1 -3
- package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/content.tsx +42 -6
- package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/model.ts +415 -64
- package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/settings.ts +23 -2
- package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/types.ts +8 -1
- package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph.tsx +3 -1
- package/src/plugins/builtin/ticker-detail/data-panes.test.ts +95 -1
- package/src/plugins/builtin/ticker-detail/financials/tab.tsx +0 -7
- package/src/plugins/builtin/ticker-detail/index.tsx +2 -0
- package/src/plugins/builtin/ticker-detail/quote-monitor/index.test.tsx +5 -34
- package/src/plugins/builtin/world-indices/index.tsx +0 -4
- package/src/plugins/builtin/world-indices/model.ts +0 -7
- package/src/plugins/ibkr/account-cache.ts +1 -1
- package/src/plugins/ibkr/gateway/market-data.ts +12 -5
- package/src/plugins/ibkr/gateway/service/index.test.ts +31 -0
- package/src/plugins/ibkr/portfolio-performance.ts +1 -1
- package/src/plugins/ibkr/trade/utils.ts +1 -7
- package/src/plugins/loader.ts +2 -1
- package/src/plugins/prediction-markets/detail.test.tsx +0 -34
- package/src/plugins/registry/context.ts +15 -0
- package/src/plugins/registry/contributions.ts +22 -11
- package/src/plugins/registry/index.test.ts +44 -0
- package/src/plugins/registry/index.ts +75 -26
- package/src/remote/controller.ts +1 -7
- package/src/remote/json-patch.ts +15 -1
- package/src/remote/server.ts +16 -6
- package/src/renderers/electrobun/bun/application-menu/index.ts +1 -1
- package/src/renderers/electrobun/bun/core-capabilities.ts +2 -0
- package/src/renderers/electrobun/bun/desktop/http-fetch.test.ts +14 -0
- package/src/renderers/electrobun/bun/desktop/http-fetch.ts +8 -0
- package/src/renderers/electrobun/bun/desktop/initialization.ts +2 -1
- package/src/renderers/electrobun/bun/index.ts +42 -0
- package/src/renderers/electrobun/shared/protocol.ts +5 -0
- package/src/renderers/electrobun/view/backend-rpc.ts +29 -0
- package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +1 -0
- package/src/renderers/electrobun/view/data-table/dom.ts +0 -1
- package/src/renderers/electrobun/view/desktop/controls.tsx +135 -4
- package/src/renderers/electrobun/view/desktop/list-view.tsx +38 -25
- package/src/renderers/electrobun/view/desktop-deeplink-bridge.ts +10 -0
- package/src/renderers/electrobun/view/host/box.tsx +0 -2
- package/src/renderers/electrobun/view/host/mouse.ts +0 -11
- package/src/renderers/electrobun/view/host/scroll-box.tsx +1 -2
- package/src/renderers/electrobun/view/host/text.tsx +1 -2
- package/src/renderers/electrobun/view/http-fetch.ts +15 -2
- package/src/renderers/electrobun/view/main.tsx +3 -0
- package/src/renderers/electrobun/view/remote/asset-data-client.ts +9 -18
- package/src/renderers/electrobun/view/remote/capability-invoker.test.ts +50 -0
- package/src/renderers/electrobun/view/remote/capability-invoker.ts +36 -0
- package/src/renderers/electrobun/view/ui-host.tsx +2 -0
- package/src/renderers/opentui/host.tsx +11 -3
- package/src/renderers/opentui/input-host.tsx +5 -1
- package/src/sources/gloomberb-cloud/index.test.ts +0 -72
- package/src/sources/gloomberb-cloud/index.ts +1 -30
- package/src/sources/gloomberb-cloud/news.ts +0 -12
- package/src/sources/provider-router/batches.ts +11 -3
- package/src/sources/provider-router/cached-financials.test.ts +123 -0
- package/src/sources/provider-router/financial-routes.ts +77 -5
- package/src/sources/provider-router/financials.test.ts +49 -0
- package/src/sources/provider-router/financials.ts +40 -3
- package/src/sources/provider-router/index.test.ts +314 -0
- package/src/sources/provider-router/primary.ts +17 -3
- package/src/sources/provider-router/search.ts +5 -0
- package/src/sources/provider-router/streaming.ts +6 -1
- package/src/sources/provider-router/supplemental.ts +4 -3
- package/src/sources/sec-edgar.test.ts +42 -0
- package/src/sources/sec-edgar.ts +1 -2
- package/src/sources/yahoo-finance/history.ts +5 -1
- package/src/sources/yahoo-finance/mappers.test.ts +29 -0
- package/src/sources/yahoo-finance/mappers.ts +2 -1
- package/src/sources/yahoo-finance/options.ts +59 -14
- package/src/sources/yahoo-finance/quote-summary.ts +4 -1
- package/src/sources/yahoo-finance/requests.ts +2 -1
- package/src/sources/yahoo-finance/snapshots.ts +13 -2
- package/src/sources/yahoo-finance.test.ts +40 -21
- package/src/sources/yahoo-finance.ts +19 -10
- package/src/state/app/context/index.test.tsx +0 -81
- package/src/state/app/context/selector.test.tsx +6 -2
- package/src/state/hooks/quote-streaming.test.ts +14 -1
- package/src/state/hooks/quote-streaming.ts +5 -4
- package/src/sync/controller.test.ts +31 -0
- package/src/sync/controller.ts +300 -0
- package/src/sync/core-contributors.test.ts +251 -0
- package/src/sync/core-contributors.ts +400 -0
- package/src/sync/profile-analytics.ts +39 -0
- package/src/sync/react.ts +53 -0
- package/src/sync/types.ts +77 -0
- package/src/test-support/data-provider.ts +0 -1
- package/src/types/desktop-deeplink.ts +7 -0
- package/src/types/plugin.ts +3 -0
- package/src/ui/host.tsx +15 -1
- package/src/ui/index.tsx +2 -0
- package/src/updater.test.ts +0 -21
- package/src/updater.ts +20 -16
- package/src/utils/async-deadline.ts +26 -0
- package/src/utils/exchanges.test.ts +11 -1
- package/src/utils/exchanges.ts +5 -1
- package/src/utils/selection-clipboard.ts +2 -2
- package/src/version.ts +1 -1
- package/src/components/layout/pane/header.test.tsx +0 -39
- package/src/plugins/builtin/chat/channels.test.ts +0 -28
- package/src/plugins/builtin/chat/index.tsx +0 -4
- package/src/plugins/builtin/cloud-tweets/index.ts +0 -1
- package/src/plugins/builtin/earnings/index.test.ts +0 -12
- package/src/plugins/builtin/help/index.test.tsx +0 -242
- package/src/plugins/builtin/substack/article-detail.test.tsx +0 -10
- package/src/renderers/electrobun/bun/application-menu/index.test.ts +0 -21
- package/src/renderers/electrobun/bun/window/focus.test.ts +0 -35
- package/src/renderers/electrobun/view/host/mouse.test.ts +0 -37
- package/src/renderers/electrobun/view/host/text.test.ts +0 -22
- package/src/renderers/opentui/dialog-host.test.tsx +0 -44
- /package/src/{plugins/ibkr → utils}/hash.ts +0 -0
|
@@ -34,6 +34,9 @@ export interface BarChartBar {
|
|
|
34
34
|
category: string;
|
|
35
35
|
value: number;
|
|
36
36
|
color: string;
|
|
37
|
+
categoryIndex: number;
|
|
38
|
+
seriesIndex: number;
|
|
39
|
+
seriesCount: number;
|
|
37
40
|
x: number;
|
|
38
41
|
width: number;
|
|
39
42
|
row: number;
|
|
@@ -69,17 +72,6 @@ export interface NativeBarChartPixelBounds {
|
|
|
69
72
|
|
|
70
73
|
const clamp = (value: number, min: number, max: number) => Math.max(min, Math.min(max, value));
|
|
71
74
|
|
|
72
|
-
function categorySlotBounds(width: number, categoryCount: number, index: number): { left: number; width: number } {
|
|
73
|
-
const left = Math.floor((index * width) / categoryCount);
|
|
74
|
-
const right = index === categoryCount - 1
|
|
75
|
-
? width
|
|
76
|
-
: Math.floor(((index + 1) * width) / categoryCount);
|
|
77
|
-
return {
|
|
78
|
-
left,
|
|
79
|
-
width: Math.max(1, right - left),
|
|
80
|
-
};
|
|
81
|
-
}
|
|
82
|
-
|
|
83
75
|
function collectCategories(series: BarChartSeries[]): string[] {
|
|
84
76
|
const categories: string[] = [];
|
|
85
77
|
const seen = new Set<string>();
|
|
@@ -90,7 +82,10 @@ function collectCategories(series: BarChartSeries[]): string[] {
|
|
|
90
82
|
categories.push(point.category);
|
|
91
83
|
}
|
|
92
84
|
}
|
|
93
|
-
return categories
|
|
85
|
+
return categories.filter((category) => series.some((item) => {
|
|
86
|
+
const value = item.points.find((point) => point.category === category)?.value;
|
|
87
|
+
return typeof value === "number" && Number.isFinite(value);
|
|
88
|
+
}));
|
|
94
89
|
}
|
|
95
90
|
|
|
96
91
|
function normalizeRange(values: number[]): { min: number; max: number } {
|
|
@@ -133,32 +128,28 @@ export function buildBarChartScene(series: BarChartSeries[], options: BarChartSc
|
|
|
133
128
|
|
|
134
129
|
const { min, max } = normalizeRange(values);
|
|
135
130
|
const zeroRow = valueToRow(0, min, max, height);
|
|
136
|
-
const
|
|
137
|
-
const categorySlots = categories.map((_, index) => categorySlotBounds(width, categories.length, index));
|
|
138
|
-
const minSlotWidth = Math.max(1, Math.min(...categorySlots.map((slot) => slot.width)));
|
|
139
|
-
const groupedInnerWidth = Math.max(seriesCount, minSlotWidth - (minSlotWidth > seriesCount * 2 ? 1 : 0));
|
|
140
|
-
const groupedBarWidth = Math.max(1, Math.floor(groupedInnerWidth / seriesCount));
|
|
131
|
+
const seriesSlotCount = Math.max(series.length, 1);
|
|
141
132
|
const bars: BarChartBar[] = [];
|
|
142
133
|
|
|
143
134
|
categories.forEach((category, categoryIndex) => {
|
|
144
|
-
const group = categorySlots[categoryIndex]!;
|
|
145
|
-
const barWidth = seriesCount === 1 ? group.width : groupedBarWidth;
|
|
146
|
-
const barGroupWidth = barWidth * seriesCount;
|
|
147
|
-
const groupOffset = Math.max(0, Math.floor((group.width - barGroupWidth) / 2));
|
|
148
135
|
series.forEach((item, seriesIndex) => {
|
|
149
136
|
const value = item.points.find((point) => point.category === category)?.value;
|
|
150
137
|
if (typeof value !== "number" || !Number.isFinite(value)) return;
|
|
151
138
|
const color = value < 0 && options.colors.negativeColor
|
|
152
139
|
? options.colors.negativeColor
|
|
153
140
|
: item.color;
|
|
141
|
+
const barBounds = chartSlotBounds(width, categories.length * seriesSlotCount, categoryIndex * seriesSlotCount + seriesIndex);
|
|
154
142
|
bars.push({
|
|
155
143
|
seriesId: item.id,
|
|
156
144
|
seriesLabel: item.label,
|
|
157
145
|
category,
|
|
158
146
|
value,
|
|
159
147
|
color,
|
|
160
|
-
|
|
161
|
-
|
|
148
|
+
categoryIndex,
|
|
149
|
+
seriesIndex,
|
|
150
|
+
seriesCount: seriesSlotCount,
|
|
151
|
+
x: barBounds.x,
|
|
152
|
+
width: barBounds.width,
|
|
162
153
|
row: valueToRow(value, min, max, height),
|
|
163
154
|
});
|
|
164
155
|
});
|
|
@@ -177,6 +168,47 @@ export function buildBarChartScene(series: BarChartSeries[], options: BarChartSc
|
|
|
177
168
|
};
|
|
178
169
|
}
|
|
179
170
|
|
|
171
|
+
function chartSlotBounds(
|
|
172
|
+
width: number,
|
|
173
|
+
slotCount: number,
|
|
174
|
+
slotIndex: number,
|
|
175
|
+
): { x: number; width: number } {
|
|
176
|
+
const slots = Math.max(1, slotCount);
|
|
177
|
+
if (slots > width) {
|
|
178
|
+
const x = clamp(Math.floor(slotIndex * width / slots), 0, width - 1);
|
|
179
|
+
const right = clamp(Math.floor((slotIndex + 1) * width / slots), x + 1, width);
|
|
180
|
+
return {
|
|
181
|
+
x,
|
|
182
|
+
width: Math.max(1, right - x),
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
const gap = slots > 1 && width >= slots * 2 - 1 ? 1 : 0;
|
|
186
|
+
const usableWidth = Math.max(slots, width - gap * (slots - 1));
|
|
187
|
+
const slotWidth = Math.max(1, Math.floor(usableWidth / slots));
|
|
188
|
+
const usedWidth = slotWidth * slots + gap * (slots - 1);
|
|
189
|
+
const offset = Math.max(0, Math.floor((width - usedWidth) / 2));
|
|
190
|
+
const x = clamp(offset + slotIndex * (slotWidth + gap), 0, width - 1);
|
|
191
|
+
return {
|
|
192
|
+
x,
|
|
193
|
+
width: Math.max(1, Math.min(slotWidth, width - x)),
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function categoryCellBounds(
|
|
198
|
+
width: number,
|
|
199
|
+
categoryCount: number,
|
|
200
|
+
seriesCount: number,
|
|
201
|
+
categoryIndex: number,
|
|
202
|
+
): { left: number; width: number } {
|
|
203
|
+
const first = chartSlotBounds(width, categoryCount * seriesCount, categoryIndex * seriesCount);
|
|
204
|
+
const last = chartSlotBounds(width, categoryCount * seriesCount, categoryIndex * seriesCount + seriesCount - 1);
|
|
205
|
+
const right = last.x + last.width;
|
|
206
|
+
return {
|
|
207
|
+
left: first.x,
|
|
208
|
+
width: Math.max(1, right - first.x),
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
180
212
|
function labelAtRow(labels: Array<{ row: number; value: number }>, row: number): number | null {
|
|
181
213
|
const match = labels.find((label) => label.row === row);
|
|
182
214
|
return match ? match.value : null;
|
|
@@ -227,6 +259,10 @@ export function resolveBarChartHover(scene: BarChartScene, cellX: number): BarCh
|
|
|
227
259
|
return Math.abs(currentCenter - x) < Math.abs(closestCenter - x) ? current : closest;
|
|
228
260
|
}, scene.bars[0]!);
|
|
229
261
|
|
|
262
|
+
return barToHover(bar);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function barToHover(bar: BarChartBar): BarChartHover {
|
|
230
266
|
return {
|
|
231
267
|
seriesId: bar.seriesId,
|
|
232
268
|
seriesLabel: bar.seriesLabel,
|
|
@@ -239,6 +275,29 @@ export function resolveBarChartHover(scene: BarChartScene, cellX: number): BarCh
|
|
|
239
275
|
};
|
|
240
276
|
}
|
|
241
277
|
|
|
278
|
+
export function resolveNativeBarChartHover(
|
|
279
|
+
scene: BarChartScene,
|
|
280
|
+
pixelX: number,
|
|
281
|
+
pixelWidth: number,
|
|
282
|
+
): BarChartHover | null {
|
|
283
|
+
if (!Number.isFinite(pixelX) || scene.bars.length === 0) return null;
|
|
284
|
+
const width = Math.max(1, Math.floor(pixelWidth));
|
|
285
|
+
const x = clamp(Math.floor(pixelX), 0, width - 1);
|
|
286
|
+
const direct = scene.bars.find((bar) => {
|
|
287
|
+
const bounds = resolveNativeBarPixelBounds(scene, bar, width);
|
|
288
|
+
return x >= bounds.left && x < bounds.rightExclusive;
|
|
289
|
+
});
|
|
290
|
+
const bar = direct ?? scene.bars.reduce((closest, current) => {
|
|
291
|
+
const closestBounds = resolveNativeBarPixelBounds(scene, closest, width);
|
|
292
|
+
const currentBounds = resolveNativeBarPixelBounds(scene, current, width);
|
|
293
|
+
const closestCenter = (closestBounds.left + closestBounds.rightExclusive - 1) / 2;
|
|
294
|
+
const currentCenter = (currentBounds.left + currentBounds.rightExclusive - 1) / 2;
|
|
295
|
+
return Math.abs(currentCenter - x) < Math.abs(closestCenter - x) ? current : closest;
|
|
296
|
+
}, scene.bars[0]!);
|
|
297
|
+
|
|
298
|
+
return barToHover(bar);
|
|
299
|
+
}
|
|
300
|
+
|
|
242
301
|
export function renderBarChart(scene: BarChartScene, hover: BarChartHover | null = null): string[] {
|
|
243
302
|
const rows = Array.from({ length: scene.height }, () => Array(scene.width).fill(" "));
|
|
244
303
|
for (let x = 0; x < scene.width; x++) {
|
|
@@ -288,7 +347,7 @@ export function renderBarChartAxis(scene: BarChartScene, rowCount = 1): string[]
|
|
|
288
347
|
scene.categories.forEach((category, index) => {
|
|
289
348
|
const label = compactCategoryLabel(category, scene.categories[index - 1], scene.categories.length);
|
|
290
349
|
if (!label) return;
|
|
291
|
-
const group =
|
|
350
|
+
const group = categoryCellBounds(scene.width, scene.categories.length, Math.max(scene.series.length, 1), index);
|
|
292
351
|
const start = centeredLabelStart(group.left + group.width / 2, label, scene.width, rightInset);
|
|
293
352
|
const row = rows.find((candidate) => canPlaceLabel(candidate, start, label, minGap));
|
|
294
353
|
if (!row) return;
|
|
@@ -305,42 +364,39 @@ export function resolveNativeBarPixelBounds(
|
|
|
305
364
|
pixelWidth: number,
|
|
306
365
|
): NativeBarChartPixelBounds {
|
|
307
366
|
const width = Math.max(1, Math.floor(pixelWidth));
|
|
308
|
-
|
|
309
|
-
if (scene.series.length === 1) {
|
|
310
|
-
const categoryIndex = scene.categories.indexOf(bar.category);
|
|
311
|
-
if (categoryIndex >= 0) {
|
|
312
|
-
const separatorWidth = scene.categories.length > 1 && width >= scene.categories.length * 3 ? 1 : 0;
|
|
313
|
-
const usableWidth = Math.max(
|
|
314
|
-
scene.categories.length,
|
|
315
|
-
width - separatorWidth * (scene.categories.length - 1),
|
|
316
|
-
);
|
|
317
|
-
const left = Math.floor((categoryIndex * usableWidth) / scene.categories.length)
|
|
318
|
-
+ categoryIndex * separatorWidth;
|
|
319
|
-
const rightExclusive = categoryIndex === scene.categories.length - 1
|
|
320
|
-
? width
|
|
321
|
-
: Math.floor(((categoryIndex + 1) * usableWidth) / scene.categories.length)
|
|
322
|
-
+ categoryIndex * separatorWidth;
|
|
323
|
-
return {
|
|
324
|
-
left,
|
|
325
|
-
rightExclusive: Math.max(left + 1, rightExclusive),
|
|
326
|
-
};
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
const horizontalInset = 1;
|
|
331
|
-
const left = clamp(
|
|
332
|
-
Math.floor((bar.x / scene.width) * width + horizontalInset),
|
|
333
|
-
0,
|
|
334
|
-
width - 1,
|
|
335
|
-
);
|
|
336
|
-
const rightExclusive = clamp(
|
|
337
|
-
Math.ceil(((bar.x + bar.width) / scene.width) * width - horizontalInset),
|
|
338
|
-
left + 1,
|
|
367
|
+
const bounds = nativeSlotBounds(
|
|
339
368
|
width,
|
|
369
|
+
scene.categories.length * bar.seriesCount,
|
|
370
|
+
bar.categoryIndex * bar.seriesCount + bar.seriesIndex,
|
|
340
371
|
);
|
|
372
|
+
const left = clamp(bounds.left, 0, width - 1);
|
|
373
|
+
const rightExclusive = clamp(bounds.rightExclusive, left + 1, width);
|
|
341
374
|
return { left, rightExclusive };
|
|
342
375
|
}
|
|
343
376
|
|
|
377
|
+
function nativeSlotBounds(
|
|
378
|
+
width: number,
|
|
379
|
+
slotCount: number,
|
|
380
|
+
slotIndex: number,
|
|
381
|
+
): NativeBarChartPixelBounds {
|
|
382
|
+
const slots = Math.max(1, slotCount);
|
|
383
|
+
if (slots > width) {
|
|
384
|
+
const left = clamp(Math.floor(slotIndex * width / slots), 0, width - 1);
|
|
385
|
+
const rightExclusive = clamp(Math.floor((slotIndex + 1) * width / slots), left + 1, width);
|
|
386
|
+
return { left, rightExclusive };
|
|
387
|
+
}
|
|
388
|
+
const gap = slots > 1 && width >= slots * 2 - 1 ? 1 : 0;
|
|
389
|
+
const usableWidth = Math.max(slots, width - gap * (slots - 1));
|
|
390
|
+
const slotWidth = Math.max(1, Math.floor(usableWidth / slots));
|
|
391
|
+
const usedWidth = slotWidth * slots + gap * (slots - 1);
|
|
392
|
+
const offset = Math.max(0, Math.floor((width - usedWidth) / 2));
|
|
393
|
+
const left = offset + slotIndex * (slotWidth + gap);
|
|
394
|
+
return {
|
|
395
|
+
left,
|
|
396
|
+
rightExclusive: Math.max(left + 1, left + slotWidth),
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
|
|
344
400
|
export function renderNativeBarChart(
|
|
345
401
|
scene: BarChartScene,
|
|
346
402
|
pixelWidth: number,
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { useMemo } from "react";
|
|
2
|
-
import type { DisplayCursorState } from "../core/pointer";
|
|
3
2
|
import type { ComparisonChartProjection } from "../comparison/data";
|
|
4
3
|
import {
|
|
5
4
|
buildComparisonChartScene,
|
|
@@ -8,16 +7,14 @@ import {
|
|
|
8
7
|
type ComparisonChartScene,
|
|
9
8
|
type RenderComparisonChartResult,
|
|
10
9
|
} from "../comparison/renderer";
|
|
11
|
-
import type {
|
|
10
|
+
import type { ChartMarketSession, ResolvedChartRenderer } from "../core/types";
|
|
12
11
|
|
|
13
12
|
type ComparisonChartColors = Parameters<typeof buildComparisonChartScene>[1]["colors"];
|
|
14
13
|
|
|
15
14
|
interface UseComparisonChartRenderOutputOptions {
|
|
16
|
-
axisMode: ChartAxisMode;
|
|
17
15
|
chartColors: ComparisonChartColors;
|
|
18
16
|
chartHeight: number;
|
|
19
17
|
chartWidth: number;
|
|
20
|
-
displayCursor: DisplayCursorState;
|
|
21
18
|
displayCursorX: number | null;
|
|
22
19
|
displayCursorY: number | null;
|
|
23
20
|
effectiveRenderer: ResolvedChartRenderer;
|
|
@@ -34,22 +31,20 @@ interface UseComparisonChartRenderOutputResult {
|
|
|
34
31
|
cursorRow: number | null;
|
|
35
32
|
cursorTimeAxisColumn: number | null;
|
|
36
33
|
cursorTimeAxisDate: Date | null;
|
|
37
|
-
displayScene: ComparisonChartScene;
|
|
34
|
+
displayScene: ComparisonChartScene | null;
|
|
38
35
|
hasDisplayCursor: boolean;
|
|
39
36
|
legendActiveIndex: number | null;
|
|
40
37
|
result: RenderComparisonChartResult;
|
|
41
38
|
staticResult: RenderComparisonChartResult;
|
|
42
|
-
staticScene: ComparisonChartScene;
|
|
39
|
+
staticScene: ComparisonChartScene | null;
|
|
43
40
|
timeAxisLabel: string;
|
|
44
41
|
visiblePriceRange: number | undefined;
|
|
45
42
|
}
|
|
46
43
|
|
|
47
44
|
export function useComparisonChartRenderOutput({
|
|
48
|
-
axisMode,
|
|
49
45
|
chartColors,
|
|
50
46
|
chartHeight,
|
|
51
47
|
chartWidth,
|
|
52
|
-
displayCursor,
|
|
53
48
|
displayCursorX,
|
|
54
49
|
displayCursorY,
|
|
55
50
|
effectiveRenderer,
|
|
@@ -402,11 +402,9 @@ function ComparisonStockChartView({
|
|
|
402
402
|
staticScene,
|
|
403
403
|
timeAxisLabel,
|
|
404
404
|
} = useComparisonChartRenderOutput({
|
|
405
|
-
axisMode,
|
|
406
405
|
chartColors,
|
|
407
406
|
chartHeight,
|
|
408
407
|
chartWidth,
|
|
409
|
-
displayCursor,
|
|
410
408
|
displayCursorX,
|
|
411
409
|
displayCursorY,
|
|
412
410
|
effectiveRenderer,
|
|
@@ -918,30 +918,6 @@ describe("renderChart", () => {
|
|
|
918
918
|
}
|
|
919
919
|
});
|
|
920
920
|
|
|
921
|
-
test("keeps the default area renderer stable", () => {
|
|
922
|
-
const projection = projectChartData(chartFixture, 12, undefined, false);
|
|
923
|
-
const result = renderChart(projection.points, {
|
|
924
|
-
width: 12,
|
|
925
|
-
height: 5,
|
|
926
|
-
showVolume: false,
|
|
927
|
-
volumeHeight: 0,
|
|
928
|
-
cursorX: null,
|
|
929
|
-
cursorY: null,
|
|
930
|
-
mode: projection.effectiveMode,
|
|
931
|
-
colors: palette,
|
|
932
|
-
});
|
|
933
|
-
|
|
934
|
-
expect(projection.requestedMode).toBe("area");
|
|
935
|
-
expect(projection.effectiveMode).toBe("area");
|
|
936
|
-
expect(textLines(result)).toEqual([
|
|
937
|
-
"⠁ ⡠⠔⢣ ⠁ ⠁ ",
|
|
938
|
-
"⠔⠊⣿⣿⠈⢆⠄ ⠄ ",
|
|
939
|
-
"⣿⣿⣿⣿⣿⠘⡄ ⡠",
|
|
940
|
-
"⣿⣿⣿⣿⣿⣿⠱⡀ ⢠⠊⣿",
|
|
941
|
-
"⣿⣿⣿⣿⣿⣿⣿⢣⠔⠁⣿⣿",
|
|
942
|
-
]);
|
|
943
|
-
});
|
|
944
|
-
|
|
945
921
|
test("formats y-axis labels as percent change when requested", () => {
|
|
946
922
|
const projection = projectChartData(chartFixture, 12, "line", false);
|
|
947
923
|
const result = renderChart(projection.points, {
|
|
@@ -2,20 +2,16 @@ import type { PixelBuffer } from "./types";
|
|
|
2
2
|
import type { ProjectedChartPoint } from "./data";
|
|
3
3
|
import { buildTimeAxis } from "./time-axis";
|
|
4
4
|
import {
|
|
5
|
-
formatAxisCell,
|
|
6
5
|
formatAxisValue,
|
|
7
|
-
formatCursorAxisValue,
|
|
8
6
|
formatPriceWithCurrency,
|
|
9
7
|
getAxisFractionDigitFloor,
|
|
10
8
|
resolveAxisFractionDigits,
|
|
11
|
-
resolveChartAxisWidth,
|
|
12
9
|
} from "./axis-format";
|
|
13
10
|
import {
|
|
14
11
|
bufferToBrailleLines,
|
|
15
12
|
createPixelBuffer,
|
|
16
13
|
drawCrosshair,
|
|
17
14
|
drawGridLines,
|
|
18
|
-
drawLine,
|
|
19
15
|
type StyledContent,
|
|
20
16
|
} from "../rendering/pixel-buffer";
|
|
21
17
|
import {
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
2
|
import {
|
|
3
3
|
CHART_RESOLUTION_STEP_MS,
|
|
4
|
-
clampTimeRangeForResolution,
|
|
5
|
-
getActiveRangePreset,
|
|
6
4
|
getBestSupportedResolutionForVisibleWindow,
|
|
7
5
|
getExpandedBufferRange,
|
|
8
6
|
getPresetResolution,
|
|
@@ -22,26 +20,11 @@ describe("chart-resolution", () => {
|
|
|
22
20
|
expect(getPresetResolution("ALL")).toBe("1mo");
|
|
23
21
|
});
|
|
24
22
|
|
|
25
|
-
test("derives the active range preset only for the exact reset preset pair", () => {
|
|
26
|
-
expect(getActiveRangePreset("1Y", "1d", 1, 0)).toBe("1Y");
|
|
27
|
-
expect(getActiveRangePreset("1Y", "auto", 1, 0)).toBeNull();
|
|
28
|
-
expect(getActiveRangePreset("1Y", "1d", 1.2, 0)).toBeNull();
|
|
29
|
-
expect(getActiveRangePreset("1Y", "1d", 1, 2)).toBeNull();
|
|
30
|
-
});
|
|
31
|
-
|
|
32
23
|
test("checks whether a range preset is supported by the visible capability set", () => {
|
|
33
24
|
expect(isRangePresetSupported("1Y", ["1d", "1wk"])).toBe(true);
|
|
34
25
|
expect(isRangePresetSupported("ALL", ["1d", "1wk"])).toBe(false);
|
|
35
26
|
});
|
|
36
27
|
|
|
37
|
-
test("clamps overly-wide ranges for manual intraday resolutions", () => {
|
|
38
|
-
expect(clampTimeRangeForResolution("5Y", "5m")).toBe("1W");
|
|
39
|
-
expect(clampTimeRangeForResolution("6M", "1h")).toBe("3M");
|
|
40
|
-
expect(clampTimeRangeForResolution("1M", "15m")).toBe("1M");
|
|
41
|
-
expect(clampTimeRangeForResolution("5Y", "1d")).toBe("5Y");
|
|
42
|
-
expect(clampTimeRangeForResolution("5Y", "auto")).toBe("5Y");
|
|
43
|
-
});
|
|
44
|
-
|
|
45
28
|
test("expands buffers only toward wider supported ranges", () => {
|
|
46
29
|
const support = new Map([
|
|
47
30
|
["1h", "3M"],
|
|
@@ -239,22 +239,6 @@ export function clampTimeRangeToMaxRange(range: TimeRange, maxRange: TimeRange):
|
|
|
239
239
|
return isTimeRangeAtOrBelow(range, maxRange) ? range : maxRange;
|
|
240
240
|
}
|
|
241
241
|
|
|
242
|
-
export function clampTimeRangeForResolution(range: TimeRange, resolution: ChartResolution): TimeRange {
|
|
243
|
-
if (resolution === "auto") return range;
|
|
244
|
-
const maxRange = getSupportMaxRange(
|
|
245
|
-
normalizeChartResolutionSupport([{ resolution, maxRange: "ALL" }]),
|
|
246
|
-
resolution,
|
|
247
|
-
) ?? "ALL";
|
|
248
|
-
const intrinsicMaxRange = resolution === "1m" || resolution === "5m"
|
|
249
|
-
? "1W"
|
|
250
|
-
: resolution === "15m"
|
|
251
|
-
? "1M"
|
|
252
|
-
: resolution === "30m" || resolution === "45m" || resolution === "1h"
|
|
253
|
-
? "3M"
|
|
254
|
-
: "ALL";
|
|
255
|
-
return clampTimeRangeToMaxRange(range, minTimeRange(maxRange, intrinsicMaxRange));
|
|
256
|
-
}
|
|
257
|
-
|
|
258
242
|
export function isIntradayResolution(resolution: ManualChartResolution): boolean {
|
|
259
243
|
return resolution === "1m"
|
|
260
244
|
|| resolution === "5m"
|
|
@@ -277,16 +261,6 @@ export function isRangePresetSupported(
|
|
|
277
261
|
return maxRange !== null && isTimeRangeAtOrBelow(range, maxRange);
|
|
278
262
|
}
|
|
279
263
|
|
|
280
|
-
export function getActiveRangePreset(
|
|
281
|
-
range: TimeRange,
|
|
282
|
-
resolution: ChartResolution,
|
|
283
|
-
zoomLevel: number,
|
|
284
|
-
panOffset: number,
|
|
285
|
-
): TimeRange | null {
|
|
286
|
-
if (zoomLevel !== 1 || panOffset !== 0) return null;
|
|
287
|
-
return RANGE_PRESET_RESOLUTION[range] === resolution ? range : null;
|
|
288
|
-
}
|
|
289
|
-
|
|
290
264
|
export function getWidestPresetForResolution(
|
|
291
265
|
resolution: ManualChartResolution,
|
|
292
266
|
maxRange: TimeRange,
|
|
@@ -3,10 +3,8 @@ import {
|
|
|
3
3
|
applyBufferedPanExpansion,
|
|
4
4
|
consumeScrollPanCellDelta,
|
|
5
5
|
consumeScrollPanMovement,
|
|
6
|
-
getDragPanPointDelta,
|
|
7
6
|
getDragPanWindowRatio,
|
|
8
7
|
getKeyboardPanCellCount,
|
|
9
|
-
resolveDragPanOffset,
|
|
10
8
|
resolveHorizontalScrollPanDirection,
|
|
11
9
|
} from "./scroll";
|
|
12
10
|
|
|
@@ -34,11 +32,7 @@ describe("chart-scroll", () => {
|
|
|
34
32
|
|
|
35
33
|
test("keeps keyboard pan small while matching drag to the visible window", () => {
|
|
36
34
|
expect(getKeyboardPanCellCount(100)).toBe(2);
|
|
37
|
-
expect(getDragPanPointDelta(10, 100, 200)).toBe(20);
|
|
38
35
|
expect(getDragPanWindowRatio(10, 100)).toBeCloseTo(0.1);
|
|
39
|
-
expect(resolveDragPanOffset(20, 10, 100, 200, 40)).toBe(40);
|
|
40
|
-
expect(resolveDragPanOffset(20, -10, 100, 200, 40)).toBe(0);
|
|
41
|
-
expect(resolveDragPanOffset(38, 10, 100, 200, 40)).toBe(40);
|
|
42
36
|
});
|
|
43
37
|
|
|
44
38
|
test("resolves wheel direction, cell delta, remainder, and ratio together", () => {
|
|
@@ -21,36 +21,14 @@ export interface ScrollPanMovement extends ScrollPanCellDelta {
|
|
|
21
21
|
ratio: number;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
function clamp(value: number, min: number, max: number): number {
|
|
25
|
-
return Math.max(min, Math.min(max, value));
|
|
26
|
-
}
|
|
27
|
-
|
|
28
24
|
export function getKeyboardPanCellCount(chartWidth: number): number {
|
|
29
25
|
return Math.max(Math.round(chartWidth * KEYBOARD_PAN_WIDTH_RATIO), 1);
|
|
30
26
|
}
|
|
31
27
|
|
|
32
|
-
export function getDragPanPointDelta(deltaCells: number, chartWidth: number, visibleCount: number): number {
|
|
33
|
-
return Math.round((deltaCells / Math.max(chartWidth, 1)) * Math.max(visibleCount, 1) * DRAG_PAN_VISIBLE_RATIO);
|
|
34
|
-
}
|
|
35
|
-
|
|
36
28
|
export function getDragPanWindowRatio(deltaCells: number, chartWidth: number): number {
|
|
37
29
|
return (deltaCells / Math.max(chartWidth, 1)) * DRAG_PAN_VISIBLE_RATIO;
|
|
38
30
|
}
|
|
39
31
|
|
|
40
|
-
export function resolveDragPanOffset(
|
|
41
|
-
startPanOffset: number,
|
|
42
|
-
deltaCells: number,
|
|
43
|
-
chartWidth: number,
|
|
44
|
-
visibleCount: number,
|
|
45
|
-
maxPanOffset: number,
|
|
46
|
-
): number {
|
|
47
|
-
return clamp(
|
|
48
|
-
startPanOffset + getDragPanPointDelta(deltaCells, chartWidth, visibleCount),
|
|
49
|
-
0,
|
|
50
|
-
Math.max(maxPanOffset, 0),
|
|
51
|
-
);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
32
|
export function consumeScrollPanCellDelta(
|
|
55
33
|
chartWidth: number,
|
|
56
34
|
delta: number | undefined,
|
|
@@ -1,28 +1,7 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
|
-
import { getVisiblePointCount
|
|
3
|
-
|
|
4
|
-
describe("resolveAnchoredChartZoom", () => {
|
|
5
|
-
test("keeps keyboard zoom pinned to the latest data when anchored to the right edge", () => {
|
|
6
|
-
expect(resolveAnchoredChartZoom(252, 1, 0, 2, RIGHT_EDGE_ANCHOR_RATIO)).toEqual({
|
|
7
|
-
zoomLevel: 2,
|
|
8
|
-
panOffset: 0,
|
|
9
|
-
});
|
|
10
|
-
});
|
|
11
|
-
|
|
12
|
-
test("preserves the current right edge when zooming a panned view", () => {
|
|
13
|
-
expect(resolveAnchoredChartZoom(252, 2, 20, 3, RIGHT_EDGE_ANCHOR_RATIO)).toEqual({
|
|
14
|
-
zoomLevel: 3,
|
|
15
|
-
panOffset: 20,
|
|
16
|
-
});
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
test("still supports center-anchored zoom for pointer-driven interactions", () => {
|
|
20
|
-
expect(resolveAnchoredChartZoom(252, 1, 0, 2, 0.5)).toEqual({
|
|
21
|
-
zoomLevel: 2,
|
|
22
|
-
panOffset: 63,
|
|
23
|
-
});
|
|
24
|
-
});
|
|
2
|
+
import { getVisiblePointCount } from "./viewport";
|
|
25
3
|
|
|
4
|
+
describe("chart viewport", () => {
|
|
26
5
|
test("allows zooming down to a two-point viewport", () => {
|
|
27
6
|
expect(getVisiblePointCount(433, 1000)).toBe(2);
|
|
28
7
|
});
|
|
@@ -22,30 +22,3 @@ export function getVisiblePointCount(totalPoints: number, zoomLevel: number): nu
|
|
|
22
22
|
const clampedZoom = clampChartZoom(totalPoints, zoomLevel);
|
|
23
23
|
return Math.min(totalPoints, Math.max(Math.floor(totalPoints / clampedZoom), MIN_VISIBLE_POINTS));
|
|
24
24
|
}
|
|
25
|
-
|
|
26
|
-
export function resolveAnchoredChartZoom(
|
|
27
|
-
totalPoints: number,
|
|
28
|
-
currentZoomLevel: number,
|
|
29
|
-
currentPanOffset: number,
|
|
30
|
-
nextZoomLevel: number,
|
|
31
|
-
anchorRatio: number,
|
|
32
|
-
): { zoomLevel: number; panOffset: number } {
|
|
33
|
-
if (totalPoints <= 0) {
|
|
34
|
-
return { zoomLevel: 1, panOffset: 0 };
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
const clampedZoom = clampChartZoom(totalPoints, nextZoomLevel);
|
|
38
|
-
const currentVisibleCount = getVisiblePointCount(totalPoints, currentZoomLevel);
|
|
39
|
-
const nextVisibleCount = getVisiblePointCount(totalPoints, clampedZoom);
|
|
40
|
-
const currentPan = clamp(currentPanOffset, 0, Math.max(totalPoints - currentVisibleCount, 0));
|
|
41
|
-
const ratio = clamp(anchorRatio, 0, 1);
|
|
42
|
-
const anchorIndex = totalPoints - currentPan - currentVisibleCount + ratio * Math.max(currentVisibleCount - 1, 0);
|
|
43
|
-
const nextStart = Math.round(anchorIndex - ratio * Math.max(nextVisibleCount - 1, 0));
|
|
44
|
-
const clampedStart = clamp(nextStart, 0, Math.max(totalPoints - nextVisibleCount, 0));
|
|
45
|
-
const nextPanOffset = totalPoints - nextVisibleCount - clampedStart;
|
|
46
|
-
|
|
47
|
-
return {
|
|
48
|
-
zoomLevel: clampedZoom,
|
|
49
|
-
panOffset: clamp(nextPanOffset, 0, Math.max(totalPoints - nextVisibleCount, 0)),
|
|
50
|
-
};
|
|
51
|
-
}
|
|
@@ -148,7 +148,7 @@ export function getChartMarketSessionKey(session: ChartMarketSession | null | un
|
|
|
148
148
|
}
|
|
149
149
|
|
|
150
150
|
export function resolveExtendedHoursBackgroundSpans(
|
|
151
|
-
values: readonly
|
|
151
|
+
values: readonly (Date | string | number)[],
|
|
152
152
|
session: ChartMarketSession | null | undefined,
|
|
153
153
|
): ChartSessionBackgroundSpan[] {
|
|
154
154
|
if (!session) return [];
|
|
@@ -71,8 +71,9 @@ export function useNativeChartSurfaceLifecycle({
|
|
|
71
71
|
|
|
72
72
|
useEffect(() => {
|
|
73
73
|
if (effectiveRenderer !== "kitty" || !nativeReady || !plotRef.current) return;
|
|
74
|
-
const plot = plotRef.current;
|
|
74
|
+
const plot = plotRef.current as BoxRenderable & { onLifecyclePass?: (() => void) | null };
|
|
75
75
|
let mountTimer: ReturnType<typeof setTimeout> | null = null;
|
|
76
|
+
const previousLifecyclePass = plot.onLifecyclePass;
|
|
76
77
|
|
|
77
78
|
const syncPlacement = () => {
|
|
78
79
|
if (effectiveRenderer !== "kitty" || !nativeReady || !plotRef.current) return;
|
|
@@ -108,7 +109,11 @@ export function useNativeChartSurfaceLifecycle({
|
|
|
108
109
|
);
|
|
109
110
|
};
|
|
110
111
|
|
|
111
|
-
|
|
112
|
+
const lifecyclePass = () => {
|
|
113
|
+
previousLifecyclePass?.();
|
|
114
|
+
syncPlacement();
|
|
115
|
+
};
|
|
116
|
+
plot.onLifecyclePass = lifecyclePass;
|
|
112
117
|
renderer.registerLifecyclePass(plot);
|
|
113
118
|
syncPlacement();
|
|
114
119
|
mountTimer = setTimeout(() => {
|
|
@@ -118,7 +123,9 @@ export function useNativeChartSurfaceLifecycle({
|
|
|
118
123
|
|
|
119
124
|
return () => {
|
|
120
125
|
if (mountTimer) clearTimeout(mountTimer);
|
|
121
|
-
plot.onLifecyclePass
|
|
126
|
+
if (plot.onLifecyclePass === lifecyclePass) {
|
|
127
|
+
plot.onLifecyclePass = previousLifecyclePass;
|
|
128
|
+
}
|
|
122
129
|
renderer.unregisterLifecyclePass(plot);
|
|
123
130
|
lastGeometryRef.current = null;
|
|
124
131
|
};
|